Expose open order runtime fields

This commit is contained in:
boris
2026-04-23 19:47:56 -07:00
parent f4030f2607
commit c12a883d28
6 changed files with 132 additions and 5 deletions

View File

@@ -208,6 +208,10 @@ impl<C, R> BrokerSimulator<C, R> {
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(),
})

View File

@@ -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")]

View File

@@ -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(),
},

View File

@@ -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))
}

View File

@@ -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() },