Showing posts with label leveraged trading book. Show all posts
Showing posts with label leveraged trading book. Show all posts

Friday, 3 July 2020

Do non binary forecasts work?


This is a post about forecasts in trading systems. A forecast is a calibrated expectation for future risk adjusted returns. In more layman like terms, it is a measure of how confident we are about a bullish (positive forecast) or bearish (negative forecast).

Perhaps it is easiest to think about forecasts if we compare them to what is not: a forecast is non binary. A binary trading system will decide whether to go long, or short, but it does not get more granular than that. It will buy, or sell, some fixed size of position. The size of the position may vary according to various factors such as risk or account size (enumerated in this recent post) but importantly it won't depend on the level of forecast conviction.

In my two books on trading ('Systematic' and 'Leveraged' Trading) I confidently stated that non binary forecasts work: in other words that you should scale your positions according to the conviction of your forecasts, and doing so will improve your risk adjusted returns compared to using binary forecasts. 

I did present some evidence for this in 'Leveraged Trading', but in this post I will go into a lot more detail of this finding, and explore some nuances.

This will be the first in a series of four broadly related posts. The second post will explore volatility forecasting, and whether it improving it can improve forecasting.

In the third post I explore the issue of whether it makes sense to fix your expected portfolio risk (a question that was prompted by a comment on a recent post I did on exogenous risk management).  This is related to forecasting, because the use of forecasts imply that you should let your expected risk vary according to how strong your forecasts are. If forecasting works, then fixing your risk should make no sense.

The final post (as yet unwritten) will be about the efficient use of capital for small traders. If forecasts work, then we can use capital more efficiently by only taking positions in instruments with large forecasts. I explored this to some degree in a previous post where I used a (rather hacky) non linear scaling to exploit this property. I have recently had an idea for doing this in a fancier way that will allow very large portfolios with very limited capital. This might end up being more than one post... and may take a while to come out.

Let us begin.

<UPDATE 6th July: Added 'all instruments' plots without capping>


Forecasts and risk adjusted returns


Econometrics 101 says that if you want to see wether there is a relationship between two variables you should start off by doing some kind of scatter plot. Forecasts try and predict future risk adjusted returns, so we'll plot the return for the N days, divided by the daily volatility estimate for the return. We get N days by first estimating the average holding period of the forecast. On the x-axis we'll plot the forecast, scaled to an average absolute value of 10.


# pysystemtrade code:
from syscore.pdutils import turnover
import numpy as np
from systems.provided.futures_chapter15.basesystem import futures_system
system = futures_system()

def get_forecast_and_normalised_return(instrument, rule):
forecast = system.forecastScaleCap.get_scaled_forecast(instrument, rule)

# holding period
Ndays = int(np.ceil(get_avg_holding_period_for_rule(forecast)))


raw_price = system.data.get_raw_price(instrument)
## this is a daily vol, adjust for time period
returns_vol = system.rawdata.daily_returns_volatility(instrument)
scaled_returns_vol = returns_vol * (Ndays**.5)

raw_daily_price = raw_price.resample("1B").last().ffill()
## price Ndays in the future
future_raw_price = raw_daily_price.shift(-Ndays)
price_change = future_raw_price - raw_daily_price
    # these normalised change will have E(standard deviation) 1
normalised_price_change = price_change / scaled_returns_vol.ffill()

pd_result = pd.concat([forecast, normalised_price_change], axis=1)
pd_result.columns = ['forecast', 'normalised_return']

pd_result = pd_result[:-Ndays]

return pd_result

def get_avg_holding_period_for_rule(forecast):
avg_annual_turnover = turnover(forecast, 10)
holding_period = 256 / avg_annual_turnover

return holding_period

Let's use the trading rule from chapter six of "Leveraged Trading", EWMAC 16,64*; and pick an instrument I don't know Eurodollar**. 
* That's a moving average crossover between two exponentially weighted moving averages, with a 16 day and a 64 day span respectively
** Yes I've cherry picked this to make the initial results look nice and bring out some interesting points, but I will be doing this properly across my entire universe of futures later

instrument="EDOLLAR"
rule = "ewmac16_64"

pd_result = get_forecast_and_normalised_return(instrument, rule)
pd_result.plot.scatter('forecast', 'normalised_return')
X-Axis forecast, Y-axis subsequent risk adjusted return over average holding period of 17 weekdays

That is quite pretty, but not especially informative. It's hard to tell whether the trading rule even works, i.e. is a positive forecast followed by a positive return over the next 17 business days (which happens to be the holding period for this rule), and vice versa? We can check that easily enough by seeing what the returns are like conditioned on the sign of the forecast:
pos_returns = pd_result[pd_result.forecast>0].normalised_return
neg_returns = pd_result[pd_result.forecast<0].normalised_return
print(pos_returns.mean())
print(neg_returns.mean())

print(stats.ttest_ind(pos_returns, neg_returns, axis=0, equal_var=True))

