1use super::{DataContent, DataItem, StreamService};
38use crate::common::{AssetMainType, AssetSubType};
39use serde::{Deserialize, Serialize};
40use serde_json::{Map, Value};
41use std::collections::HashMap;
42
43#[derive(Debug, Clone, Copy)]
45#[non_exhaustive]
46pub struct Field {
47 pub number: u32,
49 pub name: &'static str,
51 pub nested: Option<&'static [Field]>,
54}
55
56impl Field {
57 const fn new(number: u32, name: &'static str) -> Self {
58 Self {
59 number,
60 name,
61 nested: None,
62 }
63 }
64
65 const fn nested(number: u32, name: &'static str, table: &'static [Field]) -> Self {
66 Self {
67 number,
68 name,
69 nested: Some(table),
70 }
71 }
72}
73
74pub(super) fn csv(numbers: impl Iterator<Item = u32>) -> String {
76 numbers
77 .map(|number| number.to_string())
78 .collect::<Vec<_>>()
79 .join(",")
80}
81
82fn lookup<'t>(key: &str, table: &'t [Field]) -> Option<&'t Field> {
86 let number = key.parse::<u32>().ok()?;
87 table
88 .get(number as usize)
89 .filter(|field| field.number == number)
90 .or_else(|| table.iter().find(|field| field.number == number))
91}
92
93fn rename(
96 entries: impl IntoIterator<Item = (String, Value)>,
97 table: &[Field],
98) -> Map<String, Value> {
99 entries
100 .into_iter()
101 .map(|(key, value)| match lookup(&key, table) {
102 Some(field) => {
103 let value = match field.nested {
104 Some(nested) => rename_nested(value, nested),
105 None => value,
106 };
107 (field.name.to_owned(), value)
108 }
109 None => (key, value),
110 })
111 .collect()
112}
113
114fn rename_nested(value: Value, table: &[Field]) -> Value {
115 match value {
116 Value::Array(items) => Value::Array(
117 items
118 .into_iter()
119 .map(|item| rename_nested(item, table))
120 .collect(),
121 ),
122 Value::Object(map) => Value::Object(rename(map, table)),
123 other => other,
124 }
125}
126
127impl DataContent {
128 pub fn named_fields(&self, table: &[Field]) -> Map<String, Value> {
132 rename(
133 self.fields.iter().map(|(k, v)| (k.clone(), v.clone())),
134 table,
135 )
136 }
137
138 pub fn to_named_value(&self, table: &[Field]) -> Value {
142 self.clone().into_named_value(table)
143 }
144
145 pub fn into_named_value(self, table: &[Field]) -> Value {
148 let mut map = rename(self.fields, table);
149 map.insert("key".into(), Value::String(self.key));
150 if let Some(delayed) = self.delayed {
151 map.insert("delayed".into(), Value::Bool(delayed));
152 }
153 if let Some(Ok(asset)) = self.asset_main_type.map(serde_json::to_value) {
154 map.insert("assetMainType".into(), asset);
155 }
156 if let Some(Ok(asset)) = self.asset_sub_type.map(serde_json::to_value) {
157 map.insert("assetSubType".into(), asset);
158 }
159 if let Some(cusip) = self.cusip {
160 map.insert("cusip".into(), Value::String(cusip));
161 }
162 Value::Object(map)
163 }
164}
165
166macro_rules! field_table {
175 (
176 $(#[$doc:meta])*
177 $table:ident => $payload:ident {
178 key = $key:ident;
179 envelope = { $( $env:ident : $env_ty:ty ),* $(,)? };
180 $( default = [ $( $default:ident ),* $(,)? ]; )?
181 $(
182 $(#[$field_doc:meta])*
183 $number:literal $konst:ident $field:ident $name:literal : $ty:ty
184 $([ $nested:ident ])? ,
185 )*
186 }
187 ) => {
188 #[doc = concat!("Field numbers and names for [`", stringify!($payload), "`].")]
189 #[derive(Debug, Clone, Copy)]
190 pub struct $table;
191
192 impl $table {
193 pub const $key: u32 = 0;
195 $( pub const $konst: u32 = $number; )*
196
197 pub const FIELDS: &'static [Field] = &[
199 Field::new(0, "key"),
200 $( field_table!(@field $number $name $($nested)?), )*
201 ];
202
203 pub fn name(number: u32) -> Option<&'static str> {
205 Self::FIELDS
206 .iter()
207 .find(|field| field.number == number)
208 .map(|field| field.name)
209 }
210
211 pub fn all_fields() -> String {
213 csv(Self::FIELDS.iter().map(|field| field.number))
214 }
215
216 $(
217 pub fn default_fields() -> String {
220 csv([$( Self::$default ),*].into_iter())
221 }
222 )?
223 }
224
225 $(#[$doc])*
226 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
227 #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
228 #[serde(rename_all = "camelCase")]
229 pub struct $payload {
230 pub key: String,
232 $(
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub $env: Option<$env_ty>,
235 )*
236 $(
237 $(#[$field_doc])*
238 #[serde(rename = $name, default, skip_serializing_if = "Option::is_none")]
239 pub $field: Option<$ty>,
240 )*
241 #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
243 pub extra: HashMap<String, Value>,
244 }
245
246 impl $payload {
247 #[doc = concat!(
248 "Decode a content item using [`", stringify!($table), "::FIELDS`]."
249 )]
250 pub fn from_content(content: &DataContent) -> Result<Self, serde_json::Error> {
251 serde_json::from_value(content.to_named_value($table::FIELDS))
252 }
253 }
254 };
255 (@field $number:literal $name:literal) => {
256 Field::new($number, $name)
257 };
258 (@field $number:literal $name:literal $nested:ident) => {
259 Field::nested($number, $name, $nested::FIELDS)
260 };
261}
262
263field_table! {
264 LevelOneEquityFields => LevelOneEquity {
266 key = SYMBOL;
267 envelope = {
268 delayed: bool,
269 asset_main_type: AssetMainType,
270 asset_sub_type: AssetSubType,
271 cusip: String,
272 };
273 default = [
274 SYMBOL, BID_PRICE, ASK_PRICE, LAST_PRICE, BID_SIZE, ASK_SIZE, TOTAL_VOLUME,
275 LAST_SIZE, HIGH_PRICE, LOW_PRICE, CLOSE_PRICE, OPEN_PRICE, NET_CHANGE,
276 NET_CHANGE_PERCENT, QUOTE_TIME_MILLIS, TRADE_TIME_MILLIS,
277 ];
278 1 BID_PRICE bid_price "bidPrice": f64,
279 2 ASK_PRICE ask_price "askPrice": f64,
280 3 LAST_PRICE last_price "lastPrice": f64,
281 4 BID_SIZE bid_size "bidSize": i64,
282 5 ASK_SIZE ask_size "askSize": i64,
283 6 ASK_ID ask_id "askId": String,
285 7 BID_ID bid_id "bidId": String,
287 8 TOTAL_VOLUME total_volume "totalVolume": i64,
288 9 LAST_SIZE last_size "lastSize": i64,
289 10 HIGH_PRICE high_price "highPrice": f64,
290 11 LOW_PRICE low_price "lowPrice": f64,
291 12 CLOSE_PRICE close_price "closePrice": f64,
293 13 EXCHANGE_ID exchange_id "exchangeId": String,
295 14 MARGINABLE marginable "marginable": bool,
296 15 DESCRIPTION description "description": String,
297 16 LAST_ID last_id "lastId": String,
299 17 OPEN_PRICE open_price "openPrice": f64,
300 18 NET_CHANGE net_change "netChange": f64,
301 19 HIGH_52_WEEK fifty_two_week_high "52WeekHigh": f64,
302 20 LOW_52_WEEK fifty_two_week_low "52WeekLow": f64,
303 21 PE_RATIO pe_ratio "peRatio": f64,
304 22 DIV_AMOUNT div_amount "divAmount": f64,
306 23 DIV_YIELD div_yield "divYield": f64,
307 24 NAV nav "nav": f64,
309 25 EXCHANGE_NAME exchange_name "exchangeName": String,
310 26 DIV_DATE div_date "divDate": String,
311 27 IS_REGULAR_MARKET_QUOTE regular_market_quote "regularMarketQuote": bool,
313 28 IS_REGULAR_MARKET_TRADE regular_market_trade "regularMarketTrade": bool,
315 29 REGULAR_MARKET_LAST_PRICE regular_market_last_price "regularMarketLastPrice": f64,
316 30 REGULAR_MARKET_LAST_SIZE regular_market_last_size "regularMarketLastSize": i64,
317 31 REGULAR_MARKET_NET_CHANGE regular_market_net_change "regularMarketNetChange": f64,
318 32 SECURITY_STATUS security_status "securityStatus": String,
319 33 MARK mark "mark": f64,
320 34 QUOTE_TIME_MILLIS quote_time "quoteTime": i64,
322 35 TRADE_TIME_MILLIS trade_time "tradeTime": i64,
324 36 REGULAR_MARKET_TRADE_MILLIS regular_market_trade_time "regularMarketTradeTime": i64,
326 37 BID_TIME_MILLIS bid_time "bidTime": i64,
328 38 ASK_TIME_MILLIS ask_time "askTime": i64,
330 39 ASK_MIC_ID ask_mic_id "askMICId": String,
331 40 BID_MIC_ID bid_mic_id "bidMICId": String,
332 41 LAST_MIC_ID last_mic_id "lastMICId": String,
333 42 NET_CHANGE_PERCENT net_percent_change "netPercentChange": f64,
334 43 REGULAR_MARKET_CHANGE_PERCENT regular_market_percent_change
335 "regularMarketPercentChange": f64,
336 44 MARK_CHANGE mark_change "markChange": f64,
337 45 MARK_CHANGE_PERCENT mark_percent_change "markPercentChange": f64,
338 46 HTB_QUANTITY htb_quantity "htbQuantity": i64,
339 47 HTB_RATE htb_rate "htbRate": f64,
340 48 HARD_TO_BORROW hard_to_borrow "hardToBorrow": i64,
342 49 IS_SHORTABLE shortable "shortable": i64,
344 50 POST_MARKET_NET_CHANGE post_market_change "postMarketChange": f64,
345 51 POST_MARKET_CHANGE_PERCENT post_market_percent_change "postMarketPercentChange": f64,
346 }
347}
348
349field_table! {
350 LevelOneOptionFields => LevelOneOption {
352 key = SYMBOL;
353 envelope = { delayed: bool, asset_main_type: AssetMainType };
354 1 DESCRIPTION description "description": String,
355 2 BID_PRICE bid_price "bidPrice": f64,
356 3 ASK_PRICE ask_price "askPrice": f64,
357 4 LAST_PRICE last_price "lastPrice": f64,
358 5 HIGH_PRICE high_price "highPrice": f64,
359 6 LOW_PRICE low_price "lowPrice": f64,
360 7 CLOSE_PRICE close_price "closePrice": f64,
362 8 TOTAL_VOLUME total_volume "totalVolume": i64,
363 9 OPEN_INTEREST open_interest "openInterest": i64,
364 10 VOLATILITY volatility "volatility": f64,
365 11 INTRINSIC_VALUE money_intrinsic_value "moneyIntrinsicValue": f64,
366 12 EXP_YEAR expiration_year "expirationYear": i64,
367 13 MULTIPLIER multiplier "multiplier": f64,
368 14 DIGITS digits "digits": i64,
370 15 OPEN_PRICE open_price "openPrice": f64,
371 16 BID_SIZE bid_size "bidSize": i64,
372 17 ASK_SIZE ask_size "askSize": i64,
373 18 LAST_SIZE last_size "lastSize": i64,
374 19 NET_CHANGE net_change "netChange": f64,
375 20 STRIKE_PRICE strike_price "strikePrice": f64,
376 21 CONTRACT_TYPE contract_type "contractType": String,
378 22 UNDERLYING underlying "underlying": String,
379 23 EXP_MONTH expiration_month "expirationMonth": i64,
380 24 DELIVERABLES deliverables "deliverables": String,
381 25 TIME_VALUE time_value "timeValue": f64,
382 26 EXP_DAY expiration_day "expirationDay": i64,
383 27 DAYS_TO_EXP days_to_expiration "daysToExpiration": i64,
384 28 DELTA delta "delta": f64,
385 29 GAMMA gamma "gamma": f64,
386 30 THETA theta "theta": f64,
387 31 VEGA vega "vega": f64,
388 32 RHO rho "rho": f64,
389 33 SECURITY_STATUS security_status "securityStatus": String,
390 34 THEORETICAL_VALUE theoretical_option_value "theoreticalOptionValue": f64,
391 35 UNDERLYING_PRICE underlying_price "underlyingPrice": f64,
392 36 UV_EXPIRATION_TYPE uv_expiration_type "uvExpirationType": String,
393 37 MARK mark "mark": f64,
394 38 QUOTE_TIME_MILLIS quote_time "quoteTime": i64,
396 39 TRADE_TIME_MILLIS trade_time "tradeTime": i64,
398 40 EXCHANGE exchange "exchange": String,
399 41 EXCHANGE_NAME exchange_name "exchangeName": String,
400 42 LAST_TRADING_DAY last_trading_day "lastTradingDay": i64,
402 43 SETTLEMENT_TYPE settlement_type "settlementType": String,
403 44 NET_PERCENT_CHANGE net_percent_change "netPercentChange": f64,
404 45 MARK_NET_CHANGE mark_change "markChange": f64,
405 46 MARK_PERCENT_CHANGE mark_percent_change "markPercentChange": f64,
406 47 IMPLIED_YIELD implied_yield "impliedYield": f64,
407 48 IS_PENNY_PILOT is_penny_pilot "isPennyPilot": bool,
408 49 OPTION_ROOT option_root "optionRoot": String,
409 50 HIGH_52_WEEK fifty_two_week_high "52WeekHigh": f64,
410 51 LOW_52_WEEK fifty_two_week_low "52WeekLow": f64,
411 52 INDICATIVE_ASK_PRICE indicative_ask_price "indAskPrice": f64,
412 53 INDICATIVE_BID_PRICE indicative_bid_price "indBidPrice": f64,
413 54 INDICATIVE_QUOTE_TIME indicative_quote_time "indQuoteTime": i64,
415 55 EXERCISE_TYPE exercise_type "exerciseType": String,
416 }
417}
418
419field_table! {
420 LevelOneFuturesFields => LevelOneFutures {
422 key = SYMBOL;
423 envelope = { delayed: bool, asset_main_type: AssetMainType };
424 1 BID_PRICE bid_price "bidPrice": f64,
425 2 ASK_PRICE ask_price "askPrice": f64,
426 3 LAST_PRICE last_price "lastPrice": f64,
427 4 BID_SIZE bid_size "bidSize": i64,
428 5 ASK_SIZE ask_size "askSize": i64,
429 6 BID_ID bid_id "bidId": String,
431 7 ASK_ID ask_id "askId": String,
433 8 TOTAL_VOLUME total_volume "totalVolume": f64,
434 9 LAST_SIZE last_size "lastSize": i64,
435 10 QUOTE_TIME quote_time "quoteTime": i64,
437 11 TRADE_TIME trade_time "tradeTime": i64,
439 12 HIGH_PRICE high_price "highPrice": f64,
440 13 LOW_PRICE low_price "lowPrice": f64,
441 14 CLOSE_PRICE close_price "closePrice": f64,
443 15 EXCHANGE_ID exchange_id "exchangeId": String,
444 16 DESCRIPTION description "description": String,
445 17 LAST_ID last_id "lastId": String,
447 18 OPEN_PRICE open_price "openPrice": f64,
448 19 NET_CHANGE net_change "netChange": f64,
449 20 FUTURE_PERCENT_CHANGE future_percent_change "futurePercentChange": f64,
450 21 EXCHANGE_NAME exchange_name "exchangeName": String,
451 22 SECURITY_STATUS security_status "securityStatus": String,
452 23 OPEN_INTEREST open_interest "openInterest": i64,
453 24 MARK mark "mark": f64,
454 25 TICK tick "tick": f64,
456 26 TICK_AMOUNT tick_amount "tickAmount": f64,
458 27 PRODUCT product "product": String,
459 28 FUTURE_PRICE_FORMAT future_price_format "futurePriceFormat": String,
460 29 FUTURE_TRADING_HOURS future_trading_hours "futureTradingHours": String,
461 30 FUTURE_IS_TRADABLE future_is_tradable "futureIsTradable": bool,
462 31 FUTURE_MULTIPLIER future_multiplier "futureMultiplier": f64,
463 32 FUTURE_IS_ACTIVE future_is_active "futureIsActive": bool,
464 33 FUTURE_SETTLEMENT_PRICE future_settlement_price "futureSettlementPrice": f64,
465 34 FUTURE_ACTIVE_SYMBOL future_active_symbol "futureActiveSymbol": String,
466 35 FUTURE_EXPIRATION_DATE future_expiration_date "futureExpirationDate": i64,
468 36 EXPIRATION_STYLE expiration_style "expirationStyle": String,
469 37 ASK_TIME ask_time "askTime": i64,
471 38 BID_TIME bid_time "bidTime": i64,
473 39 QUOTED_IN_SESSION quoted_in_session "quotedInSession": bool,
474 40 SETTLEMENT_DATE settlement_date "settlementDate": i64,
476 }
477}
478
479field_table! {
480 LevelOneFuturesOptionFields => LevelOneFuturesOption {
482 key = SYMBOL;
483 envelope = { delayed: bool, asset_main_type: AssetMainType };
484 1 BID_PRICE bid_price "bidPrice": f64,
485 2 ASK_PRICE ask_price "askPrice": f64,
486 3 LAST_PRICE last_price "lastPrice": f64,
487 4 BID_SIZE bid_size "bidSize": i64,
488 5 ASK_SIZE ask_size "askSize": i64,
489 6 BID_ID bid_id "bidId": String,
491 7 ASK_ID ask_id "askId": String,
493 8 TOTAL_VOLUME total_volume "totalVolume": i64,
494 9 LAST_SIZE last_size "lastSize": i64,
495 10 QUOTE_TIME quote_time "quoteTime": i64,
497 11 TRADE_TIME trade_time "tradeTime": i64,
499 12 HIGH_PRICE high_price "highPrice": f64,
500 13 LOW_PRICE low_price "lowPrice": f64,
501 14 CLOSE_PRICE close_price "closePrice": f64,
503 15 LAST_ID last_id "lastId": String,
505 16 DESCRIPTION description "description": String,
506 17 OPEN_PRICE open_price "openPrice": f64,
507 18 OPEN_INTEREST open_interest "openInterest": f64,
508 19 MARK mark "mark": f64,
509 20 TICK tick "tick": f64,
511 21 TICK_AMOUNT tick_amount "tickAmount": f64,
513 22 FUTURE_MULTIPLIER future_multiplier "futureMultiplier": f64,
514 23 FUTURE_SETTLEMENT_PRICE future_settlement_price "futureSettlementPrice": f64,
515 24 UNDERLYING_SYMBOL underlying_symbol "underlyingSymbol": String,
516 25 STRIKE_PRICE strike_price "strikePrice": f64,
517 26 FUTURE_EXPIRATION_DATE future_expiration_date "futureExpirationDate": i64,
519 27 EXPIRATION_STYLE expiration_style "expirationStyle": String,
520 28 CONTRACT_TYPE contract_type "contractType": String,
522 29 SECURITY_STATUS security_status "securityStatus": String,
523 30 EXCHANGE exchange "exchange": String,
524 31 EXCHANGE_NAME exchange_name "exchangeName": String,
525 }
526}
527
528field_table! {
529 LevelOneForexFields => LevelOneForex {
531 key = SYMBOL;
532 envelope = { delayed: bool, asset_main_type: AssetMainType };
533 1 BID_PRICE bid_price "bidPrice": f64,
534 2 ASK_PRICE ask_price "askPrice": f64,
535 3 LAST_PRICE last_price "lastPrice": f64,
536 4 BID_SIZE bid_size "bidSize": i64,
537 5 ASK_SIZE ask_size "askSize": i64,
538 6 TOTAL_VOLUME total_volume "totalVolume": f64,
539 7 LAST_SIZE last_size "lastSize": i64,
540 8 QUOTE_TIME quote_time "quoteTime": i64,
542 9 TRADE_TIME trade_time "tradeTime": i64,
544 10 HIGH_PRICE high_price "highPrice": f64,
545 11 LOW_PRICE low_price "lowPrice": f64,
546 12 CLOSE_PRICE close_price "closePrice": f64,
548 13 EXCHANGE exchange "exchange": String,
549 14 DESCRIPTION description "description": String,
550 15 OPEN_PRICE open_price "openPrice": f64,
551 16 NET_CHANGE net_change "netChange": f64,
552 17 PERCENT_CHANGE percent_change "percentChange": f64,
553 18 EXCHANGE_NAME exchange_name "exchangeName": String,
554 19 DIGITS digits "digits": i64,
556 20 SECURITY_STATUS security_status "securityStatus": String,
557 21 TICK tick "tick": f64,
559 22 TICK_AMOUNT tick_amount "tickAmount": f64,
561 23 PRODUCT product "product": String,
562 24 TRADING_HOURS trading_hours "tradingHours": String,
563 25 IS_TRADABLE is_tradable "isTradable": bool,
564 26 MARKET_MAKER market_maker "marketMaker": String,
565 27 HIGH_52_WEEK fifty_two_week_high "52WeekHigh": f64,
566 28 LOW_52_WEEK fifty_two_week_low "52WeekLow": f64,
567 29 MARK mark "mark": f64,
568 }
569}
570
571#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
573#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
574#[serde(rename_all = "camelCase")]
575#[non_exhaustive]
576pub struct BookMarketMaker {
577 pub market_maker_id: String,
578 pub size: i64,
579 pub quote_time: i64,
581}
582
583impl BookMarketMaker {
584 pub const FIELDS: &'static [Field] = &[
586 Field::new(0, "marketMakerId"),
587 Field::new(1, "size"),
588 Field::new(2, "quoteTime"),
589 ];
590}
591
592#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
594#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
595#[serde(rename_all = "camelCase")]
596#[non_exhaustive]
597pub struct BookLevel {
598 pub price: f64,
599 pub aggregate_size: i64,
600 pub market_maker_count: i64,
601 #[serde(default)]
602 pub market_makers: Vec<BookMarketMaker>,
603}
604
605impl BookLevel {
606 pub const FIELDS: &'static [Field] = &[
608 Field::new(0, "price"),
609 Field::new(1, "aggregateSize"),
610 Field::new(2, "marketMakerCount"),
611 Field::nested(3, "marketMakers", BookMarketMaker::FIELDS),
612 ];
613}
614
615field_table! {
616 BookFields => Book {
619 key = SYMBOL;
620 envelope = {};
621 1 MARKET_SNAPSHOT_TIME market_snapshot_time "marketSnapshotTime": i64,
623 2 BID_SIDE_LEVELS bid_side_levels "bidSideLevels": Vec<BookLevel> [BookLevel],
624 3 ASK_SIDE_LEVELS ask_side_levels "askSideLevels": Vec<BookLevel> [BookLevel],
625 }
626}
627
628field_table! {
629 ChartEquityFields => ChartEquity {
633 key = KEY;
634 envelope = { seq: i64 };
635 1 SEQUENCE sequence "sequence": i64,
637 2 OPEN_PRICE open "open": f64,
638 3 HIGH_PRICE high "high": f64,
639 4 LOW_PRICE low "low": f64,
640 5 CLOSE_PRICE close "close": f64,
641 6 VOLUME volume "volume": f64,
642 7 CHART_TIME chart_time "chartTime": i64,
644 8 CHART_DAY chart_day "chartDay": i64,
646 }
647}
648
649field_table! {
650 ChartFuturesFields => ChartFutures {
652 key = KEY;
653 envelope = { seq: i64 };
654 1 CHART_TIME chart_time "chartTime": i64,
656 2 OPEN_PRICE open "open": f64,
657 3 HIGH_PRICE high "high": f64,
658 4 LOW_PRICE low "low": f64,
659 5 CLOSE_PRICE close "close": f64,
660 6 VOLUME volume "volume": f64,
661 }
662}
663
664#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
666#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
667#[serde(rename_all = "camelCase")]
668#[non_exhaustive]
669pub struct ScreenerItem {
670 pub symbol: String,
671 #[serde(default, skip_serializing_if = "Option::is_none")]
672 pub description: Option<String>,
673 #[serde(default, skip_serializing_if = "Option::is_none")]
674 pub last_price: Option<f64>,
675 #[serde(default, skip_serializing_if = "Option::is_none")]
676 pub net_change: Option<f64>,
677 #[serde(default, skip_serializing_if = "Option::is_none")]
678 pub net_percent_change: Option<f64>,
679 #[serde(default, skip_serializing_if = "Option::is_none")]
680 pub market_share: Option<f64>,
681 #[serde(default, skip_serializing_if = "Option::is_none")]
682 pub total_volume: Option<i64>,
683 #[serde(default, skip_serializing_if = "Option::is_none")]
684 pub volume: Option<i64>,
685 #[serde(default, skip_serializing_if = "Option::is_none")]
686 pub trades: Option<i64>,
687 #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
688 pub extra: HashMap<String, Value>,
689}
690
691field_table! {
692 ScreenerFields => Screener {
695 key = SYMBOL;
696 envelope = {};
697 1 TIMESTAMP timestamp "timestamp": i64,
699 2 SORT_FIELD sort_field "sortField": String,
700 3 FREQUENCY frequency "frequency": i64,
702 4 ITEMS items "items": Vec<ScreenerItem>,
703 }
704}
705
706field_table! {
707 AcctActivityFields => AccountActivity {
711 key = KEY;
712 envelope = { seq: i64 };
713 1 ACCOUNT account_number "accountNumber": String,
714 2 MESSAGE_TYPE message_type "messageType": String,
716 3 MESSAGE_DATA message_data "messageData": String,
717 }
718}
719
720impl AccountActivity {
721 pub fn message(&self) -> Option<Map<String, Value>> {
741 match serde_json::from_str(self.message_data.as_deref()?) {
742 Ok(Value::Object(map)) => Some(map),
743 _ => None,
744 }
745 }
746}
747
748#[derive(Debug, Clone, PartialEq)]
750#[non_exhaustive]
751pub enum StreamPayload {
752 LevelOneEquity(LevelOneEquity),
753 LevelOneOption(LevelOneOption),
754 LevelOneFutures(LevelOneFutures),
755 LevelOneFuturesOption(LevelOneFuturesOption),
756 LevelOneForex(LevelOneForex),
757 Book(Book),
758 ChartEquity(ChartEquity),
759 ChartFutures(ChartFutures),
760 Screener(Screener),
761 AccountActivity(AccountActivity),
762}
763
764impl StreamService {
765 pub fn fields(self) -> &'static [Field] {
768 match self {
769 Self::Admin => &[],
770 Self::LeveloneEquities => LevelOneEquityFields::FIELDS,
771 Self::LeveloneOptions => LevelOneOptionFields::FIELDS,
772 Self::LeveloneFutures => LevelOneFuturesFields::FIELDS,
773 Self::LeveloneFuturesOptions => LevelOneFuturesOptionFields::FIELDS,
774 Self::LeveloneForex => LevelOneForexFields::FIELDS,
775 Self::NyseBook | Self::NasdaqBook | Self::OptionsBook => BookFields::FIELDS,
776 Self::ChartEquity => ChartEquityFields::FIELDS,
777 Self::ChartFutures => ChartFuturesFields::FIELDS,
778 Self::ScreenerEquity | Self::ScreenerOption => ScreenerFields::FIELDS,
779 Self::AcctActivity => AcctActivityFields::FIELDS,
780 }
781 }
782
783 pub fn decode(self, content: &DataContent) -> Result<StreamPayload, serde_json::Error> {
785 use serde::de::Error as _;
786 Ok(match self {
787 Self::Admin => {
788 return Err(serde_json::Error::custom("ADMIN frames carry no data"));
789 }
790 Self::LeveloneEquities => {
791 StreamPayload::LevelOneEquity(LevelOneEquity::from_content(content)?)
792 }
793 Self::LeveloneOptions => {
794 StreamPayload::LevelOneOption(LevelOneOption::from_content(content)?)
795 }
796 Self::LeveloneFutures => {
797 StreamPayload::LevelOneFutures(LevelOneFutures::from_content(content)?)
798 }
799 Self::LeveloneFuturesOptions => {
800 StreamPayload::LevelOneFuturesOption(LevelOneFuturesOption::from_content(content)?)
801 }
802 Self::LeveloneForex => {
803 StreamPayload::LevelOneForex(LevelOneForex::from_content(content)?)
804 }
805 Self::NyseBook | Self::NasdaqBook | Self::OptionsBook => {
806 StreamPayload::Book(Book::from_content(content)?)
807 }
808 Self::ChartEquity => StreamPayload::ChartEquity(ChartEquity::from_content(content)?),
809 Self::ChartFutures => StreamPayload::ChartFutures(ChartFutures::from_content(content)?),
810 Self::ScreenerEquity | Self::ScreenerOption => {
811 StreamPayload::Screener(Screener::from_content(content)?)
812 }
813 Self::AcctActivity => {
814 StreamPayload::AccountActivity(AccountActivity::from_content(content)?)
815 }
816 })
817 }
818}
819
820impl DataItem {
821 pub fn service(&self) -> Option<StreamService> {
823 StreamService::from_api_str(&self.service)
824 }
825
826 pub fn decode(&self) -> Result<Vec<StreamPayload>, serde_json::Error> {
828 use serde::de::Error as _;
829 let service = self.service().ok_or_else(|| {
830 serde_json::Error::custom(format!("unknown streamer service `{}`", self.service))
831 })?;
832 self.content
833 .iter()
834 .map(|content| service.decode(content))
835 .collect()
836 }
837}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842 use serde_json::json;
843
844 fn content(value: Value) -> DataContent {
845 serde_json::from_value(value).unwrap()
846 }
847
848 #[test]
849 fn tables_are_dense_and_unique() {
850 for service in [
851 StreamService::LeveloneEquities,
852 StreamService::LeveloneOptions,
853 StreamService::LeveloneFutures,
854 StreamService::LeveloneFuturesOptions,
855 StreamService::LeveloneForex,
856 StreamService::NyseBook,
857 StreamService::ChartEquity,
858 StreamService::ChartFutures,
859 StreamService::ScreenerEquity,
860 StreamService::AcctActivity,
861 ] {
862 let table = service.fields();
863 for (index, field) in table.iter().enumerate() {
864 assert_eq!(field.number as usize, index, "{service:?} {}", field.name);
865 }
866 let mut names: Vec<_> = table.iter().map(|field| field.name).collect();
867 names.sort_unstable();
868 names.dedup();
869 assert_eq!(names.len(), table.len(), "{service:?} names repeat");
870 }
871 assert_eq!(LevelOneEquityFields::FIELDS.len(), 52);
872 assert_eq!(LevelOneOptionFields::FIELDS.len(), 56);
873 assert_eq!(LevelOneFuturesFields::FIELDS.len(), 41);
874 assert_eq!(LevelOneFuturesOptionFields::FIELDS.len(), 32);
875 assert_eq!(LevelOneForexFields::FIELDS.len(), 30);
876 assert!(StreamService::Admin.fields().is_empty());
877 }
878
879 #[test]
880 fn payload_names_match_the_table() {
881 let numbered: Map<String, Value> = LevelOneEquityFields::FIELDS
884 .iter()
885 .skip(1)
886 .map(|field| {
887 let value = match field.name {
888 "askId" | "bidId" | "lastId" | "exchangeId" | "description"
889 | "exchangeName" | "divDate" | "securityStatus" | "askMICId" | "bidMICId"
890 | "lastMICId" => json!("x"),
891 "marginable" | "regularMarketQuote" | "regularMarketTrade" => json!(true),
892 _ => json!(1),
893 };
894 (field.number.to_string(), value)
895 })
896 .collect();
897 let mut wire = numbered.clone();
898 wire.insert("key".into(), json!("AAPL"));
899 let quote = LevelOneEquity::from_content(&content(Value::Object(wire))).unwrap();
900 assert!(quote.extra.is_empty(), "unmapped: {:?}", quote.extra);
901 assert_eq!(quote.post_market_percent_change, Some(1.0));
902
903 let named = serde_json::to_value("e).unwrap();
904 for field in LevelOneEquityFields::FIELDS {
905 assert!(
906 named.get(field.name).is_some(),
907 "{} not serialized",
908 field.name
909 );
910 }
911 }
912
913 #[test]
914 fn decodes_deltas_and_keeps_unknown_fields() {
915 let content = content(json!({
916 "key": "AMD", "delayed": false, "assetMainType": "EQUITY",
917 "assetSubType": "COE", "cusip": "007903107",
918 "1": 217.86, "2": 217.95, "6": "P", "8": 33292396, "99": "future"
919 }));
920 let quote = LevelOneEquity::from_content(&content).unwrap();
921 assert_eq!(quote.key, "AMD");
922 assert_eq!(quote.delayed, Some(false));
923 assert_eq!(quote.asset_main_type, Some(AssetMainType::Equity));
924 assert_eq!(quote.asset_sub_type, Some(AssetSubType::Coe));
925 assert_eq!(quote.cusip.as_deref(), Some("007903107"));
926 assert_eq!(quote.ask_price, Some(217.95));
927 assert_eq!(quote.ask_id.as_deref(), Some("P"));
928 assert_eq!(quote.last_price, None);
929 assert_eq!(quote.extra["99"], "future");
930
931 let named = content.to_named_value(LevelOneEquityFields::FIELDS);
932 assert_eq!(named["bidPrice"], 217.86);
933 assert_eq!(named["cusip"], "007903107");
934 assert_eq!(named["99"], "future");
935 assert!(named.get("1").is_none());
936 }
937
938 #[test]
939 fn option_field_one_is_the_description() {
940 let quote = LevelOneOption::from_content(&content(json!({
941 "key": "SPXW 251208C06880000", "delayed": false, "assetMainType": "OPTION",
942 "1": "SPXW 12/08/2025 6880.00 C", "2": 10.2, "3": 10.6
943 })))
944 .unwrap();
945 assert_eq!(
946 quote.description.as_deref(),
947 Some("SPXW 12/08/2025 6880.00 C")
948 );
949 assert_eq!(quote.bid_price, Some(10.2));
950 }
951
952 #[test]
953 fn envelope_classes_outside_the_enum_decode_as_unknown() {
954 let quote = LevelOneEquity::from_content(&content(json!({
955 "key": "X", "assetMainType": "CRYPTO", "assetSubType": "NEW", "1": 1.0
956 })))
957 .unwrap();
958 assert_eq!(quote.asset_main_type, Some(AssetMainType::Unknown));
959 assert_eq!(quote.asset_sub_type, Some(AssetSubType::Unknown));
960 assert_eq!(quote.bid_price, Some(1.0));
961 }
962
963 #[test]
964 fn book_levels_decode_recursively() {
965 let book = Book::from_content(&content(json!({
966 "key": "F",
967 "1": 1765306322349i64,
968 "2": [{"0": 13.12, "1": 31600, "2": 2, "3": [
969 {"0": "NYSE", "1": 8800, "2": 49786299},
970 {"0": "ARCX", "1": 4200, "2": 49904606}
971 ]}],
972 "3": [{"0": 13.13, "1": 900, "2": 0, "3": []}]
973 })))
974 .unwrap();
975 let bids = book.bid_side_levels.as_ref().unwrap();
976 assert_eq!(bids[0].price, 13.12);
977 assert_eq!(bids[0].market_makers[1].market_maker_id, "ARCX");
978 assert_eq!(book.ask_side_levels.unwrap()[0].aggregate_size, 900);
979 }
980
981 #[test]
982 fn charts_screeners_and_activity_decode() {
983 let candle = ChartEquity::from_content(&content(json!({
984 "seq": 202, "key": "AMD", "1": 425, "2": 221.133, "5": 221.02,
985 "6": 11730, "7": 1765307100000i64, "8": 20431
986 })))
987 .unwrap();
988 assert_eq!(candle.seq, Some(202));
989 assert_eq!(candle.sequence, Some(425));
990 assert_eq!(candle.volume, Some(11730.0));
991
992 let screener = Screener::from_content(&content(json!({
993 "key": "NASDAQ_VOLUME_30", "1": 1765307297706i64, "2": "VOLUME", "3": 30,
994 "4": [{"symbol": "WBD", "lastPrice": 28.095, "trades": 21302}]
995 })))
996 .unwrap();
997 assert_eq!(screener.items.unwrap()[0].trades, Some(21302));
998
999 let activity = AccountActivity::from_content(&content(json!({
1000 "seq": 1, "key": "Account Activity",
1001 "1": "123456789", "2": "OrderCreated", "3": "{\"SchwabOrderID\":\"1\"}"
1002 })))
1003 .unwrap();
1004 assert_eq!(activity.account_number.as_deref(), Some("123456789"));
1005 assert_eq!(activity.message_type.as_deref(), Some("OrderCreated"));
1006 }
1007
1008 #[test]
1009 fn items_decode_by_service() {
1010 let item: DataItem = serde_json::from_value(json!({
1011 "service": "LEVELONE_FOREX", "timestamp": 1, "command": "SUBS",
1012 "content": [{"key": "EUR/USD", "1": 1.1643, "2": 1.16443}]
1013 }))
1014 .unwrap();
1015 assert_eq!(item.service(), Some(StreamService::LeveloneForex));
1016 match item.decode().unwrap().as_slice() {
1017 [StreamPayload::LevelOneForex(quote)] => assert_eq!(quote.ask_price, Some(1.16443)),
1018 other => panic!("{other:?}"),
1019 }
1020
1021 let unknown = DataItem {
1022 service: "LEVELTWO".into(),
1023 ..item.clone()
1024 };
1025 assert!(
1026 unknown
1027 .decode()
1028 .unwrap_err()
1029 .to_string()
1030 .contains("LEVELTWO")
1031 );
1032 assert!(StreamService::Admin.decode(&item.content[0]).is_err());
1033
1034 let wrong_type = content(json!({"key": "EUR/USD", "1": "not a price"}));
1035 assert!(LevelOneForex::from_content(&wrong_type).is_err());
1036 }
1037
1038 #[test]
1039 fn subscription_field_lists_come_from_the_tables() {
1040 assert_eq!(
1041 LevelOneEquityFields::default_fields(),
1042 "0,1,2,3,4,5,8,9,10,11,12,17,18,42,34,35"
1043 );
1044 assert_eq!(LevelOneEquityFields::all_fields(), csv(0..=51));
1045 assert_eq!(
1046 StreamService::LeveloneEquities.default_fields(),
1047 LevelOneEquityFields::default_fields()
1048 );
1049 assert_eq!(
1050 StreamService::LeveloneOptions.default_fields(),
1051 LevelOneOptionFields::all_fields()
1052 );
1053 assert_eq!(StreamService::NasdaqBook.default_fields(), "0,1,2,3");
1054 assert_eq!(StreamService::AcctActivity.default_fields(), "0,1,2,3");
1055 assert_eq!(StreamService::Admin.default_fields(), "");
1056 assert_eq!(LevelOneEquityFields::name(0), Some("key"));
1057 assert_eq!(LevelOneEquityFields::name(52), None);
1058 }
1059}