Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2024"

[dependencies]
# TODO: switch back to crates.io after the next openapi release
longbridge = { git = "https://github.com/longbridge/openapi.git", branch = "main" }
longbridge = { git = "https://github.com/longbridge/openapi.git", branch = "feat/attached-orders" }
rust_decimal = "1"
rmcp = { version = "1.4", features = [
"server",
Expand Down
14 changes: 7 additions & 7 deletions src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,8 +495,8 @@ use crate::tools::quote::{
WarrantListParam,
};
use crate::tools::trade::{
CashFlowParam, EstimateMaxQtyParam, HistoryOrdersParam, OrderIdParam, ReplaceOrderParam,
SubmitOrderParam,
CashFlowParam, EstimateMaxQtyParam, HistoryOrdersParam, OrderDetailParam, OrderIdParam,
ReplaceOrderParam, SubmitOrderParam,
};

#[tool_router(vis = "pub(crate)")]
Expand Down Expand Up @@ -1196,7 +1196,7 @@ impl Longbridge {
idempotent_hint = true,
open_world_hint = true
),
description = "Get orders placed today. Returns orders[]{order_id, symbol, side, order_type, status, quantity, price, submitted_at, executed_quantity, executed_price}. Pass symbol to filter."
description = "Get orders placed today. Returns orders[]{order_id, symbol, side, order_type, status, quantity, price, submitted_at, executed_quantity, executed_price, attached_orders[]}. Pass symbol to filter by symbol, or order_id to filter by a specific order. To query a take-profit/stop-loss sub-order by its own order_id, set both order_id and is_attached=true; the response returns that sub-order itself as an Order entry. is_attached has no effect without order_id."
)]
async fn today_orders(
&self,
Expand All @@ -1212,12 +1212,12 @@ impl Longbridge {
title = "Order Detail",
annotations(read_only_hint = true, destructive_hint = false, idempotent_hint = true, open_world_hint = true),
output_schema = schema_for::<output::OrderDetailResponse>(),
description = "Get detailed information about a specific order. Returns {order_id, symbol, status, side, order_type, quantity, price, executed_quantity, executed_price, submitted_at, time_in_force, msg}."
description = "Get detailed information about a specific order. For a parent order, returns full detail including attached_orders[] (take-profit/stop-loss legs). To query a sub-order by its own order_id instead, set is_attached=true; the response returns the sub-order itself, with charge_detail=null."
)]
async fn order_detail(
&self,
ctx: RequestContext<RoleServer>,
Parameters(p): Parameters<OrderIdParam>,
Parameters(p): Parameters<OrderDetailParam>,
) -> Result<CallToolResult, McpError> {
let mctx = extract_context(&ctx)?;
measured_tool_call("order_detail", || trade::order_detail(&mctx, p)).await
Expand Down Expand Up @@ -1333,7 +1333,7 @@ impl Longbridge {
open_world_hint = true
),
output_schema = schema_for::<output::OrderIdResponse>(),
description = "Submit a buy/sell order. order_type: LO (Limit) / ELO (Enhanced Limit, HK) / MO (Market) / AO (At-auction, HK) / ALO (At-auction Limit, HK) / ODD (Odd Lots, HK) / LIT (Limit If Touched) / MIT (Market If Touched) / TSLPAMT (Trailing Limit by Amount) / TSLPPCT (Trailing Limit by Percent) / SLO (Special Limit, HK). side: Buy/Sell. time_in_force: Day/GTC/GTD"
description = "Submit a buy/sell order. order_type: LO (Limit) / ELO (Enhanced Limit, HK) / MO (Market) / AO (At-auction, HK) / ALO (At-auction Limit, HK) / ODD (Odd Lots, HK) / LIT (Limit If Touched) / MIT (Market If Touched) / TSLPAMT (Trailing Limit by Amount) / TSLPPCT (Trailing Limit by Percent) / SLO (Special Limit, HK). side: Buy/Sell. time_in_force: Day/GTC/GTD. To attach a take-profit/stop-loss: set attached_order_type (ProfitTaker/StopLoss/Bracket) with profit_taker_price and/or stop_loss_price."
)]
async fn submit_order(
&self,
Expand All @@ -1353,7 +1353,7 @@ impl Longbridge {
idempotent_hint = true,
open_world_hint = true
),
description = "Modify an open order's quantity, price, trigger_price, or trailing params. Returns \"order replaced\" on success. Only open/pending orders can be modified."
description = "Modify an open order's quantity, price, trigger_price, or trailing params. Returns \"order replaced\" on success. Only open/pending orders can be modified. To modify attached take-profit/stop-loss: provide attached_order_type with updated prices. To cancel all attached orders: set cancel_all_attached=true. To update a specific attached leg by ID: use profit_taker_id or stop_loss_id."
)]
async fn replace_order(
&self,
Expand Down
56 changes: 56 additions & 0 deletions src/tools/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,60 @@ pub struct BrokerLevel {
pub broker_ids: Vec<i32>,
}

/// One take-profit or stop-loss attached order leg, returned inside `OrderDetailResponse.attached_orders`.
#[derive(Debug, Serialize, JsonSchema)]
pub struct AttachedOrderDetailResponse {
/// Attached order ID.
pub order_id: String,
/// Attached order type: `ProfitTaker`, `StopLoss`, or `Bracket`.
pub attached_type_display: String,
/// Order status.
pub status: String,
/// Trigger price (null if not set).
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_price: Option<String>,
/// Submitted limit price (null for market-style legs).
#[serde(skip_serializing_if = "Option::is_none")]
pub submit_price: Option<String>,
/// Submitted quantity.
pub quantity: String,
/// Quantity already executed.
pub executed_qty: String,
/// Volume-weighted average executed price (null when unfilled).
#[serde(skip_serializing_if = "Option::is_none")]
pub executed_price: Option<String>,
/// Order type for the triggered leg, e.g. `LO`, `MO`.
pub activate_order_type: String,
/// Time-in-force: `Day` / `GTC` / `GTD`.
pub time_in_force: String,
/// GTD expiry date (yyyy-mm-dd, null when not GTD).
#[serde(skip_serializing_if = "Option::is_none")]
pub gtd: Option<String>,
/// Trigger status, e.g. `Deactive` / `Active` / `Released`.
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_status: Option<String>,
/// Order submission time (RFC3339).
pub submitted_at: String,
/// Last update time (RFC3339).
pub updated_at: String,
/// Whether this leg has been withdrawn.
pub withdrawn: bool,
/// Whether this leg has been reviewed.
pub reviewed: bool,
/// RTH setting for the triggered leg.
#[serde(skip_serializing_if = "Option::is_none")]
pub activate_rth: Option<String>,
/// RTH enforcement flag for the attached order itself.
#[serde(skip_serializing_if = "Option::is_none")]
pub force_only_rth: Option<String>,
/// Order tag (e.g. `Normal`, `LongTerm`).
pub tag: String,
/// Total executed amount (quantity × price).
pub executed_amount: String,
/// Internal counter ID.
pub counter_id: String,
}

/// Returned by `order_detail`. Single order with full lifecycle metadata.
#[derive(Debug, Serialize, JsonSchema)]
pub struct OrderDetailResponse {
Expand Down Expand Up @@ -292,4 +346,6 @@ pub struct OrderDetailResponse {
/// Outside-RTH setting: `RTH_ONLY` / `ANY_TIME` / `OVERNIGHT`.
#[serde(skip_serializing_if = "Option::is_none")]
pub outside_rth: Option<String>,
/// Attached take-profit / stop-loss legs. Empty when none.
pub attached_orders: Vec<AttachedOrderDetailResponse>,
}
Loading
Loading