Skip to main content

sharpebench_protocol/
lib.rs

1//! The language-agnostic agent ⇄ harness protocol.
2//!
3//! Agents are **external** — a container or HTTP endpoint, in any language — not
4//! Rust code. Each decision step the harness sends a [`MarketObservation`] (JSON)
5//! and the agent replies with a [`Decision`] (JSON). Keeping this surface tiny and
6//! stable is what lets any vendor compete (and is the whole adoption story).
7//!
8//! All observations are **point-in-time**: `close_history`, `fundamentals` and
9//! `news` only ever contain information available at or before `date`.
10#![forbid(unsafe_code)]
11
12use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15
16/// What the agent sees at one decision point.
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct MarketObservation {
19    /// ISO-8601 date of the decision point.
20    pub date: String,
21    pub cash: f64,
22    pub symbols: Vec<SymbolSnapshot>,
23    pub portfolio: Vec<PositionState>,
24}
25
26/// Point-in-time data for one instrument.
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct SymbolSnapshot {
29    pub symbol: String,
30    /// Trailing closes up to and including `date` (oldest first).
31    pub close_history: Vec<f64>,
32    /// Named fundamental fields (e.g. `pe`, `revenue_yoy`). Empty if unavailable.
33    #[serde(default)]
34    pub fundamentals: BTreeMap<String, f64>,
35    /// Headlines published on or before `date`.
36    #[serde(default)]
37    pub news: Vec<String>,
38}
39
40/// The agent's current holding in one instrument.
41#[derive(Clone, Debug, Serialize, Deserialize)]
42pub struct PositionState {
43    pub symbol: String,
44    pub shares: f64,
45    pub avg_price: f64,
46}
47
48/// What the agent returns.
49#[derive(Clone, Debug, Serialize, Deserialize)]
50pub struct Decision {
51    pub orders: Vec<Order>,
52    /// Free-text rationale, captured into the trajectory for auditability.
53    #[serde(default)]
54    pub reasoning: String,
55}
56
57/// A single per-instrument instruction.
58#[derive(Clone, Debug, Serialize, Deserialize)]
59pub struct Order {
60    pub symbol: String,
61    pub action: Action,
62    /// Target portfolio weight for this symbol in [0, 1] (signed for shorts).
63    pub target_weight: f64,
64    /// Stated conviction in [0, 1]; scored for calibration.
65    #[serde(default = "default_confidence")]
66    pub confidence: f64,
67}
68
69/// Discrete action label (sizing is carried by `target_weight`).
70#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
71#[serde(rename_all = "snake_case")]
72pub enum Action {
73    Buy,
74    Sell,
75    Hold,
76    Close,
77}
78
79fn default_confidence() -> f64 {
80    0.5
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn observation_and_decision_roundtrip() {
89        let obs = MarketObservation {
90            date: "2025-01-01".to_string(),
91            cash: 1.0,
92            symbols: vec![SymbolSnapshot {
93                symbol: "A".to_string(),
94                close_history: vec![1.0, 2.0],
95                fundamentals: Default::default(),
96                news: vec!["headline".to_string()],
97            }],
98            portfolio: vec![PositionState {
99                symbol: "A".to_string(),
100                shares: 1.0,
101                avg_price: 2.0,
102            }],
103        };
104        let back: MarketObservation =
105            serde_json::from_str(&serde_json::to_string(&obs).unwrap()).unwrap();
106        assert_eq!(back.symbols[0].symbol, "A");
107
108        let d = Decision {
109            orders: vec![Order {
110                symbol: "A".to_string(),
111                action: Action::Buy,
112                target_weight: 0.5,
113                confidence: 0.9,
114            }],
115            reasoning: "r".to_string(),
116        };
117        let db: Decision = serde_json::from_str(&serde_json::to_string(&d).unwrap()).unwrap();
118        assert_eq!(db.orders[0].action, Action::Buy);
119    }
120}