The returns, conditional on a positive forecast, are 0.21 versus 0.02 for a negative forecast. The t-test produces a T-statistic of 7.6, and the p-value is one of those numbers with e-14 at the end of it so basically zero. Incidentally there were more positive forecasts than negative by a ratio of ~2:1, as Eurodollar has generally gone up.


Is the response of normalised return linear or binary?


So far we have proven that the trading rule works, and that a binary trading rule would do just fine thanks very much. But I haven't yet checked whether taking a larger forecast would make more sense. I could do a regression, but that could produce the same result if the relationship was linear or if it was binary (and the point cloud above indicates that the R^2 is going to be pretty dire in any case).

Let's do the above analysis but in a slightly more complicated way:

from matplotlib import pyplot as plt

def plot_results_for_bin_size(size, pd_result):
bins = get_bins_for_size(size, pd_result)
results = calculate_results_for_bins(bins, pd_result)
avg_results = [x.mean() for x in results]
centre_bins = [np.mean([bins[idx], bins[idx - 1]]) for idx in range(len(bins))[1:]]

plt.plot(centre_bins, avg_results)
    ans = print_t_stats(results)

return ans

def print_t_stats(results):
t_results = []
for idx in range(len(results))[1:]:
t_stat = stats.ttest_ind(results[idx], results[idx-1], axis=0, equal_var=True)
t_results.append(t_stat)
print(t_stat)
    return t_results
def get_bins_for_size(size, pd_result):
positive_quantiles = quantile_in_range(size, pd_result, min=-0.001)
negative_quantiles = quantile_in_range(size, pd_result, max=0.001)
return negative_quantiles[:-1]+[0.0]+positive_quantiles[1:]

def quantile_in_range(size, pd_result, min=-9999, max=9999):
forecast = pd_result.forecast
signed_distribution = forecast[(forecast>min) & (forecast<max)]
quantile_ranges = get_quantile_ranges(size)
quantile_points = [signed_distribution.quantile(q) for q in quantile_ranges]
return quantile_points

def get_quantile_ranges(size):
quantile_ranges = np.arange(0,1.0000001,1.0/size)
return quantile_ranges

def calculate_results_for_bins(bins, pd_result):
results = []
for idx in range(len(bins))[1:]:
selected_results = pd_result[(pd_result.forecast>bins[idx-1]) & (pd_result.forecast < bins[idx])]
results.append(selected_results.normalised_return)
return results

Typing plot_results_for_bin_size(1, pd_result) will give the same results as before, plotted on the worlds dullest graph:


Forecast and subsequent risk adjusted return for ewmac16_64 trading rule for Eurodollar. Mean risk adjusted return for two buckets, conditioned on sign of forecast.

Ttest_indResult(statistic=7.614065523409865, pvalue=2.907839550447572e-14)

Now let's up the ante, and use a bin size of 2, which means plotting 4 'buckets'. This means we're looking at normalised returns, conditional on forecast values being in the following ranges: [-32.3,-6.6], [-6.6, 0], [0, 9.0], [9.0, 40.1]. These might seem random but as the code shows the positive and negative region have been split, and then split further into 2 'bins' with 50% of the data put in one sub-region and 50% in the next. Roughly speaking then 25% of the forecast values will fall in each bucket (although we know that is not the case because there are more positive than negative forecasts).

Each point on the plot shows the average return within a 'bucket' on the y-axis, with the x-axis point in the centre of the 'bucket'.

Forecast and subsequent risk adjusted return for ewmac16_64 trading rule for Eurodollar. Mean risk adjusted return for 4 buckets, conditioned on sign and distributional points of forecast.


What crazy non-linear stuff is this? Negative forecasts sure are bad (although this is Eurodollar, and it normally goes up so not that bad), and statistically worse than any positive forecast. But a modestly positive forecast is about as good as a large positive forecast. We can see a little more detail with 12 buckets (bins=6):
Forecast and subsequent risk adjusted return for ewmac16_64 trading rule for Eurodollar. Mean risk adjusted return for 12 buckets, conditioned on sign and distributional points of forecast.


It's clear that, ignoring the wiggling around which is just noise, that there is indeed a roughly linear and fairly monotonic positive relationship between forecast and subsequent risk adjusted return, until the final bin (which represents forecast values of over 17). The forecast line reverts at the extremes.

This is a pretty well known effect in trend following, and there are a few different explanations. One is that trends tend to get exhausted after a while, so a very strong trend is due a reversal. Another is that high forecasts are usually caused by very low volatility (since forecasts are in risk adjusted space, low vol = high forecast), and very low vol has a tendency to mean revert at the same time as markets sharply change direction. Neithier of these explain why the result is assymetric; but in fact it's just that positive trends are more common in Eurodollar.

Here's the plot for Gold for example:

Forecast and subsequent risk adjusted return for ewmac16_64 trading rule for Gold. Mean risk adjusted return for 12 buckets, conditioned on sign and distributional points of forecast.

There is clear reversion in both wings. And here's Wheat:

Forecast and subsequent risk adjusted return for ewmac16_64 trading rule for Wheat. Mean risk adjusted return for 12 buckets, conditioned on sign and distributional points of forecast.


Here there is reversion for negative forecasts, but not for extreme positive forecasts.


