Skip to content

Commit 24afc50

Browse files
authored
feature: add webull brokerage core integration (#9362)
* 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 * 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 * fix: add market order type to option and index option supported orders * chore: add webull crypto symbols to symbol-properties-database * 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 * remove: restriction GTC for Option and Buy side only * refactor: limit webull to equity and options - remove crypto and future support from order types and tests - remove crypto fee logic and crypto symbol-properties rows - reject market orders with outsideRth on equity - log info when market order uses non-day tif
1 parent 96a670a commit 24afc50

10 files changed

Lines changed: 968 additions & 2 deletions

File tree

Common/Brokerages/BrokerageName.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,11 @@ public enum BrokerageName
197197
/// <summary>
198198
/// Transaction and submit/execution rules will use dYdX models
199199
/// </summary>
200-
DYDX
200+
DYDX,
201+
202+
/// <summary>
203+
/// Transaction and submit/execution rules will use Webull models
204+
/// </summary>
205+
Webull
201206
}
202207
}

Common/Brokerages/IBrokerageModel.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,9 @@ public static IBrokerageModel Create(IOrderProvider orderProvider, BrokerageName
291291
case BrokerageName.DYDX:
292292
return new dYdXBrokerageModel(accountType);
293293

294+
case BrokerageName.Webull:
295+
return new WebullBrokerageModel(accountType);
296+
294297
default:
295298
throw new ArgumentOutOfRangeException(nameof(brokerage), brokerage, null);
296299
}
@@ -394,6 +397,9 @@ public static BrokerageName GetBrokerageName(IBrokerageModel brokerageModel)
394397
case TastytradeBrokerageModel:
395398
return BrokerageName.Tastytrade;
396399

400+
case WebullBrokerageModel:
401+
return BrokerageName.Webull;
402+
397403
case DefaultBrokerageModel _:
398404
return BrokerageName.Default;
399405

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Collections.Generic;
17+
using QuantConnect.Logging;
18+
using QuantConnect.Orders;
19+
using QuantConnect.Orders.Fees;
20+
using QuantConnect.Orders.TimeInForces;
21+
using QuantConnect.Securities;
22+
23+
namespace QuantConnect.Brokerages
24+
{
25+
/// <summary>
26+
/// Represents a brokerage model specific to Webull.
27+
/// </summary>
28+
public class WebullBrokerageModel : DefaultBrokerageModel
29+
{
30+
/// <summary>
31+
/// Flag to track if we've already logged a message about market orders only supporting Day TIF. We only want to log this once to avoid spamming the logs.
32+
/// </summary>
33+
private bool _marketOrderDayTimeInForceLogged;
34+
35+
/// <summary>
36+
/// Maps each supported security type to the order types Webull allows for it.
37+
/// </summary>
38+
private static readonly Dictionary<SecurityType, HashSet<OrderType>> _supportedOrderTypesBySecurityType =
39+
new Dictionary<SecurityType, HashSet<OrderType>>
40+
{
41+
{
42+
SecurityType.Equity, new HashSet<OrderType>
43+
{
44+
OrderType.Market,
45+
OrderType.Limit,
46+
OrderType.StopMarket,
47+
OrderType.StopLimit,
48+
OrderType.TrailingStop
49+
}
50+
},
51+
{
52+
SecurityType.Option, new HashSet<OrderType>
53+
{
54+
OrderType.Market,
55+
OrderType.Limit,
56+
OrderType.StopMarket,
57+
OrderType.StopLimit
58+
}
59+
},
60+
{
61+
SecurityType.IndexOption, new HashSet<OrderType>
62+
{
63+
OrderType.Market,
64+
OrderType.Limit,
65+
OrderType.StopMarket,
66+
OrderType.StopLimit
67+
}
68+
}
69+
};
70+
71+
/// <summary>
72+
/// Constructor for Webull brokerage model.
73+
/// </summary>
74+
/// <param name="accountType">Cash or Margin</param>
75+
public WebullBrokerageModel(AccountType accountType = AccountType.Margin)
76+
: base(accountType)
77+
{
78+
}
79+
80+
/// <summary>
81+
/// Provides the Webull fee model.
82+
/// </summary>
83+
/// <param name="security">Security</param>
84+
/// <returns>Webull fee model</returns>
85+
public override IFeeModel GetFeeModel(Security security)
86+
{
87+
return new WebullFeeModel();
88+
}
89+
90+
/// <summary>
91+
/// Returns true if the brokerage could accept this order. This takes into account
92+
/// order type, security type, and order size limits.
93+
/// </summary>
94+
/// <remarks>
95+
/// For example, a brokerage may have no connectivity at certain times, or an order rate/size limit.
96+
/// </remarks>
97+
/// <param name="security">The security of the order</param>
98+
/// <param name="order">The order to be processed</param>
99+
/// <param name="message">If this function returns false, a brokerage message detailing why the order may not be submitted</param>
100+
/// <returns>True if the brokerage could process the order, false otherwise</returns>
101+
public override bool CanSubmitOrder(Security security, Order order, out BrokerageMessageEvent message)
102+
{
103+
message = default;
104+
105+
if (!_supportedOrderTypesBySecurityType.TryGetValue(security.Type, out var supportedOrderTypes))
106+
{
107+
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
108+
Messages.DefaultBrokerageModel.UnsupportedSecurityType(this, security));
109+
return false;
110+
}
111+
112+
if (!supportedOrderTypes.Contains(order.Type))
113+
{
114+
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
115+
Messages.DefaultBrokerageModel.UnsupportedOrderType(this, order, supportedOrderTypes));
116+
return false;
117+
}
118+
119+
if (!_marketOrderDayTimeInForceLogged && order.Type == OrderType.Market && order.TimeInForce is not DayTimeInForce)
120+
{
121+
_marketOrderDayTimeInForceLogged = true;
122+
Log.Trace("WebullBrokerageModel.CanSubmitOrder: Market orders support only Day TIF, which is set automatically by the brokerage.");
123+
}
124+
125+
// Options and IndexOptions have per-direction TimeInForce restrictions.
126+
// https://developer.webull.com/apis/docs/trade-api/options#time-in-force
127+
// - Sell orders: Day only
128+
if (security.Type == SecurityType.Option || security.Type == SecurityType.IndexOption)
129+
{
130+
if (order.Direction == OrderDirection.Sell && order.TimeInForce is not DayTimeInForce)
131+
{
132+
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
133+
Messages.WebullBrokerageModel.InvalidTimeInForceForOptionSellOrder(order));
134+
return false;
135+
}
136+
}
137+
138+
if (order.Properties is WebullOrderProperties { OutsideRegularTradingHours: true })
139+
{
140+
if (security.Type is not SecurityType.Equity)
141+
{
142+
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
143+
Messages.WebullBrokerageModel.OutsideRegularTradingHoursNotSupportedForSecurityType(security));
144+
return false;
145+
}
146+
147+
if (order.Type == OrderType.Market)
148+
{
149+
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported",
150+
Messages.WebullBrokerageModel.MarketOrdersNotSupportedOutsideRegularTradingHours());
151+
return false;
152+
}
153+
}
154+
155+
return base.CanSubmitOrder(security, order, out message);
156+
}
157+
}
158+
}

