Skip to main content

longbridge/screener/
context.rs

1use std::sync::Arc;
2
3use longbridge_httpcli::{HttpClient, Json, Method};
4use serde::{Serialize, de::DeserializeOwned};
5use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
6
7use crate::{Config, Result, screener::types::*};
8
9struct InnerScreenerContext {
10    http_cli: HttpClient,
11    log_subscriber: Arc<dyn Subscriber + Send + Sync>,
12}
13
14impl Drop for InnerScreenerContext {
15    fn drop(&mut self) {
16        dispatcher::with_default(&self.log_subscriber.clone().into(), || {
17            tracing::info!("screener context dropped");
18        });
19    }
20}
21
22/// Screener context — stock screener strategies, search, and indicators.
23#[derive(Clone)]
24pub struct ScreenerContext(Arc<InnerScreenerContext>);
25
26impl ScreenerContext {
27    /// Create a [`ScreenerContext`]
28    pub fn new(config: Arc<Config>) -> Self {
29        let log_subscriber = config.create_log_subscriber("screener");
30        dispatcher::with_default(&log_subscriber.clone().into(), || {
31            tracing::info!(language = ?config.language, "creating screener context");
32        });
33        let ctx = Self(Arc::new(InnerScreenerContext {
34            http_cli: config.create_http_client(),
35            log_subscriber,
36        }));
37        dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || {
38            tracing::info!("screener 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    async fn get<R, Q>(&self, path: &'static str, query: Q) -> Result<R>
50    where
51        R: DeserializeOwned + Send + Sync + 'static,
52        Q: Serialize + Send + Sync,
53    {
54        Ok(self
55            .0
56            .http_cli
57            .request(Method::GET, path)
58            .query_params(query)
59            .response::<Json<R>>()
60            .send()
61            .with_subscriber(self.0.log_subscriber.clone())
62            .await?
63            .0)
64    }
65
66    async fn post<R, B>(&self, path: &'static str, body: B) -> Result<R>
67    where
68        R: DeserializeOwned + Send + Sync + 'static,
69        B: std::fmt::Debug + Serialize + Send + Sync + 'static,
70    {
71        Ok(self
72            .0
73            .http_cli
74            .request(Method::POST, path)
75            .body(Json(body))
76            .response::<Json<R>>()
77            .send()
78            .with_subscriber(self.0.log_subscriber.clone())
79            .await?
80            .0)
81    }
82
83    // ── screener_recommend_strategies ─────────────────────────────
84
85    /// Get recommended built-in screener strategies.
86    ///
87    /// Path: `GET /v1/quote/screener/strategies/recommend`
88    pub async fn screener_recommend_strategies(
89        &self,
90    ) -> Result<ScreenerRecommendStrategiesResponse> {
91        #[derive(Serialize)]
92        struct Empty {}
93        let raw: serde_json::Value = self
94            .get("/v1/quote/screener/strategies/recommend", Empty {})
95            .await?;
96        Ok(ScreenerRecommendStrategiesResponse { data: raw })
97    }
98
99    // ── screener_user_strategies ──────────────────────────────────
100
101    /// Get the current user's saved screener strategies.
102    ///
103    /// Path: `GET /v1/quote/screener/strategies/mine`
104    pub async fn screener_user_strategies(&self) -> Result<ScreenerUserStrategiesResponse> {
105        #[derive(Serialize)]
106        struct Empty {}
107        let raw: serde_json::Value = self
108            .get("/v1/quote/screener/strategies/mine", Empty {})
109            .await?;
110        Ok(ScreenerUserStrategiesResponse { data: raw })
111    }
112
113    // ── screener_strategy ─────────────────────────────────────────
114
115    /// Get detail for one screener strategy by ID.
116    ///
117    /// Path: `GET /v1/quote/screener/strategy?id=<id>`
118    pub async fn screener_strategy(&self, id: i64) -> Result<ScreenerStrategyResponse> {
119        #[derive(Serialize)]
120        struct Query {
121            id: i64,
122        }
123        let raw: serde_json::Value = self
124            .get("/v1/quote/screener/strategy", Query { id })
125            .await?;
126        Ok(ScreenerStrategyResponse { data: raw })
127    }
128
129    // ── screener_search ───────────────────────────────────────────
130
131    /// Search / screen securities using a strategy.
132    ///
133    /// Path: `POST /v1/quote/screener/search`
134    ///
135    /// When `strategy_id` is `Some`, it is included in the request body.
136    /// When `None`, only `market`, `page`, and `size` are sent (custom
137    /// filter support is out of scope for this SDK).
138    pub async fn screener_search(
139        &self,
140        market: impl Into<String>,
141        strategy_id: Option<i64>,
142        page: u32,
143        size: u32,
144    ) -> Result<ScreenerSearchResponse> {
145        #[derive(Debug, Serialize)]
146        struct Body {
147            market: String,
148            #[serde(skip_serializing_if = "Option::is_none")]
149            strategy_id: Option<i64>,
150            page: u32,
151            size: u32,
152        }
153        let raw: serde_json::Value = self
154            .post(
155                "/v1/quote/screener/search",
156                Body {
157                    market: market.into(),
158                    strategy_id,
159                    page,
160                    size,
161                },
162            )
163            .await?;
164        Ok(ScreenerSearchResponse { data: raw })
165    }
166
167    // ── screener_indicators ───────────────────────────────────────
168
169    /// Get all available screener indicator definitions.
170    ///
171    /// Path: `GET /v1/quote/screener/indicators`
172    pub async fn screener_indicators(&self) -> Result<ScreenerIndicatorsResponse> {
173        #[derive(Serialize)]
174        struct Empty {}
175        let raw: serde_json::Value = self.get("/v1/quote/screener/indicators", Empty {}).await?;
176        Ok(ScreenerIndicatorsResponse { data: raw })
177    }
178}