chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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 firm’s assets
|
||||
σ (sigma) = standard deviation of firm value changes (returns in V)
|
||||
τ (tau) = time to debt’s 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. 90–91.
|
||||
#
|
||||
# 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
|
||||
Reference in New Issue
Block a user