fix: bind signal availability to the actual consumption clock
This commit is contained in:
@@ -288,13 +288,22 @@ impl ValidatedSignalBook {
|
|||||||
|
|
||||||
pub fn snapshot_for(&self, ctx: &StrategyContext<'_>) -> Result<&SignalSnapshot, String> {
|
pub fn snapshot_for(&self, ctx: &StrategyContext<'_>) -> Result<&SignalSnapshot, String> {
|
||||||
let snapshot = self.snapshot_at(ctx.execution_date, ctx.current_time(), ctx.is_lagged_execution())?;
|
let snapshot = self.snapshot_at(ctx.execution_date, ctx.current_time(), ctx.is_lagged_execution())?;
|
||||||
let logical_clock=ctx.current_datetime().filter(|at|at.date()==ctx.decision_date)
|
if self.book.provenance == SignalProvenance::Observed && ctx.current_datetime().is_none() {
|
||||||
|
return Err("observed_signal_consumption_clock_missing".into());
|
||||||
|
}
|
||||||
|
let consumption_clock=ctx.current_datetime()
|
||||||
.unwrap_or(ctx.decision_date.and_hms_opt(15,0,0).expect("completed decision session"));
|
.unwrap_or(ctx.decision_date.and_hms_opt(15,0,0).expect("completed decision session"));
|
||||||
let lagged_daily=ctx.is_lagged_execution() && self.book.frequency==SignalFrequency::Daily;
|
let lagged_daily=ctx.is_lagged_execution() && self.book.frequency==SignalFrequency::Daily;
|
||||||
if (lagged_daily && shanghai(snapshot.input_as_of).date()>ctx.decision_date)
|
if lagged_daily && shanghai(snapshot.input_as_of).date()>ctx.decision_date {
|
||||||
|| (!lagged_daily && shanghai(snapshot.signal_at)>logical_clock) {
|
|
||||||
return Err("next_open_signal_contains_execution_session_inputs".into());
|
return Err("next_open_signal_contains_execution_session_inputs".into());
|
||||||
}
|
}
|
||||||
|
if shanghai(snapshot.input_available_at)>consumption_clock || shanghai(snapshot.signal_at)>consumption_clock {
|
||||||
|
return Err("signal_not_available_at_consumption_clock".into());
|
||||||
|
}
|
||||||
|
if self.book.provenance == SignalProvenance::Observed
|
||||||
|
&& (shanghai(snapshot.generated_at)>consumption_clock || shanghai(snapshot.published_at)>consumption_clock) {
|
||||||
|
return Err("observed_signal_published_after_consumption_clock".into());
|
||||||
|
}
|
||||||
Ok(snapshot)
|
Ok(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,6 +402,56 @@ mod tests {
|
|||||||
book
|
book
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn at_context<T>(at: Option<NaiveDateTime>, action: impl FnOnce(&StrategyContext<'_>) -> T) -> T {
|
||||||
|
let data = crate::DataSet::from_components(vec![], vec![], vec![], vec![], vec![]).unwrap();
|
||||||
|
let portfolio = PortfolioState::new(10_000.0);
|
||||||
|
let symbols = BTreeSet::new();
|
||||||
|
action(&StrategyContext {
|
||||||
|
execution_date: NaiveDate::from_ymd_opt(2025,1,7).unwrap(),
|
||||||
|
decision_date: NaiveDate::from_ymd_opt(2025,1,6).unwrap(), decision_index:0,
|
||||||
|
data:&data, portfolio:&portfolio, futures_account:None, open_orders:&[],
|
||||||
|
dynamic_universe:None, subscriptions:&symbols, process_events:&[], active_process_event:None,
|
||||||
|
active_datetime:at, order_events:&[], fills:&[],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn observed_next_open_never_backdates_a_morning_publication_into_yesterdays_orders() {
|
||||||
|
let mut raw = book();
|
||||||
|
raw.provenance=SignalProvenance::Observed;
|
||||||
|
raw.snapshots[0].generated_at="2025-01-07T08:45:00+08:00".parse().unwrap();
|
||||||
|
raw.snapshots[0].published_at="2025-01-07T08:46:00+08:00".parse().unwrap();
|
||||||
|
let value=seal(raw).validate().unwrap();
|
||||||
|
for clock in ["2025-01-06 15:00:00", "2025-01-07 08:45:00"] {
|
||||||
|
at_context(Some(clock.parse().unwrap()), |ctx| {
|
||||||
|
assert_eq!(value.intents(ctx).unwrap_err(),"observed_signal_published_after_consumption_clock");
|
||||||
|
assert!(ctx.portfolio.positions().is_empty());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
at_context(Some("2025-01-07 09:30:00".parse().unwrap()), |ctx| {
|
||||||
|
assert_eq!(value.intents(ctx).unwrap().len(),1);
|
||||||
|
assert!(ctx.portfolio.positions().is_empty());
|
||||||
|
});
|
||||||
|
at_context(None, |ctx| assert_eq!(value.intents(ctx).unwrap_err(),"observed_signal_consumption_clock_missing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reconstruction_ignores_research_wall_clock_but_never_early_input_availability() {
|
||||||
|
let value=book().validate().unwrap();
|
||||||
|
at_context(Some("2025-01-06 15:00:00".parse().unwrap()), |ctx| assert!(value.intents(ctx).is_ok()));
|
||||||
|
at_context(Some("2025-01-06 14:59:59".parse().unwrap()), |ctx| {
|
||||||
|
assert_eq!(value.intents(ctx).unwrap_err(),"signal_not_available_at_consumption_clock");
|
||||||
|
});
|
||||||
|
let mut raw=book();
|
||||||
|
raw.snapshots[0].input_as_of="2025-01-07T08:30:00+08:00".parse().unwrap();
|
||||||
|
raw.snapshots[0].input_available_at=raw.snapshots[0].input_as_of;
|
||||||
|
raw.snapshots[0].signal_at=raw.snapshots[0].input_as_of;
|
||||||
|
let value=seal(raw).validate().unwrap();
|
||||||
|
at_context(Some("2025-01-07 09:30:00".parse().unwrap()), |ctx| {
|
||||||
|
assert_eq!(value.intents(ctx).unwrap_err(),"next_open_signal_contains_execution_session_inputs");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn historical_reconstruction_is_not_online_publication() {
|
fn historical_reconstruction_is_not_online_publication() {
|
||||||
let validated = book().validate().unwrap();
|
let validated = book().validate().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user