Skip to content

Commit

Permalink
Collective2 - No index option (#8494)
Browse files Browse the repository at this point in the history
* Inicial solution

* Use InitialMargin instead of MaintenanceMargin

* Addressed review comments

* Update unit test

* Refactor GenerateOptionTicker method to handle IndexOption

* Addressed PR comments

* Update unit test
  • Loading branch information
JosueNina authored Jan 8, 2025
1 parent d71dd86 commit 3569a1f
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 12 deletions.
157 changes: 157 additions & 0 deletions Algorithm.CSharp/RegressionTests/Collective2IndexOptionAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* 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 QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Data;
using QuantConnect.Indicators;
using QuantConnect.Interfaces;

namespace QuantConnect.Algorithm.CSharp.RegressionTests
{
public class Collective2IndexOptionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
/// <summary>
/// Collective2 APIv4 KEY: This value is provided by Collective2 in your account section (See https://collective2.com/account-info)
/// See API documentation at https://trade.collective2.com/c2-api
/// </summary>
private const string _collective2ApiKey = "YOUR APIV4 KEY";

/// <summary>
/// Collective2 System ID: This value is found beside the system's name (strategy's name) on the main system page
/// </summary>
private const int _collective2SystemId = 0;

private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _slow;
private Symbol _symbol;
private bool _firstCall = true;

public override void Initialize()
{
SetStartDate(2021, 1, 4);
SetEndDate(2021, 1, 18);
SetCash(100000);

var underlying = AddIndex("SPX", Resolution.Minute).Symbol;

// Create an SPXW option contract with a specific strike price and expiration date
var option = QuantConnect.Symbol.CreateOption(
underlying,
"SPXW",
Market.USA,
OptionStyle.European,
OptionRight.Call,
3800m,
new DateTime(2021, 1, 04));

_symbol = AddIndexOptionContract(option, Resolution.Minute).Symbol;

_fast = EMA(underlying, 10, Resolution.Minute);
_slow = EMA(underlying, 50, Resolution.Minute);

// Set up the Collective2 Signal Export with the provided API key and system ID
SignalExport.AddSignalExportProviders(new Collective2SignalExport(_collective2ApiKey, _collective2SystemId));

// Set warm-up period for the indicators
SetWarmUp(50);
}

public override void OnData(Slice slice)
{
// Execute only on the first data call to set initial portfolio
if (_firstCall)
{
SetHoldings(_symbol, 0.1);
SignalExport.SetTargetPortfolioFromPortfolio();
_firstCall = false;
}

// If the fast EMA crosses above the slow EMA, open a long position
if (_fast > _slow && !Portfolio.Invested)
{
MarketOrder(_symbol, 1);
SignalExport.SetTargetPortfolioFromPortfolio();
}

// If the fast EMA crosses below the slow EMA, open a short position
else if (_fast < _slow && Portfolio.Invested)
{
MarketOrder(_symbol, -1);
SignalExport.SetTargetPortfolioFromPortfolio();
}
}

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

/// <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 };

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

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

/// <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", "10"},
{"Average Win", "0%"},
{"Average Loss", "0.00%"},
{"Compounding Annual Return", "-0.468%"},
{"Drawdown", "0.000%"},
{"Expectancy", "-1"},
{"Start Equity", "100000"},
{"End Equity", "99985"},
{"Net Profit", "-0.015%"},
{"Sharpe Ratio", "-15.229"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "0.781%"},
{"Loss Rate", "100%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "-0.003"},
{"Beta", "-0.001"},
{"Annual Standard Deviation", "0"},
{"Annual Variance", "0"},
{"Information Ratio", "-5.216"},
{"Tracking Error", "0.103"},
{"Treynor Ratio", "5.946"},
{"Total Fees", "$0.00"},
{"Estimated Strategy Capacity", "$8000.00"},
{"Lowest Capacity Asset", "SPXW XKX6S2GM9PGU|SPX 31"},
{"Portfolio Turnover", "0.01%"},
{"OrderListHash", "5b50a3d9e0afad859c3f7e2580a4f3be"}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ private bool ConvertTypeOfSymbol(Symbol targetSymbol, out string typeOfSymbol)
case SecurityType.Forex:
typeOfSymbol = "forex";
break;
case SecurityType.IndexOption:
typeOfSymbol = "option";
break;
default:
typeOfSymbol = "NotImplemented";
break;
Expand Down Expand Up @@ -263,7 +266,7 @@ private bool SendPositions(string message)
{
_algorithm.Debug($"Collective2: NewSignals={string.Join(',', responseObject.Results[0].NewSignals)} | CanceledSignals={string.Join(',', responseObject.Results[0].CanceledSignals)}");
}

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,9 @@ private IEnumerable<PortfolioTarget> GetPortfolioTargets(decimal totalPortfolioV
continue;
}

var marginParameters = MaintenanceMarginParameters.ForQuantityAtCurrentPrice(security, holding.Quantity);
var adjustedPercent = security.BuyingPowerModel.GetMaintenanceMargin(marginParameters) / totalPortfolioValue;
var marginParameters = new InitialMarginParameters(security, holding.Quantity);
var adjustedPercent = Math.Abs(security.BuyingPowerModel.GetInitialMarginRequirement(marginParameters) / totalPortfolioValue);

// See PortfolioTarget.Percent:
// we normalize the target buying power by the leverage so we work in the land of margin
var holdingPercent = adjustedPercent * security.BuyingPowerModel.GetLeverage(security);
Expand Down
17 changes: 9 additions & 8 deletions Common/SymbolRepresentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class FutureTickerProperties
/// <summary>
/// Underlying name
/// </summary>
public string Underlying { get; set; }
public string Underlying { get; set; }

/// <summary>
/// Short expiration year
Expand Down Expand Up @@ -167,8 +167,7 @@ public static Symbol ParseFutureSymbol(string ticker, int? futureYear = null)

if (!SymbolPropertiesDatabase.FromDataFolder().TryGetMarket(underlying, SecurityType.Future, out var market))
{
Log.Debug($@"SymbolRepresentation.ParseFutureSymbol(): {
Messages.SymbolRepresentation.FailedToGetMarketForTickerAndUnderlying(ticker, underlying)}");
Log.Debug($@"SymbolRepresentation.ParseFutureSymbol(): {Messages.SymbolRepresentation.FailedToGetMarketForTickerAndUnderlying(ticker, underlying)}");
return null;
}

Expand Down Expand Up @@ -269,7 +268,8 @@ public static string GenerateFutureTicker(string underlying, DateTime expiration
month = expirationMonth.Month;
year = doubleDigitsYear ? expirationMonth.Year % 100 : expirationMonth.Year % 10;
}
else {
else
{
// These futures expire in the month before or in the contract month
month += contractMonthDelta;

Expand Down Expand Up @@ -389,7 +389,7 @@ public static Symbol ParseOptionTickerOSI(string ticker, SecurityType securityTy
// let it fallback to it's default handling, which include mapping
optionTicker = null;
}
else if(securityType == SecurityType.IndexOption)
else if (securityType == SecurityType.IndexOption)
{
underlyingSid = SecurityIdentifier.GenerateIndex(OptionSymbol.MapToUnderlying(optionTicker, securityType), market);
underlyingSymbolValue = underlyingSid.Symbol;
Expand Down Expand Up @@ -478,9 +478,10 @@ public static bool TryDecomposeOptionTickerOSI(string ticker, SecurityType secur
/// <returns>The option ticker</returns>
public static string GenerateOptionTicker(Symbol symbol)
{
var symbolTicker = symbol.SecurityType == SecurityType.IndexOption ? symbol.Canonical.Value.Replace("?", string.Empty) : SecurityIdentifier.Ticker(symbol.Underlying, symbol.ID.Date);
var letter = _optionSymbology.Where(x => x.Value.Item2 == symbol.ID.OptionRight && x.Value.Item1 == symbol.ID.Date.Month).Select(x => x.Key).Single();
var twoYearDigit = symbol.ID.Date.ToString("yy");
return $"{SecurityIdentifier.Ticker(symbol.Underlying, symbol.ID.Date)}{twoYearDigit}{symbol.ID.Date.Day:00}{letter}{symbol.ID.StrikePrice.ToStringInvariant()}";
return $"{symbolTicker}{twoYearDigit}{symbol.ID.Date.Day:00}{letter}{symbol.ID.StrikePrice.ToStringInvariant()}";
}

/// <summary>
Expand Down Expand Up @@ -590,10 +591,10 @@ public static OptionTickerProperties ParseOptionTickerIQFeed(string ticker)
/// <remarks>Tickers from live trading may not provide the four-digit year.</remarks>
private static int GetExpirationYear(int? futureYear, FutureTickerProperties parsed)
{
if(futureYear.HasValue)
if (futureYear.HasValue)
{
var referenceYear = 1900 + parsed.ExpirationYearShort;
while(referenceYear < futureYear.Value)
while (referenceYear < futureYear.Value)
{
referenceYear += 10;
}
Expand Down
40 changes: 40 additions & 0 deletions Tests/Algorithm/Framework/Portfolio/SignalExportTargetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,46 @@ public void SignalExportManagerGetsCorrectPortfolioTargetArray(SecurityType secu
Assert.AreEqual(quantity, targetQuantity, 1);
}

[Test]
public void SignalExportManagerHandlesIndexOptions()
{
var algorithm = new AlgorithmStub(true);
algorithm.SetFinishedWarmingUp();
algorithm.SetCash(100000);

int quantity = 123;
var underlying = algorithm.AddIndex("SPX", Resolution.Minute).Symbol;

// Create the option contract (IndexOption) with specific parameters
var option = Symbol.CreateOption(
underlying,
"SPXW",
Market.USA,
OptionStyle.European,
OptionRight.Call,
3800m,
new DateTime(2021, 1, 04));

var security = algorithm.AddIndexOptionContract(option, Resolution.Minute);
security.SetMarketPrice(new Tick(new DateTime(2022, 01, 04), security.Symbol, 144.80m, 144.82m));
security.Holdings.SetHoldings(144.81m, quantity);

// Initialize the SignalExportManagerHandler and get portfolio targets
var signalExportManagerHandler = new SignalExportManagerHandler(algorithm);
var result = signalExportManagerHandler.GetPortfolioTargets(out PortfolioTarget[] portfolioTargets);

// Assert that the result is successful
Assert.IsTrue(result);

// Get the portfolio target and verify the quantity matches
var target = portfolioTargets[0];
var targetQuantity = (int)PortfolioTarget.Percent(algorithm, target.Symbol, target.Quantity).Quantity;
Assert.AreEqual(quantity, targetQuantity);

// Ensure the symbol is of type IndexOption
Assert.IsTrue(target.Symbol.SecurityType == SecurityType.IndexOption);
}

[Test]
public void SignalExportManagerIgnoresIndexSecurities()
{
Expand Down
26 changes: 25 additions & 1 deletion Tests/Common/SymbolRepresentationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,30 @@ public void ParseInvalidFuturesTickers()
Assert.AreEqual(result, null);
}

[Test]
public void GenerateOptionTickerWithIndexOptionReturnsCorrectTicker()
{
// Expected ticker for the option contract
var expected = "SPXW2104A3800";

var underlying = Symbols.SPX;

// Create the option contract (IndexOption) with specific parameters
var option = Symbol.CreateOption(
underlying,
"SPXW",
Market.USA,
OptionStyle.European,
OptionRight.Call,
3800m,
new DateTime(2021, 1, 04));

var result = SymbolRepresentation.GenerateOptionTicker(option);

// Assert that the result matches the expected ticker
Assert.AreEqual(expected, result);
}

[TestCase(Futures.Energy.ArgusLLSvsWTIArgusTradeMonth, 2017, 1, 29, "AE529G7", false)] // Previous month
[TestCase(Futures.Energy.ArgusPropaneSaudiAramco, 2017, 1, 29, "A9N29G7", false)] // Previous month
[TestCase(Futures.Energy.BrentCrude, 2017, 1, 29, "B29H7", false)] // Second prior month
Expand Down Expand Up @@ -287,7 +311,7 @@ public void GenerateFutureSymbolFromTickerExpiringBefore2000(string ticker)
[TestCase("PROPANE_NON_LDH_MONT_BELVIEU", QuantConnect.Securities.Futures.Energy.PropaneNonLDHMontBelvieu)]
[TestCase("ARGUS_PROPANE_FAR_EAST_INDEX_BALMO", QuantConnect.Securities.Futures.Energy.ArgusPropaneFarEastIndexBALMO)]
[TestCase("GASOLINE", QuantConnect.Securities.Futures.Energy.Gasoline)]
[TestCase("NATURAL_GAS",QuantConnect.Securities.Futures.Energy.NaturalGas)]
[TestCase("NATURAL_GAS", QuantConnect.Securities.Futures.Energy.NaturalGas)]
public void FutureEnergySymbolsWorkInPythonWithPEP8(string FutureEnergyName, string expectedFutureEnergyValue)
{
using (Py.GIL())
Expand Down

0 comments on commit 3569a1f

Please sign in to comment.