chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:02:50 +08:00
commit 0fc60fdcb1
5008 changed files with 910633 additions and 0 deletions
@@ -0,0 +1,286 @@
/*
* 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.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Indicators;
using QuantConnect.Securities;
using static System.FormattableString;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// This alpha model is designed to accept every possible pair combination
/// from securities selected by the universe selection model
/// This model generates alternating long ratio/short ratio insights emitted as a group
/// </summary>
public class BasePairsTradingAlphaModel : AlphaModel
{
private readonly int _lookback;
private readonly Resolution _resolution;
private readonly TimeSpan _predictionInterval;
private readonly decimal _threshold;
private readonly Dictionary<Tuple<Symbol, Symbol>, PairData> _pairs;
/// <summary>
/// List of security objects present in the universe
/// </summary>
public HashSet<Security> Securities { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BasePairsTradingAlphaModel"/> class
/// </summary>
/// <param name="lookback">Lookback period of the analysis</param>
/// <param name="resolution">Analysis resolution</param>
/// <param name="threshold">The percent [0, 100] deviation of the ratio from the mean before emitting an insight</param>
public BasePairsTradingAlphaModel(
int lookback = 1,
Resolution resolution = Resolution.Daily,
decimal threshold = 1m
)
{
_lookback = lookback;
_resolution = resolution;
_threshold = threshold;
_predictionInterval = _resolution.ToTimeSpan().Multiply(_lookback);
_pairs = new Dictionary<Tuple<Symbol, Symbol>, PairData>();
Securities = new HashSet<Security>();
Name = Invariant($"{nameof(BasePairsTradingAlphaModel)}({_lookback},{_resolution},{_threshold.Normalize()})");
}
/// <summary>
/// Updates this alpha model with the latest data from the algorithm.
/// This is called each time the algorithm receives data for subscribed securities
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="data">The new data available</param>
/// <returns>The new insights generated</returns>
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
var insights = new List<Insight>();
foreach (var kvp in _pairs)
{
insights.AddRange(kvp.Value.GetInsightGroup());
}
return insights;
}
/// <summary>
/// Event fired each time the we add/remove securities from the data feed
/// </summary>
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
/// <param name="changes">The security additions and removals from the algorithm</param>
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
NotifiedSecurityChanges.UpdateCollection(Securities, changes);
UpdatePairs(algorithm);
// Remove pairs that has assets that were removed from the universe
foreach (var security in changes.RemovedSecurities)
{
var symbol = security.Symbol;
var keys = _pairs.Keys.Where(k => k.Item1 == symbol || k.Item2 == symbol).ToList();
foreach (var key in keys)
{
var pair = _pairs[key];
pair.Dispose();
_pairs.Remove(key);
}
}
}
/// <summary>
/// Check whether the assets pass a pairs trading test
/// </summary>
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
/// <param name="asset1">The first asset's symbol in the pair</param>
/// <param name="asset2">The second asset's symbol in the pair</param>
/// <returns>True if the statistical test for the pair is successful</returns>
public virtual bool HasPassedTest(QCAlgorithm algorithm, Symbol asset1, Symbol asset2)
{
// Check if this method was overridden in Python
if (TryInvokePythonOverride(nameof(HasPassedTest), out bool result, algorithm, asset1, asset2))
{
return result;
}
return true;
}
private void UpdatePairs(QCAlgorithm algorithm)
{
var assets = Securities.Select(x => x.Symbol).ToArray();
for (var i = 0; i < assets.Length; i++)
{
var assetI = assets[i];
for (var j = i + 1; j < assets.Length; j++)
{
var assetJ = assets[j];
var pairSymbol = Tuple.Create(assetI, assetJ);
var invert = Tuple.Create(assetJ, assetI);
if (_pairs.ContainsKey(pairSymbol) || _pairs.ContainsKey(invert))
{
continue;
}
if (!HasPassedTest(algorithm, assetI, assetJ))
{
continue;
}
var pairData = new PairData(algorithm, assetI, assetJ, _predictionInterval, _threshold);
_pairs.Add(pairSymbol, pairData);
}
}
}
private class PairData : IDisposable
{
private enum State
{
ShortRatio,
FlatRatio,
LongRatio
};
private State _state = State.FlatRatio;
private readonly QCAlgorithm _algorithm;
private readonly Symbol _asset1;
private readonly Symbol _asset2;
private readonly IDataConsolidator _identityConsolidator1;
private readonly IDataConsolidator _identityConsolidator2;
private readonly IndicatorBase<IndicatorDataPoint> _asset1Price;
private readonly IndicatorBase<IndicatorDataPoint> _asset2Price;
private readonly IndicatorBase<IndicatorDataPoint> _ratio;
private readonly IndicatorBase<IndicatorDataPoint> _mean;
private readonly IndicatorBase<IndicatorDataPoint> _upperThreshold;
private readonly IndicatorBase<IndicatorDataPoint> _lowerThreshold;
private readonly TimeSpan _predictionInterval;
/// <summary>
/// Create a new pair
/// </summary>
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
/// <param name="asset1">The first asset's symbol in the pair</param>
/// <param name="asset2">The second asset's symbol in the pair</param>
/// <param name="period">Period over which this insight is expected to come to fruition</param>
/// <param name="threshold">The percent [0, 100] deviation of the ratio from the mean before emitting an insight</param>
public PairData(QCAlgorithm algorithm, Symbol asset1, Symbol asset2, TimeSpan period, decimal threshold)
{
_algorithm = algorithm;
_asset1 = asset1;
_asset2 = asset2;
// Created the Identity indicator for a given Symbol and
// the consolidator it is registered to. The consolidator reference
// will be used to remove it from SubscriptionManager
(Identity, IDataConsolidator) CreateIdentityIndicator(Symbol symbol)
{
var resolution = algorithm.SubscriptionManager
.SubscriptionDataConfigService
.GetSubscriptionDataConfigs(symbol)
.Min(x => x.Resolution);
var name = algorithm.CreateIndicatorName(symbol, "close", resolution);
var identity = new Identity(name);
var consolidator = algorithm.ResolveConsolidator(symbol, resolution);
algorithm.RegisterIndicator(symbol, identity, consolidator);
return (identity, consolidator);
}
(_asset1Price, _identityConsolidator1) = CreateIdentityIndicator(asset1);
(_asset2Price, _identityConsolidator2) = CreateIdentityIndicator(asset2);
_ratio = _asset1Price.Over(_asset2Price);
_mean = new ExponentialMovingAverage(500).Of(_ratio);
var upper = new ConstantIndicator<IndicatorDataPoint>("ct", 1 + threshold / 100m);
_upperThreshold = _mean.Times(upper, "UpperThreshold");
var lower = new ConstantIndicator<IndicatorDataPoint>("ct", 1 - threshold / 100m);
_lowerThreshold = _mean.Times(lower, "LowerThreshold");
_predictionInterval = period;
}
/// <summary>
/// On disposal, remove the consolidators from the subscription manager
/// </summary>
public void Dispose()
{
_algorithm.SubscriptionManager.RemoveConsolidator(_asset1, _identityConsolidator1);
_algorithm.SubscriptionManager.RemoveConsolidator(_asset2, _identityConsolidator2);
}
/// <summary>
/// Gets the insights group for the pair
/// </summary>
/// <returns>Insights grouped by an unique group id</returns>
public IEnumerable<Insight> GetInsightGroup()
{
if (!_mean.IsReady)
{
return Enumerable.Empty<Insight>();
}
// don't re-emit the same direction
if (_state != State.LongRatio && _ratio > _upperThreshold)
{
_state = State.LongRatio;
// asset1/asset2 is more than 2 std away from mean, short asset1, long asset2
var shortAsset1 = Insight.Price(_asset1, _predictionInterval, InsightDirection.Down);
var longAsset2 = Insight.Price(_asset2, _predictionInterval, InsightDirection.Up);
// creates a group id and set the GroupId property on each insight object
return Insight.Group(shortAsset1, longAsset2);
}
// don't re-emit the same direction
if (_state != State.ShortRatio && _ratio < _lowerThreshold)
{
_state = State.ShortRatio;
// asset1/asset2 is less than 2 std away from mean, long asset1, short asset2
var longAsset1 = Insight.Price(_asset1, _predictionInterval, InsightDirection.Up);
var shortAsset2 = Insight.Price(_asset2, _predictionInterval, InsightDirection.Down);
// creates a group id and set the GroupId property on each insight object
return Insight.Group(longAsset1, shortAsset2);
}
return Enumerable.Empty<Insight>();
}
}
}
}
@@ -0,0 +1,194 @@
# 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 enum import Enum
class BasePairsTradingAlphaModel(AlphaModel):
'''This alpha model is designed to accept every possible pair combination
from securities selected by the universe selection model
This model generates alternating long ratio/short ratio insights emitted as a group'''
def __init__(self, lookback = 1,
resolution = Resolution.DAILY,
threshold = 1):
''' Initializes a new instance of the PairsTradingAlphaModel class
Args:
lookback: Lookback period of the analysis
resolution: Analysis resolution
threshold: The percent [0, 100] deviation of the ratio from the mean before emitting an insight'''
self.lookback = lookback
self.resolution = resolution
self.threshold = threshold
self.prediction_interval = Time.multiply(Extensions.to_time_span(self.resolution), self.lookback)
self.pairs = dict()
self.securities = set()
self.name = f'{self.__class__.__name__}({self.lookback},{resolution},{Extensions.normalize_to_str(threshold)})'
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'''
insights = []
for key, pair in self.pairs.items():
insights.extend(pair.get_insight_group())
return insights
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 security in changes.added_securities:
self.securities.add(security)
for security in changes.removed_securities:
if security in self.securities:
self.securities.remove(security)
self.update_pairs(algorithm)
for security in changes.removed_securities:
keys = [k for k in self.pairs.keys() if security.symbol in k]
for key in keys:
self.pairs.pop(key).dispose()
def update_pairs(self, algorithm):
symbols = sorted([x.symbol for x in self.securities])
for i in range(0, len(symbols)):
asset_i = symbols[i]
for j in range(1 + i, len(symbols)):
asset_j = symbols[j]
pair_symbol = (asset_i, asset_j)
invert = (asset_j, asset_i)
if pair_symbol in self.pairs or invert in self.pairs:
continue
if not self.has_passed_test(algorithm, asset_i, asset_j):
continue
pair = self.Pair(algorithm, asset_i, asset_j, self.prediction_interval, self.threshold)
self.pairs[pair_symbol] = pair
def has_passed_test(self, algorithm, asset1, asset2):
'''Check whether the assets pass a pairs trading test
Args:
algorithm: The algorithm instance that experienced the change in securities
asset1: The first asset's symbol in the pair
asset2: The second asset's symbol in the pair
Returns:
True if the statistical test for the pair is successful'''
return True
class Pair:
class State(Enum):
SHORT_RATIO = -1
FLAT_RATIO = 0
LONG_RATIO = 1
def __init__(self, algorithm, asset1, asset2, prediction_interval, threshold):
'''Create a new pair
Args:
algorithm: The algorithm instance that experienced the change in securities
asset1: The first asset's symbol in the pair
asset2: The second asset's symbol in the pair
prediction_interval: Period over which this insight is expected to come to fruition
threshold: The percent [0, 100] deviation of the ratio from the mean before emitting an insight'''
self.state = self.State.FLAT_RATIO
self.algorithm = algorithm
self.asset1 = asset1
self.asset2 = asset2
# Created the Identity indicator for a given Symbol and
# the consolidator it is registered to. The consolidator reference
# will be used to remove it from SubscriptionManager
def create_identity_indicator(symbol: Symbol):
resolution = min([x.resolution for x in algorithm.subscription_manager.subscription_data_config_service.get_subscription_data_configs(symbol)])
name = algorithm.create_indicator_name(symbol, "close", resolution)
identity = Identity(name)
consolidator = algorithm.resolve_consolidator(symbol, resolution)
algorithm.register_indicator(symbol, identity, consolidator)
return identity, consolidator
self.asset1_price, self.identity_consolidator1 = create_identity_indicator(asset1);
self.asset2_price, self.identity_consolidator2 = create_identity_indicator(asset2);
self.ratio = IndicatorExtensions.over(self.asset1_price, self.asset2_price)
self.mean = IndicatorExtensions.of(ExponentialMovingAverage(500), self.ratio)
upper = ConstantIndicator[IndicatorDataPoint]("ct", 1 + threshold / 100)
self.upper_threshold = IndicatorExtensions.times(self.mean, upper)
lower = ConstantIndicator[IndicatorDataPoint]("ct", 1 - threshold / 100)
self.lower_threshold = IndicatorExtensions.times(self.mean, lower)
self.prediction_interval = prediction_interval
def dispose(self):
'''
On disposal, remove the consolidators from the subscription manager
'''
self.algorithm.subscription_manager.remove_consolidator(self.asset1, self.identity_consolidator1)
self.algorithm.subscription_manager.remove_consolidator(self.asset2, self.identity_consolidator2)
def get_insight_group(self):
'''Gets the insights group for the pair
Returns:
Insights grouped by an unique group id'''
if not self.mean.is_ready:
return []
# don't re-emit the same direction
if self.state is not self.State.LONG_RATIO and self.ratio > self.upper_threshold:
self.state = self.State.LONG_RATIO
# asset1/asset2 is more than 2 std away from mean, short asset1, long asset2
short_asset_1 = Insight.price(self.asset1, self.prediction_interval, InsightDirection.DOWN)
long_asset_2 = Insight.price(self.asset2, self.prediction_interval, InsightDirection.UP)
# creates a group id and set the GroupId property on each insight object
return Insight.group(short_asset_1, long_asset_2)
# don't re-emit the same direction
if self.state is not self.State.SHORT_RATIO and self.ratio < self.lower_threshold:
self.state = self.State.SHORT_RATIO
# asset1/asset2 is less than 2 std away from mean, long asset1, short asset2
long_asset_1 = Insight.price(self.asset1, self.prediction_interval, InsightDirection.UP)
short_asset_2 = Insight.price(self.asset2, self.prediction_interval, InsightDirection.DOWN)
# creates a group id and set the GroupId property on each insight object
return Insight.group(long_asset_1, short_asset_2)
return []
@@ -0,0 +1,141 @@
/*
* 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.
*/
using System;
using System.Collections.Generic;
using QuantConnect.Data;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities;
using static System.FormattableString;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Provides an implementation of <see cref="IAlphaModel"/> that always returns the same insight for each security
/// </summary>
public class ConstantAlphaModel : AlphaModel
{
private readonly InsightType _type;
private readonly InsightDirection _direction;
private readonly TimeSpan _period;
private readonly double? _magnitude;
private readonly double? _confidence;
private readonly double? _weight;
private readonly HashSet<Security> _securities;
private readonly Dictionary<Symbol, DateTime> _insightsTimeBySymbol;
/// <summary>
/// Initializes a new instance of the <see cref="ConstantAlphaModel"/> class
/// </summary>
/// <param name="type">The type of insight</param>
/// <param name="direction">The direction of the insight</param>
/// <param name="period">The period over which the insight with come to fruition</param>
/// <param name="magnitude">The predicted change in magnitude as a +- percentage</param>
/// <param name="confidence">The confidence in the insight</param>
/// <param name="weight">The portfolio weight of the insights</param>
public ConstantAlphaModel(InsightType type, InsightDirection direction, TimeSpan period, double? magnitude = null, double? confidence = null, double? weight = null)
{
_type = type;
_direction = direction;
_period = period;
// Optional
_magnitude = magnitude;
_confidence = confidence;
_weight = weight;
_securities = new HashSet<Security>();
_insightsTimeBySymbol = new Dictionary<Symbol, DateTime>();
Name = $"{nameof(ConstantAlphaModel)}({type},{direction},{period}";
if (magnitude.HasValue)
{
Name += Invariant($",{magnitude.Value}");
}
if (confidence.HasValue)
{
Name += Invariant($",{confidence.Value}");
}
Name += ")";
}
/// <summary>
/// Creates a constant insight for each security as specified via the constructor
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="data">The new data available</param>
/// <returns>The new insights generated</returns>
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
foreach (var security in _securities)
{
// security price could be zero until we get the first data point. e.g. this could happen
// when adding both forex and equities, we will first get a forex data point
if (security.Price != 0
&& ShouldEmitInsight(algorithm.UtcTime, security.Symbol))
{
yield return new Insight(security.Symbol, _period, _type, _direction, _magnitude, _confidence, weight: _weight);
}
}
}
/// <summary>
/// Event fired each time the we add/remove securities from the data feed
/// </summary>
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
/// <param name="changes">The security additions and removals from the algorithm</param>
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
NotifiedSecurityChanges.UpdateCollection(_securities, changes);
// this will allow the insight to be re-sent when the security re-joins the universe
foreach (var removed in changes.RemovedSecurities)
{
_insightsTimeBySymbol.Remove(removed.Symbol);
}
}
/// <summary>
/// Determine if its time to emit insight for this symbol
/// </summary>
/// <param name="utcTime">Time of the insight</param>
/// <param name="symbol">The symbol to emit an insight for</param>
protected virtual bool ShouldEmitInsight(DateTime utcTime, Symbol symbol)
{
if (symbol.IsCanonical())
{
// canonical futures & options are none tradable
return false;
}
DateTime generatedTimeUtc;
if (_insightsTimeBySymbol.TryGetValue(symbol, out generatedTimeUtc))
{
// we previously emitted a insight for this symbol, check it's period to see
// if we should emit another insight
if (utcTime - generatedTimeUtc < _period)
{
return false;
}
}
// we either haven't emitted a insight for this symbol or the previous
// insight's period has expired, so emit a new insight now for this symbol
_insightsTimeBySymbol[symbol] = utcTime;
return true;
}
}
}
@@ -0,0 +1,102 @@
# 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 ConstantAlphaModel(AlphaModel):
''' Provides an implementation of IAlphaModel that always returns the same insight for each security'''
def __init__(self, type, direction, period, magnitude = None, confidence = None, weight = None):
'''Initializes a new instance of the ConstantAlphaModel class
Args:
type: The type of insight
direction: The direction of the insight
period: The period over which the insight with come to fruition
magnitude: The predicted change in magnitude as a +- percentage
confidence: The confidence in the insight
weight: The portfolio weight of the insights'''
self.type = type
self.direction = direction
self.period = period
self.magnitude = magnitude
self.confidence = confidence
self.weight = weight
self.securities = []
self.insights_time_by_symbol = {}
self.Name = '{}({},{},{}'.format(self.__class__.__name__, type, direction, strfdelta(period))
if magnitude is not None:
self.Name += ',{}'.format(magnitude)
if confidence is not None:
self.Name += ',{}'.format(confidence)
self.Name += ')'
def update(self, algorithm, data):
''' Creates a constant insight for each security as specified via the constructor
Args:
algorithm: The algorithm instance
data: The new data available
Returns:
The new insights generated'''
insights = []
for security in self.securities:
# security price could be zero until we get the first data point. e.g. this could happen
# when adding both forex and equities, we will first get a forex data point
if security.price != 0 and self.should_emit_insight(algorithm.utc_time, security.symbol):
insights.append(Insight(security.symbol, self.period, self.type, self.direction, self.magnitude, self.confidence, weight = self.weight))
return insights
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 added in changes.added_securities:
self.securities.append(added)
# this will allow the insight to be re-sent when the security re-joins the universe
for removed in changes.removed_securities:
if removed in self.securities:
self.securities.remove(removed)
if removed.symbol in self.insights_time_by_symbol:
self.insights_time_by_symbol.pop(removed.symbol)
def should_emit_insight(self, utc_time, symbol):
if symbol.is_canonical():
# canonical futures & options are none tradable
return False
generated_time_utc = self.insights_time_by_symbol.get(symbol)
if generated_time_utc is not None:
# we previously emitted a insight for this symbol, check it's period to see
# if we should emit another insight
if utc_time - generated_time_utc < self.period:
return False
# we either haven't emitted a insight for this symbol or the previous
# insight's period has expired, so emit a new insight now for this symbol
self.insights_time_by_symbol[symbol] = utc_time
return True
def strfdelta(tdelta):
d = tdelta.days
h, rem = divmod(tdelta.seconds, 3600)
m, s = divmod(rem, 60)
return "{}.{:02d}:{:02d}:{:02d}".format(d,h,m,s)
@@ -0,0 +1,209 @@
/*
* 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.
*/
using System.Collections.Generic;
using QuantConnect.Data;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Data.Consolidators;
using QuantConnect.Indicators;
using QuantConnect.Securities;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Alpha model that uses an EMA cross to create insights
/// </summary>
public class EmaCrossAlphaModel : AlphaModel
{
private readonly int _fastPeriod;
private readonly int _slowPeriod;
private readonly Resolution _resolution;
private readonly int _predictionInterval;
/// <summary>
/// This is made protected for testing purposes
/// </summary>
protected Dictionary<Symbol, SymbolData> SymbolDataBySymbol { get; }
/// <summary>
/// Initializes a new instance of the <see cref="EmaCrossAlphaModel"/> class
/// </summary>
/// <param name="fastPeriod">The fast EMA period</param>
/// <param name="slowPeriod">The slow EMA period</param>
/// <param name="resolution">The resolution of data sent into the EMA indicators</param>
public EmaCrossAlphaModel(
int fastPeriod = 12,
int slowPeriod = 26,
Resolution resolution = Resolution.Daily
)
{
_fastPeriod = fastPeriod;
_slowPeriod = slowPeriod;
_resolution = resolution;
_predictionInterval = fastPeriod;
SymbolDataBySymbol = new Dictionary<Symbol, SymbolData>();
Name = $"{nameof(EmaCrossAlphaModel)}({fastPeriod},{slowPeriod},{resolution})";
}
/// <summary>
/// Updates this alpha model with the latest data from the algorithm.
/// This is called each time the algorithm receives data for subscribed securities
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="data">The new data available</param>
/// <returns>The new insights generated</returns>
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
var insights = new List<Insight>();
foreach (var symbolData in SymbolDataBySymbol.Values)
{
if (symbolData.Fast.IsReady && symbolData.Slow.IsReady)
{
var insightPeriod = _resolution.ToTimeSpan().Multiply(_predictionInterval);
if (symbolData.FastIsOverSlow)
{
if (symbolData.Slow > symbolData.Fast)
{
insights.Add(Insight.Price(symbolData.Symbol, insightPeriod, InsightDirection.Down));
}
}
else if (symbolData.SlowIsOverFast)
{
if (symbolData.Fast > symbolData.Slow)
{
insights.Add(Insight.Price(symbolData.Symbol, insightPeriod, InsightDirection.Up));
}
}
}
symbolData.FastIsOverSlow = symbolData.Fast > symbolData.Slow;
}
return insights;
}
/// <summary>
/// Event fired each time the we add/remove securities from the data feed
/// </summary>
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
/// <param name="changes">The security additions and removals from the algorithm</param>
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
foreach (var added in changes.AddedSecurities)
{
SymbolData symbolData;
if (!SymbolDataBySymbol.TryGetValue(added.Symbol, out symbolData))
{
SymbolDataBySymbol[added.Symbol] = new SymbolData(added, _fastPeriod, _slowPeriod, algorithm, _resolution);
}
else
{
// a security that was already initialized was re-added, reset the indicators
symbolData.Fast.Reset();
symbolData.Slow.Reset();
}
}
foreach (var removed in changes.RemovedSecurities)
{
SymbolData symbolData;
if (SymbolDataBySymbol.TryGetValue(removed.Symbol, out symbolData))
{
// clean up our consolidators
symbolData.RemoveConsolidators();
SymbolDataBySymbol.Remove(removed.Symbol);
}
}
}
/// <summary>
/// Contains data specific to a symbol required by this model
/// </summary>
public class SymbolData
{
private readonly QCAlgorithm _algorithm;
private readonly IDataConsolidator _fastConsolidator;
private readonly IDataConsolidator _slowConsolidator;
private readonly ExponentialMovingAverage _fast;
private readonly ExponentialMovingAverage _slow;
private readonly Security _security;
/// <summary>
/// Symbol associated with the data
/// </summary>
public Symbol Symbol => _security.Symbol;
/// <summary>
/// Fast Exponential Moving Average (EMA)
/// </summary>
public ExponentialMovingAverage Fast => _fast;
/// <summary>
/// Slow Exponential Moving Average (EMA)
/// </summary>
public ExponentialMovingAverage Slow => _slow;
/// <summary>
/// True if the fast is above the slow, otherwise false.
/// This is used to prevent emitting the same signal repeatedly
/// </summary>
public bool FastIsOverSlow { get; set; }
/// <summary>
/// Flag indicating if the Slow EMA is over the Fast one
/// </summary>
public bool SlowIsOverFast => !FastIsOverSlow;
/// <summary>
/// Initializes an instance of the class SymbolData with the given arguments
/// </summary>
public SymbolData(
Security security,
int fastPeriod,
int slowPeriod,
QCAlgorithm algorithm,
Resolution resolution)
{
_algorithm = algorithm;
_security = security;
_fastConsolidator = algorithm.ResolveConsolidator(security.Symbol, resolution);
_slowConsolidator = algorithm.ResolveConsolidator(security.Symbol, resolution);
algorithm.SubscriptionManager.AddConsolidator(security.Symbol, _fastConsolidator);
algorithm.SubscriptionManager.AddConsolidator(security.Symbol, _slowConsolidator);
// create fast/slow EMAs
_fast = new ExponentialMovingAverage(security.Symbol, fastPeriod, ExponentialMovingAverage.SmoothingFactorDefault(fastPeriod));
_slow = new ExponentialMovingAverage(security.Symbol, slowPeriod, ExponentialMovingAverage.SmoothingFactorDefault(slowPeriod));
algorithm.RegisterIndicator(security.Symbol, _fast, _fastConsolidator);
algorithm.RegisterIndicator(security.Symbol, _slow, _slowConsolidator);
algorithm.WarmUpIndicator(security.Symbol, _fast, resolution);
algorithm.WarmUpIndicator(security.Symbol, _slow, resolution);
}
/// <summary>
/// Remove Fast and Slow consolidators
/// </summary>
public void RemoveConsolidators()
{
_algorithm.SubscriptionManager.RemoveConsolidator(Symbol, _fastConsolidator);
_algorithm.SubscriptionManager.RemoveConsolidator(Symbol, _slowConsolidator);
}
}
}
}
@@ -0,0 +1,115 @@
# 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 EmaCrossAlphaModel(AlphaModel):
'''Alpha model that uses an EMA cross to create insights'''
def __init__(self,
fast_period = 12,
slow_period = 26,
resolution = Resolution.DAILY):
'''Initializes a new instance of the EmaCrossAlphaModel class
Args:
fast_period: The fast EMA period
slow_period: The slow EMA period'''
self.fast_period = fast_period
self.slow_period = slow_period
self.resolution = resolution
self.prediction_interval = Time.multiply(Extensions.to_time_span(resolution), fast_period)
self.symbol_data_by_symbol = {}
self.name = '{}({},{},{})'.format(self.__class__.__name__, fast_period, slow_period, resolution)
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'''
insights = []
for symbol, symbol_data in self.symbol_data_by_symbol.items():
if symbol_data.fast.is_ready and symbol_data.slow.is_ready:
if symbol_data.fast_is_over_slow:
if symbol_data.slow > symbol_data.fast:
insights.append(Insight.price(symbol_data.symbol, self.prediction_interval, InsightDirection.DOWN))
elif symbol_data.slow_is_over_fast:
if symbol_data.fast > symbol_data.slow:
insights.append(Insight.price(symbol_data.symbol, self.prediction_interval, InsightDirection.UP))
symbol_data.fast_is_over_slow = symbol_data.fast > symbol_data.slow
return insights
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 added in changes.added_securities:
symbol_data = self.symbol_data_by_symbol.get(added.symbol)
if symbol_data is None:
symbol_data = SymbolData(added, self.fast_period, self.slow_period, algorithm, self.resolution)
self.symbol_data_by_symbol[added.symbol] = symbol_data
else:
# a security that was already initialized was re-added, reset the indicators
symbol_data.fast.reset()
symbol_data.slow.reset()
for removed in changes.removed_securities:
data = self.symbol_data_by_symbol.pop(removed.symbol, None)
if data is not None:
# clean up our consolidators
data.remove_consolidators()
class SymbolData:
'''Contains data specific to a symbol required by this model'''
def __init__(self, security, fast_period, slow_period, algorithm, resolution):
self.security = security
self.symbol = security.symbol
self.algorithm = algorithm
self.fast_consolidator = algorithm.resolve_consolidator(security.symbol, resolution)
self.slow_consolidator = algorithm.resolve_consolidator(security.symbol, resolution)
algorithm.subscription_manager.add_consolidator(security.symbol, self.fast_consolidator)
algorithm.subscription_manager.add_consolidator(security.symbol, self.slow_consolidator)
# create fast/slow EMAs
self.fast = ExponentialMovingAverage(security.symbol, fast_period, ExponentialMovingAverage.smoothing_factor_default(fast_period))
self.slow = ExponentialMovingAverage(security.symbol, slow_period, ExponentialMovingAverage.smoothing_factor_default(slow_period))
algorithm.register_indicator(security.symbol, self.fast, self.fast_consolidator);
algorithm.register_indicator(security.symbol, self.slow, self.slow_consolidator);
algorithm.warm_up_indicator(security.symbol, self.fast, resolution);
algorithm.warm_up_indicator(security.symbol, self.slow, resolution);
# True if the fast is above the slow, otherwise false.
# This is used to prevent emitting the same signal repeatedly
self.fast_is_over_slow = False
def remove_consolidators(self):
self.algorithm.subscription_manager.remove_consolidator(self.security.symbol, self.fast_consolidator)
self.algorithm.subscription_manager.remove_consolidator(self.security.symbol, self.slow_consolidator)
@property
def slow_is_over_fast(self):
return not self.fast_is_over_slow
@@ -0,0 +1,170 @@
/*
* 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.
*/
using System;
using System.Collections.Generic;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Indicators;
using QuantConnect.Securities;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Alpha model that uses historical returns to create insights
/// </summary>
public class HistoricalReturnsAlphaModel : AlphaModel
{
private readonly int _lookback;
private readonly Resolution _resolution;
private readonly TimeSpan _predictionInterval;
private readonly Dictionary<Symbol, SymbolData> _symbolDataBySymbol;
private readonly InsightCollection _insightCollection;
/// <summary>
/// Initializes a new instance of the <see cref="HistoricalReturnsAlphaModel"/> class
/// </summary>
/// <param name="lookback">Historical return lookback period</param>
/// <param name="resolution">The resolution of historical data</param>
public HistoricalReturnsAlphaModel(
int lookback = 1,
Resolution resolution = Resolution.Daily
)
{
_lookback = lookback;
_resolution = resolution;
_predictionInterval = _resolution.ToTimeSpan().Multiply(_lookback);
_symbolDataBySymbol = new Dictionary<Symbol, SymbolData>();
_insightCollection = new InsightCollection();
Name = $"{nameof(HistoricalReturnsAlphaModel)}({lookback},{resolution})";
}
/// <summary>
/// Updates this alpha model with the latest data from the algorithm.
/// This is called each time the algorithm receives data for subscribed securities
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="data">The new data available</param>
/// <returns>The new insights generated</returns>
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
var insights = new List<Insight>();
foreach (var (symbol, symbolData) in _symbolDataBySymbol)
{
if (symbolData.CanEmit())
{
var direction = InsightDirection.Flat;
var magnitude = (double)symbolData.ROC.Current.Value;
if (magnitude > 0) direction = InsightDirection.Up;
if (magnitude < 0) direction = InsightDirection.Down;
if (direction == InsightDirection.Flat)
{
CancelInsights(algorithm, symbol);
continue;
}
insights.Add(Insight.Price(symbolData.Security.Symbol, _predictionInterval, direction, magnitude, null));
}
}
_insightCollection.AddRange(insights);
return insights;
}
/// <summary>
/// Event fired each time the we add/remove securities from the data feed
/// </summary>
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
/// <param name="changes">The security additions and removals from the algorithm</param>
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
// clean up data for removed securities
foreach (var removed in changes.RemovedSecurities)
{
SymbolData data;
if (_symbolDataBySymbol.TryGetValue(removed.Symbol, out data))
{
_symbolDataBySymbol.Remove(removed.Symbol);
algorithm.SubscriptionManager.RemoveConsolidator(removed.Symbol, data.Consolidator);
}
CancelInsights(algorithm, removed.Symbol);
}
// initialize data for added securities
var addedSymbols = new List<Symbol>();
foreach (var added in changes.AddedSecurities)
{
if (!_symbolDataBySymbol.ContainsKey(added.Symbol))
{
var symbolData = new SymbolData(algorithm, added, _lookback, _resolution);
_symbolDataBySymbol[added.Symbol] = symbolData;
addedSymbols.Add(symbolData.Security.Symbol);
}
}
if (addedSymbols.Count > 0)
{
// warmup our indicators by pushing history through the consolidators
algorithm.History(addedSymbols, _lookback, _resolution)
.PushThrough(bar =>
{
SymbolData symbolData;
if (_symbolDataBySymbol.TryGetValue(bar.Symbol, out symbolData))
{
symbolData.ROC.Update(bar.EndTime, bar.Value);
}
});
}
}
private void CancelInsights(QCAlgorithm algorithm, Symbol symbol)
{
if (_insightCollection.TryGetValue(symbol, out var insights))
{
algorithm.Insights.Cancel(insights);
_insightCollection.Clear(new[] { symbol });
}
}
/// <summary>
/// Contains data specific to a symbol required by this model
/// </summary>
private class SymbolData
{
public Security Security;
public IDataConsolidator Consolidator;
public RateOfChange ROC;
public long previous = 0;
public SymbolData(QCAlgorithm algorithm, Security security, int lookback, Resolution resolution)
{
Security = security;
Consolidator = algorithm.ResolveConsolidator(security.Symbol, resolution);
algorithm.SubscriptionManager.AddConsolidator(security.Symbol, Consolidator);
ROC = new RateOfChange(security.Symbol.ToString(), lookback);
algorithm.RegisterIndicator(security.Symbol, ROC, Consolidator);
}
public bool CanEmit()
{
if (previous == ROC.Samples) return false;
previous = ROC.Samples;
return ROC.IsReady;
}
}
}
}
@@ -0,0 +1,126 @@
# 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 HistoricalReturnsAlphaModel(AlphaModel):
'''Uses Historical returns to create insights.'''
def __init__(self, *args, **kwargs):
'''Initializes a new default instance of the HistoricalReturnsAlphaModel class.
Args:
lookback(int): Historical return lookback period
resolution: The resolution of historical data'''
self.lookback = kwargs['lookback'] if 'lookback' in kwargs else 1
self.resolution = kwargs['resolution'] if 'resolution' in kwargs else Resolution.DAILY
self.prediction_interval = Time.multiply(Extensions.to_time_span(self.resolution), self.lookback)
self._symbol_data_by_symbol = {}
self.insight_collection = InsightCollection()
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'''
insights = []
for symbol, symbol_data in self._symbol_data_by_symbol.items():
if symbol_data.can_emit:
direction = InsightDirection.FLAT
magnitude = symbol_data.return_
if magnitude > 0: direction = InsightDirection.UP
if magnitude < 0: direction = InsightDirection.DOWN
if direction == InsightDirection.FLAT:
self.cancel_insights(algorithm, symbol)
continue
insights.append(Insight.price(symbol, self.prediction_interval, direction, magnitude, None))
self.insight_collection.add_range(insights)
return insights
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'''
# 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)
self.cancel_insights(algorithm, removed.symbol)
# initialize data for added securities
symbols = [ x.symbol for x in changes.added_securities ]
history = algorithm.history(symbols, self.lookback, self.resolution)
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.lookback)
self._symbol_data_by_symbol[symbol] = symbol_data
symbol_data.register_indicators(algorithm, self.resolution)
symbol_data.warm_up_indicators(history.loc[ticker])
def cancel_insights(self, algorithm, symbol):
if not self.insight_collection.contains_key(symbol):
return
insights = self.insight_collection[symbol]
algorithm.insights.cancel(insights)
self.insight_collection.clear([ symbol ]);
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 register_indicators(self, algorithm, resolution):
self.consolidator = algorithm.resolve_consolidator(self.symbol, resolution)
algorithm.register_indicator(self.symbol, self.roc, self.consolidator)
def remove_consolidators(self, algorithm):
if self.consolidator is not None:
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 return_(self):
return float(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 '{}: {:.2%}'.format(self.roc.name, (1 + self.return_)**252 - 1)
@@ -0,0 +1,203 @@
/*
* 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.
*/
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Indicators;
using QuantConnect.Securities;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Defines a custom alpha model that uses MACD crossovers. The MACD signal line is
/// used to generate up/down insights if it's stronger than the bounce threshold.
/// If the MACD signal is within the bounce threshold then a flat price insight is returned.
/// </summary>
public class MacdAlphaModel : AlphaModel
{
private readonly int _fastPeriod;
private readonly int _slowPeriod;
private readonly int _signalPeriod;
private readonly MovingAverageType _movingAverageType;
private readonly Resolution _resolution;
private const decimal BounceThresholdPercent = 0.01m;
private InsightCollection _insightCollection = new();
/// <summary>
/// Dictionary containing basic information for each symbol present as key
/// </summary>
protected Dictionary<Symbol, SymbolData> _symbolData { get; init; }
/// <summary>
/// Initializes a new instance of the <see cref="MacdAlphaModel"/> class
/// </summary>
/// <param name="fastPeriod">The MACD fast period</param>
/// <param name="slowPeriod">The MACD slow period</param>
/// <param name="signalPeriod">The smoothing period for the MACD signal</param>
/// <param name="movingAverageType">The type of moving average to use in the MACD</param>
/// <param name="resolution">The resolution of data sent into the MACD indicator</param>
public MacdAlphaModel(
int fastPeriod = 12,
int slowPeriod = 26,
int signalPeriod = 9,
MovingAverageType movingAverageType = MovingAverageType.Exponential,
Resolution resolution = Resolution.Daily
)
{
_fastPeriod = fastPeriod;
_slowPeriod = slowPeriod;
_signalPeriod = signalPeriod;
_movingAverageType = movingAverageType;
_resolution = resolution;
_symbolData = new Dictionary<Symbol, SymbolData>();
Name = $"{nameof(MacdAlphaModel)}({fastPeriod},{slowPeriod},{signalPeriod},{movingAverageType},{resolution})";
}
/// <summary>
/// Determines an insight for each security based on it's current MACD signal
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="data">The new data available</param>
/// <returns>The new insights generated</returns>
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
foreach (var sd in _symbolData.Values)
{
if (sd.Security.Price == 0)
{
continue;
}
var direction = InsightDirection.Flat;
var normalizedSignal = sd.MACD.Signal / sd.Security.Price;
if (normalizedSignal > BounceThresholdPercent)
{
direction = InsightDirection.Up;
}
else if (normalizedSignal < -BounceThresholdPercent)
{
direction = InsightDirection.Down;
}
// ignore signal for same direction as previous signal
if (direction == sd.PreviousDirection)
{
continue;
}
sd.PreviousDirection = direction;
if (direction == InsightDirection.Flat)
{
CancelInsights(algorithm, sd.Security.Symbol);
continue;
}
var insightPeriod = _resolution.ToTimeSpan().Multiply(_fastPeriod);
var insight = Insight.Price(sd.Security.Symbol, insightPeriod, direction);
_insightCollection.Add(insight);
yield return insight;
}
}
/// <summary>
/// Event fired each time the we add/remove securities from the data feed.
/// This initializes the MACD for each added security and cleans up the indicator for each removed security.
/// </summary>
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
/// <param name="changes">The security additions and removals from the algorithm</param>
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
foreach (var added in changes.AddedSecurities)
{
if (_symbolData.ContainsKey(added.Symbol))
{
continue;
}
_symbolData.Add(added.Symbol, new SymbolData(algorithm, added, _fastPeriod, _slowPeriod, _signalPeriod, _movingAverageType, _resolution));
}
foreach (var removed in changes.RemovedSecurities)
{
var symbol = removed.Symbol;
SymbolData data;
if (_symbolData.TryGetValue(symbol, out data))
{
// clean up our consolidator
algorithm.SubscriptionManager.RemoveConsolidator(symbol, data.Consolidator);
_symbolData.Remove(symbol);
}
// remove from insight collection manager
CancelInsights(algorithm, symbol);
}
}
private void CancelInsights(QCAlgorithm algorithm, Symbol symbol)
{
if (_insightCollection.TryGetValue(symbol, out var insights))
{
algorithm.Insights.Cancel(insights);
_insightCollection.Clear(new[] { symbol });
}
}
/// <summary>
/// Class representing basic data of a symbol
/// </summary>
public class SymbolData
{
/// <summary>
/// Previous direction property
/// </summary>
public InsightDirection? PreviousDirection { get; set; }
/// <summary>
/// Security of the Symbol Data
/// </summary>
public Security Security { get; init; }
/// <summary>
/// Consolidator property
/// </summary>
public IDataConsolidator Consolidator { get; init; }
/// <summary>
/// Moving Average Convergence Divergence indicator
/// </summary>
public MovingAverageConvergenceDivergence MACD { get; init; }
/// <summary>
/// Initializes an instance of the SymbolData class with the given arguments
/// </summary>
public SymbolData(QCAlgorithm algorithm, Security security, int fastPeriod, int slowPeriod, int signalPeriod, MovingAverageType movingAverageType, Resolution resolution)
{
Security = security;
Consolidator = algorithm.ResolveConsolidator(security.Symbol, resolution);
algorithm.SubscriptionManager.AddConsolidator(security.Symbol, Consolidator);
MACD = new MovingAverageConvergenceDivergence(fastPeriod, slowPeriod, signalPeriod, movingAverageType);
algorithm.RegisterIndicator(security.Symbol, MACD, Consolidator);
algorithm.WarmUpIndicator(security.Symbol, MACD, resolution);
}
}
}
}
@@ -0,0 +1,121 @@
# 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 MacdAlphaModel(AlphaModel):
'''Defines a custom alpha model that uses MACD crossovers. The MACD signal line
is used to generate up/down insights if it's stronger than the bounce threshold.
If the MACD signal is within the bounce threshold then a flat price insight is returned.'''
def __init__(self,
fastPeriod = 12,
slowPeriod = 26,
signalPeriod = 9,
movingAverageType = MovingAverageType.Exponential,
resolution = Resolution.Daily):
''' Initializes a new instance of the MacdAlphaModel class
Args:
fastPeriod: The MACD fast period
slowPeriod: The MACD slow period</param>
signalPeriod: The smoothing period for the MACD signal
movingAverageType: The type of moving average to use in the MACD'''
self.fastPeriod = fastPeriod
self.slowPeriod = slowPeriod
self.signalPeriod = signalPeriod
self.movingAverageType = movingAverageType
self.resolution = resolution
self.insightPeriod = Time.Multiply(Extensions.ToTimeSpan(resolution), fastPeriod)
self.bounceThresholdPercent = 0.01
self.insightCollection = InsightCollection()
self.symbolData = {}
self.Name = '{}({},{},{},{},{})'.format(self.__class__.__name__, fastPeriod, slowPeriod, signalPeriod, movingAverageType, resolution)
def Update(self, algorithm, data):
''' Determines an insight for each security based on it's current MACD signal
Args:
algorithm: The algorithm instance
data: The new data available
Returns:
The new insights generated'''
insights = []
for key, sd in self.symbolData.items():
if sd.Security.Price == 0:
continue
direction = InsightDirection.Flat
normalized_signal = sd.MACD.Signal.Current.Value / sd.Security.Price
if normalized_signal > self.bounceThresholdPercent:
direction = InsightDirection.Up
elif normalized_signal < -self.bounceThresholdPercent:
direction = InsightDirection.Down
# ignore signal for same direction as previous signal
if direction == sd.PreviousDirection:
continue
sd.PreviousDirection = direction
if direction == InsightDirection.Flat:
self.CancelInsights(algorithm, sd.Security.Symbol)
continue
insight = Insight.Price(sd.Security.Symbol, self.insightPeriod, direction)
insights.append(insight)
self.insightCollection.Add(insight)
return insights
def OnSecuritiesChanged(self, algorithm, changes):
'''Event fired each time the we add/remove securities from the data feed.
This initializes the MACD for each added security and cleans up the indicator for each removed security.
Args:
algorithm: The algorithm instance that experienced the change in securities
changes: The security additions and removals from the algorithm'''
for added in changes.AddedSecurities:
self.symbolData[added.Symbol] = SymbolData(algorithm, added, self.fastPeriod, self.slowPeriod, self.signalPeriod, self.movingAverageType, self.resolution)
for removed in changes.RemovedSecurities:
symbol = removed.Symbol
data = self.symbolData.pop(symbol, None)
if data is not None:
# clean up our consolidator
algorithm.SubscriptionManager.RemoveConsolidator(symbol, data.Consolidator)
# remove from insight collection manager
self.CancelInsights(algorithm, symbol)
def CancelInsights(self, algorithm, symbol):
if not self.insightCollection.ContainsKey(symbol):
return
insights = self.insightCollection[symbol]
algorithm.Insights.Cancel(insights)
self.insightCollection.Clear([ symbol ]);
class SymbolData:
def __init__(self, algorithm, security, fastPeriod, slowPeriod, signalPeriod, movingAverageType, resolution):
self.Security = security
self.MACD = MovingAverageConvergenceDivergence(fastPeriod, slowPeriod, signalPeriod, movingAverageType)
self.Consolidator = algorithm.ResolveConsolidator(security.Symbol, resolution)
algorithm.RegisterIndicator(security.Symbol, self.MACD, self.Consolidator)
algorithm.WarmUpIndicator(security.Symbol, self.MACD, resolution)
self.PreviousDirection = None
@@ -0,0 +1,149 @@
/*
* 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.
*/
using MathNet.Numerics.Statistics;
using QuantConnect.Data;
using QuantConnect.Data.UniverseSelection;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// This alpha model is designed to rank every pair combination by its pearson correlation
/// and trade the pair with the hightest correlation
/// This model generates alternating long ratio/short ratio insights emitted as a group
/// </summary>
public class PearsonCorrelationPairsTradingAlphaModel : BasePairsTradingAlphaModel
{
private readonly int _lookback;
private readonly Resolution _resolution;
private readonly double _minimumCorrelation;
private Tuple<Symbol, Symbol> _bestPair;
/// <summary>
/// Initializes a new instance of the <see cref="PearsonCorrelationPairsTradingAlphaModel"/> class
/// </summary>
/// <param name="lookback">Lookback period of the analysis</param>
/// <param name="resolution">Analysis resolution</param>
/// <param name="threshold">The percent [0, 100] deviation of the ratio from the mean before emitting an insight</param>
/// <param name="minimumCorrelation">The minimum correlation to consider a tradable pair</param>
public PearsonCorrelationPairsTradingAlphaModel(int lookback = 15, Resolution resolution = Resolution.Minute, decimal threshold = 1m, double minimumCorrelation = .5)
: base(lookback, resolution, threshold)
{
_lookback = lookback;
_resolution = resolution;
_minimumCorrelation = minimumCorrelation;
}
/// <summary>
/// Event fired each time the we add/remove securities from the data feed
/// </summary>
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
/// <param name="changes">The security additions and removals from the algorithm</param>
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
NotifiedSecurityChanges.UpdateCollection(Securities, changes);
var symbols = Securities.Select(x => x.Symbol).ToArray();
var history = algorithm.History(symbols, _lookback, _resolution);
var vectors = GetPriceVectors(history);
if (vectors.LongLength == 0)
{
algorithm.Debug($"PearsonCorrelationPairsTradingAlphaModel.OnSecuritiesChanged(): The requested historical data does not have series of prices with the same date/time. Please consider increasing the looback period. Current lookback: {_lookback}");
}
else
{
var pearsonMatrix = Correlation.PearsonMatrix(vectors).UpperTriangle();
var maxValue = pearsonMatrix.Enumerate().Where(x => Math.Abs(x) < 1).Max();
if (maxValue >= _minimumCorrelation)
{
var maxTuple = pearsonMatrix.Find(x => x == maxValue);
_bestPair = Tuple.Create(symbols[maxTuple.Item1], symbols[maxTuple.Item2]);
}
}
base.OnSecuritiesChanged(algorithm, changes);
}
/// <summary>
/// Check whether the assets pass a pairs trading test
/// </summary>
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
/// <param name="asset1">The first asset's symbol in the pair</param>
/// <param name="asset2">The second asset's symbol in the pair</param>
/// <returns>True if the statistical test for the pair is successful</returns>
public override bool HasPassedTest(QCAlgorithm algorithm, Symbol asset1, Symbol asset2)
{
// Check if this method was overridden in Python
if (TryInvokePythonOverride(nameof(HasPassedTest), out bool result, algorithm, asset1, asset2))
{
return result;
}
return _bestPair != null && asset1 == _bestPair.Item1 && asset2 == _bestPair.Item2;
}
private double[][] GetPriceVectors(IEnumerable<Slice> slices)
{
var symbols = Securities.Select(x => x.Symbol).ToArray();
var timeZones = Securities.ToDictionary(x => x.Symbol, y => y.Exchange.TimeZone);
// Special case: daily data and securities from different timezone
var isDailyAndMultipleTimeZone = _resolution == Resolution.Daily && timeZones.Values.Distinct().Count() > 1;
var bars = new List<BaseData>();
if (isDailyAndMultipleTimeZone)
{
bars.AddRange(slices
.GroupBy(x => x.Time.Date)
.Where(x => x.Sum(k => k.Count) == symbols.Length)
.SelectMany(x => x.SelectMany(y => y.Values)));
}
else
{
bars.AddRange(slices
.Where(x => x.Count == symbols.Length)
.SelectMany(x => x.Values));
}
return bars
.GroupBy(x => x.Symbol)
.Select(x =>
{
var array = x.Select(b => Math.Log((double)b.Price)).ToArray();
if (array.Length > 1)
{
for (var i = array.Length - 1; i > 0; i--)
{
array[i] = array[i] - array[i - 1];
}
array[0] = array[1];
return array;
}
else
{
return new double[0];
}
}).ToArray();
}
}
}
@@ -0,0 +1,108 @@
# 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 Alphas.BasePairsTradingAlphaModel import BasePairsTradingAlphaModel
from scipy.stats import pearsonr
class PearsonCorrelationPairsTradingAlphaModel(BasePairsTradingAlphaModel):
''' This alpha model is designed to rank every pair combination by its pearson correlation
and trade the pair with the hightest correlation
This model generates alternating long ratio/short ratio insights emitted as a group'''
def __init__(self, lookback = 15,
resolution = Resolution.MINUTE,
threshold = 1,
minimum_correlation = .5):
'''Initializes a new instance of the PearsonCorrelationPairsTradingAlphaModel class
Args:
lookback: lookback period of the analysis
resolution: analysis resolution
threshold: The percent [0, 100] deviation of the ratio from the mean before emitting an insight
minimum_correlation: The minimum correlation to consider a tradable pair'''
super().__init__(lookback, resolution, threshold)
self.lookback = lookback
self.resolution = resolution
self.minimum_correlation = minimum_correlation
self.best_pair = ()
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 security in changes.added_securities:
self.securities.add(security)
for security in changes.removed_securities:
if security in self.securities:
self.securities.remove(security)
symbols = sorted([ x.symbol for x in self.securities ])
history = algorithm.history(symbols, self.lookback, self.resolution)
if not history.empty:
history = history.close.unstack(level=0)
df = self.get_price_dataframe(history)
stop = len(df.columns)
corr = dict()
for i in range(0, stop):
for j in range(i+1, stop):
if (j, i) not in corr:
corr[(i, j)] = pearsonr(df.iloc[:,i], df.iloc[:,j])[0]
corr = sorted(corr.items(), key = lambda kv: kv[1])
if corr[-1][1] >= self.minimum_correlation:
self.best_pair = (symbols[corr[-1][0][0]], symbols[corr[-1][0][1]])
super().on_securities_changed(algorithm, changes)
def has_passed_test(self, algorithm, asset1, asset2):
'''Check whether the assets pass a pairs trading test
Args:
algorithm: The algorithm instance that experienced the change in securities
asset1: The first asset's symbol in the pair
asset2: The second asset's symbol in the pair
Returns:
True if the statistical test for the pair is successful'''
return self.best_pair is not None and self.best_pair[0] == asset1 and self.best_pair[1] == asset2
def get_price_dataframe(self, df):
timezones = { x.symbol.value: x.exchange.time_zone for x in self.securities }
# Use log prices
df = np.log(df)
is_single_timeZone = len(set(timezones.values())) == 1
if not is_single_timeZone:
series_dict = dict()
for column in df:
# Change the dataframe index from data time to UTC time
to_utc = lambda x: Extensions.convert_to_utc(x, timezones[column])
if self.resolution == Resolution.DAILY:
to_utc = lambda x: Extensions.convert_to_utc(x, timezones[column]).date()
data = df[[column]]
data.index = data.index.map(to_utc)
series_dict[column] = data[column]
df = pd.DataFrame(series_dict).dropna()
return (df - df.shift(1)).dropna()
+214
View File
@@ -0,0 +1,214 @@
/*
* 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.
*/
using System;
using System.Collections.Generic;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Indicators;
namespace QuantConnect.Algorithm.Framework.Alphas
{
/// <summary>
/// Uses Wilder's RSI to create insights. Using default settings, a cross over below 30 or above 70 will
/// trigger a new insight.
/// </summary>
public class RsiAlphaModel : AlphaModel
{
private readonly Dictionary<Symbol, SymbolData> _symbolDataBySymbol = new Dictionary<Symbol, SymbolData>();
private readonly int _period;
private readonly Resolution _resolution;
/// <summary>
/// Initializes a new instance of the <see cref="RsiAlphaModel"/> class
/// </summary>
/// <param name="period">The RSI indicator period</param>
/// <param name="resolution">The resolution of data sent into the RSI indicator</param>
public RsiAlphaModel(
int period = 14,
Resolution resolution = Resolution.Daily
)
{
_period = period;
_resolution = resolution;
Name = $"{nameof(RsiAlphaModel)}({_period},{_resolution})";
}
/// <summary>
/// Updates this alpha model with the latest data from the algorithm.
/// This is called each time the algorithm receives data for subscribed securities
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="data">The new data available</param>
/// <returns>The new insights generated</returns>
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
var insights = new List<Insight>();
foreach (var kvp in _symbolDataBySymbol)
{
var symbol = kvp.Key;
var rsi = kvp.Value.RSI;
var previousState = kvp.Value.State;
var state = GetState(rsi, previousState);
if (state != previousState && rsi.IsReady)
{
var insightPeriod = _resolution.ToTimeSpan().Multiply(_period);
switch (state)
{
case State.TrippedLow:
insights.Add(Insight.Price(symbol, insightPeriod, InsightDirection.Up));
break;
case State.TrippedHigh:
insights.Add(Insight.Price(symbol, insightPeriod, InsightDirection.Down));
break;
}
}
kvp.Value.State = state;
}
return insights;
}
/// <summary>
/// Cleans out old security data and initializes the RSI for any newly added securities.
/// This functional also seeds any new indicators using a history request.
/// </summary>
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
/// <param name="changes">The security additions and removals from the algorithm</param>
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
// clean up data for removed securities
foreach (var security in changes.RemovedSecurities)
{
SymbolData symbolData;
if (_symbolDataBySymbol.TryGetValue(security.Symbol, out symbolData))
{
_symbolDataBySymbol.Remove(security.Symbol);
symbolData.Dispose();
}
}
// initialize data for added securities
var addedSymbols = new List<Symbol>();
foreach (var added in changes.AddedSecurities)
{
if (!_symbolDataBySymbol.ContainsKey(added.Symbol))
{
var symbolData = new SymbolData(algorithm, added.Symbol, _period, _resolution);
_symbolDataBySymbol[added.Symbol] = symbolData;
addedSymbols.Add(added.Symbol);
}
}
if (addedSymbols.Count > 0)
{
// warmup our indicators by pushing history through the consolidators
algorithm.History(addedSymbols, _period, _resolution)
.PushThrough(data =>
{
SymbolData symbolData;
if (_symbolDataBySymbol.TryGetValue(data.Symbol, out symbolData))
{
symbolData.Update(data);
}
});
}
}
/// <summary>
/// Determines the new state. This is basically cross-over detection logic that
/// includes considerations for bouncing using the configured bounce tolerance.
/// </summary>
private State GetState(RelativeStrengthIndex rsi, State previous)
{
if (rsi > 70m)
{
return State.TrippedHigh;
}
if (rsi < 30m)
{
return State.TrippedLow;
}
if (previous == State.TrippedLow)
{
if (rsi > 35m)
{
return State.Middle;
}
}
if (previous == State.TrippedHigh)
{
if (rsi < 65m)
{
return State.Middle;
}
}
return previous;
}
/// <summary>
/// Contains data specific to a symbol required by this model
/// </summary>
private class SymbolData : IDisposable
{
public State State { get; set; }
public RelativeStrengthIndex RSI { get; }
private Symbol _symbol { get; }
private QCAlgorithm _algorithm;
private IDataConsolidator _consolidator;
public SymbolData(QCAlgorithm algorithm, Symbol symbol, int period, Resolution resolution)
{
_algorithm = algorithm;
_symbol = symbol;
State = State.Middle;
RSI = new RelativeStrengthIndex(period, MovingAverageType.Wilders);
_consolidator = _algorithm.ResolveConsolidator(symbol, resolution);
algorithm.RegisterIndicator(symbol, RSI, _consolidator);
}
public void Update(BaseData bar)
{
_consolidator.Update(bar);
}
public void Dispose()
{
_algorithm.SubscriptionManager.RemoveConsolidator(_symbol, _consolidator);
}
}
/// <summary>
/// Defines the state. This is used to prevent signal spamming and aid in bounce detection.
/// </summary>
private enum State
{
TrippedLow,
Middle,
TrippedHigh
}
}
}
+127
View File
@@ -0,0 +1,127 @@
# 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 QuantConnect.Logging import *
from enum import Enum
class RsiAlphaModel(AlphaModel):
'''Uses Wilder's RSI to create insights.
Using default settings, a cross over below 30 or above 70 will trigger a new insight.'''
def __init__(self,
period = 14,
resolution = Resolution.DAILY):
'''Initializes a new instance of the RsiAlphaModel class
Args:
period: The RSI indicator period'''
self.period = period
self.resolution = resolution
self.insight_period = Time.multiply(Extensions.to_time_span(resolution), period)
self.symbol_data_by_symbol ={}
self.name = '{}({},{})'.format(self.__class__.__name__, period, resolution)
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'''
insights = []
for symbol, symbol_data in self.symbol_data_by_symbol.items():
rsi = symbol_data.rsi
previous_state = symbol_data.state
state = self.get_state(rsi, previous_state)
if state != previous_state and rsi.is_ready:
if state == State.TRIPPED_LOW:
insights.append(Insight.price(symbol, self.insight_period, InsightDirection.UP))
if state == State.TRIPPED_HIGH:
insights.append(Insight.price(symbol, self.insight_period, InsightDirection.DOWN))
symbol_data.state = state
return insights
def on_securities_changed(self, algorithm, changes):
'''Cleans out old security data and initializes the RSI for any newly added securities.
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'''
# clean up data for removed securities
for security in changes.removed_securities:
symbol_data = self.symbol_data_by_symbol.pop(security.symbol, None)
if symbol_data:
symbol_data.dispose()
# initialize data for added securities
added_symbols = []
for security in changes.added_securities:
symbol = security.symbol
if symbol not in self.symbol_data_by_symbol:
symbol_data = SymbolData(algorithm, symbol, self.period, self.resolution)
self.symbol_data_by_symbol[symbol] = symbol_data
added_symbols.append(symbol)
if added_symbols:
history = algorithm.history[TradeBar](added_symbols, self.period, self.resolution)
for trade_bars in history:
for bar in trade_bars.values():
self.symbol_data_by_symbol[bar.symbol].update(bar)
def get_state(self, rsi, previous):
''' Determines the new state. This is basically cross-over detection logic that
includes considerations for bouncing using the configured bounce tolerance.'''
if rsi.current.value > 70:
return State.TRIPPED_HIGH
if rsi.current.value < 30:
return State.TRIPPED_LOW
if previous == State.TRIPPED_LOW:
if rsi.current.value > 35:
return State.MIDDLE
if previous == State.TRIPPED_HIGH:
if rsi.current.value < 65:
return State.MIDDLE
return previous
class SymbolData:
'''Contains data specific to a symbol required by this model'''
def __init__(self, algorithm, symbol, period, resolution):
self.algorithm = algorithm
self.symbol = symbol
self.state = State.MIDDLE
self.rsi = RelativeStrengthIndex(period, MovingAverageType.WILDERS)
self.consolidator = algorithm.resolve_consolidator(symbol, resolution)
algorithm.register_indicator(symbol, self.rsi, self.consolidator)
def update(self, bar):
self.consolidator.update(bar)
def dispose(self):
self.algorithm.subscription_manager.remove_consolidator(self.symbol, self.consolidator)
class State(Enum):
'''Defines the state. This is used to prevent signal spamming and aid in bounce detection.'''
TRIPPED_LOW = 0
MIDDLE = 1
TRIPPED_HIGH = 2