Skip to main content

zero_operator_state/
events.rs

1//! The event stream fed into the classifier.
2//!
3//! Events are recorded in `~/.zero/state/events.log` and replayed on
4//! start. The enum is exhaustive on purpose: adding a new event type
5//! fails every match arm in the classifier until the author decides
6//! how it maps to the state vector.
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11/// Where a trading decision originated.
12///
13/// Used to compute the override rate in §2.1 and to separate operator
14/// initiative from automated flow.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum Source {
18    /// Operator accepted a Plan-mode verdict without modification.
19    Plan,
20    /// Engine executed under Auto mode with no operator touch.
21    Auto,
22    /// Operator executed under Headless policy.
23    Headless,
24    /// Operator override — rejected Plan's recommendation and acted
25    /// anyway. The strongest signal for deviation-rate.
26    Override,
27    /// Operator-initiated trade not tied to any engine proposal.
28    Manual,
29}
30
31/// Outcome of a completed trade. Used for loss-reaction timing and
32/// the conviction-calibration report.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum Outcome {
36    Win,
37    Loss,
38    Scratch,
39}
40
41/// The event kinds the classifier understands.
42///
43/// New variants added here force every match arm in `classifier.rs`
44/// to decide how the event affects the vector — the compiler becomes
45/// the reviewer.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(tag = "kind", rename_all = "snake_case")]
48pub enum EventKind {
49    /// Operator took an action that could open or modify a position.
50    DecisionMade { symbol: String, source: Source },
51    /// A position closed. `loss_reaction_ms` is filled when the prior
52    /// event was also a close on the same symbol.
53    TradeClosed {
54        symbol: String,
55        outcome: Outcome,
56        pnl_r: f64,
57        conviction: Option<u8>,
58    },
59    /// Operator hit `/break` (or a similar risk-reducing rest).
60    BreakStarted { planned_ms: Option<u64> },
61    /// Break ended (timer, keypress, or session resume).
62    BreakEnded,
63    /// Operator has been idle for more than the sleep-proxy threshold.
64    Idle { since_ms: u64 },
65    /// Operator returned from idle.
66    Resumed,
67    /// Plan-mode verdict shown to the operator (count of how many
68    /// they've seen drives the override-rate denominator).
69    VerdictShown,
70    /// Plan-mode verdict was explicitly rejected by the operator.
71    VerdictOverridden,
72    /// Session began (launch or resume).
73    SessionStarted,
74    /// Session ended.
75    SessionEnded,
76    /// Operator-supplied conviction rating for a past trade
77    /// (`/rate <trade_id> <1..=10>`). The classifier does not
78    /// attribute the rating back onto the original `TradeClosed`
79    /// variant because the two events are separated by human
80    /// latency — merging them would force the classifier to
81    /// carry a mutable trade index. Keeping `Conviction` as its
82    /// own event lets the downstream consumer (operator-state
83    /// engine POST, future calibration overlay) join on
84    /// `trade_id` without the classifier needing to know how.
85    ///
86    /// `rating` is a `u8` in `1..=10` — the parser enforces the
87    /// range before pushing. `trade_id` is the engine's opaque
88    /// trade identifier, never parsed CLI-side.
89    Conviction { trade_id: String, rating: u8 },
90}
91
92/// Wall-clock-timestamped event. Instances are the only thing that
93/// [`crate::Classifier`] consumes.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct Event {
96    pub ts: DateTime<Utc>,
97    #[serde(flatten)]
98    pub kind: EventKind,
99}
100
101impl Event {
102    #[must_use]
103    pub fn new(ts: DateTime<Utc>, kind: EventKind) -> Self {
104        Self { ts, kind }
105    }
106}