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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user