Introduction
In the previous Futures strategies that incorporated both trend following and carry trading rules (Combined Carry and Trend and Futures Trend Following and Carry in Different Risk Regimes), we fed the continuous contract (adjusted) prices into the exponential moving average crossover (EMAC) trend following filters to produce trend forecasts. However, continuous contract prices incorporate the returns from spot price changes plus the carry return. For example, the following image shows the adjusted price for the 10-Year T-Note Future and its subcomponents, the spot price and carry return. To reproduce the image, see this project.
The question is: if the adjusted price is distorted by the carry return and we are feeding the adjusted prices into the trend filters, won’t the trend filter forecasts be correlated with the carry forecasts? Moreover, if we give 60% weight to the mean trend forecast and a 40% weight to the mean carry forecast, aren’t we actually giving the carry forecast more than 40% weight? In this tutorial, we investigate whether feeding just the spot prices into the trend filters reduces the correlation between the trend following forecasts and the carry forecasts. If so, a strategy that combines both trend following and carry factors could improve several performance metrics by changing the trend filter input from adjusted prices to spot prices.
This algorithm is a re-creation of strategy #14 from Advanced Futures Trading Strategies (Carver, 2023). Our results follow Carver’s results, showing that producing trend forecasts with spot prices reduces the correlation between the carry and trend forecasts, but it also slightly decreases risk-adjusted returns.
Synthetic Spot Prices
The Dataset Market doesn’t currently provide spot prices for most of the underlying assets for US Futures, but we can derive synthetic spot prices from just the Futures prices. As mentioned above, the continuous contract price is the sum of spot price return and the carry return.
We can decompose the accrued carry to
Where \(\text{year fraction}\) is the percentage of a year between \(t-1\) and \(t\). To calculate the annualized raw carry, see the Combined Carry and Trend strategy.
If the adjusted price is \(P_t\) and the synthetic spot price is \(S_t\), then
Method
Let’s walk through how we can implement this trading algorithm with the LEAN trading engine.
Universe Selection
We keep the same universe from the Combined Carry and Trend and Futures Trend Following and Carry in Different Risk Regimes strategies, which contains a diversified set of 10 Futures.
Synthetic Spot Prices
To calculate the synthetic spot prices, use the following procedure:
- Calculate the timespan between each sample in the continuous contract history.
- Calculate the carry return for each time step by multiplying the series from step 1 and the annualized raw carry.
- Calculate the carry return series by taking the cumulative sum of the series from step 2.
- Take the difference between the continuous contract history and the carry return series to get the synthetic spot price series.
def calculate_synthetic_spot_prices(self, annualized_raw_carry, continuous_contract_history):
# Calculate the fraction of a year between each sample
diff_index = annualized_raw_carry[1:].index - annualized_raw_carry[:-1].index
diff_index_in_seconds = [index_item.total_seconds() for index_item in list(diff_index)]
diff_index_in_years = [index_item_in_seconds / self.SECONDS_IN_YEAR for index_item_in_seconds in diff_index_in_seconds]
diff_index_in_years_as_pd = pd.Series([0] + diff_index_in_years, annualized_raw_carry.index)
# Calculate synthetic spot prices
carry_per_period = diff_index_in_years_as_pd * annualized_raw_carry
cumulative_carry = carry_per_period.cumsum()
cumulative_carry_aligned, continuous_contract_history_aligned = self.align_history(cumulative_carry, continuous_contract_history)
synthetic_spot_prices = continuous_contract_history_aligned - cumulative_carry_aligned
return synthetic_spot_prices
Trend Forecasts
To calculate the trend forecast, feed the synthetic spot prices into the EMAC trend filters. As in the previous strategies, adjust the forecast for the instrument volatility, scale it by the forecast scalar, and then cap the forecast to the range [-20, 20].
def calculate_emac_forecasts_on_spot_prices(self, annualized_raw_carry, continuous_contract_history, daily_risk_price_terms):
# Calculate synthetic spot prices
spot_prices = self.calculate_synthetic_spot_prices(annualized_raw_carry, continuous_contract_history)
# Calculate EMA histories
ema_by_span = {}
for span in self.all_ema_spans:
ema_by_span[span] = spot_prices.ewm(span=span, min_periods=span).mean().dropna()
# Calculate EMAC forecasts from spot prices
forecasts = []
for i, fast_span in enumerate(self.emac_spans):
ewmac = (ema_by_span[fast_span] - ema_by_span[self.slow_ema_spans[i]]).dropna()
if ewmac.empty:
continue
risk_adjusted_ewmac = ewmac.iloc[-1] / daily_risk_price_terms
scaled_forecast_for_ewmac = risk_adjusted_ewmac * self.TREND_FORECAST_SCALAR_BY_SPAN[fast_span]
capped_forecast_for_ewmac = max(min(scaled_forecast_for_ewmac, self.abs_forecast_cap), -self.abs_forecast_cap)
forecasts.append(capped_forecast_for_ewmac)
return forecasts
Carry Forecasts
The process to calculate the carry forecast is the same as in the Combined Carry and Trend strategy.
def calculate_carry_forecasts(self, annualized_raw_carry, daily_risk_price_terms):
carry_forecast = annualized_raw_carry / daily_risk_price_terms
forecasts = []
for span in self.carry_spans:
## Smooth out carry forecast
smoothed_carry_forecast = carry_forecast.ewm(span=span, min_periods=span).mean().dropna()
if smoothed_carry_forecast.empty:
continue
smoothed_carry_forecast = smoothed_carry_forecast.iloc[-1]
## Apply forecast scalar (p. 264)
scaled_carry_forecast = smoothed_carry_forecast * self.CARRY_FORECAST_SCALAR
## Cap forecast
capped_carry_forecast = max(min(scaled_carry_forecast, self.abs_forecast_cap), -self.abs_forecast_cap)
forecasts.append(capped_carry_forecast)
return forecasts
Aggregating Forecasts
Carver tests the strategy using different weights for the trend and carry forecasts. He tests a 0% to 100% allocation to each factor in steps of 10%. The test results show that a 40% allocation to synthetic spot trend and a 60% allocation to carry produced the greatest Sharpe ratio and one of the largest alpha values. We follow the same weighting scheme here before scaling and capping the final forecast value.
def Update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
# . . .
for symbol, instrument_weight in weight_by_symbol.items():
# . . .
# Calculate forecast type 1: EMAC on spot prices
trend_forecasts = self.calculate_emac_forecasts_on_spot_prices(
future.annualized_raw_carry_history, future.adjusted_history, daily_risk_price_terms
)
if not trend_forecasts:
continue
emac_combined_forecasts = sum(trend_forecasts) / len(trend_forecasts) # Aggregate EMAC factors -- equal-weight
# Calculate factor type 2: Carry
carry_forecasts = self.calculate_carry_forecasts(future.annualized_raw_carry_history, daily_risk_price_terms)
if not carry_forecasts:
continue
carry_combined_forecasts = sum(carry_forecasts) / len(carry_forecasts) # Aggregate Carry factors -- equal-weight
# Aggregate factors -- 40% for trend, 60% for carry
raw_combined_forecast = 0.4 * emac_combined_forecasts + 0.6 * carry_combined_forecasts
scaled_combined_forecast = raw_combined_forecast * self.FDM_BY_RULE_COUNT[len(trend_forecasts) + len(carry_forecasts)]
capped_combined_forecast = max(min(scaled_combined_forecast, self.abs_forecast_cap), -self.abs_forecast_cap)
# . . .
Results
We backtested the strategy from July 1, 2020 to July 1, 2023. The algorithm achieved a 0.608 Sharpe ratio. In contrast, the Combined Carry and Trend strategy achieved a 0.749 Sharpe ratio. Therefore, replacing the adjusted prices for the spot prices in the EMAC filters and changing the weighting scheme from 40%-60% to 60%-40% decreases the risk-adjusted returns by about 19%.
We also measured the correlation between all of the trend and carry forecasts for each Future. When we replaced the adjusted prices for spot prices, the correlation between the two factors decreased for every Future in the universe. When using adjusted prices, the average correlation between the forecasts was 0.3216. When using spot prices, the average correlation between the forecasts was 0.2419. Therefore, using spot prices instead of adjusted prices reduced the average correlation between the two factors by about 25%.
Further Research
If you want to continue developing this strategy, some areas of further research include:
- Tracking the performance of each factor and removing the ones that are too expensive to trade
- Incorporating other convergent/divergent factors
- Adjusted the weights of each factor on-the-fly instead of using the hard-coded values provided by Carver
Reference
- Carver, R. (2023). Advanced Futures Trading Strategies: 30 fully tested strategies for multiple trading styles and time frames. Harriman House Ltd.
Derek Melchin
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!