chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:02:50 +08:00
commit 0fc60fdcb1
5008 changed files with 910633 additions and 0 deletions
@@ -0,0 +1,179 @@
# 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 scipy.stats as sp
from Risk.NullRiskManagementModel import NullRiskManagementModel
from Portfolio.EqualWeightingPortfolioConstructionModel import EqualWeightingPortfolioConstructionModel
from Execution.ImmediateExecutionModel import ImmediateExecutionModel
class ContingentClaimsAnalysisDefaultPredictionAlpha(QCAlgorithm):
''' Contingent Claim Analysis is put forth by Robert Merton, recepient of the Noble Prize in Economics in 1997 for his work in contributing to
Black-Scholes option pricing theory, which says that the equity market value of stockholders equity is given by the Black-Scholes solution
for a European call option. This equation takes into account Debt, which in CCA is the equivalent to a strike price in the BS solution. The probability
of default on corporate debt can be calculated as the N(-d2) term, where d2 is a function of the interest rate on debt(µ), face value of the debt (B), value of the firm's assets (V),
standard deviation of the change in a firm's asset value (σ), the dividend and interest payouts due (D), and the time to maturity of the firm's debt(τ). N(*) is the cumulative
distribution function of a standard normal distribution, and calculating N(-d2) gives us the probability of the firm's assets being worth less
than the debt of the company at the time that the debt reaches maturity -- that is, the firm doesn't have enough in assets to pay off its debt and defaults.
We use a Fine/Coarse Universe Selection model to select small cap stocks, who we postulate are more likely to default
on debt in general than blue-chip companies, and extract Fundamental data to plug into the CCA formula.
This Alpha emits insights based on whether or not a company is likely to default given its probability of default vs a default probability threshold that we set arbitrarily.
Prob. default (on principal B at maturity T) = Prob(VT < B) = 1 - N(d2) = N(-d2) where -d2(µ) = -{ln(V/B) + [(µ - D) - ½σ2]τ}/ σ √τ.
N(d) = (univariate) cumulative standard normal distribution function (from -inf to d)
B = face value (principal) of the debt
D = dividend + interest payout
V = value of firms assets
σ (sigma) = standard deviation of firm value changes (returns in V)
τ (tau) = time to debts maturity
µ (mu) = interest rate
This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open
sourced so the community and client funds can see an example of an alpha.'''
def initialize(self):
## Set requested data resolution and variables to help with Universe Selection control
self.universe_settings.resolution = Resolution.DAILY
self.month = -1
## Declare single variable to be passed in multiple places -- prevents issue with conflicting start dates declared in different places
self.set_start_date(2018,1,1)
self.set_cash(100000)
## SPDR Small Cap ETF is a better benchmark than the default SP500
self.set_benchmark('IJR')
## Set Universe Selection Model
self.set_universe_selection(FineFundamentalUniverseSelectionModel(self.coarse_selection_function, self.fine_selection_function))
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
## Set CCA Alpha Model
self.set_alpha(ContingentClaimsAnalysisAlphaModel())
## Set Portfolio Construction Model
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
## Set Execution Model
self.set_execution(ImmediateExecutionModel())
## Set Risk Management Model
self.set_risk_management(NullRiskManagementModel())
def coarse_selection_function(self, coarse):
## Boolean controls so that our symbol universe is only updated once per month
if self.time.month == self.month:
return Universe.UNCHANGED
self.month = self.time.month
## Sort by dollar volume, lowest to highest
sorted_by_dollar_volume = sorted([x for x in coarse if x.has_fundamental_data],
key=lambda x: x.dollar_volume, reverse=True)
## Return smallest 750 -- idea is that smaller companies are most likely to go bankrupt than blue-chip companies
## Filter for assets with fundamental data
return [x.symbol for x in sorted_by_dollar_volume[:750]]
def fine_selection_function(self, fine):
def is_valid(x):
statement = x.financial_statements
sheet = statement.balance_sheet
total_assets = sheet.total_assets
ratios = x.operation_ratios
return total_assets.one_month > 0 and \
total_assets.three_months > 0 and \
total_assets.six_months > 0 and \
total_assets.twelve_months > 0 and \
sheet.current_liabilities.twelve_months > 0 and \
sheet.interest_payable.twelve_months > 0 and \
ratios.total_assets_growth.one_year > 0 and \
statement.income_statement.gross_dividend_payment.twelve_months > 0 and \
ratios.roa.one_year > 0
return [x.symbol for x in sorted(fine, key=lambda x: is_valid(x))]
class ContingentClaimsAnalysisAlphaModel(AlphaModel):
def __init__(self, *args, **kwargs):
self.probability_of_default_by_symbol = {}
self.default_threshold = kwargs['default_threshold'] if 'default_threshold' in kwargs else 0.25
def update(self, algorithm, data):
'''Updates this alpha model with the latest data from the algorithm.
This is called each time the algorithm receives data for subscribed securities
Args:
algorithm: The algorithm instance
data: The new data available
Returns:
The new insights generated'''
## Build a list to hold our insights
insights = []
for symbol, pod in self.probability_of_default_by_symbol.items():
## If Prob. of Default is greater than our set threshold, then emit an insight indicating that this asset is trending downward
if pod >= self.default_threshold and pod != 1.0:
insights.append(Insight.price(symbol, timedelta(30), InsightDirection.DOWN, pod, None))
return insights
def on_securities_changed(self, algorithm, changes):
for removed in changes.removed_securities:
self.probability_of_default_by_symbol.pop(removed.symbol, None)
# initialize data for added securities
symbols = [ x.symbol for x in changes.added_securities ]
for symbol in symbols:
if symbol not in self.probability_of_default_by_symbol:
## CCA valuation
pod = self.get_probability_of_default(algorithm, symbol)
if pod is not None:
self.probability_of_default_by_symbol[symbol] = pod
def get_probability_of_default(self, algorithm, symbol):
'''This model applies options pricing theory, Black-Scholes specifically,
to fundamental data to give the probability of a default'''
security = algorithm.securities[symbol]
if security.fundamentals is None or security.fundamentals.financial_statements is None or security.fundamentals.operation_ratios is None:
return None
statement = security.fundamentals.financial_statements
sheet = statement.balance_sheet
total_assets = sheet.total_assets
tau = 360 ## Days
mu = security.fundamentals.operation_ratios.roa.one_year
V = total_assets.twelve_months
B = sheet.current_liabilities.twelve_months
D = statement.income_statement.gross_dividend_payment.twelve_months + sheet.interest_payable.twelve_months
series = pd.Series(
[
total_assets.one_month,
total_assets.three_months,
total_assets.six_months,
V
])
sigma = series.iloc[series.nonzero()[0]]
sigma = np.std(sigma.pct_change()[1:len(sigma)])
d2 = ((np.log(V) - np.log(B)) + ((mu - D) - 0.5*sigma**2.0)*tau)/ (sigma*np.sqrt(tau))
return sp.norm.cdf(-d2)
@@ -0,0 +1,219 @@
# 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.
'''
Energy prices, especially Oil and Natural Gas, are in general fairly correlated,
meaning they typically move in the same direction as an overall trend. This Alpha
uses this idea and implements an Alpha Model that takes Natural Gas ETF price
movements as a leading indicator for Crude Oil ETF price movements. We take the
Natural Gas/Crude Oil ETF pair with the highest historical price correlation and
then create insights for Crude Oil depending on whether or not the Natural Gas ETF price change
is above/below a certain threshold that we set (arbitrarily).
This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open
sourced so the community and client funds can see an example of an alpha.
'''
from AlgorithmImports import *
class GasAndCrudeOilEnergyCorrelationAlpha(QCAlgorithm):
def initialize(self):
self.set_start_date(2018, 1, 1) #Set Start Date
self.set_cash(100000) #Set Strategy Cash
natural_gas = [Symbol.create(x, SecurityType.EQUITY, Market.USA) for x in ['UNG','BOIL','FCG']]
crude_oil = [Symbol.create(x, SecurityType.EQUITY, Market.USA) for x in ['USO','UCO','DBO']]
## Set Universe Selection
self.universe_settings.resolution = Resolution.MINUTE
self.set_universe_selection( ManualUniverseSelectionModel(natural_gas + crude_oil) )
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
## Custom Alpha Model
self.set_alpha(PairsAlphaModel(leading = natural_gas, following = crude_oil, history_days = 90, resolution = Resolution.MINUTE))
## Equal-weight our positions, in this case 100% in USO
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel(resolution = Resolution.MINUTE))
## Immediate Execution Fill Model
self.set_execution(CustomExecutionModel())
## Null Risk-Management Model
self.set_risk_management(NullRiskManagementModel())
def on_order_event(self, order_event):
if order_event.status == OrderStatus.FILLED:
self.debug(f'Purchased Stock: {order_event.symbol}')
def on_end_of_algorithm(self):
for kvp in self.portfolio:
if kvp.value.invested:
self.log(f'Invested in: {kvp.key}')
class PairsAlphaModel(AlphaModel):
'''This Alpha model assumes that the ETF for natural gas is a good leading-indicator
of the price of the crude oil ETF. The model will take in arguments for a threshold
at which the model triggers an insight, the length of the look-back period for evaluating
rate-of-change of UNG prices, and the duration of the insight'''
def __init__(self, *args, **kwargs):
self.leading = kwargs.get('leading', [])
self.following = kwargs.get('following', [])
self.history_days = kwargs.get('history_days', 90) ## In days
self.lookback = kwargs.get('lookback', 5)
self.resolution = kwargs.get('resolution', Resolution.HOUR)
self.prediction_interval = Time.multiply(Extensions.to_time_span(self.resolution), 5) ## Arbitrary
self.difference_trigger = kwargs.get('difference_trigger', 0.75)
self._symbol_data_by_symbol = {}
self.next_update = None
def update(self, algorithm, data):
if (self.next_update is None) or (algorithm.time > self.next_update):
self.correlation_pairs_selection()
self.next_update = algorithm.time + timedelta(30)
magnitude = round(self.pairs[0].rate_of_return / 100, 6)
## Check if Natural Gas returns are greater than the threshold we've set
if self.pairs[0].rate_of_return > self.difference_trigger:
return [Insight.price(self.pairs[1].symbol, self.prediction_interval, InsightDirection.UP, magnitude)]
if self.pairs[0].rate_of_return < -self.difference_trigger:
return [Insight.price(self.pairs[1].symbol, self.prediction_interval, InsightDirection.DOWN, magnitude)]
return []
def correlation_pairs_selection(self):
## Get returns for each natural gas/oil ETF
daily_return = {}
for symbol, symbol_data in self._symbol_data_by_symbol.items():
daily_return[symbol] = symbol_data.daily_return_array
## Estimate coefficients of different correlation measures
tau = pd.DataFrame.from_dict(daily_return).corr(method='kendall')
## Calculate the pair with highest historical correlation
max_corr = -1
for x in self.leading:
df = tau[[x]].loc[self.following]
corr = float(df.max())
if corr > max_corr:
self.pairs = (
self._symbol_data_by_symbol[x],
self._symbol_data_by_symbol[df.idxmax()[0]])
max_corr = corr
def on_securities_changed(self, algorithm, changes):
'''Event fired each time the we add/remove securities from the data feed
Args:
algorithm: The algorithm instance that experienced the change in securities
changes: The security additions and removals from the algorithm'''
for removed in changes.removed_securities:
symbol_data = self._symbol_data_by_symbol.pop(removed.symbol, None)
if symbol_data is not None:
symbol_data.remove_consolidators(algorithm)
# initialize data for added securities
symbols = [ x.symbol for x in changes.added_securities ]
history = algorithm.history(symbols, self.history_days + 1, Resolution.DAILY)
if history.empty: return
tickers = history.index.levels[0]
for ticker in tickers:
symbol = SymbolCache.get_symbol(ticker)
if symbol not in self._symbol_data_by_symbol:
symbol_data = SymbolData(symbol, self.history_days, self.lookback, self.resolution, algorithm)
self._symbol_data_by_symbol[symbol] = symbol_data
symbol_data.update_daily_rate_of_change(history.loc[ticker])
history = algorithm.history(symbols, self.lookback, self.resolution)
if history.empty: return
for ticker in tickers:
symbol = SymbolCache.get_symbol(ticker)
if symbol in self._symbol_data_by_symbol:
self._symbol_data_by_symbol[symbol].update_rate_of_change(history.loc[ticker])
class SymbolData:
'''Contains data specific to a symbol required by this model'''
def __init__(self, symbol, daily_lookback, lookback, resolution, algorithm):
self.symbol = symbol
self.daily_return = RateOfChangePercent(f'{symbol}.daily_rocp({1})', 1)
self.daily_consolidator = algorithm.resolve_consolidator(symbol, Resolution.DAILY)
self.daily_return_history = RollingWindow(daily_lookback)
def updatedaily_return_history(s, e):
self.daily_return_history.add(e)
self.daily_return.updated += updatedaily_return_history
algorithm.register_indicator(symbol, self.daily_return, self.daily_consolidator)
self.rocp = RateOfChangePercent(f'{symbol}.rocp({lookback})', lookback)
self.consolidator = algorithm.resolve_consolidator(symbol, resolution)
algorithm.register_indicator(symbol, self.rocp, self.consolidator)
def remove_consolidators(self, algorithm):
algorithm.subscription_manager.remove_consolidator(self.symbol, self.consolidator)
algorithm.subscription_manager.remove_consolidator(self.symbol, self.daily_consolidator)
def update_rate_of_change(self, history):
for tuple in history.itertuples():
self.rocp.update(tuple.Index, tuple.close)
def update_daily_rate_of_change(self, history):
for tuple in history.itertuples():
self.daily_return.update(tuple.Index, tuple.close)
@property
def rate_of_return(self):
return float(self.rocp.current.value)
@property
def daily_return_array(self):
return pd.Series({x.end_time: x.value for x in self.daily_return_history})
def __repr__(self):
return f"{self.rocp.name} - {self.daily_return}"
class CustomExecutionModel(ExecutionModel):
'''Provides an implementation of IExecutionModel that immediately submits market orders to achieve the desired portfolio targets'''
def __init__(self):
'''Initializes a new instance of the ImmediateExecutionModel class'''
self.targets_collection = PortfolioTargetCollection()
self.previous_symbol = None
def execute(self, algorithm, targets):
'''Immediately submits orders for the specified portfolio targets.
Args:
algorithm: The algorithm instance
targets: The portfolio targets to be ordered'''
self.targets_collection.add_range(targets)
for target in self.targets_collection.order_by_margin_impact(algorithm):
open_quantity = sum([x.quantity for x in algorithm.transactions.get_open_orders(target.symbol)])
existing = algorithm.securities[target.symbol].holdings.quantity + open_quantity
quantity = target.quantity - existing
## Liquidate positions in Crude Oil ETF that is no longer part of the highest-correlation pair
if (str(target.symbol) != str(self.previous_symbol)) and (self.previous_symbol is not None):
algorithm.liquidate(self.previous_symbol)
if quantity != 0:
algorithm.market_order(target.symbol, quantity)
self.previous_symbol = target.symbol
self.targets_collection.clear_fulfilled(algorithm)
@@ -0,0 +1,111 @@
# 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 *
#
# Equity indices exhibit mean reversion in daily returns. The Internal Bar Strength indicator (IBS),
# which relates the closing price of a security to its daily range can be used to identify overbought
# and oversold securities.
#
# This alpha ranks 33 global equity ETFs on its IBS value the previous day and predicts for the following day
# that the ETF with the highest IBS value will decrease in price, and the ETF with the lowest IBS value
# will increase in price.
#
# Source: Kakushadze, Zura, and Juan Andrés Serur. “4. Exchange-Traded Funds (ETFs).” 151 Trading Strategies, Palgrave Macmillan, 2018, pp. 9091.
#
# This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open sourced so the community and client funds can see an example of an alpha.
#
class GlobalEquityMeanReversionIBSAlpha(QCAlgorithm):
def initialize(self):
self.set_start_date(2018, 1, 1)
self.set_cash(100000)
# Set zero transaction fees
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
# Global Equity ETF tickers
tickers = ["ECH","EEM","EFA","EPHE","EPP","EWA","EWC","EWG",
"EWH","EWI","EWJ","EWL","EWM","EWM","EWO","EWP",
"EWQ","EWS","EWT","EWU","EWY","EWZ","EZA","FXI",
"GXG","IDX","ILF","EWM","QQQ","RSX","SPY","THD"]
symbols = [Symbol.create(ticker, SecurityType.EQUITY, Market.USA) for ticker in tickers]
# Manually curated universe
self.universe_settings.resolution = Resolution.DAILY
self.set_universe_selection(ManualUniverseSelectionModel(symbols))
# Use GlobalEquityMeanReversionAlphaModel to establish insights
self.set_alpha(MeanReversionIBSAlphaModel())
# Equally weigh securities in portfolio, based on insights
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
# Set Immediate Execution Model
self.set_execution(ImmediateExecutionModel())
# Set Null Risk Management Model
self.set_risk_management(NullRiskManagementModel())
class MeanReversionIBSAlphaModel(AlphaModel):
'''Uses ranking of Internal Bar Strength (IBS) to create direction prediction for insights'''
def __init__(self, *args, **kwargs):
lookback = kwargs['lookback'] if 'lookback' in kwargs else 1
resolution = kwargs['resolution'] if 'resolution' in kwargs else Resolution.DAILY
self.prediction_interval = Time.multiply(Extensions.to_time_span(resolution), lookback)
self.number_of_stocks = kwargs['number_of_stocks'] if 'number_of_stocks' in kwargs else 2
def update(self, algorithm, data):
insights = []
symbols_ibs = dict()
returns = dict()
for security in algorithm.active_securities.values:
if security.has_data:
high = security.high
low = security.low
hilo = high - low
# Do not consider symbol with zero open and avoid division by zero
if security.open * hilo != 0:
# Internal bar strength (IBS)
symbols_ibs[security.symbol] = (security.close - low)/hilo
returns[security.symbol] = security.close/security.open-1
# Number of stocks cannot be higher than half of symbols_ibs length
number_of_stocks = min(int(len(symbols_ibs)/2), self.number_of_stocks)
if number_of_stocks == 0:
return []
# Rank securities with the highest IBS value
ordered = sorted(symbols_ibs.items(), key=lambda kv: (round(kv[1], 6), kv[0]), reverse=True)
high_ibs = dict(ordered[0:number_of_stocks]) # Get highest IBS
low_ibs = dict(ordered[-number_of_stocks:]) # Get lowest IBS
# Emit "down" insight for the securities with the highest IBS value
for key,value in high_ibs.items():
insights.append(Insight.price(key, self.prediction_interval, InsightDirection.DOWN, abs(returns[key]), None))
# Emit "up" insight for the securities with the lowest IBS value
for key,value in low_ibs.items():
insights.append(Insight.price(key, self.prediction_interval, InsightDirection.UP, abs(returns[key]), None))
return insights
@@ -0,0 +1,211 @@
# 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 *
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel
from math import ceil
from itertools import chain
class GreenblattMagicFormulaAlpha(QCAlgorithm):
''' Alpha Streams: Benchmark Alpha: Pick stocks according to Joel Greenblatt's Magic Formula
This alpha picks stocks according to Joel Greenblatt's Magic Formula.
First, each stock is ranked depending on the relative value of the ratio EV/EBITDA. For example, a stock
that has the lowest EV/EBITDA ratio in the security universe receives a score of one while a stock that has
the tenth lowest EV/EBITDA score would be assigned 10 points.
Then, each stock is ranked and given a score for the second valuation ratio, Return on Capital (ROC).
Similarly, a stock that has the highest ROC value in the universe gets one score point.
The stocks that receive the lowest combined score are chosen for insights.
Source: Greenblatt, J. (2010) The Little Book That Beats the Market
This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open
sourced so the community and client funds can see an example of an alpha.'''
def initialize(self):
self.set_start_date(2018, 1, 1)
self.set_cash(100000)
#Set zero transaction fees
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
# select stocks using MagicFormulaUniverseSelectionModel
self.set_universe_selection(GreenBlattMagicFormulaUniverseSelectionModel())
# Use MagicFormulaAlphaModel to establish insights
self.set_alpha(RateOfChangeAlphaModel())
# Equally weigh securities in portfolio, based on insights
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
## Set Immediate Execution Model
self.set_execution(ImmediateExecutionModel())
## Set Null Risk Management Model
self.set_risk_management(NullRiskManagementModel())
class RateOfChangeAlphaModel(AlphaModel):
'''Uses Rate of Change (ROC) to create magnitude prediction for insights.'''
def __init__(self, *args, **kwargs):
self.lookback = kwargs.get('lookback', 1)
self.resolution = kwargs.get('resolution', Resolution.DAILY)
self.prediction_interval = Time.multiply(Extensions.to_time_span(self.resolution), self.lookback)
self._symbol_data_by_symbol = {}
def update(self, algorithm, data):
insights = []
for symbol, symbol_data in self._symbol_data_by_symbol.items():
if symbol_data.can_emit:
insights.append(Insight.price(symbol, self.prediction_interval, InsightDirection.UP, symbol_data.returns, None))
return insights
def on_securities_changed(self, algorithm, changes):
# clean up data for removed securities
for removed in changes.removed_securities:
symbol_data = self._symbol_data_by_symbol.pop(removed.symbol, None)
if symbol_data is not None:
symbol_data.remove_consolidators(algorithm)
# initialize data for added securities
symbols = [ x.symbol for x in changes.added_securities
if x.symbol not in self._symbol_data_by_symbol]
history = algorithm.history(symbols, self.lookback, self.resolution)
if history.empty: return
for symbol in symbols:
symbol_data = SymbolData(algorithm, symbol, self.lookback, self.resolution)
self._symbol_data_by_symbol[symbol] = symbol_data
symbol_data.warm_up_indicators(history.loc[symbol])
class SymbolData:
'''Contains data specific to a symbol required by this model'''
def __init__(self, algorithm, symbol, lookback, resolution):
self.previous = 0
self._symbol = symbol
self.roc = RateOfChange(f'{symbol}.roc({lookback})', lookback)
self.consolidator = algorithm.resolve_consolidator(symbol, resolution)
algorithm.register_indicator(symbol, self.roc, self.consolidator)
def remove_consolidators(self, algorithm):
algorithm.subscription_manager.remove_consolidator(self._symbol, self.consolidator)
def warm_up_indicators(self, history):
for tuple in history.itertuples():
self.roc.update(tuple.Index, tuple.close)
@property
def returns(self):
return self.roc.current.value
@property
def can_emit(self):
if self.previous == self.roc.samples:
return False
self.previous = self.roc.samples
return self.roc.is_ready
def __str__(self, **kwargs):
return f'{self.roc.name}: {(1 + self.returns)**252 - 1:.2%}'
class GreenBlattMagicFormulaUniverseSelectionModel(FundamentalUniverseSelectionModel):
'''Defines a universe according to Joel Greenblatt's Magic Formula, as a universe selection model for the framework algorithm.
From the universe QC500, stocks are ranked using the valuation ratios, Enterprise Value to EBITDA (EV/EBITDA) and Return on Assets (ROA).
'''
def __init__(self,
filter_fine_data = True,
universe_settings = None):
'''Initializes a new default instance of the MagicFormulaUniverseSelectionModel'''
super().__init__(filter_fine_data, universe_settings)
# Number of stocks in Coarse Universe
self.number_of_symbols_coarse = 500
# Number of sorted stocks in the fine selection subset using the valuation ratio, EV to EBITDA (EV/EBITDA)
self.number_of_symbols_fine = 20
# Final number of stocks in security list, after sorted by the valuation ratio, Return on Assets (ROA)
self.number_of_symbols_in_portfolio = 10
self.last_month = -1
self.dollar_volume_by_symbol = {}
def select_coarse(self, algorithm, coarse):
'''Performs coarse selection for constituents.
The stocks must have fundamental data'''
month = algorithm.time.month
if month == self.last_month:
return Universe.UNCHANGED
self.last_month = month
# sort the stocks by dollar volume and take the top 1000
top = sorted([x for x in coarse if x.has_fundamental_data],
key=lambda x: x.dollar_volume, reverse=True)[:self.number_of_symbols_coarse]
self.dollar_volume_by_symbol = { i.symbol: i.dollar_volume for i in top }
return list(self.dollar_volume_by_symbol.keys())
def select_fine(self, algorithm, fine):
'''QC500: Performs fine selection for the coarse selection constituents
The company's headquarter must in the U.S.
The stock must be traded on either the NYSE or NASDAQ
At least half a year since its initial public offering
The stock's market cap must be greater than 500 million
Magic Formula: Rank stocks by Enterprise Value to EBITDA (EV/EBITDA)
Rank subset of previously ranked stocks (EV/EBITDA), using the valuation ratio Return on Assets (ROA)'''
# QC500:
## The company's headquarter must in the U.S.
## The stock must be traded on either the NYSE or NASDAQ
## At least half a year since its initial public offering
## The stock's market cap must be greater than 500 million
filtered_fine = [x for x in fine if x.company_reference.country_id == "USA"
and (x.company_reference.primary_exchange_id == "NYS" or x.company_reference.primary_exchange_id == "NAS")
and (algorithm.time - x.security_reference.ipo_date).days > 180
and x.earning_reports.basic_average_shares.three_months * x.earning_reports.basic_eps.twelve_months * x.valuation_ratios.pe_ratio > 5e8]
count = len(filtered_fine)
if count == 0: return []
my_dict = dict()
percent = self.number_of_symbols_fine / count
# select stocks with top dollar volume in every single sector
for key in ["N", "M", "U", "T", "B", "I"]:
value = [x for x in filtered_fine if x.company_reference.industry_template_code == key]
value = sorted(value, key=lambda x: self.dollar_volume_by_symbol[x.symbol], reverse = True)
my_dict[key] = value[:ceil(len(value) * percent)]
# stocks in QC500 universe
top_fine = chain.from_iterable(my_dict.values())
# Magic Formula:
## Rank stocks by Enterprise Value to EBITDA (EV/EBITDA)
## Rank subset of previously ranked stocks (EV/EBITDA), using the valuation ratio Return on Assets (ROA)
# sort stocks in the security universe of QC500 based on Enterprise Value to EBITDA valuation ratio
sorted_by_ev_to_ebitda = sorted(top_fine, key=lambda x: x.valuation_ratios.ev_to_ebitda , reverse=True)
# sort subset of stocks that have been sorted by Enterprise Value to EBITDA, based on the valuation ratio Return on Assets (ROA)
sorted_by_roa = sorted(sorted_by_ev_to_ebitda[:self.number_of_symbols_fine], key=lambda x: x.valuation_ratios.forward_roa, reverse=False)
# retrieve list of securites in portfolio
return [f.symbol for f in sorted_by_roa[:self.number_of_symbols_in_portfolio]]
@@ -0,0 +1,122 @@
# 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 *
#
# Reversal strategy that goes long when price crosses below SMA and Short when price crosses above SMA.
# The trading strategy is implemented only between 10AM - 3PM (NY time). Research suggests this is due to
# institutional trades during market hours which need hedging with the USD. Source paper:
# LeBaron, Zhao: Intraday Foreign Exchange Reversals
# http://people.brandeis.edu/~blebaron/wps/fxnyc.pdf
# http://www.fma.org/Reno/Papers/ForeignExchangeReversalsinNewYorkTime.PDF
#
# This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open sourced so the community and client funds can see an example of an alpha.
#
class IntradayReversalCurrencyMarketsAlpha(QCAlgorithm):
def initialize(self):
self.set_start_date(2015, 1, 1)
self.set_cash(100000)
# Set zero transaction fees
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
# Select resolution
resolution = Resolution.HOUR
# Reversion on the USD.
symbols = [Symbol.create("EURUSD", SecurityType.FOREX, Market.OANDA)]
# Set requested data resolution
self.universe_settings.resolution = resolution
self.set_universe_selection(ManualUniverseSelectionModel(symbols))
self.set_alpha(IntradayReversalAlphaModel(5, resolution))
# Equally weigh securities in portfolio, based on insights
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
# Set Immediate Execution Model
self.set_execution(ImmediateExecutionModel())
# Set Null Risk Management Model
self.set_risk_management(NullRiskManagementModel())
#Set WarmUp for Indicators
self.set_warm_up(20)
class IntradayReversalAlphaModel(AlphaModel):
'''Alpha model that uses a Price/SMA Crossover to create insights on Hourly Frequency.
Frequency: Hourly data with 5-hour simple moving average.
Strategy:
Reversal strategy that goes Long when price crosses below SMA and Short when price crosses above SMA.
The trading strategy is implemented only between 10AM - 3PM (NY time)'''
# Initialize variables
def __init__(self, period_sma = 5, resolution = Resolution.HOUR):
self.period_sma = period_sma
self.resolution = resolution
self.cache = {} # Cache for SymbolData
self.name = 'IntradayReversalAlphaModel'
def update(self, algorithm, data):
# Set the time to close all positions at 3PM
time_to_close = algorithm.time.replace(hour=15, minute=1, second=0)
insights = []
for kvp in algorithm.active_securities:
symbol = kvp.key
if self.should_emit_insight(algorithm, symbol) and symbol in self.cache:
price = kvp.value.price
symbol_data = self.cache[symbol]
direction = InsightDirection.UP if symbol_data.is_uptrend(price) else InsightDirection.DOWN
# Ignore signal for same direction as previous signal (when no crossover)
if direction == symbol_data.previous_direction:
continue
# Save the current Insight Direction to check when the crossover happens
symbol_data.previous_direction = direction
# Generate insight
insights.append(Insight.price(symbol, time_to_close, direction))
return insights
def on_securities_changed(self, algorithm, changes):
'''Handle creation of the new security and its cache class.
Simplified in this example as there is 1 asset.'''
for security in changes.added_securities:
self.cache[security.symbol] = SymbolData(algorithm, security.symbol, self.period_sma, self.resolution)
def should_emit_insight(self, algorithm, symbol):
'''Time to control when to start and finish emitting (10AM to 3PM)'''
time_of_day = algorithm.time.time()
return algorithm.securities[symbol].has_data and time_of_day >= time(10) and time_of_day <= time(15)
class SymbolData:
def __init__(self, algorithm, symbol, period_sma, resolution):
self.previous_direction = InsightDirection.FLAT
self.price_sma = algorithm.sma(symbol, period_sma, resolution)
def is_uptrend(self, price):
return self.price_sma.is_ready and price < round(self.price_sma.current.value * 1.001, 6)
@@ -0,0 +1,123 @@
# 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 *
#
# Academic research suggests that stock market participants generally place their orders at the market open and close.
# Intraday trading volume is J-Shaped, where the minimum trading volume of the day is during lunch-break. Stocks become
# more volatile as order flow is reduced and tend to mean-revert during lunch-break.
#
# This alpha aims to capture the mean-reversion effect of ETFs during lunch-break by ranking 20 ETFs
# on their return between the close of the previous day to 12:00 the day after and predicting mean-reversion
# in price during lunch-break.
#
# Source: Lunina, V. (June 2011). The Intraday Dynamics of Stock Returns and Trading Activity: Evidence from OMXS 30 (Master's Essay, Lund University).
# Retrieved from http://lup.lub.lu.se/luur/download?func=downloadFile&recordOId=1973850&fileOId=1973852
#
# This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open sourced so the community and client funds can see an example of an alpha.
#
class MeanReversionLunchBreakAlpha(QCAlgorithm):
def initialize(self):
self.set_start_date(2018, 1, 1)
self.set_cash(100000)
# Set zero transaction fees
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
# Use Hourly Data For Simplicity
self.universe_settings.resolution = Resolution.HOUR
self.set_universe_selection(CoarseFundamentalUniverseSelectionModel(self.coarse_selection_function))
# Use MeanReversionLunchBreakAlphaModel to establish insights
self.set_alpha(MeanReversionLunchBreakAlphaModel())
# Equally weigh securities in portfolio, based on insights
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
# Set Immediate Execution Model
self.set_execution(ImmediateExecutionModel())
# Set Null Risk Management Model
self.set_risk_management(NullRiskManagementModel())
# Sort the data by daily dollar volume and take the top '20' ETFs
def coarse_selection_function(self, coarse):
sorted_by_dollar_volume = sorted(coarse, key=lambda x: x.dollar_volume, reverse=True)
filtered = [ x.symbol for x in sorted_by_dollar_volume if not x.has_fundamental_data ]
return filtered[:20]
class MeanReversionLunchBreakAlphaModel(AlphaModel):
'''Uses the price return between the close of previous day to 12:00 the day after to
predict mean-reversion of stock price during lunch break and creates direction prediction
for insights accordingly.'''
def __init__(self, *args, **kwargs):
lookback = kwargs['lookback'] if 'lookback' in kwargs else 1
self.resolution = Resolution.HOUR
self.prediction_interval = Time.multiply(Extensions.to_time_span(self.resolution), lookback)
self._symbol_data_by_symbol = dict()
def update(self, algorithm, data):
for symbol, symbol_data in self._symbol_data_by_symbol.items():
if data.bars.contains_key(symbol):
bar = data.bars.get(symbol)
symbol_data.update(bar.end_time, bar.close)
return [] if algorithm.time.hour != 12 else \
[x.insight for x in self._symbol_data_by_symbol.values()]
def on_securities_changed(self, algorithm, changes):
for security in changes.removed_securities:
self._symbol_data_by_symbol.pop(security.symbol, None)
# Retrieve price history for all securities in the security universe
# and update the indicators in the SymbolData object
symbols = [x.symbol for x in changes.added_securities]
history = algorithm.history(symbols, 1, self.resolution)
if history.empty:
algorithm.debug(f"No data on {algorithm.time}")
return
history = history.close.unstack(level = 0)
for ticker, values in history.items():
symbol = next((x for x in symbols if str(x) == ticker ), None)
if symbol in self._symbol_data_by_symbol or symbol is None: continue
self._symbol_data_by_symbol[symbol] = self.SymbolData(symbol, self.prediction_interval)
self._symbol_data_by_symbol[symbol].update(values.index[0], values[0])
class SymbolData:
def __init__(self, symbol, period):
self._symbol = symbol
self.period = period
# Mean value of returns for magnitude prediction
self.mean_of_price_change = IndicatorExtensions.sma(RateOfChangePercent(1),3)
# Price change from close price the previous day
self.price_change = RateOfChangePercent(3)
def update(self, time, value):
return self.mean_of_price_change.update(time, value) and \
self.price_change.update(time, value)
@property
def insight(self):
direction = InsightDirection.DOWN if self.price_change.current.value > 0 else InsightDirection.UP
margnitude = abs(self.mean_of_price_change.current.value)
return Insight.price(self._symbol, self.period, direction, margnitude, None)
@@ -0,0 +1,109 @@
# 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.
'''
This Alpha Model uses Wells Fargo 30-year Fixed Rate Mortgage data from Quandl to
generate Insights about the movement of Real Estate ETFs. Mortgage rates can provide information
regarding the general price trend of real estate, and ETFs provide good continuous-time instruments
to measure the impact against. Volatility in mortgage rates tends to put downward pressure on real
estate prices, whereas stable mortgage rates, regardless of true rate, lead to stable or higher real
estate prices. This Alpha model seeks to take advantage of this correlation by emitting insights
based on volatility and rate deviation from its historic mean.
This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open
sourced so the community and client funds can see an example of an alpha.
'''
from AlgorithmImports import *
class MortgageRateVolatilityAlpha(QCAlgorithmFramework):
def initialize(self):
# Set requested data resolution
self.set_start_date(2017, 1, 1) #Set Start Date
self.set_cash(100000) #Set Strategy Cash
self.universe_settings.resolution = Resolution.DAILY
## Universe of six liquid real estate ETFs
etfs = ['VNQ', 'REET', 'TAO', 'FREL', 'SRET', 'HIPS']
symbols = [ Symbol.create(etf, SecurityType.EQUITY, Market.USA) for etf in etfs ]
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
self.set_universe_selection(ManualUniverseSelectionModel(symbols) )
self.set_alpha(MortgageRateVolatilityAlphaModel(self))
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
self.set_execution(ImmediateExecutionModel())
self.set_risk_management(NullRiskManagementModel())
class MortgageRateVolatilityAlphaModel(AlphaModel):
def __init__(self, algorithm, indicator_period = 15, insight_magnitude = 0.005, deviations = 2):
## Add Quandl data for a Well's Fargo 30-year Fixed Rate mortgage
self.mortgage_rate = algorithm.add_data(QuandlMortgagePriceColumns, 'WFC/PR_GOV_30YFIXEDVA_APR').symbol
self.indicator_period = indicator_period
self.insight_duration = TimeSpan.from_days(indicator_period)
self.insight_magnitude = insight_magnitude
self.deviations = deviations
## Add indicators for the mortgage rate -- Standard Deviation and Simple Moving Average
self.mortgage_rate_std = algorithm.std(self.mortgage_rate, indicator_period)
self.mortgage_rate_sma = algorithm.sma(self.mortgage_rate, indicator_period)
## Use a history call to warm-up the indicators
self.warmup_indicators(algorithm)
def update(self, algorithm, data):
insights = []
## Return empty list if data slice doesn't contain monrtgage rate data
if self.mortgage_rate not in data.keys():
return []
## Extract current mortgage rate, the current STD indicator value, and current SMA value
mortgage_rate = data[self.mortgage_rate].value
deviation = self.deviations * self.mortgage_rate_std.current.value
sma = self.mortgage_rate_sma.current.value
## If volatility in mortgage rates is high, then we emit an Insight to sell
if (mortgage_rate < sma - deviation) or (mortgage_rate > sma + deviation):
## Emit insights for all securities that are currently in the Universe,
## except for the Quandl Symbol
insights = [Insight(security, self.insight_duration, InsightType.PRICE, InsightDirection.DOWN, self.insight_magnitude, None) \
for security in algorithm.active_securities.keys if security != self.mortgage_rate]
## If volatility in mortgage rates is low, then we emit an Insight to buy
if (mortgage_rate < sma - deviation/2) or (mortgage_rate > sma + deviation/2):
insights = [Insight(security, self.insight_duration, InsightType.PRICE, InsightDirection.UP, self.insight_magnitude, None) \
for security in algorithm.active_securities.keys if security != self.mortgage_rate]
return insights
def warmup_indicators(self, algorithm):
## Make a history call and update the indicators
history = algorithm.history(self.mortgage_rate, self.indicator_period, Resolution.DAILY)
for index, row in history.iterrows():
self.mortgage_rate_std.update(index[1], row['value'])
self.mortgage_rate_sma.update(index[1], row['value'])
class QuandlMortgagePriceColumns(PythonQuandl):
def __init__(self):
## Rename the Quandl object column to the data we want, which is the 'Value' column
## of the CSV that our API call returns
self.value_column_name = "Value"
@@ -0,0 +1,139 @@
# 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 *
class PriceGapMeanReversionAlpha(QCAlgorithm):
'''The motivating idea for this Alpha Model is that a large price gap (here we use true outliers --
price gaps that whose absolutely values are greater than 3 * Volatility) is due to rebound
back to an appropriate price or at least retreat from its brief extreme. Using a Coarse Universe selection
function, the algorithm selects the top x-companies by Dollar Volume (x can be any number you choose)
to trade with, and then uses the Standard Deviation of the 100 most-recent closing prices to determine
which price movements are outliers that warrant emitting insights.
This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open
sourced so the community and client funds can see an example of an alpha.'''
def initialize(self):
self.set_start_date(2018, 1, 1) #Set Start Date
self.set_cash(100000) #Set Strategy Cash
## Initialize variables to be used in controlling frequency of universe selection
self.week = -1
## Manual Universe Selection
self.universe_settings.resolution = Resolution.MINUTE
self.set_universe_selection(CoarseFundamentalUniverseSelectionModel(self.coarse_selection_function))
## Set trading fees to $0
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
## Set custom Alpha Model
self.set_alpha(PriceGapMeanReversionAlphaModel())
## Set equal-weighting Portfolio Construction Model
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
## Set Execution Model
self.set_execution(ImmediateExecutionModel())
## Set Risk Management Model
self.set_risk_management(NullRiskManagementModel())
def coarse_selection_function(self, coarse):
## If it isn't a new week, return the same symbols
current_week = self.time.isocalendar()[1]
if current_week == self.week:
return Universe.UNCHANGED
self.week = current_week
## If its a new week, then re-filter stocks by Dollar Volume
sorted_by_dollar_volume = sorted(coarse, key=lambda x: x.dollar_volume, reverse=True)
return [ x.symbol for x in sorted_by_dollar_volume[:25] ]
class PriceGapMeanReversionAlphaModel(AlphaModel):
def __init__(self, *args, **kwargs):
''' Initialize variables and dictionary for Symbol Data to support algorithm's function '''
self.lookback = 100
self.resolution = kwargs['resolution'] if 'resolution' in kwargs else Resolution.MINUTE
self.prediction_interval = Time.multiply(Extensions.to_time_span(self.resolution), 5) ## Arbitrary
self._symbol_data_by_symbol = {}
def update(self, algorithm, data):
insights = []
## Loop through all Symbol Data objects
for symbol, symbol_data in self._symbol_data_by_symbol.items():
## Evaluate whether or not the price jump is expected to rebound
if not symbol_data.is_trend(data):
continue
## Emit insights accordingly to the price jump sign
direction = InsightDirection.DOWN if symbol_data.price_jump > 0 else InsightDirection.UP
insights.append(Insight.price(symbol, self.prediction_interval, direction, symbol_data.price_jump, None))
return insights
def on_securities_changed(self, algorithm, changes):
# Clean up data for removed securities
for removed in changes.removed_securities:
symbol_data = self._symbol_data_by_symbol.pop(removed.symbol, None)
if symbol_data is not None:
symbol_data.remove_consolidators(algorithm)
symbols = [x.symbol for x in changes.added_securities
if x.symbol not in self._symbol_data_by_symbol]
history = algorithm.history(symbols, self.lookback, self.resolution)
if history.empty: return
## Create and initialize SymbolData objects
for symbol in symbols:
symbol_data = SymbolData(algorithm, symbol, self.lookback, self.resolution)
symbol_data.warm_up_indicators(history.loc[symbol])
self._symbol_data_by_symbol[symbol] = symbol_data
class SymbolData:
def __init__(self, algorithm, symbol, lookback, resolution):
self._symbol = symbol
self.close = 0
self.last_price = 0
self.price_jump = 0
self.consolidator = algorithm.resolve_consolidator(symbol, resolution)
self.volatility = StandardDeviation(f'{symbol}.std({lookback})', lookback)
algorithm.register_indicator(symbol, self.volatility, self.consolidator)
def remove_consolidators(self, algorithm):
algorithm.subscription_manager.remove_consolidator(self._symbol, self.consolidator)
def warm_up_indicators(self, history):
self.close = history.iloc[-1].close
for tuple in history.itertuples():
self.volatility.update(tuple.Index, tuple.close)
def is_trend(self, data):
## Check for any data events that would return a NoneBar in the Alpha Model Update() method
if not data.bars.contains_key(self._symbol):
return False
self.last_price = self.close
self.close = data.bars[self._symbol].close
self.price_jump = (self.close / self.last_price) - 1
return abs(100*self.price_jump) > 3*self.volatility.current.value
@@ -0,0 +1,113 @@
# 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 *
### <summary>
### Alpha Benchmark Strategy capitalizing on ETF rebalancing causing momentum during trending markets.
### </summary>
### <meta name="tag" content="alphastream" />
### <meta name="tag" content="etf" />
### <meta name="tag" content="algorithm framework" />
class RebalancingLeveragedETFAlpha(QCAlgorithm):
''' Alpha Streams: Benchmark Alpha: Leveraged ETF Rebalancing
Strategy by Prof. Shum, reposted by Ernie Chan.
Source: http://epchan.blogspot.com/2012/10/a-leveraged-etfs-strategy.html'''
def initialize(self):
self.set_start_date(2017, 6, 1)
self.set_end_date(2018, 8, 1)
self.set_cash(100000)
underlying = ["SPY","QLD","DIA","IJR","MDY","IWM","QQQ","IYE","EEM","IYW","EFA","GAZB","SLV","IEF","IYM","IYF","IYH","IYR","IYC","IBB","FEZ","USO","TLT"]
ultra_long = ["SSO","UGL","DDM","SAA","MZZ","UWM","QLD","DIG","EET","ROM","EFO","BOIL","AGQ","UST","UYM","UYG","RXL","URE","UCC","BIB","ULE","UCO","UBT"]
ultra_short = ["SDS","GLL","DXD","SDD","MVV","TWM","QID","DUG","EEV","REW","EFU","KOLD","ZSL","PST","SMN","SKF","RXD","SRS","SCC","BIS","EPV","SCO","TBT"]
groups = []
for i in range(len(underlying)):
group = ETFGroup(self.add_equity(underlying[i], Resolution.MINUTE).symbol,
self.add_equity(ultra_long[i], Resolution.MINUTE).symbol,
self.add_equity(ultra_short[i], Resolution.MINUTE).symbol)
groups.append(group)
# Manually curated universe
self.set_universe_selection(ManualUniverseSelectionModel())
# Select the demonstration alpha model
self.set_alpha(RebalancingLeveragedETFAlphaModel(groups))
# Equally weigh securities in portfolio, based on insights
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
# Set Immediate Execution Model
self.set_execution(ImmediateExecutionModel())
# Set Null Risk Management Model
self.set_risk_management(NullRiskManagementModel())
class RebalancingLeveragedETFAlphaModel(AlphaModel):
'''
If the underlying ETF has experienced a return >= 1% since the previous day's close up to the current time at 14:15,
then buy it's ultra ETF right away, and exit at the close. If the return is <= -1%, sell it's ultra-short ETF.
'''
def __init__(self, ETFgroups):
self.etfgroups = ETFgroups
self.date = datetime.min.date
self.name = "RebalancingLeveragedETFAlphaModel"
def update(self, algorithm, data):
'''Scan to see if the returns are greater than 1% at 2.15pm to emit an insight.'''
insights = []
magnitude = 0.0005
# Paper suggests leveraged ETF's rebalance from 2.15pm - to close
# giving an insight period of 105 minutes.
period = timedelta(minutes=105)
# Get yesterday's close price at the market open
if algorithm.time.date() != self.date:
self.date = algorithm.time.date()
# Save yesterday's price and reset the signal
for group in self.etfgroups:
history = algorithm.history([group.underlying], 1, Resolution.DAILY)
group.yesterday_close = None if history.empty else history.loc[str(group.underlying)]['close'][0]
# Check if the returns are > 1% at 14.15
if algorithm.time.hour == 14 and algorithm.time.minute == 15:
for group in self.etfgroups:
if group.yesterday_close == 0 or group.yesterday_close is None: continue
returns = round((algorithm.portfolio[group.underlying].price - group.yesterday_close) / group.yesterday_close, 10)
if returns > 0.01:
insights.append(Insight.price(group.ultra_long, period, InsightDirection.UP, magnitude))
elif returns < -0.01:
insights.append(Insight.price(group.ultra_short, period, InsightDirection.DOWN, magnitude))
return insights
class ETFGroup:
'''
Group the underlying ETF and it's ultra ETFs
Args:
underlying: The underlying index ETF
ultra_long: The long-leveraged version of underlying ETF
ultra_short: The short-leveraged version of the underlying ETF
'''
def __init__(self,underlying, ultra_long, ultra_short):
self.underlying = underlying
self.ultra_long = ultra_long
self.ultra_short = ultra_short
self.yesterday_close = 0
@@ -0,0 +1,152 @@
# 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 *
#
# A number of companies publicly trade two different classes of shares
# in US equity markets. If both assets trade with reasonable volume, then
# the underlying driving forces of each should be similar or the same. Given
# this, we can create a relatively dollar-neutral long/short portfolio using
# the dual share classes. Theoretically, any deviation of this portfolio from
# its mean-value should be corrected, and so the motivating idea is based on
# mean-reversion. Using a Simple Moving Average indicator, we can
# compare the value of this portfolio against its SMA and generate insights
# to buy the under-valued symbol and sell the over-valued symbol.
#
# This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open
# sourced so the community and client funds can see an example of an alpha.
#
class ShareClassMeanReversionAlpha(QCAlgorithm):
def initialize(self):
self.set_start_date(2019, 1, 1) #Set Start Date
self.set_cash(100000) #Set Strategy Cash
self.set_warm_up(20)
## Setup Universe settings and tickers to be used
tickers = ['VIA','VIAB']
self.universe_settings.resolution = Resolution.MINUTE
symbols = [ Symbol.create(ticker, SecurityType.EQUITY, Market.USA) for ticker in tickers]
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0))) ## Set $0 fees to mimic High-Frequency Trading
## Set Manual Universe Selection
self.set_universe_selection( ManualUniverseSelectionModel(symbols) )
## Set Custom Alpha Model
self.set_alpha(ShareClassMeanReversionAlphaModel(tickers = tickers))
## Set Equal Weighting Portfolio Construction Model
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
## Set Immediate Execution Model
self.set_execution(ImmediateExecutionModel())
## Set Null Risk Management Model
self.set_risk_management(NullRiskManagementModel())
class ShareClassMeanReversionAlphaModel(AlphaModel):
''' Initialize helper variables for the algorithm'''
def __init__(self, *args, **kwargs):
self.sma = SimpleMovingAverage(10)
self.position_window = RollingWindow(2)
self.alpha = None
self.beta = None
if 'tickers' not in kwargs:
raise AssertionError('ShareClassMeanReversionAlphaModel: Missing argument: "tickers"')
self.tickers = kwargs['tickers']
self.position_value = None
self.invested = False
self.liquidate = 'liquidate'
self.long_symbol = self.tickers[0]
self.short_symbol = self.tickers[1]
self.resolution = kwargs['resolution'] if 'resolution' in kwargs else Resolution.MINUTE
self.prediction_interval = Time.multiply(Extensions.to_time_span(self.resolution), 5) ## Arbitrary
self.insight_magnitude = 0.001
def update(self, algorithm, data):
insights = []
## Check to see if either ticker will return a NoneBar, and skip the data slice if so
for security in algorithm.securities:
if self.data_event_occured(data, security.key):
return insights
## If Alpha and Beta haven't been calculated yet, then do so
if (self.alpha is None) or (self.beta is None):
self.calculate_alpha_beta(algorithm, data)
algorithm.log('Alpha: ' + str(self.alpha))
algorithm.log('Beta: ' + str(self.beta))
## If the SMA isn't fully warmed up, then perform an update
if not self.sma.is_ready:
self.update_indicators(data)
return insights
## Update indicator and Rolling Window for each data slice passed into Update() method
self.update_indicators(data)
## Check to see if the portfolio is invested. If no, then perform value comparisons and emit insights accordingly
if not self.invested:
if self.position_value >= self.sma.current.value:
insights.append(Insight(self.long_symbol, self.prediction_interval, InsightType.PRICE, InsightDirection.DOWN, self.insight_magnitude, None))
insights.append(Insight(self.short_symbol, self.prediction_interval, InsightType.PRICE, InsightDirection.UP, self.insight_magnitude, None))
## Reset invested boolean
self.invested = True
elif self.position_value < self.sma.current.value:
insights.append(Insight(self.long_symbol, self.prediction_interval, InsightType.PRICE, InsightDirection.UP, self.insight_magnitude, None))
insights.append(Insight(self.short_symbol, self.prediction_interval, InsightType.PRICE, InsightDirection.DOWN, self.insight_magnitude, None))
## Reset invested boolean
self.invested = True
## If the portfolio is invested and crossed back over the SMA, then emit flat insights
elif self.invested and self.crossed_mean():
## Reset invested boolean
self.invested = False
return Insight.group(insights)
def data_event_occured(self, data, symbol):
## Helper function to check to see if data slice will contain a symbol
if data.splits.contains_key(symbol) or \
data.dividends.contains_key(symbol) or \
data.delistings.contains_key(symbol) or \
data.symbol_changed_events.contains_key(symbol):
return True
def update_indicators(self, data):
## Calculate position value and update the SMA indicator and Rolling Window
self.position_value = (self.alpha * data[self.long_symbol].close) - (self.beta * data[self.short_symbol].close)
self.sma.update(data[self.long_symbol].end_time, self.position_value)
self.position_window.add(self.position_value)
def crossed_mean(self):
## Check to see if the position value has crossed the SMA and then return a boolean value
if (self.position_window[0] >= self.sma.current.value) and (self.position_window[1] < self.sma.current.value):
return True
elif (self.position_window[0] < self.sma.current.value) and (self.position_window[1] >= self.sma.current.value):
return True
else:
return False
def calculate_alpha_beta(self, algorithm, data):
## Calculate Alpha and Beta, the initial number of shares for each security needed to achieve a 50/50 weighting
self.alpha = algorithm.calculate_order_quantity(self.long_symbol, 0.5)
self.beta = algorithm.calculate_order_quantity(self.short_symbol, 0.5)
@@ -0,0 +1,99 @@
# 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 *
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel
class SykesShortMicroCapAlpha(QCAlgorithm):
''' Alpha Streams: Benchmark Alpha: Identify "pumped" penny stocks and predict that the price of a "pumped" penny stock reverts to mean
This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open
sourced so the community and client funds can see an example of an alpha.'''
def initialize(self):
self.set_start_date(2018, 1, 1)
self.set_cash(100000)
# Set zero transaction fees
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
# select stocks using PennyStockUniverseSelectionModel
self.universe_settings.resolution = Resolution.DAILY
self.universe_settings.schedule.on(self.date_rules.month_start())
self.set_universe_selection(PennyStockUniverseSelectionModel())
# Use SykesShortMicroCapAlphaModel to establish insights
self.set_alpha(SykesShortMicroCapAlphaModel())
# Equally weigh securities in portfolio, based on insights
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
# Set Immediate Execution Model
self.set_execution(ImmediateExecutionModel())
# Set Null Risk Management Model
self.set_risk_management(NullRiskManagementModel())
class SykesShortMicroCapAlphaModel(AlphaModel):
'''Uses ranking of intraday percentage difference between open price and close price to create magnitude and direction prediction for insights'''
def __init__(self, *args, **kwargs):
lookback = kwargs['lookback'] if 'lookback' in kwargs else 1
resolution = kwargs['resolution'] if 'resolution' in kwargs else Resolution.DAILY
self.prediction_interval = Time.multiply(Extensions.to_time_span(resolution), lookback)
self.number_of_stocks = kwargs['number_of_stocks'] if 'number_of_stocks' in kwargs else 10
def update(self, algorithm, data):
insights = []
symbols_ret = dict()
for security in algorithm.active_securities.values:
if security.has_data:
open_ = security.open
if open_ != 0:
# Intraday price change for penny stocks
symbols_ret[security.symbol] = security.close / open_ - 1
# Rank penny stocks on one day price change and retrieve list of ten "pumped" penny stocks
pumped_stocks = dict(sorted(symbols_ret.items(),
key = lambda kv: (-round(kv[1], 6), kv[0]))[:self.number_of_stocks])
# Emit "down" insight for "pumped" penny stocks
for symbol, value in pumped_stocks.items():
insights.append(Insight.price(symbol, self.prediction_interval, InsightDirection.DOWN, abs(value), None))
return insights
class PennyStockUniverseSelectionModel(FundamentalUniverseSelectionModel):
'''Defines a universe of penny stocks, as a universe selection model for the framework algorithm:
The stocks must have fundamental data
The stock must have positive previous-day close price
The stock must have volume between $1000000 and $10000 on the previous trading day
The stock must cost less than $5'''
def __init__(self):
super().__init__()
# Number of stocks in Coarse Universe
self.number_of_symbols_coarse = 500
def select(self, algorithm, fundamental):
# sort the stocks by dollar volume and take the top 500
top = sorted([x for x in fundamental if x.has_fundamental_data
and 5 > x.price > 0
and 1000000 > x.volume > 10000],
key=lambda x: x.dollar_volume, reverse=True)[:self.number_of_symbols_coarse]
return [x.symbol for x in top]
@@ -0,0 +1,92 @@
# 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 *
#
# In a perfect market, you could buy 100 EUR worth of USD, sell 100 EUR worth of GBP,
# and then use the GBP to buy USD and wind up with the same amount in USD as you received when
# you bought them with EUR. This relationship is expressed by the Triangle Exchange Rate, which is
#
# Triangle Exchange Rate = (A/B) * (B/C) * (C/A)
#
# where (A/B) is the exchange rate of A-to-B. In a perfect market, TER = 1, and so when
# there is a mispricing in the market, then TER will not be 1 and there exists an arbitrage opportunity.
#
# This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open sourced so the community and client funds can see an example of an alpha.
#
class TriangleExchangeRateArbitrageAlpha(QCAlgorithm):
def initialize(self):
self.set_start_date(2019, 2, 1) #Set Start Date
self.set_cash(100000) #Set Strategy Cash
# Set zero transaction fees
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
## Select trio of currencies to trade where
## Currency A = USD
## Currency B = EUR
## Currency C = GBP
currencies = ['EURUSD','EURGBP','GBPUSD']
symbols = [ Symbol.create(currency, SecurityType.FOREX, Market.OANDA) for currency in currencies]
## Manual universe selection with tick-resolution data
self.universe_settings.resolution = Resolution.MINUTE
self.set_universe_selection( ManualUniverseSelectionModel(symbols) )
self.set_alpha(ForexTriangleArbitrageAlphaModel(Resolution.MINUTE, symbols))
## Set Equal Weighting Portfolio Construction Model
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
## Set Immediate Execution Model
self.set_execution(ImmediateExecutionModel())
## Set Null Risk Management Model
self.set_risk_management(NullRiskManagementModel())
class ForexTriangleArbitrageAlphaModel(AlphaModel):
def __init__(self, insight_resolution, symbols):
self.insight_period = Time.multiply(Extensions.to_time_span(insight_resolution), 5)
self._symbols = symbols
def update(self, algorithm, data):
## Check to make sure all currency symbols are present
if len(data.keys()) < 3:
return []
## Extract QuoteBars for all three Forex securities
bar_a = data[self._symbols[0]]
bar_b = data[self._symbols[1]]
bar_c = data[self._symbols[2]]
## Calculate the triangle exchange rate
## Bid(Currency A -> Currency B) * Bid(Currency B -> Currency C) * Bid(Currency C -> Currency A)
## If exchange rates are priced perfectly, then this yield 1. If it is different than 1, then an arbitrage opportunity exists
triangle_rate = bar_a.ask.close / bar_b.bid.close / bar_c.ask.close
## If the triangle rate is significantly different than 1, then emit insights
if triangle_rate > 1.0005:
return Insight.group(
[
Insight.price(self._symbols[0], self.insight_period, InsightDirection.UP, 0.0001, None),
Insight.price(self._symbols[1], self.insight_period, InsightDirection.DOWN, 0.0001, None),
Insight.price(self._symbols[2], self.insight_period, InsightDirection.UP, 0.0001, None)
] )
return []
@@ -0,0 +1,81 @@
# 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 *
#
# Leveraged ETFs (LETF) promise a fixed leverage ratio with respect to an underlying asset or an index.
# A Triple-Leveraged ETF allows speculators to amplify their exposure to the daily returns of an underlying index by a factor of 3.
#
# Increased volatility generally decreases the value of a LETF over an extended period of time as daily compounding is amplified.
#
# This alpha emits short-biased insight to capitalize on volatility decay for each listed pair of TL-ETFs, by rebalancing the
# ETFs with equal weights each day.
#
# This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open sourced so the community and client funds can see an example of an alpha.
#
class TripleLeverageETFPairVolatilityDecayAlpha(QCAlgorithm):
def initialize(self):
self.set_start_date(2018, 1, 1)
self.set_cash(100000)
# Set zero transaction fees
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
# 3X ETF pair tickers
ultra_long = Symbol.create("UGLD", SecurityType.EQUITY, Market.USA)
ultra_short = Symbol.create("DGLD", SecurityType.EQUITY, Market.USA)
# Manually curated universe
self.universe_settings.resolution = Resolution.DAILY
self.set_universe_selection(ManualUniverseSelectionModel([ultra_long, ultra_short]))
# Select the demonstration alpha model
self.set_alpha(RebalancingTripleLeveragedETFAlphaModel(ultra_long, ultra_short))
## Set Equal Weighting Portfolio Construction Model
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
## Set Immediate Execution Model
self.set_execution(ImmediateExecutionModel())
## Set Null Risk Management Model
self.set_risk_management(NullRiskManagementModel())
class RebalancingTripleLeveragedETFAlphaModel(AlphaModel):
'''
Rebalance a pair of 3x leveraged ETFs and predict that the value of both ETFs in each pair will decrease.
'''
def __init__(self, ultra_long, ultra_short):
# Giving an insight period 1 days.
self.period = timedelta(1)
self.magnitude = 0.001
self.ultra_long = ultra_long
self.ultra_short = ultra_short
self.name = "RebalancingTripleLeveragedETFAlphaModel"
def update(self, algorithm, data):
return Insight.group(
[
Insight.price(self.ultra_long, self.period, InsightDirection.DOWN, self.magnitude),
Insight.price(self.ultra_short, self.period, InsightDirection.DOWN, self.magnitude)
] )
@@ -0,0 +1,177 @@
# 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 *
#
# This is a demonstration algorithm. It trades UVXY.
# Dual Thrust alpha model is used to produce insights.
# Those input parameters have been chosen that gave acceptable results on a series
# of random backtests run for the period from Oct, 2016 till Feb, 2019.
#
class VIXDualThrustAlpha(QCAlgorithm):
def initialize(self):
# -- STRATEGY INPUT PARAMETERS --
self.k1 = 0.63
self.k2 = 0.63
self.range_period = 20
self.consolidator_bars = 30
# Settings
self.set_start_date(2018, 10, 1)
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
# Universe Selection
self.universe_settings.resolution = Resolution.MINUTE # it's minute by default, but lets leave this param here
symbols = [Symbol.create("SPY", SecurityType.EQUITY, Market.USA)]
self.set_universe_selection(ManualUniverseSelectionModel(symbols))
# Warming up
resolution_in_time_span = Extensions.to_time_span(self.universe_settings.resolution)
warm_up_time_span = Time.multiply(resolution_in_time_span, self.consolidator_bars)
self.set_warm_up(warm_up_time_span)
# Alpha Model
self.set_alpha(DualThrustAlphaModel(self.k1, self.k2, self.range_period, self.universe_settings.resolution, self.consolidator_bars))
## Portfolio Construction
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
## Execution
self.set_execution(ImmediateExecutionModel())
## Risk Management
self.set_risk_management(MaximumDrawdownPercentPerSecurity(0.03))
class DualThrustAlphaModel(AlphaModel):
'''Alpha model that uses dual-thrust strategy to create insights
https://medium.com/@FMZ_Quant/dual-thrust-trading-strategy-2cc74101a626
or here:
https://www.quantconnect.com/tutorials/strategy-library/dual-thrust-trading-algorithm'''
def __init__(self,
k1,
k2,
range_period,
resolution = Resolution.DAILY,
bars_to_consolidate = 1):
'''Initializes a new instance of the class
Args:
k1: Coefficient for upper band
k2: Coefficient for lower band
range_period: Amount of last bars to calculate the range
resolution: The resolution of data sent into the EMA indicators
bars_to_consolidate: If we want alpha to work on trade bars whose length is different
from the standard resolution - 1m 1h etc. - we need to pass this parameters along
with proper data resolution'''
# coefficient that used to determine upper and lower borders of a breakout channel
self.k1 = k1
self.k2 = k2
# period the range is calculated over
self.range_period = range_period
# initialize with empty dict.
self._symbol_data_by_symbol = dict()
# time for bars we make the calculations on
resolution_in_time_span = Extensions.to_time_span(resolution)
self.consolidator_time_span = Time.multiply(resolution_in_time_span, bars_to_consolidate)
# in 5 days after emission an insight is to be considered expired
self.period = timedelta(5)
def update(self, algorithm, data):
insights = []
for symbol, symbol_data in self._symbol_data_by_symbol.items():
if not symbol_data.is_ready:
continue
holding = algorithm.portfolio[symbol]
price = algorithm.securities[symbol].price
# buying condition
# - (1) price is above upper line
# - (2) and we are not long. this is a first time we crossed the line lately
if price > symbol_data.upper_line and not holding.is_long:
insight_close_time_utc = algorithm.utc_time + self.period
insights.append(Insight.price(symbol, insight_close_time_utc, InsightDirection.UP))
# selling condition
# - (1) price is lower that lower line
# - (2) and we are not short. this is a first time we crossed the line lately
if price < symbol_data.lower_line and not holding.is_short:
insight_close_time_utc = algorithm.utc_time + self.period
insights.append(Insight.price(symbol, insight_close_time_utc, InsightDirection.DOWN))
return insights
def on_securities_changed(self, algorithm, changes):
# added
for symbol in [x.symbol for x in changes.added_securities]:
if symbol not in self._symbol_data_by_symbol:
# add symbol/symbol_data pair to collection
symbol_data = self.SymbolData(symbol, self.k1, self.k2, self.range_period, self.consolidator_time_span)
self._symbol_data_by_symbol[symbol] = symbol_data
# register consolidator
algorithm.subscription_manager.add_consolidator(symbol, symbol_data.get_consolidator())
# removed
for symbol in [x.symbol for x in changes.removed_securities]:
symbol_data = self._symbol_data_by_symbol.pop(symbol, None)
if symbol_data is None:
algorithm.error("Unable to remove data from collection: DualThrustAlphaModel")
else:
# unsubscribe consolidator from data updates
algorithm.subscription_manager.remove_consolidator(symbol, symbol_data.get_consolidator())
class SymbolData:
'''Contains data specific to a symbol required by this model'''
def __init__(self, symbol, k1, k2, range_period, consolidator_resolution):
self.symbol = symbol
self.range_window = RollingWindow(range_period)
self.consolidator = TradeBarConsolidator(consolidator_resolution)
def on_data_consolidated(sender, consolidated):
# add new tradebar to
self.range_window.add(consolidated)
if self.range_window.is_ready:
hh = max([x.high for x in self.range_window])
hc = max([x.close for x in self.range_window])
lc = min([x.close for x in self.range_window])
ll = min([x.low for x in self.range_window])
range = max([hh - lc, hc - ll])
self.upper_line = consolidated.close + k1 * range
self.lower_line = consolidated.close - k2 * range
# event fired at new consolidated trade bar
self.consolidator.data_consolidated += on_data_consolidated
# Returns the interior consolidator
def get_consolidator(self):
return self.consolidator
@property
def is_ready(self):
return self.range_window.is_ready