webull-openapi-mcp 1.2.2__tar.gz → 1.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/PKG-INFO +1 -1
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/pyproject.toml +1 -1
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/__init__.py +1 -1
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/constants.py +3 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/formatters.py +100 -55
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/option_order.py +41 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/stock_order.py +35 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/.env.example +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/.github/workflows/pypi-ci.yml +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/.gitignore +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/DISCLAIMER.md +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/LICENSE +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/README.md +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/mcpb/.mcpbignore +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/mcpb/build.sh +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/mcpb/build_manifest.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/mcpb/src/server.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/server.json +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/__init__.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_account_tools.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_audit.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_cli.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_config.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_errors.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_formatters.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_fundamental_screener_registration.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_fundamental_tools.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_guards.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_instrument_tools.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_market_data_tools.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_property_audit.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_property_config.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_property_formatters.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_property_guards.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_property_sdk.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_property_server.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_screener_tools.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_sdk_client.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/tests/test_stock_order_tools.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/__main__.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/audit.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/cli.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/config.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/errors.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/guards.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/region_config.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/sdk_client.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/server.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/__init__.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/__init__.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/crypto.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/event.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/financial.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/fundamental.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/futures.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/option.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/screener.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/stock.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/watchlist.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/__init__.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/account.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/assets.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/crypto_order.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/event_order.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/futures_order.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/instrument.py +0 -0
- {webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/order.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: webull-openapi-mcp
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: MCP Server for Webull OpenAPI - enables AI assistants to securely access Webull trading and market data
|
|
5
5
|
Project-URL: Homepage, https://github.com/webull-inc/webull-openapi-mcp
|
|
6
6
|
Project-URL: Repository, https://github.com/webull-inc/webull-openapi-mcp
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "webull-openapi-mcp"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.3"
|
|
8
8
|
description = "MCP Server for Webull OpenAPI - enables AI assistants to securely access Webull trading and market data"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -35,6 +35,9 @@ VALID_POSITION_INTENTS: frozenset[str] = frozenset({
|
|
|
35
35
|
"SELL_TO_CLOSE",
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
+
# Option leg-in / leg-out values (US only)
|
|
39
|
+
VALID_LEG_IN_OUT: frozenset[str] = frozenset({"LEG_IN", "LEG_OUT"})
|
|
40
|
+
|
|
38
41
|
# Trading session types
|
|
39
42
|
VALID_TRADING_SESSIONS: frozenset[str] = frozenset({"ALL", "CORE", "NIGHT"})
|
|
40
43
|
|
|
@@ -215,34 +215,86 @@ def format_account_balance(data: dict | None) -> str:
|
|
|
215
215
|
|
|
216
216
|
def format_positions(data: list[dict] | None) -> str:
|
|
217
217
|
"""Format account positions response.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
instrument_type, last_price, cost_price,
|
|
218
|
+
|
|
219
|
+
Top-level core (all regions): position_id, currency, quantity, symbol,
|
|
220
|
+
option_strategy, instrument_type, last_price, cost_price,
|
|
221
|
+
unrealized_profit_loss, legs: [...].
|
|
222
|
+
Top-level region-specific:
|
|
223
|
+
- US: event_outcome (event orders only)
|
|
224
|
+
- JP: available_quantity, market_value, symbol_name, exchange_code,
|
|
225
|
+
instrument_id, account_tax_type, base_currency, fx_rate,
|
|
226
|
+
base_currency_market_value
|
|
227
|
+
|
|
228
|
+
Leg fields differ by region:
|
|
229
|
+
- US: side, quantity, market, instrument_type, symbol, strike_price,
|
|
230
|
+
option_expire_date, option_type (no leg_id)
|
|
231
|
+
- HK: symbol, quantity, option_type, option_expire_date,
|
|
232
|
+
option_exercise_price, option_contract_multiplier,
|
|
233
|
+
option_contract_deliverable, expiration_type (no leg_id)
|
|
234
|
+
- JP: same as HK plus leg_id
|
|
235
|
+
|
|
236
|
+
The formatter reads whichever fields are present so it works across regions.
|
|
221
237
|
"""
|
|
222
238
|
if not data:
|
|
223
239
|
return _NO_DATA
|
|
224
240
|
lines: list[str] = ["=== Positions ==="]
|
|
225
241
|
for pos in data:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
242
|
+
# Top-level fields — show only present fields (varies by region).
|
|
243
|
+
head_fields: list[tuple[str, str]] = [
|
|
244
|
+
("Symbol", "symbol"),
|
|
245
|
+
("Position ID", "position_id"),
|
|
246
|
+
("Qty", "quantity"),
|
|
247
|
+
("Available Qty", "available_quantity"),
|
|
248
|
+
("Type", "instrument_type"),
|
|
249
|
+
("Strategy", "option_strategy"),
|
|
250
|
+
("Name", "symbol_name"),
|
|
251
|
+
("Exchange", "exchange_code"),
|
|
252
|
+
("Instrument ID", "instrument_id"),
|
|
253
|
+
("Cost", "cost_price"),
|
|
254
|
+
("Last", "last_price"),
|
|
255
|
+
("Market Value", "market_value"),
|
|
256
|
+
("Unrealized P&L", "unrealized_profit_loss"),
|
|
257
|
+
("Currency", "currency"),
|
|
258
|
+
("Event Outcome", "event_outcome"),
|
|
259
|
+
("Tax Type", "account_tax_type"),
|
|
260
|
+
("Base Currency", "base_currency"),
|
|
261
|
+
("FX Rate", "fx_rate"),
|
|
262
|
+
("Base Market Value", "base_currency_market_value"),
|
|
263
|
+
]
|
|
264
|
+
head_parts = [
|
|
265
|
+
f"{label}: {pos[key]}"
|
|
266
|
+
for label, key in head_fields
|
|
267
|
+
if pos.get(key) is not None
|
|
268
|
+
]
|
|
269
|
+
lines.append(" " + " ".join(head_parts))
|
|
270
|
+
|
|
271
|
+
# Option legs — field set varies by region; show only present fields.
|
|
236
272
|
legs = pos.get("legs", [])
|
|
237
273
|
for leg in legs:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
f"Leg: {
|
|
241
|
-
|
|
242
|
-
f"
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
274
|
+
parts: list[str] = [f"Leg: {_get(leg, 'symbol')}"]
|
|
275
|
+
if leg.get("leg_id") is not None:
|
|
276
|
+
parts.append(f"Leg ID: {leg['leg_id']}")
|
|
277
|
+
if leg.get("side") is not None:
|
|
278
|
+
parts.append(f"Side: {leg['side']}")
|
|
279
|
+
parts.append(f"Qty: {_get(leg, 'quantity')}")
|
|
280
|
+
if leg.get("instrument_type") is not None:
|
|
281
|
+
parts.append(f"Type: {leg['instrument_type']}")
|
|
282
|
+
if leg.get("option_type") is not None:
|
|
283
|
+
parts.append(f"Option: {leg['option_type']}")
|
|
284
|
+
# Strike: US uses strike_price, HK/JP use option_exercise_price
|
|
285
|
+
strike = leg.get("strike_price") or leg.get("option_exercise_price")
|
|
286
|
+
if strike is not None:
|
|
287
|
+
parts.append(f"Strike: {strike}")
|
|
288
|
+
if leg.get("option_expire_date") is not None:
|
|
289
|
+
parts.append(f"Exp: {leg['option_expire_date']}")
|
|
290
|
+
# HK/JP-only option contract fields
|
|
291
|
+
if leg.get("option_contract_multiplier") is not None:
|
|
292
|
+
parts.append(f"Multiplier: {leg['option_contract_multiplier']}")
|
|
293
|
+
if leg.get("option_contract_deliverable") is not None:
|
|
294
|
+
parts.append(f"Deliverable: {leg['option_contract_deliverable']}")
|
|
295
|
+
if leg.get("expiration_type") is not None:
|
|
296
|
+
parts.append(f"ExpType: {leg['expiration_type']}")
|
|
297
|
+
lines.append(f"{'':>10s} " + " ".join(parts))
|
|
246
298
|
return "\n".join(lines)
|
|
247
299
|
|
|
248
300
|
|
|
@@ -262,45 +314,38 @@ def format_position_details(data: list[dict] | dict | None) -> str:
|
|
|
262
314
|
if not isinstance(data, list) or not data:
|
|
263
315
|
return _NO_DATA
|
|
264
316
|
|
|
317
|
+
# Field labels in display order; only present fields are shown.
|
|
318
|
+
fields: list[tuple[str, str]] = [
|
|
319
|
+
("Position ID", "id"),
|
|
320
|
+
("Symbol", "symbol"),
|
|
321
|
+
("Name", "symbol_name"),
|
|
322
|
+
("Exchange", "exchange_code"),
|
|
323
|
+
("Instrument ID", "instrument_id"),
|
|
324
|
+
("Contract ID", "contract_id"),
|
|
325
|
+
("Qty", "quantity"),
|
|
326
|
+
("Hold Type", "hold_type"),
|
|
327
|
+
("Margin Type", "margin_type"),
|
|
328
|
+
("Average Price", "average_price"),
|
|
329
|
+
("Market Value", "market_value"),
|
|
330
|
+
("Unrealized P&L", "unrealized_pl"),
|
|
331
|
+
("Currency", "currency"),
|
|
332
|
+
("Account Tax Type", "account_tax_type"),
|
|
333
|
+
("Base Currency", "base_currency"),
|
|
334
|
+
("FX Rate", "fx_rate"),
|
|
335
|
+
("Base Currency Market Value", "base_currency_market_value"),
|
|
336
|
+
]
|
|
337
|
+
|
|
265
338
|
lines: list[str] = ["=== Position Details ==="]
|
|
266
339
|
for i, detail in enumerate(data, 1):
|
|
267
340
|
if not isinstance(detail, dict):
|
|
268
341
|
continue
|
|
269
342
|
lines.append(f"\n[Position Detail {i}]")
|
|
270
|
-
|
|
271
|
-
f"
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
)
|
|
277
|
-
lines.append(
|
|
278
|
-
f"{'':>10s} "
|
|
279
|
-
f"Name: {_get(detail, 'symbol_name')} "
|
|
280
|
-
f"Exchange: {_get(detail, 'exchange_code')}"
|
|
281
|
-
)
|
|
282
|
-
lines.append(
|
|
283
|
-
f"{'':>10s} "
|
|
284
|
-
f"Average Price: {_get(detail, 'average_price'):>10s} "
|
|
285
|
-
f"Unrealized P&L: {_get(detail, 'unrealized_pl'):>10s}"
|
|
286
|
-
)
|
|
287
|
-
lines.append(
|
|
288
|
-
f"{'':>10s} "
|
|
289
|
-
f"Account Tax Type: {_get(detail, 'account_tax_type')} "
|
|
290
|
-
f"Margin Type: {_get(detail, 'margin_type')}"
|
|
291
|
-
)
|
|
292
|
-
lines.append(
|
|
293
|
-
f"{'':>10s} "
|
|
294
|
-
f"Instrument ID: {_get(detail, 'instrument_id')} "
|
|
295
|
-
f"Contract ID: {_get(detail, 'contract_id')} "
|
|
296
|
-
f"Position ID: {_get(detail, 'id')}"
|
|
297
|
-
)
|
|
298
|
-
lines.append(
|
|
299
|
-
f"{'':>10s} "
|
|
300
|
-
f"Base Currency: {_get(detail, 'base_currency')} "
|
|
301
|
-
f"FX Rate: {_get(detail, 'fx_rate')} "
|
|
302
|
-
f"Base Currency Market Value: {_get(detail, 'base_currency_market_value')}"
|
|
303
|
-
)
|
|
343
|
+
parts = [
|
|
344
|
+
f"{label}: {detail[key]}"
|
|
345
|
+
for label, key in fields
|
|
346
|
+
if detail.get(key) is not None
|
|
347
|
+
]
|
|
348
|
+
lines.append(" " + " ".join(parts))
|
|
304
349
|
|
|
305
350
|
if len(lines) == 1:
|
|
306
351
|
return _NO_DATA
|
|
@@ -81,6 +81,9 @@ def _build_option_order(
|
|
|
81
81
|
limit_price: float | None,
|
|
82
82
|
stop_price: float | None,
|
|
83
83
|
position_intent: str | None = None,
|
|
84
|
+
leg_in_or_out: str | None = None,
|
|
85
|
+
position_id: str | None = None,
|
|
86
|
+
leg_in_strategy: str | None = None,
|
|
84
87
|
) -> dict:
|
|
85
88
|
"""Build a single-leg option order dict for the SDK."""
|
|
86
89
|
order: dict = {
|
|
@@ -109,6 +112,13 @@ def _build_option_order(
|
|
|
109
112
|
_add_optional_str(order, "stop_price", stop_price)
|
|
110
113
|
if position_intent is not None:
|
|
111
114
|
order["position_intent"] = position_intent
|
|
115
|
+
# Option leg-in / leg-out fields (US only)
|
|
116
|
+
if leg_in_or_out is not None:
|
|
117
|
+
order["leg_in_or_out"] = leg_in_or_out
|
|
118
|
+
if position_id is not None:
|
|
119
|
+
order["position_id"] = position_id
|
|
120
|
+
if leg_in_strategy is not None:
|
|
121
|
+
order["leg_in_strategy"] = leg_in_strategy
|
|
112
122
|
return order
|
|
113
123
|
|
|
114
124
|
|
|
@@ -237,6 +247,10 @@ def register_option_single_tools(
|
|
|
237
247
|
"order_type: MARKET, LIMIT, STOP_LOSS, STOP_LOSS_LIMIT.\n"
|
|
238
248
|
"time_in_force: DAY, GTC. trading_session: CORE only.\n"
|
|
239
249
|
"position_intent: BUY_TO_OPEN, BUY_TO_CLOSE, SELL_TO_OPEN, SELL_TO_CLOSE (optional, US only).\n"
|
|
250
|
+
"Leg-in / leg-out (US only, single-leg option): "
|
|
251
|
+
"leg_in_or_out=LEG_IN adds this leg to an existing position; LEG_OUT closes a leg from a multi-leg position. "
|
|
252
|
+
"position_id (from Account Positions API) is required when leg_in_or_out is set. "
|
|
253
|
+
"leg_in_strategy (COVERED_STOCK/VERTICAL/STRADDLE/STRANGLE/CALENDAR/DIAGONAL) is required for LEG_IN, not allowed for LEG_OUT.\n"
|
|
240
254
|
"Returns: {client_order_id, order_id}"
|
|
241
255
|
),
|
|
242
256
|
)
|
|
@@ -254,6 +268,9 @@ def register_option_single_tools(
|
|
|
254
268
|
limit_price: Optional[float] = None,
|
|
255
269
|
stop_price: Optional[float] = None,
|
|
256
270
|
position_intent: Optional[str] = None,
|
|
271
|
+
leg_in_or_out: Optional[str] = None,
|
|
272
|
+
position_id: Optional[str] = None,
|
|
273
|
+
leg_in_strategy: Optional[str] = None,
|
|
257
274
|
) -> str:
|
|
258
275
|
"""Place a single-leg option order."""
|
|
259
276
|
audit.log_tool_call("place_option_single_order", {"symbol": symbol, "side": side})
|
|
@@ -283,6 +300,27 @@ def register_option_single_tools(
|
|
|
283
300
|
f"must be one of {sorted(VALID_POSITION_INTENTS)}"
|
|
284
301
|
)
|
|
285
302
|
|
|
303
|
+
# Leg-in / leg-out validation (US only)
|
|
304
|
+
if leg_in_or_out is not None or position_id is not None or leg_in_strategy is not None:
|
|
305
|
+
from webull_openapi_mcp.constants import VALID_LEG_IN_OUT
|
|
306
|
+
if config.region_id != "us":
|
|
307
|
+
return "Validation error: leg-in / leg-out is only supported for US region"
|
|
308
|
+
if leg_in_or_out is None:
|
|
309
|
+
return "Validation error: leg_in_or_out is required when position_id or leg_in_strategy is provided"
|
|
310
|
+
if leg_in_or_out not in VALID_LEG_IN_OUT:
|
|
311
|
+
return (
|
|
312
|
+
f"Validation error: Invalid leg_in_or_out '{leg_in_or_out}', "
|
|
313
|
+
f"must be one of {sorted(VALID_LEG_IN_OUT)}"
|
|
314
|
+
)
|
|
315
|
+
if position_id is None:
|
|
316
|
+
return "Validation error: position_id is required when leg_in_or_out is specified"
|
|
317
|
+
if leg_in_or_out == "LEG_IN":
|
|
318
|
+
if leg_in_strategy is None:
|
|
319
|
+
return "Validation error: leg_in_strategy is required when leg_in_or_out is LEG_IN"
|
|
320
|
+
else: # LEG_OUT
|
|
321
|
+
if leg_in_strategy is not None:
|
|
322
|
+
return "Validation error: leg_in_strategy is not allowed when leg_in_or_out is LEG_OUT"
|
|
323
|
+
|
|
286
324
|
try:
|
|
287
325
|
validate_client_order_id(client_order_id)
|
|
288
326
|
except ValidationError as e:
|
|
@@ -297,6 +335,9 @@ def register_option_single_tools(
|
|
|
297
335
|
time_in_force=time_in_force,
|
|
298
336
|
limit_price=limit_price, stop_price=stop_price,
|
|
299
337
|
position_intent=position_intent,
|
|
338
|
+
leg_in_or_out=leg_in_or_out,
|
|
339
|
+
position_id=position_id,
|
|
340
|
+
leg_in_strategy=leg_in_strategy,
|
|
300
341
|
)
|
|
301
342
|
|
|
302
343
|
audit.log_order_attempt(
|
|
@@ -128,6 +128,10 @@ def _build_stock_order(params: dict) -> dict:
|
|
|
128
128
|
_add_optional_str(order, "position_intent", params.get("position_intent"))
|
|
129
129
|
if params.get("close_contracts") is not None:
|
|
130
130
|
order["close_contracts"] = params["close_contracts"]
|
|
131
|
+
# Option leg-in / leg-out fields (US only) — e.g. legging a stock leg out of a combo
|
|
132
|
+
_add_optional_str(order, "leg_in_or_out", params.get("leg_in_or_out"))
|
|
133
|
+
_add_optional_str(order, "position_id", params.get("position_id"))
|
|
134
|
+
_add_optional_str(order, "leg_in_strategy", params.get("leg_in_strategy"))
|
|
131
135
|
|
|
132
136
|
return order
|
|
133
137
|
|
|
@@ -447,6 +451,10 @@ def register_stock_order_tools(
|
|
|
447
451
|
"margin_type must be ONE_DAY or INDEFINITE and is margin-account-only; "
|
|
448
452
|
"position_intent must be BUY_TO_OPEN, BUY_TO_CLOSE, SELL_TO_OPEN, or SELL_TO_CLOSE and is margin-account-only; "
|
|
449
453
|
"close_contracts account eligibility is checked by the API backend.\n"
|
|
454
|
+
"Leg-in / leg-out (US only): use to leg a STOCK leg into/out of an option combo position. "
|
|
455
|
+
"leg_in_or_out=LEG_IN adds this stock leg to an existing position; LEG_OUT closes the stock leg from a multi-leg combo. "
|
|
456
|
+
"position_id (from Account Positions API) is required when leg_in_or_out is set. "
|
|
457
|
+
"leg_in_strategy (COVERED_STOCK/VERTICAL/STRADDLE/STRANGLE/CALENDAR/DIAGONAL) is required for LEG_IN, not allowed for LEG_OUT.\n"
|
|
450
458
|
"Returns: {client_order_id, order_id}"
|
|
451
459
|
),
|
|
452
460
|
)
|
|
@@ -472,6 +480,9 @@ def register_stock_order_tools(
|
|
|
472
480
|
margin_type: Optional[MarginType] = None,
|
|
473
481
|
position_intent: Optional[PositionIntent] = None,
|
|
474
482
|
close_contracts: Optional[list[dict]] = None,
|
|
483
|
+
leg_in_or_out: Optional[str] = None,
|
|
484
|
+
position_id: Optional[str] = None,
|
|
485
|
+
leg_in_strategy: Optional[str] = None,
|
|
475
486
|
) -> str:
|
|
476
487
|
"""Place a single stock order (non-combo).
|
|
477
488
|
|
|
@@ -531,6 +542,27 @@ def register_stock_order_tools(
|
|
|
531
542
|
except ValidationError as e:
|
|
532
543
|
return f"Validation error: {e.message}"
|
|
533
544
|
|
|
545
|
+
# Leg-in / leg-out validation (US only) — e.g. legging a stock leg into/out of a combo
|
|
546
|
+
if leg_in_or_out is not None or position_id is not None or leg_in_strategy is not None:
|
|
547
|
+
from webull_openapi_mcp.constants import VALID_LEG_IN_OUT
|
|
548
|
+
if config.region_id != "us":
|
|
549
|
+
return "Validation error: leg-in / leg-out is only supported for US region"
|
|
550
|
+
if leg_in_or_out is None:
|
|
551
|
+
return "Validation error: leg_in_or_out is required when position_id or leg_in_strategy is provided"
|
|
552
|
+
if leg_in_or_out not in VALID_LEG_IN_OUT:
|
|
553
|
+
return (
|
|
554
|
+
f"Validation error: Invalid leg_in_or_out '{leg_in_or_out}', "
|
|
555
|
+
f"must be one of {sorted(VALID_LEG_IN_OUT)}"
|
|
556
|
+
)
|
|
557
|
+
if position_id is None:
|
|
558
|
+
return "Validation error: position_id is required when leg_in_or_out is specified"
|
|
559
|
+
if leg_in_or_out == "LEG_IN":
|
|
560
|
+
if leg_in_strategy is None:
|
|
561
|
+
return "Validation error: leg_in_strategy is required when leg_in_or_out is LEG_IN"
|
|
562
|
+
else: # LEG_OUT
|
|
563
|
+
if leg_in_strategy is not None:
|
|
564
|
+
return "Validation error: leg_in_strategy is not allowed when leg_in_or_out is LEG_OUT"
|
|
565
|
+
|
|
534
566
|
coid = client_order_id or _generate_client_order_id()
|
|
535
567
|
|
|
536
568
|
order = _build_stock_order({
|
|
@@ -546,6 +578,9 @@ def register_stock_order_tools(
|
|
|
546
578
|
"margin_type": margin_type,
|
|
547
579
|
"position_intent": position_intent,
|
|
548
580
|
"close_contracts": close_contracts,
|
|
581
|
+
"leg_in_or_out": leg_in_or_out,
|
|
582
|
+
"position_id": position_id,
|
|
583
|
+
"leg_in_strategy": leg_in_strategy,
|
|
549
584
|
})
|
|
550
585
|
|
|
551
586
|
audit.log_order_attempt(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/crypto.py
RENAMED
|
File without changes
|
{webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/event.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/option.py
RENAMED
|
File without changes
|
|
File without changes
|
{webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/market_data/stock.py
RENAMED
|
File without changes
|
|
File without changes
|
{webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/__init__.py
RENAMED
|
File without changes
|
{webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/account.py
RENAMED
|
File without changes
|
{webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/assets.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/instrument.py
RENAMED
|
File without changes
|
{webull_openapi_mcp-1.2.2 → webull_openapi_mcp-1.2.3}/webull_openapi_mcp/tools/trading/order.py
RENAMED
|
File without changes
|