Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions Algorithm.CSharp/FutureOptionChainFiltersRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* 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.Data;
using QuantConnect.Data.Market;
using QuantConnect.Interfaces;
using QuantConnect.Securities;

namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Regression algorithm using the strike, expiration and moneyness filters on future options: in the universe selection
/// of the future and of its options, on the chains of the <see cref="Slice"/> and on <see cref="QCAlgorithm.OptionChain(Symbol, bool)"/>
/// </summary>
public class FutureOptionChainFiltersRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
private static readonly DateTime MarchExpiry = new(2020, 3, 20);
private static readonly decimal[] SelectedStrikes = [3200m, 3210m, 3220m, 3230m, 3240m, 3250m];

private Symbol _es;
private bool _chainSeen;
private bool _traded;

public override void Initialize()
{
SetStartDate(2020, 1, 5);
SetEndDate(2020, 1, 6);
SetCash(1000000);

// The March 2020 future, by its expiration date
var es = AddFuture(Futures.Indices.SP500EMini, Resolution.Minute, Market.CME);
es.SetFilter(universe => universe.Expiration([MarchExpiry]));
_es = es.Symbol;

// Its options: the out of the money contracts within three strikes of the future price
AddFutureOption(_es, universe => universe.Strikes(-3, 3).OutOfTheMoney());

// The option chain of the March future from the universe data: one expiration, the future at 3223.75
var chain = OptionChain(QuantConnect.Symbol.CreateFuture(Futures.Indices.SP500EMini, Market.CME, MarchExpiry));
if (chain.Count == 0 || chain.Underlying.Price != 3223.75m || chain.Symbol.SecurityType != SecurityType.FutureOption)
{
throw new RegressionTestException($"Expected the March ES option chain at 3223.75 but got {chain.Count} contracts at {chain.Underlying.Price}");
}
// Strikes are 10 points apart around the money: three each side of 3223.75 are 3200 to 3250
AssertStrikes(chain.Strikes(-3, 3).OutOfTheMoney().CallsOnly(), "Strikes(-3, 3).OutOfTheMoney().CallsOnly()", 3230m, 3240m, 3250m);
AssertStrikes(chain.Strikes(-3, 3).OutOfTheMoney().PutsOnly(), "Strikes(-3, 3).OutOfTheMoney().PutsOnly()", 3200m, 3210m, 3220m);
// Only the put is listed at 3310
AssertStrikes(chain.StrikesAbove(3300m).StrikesBelow(3320m), "StrikesAbove(3300).StrikesBelow(3320)", 3310m);
AssertStrikes(chain.StrikesAbove(3300m).StrikesBelow(3320m).CallsOnly(), "StrikesAbove(3300).StrikesBelow(3320).CallsOnly()");
// The strikes on either side of 3223.75 are 3220 and 3230; within 5 points only 3220
AssertStrikes(chain.AtTheMoney(), "AtTheMoney()", 3220m, 3220m, 3230m, 3230m);
AssertStrikes(chain.AtTheMoney(5m), "AtTheMoney(5)", 3220m, 3220m);
if (chain.AtTheMoney(0).Count != 0 || chain.Expiration([MarchExpiry]).Count != chain.Count || chain.FarthestExpiration().Count != chain.Count
|| chain.ExpiringAfter(MarchExpiry).Count != 0 || chain.ZeroDte().Count != 0
|| chain.StandardsOnly().Count != chain.Count || chain.WeeklysOnly().Count != 0)
{
throw new RegressionTestException("Expiration or contract type filters mismatch on the March ES option chain");
}
}

