Overall Statistics |
Total Trades 599 Average Win 0.19% Average Loss -0.25% Compounding Annual Return 6.175% Drawdown 34.500% Expectancy 0.119 Net Profit 6.693% Sharpe Ratio 0.372 Probabilistic Sharpe Ratio 25.396% Loss Rate 37% Win Rate 63% Profit-Loss Ratio 0.77 Alpha -0.075 Beta 0.846 Annual Standard Deviation 0.29 Annual Variance 0.084 Information Ratio -1.026 Tracking Error 0.105 Treynor Ratio 0.128 Total Fees $641.83 |
from QuantConnect import * from QuantConnect.Algorithm import * from QuantConnect.Algorithm.Framework.Alphas import * from QuantConnect.Algorithm.Framework.Execution import * from QuantConnect.Algorithm.Framework.Portfolio import * from QuantConnect.Algorithm.Framework.Selection import * from datetime import timedelta import pandas as pd BARS_PER_YEAR = 252 class PriceHistoryManager: def __init__(self, num_bars): self._num_bars = num_bars self._close_histories = {} self._last_time = None @property def close_histories(self): return self._close_histories def apply_fetched_close_histories(self, algorithm, history_df, updated_closes): history_by_syms = history_df["close"].unstack(level=0) for s in history_by_syms.columns: s_new_hist = history_by_syms[s] s_sym = algorithm.Symbol(s) if s_sym in updated_closes: s_old_hist = updated_closes[s_sym] updated_closes[s_sym] = pd.concat([s_old_hist, s_new_hist]).sort_index()[-self._num_bars:] else: updated_closes[s_sym] = s_new_hist.sort_index()[-self._num_bars:] def update_close_histories(self, algorithm, symbols): updated_closes = {} symbols_to_update = [] new_symbols = [] # For each symbol that already has history, copy that history to the next_indicators. for s in symbols: if s in self._close_histories: symbols_to_update.append(s) updated_closes[s] = self._close_histories[s] else: new_symbols.append(s) # If there are symbols to update, get the incremental history if symbols_to_update: # Because we drop all history that isn't right now relevant, we are assured that all the # history we require will be from the last run to now. inc_history = algorithm.History(symbols_to_update, self._last_time, algorithm.Time, Resolution.Daily) if not inc_history.empty: self.apply_fetched_close_histories(algorithm, inc_history, updated_closes) # If there are new symbols, get the full history if new_symbols: full_history = algorithm.History(new_symbols, self._num_bars, Resolution.Daily) if not full_history.empty: self.apply_fetched_close_histories(algorithm, full_history, updated_closes) self._close_histories = updated_closes self._last_time = algorithm.Time assert len(self._close_histories) <= len(symbols), f'close histories does not match symbols count: {len(self._close_histories)} > {len(symbols)}' for s in symbols: assert len(self._close_histories.get(s, [])) <= self._num_bars, f'close histories for {s} is too large: {len(self._close_histories[s])}' def split_at_ratio(self, ratio, symbols=None): assert 1.0 >= ratio >= 0.0 return self.split_at(int(self._num_bars * ratio), symbols) def split_at(self, bars, symbols=None): return self.tail(bars, symbols), self.head(self._num_bars - bars, symbols) def tail(self, bars, symbols=None): """Grabs the oldest bars.""" assert bars >= 0 symbols = symbols and set(symbols) return {sym: history[:bars] for sym, history in self._close_histories.items() if symbols is None or sym in symbols} def head(self, bars, symbols=None): """Grabs the newest bars""" assert bars >= 0 return {sym: history[-bars:] for sym, history in self._close_histories.items() if symbols is None or sym in symbols} class EtfMomentum(QCAlgorithm): def Initialize(self): self.SetStartDate(2020, 1, 1) # Set Start Date self.SetCash(100000) # Set Strategy Cash # self.AddEquity("SPY", Resolution.Minute) symbols = [Symbol.Create(ticker, SecurityType.Equity, Market.USA) for ticker in ["SPY", "QQQ", "VBR", "IPAC", "IEUR", "ILTB", "IUSG", "IUSB", "VEA", "VWO", "XCEM"]] self.SetUniverseSelection(ManualUniverseSelectionModel(symbols)) self.AddAlpha(SimpleMomentumAlphaModel(4)) self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel(rebalance=None)) self.SetExecution(ImmediateExecutionModel()) def compute_returns(history): return history[-1]/history[0] - 1 class SimpleMomentumAlphaModel(AlphaModel): def __init__(self, _alpha_max_count): self._last_month = None self._max_long_count = _alpha_max_count # For the purposes of the algorithm, the number of days we want to assume # for each month. This is not exact, but it should be a good enough measure self._month_days = timedelta(int(BARS_PER_YEAR/12)) # Longs and shorts will be set by OnSecuritiesChanged, and will be used in # Update. The latter will not do any data retrieval. self._longs = [] self._price_history_manager = PriceHistoryManager(int(BARS_PER_YEAR/2)) self._universe = set([]) def order_candidates(self, candidates): up_phase, ignored = self._price_history_manager.split_at_ratio(11.0/12.0, candidates) up_returns = pd.Series({s: compute_returns(h) for s, h in up_phase.items()}) up_ranks = up_returns.rank(ascending=False) sorted_up_ranks = up_ranks.sort_index() sorted_candidates = sorted_up_ranks.sort_values().index return sorted_candidates def OnSecuritiesChanged(self, algorithm, changes): added = set(changes.AddedSecurities) removed = set(changes.RemovedSecurities) self._universe = self._universe.union(added).difference(removed) algorithm.Log(f'Updating securities in alpha model.' f' Added: {len(added)}. Removed: {len(removed)}. Universe: {len(self._universe)}') algorithm.Log(f'Active securities: {algorithm.ActiveSecurities.Count}') def Update(self, algorithm, data): # Emit signals once a month if self._last_month == algorithm.Time.month: return [] self._last_month = algorithm.Time.month long_candidates = [s.Symbol for s in self._universe] self._price_history_manager.update_close_histories(algorithm, long_candidates) self._longs = self.order_candidates(long_candidates)[:self._max_long_count] insights = [] for kvp in algorithm.Portfolio: holding = kvp.Value symbol = holding.Symbol if holding.Invested and symbol not in self._longs: insights.append(Insight.Price(symbol, self._month_days, InsightDirection.Flat)) for symbol in self._longs: insights.append(Insight.Price(symbol, self._month_days, InsightDirection.Up)) return insights