Overall Statistics
Total Orders
2407
Average Win
1.11%
Average Loss
-1.02%
Compounding Annual Return
-13.913%
Drawdown
93.400%
Expectancy
-0.217
Start Equity
1000000
End Equity
94341.52
Net Profit
-90.566%
Sharpe Ratio
-0.697
Sortino Ratio
-0.611
Probabilistic Sharpe Ratio
0.000%
Loss Rate
63%
Win Rate
37%
Profit-Loss Ratio
1.09
Alpha
-0.124
Beta
0.197
Annual Standard Deviation
0.153
Annual Variance
0.023
Information Ratio
-1.025
Tracking Error
0.189
Treynor Ratio
-0.54
Total Fees
$10960.68
Estimated Strategy Capacity
$820000000.00
Lowest Capacity Asset
MES YVXOP65RE0HT
Portfolio Turnover
13.76%
Drawdown Recovery
139
# region imports
from AlgorithmImports import *
# endregion

class BasicFutureAlgorithm(QCAlgorithm):
    def initialize(self):
        #self.universe_settings.asynchronous = True
        #self.SetTimeZone("America/New_York")
        self.set_start_date(2010, 1, 1)
        self.set_cash(1_000_000)
        self.set_security_initializer(
            BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices))
        )
        #self.set_warm_up(210)
        self.risk_factor = .0015
        
        self.futures_raw_list = [
            Futures.Grains.CORN, # /ZC
            Futures.Grains.OATS, # /ZO
            Futures.Grains.SOYBEANS, # /ZS
            Futures.Grains.WHEAT, # /ZW
            Futures.Grains.SOYBEAN_OIL, # /ZL
            Futures.Grains.HRW_WHEAT, # /KE
            Futures.Grains.SOYBEAN_MEAL, # /ZM
            Futures.Grains.HRW_WHEAT, # /KE

            Futures.Softs.SUGAR_11_CME, # /YO

            Futures.Meats.LIVE_CATTLE, # /LE
            Futures.Meats.FEEDER_CATTLE, # /GF
            Futures.Meats.LEAN_HOGS, # /HE

            Futures.Forestry.LUMBER, # /LBR
            Futures.Dairy.CLASS_III_MILK, # /DC
            Futures.Dairy.CASH_SETTLED_CHEESE, # /CSC
            
            Futures.Energy.HEATING_OIL, # /HO
            Futures.Energy.GASOLINE, # /RB

            Futures.Metals.COPPER, # /HG
            #Futures.Metals.PALLADIUM, # /PA
            Futures.Metals.PLATINUM, # /PL
            Futures.Metals.MICRO_SILVER, # /SIL

            Futures.Currencies.AUD, # /6A
            Futures.Currencies.GBP, # /6B
            Futures.Currencies.MICRO_EUR, # /M6E
            Futures.Currencies.JPY, # /6J
            Futures.Currencies.NZD, # /6N
            Futures.Currencies.CHF, # /6S
            Futures.Currencies.CAD, # /6C
            Futures.Currencies.MXN, # /6M

            Futures.Indices.MICRO_NASDAQ_100_E_MINI, # /MNQ
            Futures.Indices.NIKKEI_225_DOLLAR, # /NKD
            Futures.Indices.MICRO_RUSSELL_2000_E_MINI, # /M2K
            Futures.Indices.MICRO_DOW_30_E_MINI, # /MYM
            Futures.Indices.MICRO_SP_500_E_MINI, # /MES

            Futures.Financials.Y_2_TREASURY_NOTE, # /ZT
            Futures.Financials.Y_10_TREASURY_NOTE, # /ZN
            Futures.Financials.Y_5_TREASURY_NOTE # /ZF
        ]

        self.futures_list = [
            self.add_future(future, 
                resolution=Resolution.DAILY,
                extended_market_hours=False,
                data_mapping_mode=DataMappingMode.OPEN_INTEREST,
                data_normalization_mode=DataNormalizationMode.BACKWARDS_RATIO,
                contract_depth_offset=0
                )
            for future in self.futures_raw_list
        ]
        
        self.old_symbol = None
        self.new_symbol = None
        self.quantity = None

        self.futures_data = {}

        for item in self.futures_list:
            item.set_filter(0,182)
            self.futures_data[item.symbol] = symbol_data(item.symbol, self)

        
    def on_data(self, data):
        if self.is_warming_up:
            return

        invested = [x.Symbol for x in self.Portfolio.Values if x.Invested]

        for symbol, changed_event in data.symbol_changed_events.items():
            old_symbol = changed_event.old_symbol
            new_symbol = changed_event.new_symbol
            quantity = self.portfolio[old_symbol].quantity
            self.futures_data[symbol].pending_roll = (old_symbol, new_symbol, quantity)
        
        for item in self.futures_list:
            self.futures_data[item.symbol].update(self)

        for mapped in invested:
            symbol = mapped.Canonical
            exit_signal = self.futures_data[symbol].exit_signal
            if exit_signal:
                tag = "Liquidated. Net Profit: " + str(self.portfolio[mapped].unrealized_profit) + "; Percent Profit: " + str(round(self.portfolio[mapped].unrealized_profit_percent, 2))
                self.liquidate(mapped, tag=tag)

        for item in self.futures_list:
            if self.futures_data[item.symbol].pending_roll:
                self.roll(item.symbol, data)
                
            if item.mapped in invested:
                continue

            entry_signal = self.futures_data[item.symbol].entry_signal
            if entry_signal:
                atr20 = self.futures_data[item.symbol].atr20.current.value
                point_value = item.symbol_properties.contract_multiplier
                if atr20 > 0 and point_value > 0:
                    num_contracts = round((self.risk_factor * self.portfolio.total_portfolio_value) / (atr20 * point_value))
                    if data.bars.get(item.mapped):
                        self.market_order(item.mapped, num_contracts, tag="Long Entry")
                else:
                    self.debug("Could not enter market order")
        
    def roll(self, symbol, data):
        pending_roll = self.futures_data[symbol].pending_roll
        old_symbol = pending_roll[0]
        new_symbol = pending_roll[1]
        quantity = pending_roll[2]

        if data.bars.get(old_symbol) and data.bars.get(new_symbol) and quantity != 0:
            self.market_order(old_symbol, -quantity, tag="Rollover")
            self.market_order(new_symbol, quantity, tag="Rollover")
            self.futures_data[symbol].pending_roll = None

    def on_order_event(self, order_event: OrderEvent):
        if order_event.Status == OrderStatus.FILLED:
            order_ticket = self.transactions.get_order_ticket(order_event.order_id)

            symbol = order_ticket.symbol
            tag = order_ticket.tag
            entry_price = order_ticket.average_fill_price
            qty = order_ticket.quantity_filled

            if "Long Entry" in tag:
                self.trailing_stop_order(symbol, -qty, 0.15, True)