public override void OnData(Slice slice)
{
// One chain per future contract, keyed by its canonical option symbol
foreach (var chain in slice.OptionChains.Values)
{
if (chain.Symbol.Underlying.ID.Date != MarchExpiry)
{
throw new RegressionTestException($"Unexpected option chain for {chain.Symbol.Underlying}");
}
_chainSeen = true;

// The universe selected the out of the money contracts within three strikes of the previous close: 3200 to 3250
if (chain.Count == 0 || chain.Strikes(SelectedStrikes).Count != chain.Count || chain.Expiration([MarchExpiry]).Count != chain.Count)
{
throw new RegressionTestException($"The option chain disagrees with the universe filter: {string.Join(", ", chain.Select(x => x.Symbol.Value))}");
}

// The moneyness filters partition the chain around the current future price, and match the strike bounds for a single right
var price = chain.Underlying.Price;
var otm = chain.OutOfTheMoney();
var itm = chain.InTheMoney();
if (otm.Count + itm.Count + chain.Strikes([price]).Count != chain.Count
|| otm.Any(x => x.Right == OptionRight.Call ? x.Strike <= price : x.Strike >= price)
|| itm.Any(x => x.Right == OptionRight.Call ? x.Strike >= price : x.Strike <= price)
|| chain.CallsOnly().OutOfTheMoney().Count != chain.CallsOnly().StrikesAbove(price).Count
|| chain.PutsOnly().OutOfTheMoney().Count != chain.PutsOnly().StrikesBelow(price).Count)
{
throw new RegressionTestException($"Moneyness filters mismatch at {price}");
}

// Buy the out of the money call closest to the future price
if (!_traded)
{
var contract = otm.CallsOnly().OrderBy(x => x.Strike).FirstOrDefault();
if (contract != null)
{
MarketOrder(contract.Symbol, 1);
_traded = true;
}
}
}
}

public override void OnEndOfAlgorithm()
{
if (!_chainSeen || !_traded)
{
throw new RegressionTestException($"Expected the March ES option chain ({_chainSeen}) and a trade ({_traded})");
}
}

private static void AssertStrikes(OptionChain chain, string filter, params decimal[] expected)
{
var actual = chain.Select(x => x.Strike).OrderBy(x => x).ToList();
if (!actual.SequenceEqual(expected.OrderBy(x => x)))
{
throw new RegressionTestException($"{filter}: expected strikes {string.Join(", ", expected)} but got {string.Join(", ", actual)}");
}
}

/// <summary>
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
/// </summary>
public bool CanRunLocally { get; } = true;

/// <summary>
/// This is used by the regression test system to indicate which languages this algorithm is written in.
/// </summary>
public virtual List<Language> Languages { get; } = new() { Language.CSharp, Language.Python };

/// <summary>
/// Data Points count of all timeslices of algorithm
/// </summary>
public long DataPoints => 7888;

/// <summary>
/// Data Points count of the algorithm history
/// </summary>
public int AlgorithmHistoryDataPoints => 1;

/// <summary>
/// Final status of the algorithm
/// </summary>
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;

/// <summary>
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
/// </summary>
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "1"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "0%"},
{"Drawdown", "0%"},
{"Expectancy", "0"},
{"Start Equity", "1000000"},
{"End Equity", "1000586.08"},
{"Net Profit", "0%"},
{"Sharpe Ratio", "0"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "0%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0"},
{"Beta", "0"},
{"Annual Standard Deviation", "0"},
{"Annual Variance", "0"},
{"Information Ratio", "0"},
{"Tracking Error", "0"},
{"Treynor Ratio", "0"},
{"Total Fees", "$1.42"},
{"Estimated Strategy Capacity", "$6900000.00"},
{"Lowest Capacity Asset", "ES XCZJLDR35F50|ES XCZJLC9NOB29"},
{"Portfolio Turnover", "0.18%"},
{"Drawdown Recovery", "0"},
{"OrderListHash", "8786bed30a9a11b79580196098932f23"}
};
}
}
161 changes: 161 additions & 0 deletions Algorithm.CSharp/FutureUniverseFiltersRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* 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.Data;
using QuantConnect.Interfaces;
using QuantConnect.Securities;

namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Regression algorithm using the expiration set and bound filters in the futures universe selection,
/// the same ones the option universes and chains offer, and checking the selected chains in the <see cref="Slice"/>
/// </summary>
public class FutureUniverseFiltersRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
private static readonly DateTime EndOf2013 = new(2013, 12, 31);
private static readonly DateTime EndOfNovember2014 = new(2014, 11, 30);

private Symbol _es;
private Symbol _gc;
private bool _esChainSeen;
private bool _gcChainSeen;
private bool _traded;

