Skip to main content

longbridge/signal/
context.rs

1use std::sync::Arc;
2
3use longbridge_httpcli::{HttpClient, Json, Method};
4use serde::Deserialize;
5use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
6
7use crate::{Config, Result, signal::types::*};
8
9struct InnerSignalContext {
10    http_cli: HttpClient,
11    log_subscriber: Arc<dyn Subscriber + Send + Sync>,
12}
13
14impl Drop for InnerSignalContext {
15    fn drop(&mut self) {
16        dispatcher::with_default(&self.log_subscriber.clone().into(), || {
17            tracing::info!("signal context dropped");
18        });
19    }
20}
21
22/// Signal context — strategy signals and the catalyst facts behind them.
23#[derive(Clone)]
24pub struct SignalContext(Arc<InnerSignalContext>);
25
26impl SignalContext {
27    /// Create a [`SignalContext`]
28    pub fn new(config: Arc<Config>) -> Self {
29        let log_subscriber = config.create_log_subscriber("signal");
30        dispatcher::with_default(&log_subscriber.clone().into(), || {
31            tracing::info!(language = ?config.language, "creating signal context");
32        });
33        let ctx = Self(Arc::new(InnerSignalContext {
34            http_cli: config.create_http_client(),
35            log_subscriber,
36        }));
37        dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || {
38            tracing::info!("signal context created");
39        });
40        ctx
41    }
42
43    /// Returns the log subscriber
44    #[inline]
45    pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
46        self.0.log_subscriber.clone()
47    }
48
49    /// Query signals, filtered by symbol, strategy, catalyst and time range.
50    ///
51    /// Path: `GET /v1/signals`
52    pub async fn signals(&self, opts: SignalsOptions) -> Result<SignalsResponse> {
53        Ok(self
54            .0
55            .http_cli
56            .request(Method::GET, "/v1/signals")
57            .query_params(opts)
58            .response::<Json<SignalsResponse>>()
59            .send()
60            .with_subscriber(self.0.log_subscriber.clone())
61            .await?
62            .0)
63    }
64
65    /// Get one signal by ID, including the full analysis in
66    /// [`Signal::json_data`].
67    ///
68    /// Path: `GET /v1/signals/{signal_id}`
69    pub async fn signal(&self, signal_id: impl Into<String>) -> Result<Signal> {
70        #[derive(Debug, Deserialize)]
71        struct Response {
72            signal: Signal,
73        }
74
75        let signal_id = signal_id.into();
76        Ok(self
77            .0
78            .http_cli
79            .request(Method::GET, format!("/v1/signals/{signal_id}"))
80            .response::<Json<Response>>()
81            .send()
82            .with_subscriber(self.0.log_subscriber.clone())
83            .await?
84            .0
85            .signal)
86    }
87
88    /// List the fact (catalyst) events for one security — anomaly detections,
89    /// factor readings, data sources and natural-language summaries.
90    ///
91    /// Facts are what strategies react to: a signal names the fact that
92    /// triggered it in [`Signal::key_fact_id`].
93    ///
94    /// Path: `GET /v1/facts/security_facts`
95    pub async fn security_facts(&self, opts: SecurityFactsOptions) -> Result<Vec<SecurityFact>> {
96        #[derive(Debug, Deserialize)]
97        struct Response {
98            #[serde(default)]
99            facts: Vec<SecurityFact>,
100        }
101
102        Ok(self
103            .0
104            .http_cli
105            .request(Method::GET, "/v1/facts/security_facts")
106            .query_params(opts)
107            .response::<Json<Response>>()
108            .send()
109            .with_subscriber(self.0.log_subscriber.clone())
110            .await?
111            .0
112            .facts)
113    }
114}