统一第二版信号簿字段并提供原生校验入口

This commit is contained in:
boris
2026-09-10 23:27:43 +08:00
parent 75e5e32281
commit e6746a7a0e
2 changed files with 36 additions and 8 deletions
@@ -0,0 +1,15 @@
use std::io::{Read, Write};
fn main() -> Result<(), Box<dyn std::error::Error>> {
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(())
}
+21 -8
View File
@@ -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<Utc>,
pub decision_at: DateTime<Utc>,
pub input_as_of: DateTime<Utc>,
pub input_available_at: DateTime<Utc>,
@@ -65,7 +66,8 @@ pub struct SignalBook {
pub schema: String,
pub version_sha256: String,
pub generator_sha256: String,
pub knowledge_cutoff: DateTime<Utc>,
pub model_sha256: Option<String>,
pub knowledge_cutoff: Option<DateTime<Utc>>,
pub provenance: SignalProvenance,
pub frequency: SignalFrequency,
pub expected_decisions: Vec<DateTime<Utc>>,
@@ -90,6 +92,10 @@ 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");
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<cutoff)
{
return Err("signal_book_future_or_invalid_input".into());
}
@@ -273,10 +284,11 @@ mod tests {
let source: DateTime<Utc> = "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;