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

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
+40 -2
View File
@@ -12,7 +12,7 @@ use crate::events::OrderSide;
use crate::{DataSet, FixedMoney, PortfolioState};
use rust_decimal::prelude::ToPrimitive;
pub const MANUAL_REPLAY_SCHEMA: &str = "fidc.observed-manual-executions/v2";
pub const MANUAL_REPLAY_SCHEMA: &str = "fidc.observed-manual-executions/v3";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
@@ -24,6 +24,10 @@ pub struct ManualExecutionReplay {
pub content_sha256: String,
pub observation_cutoff: DateTime<Utc>,
pub actions: Vec<ManualExecutionAction>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub position_exposure_events: Vec<crate::position_exposure::PositionExposureEvent>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub legacy_position_exposure_bps: BTreeMap<NaiveDate, i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@@ -162,6 +166,19 @@ fn identifier(value: &str) -> Result<(), String> {
}
impl ManualExecutionReplay {
/// Market/indicator data is needed for securities whose observed fills
/// change the portfolio. A rejected, never-filled order is not data demand.
pub fn required_data_symbols(&self) -> Result<BTreeSet<String>, String> {
self.validate()?;
Ok(self
.actions
.iter()
.flat_map(|action| &action.orders)
.filter(|order| !order.fills.is_empty())
.map(|order| order.symbol.clone())
.collect())
}
pub fn observations(&self) -> Result<Vec<ManualFillObservation<'_>>, String> {
self.validate()?;
let mut observations = Vec::new();
@@ -190,9 +207,30 @@ impl ManualExecutionReplay {
}
pub fn validate(&self) -> Result<(), String> {
if self.schema != MANUAL_REPLAY_SCHEMA {
if self.schema != MANUAL_REPLAY_SCHEMA
&& self.schema != "fidc.observed-manual-executions/v2"
{
return Err("unsupported manual replay schema".into());
}
if self.schema == "fidc.observed-manual-executions/v2"
&& (!self.position_exposure_events.is_empty()
|| !self.legacy_position_exposure_bps.is_empty())
{
return Err("runtime configuration requires manual replay v3".into());
}
crate::position_exposure::PositionExposureTimeline::from_events(
&self.position_exposure_events,
)?;
if self.position_exposure_events.iter().any(|event| event.effective_at > self.observation_cutoff) {
return Err("observed runtime position event is after the evidence cutoff".into());
}
if self
.legacy_position_exposure_bps
.values()
.any(|value| !(0..=10000).contains(value))
{
return Err("legacy manual exposure is outside 0..10000 bps".into());
}
identifier(&self.runtime_id)?;
identifier(&self.account_id)?;
if self.source_contract_sha256.len() != 64