Created with Highcharts 12.1.2EquityJan 2019Jan…May 2019Sep 2019Jan 2020May 2020Sep 2020Jan 2021May 2021Sep 2021Jan 2022May 2022Sep 2022Jan 2023May 2023Sep 20235G7.5G10G12.5G-50-25000.51010250M500M025G50G024
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
    ]