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

171 lines
8.2 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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<Security> 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");
}
/// <summary>
/// Equity and non-index options are commission-free on Webull.
/// </summary>
[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));
}
/// <summary>
/// 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
/// </summary>
[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));
}
/// <summary>
/// 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
/// </summary>
[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));
}
/// <summary>
/// VIXW uses identical tier schedule to VIX; verify tier 2.
/// </summary>
[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));
}
/// <summary>
/// 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)
/// </summary>
[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));
}
/// <summary>
/// Creates a test security of the requested <paramref name="securityType"/> priced at <paramref name="price"/>.
/// Supported types: <see cref="SecurityType.IndexOption"/>, <see cref="SecurityType.Option"/>.
/// <paramref name="ticker"/> identifies the underlying symbol.
/// </summary>
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;
}
}
}