chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* 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 Accord.Math;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Algorithm.Framework.Alphas;
|
||||
using QuantConnect.Data.UniverseSelection;
|
||||
using QuantConnect.Indicators;
|
||||
using QuantConnect.Scheduling;
|
||||
using QuantConnect.Util;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of On-Line Moving Average Reversion (OLMAR)
|
||||
/// </summary>
|
||||
/// <remarks>Li, B., Hoi, S. C. (2012). On-line portfolio selection with moving average reversion. arXiv preprint arXiv:1206.4626.
|
||||
/// Available at https://arxiv.org/ftp/arxiv/papers/1206/1206.4626.pdf</remarks>
|
||||
/// <remarks>Using windowSize = 1 => Passive Aggressive Mean Reversion (PAMR) Portfolio</remarks>
|
||||
public class MeanReversionPortfolioConstructionModel : PortfolioConstructionModel
|
||||
{
|
||||
private int _numOfAssets;
|
||||
private double[] _weightVector;
|
||||
private decimal _reversionThreshold;
|
||||
private int _windowSize;
|
||||
private Resolution _resolution;
|
||||
private Dictionary<Symbol, MeanReversionSymbolData> _symbolData = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="rebalancingDateRules">The date rules used to define the next expected rebalance time
|
||||
/// in UTC</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(IDateRule rebalancingDateRules,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: this(rebalancingDateRules.ToFunc(), portfolioBias, reversionThreshold, windowSize, resolution)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="rebalanceResolution">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(Resolution rebalanceResolution = Resolution.Daily,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: this(rebalanceResolution.ToTimeSpan(), portfolioBias, reversionThreshold, windowSize, resolution)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(TimeSpan timeSpan,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: this(dt => dt.Add(timeSpan), portfolioBias, reversionThreshold, windowSize, resolution)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
|
||||
/// For a given algorithm UTC DateTime the func returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(PyObject rebalance,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: this((Func<DateTime, DateTime?>)null, portfolioBias, reversionThreshold, windowSize, resolution)
|
||||
{
|
||||
SetRebalancingFunc(rebalance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored.</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: this(rebalancingFunc != null ? (Func<DateTime, DateTime?>)(timeUtc => rebalancingFunc(timeUtc)) : null,
|
||||
portfolioBias, reversionThreshold, windowSize, resolution)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance.</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: base(rebalancingFunc)
|
||||
{
|
||||
if (portfolioBias == PortfolioBias.Short)
|
||||
{
|
||||
throw new ArgumentException("Long position must be allowed in MeanReversionPortfolioConstructionModel.");
|
||||
}
|
||||
|
||||
_reversionThreshold = reversionThreshold;
|
||||
_resolution = resolution;
|
||||
_windowSize = windowSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will determine the target percent for each insight
|
||||
/// </summary>
|
||||
/// <param name="activeInsights">list of active insights</param>
|
||||
/// <return>dictionary of insight and respective target weight</return>
|
||||
protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
|
||||
{
|
||||
var targets = new Dictionary<Insight, double>();
|
||||
|
||||
// If we have no insights or non-ready just return an empty target list
|
||||
if (activeInsights.IsNullOrEmpty() ||
|
||||
!activeInsights.All(x => _symbolData[x.Symbol].IsReady()))
|
||||
{
|
||||
return targets;
|
||||
}
|
||||
|
||||
var numOfAssets = activeInsights.Count;
|
||||
if (_numOfAssets != numOfAssets)
|
||||
{
|
||||
_numOfAssets = numOfAssets;
|
||||
// Initialize price vector and portfolio weightings vector
|
||||
_weightVector = Enumerable.Repeat((double) 1/_numOfAssets, _numOfAssets).ToArray();
|
||||
}
|
||||
|
||||
// Get price relatives vs expected price (SMA)
|
||||
var priceRelatives = GetPriceRelatives(activeInsights); // \tilde{x}_{t+1}
|
||||
|
||||
// Get step size of next portfolio
|
||||
// \bar{x}_{t+1} = 1^T * \tilde{x}_{t+1} / m
|
||||
// \lambda_{t+1} = max( 0, ( b_t * \tilde{x}_{t+1} - \epsilon ) / ||\tilde{x}_{t+1} - \bar{x}_{t+1} * 1|| ^ 2 )
|
||||
var nextPrediction = priceRelatives.Average(); // \bar{x}_{t+1}
|
||||
var assetsMeanDev = priceRelatives.Select(x => x - nextPrediction).ToArray();
|
||||
var secondNorm = Math.Pow(assetsMeanDev.Euclidean(), 2);
|
||||
double stepSize; // \lambda_{t+1}
|
||||
|
||||
if (secondNorm == 0d)
|
||||
{
|
||||
stepSize = 0d;
|
||||
}
|
||||
else
|
||||
{
|
||||
stepSize = (_weightVector.InnerProduct(priceRelatives) - (double)_reversionThreshold) / secondNorm;
|
||||
stepSize = Math.Max(0d, stepSize);
|
||||
}
|
||||
|
||||
// Get next portfolio weightings
|
||||
// b_{t+1} = b_t - step_size * ( \tilde{x}_{t+1} - \bar{x}_{t+1} * 1 )
|
||||
var nextPortfolio = _weightVector.Select((x, i) => x - assetsMeanDev[i] * stepSize);
|
||||
// Normalize
|
||||
var normalizedPortfolioWeightVector = SimplexProjection(nextPortfolio);
|
||||
// Save normalized result for the next portfolio step
|
||||
_weightVector = normalizedPortfolioWeightVector;
|
||||
|
||||
// Update portfolio state
|
||||
for (int i = 0; i < _numOfAssets; i++)
|
||||
{
|
||||
targets.Add(activeInsights[i], normalizedPortfolioWeightVector[i]);
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get price relatives with reference level of SMA
|
||||
/// </summary>
|
||||
/// <param name="activeInsights">list of active insights</param>
|
||||
/// <return>array of price relatives vector</return>
|
||||
protected virtual double[] GetPriceRelatives(List<Insight> activeInsights)
|
||||
{
|
||||
var numOfInsights = activeInsights.Count;
|
||||
|
||||
// Initialize a price vector of the next prices relatives' projection
|
||||
var nextPriceRelatives = new double[numOfInsights];
|
||||
|
||||
for (int i = 0; i < numOfInsights; i++)
|
||||
{
|
||||
var insight = activeInsights[i];
|
||||
var symbolData = _symbolData[insight.Symbol];
|
||||
|
||||
nextPriceRelatives[i] = insight.Magnitude != null ?
|
||||
1 + (double)insight.Magnitude * (int)insight.Direction:
|
||||
(double)symbolData.Identity.Current.Value / (double)symbolData.Sma.Current.Value;
|
||||
}
|
||||
|
||||
return nextPriceRelatives;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
base.OnSecuritiesChanged(algorithm, changes);
|
||||
|
||||
// clean up data for removed securities
|
||||
foreach (var removed in changes.RemovedSecurities)
|
||||
{
|
||||
_symbolData.Remove(removed.Symbol, out var symbolData);
|
||||
symbolData.Reset();
|
||||
}
|
||||
|
||||
// initialize data for added securities
|
||||
var symbols = changes.AddedSecurities.Select(x => x.Symbol);
|
||||
|
||||
foreach(var symbol in symbols)
|
||||
{
|
||||
if (!_symbolData.ContainsKey(symbol))
|
||||
{
|
||||
_symbolData.Add(symbol, new MeanReversionSymbolData(algorithm, symbol, _windowSize, _resolution));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cumulative Sum of a given sequence
|
||||
/// </summary>
|
||||
/// <param name="sequence">sequence to obtain cumulative sum</param>
|
||||
/// <return>cumulative sum</return>
|
||||
public static IEnumerable<double> CumulativeSum(IEnumerable<double> sequence)
|
||||
{
|
||||
double sum = 0;
|
||||
foreach(var item in sequence)
|
||||
{
|
||||
sum += item;
|
||||
yield return sum;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalize the updated portfolio into weight vector:
|
||||
/// v_{t+1} = arg min || v - v_{t+1} || ^ 2
|
||||
/// </summary>
|
||||
/// <remark>Duchi, J., Shalev-Shwartz, S., Singer, Y., and Chandra, T. (2008, July).
|
||||
/// Efficient projections onto the l1-ball for learning in high dimensions.
|
||||
/// In Proceedings of the 25th international conference on Machine learning (pp. 272-279).</remark>
|
||||
/// <param name="vector">unnormalized weight vector</param>
|
||||
/// <param name="total">regulator, default to be 1, making it a probabilistic simplex</param>
|
||||
/// <return>normalized weight vector</return>
|
||||
public static double[] SimplexProjection(IEnumerable<double> vector, double total = 1)
|
||||
{
|
||||
if (total <= 0)
|
||||
{
|
||||
throw new ArgumentException("Total must be > 0 for Euclidean Projection onto the Simplex.");
|
||||
}
|
||||
|
||||
// Sort v into u in descending order
|
||||
var mu = vector.OrderByDescending(x => x).ToArray();
|
||||
var sv = CumulativeSum(mu).ToArray();
|
||||
|
||||
var rho = Enumerable.Range(0, vector.Count()).Where(i => mu[i] > (sv[i] - total) / (i+1)).Last();
|
||||
var theta = (sv[rho] - total) / (rho + 1);
|
||||
var w = vector.Select(x => Math.Max(x - theta, 0d)).ToArray();
|
||||
return w;
|
||||
}
|
||||
|
||||
private class MeanReversionSymbolData
|
||||
{
|
||||
public Identity Identity;
|
||||
public SimpleMovingAverage Sma;
|
||||
|
||||
public MeanReversionSymbolData(QCAlgorithm algo, Symbol symbol, int windowSize, Resolution resolution)
|
||||
{
|
||||
// Indicator of price
|
||||
Identity = algo.Identity(symbol, resolution);
|
||||
// Moving average indicator for mean reversion level
|
||||
Sma = algo.SMA(symbol, windowSize, resolution);
|
||||
|
||||
// Warmup indicator
|
||||
algo.WarmUpIndicator(symbol, Identity, resolution);
|
||||
algo.WarmUpIndicator(symbol, Sma, resolution);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Identity.Reset();
|
||||
Sma.Reset();
|
||||
}
|
||||
|
||||
public bool IsReady()
|
||||
{
|
||||
return (Identity.IsReady & Sma.IsReady);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user