Monday, 8 June 2020

A curated ETF list and a model portfolio


I set myself last year a goal of doing one blog post a month. I have an idea for an interesting series of posts related to forecast strength, which I'd hoped to find time to research and post about. However, I've been quite busy marking exams and pushing through the production trading code for pysystemtrade (it's now at the point where I can trade manually; auto trading is next. Expect a lot of posts once it's finished explaining how to use it for live trading).

So instead here's a post about systematic investing rather than trading. Although this blog is ostensibly for both traders and investors, I do tend to focus more on the former. To be fair this is because I try to only spend a few days a year rebalancing my long only investment portfolio, whereas I trade every day (or rather my computer does). 

However the recent activity in the markets, and resulting churning of my portfolio, made me realise that I needed to seriously update my long only investment toolkit. At some points during March I was trading my investing portfolio every day. Even now, I expect to be rebalancing monthly as I gradually get back to my strategic weighting. 

I though I might as well share with you the results of that updating.

So, in this post I:

  • Come up with a list of curated ETF's to match the major asset classes, regions and categories.
  •  Use the principles in my second book, Smart Portfolios, to create a model portfolio of these ETFs.
  • The model portfolio can be found in this google docs spreadsheet, and I hope to update it every month or so.

(I have to say this every time: This link is for a read only version of the spreadsheet. If you want to modify it then do not ask for edit permissions. Think about it - why would I allow anyone to do that? Instead download it or copy it inside google docs).

A curated list of ETFs


First a caveat: this is a list of UK listed ETFs. I am doing this for my own purposes, so it doesn't make sense for me to do a load more work for US or European readers. However hopefully you can use the principles here (and covered in more detail in Smart Portfolios) to choose equivalents. I used the excellent service, justetf.com, to research this list. Most of these ETFs are listed on European exchanges as well, so if you search the ticker you can find an alternative easily. In the US I would use etfdb.com. 


Which categories?


Using my top down methodology I decided to use the following categorisation:

  • Equities
    • US equities
      • Beta (the whole market), High yield, Value
    • European equities
      • Beta ....
    • UK equities
      • Beta ....
    • Asian equities
      • Beta ....
    • Emerging market equities
      • Beta ....
  • Bonds
    • US bonds
      • Government, Corporate IG, High yield
    • European bonds
      • Government...
    • UK bonds
      • Government...
    • Asian bonds
      • Government...
    • Emerging market bonds
      • Government...
There are no alternative assets here. In practice my alternative asset is effectively my trading account, which gives me exposure to a large variety of risk factors. This has a target allocation of 25% of my risk, but that isn't included here. I also have a ragtag of a few property and gold ETFs that I am excluding for simplicity.

I went for the Beta, High Yield, Value split because this is a particular bias I wanted in my equity portfolio. I did not want to go any deeper into the structure (eg down to equity sectors), as I didn't want this portfolio to end up requiring too much work on a regular basis. In practice I invest in individual UK equities and there may be other places in my real portfolio where I go into a more granular portfolio than implied here.

I also needed a global equity and global bond ETF to do my tactical asset allocation (of which more later).


Selection criteria


