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

350 lines
16 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 System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using QuantConnect.Brokerages;
using QuantConnect.Data.Market;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Data;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Forex;
using QuantConnect.Tests.Engine.DataFeeds;
using QuantConnect.Securities.Cfd;
namespace QuantConnect.Tests.Common.Brokerages
{
[TestFixture, Parallelizable(ParallelScope.All)]
public class InteractiveBrokersBrokerageModelTests
{
private readonly InteractiveBrokersBrokerageModel _interactiveBrokersBrokerageModel = new InteractiveBrokersBrokerageModel();
[TestCaseSource(nameof(GetUnsupportedOptions))]
public void CannotSubmitOrder_IndexOptionExercise(Security security)
{
var order = new Mock<OptionExerciseOrder>();
order.Setup(x => x.Type).Returns(OrderType.OptionExercise);
var canSubmit = _interactiveBrokersBrokerageModel.CanSubmitOrder(security, order.Object, out var message);
Assert.IsFalse(canSubmit, message.Message);
Assert.AreEqual(BrokerageMessageType.Warning, message.Type);
Assert.AreEqual("NotSupported", message.Code);
StringAssert.Contains("exercises for index and cash-settled options", message.Message);
}
[TestCaseSource(nameof(GetForexOrderTestCases))]
public void CanSubmitOrder_ForexWithinAllowableOrderSize(Forex security, decimal quantity, bool shouldSubmit)
{
var order = new MarketOrder(security.Symbol, quantity, new DateTime(2023, 1, 20));
var canSubmit = _interactiveBrokersBrokerageModel.CanSubmitOrder(security, order, out var message);
Assert.AreEqual(shouldSubmit, canSubmit);
if (shouldSubmit)
{
Assert.IsNull(message);
}
else
{
Assert.AreEqual(BrokerageMessageType.Warning, message.Type);
Assert.AreEqual("OrderSizeLimit", message.Code);
StringAssert.Contains("minimum and maximum limits for the allowable order size are", message.Message);
}
}
[TestCase("SPY", SecurityType.Option)]
[TestCase("SPX", SecurityType.IndexOption)]
[TestCase("ES", SecurityType.FutureOption)]
public void CannotSubmitMOCOrdersForOptions(string ticker, SecurityType securityType)
{
var algo = new AlgorithmStub();
var security = algo.AddSecurity(securityType, ticker);
if (securityType == SecurityType.FutureOption)
{
var underlyingFuture = Symbol.CreateFuture(
QuantConnect.Securities.Futures.Indices.SP500EMini,
Market.CME,
new DateTime(2021, 3, 19));
var futureOption = Symbol.CreateOption(underlyingFuture,
Market.CME,
OptionStyle.American,
OptionRight.Call,
2550m,
new DateTime(2021, 3, 19));
security = new QuantConnect.Securities.FutureOption.FutureOption(
futureOption,
MarketHoursDatabase.FromDataFolder().GetExchangeHours(Market.CME, futureOption, futureOption.SecurityType),
new Cash("USD", 100000m, 1m),
new OptionSymbolProperties(string.Empty, "USD", 1m, 0.01m, 1m),
new CashBook(),
new RegisteredSecurityDataTypesProvider(),
new SecurityCache(),
null);
}
var order = new MarketOnCloseOrder(security.Symbol, 1, DateTime.UtcNow);
var result = _interactiveBrokersBrokerageModel.CanSubmitOrder(security, order, out var message);
Assert.IsFalse(result);
var expectedMessage = "InteractiveBrokers does not support Market-on-Close orders for other security types different than Future and Equity.";
Assert.AreEqual(expectedMessage, message.Message);
}
[TestCase("EURGBP", SecurityType.Forex)]
public void CannotSubmitMOCOrdersForForexAndCfd(string ticker, SecurityType securityType)
{
var algo = new AlgorithmStub();
var security = algo.AddSecurity(securityType, ticker);
var order = new MarketOnCloseOrder(security.Symbol, 1, DateTime.UtcNow);
var result = _interactiveBrokersBrokerageModel.CanSubmitOrder(security, order, out var message);
Assert.IsFalse(result);
var expectedMessage = "InteractiveBrokers does not support Market-on-Close orders for other security types different than Future and Equity.";
Assert.AreEqual(expectedMessage, message.Message);
}
[TestCase("EURGBP", SecurityType.Forex)]
[TestCase("ES", SecurityType.Future)]
public void CannotSubmitMOOOrdersForForexCfdAndFutureOrders(string ticker, SecurityType securityType)
{
var algo = new AlgorithmStub();
var security = algo.AddSecurity(securityType, ticker);
var order = new MarketOnOpenOrder(security.Symbol, 1, DateTime.UtcNow);
var result = _interactiveBrokersBrokerageModel.CanSubmitOrder(security, order, out var message);
Assert.IsFalse(result);
var expectedMessage = $"The broker does not support Market-on-Open orders for security type {security.Type}";
Assert.AreEqual(expectedMessage, message.Message);
}
[TestCase("SPY", SecurityType.Option)]
[TestCase("SPY", SecurityType.Equity)]
[TestCase("DE10YBEUR", SecurityType.Cfd)]
public void CanSubmitMOOOrdersForOptionAndEquity(string ticker, SecurityType securityType)
{
var algo = new AlgorithmStub();
var security = algo.AddSecurity(securityType, ticker);
var order = new MarketOnOpenOrder(security.Symbol, 1, DateTime.UtcNow);
var result = _interactiveBrokersBrokerageModel.CanSubmitOrder(security, order, out var message);
Assert.IsTrue(result);
}
[TestCase(OrderType.ComboLegLimit, 2, true)]
[TestCase(OrderType.ComboLimit, 4, true)]
[TestCase(OrderType.ComboLegLimit, 4, false)]
public void CanSubmitComboOrdersWithExpectedLegValidation(OrderType orderType, int legCount, bool shouldSubmit)
{
var algo = new AlgorithmStub();
var security = algo.AddSecurity(SecurityType.Option, "SPY");
var groupOrderManager = new GroupOrderManager(1, legCount, 1, 100m);
Order order = orderType switch
{
OrderType.ComboLimit => new ComboLimitOrder(security.Symbol, 1, 100m, DateTime.UtcNow, groupOrderManager),
OrderType.ComboLegLimit => new ComboLegLimitOrder(security.Symbol, 1, 100m, DateTime.UtcNow, groupOrderManager),
_ => throw new ArgumentOutOfRangeException(nameof(orderType), orderType, "Unexpected combo order type")
};
var canSubmit = _interactiveBrokersBrokerageModel.CanSubmitOrder(security, order, out var message);
Assert.AreEqual(shouldSubmit, canSubmit);
if (shouldSubmit)
{
Assert.IsNull(message);
}
else
{
Assert.AreEqual(BrokerageMessageType.Warning, message.Type);
Assert.AreEqual("NotSupported", message.Code);
StringAssert.Contains("does not support four-leg ComboLegLimit orders", message.Message);
}
}
[TestCase("ES", SecurityType.Future)]
[TestCase("SPY", SecurityType.Equity)]
[TestCase("DE10YBEUR", SecurityType.Cfd)]
public void CanSubmitMOCOrdersForFutureAndEquity(string ticker, SecurityType securityType)
{
var algo = new AlgorithmStub();
var security = algo.AddSecurity(securityType, ticker);
var order = new MarketOnCloseOrder(security.Symbol, 1, DateTime.UtcNow);
var result = _interactiveBrokersBrokerageModel.CanSubmitOrder(security, order, out var message);
Assert.IsTrue(result);
}
[TestCase(AccountType.Cash, 1)]
[TestCase(AccountType.Margin, 10)]
public void GetsCorrectLeverageForCfds(AccountType accounType, decimal expectedLeverage)
{
var brokerageModel = new InteractiveBrokersBrokerageModel(accounType);
var security = new Cfd(Symbols.DE10YBEUR,
SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork),
new Cash("USD", 0, 0),
SymbolProperties.GetDefault("USD"),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache());
Assert.AreEqual(expectedLeverage, brokerageModel.GetLeverage(security));
}
[Test]
public void CanSubmitCfdOrder()
{
var security = new Cfd(Symbols.DE10YBEUR,
SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork),
new Cash("USD", 0, 0),
SymbolProperties.GetDefault("USD"),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache());
var order = new MarketOrder(security.Symbol, 1, new DateTime(2023, 1, 20));
var canSubmit = _interactiveBrokersBrokerageModel.CanSubmitOrder(security, order, out var message);
Assert.IsTrue(canSubmit);
}
private static List<Security> GetUnsupportedOptions()
{
// Index option
var spxSymbol = Symbol.Create("SPX", SecurityType.IndexOption, Market.USA);
var spx = new Security(
SecurityExchangeHours.AlwaysOpen(TimeZones.Utc),
new SubscriptionDataConfig(typeof(TradeBar), spxSymbol, Resolution.Minute, TimeZones.Utc, TimeZones.Utc, false, true, false),
new Cash("USD", 1000, 1),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache());
//Cash settled option
var vixSymbol = Symbol.Create("VIX", SecurityType.Option, Market.USA);
var vix = new Option(
SecurityExchangeHours.AlwaysOpen(TimeZones.Utc),
new SubscriptionDataConfig(typeof(TradeBar), vixSymbol, Resolution.Minute, TimeZones.Utc, TimeZones.Utc, false, true, false),
new Cash("USD", 1000, 1),
new OptionSymbolProperties(SymbolProperties.GetDefault(Currencies.USD)),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null);
vix.ExerciseSettlement = SettlementType.Cash;
return new() {spx, vix};
}
private static TestCaseData[] GetForexOrderTestCases()
{
return new[]
{
Tuple.Create("USDCAD", 25000m, 7000000m),
Tuple.Create("AUDUSD", 25000m, 6000000m),
Tuple.Create("CADUSD", 25000m, 6000000m),
Tuple.Create("CHFUSD", 25000m, 6000000m),
Tuple.Create("CNHUSD", 150000m, 40000000m),
Tuple.Create("CZKUSD", 0m, 0m), // need market price in USD or EUR -- do later when we support
Tuple.Create("DKKUSD", 150000m, 35000000m),
Tuple.Create("EURUSD", 20000m, 6000000m),
Tuple.Create("GBPUSD", 20000m, 5000000m),
Tuple.Create("HKDUSD", 200000m, 50000000m),
Tuple.Create("HUFUSD", 0m, 0m), // need market price in USD or EUR -- do later when we support
Tuple.Create("ILSUSD", 0m, 0m), // need market price in USD or EUR -- do later when we support
Tuple.Create("KRWUSD", 0m, 200000000m),
Tuple.Create("JPYUSD", 2500000m, 550000000m),
Tuple.Create("MXNUSD", 300000m, 70000000m),
Tuple.Create("NOKUSD", 150000m, 35000000m),
Tuple.Create("NZDUSD", 35000m, 8000000m),
Tuple.Create("PLNUSD", 0m, 0m), // need market price in USD or EUR -- do later when we support
Tuple.Create("RUBUSD", 750000m, 30000000m),
Tuple.Create("SEKUSD", 175000m, 40000000m),
Tuple.Create("SGDUSD", 35000m, 8000000m),
Tuple.Create("ZARUSD", 350000m, 100000000m),
Tuple.Create("INRUSD", 0m, 0m) // not in the limits dictionary, should always return false
}
.Select(x =>
{
var currencyPair = x.Item1;
Forex.DecomposeCurrencyPair(currencyPair, out var baseCurrency, out var quoteCurrency);
var forexSymbol = Symbol.Create(currencyPair, SecurityType.Forex, Market.USA);
var forex = new Forex(
forexSymbol,
SecurityExchangeHours.AlwaysOpen(TimeZones.Utc),
new Cash(quoteCurrency, 0, 0.7m),
new Cash(baseCurrency, 0, 1),
SymbolProperties.GetDefault(quoteCurrency),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new ForexCache());
var min = x.Item2;
var max = x.Item3;
if (min != 0m || max != 0)
{
if (min == 0m)
{
return new[]
{
// buy
new TestCaseData(forex, min, false),
new TestCaseData(forex, max * 1.001m, false),
new TestCaseData(forex, 0.001m, true),
new TestCaseData(forex, max, true),
new TestCaseData(forex, max / 2, true),
// sell
new TestCaseData(forex, -max * 1.001m, false),
new TestCaseData(forex, -0.001m, true),
new TestCaseData(forex, -max, true),
new TestCaseData(forex, -max / 2, true)
};
}
return new[]
{
// buy
new TestCaseData(forex, min * 0.999m, false),
new TestCaseData(forex, max * 1.001m, false),
new TestCaseData(forex, min, true),
new TestCaseData(forex, max, true),
new TestCaseData(forex, (min + max) / 2, true),
// sell
new TestCaseData(forex, -min * 0.999m, false),
new TestCaseData(forex, -max * 1.001m, false),
new TestCaseData(forex, -min, true),
new TestCaseData(forex, -max, true),
new TestCaseData(forex, -(min + max) / 2, true)
};
}
// min and max are 0, need market price in USD or EUR, we don't support yet
return new[] { new TestCaseData(forex, 100000m, false) };
})
.SelectMany(x => x)
.ToArray();
}
}
}