## Introduction

In this tutorial, we use a trend filter to forecast the upcoming trend of Futures contracts and to determine position sizing. We also add buffering to reduce trading fees. This algorithm is a re-creation of strategy #8 from Advanced Futures Trading Strategies (Carver, 2023). The results show that applying the trend filter and diversified position sizing logic to a portfolio of Futures contracts for the S&P 500 E-mini and 10-year treasury note outperforms a buy-and-hold benchmark strategy with the same contracts.

## Forecasting Trends

A common technique to quantitatively classify the trend direction of an instrument is to use an exponential moving average crossover (EMAC) on historical prices.

$\textrm{EMAC(16, 64)} = \textrm{EMA(16)} - \textrm{EMA(64)}$

When the EMAC is positive, the instrument is in an uptrend and when the EMAC is negative, the instrument is in a downtrend. In the case of Futures, the EMA indicators are applied to the adjusted prices of the continuous contract because their adjusted prices remove price jumps that could give a false change of the EMAC sign.

The sign of the EMCA can only signal the trend direction for an individual security. To convert the EMAC value to something that you can use to compare the trend of two different securities, divide the EMAC value by the security’s standard deviation of returns.

$\textrm{Raw forecast} = \frac{EMAC(16, 64)}{\sigma_p}$

We want to know whether the raw forecast is relatively large or small for an individual security. To this end, we can divide the raw forecast by the average absolute value of trailing raw forecasts. Furthermore, Carver suggests scaling the forecast to have an average absolute value of 10 to make it easier to understand.

$\textrm{Scaled forecast} = \textrm{Raw forecast} \times 10 \div \textrm{Average absolute value of raw forecast}$

We can name the latter part of this equation the forecast scalar.

$\textrm{Forecast scalar} = 10 \div \textrm{Average absolute value of raw forecast}$

$\textrm{Scaled forecast} = \textrm{Raw forecast} \times \textrm{Forecast scalar}$

Carver provides the forecast scalar value of 4.1 for this strategy.

$\textrm{Scaled forecast} = \textrm{Raw forecast} \times 4.1$

We then cap the scaled forecast to fall within the interval [-20, 20].

$\textrm{Capped forecast} = \max(\min(\textrm{Scaled forecast}, 20), -20)$

The capped forecast is a factor used to determine our Future contract trade direction and position size.

## Position Sizing

To determine the optimal position size of each Future under consideration, we use the following formula:

$N_{i,t} = \frac{\textrm{Capped forecast}_{i,t} \times \textrm{Capital} \times \textrm{IDM} \times \textrm{Weight}_i \times \tau }{10 \times \textrm{Multiplier}_i \times \textrm{Price}_{i,t} \times \sigma_{\%i,t}}$

where

• $$N_{i,t}$$ is the number of contracts to hold for instrument $$i$$ at time $$t$$.
• $$\textrm{Capped forecast}_{i,t}$$ is the capped forecast of instrument $$i$$ at time $$t$$.
• $$\textrm{Capital}$$ is the current total portfolio value.
• $$\textrm{IDM}$$ is the instrument diversification multiplier. Carver provides a methodology for dynamically calculating the IDM in later strategies, but this algorithm only trades two instruments, so we follow the recommendation of setting the IDM to 1.5.
• $$\textrm{Weight}_i$$ is the classification weight of instrument $$i$$. The algorithm we create here only trades 2 Futures, so $$\textrm{Weight}_i$$ always 0.5.
• $$\tau$$ is the target portfolio risk. This is oftentimes a subjective value that’s a function of the investor’s risk profile. In this algorithm, we use the recommended value of 0.2 (20%) from Carver.
• $$\textrm{Multiplier}_i$$ is the contract multiplier of instrument $$i$$.
• $$\textrm{Price}_{i,t}$$ is the price of instrument $$i$$ at time $$t$$.
• $$\sigma_{\%i,t}$$ is the annualized risk of instrument $$i$$ at time $$t$$.

To avoid very small orders and reduce trading fees, we set a trading buffer. The trading buffer is a region surrounding the optimal position size where we don’t bother trading. To calculate the buffer size, we apply a fraction $$F$$ in the following equation:

$B_{i,t} = \frac{\textrm{F} \times \textrm{Capital} \times \textrm{IDM} \times \textrm{Weight}_i \times \tau }{\textrm{Multiplier}_i \times \textrm{Price}_{i,t} \times \sigma_{\%i,t}}$

It’s possible to calculate $$F$$ from factors like the profitability of the trading rule and its associated costs, but Carver suggests a conservative value of 0.1 (10%).

