Add process event stream for backtests
This commit is contained in:
@@ -5,7 +5,10 @@ use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use crate::cost::CostModel;
|
||||
use crate::data::{DataSet, IntradayExecutionQuote, PriceField};
|
||||
use crate::engine::BacktestError;
|
||||
use crate::events::{AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent};
|
||||
use crate::events::{
|
||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
ProcessEventKind,
|
||||
};
|
||||
use crate::portfolio::PortfolioState;
|
||||
use crate::rules::EquityRuleHooks;
|
||||
use crate::strategy::{OrderIntent, StrategyDecision};
|
||||
@@ -16,6 +19,7 @@ pub struct BrokerExecutionReport {
|
||||
pub fill_events: Vec<FillEvent>,
|
||||
pub position_events: Vec<PositionEvent>,
|
||||
pub account_events: Vec<AccountEvent>,
|
||||
pub process_events: Vec<ProcessEvent>,
|
||||
pub diagnostics: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -440,6 +444,25 @@ where
|
||||
order_id
|
||||
}
|
||||
|
||||
fn emit_order_process_event(
|
||||
report: &mut BrokerExecutionReport,
|
||||
date: NaiveDate,
|
||||
kind: ProcessEventKind,
|
||||
order_id: u64,
|
||||
symbol: &str,
|
||||
side: OrderSide,
|
||||
detail: impl Into<String>,
|
||||
) {
|
||||
report.process_events.push(ProcessEvent {
|
||||
date,
|
||||
kind,
|
||||
order_id: Some(order_id),
|
||||
symbol: Some(symbol.to_string()),
|
||||
side: Some(side),
|
||||
detail: detail.into(),
|
||||
});
|
||||
}
|
||||
|
||||
fn target_quantities(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
@@ -881,6 +904,16 @@ where
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderPendingNew,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Sell,
|
||||
format!("requested_quantity={requested_qty} reason={reason}"),
|
||||
);
|
||||
|
||||
let rule = self.rules.can_sell(
|
||||
date,
|
||||
snapshot,
|
||||
@@ -889,6 +922,7 @@ where
|
||||
self.execution_price_field,
|
||||
);
|
||||
if !rule.allowed {
|
||||
let rule_reason = rule.reason.as_deref().unwrap_or_default().to_string();
|
||||
let status = match rule.reason.as_deref() {
|
||||
Some("paused")
|
||||
| Some("sell disabled by eligibility flags")
|
||||
@@ -903,11 +937,30 @@ where
|
||||
requested_quantity: requested_qty,
|
||||
filled_quantity: 0,
|
||||
status,
|
||||
reason: format!("{reason}: {}", rule.reason.unwrap_or_default()),
|
||||
reason: format!("{reason}: {rule_reason}"),
|
||||
});
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderUnsolicitedUpdate,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Sell,
|
||||
format!("status={status:?} reason={rule_reason}"),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderCreationPass,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Sell,
|
||||
"sell order passed rule checks",
|
||||
);
|
||||
|
||||
let sellable = position.sellable_qty(date);
|
||||
let mut partial_fill_reason = if sellable < requested_qty {
|
||||
Some("sellable quantity limit".to_string())
|
||||
@@ -945,6 +998,18 @@ where
|
||||
status: zero_fill_status_for_reason(&limit_reason),
|
||||
reason: format!("{reason}: {limit_reason}"),
|
||||
});
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderUnsolicitedUpdate,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Sell,
|
||||
format!(
|
||||
"status={:?} reason={limit_reason}",
|
||||
zero_fill_status_for_reason(&limit_reason)
|
||||
),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -959,6 +1024,15 @@ where
|
||||
status: OrderStatus::Rejected,
|
||||
reason: format!("{reason}: no sellable quantity"),
|
||||
});
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderUnsolicitedUpdate,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Sell,
|
||||
"status=Rejected reason=no sellable quantity",
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1031,6 +1105,15 @@ where
|
||||
net_cash_flow: net_cash,
|
||||
reason: reason.to_string(),
|
||||
});
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::Trade,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Sell,
|
||||
format!("filled_quantity={} price={}", leg.quantity, leg.price),
|
||||
);
|
||||
report.position_events.push(PositionEvent {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
@@ -1097,6 +1180,17 @@ where
|
||||
status,
|
||||
reason: order_reason,
|
||||
});
|
||||
if matches!(status, OrderStatus::Canceled | OrderStatus::Rejected) {
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderUnsolicitedUpdate,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Sell,
|
||||
format!("status={status:?} filled_quantity={filled_qty}"),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1306,10 +1400,21 @@ where
|
||||
let snapshot = data.require_market(date, symbol)?;
|
||||
let candidate = data.require_candidate(date, symbol)?;
|
||||
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderPendingNew,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
format!("requested_quantity={requested_qty} reason={reason}"),
|
||||
);
|
||||
|
||||
let rule = self
|
||||
.rules
|
||||
.can_buy(date, snapshot, candidate, self.execution_price_field);
|
||||
if !rule.allowed {
|
||||
let rule_reason = rule.reason.as_deref().unwrap_or_default().to_string();
|
||||
let status = match rule.reason.as_deref() {
|
||||
Some("paused")
|
||||
| Some("buy disabled by eligibility flags")
|
||||
@@ -1324,11 +1429,30 @@ where
|
||||
requested_quantity: requested_qty,
|
||||
filled_quantity: 0,
|
||||
status,
|
||||
reason: format!("{reason}: {}", rule.reason.unwrap_or_default()),
|
||||
reason: format!("{reason}: {rule_reason}"),
|
||||
});
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderUnsolicitedUpdate,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
format!("status={status:?} reason={rule_reason}"),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderCreationPass,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
"buy order passed rule checks",
|
||||
);
|
||||
|
||||
let mut partial_fill_reason = None;
|
||||
let market_limited_qty = self.market_fillable_quantity(
|
||||
snapshot,
|
||||
@@ -1357,6 +1481,18 @@ where
|
||||
status: zero_fill_status_for_reason(&limit_reason),
|
||||
reason: format!("{reason}: {limit_reason}"),
|
||||
});
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderUnsolicitedUpdate,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
format!(
|
||||
"status={:?} reason={limit_reason}",
|
||||
zero_fill_status_for_reason(&limit_reason)
|
||||
),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -1432,6 +1568,20 @@ where
|
||||
.unwrap_or("insufficient cash after fees")
|
||||
),
|
||||
});
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderUnsolicitedUpdate,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
format!(
|
||||
"status=Rejected reason={}",
|
||||
partial_fill_reason
|
||||
.as_deref()
|
||||
.unwrap_or("insufficient cash after fees")
|
||||
),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1471,6 +1621,15 @@ where
|
||||
net_cash_flow: -cash_out,
|
||||
reason: reason.to_string(),
|
||||
});
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::Trade,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
format!("filled_quantity={} price={}", leg.quantity, leg.price),
|
||||
);
|
||||
report.position_events.push(PositionEvent {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
@@ -1536,6 +1695,17 @@ where
|
||||
status,
|
||||
reason: order_reason,
|
||||
});
|
||||
if matches!(status, OrderStatus::Canceled | OrderStatus::Rejected) {
|
||||
Self::emit_order_process_event(
|
||||
report,
|
||||
date,
|
||||
ProcessEventKind::OrderUnsolicitedUpdate,
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
format!("status={status:?} filled_quantity={filled_qty}"),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ use thiserror::Error;
|
||||
use crate::broker::{BrokerExecutionReport, BrokerSimulator};
|
||||
use crate::cost::CostModel;
|
||||
use crate::data::{BenchmarkSnapshot, DataSet, DataSetError, PriceField};
|
||||
use crate::events::{AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent};
|
||||
use crate::events::{
|
||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
ProcessEventKind,
|
||||
};
|
||||
use crate::metrics::{BacktestMetrics, compute_backtest_metrics};
|
||||
use crate::portfolio::{CashReceivable, HoldingSummary, PortfolioState};
|
||||
use crate::rules::EquityRuleHooks;
|
||||
@@ -59,6 +62,7 @@ pub struct BacktestResult {
|
||||
pub fills: Vec<FillEvent>,
|
||||
pub position_events: Vec<PositionEvent>,
|
||||
pub account_events: Vec<AccountEvent>,
|
||||
pub process_events: Vec<ProcessEvent>,
|
||||
pub holdings_summary: Vec<HoldingSummary>,
|
||||
pub daily_holdings: Vec<HoldingSummary>,
|
||||
pub metrics: BacktestMetrics,
|
||||
@@ -82,6 +86,7 @@ pub struct BacktestDayProgress {
|
||||
pub orders: Vec<OrderEvent>,
|
||||
pub fills: Vec<FillEvent>,
|
||||
pub holdings: Vec<HoldingSummary>,
|
||||
pub process_events: Vec<ProcessEvent>,
|
||||
}
|
||||
|
||||
pub struct BacktestEngine<S, C, R> {
|
||||
@@ -166,6 +171,7 @@ where
|
||||
fills: Vec::new(),
|
||||
position_events: Vec::new(),
|
||||
account_events: Vec::new(),
|
||||
process_events: Vec::new(),
|
||||
equity_curve: Vec::new(),
|
||||
holdings_summary: Vec::new(),
|
||||
daily_holdings: Vec::new(),
|
||||
@@ -206,7 +212,32 @@ where
|
||||
portfolio: &portfolio,
|
||||
};
|
||||
let schedule_rules = self.strategy.schedule_rules();
|
||||
let mut process_events = Vec::new();
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PreBeforeTrading,
|
||||
"before_trading:pre",
|
||||
);
|
||||
self.strategy.before_trading(&daily_context)?;
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::BeforeTrading,
|
||||
"before_trading",
|
||||
);
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PostBeforeTrading,
|
||||
"before_trading:post",
|
||||
);
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PreOpenAuction,
|
||||
"open_auction:pre",
|
||||
);
|
||||
let mut auction_decision = collect_scheduled_decisions(
|
||||
&mut self.strategy,
|
||||
&scheduler,
|
||||
@@ -216,13 +247,32 @@ where
|
||||
&daily_context,
|
||||
)?;
|
||||
auction_decision.merge_from(self.strategy.open_auction(&daily_context)?);
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::OpenAuction,
|
||||
"open_auction",
|
||||
);
|
||||
let mut report = self.broker.execute(
|
||||
execution_date,
|
||||
&mut portfolio,
|
||||
&self.data,
|
||||
&auction_decision,
|
||||
)?;
|
||||
process_events.append(&mut report.process_events);
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PostOpenAuction,
|
||||
"open_auction:post",
|
||||
);
|
||||
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PreOnDay,
|
||||
"on_day:pre",
|
||||
);
|
||||
let mut decision = decision_slot
|
||||
.map(|(decision_idx, decision_date)| {
|
||||
self.strategy.on_day(&StrategyContext {
|
||||
@@ -249,10 +299,17 @@ where
|
||||
portfolio: &portfolio,
|
||||
},
|
||||
)?);
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::OnDay,
|
||||
"on_day",
|
||||
);
|
||||
|
||||
let intraday_report =
|
||||
let mut intraday_report =
|
||||
self.broker
|
||||
.execute(execution_date, &mut portfolio, &self.data, &decision)?;
|
||||
process_events.append(&mut intraday_report.process_events);
|
||||
report.order_events.extend(intraday_report.order_events);
|
||||
report.fill_events.extend(intraday_report.fill_events);
|
||||
report
|
||||
@@ -260,6 +317,12 @@ where
|
||||
.extend(intraday_report.position_events);
|
||||
report.account_events.extend(intraday_report.account_events);
|
||||
report.diagnostics.extend(intraday_report.diagnostics);
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PostOnDay,
|
||||
"on_day:post",
|
||||
);
|
||||
let daily_fill_count = report.fill_events.len();
|
||||
let day_orders = report.order_events.clone();
|
||||
let day_fills = report.fill_events.clone();
|
||||
@@ -275,8 +338,44 @@ where
|
||||
data: &self.data,
|
||||
portfolio: &portfolio,
|
||||
};
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PreAfterTrading,
|
||||
"after_trading:pre",
|
||||
);
|
||||
self.strategy.after_trading(&post_trade_context)?;
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::AfterTrading,
|
||||
"after_trading",
|
||||
);
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PostAfterTrading,
|
||||
"after_trading:post",
|
||||
);
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PreSettlement,
|
||||
"settlement:pre",
|
||||
);
|
||||
self.strategy.on_settlement(&post_trade_context)?;
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::Settlement,
|
||||
"settlement",
|
||||
);
|
||||
push_phase_event(
|
||||
&mut process_events,
|
||||
execution_date,
|
||||
ProcessEventKind::PostSettlement,
|
||||
"settlement:post",
|
||||
);
|
||||
|
||||
let benchmark =
|
||||
self.data
|
||||
@@ -296,6 +395,7 @@ where
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | ");
|
||||
let holdings_for_day = portfolio.holdings_summary(execution_date);
|
||||
let day_process_events = process_events.clone();
|
||||
|
||||
result.equity_curve.push(DailyEquityPoint {
|
||||
date: execution_date,
|
||||
@@ -335,7 +435,9 @@ where
|
||||
orders: day_orders,
|
||||
fills: day_fills,
|
||||
holdings: holdings_for_day,
|
||||
process_events: day_process_events,
|
||||
});
|
||||
result.process_events.extend(process_events);
|
||||
}
|
||||
|
||||
if let Some(last_date) = execution_dates.last().copied() {
|
||||
@@ -676,6 +778,22 @@ fn collect_scheduled_decisions<S: Strategy>(
|
||||
Ok(combined)
|
||||
}
|
||||
|
||||
fn push_phase_event(
|
||||
events: &mut Vec<ProcessEvent>,
|
||||
date: NaiveDate,
|
||||
kind: ProcessEventKind,
|
||||
detail: impl Into<String>,
|
||||
) {
|
||||
events.push(ProcessEvent {
|
||||
date,
|
||||
kind,
|
||||
order_id: None,
|
||||
symbol: None,
|
||||
side: None,
|
||||
detail: detail.into(),
|
||||
});
|
||||
}
|
||||
|
||||
mod date_format {
|
||||
use chrono::NaiveDate;
|
||||
use serde::Serializer;
|
||||
|
||||
@@ -89,3 +89,40 @@ pub struct AccountEvent {
|
||||
pub total_equity: f64,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ProcessEventKind {
|
||||
PreBeforeTrading,
|
||||
BeforeTrading,
|
||||
PostBeforeTrading,
|
||||
PreOpenAuction,
|
||||
OpenAuction,
|
||||
PostOpenAuction,
|
||||
PreOnDay,
|
||||
OnDay,
|
||||
PostOnDay,
|
||||
PreAfterTrading,
|
||||
AfterTrading,
|
||||
PostAfterTrading,
|
||||
PreSettlement,
|
||||
Settlement,
|
||||
PostSettlement,
|
||||
OrderPendingNew,
|
||||
OrderCreationPass,
|
||||
OrderUnsolicitedUpdate,
|
||||
Trade,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProcessEvent {
|
||||
#[serde(with = "date_format")]
|
||||
pub date: NaiveDate,
|
||||
pub kind: ProcessEventKind,
|
||||
#[serde(default)]
|
||||
pub order_id: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub symbol: Option<String>,
|
||||
#[serde(default)]
|
||||
pub side: Option<OrderSide>,
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ pub use engine::{
|
||||
BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError, BacktestResult,
|
||||
DailyEquityPoint,
|
||||
};
|
||||
pub use events::{AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent};
|
||||
pub use events::{
|
||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
ProcessEventKind,
|
||||
};
|
||||
pub use instrument::Instrument;
|
||||
pub use metrics::{BacktestMetrics, compute_backtest_metrics};
|
||||
pub use platform_expr_strategy::{PlatformExprStrategy, PlatformExprStrategyConfig};
|
||||
|
||||
@@ -6,8 +6,8 @@ use chrono::NaiveDate;
|
||||
use fidc_core::{
|
||||
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
|
||||
ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
||||
Instrument, PriceField, ScheduleRule, ScheduleStage, Strategy, StrategyContext,
|
||||
StrategyDecision,
|
||||
Instrument, PriceField, ProcessEventKind, ScheduleRule, ScheduleStage, Strategy,
|
||||
StrategyContext, StrategyDecision,
|
||||
};
|
||||
|
||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||
@@ -302,7 +302,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
||||
},
|
||||
);
|
||||
|
||||
engine.run().expect("backtest succeeds");
|
||||
let result = engine.run().expect("backtest succeeds");
|
||||
|
||||
assert_eq!(
|
||||
log.borrow().as_slice(),
|
||||
@@ -319,6 +319,30 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
||||
"settlement:2025-01-03",
|
||||
]
|
||||
);
|
||||
assert_eq!(result.process_events.len(), 30);
|
||||
assert_eq!(
|
||||
result.process_events[..15]
|
||||
.iter()
|
||||
.map(|event| &event.kind)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
&ProcessEventKind::PreBeforeTrading,
|
||||
&ProcessEventKind::BeforeTrading,
|
||||
&ProcessEventKind::PostBeforeTrading,
|
||||
&ProcessEventKind::PreOpenAuction,
|
||||
&ProcessEventKind::OpenAuction,
|
||||
&ProcessEventKind::PostOpenAuction,
|
||||
&ProcessEventKind::PreOnDay,
|
||||
&ProcessEventKind::OnDay,
|
||||
&ProcessEventKind::PostOnDay,
|
||||
&ProcessEventKind::PreAfterTrading,
|
||||
&ProcessEventKind::AfterTrading,
|
||||
&ProcessEventKind::PostAfterTrading,
|
||||
&ProcessEventKind::PreSettlement,
|
||||
&ProcessEventKind::Settlement,
|
||||
&ProcessEventKind::PostSettlement,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,8 +2,8 @@ use chrono::NaiveDate;
|
||||
use fidc_core::{
|
||||
BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel,
|
||||
ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, Instrument,
|
||||
IntradayExecutionQuote, OrderIntent, PortfolioState, PriceField, SlippageModel,
|
||||
StrategyDecision,
|
||||
IntradayExecutionQuote, OrderIntent, PortfolioState, PriceField, ProcessEventKind,
|
||||
SlippageModel, StrategyDecision,
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
@@ -416,6 +416,11 @@ fn broker_cancels_buy_when_open_hits_upper_limit() {
|
||||
.reason
|
||||
.contains("open at or above upper limit")
|
||||
);
|
||||
assert!(report.process_events.iter().any(|event| {
|
||||
event.kind == ProcessEventKind::OrderUnsolicitedUpdate
|
||||
&& event.symbol.as_deref() == Some("000002.SZ")
|
||||
&& event.side == Some(fidc_core::OrderSide::Buy)
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -989,6 +994,22 @@ fn broker_splits_intraday_quote_fills_and_tracks_commission_by_order() {
|
||||
.iter()
|
||||
.any(|item| item.contains("order_split_fill symbol=000002.SZ side=buy"))
|
||||
);
|
||||
assert_eq!(
|
||||
report
|
||||
.process_events
|
||||
.iter()
|
||||
.filter(|event| event.kind == ProcessEventKind::Trade)
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(report.process_events.iter().any(|event| {
|
||||
event.kind == ProcessEventKind::OrderPendingNew
|
||||
&& event.symbol.as_deref() == Some("000002.SZ")
|
||||
}));
|
||||
assert!(report.process_events.iter().any(|event| {
|
||||
event.kind == ProcessEventKind::OrderCreationPass
|
||||
&& event.symbol.as_deref() == Some("000002.SZ")
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user