public override void Initialize()
{
SetStartDate(2013, 10, 7);
SetEndDate(2013, 10, 9);
SetCash(1000000);

// The 2014 contracts up to September
var es = AddFuture(Futures.Indices.SP500EMini, Resolution.Minute, Market.CME);
es.SetFilter(universe => universe.ExpiringAfter(EndOf2013).ExpiringBefore(EndOfNovember2014));
_es = es.Symbol;

// The contracts expiring this year
var gc = AddFuture(Futures.Metals.Gold, Resolution.Minute, Market.COMEX);
gc.SetFilter(universe => universe.ExpiringBefore(new DateTime(2014, 1, 1)));
_gc = gc.Symbol;

// The full chain from the universe data lists the December 2013 contract and the March to December 2014 ones
var chain = FuturesChain(_es);
var expiries = chain.Select(x => x.Expiry).OrderBy(x => x).ToList();
if (expiries.Count != 5 || expiries[0] > EndOf2013 || expiries.Skip(1).Any(x => x.Year != 2014))
{
throw new RegressionTestException($"Unexpected ES chain expiries: {string.Join(", ", expiries)}");
}
}

public override void OnData(Slice slice)
{
if (slice.FuturesChains.TryGetValue(_es, out var esChain))
{
_esChainSeen = true;
// March, June and September 2014
if (esChain.Count == 0 || esChain.Count > 3 || esChain.Any(x => x.Expiry <= EndOf2013 || x.Expiry >= EndOfNovember2014))
{
throw new RegressionTestException($"The ES chain disagrees with the universe filter: {string.Join(", ", esChain.Select(x => x.Expiry))}");
}
if (!_traded)
{
MarketOrder(esChain.OrderBy(x => x.Expiry).First().Symbol, 1);
_traded = true;
}
}

if (slice.FuturesChains.TryGetValue(_gc, out var gcChain))
{
_gcChainSeen = true;
// October, November and December 2013
if (gcChain.Count == 0 || gcChain.Count > 3 || gcChain.Any(x => x.Expiry.Year != 2013))
{
throw new RegressionTestException($"The GC chain disagrees with the universe filter: {string.Join(", ", gcChain.Select(x => x.Expiry))}");
}
}
}

public override void OnEndOfAlgorithm()
{
if (!_esChainSeen || !_gcChainSeen || !_traded)
{
throw new RegressionTestException($"Expected the ES chain ({_esChainSeen}), the GC chain ({_gcChainSeen}) and a trade ({_traded})");
}
}

/// <summary>
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
/// </summary>
public bool CanRunLocally { get; } = true;

/// <summary>
/// This is used by the regression test system to indicate which languages this algorithm is written in.
/// </summary>
public virtual List<Language> Languages { get; } = new() { Language.CSharp, Language.Python };

/// <summary>
/// Data Points count of all timeslices of algorithm
/// </summary>
public long DataPoints => 38894;

/// <summary>
/// Data Points count of the algorithm history
/// </summary>
public int AlgorithmHistoryDataPoints => 1;

/// <summary>
/// Final status of the algorithm
/// </summary>
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;

/// <summary>
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
/// </summary>
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "1"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "-11.911%"},
{"Drawdown", "0.200%"},
{"Expectancy", "0"},
{"Start Equity", "1000000"},
{"End Equity", "998958.2"},
{"Net Profit", "-0.104%"},
{"Sharpe Ratio", "-9.32"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "0%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "-0.048"},
{"Beta", "0.095"},
{"Annual Standard Deviation", "0.013"},
{"Annual Variance", "0"},
{"Information Ratio", "5.187"},
{"Tracking Error", "0.123"},
{"Treynor Ratio", "-1.269"},
{"Total Fees", "$2.15"},
{"Estimated Strategy Capacity", "$940000000.00"},
{"Lowest Capacity Asset", "ES VP274HSU1AF5"},
{"Portfolio Turnover", "2.77%"},
{"Drawdown Recovery", "0"},
{"OrderListHash", "3b6b723d50c0d435d763aa456af197a6"}
};
}
}
Loading
Loading