Introducing forecast capping


There are different ways to deal with this problem. At one extreme we could fit some kind of cubic spline to the points in these graphs, and create a non linear response function for the forecast. That smacks of overfitting to me. 

There are slightly less mad approaches, such as creating a fixed sine wave type function or a linear approximation thereof. This has very few parameters but still leads to weird behaviour: when a trend reverses you initially increase your position unless you introduce hysteresis into your trading system (i.e. you behave differently when your forecast has been decreasing than when it is increasing). 

A much simpler approach is to do what I actually do: cap the forecasts at a value of -20,20 (which is exactly double my target absolute value of 10). This also makes sense from a risk control point of view.

There are some other reasons for doing this, discussed in both my books on trading.

We just need to change one line in the code:
forecast = system.forecastScaleCap.get_capped_forecast(instrument, rule)

And here is the revised plot for Eurodollar with a bin size of 2:

Capped forecast and subsequent risk adjusted return for ewmac16_64 trading rule for Eurodollar. Mean risk adjusted return for 4 buckets, conditioned on sign and distributional points of forecast.


That's basically linear, ish. With bin size of 6:

Capped forecast and subsequent risk adjusted return for ewmac16_64 trading rule for Eurodollar. Mean risk adjusted return for 12 buckets, conditioned on sign and distributional points of forecast.



There is still a little reversion in the wings, but it's more symmetric and ignoring the wiggling there is clearly a linear relationship here. I will leave the problem of whether you should behave differently in the extremes for another day. 


Formally testing for non-binaryness


We'll focus on a bin size of 2 (i.e. a total of 4 buckets), which is adequate to see whether non binary forecasts make sense or not without having to look at a ton of numbers, many of which won't be significant (as the bucket size gets more granular, there is less data in each bucket, and so less significance).

We have the following possibilities drawn on the whiteboard. There are four points in each figure and thus 3 lines connecting them. From top to bottom:
  • binary forecasts make sense
  • linear forecasts make sense
  • reverting forecasts make sense

In black are the results we'd get if the forecast worked (a positive relationship between normalised return and forecast). In red are the results if the forecast didn't work.



So we want a significantly positive slope for the first and third lines as in the middle black plot. But we'd also get that if we had a reverting incorrect forecast (bottom plot in red). So, I add an additional condition that a line drawn between the first and final points should also be positive.  We don't test the slope of the second line. This means that we'd ignore a response with an overall positive slope, but which has a slight negative 'flat spot' in the middle line.

Note: the first and third T-test comparisions are (by construction) between buckets of exactly the same size, which is nice.