class symbol_data():
    def __init__(self, symbol, algo):
        self.symbol = symbol
        self.sma200 = algo.sma(symbol, 200, Resolution.DAILY)
        self.ema50 = algo.ema(symbol, 50, Resolution.DAILY)
        self.ema100 = algo.ema(symbol, 100, Resolution.DAILY)
        self.max100 = algo.max(symbol, 100, selector=Field.CLOSE, resolution=Resolution.DAILY)
        self.min50 = algo.min(symbol, 50, selector=Field.CLOSE, resolution=Resolution.DAILY)
        self.atr20 = algo.atr(symbol, 20, MovingAverageType.SIMPLE, resolution=Resolution.DAILY)
        #self.dch = algo.dch(symbol=symbol, upper_period=100, lower_period=50, resolution=Resolution.DAILY, selector=Field.CLOSE)
        self.dch = algo.dch(symbol, 100, 50, resolution=Resolution.DAILY)
        #self.bb_upper = algo.bb(symbol, )
        self.close = algo.securities[symbol].close
        self.entry_signal = False
        self.exit_signal = False
        self.pending_roll = None

        algo.warm_up_indicator(symbol, self.sma200, Resolution.DAILY)
        algo.warm_up_indicator(symbol, self.ema50, Resolution.DAILY)
        algo.warm_up_indicator(symbol, self.ema100, Resolution.DAILY)
        algo.warm_up_indicator(symbol, self.max100, Resolution.DAILY)
        algo.warm_up_indicator(symbol, self.min50, Resolution.DAILY)
        algo.warm_up_indicator(symbol, self.atr20, Resolution.DAILY)
        algo.warm_up_indicator(symbol, self.dch, Resolution.DAILY)
    
    def update(self, algo):
        self.close = algo.securities[self.symbol].close
        self.entry_signal = self.close >= self.max100.current.value and self.ema50.current.value > self.ema100.current.value
        self.exit_signal = self.close <= self.min50.current.value