接入独立手工仓位时间线并保留原策略配置

This commit is contained in:
boris
2026-09-14 15:48:44 +08:00
parent fb8192a286
commit f8955bfb18
9 changed files with 561 additions and 50 deletions
+78 -23
View File
@@ -627,8 +627,18 @@ impl<S, C, R> BacktestEngine<S, C, R> {
}
}
pub fn with_observed_manual_executions(mut self, replay: crate::manual_execution::ManualExecutionReplay) -> Result<Self, BacktestError> {
pub fn with_observed_manual_executions(
mut self,
replay: crate::manual_execution::ManualExecutionReplay,
) -> Result<Self, BacktestError>
where
S: Strategy,
{
replay.validate().map_err(BacktestError::Execution)?;
self.strategy.bind_runtime_position_configuration(
&replay.position_exposure_events,
&replay.legacy_position_exposure_bps,
)?;
self.manual_execution_source = Some(std::sync::Arc::new(replay));
Ok(self)
}
@@ -5416,28 +5426,73 @@ mod tests {
fn observed_manual_replay(date: NaiveDate, times: &[(u32, u32)]) -> crate::manual_execution::ManualExecutionReplay {
use crate::manual_execution::*;
let utc = |local: NaiveDateTime| chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(local - chrono::Duration::hours(8), chrono::Utc);
let actions = times.iter().enumerate().map(|(index, &(hour, minute))| {
let observed = date.and_hms_opt(hour, minute, 0).unwrap();
let executed = if hour < 9 || (hour == 9 && minute < 25) {
date.pred_opt().unwrap().and_hms_opt(15, 20, 0).unwrap()
} else { observed.min(date.and_hms_opt(15, 20, 0).unwrap()) };
let created = utc(executed - chrono::Duration::seconds(1));
ManualExecutionAction { action_id: format!("manual-action-{index}"), source: ManualExecutionSource::ManualSecurityTrade,
audit_event_ids: vec![format!("manual-audit-{index}")], confirmed_at: created, confirmation_observed_at: created,
outcome: ManualActionOutcome::OrdersTerminal, orders: vec![ManualExecutionOrder {
order_id: format!("manual-order-{index}"), broker_order_id: Some(format!("broker-order-{index}")), source_adapter: Some("gt-api".into()),
symbol: SYMBOL.into(), side: OrderSide::Buy, quantity: 100, order_created_at: created,
terminal_observed_at: utc(observed), terminal_status: ManualOrderTerminalStatus::Filled,
fills: vec![ManualExecutionFill { trade_id: format!("manual-trade-{index}"), observation_event_id: format!("manual-receipt-{index}"),
observation_sequence: index as u64 + 1, fee_observation_event_id: format!("manual-receipt-{index}"), fee_observation_sequence: index as u64 + 1,
fee_observed_at: utc(observed), trade_date: executed.date(), executed_at: utc(executed), observed_at: utc(observed),
timestamp_precision: ManualTimestampPrecision::Second, quantity: 100, price: 10.into(), commission: Some("1.5".parse().unwrap()),
stamp_tax: None, transfer_fee: None, total_fee: "1.5".parse().unwrap() }],
}] }
}).collect();
let mut replay = ManualExecutionReplay { schema: MANUAL_REPLAY_SCHEMA.into(), runtime_id: "manual-runtime".into(), account_id: "manual-account".into(),
source_contract_sha256: "a".repeat(64), content_sha256: String::new(), observation_cutoff: utc(date.and_hms_opt(23, 59, 59).unwrap()), actions };
let utc = |local: NaiveDateTime| {
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
local - chrono::Duration::hours(8),
chrono::Utc,
)
};
let actions = times
.iter()
.enumerate()
.map(|(index, &(hour, minute))| {
let observed = date.and_hms_opt(hour, minute, 0).unwrap();
let executed = if hour < 9 || (hour == 9 && minute < 25) {
date.pred_opt().unwrap().and_hms_opt(15, 20, 0).unwrap()
} else {
observed.min(date.and_hms_opt(15, 20, 0).unwrap())
};
let created = utc(executed - chrono::Duration::seconds(1));
ManualExecutionAction {
action_id: format!("manual-action-{index}"),
source: ManualExecutionSource::ManualSecurityTrade,
audit_event_ids: vec![format!("manual-audit-{index}")],
confirmed_at: created,
confirmation_observed_at: created,
outcome: ManualActionOutcome::OrdersTerminal,
orders: vec![ManualExecutionOrder {
order_id: format!("manual-order-{index}"),
broker_order_id: Some(format!("broker-order-{index}")),
source_adapter: Some("gt-api".into()),
symbol: SYMBOL.into(),
side: OrderSide::Buy,
quantity: 100,
order_created_at: created,
terminal_observed_at: utc(observed),
terminal_status: ManualOrderTerminalStatus::Filled,
fills: vec![ManualExecutionFill {
trade_id: format!("manual-trade-{index}"),
observation_event_id: format!("manual-receipt-{index}"),
observation_sequence: index as u64 + 1,
fee_observation_event_id: format!("manual-receipt-{index}"),
fee_observation_sequence: index as u64 + 1,
fee_observed_at: utc(observed),
trade_date: executed.date(),
executed_at: utc(executed),
observed_at: utc(observed),
timestamp_precision: ManualTimestampPrecision::Second,
quantity: 100,
price: 10.into(),
commission: Some("1.5".parse().unwrap()),
stamp_tax: None,
transfer_fee: None,
total_fee: "1.5".parse().unwrap(),
}],
}],
}
})
.collect();
let mut replay = ManualExecutionReplay {
schema: MANUAL_REPLAY_SCHEMA.into(),
runtime_id: "manual-runtime".into(),
account_id: "manual-account".into(),
source_contract_sha256: "a".repeat(64),
content_sha256: String::new(),
observation_cutoff: utc(date.and_hms_opt(23, 59, 59).unwrap()),
actions,
position_exposure_events: vec![],
legacy_position_exposure_bps: BTreeMap::new(),
};
replay.content_sha256 = replay.content_digest().unwrap();
replay.validate().unwrap();
replay