Files
2026-07-13 13:02:50 +08:00

214 lines
10 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 Moq;
using NUnit.Framework;
using QuantConnect.Brokerages;
using QuantConnect.Securities;
using QuantConnect.Orders;
using QuantConnect.Tests.Brokerages;
using QuantConnect.Data.Market;
using System;
using QuantConnect.Orders.TimeInForces;
using System.Collections.Generic;
using QuantConnect.Data;
using QuantConnect.Securities.Option;
namespace QuantConnect.Tests.Common.Brokerages
{
[TestFixture]
public class TradierBrokerageModelTests
{
private TradierBrokerageModel _tradierBrokerageModel = new TradierBrokerageModel();
private static Security _security = GetSecurity(new DateTime(2025, 05, 28, 10, 0, 0));
[SetUp]
public void Init()
{
_security.Holdings.SetHoldings(1, 100);
}
[Test]
public void CanSubmitOrderReturnsFalseWhenShortGTCOrder()
{
var order = GetOrder();
order.Setup(x => x.Quantity).Returns(-101);
Assert.IsFalse(_tradierBrokerageModel.CanSubmitOrder(_security, order.Object, out var message));
var expectedMessage = new BrokerageMessageEvent(BrokerageMessageType.Warning, "ShortOrderIsGtc", "You cannot place short stock orders with GTC, only day orders are allowed");
Assert.AreEqual(expectedMessage.Message, message.Message);
}
[Test]
public void CanSubmitOrderReturnsFalseWhenSellShortOrderLastPriceBelow5()
{
var order = GetOrder();
order.Setup(x => x.Quantity).Returns(-101);
order.Object.Properties.TimeInForce = TimeInForce.Day;
Assert.IsFalse(_tradierBrokerageModel.CanSubmitOrder(_security, order.Object, out var message));
var expectedMessage = new BrokerageMessageEvent(BrokerageMessageType.Warning, "SellShortOrderLastPriceBelow5", "Sell Short order cannot be placed for stock priced below $5");
Assert.AreEqual(expectedMessage.Message, message.Message);
}
[Test]
public void CanSubmitOrderReturnsFalseWhenTimeInForceIsGoodTilDate()
{
var order = GetOrder();
order.Setup(x => x.Quantity).Returns(101);
order.Object.Properties.TimeInForce = TimeInForce.GoodTilDate(new DateTime());
Assert.IsFalse(_tradierBrokerageModel.CanSubmitOrder(_security, order.Object, out var message));
var expectedMessage = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", $"This model only supports orders with the following time in force types: {typeof(DayTimeInForce)} and {typeof(GoodTilCanceledTimeInForce)}");
Assert.AreEqual(expectedMessage.Message, message.Message);
}
[TestCase(0.5)]
[TestCase(10000001)]
public void CanSubmitOrderReturnsFalseWhenIncorrectOrderQuantity(decimal quantity)
{
var order = GetOrder();
order.Object.Properties.TimeInForce = TimeInForce.Day;
order.Setup(x => x.Quantity).Returns(quantity);
Assert.IsFalse(_tradierBrokerageModel.CanSubmitOrder(_security, order.Object, out var message));
var expectedMessage = new BrokerageMessageEvent(BrokerageMessageType.Warning, "IncorrectOrderQuantity", "Quantity should be between 1 and 10,000,000");
Assert.AreEqual(expectedMessage.Message, message.Message);
}
[Test]
public void CanSubmitOrderReturnsTrueQuantityIsValidAndNotGTC()
{
var order = GetOrder();
order.Setup(x => x.Quantity).Returns(-100);
order.Object.Properties.TimeInForce = TimeInForce.Day;
Assert.IsTrue(_tradierBrokerageModel.CanSubmitOrder(_security, order.Object, out var message));
}
[Test]
public void CanSubmitOrderReturnsTrueWhenQuantityIsValidAndNotGTCAndPriceAbove5()
{
var order = new Mock<Order>();
order.Setup(x => x.Quantity).Returns(-100);
order.Object.Properties.TimeInForce = TimeInForce.Day;
var security = GetSecurity(new DateTime(2025, 05, 28, 10, 0, 0));
security.SetMarketPrice(new Tick(security.LocalTime, security.Symbol, 100, 1000));
security.Holdings.SetHoldings(6, 100);
order.Object.Symbol = security.Symbol;
Assert.IsTrue(_tradierBrokerageModel.CanSubmitOrder(security, order.Object, out var message));
}
[Test]
public void CanSubmitOrderReturnsTrueWhenQuantityIsValidIsMarketOrderAndPriceAbove5()
{
var order = new Mock<Order>();
order.Setup(x => x.Quantity).Returns(-100);
var security = GetSecurity(new DateTime(2025, 05, 28, 10, 0, 0));
security.SetMarketPrice(new Tick(security.LocalTime, security.Symbol, 100, 1000));
security.Holdings.SetHoldings(6, 100);
order.Object.Symbol = security.Symbol;
Assert.IsTrue(_tradierBrokerageModel.CanSubmitOrder(security, order.Object, out var message));
}
[Test]
public void CanSubmitOrderReturnsTrueForIndexOptions()
{
var symbol = Symbol.Create("SPX", SecurityType.IndexOption, Market.USA);
var time = new DateTime(2025, 05, 28, 10, 0, 0);
var tz = TimeZones.NewYork;
var indexOption = 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
);
var localTimeKeeper = new LocalTimeKeeper(time.ConvertToUtc(tz), tz);
indexOption.Exchange.SetLocalDateTimeFrontierProvider(localTimeKeeper);
var order = new LimitOrder(symbol, 1, 100m, time);
var result = _tradierBrokerageModel.CanSubmitOrder(indexOption, order, out var message);
Assert.IsTrue(result);
Assert.IsNull(message);
}
private static IEnumerable<TestCaseData> ExtendedHoursTestCases
{
get
{
var preMarketTime = new DateTime(2025, 05, 28, 8, 0, 0);
var postMarketTime = new DateTime(2025, 05, 28, 19, 0, 0);
foreach (var time in new[] { preMarketTime, postMarketTime })
{
var equity = GetSecurity(time, marketAlwaysOpen: false);
yield return new TestCaseData(time, equity, OrderType.Limit, true);
yield return new TestCaseData(time, equity, OrderType.Market, false);
yield return new TestCaseData(time, equity, OrderType.StopMarket, false);
yield return new TestCaseData(time, equity, OrderType.StopLimit, false);
var option = GetSecurity(time, securityType: SecurityType.Option, marketAlwaysOpen: false);
yield return new TestCaseData(time, option, OrderType.Limit, false);
yield return new TestCaseData(time, option, OrderType.Market, false);
yield return new TestCaseData(time, option, OrderType.StopMarket, false);
yield return new TestCaseData(time, option, OrderType.StopLimit, false);
}
}
}
[TestCaseSource(nameof(ExtendedHoursTestCases))]
public void CanSubmitOrderOnExtendedHours(DateTime time, Security security, OrderType orderType, bool expectedResult)
{
var orderProperties = new TradierOrderProperties { OutsideRegularTradingHours = true };
Order order = orderType switch
{
OrderType.Market => new MarketOrder(security.Symbol, 100, time, properties: orderProperties),
OrderType.Limit => new LimitOrder(security.Symbol, 100, 100, time, properties: orderProperties),
OrderType.StopMarket => new StopMarketOrder(security.Symbol, 100, 100, time, properties: orderProperties),
OrderType.StopLimit => new StopLimitOrder(security.Symbol, 100, 100, 100, time, properties: orderProperties),
_ => throw new ArgumentException($"Unsupported order type: {orderType}", nameof(orderType))
};
Assert.AreEqual(expectedResult, _tradierBrokerageModel.CanSubmitOrder(security, order, out var message));
if (!expectedResult)
{
Assert.AreEqual(BrokerageMessageType.Warning, message.Type);
Assert.AreEqual("ExtendedMarket", message.Code);
Assert.IsTrue(message.Message.StartsWith("Tradier does not support explicitly placing out-of-regular-hours orders if not currently during the pre or post market session.", StringComparison.InvariantCultureIgnoreCase));
}
}
private Mock<Order> GetOrder()
{
var order = new Mock<Order>();
order.Object.Symbol = _security.Symbol;
return order;
}
private static Security GetSecurity(DateTime time, SecurityType securityType = SecurityType.Equity, bool marketAlwaysOpen = true)
{
var security = TestsHelpers.GetSecurity(securityType: securityType, symbol: "IBM", market: Market.USA,
marketAlwaysOpen: marketAlwaysOpen);
var localTimeKeeper = new LocalTimeKeeper(time.ConvertToUtc(security.Exchange.TimeZone),
security.Exchange.TimeZone);
security.SetLocalTimeKeeper(localTimeKeeper);
return security;
}
}
}