Overall Statistics |
Total Trades 2092 Average Win 0.17% Average Loss -0.21% Compounding Annual Return 7.799% Drawdown 45.600% Expectancy 0.083 Net Profit 25.684% Sharpe Ratio 0.383 Probabilistic Sharpe Ratio 11.275% Loss Rate 40% Win Rate 60% Profit-Loss Ratio 0.80 Alpha 0.127 Beta -0.177 Annual Standard Deviation 0.269 Annual Variance 0.072 Information Ratio -0.092 Tracking Error 0.36 Treynor Ratio -0.583 Total Fees $2123.96 |
from QuantConnect import * from QuantConnect.Algorithm import * from QuantConnect.Algorithm.Framework import * from QuantConnect.Algorithm.Framework.Alphas import * from QuantConnect.Algorithm.Framework.Execution import * from QuantConnect.Algorithm.Framework.Portfolio import * from QuantConnect.Algorithm.Framework.Risk import * from QuantConnect.Algorithm.Framework.Selection import * from QuantConnect.Data.UniverseSelection import * from QuantConnect.Indicators import Variance from QuantConnect.Orders import * from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel from datetime import timedelta import math as m import numpy as np import pandas as pd import ev_ebit from pricehist import PriceHistoryManager """ Algorithm selects for high ebit/ev and low variance in the coarse selection phase. Then it selects for highest returns over the previous 11 months """ BARS_PER_YEAR = 252 class SimpleMomentum(QCAlgorithm): def __init__(self): # Number of stocks to select from the alpha model self._alpha_selection_max = 20 def Initialize(self): self.SetCash(100000) self.SetStartDate(2018, 1, 1) # If not specified, the backtesting EndDate would be today self.UniverseSettings.Resolution = Resolution.Daily # self.SetEndDate(2015, 7, 1) self.AddUniverseSelection(ValueUniverseSelection(0.4, 0.7)) self.AddAlpha(LongShortAllValueAlphaModel(self._alpha_selection_max)) self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel(timedelta(int(BARS_PER_YEAR/24) + 1))) self.SetExecution(VolumeWeightedAveragePriceExecutionModel()) # self.SetExecution(StandardDeviationExecutionModel(deviations=1, period=5, resolution=Resolution.Daily)) def has_all_fundamentals(fine_data): return (ev_ebit.has_ebit_ev_fundamentals(fine_data) and fine_data.MarketCap and fine_data.FinancialStatements.BalanceSheet.CurrentLiabilities and fine_data.OperationRatios.AssetsTurnover) def select_from(algorithm, pool, sort_value, reverse=False, proportion=None, value_name='Value'): if proportion is not None: select_count = int(len(pool) * proportion) else: raise Exception("Proportion not provided") selection = sorted(pool, key=sort_value, reverse=reverse)[:select_count] algorithm.Log(f'{value_name} between {sort_value(selection[0])} and {sort_value(selection[-1])}') return selection class ValueUniverseSelection(FundamentalUniverseSelectionModel): def __init__(self, _percent_volume, _percent_price): super().__init__(True, None, None) self._last_month = None # Number of stocks to pass CoarseSelection process self._p_coarse_vol = _percent_volume self._p_coarse_price = _percent_price # One quarter of trading year self._num_bars = int(BARS_PER_YEAR / 4) self._price_history_manager = PriceHistoryManager(self._num_bars) @property def close_histories(self): return self._price_history_manager.close_histories def select_by_variance(self, algorithm): close_variances = {} for sym, closes in self.close_histories.items(): variance = closes.diff().var() if m.isfinite(variance): close_variances[sym] = variance else: algorithm.Log(f'Dropping {sym} which has variance {variance}') sorted_by_var = sorted(close_variances.items(), key=lambda kv: kv[1]) head_tail_size = int(len(sorted_by_var) / 10) medium_var = sorted_by_var[head_tail_size:-head_tail_size] algorithm.Log('Dropping highest and lowest {} by variance. Min/max: {}/{}' .format(head_tail_size, medium_var[0][1], medium_var[-1][1])) return [s[0] for s in medium_var] def SelectCoarse(self, algorithm, coarse): if self._last_month == algorithm.Time.month: return Universe.Unchanged # Setting last_month happens in fine selection algorithm.Log('Running coarse selection') with_fundamental = [x for x in coarse if x.HasFundamentalData] by_vol = select_from(algorithm, with_fundamental, lambda s: s.DollarVolume, reverse=True, proportion=self._p_coarse_vol, value_name='Volume') by_price = select_from(algorithm, with_fundamental, lambda s: s.Price, reverse=True, proportion=self._p_coarse_price, value_name='Price') by_price_and_vol = set(by_vol).intersection(by_price) symbols_by_pv = [s.Symbol for s in by_price_and_vol] self._price_history_manager.update_close_histories(algorithm, symbols_by_pv) return self.select_by_variance(algorithm) def SelectFine(self, algorithm, fine): if self._last_month == algorithm.Time.month: return Universe.Unchanged self._last_month = algorithm.Time.month algorithm.Log('Running fine selection') with_fundamentals = [f for f in fine if has_all_fundamentals(f)] with_cap = self.select_by_market_cap(algorithm, with_fundamentals) return [f.Symbol for f in with_cap] def select_by_market_cap(self, algorithm, fine): min_cap = np.percentile([f.MarketCap for f in fine], 60) with_cap = [f for f in fine if f.MarketCap >= min_cap] algorithm.Log(f'Market cap limited at {min_cap} yields {len(with_cap)} candidates.') return with_cap def compute_returns(history): return history[-1]/history[0] - 1 class LongShortAllValueAlphaModel(AlphaModel): def __init__(self, _alpha_max_count): self._last_month = None self._max_long_count = _alpha_max_count self._max_short_count = _alpha_max_count self._candidate_factor = 3 # 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(28) # 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._shorts = [] self._price_history_manager = PriceHistoryManager(BARS_PER_YEAR) self._universe = set([]) def select_by_debt(self, algorithm, fine): all_debt_to_ebit = {f: (f.FinancialStatements.BalanceSheet.CurrentLiabilities.TwelveMonths / f.FinancialStatements.IncomeStatement.EBIT.TwelveMonths) for f in fine if f.FinancialStatements.IncomeStatement.EBIT.TwelveMonths > 0} max_debt_to_ebit = np.percentile(list(all_debt_to_ebit.values()), 40) with_debt = [i[0] for i in all_debt_to_ebit.items() if i[1] <= max_debt_to_ebit] algorithm.Log(f'CurrentLiabilities/EBIT limited at {max_debt_to_ebit} yields {len(with_debt)} candidates.') return with_debt def rank_by_assets_turnover(self): all_turnovers = [(f.Symbol, f.Fundamentals.OperationRatios.AssetsTurnover.OneYear) for f in self._universe] return dict(map(lambda zkv: (zkv[1][0], zkv[0]), zip(range(len(all_turnovers)), sorted(all_turnovers, key=lambda kv: kv[1], reverse=True)))) def rank_by_ebit_ev(self): ebit_evs = [ev_ebit.ebit_to_ev(f.Fundamentals) for f in self._universe] ebit_ev_syms = [(e.Symbol, ee) for e, ee in zip(self._universe, ebit_evs) if m.isfinite(ee) and ee != 0] return dict(map(lambda zkv: (zkv[1][0], zkv[0]), zip(range(len(ebit_ev_syms)), sorted(ebit_ev_syms, key=lambda kv: kv[1], reverse=True)))) def split_long_short_candidates(self): ordered_candidates = self.candidates_ordered_by_combined_rank( self.rank_by_assets_turnover(), self.rank_by_ebit_ev()) long_candidates_count = self._max_long_count * self._candidate_factor short_candidates_count = self._max_short_count * self._candidate_factor if long_candidates_count + short_candidates_count > len(ordered_candidates): long_candidates_count = int(len(ordered_candidates) / 2) short_candidates_count = len(ordered_candidates) - long_candidates_count return ordered_candidates[:long_candidates_count], list(reversed(ordered_candidates[-short_candidates_count:])) def candidates_ordered_by_combined_rank(self, ranks1, *rest_ranks): net_ranked = {} for symbol, symbol_rank_1 in ranks1.items(): symbol_rest_ranks = [ranks2.get(symbol) for ranks2 in rest_ranks] if None not in symbol_rest_ranks: net_ranked[symbol] = symbol_rank_1 + sum(symbol_rest_ranks) return [kv[0] for kv in sorted(list(net_ranked.items()), key=lambda kv: kv[1])] def order_candidates(self, candidates): down_phase, flat_phase = self._price_history_manager.split_at_ratio(0.75, candidates) down_ranks = (pd.Series({s: compute_returns(h) for s, h in down_phase.items()}) .rank(ascending=True) .sort_index()) flat_ranks = (pd.Series({s1: compute_returns(h1) for s1, h1 in flat_phase.items()}) .abs() .rank(ascending=True) .sort_index()) aggregate_rank = pd.DataFrame({'down': down_ranks, 'flat': flat_ranks}).max(1) return aggregate_rank.sort_values().index 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}') long_candidates, short_candidates = self.split_long_short_candidates() self._price_history_manager.update_close_histories(algorithm, long_candidates + short_candidates) self._longs = self.order_candidates(long_candidates)[:self._max_long_count] self._shorts = list(reversed(self.order_candidates(short_candidates)))[:self._max_short_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 insights = [] for kvp in algorithm.Portfolio: holding = kvp.Value symbol = holding.Symbol if holding.Invested and symbol not in self._longs and symbol not in self._shorts: 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)) # For now, just focus on long positions # for symbol in self._shorts: # insights.append(Insight.Price(symbol, self._month_days, InsightDirection.Down)) return insights