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}