Skip to main content

longbridge/signal/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_repr::{Deserialize_repr, Serialize_repr};
3use strum_macros::{Display, EnumString};
4use time::OffsetDateTime;
5
6use crate::serde_utils;
7
8/// Options for [`crate::SignalContext::signals`]
9///
10/// Every field is a filter; leaving one unset removes that filter.
11#[derive(Debug, Clone, Default, Serialize)]
12pub struct SignalsOptions {
13    /// Filter by security symbol, e.g. `AAPL.US` or `700.HK`. If omitted,
14    /// returns signals for all symbols.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub symbol_name: Option<String>,
17    /// Filter by strategy id, e.g. `buffett-value`. Preferred over the
18    /// deprecated `strategy_name`; takes precedence when both are provided.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub strategy_id: Option<String>,
21    /// Filter by strategy name. If omitted, returns signals from all
22    /// strategies.
23    ///
24    /// Deprecated in favour of [`SignalsOptions::strategy_id`].
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub strategy_name: Option<String>,
27    /// Filter by the name of the factor that triggered the signal, e.g.
28    /// `EARNINGS_RELEASED` or `macd_12_26_9` — the `factors[].name` of the
29    /// triggering fact, not the display label a signal carries in
30    /// [`Signal::key_catalyst`]. If omitted, signals with any catalyst name
31    /// are returned.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub catalyst_name: Option<String>,
34    /// Filter by the catalyst type that triggered the signal, e.g. `News`,
35    /// `Fundamental`, `Technical`. If omitted, signals with any catalyst type
36    /// are returned.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub catalyst_type: Option<String>,
39    /// Only return signals created at or after this time. Sent as RFC3339;
40    /// the API also accepts a Unix timestamp. If omitted, no lower bound.
41    #[serde(
42        skip_serializing_if = "Option::is_none",
43        with = "serde_utils::rfc3339_opt"
44    )]
45    pub start_time: Option<OffsetDateTime>,
46    /// Only return signals created at or before this time. Sent as RFC3339;
47    /// the API also accepts a Unix timestamp. If omitted, no upper bound.
48    #[serde(
49        skip_serializing_if = "Option::is_none",
50        with = "serde_utils::rfc3339_opt"
51    )]
52    pub end_time: Option<OffsetDateTime>,
53    /// Maximum number of results to return. Defaults to 20.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub limit: Option<i32>,
56    /// Number of results to skip for pagination. Defaults to 0.
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub offset: Option<i32>,
59}
60
61/// Direction a strategy expects the security to take.
62///
63/// Wire values are the five labels the API returns, matching the
64/// `core_conclusion.outlook_enum` scale 1..=5 inside [`Signal::json_data`].
65#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
66pub enum Outlook {
67    /// Unknown
68    Unknown,
69    /// Strong bullish (`outlook_enum` 1)
70    #[strum(serialize = "Strong bullish")]
71    StrongBullish,
72    /// Bullish (`outlook_enum` 2)
73    Bullish,
74    /// Neutral (`outlook_enum` 3)
75    Neutral,
76    /// Bearish (`outlook_enum` 4)
77    Bearish,
78    /// Strong bearish (`outlook_enum` 5)
79    #[strum(serialize = "Strong bearish")]
80    StrongBearish,
81}
82
83impl_default_for_enum_string!(Outlook);
84impl_serde_for_enum_string!(Outlook);
85
86/// Where a signal is in its lifecycle.
87#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)]
88#[repr(i32)]
89pub enum SignalStatus {
90    /// Generated, not yet published
91    Pending = 0,
92    /// Published and current — the only status the API serves today
93    #[default]
94    Active = 1,
95    /// Deleted
96    Deleted = 2,
97    /// The strategy analysis failed to generate
98    AiFailed = 3,
99    /// Filtered out by a human reviewer
100    FilteredByManual = 4,
101    /// The strategy analysis failed to submit
102    AiSubmitFailed = 5,
103    /// A status this SDK does not know yet
104    #[serde(other)]
105    Unknown = -1,
106}
107
108/// One signal: a strategy's take on a security, triggered by a catalyst.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct Signal {
111    /// Signal ID, e.g. `sign_992_1a00c9425c3_48ab`. Pass it to
112    /// [`crate::SignalContext::signal`] for the full record.
113    pub id: String,
114    /// Security symbol, e.g. `992.HK`
115    #[serde(default)]
116    pub symbol: String,
117    /// Company name
118    #[serde(default)]
119    pub company_name: String,
120    /// Market the security trades in, e.g. `HK`
121    #[serde(default)]
122    pub market: String,
123    /// Signal headline
124    #[serde(default)]
125    pub title: String,
126    /// Natural-language summary of the signal, in Markdown
127    #[serde(default)]
128    pub summary: String,
129    /// Strategy ID that produced the signal
130    #[serde(default)]
131    pub strategy_id: String,
132    /// Strategy name that produced the signal
133    #[serde(default)]
134    pub strategy_name: String,
135    /// Who recommended the signal; empty for strategy-generated signals
136    #[serde(default)]
137    pub recommend_by: String,
138    /// Strategy expression, e.g. `992.HK:GROWTH:long`
139    #[serde(default)]
140    pub expression: String,
141    /// ID of the fact that triggered the signal
142    #[serde(default)]
143    pub key_fact_id: String,
144    /// Display label of the catalyst that triggered the signal, e.g.
145    /// `Q1 Revenue Surge`. This is prose meant for display — filtering with
146    /// [`SignalsOptions::catalyst_name`] matches the underlying factor name
147    /// instead
148    #[serde(default)]
149    pub key_catalyst: String,
150    /// Price the analysis was based on
151    #[serde(default)]
152    pub analysis_price: f64,
153    /// Conservative-scenario target price
154    #[serde(default)]
155    pub conservative_price: f64,
156    /// Benchmark-scenario target price
157    #[serde(default)]
158    pub benchmark_price: f64,
159    /// Optimistic-scenario target price
160    #[serde(default)]
161    pub optimistic_price: f64,
162    /// Outlook the strategy takes on the security
163    #[serde(default)]
164    pub outlook: Outlook,
165    /// Outlook label in the caller's language — the localized rendering of
166    /// [`Signal::outlook`]
167    #[serde(default)]
168    pub outlook_desc: String,
169    /// Where the signal is in its lifecycle
170    #[serde(default)]
171    pub status: SignalStatus,
172    /// Full analysis behind the signal, as a JSON document: strategy fit
173    /// scores, valuation scenarios, evidence sources and related fact IDs.
174    /// Carried verbatim because its shape is strategy-specific.
175    #[serde(default)]
176    pub json_data: String,
177    /// Creation time
178    #[serde(
179        serialize_with = "time::serde::rfc3339::serialize",
180        deserialize_with = "serde_utils::timestamp_ms::deserialize"
181    )]
182    pub created_at: OffsetDateTime,
183    /// Last update time
184    #[serde(
185        serialize_with = "time::serde::rfc3339::serialize",
186        deserialize_with = "serde_utils::timestamp_ms::deserialize"
187    )]
188    pub updated_at: OffsetDateTime,
189}
190
191/// A page of signals returned by [`crate::SignalContext::signals`]
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct SignalsResponse {
194    /// Signals on this page
195    #[serde(default)]
196    pub signals: Vec<Signal>,
197    /// Total number of signals matching the filters, for paging with
198    /// [`SignalsOptions::offset`]
199    #[serde(default)]
200    pub total: i32,
201}
202
203/// Options for [`crate::SignalContext::security_facts`]
204#[derive(Debug, Clone, Default, Serialize)]
205pub struct SecurityFactsOptions {
206    /// The security to query, e.g. `AAPL.US` or `700.HK`. Required.
207    pub symbol: String,
208    /// Start of the query window. If omitted, the query includes the earliest
209    /// available data.
210    #[serde(
211        skip_serializing_if = "Option::is_none",
212        with = "serde_utils::rfc3339_opt"
213    )]
214    pub begin_time: Option<OffsetDateTime>,
215    /// End of the query window. If omitted, the query returns the latest data.
216    #[serde(
217        skip_serializing_if = "Option::is_none",
218        with = "serde_utils::rfc3339_opt"
219    )]
220    pub end_time: Option<OffsetDateTime>,
221    /// Maximum number of facts to return. When more facts fall inside the time
222    /// range, only the latest `limit` are returned. Defaults to 100.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub limit: Option<i32>,
225}
226
227impl SecurityFactsOptions {
228    /// Create a [`SecurityFactsOptions`] for one security
229    pub fn new(symbol: impl Into<String>) -> Self {
230        Self {
231            symbol: symbol.into(),
232            ..Default::default()
233        }
234    }
235}
236
237/// Kind of fact, and of the source that produced it.
238#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
239pub enum FactType {
240    /// Unknown
241    Unknown,
242    /// Derived from news
243    News,
244    /// Derived from fundamentals
245    Fundamental,
246    /// Derived from technical indicators
247    Technical,
248}
249
250impl_default_for_enum_string!(FactType);
251impl_serde_for_enum_string!(FactType);
252
253/// Side a fact or factor points to.
254#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
255pub enum FactDirection {
256    /// Unknown
257    #[strum(serialize = "")]
258    Unknown,
259    /// Long
260    #[strum(serialize = "long")]
261    Long,
262    /// Short
263    #[strum(serialize = "short")]
264    Short,
265    /// Neutral — the fact points either way
266    #[strum(serialize = "neutral")]
267    Neutral,
268}
269
270impl_default_for_enum_string!(FactDirection);
271impl_serde_for_enum_string!(FactDirection);
272
273/// Where a fact came from.
274#[derive(Debug, Clone, Default, Serialize, Deserialize)]
275pub struct FactDataSource {
276    /// Source name, e.g. `Nasdaq`
277    #[serde(default)]
278    pub source_name: String,
279    /// Kind of source
280    #[serde(default, rename = "type")]
281    pub source_type: FactType,
282    /// Link to the source, when it has one
283    #[serde(default)]
284    pub url: String,
285    /// Source icon URL, when it has one
286    #[serde(default)]
287    pub icon: String,
288}
289
290/// Thresholds an anomaly test was scored against.
291#[derive(Debug, Clone, Default, Serialize, Deserialize)]
292pub struct AnomalyThresholds {
293    /// Low threshold
294    #[serde(default)]
295    pub low: String,
296    /// Medium threshold
297    #[serde(default)]
298    pub medium: String,
299    /// High threshold
300    #[serde(default)]
301    pub high: String,
302}
303
304/// Outcome of the anomaly test behind a factor. Fields are empty for factors
305/// that did not run one.
306#[derive(Debug, Clone, Default, Serialize, Deserialize)]
307pub struct AnomalyDetection {
308    /// Test outcome
309    #[serde(default)]
310    pub anomaly_result: String,
311    /// Significance level of the test
312    #[serde(default)]
313    pub significance_level: String,
314    /// Method used, e.g. a statistical test name
315    #[serde(default)]
316    pub test_method: String,
317    /// Thresholds the result was scored against
318    #[serde(default)]
319    pub thresholds: AnomalyThresholds,
320}
321
322/// One factor that contributed to a fact.
323#[derive(Debug, Clone, Default, Serialize, Deserialize)]
324pub struct FactFactor {
325    /// Factor name, e.g. `rsi_14`
326    #[serde(default)]
327    pub name: String,
328    /// Groups the factor belongs to, e.g. `MOMENTUM`
329    #[serde(default)]
330    pub factor_groups: Vec<String>,
331    /// Side the factor points to
332    #[serde(default)]
333    pub long_short_direction: FactDirection,
334    /// Condition that fired the factor
335    #[serde(default)]
336    pub trigger_condition: String,
337    /// Anomaly test behind the factor
338    #[serde(default)]
339    pub anomaly_detection: AnomalyDetection,
340}
341
342/// A security a fact is about.
343#[derive(Debug, Clone, Default, Serialize, Deserialize)]
344pub struct FactSymbol {
345    /// Security symbol, e.g. `AAPL.US`
346    #[serde(default)]
347    pub symbol: String,
348    /// Security name in the caller's language
349    #[serde(default)]
350    pub security_name: String,
351}
352
353/// One `{tag, value}` entry from a natural-language field.
354#[derive(Debug, Clone, Default, Serialize, Deserialize)]
355pub struct NlTag {
356    /// What the entry is about, e.g. `RSI`
357    #[serde(default)]
358    pub tag: String,
359    /// The prose
360    #[serde(default)]
361    pub value: String,
362}
363
364/// The natural-language rendering of a fact, in the caller's language.
365#[derive(Debug, Clone, Default, Serialize, Deserialize)]
366pub struct FactNlInfo {
367    /// Headline
368    #[serde(default)]
369    pub title: String,
370    /// Sub-headline
371    #[serde(default)]
372    pub sub_title: String,
373    /// What happened, as a JSON array of `{tag, value}` entries carried in a
374    /// string. Use [`FactNlInfo::summary_tags`] to read it.
375    #[serde(default)]
376    pub summary: String,
377    /// What it may mean for an investor, in the same JSON-in-a-string shape as
378    /// [`FactNlInfo::summary`]. Use [`FactNlInfo::invest_anal_tags`] to read
379    /// it.
380    #[serde(default)]
381    pub invest_anal: String,
382    /// A plain-language walk-through of the fact, in the same JSON-in-a-string
383    /// shape as [`FactNlInfo::summary`]. Use [`FactNlInfo::eli_explain_tags`]
384    /// to read it.
385    #[serde(default)]
386    pub eli_explain: String,
387}
388
389impl FactNlInfo {
390    /// Parse [`FactNlInfo::summary`] into its `{tag, value}` entries.
391    ///
392    /// Returns an empty list when the field is empty or not the expected JSON —
393    /// the raw string stays available either way.
394    pub fn summary_tags(&self) -> Vec<NlTag> {
395        serde_json::from_str(&self.summary).unwrap_or_default()
396    }
397
398    /// Parse [`FactNlInfo::invest_anal`] into its `{tag, value}` entries.
399    ///
400    /// Returns an empty list when the field is empty or not the expected JSON —
401    /// the raw string stays available either way.
402    pub fn invest_anal_tags(&self) -> Vec<NlTag> {
403        serde_json::from_str(&self.invest_anal).unwrap_or_default()
404    }
405
406    /// Parse [`FactNlInfo::eli_explain`] into its `{tag, value}` entries.
407    ///
408    /// Returns an empty list when the field is empty or not the expected JSON —
409    /// the raw string stays available either way.
410    pub fn eli_explain_tags(&self) -> Vec<NlTag> {
411        serde_json::from_str(&self.eli_explain).unwrap_or_default()
412    }
413}
414
415/// A fact (catalyst) event: something that happened to a security, with the
416/// factors, sources and prose behind it.
417///
418/// Facts are what strategies react to — a signal names the fact that triggered
419/// it in [`Signal::key_fact_id`].
420#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct SecurityFact {
422    /// Fact ID, e.g. `technical_rsi_14_short_1783674041337603409`
423    #[serde(default)]
424    pub fact_id: String,
425    /// What kind of fact this is
426    #[serde(default)]
427    pub fact_type: FactType,
428    /// Side the fact points to
429    #[serde(default)]
430    pub direction: FactDirection,
431    /// When the fact occurred
432    #[serde(with = "time::serde::rfc3339")]
433    pub occur_time: OffsetDateTime,
434    /// Securities the fact is about
435    #[serde(default)]
436    pub symbols_info: Vec<FactSymbol>,
437    /// Factors that contributed to the fact
438    #[serde(default)]
439    pub factors: Vec<FactFactor>,
440    /// Where the fact came from
441    #[serde(default)]
442    pub data_source: Vec<FactDataSource>,
443    /// Natural-language rendering of the fact
444    #[serde(default)]
445    pub nl_info: FactNlInfo,
446}