From 03b8912406f49d365e0c8d8d9598b65b93014e62 Mon Sep 17 00:00:00 2001 From: Romazes Date: Sat, 28 Mar 2026 01:43:39 +0200 Subject: [PATCH 1/5] feature: add webull brokerage core integration - add BrokerageName.Webull enum value - add WebullBrokerageModel supporting Equity, Option, IndexOption - add WebullFeeModel with zero commission for equity/option, tiered index option fees (SPX, SPXW, VIX/VIXW, XSP, DJX, NDX/NDXP), 0.6% crypto fee - register Webull in IBrokerageModel factory switch and GetBrokerageName - add webull config keys and live-webull environment to Launcher/config.json - add WebullBrokerageModelTests (CanSubmitOrder, GetFeeModel) - add WebullFeeModelTests covering all index option tiers and crypto --- Common/Brokerages/BrokerageName.cs | 7 +- Common/Brokerages/IBrokerageModel.cs | 6 + Common/Brokerages/WebullBrokerageModel.cs | 102 +++++ Common/Orders/Fees/WebullFeeModel.cs | 236 +++++++++++ Launcher/config.json | 23 + .../Brokerages/WebullBrokerageModelTests.cs | 127 ++++++ .../Common/Orders/Fees/WebullFeeModelTests.cs | 393 ++++++++++++++++++ 7 files changed, 893 insertions(+), 1 deletion(-) create mode 100644 Common/Brokerages/WebullBrokerageModel.cs create mode 100644 Common/Orders/Fees/WebullFeeModel.cs create mode 100644 Tests/Common/Brokerages/WebullBrokerageModelTests.cs create mode 100644 Tests/Common/Orders/Fees/WebullFeeModelTests.cs diff --git a/Common/Brokerages/BrokerageName.cs b/Common/Brokerages/BrokerageName.cs index 7f6edaab7a5e..9a1b7a994c71 100644 --- a/Common/Brokerages/BrokerageName.cs +++ b/Common/Brokerages/BrokerageName.cs @@ -197,6 +197,11 @@ public enum BrokerageName /// /// Transaction and submit/execution rules will use dYdX models /// - DYDX + DYDX, + + /// + /// Transaction and submit/execution rules will use Webull models + /// + Webull } } diff --git a/Common/Brokerages/IBrokerageModel.cs b/Common/Brokerages/IBrokerageModel.cs index 4544c388d8c1..c8c43f2ff69a 100644 --- a/Common/Brokerages/IBrokerageModel.cs +++ b/Common/Brokerages/IBrokerageModel.cs @@ -291,6 +291,9 @@ public static IBrokerageModel Create(IOrderProvider orderProvider, BrokerageName case BrokerageName.DYDX: return new dYdXBrokerageModel(accountType); + case BrokerageName.Webull: + return new WebullBrokerageModel(accountType); + default: throw new ArgumentOutOfRangeException(nameof(brokerage), brokerage, null); } @@ -394,6 +397,9 @@ public static BrokerageName GetBrokerageName(IBrokerageModel brokerageModel) case TastytradeBrokerageModel: return BrokerageName.Tastytrade; + case WebullBrokerageModel: + return BrokerageName.Webull; + case DefaultBrokerageModel _: return BrokerageName.Default; diff --git a/Common/Brokerages/WebullBrokerageModel.cs b/Common/Brokerages/WebullBrokerageModel.cs new file mode 100644 index 000000000000..683efb1927b5 --- /dev/null +++ b/Common/Brokerages/WebullBrokerageModel.cs @@ -0,0 +1,102 @@ +/* + * 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 QuantConnect.Orders; +using QuantConnect.Orders.Fees; +using QuantConnect.Securities; +using System.Collections.Generic; + +namespace QuantConnect.Brokerages +{ + /// + /// Represents a brokerage model specific to Webull. + /// + public class WebullBrokerageModel : DefaultBrokerageModel + { + /// + /// HashSet containing the security types supported by Webull. + /// + private readonly HashSet _supportSecurityTypes = new( + new[] + { + SecurityType.Equity, + SecurityType.Option, + SecurityType.IndexOption + }); + + /// + /// HashSet containing the order types supported by the operation in Webull. + /// + private readonly HashSet _supportOrderTypes = new( + new[] + { + OrderType.Market, + OrderType.Limit, + OrderType.StopMarket, + OrderType.StopLimit + }); + + /// + /// Constructor for Webull brokerage model. + /// + /// Cash or Margin + public WebullBrokerageModel(AccountType accountType = AccountType.Margin) + : base(accountType) + { + } + + /// + /// Provides the Webull fee model. + /// + /// Security + /// Webull fee model + public override IFeeModel GetFeeModel(Security security) + { + return new WebullFeeModel(); + } + + /// + /// Returns true if the brokerage could accept this order. This takes into account + /// order type, security type, and order size limits. + /// + /// + /// For example, a brokerage may have no connectivity at certain times, or an order rate/size limit. + /// + /// The security of the order + /// The order to be processed + /// If this function returns false, a brokerage message detailing why the order may not be submitted + /// True if the brokerage could process the order, false otherwise + public override bool CanSubmitOrder(Security security, Order order, out BrokerageMessageEvent message) + { + message = default; + + if (!_supportSecurityTypes.Contains(security.Type)) + { + message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", + Messages.DefaultBrokerageModel.UnsupportedSecurityType(this, security)); + return false; + } + + if (!_supportOrderTypes.Contains(order.Type)) + { + message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", + Messages.DefaultBrokerageModel.UnsupportedOrderType(this, order, _supportOrderTypes)); + return false; + } + + return base.CanSubmitOrder(security, order, out message); + } + } +} diff --git a/Common/Orders/Fees/WebullFeeModel.cs b/Common/Orders/Fees/WebullFeeModel.cs new file mode 100644 index 000000000000..45084010f4f9 --- /dev/null +++ b/Common/Orders/Fees/WebullFeeModel.cs @@ -0,0 +1,236 @@ +/* + * 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 QuantConnect.Securities; + +namespace QuantConnect.Orders.Fees +{ + /// + /// Represents a fee model specific to Webull. + /// + /// + /// + /// Equity and standard options trades are commission-free on Webull. + /// Index options carry a flat $0.50 Webull contract fee plus a variable exchange proprietary fee + /// that depends on the underlying index symbol and the option's market price. + /// Cryptocurrency trades carry a 0.6% fee on the notional trade value. + /// + public class WebullFeeModel : FeeModel + { + /// + /// Webull contract fee applied to every index option contract, regardless of underlying. + /// + private const decimal _webullIndexOptionContractFee = 0.50m; + + /// + /// Exchange proprietary fee for SPX options priced below $1.00. + /// + private const decimal _spxExchangeFeeBelow1 = 0.57m; + + /// + /// Exchange proprietary fee for SPX options priced at or above $1.00. + /// + private const decimal _spxExchangeFeeAbove1 = 0.66m; + + /// + /// Exchange proprietary fee for SPXW options priced below $1.00. + /// + private const decimal _spxwExchangeFeeBelow1 = 0.50m; + + /// + /// Exchange proprietary fee for SPXW options priced at or above $1.00. + /// + private const decimal _spxwExchangeFeeAbove1 = 0.59m; + + /// + /// VIX/VIXW exchange fee tier 1: option price at or below $0.10. + /// + private const decimal _vixExchangeFeeTier1 = 0.10m; + + /// + /// VIX/VIXW exchange fee tier 2: option price between $0.11 and $0.99. + /// + private const decimal _vixExchangeFeeTier2 = 0.25m; + + /// + /// VIX/VIXW exchange fee tier 3: option price between $1.00 and $1.99. + /// + private const decimal _vixExchangeFeeTier3 = 0.40m; + + /// + /// VIX/VIXW exchange fee tier 4: option price at or above $2.00. + /// + private const decimal _vixExchangeFeeTier4 = 0.45m; + + /// + /// XSP exchange fee for orders with fewer than 10 contracts. + /// + private const decimal _xspExchangeFeeSmall = 0.00m; + + /// + /// XSP exchange fee for orders with 10 or more contracts. + /// + private const decimal _xspExchangeFeeLarge = 0.07m; + + /// + /// DJX flat exchange proprietary fee per contract. + /// + private const decimal _djxExchangeFee = 0.18m; + + /// + /// NDX/NDXP exchange fee for single-leg orders with premium below $25. + /// + private const decimal _ndxSingleLegFeeBelow25 = 0.50m; + + /// + /// NDX/NDXP exchange fee for single-leg orders with premium at or above $25. + /// + private const decimal _ndxSingleLegFeeAbove25 = 0.75m; + + /// + /// NDX/NDXP exchange fee for multi-leg orders with premium below $25. + /// + private const decimal _ndxMultiLegFeeBelow25 = 0.65m; + + /// + /// NDX/NDXP exchange fee for multi-leg orders with premium at or above $25. + /// + private const decimal _ndxMultiLegFeeAbove25 = 0.90m; + + /// + /// Crypto fee rate applied as a percentage of the notional trade value (0.6%). + /// + private const decimal _cryptoFeeRate = 0.006m; + + /// + /// Gets the order fee for a given security and order. + /// + /// The parameters including the security and order details. + /// + /// for equity and standard options; + /// a per-contract fee for index options; + /// a percentage-of-notional fee for crypto. + /// + public override OrderFee GetOrderFee(OrderFeeParameters parameters) + { + switch (parameters.Security.Type) + { + case SecurityType.IndexOption: + return new OrderFee(new CashAmount(GetIndexOptionFee(parameters), Currencies.USD)); + case SecurityType.Crypto: + var notional = parameters.Order.AbsoluteQuantity * parameters.Security.Price; + return new OrderFee(new CashAmount(notional * _cryptoFeeRate, Currencies.USD)); + default: + // Equity and Option are commission-free on Webull. + return OrderFee.Zero; + } + } + + /// + /// Calculates the total per-contract fee for an index option order. + /// The total fee = (exchange proprietary fee + Webull contract fee) × quantity. + /// + /// Order fee parameters containing the security and order. + /// Total fee amount in USD. + private static decimal GetIndexOptionFee(OrderFeeParameters parameters) + { + var order = parameters.Order; + var security = parameters.Security; + var quantity = order.AbsoluteQuantity; + var price = security.Price; + var underlying = security.Symbol.Underlying?.Value?.ToUpperInvariant() ?? string.Empty; + var isMultiLeg = order.Type == OrderType.ComboMarket + || order.Type == OrderType.ComboLimit + || order.Type == OrderType.ComboLegLimit; + + var exchangeFee = GetIndexOptionExchangeFee(underlying, price, quantity, isMultiLeg); + return quantity * (exchangeFee + _webullIndexOptionContractFee); + } + + /// + /// Returns the exchange proprietary fee per contract for an index option, based on + /// the underlying ticker, the option's current price, order quantity, and leg type. + /// + /// Uppercase underlying ticker (e.g. "SPX", "VIX"). + /// Current market price of the option. + /// Absolute number of contracts in the order. + /// True when the order is a combo/multi-leg order. + /// Exchange fee per contract in USD. + private static decimal GetIndexOptionExchangeFee(string underlying, decimal price, decimal quantity, bool isMultiLeg) + { + switch (underlying) + { + case "SPX": + return price < 1m ? _spxExchangeFeeBelow1 : _spxExchangeFeeAbove1; + + case "SPXW": + return price < 1m ? _spxwExchangeFeeBelow1 : _spxwExchangeFeeAbove1; + + case "VIX": + case "VIXW": + return GetVixExchangeFee(price); + + case "XSP": + return quantity < 10m ? _xspExchangeFeeSmall : _xspExchangeFeeLarge; + + case "DJX": + return _djxExchangeFee; + + case "NDX": + case "NDXP": + return GetNdxExchangeFee(price, isMultiLeg); + + default: + return 0m; + } + } + + /// + /// Returns the VIX/VIXW exchange fee for a simple order based on the option price tier. + /// + private static decimal GetVixExchangeFee(decimal price) + { + if (price <= 0.10m) + { + return _vixExchangeFeeTier1; + } + + if (price <= 0.99m) + { + return _vixExchangeFeeTier2; + } + + if (price <= 1.99m) + { + return _vixExchangeFeeTier3; + } + + return _vixExchangeFeeTier4; + } + + /// + /// Returns the NDX/NDXP exchange fee per contract based on premium tier and order leg type. + /// + private static decimal GetNdxExchangeFee(decimal price, bool isMultiLeg) + { + if (isMultiLeg) + { + return price < 25m ? _ndxMultiLegFeeBelow25 : _ndxMultiLegFeeAbove25; + } + + return price < 25m ? _ndxSingleLegFeeBelow25 : _ndxSingleLegFeeAbove25; + } + } +} diff --git a/Launcher/config.json b/Launcher/config.json index cbac82fed394..49826b782165 100644 --- a/Launcher/config.json +++ b/Launcher/config.json @@ -255,6 +255,14 @@ "charles-schwab-authorization-code-from-url": "", "charles-schwab-redirect-url": "", + // Webull configuration + "webull-api-url": "https://api.webull.com", + "webull-data-mqtt-url": "wss://data-api.webull.com:8883/mqtt", + "webull-trade-grpc-url": "https://events-api.webull.com", + "webull-app-key": "", + "webull-app-secret": "", + "webull-account-id": "", + // Tastytrade configuration "tastytrade-api-url": "", "tastytrade-websocket-url": "", @@ -758,6 +766,21 @@ "history-provider": [ "BrokerageHistoryProvider", "SubscriptionDataReaderHistoryProvider" ] }, + // defines the 'live-webull' environment + "live-webull": { + "live-mode": true, + + // real brokerage implementations require the BrokerageTransactionHandler + "live-mode-brokerage": "WebullBrokerage", + "data-queue-handler": [ "WebullBrokerage" ], + "setup-handler": "QuantConnect.Lean.Engine.Setup.BrokerageSetupHandler", + "result-handler": "QuantConnect.Lean.Engine.Results.LiveTradingResultHandler", + "data-feed-handler": "QuantConnect.Lean.Engine.DataFeeds.LiveTradingDataFeed", + "real-time-handler": "QuantConnect.Lean.Engine.RealTime.LiveTradingRealTimeHandler", + "transaction-handler": "QuantConnect.Lean.Engine.TransactionHandlers.BrokerageTransactionHandler", + "history-provider": [ "BrokerageHistoryProvider", "SubscriptionDataReaderHistoryProvider" ] + }, + // defines the 'live-dydx' environment "live-dydx": { "live-mode": true, diff --git a/Tests/Common/Brokerages/WebullBrokerageModelTests.cs b/Tests/Common/Brokerages/WebullBrokerageModelTests.cs new file mode 100644 index 000000000000..99d879b4b6f5 --- /dev/null +++ b/Tests/Common/Brokerages/WebullBrokerageModelTests.cs @@ -0,0 +1,127 @@ +/* + * 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 NUnit.Framework; +using QuantConnect.Orders; +using QuantConnect.Brokerages; +using QuantConnect.Orders.Fees; +using QuantConnect.Securities; +using QuantConnect.Tests.Brokerages; + +namespace QuantConnect.Tests.Common.Brokerages +{ + [TestFixture] + public class WebullBrokerageModelTests + { + private readonly WebullBrokerageModel _brokerageModel = new WebullBrokerageModel(); + + // ── CanSubmitOrder — valid combinations ─────────────────────────────────── + + [TestCase(SecurityType.Equity, OrderType.Market)] + [TestCase(SecurityType.Equity, OrderType.Limit)] + [TestCase(SecurityType.Equity, OrderType.StopMarket)] + [TestCase(SecurityType.Equity, OrderType.StopLimit)] + [TestCase(SecurityType.Option, OrderType.Market)] + [TestCase(SecurityType.Option, OrderType.Limit)] + [TestCase(SecurityType.Option, OrderType.StopMarket)] + [TestCase(SecurityType.Option, OrderType.StopLimit)] + [TestCase(SecurityType.IndexOption, OrderType.Market)] + [TestCase(SecurityType.IndexOption, OrderType.Limit)] + [TestCase(SecurityType.IndexOption, OrderType.StopMarket)] + [TestCase(SecurityType.IndexOption, OrderType.StopLimit)] + public void CanSubmitOrder_ValidSecurityAndOrderType_ReturnsTrue(SecurityType securityType, OrderType orderType) + { + var security = TestsHelpers.GetSecurity(securityType: securityType, symbol: "AAPL", market: Market.USA); + var order = CreateOrder(orderType, security.Symbol); + + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + Assert.That(canSubmit, Is.True); + Assert.That(message, Is.Null); + } + + // ── CanSubmitOrder — unsupported security types ─────────────────────────── + + [TestCase(SecurityType.Forex)] + [TestCase(SecurityType.Future)] + [TestCase(SecurityType.Crypto)] + [TestCase(SecurityType.Cfd)] + public void CanSubmitOrder_UnsupportedSecurityType_ReturnsFalse(SecurityType securityType) + { + var security = TestsHelpers.GetSecurity(securityType: securityType, symbol: "EURUSD", market: Market.Oanda); + var order = new MarketOrder(security.Symbol, 1m, DateTime.UtcNow); + + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + Assert.That(canSubmit, Is.False); + Assert.That(message, Is.Not.Null); + } + + // ── CanSubmitOrder — unsupported order types ────────────────────────────── + + [TestCase(OrderType.MarketOnClose)] + [TestCase(OrderType.MarketOnOpen)] + [TestCase(OrderType.TrailingStop)] + [TestCase(OrderType.ComboMarket)] + public void CanSubmitOrder_UnsupportedOrderType_ReturnsFalse(OrderType orderType) + { + var security = TestsHelpers.GetSecurity(securityType: SecurityType.Equity, symbol: "AAPL", market: Market.USA); + var order = CreateOrder(orderType, security.Symbol); + + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + Assert.That(canSubmit, Is.False); + Assert.That(message, Is.Not.Null); + } + + // ── GetFeeModel ─────────────────────────────────────────────────────────── + + [Test] + public void GetFeeModel_ReturnsWebullFeeModel() + { + var security = TestsHelpers.GetSecurity(securityType: SecurityType.Equity, symbol: "AAPL", market: Market.USA); + + Assert.That(_brokerageModel.GetFeeModel(security), Is.InstanceOf()); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private static Order CreateOrder(OrderType orderType, Symbol symbol) + { + switch (orderType) + { + case OrderType.Market: + return new MarketOrder(symbol, 1m, DateTime.UtcNow); + case OrderType.Limit: + return new LimitOrder(symbol, 1m, 100m, DateTime.UtcNow); + case OrderType.StopMarket: + return new StopMarketOrder(symbol, 1m, 100m, DateTime.UtcNow); + case OrderType.StopLimit: + return new StopLimitOrder(symbol, 1m, 105m, 100m, DateTime.UtcNow); + case OrderType.MarketOnClose: + return new MarketOnCloseOrder(symbol, 1m, DateTime.UtcNow); + case OrderType.MarketOnOpen: + return new MarketOnOpenOrder(symbol, 1m, DateTime.UtcNow); + case OrderType.TrailingStop: + return new TrailingStopOrder(symbol, 1m, 100m, 1m, false, DateTime.UtcNow); + case OrderType.ComboMarket: + return new ComboMarketOrder(symbol, 1m, DateTime.UtcNow, new GroupOrderManager(1, 1, 1m)); + default: + throw new ArgumentOutOfRangeException(nameof(orderType), orderType, null); + } + } + } +} diff --git a/Tests/Common/Orders/Fees/WebullFeeModelTests.cs b/Tests/Common/Orders/Fees/WebullFeeModelTests.cs new file mode 100644 index 000000000000..175bde04c951 --- /dev/null +++ b/Tests/Common/Orders/Fees/WebullFeeModelTests.cs @@ -0,0 +1,393 @@ +/* + * 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 NUnit.Framework; +using QuantConnect.Data; +using QuantConnect.Data.Market; +using QuantConnect.Orders; +using QuantConnect.Orders.Fees; +using QuantConnect.Securities; +using QuantConnect.Securities.Crypto; +using QuantConnect.Tests.Common.Securities; + +namespace QuantConnect.Tests.Common.Orders.Fees +{ + [TestFixture] + public class WebullFeeModelTests + { + private readonly WebullFeeModel _feeModel = new WebullFeeModel(); + + // ── Equity / Option — zero commission ──────────────────────────────────── + + [Test] + public void GetOrderFee_Equity_ReturnsZero() + { + var security = SecurityTests.GetSecurity(); + security.SetMarketPrice(new Tick(DateTime.UtcNow, security.Symbol, 100m, 100m)); + var order = new MarketOrder(security.Symbol, 10m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(0m)); + } + + [Test] + public void GetOrderFee_Option_ReturnsZero() + { + var security = CreateOptionSecurity("AAPL", 5m); + var order = new MarketOrder(security.Symbol, 3m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(0m)); + } + + // ── IndexOption — SPX ───────────────────────────────────────────────────── + + /// + /// SPX, price < $1 → exchange $0.57 + Webull $0.50 = $1.07/contract. + /// 2 contracts → $2.14. + /// + [Test] + public void GetOrderFee_SpxPriceBelow1_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("SPX", price: 0.50m); + var order = new MarketOrder(security.Symbol, 2m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(2.14m)); + Assert.That(fee.Value.Currency, Is.EqualTo(Currencies.USD)); + } + + /// + /// SPX, price ≥ $1 → exchange $0.66 + Webull $0.50 = $1.16/contract. + /// 3 contracts → $3.48. + /// + [Test] + public void GetOrderFee_SpxPriceAbove1_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("SPX", price: 1.50m); + var order = new MarketOrder(security.Symbol, 3m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(3.48m)); + } + + // ── IndexOption — SPXW ──────────────────────────────────────────────────── + + /// + /// SPXW, price < $1 → exchange $0.50 + Webull $0.50 = $1.00/contract. + /// + [Test] + public void GetOrderFee_SpxwPriceBelow1_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("SPXW", price: 0.80m); + var order = new MarketOrder(security.Symbol, 1m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(1.00m)); + } + + /// + /// SPXW, price ≥ $1 → exchange $0.59 + Webull $0.50 = $1.09/contract. + /// 4 contracts → $4.36. + /// + [Test] + public void GetOrderFee_SpxwPriceAbove1_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("SPXW", price: 2.00m); + var order = new MarketOrder(security.Symbol, 4m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(4.36m)); + } + + // ── IndexOption — VIX ───────────────────────────────────────────────────── + + /// + /// VIX, price ≤ $0.10 → exchange $0.10 + Webull $0.50 = $0.60/contract. + /// 4 contracts → $2.40. + /// + [Test] + public void GetOrderFee_VixPriceTier1_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("VIX", price: 0.05m); + var order = new MarketOrder(security.Symbol, 4m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(2.40m)); + } + + /// + /// VIX, price $0.11–$0.99 → exchange $0.25 + Webull $0.50 = $0.75/contract. + /// 2 contracts → $1.50. + /// + [Test] + public void GetOrderFee_VixPriceTier2_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("VIX", price: 0.50m); + var order = new MarketOrder(security.Symbol, 2m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(1.50m)); + } + + /// + /// VIX, price $1.00–$1.99 → exchange $0.40 + Webull $0.50 = $0.90/contract. + /// + [Test] + public void GetOrderFee_VixPriceTier3_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("VIX", price: 1.50m); + var order = new MarketOrder(security.Symbol, 1m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(0.90m)); + } + + /// + /// VIX, price ≥ $2.00 → exchange $0.45 + Webull $0.50 = $0.95/contract. + /// 5 contracts → $4.75. + /// + [Test] + public void GetOrderFee_VixPriceTier4_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("VIX", price: 3.00m); + var order = new MarketOrder(security.Symbol, 5m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(4.75m)); + } + + // ── IndexOption — VIXW ──────────────────────────────────────────────────── + + /// + /// VIXW uses identical tier schedule to VIX; verify tier 2. + /// + [Test] + public void GetOrderFee_VixwPriceTier2_MatchesVixFee() + { + var vix = CreateIndexOptionSecurity("VIX", price: 0.50m); + var vixw = CreateIndexOptionSecurity("VIXW", price: 0.50m); + var order = new MarketOrder(vix.Symbol, 2m, DateTime.UtcNow); + + var vixFee = _feeModel.GetOrderFee(new OrderFeeParameters(vix, order)); + var vixwFee = _feeModel.GetOrderFee(new OrderFeeParameters(vixw, new MarketOrder(vixw.Symbol, 2m, DateTime.UtcNow))); + + Assert.That(vixFee.Value.Amount, Is.EqualTo(vixwFee.Value.Amount)); + } + + // ── IndexOption — XSP ───────────────────────────────────────────────────── + + /// + /// XSP, qty < 10 → exchange $0.00 + Webull $0.50 = $0.50/contract. + /// 5 contracts → $2.50. + /// + [Test] + public void GetOrderFee_XspSmallQuantity_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("XSP", price: 1.00m); + var order = new MarketOrder(security.Symbol, 5m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(2.50m)); + } + + /// + /// XSP, qty ≥ 10 → exchange $0.07 + Webull $0.50 = $0.57/contract. + /// 10 contracts → $5.70. + /// + [Test] + public void GetOrderFee_XspLargeQuantity_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("XSP", price: 1.00m); + var order = new MarketOrder(security.Symbol, 10m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(5.70m)); + } + + // ── IndexOption — DJX ───────────────────────────────────────────────────── + + /// + /// DJX flat → exchange $0.18 + Webull $0.50 = $0.68/contract. + /// 2 contracts → $1.36. + /// + [Test] + public void GetOrderFee_Djx_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("DJX", price: 2.00m); + var order = new MarketOrder(security.Symbol, 2m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(1.36m)); + } + + // ── IndexOption — NDX / NDXP ────────────────────────────────────────────── + + /// + /// NDX, single-leg, premium < $25 → exchange $0.50 + Webull $0.50 = $1.00/contract. + /// 3 contracts → $3.00. + /// + [Test] + public void GetOrderFee_NdxSingleLegPriceBelow25_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("NDX", price: 10.00m); + var order = new MarketOrder(security.Symbol, 3m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(3.00m)); + } + + /// + /// NDX, single-leg, premium ≥ $25 → exchange $0.75 + Webull $0.50 = $1.25/contract. + /// 2 contracts → $2.50. + /// + [Test] + public void GetOrderFee_NdxSingleLegPriceAbove25_ReturnsCorrectFee() + { + var security = CreateIndexOptionSecurity("NDX", price: 50.00m); + var order = new MarketOrder(security.Symbol, 2m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); + + Assert.That(fee.Value.Amount, Is.EqualTo(2.50m)); + } + + /// + /// NDXP uses the same fee schedule as NDX. + /// + [Test] + public void GetOrderFee_NdxpSingleLegPriceBelow25_MatchesNdxFee() + { + var ndx = CreateIndexOptionSecurity("NDX", price: 10.00m); + var ndxp = CreateIndexOptionSecurity("NDXP", price: 10.00m); + var ndxOrder = new MarketOrder(ndx.Symbol, 1m, DateTime.UtcNow); + var ndxpOrder = new MarketOrder(ndxp.Symbol, 1m, DateTime.UtcNow); + + var ndxFee = _feeModel.GetOrderFee(new OrderFeeParameters(ndx, ndxOrder)); + var ndxpFee = _feeModel.GetOrderFee(new OrderFeeParameters(ndxp, ndxpOrder)); + + Assert.That(ndxFee.Value.Amount, Is.EqualTo(ndxpFee.Value.Amount)); + } + + // ── Crypto ──────────────────────────────────────────────────────────────── + + /// + /// Crypto fee = 0.6% of notional (quantity × price). + /// 2 BTC × $50,000 = $100,000 notional → fee = $600. + /// + [Test] + public void GetOrderFee_Crypto_ReturnsPointSixPercentOfNotional() + { + var btcusd = CreateCryptoSecurity(price: 50_000m); + var order = new MarketOrder(btcusd.Symbol, 2m, DateTime.UtcNow); + + var fee = _feeModel.GetOrderFee(new OrderFeeParameters(btcusd, order)); + + // 2 * 50000 * 0.006 = 600 + Assert.That(fee.Value.Amount, Is.EqualTo(600m)); + Assert.That(fee.Value.Currency, Is.EqualTo(Currencies.USD)); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + /// + /// Creates an index option security with the given underlying ticker and option price. + /// Uses + /// with an Index underlying to produce a symbol. + /// + private static Security CreateIndexOptionSecurity(string underlyingTicker, decimal price) + { + var underlying = Symbol.Create(underlyingTicker, SecurityType.Index, Market.USA); + var symbol = Symbol.CreateOption( + underlying, Market.USA, OptionStyle.European, OptionRight.Call, 1000m, new DateTime(2026, 6, 20)); + + var config = new SubscriptionDataConfig( + typeof(TradeBar), symbol, Resolution.Minute, + TimeZones.Utc, TimeZones.Utc, false, true, false); + + var security = new Security( + SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), + config, + new Cash(Currencies.USD, 0, 1m), + SymbolProperties.GetDefault(Currencies.USD), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache()); + + security.SetMarketPrice(new Tick(DateTime.UtcNow, symbol, price, price)); + return security; + } + + /// + /// Creates a standard equity option security (SecurityType.Option) for zero-fee assertions. + /// + private static Security CreateOptionSecurity(string underlyingTicker, decimal price) + { + var underlying = Symbol.Create(underlyingTicker, SecurityType.Equity, Market.USA); + var symbol = Symbol.CreateOption( + underlying, Market.USA, OptionStyle.American, OptionRight.Call, 150m, new DateTime(2026, 6, 20)); + + var config = new SubscriptionDataConfig( + typeof(TradeBar), symbol, Resolution.Minute, + TimeZones.Utc, TimeZones.Utc, false, true, false); + + var security = new Security( + SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), + config, + new Cash(Currencies.USD, 0, 1m), + SymbolProperties.GetDefault(Currencies.USD), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache()); + + security.SetMarketPrice(new Tick(DateTime.UtcNow, symbol, price, price)); + return security; + } + + /// + /// Creates a crypto security with the given USD price set via . + /// + private static Crypto CreateCryptoSecurity(decimal price) + { + var btcusd = new Crypto( + SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), + new Cash(Currencies.USD, 0, 1m), + new Cash("BTC", 0, price), + new SubscriptionDataConfig(typeof(TradeBar), Symbols.BTCUSD, Resolution.Minute, + TimeZones.Utc, TimeZones.Utc, true, false, false), + new SymbolProperties("BTCUSD", Currencies.USD, 1, 0.01m, 0.00000001m, string.Empty), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null); + + btcusd.SetMarketPrice(new Tick(DateTime.UtcNow, btcusd.Symbol, price, price)); + return btcusd; + } + } +} From fa64b589f326992c447a77c6003340ddf0375902 Mon Sep 17 00:00:00 2001 From: Romazes Date: Tue, 31 Mar 2026 01:28:53 +0300 Subject: [PATCH 2/5] refactor: split webull supported order types per security type - replace flat _supportSecurityTypes/_supportOrderTypes with _supportedOrderTypesBySecurityType dictionary - options and index options: Limit, StopMarket, StopLimit only - equity, future, crypto: Market, Limit, StopMarket, StopLimit, TrailingStop - add WebullOrderProperties with OutsideRegularTradingHours flag - add messages for unsupported order type validation --- Common/Brokerages/WebullBrokerageModel.cs | 105 +++++++--- Common/Messages/Messages.Brokerages.cs | 33 +++ Common/Orders/WebullOrderProperties.cs | 34 ++++ Launcher/config.json | 1 - .../Brokerages/WebullBrokerageModelTests.cs | 192 ++++++++++++++++-- 5 files changed, 329 insertions(+), 36 deletions(-) create mode 100644 Common/Orders/WebullOrderProperties.cs diff --git a/Common/Brokerages/WebullBrokerageModel.cs b/Common/Brokerages/WebullBrokerageModel.cs index 683efb1927b5..1ca0ecd70837 100644 --- a/Common/Brokerages/WebullBrokerageModel.cs +++ b/Common/Brokerages/WebullBrokerageModel.cs @@ -13,10 +13,11 @@ * limitations under the License. */ +using System.Collections.Generic; using QuantConnect.Orders; using QuantConnect.Orders.Fees; +using QuantConnect.Orders.TimeInForces; using QuantConnect.Securities; -using System.Collections.Generic; namespace QuantConnect.Brokerages { @@ -26,27 +27,56 @@ namespace QuantConnect.Brokerages public class WebullBrokerageModel : DefaultBrokerageModel { /// - /// HashSet containing the security types supported by Webull. - /// - private readonly HashSet _supportSecurityTypes = new( - new[] - { - SecurityType.Equity, - SecurityType.Option, - SecurityType.IndexOption - }); - - /// - /// HashSet containing the order types supported by the operation in Webull. + /// Maps each supported security type to the order types Webull allows for it. /// - private readonly HashSet _supportOrderTypes = new( - new[] + private static readonly Dictionary> _supportedOrderTypesBySecurityType = + new Dictionary> { - OrderType.Market, - OrderType.Limit, - OrderType.StopMarket, - OrderType.StopLimit - }); + { + SecurityType.Equity, new HashSet + { + OrderType.Market, + OrderType.Limit, + OrderType.StopMarket, + OrderType.StopLimit, + OrderType.TrailingStop + } + }, + { + SecurityType.Option, new HashSet + { + OrderType.Limit, + OrderType.StopMarket, + OrderType.StopLimit + } + }, + { + SecurityType.IndexOption, new HashSet + { + OrderType.Limit, + OrderType.StopMarket, + OrderType.StopLimit + } + }, + { + SecurityType.Future, new HashSet + { + OrderType.Market, + OrderType.Limit, + OrderType.StopMarket, + OrderType.StopLimit, + OrderType.TrailingStop + } + }, + { + SecurityType.Crypto, new HashSet + { + OrderType.Market, + OrderType.Limit, + OrderType.StopLimit + } + } + }; /// /// Constructor for Webull brokerage model. @@ -82,17 +112,46 @@ public override bool CanSubmitOrder(Security security, Order order, out Brokerag { message = default; - if (!_supportSecurityTypes.Contains(security.Type)) + if (!_supportedOrderTypesBySecurityType.TryGetValue(security.Type, out var supportedOrderTypes)) { message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", Messages.DefaultBrokerageModel.UnsupportedSecurityType(this, security)); return false; } - if (!_supportOrderTypes.Contains(order.Type)) + if (!supportedOrderTypes.Contains(order.Type)) + { + message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", + Messages.DefaultBrokerageModel.UnsupportedOrderType(this, order, supportedOrderTypes)); + return false; + } + + // Options and IndexOptions have per-direction TimeInForce restrictions. + // https://developer.webull.com/apis/docs/trade-api/options#time-in-force + // - Sell orders: Day only + // - Buy orders: GoodTilCanceled only + if (security.Type == SecurityType.Option || security.Type == SecurityType.IndexOption) + { + if (order.Direction == OrderDirection.Sell && order.TimeInForce is not DayTimeInForce) + { + message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", + Messages.WebullBrokerageModel.InvalidTimeInForceForOptionSellOrder(order)); + return false; + } + + if (order.Direction == OrderDirection.Buy && order.TimeInForce is not GoodTilCanceledTimeInForce) + { + message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", + Messages.WebullBrokerageModel.InvalidTimeInForceForOptionBuyOrder(order)); + return false; + } + } + + if (order.Properties is WebullOrderProperties { OutsideRegularTradingHours: true } && + security.Type != SecurityType.Equity) { message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", - Messages.DefaultBrokerageModel.UnsupportedOrderType(this, order, _supportOrderTypes)); + Messages.WebullBrokerageModel.OutsideRegularTradingHoursNotSupportedForSecurityType(security)); return false; } diff --git a/Common/Messages/Messages.Brokerages.cs b/Common/Messages/Messages.Brokerages.cs index b1095cacbef5..30bd315e8a46 100644 --- a/Common/Messages/Messages.Brokerages.cs +++ b/Common/Messages/Messages.Brokerages.cs @@ -595,6 +595,39 @@ public static string UnsupportedOrderType(Orders.Order order) } } + /// + /// Provides user-facing messages for the class and its consumers or related classes + /// + public static class WebullBrokerageModel + { + /// + /// Returns a message explaining that Options and IndexOptions sell orders only support Day time in force. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string InvalidTimeInForceForOptionSellOrder(Orders.Order order) + { + return Invariant($"{order.Symbol.SecurityType} sell orders only support {nameof(DayTimeInForce)} time in force, but {order.TimeInForce.GetType().Name} was specified."); + } + + /// + /// Returns a message explaining that Options and IndexOptions buy orders only support GoodTilCanceled time in force. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string InvalidTimeInForceForOptionBuyOrder(Orders.Order order) + { + return Invariant($"{order.Symbol.SecurityType} buy orders only support {nameof(GoodTilCanceledTimeInForce)} time in force, but {order.TimeInForce.GetType().Name} was specified."); + } + + /// + /// Returns a message explaining that OutsideRegularTradingHours is only supported for Equity orders. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string OutsideRegularTradingHoursNotSupportedForSecurityType(Securities.Security security) + { + return Invariant($"{nameof(WebullOrderProperties.OutsideRegularTradingHours)} is only supported for {nameof(SecurityType.Equity)} orders, but {security.Type} was specified."); + } + } + /// /// Provides user-facing messages for the class and its consumers or related classes /// diff --git a/Common/Orders/WebullOrderProperties.cs b/Common/Orders/WebullOrderProperties.cs new file mode 100644 index 000000000000..ce7932379a19 --- /dev/null +++ b/Common/Orders/WebullOrderProperties.cs @@ -0,0 +1,34 @@ +/* + * 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. + * +*/ + +namespace QuantConnect.Orders +{ + /// + /// Represents the properties of an order in Webull. + /// + public class WebullOrderProperties : OrderProperties + { + /// + /// If set to true, allows the order to trigger or fill outside of regular trading hours + /// (pre-market and after-hours sessions). + /// + /// + /// Applicable to Equity orders only. Extended-hours trading carries additional risks, + /// including lower liquidity and wider bid/ask spreads. + /// + public bool OutsideRegularTradingHours { get; set; } + } +} diff --git a/Launcher/config.json b/Launcher/config.json index 49826b782165..b6f00f025854 100644 --- a/Launcher/config.json +++ b/Launcher/config.json @@ -257,7 +257,6 @@ // Webull configuration "webull-api-url": "https://api.webull.com", - "webull-data-mqtt-url": "wss://data-api.webull.com:8883/mqtt", "webull-trade-grpc-url": "https://events-api.webull.com", "webull-app-key": "", "webull-app-secret": "", diff --git a/Tests/Common/Brokerages/WebullBrokerageModelTests.cs b/Tests/Common/Brokerages/WebullBrokerageModelTests.cs index 99d879b4b6f5..51824cc72574 100644 --- a/Tests/Common/Brokerages/WebullBrokerageModelTests.cs +++ b/Tests/Common/Brokerages/WebullBrokerageModelTests.cs @@ -18,6 +18,7 @@ using QuantConnect.Orders; using QuantConnect.Brokerages; using QuantConnect.Orders.Fees; +using QuantConnect.Orders.TimeInForces; using QuantConnect.Securities; using QuantConnect.Tests.Brokerages; @@ -30,25 +31,40 @@ public class WebullBrokerageModelTests // ── CanSubmitOrder — valid combinations ─────────────────────────────────── + // Equity: all five order types supported [TestCase(SecurityType.Equity, OrderType.Market)] [TestCase(SecurityType.Equity, OrderType.Limit)] [TestCase(SecurityType.Equity, OrderType.StopMarket)] [TestCase(SecurityType.Equity, OrderType.StopLimit)] - [TestCase(SecurityType.Option, OrderType.Market)] + [TestCase(SecurityType.Equity, OrderType.TrailingStop)] + // Option: Market and TrailingStop are not supported [TestCase(SecurityType.Option, OrderType.Limit)] [TestCase(SecurityType.Option, OrderType.StopMarket)] [TestCase(SecurityType.Option, OrderType.StopLimit)] - [TestCase(SecurityType.IndexOption, OrderType.Market)] + // IndexOption: same restrictions as Option [TestCase(SecurityType.IndexOption, OrderType.Limit)] [TestCase(SecurityType.IndexOption, OrderType.StopMarket)] [TestCase(SecurityType.IndexOption, OrderType.StopLimit)] + // Future: all five order types supported + [TestCase(SecurityType.Future, OrderType.Market)] + [TestCase(SecurityType.Future, OrderType.Limit)] + [TestCase(SecurityType.Future, OrderType.StopMarket)] + [TestCase(SecurityType.Future, OrderType.StopLimit)] + [TestCase(SecurityType.Future, OrderType.TrailingStop)] + // Crypto: StopMarket and TrailingStop are not supported + [TestCase(SecurityType.Crypto, OrderType.Market)] + [TestCase(SecurityType.Crypto, OrderType.Limit)] + [TestCase(SecurityType.Crypto, OrderType.StopLimit)] public void CanSubmitOrder_ValidSecurityAndOrderType_ReturnsTrue(SecurityType securityType, OrderType orderType) { - var security = TestsHelpers.GetSecurity(securityType: securityType, symbol: "AAPL", market: Market.USA); + // Arrange + var security = GetSecurityForType(securityType); var order = CreateOrder(orderType, security.Symbol); + // Act var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + // Assert Assert.That(canSubmit, Is.True); Assert.That(message, Is.Null); } @@ -56,35 +72,158 @@ public void CanSubmitOrder_ValidSecurityAndOrderType_ReturnsTrue(SecurityType se // ── CanSubmitOrder — unsupported security types ─────────────────────────── [TestCase(SecurityType.Forex)] - [TestCase(SecurityType.Future)] - [TestCase(SecurityType.Crypto)] [TestCase(SecurityType.Cfd)] public void CanSubmitOrder_UnsupportedSecurityType_ReturnsFalse(SecurityType securityType) { + // Arrange var security = TestsHelpers.GetSecurity(securityType: securityType, symbol: "EURUSD", market: Market.Oanda); var order = new MarketOrder(security.Symbol, 1m, DateTime.UtcNow); + // Act var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + // Assert Assert.That(canSubmit, Is.False); Assert.That(message, Is.Not.Null); } - // ── CanSubmitOrder — unsupported order types ────────────────────────────── + // ── CanSubmitOrder — order types unsupported for specific security types ── - [TestCase(OrderType.MarketOnClose)] - [TestCase(OrderType.MarketOnOpen)] - [TestCase(OrderType.TrailingStop)] - [TestCase(OrderType.ComboMarket)] - public void CanSubmitOrder_UnsupportedOrderType_ReturnsFalse(OrderType orderType) + // Equity does not support exchange-session orders or combo orders + [TestCase(SecurityType.Equity, OrderType.MarketOnClose)] + [TestCase(SecurityType.Equity, OrderType.MarketOnOpen)] + [TestCase(SecurityType.Equity, OrderType.ComboMarket)] + // Option does not support Market or TrailingStop + [TestCase(SecurityType.Option, OrderType.Market)] + [TestCase(SecurityType.Option, OrderType.TrailingStop)] + // IndexOption has the same restrictions as Option + [TestCase(SecurityType.IndexOption, OrderType.Market)] + [TestCase(SecurityType.IndexOption, OrderType.TrailingStop)] + // Crypto does not support StopMarket or TrailingStop + [TestCase(SecurityType.Crypto, OrderType.StopMarket)] + [TestCase(SecurityType.Crypto, OrderType.TrailingStop)] + public void CanSubmitOrder_UnsupportedOrderTypeForSecurityType_ReturnsFalse( + SecurityType securityType, OrderType orderType) { - var security = TestsHelpers.GetSecurity(securityType: SecurityType.Equity, symbol: "AAPL", market: Market.USA); + // Arrange + var security = GetSecurityForType(securityType); var order = CreateOrder(orderType, security.Symbol); + // Act + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + // Assert + Assert.That(canSubmit, Is.False); + Assert.That(message, Is.Not.Null); + } + + // ── CanSubmitOrder — Option/IndexOption TimeInForce restrictions ──────── + // https://developer.webull.com/apis/docs/trade-api/options#time-in-force + // Sell → Day only | Buy → GoodTilCanceled only + + [TestCase(SecurityType.Option, OrderDirection.Sell)] // Sell + Day + [TestCase(SecurityType.Option, OrderDirection.Buy)] // Buy + GTC + [TestCase(SecurityType.IndexOption, OrderDirection.Sell)] + [TestCase(SecurityType.IndexOption, OrderDirection.Buy)] + public void CanSubmitOrder_OptionOrderWithValidTimeInForce_ReturnsTrue( + SecurityType securityType, OrderDirection direction) + { + // Arrange + var security = GetSecurityForType(securityType); + var tif = direction == OrderDirection.Sell + ? TimeInForce.Day + : TimeInForce.GoodTilCanceled; + var order = CreateLimitOrder(security.Symbol, direction, tif); + + // Act var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + // Assert + Assert.That(canSubmit, Is.True); + Assert.That(message, Is.Null); + } + + [TestCase(SecurityType.Option, OrderDirection.Sell)] // Sell + GTC → rejected + [TestCase(SecurityType.Option, OrderDirection.Buy)] // Buy + Day → rejected + [TestCase(SecurityType.IndexOption, OrderDirection.Sell)] + [TestCase(SecurityType.IndexOption, OrderDirection.Buy)] + public void CanSubmitOrder_OptionOrderWithInvalidTimeInForce_ReturnsFalse( + SecurityType securityType, OrderDirection direction) + { + // Arrange + var security = GetSecurityForType(securityType); + // Deliberately use the wrong TIF for the direction + var tif = direction == OrderDirection.Sell + ? TimeInForce.GoodTilCanceled + : TimeInForce.Day; + var order = CreateLimitOrder(security.Symbol, direction, tif); + + // Act + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + // Assert Assert.That(canSubmit, Is.False); Assert.That(message, Is.Not.Null); + Assert.That(message.Message, Does.Contain(tif.GetType().Name)); + Assert.That(message.Message, Does.Contain(security.Type.ToString())); + } + + // ── CanSubmitOrder — OutsideRegularTradingHours ────────────────────────── + // https://developer.webull.com/apis/docs/trade-api — Applicable to U.S. stock market orders only. + + [Test] + public void CanSubmitOrder_OutsideRegularTradingHoursOnEquity_ReturnsTrue() + { + // Arrange + var security = GetSecurityForType(SecurityType.Equity); + var properties = new WebullOrderProperties { OutsideRegularTradingHours = true }; + var order = new MarketOrder(security.Symbol, 1m, DateTime.UtcNow, properties: properties); + + // Act + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + // Assert + Assert.That(canSubmit, Is.True); + Assert.That(message, Is.Null); + } + + [TestCase(SecurityType.Option)] + [TestCase(SecurityType.IndexOption)] + [TestCase(SecurityType.Future)] + [TestCase(SecurityType.Crypto)] + public void CanSubmitOrder_OutsideRegularTradingHoursOnNonEquity_ReturnsFalse(SecurityType securityType) + { + // Arrange + var security = GetSecurityForType(securityType); + var properties = new WebullOrderProperties { OutsideRegularTradingHours = true }; + var order = new LimitOrder(security.Symbol, 1m, 100m, DateTime.UtcNow, properties: properties); + + // Act + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + // Assert + Assert.That(canSubmit, Is.False); + Assert.That(message, Is.Not.Null); + Assert.That(message.Message, Does.Contain(nameof(WebullOrderProperties.OutsideRegularTradingHours))); + Assert.That(message.Message, Does.Contain(securityType.ToString())); + } + + [TestCase(SecurityType.Option)] + [TestCase(SecurityType.Future)] + [TestCase(SecurityType.Crypto)] + public void CanSubmitOrder_OutsideRegularTradingHoursFalseOnNonEquity_ReturnsTrue(SecurityType securityType) + { + // Arrange + var security = GetSecurityForType(securityType); + var properties = new WebullOrderProperties { OutsideRegularTradingHours = false }; + var order = new LimitOrder(security.Symbol, 1m, 100m, DateTime.UtcNow, properties: properties); + + // Act + var canSubmit = _brokerageModel.CanSubmitOrder(security, order, out var message); + + // Assert + Assert.That(canSubmit, Is.True); + Assert.That(message, Is.Null); } // ── GetFeeModel ─────────────────────────────────────────────────────────── @@ -92,13 +231,42 @@ public void CanSubmitOrder_UnsupportedOrderType_ReturnsFalse(OrderType orderType [Test] public void GetFeeModel_ReturnsWebullFeeModel() { + // Arrange var security = TestsHelpers.GetSecurity(securityType: SecurityType.Equity, symbol: "AAPL", market: Market.USA); + // Act / Assert Assert.That(_brokerageModel.GetFeeModel(security), Is.InstanceOf()); } // ── Helpers ─────────────────────────────────────────────────────────────── + private static Security GetSecurityForType(SecurityType securityType) + { + switch (securityType) + { + case SecurityType.Future: + return TestsHelpers.GetSecurity(securityType: SecurityType.Future, + symbol: Futures.Indices.SP500EMini, market: Market.CME); + case SecurityType.Crypto: + return TestsHelpers.GetSecurity(securityType: SecurityType.Crypto, + symbol: "BTCUSD", market: Market.Coinbase); + case SecurityType.Forex: + case SecurityType.Cfd: + return TestsHelpers.GetSecurity(securityType: securityType, + symbol: "EURUSD", market: Market.Oanda); + default: + return TestsHelpers.GetSecurity(securityType: securityType, + symbol: "AAPL", market: Market.USA); + } + } + + private static LimitOrder CreateLimitOrder(Symbol symbol, OrderDirection direction, TimeInForce timeInForce) + { + var quantity = direction == OrderDirection.Buy ? 1m : -1m; + var properties = new OrderProperties { TimeInForce = timeInForce }; + return new LimitOrder(symbol, quantity, 100m, DateTime.UtcNow, properties: properties); + } + private static Order CreateOrder(OrderType orderType, Symbol symbol) { switch (orderType) From d804d93c7826996b1f382b1330fabaaec0eb929e Mon Sep 17 00:00:00 2001 From: Romazes Date: Thu, 2 Apr 2026 19:09:27 +0300 Subject: [PATCH 3/5] fix: add market order type to option and index option supported orders --- Common/Brokerages/WebullBrokerageModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Common/Brokerages/WebullBrokerageModel.cs b/Common/Brokerages/WebullBrokerageModel.cs index 1ca0ecd70837..f69870b647b1 100644 --- a/Common/Brokerages/WebullBrokerageModel.cs +++ b/Common/Brokerages/WebullBrokerageModel.cs @@ -45,6 +45,7 @@ public class WebullBrokerageModel : DefaultBrokerageModel { SecurityType.Option, new HashSet { + OrderType.Market, OrderType.Limit, OrderType.StopMarket, OrderType.StopLimit @@ -53,6 +54,7 @@ public class WebullBrokerageModel : DefaultBrokerageModel { SecurityType.IndexOption, new HashSet { + OrderType.Market, OrderType.Limit, OrderType.StopMarket, OrderType.StopLimit From c7c8463a1dbea194bae1578d6196f6a6704a2eff Mon Sep 17 00:00:00 2001 From: Romazes Date: Thu, 2 Apr 2026 19:10:22 +0300 Subject: [PATCH 4/5] chore: add webull crypto symbols to symbol-properties-database --- .../symbol-properties-database.csv | 275 +++++++++++++++++- 1 file changed, 274 insertions(+), 1 deletion(-) diff --git a/Data/symbol-properties/symbol-properties-database.csv b/Data/symbol-properties/symbol-properties-database.csv index ef8d02d02d67..a075b2638537 100644 --- a/Data/symbol-properties/symbol-properties-database.csv +++ b/Data/symbol-properties/symbol-properties-database.csv @@ -10407,4 +10407,277 @@ dydx,ZETAUSD,cryptofuture,ZETA-USD,USD,1,0.0001,10,ZETA-USD,10.00000,10000000000 dydx,ZKUSD,cryptofuture,ZK-USD,USD,1,0.0001,10,ZK-USD,10.00000,10000000000,100000 dydx,ZORAUSD,cryptofuture,ZORA-USD,USD,1,0.00001,100,ZORA-USD,100.0000,100000000000,10000 dydx,ZROUSD,cryptofuture,ZRO-USD,USD,1,0.001,1,ZRO-USD,1.000000,1000000000,1000000 -dydx,ZRXUSD,cryptofuture,ZRX-USD,USD,1,0.0001,10,ZRX-USD,10.00000,10000000000,100000 \ No newline at end of file +dydx,ZRXUSD,cryptofuture,ZRX-USD,USD,1,0.0001,10,ZRX-USD,10.00000,10000000000,100000 + + +webull,1INCHUSD,crypto,1INCHUSD,USD,1,0.001,0.01,1INCHUSD,0.01 +webull,A8USD,crypto,A8USD,USD,1,0.0001,0.01,A8USD,0.01 +webull,AAVEUSD,crypto,AAVEUSD,USD,1,0.01,0.00001,AAVEUSD,0.001 +webull,ABTUSD,crypto,ABTUSD,USD,1,0.0001,0.1,ABTUSD,0.1 +webull,ACHUSD,crypto,ACHUSD,USD,1,0.000001,0.1,ACHUSD,0.1 +webull,ACSUSD,crypto,ACSUSD,USD,1,0.0000001,1,ACSUSD,1 +webull,ACXUSD,crypto,ACXUSD,USD,1,0.0001,0.1,ACXUSD,0.1 +webull,ADAUSD,crypto,ADAUSD,USD,1,0.0001,0.00000001,ADAUSD,0.00000001 +webull,AERGOUSD,crypto,AERGOUSD,USD,1,0.0001,0.1,AERGOUSD,0.1 +webull,AGLDUSD,crypto,AGLDUSD,USD,1,0.0001,0.01,AGLDUSD,0.01 +webull,AIOZUSD,crypto,AIOZUSD,USD,1,0.0001,0.1,AIOZUSD,0.1 +webull,AKTUSD,crypto,AKTUSD,USD,1,0.001,0.01,AKTUSD,0.01 +webull,ALCXUSD,crypto,ALCXUSD,USD,1,0.01,0.0001,ALCXUSD,0.0001 +webull,ALEOUSD,crypto,ALEOUSD,USD,1,0.001,0.01,ALEOUSD,0.01 +webull,ALEPHUSD,crypto,ALEPHUSD,USD,1,0.0001,0.1,ALEPHUSD,0.1 +webull,ALGOUSD,crypto,ALGOUSD,USD,1,0.0001,0.001,ALGOUSD,0.1 +webull,ALICEUSD,crypto,ALICEUSD,USD,1,0.001,0.001,ALICEUSD,0.001 +webull,ALTUSD,crypto,ALTUSD,USD,1,0.00001,1,ALTUSD,1 +webull,AMPUSD,crypto,AMPUSD,USD,1,0.00001,1,AMPUSD,1 +webull,ANKRUSD,crypto,ANKRUSD,USD,1,0.00001,1,ANKRUSD,1 +webull,APEUSD,crypto,APEUSD,USD,1,0.001,0.001,APEUSD,0.01 +webull,API3USD,crypto,API3USD,USD,1,0.001,0.01,API3USD,0.01 +webull,APTUSD,crypto,APTUSD,USD,1,0.01,0.001,APTUSD,0.001 +webull,ARBUSD,crypto,ARBUSD,USD,1,0.0001,0.01,ARBUSD,0.01 +webull,ARKMUSD,crypto,ARKMUSD,USD,1,0.001,0.01,ARKMUSD,0.01 +webull,ARPAUSD,crypto,ARPAUSD,USD,1,0.0001,0.1,ARPAUSD,0.1 +webull,ASMUSD,crypto,ASMUSD,USD,1,0.00001,1,ASMUSD,1 +webull,ASTUSD,crypto,ASTUSD,USD,1,0.0001,0.1,ASTUSD,0.1 +webull,ATOMUSD,crypto,ATOMUSD,USD,1,0.001,0.001,ATOMUSD,0.01 +webull,AUCTIONUSD,crypto,AUCTIONUSD,USD,1,0.01,0.001,AUCTIONUSD,0.001 +webull,AUDIOUSD,crypto,AUDIOUSD,USD,1,0.0001,0.1,AUDIOUSD,0.1 +webull,AURORAUSD,crypto,AURORAUSD,USD,1,0.0001,0.01,AURORAUSD,0.01 +webull,AVAXUSD,crypto,AVAXUSD,USD,1,0.01,0.00000001,AVAXUSD,0.00000001 +webull,AVTUSD,crypto,AVTUSD,USD,1,0.01,0.01,AVTUSD,0.01 +webull,AXLUSD,crypto,AXLUSD,USD,1,0.0001,0.1,AXLUSD,0.1 +webull,AXSUSD,crypto,AXSUSD,USD,1,0.001,0.001,AXSUSD,0.001 +webull,BADGERUSD,crypto,BADGERUSD,USD,1,0.01,0.001,BADGERUSD,0.001 +webull,BALUSD,crypto,BALUSD,USD,1,0.0001,0.001,BALUSD,0.001 +webull,BANDUSD,crypto,BANDUSD,USD,1,0.001,0.01,BANDUSD,0.01 +webull,BATUSD,crypto,BATUSD,USD,1,0.00001,0.001,BATUSD,0.01 +webull,BCHUSD,crypto,BCHUSD,USD,1,0.01,0.00000001,BCHUSD,0.00000001 +webull,BERAUSD,crypto,BERAUSD,USD,1,0.001,0.001,BERAUSD,0.01 +webull,BICOUSD,crypto,BICOUSD,USD,1,0.0001,0.01,BICOUSD,0.01 +webull,BIGTIMEUSD,crypto,BIGTIMEUSD,USD,1,0.00001,1,BIGTIMEUSD,1 +webull,BLASTUSD,crypto,BLASTUSD,USD,1,0.00001,1,BLASTUSD,1 +webull,BLURUSD,crypto,BLURUSD,USD,1,0.0001,0.1,BLURUSD,0.1 +webull,BLZUSD,crypto,BLZUSD,USD,1,0.0001,0.001,BLZUSD,0.1 +webull,BNTUSD,crypto,BNTUSD,USD,1,0.0001,0.000001,BNTUSD,0.000001 +webull,BOBAUSD,crypto,BOBAUSD,USD,1,0.0001,0.1,BOBAUSD,0.1 +webull,BONKUSD,crypto,BONKUSD,USD,1,0.00000001,1,BONKUSD,1 +webull,BTCUSD,crypto,BTCUSD,USD,1,0.01,0.00000001,BTCUSD,0.00000001 +webull,BTRSTUSD,crypto,BTRSTUSD,USD,1,0.001,0.01,BTRSTUSD,0.01 +webull,C98USD,crypto,C98USD,USD,1,0.0001,0.01,C98USD,0.01 +webull,CBETHUSD,crypto,CBETHUSD,USD,1,0.01,0.00001,CBETHUSD,0.00001 +webull,CELRUSD,crypto,CELRUSD,USD,1,0.00001,1,CELRUSD,1 +webull,CGLDUSD,crypto,CGLDUSD,USD,1,0.001,0.01,CGLDUSD,0.01 +webull,CHZUSD,crypto,CHZUSD,USD,1,0.0001,0.1,CHZUSD,0.1 +webull,CLVUSD,crypto,CLVUSD,USD,1,0.0001,0.001,CLVUSD,0.01 +webull,COMPUSD,crypto,COMPUSD,USD,1,0.01,0.00001,COMPUSD,0.001 +webull,COOKIEUSD,crypto,COOKIEUSD,USD,1,0.00001,0.1,COOKIEUSD,0.1 +webull,CORECHAINUSD,crypto,CORECHAINUSD,USD,1,0.001,0.01,CORECHAINUSD,0.01 +webull,COSMOSDYDXUSD,crypto,COSMOSDYDXUSD,USD,1,0.0001,0.01,COSMOSDYDXUSD,0.01 +webull,COTIUSD,crypto,COTIUSD,USD,1,0.0001,0.1,COTIUSD,0.1 +webull,COWUSD,crypto,COWUSD,USD,1,0.0001,0.1,COWUSD,0.1 +webull,CROUSD,crypto,CROUSD,USD,1,0.0001,0.1,CROUSD,0.1 +webull,CRVUSD,crypto,CRVUSD,USD,1,0.0001,0.001,CRVUSD,0.01 +webull,CTSIUSD,crypto,CTSIUSD,USD,1,0.0001,0.1,CTSIUSD,0.1 +webull,CTXUSD,crypto,CTXUSD,USD,1,0.0001,0.001,CTXUSD,0.001 +webull,CVCUSD,crypto,CVCUSD,USD,1,0.0001,0.1,CVCUSD,0.1 +webull,CVXUSD,crypto,CVXUSD,USD,1,0.001,0.001,CVXUSD,0.001 +webull,DAIUSD,crypto,DAIUSD,USD,1,0.0001,0.00001,DAIUSD,0.00001 +webull,DASHUSD,crypto,DASHUSD,USD,1,0.01,0.001,DASHUSD,0.001 +webull,DEXTUSD,crypto,DEXTUSD,USD,1,0.0001,0.1,DEXTUSD,0.1 +webull,DIAUSD,crypto,DIAUSD,USD,1,0.00001,0.01,DIAUSD,0.01 +webull,DIMOUSD,crypto,DIMOUSD,USD,1,0.00001,0.1,DIMOUSD,0.1 +webull,DNTUSD,crypto,DNTUSD,USD,1,0.0001,0.1,DNTUSD,0.1 +webull,DOGEUSD,crypto,DOGEUSD,USD,1,0.00001,0.1,DOGEUSD,0.1 +webull,DOTUSD,crypto,DOTUSD,USD,1,0.001,0.00000001,DOTUSD,0.00000001 +webull,DRIFTUSD,crypto,DRIFTUSD,USD,1,0.001,0.01,DRIFTUSD,0.01 +webull,EDGEUSD,crypto,EDGEUSD,USD,1,0.00001,0.1,EDGEUSD,0.1 +webull,EGLDUSD,crypto,EGLDUSD,USD,1,0.01,0.001,EGLDUSD,0.001 +webull,EIGENUSD,crypto,EIGENUSD,USD,1,0.001,0.01,EIGENUSD,0.01 +webull,ELAUSD,crypto,ELAUSD,USD,1,0.001,0.01,ELAUSD,0.01 +webull,ENAUSD,crypto,ENAUSD,USD,1,0.0001,0.001,ENAUSD,0.1 +webull,ENSUSD,crypto,ENSUSD,USD,1,0.01,0.001,ENSUSD,0.001 +webull,ERNUSD,crypto,ERNUSD,USD,1,0.0001,0.001,ERNUSD,0.001 +webull,ETCUSD,crypto,ETCUSD,USD,1,0.01,0.00000001,ETCUSD,0.00000001 +webull,ETHFIUSD,crypto,ETHFIUSD,USD,1,0.001,0.01,ETHFIUSD,0.01 +webull,ETHUSD,crypto,ETHUSD,USD,1,0.01,0.00000001,ETHUSD,0.00000001 +webull,FARMUSD,crypto,FARMUSD,USD,1,0.01,0.001,FARMUSD,0.001 +webull,FARTCOINUSD,crypto,FARTCOINUSD,USD,1,0.0001,0.01,FARTCOINUSD,0.01 +webull,FETUSD,crypto,FETUSD,USD,1,0.0001,0.1,FETUSD,0.1 +webull,FIDAUSD,crypto,FIDAUSD,USD,1,0.0001,0.01,FIDAUSD,0.01 +webull,FILUSD,crypto,FILUSD,USD,1,0.001,0.001,FILUSD,0.001 +webull,FISUSD,crypto,FISUSD,USD,1,0.0001,0.1,FISUSD,0.1 +webull,FLOKIUSD,crypto,FLOKIUSD,USD,1,0.0000001,0.1,FLOKIUSD,1 +webull,FLRUSD,crypto,FLRUSD,USD,1,0.00001,1,FLRUSD,1 +webull,FORTHUSD,crypto,FORTHUSD,USD,1,0.0001,0.001,FORTHUSD,0.001 +webull,FORTUSD,crypto,FORTUSD,USD,1,0.0001,0.001,FORTUSD,0.01 +webull,FOXUSD,crypto,FOXUSD,USD,1,0.0001,0.1,FOXUSD,0.1 +webull,FXUSD,crypto,FXUSD,USD,1,0.0001,0.1,FXUSD,0.1 +webull,GFIUSD,crypto,GFIUSD,USD,1,0.0001,0.01,GFIUSD,0.01 +webull,GHSTUSD,crypto,GHSTUSD,USD,1,0.001,0.01,GHSTUSD,0.01 +webull,GIGAUSD,crypto,GIGAUSD,USD,1,0.00001,0.1,GIGAUSD,0.1 +webull,GLMUSD,crypto,GLMUSD,USD,1,0.0001,0.1,GLMUSD,0.1 +webull,GMTUSD,crypto,GMTUSD,USD,1,0.0001,0.01,GMTUSD,0.01 +webull,GNOUSD,crypto,GNOUSD,USD,1,0.01,0.0001,GNOUSD,0.0001 +webull,GODSUSD,crypto,GODSUSD,USD,1,0.00001,0.001,GODSUSD,0.01 +webull,GRTUSD,crypto,GRTUSD,USD,1,0.0001,0.001,GRTUSD,0.01 +webull,GSTUSD,crypto,GSTUSD,USD,1,0.000001,0.01,GSTUSD,0.01 +webull,GTCUSD,crypto,GTCUSD,USD,1,0.01,0.01,GTCUSD,0.01 +webull,GUSD,crypto,GUSD,USD,1,0.00001,1,GUSD,1 +webull,HBARUSD,crypto,HBARUSD,USD,1,0.00001,0.001,HBARUSD,0.1 +webull,HFTUSD,crypto,HFTUSD,USD,1,0.0001,0.01,HFTUSD,0.01 +webull,HIGHUSD,crypto,HIGHUSD,USD,1,0.001,0.01,HIGHUSD,0.01 +webull,HNTUSD,crypto,HNTUSD,USD,1,0.001,0.01,HNTUSD,0.01 +webull,HONEYUSD,crypto,HONEYUSD,USD,1,0.0001,0.1,HONEYUSD,0.1 +webull,ICPUSD,crypto,ICPUSD,USD,1,0.001,0.0001,ICPUSD,0.0001 +webull,IDEXUSD,crypto,IDEXUSD,USD,1,0.0001,0.1,IDEXUSD,0.1 +webull,ILVUSD,crypto,ILVUSD,USD,1,0.01,0.0001,ILVUSD,0.0001 +webull,IMXUSD,crypto,IMXUSD,USD,1,0.0001,0.001,IMXUSD,0.01 +webull,INDEXUSD,crypto,INDEXUSD,USD,1,0.01,0.001,INDEXUSD,0.001 +webull,INJUSD,crypto,INJUSD,USD,1,0.001,0.0001,INJUSD,0.01 +webull,INVUSD,crypto,INVUSD,USD,1,0.01,0.0001,INVUSD,0.0001 +webull,IOTXUSD,crypto,IOTXUSD,USD,1,0.00001,1,IOTXUSD,1 +webull,IOUSD,crypto,IOUSD,USD,1,0.001,0.01,IOUSD,0.01 +webull,IPUSD,crypto,IPUSD,USD,1,0.001,0.01,IPUSD,0.01 +webull,JASMYUSD,crypto,JASMYUSD,USD,1,0.00001,0.001,JASMYUSD,1 +webull,JITOSOLUSD,crypto,JITOSOLUSD,USD,1,0.01,0.0001,JITOSOLUSD,0.0001 +webull,JTOUSD,crypto,JTOUSD,USD,1,0.0001,0.001,JTOUSD,0.1 +webull,KAITOUSD,crypto,KAITOUSD,USD,1,0.0001,0.01,KAITOUSD,0.01 +webull,KARRATUSD,crypto,KARRATUSD,USD,1,0.0001,0.01,KARRATUSD,0.01 +webull,KERNELUSD,crypto,KERNELUSD,USD,1,0.0001,0.01,KERNELUSD,0.01 +webull,KRLUSD,crypto,KRLUSD,USD,1,0.0001,0.1,KRLUSD,0.1 +webull,KSMUSD,crypto,KSMUSD,USD,1,0.01,0.0001,KSMUSD,0.0001 +webull,L3USD,crypto,L3USD,USD,1,0.00001,0.1,L3USD,0.1 +webull,LAUSD,crypto,LAUSD,USD,1,0.0001,0.1,LAUSD,0.1 +webull,LCXUSD,crypto,LCXUSD,USD,1,0.0001,0.1,LCXUSD,0.1 +webull,LDOUSD,crypto,LDOUSD,USD,1,0.001,0.001,LDOUSD,0.01 +webull,LINKUSD,crypto,LINKUSD,USD,1,0.001,0.0001,LINKUSD,0.01 +webull,LOKAUSD,crypto,LOKAUSD,USD,1,0.0001,0.01,LOKAUSD,0.01 +webull,LPTUSD,crypto,LPTUSD,USD,1,0.01,0.001,LPTUSD,0.001 +webull,LQTYUSD,crypto,LQTYUSD,USD,1,0.0001,0.01,LQTYUSD,0.01 +webull,LRCUSD,crypto,LRCUSD,USD,1,0.0001,0.000001,LRCUSD,0.000001 +webull,LRDSUSD,crypto,LRDSUSD,USD,1,0.0001,0.01,LRDSUSD,0.01 +webull,LSETHUSD,crypto,LSETHUSD,USD,1,0.01,0.00001,LSETHUSD,0.00001 +webull,LTCUSD,crypto,LTCUSD,USD,1,0.01,0.00000001,LTCUSD,0.00000001 +webull,MAGICUSD,crypto,MAGICUSD,USD,1,0.0001,0.01,MAGICUSD,0.01 +webull,MANAUSD,crypto,MANAUSD,USD,1,0.0001,0.001,MANAUSD,0.01 +webull,MANTLEUSD,crypto,MANTLEUSD,USD,1,0.0001,0.01,MANTLEUSD,0.01 +webull,MASKUSD,crypto,MASKUSD,USD,1,0.01,0.01,MASKUSD,0.01 +webull,MATHUSD,crypto,MATHUSD,USD,1,0.0001,0.1,MATHUSD,0.1 +webull,MATICUSD,crypto,MATICUSD,USD,1,0.0001,0.1,MATICUSD,0.1 +webull,MDTUSD,crypto,MDTUSD,USD,1,0.00001,1,MDTUSD,1 +webull,METISUSD,crypto,METISUSD,USD,1,0.01,0.001,METISUSD,0.001 +webull,MEUSD,crypto,MEUSD,USD,1,0.001,0.01,MEUSD,0.01 +webull,MINAUSD,crypto,MINAUSD,USD,1,0.001,0.001,MINAUSD,0.001 +webull,MKRUSD,crypto,MKRUSD,USD,1,0.01,0.00000001,MKRUSD,0.000001 +webull,MLNUSD,crypto,MLNUSD,USD,1,0.01,0.001,MLNUSD,0.001 +webull,MNDEUSD,crypto,MNDEUSD,USD,1,0.00001,0.1,MNDEUSD,0.1 +webull,MOODENGUSD,crypto,MOODENGUSD,USD,1,0.0001,0.001,MOODENGUSD,0.01 +webull,MSOLUSD,crypto,MSOLUSD,USD,1,0.01,0.001,MSOLUSD,0.001 +webull,MUSEUSD,crypto,MUSEUSD,USD,1,0.001,0.001,MUSEUSD,0.001 +webull,NCTUSD,crypto,NCTUSD,USD,1,0.00001,1,NCTUSD,1 +webull,NEARUSD,crypto,NEARUSD,USD,1,0.001,0.001,NEARUSD,0.001 +webull,NEONUSD,crypto,NEONUSD,USD,1,0.00001,0.01,NEONUSD,0.01 +webull,NEWTUSD,crypto,NEWTUSD,USD,1,0.0001,0.01,NEWTUSD,0.01 +webull,NKNUSD,crypto,NKNUSD,USD,1,0.0001,0.1,NKNUSD,0.1 +webull,NMRUSD,crypto,NMRUSD,USD,1,0.01,0.001,NMRUSD,0.001 +webull,OGNUSD,crypto,OGNUSD,USD,1,0.00001,0.01,OGNUSD,0.01 +webull,OMNIUSD,crypto,OMNIUSD,USD,1,0.001,0.01,OMNIUSD,0.01 +webull,ONDOUSD,crypto,ONDOUSD,USD,1,0.00001,0.001,ONDOUSD,0.01 +webull,OPUSD,crypto,OPUSD,USD,1,0.001,0.001,OPUSD,0.01 +webull,ORCAUSD,crypto,ORCAUSD,USD,1,0.0001,0.001,ORCAUSD,0.01 +webull,OSMOUSD,crypto,OSMOUSD,USD,1,0.0001,0.01,OSMOUSD,0.01 +webull,OXTUSD,crypto,OXTUSD,USD,1,0.0001,1,OXTUSD,1 +webull,PAXGUSD,crypto,PAXGUSD,USD,1,0.01,0.00001,PAXGUSD,0.00001 +webull,PAXUSD,crypto,PAXUSD,USD,1,0.0001,0.01,PAXUSD,0.01 +webull,PENDLEUSD,crypto,PENDLEUSD,USD,1,0.001,0.01,PENDLEUSD,0.01 +webull,PENGUUSD,crypto,PENGUUSD,USD,1,0.00001,0.001,PENGUUSD,1 +webull,PEPEUSD,crypto,PEPEUSD,USD,1,0.00000001,1,PEPEUSD,1 +webull,PERPUSD,crypto,PERPUSD,USD,1,0.0001,0.001,PERPUSD,0.001 +webull,PIRATEUSD,crypto,PIRATEUSD,USD,1,0.0001,0.1,PIRATEUSD,0.1 +webull,PLUUSD,crypto,PLUUSD,USD,1,0.01,0.01,PLUUSD,0.01 +webull,PNGUSD,crypto,PNGUSD,USD,1,0.00001,1,PNGUSD,1 +webull,PNUTUSD,crypto,PNUTUSD,USD,1,0.0001,0.001,PNUTUSD,0.01 +webull,POLSUSD,crypto,POLSUSD,USD,1,0.0001,0.01,POLSUSD,0.01 +webull,POLUSD,crypto,POLUSD,USD,1,0.0001,0.001,POLUSD,0.01 +webull,PONDUSD,crypto,PONDUSD,USD,1,0.00001,1,PONDUSD,1 +webull,POPCATUSD,crypto,POPCATUSD,USD,1,0.0001,0.01,POPCATUSD,0.01 +webull,POWRUSD,crypto,POWRUSD,USD,1,0.0001,0.1,POWRUSD,0.1 +webull,PRCLUSD,crypto,PRCLUSD,USD,1,0.0001,0.1,PRCLUSD,0.1 +webull,PRIMEUSD,crypto,PRIMEUSD,USD,1,0.001,0.01,PRIMEUSD,0.01 +webull,PROUSD,crypto,PROUSD,USD,1,0.0001,0.01,PROUSD,0.01 +webull,PROVEUSD,crypto,PROVEUSD,USD,1,0.0001,0.01,PROVEUSD,0.01 +webull,PUMPUSD,crypto,PUMPUSD,USD,1,0.000001,1,PUMPUSD,1 +webull,PUNDIXUSD,crypto,PUNDIXUSD,USD,1,0.0001,0.01,PUNDIXUSD,0.01 +webull,PYRUSD,crypto,PYRUSD,USD,1,0.001,0.01,PYRUSD,0.01 +webull,QIUSD,crypto,QIUSD,USD,1,0.000001,1,QIUSD,1 +webull,QNTUSD,crypto,QNTUSD,USD,1,0.01,0.001,QNTUSD,0.001 +webull,RADUSD,crypto,RADUSD,USD,1,0.01,0.01,RADUSD,0.01 +webull,RAREUSD,crypto,RAREUSD,USD,1,0.0001,0.001,RAREUSD,0.1 +webull,RARIUSD,crypto,RARIUSD,USD,1,0.01,0.001,RARIUSD,0.001 +webull,REDUSD,crypto,REDUSD,USD,1,0.0001,0.01,REDUSD,0.01 +webull,REQUSD,crypto,REQUSD,USD,1,0.0001,1,REQUSD,1 +webull,REZUSD,crypto,REZUSD,USD,1,0.00001,1,REZUSD,1 +webull,RLCUSD,crypto,RLCUSD,USD,1,0.0001,0.01,RLCUSD,0.01 +webull,RONINUSD,crypto,RONINUSD,USD,1,0.001,0.01,RONINUSD,0.01 +webull,ROSEUSD,crypto,ROSEUSD,USD,1,0.00001,0.1,ROSEUSD,0.1 +webull,SAFEUSD,crypto,SAFEUSD,USD,1,0.0001,0.01,SAFEUSD,0.01 +webull,SANDUSD,crypto,SANDUSD,USD,1,0.0001,0.001,SANDUSD,0.01 +webull,SDUSD,crypto,SDUSD,USD,1,0.0001,0.01,SDUSD,0.01 +webull,SEIUSD,crypto,SEIUSD,USD,1,0.0001,0.001,SEIUSD,0.1 +webull,SHDWUSD,crypto,SHDWUSD,USD,1,0.001,0.01,SHDWUSD,0.01 +webull,SHIBUSD,crypto,SHIBUSD,USD,1,0.00000001,1,SHIBUSD,1 +webull,SHPINGUSD,crypto,SHPINGUSD,USD,1,0.000001,1,SHPINGUSD,1 +webull,SKYUSD,crypto,SKYUSD,USD,1,0.00001,0.1,SKYUSD,0.1 +webull,SNXUSD,crypto,SNXUSD,USD,1,0.001,0.001,SNXUSD,0.001 +webull,SOLUSD,crypto,SOLUSD,USD,1,0.01,0.00000001,SOLUSD,0.00000001 +webull,SPAUSD,crypto,SPAUSD,USD,1,0.000001,0.001,SPAUSD,1 +webull,SPELLUSD,crypto,SPELLUSD,USD,1,0.0000001,1,SPELLUSD,1 +webull,SPKUSD,crypto,SPKUSD,USD,1,0.00001,0.1,SPKUSD,0.1 +webull,STGUSD,crypto,STGUSD,USD,1,0.0001,0.1,STGUSD,0.1 +webull,STORJUSD,crypto,STORJUSD,USD,1,0.0001,0.01,STORJUSD,0.01 +webull,STRKUSD,crypto,STRKUSD,USD,1,0.001,0.001,STRKUSD,0.01 +webull,STXUSD,crypto,STXUSD,USD,1,0.0001,0.01,STXUSD,0.01 +webull,SUIUSD,crypto,SUIUSD,USD,1,0.0001,0.001,SUIUSD,0.1 +webull,SUPERUSD,crypto,SUPERUSD,USD,1,0.00001,0.01,SUPERUSD,0.01 +webull,SUSD,crypto,SUSD,USD,1,0.00001,0.1,SUSD,0.1 +webull,SUSHIUSD,crypto,SUSHIUSD,USD,1,0.0001,0.001,SUSHIUSD,0.01 +webull,SWELLUSD,crypto,SWELLUSD,USD,1,0.00001,1,SWELLUSD,1 +webull,SWFTCUSD,crypto,SWFTCUSD,USD,1,0.000001,0.001,SWFTCUSD,1 +webull,SXTUSD,crypto,SXTUSD,USD,1,0.0001,0.1,SXTUSD,0.1 +webull,SYRUPUSD,crypto,SYRUPUSD,USD,1,0.0001,0.001,SYRUPUSD,0.1 +webull,TAOUSD,crypto,TAOUSD,USD,1,0.01,0.00000001,TAOUSD,0.0001 +webull,TIAUSD,crypto,TIAUSD,USD,1,0.001,0.001,TIAUSD,0.01 +webull,TIMEUSD,crypto,TIMEUSD,USD,1,0.01,0.001,TIMEUSD,0.001 +webull,TNSRUSD,crypto,TNSRUSD,USD,1,0.001,0.01,TNSRUSD,0.01 +webull,TOSHIUSD,crypto,TOSHIUSD,USD,1,0.0000001,1,TOSHIUSD,1 +webull,TRACUSD,crypto,TRACUSD,USD,1,0.0001,0.1,TRACUSD,0.1 +webull,TRBUSD,crypto,TRBUSD,USD,1,0.01,0.001,TRBUSD,0.001 +webull,TRUMPUSD,crypto,TRUMPUSD,USD,1,0.01,0.0001,TRUMPUSD,0.001 +webull,TRUUSD,crypto,TRUUSD,USD,1,0.0001,0.1,TRUUSD,0.1 +webull,TUSD,crypto,TUSD,USD,1,0.00001,1,TUSD,1 +webull,UMAUSD,crypto,UMAUSD,USD,1,0.001,0.001,UMAUSD,0.001 +webull,UNIUSD,crypto,UNIUSD,USD,1,0.001,0.000001,UNIUSD,0.000001 +webull,USDCUSD,crypto,USD Coin,USD,1,0.00001,0.01,USDCUSD,0.000001 +webull,USDTUSD,crypto,USDTUSD,USD,1,0.00001,0.001,USDTUSD,0.01 +webull,VARAUSD,crypto,VARAUSD,USD,1,0.00001,1,VARAUSD,1 +webull,VETUSD,crypto,VETUSD,USD,1,0.00001,1,VETUSD,1 +webull,VTHOUSD,crypto,VTHOUSD,USD,1,0.000001,1,VTHOUSD,1 +webull,VVVUSD,crypto,VVVUSD,USD,1,0.01,0.001,VVVUSD,0.001 +webull,WAXLUSD,crypto,WAXLUSD,USD,1,0.0001,0.01,WAXLUSD,0.01 +webull,WCFGUSD,crypto,WCFGUSD,USD,1,0.001,0.01,WCFGUSD,0.01 +webull,WIFUSD,crypto,WIFUSD,USD,1,0.001,0.0001,WIFUSD,0.01 +webull,WLDUSD,crypto,WLDUSD,USD,1,0.001,0.001,WLDUSD,0.01 +webull,WLFIUSD,crypto,WLFIUSD,USD,1,0.00001,0.1,WLFIUSD,0.1 +webull,XCNUSD,crypto,XCNUSD,USD,1,0.00001,0.001,XCNUSD,0.1 +webull,XLMUSD,crypto,XLMUSD,USD,1,0.000001,0.00000001,XLMUSD,0.00000001 +webull,XRPUSD,crypto,XRPUSD,USD,1,0.0001,0.000001,XRPUSD,0.000001 +webull,XTZUSD,crypto,XTZUSD,USD,1,0.001,0.001,XTZUSD,0.01 +webull,XYOUSD,crypto,XYOUSD,USD,1,0.00001,0.1,XYOUSD,0.1 +webull,YFIUSD,crypto,YFIUSD,USD,1,0.01,0.00000001,YFIUSD,0.000001 +webull,ZECUSD,crypto,ZECUSD,USD,1,0.01,0.00001,ZECUSD,0.00001 +webull,ZENUSD,crypto,ZENUSD,USD,1,0.001,0.001,ZENUSD,0.001 +webull,ZETACHAINUSD,crypto,ZETACHAINUSD,USD,1,0.0001,0.01,ZETACHAINUSD,0.01 +webull,ZETAUSD,crypto,ZETAUSD,USD,1,0.0001,0.01,ZETAUSD,0.1 +webull,ZKUSD,crypto,ZKUSD,USD,1,0.00001,0.1,ZKUSD,0.1 +webull,ZORAUSD,crypto,ZORAUSD,USD,1,0.00001,0.001,ZORAUSD,1 +webull,ZROUSD,crypto,ZROUSD,USD,1,0.001,0.01,ZROUSD,0.01 +webull,ZRXUSD,crypto,ZRXUSD,USD,1,0.000001,0.00001,ZRXUSD,0.00001 From 1ddebec1c4177b2294b6c1f6e5ab85792d1fa6e7 Mon Sep 17 00:00:00 2001 From: Romazes Date: Thu, 2 Apr 2026 22:30:11 +0300 Subject: [PATCH 5/5] test: consolidate webull fee model and brokerage model tests - merge per-tier and per-symbol [Test] methods into [TestCase]/[TestCaseSource] parameterized tests in WebullFeeModelTests - replace CreateIndexOptionSecurity/CreateOptionSecurity/CreateCryptoSecurity with single CreateSecurity(SecurityType, decimal, string) helper - rename test methods to PascalCase (drop underscores) in WebullBrokerageModelTests - remove section-separator comments from WebullBrokerageModelTests - update Launcher/config.json for local Webull UAT environment --- Tests/Brokerages/TestHelpers.cs | 5 +- .../Brokerages/WebullBrokerageModelTests.cs | 43 +-- .../Common/Orders/Fees/WebullFeeModelTests.cs | 361 +++++------------- 3 files changed, 106 insertions(+), 303 deletions(-) diff --git a/Tests/Brokerages/TestHelpers.cs b/Tests/Brokerages/TestHelpers.cs index 58396da12747..eeb4a31aa1fa 100644 --- a/Tests/Brokerages/TestHelpers.cs +++ b/Tests/Brokerages/TestHelpers.cs @@ -58,9 +58,12 @@ private static SubscriptionDataConfig CreateConfig(string symbol, string market, break; case SecurityType.Option: - case SecurityType.IndexOption: actualSymbol = Symbols.CreateOptionSymbol(symbol, OptionRight.Call, 1000, new DateTime(2020, 3, 26)); break; + case SecurityType.IndexOption: + var index = Symbols.CreateIndexSymbol(symbol); + actualSymbol = Symbol.CreateOption(index, index.ID.Market, SecurityType.IndexOption.DefaultOptionStyle(), OptionRight.Call, 6500m, new(2026, 04, 13)); + break; default: actualSymbol = Symbol.Create(symbol, securityType, market); diff --git a/Tests/Common/Brokerages/WebullBrokerageModelTests.cs b/Tests/Common/Brokerages/WebullBrokerageModelTests.cs index 51824cc72574..4db336cb4e3d 100644 --- a/Tests/Common/Brokerages/WebullBrokerageModelTests.cs +++ b/Tests/Common/Brokerages/WebullBrokerageModelTests.cs @@ -18,7 +18,6 @@ using QuantConnect.Orders; using QuantConnect.Brokerages; using QuantConnect.Orders.Fees; -using QuantConnect.Orders.TimeInForces; using QuantConnect.Securities; using QuantConnect.Tests.Brokerages; @@ -29,8 +28,6 @@ public class WebullBrokerageModelTests { private readonly WebullBrokerageModel _brokerageModel = new WebullBrokerageModel(); - // ── CanSubmitOrder — valid combinations ─────────────────────────────────── - // Equity: all five order types supported [TestCase(SecurityType.Equity, OrderType.Market)] [TestCase(SecurityType.Equity, OrderType.Limit)] @@ -55,7 +52,7 @@ public class WebullBrokerageModelTests [TestCase(SecurityType.Crypto, OrderType.Market)] [TestCase(SecurityType.Crypto, OrderType.Limit)] [TestCase(SecurityType.Crypto, OrderType.StopLimit)] - public void CanSubmitOrder_ValidSecurityAndOrderType_ReturnsTrue(SecurityType securityType, OrderType orderType) + public void CanSubmitOrderValidSecurityAndOrderTypeReturnsTrue(SecurityType securityType, OrderType orderType) { // Arrange var security = GetSecurityForType(securityType); @@ -69,11 +66,9 @@ public void CanSubmitOrder_ValidSecurityAndOrderType_ReturnsTrue(SecurityType se Assert.That(message, Is.Null); } - // ── CanSubmitOrder — unsupported security types ─────────────────────────── - [TestCase(SecurityType.Forex)] [TestCase(SecurityType.Cfd)] - public void CanSubmitOrder_UnsupportedSecurityType_ReturnsFalse(SecurityType securityType) + public void CanSubmitOrderUnsupportedSecurityTypeReturnsFalse(SecurityType securityType) { // Arrange var security = TestsHelpers.GetSecurity(securityType: securityType, symbol: "EURUSD", market: Market.Oanda); @@ -87,17 +82,13 @@ public void CanSubmitOrder_UnsupportedSecurityType_ReturnsFalse(SecurityType sec Assert.That(message, Is.Not.Null); } - // ── CanSubmitOrder — order types unsupported for specific security types ── - // Equity does not support exchange-session orders or combo orders [TestCase(SecurityType.Equity, OrderType.MarketOnClose)] [TestCase(SecurityType.Equity, OrderType.MarketOnOpen)] [TestCase(SecurityType.Equity, OrderType.ComboMarket)] - // Option does not support Market or TrailingStop - [TestCase(SecurityType.Option, OrderType.Market)] + // Option does not support TrailingStop [TestCase(SecurityType.Option, OrderType.TrailingStop)] // IndexOption has the same restrictions as Option - [TestCase(SecurityType.IndexOption, OrderType.Market)] [TestCase(SecurityType.IndexOption, OrderType.TrailingStop)] // Crypto does not support StopMarket or TrailingStop [TestCase(SecurityType.Crypto, OrderType.StopMarket)] @@ -121,12 +112,11 @@ public void CanSubmitOrder_UnsupportedOrderTypeForSecurityType_ReturnsFalse( // https://developer.webull.com/apis/docs/trade-api/options#time-in-force // Sell → Day only | Buy → GoodTilCanceled only - [TestCase(SecurityType.Option, OrderDirection.Sell)] // Sell + Day - [TestCase(SecurityType.Option, OrderDirection.Buy)] // Buy + GTC + [TestCase(SecurityType.Option, OrderDirection.Sell)] // Sell + Day + [TestCase(SecurityType.Option, OrderDirection.Buy)] // Buy + GTC [TestCase(SecurityType.IndexOption, OrderDirection.Sell)] [TestCase(SecurityType.IndexOption, OrderDirection.Buy)] - public void CanSubmitOrder_OptionOrderWithValidTimeInForce_ReturnsTrue( - SecurityType securityType, OrderDirection direction) + public void CanSubmitOrderOptionOrderWithValidTimeInForceReturnsTrue(SecurityType securityType, OrderDirection direction) { // Arrange var security = GetSecurityForType(securityType); @@ -143,11 +133,11 @@ public void CanSubmitOrder_OptionOrderWithValidTimeInForce_ReturnsTrue( Assert.That(message, Is.Null); } - [TestCase(SecurityType.Option, OrderDirection.Sell)] // Sell + GTC → rejected - [TestCase(SecurityType.Option, OrderDirection.Buy)] // Buy + Day → rejected + [TestCase(SecurityType.Option, OrderDirection.Sell)] // Sell + GTC → rejected + [TestCase(SecurityType.Option, OrderDirection.Buy)] // Buy + Day → rejected [TestCase(SecurityType.IndexOption, OrderDirection.Sell)] [TestCase(SecurityType.IndexOption, OrderDirection.Buy)] - public void CanSubmitOrder_OptionOrderWithInvalidTimeInForce_ReturnsFalse( + public void CanSubmitOrderOptionOrderWithInvalidTimeInForceReturnsFalse( SecurityType securityType, OrderDirection direction) { // Arrange @@ -172,7 +162,7 @@ public void CanSubmitOrder_OptionOrderWithInvalidTimeInForce_ReturnsFalse( // https://developer.webull.com/apis/docs/trade-api — Applicable to U.S. stock market orders only. [Test] - public void CanSubmitOrder_OutsideRegularTradingHoursOnEquity_ReturnsTrue() + public void CanSubmitOrderOutsideRegularTradingHoursOnEquityReturnsTrue() { // Arrange var security = GetSecurityForType(SecurityType.Equity); @@ -191,7 +181,7 @@ public void CanSubmitOrder_OutsideRegularTradingHoursOnEquity_ReturnsTrue() [TestCase(SecurityType.IndexOption)] [TestCase(SecurityType.Future)] [TestCase(SecurityType.Crypto)] - public void CanSubmitOrder_OutsideRegularTradingHoursOnNonEquity_ReturnsFalse(SecurityType securityType) + public void CanSubmitOrderOutsideRegularTradingHoursOnNonEquityReturnsFalse(SecurityType securityType) { // Arrange var security = GetSecurityForType(securityType); @@ -211,7 +201,7 @@ public void CanSubmitOrder_OutsideRegularTradingHoursOnNonEquity_ReturnsFalse(Se [TestCase(SecurityType.Option)] [TestCase(SecurityType.Future)] [TestCase(SecurityType.Crypto)] - public void CanSubmitOrder_OutsideRegularTradingHoursFalseOnNonEquity_ReturnsTrue(SecurityType securityType) + public void CanSubmitOrderOutsideRegularTradingHoursFalseOnNonEquityReturnsTrue(SecurityType securityType) { // Arrange var security = GetSecurityForType(securityType); @@ -226,10 +216,8 @@ public void CanSubmitOrder_OutsideRegularTradingHoursFalseOnNonEquity_ReturnsTru Assert.That(message, Is.Null); } - // ── GetFeeModel ─────────────────────────────────────────────────────────── - [Test] - public void GetFeeModel_ReturnsWebullFeeModel() + public void GetFeeModelReturnsWebullFeeModel() { // Arrange var security = TestsHelpers.GetSecurity(securityType: SecurityType.Equity, symbol: "AAPL", market: Market.USA); @@ -238,8 +226,6 @@ public void GetFeeModel_ReturnsWebullFeeModel() Assert.That(_brokerageModel.GetFeeModel(security), Is.InstanceOf()); } - // ── Helpers ─────────────────────────────────────────────────────────────── - private static Security GetSecurityForType(SecurityType securityType) { switch (securityType) @@ -254,6 +240,9 @@ private static Security GetSecurityForType(SecurityType securityType) case SecurityType.Cfd: return TestsHelpers.GetSecurity(securityType: securityType, symbol: "EURUSD", market: Market.Oanda); + case SecurityType.IndexOption: + return TestsHelpers.GetSecurity(securityType: SecurityType.IndexOption, + symbol: "SPX", market: Market.CBOE); default: return TestsHelpers.GetSecurity(securityType: securityType, symbol: "AAPL", market: Market.USA); diff --git a/Tests/Common/Orders/Fees/WebullFeeModelTests.cs b/Tests/Common/Orders/Fees/WebullFeeModelTests.cs index 175bde04c951..37fc62c37c31 100644 --- a/Tests/Common/Orders/Fees/WebullFeeModelTests.cs +++ b/Tests/Common/Orders/Fees/WebullFeeModelTests.cs @@ -14,6 +14,7 @@ */ using System; +using System.Collections.Generic; using NUnit.Framework; using QuantConnect.Data; using QuantConnect.Data.Market; @@ -30,166 +31,77 @@ public class WebullFeeModelTests { private readonly WebullFeeModel _feeModel = new WebullFeeModel(); - // ── Equity / Option — zero commission ──────────────────────────────────── - - [Test] - public void GetOrderFee_Equity_ReturnsZero() - { - var security = SecurityTests.GetSecurity(); - security.SetMarketPrice(new Tick(DateTime.UtcNow, security.Symbol, 100m, 100m)); - var order = new MarketOrder(security.Symbol, 10m, DateTime.UtcNow); - - var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - - Assert.That(fee.Value.Amount, Is.EqualTo(0m)); - } - - [Test] - public void GetOrderFee_Option_ReturnsZero() - { - var security = CreateOptionSecurity("AAPL", 5m); - var order = new MarketOrder(security.Symbol, 3m, DateTime.UtcNow); - - var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - - Assert.That(fee.Value.Amount, Is.EqualTo(0m)); - } - - // ── IndexOption — SPX ───────────────────────────────────────────────────── - - /// - /// SPX, price < $1 → exchange $0.57 + Webull $0.50 = $1.07/contract. - /// 2 contracts → $2.14. - /// - [Test] - public void GetOrderFee_SpxPriceBelow1_ReturnsCorrectFee() - { - var security = CreateIndexOptionSecurity("SPX", price: 0.50m); - var order = new MarketOrder(security.Symbol, 2m, DateTime.UtcNow); - - var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - - Assert.That(fee.Value.Amount, Is.EqualTo(2.14m)); - Assert.That(fee.Value.Currency, Is.EqualTo(Currencies.USD)); - } - - /// - /// SPX, price ≥ $1 → exchange $0.66 + Webull $0.50 = $1.16/contract. - /// 3 contracts → $3.48. - /// - [Test] - public void GetOrderFee_SpxPriceAbove1_ReturnsCorrectFee() - { - var security = CreateIndexOptionSecurity("SPX", price: 1.50m); - var order = new MarketOrder(security.Symbol, 3m, DateTime.UtcNow); - - var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - - Assert.That(fee.Value.Amount, Is.EqualTo(3.48m)); - } - - // ── IndexOption — SPXW ──────────────────────────────────────────────────── - - /// - /// SPXW, price < $1 → exchange $0.50 + Webull $0.50 = $1.00/contract. - /// - [Test] - public void GetOrderFee_SpxwPriceBelow1_ReturnsCorrectFee() - { - var security = CreateIndexOptionSecurity("SPXW", price: 0.80m); - var order = new MarketOrder(security.Symbol, 1m, DateTime.UtcNow); - - var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - - Assert.That(fee.Value.Amount, Is.EqualTo(1.00m)); - } - - /// - /// SPXW, price ≥ $1 → exchange $0.59 + Webull $0.50 = $1.09/contract. - /// 4 contracts → $4.36. - /// - [Test] - public void GetOrderFee_SpxwPriceAbove1_ReturnsCorrectFee() - { - var security = CreateIndexOptionSecurity("SPXW", price: 2.00m); - var order = new MarketOrder(security.Symbol, 4m, DateTime.UtcNow); - - var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - - Assert.That(fee.Value.Amount, Is.EqualTo(4.36m)); - } - - // ── IndexOption — VIX ───────────────────────────────────────────────────── - - /// - /// VIX, price ≤ $0.10 → exchange $0.10 + Webull $0.50 = $0.60/contract. - /// 4 contracts → $2.40. - /// - [Test] - public void GetOrderFee_VixPriceTier1_ReturnsCorrectFee() + private static IEnumerable ZeroFeeSecurities() { - var security = CreateIndexOptionSecurity("VIX", price: 0.05m); - var order = new MarketOrder(security.Symbol, 4m, DateTime.UtcNow); - - var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - - Assert.That(fee.Value.Amount, Is.EqualTo(2.40m)); + var equity = SecurityTests.GetSecurity(); + equity.SetMarketPrice(new Tick(DateTime.UtcNow, equity.Symbol, 100m, 100m)); + yield return equity; + yield return CreateSecurity(SecurityType.Option, 5m, "AAPL"); } /// - /// VIX, price $0.11–$0.99 → exchange $0.25 + Webull $0.50 = $0.75/contract. - /// 2 contracts → $1.50. + /// Equity and non-index options are commission-free on Webull. /// - [Test] - public void GetOrderFee_VixPriceTier2_ReturnsCorrectFee() + [TestCaseSource(nameof(ZeroFeeSecurities))] + public void GetOrderFeeReturnsZeroForFreeAssets(Security security) { - var security = CreateIndexOptionSecurity("VIX", price: 0.50m); - var order = new MarketOrder(security.Symbol, 2m, DateTime.UtcNow); + var order = new MarketOrder(security.Symbol, 10m, DateTime.UtcNow); var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - Assert.That(fee.Value.Amount, Is.EqualTo(1.50m)); + Assert.That(fee.Value.Amount, Is.EqualTo(0m)); } /// - /// VIX, price $1.00–$1.99 → exchange $0.40 + Webull $0.50 = $0.90/contract. + /// SPX/SPXW exchange fee tiers (per contract) + Webull $0.50/contract: + /// SPX price < $1 -> $0.57 + $0.50 = $1.07 + /// SPX price >= $1 -> $0.66 + $0.50 = $1.16 + /// SPXW price < $1 -> $0.50 + $0.50 = $1.00 + /// SPXW price >= $1 -> $0.59 + $0.50 = $1.09 /// - [Test] - public void GetOrderFee_VixPriceTier3_ReturnsCorrectFee() + [TestCase("SPX", 0.50, 2, 2.14, Description = "SPX price < $1 -> $1.07/contract")] + [TestCase("SPX", 1.50, 3, 3.48, Description = "SPX price >= $1 -> $1.16/contract")] + [TestCase("SPXW", 0.80, 1, 1.00, Description = "SPXW price < $1 -> $1.00/contract")] + [TestCase("SPXW", 2.00, 4, 4.36, Description = "SPXW price >= $1 -> $1.09/contract")] + public void GetOrderFeeSpxPriceTierReturnsCorrectFee(string ticker, decimal price, decimal quantity, decimal expectedFee) { - var security = CreateIndexOptionSecurity("VIX", price: 1.50m); - var order = new MarketOrder(security.Symbol, 1m, DateTime.UtcNow); + var security = CreateSecurity(SecurityType.IndexOption, price, ticker); + var order = new MarketOrder(security.Symbol, quantity, DateTime.UtcNow); var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - Assert.That(fee.Value.Amount, Is.EqualTo(0.90m)); + Assert.That(fee.Value.Amount, Is.EqualTo(expectedFee)); } /// - /// VIX, price ≥ $2.00 → exchange $0.45 + Webull $0.50 = $0.95/contract. - /// 5 contracts → $4.75. + /// VIX exchange fee tiers (per contract) + Webull $0.50/contract: + /// Tier 1: price ≤ $0.10 -> $0.10 + $0.50 = $0.60 + /// Tier 2: price $0.11–$0.99 -> $0.25 + $0.50 = $0.75 + /// Tier 3: price $1.00–$1.99 -> $0.40 + $0.50 = $0.90 + /// Tier 4: price >= $2.00 -> $0.45 + $0.50 = $0.95 /// - [Test] - public void GetOrderFee_VixPriceTier4_ReturnsCorrectFee() + [TestCase(0.05, 4, 2.40, Description = "Tier 1: price ≤ $0.10 -> $0.60/contract")] + [TestCase(0.50, 2, 1.50, Description = "Tier 2: price $0.11–$0.99 -> $0.75/contract")] + [TestCase(1.50, 1, 0.90, Description = "Tier 3: price $1.00–$1.99 -> $0.90/contract")] + [TestCase(3.00, 5, 4.75, Description = "Tier 4: price >= $2.00 -> $0.95/contract")] + public void GetOrderFeeVixPriceTierReturnsCorrectFee(decimal price, decimal quantity, decimal expectedFee) { - var security = CreateIndexOptionSecurity("VIX", price: 3.00m); - var order = new MarketOrder(security.Symbol, 5m, DateTime.UtcNow); + var security = CreateSecurity(SecurityType.IndexOption, price, "VIX"); + var order = new MarketOrder(security.Symbol, quantity, DateTime.UtcNow); var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - Assert.That(fee.Value.Amount, Is.EqualTo(4.75m)); + Assert.That(fee.Value.Amount, Is.EqualTo(expectedFee)); } - // ── IndexOption — VIXW ──────────────────────────────────────────────────── - /// /// VIXW uses identical tier schedule to VIX; verify tier 2. /// [Test] - public void GetOrderFee_VixwPriceTier2_MatchesVixFee() + public void GetOrderFeeVixwPriceTier2MatchesVixFee() { - var vix = CreateIndexOptionSecurity("VIX", price: 0.50m); - var vixw = CreateIndexOptionSecurity("VIXW", price: 0.50m); + var vix = CreateSecurity(SecurityType.IndexOption, 0.50m, "VIX"); + var vixw = CreateSecurity(SecurityType.IndexOption, 0.50m, "VIXW"); var order = new MarketOrder(vix.Symbol, 2m, DateTime.UtcNow); var vixFee = _feeModel.GetOrderFee(new OrderFeeParameters(vix, order)); @@ -198,114 +110,39 @@ public void GetOrderFee_VixwPriceTier2_MatchesVixFee() Assert.That(vixFee.Value.Amount, Is.EqualTo(vixwFee.Value.Amount)); } - // ── IndexOption — XSP ───────────────────────────────────────────────────── - /// - /// XSP, qty < 10 → exchange $0.00 + Webull $0.50 = $0.50/contract. - /// 5 contracts → $2.50. + /// Index option fee schedule (per contract) + Webull $0.50/contract: + /// XSP qty < 10 -> $0.00 + $0.50 = $0.50 + /// XSP qty >= 10 -> $0.07 + $0.50 = $0.57 + /// DJX flat -> $0.18 + $0.50 = $0.68 + /// NDX price < $25 -> $0.50 + $0.50 = $1.00 (NDXP shares the same schedule) + /// NDX price >= $25 -> $0.75 + $0.50 = $1.25 (NDXP shares the same schedule) /// - [Test] - public void GetOrderFee_XspSmallQuantity_ReturnsCorrectFee() + [TestCase("XSP", 1.00, 5, 2.50, Description = "XSP qty < 10 -> $0.50/contract")] + [TestCase("XSP", 1.00, 10, 5.70, Description = "XSP qty >= 10 -> $0.57/contract")] + [TestCase("DJX", 2.00, 2, 1.36, Description = "DJX flat -> $0.68/contract")] + [TestCase("NDX", 10.00, 3, 3.00, Description = "NDX price < $25 -> $1.00/contract")] + [TestCase("NDX", 50.00, 2, 2.50, Description = "NDX price >= $25 -> $1.25/contract")] + [TestCase("NDXP", 10.00, 3, 3.00, Description = "NDXP price < $25 matches NDX schedule")] + [TestCase("NDXP", 50.00, 2, 2.50, Description = "NDXP price >= $25 matches NDX schedule")] + public void GetOrderFeeIndexOptionReturnsCorrectFee(string ticker, decimal price, decimal quantity, decimal expectedFee) { - var security = CreateIndexOptionSecurity("XSP", price: 1.00m); - var order = new MarketOrder(security.Symbol, 5m, DateTime.UtcNow); + var security = CreateSecurity(SecurityType.IndexOption, price, ticker); + var order = new MarketOrder(security.Symbol, quantity, DateTime.UtcNow); var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - Assert.That(fee.Value.Amount, Is.EqualTo(2.50m)); + Assert.That(fee.Value.Amount, Is.EqualTo(expectedFee)); } - /// - /// XSP, qty ≥ 10 → exchange $0.07 + Webull $0.50 = $0.57/contract. - /// 10 contracts → $5.70. - /// - [Test] - public void GetOrderFee_XspLargeQuantity_ReturnsCorrectFee() - { - var security = CreateIndexOptionSecurity("XSP", price: 1.00m); - var order = new MarketOrder(security.Symbol, 10m, DateTime.UtcNow); - - var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - - Assert.That(fee.Value.Amount, Is.EqualTo(5.70m)); - } - - // ── IndexOption — DJX ───────────────────────────────────────────────────── - - /// - /// DJX flat → exchange $0.18 + Webull $0.50 = $0.68/contract. - /// 2 contracts → $1.36. - /// - [Test] - public void GetOrderFee_Djx_ReturnsCorrectFee() - { - var security = CreateIndexOptionSecurity("DJX", price: 2.00m); - var order = new MarketOrder(security.Symbol, 2m, DateTime.UtcNow); - - var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - - Assert.That(fee.Value.Amount, Is.EqualTo(1.36m)); - } - - // ── IndexOption — NDX / NDXP ────────────────────────────────────────────── - - /// - /// NDX, single-leg, premium < $25 → exchange $0.50 + Webull $0.50 = $1.00/contract. - /// 3 contracts → $3.00. - /// - [Test] - public void GetOrderFee_NdxSingleLegPriceBelow25_ReturnsCorrectFee() - { - var security = CreateIndexOptionSecurity("NDX", price: 10.00m); - var order = new MarketOrder(security.Symbol, 3m, DateTime.UtcNow); - - var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - - Assert.That(fee.Value.Amount, Is.EqualTo(3.00m)); - } - - /// - /// NDX, single-leg, premium ≥ $25 → exchange $0.75 + Webull $0.50 = $1.25/contract. - /// 2 contracts → $2.50. - /// - [Test] - public void GetOrderFee_NdxSingleLegPriceAbove25_ReturnsCorrectFee() - { - var security = CreateIndexOptionSecurity("NDX", price: 50.00m); - var order = new MarketOrder(security.Symbol, 2m, DateTime.UtcNow); - - var fee = _feeModel.GetOrderFee(new OrderFeeParameters(security, order)); - - Assert.That(fee.Value.Amount, Is.EqualTo(2.50m)); - } - - /// - /// NDXP uses the same fee schedule as NDX. - /// - [Test] - public void GetOrderFee_NdxpSingleLegPriceBelow25_MatchesNdxFee() - { - var ndx = CreateIndexOptionSecurity("NDX", price: 10.00m); - var ndxp = CreateIndexOptionSecurity("NDXP", price: 10.00m); - var ndxOrder = new MarketOrder(ndx.Symbol, 1m, DateTime.UtcNow); - var ndxpOrder = new MarketOrder(ndxp.Symbol, 1m, DateTime.UtcNow); - - var ndxFee = _feeModel.GetOrderFee(new OrderFeeParameters(ndx, ndxOrder)); - var ndxpFee = _feeModel.GetOrderFee(new OrderFeeParameters(ndxp, ndxpOrder)); - - Assert.That(ndxFee.Value.Amount, Is.EqualTo(ndxpFee.Value.Amount)); - } - - // ── Crypto ──────────────────────────────────────────────────────────────── - /// /// Crypto fee = 0.6% of notional (quantity × price). - /// 2 BTC × $50,000 = $100,000 notional → fee = $600. + /// 2 BTC × $50,000 = $100,000 notional -> fee = $600. /// [Test] - public void GetOrderFee_Crypto_ReturnsPointSixPercentOfNotional() + public void GetOrderFeeCryptoReturnsPointSixPercentOfNotional() { - var btcusd = CreateCryptoSecurity(price: 50_000m); + var btcusd = CreateSecurity(SecurityType.Crypto, 50_000m); var order = new MarketOrder(btcusd.Symbol, 2m, DateTime.UtcNow); var fee = _feeModel.GetOrderFee(new OrderFeeParameters(btcusd, order)); @@ -315,44 +152,37 @@ public void GetOrderFee_Crypto_ReturnsPointSixPercentOfNotional() Assert.That(fee.Value.Currency, Is.EqualTo(Currencies.USD)); } - // ── Helpers ─────────────────────────────────────────────────────────────── - /// - /// Creates an index option security with the given underlying ticker and option price. - /// Uses - /// with an Index underlying to produce a symbol. + /// Creates a test security of the requested priced at . + /// Supported types: , , . + /// For option types identifies the underlying symbol; + /// it is ignored for (BTC/USD is always used). /// - private static Security CreateIndexOptionSecurity(string underlyingTicker, decimal price) + private static Security CreateSecurity(SecurityType securityType, decimal price, string ticker = "AAPL") { - var underlying = Symbol.Create(underlyingTicker, SecurityType.Index, Market.USA); - var symbol = Symbol.CreateOption( - underlying, Market.USA, OptionStyle.European, OptionRight.Call, 1000m, new DateTime(2026, 6, 20)); + if (securityType == SecurityType.Crypto) + { + var btcusd = new Crypto( + SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), + new Cash(Currencies.USD, 0, 1m), + new Cash("BTC", 0, price), + new SubscriptionDataConfig(typeof(TradeBar), Symbols.BTCUSD, Resolution.Minute, + TimeZones.Utc, TimeZones.Utc, true, false, false), + new SymbolProperties("BTCUSD", Currencies.USD, 1, 0.01m, 0.00000001m, string.Empty), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null); + btcusd.SetMarketPrice(new Tick(DateTime.UtcNow, btcusd.Symbol, price, price)); + return btcusd; + } - var config = new SubscriptionDataConfig( - typeof(TradeBar), symbol, Resolution.Minute, - TimeZones.Utc, TimeZones.Utc, false, true, false); - - var security = new Security( - SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), - config, - new Cash(Currencies.USD, 0, 1m), - SymbolProperties.GetDefault(Currencies.USD), - ErrorCurrencyConverter.Instance, - RegisteredSecurityDataTypesProvider.Null, - new SecurityCache()); - - security.SetMarketPrice(new Tick(DateTime.UtcNow, symbol, price, price)); - return security; - } - - /// - /// Creates a standard equity option security (SecurityType.Option) for zero-fee assertions. - /// - private static Security CreateOptionSecurity(string underlyingTicker, decimal price) - { - var underlying = Symbol.Create(underlyingTicker, SecurityType.Equity, Market.USA); + var isIndex = securityType == SecurityType.IndexOption; + var underlying = Symbol.Create(ticker, isIndex ? SecurityType.Index : SecurityType.Equity, Market.USA); var symbol = Symbol.CreateOption( - underlying, Market.USA, OptionStyle.American, OptionRight.Call, 150m, new DateTime(2026, 6, 20)); + underlying, Market.USA, + isIndex ? OptionStyle.European : OptionStyle.American, + OptionRight.Call, + isIndex ? 1000m : 150m, + new DateTime(2026, 6, 20)); var config = new SubscriptionDataConfig( typeof(TradeBar), symbol, Resolution.Minute, @@ -370,24 +200,5 @@ private static Security CreateOptionSecurity(string underlyingTicker, decimal pr security.SetMarketPrice(new Tick(DateTime.UtcNow, symbol, price, price)); return security; } - - /// - /// Creates a crypto security with the given USD price set via . - /// - private static Crypto CreateCryptoSecurity(decimal price) - { - var btcusd = new Crypto( - SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), - new Cash(Currencies.USD, 0, 1m), - new Cash("BTC", 0, price), - new SubscriptionDataConfig(typeof(TradeBar), Symbols.BTCUSD, Resolution.Minute, - TimeZones.Utc, TimeZones.Utc, true, false, false), - new SymbolProperties("BTCUSD", Currencies.USD, 1, 0.01m, 0.00000001m, string.Empty), - ErrorCurrencyConverter.Instance, - RegisteredSecurityDataTypesProvider.Null); - - btcusd.SetMarketPrice(new Tick(DateTime.UtcNow, btcusd.Symbol, price, price)); - return btcusd; - } } }