diff --git a/CHANGELOG.md b/CHANGELOG.md index 9231b6f25..07bfd7b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **Trade: estimate multi-leg option trading info** — new `TradeContext.estimate_multileg_available_quantity` (`POST /v1/trade/estimate/multileg`) estimating a US option combination strategy's maximum tradable quantity and margin impact before submitting. Takes `side`, `order_type`, `submitted_quantity`, `strategy` (`MultiLegStrategy`), the combination `legs` (each an `EstimateMultiLegOrderLeg` — symbol plus a reserved `side`), and an optional `submitted_price`; returns `max_open_qty`, `unit_margin`, `initial_margin_change` and `maintenance_margin_change`. Unlike the single-leg `estimate_max_purchase_quantity` (a `GET` with scalar query params), the multi-leg estimate carries a `legs` array so it is a `POST` with a JSON body, matching the multi-leg submit endpoint. Added across the Rust core (async + blocking) and mirrored to the C, C++, Java, Node.js and Python layers; the separate Go SDK is tracked separately. + ## [5.2.0] - 2026-09-30 ### Added diff --git a/c/cbindgen.toml b/c/cbindgen.toml index 24750a1fd..c1e56d355 100644 --- a/c/cbindgen.toml +++ b/c/cbindgen.toml @@ -190,6 +190,9 @@ cpp_compat = true "CPushGridOrderChanged" = "lb_push_grid_order_changed_t" "CEstimateMaxPurchaseQuantityOptions" = "lb_estimate_max_purchase_quantity_options_t" "CEstimateMaxPurchaseQuantityResponse" = "lb_estimate_max_purchase_quantity_response_t" +"CEstimateMultiLegOrderLeg" = "lb_estimate_multileg_order_leg_t" +"CEstimateMultiLegAvailableQuantityOptions" = "lb_estimate_multileg_available_quantity_options_t" +"CEstimateMultiLegAvailableQuantityResponse" = "lb_estimate_multileg_available_quantity_response_t" "CCreateWatchlistGroup" = "lb_create_watchlist_group_t" "CUpdateWatchlistGroup" = "lb_update_watchlist_group_t" "CSecuritiesUpdateMode" = "lb_securities_update_mode_t" @@ -514,6 +517,7 @@ include = [ # TradeContext: grid trading push (stays on trade side) "CPushGridOrderChanged", "CEstimateMaxPurchaseQuantityResponse", + "CEstimateMultiLegAvailableQuantityResponse", "CSecurityCalcIndex", "CWarrantInfo", "CQuotePackageDetail", diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index 9d281ca68..f2b9f002b 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -4958,6 +4958,57 @@ typedef struct lb_estimate_max_purchase_quantity_options_t { bool fractional_shares; } lb_estimate_max_purchase_quantity_options_t; +/** + * A leg of a multi-leg combination for the pre-trade estimate + */ +typedef struct lb_estimate_multileg_order_leg_t { + /** + * Option or underlying-stock symbol, in `ticker.region` format + * (e.g. `QQQ260731C764000.US`) + */ + const char *symbol; + /** + * Leg order side (reserved; not in use for now) (can be null) + */ + const enum lb_order_side_t *side; +} lb_estimate_multileg_order_leg_t; + +/** + * Options for estimating a multi-leg option combination's tradable quantity + * and margin impact + */ +typedef struct lb_estimate_multileg_available_quantity_options_t { + /** + * Order side of the combination + */ + enum lb_order_side_t side; + /** + * Order type + */ + enum lb_order_type_t order_type; + /** + * Submitted quantity (number of combinations) + */ + const struct lb_decimal_t *submitted_quantity; + /** + * Multi-leg strategy + */ + enum CMultiLegStrategy strategy; + /** + * Legs of the combination + */ + const struct lb_estimate_multileg_order_leg_t *legs; + /** + * Number of legs + */ + uintptr_t num_legs; + /** + * Submitted price (required for limit order types such as `LO`) (can be + * null) + */ + const struct lb_decimal_t *submitted_price; +} lb_estimate_multileg_available_quantity_options_t; + /** * Active subscription for a security */ @@ -9069,6 +9120,29 @@ typedef struct lb_estimate_max_purchase_quantity_response_t { const struct lb_decimal_t *margin_max_qty; } lb_estimate_max_purchase_quantity_response_t; +/** + * Response for estimating a multi-leg option combination's tradable quantity + * and margin impact + */ +typedef struct lb_estimate_multileg_available_quantity_response_t { + /** + * Maximum open quantity of the combination + */ + const struct lb_decimal_t *max_open_qty; + /** + * Margin required per combination unit + */ + const struct lb_decimal_t *unit_margin; + /** + * Change of the initial margin after placing the order + */ + const struct lb_decimal_t *initial_margin_change; + /** + * Change of the maintenance margin after placing the order + */ + const struct lb_decimal_t *maintenance_margin_change; +} lb_estimate_multileg_available_quantity_response_t; + /** * Security calc index response */ @@ -16466,6 +16540,17 @@ void lb_trade_context_estimate_max_purchase_quantity(const struct lb_trade_conte lb_async_callback_t callback, void *userdata); +/** + * Estimate a multi-leg option combination's maximum tradable quantity and + * margin impact before submitting the order. + * + * @param[in] opts Options for the multi-leg estimate request + */ +void lb_trade_context_estimate_multileg_available_quantity(const struct lb_trade_context_t *ctx, + const struct lb_estimate_multileg_available_quantity_options_t *opts, + lb_async_callback_t callback, + void *userdata); + /** * Create a decimal value with a 64 bit `m` representation and corresponding * `e` scale. diff --git a/c/src/trade_context/context.rs b/c/src/trade_context/context.rs index 897021a93..82a5224b0 100644 --- a/c/src/trade_context/context.rs +++ b/c/src/trade_context/context.rs @@ -4,6 +4,7 @@ use longbridge::{ TradeContext, trade::{ AttachedOrderType, CancelOrderOptions, EstimateMaxPurchaseQuantityOptions, + EstimateMultiLegAvailableQuantityOptions, EstimateMultiLegOrderLeg, GetAllExecutionsOptions, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetOrderDetailOptions, GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, PushEvent, @@ -23,13 +24,15 @@ use crate::{ types::{ CAccountBalanceOwned, CAllExecutionsResponseOwned, CCashFlowOwned, CEstimateMaxPurchaseQuantityOptions, CEstimateMaxPurchaseQuantityResponseOwned, - CExecutionOwned, CFundPositionsResponseOwned, CGetAllExecutionsOptions, - CGetCashFlowOptions, CGetFundPositionsOptions, CGetHistoryExecutionsOptions, - CGetHistoryOrdersOptions, CGetStockPositionsOptions, CGetTodayExecutionsOptions, - CGetTodayOrdersOptions, CMarginRatioOwned, COrderDetailOwned, COrderOwned, - CPushGridOrderChanged, CPushGridOrderChangedOwned, CPushOrderChanged, - CPushOrderChangedOwned, CReplaceOrderOptions, CStockPositionsResponseOwned, - CSubmitMultiLegOrderOptions, CSubmitOrderOptions, CSubmitOrderResponseOwned, + CEstimateMultiLegAvailableQuantityOptions, + CEstimateMultiLegAvailableQuantityResponseOwned, CExecutionOwned, + CFundPositionsResponseOwned, CGetAllExecutionsOptions, CGetCashFlowOptions, + CGetFundPositionsOptions, CGetHistoryExecutionsOptions, CGetHistoryOrdersOptions, + CGetStockPositionsOptions, CGetTodayExecutionsOptions, CGetTodayOrdersOptions, + CMarginRatioOwned, COrderDetailOwned, COrderOwned, CPushGridOrderChanged, + CPushGridOrderChangedOwned, CPushOrderChanged, CPushOrderChangedOwned, + CReplaceOrderOptions, CStockPositionsResponseOwned, CSubmitMultiLegOrderOptions, + CSubmitOrderOptions, CSubmitOrderResponseOwned, }, }, types::{CCow, CVec, ToFFI, cstr_array_to_rust, cstr_to_rust, slice_from_raw_parts}, @@ -873,3 +876,49 @@ pub unsafe extern "C" fn lb_trade_context_estimate_max_purchase_quantity( Ok(resp) }); } + +/// Estimate a multi-leg option combination's maximum tradable quantity and +/// margin impact before submitting the order. +/// +/// @param[in] opts Options for the multi-leg estimate request +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_trade_context_estimate_multileg_available_quantity( + ctx: *const CTradeContext, + opts: *const CEstimateMultiLegAvailableQuantityOptions, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let side = (*opts).side.into(); + let order_type = (*opts).order_type.into(); + let submitted_quantity = (*(*opts).submitted_quantity).value; + let strategy = (*opts).strategy.into(); + let legs = slice_from_raw_parts((*opts).legs, (*opts).num_legs) + .iter() + .map(|leg| { + let mut l = EstimateMultiLegOrderLeg::new(cstr_to_rust(leg.symbol)); + if !leg.side.is_null() { + l = l.side((*leg.side).into()); + } + l + }) + .collect::>(); + let mut opts2 = EstimateMultiLegAvailableQuantityOptions::new( + side, + order_type, + submitted_quantity, + strategy, + legs, + ); + if !(*opts).submitted_price.is_null() { + opts2 = opts2.submitted_price((*(*opts).submitted_price).value); + } + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new( + ctx_inner + .estimate_multileg_available_quantity(opts2) + .await?, + ); + Ok(resp) + }); +} diff --git a/c/src/trade_context/types.rs b/c/src/trade_context/types.rs index eb14ae2bf..23bcf2ba6 100644 --- a/c/src/trade_context/types.rs +++ b/c/src/trade_context/types.rs @@ -5,12 +5,13 @@ use longbridge::{ trade::{ AccountBalance, AllExecutionsResponse, AttachedOrderDetail, AttachedOrderType, BalanceType, CashFlow, CashFlowDirection, CashInfo, ContractDirection, - EstimateMaxPurchaseQuantityResponse, Execution, FrozenTransactionFee, FundPosition, - FundPositionChannel, FundPositionsResponse, MarginRatio, MultiLegInfo, MultiLegOrderLeg, - MultiLegPosition, MultiLegStrategy, Order, OrderChargeDetail, OrderChargeFee, - OrderChargeItem, OrderDetail, OrderHistoryDetail, OrderSide, OrderStatus, OrderTag, - OrderType, PushGridOrderChanged, PushOrderChanged, StockPosition, StockPositionChannel, - StockPositionsResponse, SubmitOrderResponse, TimeInForceType, + EstimateMaxPurchaseQuantityResponse, EstimateMultiLegAvailableQuantityResponse, Execution, + FrozenTransactionFee, FundPosition, FundPositionChannel, FundPositionsResponse, + MarginRatio, MultiLegInfo, MultiLegOrderLeg, MultiLegPosition, MultiLegStrategy, Order, + OrderChargeDetail, OrderChargeFee, OrderChargeItem, OrderDetail, OrderHistoryDetail, + OrderSide, OrderStatus, OrderTag, OrderType, PushGridOrderChanged, PushOrderChanged, + StockPosition, StockPositionChannel, StockPositionsResponse, SubmitOrderResponse, + TimeInForceType, }, }; use time::OffsetDateTime; @@ -2769,6 +2770,86 @@ impl ToFFI for CEstimateMaxPurchaseQuantityResponseOwned { } } +/// A leg of a multi-leg combination for the pre-trade estimate +#[repr(C)] +pub struct CEstimateMultiLegOrderLeg { + /// Option or underlying-stock symbol, in `ticker.region` format + /// (e.g. `QQQ260731C764000.US`) + pub symbol: *const c_char, + /// Leg order side (reserved; not in use for now) (can be null) + pub side: *const COrderSide, +} + +/// Options for estimating a multi-leg option combination's tradable quantity +/// and margin impact +#[repr(C)] +pub struct CEstimateMultiLegAvailableQuantityOptions { + /// Order side of the combination + pub side: COrderSide, + /// Order type + pub order_type: COrderType, + /// Submitted quantity (number of combinations) + pub submitted_quantity: *const CDecimal, + /// Multi-leg strategy + pub strategy: CMultiLegStrategy, + /// Legs of the combination + pub legs: *const CEstimateMultiLegOrderLeg, + /// Number of legs + pub num_legs: usize, + /// Submitted price (required for limit order types such as `LO`) (can be + /// null) + pub submitted_price: *const CDecimal, +} + +/// Response for estimating a multi-leg option combination's tradable quantity +/// and margin impact +#[repr(C)] +pub struct CEstimateMultiLegAvailableQuantityResponse { + /// Maximum open quantity of the combination + pub max_open_qty: *const CDecimal, + /// Margin required per combination unit + pub unit_margin: *const CDecimal, + /// Change of the initial margin after placing the order + pub initial_margin_change: *const CDecimal, + /// Change of the maintenance margin after placing the order + pub maintenance_margin_change: *const CDecimal, +} + +#[derive(Debug)] +#[repr(C)] +pub struct CEstimateMultiLegAvailableQuantityResponseOwned { + max_open_qty: CDecimal, + unit_margin: CDecimal, + initial_margin_change: CDecimal, + maintenance_margin_change: CDecimal, +} + +impl From + for CEstimateMultiLegAvailableQuantityResponseOwned +{ + fn from(value: EstimateMultiLegAvailableQuantityResponse) -> Self { + CEstimateMultiLegAvailableQuantityResponseOwned { + max_open_qty: value.max_open_qty.into(), + unit_margin: value.unit_margin.into(), + initial_margin_change: value.initial_margin_change.into(), + maintenance_margin_change: value.maintenance_margin_change.into(), + } + } +} + +impl ToFFI for CEstimateMultiLegAvailableQuantityResponseOwned { + type FFIType = CEstimateMultiLegAvailableQuantityResponse; + + fn to_ffi_type(&self) -> Self::FFIType { + CEstimateMultiLegAvailableQuantityResponse { + max_open_qty: self.max_open_qty.to_ffi_type(), + unit_margin: self.unit_margin.to_ffi_type(), + initial_margin_change: self.initial_margin_change.to_ffi_type(), + maintenance_margin_change: self.maintenance_margin_change.to_ffi_type(), + } + } +} + // ── Grid trading push (stays on the trade context) // ────────────────────────────────── diff --git a/cpp/include/trade_context.hpp b/cpp/include/trade_context.hpp index 32e85f08c..ff1815b4f 100644 --- a/cpp/include/trade_context.hpp +++ b/cpp/include/trade_context.hpp @@ -133,6 +133,13 @@ class TradeContext AsyncCallback callback) const; + /// Estimate a multi-leg option combination's maximum tradable quantity and + /// margin impact before submitting the order + void estimate_multileg_available_quantity( + const EstimateMultiLegAvailableQuantityOptions& opts, + AsyncCallback + callback) const; + /// Set grid order changed callback, after receiving the grid order changed /// event, it will call back to this function. void set_on_grid_order_changed( diff --git a/cpp/include/types.hpp b/cpp/include/types.hpp index f4114ae69..43f3ff4d6 100644 --- a/cpp/include/types.hpp +++ b/cpp/include/types.hpp @@ -2444,6 +2444,48 @@ struct EstimateMaxPurchaseQuantityResponse Decimal margin_max_qty; }; +/// A leg of a multi-leg combination for the pre-trade estimate +struct EstimateMultiLegOrderLeg +{ + /// Option or underlying-stock symbol, in `ticker.region` format + /// (e.g. `QQQ260731C764000.US`) + std::string symbol; + /// Leg order side (reserved; not in use for now) + std::optional side; +}; + +/// Options for estimating a multi-leg option combination's tradable quantity +/// and margin impact +struct EstimateMultiLegAvailableQuantityOptions +{ + /// Order side of the combination + OrderSide side; + /// Order type + OrderType order_type; + /// Submitted quantity (number of combinations) + Decimal submitted_quantity; + /// Multi-leg strategy + MultiLegStrategy strategy; + /// Legs of the combination + std::vector legs; + /// Submitted price (required for limit order types such as `LO`) + std::optional submitted_price; +}; + +/// Response for estimating a multi-leg option combination's tradable quantity +/// and margin impact +struct EstimateMultiLegAvailableQuantityResponse +{ + /// Maximum open quantity of the combination + Decimal max_open_qty; + /// Margin required per combination unit + Decimal unit_margin; + /// Change of the initial margin after placing the order + Decimal initial_margin_change; + /// Change of the maintenance margin after placing the order + Decimal maintenance_margin_change; +}; + /// Grid trading master-order changed message. struct PushGridOrderChanged diff --git a/cpp/src/trade_context.cpp b/cpp/src/trade_context.cpp index b568242ea..0da4ed044 100644 --- a/cpp/src/trade_context.cpp +++ b/cpp/src/trade_context.cpp @@ -950,6 +950,72 @@ TradeContext::estimate_max_purchase_quantity( callback)); } +void +TradeContext::estimate_multileg_available_quantity( + const EstimateMultiLegAvailableQuantityOptions& opts, + AsyncCallback + callback) const +{ + // `leg_sides` holds the converted optional leg sides so each leg's `side` + // pointer stays valid for the duration of the call; reserve to the final + // size up front so no push_back reallocates and invalidates a pointer. + std::vector leg_sides; + leg_sides.reserve(opts.legs.size()); + std::vector legs; + legs.reserve(opts.legs.size()); + for (const auto& leg : opts.legs) { + const lb_order_side_t* side_ptr = nullptr; + if (leg.side) { + leg_sides.push_back(convert(leg.side.value())); + side_ptr = &leg_sides.back(); + } + legs.push_back( + lb_estimate_multileg_order_leg_t{ leg.symbol.c_str(), side_ptr }); + } + + lb_estimate_multileg_available_quantity_options_t opts2 = { + convert(opts.side), + convert(opts.order_type), + (const lb_decimal_t*)opts.submitted_quantity, + convert(opts.strategy), + legs.data(), + legs.size(), + opts.submitted_price ? (const lb_decimal_t*)opts.submitted_price.value() + : nullptr, + }; + + lb_trade_context_estimate_multileg_available_quantity( + ctx_, + &opts2, + [](auto res) { + auto callback_ptr = callback::get_async_callback< + TradeContext, + EstimateMultiLegAvailableQuantityResponse>(res->userdata); + TradeContext ctx((const lb_trade_context_t*)res->ctx); + Status status(res->error); + + if (status) { + auto res_data = + (const lb_estimate_multileg_available_quantity_response_t*)res->data; + EstimateMultiLegAvailableQuantityResponse resp = { + res_data->max_open_qty, + res_data->unit_margin, + res_data->initial_margin_change, + res_data->maintenance_margin_change, + }; + (*callback_ptr)( + AsyncResult( + ctx, std::move(status), &resp)); + } else { + (*callback_ptr)( + AsyncResult( + ctx, std::move(status), nullptr)); + } + }, + new AsyncCallback( + callback)); +} + void TradeContext::set_on_grid_order_changed( PushCallback callback) const diff --git a/java/javasrc/src/main/java/com/longbridge/SdkNative.java b/java/javasrc/src/main/java/com/longbridge/SdkNative.java index a3c6b7456..0325a5ded 100644 --- a/java/javasrc/src/main/java/com/longbridge/SdkNative.java +++ b/java/javasrc/src/main/java/com/longbridge/SdkNative.java @@ -266,6 +266,10 @@ public static native void tradeContextEstimateMaxPurchaseQuantity(long context, EstimateMaxPurchaseQuantityOptions opts, AsyncCallback callback); + public static native void tradeContextEstimateMultilegAvailableQuantity(long context, + com.longbridge.trade.EstimateMultiLegAvailableQuantityOptions opts, + AsyncCallback callback); + // ── Grid trading push (stays on the trade side) ─────────────── public static native void tradeContextSetOnGridOrderChanged(long context, GridOrderChangedHandler handler); diff --git a/java/javasrc/src/main/java/com/longbridge/trade/EstimateMultiLegAvailableQuantityOptions.java b/java/javasrc/src/main/java/com/longbridge/trade/EstimateMultiLegAvailableQuantityOptions.java new file mode 100644 index 000000000..1a251e15a --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/trade/EstimateMultiLegAvailableQuantityOptions.java @@ -0,0 +1,105 @@ +package com.longbridge.trade; + +import java.math.BigDecimal; + +/** + * Options for estimating a multi-leg option combination's maximum tradable + * quantity and margin impact. + */ +@SuppressWarnings("unused") +public class EstimateMultiLegAvailableQuantityOptions { + private OrderSide side; + private OrderType orderType; + private BigDecimal submittedQuantity; + private MultiLegStrategy strategy; + private EstimateMultiLegOrderLeg[] legs; + private BigDecimal submittedPrice; + + /** + * Constructs options for a multi-leg estimate. + * + * @param side order side of the combination + * @param orderType order type + * @param submittedQuantity submitted quantity (number of combinations) + * @param strategy multi-leg strategy + * @param legs legs of the combination + */ + public EstimateMultiLegAvailableQuantityOptions( + OrderSide side, + OrderType orderType, + BigDecimal submittedQuantity, + MultiLegStrategy strategy, + EstimateMultiLegOrderLeg[] legs) { + this.side = side; + this.orderType = orderType; + this.submittedQuantity = submittedQuantity; + this.strategy = strategy; + this.legs = legs; + } + + /** + * Sets the submitted price (required for limit order types such as + * {@code LO}). + * + * @param submittedPrice submitted price + * @return this instance for chaining + */ + public EstimateMultiLegAvailableQuantityOptions setSubmittedPrice(BigDecimal submittedPrice) { + this.submittedPrice = submittedPrice; + return this; + } + + /** + * Returns the order side. + * + * @return order side + */ + public OrderSide getSide() { + return side; + } + + /** + * Returns the order type. + * + * @return order type + */ + public OrderType getOrderType() { + return orderType; + } + + /** + * Returns the submitted quantity. + * + * @return submitted quantity + */ + public BigDecimal getSubmittedQuantity() { + return submittedQuantity; + } + + /** + * Returns the multi-leg strategy. + * + * @return multi-leg strategy + */ + public MultiLegStrategy getStrategy() { + return strategy; + } + + /** + * Returns the legs of the combination. + * + * @return legs + */ + public EstimateMultiLegOrderLeg[] getLegs() { + return legs; + } + + /** + * Returns the submitted price. + * + * @return submitted price + */ + public BigDecimal getSubmittedPrice() { + return submittedPrice; + } +} diff --git a/java/javasrc/src/main/java/com/longbridge/trade/EstimateMultiLegAvailableQuantityResponse.java b/java/javasrc/src/main/java/com/longbridge/trade/EstimateMultiLegAvailableQuantityResponse.java new file mode 100644 index 000000000..527b81bc8 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/trade/EstimateMultiLegAvailableQuantityResponse.java @@ -0,0 +1,57 @@ +package com.longbridge.trade; + +import java.math.BigDecimal; + +/** + * Estimated tradable quantity and margin impact of a multi-leg option + * combination. + */ +public class EstimateMultiLegAvailableQuantityResponse { + private BigDecimal maxOpenQty; + private BigDecimal unitMargin; + private BigDecimal initialMarginChange; + private BigDecimal maintenanceMarginChange; + + /** + * Returns the maximum open quantity of the combination. + * + * @return maximum open quantity + */ + public BigDecimal getMaxOpenQty() { + return maxOpenQty; + } + + /** + * Returns the margin required per combination unit. + * + * @return unit margin + */ + public BigDecimal getUnitMargin() { + return unitMargin; + } + + /** + * Returns the change of the initial margin after placing the order. + * + * @return initial margin change + */ + public BigDecimal getInitialMarginChange() { + return initialMarginChange; + } + + /** + * Returns the change of the maintenance margin after placing the order. + * + * @return maintenance margin change + */ + public BigDecimal getMaintenanceMarginChange() { + return maintenanceMarginChange; + } + + @Override + public String toString() { + return "EstimateMultiLegAvailableQuantityResponse [maxOpenQty=" + maxOpenQty + ", unitMargin=" + unitMargin + + ", initialMarginChange=" + initialMarginChange + ", maintenanceMarginChange=" + + maintenanceMarginChange + "]"; + } +} diff --git a/java/javasrc/src/main/java/com/longbridge/trade/EstimateMultiLegOrderLeg.java b/java/javasrc/src/main/java/com/longbridge/trade/EstimateMultiLegOrderLeg.java new file mode 100644 index 000000000..9d0a352a2 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/trade/EstimateMultiLegOrderLeg.java @@ -0,0 +1,49 @@ +package com.longbridge.trade; + +/** + * A leg of a multi-leg combination for the pre-trade estimate. + */ +@SuppressWarnings("unused") +public class EstimateMultiLegOrderLeg { + private String symbol; + private OrderSide side; + + /** + * Constructs a leg of the combination. + * + * @param symbol option or underlying-stock symbol, in `ticker.region` + * format (e.g. {@code QQQ260731C764000.US}) + */ + public EstimateMultiLegOrderLeg(String symbol) { + this.symbol = symbol; + } + + /** + * Sets the leg order side (reserved; not in use for now). + * + * @param side leg order side + * @return this instance for chaining + */ + public EstimateMultiLegOrderLeg setSide(OrderSide side) { + this.side = side; + return this; + } + + /** + * Returns the leg symbol. + * + * @return leg symbol + */ + public String getSymbol() { + return symbol; + } + + /** + * Returns the leg order side (reserved). + * + * @return leg order side + */ + public OrderSide getSide() { + return side; + } +} diff --git a/java/javasrc/src/main/java/com/longbridge/trade/TradeContext.java b/java/javasrc/src/main/java/com/longbridge/trade/TradeContext.java index 71b46c4c8..971a65c2d 100644 --- a/java/javasrc/src/main/java/com/longbridge/trade/TradeContext.java +++ b/java/javasrc/src/main/java/com/longbridge/trade/TradeContext.java @@ -693,6 +693,22 @@ public synchronized CompletableFuture getEs }); } + /** + * Estimate a multi-leg option combination's maximum tradable quantity and + * margin impact before submitting the order (US market only). + * + * @param opts Options for this request + * @return A Future representing the result of the operation + * @throws OpenApiException If an error occurs + */ + public synchronized CompletableFuture estimateMultilegAvailableQuantity( + EstimateMultiLegAvailableQuantityOptions opts) + throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.tradeContextEstimateMultilegAvailableQuantity(raw(), opts, callback); + }); + } + /** * Set grid trading order changed event callback. After receiving a grid order * changed event, it will call back to this handler. diff --git a/java/src/init.rs b/java/src/init.rs index 855aa87c0..0ff4a1780 100644 --- a/java/src/init.rs +++ b/java/src/init.rs @@ -214,6 +214,7 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::trade::OrderChargeDetail, longbridge::trade::OrderDetail, longbridge::trade::EstimateMaxPurchaseQuantityResponse, + longbridge::trade::EstimateMultiLegAvailableQuantityResponse, // Grid trading (GridContext types live in `longbridge::grid`) longbridge::grid::SubmitGridOrderResponse, longbridge::grid::GridOrder, diff --git a/java/src/trade_context.rs b/java/src/trade_context.rs index 9e75f01b1..39b8d955d 100644 --- a/java/src/trade_context.rs +++ b/java/src/trade_context.rs @@ -10,6 +10,7 @@ use longbridge::{ Config, Decimal, Market, TradeContext, trade::{ AttachedOrderType, BalanceType, CancelOrderOptions, EstimateMaxPurchaseQuantityOptions, + EstimateMultiLegAvailableQuantityOptions, EstimateMultiLegOrderLeg, GetAllExecutionsOptions, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetOrderDetailOptions, GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, @@ -946,6 +947,66 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_tradeContextEstimate }) } +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_tradeContextEstimateMultilegAvailableQuantity( + mut env: JNIEnv, + _class: JClass, + context: i64, + opts: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let __owned_ctx = context.ctx.clone(); + let side: OrderSide = get_field(env, &opts, "side")?; + let order_type: OrderType = get_field(env, &opts, "orderType")?; + let submitted_quantity: Decimal = get_field(env, &opts, "submittedQuantity")?; + let strategy: MultiLegStrategy = get_field(env, &opts, "strategy")?; + + let legs_obj = env + .get_field( + &opts, + "legs", + "[Lcom/longbridge/trade/EstimateMultiLegOrderLeg;", + )? + .l()?; + let mut legs = Vec::new(); + if !legs_obj.is_null() { + let legs_array: JObjectArray = legs_obj.into(); + let len = env.get_array_length(&legs_array)?; + for i in 0..len { + let leg_obj = env.get_object_array_element(&legs_array, i)?; + let symbol: String = get_field(env, &leg_obj, "symbol")?; + let mut leg = EstimateMultiLegOrderLeg::new(symbol); + let leg_side: Option = get_field(env, &leg_obj, "side")?; + if let Some(leg_side) = leg_side { + leg = leg.side(leg_side); + } + legs.push(leg); + } + } + + let mut new_opts = EstimateMultiLegAvailableQuantityOptions::new( + side, + order_type, + submitted_quantity, + strategy, + legs, + ); + let submitted_price: Option = get_field(env, &opts, "submittedPrice")?; + if let Some(submitted_price) = submitted_price { + new_opts = new_opts.submitted_price(submitted_price); + } + + async_util::execute(env, callback, async move { + Ok(__owned_ctx + .estimate_multileg_available_quantity(new_opts) + .await?) + })?; + Ok(()) + }) +} + // ── US-market JNI stubs ────────────────────────────────────────────────────── #[unsafe(no_mangle)] diff --git a/java/src/types/classes.rs b/java/src/types/classes.rs index 0128dbd63..82de6a132 100644 --- a/java/src/types/classes.rs +++ b/java/src/types/classes.rs @@ -1032,6 +1032,17 @@ impl_java_class!( [cash_max_qty, margin_max_qty] ); +impl_java_class!( + "com/longbridge/trade/EstimateMultiLegAvailableQuantityResponse", + longbridge::trade::EstimateMultiLegAvailableQuantityResponse, + [ + max_open_qty, + unit_margin, + initial_margin_change, + maintenance_margin_change + ] +); + // ── Grid trading types ──────────────────────────────────────────── impl_java_class!( diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 5ef828a38..f53772cbd 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -694,6 +694,23 @@ export declare class EstimateMaxPurchaseQuantityResponse { get marginMaxQty(): Decimal } +/** + * Response for estimating a multi-leg option combination's tradable quantity + * and margin impact + */ +export declare class EstimateMultiLegAvailableQuantityResponse { + toString(): string + toJSON(): any + /** Maximum open quantity of the combination */ + get maxOpenQty(): Decimal + /** Margin required per combination unit */ + get unitMargin(): Decimal + /** Change of the initial margin after placing the order */ + get initialMarginChange(): Decimal + /** Change of the maintenance margin after placing the order */ + get maintenanceMarginChange(): Decimal +} + /** Trade */ export declare class Execution { toString(): string @@ -4617,6 +4634,11 @@ export declare class TradeContext { * ``` */ estimateMaxPurchaseQuantity(opts: EstimateMaxPurchaseQuantityOptions): Promise + /** + * Estimate a multi-leg option combination's maximum tradable quantity and + * margin impact before submitting the order (US market only). + */ + estimateMultilegAvailableQuantity(opts: EstimateMultiLegAvailableQuantityOptions): Promise } /** The information of trading session */ @@ -6154,6 +6176,36 @@ export interface EstimateMaxPurchaseQuantityOptions { fractionalShares: boolean } +/** + * Options for estimating a multi-leg option combination's maximum tradable + * quantity and margin impact + */ +export interface EstimateMultiLegAvailableQuantityOptions { + /** Order side of the combination */ + side: OrderSide + /** Order type */ + orderType: OrderType + /** Submitted quantity (number of combinations) */ + submittedQuantity: Decimal + /** Multi-leg strategy */ + strategy: MultiLegStrategy + /** Legs of the combination */ + legs: Array + /** Submitted price (required for limit order types such as `LO`) */ + submittedPrice?: Decimal +} + +/** A leg of a multi-leg combination for the pre-trade estimate */ +export interface EstimateMultiLegOrderLeg { + /** + * Option or underlying-stock symbol, in `ticker.region` format + * (e.g. `QQQ260731C764000.US`) + */ + symbol: string + /** Leg order side. Reserved field, not in use for now. */ + side?: OrderSide +} + /** One currency exchange rate */ export interface ExchangeRate { /** Average rate (base_currency / other_currency) */ diff --git a/nodejs/index.js b/nodejs/index.js index efec33ac5..082a2884c 100644 --- a/nodejs/index.js +++ b/nodejs/index.js @@ -597,6 +597,7 @@ module.exports.DCAContext = nativeBinding.DCAContext module.exports.Decimal = nativeBinding.Decimal module.exports.Depth = nativeBinding.Depth module.exports.EstimateMaxPurchaseQuantityResponse = nativeBinding.EstimateMaxPurchaseQuantityResponse +module.exports.EstimateMultiLegAvailableQuantityResponse = nativeBinding.EstimateMultiLegAvailableQuantityResponse module.exports.Execution = nativeBinding.Execution module.exports.FilingItem = nativeBinding.FilingItem module.exports.FrozenTransactionFee = nativeBinding.FrozenTransactionFee diff --git a/nodejs/src/trade/context.rs b/nodejs/src/trade/context.rs index dd791569d..ebcde01dc 100644 --- a/nodejs/src/trade/context.rs +++ b/nodejs/src/trade/context.rs @@ -9,16 +9,16 @@ use crate::{ error::ErrorNewType, trade::{ requests::{ - EstimateMaxPurchaseQuantityOptions, GetAllExecutionsOptions, GetCashFlowOptions, - GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetTodayExecutionsOptions, - GetTodayOrdersOptions, ReplaceOrderOptions, SubmitMultiLegOrderOptions, - SubmitOrderOptions, + EstimateMaxPurchaseQuantityOptions, EstimateMultiLegAvailableQuantityOptions, + GetAllExecutionsOptions, GetCashFlowOptions, GetHistoryExecutionsOptions, + GetHistoryOrdersOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, + ReplaceOrderOptions, SubmitMultiLegOrderOptions, SubmitOrderOptions, }, types::{ AccountBalance, AllExecutionsResponse, CashFlow, EstimateMaxPurchaseQuantityResponse, - Execution, FundPositionsResponse, MarginRatio, Order, OrderDetail, - PushGridOrderChanged, PushOrderChanged, StockPositionsResponse, SubmitOrderResponse, - TopicType, + EstimateMultiLegAvailableQuantityResponse, Execution, FundPositionsResponse, + MarginRatio, Order, OrderDetail, PushGridOrderChanged, PushOrderChanged, + StockPositionsResponse, SubmitOrderResponse, TopicType, }, }, utils::JsCallback, @@ -726,4 +726,23 @@ impl TradeContext { EstimateMaxPurchaseQuantityResponse::try_from(res) }) } + + /// Estimate a multi-leg option combination's maximum tradable quantity and + /// margin impact before submitting the order (US market only). + #[napi] + pub fn estimate_multileg_available_quantity<'env>( + &self, + env: &'env Env, + opts: EstimateMultiLegAvailableQuantityOptions<'env>, + ) -> Result> { + let ctx = self.ctx.clone(); + let opts = longbridge::trade::EstimateMultiLegAvailableQuantityOptions::from(opts); + env.spawn_future(async move { + let res = ctx + .estimate_multileg_available_quantity(opts) + .await + .map_err(ErrorNewType)?; + EstimateMultiLegAvailableQuantityResponse::try_from(res) + }) + } } diff --git a/nodejs/src/trade/requests/estimate_multileg.rs b/nodejs/src/trade/requests/estimate_multileg.rs new file mode 100644 index 000000000..9e60583a1 --- /dev/null +++ b/nodejs/src/trade/requests/estimate_multileg.rs @@ -0,0 +1,60 @@ +use napi::bindgen_prelude::ClassInstance; + +use crate::{ + decimal::Decimal, + trade::types::{MultiLegStrategy, OrderSide, OrderType}, +}; + +/// A leg of a multi-leg combination for the pre-trade estimate +#[napi_derive::napi(object)] +pub struct EstimateMultiLegOrderLeg { + /// Option or underlying-stock symbol, in `ticker.region` format + /// (e.g. `QQQ260731C764000.US`) + pub symbol: String, + /// Leg order side. Reserved field, not in use for now. + pub side: Option, +} + +/// Options for estimating a multi-leg option combination's maximum tradable +/// quantity and margin impact +#[napi_derive::napi(object)] +pub struct EstimateMultiLegAvailableQuantityOptions<'env> { + /// Order side of the combination + pub side: OrderSide, + /// Order type + pub order_type: OrderType, + /// Submitted quantity (number of combinations) + pub submitted_quantity: ClassInstance<'env, Decimal>, + /// Multi-leg strategy + pub strategy: MultiLegStrategy, + /// Legs of the combination + pub legs: Vec, + /// Submitted price (required for limit order types such as `LO`) + pub submitted_price: Option>, +} + +impl<'env> From> + for longbridge::trade::EstimateMultiLegAvailableQuantityOptions +{ + #[inline] + fn from(opts: EstimateMultiLegAvailableQuantityOptions<'env>) -> Self { + let legs = opts.legs.into_iter().map(|leg| { + let mut l = longbridge::trade::EstimateMultiLegOrderLeg::new(leg.symbol); + if let Some(side) = leg.side { + l = l.side(side.into()); + } + l + }); + let mut opts2 = longbridge::trade::EstimateMultiLegAvailableQuantityOptions::new( + opts.side.into(), + opts.order_type.into(), + opts.submitted_quantity.0, + opts.strategy.into(), + legs, + ); + if let Some(submitted_price) = opts.submitted_price { + opts2 = opts2.submitted_price(submitted_price.0); + } + opts2 + } +} diff --git a/nodejs/src/trade/requests/mod.rs b/nodejs/src/trade/requests/mod.rs index e31531851..9bc076da0 100644 --- a/nodejs/src/trade/requests/mod.rs +++ b/nodejs/src/trade/requests/mod.rs @@ -1,4 +1,5 @@ mod estimate_max_purchase_quantity; +mod estimate_multileg; mod get_all_executions; mod get_cash_flow; mod get_history_executions; @@ -10,6 +11,7 @@ mod submit_multileg; mod submit_order; pub use estimate_max_purchase_quantity::EstimateMaxPurchaseQuantityOptions; +pub use estimate_multileg::EstimateMultiLegAvailableQuantityOptions; pub use get_all_executions::GetAllExecutionsOptions; pub use get_cash_flow::GetCashFlowOptions; pub use get_history_executions::GetHistoryExecutionsOptions; diff --git a/nodejs/src/trade/types.rs b/nodejs/src/trade/types.rs index 87739a385..e47b93a27 100644 --- a/nodejs/src/trade/types.rs +++ b/nodejs/src/trade/types.rs @@ -986,6 +986,22 @@ pub struct EstimateMaxPurchaseQuantityResponse { margin_max_qty: Decimal, } +/// Response for estimating a multi-leg option combination's tradable quantity +/// and margin impact +#[napi_derive::napi] +#[derive(Debug, JsObject, Clone)] +#[js(remote = "longbridge::trade::EstimateMultiLegAvailableQuantityResponse")] +pub struct EstimateMultiLegAvailableQuantityResponse { + /// Maximum open quantity of the combination + max_open_qty: Decimal, + /// Margin required per combination unit + unit_margin: Decimal, + /// Change of the initial margin after placing the order + initial_margin_change: Decimal, + /// Change of the maintenance margin after placing the order + maintenance_margin_change: Decimal, +} + // ── US-market types ────────────────────────────────────────────────────────── /// One cash currency entry in USAssetOverview diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index aa4b7708a..59f36aa84 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -7395,6 +7395,32 @@ class EstimateMaxPurchaseQuantityResponse: Margin available quantity """ +class EstimateMultiLegAvailableQuantityResponse: + """ + Response for estimating a multi-leg option combination's tradable quantity + and margin impact + """ + + max_open_qty: Decimal + """ + Maximum open quantity of the combination + """ + + unit_margin: Decimal + """ + Margin required per combination unit + """ + + initial_margin_change: Decimal + """ + Change of the initial margin after placing the order + """ + + maintenance_margin_change: Decimal + """ + Change of the maintenance margin after placing the order + """ + class TradeContext: """ Trade context @@ -8050,6 +8076,31 @@ class TradeContext: print(resp) """ + def estimate_multileg_available_quantity( + self, + side: Type[OrderSide], + order_type: Type[OrderType], + submitted_quantity: Decimal, + strategy: Type[MultiLegStrategy], + legs: list[str], + submitted_price: Optional[Decimal] = None, + ) -> EstimateMultiLegAvailableQuantityResponse: + """ + Estimate a multi-leg option combination's maximum tradable quantity and + margin impact before submitting the order (US market only). + + Args: + side: Order side of the combination + order_type: Order type + submitted_quantity: Submitted quantity (number of combinations) + strategy: Multi-leg strategy + legs: Combination leg symbols (``ticker.region`` format) + submitted_price: Estimated order price, required for limit order types such as ``LO`` + + Returns: + Response + """ + def us_query_orders( self, symbol: Optional[str] = None, @@ -11978,6 +12029,30 @@ class AsyncTradeContext: """ ... + def estimate_multileg_available_quantity( + self, + side: Type[OrderSide], + order_type: Type[OrderType], + submitted_quantity: Decimal, + strategy: Type[MultiLegStrategy], + legs: list[str], + submitted_price: Optional[Decimal] = None, + ) -> Awaitable[EstimateMultiLegAvailableQuantityResponse]: + """ + Estimate a multi-leg option combination's maximum tradable quantity and + margin impact before submitting the order (US market only). Returns an + awaitable that resolves to the estimate response. + + Args: + side: Order side of the combination + order_type: Order type + submitted_quantity: Submitted quantity (number of combinations) + strategy: Multi-leg strategy + legs: Combination leg symbols (``ticker.region`` format) + submitted_price: Estimated order price, required for limit order types such as ``LO`` + """ + ... + def us_query_orders( self, symbol: Optional[str] = None, diff --git a/python/src/trade/context.rs b/python/src/trade/context.rs index 5629c42d2..1a9f795c7 100644 --- a/python/src/trade/context.rs +++ b/python/src/trade/context.rs @@ -3,11 +3,13 @@ use std::sync::Arc; use longbridge::{ blocking::TradeContextSync, trade::{ - CancelOrderOptions, EstimateMaxPurchaseQuantityOptions, GetAllExecutionsOptions, - GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, - GetHistoryOrdersOptions, GetOrderDetailOptions, GetStockPositionsOptions, - GetTodayExecutionsOptions, GetTodayOrdersOptions, ReplaceOrderOptions, - SubmitMultiLegOrderLeg, SubmitMultiLegOrderOptions, SubmitOrderOptions, + CancelOrderOptions, EstimateMaxPurchaseQuantityOptions, + EstimateMultiLegAvailableQuantityOptions, EstimateMultiLegOrderLeg, + GetAllExecutionsOptions, GetCashFlowOptions, GetFundPositionsOptions, + GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetOrderDetailOptions, + GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, + ReplaceOrderOptions, SubmitMultiLegOrderLeg, SubmitMultiLegOrderOptions, + SubmitOrderOptions, }, }; use parking_lot::Mutex; @@ -22,10 +24,11 @@ use crate::{ push::handle_push_event, types::{ AccountBalance, AllExecutionsResponse, BalanceType, CashFlow, - EstimateMaxPurchaseQuantityResponse, Execution, FundPositionsResponse, MarginRatio, - MultiLegStrategy, Order, OrderDetail, OrderSide, OrderStatus, OrderType, OutsideRTH, - ReplaceAttachedParams, StockPositionsResponse, SubmitAttachedParams, - SubmitOrderResponse, TimeInForceType, TopicType, + EstimateMaxPurchaseQuantityResponse, EstimateMultiLegAvailableQuantityResponse, + Execution, FundPositionsResponse, MarginRatio, MultiLegStrategy, Order, OrderDetail, + OrderSide, OrderStatus, OrderType, OutsideRTH, ReplaceAttachedParams, + StockPositionsResponse, SubmitAttachedParams, SubmitOrderResponse, TimeInForceType, + TopicType, }, }, types::Market, @@ -634,4 +637,36 @@ impl TradeContext { .map_err(ErrorNewType)? .try_into() } + + /// Estimate a multi-leg option combination's maximum tradable quantity and + /// margin impact before submitting the order (US market only). + #[pyo3(signature = (side, order_type, submitted_quantity, strategy, legs, submitted_price = None))] + fn estimate_multileg_available_quantity( + &self, + side: OrderSide, + order_type: OrderType, + submitted_quantity: PyDecimal, + strategy: MultiLegStrategy, + legs: Vec, + submitted_price: Option, + ) -> PyResult { + let legs = legs + .into_iter() + .map(EstimateMultiLegOrderLeg::new) + .collect::>(); + let mut opts = EstimateMultiLegAvailableQuantityOptions::new( + side.into(), + order_type.into(), + submitted_quantity.into(), + strategy.into(), + legs, + ); + if let Some(submitted_price) = submitted_price { + opts = opts.submitted_price(submitted_price.into()); + } + self.ctx + .estimate_multileg_available_quantity(opts) + .map_err(ErrorNewType)? + .try_into() + } } diff --git a/python/src/trade/context_async.rs b/python/src/trade/context_async.rs index 962aa8794..b47d675c9 100644 --- a/python/src/trade/context_async.rs +++ b/python/src/trade/context_async.rs @@ -3,7 +3,8 @@ use std::sync::Arc; use longbridge::trade::{ - CancelOrderOptions, EstimateMaxPurchaseQuantityOptions, GetAllExecutionsOptions, + CancelOrderOptions, EstimateMaxPurchaseQuantityOptions, + EstimateMultiLegAvailableQuantityOptions, EstimateMultiLegOrderLeg, GetAllExecutionsOptions, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetOrderDetailOptions, GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, ReplaceOrderOptions, SubmitMultiLegOrderLeg, @@ -22,10 +23,11 @@ use crate::{ push::handle_push_event, types::{ AccountBalance, AllExecutionsResponse, BalanceType, CashFlow, - EstimateMaxPurchaseQuantityResponse, Execution, FundPositionsResponse, MarginRatio, - MultiLegStrategy, Order, OrderDetail, OrderSide, OrderStatus, OrderType, OutsideRTH, - ReplaceAttachedParams, StockPositionsResponse, SubmitAttachedParams, - SubmitOrderResponse, TimeInForceType, TopicType, + EstimateMaxPurchaseQuantityResponse, EstimateMultiLegAvailableQuantityResponse, + Execution, FundPositionsResponse, MarginRatio, MultiLegStrategy, Order, OrderDetail, + OrderSide, OrderStatus, OrderType, OutsideRTH, ReplaceAttachedParams, + StockPositionsResponse, SubmitAttachedParams, SubmitOrderResponse, TimeInForceType, + TopicType, }, }, types::Market, @@ -755,4 +757,43 @@ impl AsyncTradeContext { }) .map(|b| b.unbind()) } + + /// Estimate a multi-leg option combination's maximum tradable quantity and + /// margin impact before submitting the order (US market only). + #[pyo3(signature = (side, order_type, submitted_quantity, strategy, legs, submitted_price = None))] + fn estimate_multileg_available_quantity( + &self, + py: Python<'_>, + side: OrderSide, + order_type: OrderType, + submitted_quantity: PyDecimal, + strategy: MultiLegStrategy, + legs: Vec, + submitted_price: Option, + ) -> PyResult> { + let ctx = self.ctx.clone(); + let legs = legs + .into_iter() + .map(EstimateMultiLegOrderLeg::new) + .collect::>(); + let mut opts = EstimateMultiLegAvailableQuantityOptions::new( + side.into(), + order_type.into(), + submitted_quantity.into(), + strategy.into(), + legs, + ); + if let Some(submitted_price) = submitted_price { + opts = opts.submitted_price(submitted_price.into()); + } + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let r: EstimateMultiLegAvailableQuantityResponse = ctx + .estimate_multileg_available_quantity(opts) + .await + .map_err(ErrorNewType)? + .try_into()?; + Ok(r) + }) + .map(|b| b.unbind()) + } } diff --git a/python/src/trade/mod.rs b/python/src/trade/mod.rs index 6cbe3570c..99331fc3d 100644 --- a/python/src/trade/mod.rs +++ b/python/src/trade/mod.rs @@ -38,6 +38,7 @@ pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; diff --git a/python/src/trade/types.rs b/python/src/trade/types.rs index 524db71a3..98cb1880a 100644 --- a/python/src/trade/types.rs +++ b/python/src/trade/types.rs @@ -1105,6 +1105,22 @@ pub(crate) struct EstimateMaxPurchaseQuantityResponse { pub margin_max_qty: PyDecimal, } +/// Response for estimating a multi-leg option combination's tradable quantity +/// and margin impact +#[pyclass(skip_from_py_object)] +#[derive(Debug, PyObject, Clone)] +#[py(remote = "longbridge::trade::EstimateMultiLegAvailableQuantityResponse")] +pub(crate) struct EstimateMultiLegAvailableQuantityResponse { + /// Maximum open quantity of the combination + pub max_open_qty: PyDecimal, + /// Margin required per combination unit + pub unit_margin: PyDecimal, + /// Change of the initial margin after placing the order + pub initial_margin_change: PyDecimal, + /// Change of the maintenance margin after placing the order + pub maintenance_margin_change: PyDecimal, +} + // ── US-market types ────────────────────────────────────────────────────────── /// One cash currency entry in USAssetOverview diff --git a/rust/src/blocking/trade.rs b/rust/src/blocking/trade.rs index e19498d6e..0c66b7511 100644 --- a/rust/src/blocking/trade.rs +++ b/rust/src/blocking/trade.rs @@ -5,8 +5,9 @@ use crate::{ blocking::runtime::BlockingRuntime, trade::{ AccountBalance, AllExecutionsResponse, CancelOrderOptions, CashFlow, - EstimateMaxPurchaseQuantityOptions, EstimateMaxPurchaseQuantityResponse, Execution, - FundPositionsResponse, GetAllExecutionsOptions, GetCashFlowOptions, + EstimateMaxPurchaseQuantityOptions, EstimateMaxPurchaseQuantityResponse, + EstimateMultiLegAvailableQuantityOptions, EstimateMultiLegAvailableQuantityResponse, + Execution, FundPositionsResponse, GetAllExecutionsOptions, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetOrderDetailOptions, GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, GetUSHistoryOrders, GetUSRealizedPLOptions, MarginRatio, Order, @@ -483,6 +484,16 @@ impl TradeContextSync { .call(move |ctx| async move { ctx.estimate_max_purchase_quantity(opts).await }) } + /// Estimate a multi-leg option combination's maximum tradable quantity and + /// margin impact (blocking). + pub fn estimate_multileg_available_quantity( + &self, + opts: EstimateMultiLegAvailableQuantityOptions, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.estimate_multileg_available_quantity(opts).await }) + } + // ── US-market blocking wrappers // ─────────────────────────────────────────── diff --git a/rust/src/trade/context.rs b/rust/src/trade/context.rs index aa4402bfd..6406adb2a 100644 --- a/rust/src/trade/context.rs +++ b/rust/src/trade/context.rs @@ -11,14 +11,14 @@ use crate::{ Config, Result, serde_utils, trade::{ AccountBalance, AllExecutionsResponse, CancelOrderOptions, CashFlow, - EstimateMaxPurchaseQuantityOptions, Execution, FundPositionsResponse, - GetAllExecutionsOptions, GetCashFlowOptions, GetFundPositionsOptions, - GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetOrderDetailOptions, - GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, - GetUSHistoryOrders, GetUSRealizedPLOptions, MarginRatio, Order, OrderDetail, OrderSide, - PushEvent, QueryUSOrdersResponse, ReplaceOrderOptions, StockPositionsResponse, - SubmitMultiLegOrderOptions, SubmitOrderOptions, TopicType, USAssetOverview, - USOrderDetailResponse, USRealizedPL, + EstimateMaxPurchaseQuantityOptions, EstimateMultiLegAvailableQuantityOptions, Execution, + FundPositionsResponse, GetAllExecutionsOptions, GetCashFlowOptions, + GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, + GetOrderDetailOptions, GetStockPositionsOptions, GetTodayExecutionsOptions, + GetTodayOrdersOptions, GetUSHistoryOrders, GetUSRealizedPLOptions, MarginRatio, Order, + OrderDetail, OrderSide, PushEvent, QueryUSOrdersResponse, ReplaceOrderOptions, + StockPositionsResponse, SubmitMultiLegOrderOptions, SubmitOrderOptions, TopicType, + USAssetOverview, USOrderDetailResponse, USRealizedPL, core::{Command, Core}, }, }; @@ -44,6 +44,26 @@ pub struct EstimateMaxPurchaseQuantityResponse { pub margin_max_qty: Decimal, } +/// Response for estimating a multi-leg option combination's tradable quantity +/// and margin impact. +#[derive(Debug, Serialize, Deserialize)] +pub struct EstimateMultiLegAvailableQuantityResponse { + /// Maximum open quantity of the combination + #[serde(with = "serde_utils::decimal_empty_is_0")] + pub max_open_qty: Decimal, + /// Margin required per combination unit + #[serde(with = "serde_utils::decimal_empty_is_0")] + pub unit_margin: Decimal, + /// Change of the initial margin after placing the order (after minus + /// before) + #[serde(with = "serde_utils::decimal_empty_is_0")] + pub initial_margin_change: Decimal, + /// Change of the maintenance margin after placing the order (after minus + /// before) + #[serde(with = "serde_utils::decimal_empty_is_0")] + pub maintenance_margin_change: Decimal, +} + struct InnerTradeContext { command_tx: mpsc::UnboundedSender, http_cli: HttpClient, @@ -942,6 +962,28 @@ impl TradeContext { .0) } + /// Estimate a multi-leg option combination's maximum tradable quantity and + /// margin impact before submitting the order. + /// + /// Only the US market is supported; all legs must share the same underlying + /// and settlement currency, each leg must be an option or the underlying + /// stock, and duplicate legs (same symbol and side) are rejected. + pub async fn estimate_multileg_available_quantity( + &self, + opts: EstimateMultiLegAvailableQuantityOptions, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::POST, "/v1/trade/estimate/multileg") + .body(Json(opts)) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + // ── US-market APIs // ──────────────────────────────────────────────────────── diff --git a/rust/src/trade/mod.rs b/rust/src/trade/mod.rs index 31ecdedcd..1f80757a4 100644 --- a/rust/src/trade/mod.rs +++ b/rust/src/trade/mod.rs @@ -7,10 +7,14 @@ mod push_types; mod requests; mod types; -pub use context::{EstimateMaxPurchaseQuantityResponse, SubmitOrderResponse, TradeContext}; +pub use context::{ + EstimateMaxPurchaseQuantityResponse, EstimateMultiLegAvailableQuantityResponse, + SubmitOrderResponse, TradeContext, +}; pub use push_types::{PushEvent, PushGridOrderChanged, PushOrderChanged, TopicType}; pub use requests::{ - CancelOrderOptions, EstimateMaxPurchaseQuantityOptions, GetAllExecutionsOptions, + CancelOrderOptions, EstimateMaxPurchaseQuantityOptions, + EstimateMultiLegAvailableQuantityOptions, EstimateMultiLegOrderLeg, GetAllExecutionsOptions, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetOrderDetailOptions, GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, ReplaceAttachedParams, ReplaceOrderOptions, diff --git a/rust/src/trade/requests/estimate_multileg.rs b/rust/src/trade/requests/estimate_multileg.rs new file mode 100644 index 000000000..9bf36bb24 --- /dev/null +++ b/rust/src/trade/requests/estimate_multileg.rs @@ -0,0 +1,90 @@ +use rust_decimal::Decimal; +use serde::Serialize; + +use crate::trade::{MultiLegStrategy, OrderSide, OrderType}; + +/// A leg of a multi-leg combination for the pre-trade estimate. +#[derive(Debug, Serialize, Clone)] +pub struct EstimateMultiLegOrderLeg { + /// Option or underlying-stock symbol, in `ticker.region` format + /// (e.g. `QQQ260731C764000.US`). + symbol: String, + /// Leg order side. Reserved field, not in use for now; the direction of + /// each leg is implied by `strategy` together with the order `side`. + #[serde(skip_serializing_if = "Option::is_none")] + side: Option, +} + +impl EstimateMultiLegOrderLeg { + /// Create a new `EstimateMultiLegOrderLeg`. + #[inline] + pub fn new(symbol: impl Into) -> Self { + Self { + symbol: symbol.into(), + side: None, + } + } + + /// Set the leg order side (reserved; not in use for now). + #[inline] + #[must_use] + pub fn side(self, side: OrderSide) -> Self { + Self { + side: Some(side), + ..self + } + } +} + +/// Options for estimating a multi-leg option combination's maximum tradable +/// quantity and margin impact +/// ([`TradeContext::estimate_multileg_available_quantity`]). +/// +/// Only the US market is supported; all legs must share the same underlying and +/// settlement currency, each leg must be an option or the underlying stock, and +/// duplicate legs (same symbol and side) are rejected. +/// +/// [`TradeContext::estimate_multileg_available_quantity`]: crate::trade::TradeContext::estimate_multileg_available_quantity +#[derive(Debug, Serialize, Clone)] +pub struct EstimateMultiLegAvailableQuantityOptions { + side: OrderSide, + order_type: OrderType, + submitted_quantity: Decimal, + strategy: MultiLegStrategy, + legs: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + submitted_price: Option, +} + +impl EstimateMultiLegAvailableQuantityOptions { + /// Create a new `EstimateMultiLegAvailableQuantityOptions`. + #[inline] + pub fn new( + side: OrderSide, + order_type: OrderType, + submitted_quantity: Decimal, + strategy: MultiLegStrategy, + legs: impl IntoIterator, + ) -> Self { + Self { + side, + order_type, + submitted_quantity, + strategy, + legs: legs.into_iter().collect(), + submitted_price: None, + } + } + + /// Set the submitted price. + /// + /// Required for limit order types such as `LO`. + #[inline] + #[must_use] + pub fn submitted_price(self, submitted_price: Decimal) -> Self { + Self { + submitted_price: Some(submitted_price), + ..self + } + } +} diff --git a/rust/src/trade/requests/mod.rs b/rust/src/trade/requests/mod.rs index 0563ca4dd..964def96b 100644 --- a/rust/src/trade/requests/mod.rs +++ b/rust/src/trade/requests/mod.rs @@ -1,5 +1,6 @@ mod cancel_order; mod estimate_max_purchase_quantity; +mod estimate_multileg; mod get_all_executions; mod get_cash_flow; mod get_fund_positions; @@ -15,6 +16,7 @@ mod submit_order; pub use cancel_order::CancelOrderOptions; pub use estimate_max_purchase_quantity::EstimateMaxPurchaseQuantityOptions; +pub use estimate_multileg::{EstimateMultiLegAvailableQuantityOptions, EstimateMultiLegOrderLeg}; pub use get_all_executions::GetAllExecutionsOptions; pub use get_cash_flow::GetCashFlowOptions; pub use get_fund_positions::GetFundPositionsOptions;