1use crate::daemon;
2use fs2::FileExt;
3use serde::de::DeserializeOwned;
4use serde::Serialize;
5
6#[derive(Serialize)]
7pub struct RunStrategyParams {
8 pub id: String,
9 pub symbol: Option<String>,
10 pub symbols: Option<Vec<String>>,
11 pub paper: Option<bool>,
12 pub confirm_live: Option<bool>,
13 pub agent_loop: Option<bool>,
14 pub agent_interval: Option<u64>,
15 pub tick_interval: Option<u64>,
16}
17
18#[derive(Serialize)]
19pub struct PlaceOrderParams {
20 pub market: String,
21 pub side: String,
22 pub size: f64,
23 pub price: Option<f64>,
24 pub live: Option<bool>, }
26
27#[deprecated(note = "Use hyper_agent_client::HyperAgentClient instead")]
28pub struct DaemonClient {
29 base_url: String,
30 http: reqwest::Client,
31 token: String,
32}
33
34#[derive(Debug)]
35pub enum ClientError {
36 NotRunning,
37 Http(reqwest::Error),
38 Api(String),
39}
40
41impl std::fmt::Display for ClientError {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 Self::NotRunning => write!(f, "Daemon is not running"),
45 Self::Http(e) => write!(f, "HTTP error: {}", e),
46 Self::Api(msg) => write!(f, "API error: {}", msg),
47 }
48 }
49}
50impl std::error::Error for ClientError {}
51
52#[allow(deprecated)]
53impl DaemonClient {
54 pub fn new(base_url: &str, token: &str) -> Self {
55 Self {
56 base_url: base_url.to_string(),
57 http: reqwest::Client::new(),
58 token: token.to_string(),
59 }
60 }
61
62 pub fn from_port_file() -> Result<Self, ClientError> {
63 let port = daemon::read_port().ok_or(ClientError::NotRunning)?;
64 let token = daemon::read_token().ok_or(ClientError::NotRunning)?;
65 Ok(Self::new(&format!("http://127.0.0.1:{}", port), &token))
66 }
67
68 pub async fn health(&self) -> Result<(), ClientError> {
69 let resp = self
70 .http
71 .get(format!("{}/health", self.base_url))
72 .send()
73 .await
74 .map_err(ClientError::Http)?;
75 if resp.status().is_success() {
76 Ok(())
77 } else {
78 Err(ClientError::Api("unhealthy".into()))
79 }
80 }
81
82 pub async fn status(&self) -> Result<serde_json::Value, ClientError> {
83 self.get("/status").await
84 }
85
86 pub async fn shutdown(&self) -> Result<(), ClientError> {
87 self.post("/shutdown", &())
88 .await
89 .map(|_: serde_json::Value| ())
90 }
91
92 pub async fn run_strategy(
93 &self,
94 params: &RunStrategyParams,
95 ) -> Result<serde_json::Value, ClientError> {
96 self.post("/strategies/run", params).await
97 }
98
99 pub async fn stop_strategy(&self, id: &str) -> Result<serde_json::Value, ClientError> {
100 self.post(&format!("/strategies/{}/stop", id), &()).await
101 }
102
103 pub async fn list_strategies(&self) -> Result<Vec<serde_json::Value>, ClientError> {
104 self.get("/strategies").await
105 }
106
107 pub async fn strategy_health(&self, id: &str) -> Result<serde_json::Value, ClientError> {
108 self.get(&format!("/strategies/{}/health", id)).await
109 }
110
111 pub async fn place_order(
112 &self,
113 params: &PlaceOrderParams,
114 ) -> Result<serde_json::Value, ClientError> {
115 self.post("/orders", params).await
116 }
117
118 pub async fn cancel_order(&self, id: &str) -> Result<(), ClientError> {
119 self.delete(&format!("/orders/{}", id)).await
120 }
121
122 pub async fn get_positions(&self) -> Result<Vec<serde_json::Value>, ClientError> {
123 self.get("/positions").await
124 }
125
126 pub async fn close_all_positions(&self, live: bool) -> Result<serde_json::Value, ClientError> {
127 self.post("/positions/close-all", &serde_json::json!({"live": live}))
128 .await
129 }
130
131 pub async fn get_position_history(&self) -> Result<Vec<serde_json::Value>, ClientError> {
132 self.get("/positions/history").await
133 }
134
135 pub async fn get_pnl(&self) -> Result<serde_json::Value, ClientError> {
136 self.get("/dashboard/pnl").await
137 }
138
139 pub async fn dashboard_stats(&self) -> Result<serde_json::Value, ClientError> {
140 self.get("/dashboard/stats").await
141 }
142
143 pub async fn get_equity(&self, days: u32) -> Result<serde_json::Value, ClientError> {
144 self.get(&format!("/dashboard/equity?days={}", days)).await
145 }
146
147 pub async fn get_risk_exposure(&self) -> Result<serde_json::Value, ClientError> {
148 self.get("/risk/exposure").await
149 }
150
151 pub async fn get_execution_quality(&self) -> Result<serde_json::Value, ClientError> {
152 self.get("/dashboard/execution-quality").await
153 }
154
155 pub async fn get_circuit_breaker(&self) -> Result<serde_json::Value, ClientError> {
156 self.get("/risk/circuit-breaker").await
157 }
158
159 async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, ClientError> {
160 let resp = self
161 .http
162 .get(format!("{}{}", self.base_url, path))
163 .header("Authorization", format!("Bearer {}", self.token))
164 .send()
165 .await
166 .map_err(ClientError::Http)?;
167 if !resp.status().is_success() {
168 return Err(ClientError::Api(format!("status {}", resp.status())));
169 }
170 resp.json().await.map_err(ClientError::Http)
171 }
172
173 async fn post<B: serde::Serialize, T: DeserializeOwned>(
174 &self,
175 path: &str,
176 body: &B,
177 ) -> Result<T, ClientError> {
178 let resp = self
179 .http
180 .post(format!("{}{}", self.base_url, path))
181 .header("Authorization", format!("Bearer {}", self.token))
182 .json(body)
183 .send()
184 .await
185 .map_err(ClientError::Http)?;
186 if !resp.status().is_success() {
187 let text = resp.text().await.unwrap_or_default();
188 return Err(ClientError::Api(text));
189 }
190 let text = resp.text().await.map_err(ClientError::Http)?;
192 if text.is_empty() {
193 serde_json::from_str("null").map_err(|e| ClientError::Api(e.to_string()))
194 } else {
195 serde_json::from_str(&text).map_err(|e| ClientError::Api(e.to_string()))
196 }
197 }
198
199 async fn delete(&self, path: &str) -> Result<(), ClientError> {
200 let resp = self
201 .http
202 .delete(format!("{}{}", self.base_url, path))
203 .header("Authorization", format!("Bearer {}", self.token))
204 .send()
205 .await
206 .map_err(ClientError::Http)?;
207 if !resp.status().is_success() {
208 return Err(ClientError::Api(format!("status {}", resp.status())));
209 }
210 Ok(())
211 }
212}
213
214#[allow(deprecated)]
215pub async fn ensure_daemon() -> Result<DaemonClient, Box<dyn std::error::Error>> {
216 if let Ok(client) = DaemonClient::from_port_file() {
217 if client.health().await.is_ok() {
218 return Ok(client);
219 }
220 }
221
222 let lock_path = daemon::daemon_dir().join("daemon.lock");
224 std::fs::create_dir_all(daemon::daemon_dir())?;
225 let lock_file = std::fs::File::create(&lock_path)?;
226
227 if lock_file.try_lock_exclusive().is_err() {
228 for _ in 0..20 {
230 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
231 if let Ok(client) = DaemonClient::from_port_file() {
232 if client.health().await.is_ok() {
233 return Ok(client);
234 }
235 }
236 }
237 return Err("Daemon startup locked by another process".into());
238 }
239
240 if let Ok(client) = DaemonClient::from_port_file() {
242 if client.health().await.is_ok() {
243 return Ok(client);
244 }
245 }
246
247 let exe = std::env::current_exe()?;
249 let daemon_exe = exe.parent().unwrap().join("hyper-agent-daemon");
251 if !daemon_exe.exists() {
252 return Err(format!("Daemon binary not found at {:?}", daemon_exe).into());
253 }
254
255 std::fs::create_dir_all(daemon::log_dir())?;
256 std::process::Command::new(&daemon_exe)
257 .arg(daemon::DEFAULT_PORT.to_string())
258 .stdout(std::process::Stdio::null())
259 .stderr(std::fs::File::create(daemon::log_dir().join("daemon.log"))?)
260 .spawn()?;
261
262 for _ in 0..20 {
264 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
265 if let Ok(client) = DaemonClient::from_port_file() {
266 if client.health().await.is_ok() {
267 return Ok(client);
269 }
270 }
271 }
272 Err("Daemon failed to start within 10 seconds".into())
273}