|
| 1 | +/* |
| 2 | + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. |
| 3 | + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. |
| 4 | + * |
| 5 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | + * you may not use this file except in compliance with the License. |
| 7 | + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | + * |
| 9 | + * Unless required by applicable law or agreed to in writing, software |
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | + * See the License for the specific language governing permissions and |
| 13 | + * limitations under the License. |
| 14 | +*/ |
| 15 | + |
| 16 | +using System; |
| 17 | +using System.Collections.Generic; |
| 18 | +using System.Linq; |
| 19 | +using QuantConnect.Data; |
| 20 | +using QuantConnect.Data.Market; |
| 21 | +using QuantConnect.Interfaces; |
| 22 | +using QuantConnect.Securities; |
| 23 | + |
| 24 | +namespace QuantConnect.Algorithm.CSharp |
| 25 | +{ |
| 26 | + /// <summary> |
| 27 | + /// Regression algorithm using the futures chain filters, the same ones the futures universe selection offers, |
| 28 | + /// on <see cref="QCAlgorithm.FuturesChain(Symbol, bool)"/> and on the chains of the <see cref="Slice"/> |
| 29 | + /// </summary> |
| 30 | + public class FuturesChainFiltersRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition |
| 31 | + { |
| 32 | + private static readonly DateTime EndOf2013 = new(2013, 12, 31); |
| 33 | + |
| 34 | + private Symbol _es; |
| 35 | + private Symbol _gc; |
| 36 | + private bool _esChainSeen; |
| 37 | + private bool _gcChainSeen; |
| 38 | + private bool _traded; |
| 39 | + |
| 40 | + public override void Initialize() |
| 41 | + { |
| 42 | + SetStartDate(2013, 10, 7); |
| 43 | + SetEndDate(2013, 10, 9); |
| 44 | + SetCash(1000000); |
| 45 | + |
| 46 | + // The contracts expiring within a year |
| 47 | + var es = AddFuture(Futures.Indices.SP500EMini, Resolution.Minute, Market.CME); |
| 48 | + es.SetFilter(universe => universe.Expiration(0, 365)); |
| 49 | + _es = es.Symbol; |
| 50 | + |
| 51 | + // The liquid contracts, by open interest |
| 52 | + var gc = AddFuture(Futures.Metals.Gold, Resolution.Minute, Market.COMEX); |
| 53 | + gc.SetFilter(universe => universe.OpenInterest(100000, long.MaxValue)); |
| 54 | + _gc = gc.Symbol; |
| 55 | + |
| 56 | + // The full chain from the universe data: December 2013 and March, June, September and December 2014 |
| 57 | + var chain = FuturesChain(_es); |
| 58 | + if (chain.Count != 5) |
| 59 | + { |
| 60 | + throw new RegressionTestException($"Expected 5 ES contracts but got {chain.Count}"); |
| 61 | + } |
| 62 | + AssertExpiries(chain.FrontMonth(), "FrontMonth()", (2013, 12)); |
| 63 | + AssertExpiries(chain.BackMonth(), "BackMonth()", (2014, 3)); |
| 64 | + AssertExpiries(chain.BackMonths(), "BackMonths()", (2014, 3), (2014, 6), (2014, 9), (2014, 12)); |
| 65 | + AssertExpiries(chain.FarthestExpiration(), "FarthestExpiration()", (2014, 12)); |
| 66 | + AssertExpiries(chain.ExpirationCycle([3, 9]), "ExpirationCycle([3, 9])", (2014, 3), (2014, 9)); |
| 67 | + // ES contracts are named after their expiration month, so the contract month filters agree with the expiration ones |
| 68 | + AssertExpiries(chain.ContractMonths([3, 9]), "ContractMonths([3, 9])", (2014, 3), (2014, 9)); |
| 69 | + AssertExpiries(chain.ExpiringBefore(EndOf2013), "ExpiringBefore(2013-12-31)", (2013, 12)); |
| 70 | + AssertExpiries(chain.ExpiringAfter(EndOf2013).ExpiringBefore(new DateTime(2014, 7, 1)), "ExpiringAfter(2013-12-31).ExpiringBefore(2014-07-01)", (2014, 3), (2014, 6)); |
| 71 | + AssertExpiries(chain.Expiration([chain.FrontMonth().First().Expiry]), "Expiration([front month expiry])", (2013, 12)); |
| 72 | + if (chain.ZeroDte().Count != 0 || chain.StandardsOnly().Count != chain.Count || chain.WeeklysOnly().Count != 0) |
| 73 | + { |
| 74 | + throw new RegressionTestException("Expected no contract expiring today and only standard contracts"); |
| 75 | + } |
| 76 | + |
| 77 | + // The liquidity filters read the universe data: only the front month has more than a million contracts open |
| 78 | + AssertExpiries(chain.OpenInterest(1000000, long.MaxValue), "OpenInterest(1000000, max)", (2013, 12)); |
| 79 | + if (chain.OI(0, 1000000).Count != chain.Count - 1 || chain.Volume(1, long.MaxValue).Count != chain.Count(x => x.Volume >= 1)) |
| 80 | + { |
| 81 | + throw new RegressionTestException("Open interest or volume filter mismatch"); |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + public override void OnData(Slice slice) |
| 86 | + { |
| 87 | + if (slice.FuturesChains.TryGetValue(_es, out var esChain)) |
| 88 | + { |
| 89 | + _esChainSeen = true; |
| 90 | + // The universe selected the contracts expiring within a year, so the chain filters agree with it |
| 91 | + if (esChain.Count == 0 || esChain.Count > 4 || esChain.Expiration(0, 365).Count != esChain.Count || esChain.ExpiringAfter(Time).Count != esChain.Count |
| 92 | + || esChain.ZeroDte().Count != 0 || esChain.ExpirationCycle([3, 6, 9, 12]).Count != esChain.Count || esChain.ExpirationCycle([1, 2]).Count != 0 |
| 93 | + || esChain.StandardsOnly().Count != esChain.Count || esChain.WeeklysOnly().Count != 0) |
| 94 | + { |
| 95 | + throw new RegressionTestException("The ES slice chain disagrees with the universe filter"); |
| 96 | + } |
| 97 | + var frontMonth = esChain.FrontMonth(); |
| 98 | + var farthest = esChain.FarthestExpiration(); |
| 99 | + if (frontMonth.Count == 0 || frontMonth.Any(x => x.Expiry != esChain.Min(c => c.Expiry)) || farthest.Any(x => x.Expiry != esChain.Max(c => c.Expiry)) |
| 100 | + || esChain.BackMonths().Count != esChain.Count - frontMonth.Count) |
| 101 | + { |
| 102 | + throw new RegressionTestException("Front month, back months or farthest expiration mismatch on the ES slice chain"); |
| 103 | + } |
| 104 | + if (esChain.OpenInterest(1, long.MaxValue).Count != esChain.Count(x => x.OpenInterest >= 1) || esChain.Volume(1, long.MaxValue).Count != esChain.Count(x => x.Volume >= 1)) |
| 105 | + { |
| 106 | + throw new RegressionTestException("Open interest or volume filter mismatch on the ES slice chain"); |
| 107 | + } |
| 108 | + |
| 109 | + // Buy the front contract expiring at least 90 days out |
| 110 | + if (!_traded) |
| 111 | + { |
| 112 | + var contract = esChain.ExpiringAfter(Time.Date.AddDays(90)).FrontMonth().FirstOrDefault(); |
| 113 | + if (contract != null) |
| 114 | + { |
| 115 | + MarketOrder(contract.Symbol, 1); |
| 116 | + _traded = true; |
| 117 | + } |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + if (slice.FuturesChains.TryGetValue(_gc, out var gcChain)) |
| 122 | + { |
| 123 | + _gcChainSeen = true; |
| 124 | + // Only the December 2013 contract had more than a hundred thousand contracts open |
| 125 | + if (gcChain.Count == 0 || gcChain.Any(x => x.Expiry.Year != 2013 || x.Expiry.Month != 12) || gcChain.FrontMonth().Count != gcChain.Count) |
| 126 | + { |
| 127 | + throw new RegressionTestException($"The GC slice chain disagrees with the universe filter: {string.Join(", ", gcChain.Select(x => x.Expiry))}"); |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + public override void OnEndOfAlgorithm() |
| 133 | + { |
| 134 | + if (!_esChainSeen || !_gcChainSeen || !_traded) |
| 135 | + { |
| 136 | + throw new RegressionTestException($"Expected the ES chain ({_esChainSeen}), the GC chain ({_gcChainSeen}) and a trade ({_traded})"); |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + private static void AssertExpiries(FuturesChain chain, string filter, params (int year, int month)[] expected) |
| 141 | + { |
| 142 | + var actual = chain.Select(x => (x.Expiry.Year, x.Expiry.Month)).OrderBy(x => x).ToList(); |
| 143 | + if (!actual.SequenceEqual(expected.OrderBy(x => x))) |
| 144 | + { |
| 145 | + throw new RegressionTestException($"{filter}: expected {string.Join(", ", expected)} but got {string.Join(", ", actual)}"); |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + /// <summary> |
| 150 | + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. |
| 151 | + /// </summary> |
| 152 | + public bool CanRunLocally { get; } = true; |
| 153 | + |
| 154 | + /// <summary> |
| 155 | + /// This is used by the regression test system to indicate which languages this algorithm is written in. |
| 156 | + /// </summary> |
| 157 | + public virtual List<Language> Languages { get; } = new() { Language.CSharp, Language.Python }; |
| 158 | + |
| 159 | + /// <summary> |
| 160 | + /// Data Points count of all timeslices of algorithm |
| 161 | + /// </summary> |
| 162 | + public long DataPoints => 34838; |
| 163 | + |
| 164 | + /// <summary> |
| 165 | + /// Data Points count of the algorithm history |
| 166 | + /// </summary> |
| 167 | + public int AlgorithmHistoryDataPoints => 1; |
| 168 | + |
| 169 | + /// <summary> |
| 170 | + /// Final status of the algorithm |
| 171 | + /// </summary> |
| 172 | + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; |
| 173 | + |
| 174 | + /// <summary> |
| 175 | + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm |
| 176 | + /// </summary> |
| 177 | + public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string> |
| 178 | + { |
| 179 | + {"Total Orders", "1"}, |
| 180 | + {"Average Win", "0%"}, |
| 181 | + {"Average Loss", "0%"}, |
| 182 | + {"Compounding Annual Return", "-11.911%"}, |
| 183 | + {"Drawdown", "0.200%"}, |
| 184 | + {"Expectancy", "0"}, |
| 185 | + {"Start Equity", "1000000"}, |
| 186 | + {"End Equity", "998958.2"}, |
| 187 | + {"Net Profit", "-0.104%"}, |
| 188 | + {"Sharpe Ratio", "-9.32"}, |
| 189 | + {"Sortino Ratio", "0"}, |
| 190 | + {"Probabilistic Sharpe Ratio", "0%"}, |
| 191 | + {"Loss Rate", "0%"}, |
| 192 | + {"Win Rate", "0%"}, |
| 193 | + {"Profit-Loss Ratio", "0"}, |
| 194 | + {"Alpha", "-0.048"}, |
| 195 | + {"Beta", "0.095"}, |
| 196 | + {"Annual Standard Deviation", "0.013"}, |
| 197 | + {"Annual Variance", "0"}, |
| 198 | + {"Information Ratio", "5.187"}, |
| 199 | + {"Tracking Error", "0.123"}, |
| 200 | + {"Treynor Ratio", "-1.269"}, |
| 201 | + {"Total Fees", "$2.15"}, |
| 202 | + {"Estimated Strategy Capacity", "$940000000.00"}, |
| 203 | + {"Lowest Capacity Asset", "ES VP274HSU1AF5"}, |
| 204 | + {"Portfolio Turnover", "2.77%"}, |
| 205 | + {"Drawdown Recovery", "0"}, |
| 206 | + {"OrderListHash", "3b6b723d50c0d435d763aa456af197a6"} |
| 207 | + }; |
| 208 | + } |
| 209 | +} |
0 commit comments