/*
* 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.Interfaces;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Market;
using QuantConnect.Securities;
using QuantConnect.Data;
using QuantConnect.Indicators;
using MathNet.Numerics.Statistics;
namespace QuantConnect.Orders.Slippage
{
///
/// Slippage model that mimic the effect brought by market impact,
/// i.e. consume the volume listed in the order book
///
/// Almgren, R., Thum, C., Hauptmann, E., and Li, H. (2005).
/// Direct estimation of equity market impact. Risk, 18(7), 58-62.
/// Available from: https://www.ram-ai.com/sites/default/files/2022-06/costestim.pdf
/// The default parameters are calibrated around 2 decades ago,
/// the trading time effect is not accounted (volume near market open/close is larger),
/// the market regime is not taken into account,
/// and the market environment does not have many market makers at that time,
/// so it is recommend to recalibrate with reference to the original paper.
public class MarketImpactSlippageModel : ISlippageModel
{
private readonly IAlgorithm _algorithm;
private readonly bool _nonNegative;
private readonly double _latency;
private readonly double _impactTime;
private readonly double _alpha;
private readonly double _beta;
private readonly double _gamma;
private readonly double _eta;
private readonly double _delta;
private readonly Random _random;
private SymbolData _symbolData;
///
/// Instantiate a new instance of MarketImpactSlippageModel
///
/// IAlgorithm instance
/// Indicator whether only non-negative slippage allowed
/// Time between order submitted and filled, in seconds(s)
/// Time between order filled and new equilibrium established, in second(s)
/// Exponent of the permanent impact function
/// Exponent of the temporary impact function
/// Coefficient of the permanent impact function
/// Coefficient of the temporary impact function
/// Liquidity scaling factor for permanent impact
/// Random seed for generating gaussian noise
public MarketImpactSlippageModel(IAlgorithm algorithm, bool nonNegative = true, double latency = 0.075d,
double impactTime = 1800d, double alpha = 0.891d, double beta = 0.600d,
double gamma = 0.314d, double eta = 0.142d, double delta = 0.267d,
int randomSeed = 50)
{
if (latency <= 0)
{
throw new Exception("Latency cannot be less than or equal to 0.");
}
if (impactTime <= 0)
{
throw new Exception("impactTime cannot be less than or equal to 0.");
}
_algorithm = algorithm;
_nonNegative = nonNegative;
_latency = latency;
_impactTime = impactTime;
_alpha = alpha;
_beta = beta;
_gamma = gamma;
_eta = eta;
_delta = delta;
_random = new(randomSeed);
}
///
/// Slippage Model. Return a decimal cash slippage approximation on the order.
///
public decimal GetSlippageApproximation(Security asset, Order order)
{
if (asset.Type == SecurityType.Forex || asset.Type == SecurityType.Cfd)
{
throw new Exception($"Asset of {asset.Type} is not supported as MarketImpactSlippageModel requires volume data");
}
if (_symbolData == null)
{
_symbolData = new SymbolData(_algorithm, asset, _latency, _impactTime);
}
if (_symbolData.AverageVolume == 0d)
{
return 0m;
}
// normalized volume of execution
var nu = (double)order.AbsoluteQuantity / _symbolData.ExecutionTime / _symbolData.AverageVolume;
// liquidity adjustment for temporary market impact, if any
var liquidityAdjustment = asset.Fundamentals.HasFundamentalData && asset.Fundamentals.CompanyProfile.SharesOutstanding != default ?
Math.Pow(asset.Fundamentals.CompanyProfile.SharesOutstanding / _symbolData.AverageVolume, _delta) :
1d;
// noise adjustment factor
var noise = _symbolData.Sigma * Math.Sqrt(_symbolData.ImpactTime);
// permanent market impact
var permanentImpact = _symbolData.Sigma * _symbolData.ExecutionTime * G(nu) * liquidityAdjustment + SampleGaussian() * noise;
// temporary market impact
var temporaryImpact = _symbolData.Sigma * H(nu) + SampleGaussian() * noise;
// realized market impact
var realizedImpact = temporaryImpact + permanentImpact * 0.5d;
// estimate the slippage by temporary impact
return SlippageFromImpactEstimation(realizedImpact) * asset.Price;
}
///
/// The permanent market impact function
///
/// The absolute, normalized order quantity
/// Unadjusted permanent market impact factor
private double G(double absoluteOrderQuantity)
{
return _gamma * Math.Pow(absoluteOrderQuantity, _alpha);
}
///
/// The temporary market impact function
///
/// The absolute, normalized order quantity
/// Unadjusted temporary market impact factor
private double H(double absoluteOrderQuantity)
{
return _eta * Math.Pow(absoluteOrderQuantity, _beta);
}
///
/// Estimate the slippage size from impact
///
/// The market impact of the order
/// Slippage estimation
private decimal SlippageFromImpactEstimation(double impact)
{
// The percentage of impact that an order is averagely being affected is random from 0.0 to 1.0
var ultimateSlippage = (impact * _random.NextDouble()).SafeDecimalCast();
// Impact at max can be the asset's price
ultimateSlippage = Math.Min(ultimateSlippage, 1m);
if (_nonNegative)
{
return Math.Max(0m, ultimateSlippage);
}
return ultimateSlippage;
}
private double SampleGaussian(double location = 0d, double scale = 1d)
{
var randomVariable1 = 1 - _random.NextDouble();
var randomVariable2 = 1 - _random.NextDouble();
var deviation = Math.Sqrt(-2.0 * Math.Log(randomVariable1)) * Math.Cos(2.0 * Math.PI * randomVariable2);
return deviation * scale + location;
}
}
internal class SymbolData
{
private readonly IAlgorithm _algorithm;
private readonly Symbol _symbol;
private readonly TradeBarConsolidator _consolidator;
private readonly RollingWindow _volumes = new(10);
private readonly RollingWindow _prices = new(252);
public double Sigma { get; internal set; }
public double AverageVolume { get; internal set; }
public double ExecutionTime { get; internal set; }
public double ImpactTime { get; internal set; }
public SymbolData(IAlgorithm algorithm, Security asset, double latency, double impactTime)
{
_algorithm = algorithm;
_symbol = asset.Symbol;
_consolidator = new TradeBarConsolidator(TimeSpan.FromDays(1));
_consolidator.DataConsolidated += OnDataConsolidated;
algorithm.SubscriptionManager.AddConsolidator(_symbol, _consolidator);
var configs = algorithm
.SubscriptionManager
.SubscriptionDataConfigService
.GetSubscriptionDataConfigs(_symbol, includeInternalConfigs: true);
var configToUse = configs.Where(x => x.TickType == TickType.Trade).First();
var historyRequestFactory = new HistoryRequestFactory(algorithm);
var historyRequest = historyRequestFactory.CreateHistoryRequest(configToUse,
algorithm.Time - TimeSpan.FromDays(370),
algorithm.Time,
algorithm.Securities[_symbol].Exchange.Hours,
Resolution.Daily);
foreach (var bar in algorithm.HistoryProvider.GetHistory(new List { historyRequest }, algorithm.TimeZone))
{
_consolidator.Update(bar.Bars[_symbol]);
}
// execution time is defined as time difference between order submission and filling here,
// default with 75ms latency (https://www.interactivebrokers.com/download/salesPDFs/10-PDF0513.pdf)
// it should be in unit of "trading days", so we need to divide by normal trade day's length
var normalTradeDayLength = asset.Exchange.Hours.RegularMarketDuration.TotalDays;
ExecutionTime = TimeSpan.FromSeconds(latency).TotalDays / normalTradeDayLength;
// expected valid time for impact
var adjustedImpactTime = TimeSpan.FromSeconds(impactTime).TotalDays / normalTradeDayLength;
ImpactTime = ExecutionTime + adjustedImpactTime;
}
public void OnDataConsolidated(object _, TradeBar bar)
{
_prices.Add(bar.Close);
_volumes.Add(bar.Volume);
if (_prices.Samples < 2)
{
return;
}
var rocp = new double[_prices.Samples - 1];
for (var i = 0; i < _prices.Samples - 1; i++)
{
if (_prices[i + 1] == 0) continue;
var roc = (_prices[i] - _prices[i + 1]) / _prices[i + 1];
rocp[i] = (double)roc;
}
var variance = rocp.Variance();
Sigma = Math.Sqrt(variance);
AverageVolume = (double)_volumes.Average();
}
public void Dispose()
{
_prices.Reset();
_volumes.Reset();
_consolidator.DataConsolidated -= OnDataConsolidated;
_algorithm.SubscriptionManager.RemoveConsolidator(_symbol, _consolidator);
}
}
}