Overall Statistics
Total Orders
11198
Average Win
1.62%
Average Loss
-0.81%
Compounding Annual Return
118.587%
Drawdown
59.000%
Expectancy
0.128
Start Equity
1000000
End Equity
109793712.32
Net Profit
10879.371%
Sharpe Ratio
1.582
Sortino Ratio
2.63
Probabilistic Sharpe Ratio
64.409%
Loss Rate
62%
Win Rate
38%
Profit-Loss Ratio
2.00
Alpha
1.031
Beta
-0.192
Annual Standard Deviation
0.643
Annual Variance
0.413
Information Ratio
1.394
Tracking Error
0.674
Treynor Ratio
-5.294
Total Fees
$17291945.18
Estimated Strategy Capacity
$26000000.00
Lowest Capacity Asset
BTC YSVEMP6UTIIP
Portfolio Turnover
1732.12%
# region imports
from AlgorithmImports import *
# endregion


class NoiseAreaBreakoutAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2019, 5, 1)
        self.set_end_date(2025, 5, 1)
        self.set_cash(1_000_000)
        # Set some parameters.
        self._trading_interval_length = timedelta(minutes=30)
        self._lookback = 14  # days
        self._target_volatility = 0.02 # 0.02 = 2%
        self._max_exposure = 4  # 4 = 400%
        self._max_positions = 3
        # Add a universe of Futures contracts to trade.
        self._futures = []
        tickers = [
            Futures.Currencies.BTC,
            Futures.Indices.SP_500_E_MINI,
            Futures.Indices.NASDAQ_100_E_MINI,
            Futures.Indices.DOW_30_E_MINI,
        ]
        for ticker in tickers:
            future = self.add_future(
                ticker,
                data_mapping_mode=DataMappingMode.LAST_TRADING_DAY,
                data_normalization_mode=DataNormalizationMode.BACKWARDS_PANAMA_CANAL,
                contract_depth_offset=0
            )
            future.set_filter(0, 180)
            future.vwap = self.vwap(future.symbol)
            future.daily_volatility = IndicatorExtensions.of(StandardDeviation(self._lookback), self.roc(future.symbol, 1, Resolution.DAILY))
            future.avg_move_by_interval = {}
            future.yesterdays_close = None
            future.todays_open = None
            self._futures.append(future)
            # Add a Scheduled Event to place orders 30 minutes after market open.
            date_rule = self.date_rules.every_day(future.symbol)
            self.schedule.on(date_rule, self.time_rules.after_market_close(future.symbol, 1), lambda future=future: setattr(future, 'yesterdays_close', future.price))
            self.schedule.on(date_rule, self.time_rules.after_market_open(future.symbol, 1), lambda future=future: setattr(future, 'todays_open', future.open))
            self.schedule.on(date_rule, self.time_rules.before_market_close(future.symbol, 1), lambda future=future: self.liquidate(future.mapped))
        self.schedule.on(self.date_rules.every_day(), self.time_rules.every(self._trading_interval_length), self._rebalance)
        # Set a warm-up period to warm-up the indicators.
        self.set_warm_up(timedelta(30))

    def _rebalance(self):
        t = self.time
        trading_interval = (t.hour, t.minute)
        unchanged_positions = 0
        entry_targets = []
        exit_targets = []
        for future in self._futures:
            # Wait until the market is open.
            if (not future.yesterdays_close or
                not future.todays_open or
                not future.exchange.hours.is_open(t, False) or
                not future.exchange.hours.is_open(t - self._trading_interval_length, False)):
                if future.mapped and self.portfolio[future.mapped].invested:
                    unchanged_positions += 1
                continue
            # Create an indicator for this time interval if it doesn't already exist.
            if trading_interval not in future.avg_move_by_interval:
                future.avg_move_by_interval[trading_interval] = SimpleMovingAverage(self._lookback)
            avg_move = future.avg_move_by_interval[trading_interval]
            # Update the average move indicator.
            move = abs(future.price / future.todays_open - 1)
            if not avg_move.update(t, move):
                continue
            # Wait until the daily volatility indicator is ready.
            if not future.daily_volatility.is_ready or not future.daily_volatility.current.value or self.is_warming_up:
                continue
            # Calculate the noise area.
            upper_bound = max(future.yesterdays_close, future.todays_open) * (1+avg_move.current.value)
            lower_bound = min(future.yesterdays_close, future.todays_open) * (1-avg_move.current.value)
            # Scan for an entry.
            weight = min(self._max_exposure, self._target_volatility/future.daily_volatility.current.value) / self._max_exposure / self._max_positions
            contract = self.securities[future.mapped]
            if not contract.holdings.is_long and future.price > upper_bound:
                entry_targets = self._add_target(contract, weight, entry_targets)
            elif not contract.holdings.is_short and future.price < lower_bound:
                entry_targets = self._add_target(contract, -weight, entry_targets)
            # Scan for an exit.
            elif (contract.holdings.is_long and future.price < max(upper_bound, future.vwap.current.value) or
                contract.holdings.is_short and future.price > min(lower_bound, future.vwap.current.value)):
                exit_targets.append(PortfolioTarget(contract.symbol, 0))
            # Record the open position.
            elif contract.invested:
                unchanged_positions += 1
            # Plot the current state.
            #self.plot('Weight', str(future.symbol), weight)
            #self.plot('Noise Area', 'Upper Bound', upper_bound)
            #self.plot('Noise Area', 'Lower Bound', lower_bound)
            #self.plot('Noise Area', 'Price', future.price)
            #self.plot('Noise Area', 'VWAP', future.vwap.current.value)
            #self.plot('Volatility', 'Future', future.daily_volatility.current.value)

        self.set_holdings(entry_targets[:self._max_positions-unchanged_positions] + exit_targets)

    def _add_target(self, contract, weight, targets):
        target = PortfolioTarget(contract.symbol, weight)
        if not contract.invested:
            return targets + [target]
        # When flipping from long to short (or vice versa), insert at the beginning of the list to ensure the position changes.
        return [target] + targets