hyper_agent_ai/
adjustment_history.rs1use rusqlite::{params, Connection};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AdjustmentRecord {
12 pub id: i64,
13 pub timestamp: String,
14 pub strategy_id: String,
15 pub adjustment_json: String,
17 pub reasoning: Option<String>,
19 pub changes_summary: String,
21}
22
23#[derive(Debug, thiserror::Error)]
25pub enum AdjustmentHistoryError {
26 #[error("Database error: {0}")]
27 Db(#[from] rusqlite::Error),
28}
29
30pub struct AdjustmentHistoryStore {
32 conn: Connection,
33}
34
35impl AdjustmentHistoryStore {
36 pub fn new(db_path: &str) -> Result<Self, AdjustmentHistoryError> {
40 let conn = Connection::open(db_path)?;
41
42 conn.execute_batch(
43 "CREATE TABLE IF NOT EXISTS adjustment_history (
44 id INTEGER PRIMARY KEY AUTOINCREMENT,
45 timestamp TEXT NOT NULL DEFAULT (datetime('now')),
46 strategy_id TEXT NOT NULL,
47 adjustment_json TEXT NOT NULL,
48 reasoning TEXT,
49 changes_summary TEXT NOT NULL
50 );
51 CREATE INDEX IF NOT EXISTS idx_adj_strategy
52 ON adjustment_history(strategy_id, id DESC);",
53 )?;
54
55 Ok(Self { conn })
56 }
57
58 pub fn insert(
60 &self,
61 strategy_id: &str,
62 adjustment_json: &str,
63 reasoning: Option<&str>,
64 changes_summary: &str,
65 ) -> Result<(), AdjustmentHistoryError> {
66 self.conn.execute(
67 "INSERT INTO adjustment_history
68 (strategy_id, adjustment_json, reasoning, changes_summary)
69 VALUES (?1, ?2, ?3, ?4)",
70 params![strategy_id, adjustment_json, reasoning, changes_summary],
71 )?;
72 Ok(())
73 }
74
75 pub fn list(
77 &self,
78 strategy_id: &str,
79 limit: usize,
80 ) -> Result<Vec<AdjustmentRecord>, AdjustmentHistoryError> {
81 let mut stmt = self.conn.prepare(
82 "SELECT id, timestamp, strategy_id, adjustment_json, reasoning, changes_summary
83 FROM adjustment_history
84 WHERE strategy_id = ?1
85 ORDER BY id DESC
86 LIMIT ?2",
87 )?;
88
89 let rows = stmt.query_map(params![strategy_id, limit as i64], |row| {
90 Ok(AdjustmentRecord {
91 id: row.get(0)?,
92 timestamp: row.get(1)?,
93 strategy_id: row.get(2)?,
94 adjustment_json: row.get(3)?,
95 reasoning: row.get(4)?,
96 changes_summary: row.get(5)?,
97 })
98 })?;
99
100 let mut records = Vec::new();
101 for row in rows {
102 records.push(row?);
103 }
104 Ok(records)
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn insert_and_list() {
114 let store = AdjustmentHistoryStore::new(":memory:").unwrap();
115 store
116 .insert(
117 "strategy-1",
118 r#"{"regime_rules": null}"#,
119 Some("Market is volatile"),
120 "stop_loss_pct 3.0->5.0",
121 )
122 .unwrap();
123 store
124 .insert("strategy-1", "{}", Some("No changes needed"), "no changes")
125 .unwrap();
126 store
127 .insert("strategy-2", "{}", None, "no changes")
128 .unwrap();
129
130 let records = store.list("strategy-1", 10).unwrap();
131 assert_eq!(records.len(), 2);
132 assert_eq!(records[0].changes_summary, "no changes");
134 assert_eq!(records[1].changes_summary, "stop_loss_pct 3.0->5.0");
135
136 let records2 = store.list("strategy-2", 10).unwrap();
137 assert_eq!(records2.len(), 1);
138 }
139
140 #[test]
141 fn list_respects_limit() {
142 let store = AdjustmentHistoryStore::new(":memory:").unwrap();
143 for i in 0..10 {
144 store
145 .insert("s1", "{}", None, &format!("change {}", i))
146 .unwrap();
147 }
148 let records = store.list("s1", 3).unwrap();
149 assert_eq!(records.len(), 3);
150 assert_eq!(records[0].changes_summary, "change 9");
151 }
152
153 #[test]
154 fn empty_list() {
155 let store = AdjustmentHistoryStore::new(":memory:").unwrap();
156 let records = store.list("nonexistent", 10).unwrap();
157 assert!(records.is_empty());
158 }
159
160 #[test]
161 fn timestamp_is_auto_populated() {
162 let store = AdjustmentHistoryStore::new(":memory:").unwrap();
163 store.insert("s1", "{}", None, "test").unwrap();
164 let records = store.list("s1", 1).unwrap();
165 assert!(!records[0].timestamp.is_empty());
166 }
167}