1use std::sync::{Arc, Mutex};
4use std::time::Instant;
5
6use serde::Deserialize;
7use serde_json::Value;
8use tracing::{debug, error, instrument, trace, warn};
9
10use crate::Result;
11use crate::chart::{DataPoint, SymbolInfo};
12use crate::error::{Error, TradingViewError};
13use crate::live::handler::command::Command;
14use crate::live::handler::{CommandTx, Handler, HandlerFactory};
15use crate::live::models::{DataServer, TradingViewDataEvent};
16use crate::live::websocket::WebSocketClient;
17use crate::study::request::StudyRequest;
18use crate::study::result::StudyResult;
19use crate::study::state::StudyState;
20use crate::utils::symbol_init;
21
22pub struct StudyClient {
24 pub(crate) auth_token: String,
25 pub(crate) server: DataServer,
26}
27
28impl StudyClient {
29 pub fn new(auth_token: impl Into<String>, server: DataServer) -> Self {
31 Self {
32 auth_token: auth_token.into(),
33 server,
34 }
35 }
36
37 #[instrument(skip(self), fields(symbol, exchange))]
39 pub async fn retrieve(&self, request: StudyRequest) -> Result<StudyResult> {
40 let started = Instant::now();
41 let (symbol, exchange) = request.resolve_symbol_exchange()?;
42 debug!(symbol = %symbol, exchange = %exchange, "study retrieval started");
43
44 let study_id = request
45 .study_id
46 .clone()
47 .unwrap_or_else(|| format!("st_{}", crate::utils::gen_id()));
48 let study_sub_id = "st1".to_string();
49
50 let state = Arc::new(Mutex::new(StudyState::with_capacity_and_notify(
51 request.base_bar_count as usize,
52 Arc::new(tokio::sync::Notify::new()),
53 )));
54
55 let (cmd_tx, _cmd_rx) = tokio::sync::mpsc::channel::<Command>(16);
56 let factory = StudyDataHandlerFactory::new(Arc::clone(&state), &study_id);
57 let handler = factory.create(cmd_tx);
58
59 let ws = WebSocketClient::builder()
60 .auth_token(&self.auth_token)
61 .server(self.server)
62 .handler(handler)
63 .build()
64 .await?;
65
66 let instrument = format!("{exchange}:{symbol}");
74 let chart_session = format!("cs_{}", crate::utils::gen_id());
75 let symbol_series_id = format!("sds_sym_{}", crate::utils::gen_id());
76 let series_identifier = "sds_1".to_string();
77 let series_id = "s1".to_string();
78
79 ws.create_chart_session(&chart_session).await?;
81 debug!(session = %chart_session, "chart session created");
82
83 let symbol_init_str = symbol_init().instrument(&instrument).call()?;
85 ws.send(
86 "resolve_symbol",
87 &[
88 Value::from(chart_session.as_str()),
89 Value::from(symbol_series_id.as_str()),
90 Value::from(symbol_init_str),
91 ],
92 )
93 .await?;
94 debug!(instrument = %instrument, "symbol resolution requested");
95
96 let create_series_args = crate::historical::client::build_create_series_args(
98 &chart_session,
99 &series_identifier,
100 &series_id,
101 &symbol_series_id,
102 request.interval,
103 Some(request.base_bar_count),
104 None,
105 );
106 ws.send("create_series", &create_series_args).await?;
107 debug!(
108 interval = ?request.interval,
109 bars = request.base_bar_count,
110 "base series created"
111 );
112
113 ws.create_study()
115 .chart_session(&chart_session)
116 .study_ids(&[&study_id, &study_sub_id])
117 .chart_series_id(&series_identifier)
118 .study(request.study)
119 .call()
120 .await?;
121 debug!(study_id = %study_id, "study created");
122
123 Arc::clone(&ws).spawn_reader_task();
124
125 let result = tokio::time::timeout(request.timeout, Self::wait_for_completion(&state)).await;
126
127 let mut state_guard = state.lock().unwrap();
128 let total_points = state_guard.total_points;
129 let data = state_guard.finalize();
130 let elapsed = started.elapsed();
131
132 match result {
133 Ok(_) => {
134 if state_guard.errored {
135 let msg = state_guard
136 .error_message
137 .take()
138 .unwrap_or_else(|| "study data retrieval failed".to_string());
139 return Err(Error::Internal(msg.into()));
140 }
141 let symbol_info = state_guard
142 .symbol_info
143 .take()
144 .ok_or_else(|| Error::Internal("no symbol info received".into()))?;
145 Ok(StudyResult {
146 symbol_info,
147 data,
148 study_id,
149 total_points_received: total_points,
150 elapsed,
151 })
152 }
153 Err(_) => Err(Error::Timeout("study data retrieval timed out".into())),
154 }
155 }
156
157 pub(crate) async fn wait_for_completion(state: &Arc<Mutex<StudyState>>) {
158 let notify = {
159 let guard = state.lock().unwrap();
160 guard.notify.clone()
161 };
162 loop {
163 let notified = notify.notified();
165 tokio::pin!(notified);
166 notified.as_mut().enable();
167
168 {
169 let guard = state.lock().unwrap();
170 if guard.is_done() {
171 break;
172 }
173 }
174
175 notified.await;
176 }
177 }
178}
179
180#[derive(Clone)]
186pub(crate) struct StudyDataHandler {
187 state: Arc<Mutex<StudyState>>,
188 study_id: String,
189 #[allow(dead_code)]
190 cmd_tx: CommandTx,
191}
192
193impl Handler for StudyDataHandler {
194 fn handle_events(&self, event: TradingViewDataEvent, message: &[Value]) {
195 match event {
196 TradingViewDataEvent::OnSymbolResolved => {
197 if let Some(sym_info) = message.get(2)
199 && let Ok(info) = SymbolInfo::deserialize(sym_info)
200 {
201 debug!(name = %info.name, "symbol resolved");
202 self.state.lock().unwrap().record_symbol_info(info);
203 }
204 }
205 TradingViewDataEvent::OnChartData | TradingViewDataEvent::OnChartDataUpdate => {
206 if message.len() < 2 {
210 return;
211 }
212 if let Some(obj) = message[1].as_object()
213 && let Some(study_val) = obj.get(&self.study_id)
214 && let Some(st_arr) = study_val.get("st").and_then(|v| v.as_array())
215 {
216 let mut points = Vec::with_capacity(st_arr.len());
217 for v in st_arr {
218 if let Ok(point) = DataPoint::deserialize(v) {
219 points.push(point);
220 }
221 }
222 if !points.is_empty() {
223 let count = points.len();
224 let mut state = self.state.lock().unwrap();
225 state.record_points(points, count);
226 }
227 }
228 }
229 TradingViewDataEvent::OnStudyCompleted => {
230 let completed_id = message.get(1).and_then(|v| v.as_str());
232 if completed_id == Some(&self.study_id) {
233 debug!(study_id = %self.study_id, "study completed");
234 self.state.lock().unwrap().complete();
235 } else {
236 trace!(
237 completed = ?completed_id,
238 expected = %self.study_id,
239 "ignoring study_completed for unrelated study"
240 );
241 }
242 }
243 TradingViewDataEvent::OnError(tv_error) => {
244 if tv_error == TradingViewError::StudyError
246 && let Some(err_study_id) = message.get(1).and_then(|v| v.as_str())
247 && err_study_id != self.study_id
248 {
249 warn!(
250 err_study_id = %err_study_id,
251 expected = %self.study_id,
252 "ignoring study_error for unrelated study"
253 );
254 return;
255 }
256
257 error!(?tv_error, "tradingview study/protocol error");
258 let err_details = message
259 .iter()
260 .filter_map(|v| v.as_str())
261 .collect::<Vec<_>>()
262 .join(" ");
263 let msg = if err_details.is_empty() {
264 format!("tradingview error: {tv_error:?}")
265 } else {
266 format!("tradingview error: {tv_error:?}: {err_details}")
267 };
268 let mut state = self.state.lock().unwrap();
269 state.fail(msg);
270 }
271 _ => {}
272 }
273 }
274
275 fn handle_quote_data(&self, _message: &[Value]) {}
276 fn handle_series_data(&self, _event: TradingViewDataEvent, _messages: &[Value]) {}
277
278 fn notify_error(&self, error: Error, _message: &[Value]) {
279 warn!(?error, "study handler error");
280 let mut state = self.state.lock().unwrap();
281 if state.record_error() {
282 state.fail(format!("too many errors: {error:?}"));
283 }
284 }
285}
286
287pub(crate) struct StudyDataHandlerFactory {
293 state: Arc<Mutex<StudyState>>,
294 study_id: String,
295}
296
297impl StudyDataHandlerFactory {
298 pub fn new(state: Arc<Mutex<StudyState>>, study_id: impl Into<String>) -> Self {
300 Self {
301 state,
302 study_id: study_id.into(),
303 }
304 }
305}
306
307impl HandlerFactory for StudyDataHandlerFactory {
308 type Handler = StudyDataHandler;
309
310 fn create(&self, command_tx: CommandTx) -> Self::Handler {
311 StudyDataHandler {
312 state: Arc::clone(&self.state),
313 study_id: self.study_id.clone(),
314 cmd_tx: command_tx,
315 }
316 }
317}
318
319#[cfg(test)]
324mod tests {
325 use super::*;
326 use serde_json::json;
327
328 fn setup_handler(study_id: &str) -> (StudyDataHandler, Arc<Mutex<StudyState>>) {
329 let state = Arc::new(Mutex::new(StudyState::new()));
330 let (cmd_tx, _cmd_rx) = tokio::sync::mpsc::channel(16);
331 let handler = StudyDataHandler {
332 state: Arc::clone(&state),
333 study_id: study_id.to_string(),
334 cmd_tx,
335 };
336 (handler, state)
337 }
338
339 #[test]
340 fn test_matching_study_id_routing() {
341 let (handler, state) = setup_handler("st_target");
342
343 let payload = json!([
345 "cs_test",
346 {
347 "st_target": {
348 "st": [
349 {"i": -10, "v": [1600000000.0, 100.5, 200.5]},
350 {"i": -9, "v": [1600086400.0, 105.0, 210.0]}
351 ]
352 }
353 }
354 ]);
355
356 handler.handle_events(
357 TradingViewDataEvent::OnChartData,
358 payload.as_array().unwrap(),
359 );
360
361 let guard = state.lock().unwrap();
362 assert_eq!(guard.data.len(), 2);
363 assert_eq!(guard.data[0].index, -10);
364 assert_eq!(guard.data[0].value, vec![1600000000.0, 100.5, 200.5]);
365 assert_eq!(guard.data[1].index, -9);
366 assert_eq!(guard.data[1].value, vec![1600086400.0, 105.0, 210.0]);
367 assert_eq!(guard.total_points, 2);
368 }
369
370 #[test]
371 fn test_ignoring_unrelated_studies() {
372 let (handler, state) = setup_handler("st_target");
373
374 let payload = json!([
376 "cs_test",
377 {
378 "st_unrelated": {
379 "st": [
380 {"i": 0, "v": [1600000000.0, 999.0]}
381 ]
382 },
383 "sds_1": {
384 "s": [
385 {"i": 0, "v": [1600000000.0, 10.0, 20.0, 5.0, 15.0, 1000.0]}
386 ]
387 }
388 }
389 ]);
390
391 handler.handle_events(
392 TradingViewDataEvent::OnChartDataUpdate,
393 payload.as_array().unwrap(),
394 );
395
396 let guard = state.lock().unwrap();
397 assert!(guard.data.is_empty());
398 assert_eq!(guard.total_points, 0);
399 }
400
401 #[test]
402 fn test_mixed_payload_extracts_only_target() {
403 let (handler, state) = setup_handler("st_target");
404
405 let payload = json!([
406 "cs_test",
407 {
408 "st_unrelated": {
409 "st": [{"i": 1, "v": [100.0, 999.0]}]
410 },
411 "st_target": {
412 "st": [{"i": 2, "v": [200.0, 888.0]}]
413 }
414 }
415 ]);
416
417 handler.handle_events(
418 TradingViewDataEvent::OnChartData,
419 payload.as_array().unwrap(),
420 );
421
422 let guard = state.lock().unwrap();
423 assert_eq!(guard.data.len(), 1);
424 assert_eq!(guard.data[0].index, 2);
425 assert_eq!(guard.data[0].value, vec![200.0, 888.0]);
426 }
427
428 #[test]
429 fn test_completion_filtering() {
430 let (handler, state) = setup_handler("st_target");
431
432 let unrelated = json!(["cs_test", "st_other", "s1_st1"]);
434 handler.handle_events(
435 TradingViewDataEvent::OnStudyCompleted,
436 unrelated.as_array().unwrap(),
437 );
438 assert!(!state.lock().unwrap().completed);
439
440 let matching = json!(["cs_test", "st_target", "s1_st1"]);
442 handler.handle_events(
443 TradingViewDataEvent::OnStudyCompleted,
444 matching.as_array().unwrap(),
445 );
446 assert!(state.lock().unwrap().completed);
447 }
448
449 #[test]
450 fn test_study_error_filtering() {
451 let (handler, state) = setup_handler("st_target");
452
453 let unrelated_err = json!(["cs_test", "st_other", "study initialization failed"]);
455 handler.handle_events(
456 TradingViewDataEvent::OnError(TradingViewError::StudyError),
457 unrelated_err.as_array().unwrap(),
458 );
459 assert!(!state.lock().unwrap().errored);
460
461 let matching_err = json!(["cs_test", "st_target", "invalid indicator parameters"]);
463 handler.handle_events(
464 TradingViewDataEvent::OnError(TradingViewError::StudyError),
465 matching_err.as_array().unwrap(),
466 );
467 let guard = state.lock().unwrap();
468 assert!(guard.errored);
469 assert!(
470 guard
471 .error_message
472 .as_ref()
473 .unwrap()
474 .contains("invalid indicator parameters")
475 );
476 }
477
478 #[test]
479 fn test_protocol_error_triggers_failure() {
480 let (handler, state) = setup_handler("st_target");
481
482 let protocol_err = json!(["cs_test", "critical error occurred"]);
483 handler.handle_events(
484 TradingViewDataEvent::OnError(TradingViewError::CriticalError),
485 protocol_err.as_array().unwrap(),
486 );
487 let guard = state.lock().unwrap();
488 assert!(guard.errored);
489 assert!(guard.error_message.is_some());
490 }
491
492 #[test]
493 fn test_sort_and_dedup_points_replaces_with_latest_point() {
494 let mut state = StudyState::new();
495
496 let p1 = DataPoint {
502 index: 10,
503 value: vec![1600020000.0, 10.0],
504 };
505 let p2 = DataPoint {
506 index: 5,
507 value: vec![1600010000.0, 100.0],
508 };
509 let p1_update = DataPoint {
510 index: 10,
511 value: vec![1600020000.0, -42.5, 1e100],
512 };
513 let p0 = DataPoint {
514 index: 1,
515 value: vec![1600000000.0, 0.0],
516 };
517
518 state.record_points(vec![p1, p2, p1_update, p0], 4);
519 let finalized = state.finalize();
520
521 assert_eq!(finalized.len(), 3);
523 assert_eq!(finalized[0].value[0] as i64, 1600000000);
524 assert_eq!(finalized[1].value[0] as i64, 1600010000);
525 assert_eq!(finalized[2].value[0] as i64, 1600020000);
526 assert_eq!(finalized[2].value[1], -42.5);
528 assert_eq!(finalized[2].value[2], 1e100);
529 }
530
531 #[test]
532 fn test_captured_time_scale_update_fixture() {
533 let fixture_str = include_str!("../../tests/data/time_scale_update.json");
534 let messages: Vec<Value> = serde_json::from_str(fixture_str).unwrap();
535
536 let (handler, state) = setup_handler("st3");
538
539 for msg in &messages {
540 let m = msg["m"].as_str().unwrap();
541 let p = msg["p"].as_array().unwrap();
542 let event = TradingViewDataEvent::from(m.to_string());
543 handler.handle_events(event, p);
544 }
545
546 let mut guard = state.lock().unwrap();
547 assert!(guard.completed, "st3 should have received study_completed");
548 assert!(!guard.errored);
549 assert!(!guard.data.is_empty(), "st3 should have captured points");
550
551 let points = guard.finalize();
552 for window in points.windows(2) {
554 let ts_a = window[0].value[0] as i64;
555 let ts_b = window[1].value[0] as i64;
556 assert!(
557 ts_a <= ts_b,
558 "points must be ordered ascending: {ts_a} > {ts_b}"
559 );
560 }
561 }
562
563 #[test]
564 fn test_captured_fixture_ignores_unrelated() {
565 let fixture_str = include_str!("../../tests/data/time_scale_update.json");
566 let messages: Vec<Value> = serde_json::from_str(fixture_str).unwrap();
567
568 let (handler, state) = setup_handler("st_nonexistent");
570
571 for msg in &messages {
572 let m = msg["m"].as_str().unwrap();
573 let p = msg["p"].as_array().unwrap();
574 let event = TradingViewDataEvent::from(m.to_string());
575 handler.handle_events(event, p);
576 }
577
578 let guard = state.lock().unwrap();
579 assert!(!guard.completed);
580 assert!(guard.data.is_empty());
581 }
582}