项目文件夹

文件
2026-07-13 13:02:50 +08:00

171 行
8.2 KiB
C#

/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using 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;
}
}
}