From e6746a7a0e92ef01ab6d7ee9fc380fc3042572c8 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 10 Sep 2026 23:27:43 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E7=AC=AC=E4=BA=8C=E7=89=88?= =?UTF-8?q?=E4=BF=A1=E5=8F=B7=E7=B0=BF=E5=AD=97=E6=AE=B5=E5=B9=B6=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E5=8E=9F=E7=94=9F=E6=A0=A1=E9=AA=8C=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/examples/signal-book-kernel.rs | 15 ++++++++++ crates/fidc-core/src/signal_contract.rs | 29 ++++++++++++++----- 2 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 crates/fidc-core/examples/signal-book-kernel.rs diff --git a/crates/fidc-core/examples/signal-book-kernel.rs b/crates/fidc-core/examples/signal-book-kernel.rs new file mode 100644 index 0000000..b08dc50 --- /dev/null +++ b/crates/fidc-core/examples/signal-book-kernel.rs @@ -0,0 +1,15 @@ +use std::io::{Read, Write}; + +fn main() -> Result<(), Box> { + let mut raw=Vec::new(); + std::io::stdin().take(64*1024*1024+1).read_to_end(&mut raw)?; + if raw.len()>64*1024*1024 {return Err("signal_book_transport_limit".into());} + let book:fidc_core::signal_contract::SignalBook=serde_json::from_slice(&raw)?; + let version=book.content_sha256()?; + let validated=book.validate()?; + let result=serde_json::json!({"schema":fidc_core::signal_contract::SIGNAL_BOOK_SCHEMA, + "versionSha256":version,"symbols":validated.symbols(), + "onlineAllowed":validated.require_observed().is_ok()}); + std::io::stdout().write_all(serde_json::to_string(&result)?.as_bytes())?; + Ok(()) +} diff --git a/crates/fidc-core/src/signal_contract.rs b/crates/fidc-core/src/signal_contract.rs index 07f4247..bfc83ef 100644 --- a/crates/fidc-core/src/signal_contract.rs +++ b/crates/fidc-core/src/signal_contract.rs @@ -10,7 +10,7 @@ use sha2::{Digest, Sha256}; use crate::strategy::{OrderIntent, StrategyContext}; use crate::portfolio::PortfolioState; -pub const SIGNAL_BOOK_SCHEMA: &str = "fidc.signal-book/v1"; +pub const SIGNAL_BOOK_SCHEMA: &str = "fidc.signal-book/v2"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] @@ -49,6 +49,7 @@ impl SignalAction { #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct SignalSnapshot { + pub signal_at: DateTime, pub decision_at: DateTime, pub input_as_of: DateTime, pub input_available_at: DateTime, @@ -65,7 +66,8 @@ pub struct SignalBook { pub schema: String, pub version_sha256: String, pub generator_sha256: String, - pub knowledge_cutoff: DateTime, + pub model_sha256: Option, + pub knowledge_cutoff: Option>, pub provenance: SignalProvenance, pub frequency: SignalFrequency, pub expected_decisions: Vec>, @@ -90,6 +92,10 @@ impl SignalBook { pub fn content_sha256(&self) -> Result { 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"); + for snapshot in value["snapshots"].as_array_mut().ok_or("signal_snapshots_required")? { + let object=snapshot.as_object_mut().ok_or("signal_snapshot_object_required")?; + object.remove("generatedAt"); object.remove("publishedAt"); + } let raw=serde_json::to_vec(&value).map_err(|error|error.to_string())?; Ok(format!("{:x}",Sha256::digest(raw))) } @@ -100,6 +106,10 @@ impl SignalBook { { return Err("signal_book_identity_invalid".into()); } + if self.model_sha256.is_some()!=self.knowledge_cutoff.is_some() + || self.model_sha256.as_ref().is_some_and(|value|!valid_sha(value)) { + return Err("signal_model_training_identity_incomplete".into()); + } if self.expected_decisions.is_empty() || self.expected_decisions.len() > 100_000 || self.expected_decisions.len() != self.snapshots.len() { @@ -113,11 +123,12 @@ impl SignalBook { return Err("signal_book_decisions_duplicate_or_unordered".into()); } previous = Some(*expected); - if self.knowledge_cutoff >= *expected || snapshot.input_as_of > *expected - || snapshot.input_available_at > *expected || snapshot.input_as_of > snapshot.input_available_at + if self.knowledge_cutoff.is_some_and(|cutoff|cutoff>snapshot.signal_at) + || snapshot.input_as_of > snapshot.input_available_at + || snapshot.input_available_at > snapshot.signal_at || snapshot.signal_at > *expected || snapshot.published_at < snapshot.generated_at || !valid_sha(&snapshot.input_sha256) || snapshot.generated_at < snapshot.input_available_at - || snapshot.generated_at < self.knowledge_cutoff + || self.knowledge_cutoff.is_some_and(|cutoff|snapshot.generated_at = "2025-01-06T15:00:00+08:00".parse().unwrap(); seal(SignalBook { 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(), + model_sha256:Some("d".repeat(64)), + knowledge_cutoff: Some("2024-12-31T15:00:00+08:00".parse().unwrap()), provenance: SignalProvenance::Reconstructed, frequency: SignalFrequency::Daily, expected_decisions: vec![decision], snapshots: vec![SignalSnapshot { - decision_at: decision, input_as_of: source, input_available_at: source, + signal_at:source, decision_at: decision, input_as_of: source, input_available_at: source, generated_at: decision + Duration::days(10), published_at: decision + Duration::days(10), input_sha256: "c".repeat(64), complete_targets: true, actions: vec![SignalAction::TargetWeight { symbol: "000001.SZ".into(), weight: 0.5 }], @@ -309,7 +321,7 @@ mod tests { match field { 0 => value.snapshots[0].input_as_of = future, 1 => value.snapshots[0].input_available_at = future, - _ => value.knowledge_cutoff = future, + _ => value.knowledge_cutoff = Some(future), } assert!(value.validate().unwrap_err().contains("future")); } @@ -415,6 +427,7 @@ mod tests { 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].signal_at=raw.snapshots[0].input_available_at; 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;