/*
* 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);
}
}
}
}