schwab_cli/agent/
state.rs1use 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}