Merge branch 'master' into feature-2950-adds-csharp-alpha-stream-examples

This commit is contained in:
Jared
2019-03-01 17:10:07 -08:00
committed by GitHub
17 changed files with 968 additions and 31 deletions
@@ -0,0 +1,253 @@
# 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 clr import AddReference
AddReference("System")
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Algorithm.Framework")
from System import *
from QuantConnect import *
from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *
from QuantConnect.Algorithm.Framework import *
from QuantConnect.Algorithm.Framework.Risk import *
from QuantConnect.Algorithm.Framework.Alphas import *
from QuantConnect.Algorithm.Framework.Execution import *
from QuantConnect.Algorithm.Framework.Portfolio import *
from QuantConnect.Algorithm.Framework.Selection import *
import numpy as np
from scipy import stats
from scipy.stats import kendalltau
from datetime import timedelta, datetime
class EnergyETFPairsTradingAlgorithm(QCAlgorithmFramework):
def Initialize(self):
self.SetStartDate(2018, 1, 1) #Set Start Date
self.SetCash(100000) #Set Strategy Cash
natural_gas = ['UNG','BOIL','FCG']
crude_oil = ['USO','UCO','DBO']
symbols = [ Symbol.Create(ticker, SecurityType.Equity, Market.USA) for ticker in natural_gas + crude_oil ]
## Set Universe Selection
self.UniverseSettings.Resolution = Resolution.Minute
self.SetUniverseSelection( ManualUniverseSelectionModel(symbols) )
self.SetSecurityInitializer(lambda security: security.SetFeeModel(ConstantFeeModel(0)))
## Custom Alpha Model
self.SetAlpha(PairsAlphaModel(leading = natural_gas, following = crude_oil, history_days = 90, resolution = Resolution.Minute))
## Equal-weight our positions, in this case 100% in USO
self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel(resolution = Resolution.Minute))
## Immediate Execution Fill Model
self.SetExecution(CustomExecutionModel())
## Null Risk-Management Model
self.SetRiskManagement(NullRiskManagementModel())
def OnOrderEvent(self, orderEvent):
if orderEvent.Status == OrderStatus.Filled:
self.Debug("Purchased Stock: {0}".format(orderEvent.Symbol))
def OnEndOfAlgorithm(self):
for kvp in self.Portfolio:
if self.Portfolio[kvp.Key].Invested:
self.Log('Invested in: ' + str(kvp.Key))
class PairsAlphaModel:
'''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.difference_trigger = kwargs['difference_trigger'] if 'difference_trigger' in kwargs else 0.75
self.lookback = kwargs['lookback'] if 'lookback' in kwargs else 5
self.history_days = kwargs['history_days'] if 'history_days' in kwargs else 90 ## In days
self.resolution = kwargs['resolution'] if 'resolution' in kwargs else Resolution.Hour
self.prediction_interval = Time.Multiply(Extensions.ToTimeSpan(self.resolution), 5) ## Arbitrary
self.symbolDataBySymbol = {}
self.next_update = None
self.leading = kwargs['leading'] if 'leading' in kwargs else None
self.following = kwargs['following'] if 'following' in kwargs else None
self.tickers = self.leading + self.following
self.ticker_list_of_lists = [[],[]]
for i in range(len(self.leading)):
for j in range(len(self.following)):
self.ticker_list_of_lists[0].append(self.leading[i])
self.ticker_list_of_lists[1] = self.following * 3
def Update(self, algorithm, data):
if (self.next_update is None) or (algorithm.Time > self.next_update):
self.pairs = self.CorrelationPairsSelection(algorithm)
self.next_update = algorithm.Time + (timedelta(days = 30))
## Build a list to hold our insights
insights = []
## These lists hold the Symbol for the following ETF and data from the leading ETF that we need to pass into the Insight() constructor
leading_data = []
following_symbol = []
for symbol, symbolData in self.symbolDataBySymbol.items():
if symbol.Value == self.pairs[0]:
leading_data.append(symbolData) ## Add symbol data if it's Natural Gas
elif symbol.Value == self.pairs[1]:
following_symbol.append(symbol) ## Stash the Symbol object if it's Crude Oil
for i in range(len(following_symbol)):
symbolData = leading_data[i]
if symbolData.Return > self.difference_trigger: ## Check if Natural Gas returns are greater than the threshold we've set
## If so, create and Insight with this information for Crude Oil
insights.append(Insight(following_symbol[i], self.prediction_interval, InsightType.Price, InsightDirection.Up, symbolData.Return/100, None))
elif symbolData.Return < -self.difference_trigger: ## Check if UNG returns are greater than the threshold we've set
insights.append(Insight(following_symbol[i], self.prediction_interval, InsightType.Price, InsightDirection.Down, symbolData.Return/100, None))
return insights
def CorrelationPairsSelection(self, algorithm):
tick_syl = self.ticker_list_of_lists
tickers = self.tickers
logreturn={}
## Get log returns for each natural gas/oil ETF pair
unique = list(set([item for sublist in self.ticker_list_of_lists for item in sublist]))
df = algorithm.History(unique, self.history_days, Resolution.Daily)
df = df['close'].unstack(level=0)
df = (np.log(df) - np.log(df.shift(1))).dropna()
for tick in tickers:
logreturn[tick] = df[[tick]]
## Estimate coefficients of different correlation measures
tau_coef = []
for i in range(len(tick_syl[0])):
tik_x, tik_y= logreturn[tick_syl[0][i]], logreturn[tick_syl[1][i]]
min_length = min(len(tik_x), len(tik_y))
tau_coef.append(kendalltau(tik_x[:min_length], tik_y[:min_length])[0])
index_max = tau_coef.index(max(tau_coef))
pair = [tick_syl[0][index_max],tick_syl[1][index_max]]
## Return the pair with highest historical correlation
return pair
def OnSecuritiesChanged(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.RemovedSecurities:
algorithm.Log('Removed: ' + str(removed.Symbol))
symbolData = self.symbolDataBySymbol.pop(removed.Symbol, None)
if symbolData is not None:
symbolData.RemoveConsolidators(algorithm)
# initialize data for added securities
symbols = [ x.Symbol for x in changes.AddedSecurities ]
history = algorithm.History(symbols, self.lookback, self.resolution)
if history.empty: return
tickers = history.index.levels[0]
for ticker in tickers:
symbol = SymbolCache.GetSymbol(ticker)
if symbol not in self.symbolDataBySymbol:
symbolData = SymbolData(symbol, self.lookback, self.resolution, algorithm)
self.symbolDataBySymbol[symbol] = symbolData
symbolData.WarmUpIndicators(history.loc[ticker])
class SymbolData:
'''Contains data specific to a symbol required by this model'''
def __init__(self, symbol, lookback, resolution, algorithm):
self.Symbol = symbol
self.ROCP = RateOfChangePercent('{}.ROCP({})'.format(symbol, lookback), lookback)
self.Consolidator = algorithm.ResolveConsolidator(self.Symbol, resolution)
algorithm.RegisterIndicator(symbol, self.ROCP, self.Consolidator)
self.previous = 0
def RemoveConsolidators(self, algorithm):
if self.Consolidator is not None:
algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.Consolidator)
def WarmUpIndicators(self, history):
for tuple in history.itertuples():
self.ROCP.Update(tuple.Index, tuple.close)
@property
def Return(self):
return float(self.ROCP.Current.Value)
@property
def CanEmit(self):
if self.previous == self.ROCP.Samples:
return False
self.previous = self.ROCP.Samples
return self.ROCP.IsReady
def __str__(self, **kwargs):
return '{}: {:.2%}'.format(self.ROCP.Name, (1 + self.Return)**252 - 1)
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.targetsCollection = 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.targetsCollection.AddRange(targets)
for target in self.targetsCollection.OrderByMarginImpact(algorithm):
open_quantity = sum([x.Quantity for x in algorithm.Transactions.GetOpenOrders(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.MarketOrder(target.Symbol, quantity)
self.previous_symbol = target.Symbol
self.targetsCollection.ClearFulfilled(algorithm)
@@ -0,0 +1,254 @@
# 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 clr import AddReference
AddReference("System")
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Algorithm.Framework")
from System import *
from QuantConnect import *
from QuantConnect.Orders.Fees import ConstantFeeModel
from QuantConnect.Data.UniverseSelection import *
from QuantConnect.Indicators import *
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel
from datetime import timedelta, datetime
from math import ceil
from itertools import chain
#
# 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.
#
class GreenblattMagicFormulaAlgorithm(QCAlgorithmFramework):
''' Alpha Streams: Benchmark Alpha: Pick stocks according to Joel Greenblatt's Magic Formula'''
def Initialize(self):
self.SetStartDate(2018, 1, 1)
self.SetCash(100000)
#Set zero transaction fees
self.SetSecurityInitializer(lambda security: security.SetFeeModel(ConstantFeeModel(0)))
# select stocks using MagicFormulaUniverseSelectionModel
self.SetUniverseSelection(GreenBlattMagicFormulaUniverseSelectionModel())
# Use MagicFormulaAlphaModel to establish insights
self.SetAlpha(RateOfChangeAlphaModel())
# Equally weigh securities in portfolio, based on insights
self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
## Set Immediate Execution Model
self.SetExecution(ImmediateExecutionModel())
## Set Null Risk Management Model
self.SetRiskManagement(NullRiskManagementModel())
class RateOfChangeAlphaModel(AlphaModel):
'''Uses Rate of Change (ROC) to create magnitude prediction for insights.'''
def __init__(self, *args, **kwargs):
self.lookback = kwargs['lookback'] if 'lookback' in kwargs else 1
self.resolution = kwargs['resolution'] if 'resolution' in kwargs else Resolution.Daily
self.predictionInterval = Time.Multiply(Extensions.ToTimeSpan(self.resolution), self.lookback)
self.symbolDataBySymbol = {}
def Update(self, algorithm, data):
insights = []
for symbol, symbolData in self.symbolDataBySymbol.items():
if symbolData.CanEmit:
insights.append(Insight.Price(symbol, self.predictionInterval, InsightDirection.Up, symbolData.Return, None))
return insights
def OnSecuritiesChanged(self, algorithm, changes):
# clean up data for removed securities
for removed in changes.RemovedSecurities:
symbolData = self.symbolDataBySymbol.pop(removed.Symbol, None)
if symbolData is not None:
symbolData.RemoveConsolidators(algorithm)
# initialize data for added securities
symbols = [ x.Symbol for x in changes.AddedSecurities ]
history = algorithm.History(symbols, self.lookback, self.resolution)
if history.empty: return
tickers = history.index.levels[0]
for ticker in tickers:
symbol = SymbolCache.GetSymbol(ticker)
if symbol not in self.symbolDataBySymbol:
symbolData = SymbolData(symbol, self.lookback)
self.symbolDataBySymbol[symbol] = symbolData
symbolData.RegisterIndicators(algorithm, self.resolution)
symbolData.WarmUpIndicators(history.loc[ticker])
class SymbolData:
'''Contains data specific to a symbol required by this model'''
def __init__(self, symbol, lookback):
self.Symbol = symbol
self.ROC = RateOfChange('{}.ROC({})'.format(symbol, lookback), lookback)
self.Consolidator = None
self.previous = 0
def RegisterIndicators(self, algorithm, resolution):
self.Consolidator = algorithm.ResolveConsolidator(self.Symbol, resolution)
algorithm.RegisterIndicator(self.Symbol, self.ROC, self.Consolidator)
def RemoveConsolidators(self, algorithm):
if self.Consolidator is not None:
algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.Consolidator)
def WarmUpIndicators(self, history):
for tuple in history.itertuples():
self.ROC.Update(tuple.Index, tuple.close)
@property
def Return(self):
return float(self.ROC.Current.Value)
@property
def CanEmit(self):
if self.previous == self.ROC.Samples:
return False
self.previous = self.ROC.Samples
return self.ROC.IsReady
def __str__(self, **kwargs):
return '{}: {:.2%}'.format(self.ROC.Name, (1 + self.Return)**252 - 1)
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,
filterFineData = True,
universeSettings = None,
securityInitializer = None):
'''Initializes a new default instance of the MagicFormulaUniverseSelectionModel'''
super().__init__(filterFineData, universeSettings, securityInitializer)
# Number of stocks in Coarse Universe
self.NumberOfSymbolsCoarse = 500
# Number of sorted stocks in the fine selection subset using the valuation ratio, EV to EBITDA (EV/EBITDA)
self.NumberOfSymbolsFine = 20
# Final number of stocks in security list, after sorted by the valuation ratio, Return on Assets (ROA)
self.NumberOfSymbolsInPortfolio = 10
self.lastMonth = -1
self.dollarVolumeBySymbol = {}
self.symbols = []
def SelectCoarse(self, algorithm, coarse):
'''Performs coarse selection for constituents.
The stocks must have fundamental data
The stock must have positive previous-day close price
The stock must have positive volume on the previous trading day'''
month = algorithm.Time.month
if month == self.lastMonth:
return self.symbols
self.lastMonth = month
# The stocks must have fundamental data
# The stock must have positive previous-day close price
# The stock must have positive volume on the previous trading day
filtered = [x for x in coarse if x.HasFundamentalData
and x.Volume > 0
and x.Price > 0]
# sort the stocks by dollar volume and take the top 1000
top = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)[:self.NumberOfSymbolsCoarse]
self.dollarVolumeBySymbol = { i.Symbol: i.DollarVolume for i in top }
self.symbols = list(self.dollarVolumeBySymbol.keys())
return self.symbols
def SelectFine(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
filteredFine = [x for x in fine if x.CompanyReference.CountryId == "USA"
and (x.CompanyReference.PrimaryExchangeID == "NYS" or x.CompanyReference.PrimaryExchangeID == "NAS")
and (algorithm.Time - x.SecurityReference.IPODate).days > 180
and x.EarningReports.BasicAverageShares.ThreeMonths * x.EarningReports.BasicEPS.TwelveMonths * x.ValuationRatios.PERatio > 5e8]
count = len(filteredFine)
if count == 0: return []
myDict = dict()
percent = float(self.NumberOfSymbolsFine / 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 filteredFine if x.CompanyReference.IndustryTemplateCode == key]
value = sorted(value, key=lambda x: self.dollarVolumeBySymbol[x.Symbol], reverse = True)
myDict[key] = value[:ceil(len(value) * percent)]
# stocks in QC500 universe
topFine = list(chain.from_iterable(myDict.values()))[:self.NumberOfSymbolsCoarse]
# 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
sortedByEVToEBITDA = sorted(topFine, key=lambda x: x.ValuationRatios.EVToEBITDA , reverse=True)
# sort subset of stocks that have been sorted by Enterprise Value to EBITDA, based on the valuation ratio Return on Assets (ROA)
sortedByROA = sorted(sortedByEVToEBITDA[:self.NumberOfSymbolsFine], key=lambda x: x.ValuationRatios.ForwardROA, reverse=False)
# retrieve list of securites in portfolio
top = sortedByROA[:self.NumberOfSymbolsInPortfolio]
self.symbols = [f.Symbol for f in top]
return self.symbols
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@@ -37,8 +37,10 @@
</PropertyGroup>
<ItemGroup>
<Content Include="Alphas\ForexCalendarAlpha.py" />
<Content Include="Alphas\GasAndCrudeOilEnergyCorrelationAlpha.py" />
<Content Include="Alphas\GlobalEquityMeanReversionIBSAlpha.py" />
<Content Include="Alphas\IntradayReversalCurrencyMarketsAlpha.py" />
<Content Include="Alphas\GreenblattMagicFormulaAlgorithm.py" />
<Content Include="Alphas\MeanReversionLunchBreakAlpha.py" />
<Content Include="Alphas\SykesShortMicroCapAlpha.py" />
<Content Include="Alphas\RebalancingLeveragedETFAlpha.py" />