Files
quantconnect--lean/Tests/Common/Securities/OptionMarginBuyingPowerModelTests.cs
2026-07-13 13:02:50 +08:00

485 lines
22 KiB
C#

/*
* 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 Castle.Core.Internal;
using NUnit.Framework;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Securities;
using QuantConnect.Securities.IndexOption;
using QuantConnect.Securities.Option;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Common.Securities
{
// The tests have been verified using the CBOE Margin Calculator
// http://www.cboe.com/trading-tools/calculators/margin-calculator
[TestFixture]
public class OptionMarginBuyingPowerModelTests
{
// Test class to enable calling protected methods
[Test]
public void OptionMarginBuyingPowerModelInitializationTests()
{
var option = CreateOption(Symbols.SPY_P_192_Feb19_2016);
var buyingPowerModel = new OptionMarginModel();
// we test that options dont have leverage (100%) and it cannot be changed
Assert.AreEqual(1m, buyingPowerModel.GetLeverage(option));
Assert.Throws<InvalidOperationException>(() => buyingPowerModel.SetLeverage(option, 10m));
Assert.AreEqual(1m, buyingPowerModel.GetLeverage(option));
}
[Test]
public void TestLongCallsPuts()
{
const decimal price = 1.2345m;
const decimal underlyingPrice = 200m;
var equity = CreateEquity();
equity.SetMarketPrice(new Tick { Value = underlyingPrice });
var optionPut = CreateOption(Symbols.SPY_P_192_Feb19_2016);
optionPut.SetMarketPrice(new Tick { Value = price });
optionPut.Underlying = equity;
optionPut.Holdings.SetHoldings(1m, 2);
var optionCall = CreateOption(Symbols.SPY_C_192_Feb19_2016);
optionCall.SetMarketPrice(new Tick { Value = price });
optionCall.Underlying = equity;
optionCall.Holdings.SetHoldings(1.5m, 2);
var buyingPowerModel = new OptionMarginModel();
// we expect long positions to be 100% charged.
Assert.AreEqual(optionPut.Holdings.AbsoluteHoldingsValue, buyingPowerModel.GetInitialMarginRequirement(optionPut, optionPut.Holdings.Quantity));
Assert.AreEqual(optionCall.Holdings.AbsoluteHoldingsValue, buyingPowerModel.GetInitialMarginRequirement(optionCall, optionCall.Holdings.Quantity));
// long option position have zero maintenance margin requirement
Assert.AreEqual(0m, buyingPowerModel.GetMaintenanceMargin(optionPut));
Assert.AreEqual(0m, buyingPowerModel.GetMaintenanceMargin(optionCall));
}
[Test]
public void TestShortCallsITM()
{
const decimal price = 14m;
const decimal underlyingPrice = 196m;
var equity = CreateEquity();
equity.SetMarketPrice(new Tick { Value = underlyingPrice });
var optionCall = CreateOption(Symbols.SPY_C_192_Feb19_2016);
optionCall.SetMarketPrice(new Tick { Value = price });
optionCall.Underlying = equity;
optionCall.Holdings.SetHoldings(price, -2);
var buyingPowerModel = new OptionMarginModel();
// short option positions are very expensive in terms of margin.
// they do not include premium since the user gets paid for the premium up front.
// Margin = quantity * contract multiplier * [option price + MAX(A, B)]
// A = 20% * underlying price - OTM amount = 0.2 * 196 - 0 = 39.2
// B = 10% * underlying price = 0.1 * 196 = 19.6
// Margin = 2 * 100 * (14 + MAX(39.2, 19.6)) = 10640
Assert.AreEqual(10640m, buyingPowerModel.GetMaintenanceMargin(optionCall));
}
[Test]
public void TestShortCallsOTM()
{
const decimal price = 14m;
const decimal underlyingPrice = 180m;
var equity = CreateEquity();
equity.SetMarketPrice(new Tick { Value = underlyingPrice });
var optionCall = CreateOption(Symbols.SPY_C_192_Feb19_2016);
optionCall.SetMarketPrice(new Tick { Value = price });
optionCall.Underlying = equity;
optionCall.Holdings.SetHoldings(price, -2);
var buyingPowerModel = new OptionMarginModel();
// short option positions are very expensive in terms of margin.
// Margin = 2 * 100 * (14 + 0.2 * 180 - (192 - 180)) = 7600
Assert.AreEqual(7600, (double)buyingPowerModel.GetMaintenanceMargin(optionCall), 0.01);
}
[Test]
public void TestShortPutsITM()
{
const decimal price = 14m;
const decimal underlyingPrice = 182m;
var equity = CreateEquity();
equity.SetMarketPrice(new Tick { Value = underlyingPrice });
var optionPut = CreateOption(Symbols.SPY_P_192_Feb19_2016);
optionPut.SetMarketPrice(new Tick { Value = price });
optionPut.Underlying = equity;
optionPut.Holdings.SetHoldings(price, -2);
var buyingPowerModel = new OptionMarginModel();
// short option positions are very expensive in terms of margin.
// Margin = 2 * 100 * (14 + 0.2 * 182) = 10080
Assert.AreEqual(10080m, buyingPowerModel.GetMaintenanceMargin(optionPut));
}
[Test]
public void TestShortPutsOTM()
{
const decimal price = 14m;
const decimal underlyingPrice = 196m;
var equity = CreateEquity();
equity.SetMarketPrice(new Tick { Value = underlyingPrice });
var optionPut = CreateOption(Symbols.SPY_P_192_Feb19_2016);
optionPut.SetMarketPrice(new Tick { Value = price });
optionPut.Underlying = equity;
optionPut.Holdings.SetHoldings(price, -2);
var buyingPowerModel = new OptionMarginModel();
// short option positions are very expensive in terms of margin.
// Margin = 2 * 100 * (14 + 0.2 * 196 - (196 - 192)) = 9840
Assert.AreEqual(9840, (double)buyingPowerModel.GetMaintenanceMargin(optionPut), 0.01);
}
[Test]
public void TestShortPutFarITM()
{
const decimal price = 0.18m;
const decimal underlyingPrice = 200m;
var equity = CreateEquity();
equity.SetMarketPrice(new Tick { Value = underlyingPrice });
var optionPut = CreateOption(equity, OptionRight.Put, 207m);
optionPut.SetMarketPrice(new Tick { Value = price });
optionPut.Holdings.SetHoldings(price, -2);
var buyingPowerModel = new OptionMarginModel();
// short option positions are very expensive in terms of margin.
// Margin = 2 * 100 * (0.18 + 0.2 * 200) = 8036
Assert.AreEqual(8036, (double)buyingPowerModel.GetMaintenanceMargin(optionPut), 0.01);
}
[Test]
public void TestShortPutMovingFarITM()
{
const decimal optionPriceStart = 4.68m;
const decimal underlyingPriceStart = 192m;
const decimal optionPriceEnd = 0.18m;
const decimal underlyingPriceEnd = 200m;
var equity = CreateEquity();
equity.SetMarketPrice(new Tick { Value = underlyingPriceStart });
var optionPut = CreateOption(equity, OptionRight.Put, 207m);
optionPut.SetMarketPrice(new Tick { Value = optionPriceStart });
optionPut.Holdings.SetHoldings(optionPriceStart, -2);
var buyingPowerModel = new OptionMarginModel();
// short option positions are very expensive in terms of margin.
// Margin = 2 * 100 * (4.68 + 0.2 * 192) = 8616
Assert.AreEqual(8616, (double)buyingPowerModel.GetMaintenanceMargin(optionPut), 0.01);
equity.SetMarketPrice(new Tick { Value = underlyingPriceEnd });
optionPut.SetMarketPrice(new Tick { Value = optionPriceEnd });
// short option positions are very expensive in terms of margin.
// Margin = 2 * 100 * (4.68 + 0.2 * 200) = 8936
Assert.AreEqual(8936, (double)buyingPowerModel.GetMaintenanceMargin(optionPut), 0.01);
}
// ITM
[TestCase(OptionRight.Call, 300, 115.75, 415, 19800)] // IB: 19837
// OTM
[TestCase(OptionRight.Put, 300, 0.45, 415, 3000)] // IB: 3044
// ITM
[TestCase(OptionRight.Call, 390, 27.5, 415, 11000)] // IB: 11022
// OTM
[TestCase(OptionRight.Put, 390, 1.85, 415, 6000)] // IB: 6042
// OTM
[TestCase(OptionRight.Call, 430, 0.85, 415, 6800)] // IB: 6803
// ITM
[TestCase(OptionRight.Put, 430, 16.80, 415, 9900)] // IB: 9929
public void ShortOptionsMargin(OptionRight optionRight, decimal strikePrice, decimal optionPrice, decimal underlyingPrice,
double expectedUnitMargin)
{
var equity = CreateEquity();
equity.SetMarketPrice(new Tick { Value = underlyingPrice });
var option = CreateOption(equity, optionRight, strikePrice);
option.SetMarketPrice(new Tick { Value = optionPrice });
option.Holdings.SetHoldings(optionPrice, -1);
var buyingPowerModel = new OptionMarginModel();
Assert.AreEqual(expectedUnitMargin, (double)buyingPowerModel.GetMaintenanceMargin(option), delta: 0.05 * expectedUnitMargin);
Assert.AreEqual(10 * expectedUnitMargin,
(double)buyingPowerModel.GetMaintenanceMargin(MaintenanceMarginParameters.ForQuantityAtCurrentPrice(option, -10)).Value,
delta: 0.05 * 10 * expectedUnitMargin);
}
// ITM
[TestCase(OptionRight.Call, 3800, 750, 4550, 143000)] // IB: 143275
[TestCase(OptionRight.Put, 3800, 0.05, 4550, 38000)] // IB: 38000
// OTM
[TestCase(OptionRight.Call, 5000, 0.05, 4550, 45500)] // IB: 45537
[TestCase(OptionRight.Put, 5000, 445, 4550, 112800)] // IB: 112876
public void ShortIndexOptionsMargin(OptionRight optionRight, decimal strikePrice, decimal optionPrice, decimal underlyingPrice,
double expectedUnitMargin)
{
var index = CreateIndex();
index.SetMarketPrice(new Tick { Value = underlyingPrice });
var indexOption = CreateOption(index, optionRight, strikePrice);
indexOption.SetMarketPrice(new Tick { Value = optionPrice });
indexOption.Holdings.SetHoldings(optionPrice, -1);
var buyingPowerModel = new OptionMarginModel();
Assert.AreEqual(expectedUnitMargin, (double)buyingPowerModel.GetMaintenanceMargin(indexOption), delta: 0.05 * expectedUnitMargin);
Assert.AreEqual(10 * expectedUnitMargin,
(double)buyingPowerModel.GetMaintenanceMargin(MaintenanceMarginParameters.ForQuantityAtCurrentPrice(indexOption, -10)).Value,
delta: 0.05 * 10 * expectedUnitMargin);
}
[TestCase(0)]
[TestCase(10000)]
public void NonAccountCurrency_GetBuyingPower(decimal nonAccountCurrencyCash)
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.Portfolio.SetAccountCurrency("EUR");
algorithm.Portfolio.SetCash(10000);
algorithm.Portfolio.SetCash(Currencies.USD, nonAccountCurrencyCash, 0.88m);
var option = algorithm.AddOption("SPY");
var buyingPowerModel = new OptionMarginModel();
var quantity = buyingPowerModel.GetBuyingPower(new BuyingPowerParameters(
algorithm.Portfolio, option, OrderDirection.Buy));
Assert.AreEqual(10000m + algorithm.Portfolio.CashBook[Currencies.USD].ValueInAccountCurrency,
quantity.Value);
}
// For -1.5% target (15k), we can short -2 contracts for 478 margin requirement per unit
[TestCase(0, -2, -.015)] // Open Short (0 + -2 = -2)
[TestCase(-1, -1, -.015)] // Short to Shorter (-1 + -1 = -2)
[TestCase(-2, 0, -.015)] // No action
[TestCase(2, -4, -.015)] // Long To Short (2 + -4 = -2)
// -40% Target (~-400k), we can short -58 contracts for 478 margin requirement per unit
[TestCase(0, -58, -0.40)] // Open Short (0 + -58 = -58)
[TestCase(-2, -56, -0.40)] // Short to Shorter (-2 + -56 = -58)
[TestCase(2, -60, -0.40)] // Long To Short (2 + -60 = -58)
// 40% Target (~400k), we can buy 836 contracts
[TestCase(0, 836, 0.40)] // Open Long (0 + 836 = 836)
[TestCase(-2, 838, 0.40)] // Short to Long (-2 + 838 = 836)
[TestCase(2, 834, 0.40)] // Long To Longer (2 + 834 = 836)
// ~0.04% Target (~400). This is below the needed margin for one unit. We end up at 0 holdings for all cases.
[TestCase(0, 0, 0.0004)] // Open Long (0 + 0 = 0)
[TestCase(-2, 2, 0.0004)] // Short to Long (-2 + 2 = 0)
[TestCase(2, -2, 0.0004)] // Long To Longer (2 + -2 = 0)
public void CallOTM_MarginRequirement(int startingHoldings, int expectedOrderSize, decimal targetPercentage)
{
// Initialize algorithm
var algorithm = new QCAlgorithm();
algorithm.SetFinishedWarmingUp();
algorithm.Transactions.SetOrderProcessor(new FakeOrderProcessor());
algorithm.SetCash(1000000);
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
var optionSymbol = Symbols.CreateOptionSymbol("SPY", OptionRight.Call, 411m, DateTime.UtcNow);
var option = algorithm.AddOptionContract(optionSymbol);
option.Holdings.SetHoldings(4.74m, startingHoldings);
Assert.GreaterOrEqual(algorithm.Portfolio.MarginRemaining, 0);
option.FeeModel = new ConstantFeeModel(0);
option.SetLeverage(1);
// Update option data
UpdatePrice(option, 4.78m);
// Update the underlying data
UpdatePrice(option.Underlying, 395.51m);
var model = new OptionMarginModel();
var result = model.GetMaximumOrderQuantityForTargetBuyingPower(algorithm.Portfolio, option, targetPercentage, 0);
Assert.AreEqual(expectedOrderSize, result.Quantity);
var initialPortfolioValue = algorithm.Portfolio.TotalPortfolioValue;
var initialMarginUsed = algorithm.Portfolio.TotalMarginUsed;
option.Holdings.SetHoldings(4.74m, result.Quantity + startingHoldings);
if (option.Holdings.Invested)
{
Assert.LessOrEqual(Math.Abs(initialMarginUsed - algorithm.Portfolio.TotalMarginUsed), initialPortfolioValue * Math.Abs(targetPercentage));
}
}
[TestCase(0)]
[TestCase(-10)]
public void GetsMaintenanceMarginForAPotentialShortPositionWithoutInitialHoldings(decimal initialHoldings)
{
// Computing the maintenance margin for a potential position is useful because it will be used to check whether there is
// enough available buying power to open said new position.
const decimal price = 1.6m;
const decimal underlyingPrice = 410m;
var equity = CreateEquity();
equity.SetMarketPrice(new Tick { Value = underlyingPrice });
var optionCall = CreateOption(equity, OptionRight.Call, 408m);
optionCall.SetMarketPrice(new Tick { Value = price });
optionCall.Holdings.SetHoldings(price, initialHoldings);
var buyingPowerModel = new OptionMarginModel();
if (initialHoldings == 0)
{
// No holdings for the option, so no maintenance margin expected
Assert.AreEqual(0m, buyingPowerModel.GetMaintenanceMargin(optionCall));
}
else
{
// Margin = 10 * 100 * (1.6 + 0.2 * 410) = 83600
Assert.AreEqual(83600m, buyingPowerModel.GetMaintenanceMargin(optionCall));
}
// Short option positions are very expensive in terms of margin.
// Margin = 2 * 100 * (1.6 + 0.2 * 410) = 16720
Assert.AreEqual(16720m, buyingPowerModel.GetMaintenanceMargin(MaintenanceMarginParameters.ForQuantityAtCurrentPrice(optionCall, -2)).Value);
}
// OTM
[TestCase(1, 3500, 140)] // IB: 0 (GetInitialMarginRequirement() returns the value with the premium)
[TestCase(-1, 3500, -52340)] // IB: 40781
// ITM
[TestCase(1, 3450, 140)] // IB: 0 (GetInitialMarginRequirement() returns the value with the premium)
[TestCase(-1, 3450, -37340)] // IB: 36081
public void GetInitialMarginRequiredForOrderWithIndexOption(decimal quantity, decimal strikePrice, decimal expectedInitialMargin)
{
var price = 1.40m;
var underlyingPrice = 17400m;
var indexSymbol = Symbol.Create("NDX", SecurityType.Index, Market.USA);
var index = CreateIndex(indexSymbol);
index.SetMarketPrice(new Tick { Value = underlyingPrice });
var optionPut = CreateOption(index, OptionRight.Put, strikePrice, "NQX");
optionPut.SetMarketPrice(new Tick { Value = price });
var buyingPowerModel = new OptionMarginModel();
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
var initialMargin = buyingPowerModel.GetInitialMarginRequirement(optionPut, quantity);
Assert.AreEqual((double)expectedInitialMargin, (double)initialMargin, delta: 0.01);
}
private static void UpdatePrice(Security security, decimal close)
{
security.SetMarketPrice(new TradeBar
{
Time = DateTime.Now,
Symbol = security.Symbol,
Open = close,
High = close,
Low = close,
Close = close
});
}
private static QuantConnect.Securities.Equity.Equity CreateEquity()
{
var tz = TimeZones.NewYork;
return new QuantConnect.Securities.Equity.Equity(
SecurityExchangeHours.AlwaysOpen(tz),
new SubscriptionDataConfig(typeof(TradeBar), Symbols.SPY, Resolution.Minute, tz, tz, true, false, false),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
);
}
private static Option CreateOption(Security underlying, OptionRight optionRight, decimal strikePrice, string targetOption = null)
{
var tz = TimeZones.NewYork;
var optionSymbol = targetOption.IsNullOrEmpty() ? Symbol.CreateOption(underlying.Symbol, Market.USA, OptionStyle.American, optionRight, strikePrice,
new DateTime(2015, 02, 27)) : Symbol.CreateOption(underlying.Symbol, targetOption, Market.USA, OptionStyle.American, optionRight, strikePrice,
new DateTime(2015, 02, 27));
var option = new Option(
SecurityExchangeHours.AlwaysOpen(tz),
new SubscriptionDataConfig(typeof(TradeBar), optionSymbol, Resolution.Minute, tz, tz, true, false, false),
new Cash(Currencies.USD, 0, 1m),
new OptionSymbolProperties(SymbolPropertiesDatabase.FromDataFolder().GetSymbolProperties(Market.USA, optionSymbol, optionSymbol.SecurityType, "USD")),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
);
option.Underlying = underlying;
return option;
}
private static Option CreateOption(Symbol symbol)
{
var tz = TimeZones.NewYork;
var option = new Option(
SecurityExchangeHours.AlwaysOpen(tz),
new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, tz, tz, true, false, false),
new Cash(Currencies.USD, 0, 1m),
new OptionSymbolProperties("", Currencies.USD, 100, 0.01m, 1),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
);
return option;
}
private static QuantConnect.Securities.Index.Index CreateIndex(Symbol symbol = null)
{
var tz = TimeZones.NewYork;
return new QuantConnect.Securities.Index.Index(
SecurityExchangeHours.AlwaysOpen(tz),
new Cash(Currencies.USD, 0, 1m),
new SubscriptionDataConfig(typeof(TradeBar), symbol ?? Symbols.SPX, Resolution.Minute, tz, tz, true, false, false),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
);
}
}
}