| Overall Statistics |
|
Total Trades 2188 Average Win 0.07% Average Loss -0.09% Compounding Annual Return 5.062% Drawdown 44.700% Expectancy 0.033 Net Profit 5.461% Sharpe Ratio 0.314 Probabilistic Sharpe Ratio 21.114% Loss Rate 40% Win Rate 60% Profit-Loss Ratio 0.72 Alpha 0.164 Beta -0.21 Annual Standard Deviation 0.381 Annual Variance 0.145 Information Ratio -0.172 Tracking Error 0.518 Treynor Ratio -0.571 Total Fees $2195.86 |
from datetime import timedelta
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 QuantConnect.Data.UniverseSelection import *
from holdmonths import HoldMonthsTracker
from pricehist import PriceHistoryManager
from qcconsts import *
from qcselectors import *
"""
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
"""
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 compute_returns(history):
return history[-1]/history[0] - 1
def log_symbols(algorithm, prefix, symbols):
algorithm.Log(prefix + ' '.join(map(lambda s: s.Value, symbols)))
class ValueWithDownFlatMomentum(QCAlgorithm):
def __init__(self):
# Number of stocks to select from the alpha model
super().__init__()
self._alpha_target_count = 20
self._hold_months = HoldMonthsTracker(3)
self._p_coarse_vol = 0.5
self._p_coarse_price = 0.7
self._price_history_manager = PriceHistoryManager(int(BARS_PER_YEAR / 4))
self._market_cap_percentile = 50
def Initialize(self):
self.SetCash(100000)
self.SetStartDate(2020, 1, 1)
# If not specified, the backtesting EndDate would be today
self.UniverseSettings.Resolution = Resolution.Daily
# self.SetEndDate(2015, 7, 1)
self.AddUniverseSelection(FineFundamentalUniverseSelectionModel(self.select_coarse, self.select_fine))
self.AddAlpha(LongShortValueDownFlatAlphaModel(self._alpha_target_count, self._hold_months.clone()))
self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel(rebalance=None))
self.SetExecution(VolumeWeightedAveragePriceExecutionModel())
# self.SetExecution(StandardDeviationExecutionModel(deviations=1, period=5, resolution=Resolution.Daily))
@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) / 4)
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 select_coarse(self, coarse):
if not self._hold_months.should_trade(self.Time.month):
return Universe.Unchanged
self.Log('Running coarse selection')
with_fundamental = [x for x in coarse if x.HasFundamentalData]
by_vol = sort_and_select_subset(self, with_fundamental, lambda s: s.DollarVolume,
reverse=True, proportion=self._p_coarse_vol, value_name='Volume')
by_price = sort_and_select_subset(self, with_fundamental, lambda s: s.Price,
reverse=True, proportion=self._p_coarse_price, value_name='Price')
symbols_by_pv = [s.Symbol for s in (set(by_vol).intersection(by_price))]
self._price_history_manager.update_close_histories(self, symbols_by_pv)
return self.select_by_variance(self)
def select_fine(self, fine):
if not self._hold_months.should_trade_and_set(self.Time.month):
return Universe.Unchanged
self.Log('Running fine selection')
with_fundamentals = [f for f in fine if has_all_fundamentals(f) and is_tradable(f)]
with_cap = select_by_market_cap(self, with_fundamentals, self._market_cap_percentile)
return [f.Symbol for f in with_cap]
class LongShortValueDownFlatAlphaModel(AlphaModel):
def __init__(self, _alpha_max_count, _hold_months):
super().__init__()
self._hold_months = _hold_months
self._max_long_count = _alpha_max_count
self._max_short_count = _alpha_max_count
self._candidate_factor = 4
# 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 split_long_short_candidates(self):
ordered_candidates = candidates_ordered_by_combined_rank(
rank_by_long_term_debt_to_equity(self._universe),
rank_by_ebit_ev(self._universe))
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 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]
# Reversing the list that's sorted by max of down-rank and flat-rank is
# not the same as taking the highest min values of the down-rank and the
# flat rank. We will need to implement long and short computation better.
self._shorts = list(reversed(self.order_candidates(short_candidates)))[:self._max_short_count]
def Update(self, algorithm, data):
if not self._hold_months.should_trade_and_set(algorithm.Time.month):
return []
insights = []
# Months have different numbers of days, so this will be approximate
hold_duration = timedelta(BARS_PER_MONTH * self._hold_months.get - 1)
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, hold_duration, InsightDirection.Flat))
log_symbols(algorithm, "Long positions selected: ", self._longs)
for symbol in self._longs:
insights.append(Insight.Price(symbol, hold_duration, InsightDirection.Up))
# For now, just focus on long positions
# log_symbols(algorithm, "Short positions selected: ", self._longs)
# for symbol in self._shorts:
# insights.append(Insight.Price(symbol, hold_duration, InsightDirection.Down))
return insights