The lines will be positive if the T-test statisics are positive (since they're one sided tests), and they will be significantly positive if the T-statistics give p-values of less than 0.05.

Let's modify the code so it reports the difference between the first and final points as well:

def print_t_stats(results):
t_results = []
print("For each bin:")
for idx in range(len(results))[1:]:
t_stat = stats.ttest_ind(results[idx], results[idx-1], axis=0, equal_var=True)
t_results.append(t_stat)
print("%d %s " % (idx, str(t_stat))
print("Comparing final and first bins:")
t_stat = stats.ttest_ind(results[-1], results[0], axis=0, equal_var=True)
t_results.append(t_stat)
print(t_stat)

return t_results

Here is the output for Eurodollar

>> plot_results_for_bin_size(2, pd_result)
For each bin:
Ttest_indResult(statistic=4.225710114631642, pvalue=2.44857998636189e-05)
Ttest_indResult(statistic=1.814973164207728, pvalue=0.06959073262053131)
Ttest_indResult(statistic=1.9782202453688769, pvalue=0.04795295675153716)
Comparing final and first bins:
Ttest_indResult(statistic=7.36610843915252, pvalue=2.1225843317794611e-13)

The key numbers are in bold: we can see that with a p-value of 0.0479 the third line just passes the test. But the first line and the overall slope tests are passed easily.


Pooling data across instruments


Looking at one trading rule for one instrument is sort of pointless. We have quite a lot of price history for Eurodollar and we only just get statistical significance, for plenty of other instruments we wouldn't. 

Earlier I openly admitted that I cherry picked Eurodollar; readers of Leveraged Trading will know that there are 8 futures markets in my dataset for which the test would definitely fail as this particular trading rule doesn't work (so we will be on one of the 'red line' plots). 

I should probably have cherry picked a market with a clearer linear relationship, but I wanted to show you the funky reversion effect.

Checking each market is also going to result in an awful lot of plots! Instead I'm going to pool the results across instruments. Because the returns and forecasts are all risk adjusted to be in the same scale we can do this by simply stacking up dataframes. Note this will give a higher weight to instruments with more data.

instrument_list = system.data.get_instrument_list()
all_results = []
for instrument_code in instrument_list:
pd_result = get_forecast_and_normalised_return(instrument_code, rule)
all_results.append(pd_result)

all_results = pd.concat(all_results, axis=0)
plot_results_for_bin_size(6, all_results)
Capped forecast and subsequent risk adjusted return for ewmac16_64 trading rule pooled across all instruments. Mean risk adjusted return for 12 buckets, conditioned on sign and distributional points of forecast.

We didn't need to do this plot for the formal analysis, but I thought it would be instructive to show you that once the noise for individual instruments is taken away we basically have a linear relationship, with some flattening in the extremes for forecasts out of the range [-12,+12]. 

For the formal test we want to focus on the bin=2 case, with 4 points:
plot_results_for_bin_size(2, all_results)

Capped forecast and subsequent risk adjusted return for ewmac16_64 trading rule pooled across all instruments. Mean risk adjusted return for 4 buckets, conditioned on sign and distributional points of forecast.



1 Ttest_indResult(statistic=10.359086377726523, pvalue=3.909e-25)
2 Ttest_indResult(statistic=13.502334211993352, pvalue=1.617e-41)
3 Ttest_indResult(statistic=15.974961084038702, pvalue=2.156e-57)
Comparing final and first bins:
Ttest_indResult(statistic=35.73832257341082, pvalue=3.421-e278)

Remember: we want a significantly positive slope for the first and third lines: yes without question. We also want a significantly positive slope between the first and final bins, again no problems here.

For the overall slope, I didn't even know python could represent a p-value that small in floating point. Apparently we can get down to 1.79e-308!

Note that if the first and third T-tests statistics were zero, that would indicate a binary rule would make sense. If they were negative, it would indicate reversion. Finally, if the final comparision between the last and first bins was negative, then the trading rule wouldn't work 

I think we can all agree that for this specific trading rule, a non binary forecast makes sense.


Testing all momentum rules


We can extend this to the other momentum rules in our armoury. For all of these I'm going to plot the bins =6 case with and without capping (because they're usually more fun to look at, and because <spolier alert> they show an interesting pattern in the tails which is more obvious without capping), and then analyse the bins=2 results with capping using the methodology above. Let's start at the faster end with ewmac2_8. 

Forecast without capping and subsequent risk adjusted return for ewmac2_8 trading rule, pooled across all instruments.

Forecast with capping and subsequent risk adjusted return for ewmac2_8 trading rule, pooled across all instruments.

Notice that for this very fast trading rule (too expensive indeed to trade even for many futures), the behaviour in the tails is quite different: the slope definitely does not revert. We can see how people might be tempted to start fitting these response functions, but let's move on to the figures. We want all the T-statistics in bold to be positive and well above 2:

Ttest_indResult(statistic=3.62040542155758, pvalue=0.00029426)
Ttest_indResult(statistic=7.166027593239416, pvalue=7.761585-13)
Ttest_indResult(statistic=2.735993316153726, pvalue=0.006220)
Comparing final and first bins:
Ttest_indResult(statistic=12.883660014469108, pvalue=5.9049-38)

A resounding pass again. Here's ewmac4_8:

Forecast without capping and subsequent risk adjusted return for ewmac4_16 trading rule, pooled across all instruments


Forecast with capping and subsequent risk adjusted return for ewmac4_16 trading rule, pooled across all instruments

We have a pretty smooth linear picture again. I won't bore you with the T-tests, which are all above 6.0 and positive.

Forecast without capping and subsequent risk adjusted return for ewmac8_32 trading rule, pooled across all instruments


Forecast with capping and subsequent risk adjusted return for ewmac8_32 trading rule, pooled across all instruments

The t-statistics are now above 9. To keep things in order, and so you can see the pattern, here is the plot for ewmac16_64 (without capping, and with capping which we've already seen):

Forecast without capping and subsequent risk adjusted return for ewmac16_64 trading rule, pooled across all instruments


Forecast with capping and subsequent risk adjusted return for ewmac16_64 trading rule, pooled across all instruments

Can you see the pattern? Look at the tails. In the very fastest crossover we saw a linear relationship all the way out. Then for the next two plots as the rule slowed down it became more linear. Now we're seeing the tails start to flatten, with strong reversion at the extreme bullish end (although this goes away with capping). 

We already know this rule passes easily, so let's move on.
Forecast without capping and subsequent risk adjusted return for ewmac32_128 trading rule, pooled across all instruments

Forecast with capping and subsequent risk adjusted return for ewmac32_128 trading rule, pooled across all instruments

Now there is a clear flat spot in both tails, so the pattern continues. Oh and the t-statistics are all well above 12. 

One more to go:

Forecast without capping and subsequent risk adjusted return for ewmac64_256 trading rule, pooled across all instruments


Forecast with capping and subsequent risk adjusted return for ewmac64_256 trading rule, pooled across all instruments

It's a pass in case you haven't noticed. And there is some evidence that the flattening/reversion is continuing to become more pronounced on the negative end.

Anyway to summarise, all EWMAC rules have non binary responses.


What about carry?


Now let's turn to the carry trading rule. Again I will plot the bin=6 case, and then analyse the statistics based on bin=2.

Forecast without capping and subsequent risk adjusted return for carry trading rule, pooled across all instruments. Note that the x-axis has been truncated as carry signals without capping are in the range [-220,+160]


Forecast with capping and subsequent risk adjusted return for carry trading rule, pooled across all instruments


That is pretty funky to say the least, and exploring it could easily occupy another post, but let's be consistent and stick to the methodology of analysing the bins=2 results:

Ttest_indResult(statistic=5.3244949302972255, pvalue=1.0147-07)
Ttest_indResult(statistic=36.3351610955016, pvalue=1.4856-287)
Ttest_indResult(statistic=14.78654199081023, pvalue=2.004e-49)
Comparing final and first bins:
Ttest_indResult(statistic=40.85442806158974, pvalue=0.0)

Another clear pass. The carry rule also has a non binary response.


Summary


I hope I've managed to convince you all that non binary is better: the stronger your forecast, the larger your position should be.  Along the way we've uncovered some curious behaviour particularly for slower momentum rules where it looks like the forecast response is dampened or even reverts at more extreme levals. This suggests some opportunties for gratuitous overfitting of a non linear response function, or at the very least a selective reduction in the forecast cap from 20 to 12, but we'll return to that subject in the future. 

Non binary means that we should change our expected risk according to the strength of our forecasts. In the next post I'll test whether this means that fixing our ex-ante risk is a bad thing.

A disadvantage of non binary trading is it needs more capital (as discussed here and in Leveraged Trading). At some point I'll explore how we can exploit the non binary effect to make best use of limited capital. 

This is part one in a series of posts on forecasting. Part two is here.



Thursday, 2 April 2020

How fast should we trade?

This is the final post in a series aimed at answering three fundamental questions in trading:


  • How fast should we trade? (this post)
Understanding these questions will allow you to avoid the two main mistakes made when trading: taking on too much risk and trading too frequently. Incidentally, systematic traders can add another list to that sin: overfitting. But that is a topic too large to be covered in a single post, and I've written about it enough elsewhere in the blog.

As with the other two posts this topic is covered in much more detail in my two books on trading: "Systematic Trading" and "Leveraged Trading"; although there is plenty of new material in this post as well. If you want more information, then it might be worth investing in one of those books. "Leveraged Trading" is an easier read if you're relatively new to trading.

The timing of my posts about risk has turned out to be perfect, with the Coronavirus currently responsible for severe market movements as well as thousands of deaths. It's less obvious why trading too frequently is a problem. The reason is costs. Taking on too much risk will lead to a fast blowup in your account. Trading too often will result in high costs being paid, which means your account will gradually bleed to zero. As I write this, I notice for the first time how often we use metaphors about losing money which relate to death. For obvious reasons I will try and avoid these for the rest of the post.

Incidentally, I'm not going to post anything about 'trading and investing through the Coronavirus'. I have put a few bits and pieces on twitter, but I don't feel in the mood for writing a long post about exploiting this tragedy for financial gain.

Neithier will I be writing anything about the likely future path of markets from here. As you know, I don't feel that making predictions about price movements is something I'm especially good at. I leave that to my trading systems. Finally, I won't be doing any analysis of the models used for predicting Coronavirus deaths. I leave that to epidemiologists.

I will however be posting my normal annual update on performance after the UK tax year ends in a few days time. And I will probably, at some point in the future, write a post reviewing what has happened. But not yet.



Overview


How fast should we trade? We want to maximise our expected returns after costs. That's the difference between two things:


  • Our pre-cost returns
  • Our costs

The structure of this post is as follows: Firstly I'll discuss the measurement and forecasting of trading costs. Then I will discuss how expected returns are affected by trading speed. Finally I will talk about the interaction between these two quantities, and how you can use them to decide how quickly to trade.



Types of costs


There are many different kinds of costs involved in trading. However there are two key categories:


  • Holding costs
  • Trading costs

Holding costs are costs you pay the whole time you have a position on, regardless of whether you are trading it. Examples of holding costs include brokerage account fees, the costs of rolling futures or similar derivatives, interest payments on borrowing to buy shares, funding costs for FX positions, and management fees on ETFs or other funds.

Trading costs are paid every time you trade. Trading costs include brokerage commissions, taxes like UK stamp duty, and the execution costs (which I will define in more detail below). 

Some large traders also pay exchange fees, although these are normally included as part of the brokerage commission. Other traders may receive rebates from exchanges if they provide liquidity.

The basic formula for calculating costs then is:

Total cost per year = Holding cost + (Trading cost * Number of trades)




Execution costs


Most types of costs are pretty easy to define and forecast, but execution costs are a little different. Firstly a definition: the execution cost for a trade is the difference between the cost of trading at the mid-price, and the actual price traded at.

So for example if a market is 100 bid, 101 offer, then the mid-price is just the average of those: 100.5

Some people calculate the mid price as a weighted average, using the volume on each side as the weight. Another term for this cost is market impact.

If we do a market order, and our trade is small enough to be met by the volume at the bid and offer, then our execution cost will be exactly half the spread. If our order is too large, then our execution cost will be larger.

Who actually earns the execution cost you pay? Judging by his smile, it's this guy

Broadly speaking, we can estimate execution costs or measure them from actual trading. You can estimate costs by looking at the spreads in the markets you trade, or using someone elses estimates.

A nice paper with estimates for larger traders is this paper by Frazzini et al, check out figure 4. You can see that someone trading 0.1% of the market volume in a day will pay about 5bp (0.05%) in execution costs. Someone trading 0.2% of the volume will pay 50% more, 7.5bp (0.075%).

When estimating costs, there are a few factors you need to bear in mind. Firstly, the kind of trading you are doing. Secondly, the size of trading.

  • Smaller traders using market orders: Assume you pay half the spread
  • Smaller traders using limit orders or execution algos: You can pay less, but  (I pay about a quarter of the spread on average, using a simple execution algo)
  • Larger traders: Will pay more than half the spread, and will need to acccount for their trading volume.
You can use execution algos (which mix limit and market orders) if you are trading reasonably slowly. You can use limit orders if you're trading a mean reversion type strategy of any speed, with the limits placed around your estimate of fair value (though you may want to implement stop-losses, using market orders). If you are trading a fast trend following strategy, then you're going to have to use market orders.

If you're trading very quickly, then assuming a constant cost of trading is probably unrealistic since the market will react to your order flow and this will significantly change your costs. In this case I'd suggest only using figures from actual trades.

There are other ways to reduce costs, such as smoothing your position or using buffering. If you are trading systematically you can incorporate these into your back-test to see what effect they have on your cost estimates.


Linear and non-linear



An important point here is that smaller traders, to all intents and purposes, face fixed execution costs per trade. If they double the number of trades they do, then their trading costs will also double. Smaller traders have linear trading costs. 

Holding costs will be unaffected by trading, and other costs eg commissions may not increase linearly with trade size and frequency, but this is a reasonable approximation to make.

But larger traders face increasing trading costs per trade. If they do larger size or or more trades, their costs per trade will increase (eg from 5bp to 7.5bp in the figures given in the Frazzini paper above). If they double the number of trades they do their execution costs will more than double; using the figures above they will increase be a factor of 3: twice because they are doing double the number of trades, and then by another 50% as the cost per trade is increasing. Larger traders have non linear trading costs.

Normalisation of costs

What units should we measure costs in? Should it be in pips or basis points? Dollars or as a percentage of our acount value?

For many different reasons I think the best way to measure costs is as a return adjusted for risk. Risk is measured, as in previous posts, as the expected annualised standard deviation of returns.

Suppose for example that we are buying and selling 100 block shares priced at $100 each. The value of each trade is $10,000. We work out our trading costs at $10 per trade, which is 0.1%. The shares have a standard deviation of 20% a year. So each trade will cost us 0.1 / 20 = 0.005 units of risk adjusted return. Notice how similar this is to the usual measure of risk adjusted returns, the Sharpe Ratio. We are effectively measuring costs as a negative Sharpe Ratio.

We don't include a risk free rate in this calculation, as otherwise we'd end up cancelling it out when we subtract costs as a Sharpe Ratio from pre-cost returns measured in the same units.

Why does this make sense? Well, it makes it easier to compare trading costs across different instruments, account sizes, and time periods. Trading costs measured in dollar terms look very high for a large futures contract like the S&P 500, but they're actually quite low. Because of the COVID-19 crisis, spreads in most markets are pretty wide at the moment, but this means costs in risk adjusted terms are actually pretty similar. 

It also relates to how we scale positions in the second post of this series. Since we scale positions according to the risk of an instrument, it makes sense to scale costs accordingly.



Estimating the number of trades


Let's return to the basic formula above: 

Total cost per year = Holding cost + (Trading cost * Number of trades)

We're going to need to calculate the expected number of trades. How to do this?

  • We can infer it from the size of our stop-loss relative to volatility, defined in the first post as X (this works no matter what kind of trader you are)
  • Systematic Traders: We can get it from a backtest
  • Systematic Traders: We can use some heuristics based on the kind of trading system we are running

You can find heuristics for different trading systems in both of my books on trading; in this post I'm going to focus on the stop loss method as it's simpler, applies to all traders, and is consistent with the methodology I'm using in the other posts.

Here's the table you need:

Fraction of volatility 'X'    Average trades per year

0.025                                97.5
0.05                                 76.5
0.1                                  46.9
0.2                                  21.4
0.3                                  11.9
0.4                                   7.8
0.5                                   5.4
0.6                                   4.0
0.7                                   3.1
0.8                                   2.4
0.9                                   2.1
1.0                                   1.7



We will use the data in this table later when we try and work out how fast we should be trading.


Trading cost calculations: example


We know have enough information to work out how the trading costs for a given instrument and stop loss fraction.

In my book, "Leveraged Trading", I include examples for all the main types of traded instruments (futures, spot FX, spread bets, CFDs and stock/ETF trading). Here however there isn't really enough space, so I'm just going to focus on my favourite: futures.

As I started out life as a fixed income trader, let's consider the costs of the Eurodollar future. Eurodollars are relatively pricey to trade for a future, but still cheaper than the products most retail investors prefer like CFDs, spread bets and spot FX.

Each contract index point is worth $2500 and the current price of the June 2023 I hold is $99.45 (but that may change!). So each contract has a current notional value of 2500*99.45 = $248,625. My broker charges $1 per contract in commission, and the spread is 0.005 of a point wide (except on the front contract: but don't trade that!).

To trade one contract as a small trader with a market order will cost half of the spread: 0.5*0.005*$2500 = $6.25 plus the commission of a $1 = $7.25. That is 0.0029% of the notional value. There are no taxes or further fees due. It doesn't matter how many contracts we trade, it will always cost 0.0029% of the notional value per trade.

What about holding costs? Each contract has to be rolled quarterly. It's usually possible to do the roll as a calendar spread rather than two seperate trades. This reduces risk, but also means it will cost the same as a regular trade in execution cost (though we will pay two lots of commission). So each roll trade will cost $6.25 plus $2 = $8.25, or 0.00332% of the notional value. Four lots of that per year adds up to 0.0132% in holding costs.

Let's convert these into risk adjusted terms. The risk of Eurodollars is currently elevated, but in more normal times it averages about 0.5% a year. So the execution cost will be 0.0029/0.5 = 0.0058 and the holding cost is 0.0132/0.5 = 0.026. Both in units of Sharpe Ratio.

Here's our formula again:

Total cost per year = Holding cost + (Trading cost * Number of trades)

Total cost per year = 0.026 + (0.0058 * Number of trades)

We could now plug in a value of X into the table above, for example if we used X=0.5 -> 5.4 trades per yer:

Total cost per year = 0.026 + (0.0058 *5.4) = 0.058


Pre-cost returns: Theory


Let us now turn our attention to pre-cost returns. How are these affected by trading speed? Naively, if we double the number of trades we do in a given timeframe, can we double our profits?

We can't double our profits, but they should increase. Theoretically if we double the number of trades we do we will increase our profits  by the square root of 2: 1.414 and so on. This is down to something called The Law Of Active Management. This states that your 'information ratio' will be proportional to the square root of the number of uncorrelated bets that you make. If we make some assumptions then we can boil this down to your return (or Sharpe Ratio) being proportional to the square root of the number of trades you make in a given time frame.


Pre-cost returns: Practice


LAM is a theory, and effectively represents an upper bound on what is possible. In practice it's extremely unlikely that LAM will always hold. Take for example, the Sage of Omaha.
Ladies and Gentlemen, I give you Mr Warren Buffet.

His information ratio is around 0.7 (which is exceptionally good for a long term buy and hold investor), and his average holding period is... well a long time but let's say it's around 5 years. Now under the Law of Active Management what will Warren's IR be if he shortens his holding period and trades more?

X-Axis: Holding Period. Y-Axis: Information ratio


Shortening it to 2 years pushes it up to just over 1.0; pretty good and probably achievable. Then things get silly and we need a log scale to show what's going on. By the time Warren is down to a one week holding period his IR of over 10 put's him amongst the best high frequency trading firms on the planet, despite holding positions for much longer.

When the graph finishes with a holding period of one second, still well short of HFT territory, Warren has a four figure IR. Nice, but very unlikely.

This is a silly example, so let's take a more realistic (and relevant) one. The average Sharpe Ratio (SR) for an arbitrary instrument achieved by the slowest moving average crossover rule I use, MAV 64,256, is around 0.28. It does 1.1 trades per year. What if I speed it up by using shorter moving averages, MAV 32,128 and so on? What does the LAM say will happen to my SR, and what actually happens.

X-axis: Moving average rule N,4N. Y-axis Sharpe Ratio pre-costs

If I turn the dial all the way and start trading a MAC 2,8 (far left off the graph) the LAM says the Sharpe should be a stonking 1.68. The reality is a very poor 0.07. Momentum just doesn't work so well at shorter timeframes, although it does consistently well between MAC8 and MAC64. You can't just increase the speed of a trading rule and expect to make more money; indeed you may well make less.


Net returns


We are now finally ready to put pre-cost returns together with costs and see what they can tell us about optimal trading speeds. For now, I will stick with using a set of moving average rules and the costs of trading Eurodollar futures. Later in the post I'll discuss how you can set stop-losses correctly in the presence of trading costs.

Let's take the graph above, but now subtract the costs of trading Eurodollar futures using the formula from earlier:

Total cost per year = 0.026 + (0.0058 * Number of trades)

The number of trades for each trading rule will come from backtests, but there are also values in both of my trading books that you can use.

X-Axis: Moving average rule, Y-axis Sharpe Ratio before and after costs


The faster rules look even worse now and actually lose money. For this particular trading rule the question of how fast we can trade is clear: as slow as possible. I recommend keeping at least 3 variations of moving average in your system for adequate diversification, but the fastest two variations are clearly a waste of money.

Important: I am comparing the average SR pre-cost across all instruments with the costs just for Eurodollar. I am not using the backtested Sharpe Ratios for Eurodollar by itself, which as it happens are much higher than the average due to secular trends in interest rates. This avoids overfitting.

These results are valid for smaller traders with linear costs. Just for fun, let's apply an institutional level of non linear costs. We assume that costs per trade increase by 50% when trading volume is doubled:

X-Axis: Moving average rule, Y-axis Sharpe Ratio with LAM holding before and after costs for larger traders

I'm only showing the LAM here; the actual figures are much worse. Even if we assume that LAM is possible (which it isn't!), then speeding up will stop working at some point (here it's at around MAC16). This is because pre-cost returns are improving with square root of frequency, but costs are increasing more than linearly.


Net returns when returns are uncertain


So far I've treated pre-cost returns and costs as equally predictable. But this isn't the case. Pre-cost returns are actually very hard to predict for a number of reasons. Regular readers will know that I live to quantify this issue by looking at the amount of statistical variation in my estimates of Sharpe Ratio or returns.

Let's look at the SR for the various speeds of trading rules, but this time add some confidence intervals. We won't use the normal 95% interval, but instead I'll use 60%. That means I can be 20% confident that the SR estimate is above the lower confidence line. I also assume we have 20 years of data to estimate the SR:

X-axis: Moving average variations. Y-axis: Actual Sharpe Ratio pre-costs, with 60% confidence bounds applied

Notice that although the faster crossovers are kind of rubbish, the confidence intervals still overlap fairly heavily, so we can't actually be sure that they are rubbish.

Now let's add costs. We can treat these as perfectly forecastable with zero sampling variance, and compared to returns they certainly are:

X-axis: Moving average variations. Y-axis: Actual Sharpe Ratio net of costs, with 80% confidence bounds applied

Once we apply costs there is much clearer evidence that the fastest crossover is significantly worse than the slowest. It also looks like we can be reasonably confident (80% confident to be precise) that all the slower crossovers have an expected SR of at least zero.



A rule of thumb


All of the above stuff is interesting in the abstract, but it's clearly going to be quite a lot of work to apply it in practice. Don't panic. I have a heuristic; I call it my speed limit:



SPEED LIMIT: DO NOT SPEND MORE THAN ONE THIRD OF YOUR EXPECTED PRE-COST RETURNS ON COSTS

How can we use this in practice? Let's rearrange:


Total cost per year = Holding cost + (Trading cost * Number of trades)


(speed limit) Max cost per year = Expected SR / 3

Expected SR / 3 =  Holding cost + (Trading cost * Max number of trades)

Max number of trades = [(Expected SR / 3) - Holding cost] / Trading cost

Specifically for Eurodollar:

Total cost per year = 0.026 + (0.0058 * Number of trades)
Max number of trades = [(Expected SR / 3) - 0.026] / 0.0058

The expected SR varies for different trading rules, but if I plug it into the above formula I get the red line in the plot below:

X axis: Trading rule variation. Y-axis: Blue line: Actual trades per year, Red line: Maximum possible trades per year under speed limit

The blue line shows the actual trades per year. When the blue line is above the red we are breaking the speed limit. Our budget for trading costs and thus trades per year is being exceeded, given the expected SR. Notice that for the very fastest rule the speed limit is actually negative; this is because holding costs alone are more than a third of the expected SR for MAC2.

Using this heuristic we'd abandon the two fastest variations; whilst MAC8 just sneaks in under the wire.  This gives us identical results to the more complicated analysis above.


Closing the circle: what value of X should I use?!


The speed limit heuristic is awfully useful for systematic traders who can accurately measure their expected number of trades and . But what about traders who are using a trading strategy that they can't or won't backtest? All is not lost! If you're using the stoploss method I recommended in the first post of this series, then you can use the table I included earlier to imply what value of X you should have, based on how often you can trade given the speed limit.

For trading a single instrument I would recommend using a value for expected Sharpe Ratio of around 0.24 (roughly in line with the slower MAC rules). 

Max number of trades = [(Expected SR / 3) - Holding cost] / Trading cost
Max number of trades = [0.08 - Holding cost] / Trading cost

Let's look at an example for Eurodollars:

Max number of trades = [0.08 - 0.026] / 0.0058 = 9.3

From the table above:

Fraction of volatility 'X'    Average trades per year

...                                   ...
0.3                                  11.9
0.4                                   7.8
0.5                                   5.4
...                                   ...


This implies that the maximum value for 'X' in our stop loss is somewhere between 0.3 and 0.4; I suggest using 0.4 to be conservative. That equates to 7.8 trades a year, with a holding period of about 6 to 7 weeks.

Important: You also need to make sure your stop loss is consistent with your forecast horizon. For discretionary traders, if you're expecting to trade once a month make sure your trading is based on expected price movements over the next few weeks. For systematic traders, make sure you use a trading rule that has an expected holding period which matches the stoploss holding period.




Summary


I've gone through a lot in the last few posts, so let's quickly summarise what you now know how to do:


  • The correct way to control risk using stop losses: trailing stops as a fraction of annualised volatility ('X')
  • How to calculate the correct position size using current volatility, expected performance, account size, strength of forecast and number of positions.
  • The correct value of 'X' given your trading costs


Knowing all this won't guarantee you will be a profitable trader, but it will make it much more likely that you won't lose money doing something stupid!