Overall Statistics
Total Orders
589
Average Win
0.55%
Average Loss
-0.23%
Compounding Annual Return
51.139%
Drawdown
27.600%
Expectancy
1.020
Start Equity
100000000
End Equity
184228405.11
Net Profit
84.228%
Sharpe Ratio
1.273
Sortino Ratio
1.518
Probabilistic Sharpe Ratio
64.035%
Loss Rate
40%
Win Rate
60%
Profit-Loss Ratio
2.38
Alpha
0
Beta
0
Annual Standard Deviation
0.261
Annual Variance
0.068
Information Ratio
1.479
Tracking Error
0.261
Treynor Ratio
0
Total Fees
$66185.09
Estimated Strategy Capacity
$430000000.00
Lowest Capacity Asset
NQ YLZ9Z50BJE2P
Portfolio Turnover
12.88%
# region imports
from AlgorithmImports import *
from universe import FrontMonthFutureUniverseSelectionModel
from portfolio import InverseVolatilityPortfolioConstructionModel
# endregion

class InverseVolatilityRankAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2023, 3, 1)  # Set Start Date
        self.set_cash(100000000)     # For a large future universe, the fund needed would be large

        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
        
        self.universe_settings.extended_market_hours = True
        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))

        # Since we're using all assets for portfolio optimization, we emit constant alpha for every security
        self.add_alpha(ConstantAlphaModel(InsightType.PRICE, InsightDirection.UP, timedelta(7)))

        # A custom PCM to size by inverse volatility
        self.set_portfolio_construction(InverseVolatilityPortfolioConstructionModel())

        self.set_warmup(31, Resolution.DAILY)

    def select_future_chain_symbols(self, utcTime):
        return [
            Symbol.create(Futures.Indices.VIX, SecurityType.FUTURE, Market.CFE),
            Symbol.create(Futures.Indices.SP_500_E_MINI, SecurityType.FUTURE, Market.CME),
            Symbol.create(Futures.Indices.NASDAQ_100_E_MINI, SecurityType.FUTURE, Market.CME),
            Symbol.create(Futures.Indices.DOW_30_E_MINI, SecurityType.FUTURE, Market.CME),
            Symbol.create(Futures.Energies.BRENT_CRUDE, SecurityType.FUTURE, Market.NYMEX),
            Symbol.create(Futures.Energies.GASOLINE, SecurityType.FUTURE, Market.NYMEX),
            Symbol.create(Futures.Energies.HEATING_OIL, SecurityType.FUTURE, Market.NYMEX),
            Symbol.create(Futures.Energies.NATURAL_GAS, SecurityType.FUTURE, Market.NYMEX),
            Symbol.create(Futures.Grains.CORN, SecurityType.FUTURE, Market.CBOT),
            Symbol.create(Futures.Grains.OATS, SecurityType.FUTURE, Market.CBOT),
            Symbol.create(Futures.Grains.SOYBEANS, SecurityType.FUTURE, Market.CBOT),
            Symbol.create(Futures.Grains.WHEAT, SecurityType.FUTURE, Market.CBOT),
        ]
#region imports
from AlgorithmImports import *
from Portfolio.EqualWeightingPortfolioConstructionModel import EqualWeightingPortfolioConstructionModel
#endregion

class InverseVolatilityPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):
    def __init__(self, rebalance = Expiry.END_OF_WEEK, lookback = 30):
        '''Initialize a new instance of EqualWeightingPortfolioConstructionModel
        Args:
            rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
                              If None will be ignored.
                              The function returns the next expected rebalance time for a given algorithm UTC DateTime.
                              The function returns null if unknown, in which case the function will be called again in the
                              next loop. Returning current time will trigger rebalance.
            lookback: The lookback period of historical return to calculate the volatility'''
        super().__init__(rebalance, PortfolioBias.LONG)
        self.symbol_data = {}
        self.lookback = lookback

    def determine_target_percent(self, activeInsights):
        '''Will determine the target percent for each insight
        Args:
            activeInsights: The active insights to generate a target for'''
        result = {}

        active_symbols = [insight.symbol for insight in activeInsights]
        active_data = {symbol: data for symbol, data in self.symbol_data.items() 
            if data.is_ready and symbol in active_symbols}       # make sure data in used are ready

        # Sum the inverse STD of ROC for normalization later
        std_sum = sum([1 / data.value for data in active_data.values()])

        if std_sum == 0:
            return {insight: 0 for insight in activeInsights}
        
        for insight in activeInsights:
            if insight.symbol in active_data:
                data = active_data[insight.symbol]
                # Sizing by inverse volatility, then divide by contract multiplier to avoid unwanted leveraging
                result[insight] = 10 / data.value / std_sum / data.multiplier
            else:
                result[insight] = 0
        
        return result

    def on_securities_changed(self, algorithm, changes):
        super().on_securities_changed(algorithm, changes)
        for removed in changes.removed_securities:
            data = self.symbol_data.pop(removed.symbol, None)
            # Free up resources
            if data:
                data.dispose()

        for added in changes.added_securities:
            symbol = added.symbol
            if symbol not in self.symbol_data:
                self.symbol_data[symbol] = SymbolData(algorithm, added, self.lookback)

class SymbolData:
    '''An object to hold the daily return and volatility data for each security'''
    def __init__(self, algorithm, security, period):
        self.algorithm = algorithm
        self.symbol = security.symbol
        self.multiplier = security.symbol_properties.contract_multiplier

        self.ROC = RateOfChange(1)
        self.volatility = IndicatorExtensions.of(StandardDeviation(period), self.ROC)

        self.consolidator = TradeBarConsolidator(timedelta(1))
        self.consolidator.data_consolidated += self.on_data_update
        algorithm.subscription_manager.add_consolidator(self.symbol, self.consolidator)

        # Warm up with historical data
        history = algorithm.history[TradeBar](self.symbol, period+1, Resolution.DAILY)
        for bar in history:
            self.ROC.update(bar.end_time, bar.close)

    def on_data_update(self, sender, bar):
        self.ROC.update(bar.end_time, bar.close)

    def dispose(self):
        '''Free up memory and speed up update cycle'''
        self.consolidator.data_consolidated -= self.on_data_update
        self.algorithm.subscription_manager.remove_consolidator(self.symbol, self.consolidator)
        self.ROC.reset()
        self.volatility.reset()

    @property
    def is_ready(self):
        return self.volatility.is_ready and self.value != 0

    @property
    def value(self):
        return self.volatility.current.value
#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, rebalancePeriod = 7):
        super().__init__(timedelta(rebalancePeriod), select_future_chain_symbols)

    def filter(self, filter):
        '''Defines the futures chain universe filter'''
        return (filter.front_month()
                      .only_apply_filter_at_market_open())