| Overall Statistics |
|
Total Orders 7534 Average Win 0.02% Average Loss -0.01% Compounding Annual Return 4.948% Drawdown 27.900% Expectancy 0.166 Start Equity 1000000 End Equity 1049082.80 Net Profit 4.908% Sharpe Ratio 0.24 Sortino Ratio 0.356 Probabilistic Sharpe Ratio 18.403% Loss Rate 57% Win Rate 43% Profit-Loss Ratio 1.71 Alpha 0.119 Beta -0.375 Annual Standard Deviation 0.257 Annual Variance 0.066 Information Ratio -0.202 Tracking Error 0.45 Treynor Ratio -0.164 Total Fees $7862.17 Estimated Strategy Capacity $11000000.00 Lowest Capacity Asset LLY R735QTJ8XC9X Portfolio Turnover 3.46% |
#region imports
from AlgorithmImports import *
from indicators import *
#endregion
class RankQuantilesAlphaModel(AlphaModel):
def __init__(self, quantiles, lookback_months):
self.quantiles = quantiles
self.lookback_months = lookback_months
self.securities_list = []
self.day = -1
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
# Create and register indicator for each security in the universe
security_by_symbol = {}
for security in changes.added_securities:
# Create an indicator
security_by_symbol[security.symbol] = security
security.indicator = VwapReversion("indicator", security.symbol)
self._register_indicator(algorithm, security)
self.securities_list.append(security)
# Warm up the indicators of newly-added stocks
if security_by_symbol:
history = algorithm.history[TradeBar](list(security_by_symbol.keys()), (self.lookback_months+1) * 30, Resolution.DAILY, data_normalization_mode=DataNormalizationMode.SCALED_RAW)
for trade_bars in history:
for bar in trade_bars.values():
if type(bar) == TradeBar:
security_by_symbol[bar.symbol].consolidator.update(bar)
# Stop updating consolidator when the security is removed from the universe
for security in changes.removed_securities:
if security in self.securities_list:
algorithm.subscription_manager.remove_consolidator(security.symbol, security.consolidator)
self.securities_list.remove(security)
def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
# Reset indicators when corporate actions occur
for symbol in set(data.splits.keys() + data.dividends.keys()):
security = algorithm.securities[symbol]
if security in self.securities_list:
security.indicator.reset()
algorithm.subscription_manager.remove_consolidator(security.symbol, security.consolidator)
self._register_indicator(algorithm, security)
history = algorithm.history[TradeBar](security.symbol, (security.indicator.warm_up_period+1) * 30, Resolution.DAILY, data_normalization_mode=DataNormalizationMode.SCALED_RAW)
for bar in history:
security.consolidator.update(bar)
# Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
if data.quote_bars.count == 0:
return []
# Only emit insights once per day
if self.day == algorithm.time.day:
return []
self.day = algorithm.time.day
# Get the indicator value of each asset in the universe
indicator_by_symbol = {security.symbol : security.indicator.current.value
for security in self.securities_list if security.symbol in data.quote_bars and security.indicator.is_ready}
# Determine how many assets to hold in the portfolio
quantile_size = int(len(indicator_by_symbol)/self.quantiles)
if quantile_size == 0:
return []
# Create insights to long the assets in the universe with the greatest indicator value
weight = 1 / (quantile_size+1)
insights = []
for symbol, _ in sorted(indicator_by_symbol.items(), key=lambda x: x[1], reverse=True)[:quantile_size]:
insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.UP, weight=weight))
return insights
def _register_indicator(self, algorithm, security):
# Update the indicator with monthly bars
security.consolidator = TradeBarConsolidator(Calendar.MONTHLY)
algorithm.subscription_manager.add_consolidator(security.symbol, security.consolidator)
algorithm.register_indicator(security.symbol, security.indicator, security.consolidator)
class MomentumRank(AlphaModel):
def __init__(self, quantiles, lookback_months):
self.quantiles = quantiles
self.lookback_months = lookback_months
self.securities_list = []
self.day = -1
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
# Create and register indicator for each security in the universe
security_by_symbol = {}
for security in changes.added_securities:
# Create an indicator
security_by_symbol[security.symbol] = security
security.indicator = CustomMomentumPercent("signal", self.lookback_months) #CHANGE INDICATOR HERE
self._register_indicator(algorithm, security)
self.securities_list.append(security)
# Warm up the indicators of newly-added stocks
if security_by_symbol:
history = algorithm.history[TradeBar](list(security_by_symbol.keys()), (self.lookback_months+1) * 30, Resolution.DAILY, data_normalization_mode=DataNormalizationMode.SCALED_RAW)
for trade_bars in history:
for bar in trade_bars.values():
if type(bar) == TradeBar:
security_by_symbol[bar.symbol].consolidator.update(bar)
# Stop updating consolidator when the security is removed from the universe
for security in changes.removed_securities:
if security in self.securities_list:
algorithm.subscription_manager.remove_consolidator(security.symbol, security.consolidator)
self.securities_list.remove(security)
def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
# Reset indicators when corporate actions occur
for symbol in set(data.splits.keys() + data.dividends.keys()):
security = algorithm.securities[symbol]
if security in self.securities_list:
security.indicator.reset()
algorithm.subscription_manager.remove_consolidator(security.symbol, security.consolidator)
self._register_indicator(algorithm, security)
history = algorithm.history[TradeBar](security.symbol, (security.indicator.warm_up_period+1) * 30, Resolution.DAILY, data_normalization_mode=DataNormalizationMode.SCALED_RAW)
for bar in history:
security.consolidator.update(bar)
# Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
if data.quote_bars.count == 0:
return []
# Only emit insights once per day
if self.day == algorithm.time.day:
return []
self.day = algorithm.time.day
# Get the indicator value of each asset in the universe
indicator_by_symbol = {security.symbol : security.indicator.current.value
for security in self.securities_list if security.symbol in data.quote_bars and security.indicator.is_ready}
# Create insights to long the assets in the universe with the greatest indicator value
insights = []
sorted_security_list = sorted(indicator_by_symbol.items(), key=lambda x: x[1])
size = len(sorted_security_list)
for security in sorted_security_list:
weight = (sorted_security_list.index(security))/size-0.5
if weight >= 0:
insights.append(Insight.price(security[0], Expiry.END_OF_DAY, InsightDirection.UP, weight=weight*3))
else:
insights.append(Insight.price(security[0], Expiry.END_OF_DAY, InsightDirection.DOWN, weight=weight))
return insights
def _register_indicator(self, algorithm, security):
# Update the indicator with monthly bars
security.consolidator = TradeBarConsolidator(Calendar.MONTHLY)
algorithm.subscription_manager.add_consolidator(security.symbol, security.consolidator)
algorithm.register_indicator(security.symbol, security.indicator, security.consolidator)
class TSZscore_VWAPReversion(AlphaModel):
def __init__(self):
self.period = 20
self.securities_list = []
self.day = -1
self.count = 0
self.historical_VwapReversion_by_symbol = {}
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
# Register each security in the universe
for security in changes.added_securities:
if security not in self.securities_list:
self.historical_VwapReversion_by_symbol[security.symbol] = deque(maxlen=self.period)
self.securities_list.append(security)
for security in changes.removed_securities:
if security in self.securities_list:
self.securities_list.remove(security)
def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
if data.quote_bars.count == 0: # Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
return []
if self.day == algorithm.time.day: # Only emit insights once per day
return []
self.day = algorithm.time.day
# Neutralize Vwap/Close of securities so it's mean 0, then append them to the list
temp_list = {}
for security in self.securities_list:
if security.Close != 0:
temp_list[security.symbol] = algorithm.vwap(security.symbol).Current.Value/security.Close
else:
temp_list[security.symbol] = 1
temp_mean = sum(temp_list.values())/len(temp_list.values())
for security in self.securities_list:
self.historical_VwapReversion_by_symbol[security.symbol].appendleft(temp_list[security.symbol]-temp_mean)
# Emit insight once per 20 days
if self.count != 20:
self.count += 1
return []
else:
self.count = 0
# Compute ts_zscore of current Vwap/Close
zscore_by_symbol = {}
for security in self.securities_list:
zscore_by_symbol[security.symbol] = sp.stats.zscore(self.historical_VwapReversion_by_symbol[security.symbol])[0]
# create insights to long / short the asset
insights = []
weights = {}
for symbol, zscore in zscore_by_symbol.items():
if not np.isnan(zscore):
weight = zscore
else:
weight = 0
weights[symbol] = weight
for symbol, weight in weights.items():
if weight > 0:
insights.append(Insight.price(symbol, timedelta(20), InsightDirection.UP, weight=weight))
elif weight < 0:
insights.append(Insight.price(symbol, timedelta(20), InsightDirection.DOWN, weight=weight))
return insights
class TSZscore_DividendGrowth(AlphaModel):
def __init__(self):
self.period = 252
self.securities_list = []
self.day = -1
self.historical_dividend_by_symbol = {}
self.previous_zscore_by_symbol = {}
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
# Register each security in the universe
for security in changes.added_securities:
if security not in self.securities_list:
self.historical_dividend_by_symbol[security.symbol] = deque(maxlen=self.period)
self.securities_list.append(security)
for security in changes.removed_securities:
if security in self.securities_list:
self.securities_list.remove(security)
def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
if data.quote_bars.count == 0: # Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
return []
if self.day == algorithm.time.day: # Only emit insights once per day
return []
self.day = algorithm.time.day
# Append dividend to the list, compute ts_zscore of current dividend
zscore_by_symbol = {}
for security in self.securities_list:
self.historical_dividend_by_symbol[security.symbol].appendleft(security.fundamentals.earning_reports.dividend_per_share.Value)
zscore_by_symbol[security.symbol] = sp.stats.zscore(self.historical_dividend_by_symbol[security.symbol])[0]
# create insights to long / short the asset
insights = []
weights = {}
for symbol, zscore in zscore_by_symbol.items():
if not np.isnan(zscore):
weight = zscore
else:
weight = 0
weights[symbol] = weight
for symbol, weight in weights.items():
if weight >= 0:
insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.UP, weight=weight))
else:
insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.DOWN, weight=weight))
self.previous_zscore_by_symbol = zscore_by_symbol
return insights
class Conditional_Reversion(AlphaModel):
def __init__(self):
self.condition_period = 5
self.period = 3
self.securities_list = []
self.day = -1
self.historical_volume_by_symbol = {}
self.historical_close_by_symbol = {}
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
# Register each security in the universe
for security in changes.added_securities:
if security not in self.securities_list:
self.historical_volume_by_symbol[security.symbol] = deque(maxlen=self.condition_period)
self.historical_close_by_symbol[security.symbol] = deque(maxlen=self.period)
self.securities_list.append(security)
for security in changes.removed_securities:
if security in self.securities_list:
self.securities_list.remove(security)
def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
if data.quote_bars.count == 0: # Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
return []
if self.day == algorithm.time.day: # Only emit insights once per month
return []
self.day = algorithm.time.day
# Append volume and close to the list
zscore_by_symbol = {}
return_by_symbol = {}
for security in self.securities_list:
if (security.Close != 0 and security.Volume != 0):
self.historical_close_by_symbol[security.symbol].appendleft(security.Close)
self.historical_volume_by_symbol[security.symbol].appendleft(security.Volume)
return_by_symbol[security.symbol] = (self.historical_close_by_symbol[security.symbol][0] - self.historical_close_by_symbol[security.symbol][-1])
if return_by_symbol == {}: # Don't emit insight if there's no valid data
return []
# Rank the 3 days return among securities to return value from 0 to 1
sorted_return_by_symbol = sorted(return_by_symbol.items(), key=lambda x: x[1])
return_rank_by_symbol = {}
for item in sorted_return_by_symbol: # item is a key-value pair. [0] is the security symbol and [1] is the return
return_rank_by_symbol[item[0]] = (sorted_return_by_symbol.index(item))/ len(sorted_return_by_symbol)
# Calculating the final weight
weights = {}
for security in self.securities_list:
# If condition is met, assign weight
if len(self.historical_volume_by_symbol[security.symbol]) != 0 and max(self.historical_volume_by_symbol[security.symbol]) == security.Volume:
weight = -return_rank_by_symbol[security.symbol] # Change this sign and complete different behaviour if purely long. Investigate
else:
weight = 0
weights[security.symbol] = weight
p_count = 0
n_count = 0
# Make the weights mean 0
weights_mean = sum(weights.values())/len(weights.values())
for symbol, weight in weights.items():
weights[symbol] = weight - weights_mean
if weights[symbol] > 0:
p_count += 1
else:
n_count += 1
# Create insights to long / short the asset
insights = []
for symbol, weight in weights.items():
if weight > 0:
insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.UP, weight=weight))
elif weight < 0:
insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.DOWN, weight=weight))
#Expiry.END_OF_DAY
return insights
class Momomentum(AlphaModel):
def __init__(self):
self.condition_period = 5
self.period = 3
self.securities_list = []
self.day = -1
self.historical_volume_by_symbol = {}
self.historical_close_by_symbol = {}
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
# Register each security in the universe
for security in changes.added_securities:
if security not in self.securities_list:
self.historical_volume_by_symbol[security.symbol] = deque(maxlen=self.condition_period)
self.historical_close_by_symbol[security.symbol] = deque(maxlen=self.period)
self.securities_list.append(security)
for security in changes.removed_securities:
if security in self.securities_list:
self.securities_list.remove(security)
def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
if data.quote_bars.count == 0: # Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
return []
if self.day == algorithm.time.day: # Only emit insights once per day
return []
self.day = algorithm.time.day
# Append volume and close to the list
zscore_by_symbol = {}
return_by_symbol = {}
for security in self.securities_list:
self.historical_close_by_symbol[security.symbol].appendleft(security.Close)
self.historical_volume_by_symbol[security.symbol].appendleft(security.Volume)
return_by_symbol[security.symbol] = (self.historical_close_by_symbol[security.symbol][0] - self.historical_close_by_symbol[security.symbol][-1])
# abs return?
# Rank the 3 days return among securities to return value from 0 to 1
sorted_return_by_symbol = sorted(return_by_symbol.items(), key=lambda x: x[1])
ranked_return_by_symbol = {}
for item in sorted_return_by_symbol: # item is a key-value pair. [0] is the security symbol and [1] is the return
ranked_return_by_symbol[item[0]] = (sorted_return_by_symbol.index(item))/ len(sorted_return_by_symbol)
# Calculating the final weight
weights = {}
for symbol, rank in ranked_return_by_symbol.items():
# If condition is met, assign weight
condition = (max(self.historical_volume_by_symbol[security.symbol]) == security.Volume)
if condition:
weight = rank
else:
weight = 0
weights[symbol] = weight
# Make the weights mean 0
weights_mean = sum(weights.values())/len(weights.values())
for symbol_weight in weights.items():
weights[symbol] = weight - weights_mean
# Make the weights absolute value sum to 1
weights_sum = sum(np.abs(list(weights.values())))
for symbol, weight in weights.items():
weights[symbol] = weight/weights_sum
# # Create insights to long / short the asset
insights = []
for symbol, weight in weights.items():
if weight >= 0:
insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.UP, weight=weight))
else:
insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.DOWN, weight=weight))
return insights
class Condition_Reversion(AlphaModel):
def __init__(self):
self.condition_period = 5
self.securities_list = []
self.day = -1
self.historical_volume_by_symbol = {}
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
# Register each security in the universe
for security in changes.added_securities:
if security not in self.securities_list:
self.historical_volume_by_symbol[security.symbol] = deque(maxlen=self.condition_period)
self.securities_list.append(security)
for security in changes.removed_securities:
if security in self.securities_list:
self.securities_list.remove(security)
def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
if data.quote_bars.count == 0: # Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
return []
if self.day == algorithm.time.day: # Only emit insights once per day
return []
self.day = algorithm.time.day
# Append volume to the list
for security in self.securities_list:
if security.Volume != 0:
self.historical_volume_by_symbol[security.symbol].appendleft(security.Volume)
weights = {}
n_count = 0
p_count = 0
for security in self.securities_list:
# If condition is met, assign weight
if len(self.historical_volume_by_symbol[security.symbol]) != 0 and max(self.historical_volume_by_symbol[security.symbol]) == security.Volume:
weights[security.symbol] = -1
n_count += 1
else:
weights[security.symbol] = 1
p_count += 1
for symbol, weight in weights.items():
if weight == -1:
weights[symbol] = weight/n_count
elif weight == -1:
weights[symbol] = weight/p_count
# Create insights to long / short the asset
insights = []
for symbol, weight in weights.items():
if weight < 0:
insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.UP, weight=weight))
'''elif weight < 0:
insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.DOWN, weight=weight))'''
return insights#region imports
from AlgorithmImports import *
from collections import deque
import scipy as sp
import numpy as np
#endregion
def EWMA(value_history):
output = value_history[0]
for i in range(1, len(value_history)):
output = 0.7 * value_history[i] + 0.3 * output
return output
class CustomMomentumPercent(PythonIndicator):
def __init__(self, name, period):
self.name = name
self.time = datetime.min
self.value = 0
self.momentum = MomentumPercent(period)
def Update(self, input):
self.momentum.update(IndicatorDataPoint(input.Symbol, input.EndTime, input.Close))
self.time = input.EndTime
self.value = self.momentum.Current.Value * input.Volume
return self.momentum.IsReady
class Skewness(PythonIndicator): # Doesn't work on 3th August 2020
def __init__(self, name, period):
self.name = name
self.count = 0
self.time = datetime.min
self.value = 0
self.queue = deque(maxlen=period)
self.change_in_close = deque(maxlen=period)
def Update(self, input):
self.queue.appendleft(input.Close)
if len(self.queue) > 1:
self.change_in_close.appendleft(self.queue[0]/self.queue[1]-1)
self.time = input.EndTime
self.count = len(self.change_in_close)
if self.count == self.queue.maxlen:
self.value = sp.stats.skew(self.change_in_close, nan_policy="omit")
return count == self.change_in_close.maxlen
class VwapReversion(PythonIndicator):
def __init__(self, name, symbol, algorithm):
self.name = name
self.time = datetime.min
self.value = 0
self.previous_value = 0
self._vwap = algorithm.vwap(symbol)
self.queue = deque(maxlen=30)
def update(self, input):
self._vwap.update(input)
self.time = input.EndTime
self.queue.appendleft(self._vwap.Current.Value / input.Close)
count = len(self.queue)
if count == self.queue.maxlen:
z_array = sp.stats.zscore(self.queue)
if np.isfinite(z_array[0]):
self.previous_value = self.value
self.value = 0.7 * z_array[0] + 0.3 * self.previous_value
return count == self.queue.maxlen
# region imports
from AlgorithmImports import *
from alpha import *
# endregion
class LiquidEquityAlgorithm(QCAlgorithm):
undesired_symbols_from_previous_deployment = []
checked_symbols_from_previous_deployment = False
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_end_date(2024, 1, 1)
self.set_cash(1_000_000)
self.SetBenchmark(self.AddEquity("SPY").Symbol)
self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
self.settings.minimum_order_margin_portfolio_percentage = 0
self.settings.rebalance_portfolio_on_security_changes = False
self.settings.rebalance_portfolio_on_insight_changes = False
self.day = -1
self.set_warm_up(timedelta(20))
self.universe_settings.asynchronous = True
self.add_universe_selection(FundamentalUniverseSelectionModel(self.fundamental_filter_function))
#self.add_universe_selection(ETFConstituentsUniverseSelectionModel("SPY"))
self.add_alpha(TSZscore_VWAPReversion())
#self.add_alpha(ConstantAlphaModel(InsightType.PRICE, InsightDirection.UP, timedelta(1)))
self.set_portfolio_construction(InsightWeightingPortfolioConstructionModel(Expiry.EndOfMonth))
#self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
#rebalance=timedelta(5)
#rebalance=Expiry.EndOfMonth
self.add_risk_management(NullRiskManagementModel())
self.set_execution(ImmediateExecutionModel())
self.previousHoldingsValue = {}
self.function_time = -1
'''def on_data(self, data):
# Exit positions that aren't backed by existing insights
# If you don't want this behavior, delete this method definition.
if not self.is_warming_up and not self.checked_symbols_from_previous_deployment:
for security_holding in self.portfolio.values():
if not security_holding.invested:
continue
symbol = security_holding.symbol
if not self.insights.has_active_insights(symbol, self.utc_time):
self.undesired_symbols_from_previous_deployment.append(symbol)
self.checked_symbols_from_previous_deployment = True
for symbol in self.undesired_symbols_from_previous_deployment:
if self.is_market_open(symbol):
self.liquidate(symbol, tag="Holding from previous deployment that's no longer desired")
self.undesired_symbols_from_previous_deployment.remove(symbol)
'''
def fundamental_filter_function(self, fundamental: List[Fundamental]):
filtered = [f for f in fundamental if f.symbol.value != "AMC" and f.has_fundamental_data and not np.isnan(f.dollar_volume)]
sorted_by_dollar_volume = sorted(filtered, key=lambda f: f.dollar_volume, reverse=True)
return [f.symbol for f in sorted_by_dollar_volume[0:1000]]