Here is how I selected the ETFs:

  • Select the appropriate category
  • AUM>£100m
  • Where possible, exclude accumulating funds (since I wanted to use dividend yield as a valuation metric. In practice I might choose to invest in the accumulated fund depending on whether the ETF was in a taxable or non taxable account)
  • Exclude leveraged funds
  • Where possible, exclude currency hedged funds (unless a hedged fund was much much cheaper). I explain the problem with these in the book
  • Choose the lowest TER (as readers of my book will know there is more to costs than just this single figure; nevertheless I didn't want to spend too much time on this exercise)
There are other selection criteria discussed in the book, but again in the interests of time I just stuck to the points above.

I was also interested in introducing a degree of diversification across providers, since historically my portfolio was very heavy in iShares and latterly Vanguard (thus creating some potential counterparty concentration risk). However I was pleasantly surprised by how much more competition there is in the market now. The 28 ETFs ended coming from 10 providers, and although iShares (7 funds) and Vanguard (5 funds) are still the most popular there is plenty of diversification here.

I was also pleasantly surprised by how cheap the market has become since I last did this for Smart Portfolios about 3 years ago. The average TER was just 0.22%, with 17 funds coming in at 0.2% or less. 


The list


Here is the list of funds. Note that there may be data errors here which I am not responsible for.

TickerVendorTER
BondsEMGovtVDETVanguard0.25%
BondsEMCorpEMCPIshares0.50%
BondsEMHigh yieldUnavailable`
BondsEuroGovtPRIRAmundi0.05%
BondsEuroCorpVECPVanguard0.09%
BondsEuroHigh yieldJNKESPDR0.40%
BondsGlobalGovtIAAAIshares0.20%
BondsGlobalGovt and CorpAGGGIshares0.10%
BondsGlobalCorpUSIXLyxor0.09%
BondsGlobalHigh yieldxhygX trackers0.20%
BondsUKGovtIGLTIshares0.07%
BondsUKCorpSLXXIshares0.20%
BondsUKHigh yieldUnavailable
BondsUSGovtTRXGInvesco0.06%
BondsUSCorpUC84UBS0.18%
BondsUSHigh yieldUnavailable
BondsAsiaUnavailable
EquityAsiaBetaVAPXVanguard0.15%
EquityAsiaHigh yieldPADVSPDR0.55%
EquityAsiaValueUnavailable
EquityEMBetaHMEFHSBC0.15%
EquityEMHigh yieldSEDYIshares0.65%
EquityEMValueUnavailable
EquityEuroBetaH50EHSBC0.05%
EquityEuroHigh yieldEUDVSPDR0.30%
EquityEuroValueIEFVIshares0.25%
EquityGlobalBetaVEVEVanguard0.12%
EquityGlobalHigh yieldVHYLVanguard0.29%
EquityGlobalValueXDEVXtrackers0.25%
EquityUKBetaHUKXHSBC0.07%
EquityUKHigh yieldUKDVSPDR0.30%
EquityUKValueUnavailable
EquityUSBetaSPXDInvesco0.05%
EquityUSHigh yieldFUSDFidelity0.30%
EquityUSValueUC07UBS0.20%

Unavailable indicates there was no suitable fund available with the given criteria. In some cases I've stretched the term 'Value' to include things like Quality if no pure value fund was available. 

Note: IEFV is an accumulating fund; the yield I use will be taken from the distributing fund (IEDL)  but do not buy this fund as it only has £2m AUM.


The model portfolio


Broadly speaking the portfolio will be done in my usual top down way, with the tactical weighting done using my favourite methods:

  • Equity / Bond with a strategic 90/10 allocation (which equates to about 80/20 in cash terms). Tactical weighting, relative momentum (discussed in this post as well as the book)
  • Regional allocation with strategic weights (equal split for bonds, less equal for equities). 
  • Intra-regional allocation with strategic weights (discussed below)
  • 'Intra-asset allocation done as a single process using relative dividend yields'

'Intra-asset allocation done as a single process' might need some explaining. Don't worry, I will explain everything.

Let's dive in to the google docs spreadsheet

In sheet 'ETF list' I include the information from the curated list above, but also add columns for the price and 1 year dividend history. I then calculate the yield. The reason I do this is that the just ETF site only includes dividends as a premium product. So every time I update the sheet, I just need to change the price and the yield will update automatically. Every now and then I will need to change the 1 year dividend history (these normally pay quarterly, so 4 times a year should do).

The 'calculations' sheets deducts the TER from the dividend yield to get a net dividend (a crude way of handling costs), and then converts that to a Sharpe Ratio (SR).

In the sheet 'returns' I put the one year total return for my global asset class ETFs (I don't actually invest in any global ETFs). These are used in the momentum model on the next sheet 'asset' to calculate the adjusted asset class risk weights (cash weights are also shown for information, but not used).

The 'bonds' and 'equity' sheets do the allocation within each asset class as described above. I calculate the strategic risk weights inside each asset class across and then within regions. At this point I have a risk weight for every ETF inside it's asset class. I then use the relative dividend yield SR within the asset class to adjust those risk weights. 

Finally in the sheet 'Total' I use the asset class risk weights (adjusted for momentum) to calculate my final risk weights for each ETF. These are then converted to cash weights.

Note: The SR scaling adjustments are different in each sheet. That's because I've recalibrated them so that they are more like my trading rules. Broadly speaking the SR differences have different meaning across different places. 

In the asset allocation section, it's not unusual for equities to go up by 30% a year (about 2 SR units) with bonds flat; a SR difference of about 2 units. But within bonds you would be surprised to see one corporate bond regional ETF with a yield of 10% (about 2 SR units) and another with a yield of 0%. Similarly, you'd rarely see a developed regional equity ETF with a yield of 44% (about 2 SR units) whilst another had a zero yield. So I've changed the SR adjustments to reflect this.


Using the model portfolio


My own monthly routine will now look something like this:

  • Update the ETF prices, and possibly the dividends
  • Compare my own portfolio weights
  • Consider selling and buying to match those weights if they are too far out of line with my current portfolio weights
The question of whether to buy or sell is complex and is covered in 80 or so pages of part four of Smart Portfolios, and I certainly won't be repeating it here!

At some point I hope to go back to annual rebalancing, but if the market moves a lot I can quickly fire up this spreadsheet and do an ad-hoc rebalance if required.



I realise I wrote this whole post without including a picture, so here's a picture of my second book.







Thursday, 7 May 2020

When endogenous risk management isn't enough: a simple risk overlay

"How does your risk management work?"

... is a question I'm frequently asked.

In fact this is actually a difficult question, if you were to look at my open source python backtesting project pysystemtrade, you would struggle to point at a piece of code and say "Behold! Right there, that's the risk management part alright!". The reason is that the risk management in my trading system is endogenous (from the greek, meaning 'word used to mean internally or inside by people trying to sound clever'). Risk management is something it just does without even trying.

For example, if volatility rises, then positions will be cut. If it starts to lose money on a particular position, the position will be cut. If the amount of capital deployed reduces, the position will be cut. Many of these things look like deliberate risk management, or perhaps the term 'position management' is more appropriate. But they are just a consequence of the simple building blocks that the system is built upon: inverse vol position scaling, a preponderance of trend following rules, and liberal use of the Kelly criterion.

However, these simple building blocks make some heroic assumptions. In particular they assume that asset returns follow a joint Gaussian return distribution, where co-movements are linear, and both volatility and correlations are perfectly predictable from historic data. The system also does it's risk management on a long run average basis:

The consequences of this are, to use technical language for a moment: sometimes things could get a bit scary. This post explains how, and introduces a simple risk overlay to make things slightly less scary. Essentially this overlay sits slightly outside the main system (although it runs as part of the same code base), tweaking positions when certain risk limits are hit.

This is an overlay I have already implemented in my existing trading system, so it's worked well for over  6 years. Although this new code is designed for pysystemtrade I will make the python code as stand-alone as possible so you can adapt it for your own use if you wish. It will work equally well in other trading systems, although it will be most useful in a system that works more like mine.

This is a continuation of a series I started a few years ago, but only got around to writing a couple of posts for. In the spirit of tidiness, here are the first two posts:


You don't have to read the first two posts to understand this one, but it might help especially if you don't understand exactly what I mean by edogenous risk management.

Parts of this post will be easier to follow if you've read my first book, Systematic Trading.


Realised risk


Let's start by measuring the actual risk we realised. This should average 25%, or whatever is defined in system.config.percentage_vol_target. 

(I have grabbed a backtest to do this which essentially reflects my live system with a subset of instruments, but you can play along with a different pysystemtrade backtest if you wish, or any series of daily returns you happen to have).

# assuming we already have a pysystemtrade system object...
returns = system.accounts.portfolio().as_percent()
returns = returns[pd.datetime(1997,1,1):]
annualised_std = returns.std()*16


(I'm only showing data since 1997, because I'm in the process of cleaning up my price data which still has a few spikes in it that aren't real.)

The average standard deviation comes out at 23.7% which is a fraction below the target of 25%, but more importantly how does this vary over time? Let's plot the daily returns:




With 23.8% annual risk, which equates to around 1.5% a day, we'd expect to see around two thirds (68%) of our returns coming in at between -1.5% and +1.5% (If our mean was zero. The mean is 0.08%, so the returns will come in between -1.42% and 1.58%, which isn't very different). 

To see more clearly, let's look at the rolling standard deviation of returns over the last 125 business days (about 6 months), and multiply by 16 to annualise:

roll_std_returns = returns.rolling(125).std()*16



You can see clearly that the 6 month rolling risk is highly variable, dipping down to 10% at some points, and up above 40% in the halcoyn days of the early 2000's.

There are two explanations for this:
  1. We are very bad at predicting our risk
  2. We are allowing our expected risk to vary a lot
... or perhaps a little bit of both.


Expected risk


It's very easy to check if our expected risk should vary a lot, by measuring our expected risk (assuming, naturally, a joint Gaussian risk model).

The code for this is a bit lengthy, so rather than cut and paste I've dropped into it's own little gist. The tricky part is converting everything into notional exposure as a percentage of capital, which allows us to use percentage returns. Incidentally, I use a 30 day span for standard deviations, and 120 days for correlations. These give a fairly good estimate, but using different values won't make a huge difference.

risk_series = get_expected_risk_for_system(system)





OK, so our expected risk is expected to vary (and I will explain why below). Another way of thinking about the problem is to see how well we did at matching expected and realised risk (basically, how good is our simple Gaussian normal risk model at forecasting risk). The plot below shows the realised returns, with one and two standard deviation bands from the expected risk distribution (ignoring the tiny mean).

threshold=pd.concat([risk_series*100/16.0, -risk_series*100/16.0],axis=1)
thresh_and_returns = pd.concat([returns, threshold], axis=1)
thresh_and_returns.columns = ['returns', '+1std','-1std']



We should see about two thirds of the returns fall within the bands, and indeed they do. Also, the bands should expand when the returns do. This happens eg in 2004. The risk model isn't perfect, because there are a few large outliers that we wouldn't expect to get with Gaussian normal returns (though some of these may be due to bad data). But it's doing a reasonable job of forecasting future risk.

Mostly our actual risk is varying because our expected risk is varying. So let's find out why.


Why does expected risk vary?


It is worth briefly revisting the calculations used to work out a position size. The position measured as a percentage of capital is a product of a lot of different numbers, but it simplifies to this:

position as % of capital = (instrument forecast / average instrument forecast)* (target risk / instrument risk) * instrument weight * IDM

Where the IDM (Instrument Diversification Multiplier) is the factor applied to positions to account for the correlation between trading subsystems (i.e. the trading strategies we run for each instrument and the returns they product, not the underlying instrument returns)

And the portfolio risk calculation is wSw', where w are the weights (basically position as % of capital) and S is the covariance matrix composed of instrument standard deviations and the correlation between instrument returns (different from that used for IDM).

In a very handwaving way, it can be shown that the current expected portfolio risk will then be equal to:

Expected risk = target risk * (relative forecast strength) * (relative correlation factor)

Relative forecast strength is a measure of how strong forecasts are relative to the average; it is equal to the forecasts for each instrument, weighted by instrument weight and divided by the average forecast (set to 10 in pysystemtrade).


All other things being equal, if your forecasts are all +20, and the average is +10, then your expected portfolio risk would be twice the average risk, or roughly twice the target risk (50% in the example I've been using).

Importantly, we want risk to vary according to forecast strength. Otherwise we'd have exactly the same risk on even if our forecasts were all +0.001, as if they were +20 (the maximum allowed under forecast capping).

(There is a school of thought that says that we want risk to remain fixed, which is how a lot of long:short hedge funds construct portfolios, but that is another blog post)

The relative correlation factor (RCF) is a bit more complicated. It is equal to the ratio between the IDM (which accounts for the average correlation across subsystem returns), and the IDM that would be appropriate given the current set of positions and current correlation between instrument returns.

So for example, if you normally trade two subsystems (say US 10 year and S&P 500) with corelation between subsystems of zero then your IDM will be equal to square root of 2: 1.414

Now imagine that for some reason your system has a long average sized position in US 10 years, and a short average sized position in S&P 500 futures, and also that the correlation between these two instruments is -1. A quick calculation shows that the expected risk here will be 2.82 times the average. If the correlation was zero, then the expected risk would be twice the average; and if the correlation was +1 then the expected risk would be zero. The relevant RCF would be 2.82/1.41, 2/1.41, and zero.

Similarly if the current position was long an average sized position in both instruments, then with a correlation of +1 the risk would be 2.8 times the average, with a correlation of zero it would be twice, and again with a correlation of -1 it would be zero. The relevant RCF again are 2.82/1.41, 2/1.41 and 0. Notice the symmetry here - we'll use this result later.

Clearly the RCF can vary quite a lot depending on what the current positions are, and the current correlation matrix. You might argue that positions and correlations of this kind are unlikely given the average correlation between subsystems is zero. They are unlikely, but they aren't impossible. In particular, correlations do vary especially in the kind of crisis we've just seen.

The RCF is more of an annoyance in terms of expected risk; we wouldn't neccessarily want our risk to be a lot higher just because the positions we happen to have on are especially toxic given todays correlation factor.

Let's plot the relative forecast strength against our expected risk to see if we can decompose how much of our risk is coming from these two components: relative forecast (which we like!), and relative correlation (which we don't like!).


def forecast__strength_for_system(system):
    list_of_instruments = system.get_instrument_list()
    forecasts = [system.combForecast.get_combined_forecast(instrument_code) 
              for instrument_code in
                list_of_instruments]
    forecasts = pd.concat(forecasts, axis=1)
    forecasts.columns = list_of_instruments
    forecasts = forecasts / system.config.average_absolute_forecast
    instrument_weights = system.portfolio.get_instrument_weights()

    weighted_forecast = instrument_weights.ffill() * forecasts.abs().ffill()
    forecast_strength = weighted_forecast.sum(axis=1)

    return forecast_strength

risk_vs_average = 100*risk_series / system.config.percentage_vol_target
forecast_strength = forecast__strength_for_system(system)

to_plot = pd.concat([risk_vs_average, forecast_strength], axis=1)
to_plot.columns = ['Expected risk', 'Forecast strength']



(I've put everything in terms relative to it's expected long average so we can plot them together)

So expected risk does indeed follow forecast strength pretty well. For example, in late 2018:


... forecast strength goes up, and expected risk follows it. However this isn't always the case. Strikingly, in the past couple of months expected risk has exploded while forecast strength has been falling. This is because the relative correlation factor has increased dramatically, most likely as correlations have got really weird in the current crisis.


Overview of the risk overlay


Now we have a better understanding of what is driving our expected risk, it's time to introduce the risk overlay. The risk overlay calculates a risk position multiplier, which is between 0 and 1. When this multiplier is one we make no changes to the positions calculated by our system. If it was 0.5, then we'd reduce our positions by half. And so on.

So the overlay acts across the entire portfolio, reducing risk proportionally on all positions at the same time. 

The risk overlay has three components, designed to deal with the following issues:

- Expected risk that is too high
- Weird correlation shocks combined with extreme positions
- Jumpy volatility (non stationary and non Gaussian vol)

Each component calculates it's own risk multipler, and then we take the lowest (most conservative) value.

That's it. I could easily make this a lot more complicated, but I wanted to keep the overlay pretty simple. It's also easy to apply this overlay to other strategies, as long as you know your portfolio weights and can estimate a covariance matrix (I'm assuming anyone who has read this far can do both of those things, or knows a person that can). You don't need the same concept of a 'forecast' for example, since forecast calculations don't enter into these.

Let's dive into the individual components.


Maximum expected risk


This component assumes that Guassian risk is a good enough model for expected risk, and it also assumes that we don't want too much of it. Specifically the risk multiplier looks like this:

risk multiplier = min(1, 2*target risk / current expected risk)

So if the current expected risk is more than twice the long run target, we'll start reducing our positions. The choice of '2' is arbitrary, and down to personal preference. However, since the combined forecast for an instrument is limited to 2.0, allowing the expected risk to be double the average seems to make sense.

From the discussion above, we'll be doing that if (a) we have very strong relative forecasts, or (b) the current correlation factor is particularly nasty. I could have made this more complex to specifically target the correlation factor, but this is simple enough to understand and explain, and works nicely on any kind of strategy with a long run risk target but varying expected risk.

How often will this kick in? We've already calculated expected risk vs target earlier:

risk_vs_average = 100*risk_series / system.config.percentage_vol_target


So now plotting the series:


There are a few times when risk goes over 2, including in recent weeks. Here is the risk multiplier:

risk_multiplier = 2/risk_vs_average
risk_multiplier[risk_multiplier>1.0]=1.0


Notice the sharp drop at the end, when expected risk balloons in the COVID-19 crisis.


Correlation risk


The maximum expected risk measure assumes that Gaussian risk is sufficient, and that we can forecast it's components (correlation, and standard deviation). Now let's relax that assumption. Correlation risk is the risk that instrument correlations will do scary unusual things, that happen to be bad for my position. If this has already happened (i.e. we have a correlation factor problem) then it will be dealt with in the expected risk calculation, that uses recent historic returns to calculate the instrument correlation. But what if it is about to happen?

There is a very simple way of dealing with this, which is that we replace the estimated correlation matrix with the worst possible correlation matrix. Then we re-estimate our expected risk, and plug it into a risk multiplier formula. Because we're shocking the correlations to the extreme, we allow expected risk to be 4 times larger than our target.

(There is no justification for this number 4, it's calibrated to target a particular point on the realised distribution of the estimate of relative risk. I talk a bit about calibration at the end of the post)

Specifically the risk multiplier looks like this:

risk multiplier = min(1, 4*target risk / current expected risk with worst possible correlation)

What is the worst possible correlation matrix? Simply, it's a matrix where all the correlations are 1. But that's only bad if all of our positions are long, right? If we had offsetting long/short positions, it would help us. You're right, which is why we also use the absolute weights when calculating the expected risk, not the normal signed weights. Note that a correlation of 1 if your weights are all long is equivalent to a correlation of -1 if your weights were long/short (we already saw this in the calculations above).

Here's a horribly hacky way (ugly! slow!) to calculate this risk multiplier (there is a better implementation in pysystemtrade, of which more later). In the gist above replace this function with this code:


def calc_risk_for_date(rolling_corr, rolling_std, index_date, value_of_positions_proportion_capital, list_of_instruments):
    std_dev = rolling_std.loc[index_date].values
    std_dev[np.isnan(std_dev)] = 0.0
    ## Use absolute weights rather than signed    
    weights = value_of_positions_proportion_capital.abs().loc[index_date].values
    weights[np.isnan(weights)]=0.0    
    cmatrix = get_corr_matrix_for_date(rolling_corr, index_date, list_of_instruments)
    # replace correlation matrix with zeros
    # yeah this is ugly and slow, but makes the point clearer    cmatrix[:] = 1.0
    sigma = sigma_from_corr_and_std(std_dev, cmatrix)

    portfolio_variance = weights.dot(sigma).dot(weights.transpose())
    portfolio_std = portfolio_variance**.5
    annualised_portfolio_std = portfolio_std*16.0
    return annualised_portfolio_std

Then we just recalculate everything: expected risk, and expected vs average:

risk_series_for_correlation = get_expected_risk_for_system(system)
risk_vs_average_for_correlation = 100*risk_series_for_correlation / system.config.percentage_vol_target

Let's plot it


Now for the risk multiplier:
risk_multiplier_for_correlation = 4/risk_vs_average_for_correlation
risk_multiplier_for_correlation[risk_multiplier_for_correlation>1.0]=1.0

This is a bit more active than the expected risk filter. Interestingly, it also shows a recent application in March 2020.

Incidentally, because of the way the system scaling works this is effectively the following constraint:

IDM*sqrt[Sum_i( k_i^2) + 2*abs(k_1*k_2*k_3... )] <=4

Where k_i = [instrument weight * forecast / average forecast] for instrument i


Proof of the above result, well for 2 assets anyway. Feel free to do this properly with matrices and stuff.

So it will only go into effect when we have a lot of large forecasts kicking off at the same time. No other inputs are relevant.


Standard deviation risk


Now let's deal with standard deviation risk. Specifically, we're concerned with a situation where we're estimating a standard deviation that is relatively low, but there's a good chance it will get a lot higher. This could be because risk is Gaussian, but varies, or because risk is non Guassian. We don't care what the cause is (and in fact it's impossible to distinguish these two explanations). We just want to deal with it.

To do this we use our standard estimate of portfolio risk, but replace our standard deviation estimates with '99vol'. This rather catchily named value* is the 99th percentile of the standard deviation estimate distribution, measured over the last 10 years. It's the standard deviation we'll get 1% of the time.

* "I've got 99 problems, but vol ain't one of them..." Sorry couldn't resist.

(Incidentally, if current vol is above the 99% point I still use the 99% point in this calculation.  In this case expected risk is likely to be very high anyway)

Once the new risk estimate has been calculated, I apply a multiplier if this comes out more than 6 times the target risk (again, no deep underlying logic for this, just a calibration)

Specifically the risk multiplier looks like this:

risk multiplier = min(1, 6*target risk / current expected risk with 99vol)


Note: Relationship to VAR. Yes, this is a bit like a 99% VAR. I prefer not to use VAR, since it confounds standard deviations and correlations.

Here's the hacky way of calculating it. Using the original gist (without the hacked function above) add one line to this other function:


def get_expected_risk_for_system(system):

    value_of_positions_proportion_capital = get_positions_as_proportion_of_capital(system)

    instrument_returns = get_instrument_returns(system)
    instrument_returns = instrument_returns.ffill().reindex(value_of_positions_proportion_capital.index)

    rolling_std = instrument_returns.ewm(span=30).std()
    rolling_corr = instrument_returns.ewm(span=120).corr()

    # new line    rolling_std = rolling_std.ffill().rolling(2500, min_periods=10).quantile(.99)

    list_of_instruments = system.get_instrument_list()
    expected_risk = calc_expected_risk_over_time(rolling_corr, rolling_std, value_of_positions_proportion_capital,
                                                 list_of_instruments)

    return expected_risk

New risk series:
risk_series_stdev = get_expected_risk_for_system(system)
risk_vs_average_for_stdev = 100*risk_series_stdev / system.config.percentage_vol_target


And the risk multiplier:

risk_multiplier_for_stdev = 6/risk_vs_average_for_stdev
risk_multiplier_for_stdev[risk_multiplier_for_stdev>1.0]=1.0

Putting them together


all_mult = pd.concat([risk_multiplier_for_stdev, risk_multiplier, risk_multiplier_for_correlation], axis=1)
joint_mult = all_mult.min(axis=1)


That's the most conservative multiplier, going back to 1997. 

The results aren't too dramatic: they shouldn't be. This is a risk overlay, to deal with corner cases and potential black swans. The vast bulk of the risk management load is being carried by the core system.


Pysystemtrade implementation


Now to implement the overlay into pysystemtrade. First of all we need some configuration options: as they'd appear in your .yaml file. Here are the defaults:

risk_overlay:
  max_risk_fraction_normal_risk: 2.0
  max_risk_fraction_correlation_risk: 4.0
  max_risk_fraction_stdev_risk: 6.0


Next you need to override the portfolio stage class with an inherited class which includes risk scaling:


## run inside pysstemtraderimport matplotlib
matplotlib.use("TkAgg")
from systems.provided.futures_chapter15.basesystem import *

## use your own config here
config = Config(
            "private.legacy_system.legacy_config_all_markets.yaml")

from systems.futures.risk_overlay import portfoliosRiskOverlay

data = csvFuturesSimData()

system = System([
    Account(), portfoliosRiskOverlay(), PositionSizing(), FuturesRawData(),
    ForecastCombine(), ForecastScaleCap(), Rules()
], data, config)
system.set_logging_level("on")

There are various new methods in the portfolio stage, such as:

system.portfolio.get_risk_multiplier()

Incidentally for efficiency the calculations work a bit different in pysystemtrade; I use weekly returns for correlations, and I only calculate a covariance matrix on a monthly basis (though I do use todays position weights, so the risk multiplier is calculated on a daily basis).


A quick test


I ran a backtest with, and without, the risk overlay to see what it looks like. Firstly here's the whole account curve:

The blue line is with the overlay, the orange line is without. This isn't unexpected; the overlay can only ever reduce risk, and so it will make less in returns unless it is lucky enough to do so only when the system is losing money. Broadly speaking the risk overlay knocks about 3% anually off both the returns and the risk.

The Sharpe Ratios are pretty close though: 0.940 with the overlay and 0.956 with it. More interestingly the overlay reduces the positive skew of the system somewhat (and this holds at all frequencies- read this to see why that's important). One argument for not applying any kind of risk control to trend following is that we lose the positive skew (see here for a relevant discussion).

The kurtosis does fall however, suggesting we are doing a good job of 'tidying up the tails'. Other measures of 'left tailedness', like the 1% quantile point are also improved. Drawdowns are a litle shallower.

If the performance penalty is too great then you can change the calibration of the risk overlay. Try not to tweak these for performance though, that is implicit fitting. Instead target something like:


  • a distributional point on the turnout of the estimated risk relative to the target risk (eg 95% point),
  •  the time you want the filter switched on for on average (eg 1% of the time),
  • the average value of the risk multiplier including when it is switched off (eg 0.99),
  •  or a minimum correlation between the system with and without the overlay (eg 0.98)



Summary


This has been an interesting journey which has hopefully given some more intuition about how the risk in CTA type strategies works. I've also introduced a simple risk overlay that can be used in a number of different strategies.

As usual questions are welcome in the comment box below.


POSTSCRIPT: We can use the maximum risk constraint above to target a fixed risk, i.e. aiming for the same ex-ante risk every day. I explore that idea in this post.