From c12a883d284d9072bde543d6a8dac0e75b069ea5 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 23 Apr 2026 19:47:56 -0700 Subject: [PATCH] Expose open order runtime fields --- crates/fidc-core/src/broker.rs | 4 ++ crates/fidc-core/src/events.rs | 12 ++++ .../fidc-core/src/platform_expr_strategy.rs | 65 ++++++++++++++++++- crates/fidc-core/src/strategy.rs | 40 +++++++++++- crates/fidc-core/src/strategy_ai.rs | 2 + docs/rqalpha-gap-roadmap.md | 14 +++- 6 files changed, 132 insertions(+), 5 deletions(-) diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index ab963ea..cc9f7dc 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -208,6 +208,10 @@ impl BrokerSimulator { requested_quantity: order.requested_quantity, filled_quantity: order.filled_quantity, remaining_quantity: order.remaining_quantity, + unfilled_quantity: order.remaining_quantity, + status: OrderStatus::Pending, + avg_price: 0.0, + transaction_cost: 0.0, limit_price: order.limit_price, reason: order.reason.clone(), }) diff --git a/crates/fidc-core/src/events.rs b/crates/fidc-core/src/events.rs index 19c096d..54352fe 100644 --- a/crates/fidc-core/src/events.rs +++ b/crates/fidc-core/src/events.rs @@ -47,6 +47,18 @@ pub enum OrderStatus { Rejected, } +impl OrderStatus { + pub fn as_str(&self) -> &'static str { + match self { + Self::Pending => "pending", + Self::Filled => "filled", + Self::PartiallyFilled => "partially_filled", + Self::Canceled => "canceled", + Self::Rejected => "rejected", + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OrderEvent { #[serde(with = "date_format")] diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index bab0571..116a6b1 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -470,6 +470,15 @@ impl PlatformExprStrategy { "weekday", "is_month_start", "is_month_end", + "has_open_orders", + "open_order_count", + "open_buy_order_count", + "open_sell_order_count", + "open_buy_qty", + "open_sell_qty", + "latest_open_order_id", + "latest_open_order_status", + "latest_open_order_unfilled_qty", "has_process_events", "process_event_count", "current_process_kind", @@ -515,6 +524,12 @@ impl PlatformExprStrategy { "hit_upper_limit", "hit_lower_limit", "listed_days", + "symbol_open_order_count", + "symbol_open_buy_qty", + "symbol_open_sell_qty", + "latest_symbol_open_order_id", + "latest_symbol_open_order_status", + "latest_symbol_open_order_unfilled_qty", "stock_ma_short", "stock_ma_mid", "stock_ma_long", @@ -1240,6 +1255,14 @@ impl PlatformExprStrategy { scope.push("open_buy_qty", ctx.open_buy_quantity() as i64); scope.push("open_sell_qty", ctx.open_sell_quantity() as i64); scope.push("latest_open_order_id", ctx.latest_open_order_id() as i64); + scope.push( + "latest_open_order_status", + ctx.latest_open_order_status().to_string(), + ); + scope.push( + "latest_open_order_unfilled_qty", + ctx.latest_open_order_unfilled_quantity() as i64, + ); scope.push("has_dynamic_universe", ctx.has_dynamic_universe()); scope.push( "dynamic_universe_count", @@ -1366,6 +1389,14 @@ impl PlatformExprStrategy { "latest_open_order_id".into(), Dynamic::from(ctx.latest_open_order_id() as i64), ); + day_factors.insert( + "latest_open_order_status".into(), + Dynamic::from(ctx.latest_open_order_status().to_string()), + ); + day_factors.insert( + "latest_open_order_unfilled_qty".into(), + Dynamic::from(ctx.latest_open_order_unfilled_quantity() as i64), + ); day_factors.insert( "has_dynamic_universe".into(), Dynamic::from(ctx.has_dynamic_universe()), @@ -1495,6 +1526,15 @@ impl PlatformExprStrategy { "latest_symbol_open_order_id", ctx.latest_symbol_open_order_id(&stock.symbol) as i64, ); + scope.push( + "latest_symbol_open_order_status", + ctx.latest_symbol_open_order_status(&stock.symbol) + .to_string(), + ); + scope.push( + "latest_symbol_open_order_unfilled_qty", + ctx.latest_symbol_open_order_unfilled_quantity(&stock.symbol) as i64, + ); scope.push( "in_dynamic_universe", ctx.dynamic_universe_contains(&stock.symbol), @@ -1591,6 +1631,17 @@ impl PlatformExprStrategy { "latest_symbol_open_order_id".into(), Dynamic::from(ctx.latest_symbol_open_order_id(&stock.symbol) as i64), ); + factors.insert( + "latest_symbol_open_order_status".into(), + Dynamic::from( + ctx.latest_symbol_open_order_status(&stock.symbol) + .to_string(), + ), + ); + factors.insert( + "latest_symbol_open_order_unfilled_qty".into(), + Dynamic::from(ctx.latest_symbol_open_order_unfilled_quantity(&stock.symbol) as i64), + ); factors.insert( "in_dynamic_universe".into(), Dynamic::from(ctx.dynamic_universe_contains(&stock.symbol)), @@ -4781,6 +4832,10 @@ mod tests { requested_quantity: 300, filled_quantity: 100, remaining_quantity: 200, + unfilled_quantity: 200, + status: crate::OrderStatus::Pending, + avg_price: 0.0, + transaction_cost: 0.0, limit_price: 10.2, reason: "pending_limit_sell".to_string(), }]; @@ -4811,7 +4866,7 @@ mod tests { start_time_expr: None, end_time_expr: None, when_expr: Some( - "has_open_orders && open_order_count == 1 && open_sell_qty == 200 && symbol_open_sell_qty == 200 && symbol_open_order_count == 1".to_string(), + "has_open_orders && open_order_count == 1 && open_sell_qty == 200 && symbol_open_sell_qty == 200 && symbol_open_order_count == 1 && latest_open_order_status == \"pending\" && latest_open_order_unfilled_qty == 200 && latest_symbol_open_order_status == \"pending\" && latest_symbol_open_order_unfilled_qty == 200".to_string(), ), reason: "open_order_aware_entry".to_string(), }]; @@ -4897,6 +4952,10 @@ mod tests { requested_quantity: 100, filled_quantity: 0, remaining_quantity: 100, + unfilled_quantity: 100, + status: crate::OrderStatus::Pending, + avg_price: 0.0, + transaction_cost: 0.0, limit_price: 9.9, reason: "pending_limit_buy".to_string(), }, @@ -4907,6 +4966,10 @@ mod tests { requested_quantity: 300, filled_quantity: 100, remaining_quantity: 200, + unfilled_quantity: 200, + status: crate::OrderStatus::Pending, + avg_price: 0.0, + transaction_cost: 0.0, limit_price: 10.2, reason: "pending_limit_sell".to_string(), }, diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index b361125..ab725b3 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -9,7 +9,7 @@ use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime}; use crate::cost::ChinaAShareCostModel; use crate::data::{DailyMarketSnapshot, DataSet, IntradayExecutionQuote, PriceBar, PriceField}; use crate::engine::BacktestError; -use crate::events::{OrderSide, ProcessEvent}; +use crate::events::{OrderSide, OrderStatus, ProcessEvent}; use crate::instrument::Instrument; use crate::portfolio::PortfolioState; use crate::scheduler::ScheduleRule; @@ -72,6 +72,10 @@ pub struct OpenOrderView { pub requested_quantity: u32, pub filled_quantity: u32, pub remaining_quantity: u32, + pub unfilled_quantity: u32, + pub status: OrderStatus, + pub avg_price: f64, + pub transaction_cost: f64, pub limit_price: f64, pub reason: String, } @@ -168,6 +172,22 @@ impl StrategyContext<'_> { .unwrap_or(0) } + pub fn latest_open_order_status(&self) -> &'static str { + self.open_orders + .iter() + .max_by_key(|order| order.order_id) + .map(|order| order.status.as_str()) + .unwrap_or("") + } + + pub fn latest_open_order_unfilled_quantity(&self) -> u32 { + self.open_orders + .iter() + .max_by_key(|order| order.order_id) + .map(|order| order.unfilled_quantity) + .unwrap_or(0) + } + pub fn latest_symbol_open_order_id(&self, symbol: &str) -> u64 { self.open_orders .iter() @@ -177,6 +197,24 @@ impl StrategyContext<'_> { .unwrap_or(0) } + pub fn latest_symbol_open_order_status(&self, symbol: &str) -> &'static str { + self.open_orders + .iter() + .filter(|order| order.symbol == symbol) + .max_by_key(|order| order.order_id) + .map(|order| order.status.as_str()) + .unwrap_or("") + } + + pub fn latest_symbol_open_order_unfilled_quantity(&self, symbol: &str) -> u32 { + self.open_orders + .iter() + .filter(|order| order.symbol == symbol) + .max_by_key(|order| order.order_id) + .map(|order| order.unfilled_quantity) + .unwrap_or(0) + } + pub fn available_sellable_qty(&self, symbol: &str, raw_sellable_qty: u32) -> u32 { raw_sellable_qty.saturating_sub(self.symbol_open_sell_quantity(symbol)) } diff --git a/crates/fidc-core/src/strategy_ai.rs b/crates/fidc-core/src/strategy_ai.rs index f4debe7..9e0226b 100644 --- a/crates/fidc-core/src/strategy_ai.rs +++ b/crates/fidc-core/src/strategy_ai.rs @@ -144,6 +144,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual { ManualField { name: "position_count/max_positions/refresh_rate".to_string(), field_type: "int".to_string(), detail: "仓位计数与调仓周期。".to_string() }, ManualField { name: "has_open_orders/open_order_count/open_buy_order_count/open_sell_order_count".to_string(), field_type: "bool/int".to_string(), detail: "当前阶段挂单簿摘要。".to_string() }, ManualField { name: "open_buy_qty/open_sell_qty/latest_open_order_id".to_string(), field_type: "int".to_string(), detail: "当前阶段未成交买卖挂单的剩余数量汇总,以及最近一笔挂单 id。".to_string() }, + ManualField { name: "latest_open_order_status/latest_open_order_unfilled_qty".to_string(), field_type: "string/int".to_string(), detail: "最近一笔挂单的状态和未成交数量;当前挂单状态为 pending,字段命名对齐 RQAlpha Order 的 status/unfilled_quantity 语义。".to_string() }, ManualField { name: "has_dynamic_universe/dynamic_universe_count".to_string(), field_type: "bool/int".to_string(), detail: "当前策略上下文是否存在动态 universe,以及动态 universe 内证券数量。".to_string() }, ManualField { name: "has_subscriptions/subscription_count".to_string(), field_type: "bool/int".to_string(), detail: "当前订阅集合是否为空,以及订阅证券数量。".to_string() }, ManualField { name: "subscription_guard_required".to_string(), field_type: "bool".to_string(), detail: "当前显式交易是否启用订阅保护;启用后未订阅标的的显式订单会被拒绝生成。".to_string() }, @@ -167,6 +168,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual { ManualField { name: "allow_buy/allow_sell/at_upper_limit/at_lower_limit".to_string(), field_type: "bool".to_string(), detail: "盘中买卖与涨跌停状态。".to_string() }, ManualField { name: "touched_upper_limit/touched_lower_limit/hit_upper_limit/hit_lower_limit".to_string(), field_type: "bool".to_string(), detail: "当日 tick 曾经触达涨跌停。".to_string() }, ManualField { name: "symbol_open_order_count/symbol_open_buy_qty/symbol_open_sell_qty/latest_symbol_open_order_id".to_string(), field_type: "int".to_string(), detail: "当前证券在挂单簿中的未成交挂单摘要和最近挂单 id。".to_string() }, + ManualField { name: "latest_symbol_open_order_status/latest_symbol_open_order_unfilled_qty".to_string(), field_type: "string/int".to_string(), detail: "当前证券最近一笔挂单的状态和未成交数量。".to_string() }, ManualField { name: "in_dynamic_universe/is_subscribed".to_string(), field_type: "bool".to_string(), detail: "当前证券是否在动态 universe 内,以及是否仍在订阅集合中。".to_string() }, ManualField { name: "stock_ma5/stock_ma10/stock_ma20/stock_ma30".to_string(), field_type: "float".to_string(), detail: "个股价格均线内建别名。只内建这几个窗口;15 日、45 日等任意窗口请改用 sma(\"close\", n)。".to_string() }, ManualField { name: "stock_volume_ma5/stock_volume_ma10/stock_volume_ma20/stock_volume_ma60".to_string(), field_type: "float".to_string(), detail: "个股成交量均线内建别名。只内建这几个窗口;任意窗口请改用 rolling_mean(\"volume\", n)。".to_string() }, diff --git a/docs/rqalpha-gap-roadmap.md b/docs/rqalpha-gap-roadmap.md index ea68a0b..f35d802 100644 --- a/docs/rqalpha-gap-roadmap.md +++ b/docs/rqalpha-gap-roadmap.md @@ -64,6 +64,12 @@ current alignment pass. - [x] `active_instruments` - [x] `instruments_history` +### Phase 8: Order object API parity + +- [x] open-order status and unfilled quantity exposed to strategy runtime +- [ ] final order object lookup by order id +- [ ] order average fill price and transaction cost aggregation + ## Execution Order 1. Close the explicit order API gap with target-shares / `order_to` parity. @@ -73,9 +79,11 @@ current alignment pass. 5. Add algo-order styles. 6. Finish position accounting parity. 7. Continue stock data-source API parity. -8. Continue parity audit for remaining account and order object APIs. +8. Continue order object API parity. +9. Continue parity audit for remaining account APIs. ## Current Step -Active implementation target: continue parity audit for remaining account and -order object APIs after the core stock data-source APIs are covered. +Active implementation target: continue order object API parity after exposing +open-order status and unfilled quantity; next gaps are final order lookup and +average fill price / transaction cost aggregation by order id.