分离过程事件分发与结果保留
This commit is contained in:
@@ -71,6 +71,23 @@ impl Default for FuturesValidationConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ProcessEventRetention {
|
||||||
|
/// Retain every phase and business event in the returned result.
|
||||||
|
All,
|
||||||
|
/// Retain only lifecycle events useful for a durable business audit.
|
||||||
|
Business,
|
||||||
|
/// Dispatch events to listeners and the strategy, but do not retain them
|
||||||
|
/// in the returned result.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProcessEventRetention {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::All
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DailyEquityPoint {
|
pub struct DailyEquityPoint {
|
||||||
#[serde(with = "date_format")]
|
#[serde(with = "date_format")]
|
||||||
@@ -425,6 +442,7 @@ pub struct BacktestEngine<S, C, R> {
|
|||||||
cash_dividends_enabled: bool,
|
cash_dividends_enabled: bool,
|
||||||
cash_dividend_adjusts_cost_basis: bool,
|
cash_dividend_adjusts_cost_basis: bool,
|
||||||
process_event_bus: ProcessEventBus,
|
process_event_bus: ProcessEventBus,
|
||||||
|
process_event_retention: ProcessEventRetention,
|
||||||
dynamic_universe: Option<BTreeSet<String>>,
|
dynamic_universe: Option<BTreeSet<String>>,
|
||||||
subscriptions: BTreeSet<String>,
|
subscriptions: BTreeSet<String>,
|
||||||
futures_account: Option<FuturesAccountState>,
|
futures_account: Option<FuturesAccountState>,
|
||||||
@@ -455,6 +473,7 @@ impl<S, C, R> BacktestEngine<S, C, R> {
|
|||||||
cash_dividends_enabled: true,
|
cash_dividends_enabled: true,
|
||||||
cash_dividend_adjusts_cost_basis: true,
|
cash_dividend_adjusts_cost_basis: true,
|
||||||
process_event_bus: ProcessEventBus::new(),
|
process_event_bus: ProcessEventBus::new(),
|
||||||
|
process_event_retention: ProcessEventRetention::All,
|
||||||
dynamic_universe: None,
|
dynamic_universe: None,
|
||||||
subscriptions: BTreeSet::new(),
|
subscriptions: BTreeSet::new(),
|
||||||
futures_account: None,
|
futures_account: None,
|
||||||
@@ -488,6 +507,11 @@ impl<S, C, R> BacktestEngine<S, C, R> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_process_event_retention(mut self, retention: ProcessEventRetention) -> Self {
|
||||||
|
self.process_event_retention = retention;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_cash_dividends(mut self, enabled: bool) -> Self {
|
pub fn with_cash_dividends(mut self, enabled: bool) -> Self {
|
||||||
self.cash_dividends_enabled = enabled;
|
self.cash_dividends_enabled = enabled;
|
||||||
self
|
self
|
||||||
@@ -2118,7 +2142,7 @@ where
|
|||||||
let holding_count = holdings_for_day.len();
|
let holding_count = holdings_for_day.len();
|
||||||
result.daily_holdings.extend(holdings_for_day);
|
result.daily_holdings.extend(holdings_for_day);
|
||||||
let progress_process_start = result.process_events.len();
|
let progress_process_start = result.process_events.len();
|
||||||
result.process_events.append(&mut process_events);
|
self.retain_process_events(&mut result.process_events, &mut process_events);
|
||||||
let aggregate_cash = self.aggregate_cash(&portfolio);
|
let aggregate_cash = self.aggregate_cash(&portfolio);
|
||||||
let aggregate_market_value = self.aggregate_market_value(&portfolio);
|
let aggregate_market_value = self.aggregate_market_value(&portfolio);
|
||||||
let aggregate_total_equity = self.aggregate_total_equity(&portfolio);
|
let aggregate_total_equity = self.aggregate_total_equity(&portfolio);
|
||||||
@@ -3193,7 +3217,7 @@ where
|
|||||||
let holding_count = holdings_for_day.len();
|
let holding_count = holdings_for_day.len();
|
||||||
result.daily_holdings.extend(holdings_for_day);
|
result.daily_holdings.extend(holdings_for_day);
|
||||||
let progress_process_start = result.process_events.len();
|
let progress_process_start = result.process_events.len();
|
||||||
result.process_events.append(&mut process_events);
|
self.retain_process_events(&mut result.process_events, &mut process_events);
|
||||||
let aggregate_cash = self.aggregate_cash(&portfolio);
|
let aggregate_cash = self.aggregate_cash(&portfolio);
|
||||||
let aggregate_market_value = self.aggregate_market_value(&portfolio);
|
let aggregate_market_value = self.aggregate_market_value(&portfolio);
|
||||||
let aggregate_total_equity = self.aggregate_total_equity(&portfolio);
|
let aggregate_total_equity = self.aggregate_total_equity(&portfolio);
|
||||||
@@ -3280,7 +3304,23 @@ where
|
|||||||
result.fills.append(&mut report.fill_events);
|
result.fills.append(&mut report.fill_events);
|
||||||
result.position_events.append(&mut report.position_events);
|
result.position_events.append(&mut report.position_events);
|
||||||
result.account_events.append(&mut report.account_events);
|
result.account_events.append(&mut report.account_events);
|
||||||
result.process_events.append(&mut report.process_events);
|
self.retain_process_events(&mut result.process_events, &mut report.process_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn retain_process_events(
|
||||||
|
&self,
|
||||||
|
target: &mut Vec<ProcessEvent>,
|
||||||
|
incoming: &mut Vec<ProcessEvent>,
|
||||||
|
) {
|
||||||
|
match self.process_event_retention {
|
||||||
|
ProcessEventRetention::All => target.append(incoming),
|
||||||
|
ProcessEventRetention::Business => target.extend(
|
||||||
|
incoming
|
||||||
|
.drain(..)
|
||||||
|
.filter(|event| event.kind.is_business_lifecycle()),
|
||||||
|
),
|
||||||
|
ProcessEventRetention::None => incoming.clear(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_corporate_actions(
|
fn apply_corporate_actions(
|
||||||
|
|||||||
@@ -364,6 +364,38 @@ impl ProcessEventKind {
|
|||||||
Self::AccountManagementFee => "account_management_fee",
|
Self::AccountManagementFee => "account_management_fee",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether the event is part of the durable business lifecycle
|
||||||
|
/// audit. Phase boundary events are useful during interactive debugging,
|
||||||
|
/// but retaining every minute phase marker for a long run is unnecessary.
|
||||||
|
pub fn is_business_lifecycle(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
*self,
|
||||||
|
Self::PreScheduled
|
||||||
|
| Self::PostScheduled
|
||||||
|
| Self::PreOnDay
|
||||||
|
| Self::OnDay
|
||||||
|
| Self::PostOnDay
|
||||||
|
| Self::OrderPendingNew
|
||||||
|
| Self::OrderCreationPass
|
||||||
|
| Self::OrderCreationReject
|
||||||
|
| Self::OrderPendingCancel
|
||||||
|
| Self::OrderCancellationPass
|
||||||
|
| Self::OrderCancellationReject
|
||||||
|
| Self::OrderPendingUpdate
|
||||||
|
| Self::OrderUpdatePass
|
||||||
|
| Self::OrderUpdateReject
|
||||||
|
| Self::OrderUnsolicitedUpdate
|
||||||
|
| Self::Trade
|
||||||
|
| Self::UniverseUpdated
|
||||||
|
| Self::UniverseSubscribed
|
||||||
|
| Self::UniverseUnsubscribed
|
||||||
|
| Self::AccountDepositWithdraw
|
||||||
|
| Self::AccountFinanceRepay
|
||||||
|
| Self::AccountManagementFee
|
||||||
|
| Self::Settlement
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -384,7 +416,7 @@ pub struct ProcessEvent {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use chrono::{NaiveDate, NaiveDateTime};
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
|
|
||||||
use super::{FillEvent, OrderEvent, OrderSide, OrderStatus};
|
use super::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEventKind};
|
||||||
|
|
||||||
fn order_event(status: OrderStatus, filled_quantity: u32) -> OrderEvent {
|
fn order_event(status: OrderStatus, filled_quantity: u32) -> OrderEvent {
|
||||||
OrderEvent {
|
OrderEvent {
|
||||||
@@ -484,4 +516,12 @@ mod tests {
|
|||||||
"2025-01-02 10:18:03"
|
"2025-01-02 10:18:03"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_event_business_lifecycle_filter_keeps_audit_events_only() {
|
||||||
|
assert!(ProcessEventKind::OrderUpdateReject.is_business_lifecycle());
|
||||||
|
assert!(ProcessEventKind::Settlement.is_business_lifecycle());
|
||||||
|
assert!(!ProcessEventKind::PreMinute.is_business_lifecycle());
|
||||||
|
assert!(!ProcessEventKind::PostBar.is_business_lifecycle());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pub use engine::{
|
|||||||
AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError,
|
AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError,
|
||||||
BacktestResult, BacktestTerminalAssetClass, BacktestTerminalAudit, BacktestTerminalOpenOrder,
|
BacktestResult, BacktestTerminalAssetClass, BacktestTerminalAudit, BacktestTerminalOpenOrder,
|
||||||
BacktestTerminalStatus, DailyEquityPoint, ExecutionQuoteRequest, FuturesValidationConfig,
|
BacktestTerminalStatus, DailyEquityPoint, ExecutionQuoteRequest, FuturesValidationConfig,
|
||||||
|
ProcessEventRetention,
|
||||||
};
|
};
|
||||||
pub use event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
|
pub use event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
|
||||||
pub use events::{
|
pub use events::{
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ use fidc_core::{
|
|||||||
IntradayExecutionQuote, IntradayOrderBookDepthLevel, MatchingType, NumericFactorMap,
|
IntradayExecutionQuote, IntradayOrderBookDepthLevel, MatchingType, NumericFactorMap,
|
||||||
OpenOrderView, OrderIntent, OrderSide, OrderStatus, PlatformExprStrategy,
|
OpenOrderView, OrderIntent, OrderSide, OrderStatus, PlatformExprStrategy,
|
||||||
PlatformExprStrategyConfig, PlatformTradeAction, PortfolioState, PriceField, ProcessEvent,
|
PlatformExprStrategyConfig, PlatformTradeAction, PortfolioState, PriceField, ProcessEvent,
|
||||||
ProcessEventBus, ProcessEventKind, ScheduleRule, ScheduleStage, ScheduleTimeRule, Strategy,
|
ProcessEventBus, ProcessEventKind, ProcessEventRetention, ScheduleRule, ScheduleStage,
|
||||||
StrategyContext, StrategyDecision,
|
ScheduleTimeRule, Strategy, StrategyContext, StrategyDecision,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||||
@@ -1199,6 +1199,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
|||||||
)
|
)
|
||||||
.expect("dataset");
|
.expect("dataset");
|
||||||
|
|
||||||
|
let compact_data = data.clone();
|
||||||
let log = Rc::new(RefCell::new(Vec::new()));
|
let log = Rc::new(RefCell::new(Vec::new()));
|
||||||
let strategy = HookProbeStrategy { log: log.clone() };
|
let strategy = HookProbeStrategy { log: log.clone() };
|
||||||
let broker = BrokerSimulator::new_with_execution_price(
|
let broker = BrokerSimulator::new_with_execution_price(
|
||||||
@@ -1238,6 +1239,42 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(result.process_events.len(), 36);
|
assert_eq!(result.process_events.len(), 36);
|
||||||
|
|
||||||
|
let compact_strategy = HookProbeStrategy {
|
||||||
|
log: Rc::new(RefCell::new(Vec::new())),
|
||||||
|
};
|
||||||
|
let compact_broker = BrokerSimulator::new_with_execution_price(
|
||||||
|
ChinaAShareCostModel::default(),
|
||||||
|
ChinaEquityRuleHooks::default(),
|
||||||
|
PriceField::Open,
|
||||||
|
);
|
||||||
|
let mut compact_engine = BacktestEngine::new(
|
||||||
|
compact_data,
|
||||||
|
compact_strategy,
|
||||||
|
compact_broker,
|
||||||
|
BacktestConfig {
|
||||||
|
initial_cash: 100_000.0,
|
||||||
|
benchmark_code: "000300.SH".to_string(),
|
||||||
|
start_date: Some(date1),
|
||||||
|
end_date: Some(date2),
|
||||||
|
decision_lag_trading_days: 0,
|
||||||
|
execution_price_field: PriceField::Open,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_process_event_retention(ProcessEventRetention::Business);
|
||||||
|
let compact_result = compact_engine.run().expect("compact backtest succeeds");
|
||||||
|
assert!(compact_result
|
||||||
|
.process_events
|
||||||
|
.iter()
|
||||||
|
.all(|event| event.kind.is_business_lifecycle()));
|
||||||
|
assert!(compact_result
|
||||||
|
.process_events
|
||||||
|
.iter()
|
||||||
|
.any(|event| event.kind == ProcessEventKind::OnDay));
|
||||||
|
assert!(!compact_result
|
||||||
|
.process_events
|
||||||
|
.iter()
|
||||||
|
.any(|event| event.kind == ProcessEventKind::PreBeforeTrading));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.process_events[..18]
|
result.process_events[..18]
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
Reference in New Issue
Block a user