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]]