/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using NUnit.Framework; using QuantConnect.Data; using QuantConnect.Data.Market; using QuantConnect.Orders; using QuantConnect.Orders.Fees; using QuantConnect.Securities; using QuantConnect.Tests.Common.Securities; namespace QuantConnect.Tests.Common.Orders.Fees { [TestFixture] public class WebullFeeModelTests { private readonly WebullFeeModel _feeModel = new WebullFeeModel(); private static IEnumerable ZeroFeeSecurities() { var equity = SecurityTests.GetSecurity(); equity.SetMarketPrice(new Tick(DateTime.UtcNow, equity.Symbol, 100m, 100m)); yield return equity; yield return CreateSecurity(SecurityType.Option, 5m, "AAPL"); } /// /// Equity and non-index options are commission-free on Webull. /// [TestCaseSource(nameof(ZeroFeeSecurities))] public void GetOrderFeeReturnsZeroForFreeAssets(Security security) { 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)); } /// /// 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 /// [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 = 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(expectedFee)); } /// /// 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 /// [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 = 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(expectedFee)); } /// /// VIXW uses identical tier schedule to VIX; verify tier 2. /// [Test] public void GetOrderFeeVixwPriceTier2MatchesVixFee() { 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)); var vixwFee = _feeModel.GetOrderFee(new OrderFeeParameters(vixw, new MarketOrder(vixw.Symbol, 2m, DateTime.UtcNow))); Assert.That(vixFee.Value.Amount, Is.EqualTo(vixwFee.Value.Amount)); } /// /// 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) /// [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 = 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(expectedFee)); } /// /// Creates a test security of the requested priced at . /// Supported types: , . /// identifies the underlying symbol. /// private static Security CreateSecurity(SecurityType securityType, decimal price, string ticker = "AAPL") { var isIndex = securityType == SecurityType.IndexOption; var underlying = Symbol.Create(ticker, isIndex ? SecurityType.Index : SecurityType.Equity, Market.USA); var symbol = Symbol.CreateOption( 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, 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; } } }