using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Data;
using QuantConnect.Indicators;
using QuantConnect.Orders.Fees;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Algorithm.CSharp.Cryptos
{
/// <summary>
/// Given two cryptos highly correlated and traded in higher volume at GDAX - the stategy trades/ corrects any short term price deviations
/// of the 2 crypto instruments from its mean-value. Using a Simple Moving Average indicator, we can compare the value of this portfolio
/// against its SMA and generate insights to buy the under-valued symbol and sell the over-valued symbol.
/// </summary>
public class ShareClassMeanReversionAlpha : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2019, 1, 1);
SetEndDate(2019, 3, 1);
SetCash(1000000);
SetWarmUp(50);
// Setup Universe settings and tickers to be used
var symbols = new[] { "ETHUSD", "ETHEUR" }
.Select(x => QuantConnect.Symbol.Create(x, SecurityType.Crypto, Market.GDAX));
// Set requested data resolution
UniverseSettings.Resolution = Resolution.Second;
SetUniverseSelection(new ManualUniverseSelectionModel(symbols));
// Use ShareClassMeanReversionAlphaModel to establish insights
SetAlpha(new ShareClassMeanReversionAlphaModel(symbols));
// Equally weigh securities in portfolio, based on insights
SetPortfolioConstruction(new EqualWeightingPortfolioConstructionModel());
// Set Immediate Execution Model
SetExecution(new ImmediateExecutionModel());
//monitors portfolio holdings, and when extended beyond a predefined drawdown limit, it liquidates the portfolio.
//SetRiskManagement(new MaximumDrawdownPercentPerSecurity(0.25m));
//Lean uses a constant fee rate of 0.25% (Taker) and 0.15% (Maker) that Coinbase Pro applies for a 30-day volume of $50k - $100k
// where feePercentage is either 0.0025 or 0.0015
//var fee = unitPrice * order.AbsoluteQuantity * feePercentage;
Schedule.On(DateRules.Every(DayOfWeek.Monday, DayOfWeek.Sunday), TimeRules.At(19, 15), () =>
{
Log("Mon/Fri at 17:55pm: Fired at: " + Time);
});
}
private class ShareClassMeanReversionAlphaModel : AlphaModel
{
private const double _insightMagnitude =0.001;
private readonly Symbol _longSymbol;
private readonly Symbol _shortSymbol;
private readonly TimeSpan _insightPeriod;
private readonly SimpleMovingAverage _sma;
private readonly RollingWindow<decimal> _positionWindow;
private decimal _alpha;
private decimal _beta;
private bool _invested;
public ShareClassMeanReversionAlphaModel(
IEnumerable<Symbol> symbols,
Resolution resolution = Resolution.Hour)
{
if (symbols.Count() != 2)
{
throw new ArgumentException("ShareClassMeanReversionAlphaModel: symbols parameter must contain 2 elements");
}
_longSymbol = symbols.ToArray()[0];
_shortSymbol = symbols.ToArray()[1];
_insightPeriod = resolution.ToTimeSpan().Multiply(5);
_sma = new SimpleMovingAverage(2);
_positionWindow = new RollingWindow<decimal>(2);
}
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
// Check to see if either ticker will return a NoneBar, and skip the data slice if so
if (data.Bars.Count < 2)
{
return Enumerable.Empty<Insight>();
}
// If Alpha and Beta haven't been calculated yet, then do so
if (_alpha == 0 || _beta == 0)
{
CalculateAlphaBeta(algorithm);
}
// Update indicator and Rolling Window for each data slice passed into Update() method **************************************
if (!UpdateIndicators(data))
{
return Enumerable.Empty<Insight>();
}
// Check to see if the portfolio is invested. If no, then perform value comparisons and emit insights accordingly
if (!_invested)
{
//Reset invested boolean
_invested = true;
if (_positionWindow[0] > _sma)
{
return Insight.Group(new[]
{
Insight.Price(_longSymbol, _insightPeriod, InsightDirection.Down, _insightMagnitude),
Insight.Price(_shortSymbol, _insightPeriod, InsightDirection.Up, _insightMagnitude),
});
}
else
{
return Insight.Group(new[]
{
Insight.Price(_longSymbol, _insightPeriod, InsightDirection.Up, _insightMagnitude),
Insight.Price(_shortSymbol, _insightPeriod, InsightDirection.Down, _insightMagnitude),
});
}
}
// If the portfolio is invested and crossed back over the SMA, then emit flat insights
else if (_invested && CrossedMean())
{
_invested = false;
}
return Enumerable.Empty<Insight>();
}
/// <summary>
/// Calculate Alpha and Beta, the initial number of shares for each security needed to achieve a 50/50 weighting
/// </summary>
/// <param name="algorithm"></param>
private void CalculateAlphaBeta(QCAlgorithm algorithm)
{
_alpha = algorithm.CalculateOrderQuantity(_longSymbol, 0.5);
_beta = algorithm.CalculateOrderQuantity(_shortSymbol, 0.5);
algorithm.Log($"{algorithm.Time} :: Alpha: {_alpha} Beta: {_beta}");
}
/// <summary>
/// Calculate position value and update the SMA indicator and Rolling Window
/// </summary>
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private bool UpdateIndicators(Slice data)
{
var positionValue = (_alpha * data[_longSymbol].Close) - (_beta * data[_shortSymbol].Close);
_sma.Update(data[_longSymbol].EndTime, positionValue);
_positionWindow.Add(positionValue);
return _sma.IsReady && _positionWindow.IsReady;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Check to see if the position value has crossed the SMA and then return a boolean value
/// </summary>
/// <returns></returns>
private bool CrossedMean()
{
return (_positionWindow[0] >= _sma && _positionWindow[1] < _sma)
|| (_positionWindow[1] >= _sma && _positionWindow[0] < _sma);
}
}
}
}