Skip to content

Commit

Permalink
Invalidate option orders during a stock split (#7550)
Browse files Browse the repository at this point in the history
* Invalidate option orders during a stock split

* Add unit test

* Minor changes

* Minor comments

* Options orders on split one time warning
  • Loading branch information
jhonabreul authored Nov 1, 2023
1 parent 7c17691 commit d2f8c73
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 1 deletion.
134 changes: 134 additions & 0 deletions Algorithm.CSharp/OptionOrdersOnSplitRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* 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.Linq;
using System.Collections.Generic;

using QuantConnect.Data;
using QuantConnect.Interfaces;
using QuantConnect.Util;
using QuantConnect.Orders;

namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Regression algorithm asserting that option orders are not allowed on split dates
/// </summary>
public class OptionOrdersOnSplitRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
private Symbol _aapl;

private OrderTicket _ticket;

public override void Initialize()
{
SetStartDate(2014, 6, 5);
SetEndDate(2014, 6, 11);
SetCash(100000);

_aapl = AddEquity("AAPL", Resolution.Minute, extendedMarketHours: true, dataNormalizationMode: DataNormalizationMode.Raw).Symbol;

var option = AddOption(_aapl, Resolution.Minute);
option.SetFilter(-1, +1, 0, 365);
}

public override void OnData(Slice slice)
{
if (slice.Splits.TryGetValue(_aapl, out var split))
{
Debug($"Split: {Time} - {split}");

if (split.Type == SplitType.SplitOccurred)
{
var contract = Securities.Values
.Where(x => x.Type.IsOption() && !x.Symbol.IsCanonical())
.OrderBy(x => x.Symbol.ID.StrikePrice)
.First();
_ticket = MarketOrder(contract.Symbol, 1);

if (_ticket.Status != OrderStatus.Invalid ||
_ticket.SubmitRequest.Response.IsSuccess ||
_ticket.SubmitRequest.Response.ErrorCode != OrderResponseErrorCode.OptionOrderOnStockSplit ||
_ticket.SubmitRequest.Response.ErrorMessage != "Options orders are not allowed when a split occurred for its underlying stock")
{
throw new Exception(
$"Expected invalid order ticket with error code {nameof(OrderResponseErrorCode.OptionOrderOnStockSplit)}, " +
$"but received {_ticket.SubmitRequest.Response.ErrorCode} - {_ticket.SubmitRequest.Response.ErrorMessage}");
}
}
}
}

public override void OnEndOfAlgorithm()
{
if (_ticket == null)
{
throw new Exception("Expected invalid order ticket with error code OptionOrderOnStockSplit, but no order was submitted");
}
}

/// <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 Language[] Languages { get; } = { Language.CSharp };

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

/// <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 Trades", "0"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "0%"},
{"Drawdown", "0%"},
{"Expectancy", "0"},
{"Net Profit", "0%"},
{"Sharpe 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", "-2.491"},
{"Tracking Error", "0.042"},
{"Treynor Ratio", "0"},
{"Total Fees", "$0.00"},
{"Estimated Strategy Capacity", "$0"},
{"Lowest Capacity Asset", ""},
{"Portfolio Turnover", "0%"},
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
};
}
}
18 changes: 18 additions & 0 deletions Algorithm/QCAlgorithm.Trading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public partial class QCAlgorithm
private bool _isMarketOnOpenOrderWarningSent;
private bool _isMarketOnOpenOrderRestrictedForFuturesWarningSent;
private bool _isGtdTfiForMooAndMocOrdersValidationWarningSent;
private bool _isOptionsOrderOnStockSplitWarningSent;

/// <summary>
/// Transaction Manager - Process transaction fills and order management.
Expand Down Expand Up @@ -1175,6 +1176,23 @@ private OrderResponse PreOrderChecksImpl(SubmitOrderRequest request)
throw new ArgumentException("Can not set a limit price using market combo orders");
}

