/* * 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 { /// /// Implementation of On-Line Moving Average Reversion (OLMAR) /// /// 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 /// Using windowSize = 1 => Passive Aggressive Mean Reversion (PAMR) Portfolio public class MeanReversionPortfolioConstructionModel : PortfolioConstructionModel { private int _numOfAssets; private double[] _weightVector; private decimal _reversionThreshold; private int _windowSize; private Resolution _resolution; private Dictionary _symbolData = new(); /// /// Initializes a new instance of the class /// /// The date rules used to define the next expected rebalance time /// in UTC /// Specifies the bias of the portfolio (Short, Long/Short, Long) /// Reversion threshold /// Window size of mean price /// The resolution of the history price and rebalancing 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) { } /// /// Initializes a new instance of the class /// /// Rebalancing frequency /// Specifies the bias of the portfolio (Short, Long/Short, Long) /// Reversion threshold /// Window size of mean price /// The resolution of the history price and rebalancing 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) { } /// /// Initializes a new instance of the class /// /// Rebalancing frequency /// Specifies the bias of the portfolio (Short, Long/Short, Long) /// Reversion threshold /// Window size of mean price /// The resolution of the history price and rebalancing 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) { } /// /// Initializes a new instance of the class /// /// 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 /// Specifies the bias of the portfolio (Short, Long/Short, Long) /// Reversion threshold /// Window size of mean price /// The resolution of the history price and rebalancing public MeanReversionPortfolioConstructionModel(PyObject rebalance, PortfolioBias portfolioBias = PortfolioBias.LongShort, decimal reversionThreshold = 1, int windowSize = 20, Resolution resolution = Resolution.Daily) : this((Func)null, portfolioBias, reversionThreshold, windowSize, resolution) { SetRebalancingFunc(rebalance); } /// /// Initializes a new instance of the class /// /// 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. /// Specifies the bias of the portfolio (Short, Long/Short, Long) /// Reversion threshold /// Window size of mean price /// The resolution of the history price and rebalancing public MeanReversionPortfolioConstructionModel(Func rebalancingFunc, PortfolioBias portfolioBias = PortfolioBias.LongShort, decimal reversionThreshold = 1, int windowSize = 20, Resolution resolution = Resolution.Daily) : this(rebalancingFunc != null ? (Func)(timeUtc => rebalancingFunc(timeUtc)) : null, portfolioBias, reversionThreshold, windowSize, resolution) { } /// /// Initializes a new instance of the class /// /// 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. /// Specifies the bias of the portfolio (Short, Long/Short, Long) /// Reversion threshold /// Window size of mean price /// The resolution of the history price and rebalancing public MeanReversionPortfolioConstructionModel(Func 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; } /// /// Will determine the target percent for each insight /// /// list of active insights /// dictionary of insight and respective target weight protected override Dictionary DetermineTargetPercent(List activeInsights) { var targets = new Dictionary(); // 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; } /// /// Get price relatives with reference level of SMA /// /// list of active insights /// array of price relatives vector protected virtual double[] GetPriceRelatives(List 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; } /// /// Event fired each time the we add/remove securities from the data feed /// /// The algorithm instance that experienced the change in securities /// The security additions and removals from the algorithm 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)); } } } /// /// Cumulative Sum of a given sequence /// /// sequence to obtain cumulative sum /// cumulative sum public static IEnumerable CumulativeSum(IEnumerable sequence) { double sum = 0; foreach(var item in sequence) { sum += item; yield return sum; } } /// /// Normalize the updated portfolio into weight vector: /// v_{t+1} = arg min || v - v_{t+1} || ^ 2 /// /// 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). /// unnormalized weight vector /// regulator, default to be 1, making it a probabilistic simplex /// normalized weight vector public static double[] SimplexProjection(IEnumerable 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); } } } }