Next, we can calculate the lower and upper boundaries of the buffer.

$B^U_{i,t} = round(N_{i,t} + B_{i,t})$

$B^L_{i,t} = round(N_{i,t} - B_{i,t})$

Therefore, if our current holdings are above the buffer, we rebalance to be at $$B^U_{i,t}$$. If our current holdings are below the buffer, we rebalance to be at $$B^L_{i,t}$$. If our current holdings are inside the buffer zone, we don’t adjust the position.

## Method

Let’s walk through how we can implement this trading algorithm with the LEAN trading engine.

### Initialization

We start with the Initialization method, where we set the start date, end date, cash, and some settings.

class FuturesFastTrendFollowingLongAndShortWithTrendStrenthAlgorithm(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2020, 7, 1)
self.SetEndDate(2023, 7, 1)
self.SetCash(1_000_000)

self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
self.SetSecurityInitializer(BrokerageModelSecurityInitializer(self.BrokerageModel, FuncSecuritySeeder(self.GetLastKnownPrices)))
self.Settings.MinimumOrderMarginPortfolioPercentage = 0

### Universe Selection

We follow the example algorithm outlined in the book and configure the Universe Selection model to return Futures contracts for the S&P 500 E-mini and 10-year treasury note. To accomplish this, follow these steps:

1. In the futures.py file, define the Futures symbols and their respective market category.

categories = {
Symbol.Create(Futures.Financials.Y10TreasuryNote, SecurityType.Future, Market.CBOT): ("Fixed Income", "Bonds"),
Symbol.Create(Futures.Indices.SP500EMini, SecurityType.Future, Market.CME): ("Equity", "US")
}

2. In the universe.py file, import the categories and define the Universe Selection model.

from Selection.FutureUniverseSelectionModel import FutureUniverseSelectionModel
from futures import categories

def __init__(self) -> None:
super().__init__(timedelta(1), self.select_future_chain_symbols)
self.symbols = list(categories.keys())

def select_future_chain_symbols(self, utc_time: datetime) -> List[Symbol]:
return self.symbols

def Filter(self, filter: FutureFilterUniverse) -> FutureFilterUniverse:
return filter.Expiration(0, 365)

3. In the main.py file, extend the Initialization method to configure the universe settings and add a Universe Selection model

class FuturesFastTrendFollowingLongAndShortWithTrendStrenthAlgorithm(QCAlgorithm):
def Initialize(self):
# . . .
self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.BackwardsPanamaCanal
self.UniverseSettings.DataMappingMode = DataMappingMode.OpenInterest
self.AddUniverseSelection(AdvancedFuturesUniverseSelectionModel())

To form the continuous contract price series, select the current contract based on open interest and adjust historical prices using the BackwardsPanamaCanal DataNormalizationMode. Note that the original algorithm by Carver rolls over contracts $$n$$ days before they expire instead of rolling based on open interest, so our results will be slightly different.

### Forecasting Future Trends

We first define the constructor of the Alpha model in the alpha.py file.

from futures import categories

class FastTrendFollowingLongAndShortWithTrendStrenthAlphaModel(AlphaModel):

FORECAST_SCALAR_BY_SPAN = {64: 1.91, 32: 2.79, 16: 4.1, 8: 5.95, 4: 8.53, 2: 12.1}

def __init__(self, algorithm, slow_ema_span, abs_forecast_cap, sigma_span, target_risk, blend_years):
self.algorithm = algorithm
self.slow_ema_span = slow_ema_span
self.slow_ema_smoothing_factor = self.calculate_smoothing_factor(self.slow_ema_span)
self.fast_ema_span = int(self.slow_ema_span / 4)
self.fast_ema_smoothing_factor = self.calculate_smoothing_factor(self.fast_ema_span)

self.abs_forecast_cap = abs_forecast_cap

self.sigma_span = sigma_span
self.target_risk = target_risk
self.blend_years = blend_years

self.idm = 1.5
self.forecast_scalar = self.FORECAST_SCALAR_BY_SPAN[self.fast_ema_span]

self.categories = categories
self.total_lookback = timedelta(365*self.blend_years+self.slow_ema_span)

self.day = -1

When new Futures are added to the universe, we set up a consolidator to keep the trailing data updated as the algorithm executes. If the security that’s added is a continuous contract, we combine two EMA indicators to create the EMAC.

class FastTrendFollowingLongAndShortWithTrendStrenthAlphaModel(AlphaModel):

def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
symbol = security.Symbol

