longport/screener/
context.rs1use std::sync::Arc;
2
3use longport_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#[derive(Clone)]
24pub struct ScreenerContext(Arc<InnerScreenerContext>);
25
26impl ScreenerContext {
27 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 #[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: &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: &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 pub async fn screener_recommend_strategies(
89 &self,
90 market: impl Into<String>,
91 ) -> Result<ScreenerRecommendStrategiesResponse> {
92 #[derive(Serialize)]
93 struct Query {
94 market: String,
95 }
96 let raw: serde_json::Value = self
97 .get(
98 "/v1/quote/ai/screener/strategies/recommend",
99 Query {
100 market: market.into(),
101 },
102 )
103 .await?;
104 Ok(ScreenerRecommendStrategiesResponse { data: raw })
105 }
106
107 pub async fn screener_user_strategies(
113 &self,
114 market: impl Into<String>,
115 ) -> Result<ScreenerUserStrategiesResponse> {
116 #[derive(Serialize)]
117 struct Query {
118 market: String,
119 }
120 let raw: serde_json::Value = self
121 .get(
122 "/v1/quote/ai/screener/strategies/mine",
123 Query {
124 market: market.into(),
125 },
126 )
127 .await?;
128 Ok(ScreenerUserStrategiesResponse { data: raw })
129 }
130
131 pub async fn screener_strategy(&self, id: i64) -> Result<ScreenerStrategyResponse> {
141 let path = format!("/v1/quote/ai/screener/strategy/{id}");
142 #[derive(Serialize)]
143 struct Empty {}
144 let mut raw: serde_json::Value = self.get(&path, Empty {}).await?;
145 if let Some(filters) = raw["filter"]["filters"].as_array_mut() {
147 for f in filters.iter_mut() {
148 if let Some(k) = f["key"].as_str() {
149 let stripped = k.strip_prefix("filter_").unwrap_or(k).to_string();
150 f["key"] = serde_json::Value::String(stripped);
151 }
152 }
153 }
154 Ok(ScreenerStrategyResponse { data: raw })
155 }
156
157 const DEFAULT_RETURNS: &'static [&'static str] = &[
161 "filter_prevclose",
162 "filter_prevchg",
163 "filter_marketcap",
164 "filter_salesgrowthyoy",
165 "filter_pettm",
166 "filter_pbmrq",
167 "filter_industry",
168 ];
169
170 pub async fn screener_search(
195 &self,
196 market: impl Into<String>,
197 strategy_id: Option<i64>,
198 conditions: Vec<ScreenerCondition>,
199 show: Vec<String>,
200 page: u32,
201 size: u32,
202 ) -> Result<ScreenerSearchResponse> {
203 let market: String = market.into();
204
205 let (effective_market, filters) = if let Some(sid) = strategy_id {
207 let path = format!("/v1/quote/ai/screener/strategy/{sid}");
209 #[derive(Serialize)]
210 struct Empty {}
211 let strategy: serde_json::Value = self.get(&path, Empty {}).await?;
212
213 let mkt_val = strategy["market"].as_str().unwrap_or("US").to_uppercase();
214 let mkt = if mkt_val.is_empty() || mkt_val == "-" {
215 "US".to_string()
216 } else {
217 mkt_val
218 };
219
220 let mut filters: Vec<serde_json::Value> = Vec::new();
221 if let Some(f) = strategy["filter"]["filters"].as_array() {
222 for ind in f {
223 let key = ind["key"].as_str().unwrap_or("").to_string();
224 if key.is_empty() {
225 continue;
226 }
227 let min = ind["min"].as_str().unwrap_or("").to_string();
228 let max = ind["max"].as_str().unwrap_or("").to_string();
229 let tech_values = if ind["tech_values"].is_object() {
230 ind["tech_values"].clone()
231 } else {
232 serde_json::json!({})
233 };
234 filters.push(serde_json::json!({
235 "key": key,
236 "min": min,
237 "max": max,
238 "tech_values": tech_values,
239 }));
240 }
241 }
242 (mkt, filters)
243 } else {
244 let filters: Vec<serde_json::Value> = conditions
246 .iter()
247 .filter(|c| !c.key.is_empty())
248 .map(|c| {
249 let api_key = if c.key.starts_with("filter_") {
250 c.key.clone()
251 } else {
252 format!("filter_{}", c.key)
253 };
254 let tv = if c.tech_values.is_object() {
255 c.tech_values.clone()
256 } else {
257 serde_json::json!({})
258 };
259 serde_json::json!({
260 "key": api_key,
261 "min": c.min,
262 "max": c.max,
263 "tech_values": tv,
264 })
265 })
266 .collect();
267 (market, filters)
268 };
269
270 let mut returns: Vec<String> = Self::DEFAULT_RETURNS
272 .iter()
273 .map(|s| s.to_string())
274 .collect();
275 for f in &filters {
277 if let Some(k) = f["key"].as_str() {
278 let api_key = if k.starts_with("filter_") {
279 k.to_string()
280 } else {
281 format!("filter_{k}")
282 };
283 if !returns.contains(&api_key) {
284 returns.push(api_key);
285 }
286 }
287 }
288 for s in &show {
290 let api_key = if s.starts_with("filter_") {
291 s.clone()
292 } else {
293 format!("filter_{s}")
294 };
295 if !returns.contains(&api_key) {
296 returns.push(api_key);
297 }
298 }
299
300 let body = serde_json::json!({
302 "market": effective_market,
303 "filters": filters,
304 "returns": returns,
305 "page": page,
306 "size": size,
307 });
308
309 let raw: serde_json::Value = self.post("/v1/quote/ai/screener/search", body).await?;
310 Ok(ScreenerSearchResponse {
311 data: strip_filter_prefix_from_search_results(raw),
312 })
313 }
314
315 pub async fn screener_indicators(&self) -> Result<ScreenerIndicatorsResponse> {
326 #[derive(Serialize)]
327 struct Empty {}
328 let mut raw: serde_json::Value = self
329 .get("/v1/quote/ai/screener/indicators", Empty {})
330 .await?;
331 if let Some(groups) = raw["groups"].as_array_mut() {
332 for group in groups.iter_mut() {
333 if let Some(indicators) = group["indicators"].as_array_mut() {
334 for ind in indicators.iter_mut() {
335 if let Some(k) = ind["key"].as_str() {
337 let stripped = k.strip_prefix("filter_").unwrap_or(k).to_string();
338 ind["key"] = serde_json::Value::String(stripped);
339 }
340 if let Some(tech_inds) = ind["tech_indicators"].as_array().cloned() {
342 let tv: serde_json::Map<String, serde_json::Value> = tech_inds
343 .iter()
344 .filter_map(|ti| {
345 let key = ti["tech_key"].as_str()?.to_string();
346 let opts: Vec<serde_json::Value> = ti["tech_items"]
347 .as_array()
348 .unwrap_or(&vec![])
349 .iter()
350 .map(|item| {
351 serde_json::json!({
352 "value": item["item_value"].as_str().unwrap_or(""),
353 "label": item["item_name"].as_str().unwrap_or(""),
354 })
355 })
356 .collect();
357 Some((key, serde_json::Value::Array(opts)))
358 })
359 .collect();
360 if !tv.is_empty() {
361 ind["tech_values"] = serde_json::Value::Object(tv);
362 }
363 }
364 }
365 }
366 }
367 }
368 Ok(ScreenerIndicatorsResponse { data: raw })
369 }
370}
371
372fn strip_filter_prefix_from_search_results(mut raw: serde_json::Value) -> serde_json::Value {
375 if let Some(items) = raw["items"].as_array_mut() {
376 for item in items.iter_mut() {
377 if let Some(indicators) = item["indicators"].as_array_mut() {
378 for ind in indicators.iter_mut() {
379 if let Some(k) = ind["key"].as_str() {
380 let stripped = k.strip_prefix("filter_").unwrap_or(k).to_string();
381 ind["key"] = serde_json::Value::String(stripped);
382 }
383 }
384 }
385 }
386 }
387 raw
388}