| Overall Statistics |
|
Total Orders 1163 Average Win 0.40% Average Loss -0.42% Compounding Annual Return -1.480% Drawdown 31.500% Expectancy -0.006 Start Equity 10000000000 End Equity 9351472247.04 Net Profit -6.485% Sharpe Ratio -0.141 Sortino Ratio -0.121 Probabilistic Sharpe Ratio 0.502% Loss Rate 49% Win Rate 51% Profit-Loss Ratio 0.95 Alpha -0.031 Beta 0.12 Annual Standard Deviation 0.133 Annual Variance 0.018 Information Ratio -0.6 Tracking Error 0.206 Treynor Ratio -0.156 Total Fees $70162457.86 Estimated Strategy Capacity $62000000.00 Lowest Capacity Asset NKD Y94HV72ZKSN5 Portfolio Turnover 6.95% |
#region imports
from AlgorithmImports import *
from utils import *
#endregion
class BreakoutForecastAlphaModel(AlphaModel):
def __init__(self, algorithm, symbols, periods, multiplier=1):
self.algorithm = algorithm
self.symbols = symbols
self.periods = periods
self.multiplier = multiplier
self.day = -1
def update(self, algorithm, data):
if self.day == algorithm.time.day:
return []
self.day = algorithm.time.day
signals = {period: {} for period in self.periods}
period_weight = 1 / len(self.periods)
# Get signals for each Future
for period in self.periods:
for symbol in self.symbols:
if data.bars.contains_key(symbol):
if self.get_breakout_signal(algorithm, symbol, period):
signals[period][symbol] = 1
else:
signals[period][symbol] = -1
return get_insights(algorithm, signals, period_weight, self.multiplier)
def get_breakout_signal(self, algorithm, symbol, period):
security = algorithm.securities[symbol.canonical]
return security[f"EMA{period}"].current.value > 0
def on_securities_changed(self, algorithm, changes):
for removed in changes.removed_securities:
if removed.type != SecurityType.FUTURE or not removed.symbol.canonical in self.symbols:
continue
symbol = removed.symbol.canonical
security = algorithm.securities[symbol]
algorithm.subscription_manager.remove_consolidator(symbol, security["consolidator"])
for period in self.periods:
security[f"Maximum{period}"].reset()
security[f"Minimum{period}"].reset()
security[f"EMA{period}"].reset()
for added in changes.added_securities:
if added.type != SecurityType.FUTURE or not added.symbol.canonical in self.symbols:
continue
security = algorithm.securities[added.symbol.canonical]
security["consolidator"] = TradeBarConsolidator(timedelta(1))
for period in self.periods:
security[f"Maximum{period}"] = Maximum(period)
security[f"Minimum{period}"] = Minimum(period)
security[f"EMA{period}"] = ExponentialMovingAverage(period // 4)
security["consolidator"] = reset_and_warm_up_breakout(algorithm, security, self.periods)
security["consolidator"].data_consolidated += self.on_consolidated
algorithm.subscription_manager.add_consolidator(added.symbol.canonical, security["consolidator"])
def on_consolidated(self, _, bar):
security = self.algorithm.securities[bar.symbol.canonical]
for period in self.periods:
max_ = security[f'Maximum{period}'].current.value
min_ = security[f'Minimum{period}'].current.value
if not max_ or not min_ or max_ == min_:
continue
mean = (max_ + min_) / 2
security[f'EMA{period}'].update(bar.end_time, 40 * (bar.close - mean) / (max_ - min_))# region imports
from AlgorithmImports import *
from universe import FrontMonthFutureUniverseSelectionModel
from breakout_alpha import BreakoutForecastAlphaModel
from trend_following_alpha import TrendFollowingAlphaModel
# endregion
class BreakoutFuturesAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2019, 1, 1)
self.set_end_date(2023, 6, 30)
self.SetCash(10000000000)
breakout_weight = self.get_parameter("breakout_weight", 0.5)
trend_following_weight = self.get_parameter("trend_following_weight", 0.5)
self.symbols = [
Symbol.create(Futures.Grains.WHEAT, SecurityType.FUTURE, Market.CBOT),
Symbol.create(Futures.Grains.SOYBEANS, SecurityType.FUTURE, Market.CBOT),
Symbol.create(Futures.Meats.FEEDER_CATTLE, SecurityType.FUTURE, Market.CME),
Symbol.create(Futures.Softs.COTTON_2, SecurityType.FUTURE, Market.ICE),
Symbol.create(Futures.Financials.Y_30_TREASURY_BOND, SecurityType.FUTURE, Market.CBOT),
Symbol.create(Futures.Financials.Y_10_TREASURY_NOTE, SecurityType.FUTURE, Market.CBOT),
Symbol.create(Futures.Financials.Y_2_TREASURY_NOTE, SecurityType.FUTURE, Market.CBOT),
Symbol.create(Futures.Financials.FIVE_YEAR_USDMAC_SWAP, SecurityType.FUTURE, Market.CBOT),
Symbol.create(Futures.Indices.SP_500_E_MINI, SecurityType.FUTURE, Market.CME),
Symbol.create(Futures.Indices.MSCI_EUROPE_NTR, SecurityType.FUTURE, Market.NYSELIFFE),
Symbol.create(Futures.Indices.NIKKEI_225_DOLLAR, SecurityType.FUTURE, Market.CME),
Symbol.create(Futures.Currencies.BTC, SecurityType.FUTURE, Market.CME),
Symbol.create(Futures.Currencies.ETH, SecurityType.FUTURE, Market.CME),
Symbol.create(Futures.Metals.GOLD, SecurityType.FUTURE, Market.COMEX),
Symbol.create(Futures.Metals.PLATINUM, SecurityType.FUTURE, Market.NYMEX),
Symbol.create(Futures.Metals.PALLADIUM, SecurityType.FUTURE, Market.NYMEX),
Symbol.create(Futures.Energies.NATURAL_GAS, SecurityType.FUTURE, Market.NYMEX),
Symbol.create(Futures.Energies.CRUDE_OIL_WTI, SecurityType.FUTURE, Market.NYMEX),
Symbol.create(Futures.Energies.BRENT_CRUDE, SecurityType.FUTURE, Market.ICE),
Symbol.create(Futures.Indices.VIX, SecurityType.FUTURE, Market.CFE)
]
self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
self.universe_settings.extended_market_hours = True
self.universe_settings.data_normalization_mode = DataNormalizationMode.BACKWARDS_RATIO
self.universe_settings.data_mapping_mode = DataMappingMode.OPEN_INTEREST
self.universe_settings.contract_depth_offset = 0
self.universe_settings.resolution = Resolution.MINUTE
# Seed initial price data
self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
# We only want front month contract
self.add_universe_selection(FrontMonthFutureUniverseSelectionModel(self.select_future_chain_symbols))
# breakout +/- trend following strategies
breakout_periods = [5, 10, 20, 40, 80, 160, 320]
self.add_alpha(BreakoutForecastAlphaModel(self, self.symbols, breakout_periods, breakout_weight))
trend_periods = [2, 4, 8, 16, 32, 64, 128]
self.add_alpha(TrendFollowingAlphaModel(self.symbols, trend_periods, trend_following_weight))
# Position sizing is controlled by alpha model
self.set_portfolio_construction(InsightWeightingPortfolioConstructionModel())
# Avoid massive bid-ask spread friction
self.set_execution(SpreadExecutionModel(0.03))
# Set benchmark: since it is a buy and hold portfolio, using SPY will be appropriate
self.add_equity("SPY")
self.set_benchmark("SPY")
def select_future_chain_symbols(self, utcTime):
return self.symbols#region imports
from AlgorithmImports import *
from utils import *
#endregion
class TrendFollowingAlphaModel(AlphaModel):
def __init__(self, symbols, periods, multiplier=1):
self.symbols = symbols
self.periods = periods
self.multiplier = multiplier
self.month = -1
def update(self, algorithm, data):
if self.month == algorithm.time.month:
return []
self.month = algorithm.time.month
signals = {period: {} for period in self.periods}
period_weight = 1 / len(self.periods)
# Get signals for each Future
for period in self.periods:
for symbol in self.symbols:
if data.bars.contains_key(symbol):
if self.get_trend_signal(algorithm, symbol, period):
signals[period][symbol] = 1
else:
signals[period][symbol] = -1
return get_insights(algorithm, signals, period_weight, self.multiplier)
def get_trend_signal(self, algorithm, symbol, period):
security = algorithm.securities[symbol.canonical]
return security[f"EMA{period}"].current.value < security.price
def on_securities_changed(self, algorithm, changes):
for removed in changes.removed_securities:
if removed.type != SecurityType.FUTURE or not removed.symbol.canonical in self.symbols:
continue
symbol = removed.symbol.canonical
security = algorithm.securities[symbol]
for period in self.periods:
security[f"EMA{period}"].reset()
for added in changes.added_securities:
if added.type != SecurityType.FUTURE or not added.symbol.canonical in self.symbols:
continue
security = algorithm.securities[added.symbol.canonical]
security["consolidator"] = TradeBarConsolidator(timedelta(1))
for period in self.periods:
security[f"EMA{period}"] = ExponentialMovingAverage(period)
security["consolidator"] = reset_and_warm_up_trend_following(algorithm, security, self.periods)#region imports
from AlgorithmImports import *
from Selection.FutureUniverseSelectionModel import FutureUniverseSelectionModel
#endregion
class FrontMonthFutureUniverseSelectionModel(FutureUniverseSelectionModel):
'''Creates futures chain universes that select the front month contract and runs a user
defined futureChainSymbolSelector every day to enable choosing different futures chains'''
def __init__(self, select_future_chain_symbols, rebalance_period = 7):
super().__init__(timedelta(rebalance_period), select_future_chain_symbols)
def filter(self, filter):
'''Defines the futures chain universe filter'''
return (filter.front_month().only_apply_filter_at_market_open())#region imports
from AlgorithmImports import *
#endregion
def reset_and_warm_up_breakout(algorithm, security, periods):
consolidator = security['consolidator']
lookback = max(periods)
# historical request to update the consolidator that will warm up the indicator
history = algorithm.history[consolidator.input_type](security.symbol, lookback, Resolution.DAILY,
data_normalization_mode = DataNormalizationMode.SCALED_RAW)
# Replace the consolidator, since we cannot reset it
# Not ideal since we don't the consolidator type and period
algorithm.subscription_manager.remove_consolidator(security.symbol, consolidator)
consolidator = TradeBarConsolidator(timedelta(1))
for period in periods:
for indicator in [security[f'Maximum{period}'], security[f'Minimum{period}']]:
indicator.reset()
algorithm.register_indicator(security.symbol, indicator, consolidator)
security[f'EMA{period}'].reset()
for bar in list(history)[:-1]:
consolidator.update(bar)
for period in periods:
max_ = security[f'Maximum{period}'].current.value
min_ = security[f'Minimum{period}'].current.value
mean = (max_ + min_) / 2
security[f'EMA{period}'].update(bar.end_time, 40 * (bar.close - mean) / (max_ - min_))
return consolidator
def reset_and_warm_up_trend_following(algorithm, security, periods):
indicators = [security[f'EMA{period}'] for period in periods]
consolidator = security['consolidator']
lookback = max(periods)
# historical request to update the consolidator that will warm up the indicator
history = algorithm.history[consolidator.input_type](security.symbol, lookback, Resolution.DAILY,
data_normalization_mode = DataNormalizationMode.SCALED_RAW)
# Replace the consolidator, since we cannot reset it
# Not ideal since we don't the consolidator type and period
algorithm.subscription_manager.remove_consolidator(security.symbol, consolidator)
consolidator = TradeBarConsolidator(timedelta(1))
for indicator in indicators:
indicator.reset()
algorithm.register_indicator(security.symbol, indicator, consolidator)
for bar in list(history)[:-1]:
consolidator.update(bar)
return consolidator
def get_insights(algorithm, signals, period_weight, alpha_weight=1):
# Equal risk capital per signal group
symbol_signals = {}
for period, selected in signals.items():
if not selected:
continue
symbol_weight = 1 / len(selected)
for symbol, sign in selected.items():
mapped = algorithm.securities[symbol].mapped
multiplier = algorithm.securities[symbol].symbol_properties.contract_multiplier
weight = sign * period_weight * symbol_weight * alpha_weight / multiplier
if mapped not in symbol_signals:
symbol_signals[mapped] = 0
symbol_signals[mapped] += weight
return [
Insight.price(mapped, Expiry.END_OF_WEEK, InsightDirection.UP, weight = weight)
for mapped, weight in symbol_signals.items() if abs(weight) >= 0.005
]