# Create a consolidator to update the history
security.consolidator.DataConsolidated += self.consolidation_handler

if security.Symbol.IsCanonical():
# Add some members to track price history
security.raw_history = pd.Series()

# Create indicators for the continuous contract
security.fast_ema = algorithm.EMA(security.Symbol, self.fast_ema_span, Resolution.Daily)
security.slow_ema = algorithm.EMA(security.Symbol, self.slow_ema_span, Resolution.Daily)
security.ewmac = IndicatorExtensions.Minus(security.fast_ema, security.slow_ema)

security.automatic_indicators = [security.fast_ema, security.slow_ema]

self.futures.append(security)

for security in changes.RemovedSecurities:
# Remove consolidator + indicators
algorithm.SubscriptionManager.RemoveConsolidator(security.Symbol, security.consolidator)
if security.Symbol.IsCanonical():
for indicator in security.automatic_indicators:
algorithm.DeregisterIndicator(indicator)

The consolidation handler simply updates the security history and trims off any history that’s too old.

class FastTrendFollowingLongAndShortWithTrendStrenthAlphaModel(AlphaModel):

def consolidation_handler(self, sender: object, consolidated_bar: TradeBar) -> None:
security = self.algorithm.Securities[consolidated_bar.Symbol]
end_date = consolidated_bar.EndTime.date()
if security.Symbol.IsCanonical():
else:
# Update raw history
continuous_contract = self.algorithm.Securities[security.Symbol.Canonical]
if consolidated_bar.Symbol == continuous_contract.latest_mapped:
continuous_contract.raw_history.loc[end_date] = consolidated_bar.Close
continuous_contract.raw_history = continuous_contract.raw_history[continuous_contract.raw_history.index >= end_date - self.total_lookback]

Next, we define a helper method to estimate the standard deviation of returns.

class FastTrendFollowingLongAndShortWithTrendStrenthAlphaModel(AlphaModel):

# Align history of raw and adjusted prices
raw_history_aligned = raw_history.loc[idx]

# Calculate exponentially weighted standard deviation of returns
rolling_ewmstd_pct_returns = returns.ewm(span=self.sigma_span, min_periods=self.sigma_span).std().dropna()
if rolling_ewmstd_pct_returns.empty: # Not enough history
return None
# Annualize sigma estimate
annulized_rolling_ewmstd_pct_returns = rolling_ewmstd_pct_returns * (self.annulaization_factor)
# Blend the sigma estimate (p.80)
blended_estimate = 0.3*annulized_rolling_ewmstd_pct_returns.mean() + 0.7*annulized_rolling_ewmstd_pct_returns.iloc[-1]
return blended_estimate

The Update method calculates the forecast of the Futures at the beginning of each trading day and returns some Insight objects for the Portfolio Construction model.

class FastTrendFollowingLongAndShortWithTrendStrenthAlphaModel(AlphaModel):

def Update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
# Record the new contract in the continuous series
if data.QuoteBars.Count:
for future in self.futures:
future.latest_mapped = future.Mapped

# If warming up and still > 7 days before start date, don't do anything
# We use a 7-day buffer so that the algorithm has active insights when warm-up ends
if algorithm.StartDate - algorithm.Time > timedelta(7):
return []

if self.day == data.Time.day or data.Bars.Count == 0:
return []

# Estimate the standard deviation of % daily returns for each future
sigma_pct_by_future = {}
for future in self.futures:
# Estimate the standard deviation of % daily returns
if sigma_pct is None:
continue
sigma_pct_by_future[future] = sigma_pct

# Create insights
insights = []
weight_by_symbol = GetPositionSize({future.Symbol: self.categories[future.Symbol] for future in sigma_pct_by_future.keys()})
for symbol, instrument_weight in weight_by_symbol.items():
future = algorithm.Securities[symbol]
current_contract = algorithm.Securities[future.Mapped]
daily_risk_price_terms = sigma_pct_by_future[future] / (self.annulaization_factor) * current_contract.Price # "The price should be for the expiry date we currently hold (not the back-adjusted price)" (p.55)

# Calculate target position
position = (algorithm.Portfolio.TotalPortfolioValue * self.idm * instrument_weight * self.target_risk) \
/(future.SymbolProperties.ContractMultiplier * daily_risk_price_terms * (self.annulaization_factor))

# Adjust target position based on forecast
forecast = max(min(scaled_forecast_for_ewmac, self.abs_forecast_cap), -self.abs_forecast_cap)

if forecast * position == 0:
continue
# Save some data for the PCM
current_contract.forecast = forecast
current_contract.position = position

