Expose open order runtime fields
This commit is contained in:
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user