绑定信号簿内容摘要并按决策日校验日线输入
This commit is contained in:
@@ -5,6 +5,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
|
|
||||||
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use crate::strategy::{OrderIntent, StrategyContext};
|
use crate::strategy::{OrderIntent, StrategyContext};
|
||||||
use crate::portfolio::PortfolioState;
|
use crate::portfolio::PortfolioState;
|
||||||
@@ -86,6 +87,13 @@ fn shanghai(value: DateTime<Utc>) -> NaiveDateTime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SignalBook {
|
impl SignalBook {
|
||||||
|
pub fn content_sha256(&self) -> Result<String, String> {
|
||||||
|
let mut value=serde_json::to_value(self).map_err(|error|error.to_string())?;
|
||||||
|
value.as_object_mut().ok_or("signal_book_object_required")?.remove("versionSha256");
|
||||||
|
let raw=serde_json::to_vec(&value).map_err(|error|error.to_string())?;
|
||||||
|
Ok(format!("{:x}",Sha256::digest(raw)))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validate(self) -> Result<ValidatedSignalBook, String> {
|
pub fn validate(self) -> Result<ValidatedSignalBook, String> {
|
||||||
if self.schema != SIGNAL_BOOK_SCHEMA || !valid_sha(&self.version_sha256)
|
if self.schema != SIGNAL_BOOK_SCHEMA || !valid_sha(&self.version_sha256)
|
||||||
|| !valid_sha(&self.generator_sha256)
|
|| !valid_sha(&self.generator_sha256)
|
||||||
@@ -154,6 +162,9 @@ impl SignalBook {
|
|||||||
}
|
}
|
||||||
index.insert(shanghai(*expected), number);
|
index.insert(shanghai(*expected), number);
|
||||||
}
|
}
|
||||||
|
if self.content_sha256()? != self.version_sha256 {
|
||||||
|
return Err("signal_book_content_hash_mismatch".into());
|
||||||
|
}
|
||||||
Ok(ValidatedSignalBook { book: self, index })
|
Ok(ValidatedSignalBook { book: self, index })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +190,7 @@ 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())?;
|
||||||
if ctx.is_lagged_execution() && shanghai(snapshot.input_as_of) > ctx.decision_date.and_hms_opt(15,0,0).expect("completed decision session") {
|
if ctx.is_lagged_execution() && shanghai(snapshot.input_as_of).date() > ctx.decision_date {
|
||||||
return Err("next_open_signal_contains_execution_session_inputs".into());
|
return Err("next_open_signal_contains_execution_session_inputs".into());
|
||||||
}
|
}
|
||||||
Ok(snapshot)
|
Ok(snapshot)
|
||||||
@@ -260,7 +271,7 @@ mod tests {
|
|||||||
fn book() -> SignalBook {
|
fn book() -> SignalBook {
|
||||||
let decision: DateTime<Utc> = "2025-01-07T09:30:00+08:00".parse().unwrap();
|
let decision: DateTime<Utc> = "2025-01-07T09:30:00+08:00".parse().unwrap();
|
||||||
let source: DateTime<Utc> = "2025-01-06T15:00:00+08:00".parse().unwrap();
|
let source: DateTime<Utc> = "2025-01-06T15:00:00+08:00".parse().unwrap();
|
||||||
SignalBook {
|
seal(SignalBook {
|
||||||
schema: SIGNAL_BOOK_SCHEMA.into(), version_sha256: "a".repeat(64), generator_sha256: "b".repeat(64),
|
schema: SIGNAL_BOOK_SCHEMA.into(), version_sha256: "a".repeat(64), generator_sha256: "b".repeat(64),
|
||||||
knowledge_cutoff: "2024-12-31T15:00:00+08:00".parse().unwrap(),
|
knowledge_cutoff: "2024-12-31T15:00:00+08:00".parse().unwrap(),
|
||||||
provenance: SignalProvenance::Reconstructed, frequency: SignalFrequency::Daily,
|
provenance: SignalProvenance::Reconstructed, frequency: SignalFrequency::Daily,
|
||||||
@@ -270,7 +281,12 @@ mod tests {
|
|||||||
input_sha256: "c".repeat(64), complete_targets: true,
|
input_sha256: "c".repeat(64), complete_targets: true,
|
||||||
actions: vec![SignalAction::TargetWeight { symbol: "000001.SZ".into(), weight: 0.5 }],
|
actions: vec![SignalAction::TargetWeight { symbol: "000001.SZ".into(), weight: 0.5 }],
|
||||||
}],
|
}],
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seal(mut book:SignalBook)->SignalBook {
|
||||||
|
book.version_sha256=book.content_sha256().unwrap();
|
||||||
|
book
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -282,7 +298,7 @@ mod tests {
|
|||||||
assert!(observed.clone().validate().unwrap_err().contains("not_available"));
|
assert!(observed.clone().validate().unwrap_err().contains("not_available"));
|
||||||
observed.snapshots[0].generated_at = observed.snapshots[0].decision_at;
|
observed.snapshots[0].generated_at = observed.snapshots[0].decision_at;
|
||||||
observed.snapshots[0].published_at = observed.snapshots[0].decision_at;
|
observed.snapshots[0].published_at = observed.snapshots[0].decision_at;
|
||||||
observed.validate().unwrap().require_observed().unwrap();
|
seal(observed).validate().unwrap().require_observed().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -352,7 +368,7 @@ mod tests {
|
|||||||
let mut raw = book();
|
let mut raw = book();
|
||||||
raw.snapshots[0].complete_targets = false;
|
raw.snapshots[0].complete_targets = false;
|
||||||
raw.snapshots[0].actions = vec![SignalAction::Reduce {symbol:"000001.SZ".into(),remaining_ratio:0.5}];
|
raw.snapshots[0].actions = vec![SignalAction::Reduce {symbol:"000001.SZ".into(),remaining_ratio:0.5}];
|
||||||
let value = raw.validate().unwrap();
|
let value = seal(raw).validate().unwrap();
|
||||||
let day = NaiveDate::from_ymd_opt(2025,1,3).unwrap();
|
let day = NaiveDate::from_ymd_opt(2025,1,3).unwrap();
|
||||||
for (held, expected) in [(1000,500),(3000,1500)] {
|
for (held, expected) in [(1000,500),(3000,1500)] {
|
||||||
let mut portfolio = PortfolioState::new(100_000.0);
|
let mut portfolio = PortfolioState::new(100_000.0);
|
||||||
@@ -368,7 +384,7 @@ mod tests {
|
|||||||
fn empty_complete_snapshot_clears_only_that_accounts_holdings() {
|
fn empty_complete_snapshot_clears_only_that_accounts_holdings() {
|
||||||
let mut raw = book();
|
let mut raw = book();
|
||||||
raw.snapshots[0].actions.clear();
|
raw.snapshots[0].actions.clear();
|
||||||
let value = raw.validate().unwrap();
|
let value = seal(raw).validate().unwrap();
|
||||||
let day = NaiveDate::from_ymd_opt(2025,1,3).unwrap();
|
let day = NaiveDate::from_ymd_opt(2025,1,3).unwrap();
|
||||||
let mut portfolio = PortfolioState::new(100_000.0);
|
let mut portfolio = PortfolioState::new(100_000.0);
|
||||||
portfolio.position_mut("000002.SZ").buy(day,200,10.0);
|
portfolio.position_mut("000002.SZ").buy(day,200,10.0);
|
||||||
@@ -383,4 +399,25 @@ mod tests {
|
|||||||
assert!(!config.rotation_enabled && config.signal_book.is_some());
|
assert!(!config.rotation_enabled && config.signal_book.is_some());
|
||||||
assert!(matches!(config.explicit_actions.as_slice(),[crate::PlatformTradeAction::ConsumeSignal]));
|
assert!(matches!(config.explicit_actions.as_slice(),[crate::PlatformTradeAction::ConsumeSignal]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changed_valid_contents_must_not_reuse_a_version_hash() {
|
||||||
|
let mut raw=book();
|
||||||
|
raw.snapshots[0].actions=vec![SignalAction::TargetWeight{symbol:"000001.SZ".into(),weight:0.4}];
|
||||||
|
assert_eq!(raw.clone().validate().unwrap_err(),"signal_book_content_hash_mismatch");
|
||||||
|
seal(raw).validate().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completed_daily_inputs_may_be_published_after_market_close() {
|
||||||
|
let mut raw=book();
|
||||||
|
raw.expected_decisions=vec!["2026-07-07T09:30:00+08:00".parse().unwrap()];
|
||||||
|
raw.snapshots[0].decision_at=raw.expected_decisions[0];
|
||||||
|
raw.snapshots[0].input_as_of="2026-07-06T15:30:00+08:00".parse().unwrap();
|
||||||
|
raw.snapshots[0].input_available_at="2026-07-06T16:00:00+08:00".parse().unwrap();
|
||||||
|
raw.snapshots[0].generated_at=raw.snapshots[0].input_available_at;
|
||||||
|
raw.snapshots[0].published_at=raw.snapshots[0].generated_at;
|
||||||
|
raw.provenance=SignalProvenance::Observed;
|
||||||
|
seal(raw).validate().unwrap().require_observed().unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user