# Create the insights
local_time = Extensions.ConvertTo(algorithm.Time, algorithm.TimeZone, future.Exchange.TimeZone)
expiry = future.Exchange.Hours.GetNextMarketOpen(local_time, False) - timedelta(seconds=1)
insights.append(Insight.Price(future.Mapped, expiry, InsightDirection.Up if forecast * position > 0 else InsightDirection.Down))

if insights:
self.day = data.Time.day

return insights

Finally, to add the Alpha model to the algorithm, we extend the Initialization method in the main.py file to set the new Alpha model and add a warm-up period.

from alpha import FastTrendFollowingLongAndShortWithTrendStrenthAlphaModel

class FuturesFastTrendFollowingLongAndShortWithTrendStrenthAlgorithm(QCAlgorithm):

def Initialize(self):
# . . .
slow_ema_span = 2 ** self.GetParameter("slow_ema_span_exponent", 6)
blend_years = self.GetParameter("blend_years", 3)
self,
slow_ema_span,
self.GetParameter("abs_forecast_cap", 20),
self.GetParameter("sigma_span", 32),
self.GetParameter("target_risk", 0.2),
blend_years
))

self.SetWarmUp(timedelta(365*blend_years + slow_ema_span + 7))

### Calculating Position Sizes

We first define a custom Portfolio Construction model in the portfolio.py file that calculates the optimal position for each Future and applies the buffer zone logic.

class BufferedPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):

def __init__(self, rebalance, buffer_scaler):
super().__init__(rebalance)
self.buffer_scaler = buffer_scaler

def CreateTargets(self, algorithm: QCAlgorithm, insights: List[Insight]) -> List[PortfolioTarget]:
targets = super().CreateTargets(algorithm, insights)
for insight in insights:
future_contract = algorithm.Securities[insight.Symbol]
optimal_position = future_contract.forecast * future_contract.position / 10

# Create buffer zone to reduce churn
buffer_width = self.buffer_scaler * abs(future_contract.position)
upper_buffer = round(optimal_position + buffer_width)
lower_buffer = round(optimal_position - buffer_width)

# Determine quantity to put holdings into buffer zone
current_holdings = future_contract.Holdings.Quantity
if lower_buffer <= current_holdings <= upper_buffer:
continue
quantity = lower_buffer if current_holdings < lower_buffer else upper_buffer

# Liquidate contracts that have an expired insight
for target in targets:
if target.Quantity == 0:

return adj_targets

Then to add the Portfolio Construction model to the algorithm, we extend the Initialization method in the main.py file to set the new model.

from portfolio import BufferedPortfolioConstructionModel

class FuturesFastTrendFollowingLongAndShortWithTrendStrenthAlgorithm(QCAlgorithm):

def Initialize(self):
# . . .
self.Settings.RebalancePortfolioOnSecurityChanges = False
self.Settings.RebalancePortfolioOnInsightChanges = False
self.total_count = 0
self.day = -1
self.SetPortfolioConstruction(BufferedPortfolioConstructionModel(
self.rebalance_func,
self.GetParameter("buffer_scaler", 0.1)              # Hardcoded on p.167 & p.173
))

def rebalance_func(self, time):
if (self.total_count != self.Insights.TotalCount or self.day != self.Time.day) and not self.IsWarmingUp and self.CurrentSlice.QuoteBars.Count > 0:
self.total_count = self.Insights.TotalCount
self.day = self.Time.day
return time
return None

## Results

We backtested the strategy from July 1, 2020 to July 1, 2023. The algorithm placed 607 trades, incurred $4,724.53 in fees, and achieved a 0.294 Sharpe ratio. In contrast, if we remove the trade buffering logic, the algorithm places 738 trades, incurs$5,607.51 in fees, and achieves a 0.301 Sharpe ratio (see backtest). Therefore, the buffering logic decreased the trades and fees, but also slightly decreased the Sharpe ratio.

A second benchmark to compare the strategy to is a buy-and-hold portfolio with variable risk position sizing applied to the same Futures contracts. This benchmark achieves a -0.335 Sharpe ratio (see backtest), so the strategy outperforms it in terms of risk-adjusted returns.

Lastly, we found the position sizing logic is vital to the success of the strategy. The backtest shows that the exposure ranges from -1,000% to 1,000%. However, if we cap $$N_{i,t}$$ to fall in the interval [-1, 1], then the Sharpe ratio drops from 0.294 to 0.129 (see backtest).

## Further Research

If you want to continue developing this strategy, some areas of further research include:

• Increasing the universe size