// Check for splits. Option are selected before the security price is split-adjusted, so in this time step
// we don't allow option orders to make sure they are properly filtered using the right security price.
if (request.SecurityType.IsOption() &&
CurrentSlice != null &&
CurrentSlice.Splits.Count > 0 &&
CurrentSlice.Splits.TryGetValue(request.Symbol.Underlying, out _))
{
if (!_isOptionsOrderOnStockSplitWarningSent)
{
Debug("Warning: Options orders are not allowed when a split occurred for its underlying stock");
_isOptionsOrderOnStockSplitWarningSent = true;
}

return OrderResponse.Error(request, OrderResponseErrorCode.OptionOrderOnStockSplit,
"Options orders are not allowed when a split occurred for its underlying stock");
}

// passes all initial order checks
return OrderResponse.Success(request);
}
Expand Down
7 changes: 6 additions & 1 deletion Common/Orders/OrderResponseErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ public enum OrderResponseErrorCode
/// <summary>
/// Exercise time before expiry for European options (-33)
/// </summary>
EuropeanOptionNotExpiredOnExercise = -33
EuropeanOptionNotExpiredOnExercise = -33,

/// <summary>
/// Option order is invalid due to underlying stock split (-34)
/// </summary>
OptionOrderOnStockSplit = -34
}
}
25 changes: 25 additions & 0 deletions Tests/Algorithm/AlgorithmTradingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
using QuantConnect.Tests.Common.Securities;
using QuantConnect.Tests.Engine.DataFeeds;
using System.Linq;
using QuantConnect.Data;
using QuantConnect.Indicators;

namespace QuantConnect.Tests.Algorithm
{
Expand Down Expand Up @@ -1459,6 +1461,28 @@ public void MarketOnOpenOrdersNotSupportedForFutures()
Assert.That(ticket, Has.Property("Status").EqualTo(OrderStatus.Invalid));
}

[Test]
public void OptionOrdersAreNotAllowedDuringASplit()
{
var algo = GetAlgorithm(out _, 1, 0);
var aapl = algo.AddEquity("AAPL");
var applOptionContract = algo.AddOptionContract(
Symbol.CreateOption(aapl.Symbol, Market.USA, OptionStyle.American, OptionRight.Call, 40m, new DateTime(2014, 07, 19)));

var splitDate = new DateTime(2014, 06, 09);
aapl.SetMarketPrice(new IndicatorDataPoint(splitDate, 650m));
applOptionContract.SetMarketPrice(new IndicatorDataPoint(splitDate, 5m));

algo.SetCurrentSlice(new Slice(splitDate, new[] { new Split(aapl.Symbol, splitDate, 650m, 1 / 7, SplitType.SplitOccurred) }, splitDate));

var ticket = algo.MarketOrder(applOptionContract.Symbol, 1);
Assert.AreEqual(OrderStatus.Invalid, ticket.Status);
Assert.IsTrue(ticket.SubmitRequest.Response.IsError);
Assert.AreEqual(OrderResponseErrorCode.OptionOrderOnStockSplit, ticket.SubmitRequest.Response.ErrorCode);
Assert.IsTrue(ticket.SubmitRequest.Response.ErrorMessage.Contains(
"Options orders are not allowed when a split occurred for its underlying stock", StringComparison.InvariantCulture));
}

[TestCase(OrderType.MarketOnOpen)]
[TestCase(OrderType.MarketOnClose)]
public void GoodTilDateTimeInForceNotSupportedForMOOAndMOCOrders(OrderType orderType)
Expand Down Expand Up @@ -1631,6 +1655,7 @@ private QCAlgorithm GetAlgorithm(out Security msft, decimal leverage, decimal fe
algo.Transactions.SetOrderProcessor(_fakeOrderProcessor);
msft = algo.Securities[Symbols.MSFT];
msft.SetLeverage(leverage);
algo.SetCurrentSlice(new Slice(DateTime.MinValue, Enumerable.Empty<BaseData>(), DateTime.MinValue));
return algo;
}

Expand Down

0 comments on commit d2f8c73

Please sign in to comment.