Add order runtime lookup
This commit is contained in:
@@ -410,6 +410,8 @@ where
|
||||
execution_date,
|
||||
default_stage_time(ScheduleStage::BeforeTrading),
|
||||
),
|
||||
order_events: result.order_events.as_slice(),
|
||||
fills: result.fills.as_slice(),
|
||||
})?;
|
||||
publish_phase_event(
|
||||
&mut self.strategy,
|
||||
@@ -443,6 +445,8 @@ where
|
||||
&mut process_events,
|
||||
&mut self.process_event_bus,
|
||||
default_stage_time(ScheduleStage::BeforeTrading),
|
||||
result.order_events.as_slice(),
|
||||
result.fills.as_slice(),
|
||||
)?;
|
||||
self.apply_strategy_directives(
|
||||
execution_date,
|
||||
@@ -501,6 +505,8 @@ where
|
||||
&mut process_events,
|
||||
&mut self.process_event_bus,
|
||||
default_stage_time(ScheduleStage::OpenAuction),
|
||||
result.order_events.as_slice(),
|
||||
result.fills.as_slice(),
|
||||
)?;
|
||||
auction_decision.merge_from(self.strategy.open_auction(&StrategyContext {
|
||||
execution_date,
|
||||
@@ -517,6 +523,8 @@ where
|
||||
execution_date,
|
||||
default_stage_time(ScheduleStage::OpenAuction),
|
||||
),
|
||||
order_events: result.order_events.as_slice(),
|
||||
fills: result.fills.as_slice(),
|
||||
})?);
|
||||
publish_phase_event(
|
||||
&mut self.strategy,
|
||||
@@ -615,6 +623,8 @@ where
|
||||
execution_date,
|
||||
default_stage_time(ScheduleStage::OnDay),
|
||||
),
|
||||
order_events: result.order_events.as_slice(),
|
||||
fills: result.fills.as_slice(),
|
||||
})
|
||||
})
|
||||
.transpose()?
|
||||
@@ -635,6 +645,8 @@ where
|
||||
&mut process_events,
|
||||
&mut self.process_event_bus,
|
||||
default_stage_time(ScheduleStage::OnDay),
|
||||
result.order_events.as_slice(),
|
||||
result.fills.as_slice(),
|
||||
)?);
|
||||
publish_phase_event(
|
||||
&mut self.strategy,
|
||||
@@ -685,6 +697,8 @@ where
|
||||
&mut process_events,
|
||||
&mut self.process_event_bus,
|
||||
default_stage_time(ScheduleStage::Bar),
|
||||
result.order_events.as_slice(),
|
||||
result.fills.as_slice(),
|
||||
)?);
|
||||
decision.merge_from(self.strategy.on_bar(&StrategyContext {
|
||||
execution_date,
|
||||
@@ -701,6 +715,8 @@ where
|
||||
execution_date,
|
||||
default_stage_time(ScheduleStage::Bar),
|
||||
),
|
||||
order_events: result.order_events.as_slice(),
|
||||
fills: result.fills.as_slice(),
|
||||
})?);
|
||||
publish_phase_event(
|
||||
&mut self.strategy,
|
||||
@@ -831,6 +847,8 @@ where
|
||||
&mut process_events,
|
||||
&mut self.process_event_bus,
|
||||
Some(tick_time),
|
||||
result.order_events.as_slice(),
|
||||
result.fills.as_slice(),
|
||||
)?;
|
||||
tick_decision.merge_from(self.strategy.on_tick(
|
||||
&StrategyContext {
|
||||
@@ -845,6 +863,8 @@ where
|
||||
process_events: &process_events,
|
||||
active_process_event: None,
|
||||
active_datetime: Some(quote.timestamp),
|
||||
order_events: result.order_events.as_slice(),
|
||||
fills: result.fills.as_slice(),
|
||||
},
|
||||
"e,
|
||||
)?);
|
||||
@@ -919,6 +939,18 @@ where
|
||||
portfolio.update_prices(execution_date, &self.data, PriceField::Close)?;
|
||||
|
||||
let post_trade_open_orders = self.broker.open_order_views();
|
||||
let visible_order_events = result
|
||||
.order_events
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(report.order_events.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
let visible_fills = result
|
||||
.fills
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(report.fill_events.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
publish_phase_event(
|
||||
&mut self.strategy,
|
||||
&mut self.process_event_bus,
|
||||
@@ -950,6 +982,8 @@ where
|
||||
execution_date,
|
||||
default_stage_time(ScheduleStage::AfterTrading),
|
||||
),
|
||||
order_events: visible_order_events.as_slice(),
|
||||
fills: visible_fills.as_slice(),
|
||||
})?;
|
||||
publish_phase_event(
|
||||
&mut self.strategy,
|
||||
@@ -983,6 +1017,8 @@ where
|
||||
&mut process_events,
|
||||
&mut self.process_event_bus,
|
||||
default_stage_time(ScheduleStage::AfterTrading),
|
||||
visible_order_events.as_slice(),
|
||||
visible_fills.as_slice(),
|
||||
)?;
|
||||
self.apply_strategy_directives(
|
||||
execution_date,
|
||||
@@ -1014,6 +1050,18 @@ where
|
||||
report.account_events.extend(close_report.account_events);
|
||||
report.diagnostics.extend(close_report.diagnostics);
|
||||
let post_close_open_orders = self.broker.open_order_views();
|
||||
let visible_order_events_after_close = result
|
||||
.order_events
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(report.order_events.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
let visible_fills_after_close = result
|
||||
.fills
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(report.fill_events.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
publish_phase_event(
|
||||
&mut self.strategy,
|
||||
&mut self.process_event_bus,
|
||||
@@ -1061,6 +1109,8 @@ where
|
||||
execution_date,
|
||||
default_stage_time(ScheduleStage::Settlement),
|
||||
),
|
||||
order_events: visible_order_events_after_close.as_slice(),
|
||||
fills: visible_fills_after_close.as_slice(),
|
||||
})?;
|
||||
publish_phase_event(
|
||||
&mut self.strategy,
|
||||
@@ -1094,6 +1144,8 @@ where
|
||||
&mut process_events,
|
||||
&mut self.process_event_bus,
|
||||
default_stage_time(ScheduleStage::Settlement),
|
||||
visible_order_events_after_close.as_slice(),
|
||||
visible_fills_after_close.as_slice(),
|
||||
)?;
|
||||
self.apply_strategy_directives(
|
||||
execution_date,
|
||||
@@ -1620,6 +1672,8 @@ fn collect_scheduled_decisions<S: Strategy>(
|
||||
process_events: &mut Vec<ProcessEvent>,
|
||||
process_event_bus: &mut ProcessEventBus,
|
||||
current_time: Option<chrono::NaiveTime>,
|
||||
order_events: &[OrderEvent],
|
||||
fills: &[FillEvent],
|
||||
) -> Result<crate::strategy::StrategyDecision, BacktestError> {
|
||||
let mut combined = crate::strategy::StrategyDecision::default();
|
||||
for rule in scheduler.triggered_rules_at(execution_date, stage, current_time, rules) {
|
||||
@@ -1652,6 +1706,8 @@ fn collect_scheduled_decisions<S: Strategy>(
|
||||
process_events: process_events.as_slice(),
|
||||
active_process_event: None,
|
||||
active_datetime: stage_datetime(execution_date, current_time),
|
||||
order_events,
|
||||
fills,
|
||||
},
|
||||
rule,
|
||||
)?);
|
||||
@@ -1713,6 +1769,8 @@ fn publish_phase_event<S: Strategy>(
|
||||
process_events,
|
||||
active_process_event: Some(&event),
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
strategy.on_process_event(&event_ctx, &event)?;
|
||||
events.push(event);
|
||||
@@ -1748,6 +1806,8 @@ fn publish_process_events<S: Strategy>(
|
||||
process_events,
|
||||
active_process_event: Some(&event),
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
strategy.on_process_event(&event_ctx, &event)?;
|
||||
target.push(event);
|
||||
@@ -1783,6 +1843,8 @@ fn publish_custom_process_event<S: Strategy>(
|
||||
process_events,
|
||||
active_process_event: Some(&event),
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
strategy.on_process_event(&event_ctx, &event)?;
|
||||
target.push(event);
|
||||
|
||||
@@ -46,8 +46,8 @@ pub use scheduler::{
|
||||
};
|
||||
pub use strategy::{
|
||||
AlgoOrderStyle, CnSmallCapRotationConfig, CnSmallCapRotationStrategy, JqMicroCapConfig,
|
||||
JqMicroCapStrategy, OpenOrderView, OrderIntent, Strategy, StrategyContext, StrategyDecision,
|
||||
TargetPortfolioOrderPricing,
|
||||
JqMicroCapStrategy, OpenOrderView, OrderIntent, OrderRuntimeView, Strategy, StrategyContext,
|
||||
StrategyDecision, TargetPortfolioOrderPricing,
|
||||
};
|
||||
pub use strategy_ai::{
|
||||
ManualExample, ManualFactorSource, ManualField, ManualFieldGroup, ManualFunction,
|
||||
|
||||
@@ -3912,6 +3912,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SZ".to_string();
|
||||
@@ -4050,6 +4052,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SZ".to_string();
|
||||
@@ -4166,6 +4170,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SZ".to_string();
|
||||
@@ -4287,6 +4293,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SH".to_string();
|
||||
@@ -4391,6 +4399,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SH".to_string();
|
||||
@@ -4490,6 +4500,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SH".to_string();
|
||||
@@ -4607,6 +4619,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SZ".to_string();
|
||||
@@ -4727,6 +4741,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SZ".to_string();
|
||||
@@ -4852,6 +4868,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SZ".to_string();
|
||||
@@ -4987,6 +5005,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SZ".to_string();
|
||||
@@ -5094,6 +5114,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SZ".to_string();
|
||||
@@ -5229,6 +5251,8 @@ mod tests {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SZ".to_string();
|
||||
@@ -5346,6 +5370,8 @@ mod tests {
|
||||
process_events: &process_events,
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
cfg.signal_symbol = "000001.SZ".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, OrderStatus, ProcessEvent};
|
||||
use crate::events::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEvent};
|
||||
use crate::instrument::Instrument;
|
||||
use crate::portfolio::PortfolioState;
|
||||
use crate::scheduler::ScheduleRule;
|
||||
@@ -80,6 +80,21 @@ pub struct OpenOrderView {
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OrderRuntimeView {
|
||||
pub order_id: u64,
|
||||
pub symbol: String,
|
||||
pub side: OrderSide,
|
||||
pub requested_quantity: u32,
|
||||
pub filled_quantity: u32,
|
||||
pub unfilled_quantity: u32,
|
||||
pub status: OrderStatus,
|
||||
pub avg_price: f64,
|
||||
pub transaction_cost: f64,
|
||||
pub limit_price: f64,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
pub struct StrategyContext<'a> {
|
||||
pub execution_date: NaiveDate,
|
||||
pub decision_date: NaiveDate,
|
||||
@@ -92,6 +107,8 @@ pub struct StrategyContext<'a> {
|
||||
pub process_events: &'a [ProcessEvent],
|
||||
pub active_process_event: Option<&'a ProcessEvent>,
|
||||
pub active_datetime: Option<NaiveDateTime>,
|
||||
pub order_events: &'a [OrderEvent],
|
||||
pub fills: &'a [FillEvent],
|
||||
}
|
||||
|
||||
impl StrategyContext<'_> {
|
||||
@@ -215,6 +232,97 @@ impl StrategyContext<'_> {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn order(&self, order_id: u64) -> Option<OrderRuntimeView> {
|
||||
let fills = self
|
||||
.fills
|
||||
.iter()
|
||||
.filter(|fill| fill.order_id == Some(order_id))
|
||||
.collect::<Vec<_>>();
|
||||
let filled_quantity = fills.iter().map(|fill| fill.quantity).sum::<u32>();
|
||||
let gross_amount = fills.iter().map(|fill| fill.gross_amount).sum::<f64>();
|
||||
let transaction_cost = fills
|
||||
.iter()
|
||||
.map(|fill| fill.commission + fill.stamp_tax)
|
||||
.sum::<f64>();
|
||||
let avg_price = if filled_quantity == 0 {
|
||||
0.0
|
||||
} else {
|
||||
gross_amount / filled_quantity as f64
|
||||
};
|
||||
|
||||
if let Some(order) = self
|
||||
.open_orders
|
||||
.iter()
|
||||
.find(|order| order.order_id == order_id)
|
||||
{
|
||||
let filled_quantity = order.filled_quantity.max(filled_quantity);
|
||||
return Some(OrderRuntimeView {
|
||||
order_id,
|
||||
symbol: order.symbol.clone(),
|
||||
side: order.side,
|
||||
requested_quantity: order.requested_quantity,
|
||||
filled_quantity,
|
||||
unfilled_quantity: order
|
||||
.unfilled_quantity
|
||||
.min(order.requested_quantity.saturating_sub(filled_quantity)),
|
||||
status: order.status,
|
||||
avg_price: if avg_price > 0.0 {
|
||||
avg_price
|
||||
} else {
|
||||
order.avg_price
|
||||
},
|
||||
transaction_cost: if transaction_cost > 0.0 {
|
||||
transaction_cost
|
||||
} else {
|
||||
order.transaction_cost
|
||||
},
|
||||
limit_price: order.limit_price,
|
||||
reason: order.reason.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let latest_event = self
|
||||
.order_events
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|event| event.order_id == Some(order_id))
|
||||
.next()?;
|
||||
let filled_quantity = latest_event.filled_quantity.max(filled_quantity);
|
||||
Some(OrderRuntimeView {
|
||||
order_id,
|
||||
symbol: latest_event.symbol.clone(),
|
||||
side: latest_event.side,
|
||||
requested_quantity: latest_event.requested_quantity,
|
||||
filled_quantity,
|
||||
unfilled_quantity: latest_event
|
||||
.requested_quantity
|
||||
.saturating_sub(filled_quantity),
|
||||
status: latest_event.status,
|
||||
avg_price,
|
||||
transaction_cost,
|
||||
limit_price: 0.0,
|
||||
reason: latest_event.reason.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn order_status(&self, order_id: u64) -> &'static str {
|
||||
self.order(order_id)
|
||||
.map(|order| order.status.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
pub fn order_avg_price(&self, order_id: u64) -> f64 {
|
||||
self.order(order_id)
|
||||
.map(|order| order.avg_price)
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
pub fn order_transaction_cost(&self, order_id: u64) -> f64 {
|
||||
self.order(order_id)
|
||||
.map(|order| order.transaction_cost)
|
||||
.unwrap_or(0.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))
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
ManualFunction { name: "get_trading_dates/get_previous_trading_date/get_next_trading_date".to_string(), signature: "ctx.get_previous_trading_date(date, n)".to_string(), detail: "交易日历 API。get_trading_dates 返回闭区间交易日;previous/next 返回相对某日向前或向后的第 n 个交易日,当前日自身不计入。".to_string() },
|
||||
ManualFunction { name: "is_suspended/is_st_stock".to_string(), signature: "ctx.is_suspended(symbol, count)".to_string(), detail: "读取指定证券截至当前交易日最近 count 个交易日的停牌或 ST 标记,返回 bool 序列,顺序从旧到新;对应 RQAlpha 的 is_suspended/is_st_stock 数据源能力。".to_string() },
|
||||
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\" | \"tick\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟或 tick 返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
|
||||
ManualFunction { name: "order/order_status/order_avg_price/order_transaction_cost".to_string(), signature: "ctx.order(order_id)".to_string(), detail: "按订单 id 查询运行时订单对象,支持已结束订单和当前挂单。返回字段包括 status、filled_quantity、unfilled_quantity、avg_price、transaction_cost、symbol、side、reason;可用便捷函数读取状态、成交均价和费用,对齐 RQAlpha Order 的核心属性。".to_string() },
|
||||
ManualFunction { name: "rolling_mean".to_string(), signature: "rolling_mean(\"field\", lookback)".to_string(), detail: "任意字段滚动均值,支持 volume/amount/turnover_ratio、signal_open/signal_close、benchmark_open/benchmark_close 等。任意成交量窗口推荐用它,比如 rolling_mean(\"volume\", 15)。".to_string() },
|
||||
ManualFunction { name: "sma".to_string(), signature: "sma(\"field\", lookback)".to_string(), detail: "rolling_mean 的别名。任意价格均线窗口推荐用它,比如 sma(\"close\", 15)。".to_string() },
|
||||
ManualFunction { name: "round/floor/ceil/abs/min/max/clamp".to_string(), signature: "round(x)".to_string(), detail: "常用数值函数。".to_string() },
|
||||
|
||||
@@ -157,6 +157,10 @@ struct DataApiProbeStrategy {
|
||||
snapshots: Rc<RefCell<Vec<String>>>,
|
||||
}
|
||||
|
||||
struct OrderInspectionStrategy {
|
||||
observed: Rc<RefCell<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Strategy for ScheduledProbeStrategy {
|
||||
fn name(&self) -> &str {
|
||||
"scheduled-probe"
|
||||
@@ -448,6 +452,45 @@ impl Strategy for DataApiProbeStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
impl Strategy for OrderInspectionStrategy {
|
||||
fn name(&self) -> &str {
|
||||
"order-inspection"
|
||||
}
|
||||
|
||||
fn on_day(
|
||||
&mut self,
|
||||
_ctx: &StrategyContext<'_>,
|
||||
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||
Ok(StrategyDecision {
|
||||
rebalance: false,
|
||||
target_weights: BTreeMap::new(),
|
||||
exit_symbols: BTreeSet::new(),
|
||||
order_intents: vec![OrderIntent::Shares {
|
||||
symbol: "000001.SZ".to_string(),
|
||||
quantity: 100,
|
||||
reason: "inspect_buy".to_string(),
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn after_trading(&mut self, ctx: &StrategyContext<'_>) -> Result<(), fidc_core::BacktestError> {
|
||||
let order = ctx.order(1).expect("order 1 visible after trading");
|
||||
self.observed.borrow_mut().push(format!(
|
||||
"status={};filled={};unfilled={};avg={:.2};cost={:.2};symbol={};side={}",
|
||||
order.status.as_str(),
|
||||
order.filled_quantity,
|
||||
order.unfilled_quantity,
|
||||
order.avg_price,
|
||||
order.transaction_cost,
|
||||
order.symbol,
|
||||
order.side.as_str()
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_runs_strategy_hooks_in_daily_order() {
|
||||
let date1 = d(2025, 1, 2);
|
||||
@@ -1084,6 +1127,105 @@ fn strategy_context_exposes_rqalpha_style_data_helpers() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strategy_context_exposes_final_order_runtime_view() {
|
||||
let date = d(2025, 1, 2);
|
||||
let data = DataSet::from_components(
|
||||
vec![Instrument {
|
||||
symbol: "000001.SZ".to_string(),
|
||||
name: "Anchor".to_string(),
|
||||
board: "SZ".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(d(2020, 1, 1)),
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
}],
|
||||
vec![DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
high: 10.4,
|
||||
low: 9.9,
|
||||
close: 10.2,
|
||||
last_price: 10.2,
|
||||
bid1: 10.19,
|
||||
ask1: 10.2,
|
||||
prev_close: 10.0,
|
||||
volume: 100_000,
|
||||
tick_volume: 100_000,
|
||||
bid1_volume: 100_000,
|
||||
ask1_volume: 100_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 11.0,
|
||||
lower_limit: 9.0,
|
||||
price_tick: 0.01,
|
||||
}],
|
||||
vec![DailyFactorSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
market_cap_bn: 20.0,
|
||||
free_float_cap_bn: 18.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
extra_factors: BTreeMap::new(),
|
||||
}],
|
||||
vec![CandidateEligibility {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
is_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
}],
|
||||
vec![BenchmarkSnapshot {
|
||||
date,
|
||||
benchmark: "000300.SH".to_string(),
|
||||
open: 100.0,
|
||||
close: 100.0,
|
||||
prev_close: 99.0,
|
||||
volume: 1_000_000,
|
||||
}],
|
||||
)
|
||||
.expect("dataset");
|
||||
|
||||
let observed = Rc::new(RefCell::new(Vec::new()));
|
||||
let strategy = OrderInspectionStrategy {
|
||||
observed: observed.clone(),
|
||||
};
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Close,
|
||||
);
|
||||
let mut engine = BacktestEngine::new(
|
||||
data,
|
||||
strategy,
|
||||
broker,
|
||||
BacktestConfig {
|
||||
initial_cash: 10_000.0,
|
||||
benchmark_code: "000300.SH".to_string(),
|
||||
start_date: Some(date),
|
||||
end_date: Some(date),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Close,
|
||||
},
|
||||
);
|
||||
|
||||
engine.run().expect("backtest run");
|
||||
|
||||
assert_eq!(
|
||||
observed.borrow().as_slice(),
|
||||
["status=filled;filled=100;unfilled=0;avg=10.20;cost=5.00;symbol=000001.SZ;side=buy"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_rejects_pending_limit_orders_at_market_close() {
|
||||
let date1 = d(2025, 1, 2);
|
||||
|
||||
@@ -33,6 +33,8 @@ fn strategy_emits_target_weights_and_diagnostics() {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
})
|
||||
.expect("decision");
|
||||
|
||||
@@ -77,6 +79,8 @@ fn jq_strategy_emits_same_day_decision() {
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
})
|
||||
.expect("jq decision");
|
||||
|
||||
|
||||
@@ -67,8 +67,8 @@ current alignment pass.
|
||||
### 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
|
||||
- [x] final order object lookup by order id
|
||||
- [x] order average fill price and transaction cost aggregation
|
||||
|
||||
## Execution Order
|
||||
|
||||
@@ -84,6 +84,6 @@ current alignment pass.
|
||||
|
||||
## Current Step
|
||||
|
||||
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.
|
||||
Active implementation target: continue parity audit for remaining account APIs
|
||||
after order object lookup, status, unfilled quantity, average fill price, and
|
||||
transaction cost aggregation are covered.
|
||||
|
||||
Reference in New Issue
Block a user