/*
* 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 Accord.Math;
using Accord.Statistics;
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.Data.Consolidators;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Indicators;
using QuantConnect.Orders.Fees;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Algorithm.CSharp.Alphas
{
///
/// Energy prices, especially Oil and Natural Gas, are in general fairly correlated,
/// meaning they typically move in the same direction as an overall trend.This Alpha
/// uses this idea and implements an Alpha Model that takes Natural Gas ETF price
/// movements as a leading indicator for Crude Oil ETF price movements.We take the
/// Natural Gas/Crude Oil ETF pair with the highest historical price correlation and
/// then create insights for Crude Oil depending on whether or not the Natural Gas ETF price change
/// is above/below a certain threshold that we set (arbitrarily).
///
/// This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open
/// sourced so the community and client funds can see an example of an alpha.
///
public class GasAndCrudeOilEnergyCorrelationAlpha : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2018, 1, 1);
SetCash(100000);
// Set zero transaction fees
SetSecurityInitializer(security => security.FeeModel = new ConstantFeeModel(0));
Func ToSymbol = x => QuantConnect.Symbol.Create(x, SecurityType.Equity, Market.USA);
var naturalGas = new[] { "UNG", "BOIL", "FCG" }.Select(ToSymbol).ToArray();
var crudeOil = new[] { "USO", "UCO", "DBO" }.Select(ToSymbol).ToArray();
// Manually curated universe
UniverseSettings.Resolution = Resolution.Minute;
SetUniverseSelection(new ManualUniverseSelectionModel(naturalGas.Concat(crudeOil)));
// Use PairsAlphaModel to establish insights
SetAlpha(new PairsAlphaModel(naturalGas, crudeOil, 90, Resolution.Minute));
// Equally weigh securities in portfolio, based on insights
SetPortfolioConstruction(new EqualWeightingPortfolioConstructionModel());
// Set Custom Execution Model
SetExecution(new CustomExecutionModel());
// Set Null Risk Management Model
SetRiskManagement(new NullRiskManagementModel());
}
///
/// This Alpha model assumes that the ETF for natural gas is a good leading-indicator
/// of the price of the crude oil ETF.The model will take in arguments for a threshold
/// at which the model triggers an insight, the length of the look-back period for evaluating
/// rate-of-change of UNG prices, and the duration of the insight
///
private class PairsAlphaModel : AlphaModel
{
private readonly Symbol[] _leading;
private readonly Symbol[] _following;
private readonly int _historyDays;
private readonly int _lookback;
private readonly decimal _differenceTrigger = 0.75m;
private readonly Resolution _resolution;
private readonly TimeSpan _predictionInterval;
private readonly Dictionary _symbolDataBySymbol;
private Tuple _pair;
private DateTime _nextUpdate;
public PairsAlphaModel(
Symbol[] naturalGas,
Symbol[] crudeOil,
int historyDays = 90,
Resolution resolution = Resolution.Hour,
int lookback = 5,
decimal differenceTrigger = 0.75m)
{
_leading = naturalGas;
_following = crudeOil;
_historyDays = historyDays;
_resolution = resolution;
_lookback = lookback;
_differenceTrigger = differenceTrigger;
_symbolDataBySymbol = new Dictionary();
_predictionInterval = resolution.ToTimeSpan().Multiply(lookback);
}
public override IEnumerable Update(QCAlgorithm algorithm, Slice data)
{
if (_nextUpdate == DateTime.MinValue || algorithm.Time > _nextUpdate)
{
CorrelationPairsSelection();
_nextUpdate = algorithm.Time.AddDays(30);
}
var magnitude = (double)Math.Round(_pair.Item1.Return / 100, 6);
if (_pair.Item1.Return > _differenceTrigger)
{
yield return Insight.Price(_pair.Item2.Symbol, _predictionInterval, InsightDirection.Up, magnitude);
}
if (_pair.Item1.Return < -_differenceTrigger)
{
yield return Insight.Price(_pair.Item2.Symbol, _predictionInterval, InsightDirection.Down, magnitude);
}
}
public void CorrelationPairsSelection()
{
var maxCorrelation = -1.0;
var matrix = new double[_historyDays, _following.Length + 1];
// Get returns for each oil ETF
for (var j = 0; j < _following.Length; j++)
{
SymbolData symbolData2;
if (_symbolDataBySymbol.TryGetValue(_following[j], out symbolData2))
{
var dailyReturn2 = symbolData2.DailyReturnArray;
for (var i = 0; i < _historyDays; i++)
{
matrix[i, j + 1] = symbolData2.DailyReturnArray[i];
}
}
}
// Get returns for each natural gas ETF
for (var j = 0; j < _leading.Length; j++)
{
SymbolData symbolData1;
if (_symbolDataBySymbol.TryGetValue(_leading[j], out symbolData1))
{
for (var i = 0; i < _historyDays; i++)
{
matrix[i, 0] = symbolData1.DailyReturnArray[i];
}
var column = matrix.Correlation().GetColumn(0);
var correlation = column.RemoveAt(0).Max();
// Calculate the pair with highest historical correlation
if (correlation > maxCorrelation)
{
var maxIndex = column.IndexOf(correlation) - 1;
if (maxIndex < 0) continue;
var symbolData2 = _symbolDataBySymbol[_following[maxIndex]];
_pair = Tuple.Create(symbolData1, symbolData2);
maxCorrelation = correlation;
}
}
}
}
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
foreach (var removed in changes.RemovedSecurities)
{
if (_symbolDataBySymbol.ContainsKey(removed.Symbol))
{
_symbolDataBySymbol[removed.Symbol].RemoveConsolidators(algorithm);
_symbolDataBySymbol.Remove(removed.Symbol);
}
}
// Initialize data for added securities
var symbols = changes.AddedSecurities.Select(x => x.Symbol);
var dailyHistory = algorithm.History(symbols, _historyDays + 1, Resolution.Daily);
if (symbols.Count() > 0 && dailyHistory.Count() == 0)
{
algorithm.Debug($"{algorithm.Time} :: No daily data");
}
dailyHistory.PushThrough(bar =>
{
SymbolData symbolData;
if (!_symbolDataBySymbol.TryGetValue(bar.Symbol, out symbolData))
{
symbolData = new SymbolData(algorithm, bar.Symbol, _historyDays, _lookback, _resolution);
_symbolDataBySymbol.Add(bar.Symbol, symbolData);
}
// Update daily rate of change indicator
symbolData.UpdateDailyRateOfChange(bar);
});
algorithm.History(symbols, _lookback, _resolution).PushThrough(bar =>
{
// Update rate of change indicator with given resolution
if (_symbolDataBySymbol.ContainsKey(bar.Symbol))
{
_symbolDataBySymbol[bar.Symbol].UpdateRateOfChange(bar);
}
});
}
///
/// Contains data specific to a symbol required by this model
///
private class SymbolData
{
private readonly RateOfChangePercent _dailyReturn;
private readonly IDataConsolidator _dailyConsolidator;
private readonly RollingWindow _dailyReturnHistory;
private readonly IDataConsolidator _consolidator;
public Symbol Symbol { get; }
public RateOfChangePercent Return { get; }
public double[] DailyReturnArray => _dailyReturnHistory
.OrderBy(x => x.EndTime)
.Select(x => (double)x.Value).ToArray();
public SymbolData(QCAlgorithm algorithm, Symbol symbol, int dailyLookback, int lookback, Resolution resolution)
{
Symbol = symbol;
_dailyReturn = new RateOfChangePercent($"{symbol}.DailyROCP(1)", 1);
_dailyConsolidator = algorithm.ResolveConsolidator(symbol, Resolution.Daily);
_dailyReturnHistory = new RollingWindow(dailyLookback);
_dailyReturn.Updated += (s, e) => _dailyReturnHistory.Add(e);
algorithm.RegisterIndicator(symbol, _dailyReturn, _dailyConsolidator);
Return = new RateOfChangePercent($"{symbol}.ROCP({lookback})", lookback);
_consolidator = algorithm.ResolveConsolidator(symbol, resolution);
algorithm.RegisterIndicator(symbol, Return, _consolidator);
}
public void RemoveConsolidators(QCAlgorithm algorithm)
{
algorithm.SubscriptionManager.RemoveConsolidator(Symbol, _consolidator);
algorithm.SubscriptionManager.RemoveConsolidator(Symbol, _dailyConsolidator);
}
public void UpdateRateOfChange(BaseData data)
{
Return.Update(data.EndTime, data.Value);
}
internal void UpdateDailyRateOfChange(BaseData data)
{
_dailyReturn.Update(data.EndTime, data.Value);
}
public override string ToString() => Return.ToDetailedString();
}
}
///
/// Provides an implementation of IExecutionModel that immediately submits market orders to achieve the desired portfolio targets
///
private class CustomExecutionModel : ExecutionModel
{
private readonly PortfolioTargetCollection _targetsCollection = new PortfolioTargetCollection();
private Symbol _previousSymbol;
///
/// Immediately submits orders for the specified portfolio targets.
///
/// The algorithm instance
/// The portfolio targets to be ordered
public override void Execute(QCAlgorithm algorithm, IPortfolioTarget[] targets)
{
_targetsCollection.AddRange(targets);
foreach (var target in _targetsCollection.OrderByMarginImpact(algorithm))
{
var openQuantity = algorithm.Transactions.GetOpenOrders(target.Symbol)
.Sum(x => x.Quantity);
var existing = algorithm.Securities[target.Symbol].Holdings.Quantity + openQuantity;
var quantity = target.Quantity - existing;
// Liquidate positions in Crude Oil ETF that is no longer part of the highest-correlation pair
if (_previousSymbol != null && target.Symbol != _previousSymbol)
{
algorithm.Liquidate(_previousSymbol);
}
if (quantity != 0)
{
algorithm.MarketOrder(target.Symbol, quantity);
_previousSymbol = target.Symbol;
}
}
_targetsCollection.ClearFulfilled(algorithm);
}
}
}
}