统一第二版信号簿字段并提供原生校验入口
This commit is contained in:
@@ -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(())
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ use sha2::{Digest, Sha256};
|
|||||||
use crate::strategy::{OrderIntent, StrategyContext};
|
use crate::strategy::{OrderIntent, StrategyContext};
|
||||||
use crate::portfolio::PortfolioState;
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@@ -49,6 +49,7 @@ impl SignalAction {
|
|||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
pub struct SignalSnapshot {
|
pub struct SignalSnapshot {
|
||||||
|
pub signal_at: DateTime<Utc>,
|
||||||
pub decision_at: DateTime<Utc>,
|
pub decision_at: DateTime<Utc>,
|
||||||
pub input_as_of: DateTime<Utc>,
|
pub input_as_of: DateTime<Utc>,
|
||||||
pub input_available_at: DateTime<Utc>,
|
pub input_available_at: DateTime<Utc>,
|
||||||
@@ -65,7 +66,8 @@ pub struct SignalBook {
|
|||||||
pub schema: String,
|
pub schema: String,
|
||||||
pub version_sha256: String,
|
pub version_sha256: String,
|
||||||
pub generator_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 provenance: SignalProvenance,
|
||||||
pub frequency: SignalFrequency,
|
pub frequency: SignalFrequency,
|
||||||
pub expected_decisions: Vec<DateTime<Utc>>,
|
pub expected_decisions: Vec<DateTime<Utc>>,
|
||||||
@@ -90,6 +92,10 @@ impl SignalBook {
|
|||||||
pub fn content_sha256(&self) -> Result<String, String> {
|
pub fn content_sha256(&self) -> Result<String, String> {
|
||||||
let mut value=serde_json::to_value(self).map_err(|error|error.to_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");
|
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())?;
|
let raw=serde_json::to_vec(&value).map_err(|error|error.to_string())?;
|
||||||
Ok(format!("{:x}",Sha256::digest(raw)))
|
Ok(format!("{:x}",Sha256::digest(raw)))
|
||||||
}
|
}
|
||||||
@@ -100,6 +106,10 @@ impl SignalBook {
|
|||||||
{
|
{
|
||||||
return Err("signal_book_identity_invalid".into());
|
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
|
if self.expected_decisions.is_empty() || self.expected_decisions.len() > 100_000
|
||||||
|| self.expected_decisions.len() != self.snapshots.len()
|
|| self.expected_decisions.len() != self.snapshots.len()
|
||||||
{
|
{
|
||||||
@@ -113,11 +123,12 @@ impl SignalBook {
|
|||||||
return Err("signal_book_decisions_duplicate_or_unordered".into());
|
return Err("signal_book_decisions_duplicate_or_unordered".into());
|
||||||
}
|
}
|
||||||
previous = Some(*expected);
|
previous = Some(*expected);
|
||||||
if self.knowledge_cutoff >= *expected || snapshot.input_as_of > *expected
|
if self.knowledge_cutoff.is_some_and(|cutoff|cutoff>snapshot.signal_at)
|
||||||
|| snapshot.input_available_at > *expected || snapshot.input_as_of > snapshot.input_available_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.published_at < snapshot.generated_at || !valid_sha(&snapshot.input_sha256)
|
||||||
|| snapshot.generated_at < snapshot.input_available_at
|
|| 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());
|
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();
|
let source: DateTime<Utc> = "2025-01-06T15:00:00+08:00".parse().unwrap();
|
||||||
seal(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(),
|
model_sha256:Some("d".repeat(64)),
|
||||||
|
knowledge_cutoff: Some("2024-12-31T15:00:00+08:00".parse().unwrap()),
|
||||||
provenance: SignalProvenance::Reconstructed, frequency: SignalFrequency::Daily,
|
provenance: SignalProvenance::Reconstructed, frequency: SignalFrequency::Daily,
|
||||||
expected_decisions: vec![decision], snapshots: vec![SignalSnapshot {
|
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),
|
generated_at: decision + Duration::days(10), published_at: decision + Duration::days(10),
|
||||||
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 }],
|
||||||
@@ -309,7 +321,7 @@ mod tests {
|
|||||||
match field {
|
match field {
|
||||||
0 => value.snapshots[0].input_as_of = future,
|
0 => value.snapshots[0].input_as_of = future,
|
||||||
1 => value.snapshots[0].input_available_at = 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"));
|
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].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_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].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].generated_at=raw.snapshots[0].input_available_at;
|
||||||
raw.snapshots[0].published_at=raw.snapshots[0].generated_at;
|
raw.snapshots[0].published_at=raw.snapshots[0].generated_at;
|
||||||
raw.provenance=SignalProvenance::Observed;
|
raw.provenance=SignalProvenance::Observed;
|
||||||
|
|||||||
Reference in New Issue
Block a user