| Overall Statistics |
|
Total Orders 10237 Average Win 0.02% Average Loss -0.02% Compounding Annual Return 34.970% Drawdown 18.400% Expectancy 0.237 Start Equity 1000000 End Equity 1323313.34 Net Profit 32.331% Sharpe Ratio 1.454 Sortino Ratio 1.594 Probabilistic Sharpe Ratio 65.071% Loss Rate 40% Win Rate 60% Profit-Loss Ratio 1.06 Alpha 0.164 Beta 0.483 Annual Standard Deviation 0.166 Annual Variance 0.028 Information Ratio 0.459 Tracking Error 0.175 Treynor Ratio 0.501 Total Fees $10715.90 Estimated Strategy Capacity $3600000.00 Lowest Capacity Asset MYOK W546JDIWY3XH Portfolio Turnover 3.72% |
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from AlgorithmImports import *
import pandas as pd
import functools
import collections
import operator
class EqualWeightingPortfolioConstructionModel(PortfolioConstructionModel):
'''Provides an implementation of IPortfolioConstructionModel that gives equal weighting to all securities.
The target percent holdings of each security is 1/N where N is the number of securities.
For insights of direction InsightDirection.UP, long targets are returned and
for insights of direction InsightDirection.DOWN, short targets are returned.'''
def __init__(self, rebalance = Resolution.DAILY, portfolio_bias = PortfolioBias.LONG_SHORT):
'''Initialize a new instance of EqualWeightingPortfolioConstructionModel
Args:
rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
If None will be ignored.
The function returns the next expected rebalance time for a given algorithm UTC DateTime.
The function returns null if unknown, in which case the function will be called again in the
next loop. Returning current time will trigger rebalance.
portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long)'''
super().__init__()
self.portfolio_bias = portfolio_bias
# If the argument is an instance of Resolution or Timedelta
# Redefine rebalancing_func
rebalancing_func = rebalance
if isinstance(rebalance, int):
rebalance = Extensions.to_time_span(rebalance)
if isinstance(rebalance, timedelta):
rebalancing_func = lambda dt: dt + rebalance
if rebalancing_func:
self.set_rebalancing_func(rebalancing_func)
def determine_target_percent(self, active_insights):
'''Will determine the target percent for each insight
Args:
active_insights: The active insights to generate a target for'''
result = {}
# give equal weighting to each security
count = sum(x.direction != InsightDirection.FLAT and self.respect_portfolio_bias(x) for x in active_insights)
percent = 0 if count == 0 else 1.0 / count
for insight in active_insights:
result[insight] = (insight.direction if self.respect_portfolio_bias(insight) else InsightDirection.FLAT) * percent
return result
def respect_portfolio_bias(self, insight):
'''Method that will determine if a given insight respects the portfolio bias
Args:
insight: The insight to create a target for
'''
return self.portfolio_bias == PortfolioBias.LONG_SHORT or insight.direction == self.portfolio_bias
class MLP_PortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):
'''Provides an implementation of IPortfolioConstructionModel that generates percent targets based on the
Insight.WEIGHT. The target percent holdings of each Symbol is given by the Insight.WEIGHT from the last
active Insight for that symbol.
For insights of direction InsightDirection.UP, long targets are returned and for insights of direction
InsightDirection.DOWN, short targets are returned.
If the sum of all the last active Insight per symbol is bigger than 1, it will factor down each target
percent holdings proportionally so the sum is 1.
It will ignore Insight that have no Insight.WEIGHT value.'''
def __init__(self, algorithm, model = None, rebalance = Resolution.DAILY, portfolio_bias = PortfolioBias.LONG_SHORT):
'''Initialize a new instance of InsightWeightingPortfolioConstructionModel
Args:
rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
If None will be ignored.
The function returns the next expected rebalance time for a given algorithm UTC DateTime.
The function returns null if unknown, in which case the function will be called again in the
next loop. Returning current time will trigger rebalance.
portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long)'''
super().__init__(rebalance, portfolio_bias)
self.algorithm = algorithm
def should_create_target_for_insight(self, insight):
'''Method that will determine if the portfolio construction model should create a
target for this insight
Args:
insight: The insight to create a target for'''
# Ignore insights that don't have Weight value
return insight.weight is not None
def determine_target_percent(self, activeInsights: List[Insight])-> Dict[Insight, float]:
'''Will determine the target percent for each insight
Args:
activeInsights: The active insights to generate a target for'''
# 1. Temp solution: Sum up weights from the mulitple insights
Features = {}
for insight in activeInsights:
if insight.symbol not in Features.keys():
Features[insight.symbol] = insight.weight
else:
Features[insight.symbol] = Features[insight.symbol] + insight.weight
# 2. Compute long/ short weight sum to adjust long short ratio
p_sum = 0
n_sum = 0
for symbol, weight in Features.items():
if weight > 0:
p_sum += weight
elif weight < 0:
n_sum += np.abs(weight)
# 3. return results
result = {}
emitted_symbol = []
weight_sums = sum([np.abs(weight) for weight in Features.values()])
weight_factor = 1.0
if weight_sums > 1:
weight_factor = 1 / weight_sums
for insight in activeInsights:
if insight.weight * Features[insight.symbol] > 0:
if insight.symbol not in emitted_symbol:
emitted_symbol.append(insight.symbol)
result[insight] = Features[insight.symbol] * weight_factor
return result
def get_value(self, insight):
'''Method that will determine which member will be used to compute the weights and gets its value
Args:
insight: The insight to create a target for
Returns:
The value of the selected insight member'''
return abs(insight.weight)
# Multi-Alpha:
def get_target_insights(self) -> List[Insight]:
return list(self.algorithm.insights.get_active_insights(self.algorithm.utc_time))
#region imports
from AlgorithmImports import *
from indicators import *
from collections import deque
import numpy as np
import scipy as sp
#endregion
class TSZscore_VwapReversion(AlphaModel):
def __init__(self):
self.period = 20
self.securities_list = []
self.day = -1
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)
# 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
# Make scale similar across alphas
abs_weight = {key: abs(val) for key, val in weights.items()}
weights_sum = sum(abs_weight.values())
if weights_sum != 0:
for symbol, weight in weights.items():
weights[symbol] = weight/ weights_sum
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
class TSZscore_DividendGrowth(AlphaModel):
def __init__(self):
self.period = 252
self.day = -1
self.securities_list = []
self.dps = {}
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.dps[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:
if not np.isnan(security.fundamentals.earning_reports.dividend_per_share.Value):
self.dps[security.symbol].appendleft(security.fundamentals.earning_reports.dividend_per_share.Value)
zscore_by_symbol[security.symbol] = sp.stats.zscore(self.dps[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
# Make scale similar across alphas
abs_weight = {key: abs(val) for key, val in weights.items()}
weights_sum = sum(abs_weight.values())
if weights_sum != 0:
for symbol, weight in weights.items():
weights[symbol] = weight/ weights_sum
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 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
weights_mean = sum(weights.values())/len(weights.values())
for symbol, weight in weights.items():
weights[symbol] = weight - weights_mean
# Make scale similar across alphas
abs_weight = {key: abs(val) for key, val in weights.items()}
weights_sum = sum(abs_weight.values())
if weights_sum != 0:
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))
elif weight < 0:
insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.DOWN, weight=weight))
#Expiry.END_OF_DAY
return insights
class MomentumQuantilesAlphaModel(AlphaModel):
def __init__(self):
self.quantiles = 10
self.lookback_months = 6
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("custom", self.lookback_months, self.securities_list)
security.indicator = MomentumPercent(self.lookback_months)
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():
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 momentum of each asset in the universe
momentum_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(momentum_by_symbol)/self.quantiles)
if quantile_size == 0:
return []
# Create insights to long the assets in the universe with the greatest momentum
weight = 1 / (quantile_size+1)
insights = []
for symbol, _ in sorted(momentum_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)
#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 *
from PortfolioConstructor import MLP_PortfolioConstructionModel
# endregion
class LiquidEquityAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_end_date(2021, 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(1))
self.universe_settings.asynchronous = True
self.add_universe_selection(FundamentalUniverseSelectionModel(self.fundamental_filter_function))
self.add_alpha(TSZscore_VwapReversion())
self.add_alpha(TSZscore_DividendGrowth())
self.add_alpha(Conditional_Reversion())
self.add_alpha(MomentumQuantilesAlphaModel())
self.set_portfolio_construction(MLP_PortfolioConstructionModel(algorithm=self, rebalance=Expiry.EndOfMonth))
self.add_risk_management(NullRiskManagementModel())
self.set_execution(ImmediateExecutionModel())
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]]