Overall Statistics
Total Orders
13337
Average Win
0.20%
Average Loss
-0.08%
Compounding Annual Return
6.789%
Drawdown
21.900%
Expectancy
0.201
Start Equity
100000000
End Equity
246925033.3
Net Profit
146.925%
Sharpe Ratio
0.354
Sortino Ratio
0.473
Probabilistic Sharpe Ratio
0.619%
Loss Rate
67%
Win Rate
33%
Profit-Loss Ratio
2.62
Alpha
0.04
Beta
-0.074
Annual Standard Deviation
0.094
Annual Variance
0.009
Information Ratio
-0.295
Tracking Error
0.179
Treynor Ratio
-0.449
Total Fees
$9958666.70
Estimated Strategy Capacity
$650000000.00
Lowest Capacity Asset
ES YLZ9Z50BJE2P
Portfolio Turnover
69.60%
# region imports
from AlgorithmImports import *
# endregion

class FuturesIntradayTrendFollowingWithVolailityScalingAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2011, 1, 1)
        self.set_end_date(2024, 10, 1)
        self.set_cash(100_000_000)
        self.settings.minimum_order_margin_portfolio_percentage = 0
        self._future = self.add_future(
            Futures.Indices.SP_500_E_MINI,
            data_normalization_mode=DataNormalizationMode.BACKWARDS_RATIO,
            data_mapping_mode=DataMappingMode.OPEN_INTEREST,
            contract_depth_offset=0
        )
        self._future.set_filter(lambda universe: universe.front_month())
        self._future.std_dev_by_time = {}
        self._future.yesterdays_close = None
        self._trading_interval = timedelta(minutes=60)
        self._lookback = 252 # 1 year
        self._volatility_scaler = 0.5 # 50%
        self._weight_scaler = 5 # To utilize more cash.
        date_rule = self.date_rules.every_day(self._future.symbol)
        self.schedule.on(date_rule, self.time_rules.midnight, self._record_close_price)
        self.schedule.on(date_rule, self.time_rules.every(self._trading_interval), self._rebalance)
        self.schedule.on(date_rule, self.time_rules.before_market_close(self._future.symbol, 1), lambda: self.liquidate(self._future.mapped))
        self.set_warm_up(timedelta(int(1.5*self._lookback)))

    def _record_close_price(self):
        self._future.yesterdays_close = self._future.price

    def _rebalance(self):
        # Wait until the market is open.
        t = self.time
        if (not self._future.yesterdays_close or
            not self._future.exchange.hours.is_open(t - self._trading_interval, False) or
            not self._future.exchange.hours.is_open(t + self._trading_interval - timedelta(seconds=1), False)):
            return
        # Get the intraday standard deviation.
        time_period = (t.hour, t.minute)
        if time_period not in self._future.std_dev_by_time:
            self._future.std_dev_by_time[time_period] = StandardDeviation(self._lookback)
        intraday_roc = self._future.price / self._future.yesterdays_close - 1
        if not self._future.std_dev_by_time[time_period].update(t, intraday_roc) or self.is_warming_up:
            return
        std_dev = self._future.std_dev_by_time[time_period].current.value
        self.plot('Annualized STD %', str(time_period), 100*(std_dev*np.sqrt(252)))
        self.plot('Intraday ROC %', str(time_period), 100*intraday_roc)
        # Calculate the target weight.
        scaled_volaility = self._volatility_scaler * std_dev
        if scaled_volaility < intraday_roc:
            weight = intraday_roc - scaled_volaility
        elif intraday_roc < -scaled_volaility:
            weight = intraday_roc + scaled_volaility
        else:
            weight = 0
        self.plot('STD', str(time_period), 100*std_dev)
        self.plot('Weight %', str(time_period), 100*weight)
        # Place orders.
        self.set_holdings(self._future.mapped, self._weight_scaler * weight)