Created with Highcharts 12.1.2EquityJan 2021Jan…May 2021Sep 2021Jan 2022May 2022Sep 2022Jan 2023May 2023Sep 2023Jan 2024May 2024Sep 2024Jan 2025May 2025010M20M-100-50000.250.5-1010250k500k02.5M5M050100
Overall Statistics
Total Orders
7706
Average Win
0.18%
Average Loss
-0.18%
Compounding Annual Return
-32.492%
Drawdown
82.200%
Expectancy
-0.191
Start Equity
10000000
End Equity
1881810.63
Net Profit
-81.182%
Sharpe Ratio
-1.965
Sortino Ratio
-1.962
Probabilistic Sharpe Ratio
0.000%
Loss Rate
59%
Win Rate
41%
Profit-Loss Ratio
0.96
Alpha
-0.256
Beta
-0.145
Annual Standard Deviation
0.134
Annual Variance
0.018
Information Ratio
-1.557
Tracking Error
0.205
Treynor Ratio
1.822
Total Fees
$535534.61
Estimated Strategy Capacity
$28000.00
Lowest Capacity Asset
AFRI XZ72L9APSVOL
Portfolio Turnover
6.86%
# region imports
from AlgorithmImports import *
# endregion

class GeekyAsparagusViper(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2021, 1, 1)
        self.set_end_date(2025, 4, 1)
        self.set_cash(10_000_000)
        # Set some parameter values.
        self._universe_size = self.get_parameter('universe_size', 100)
        # Add a universe of the 100 smallest assets in IWM.
        etf = Symbol.create('IWM', SecurityType.EQUITY, Market.USA)
        date_rule = self.date_rules.week_start(etf, 1)
        self.universe_settings.schedule.on(date_rule)
        self._week = 0
        self._universe = self.add_universe(self.universe.etf(etf, universe_filter_func=self._select_assets))
        # Rebalance the portfolio every 2 weeks.
        self.schedule.on(date_rule, self.time_rules.after_market_open(etf, 1), self._rebalance)

    def _select_assets(self, constituents):
        # Only update the universe every 2 weeks.
        week = self.time.isocalendar()[1]
        if abs(week - self._week) < 2:
            return []
        self._week = week
        # Select the 100 smallest constituents.
        symbols = [c.symbol for c in sorted(constituents, key=lambda c: c.weight)[:self._universe_size]]
        # Calculate factors for all the assets.
        history = self.history(symbols, timedelta(1), Resolution.MINUTE)
        factors = pd.DataFrame(columns=['price_volatility', 'volume_volatility', 'vwap_deviation'], index=symbols)
        for symbol in symbols:
            if symbol not in history.index:
                continue
            df = history.loc[symbol][['close', 'volume']]
            df['vwap'] = self.indicator_history(IntradayVwap(''), symbol, timedelta(1)).data_frame.current.reindex(df.index).ffill()
            factors.loc[symbol] = [
                -df.close.std() / df.close.mean(),                     # price_volatility
                df.volume.std() / df.volume.mean(),                    # volume_volatility
                -np.mean(np.abs(df.close - df.vwap)) / df.vwap.mean()  # vwap_deviation
            ]
        factors.dropna(inplace=True)
        # Split assets into long/short groups based on factor values.
        sorted_by_factors = list(factors.rank().sum(axis=1).sort_values().index)
        assets_per_side = int(len(sorted_by_factors)/2)
        self._longs = sorted_by_factors[-assets_per_side:]
        self._shorts = sorted_by_factors[:assets_per_side]
        return self._longs + self._shorts

    def _rebalance(self):
        if not self._longs:
            return
        self.set_holdings(
            [PortfolioTarget(s, 0.5/len(self._longs)) for s in self._longs] + [PortfolioTarget(s, -0.5/len(self._shorts)) for s in self._shorts],
            True
        )
        self._longs = []
        self._shorts = []