Skip to main content

schwab_cli/agent/
state.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4
5use anyhow::Result;
6use chrono::{DateTime, NaiveDate, Utc};
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9
10use crate::options::types::StrategyKind;
11
12#[derive(Debug, Clone, Serialize, Deserialize, Default)]
13pub struct AgentState {
14    pub agent_id: String,
15    pub last_tick: Option<DateTime<Utc>>,
16    pub trades_today: u32,
17    pub trades_day: Option<NaiveDate>,
18    pub open_positions: HashMap<String, TrackedPosition>,
19    pub last_actions: Vec<AgentAction>,
20    pub pending_order_ids: Vec<String>,
21    #[serde(default)]
22    pub tick_count: u64,
23    #[serde(default)]
24    pub last_llm_review_tick: Option<u64>,
25    #[serde(default)]
26    pub llm_review_count: u64,
27    #[serde(default)]
28    pub last_llm_summary: Option<Value>,
29    #[serde(default)]
30    pub last_session: Option<String>,
31    #[serde(default)]
32    pub regular_tick_count: u64,
33    #[serde(default)]
34    pub last_overnight_digest_at: Option<DateTime<Utc>>,
35    #[serde(default)]
36    pub open_playbook: Option<Value>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct TrackedPosition {
41    pub position_id: String,
42    pub account_hash: String,
43    pub underlying: String,
44    pub expiry: String,
45    pub strategy: String,
46    pub opened_at: DateTime<Utc>,
47    pub entry_credit: Option<f64>,
48    pub max_loss_usd: f64,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AgentAction {
53    pub at: DateTime<Utc>,
54    pub action: String,
55    pub detail: Value,
56}
57
58pub fn load_state(path: &Path) -> Result<AgentState> {
59    if !path.exists() {
60        return Ok(AgentState::default());
61    }
62    let content = fs::read_to_string(path)?;
63    Ok(serde_json::from_str(&content)?)
64}
65
66pub fn save_state(path: &Path, state: &AgentState) -> Result<()> {
67    if let Some(parent) = path.parent() {
68        fs::create_dir_all(parent)?;
69    }
70    let content = serde_json::to_string_pretty(state)?;
71    fs::write(path, content)?;
72    Ok(())
73}
74
75impl AgentState {
76    pub fn reset_daily_if_needed(&mut self, today: NaiveDate) {
77        if self.trades_day != Some(today) {
78            self.trades_today = 0;
79            self.trades_day = Some(today);
80        }
81    }
82
83    pub fn record_action(&mut self, action: &str, detail: Value) {
84        self.last_actions.push(AgentAction {
85            at: Utc::now(),
86            action: action.to_string(),
87            detail,
88        });
89        if self.last_actions.len() > 100 {
90            let drain = self.last_actions.len() - 100;
91            self.last_actions.drain(0..drain);
92        }
93    }
94
95    pub fn count_open_for_strategy(&self, account_hash: &str, strategy: StrategyKind) -> u32 {
96        self.open_positions
97            .values()
98            .filter(|p| p.account_hash == account_hash && p.strategy == strategy.as_str())
99            .count() as u32
100    }
101}
102
103pub fn state_summary(state: &AgentState) -> Value {
104    json!({
105        "agent_id": state.agent_id,
106        "last_tick": state.last_tick,
107        "trades_today": state.trades_today,
108        "open_positions": state.open_positions.len(),
109        "tick_count": state.tick_count,
110        "last_llm_review_tick": state.last_llm_review_tick,
111        "last_llm_summary": state.last_llm_summary,
112        "last_session": state.last_session,
113        "regular_tick_count": state.regular_tick_count,
114        "last_overnight_digest_at": state.last_overnight_digest_at,
115        "open_playbook": state.open_playbook,
116        "pending_orders": state.pending_order_ids.len(),
117        "recent_actions": state.last_actions.iter().rev().take(10).collect::<Vec<_>>(),
118    })
119}