Common/Messages/Messages.Brokerages.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,39 @@ public static string UnsupportedOrderType(Orders.Order order)
595595
}
596596
}
597597

598+
/// <summary>
599+
/// Provides user-facing messages for the <see cref="Brokerages.WebullBrokerageModel"/> class and its consumers or related classes
600+
/// </summary>
601+
public static class WebullBrokerageModel
602+
{
603+
/// <summary>
604+
/// Returns a message explaining that Options and IndexOptions sell orders only support Day time in force.
605+
/// </summary>
606+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
607+
public static string InvalidTimeInForceForOptionSellOrder(Orders.Order order)
608+
{
609+
return Invariant($"{order.Symbol.SecurityType} sell orders only support {nameof(DayTimeInForce)} time in force, but {order.TimeInForce.GetType().Name} was specified.");
610+
}
611+
612+
/// <summary>
613+
/// Returns a message explaining that OutsideRegularTradingHours is only supported for Equity orders.
614+
/// </summary>
615+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
616+
public static string OutsideRegularTradingHoursNotSupportedForSecurityType(Securities.Security security)
617+
{
618+
return Invariant($"{nameof(WebullOrderProperties.OutsideRegularTradingHours)} is only supported for {nameof(SecurityType.Equity)} orders, but {security.Type} was specified.");
619+
}
620+
621+
/// <summary>
622+
/// Returns a message explaining that Market orders are not supported outside regular trading hours.
623+
/// </summary>
624+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
625+
public static string MarketOrdersNotSupportedOutsideRegularTradingHours()
626+
{
627+
return Invariant($"Market orders are not supported outside regular trading hours.");
628+
}
629+
}
630+
598631
/// <summary>
599632
/// Provides user-facing messages for the <see cref="Brokerages.RBIBrokerageModel"/> class and its consumers or related classes
600633
/// </summary>

0 commit comments

Comments
 (0)