Overall Statistics |
Total Orders 1624 Average Win 0.17% Average Loss -0.23% Compounding Annual Return 2.956% Drawdown 15.300% Expectancy 0.170 Start Equity 100000000 End Equity 154202574.08 Net Profit 54.203% Sharpe Ratio 0.088 Sortino Ratio 0.058 Probabilistic Sharpe Ratio 0.100% Loss Rate 33% Win Rate 67% Profit-Loss Ratio 0.75 Alpha -0.02 Beta 0.296 Annual Standard Deviation 0.069 Annual Variance 0.005 Information Ratio -0.722 Tracking Error 0.114 Treynor Ratio 0.02 Total Fees $832244.39 Estimated Strategy Capacity $3700000000.00 Lowest Capacity Asset QQQ RIWIV7K5Z9LX Portfolio Turnover 3.72% |
# region imports from AlgorithmImports import * # endregion class SwimmingLightBrownGalago(QCAlgorithm): def initialize(self): self.set_cash(100_000_000) self.settings.daily_precise_end_time = False self.settings.minimum_order_margin_portfolio_percentage = 0 lookback_years = 12 # 12 years includes 3 non-leap years & 3 election cycles self._lookback = lookback_years * 252 self._period = 21 # Trading days warm_up_period = timedelta(lookback_years * 365) self.set_start_date(datetime(1998, 1, 1) + warm_up_period) self.set_warm_up(warm_up_period) self._vix = self.add_data(CBOE, "VIX", Resolution.DAILY) self._vix.threshold = 30 tickers = ['SPY', 'QQQ'] self._equities = [self.add_equity(ticker, Resolution.DAILY, fill_forward=False) for ticker in tickers] def on_securities_changed(self, changes): for security in changes.added_securities: security.history = pd.Series() security.rebalance = False security.signal = 0 security.previous_weight = 0 def on_data(self, data: Slice): self.plot('VIX', 'Value', self._vix.price) for equity in self._equities: # Get the current day's trade bar. bar = data.bars.get(equity.symbol) if bar: # Update the historical data. equity.history.loc[bar.time] = bar.open # `time` and `open` since we trade at next market open. equity.history = equity.history.iloc[-self._lookback:] # Wait until there is enough history. if len(equity.history) < self._lookback or self.is_warming_up or self._vix.price >= self._vix.threshold: continue # Calculate expected return of entering at next market open. period_returns = equity.history.pct_change(self._period).shift(-self._period).dropna() expected_return_by_day = period_returns.groupby(period_returns.index.strftime('%m-%d')).mean() next_market_open = equity.exchange.hours.get_next_market_open(self.time, False) expected_return = expected_return_by_day[next_market_open.strftime('%m-%d')] # The expected return of buying next market open and holdings n days. # Calculate signal for this trade. std = expected_return_by_day.std() mean = expected_return_by_day.mean() if expected_return >= mean + std: signal = 1 elif expected_return <= mean - std: signal = -1 else: signal = 0 if signal: equity.signal += signal equity.rebalance = True # Create a Scheduled Event to remove the signal later. exit_day = next_market_open for _ in range(self._period): exit_day = equity.exchange.hours.get_next_market_open(exit_day, False) self.schedule.on( self.date_rules.on(exit_day.replace(hour=0, minute=0)), self.time_rules.midnight, lambda equity=equity, signal=signal: self._remove_signal(equity, signal) ) equities_with_enough_history = sum([len(equity.history) >= self._lookback for equity in self._equities]) for equity in self._equities: if equity.rebalance: equity.rebalance = False weight = max(0, equity.signal / self._period) # Make the strategy long-only. # Adjust weight so that it imediately goes to 100% when scaling in. adjusted_weight = weight if weight and weight > equity.previous_weight: adjusted_weight = 1 equity.previous_weight = weight self.set_holdings(equity.symbol, adjusted_weight / equities_with_enough_history) def _remove_signal(self, equity, signal): equity.signal -= signal equity.rebalance = True