#![forbid(unsafe_code)]
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MarketObservation {
pub date: String,
pub cash: f64,
pub symbols: Vec<SymbolSnapshot>,
pub portfolio: Vec<PositionState>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SymbolSnapshot {
pub symbol: String,
pub close_history: Vec<f64>,
#[serde(default)]
pub fundamentals: BTreeMap<String, f64>,
#[serde(default)]
pub news: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PositionState {
pub symbol: String,
pub shares: f64,
pub avg_price: f64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Decision {
pub orders: Vec<Order>,
#[serde(default)]
pub reasoning: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost: Option<DecisionCost>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DecisionCost {
#[serde(default)]
pub cost_usd: f64,
#[serde(default)]
pub tokens_in: u64,
#[serde(default)]
pub tokens_out: u64,
#[serde(default)]
pub reasoning_tokens: u64,
}
impl DecisionCost {
pub fn billable_units(&self) -> f64 {
if self.cost_usd > 0.0 {
self.cost_usd
} else {
(self.tokens_in + self.tokens_out) as f64
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Order {
pub symbol: String,
pub action: Action,
pub target_weight: f64,
#[serde(default = "default_confidence")]
pub confidence: f64,
#[serde(default)]
pub rationale: String,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Action {
Buy,
Sell,
Hold,
Close,
}
fn default_confidence() -> f64 {
0.5
}
pub const DECISION_SCHEMA_PATH: &str = "crates/sharpebench-protocol/schema/decision.schema.json";
pub fn decision_from_wire(json: &str) -> Result<Decision, String> {
serde_json::from_str(json).map_err(|error| {
format!(
"decision rejected by the closed wire contract: {error}. \
Unknown fields are rejected rather than ignored; validate against {DECISION_SCHEMA_PATH} \
(additionalProperties: false) and move any extra payload into `reasoning` or `cost`."
)
})
}
impl Decision {
pub fn validate_for(&self, observation: &MarketObservation) -> Result<(), String> {
let offered = observation
.symbols
.iter()
.map(|snapshot| snapshot.symbol.as_str())
.collect::<std::collections::BTreeSet<_>>();
let mut seen = std::collections::BTreeSet::new();
for (index, order) in self.orders.iter().enumerate() {
if !offered.contains(order.symbol.as_str()) {
return Err(format!(
"orders[{index}].symbol {:?} was not observed",
order.symbol
));
}
if !seen.insert(order.symbol.as_str()) {
return Err(format!("duplicate order for symbol {:?}", order.symbol));
}
if !order.target_weight.is_finite() || order.target_weight.abs() > 1.0 {
return Err(format!(
"orders[{index}].target_weight must be finite and in [-1, 1]"
));
}
if !order.confidence.is_finite() || !(0.0..=1.0).contains(&order.confidence) {
return Err(format!(
"orders[{index}].confidence must be finite and in [0, 1]"
));
}
}
if let Some(cost) = self.cost {
if !cost.cost_usd.is_finite() || cost.cost_usd < 0.0 {
return Err("cost.cost_usd must be finite and nonnegative".to_string());
}
}
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DecisionStep {
pub step: usize,
pub observation_id: String,
pub decision: Decision,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunTrajectory {
pub window_start: usize,
pub window_end: usize,
pub seed: u64,
pub steps: Vec<DecisionStep>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DeclaredMandate {
AbsoluteReturn,
RelativeTo { benchmark_id: String },
DrawdownCapped { max_per_run_drawdown: f64 },
#[serde(rename = "outperform_buy_and_hold", alias = "long_only_beta")]
OutperformBuyAndHold,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AgentTrajectory {
pub agent_id: String,
#[serde(default)]
pub in_sample_trials: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub declared_mandate: Option<DeclaredMandate>,
pub runs: Vec<RunTrajectory>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn observation_and_decision_roundtrip() {
let obs = MarketObservation {
date: "2025-01-01".to_string(),
cash: 1.0,
symbols: vec![SymbolSnapshot {
symbol: "A".to_string(),
close_history: vec![1.0, 2.0],
fundamentals: Default::default(),
news: vec!["headline".to_string()],
}],
portfolio: vec![PositionState {
symbol: "A".to_string(),
shares: 1.0,
avg_price: 2.0,
}],
};
let back: MarketObservation =
serde_json::from_str(&serde_json::to_string(&obs).unwrap()).unwrap();
assert_eq!(back.symbols[0].symbol, "A");
let d = Decision {
orders: vec![Order {
symbol: "A".to_string(),
action: Action::Buy,
target_weight: 0.5,
confidence: 0.9,
rationale: "trailing breakout".to_string(),
}],
reasoning: "r".to_string(),
cost: None,
};
let db: Decision = serde_json::from_str(&serde_json::to_string(&d).unwrap()).unwrap();
assert_eq!(db.orders[0].action, Action::Buy);
assert_eq!(db.orders[0].rationale, "trailing breakout");
let legacy = r#"{"orders":[{"symbol":"A","action":"buy","target_weight":0.5}]}"#;
let parsed: Decision = serde_json::from_str(legacy).unwrap();
assert_eq!(parsed.orders[0].rationale, "");
assert!((parsed.orders[0].confidence - 0.5).abs() < 1e-12);
assert!(parsed.cost.is_none());
}
#[test]
fn decision_cost_channel_parses_and_reduces() {
let with_cost = r#"{"orders":[],"reasoning":"","cost":{"cost_usd":0.42,
"tokens_in":1200,"tokens_out":300,"reasoning_tokens":180}}"#;
let d: Decision = serde_json::from_str(with_cost).unwrap();
let c = d.cost.expect("cost channel present");
assert!((c.cost_usd - 0.42).abs() < 1e-12);
assert_eq!(c.tokens_in, 1200);
assert!((c.billable_units() - 0.42).abs() < 1e-12);
let tokens_only = DecisionCost {
cost_usd: 0.0,
tokens_in: 1000,
tokens_out: 250,
reasoning_tokens: 200,
};
assert!((tokens_only.billable_units() - 1250.0).abs() < 1e-12);
let d2 = Decision {
orders: Vec::new(),
reasoning: String::new(),
cost: Some(tokens_only),
};
let back: Decision = serde_json::from_str(&serde_json::to_string(&d2).unwrap()).unwrap();
assert_eq!(back.cost, Some(tokens_only));
}
#[test]
fn closed_decision_contract_rejects_drift_and_semantic_faults() {
let obs = MarketObservation {
date: "2026-01-01".to_string(),
cash: 1.0,
symbols: vec![SymbolSnapshot {
symbol: "A".to_string(),
close_history: vec![1.0],
fundamentals: Default::default(),
news: Vec::new(),
}],
portfolio: Vec::new(),
};
assert!(serde_json::from_str::<Decision>(r#"{"orders":[],"typo":true}"#).is_err());
let order = |symbol: &str, weight: f64| Order {
symbol: symbol.to_string(),
action: Action::Sell,
target_weight: weight,
confidence: 0.5,
rationale: String::new(),
};
let valid = Decision {
orders: vec![order("A", -0.5)],
reasoning: String::new(),
cost: None,
};
assert!(valid.validate_for(&obs).is_ok());
for invalid in [
Decision {
orders: vec![order("UNKNOWN", 0.0)],
reasoning: String::new(),
cost: None,
},
Decision {
orders: vec![order("A", 0.1), order("A", 0.2)],
reasoning: String::new(),
cost: None,
},
Decision {
orders: vec![order("A", 1.01)],
reasoning: String::new(),
cost: None,
},
] {
assert!(invalid.validate_for(&obs).is_err());
}
}
#[test]
fn unknown_field_diagnostic_names_the_offending_field() {
let error = decision_from_wire(r#"{"orders":[],"latency_ms":12}"#)
.expect_err("the closed contract rejects an undefined key");
assert!(
error.contains("latency_ms"),
"the diagnostic must name the offending field, got: {error}"
);
assert!(
error.contains("orders") && error.contains("reasoning") && error.contains("cost"),
"the diagnostic must list the accepted fields, got: {error}"
);
assert!(
error.contains(DECISION_SCHEMA_PATH),
"the diagnostic must point at the published schema, got: {error}"
);
let malformed = decision_from_wire("not json").expect_err("malformed input is rejected");
assert!(malformed.contains("closed wire contract"));
let ok =
decision_from_wire(r#"{"orders":[{"symbol":"A","action":"buy","target_weight":0.5}]}"#)
.expect("a conforming decision parses");
assert_eq!(ok.orders[0].symbol, "A");
}
#[test]
fn trajectory_roundtrips_through_json() {
let traj = AgentTrajectory {
agent_id: "a".to_string(),
in_sample_trials: 7,
declared_mandate: None,
runs: vec![RunTrajectory {
window_start: 20,
window_end: 30,
seed: 3,
steps: vec![DecisionStep {
step: 0,
observation_id: "2025-001".to_string(),
decision: Decision {
orders: vec![Order {
symbol: "A".to_string(),
action: Action::Buy,
target_weight: 0.25,
confidence: 0.8,
rationale: String::new(),
}],
reasoning: "r".to_string(),
cost: None,
},
}],
}],
};
let back: AgentTrajectory =
serde_json::from_str(&serde_json::to_string(&traj).unwrap()).unwrap();
assert_eq!(back.agent_id, "a");
assert_eq!(back.in_sample_trials, 7);
assert_eq!(back.runs[0].seed, 3);
assert_eq!(back.runs[0].steps[0].observation_id, "2025-001");
assert_eq!(back.runs[0].steps[0].decision.orders[0].target_weight, 0.25);
assert!(!serde_json::to_string(&traj)
.unwrap()
.contains("declared_mandate"));
assert!(back.declared_mandate.is_none());
}
#[test]
fn declared_mandate_is_additive_and_round_trips() {
let legacy = r#"{"agent_id":"a","runs":[]}"#;
let t: AgentTrajectory = serde_json::from_str(legacy).unwrap();
assert!(t.declared_mandate.is_none());
for (m, json) in [
(
DeclaredMandate::AbsoluteReturn,
r#"{"kind":"absolute_return"}"#,
),
(
DeclaredMandate::RelativeTo {
benchmark_id: "buy-and-hold".to_string(),
},
r#"{"kind":"relative_to","benchmark_id":"buy-and-hold"}"#,
),
(
DeclaredMandate::DrawdownCapped {
max_per_run_drawdown: 0.2,
},
r#"{"kind":"drawdown_capped","max_per_run_drawdown":0.2}"#,
),
(
DeclaredMandate::OutperformBuyAndHold,
r#"{"kind":"outperform_buy_and_hold"}"#,
),
] {
assert_eq!(serde_json::to_string(&m).unwrap(), json);
assert_eq!(serde_json::from_str::<DeclaredMandate>(json).unwrap(), m);
}
let declared = AgentTrajectory {
agent_id: "a".to_string(),
in_sample_trials: 0,
declared_mandate: Some(DeclaredMandate::OutperformBuyAndHold),
runs: Vec::new(),
};
let back: AgentTrajectory =
serde_json::from_str(&serde_json::to_string(&declared).unwrap()).unwrap();
assert_eq!(
back.declared_mandate,
Some(DeclaredMandate::OutperformBuyAndHold)
);
assert_eq!(
serde_json::from_str::<DeclaredMandate>(r#"{"kind":"long_only_beta"}"#).unwrap(),
DeclaredMandate::OutperformBuyAndHold,
"old artifacts remain readable but are re-emitted under the honest name"
);
}
}