Add order runtime lookup
This commit is contained in:
@@ -410,6 +410,8 @@ where
|
|||||||
execution_date,
|
execution_date,
|
||||||
default_stage_time(ScheduleStage::BeforeTrading),
|
default_stage_time(ScheduleStage::BeforeTrading),
|
||||||
),
|
),
|
||||||
|
order_events: result.order_events.as_slice(),
|
||||||
|
fills: result.fills.as_slice(),
|
||||||
})?;
|
})?;
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
@@ -443,6 +445,8 @@ where
|
|||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::BeforeTrading),
|
default_stage_time(ScheduleStage::BeforeTrading),
|
||||||
|
result.order_events.as_slice(),
|
||||||
|
result.fills.as_slice(),
|
||||||
)?;
|
)?;
|
||||||
self.apply_strategy_directives(
|
self.apply_strategy_directives(
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -501,6 +505,8 @@ where
|
|||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::OpenAuction),
|
default_stage_time(ScheduleStage::OpenAuction),
|
||||||
|
result.order_events.as_slice(),
|
||||||
|
result.fills.as_slice(),
|
||||||
)?;
|
)?;
|
||||||
auction_decision.merge_from(self.strategy.open_auction(&StrategyContext {
|
auction_decision.merge_from(self.strategy.open_auction(&StrategyContext {
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -517,6 +523,8 @@ where
|
|||||||
execution_date,
|
execution_date,
|
||||||
default_stage_time(ScheduleStage::OpenAuction),
|
default_stage_time(ScheduleStage::OpenAuction),
|
||||||
),
|
),
|
||||||
|
order_events: result.order_events.as_slice(),
|
||||||
|
fills: result.fills.as_slice(),
|
||||||
})?);
|
})?);
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
@@ -615,6 +623,8 @@ where
|
|||||||
execution_date,
|
execution_date,
|
||||||
default_stage_time(ScheduleStage::OnDay),
|
default_stage_time(ScheduleStage::OnDay),
|
||||||
),
|
),
|
||||||
|
order_events: result.order_events.as_slice(),
|
||||||
|
fills: result.fills.as_slice(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.transpose()?
|
.transpose()?
|
||||||
@@ -635,6 +645,8 @@ where
|
|||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::OnDay),
|
default_stage_time(ScheduleStage::OnDay),
|
||||||
|
result.order_events.as_slice(),
|
||||||
|
result.fills.as_slice(),
|
||||||
)?);
|
)?);
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
@@ -685,6 +697,8 @@ where
|
|||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::Bar),
|
default_stage_time(ScheduleStage::Bar),
|
||||||
|
result.order_events.as_slice(),
|
||||||
|
result.fills.as_slice(),
|
||||||
)?);
|
)?);
|
||||||
decision.merge_from(self.strategy.on_bar(&StrategyContext {
|
decision.merge_from(self.strategy.on_bar(&StrategyContext {
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -701,6 +715,8 @@ where
|
|||||||
execution_date,
|
execution_date,
|
||||||
default_stage_time(ScheduleStage::Bar),
|
default_stage_time(ScheduleStage::Bar),
|
||||||
),
|
),
|
||||||
|
order_events: result.order_events.as_slice(),
|
||||||
|
fills: result.fills.as_slice(),
|
||||||
})?);
|
})?);
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
@@ -831,6 +847,8 @@ where
|
|||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
Some(tick_time),
|
Some(tick_time),
|
||||||
|
result.order_events.as_slice(),
|
||||||
|
result.fills.as_slice(),
|
||||||
)?;
|
)?;
|
||||||
tick_decision.merge_from(self.strategy.on_tick(
|
tick_decision.merge_from(self.strategy.on_tick(
|
||||||
&StrategyContext {
|
&StrategyContext {
|
||||||
@@ -845,6 +863,8 @@ where
|
|||||||
process_events: &process_events,
|
process_events: &process_events,
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: Some(quote.timestamp),
|
active_datetime: Some(quote.timestamp),
|
||||||
|
order_events: result.order_events.as_slice(),
|
||||||
|
fills: result.fills.as_slice(),
|
||||||
},
|
},
|
||||||
"e,
|
"e,
|
||||||
)?);
|
)?);
|
||||||
@@ -919,6 +939,18 @@ where
|
|||||||
portfolio.update_prices(execution_date, &self.data, PriceField::Close)?;
|
portfolio.update_prices(execution_date, &self.data, PriceField::Close)?;
|
||||||
|
|
||||||
let post_trade_open_orders = self.broker.open_order_views();
|
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(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
@@ -950,6 +982,8 @@ where
|
|||||||
execution_date,
|
execution_date,
|
||||||
default_stage_time(ScheduleStage::AfterTrading),
|
default_stage_time(ScheduleStage::AfterTrading),
|
||||||
),
|
),
|
||||||
|
order_events: visible_order_events.as_slice(),
|
||||||
|
fills: visible_fills.as_slice(),
|
||||||
})?;
|
})?;
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
@@ -983,6 +1017,8 @@ where
|
|||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::AfterTrading),
|
default_stage_time(ScheduleStage::AfterTrading),
|
||||||
|
visible_order_events.as_slice(),
|
||||||
|
visible_fills.as_slice(),
|
||||||
)?;
|
)?;
|
||||||
self.apply_strategy_directives(
|
self.apply_strategy_directives(
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -1014,6 +1050,18 @@ where
|
|||||||
report.account_events.extend(close_report.account_events);
|
report.account_events.extend(close_report.account_events);
|
||||||
report.diagnostics.extend(close_report.diagnostics);
|
report.diagnostics.extend(close_report.diagnostics);
|
||||||
let post_close_open_orders = self.broker.open_order_views();
|
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(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
@@ -1061,6 +1109,8 @@ where
|
|||||||
execution_date,
|
execution_date,
|
||||||
default_stage_time(ScheduleStage::Settlement),
|
default_stage_time(ScheduleStage::Settlement),
|
||||||
),
|
),
|
||||||
|
order_events: visible_order_events_after_close.as_slice(),
|
||||||
|
fills: visible_fills_after_close.as_slice(),
|
||||||
})?;
|
})?;
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
@@ -1094,6 +1144,8 @@ where
|
|||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::Settlement),
|
default_stage_time(ScheduleStage::Settlement),
|
||||||
|
visible_order_events_after_close.as_slice(),
|
||||||
|
visible_fills_after_close.as_slice(),
|
||||||
)?;
|
)?;
|
||||||
self.apply_strategy_directives(
|
self.apply_strategy_directives(
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -1620,6 +1672,8 @@ fn collect_scheduled_decisions<S: Strategy>(
|
|||||||
process_events: &mut Vec<ProcessEvent>,
|
process_events: &mut Vec<ProcessEvent>,
|
||||||
process_event_bus: &mut ProcessEventBus,
|
process_event_bus: &mut ProcessEventBus,
|
||||||
current_time: Option<chrono::NaiveTime>,
|
current_time: Option<chrono::NaiveTime>,
|
||||||
|
order_events: &[OrderEvent],
|
||||||
|
fills: &[FillEvent],
|
||||||
) -> Result<crate::strategy::StrategyDecision, BacktestError> {
|
) -> Result<crate::strategy::StrategyDecision, BacktestError> {
|
||||||
let mut combined = crate::strategy::StrategyDecision::default();
|
let mut combined = crate::strategy::StrategyDecision::default();
|
||||||
for rule in scheduler.triggered_rules_at(execution_date, stage, current_time, rules) {
|
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(),
|
process_events: process_events.as_slice(),
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: stage_datetime(execution_date, current_time),
|
active_datetime: stage_datetime(execution_date, current_time),
|
||||||
|
order_events,
|
||||||
|
fills,
|
||||||
},
|
},
|
||||||
rule,
|
rule,
|
||||||
)?);
|
)?);
|
||||||
@@ -1713,6 +1769,8 @@ fn publish_phase_event<S: Strategy>(
|
|||||||
process_events,
|
process_events,
|
||||||
active_process_event: Some(&event),
|
active_process_event: Some(&event),
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
strategy.on_process_event(&event_ctx, &event)?;
|
strategy.on_process_event(&event_ctx, &event)?;
|
||||||
events.push(event);
|
events.push(event);
|
||||||
@@ -1748,6 +1806,8 @@ fn publish_process_events<S: Strategy>(
|
|||||||
process_events,
|
process_events,
|
||||||
active_process_event: Some(&event),
|
active_process_event: Some(&event),
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
strategy.on_process_event(&event_ctx, &event)?;
|
strategy.on_process_event(&event_ctx, &event)?;
|
||||||
target.push(event);
|
target.push(event);
|
||||||
@@ -1783,6 +1843,8 @@ fn publish_custom_process_event<S: Strategy>(
|
|||||||
process_events,
|
process_events,
|
||||||
active_process_event: Some(&event),
|
active_process_event: Some(&event),
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
strategy.on_process_event(&event_ctx, &event)?;
|
strategy.on_process_event(&event_ctx, &event)?;
|
||||||
target.push(event);
|
target.push(event);
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ pub use scheduler::{
|
|||||||
};
|
};
|
||||||
pub use strategy::{
|
pub use strategy::{
|
||||||
AlgoOrderStyle, CnSmallCapRotationConfig, CnSmallCapRotationStrategy, JqMicroCapConfig,
|
AlgoOrderStyle, CnSmallCapRotationConfig, CnSmallCapRotationStrategy, JqMicroCapConfig,
|
||||||
JqMicroCapStrategy, OpenOrderView, OrderIntent, Strategy, StrategyContext, StrategyDecision,
|
JqMicroCapStrategy, OpenOrderView, OrderIntent, OrderRuntimeView, Strategy, StrategyContext,
|
||||||
TargetPortfolioOrderPricing,
|
StrategyDecision, TargetPortfolioOrderPricing,
|
||||||
};
|
};
|
||||||
pub use strategy_ai::{
|
pub use strategy_ai::{
|
||||||
ManualExample, ManualFactorSource, ManualField, ManualFieldGroup, ManualFunction,
|
ManualExample, ManualFactorSource, ManualField, ManualFieldGroup, ManualFunction,
|
||||||
|
|||||||
@@ -3912,6 +3912,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -4050,6 +4052,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -4166,6 +4170,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -4287,6 +4293,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SH".to_string();
|
cfg.signal_symbol = "000001.SH".to_string();
|
||||||
@@ -4391,6 +4399,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SH".to_string();
|
cfg.signal_symbol = "000001.SH".to_string();
|
||||||
@@ -4490,6 +4500,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SH".to_string();
|
cfg.signal_symbol = "000001.SH".to_string();
|
||||||
@@ -4607,6 +4619,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -4727,6 +4741,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -4852,6 +4868,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -4987,6 +5005,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -5094,6 +5114,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -5229,6 +5251,8 @@ mod tests {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -5346,6 +5370,8 @@ mod tests {
|
|||||||
process_events: &process_events,
|
process_events: &process_events,
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime};
|
|||||||
use crate::cost::ChinaAShareCostModel;
|
use crate::cost::ChinaAShareCostModel;
|
||||||
use crate::data::{DailyMarketSnapshot, DataSet, IntradayExecutionQuote, PriceBar, PriceField};
|
use crate::data::{DailyMarketSnapshot, DataSet, IntradayExecutionQuote, PriceBar, PriceField};
|
||||||
use crate::engine::BacktestError;
|
use crate::engine::BacktestError;
|
||||||
use crate::events::{OrderSide, OrderStatus, ProcessEvent};
|
use crate::events::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEvent};
|
||||||
use crate::instrument::Instrument;
|
use crate::instrument::Instrument;
|
||||||
use crate::portfolio::PortfolioState;
|
use crate::portfolio::PortfolioState;
|
||||||
use crate::scheduler::ScheduleRule;
|
use crate::scheduler::ScheduleRule;
|
||||||
@@ -80,6 +80,21 @@ pub struct OpenOrderView {
|
|||||||
pub reason: String,
|
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 struct StrategyContext<'a> {
|
||||||
pub execution_date: NaiveDate,
|
pub execution_date: NaiveDate,
|
||||||
pub decision_date: NaiveDate,
|
pub decision_date: NaiveDate,
|
||||||
@@ -92,6 +107,8 @@ pub struct StrategyContext<'a> {
|
|||||||
pub process_events: &'a [ProcessEvent],
|
pub process_events: &'a [ProcessEvent],
|
||||||
pub active_process_event: Option<&'a ProcessEvent>,
|
pub active_process_event: Option<&'a ProcessEvent>,
|
||||||
pub active_datetime: Option<NaiveDateTime>,
|
pub active_datetime: Option<NaiveDateTime>,
|
||||||
|
pub order_events: &'a [OrderEvent],
|
||||||
|
pub fills: &'a [FillEvent],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StrategyContext<'_> {
|
impl StrategyContext<'_> {
|
||||||
@@ -215,6 +232,97 @@ impl StrategyContext<'_> {
|
|||||||
.unwrap_or(0)
|
.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 {
|
pub fn available_sellable_qty(&self, symbol: &str, raw_sellable_qty: u32) -> u32 {
|
||||||
raw_sellable_qty.saturating_sub(self.symbol_open_sell_quantity(symbol))
|
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: "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: "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: "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: "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: "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() },
|
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>>>,
|
snapshots: Rc<RefCell<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct OrderInspectionStrategy {
|
||||||
|
observed: Rc<RefCell<Vec<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Strategy for ScheduledProbeStrategy {
|
impl Strategy for ScheduledProbeStrategy {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"scheduled-probe"
|
"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]
|
#[test]
|
||||||
fn engine_runs_strategy_hooks_in_daily_order() {
|
fn engine_runs_strategy_hooks_in_daily_order() {
|
||||||
let date1 = d(2025, 1, 2);
|
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]
|
#[test]
|
||||||
fn engine_rejects_pending_limit_orders_at_market_close() {
|
fn engine_rejects_pending_limit_orders_at_market_close() {
|
||||||
let date1 = d(2025, 1, 2);
|
let date1 = d(2025, 1, 2);
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ fn strategy_emits_target_weights_and_diagnostics() {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
})
|
})
|
||||||
.expect("decision");
|
.expect("decision");
|
||||||
|
|
||||||
@@ -77,6 +79,8 @@ fn jq_strategy_emits_same_day_decision() {
|
|||||||
process_events: &[],
|
process_events: &[],
|
||||||
active_process_event: None,
|
active_process_event: None,
|
||||||
active_datetime: None,
|
active_datetime: None,
|
||||||
|
order_events: &[],
|
||||||
|
fills: &[],
|
||||||
})
|
})
|
||||||
.expect("jq decision");
|
.expect("jq decision");
|
||||||
|
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ current alignment pass.
|
|||||||
### Phase 8: Order object API parity
|
### Phase 8: Order object API parity
|
||||||
|
|
||||||
- [x] open-order status and unfilled quantity exposed to strategy runtime
|
- [x] open-order status and unfilled quantity exposed to strategy runtime
|
||||||
- [ ] final order object lookup by order id
|
- [x] final order object lookup by order id
|
||||||
- [ ] order average fill price and transaction cost aggregation
|
- [x] order average fill price and transaction cost aggregation
|
||||||
|
|
||||||
## Execution Order
|
## Execution Order
|
||||||
|
|
||||||
@@ -84,6 +84,6 @@ current alignment pass.
|
|||||||
|
|
||||||
## Current Step
|
## Current Step
|
||||||
|
|
||||||
Active implementation target: continue order object API parity after exposing
|
Active implementation target: continue parity audit for remaining account APIs
|
||||||
open-order status and unfilled quantity; next gaps are final order lookup and
|
after order object lookup, status, unfilled quantity, average fill price, and
|
||||||
average fill price / transaction cost aggregation by order id.
|
transaction cost aggregation are covered.
|
||||||
|
|||||||
Reference in New Issue
Block a user