1use std::{collections::HashMap, str::FromStr};
17
18use ahash::AHashMap;
19use bytes::Bytes;
20use futures::future::join_all;
21use jiff::Timestamp;
22use nautilus_common::{
23 cache::database::{CacheMap, register_loaded_currencies},
24 enums::SerializationEncoding,
25};
26use nautilus_model::{
27 accounts::AccountAny,
28 data::{CustomData, DataType, HasTsInit, InstrumentClose},
29 enums::CurrencyType,
30 events::{AccountState, OrderEventAny, OrderFilled, PositionSnapshot},
31 identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, PositionId},
32 instruments::{InstrumentAny, SyntheticInstrument},
33 orders::OrderAny,
34 position::Position,
35 types::Currency,
36};
37use redis::{AsyncCommands, aio::ConnectionManager};
38use serde::{Deserialize, Serialize, de::DeserializeOwned};
39use serde_json::Value;
40use ustr::Ustr;
41
42use super::get_index_key;
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
50pub(crate) struct CurrencyRecord {
51 pub code: String,
52 pub precision: u8,
53 pub iso4217: u16,
54 pub name: String,
55 pub currency_type: CurrencyType,
56}
57
58impl From<&Currency> for CurrencyRecord {
59 fn from(currency: &Currency) -> Self {
60 Self {
61 code: currency.code.to_string(),
62 precision: currency.precision,
63 iso4217: currency.iso4217,
64 name: currency.name.to_string(),
65 currency_type: currency.currency_type,
66 }
67 }
68}
69
70impl TryFrom<CurrencyRecord> for Currency {
71 type Error = anyhow::Error;
72
73 fn try_from(record: CurrencyRecord) -> Result<Self, Self::Error> {
74 Self::new_checked(
75 record.code.as_str(),
76 record.precision,
77 record.iso4217,
78 record.name.as_str(),
79 record.currency_type,
80 )
81 .map_err(|e| anyhow::anyhow!("Invalid currency record: {e}"))
82 }
83}
84
85enum CurrencyDecode {
87 Currency(Currency),
88 InvalidRecord(anyhow::Error),
90 Undecodable(anyhow::Error),
92}
93
94const INDEX: &str = "index";
96const GENERAL: &str = "general";
97const CURRENCIES: &str = "currencies";
98const INSTRUMENTS: &str = "instruments";
99const INSTRUMENT_CLOSES: &str = "instrument_closes";
100const SYNTHETICS: &str = "synthetics";
101const ACCOUNTS: &str = "accounts";
102const ORDERS: &str = "orders";
103const POSITIONS: &str = "positions";
104const SNAPSHOTS: &str = "snapshots";
105const ACTORS: &str = "actors";
106const STRATEGIES: &str = "strategies";
107const CUSTOM: &str = "custom";
108const REDIS_DELIMITER: char = ':';
109
110const INDEX_ORDER_IDS: &str = "index:order_ids";
112const INDEX_ORDER_POSITION: &str = "index:order_position";
113const INDEX_ORDER_CLIENT: &str = "index:order_client";
114const INDEX_ORDERS: &str = "index:orders";
115const INDEX_ORDERS_OPEN: &str = "index:orders_open";
116const INDEX_ORDERS_CLOSED: &str = "index:orders_closed";
117const INDEX_ORDERS_EMULATED: &str = "index:orders_emulated";
118const INDEX_ORDERS_INFLIGHT: &str = "index:orders_inflight";
119const INDEX_POSITIONS: &str = "index:positions";
120const INDEX_POSITIONS_OPEN: &str = "index:positions_open";
121const INDEX_POSITIONS_CLOSED: &str = "index:positions_closed";
122
123#[derive(Debug)]
124pub struct DatabaseQueries;
125
126impl DatabaseQueries {
127 pub fn serialize_payload<T: Serialize>(
133 encoding: SerializationEncoding,
134 payload: &T,
135 ) -> anyhow::Result<Vec<u8>> {
136 match encoding {
137 SerializationEncoding::MsgPack => {
138 let mut value = serde_json::to_value(payload)?;
139 convert_timestamps(&mut value);
140 rmp_serde::to_vec(&value)
141 .map_err(|e| anyhow::anyhow!("Failed to serialize msgpack `payload`: {e}"))
142 }
143 SerializationEncoding::Json => {
144 let mut value = serde_json::to_value(payload)?;
145 convert_timestamps(&mut value);
146 serde_json::to_vec(&value)
147 .map_err(|e| anyhow::anyhow!("Failed to serialize json `payload`: {e}"))
148 }
149 SerializationEncoding::Sbe => {
150 anyhow::bail!("SBE encoding is not supported for Redis cache payloads")
151 }
152 }
153 }
154
155 pub fn deserialize_payload<T: DeserializeOwned>(
161 encoding: SerializationEncoding,
162 payload: &[u8],
163 ) -> anyhow::Result<T> {
164 let mut value = match encoding {
165 SerializationEncoding::MsgPack => rmp_serde::from_slice(payload)
166 .map_err(|e| anyhow::anyhow!("Failed to deserialize msgpack `payload`: {e}"))?,
167 SerializationEncoding::Json => serde_json::from_slice(payload)
168 .map_err(|e| anyhow::anyhow!("Failed to deserialize json `payload`: {e}"))?,
169 SerializationEncoding::Sbe => {
170 anyhow::bail!("SBE encoding is not supported for Redis cache payloads")
171 }
172 };
173
174 convert_timestamp_strings(&mut value);
175
176 serde_json::from_value(value)
177 .map_err(|e| anyhow::anyhow!("Failed to convert value to target type: {e}"))
178 }
179
180 pub async fn scan_keys(
186 con: &mut ConnectionManager,
187 pattern: String,
188 ) -> anyhow::Result<Vec<String>> {
189 let mut result = Vec::new();
190 let mut cursor = 0u64;
191
192 loop {
193 let scan_result: (u64, Vec<String>) = redis::cmd("SCAN")
194 .arg(cursor)
195 .arg("MATCH")
196 .arg(&pattern)
197 .arg("COUNT")
198 .arg(5000)
199 .query_async(con)
200 .await?;
201
202 let (new_cursor, keys) = scan_result;
203 result.extend(keys);
204
205 if new_cursor == 0 {
207 break;
208 }
209
210 cursor = new_cursor;
211 }
212
213 Ok(result)
214 }
215
216 pub async fn read_bulk(
222 con: &ConnectionManager,
223 keys: &[String],
224 ) -> anyhow::Result<Vec<Option<Bytes>>> {
225 if keys.is_empty() {
226 return Ok(vec![]);
227 }
228
229 let mut con = con.clone();
230
231 let results: Vec<Option<Vec<u8>>> =
233 redis::cmd("MGET").arg(keys).query_async(&mut con).await?;
234
235 let bytes_results: Vec<Option<Bytes>> = results
237 .into_iter()
238 .map(|opt| opt.map(Bytes::from))
239 .collect();
240
241 Ok(bytes_results)
242 }
243
244 pub async fn read_bulk_batched(
253 con: &ConnectionManager,
254 keys: &[String],
255 batch_size: usize,
256 ) -> anyhow::Result<Vec<Option<Bytes>>> {
257 if batch_size == 0 {
258 anyhow::bail!("`batch_size` must be greater than zero");
259 }
260
261 if keys.is_empty() {
262 return Ok(vec![]);
263 }
264
265 let mut all_results: Vec<Option<Bytes>> = Vec::with_capacity(keys.len());
266
267 for chunk in keys.chunks(batch_size) {
268 let mut con = con.clone();
269
270 let results: Vec<Option<Vec<u8>>> =
271 redis::cmd("MGET").arg(chunk).query_async(&mut con).await?;
272
273 all_results.extend(results.into_iter().map(|opt| opt.map(Bytes::from)));
274 }
275
276 Ok(all_results)
277 }
278
279 pub async fn read(
285 con: &ConnectionManager,
286 trader_key: &str,
287 key: &str,
288 ) -> anyhow::Result<Vec<Bytes>> {
289 let collection = Self::get_collection_key(key)?;
290 let full_key = format!("{trader_key}{REDIS_DELIMITER}{key}");
291
292 let mut con = con.clone();
293
294 match collection {
295 INDEX => Self::read_index(&mut con, &full_key).await,
296 GENERAL | CURRENCIES | INSTRUMENTS | INSTRUMENT_CLOSES | SYNTHETICS | ACTORS
297 | STRATEGIES => Self::read_string(&mut con, &full_key).await,
298 ACCOUNTS | ORDERS | POSITIONS | SNAPSHOTS => Self::read_list(&mut con, &full_key).await,
299 _ => anyhow::bail!("Unsupported operation: `read` for collection '{collection}'"),
300 }
301 }
302
303 pub async fn load_all(
309 con: &ConnectionManager,
310 encoding: SerializationEncoding,
311 trader_key: &str,
312 ) -> anyhow::Result<CacheMap> {
313 let mut currencies = Self::load_currencies(con, trader_key, encoding).await?;
316 register_loaded_currencies(&mut currencies)?;
317
318 let (instruments, instrument_closes, synthetics, accounts, orders, positions) =
319 tokio::try_join!(
320 Self::load_instruments(con, trader_key, encoding),
321 Self::load_instrument_closes(con, trader_key, encoding),
322 Self::load_synthetics(con, trader_key, encoding),
323 Self::load_accounts(con, trader_key, encoding),
324 Self::load_orders(con, trader_key, encoding),
325 Self::load_positions(con, trader_key, encoding)
326 )
327 .map_err(|e| anyhow::anyhow!("Error loading cache data: {e}"))?;
328
329 let greeks = AHashMap::new();
332 let yield_curves = AHashMap::new();
333
334 Ok(CacheMap {
335 currencies,
336 instruments,
337 instrument_closes,
338 synthetics,
339 accounts,
340 orders,
341 positions,
342 greeks,
343 yield_curves,
344 })
345 }
346
347 fn decode_currency(encoding: SerializationEncoding, value_bytes: &Bytes) -> CurrencyDecode {
355 match Self::deserialize_payload::<CurrencyRecord>(encoding, value_bytes) {
356 Ok(record) => match Currency::try_from(record) {
357 Ok(currency) => CurrencyDecode::Currency(currency),
358 Err(e) => CurrencyDecode::InvalidRecord(e),
359 },
360 Err(record_err) => match Self::deserialize_payload::<String>(encoding, value_bytes) {
361 Ok(code) => match Currency::try_from_str(&code) {
362 Some(currency) => CurrencyDecode::Currency(currency),
363 None => CurrencyDecode::Undecodable(anyhow::anyhow!(
364 "bare currency code {code} is not registered"
365 )),
366 },
367 Err(_) => CurrencyDecode::InvalidRecord(anyhow::anyhow!(
368 "payload is not a bare code and does not decode as a currency record: {record_err}"
369 )),
370 },
371 }
372 }
373
374 pub async fn load_currencies(
381 con: &ConnectionManager,
382 trader_key: &str,
383 encoding: SerializationEncoding,
384 ) -> anyhow::Result<AHashMap<Ustr, Currency>> {
385 let mut currencies = AHashMap::new();
386 let prefix = format!("{trader_key}{REDIS_DELIMITER}{CURRENCIES}{REDIS_DELIMITER}");
387 let pattern = format!("{prefix}*");
388 log::debug!("Loading {pattern}");
389
390 let mut con = con.clone();
391 let keys = Self::scan_keys(&mut con, pattern).await?;
392
393 if keys.is_empty() {
394 return Ok(currencies);
395 }
396
397 let bulk_values = Self::read_bulk(&con, &keys).await?;
399
400 for (key, value_opt) in keys.iter().zip(bulk_values.iter()) {
402 let currency_code = match key.as_str().strip_prefix(prefix.as_str()) {
405 Some(code) if !code.is_empty() => Ustr::from(code),
406 _ => {
407 log::error!("Invalid key format: {key}");
408 continue;
409 }
410 };
411
412 if let Some(value_bytes) = value_opt {
413 match Self::decode_currency(encoding, value_bytes) {
414 CurrencyDecode::Currency(currency) => {
415 if currency.code != currency_code {
418 anyhow::bail!(
419 "Currency record under key {currency_code} holds code {}",
420 currency.code
421 );
422 }
423 currencies.insert(currency_code, currency);
424 }
425 CurrencyDecode::InvalidRecord(e) => {
426 anyhow::bail!("Invalid currency record for {currency_code}: {e}");
427 }
428 CurrencyDecode::Undecodable(e) => {
429 log::error!(
430 "Skipping currency {currency_code}: {e}; payloads denominated in it will not decode"
431 );
432 }
433 }
434 } else {
435 log::error!("Currency not found in Redis: {currency_code}");
436 }
437 }
438
439 log::debug!("Loaded {} currencies(s)", currencies.len());
440
441 Ok(currencies)
442 }
443
444 pub async fn load_instruments(
455 con: &ConnectionManager,
456 trader_key: &str,
457 encoding: SerializationEncoding,
458 ) -> anyhow::Result<AHashMap<InstrumentId, InstrumentAny>> {
459 let mut instruments = AHashMap::new();
460 let prefix = format!("{trader_key}{REDIS_DELIMITER}{INSTRUMENTS}{REDIS_DELIMITER}");
461 let pattern = format!("{prefix}*");
462 log::debug!("Loading {pattern}");
463
464 let mut con = con.clone();
465 let keys = Self::scan_keys(&mut con, pattern).await?;
466
467 let futures: Vec<_> = keys
468 .iter()
469 .map(|key| {
470 let con = con.clone();
471 let prefix = &prefix;
472 async move {
473 let instrument_id = parse_instrument_key(key, prefix);
474
475 let Ok(instrument_id) = instrument_id else {
476 log::error!("Failed to parse InstrumentId from Redis key: {key}");
477 return None;
478 };
479
480 match Self::load_instrument(&con, trader_key, &instrument_id, encoding).await {
481 Ok(Some(instrument)) => Some((instrument_id, instrument)),
482 Ok(None) => {
483 log::error!("Instrument not found: {instrument_id}");
484 None
485 }
486 Err(e) => {
487 log::error!("Failed to load instrument {instrument_id}: {e}");
488 None
489 }
490 }
491 }
492 })
493 .collect();
494
495 instruments.extend(join_all(futures).await.into_iter().flatten());
497 log::debug!("Loaded {} instruments(s)", instruments.len());
498
499 Ok(instruments)
500 }
501
502 pub async fn load_instrument_closes(
510 con: &ConnectionManager,
511 trader_key: &str,
512 encoding: SerializationEncoding,
513 ) -> anyhow::Result<AHashMap<InstrumentId, InstrumentClose>> {
514 let prefix = format!("{trader_key}{REDIS_DELIMITER}{INSTRUMENT_CLOSES}{REDIS_DELIMITER}");
515 let pattern = format!("{prefix}*");
516 log::debug!("Loading {pattern}");
517
518 let mut con = con.clone();
519 let keys = Self::scan_keys(&mut con, pattern).await?;
520 let values = Self::read_bulk(&con, &keys).await?;
521 let mut closes = AHashMap::with_capacity(keys.len());
522
523 for (key, value) in keys.into_iter().zip(values) {
524 let instrument_id = parse_instrument_key(&key, &prefix)?;
525 let value = value
526 .ok_or_else(|| anyhow::anyhow!("Instrument close not found in Redis: {key}"))?;
527 let close: InstrumentClose = Self::deserialize_payload(encoding, &value)?;
528 anyhow::ensure!(
529 close.instrument_id == instrument_id,
530 "Instrument close key ID {instrument_id} did not match payload ID {}",
531 close.instrument_id,
532 );
533 closes.insert(instrument_id, close);
534 }
535
536 log::debug!("Loaded {} instrument close(s)", closes.len());
537 Ok(closes)
538 }
539
540 pub async fn load_synthetics(
546 con: &ConnectionManager,
547 trader_key: &str,
548 encoding: SerializationEncoding,
549 ) -> anyhow::Result<AHashMap<InstrumentId, SyntheticInstrument>> {
550 let mut synthetics = AHashMap::new();
551 let pattern = format!("{trader_key}{REDIS_DELIMITER}{SYNTHETICS}*");
552 log::debug!("Loading {pattern}");
553
554 let mut con = con.clone();
555 let keys = Self::scan_keys(&mut con, pattern).await?;
556
557 let futures: Vec<_> = keys
558 .iter()
559 .map(|key| {
560 let con = con.clone();
561 async move {
562 let instrument_id = key
563 .as_str()
564 .rsplit(':')
565 .next()
566 .ok_or_else(|| {
567 log::error!("Invalid key format: {key}");
568 "Invalid key format"
569 })
570 .and_then(|code| {
571 InstrumentId::from_str(code).map_err(|e| {
572 log::error!("Failed to parse InstrumentId for {key}: {e}");
573 "Invalid instrument ID"
574 })
575 });
576
577 let Ok(instrument_id) = instrument_id else {
578 return None;
579 };
580
581 match Self::load_synthetic(&con, trader_key, &instrument_id, encoding).await {
582 Ok(Some(synthetic)) => Some((instrument_id, synthetic)),
583 Ok(None) => {
584 log::error!("Synthetic not found: {instrument_id}");
585 None
586 }
587 Err(e) => {
588 log::error!("Failed to load synthetic {instrument_id}: {e}");
589 None
590 }
591 }
592 }
593 })
594 .collect();
595
596 synthetics.extend(join_all(futures).await.into_iter().flatten());
598 log::debug!("Loaded {} synthetics(s)", synthetics.len());
599
600 Ok(synthetics)
601 }
602
603 pub async fn load_accounts(
614 con: &ConnectionManager,
615 trader_key: &str,
616 encoding: SerializationEncoding,
617 ) -> anyhow::Result<AHashMap<AccountId, AccountAny>> {
618 let mut accounts = AHashMap::new();
619 let pattern = format!("{trader_key}{REDIS_DELIMITER}{ACCOUNTS}*");
620 log::debug!("Loading {pattern}");
621
622 let mut con = con.clone();
623 let keys = Self::scan_keys(&mut con, pattern).await?;
624
625 let futures: Vec<_> = keys
626 .iter()
627 .map(|key| {
628 let con = con.clone();
629 async move {
630 let account_id = if let Some(code) = key.as_str().rsplit(':').next() {
631 AccountId::from(code)
632 } else {
633 log::error!("Invalid key format: {key}");
634 return None;
635 };
636
637 match Self::load_account(&con, trader_key, &account_id, encoding).await {
638 Ok(Some(account)) => Some((account_id, account)),
639 Ok(None) => {
640 log::error!("Account not found: {account_id}");
641 None
642 }
643 Err(e) => {
644 log::error!("Failed to load account {account_id}: {e}");
645 None
646 }
647 }
648 }
649 })
650 .collect();
651
652 accounts.extend(join_all(futures).await.into_iter().flatten());
654 log::debug!("Loaded {} accounts(s)", accounts.len());
655
656 Ok(accounts)
657 }
658
659 pub async fn load_orders(
670 con: &ConnectionManager,
671 trader_key: &str,
672 encoding: SerializationEncoding,
673 ) -> anyhow::Result<AHashMap<ClientOrderId, OrderAny>> {
674 let mut orders = AHashMap::new();
675 let pattern = format!("{trader_key}{REDIS_DELIMITER}{ORDERS}*");
676 log::debug!("Loading {pattern}");
677
678 let mut con = con.clone();
679 let keys = Self::scan_keys(&mut con, pattern).await?;
680
681 let futures: Vec<_> = keys
682 .iter()
683 .map(|key| {
684 let con = con.clone();
685 async move {
686 let client_order_id = if let Some(code) = key.as_str().rsplit(':').next() {
687 ClientOrderId::from(code)
688 } else {
689 log::error!("Invalid key format: {key}");
690 return None;
691 };
692
693 match Self::load_order(&con, trader_key, &client_order_id, encoding).await {
694 Ok(Some(order)) => Some((client_order_id, order)),
695 Ok(None) => {
696 log::error!("Order not found: {client_order_id}");
697 None
698 }
699 Err(e) => {
700 log::error!("Failed to load order {client_order_id}: {e}");
701 None
702 }
703 }
704 }
705 })
706 .collect();
707
708 orders.extend(join_all(futures).await.into_iter().flatten());
710 log::debug!("Loaded {} order(s)", orders.len());
711
712 Ok(orders)
713 }
714
715 pub async fn load_positions(
726 con: &ConnectionManager,
727 trader_key: &str,
728 encoding: SerializationEncoding,
729 ) -> anyhow::Result<AHashMap<PositionId, Position>> {
730 let mut positions = AHashMap::new();
731 let pattern = format!("{trader_key}{REDIS_DELIMITER}{POSITIONS}*");
732 log::debug!("Loading {pattern}");
733
734 let mut con = con.clone();
735 let keys = Self::scan_keys(&mut con, pattern).await?;
736
737 let futures: Vec<_> = keys
738 .iter()
739 .map(|key| {
740 let con = con.clone();
741 async move {
742 let position_id = if let Some(code) = key.as_str().rsplit(':').next() {
743 PositionId::from(code)
744 } else {
745 log::error!("Invalid key format: {key}");
746 return None;
747 };
748
749 match Self::load_position(&con, trader_key, &position_id, encoding).await {
750 Ok(Some(position)) => Some((position_id, position)),
751 Ok(None) => {
752 log::error!("Position not found: {position_id}");
753 None
754 }
755 Err(e) => {
756 log::error!("Failed to load position {position_id}: {e}");
757 None
758 }
759 }
760 }
761 })
762 .collect();
763
764 positions.extend(join_all(futures).await.into_iter().flatten());
766 log::debug!("Loaded {} position(s)", positions.len());
767
768 Ok(positions)
769 }
770
771 pub async fn load_index_order_position(
777 con: &ConnectionManager,
778 trader_key: &str,
779 ) -> anyhow::Result<AHashMap<ClientOrderId, PositionId>> {
780 let index = Self::read_index_hash(con, trader_key, INDEX_ORDER_POSITION).await?;
781 Ok(index
782 .into_iter()
783 .map(|(k, v)| {
784 (
785 ClientOrderId::from(k.as_str()),
786 PositionId::from(v.as_str()),
787 )
788 })
789 .collect())
790 }
791
792 pub async fn load_index_order_client(
798 con: &ConnectionManager,
799 trader_key: &str,
800 ) -> anyhow::Result<AHashMap<ClientOrderId, ClientId>> {
801 let index = Self::read_index_hash(con, trader_key, INDEX_ORDER_CLIENT).await?;
802 Ok(index
803 .into_iter()
804 .map(|(k, v)| (ClientOrderId::from(k.as_str()), ClientId::from(v.as_str())))
805 .collect())
806 }
807
808 async fn read_index_hash(
809 con: &ConnectionManager,
810 trader_key: &str,
811 key: &str,
812 ) -> anyhow::Result<HashMap<String, String>> {
813 let result = Self::read(con, trader_key, key).await?;
814 if result.is_empty() {
815 return Ok(HashMap::new());
816 }
817
818 serde_json::from_slice(&result[0])
819 .map_err(|e| anyhow::anyhow!("Failed to parse index hash '{key}': {e}"))
820 }
821
822 pub async fn load_custom_data(
832 con: &ConnectionManager,
833 trader_key: &str,
834 data_type: &DataType,
835 ) -> anyhow::Result<Vec<CustomData>> {
836 let pattern = format!("{trader_key}{REDIS_DELIMITER}{CUSTOM}*");
837 log::debug!("Loading custom data {pattern}");
838
839 let mut con = con.clone();
840 let keys = Self::scan_keys(&mut con, pattern).await?;
841
842 if keys.is_empty() {
843 return Ok(Vec::new());
844 }
845
846 let values = Self::read_bulk(&con, &keys).await?;
847 let request_type_name = data_type.type_name();
848 let request_short = request_type_name
849 .rsplit([':', '.'])
850 .next()
851 .unwrap_or(request_type_name);
852 let request_identifier = data_type.identifier().unwrap_or("");
853
854 let mut results = Vec::new();
855
856 for value_opt in values {
857 let Some(value_bytes) = value_opt else {
858 continue;
859 };
860 let custom = match CustomData::from_json_bytes(value_bytes.as_ref()) {
861 Ok(c) => c,
862 Err(e) => {
863 log::warn!("Failed to deserialize custom data from Redis: {e}");
864 continue;
865 }
866 };
867 let stored_type_name = custom.data_type.type_name();
868 let type_match =
869 stored_type_name == request_type_name || stored_type_name == request_short;
870 let identifier_match =
871 custom.data_type.identifier().unwrap_or("") == request_identifier;
872 let metadata_match = match (data_type.metadata(), custom.data_type.metadata()) {
873 (None, None) => true,
874 (Some(a), Some(b)) => serde_json::to_value(a).ok() == serde_json::to_value(b).ok(),
875 _ => false,
876 };
877
878 if type_match && identifier_match && metadata_match {
879 results.push(custom);
880 }
881 }
882
883 results.sort_by_key(HasTsInit::ts_init);
884 log::debug!("Loaded {} custom data item(s)", results.len());
885 Ok(results)
886 }
887
888 pub async fn load_currency(
894 con: &ConnectionManager,
895 trader_key: &str,
896 code: &Ustr,
897 encoding: SerializationEncoding,
898 ) -> anyhow::Result<Option<Currency>> {
899 let key = format!("{CURRENCIES}{REDIS_DELIMITER}{code}");
900 let result = Self::read(con, trader_key, &key).await?;
901
902 if result.is_empty() {
903 return Ok(None);
904 }
905
906 match Self::decode_currency(encoding, &result[0]) {
907 CurrencyDecode::Currency(currency) => Ok(Some(currency)),
908 CurrencyDecode::InvalidRecord(e) | CurrencyDecode::Undecodable(e) => {
909 Err(e.context(format!("Failed to load currency {code}")))
910 }
911 }
912 }
913
914 pub async fn load_instrument(
920 con: &ConnectionManager,
921 trader_key: &str,
922 instrument_id: &InstrumentId,
923 encoding: SerializationEncoding,
924 ) -> anyhow::Result<Option<InstrumentAny>> {
925 let key = format!("{INSTRUMENTS}{REDIS_DELIMITER}{instrument_id}");
926 let result = Self::read(con, trader_key, &key).await?;
927 if result.is_empty() {
928 return Ok(None);
929 }
930
931 let instrument: InstrumentAny = Self::deserialize_payload(encoding, &result[0])?;
932 Ok(Some(instrument))
933 }
934
935 pub async fn load_synthetic(
941 con: &ConnectionManager,
942 trader_key: &str,
943 instrument_id: &InstrumentId,
944 encoding: SerializationEncoding,
945 ) -> anyhow::Result<Option<SyntheticInstrument>> {
946 let key = format!("{SYNTHETICS}{REDIS_DELIMITER}{instrument_id}");
947 let result = Self::read(con, trader_key, &key).await?;
948 if result.is_empty() {
949 return Ok(None);
950 }
951
952 let synthetic: SyntheticInstrument = Self::deserialize_payload(encoding, &result[0])?;
953 Ok(Some(synthetic))
954 }
955
956 pub async fn load_account(
962 con: &ConnectionManager,
963 trader_key: &str,
964 account_id: &AccountId,
965 encoding: SerializationEncoding,
966 ) -> anyhow::Result<Option<AccountAny>> {
967 let key = format!("{ACCOUNTS}{REDIS_DELIMITER}{account_id}");
968 let result = Self::read(con, trader_key, &key).await?;
969 if result.is_empty() {
970 return Ok(None);
971 }
972
973 let events: Vec<AccountState> = result
974 .iter()
975 .map(|payload| Self::deserialize_payload(encoding, payload))
976 .collect::<anyhow::Result<_>>()?;
977 let account = AccountAny::from_events(&events)?;
978 Ok(Some(account))
979 }
980
981 pub async fn load_order(
987 con: &ConnectionManager,
988 trader_key: &str,
989 client_order_id: &ClientOrderId,
990 encoding: SerializationEncoding,
991 ) -> anyhow::Result<Option<OrderAny>> {
992 let key = format!("{ORDERS}{REDIS_DELIMITER}{client_order_id}");
993 let result = Self::read(con, trader_key, &key).await?;
994 if result.is_empty() {
995 return Ok(None);
996 }
997
998 let events: Vec<OrderEventAny> = result
999 .iter()
1000 .map(|payload| Self::deserialize_payload(encoding, payload))
1001 .collect::<anyhow::Result<_>>()?;
1002 let order = OrderAny::from_events(events)?;
1003 Ok(Some(order))
1004 }
1005
1006 pub async fn load_position(
1012 con: &ConnectionManager,
1013 trader_key: &str,
1014 position_id: &PositionId,
1015 encoding: SerializationEncoding,
1016 ) -> anyhow::Result<Option<Position>> {
1017 let snapshot_key =
1018 format!("{SNAPSHOTS}{REDIS_DELIMITER}{POSITIONS}{REDIS_DELIMITER}{position_id}");
1019 let snapshots = Self::read(con, trader_key, &snapshot_key).await?;
1020 for payload in snapshots.iter().rev() {
1021 let snapshot: PositionSnapshot = Self::deserialize_payload(encoding, payload)?;
1022 if let Some(replay_state) = snapshot.replay_state {
1023 let position: Position = serde_json::from_value(replay_state)
1024 .map_err(|e| anyhow::anyhow!("Failed to decode position replay state: {e}"))?;
1025 position.check_accounting()?;
1026 return Ok(Some(position));
1027 }
1028 }
1029
1030 let key = format!("{POSITIONS}{REDIS_DELIMITER}{position_id}");
1031 let result = Self::read(con, trader_key, &key).await?;
1032 if result.is_empty() {
1033 return Ok(None);
1034 }
1035
1036 let fills: Vec<OrderFilled> = result
1037 .iter()
1038 .map(|payload| Self::deserialize_payload(encoding, payload))
1039 .collect::<anyhow::Result<_>>()?;
1040 let Some((first_fill, remaining_fills)) = fills.split_first() else {
1041 return Ok(None);
1042 };
1043 let Some(instrument) =
1044 Self::load_instrument(con, trader_key, &first_fill.instrument_id, encoding).await?
1045 else {
1046 log::error!(
1047 "Instrument not found for position {position_id}: {}",
1048 first_fill.instrument_id
1049 );
1050 return Ok(None);
1051 };
1052
1053 let mut position = Position::new_checked(&instrument, first_fill.clone())?;
1054 for fill in remaining_fills {
1055 if position.trade_ids.contains(&fill.trade_id) {
1056 anyhow::bail!(
1057 "Duplicate fill event for position {position_id}: {}",
1058 fill.trade_id
1059 );
1060 }
1061
1062 position.try_apply(fill)?;
1063 }
1064
1065 Ok(Some(position))
1066 }
1067
1068 fn get_collection_key(key: &str) -> anyhow::Result<&str> {
1069 key.split_once(REDIS_DELIMITER)
1070 .map(|(collection, _)| collection)
1071 .ok_or_else(|| {
1072 anyhow::anyhow!("Invalid `key`, missing a '{REDIS_DELIMITER}' delimiter, was {key}")
1073 })
1074 }
1075
1076 async fn read_index(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1077 let index_key = get_index_key(key)?;
1078 match index_key {
1079 INDEX_ORDER_IDS
1080 | INDEX_ORDERS
1081 | INDEX_ORDERS_OPEN
1082 | INDEX_ORDERS_CLOSED
1083 | INDEX_ORDERS_EMULATED
1084 | INDEX_ORDERS_INFLIGHT
1085 | INDEX_POSITIONS
1086 | INDEX_POSITIONS_OPEN
1087 | INDEX_POSITIONS_CLOSED => Self::read_set(conn, key).await,
1088 INDEX_ORDER_POSITION | INDEX_ORDER_CLIENT => Self::read_hset(conn, key).await,
1089 _ => anyhow::bail!("Index unknown '{index_key}' on read"),
1090 }
1091 }
1092
1093 async fn read_string(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1094 let result: Vec<u8> = conn.get(key).await?;
1095
1096 if result.is_empty() {
1097 Ok(vec![])
1098 } else {
1099 Ok(vec![Bytes::from(result)])
1100 }
1101 }
1102
1103 async fn read_set(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1104 let result: Vec<Bytes> = conn.smembers(key).await?;
1105 Ok(result)
1106 }
1107
1108 async fn read_hset(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1109 let result: HashMap<String, String> = conn.hgetall(key).await?;
1110 let json = serde_json::to_string(&result)?;
1111 Ok(vec![Bytes::from(json.into_bytes())])
1112 }
1113
1114 async fn read_list(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1115 let result: Vec<Bytes> = conn.lrange(key, 0, -1).await?;
1116 Ok(result)
1117 }
1118}
1119
1120fn parse_instrument_key(key: &str, prefix: &str) -> anyhow::Result<InstrumentId> {
1121 let value = key
1122 .strip_prefix(prefix)
1123 .filter(|value| !value.is_empty())
1124 .ok_or_else(|| anyhow::anyhow!("Invalid instrument key '{key}'"))?;
1125 InstrumentId::from_str(value)
1126 .map_err(|e| anyhow::anyhow!("Failed to parse instrument ID from key '{key}': {e}"))
1127}
1128
1129fn is_timestamp_field(key: &str) -> bool {
1130 let expire_match = key == "expire_time_ns";
1131 let ts_match = key.starts_with("ts_");
1132 expire_match || ts_match
1133}
1134
1135fn convert_timestamps(value: &mut Value) {
1136 match value {
1137 Value::Object(map) => {
1138 for (key, v) in map {
1139 if is_timestamp_field(key)
1140 && let Value::Number(n) = v
1141 && let Some(n) = n.as_u64()
1142 {
1143 let dt = Timestamp::from_nanosecond(i128::from(n))
1144 .expect("UnixNanos is within Jiff's timestamp range");
1145 *v = Value::String(format!("{dt:.9}"));
1146 }
1147 convert_timestamps(v);
1148 }
1149 }
1150 Value::Array(arr) => {
1151 for item in arr {
1152 convert_timestamps(item);
1153 }
1154 }
1155 _ => {}
1156 }
1157}
1158
1159fn convert_timestamp_strings(value: &mut Value) {
1160 match value {
1161 Value::Object(map) => {
1162 for (key, v) in map {
1163 if is_timestamp_field(key)
1164 && let Value::String(s) = v
1165 && let Ok(dt) = s.parse::<Timestamp>()
1166 {
1167 let nanos = u64::try_from(dt.as_nanosecond()).expect("Invalid timestamp");
1168 *v = Value::Number(nanos.into());
1169 }
1170 convert_timestamp_strings(v);
1171 }
1172 }
1173 Value::Array(arr) => {
1174 for item in arr {
1175 convert_timestamp_strings(item);
1176 }
1177 }
1178 _ => {}
1179 }
1180}
1181
1182#[cfg(test)]
1183mod tests {
1184 use std::{hint::black_box, str::FromStr, time::Instant};
1185
1186 use bytes::Bytes;
1187 use nautilus_common::enums::SerializationEncoding;
1188 use nautilus_core::{UUID4, UnixNanos};
1189 use nautilus_model::{
1190 data::{Data, stubs::stub_custom_data},
1191 enums::{AccountType, CurrencyType},
1192 events::{AccountState, order::spec::OrderFilledSpec},
1193 identifiers::{AccountId, InstrumentId, PositionId, TradeId},
1194 instruments::{Instrument, InstrumentAny, stubs::currency_pair_btcusdt},
1195 position::Position,
1196 types::{AccountBalance, Currency, Money, Price, Quantity},
1197 };
1198 use rstest::rstest;
1199 use serde::{Deserialize, Serialize, de::DeserializeOwned};
1200
1201 use super::{CurrencyDecode, CurrencyRecord, DatabaseQueries, parse_instrument_key};
1202
1203 #[rstest]
1208 #[case(SerializationEncoding::Json)]
1209 #[case(SerializationEncoding::MsgPack)]
1210 fn test_currency_record_round_trips_an_unregistered_code(
1211 #[case] encoding: SerializationEncoding,
1212 ) {
1213 let code = match encoding {
1215 SerializationEncoding::MsgPack => "ZZREDISM",
1216 _ => "ZZREDISJ",
1217 };
1218 assert!(
1219 Currency::try_from_str(code).is_none(),
1220 "the fixture code must be unregistered for this test to mean anything"
1221 );
1222
1223 let currency = Currency::new(code, 3, 0, "Redis Fixture", CurrencyType::Crypto);
1224 let payload =
1225 DatabaseQueries::serialize_payload(encoding, &CurrencyRecord::from(¤cy)).unwrap();
1226
1227 let CurrencyDecode::Currency(restored) =
1228 DatabaseQueries::decode_currency(encoding, &Bytes::from(payload))
1229 else {
1230 panic!("a valid record decodes to a currency");
1231 };
1232
1233 assert_eq!(restored.code.as_str(), code);
1234 assert_eq!(restored.precision, 3);
1235 assert_eq!(restored.iso4217, 0);
1236 assert_eq!(restored.name.as_str(), "Redis Fixture");
1237 assert_eq!(restored.currency_type, CurrencyType::Crypto);
1238 }
1239
1240 #[rstest]
1242 #[case(SerializationEncoding::Json)]
1243 #[case(SerializationEncoding::MsgPack)]
1244 fn test_legacy_bare_code_still_loads(#[case] encoding: SerializationEncoding) {
1245 let payload = DatabaseQueries::serialize_payload(encoding, &Currency::USD()).unwrap();
1246
1247 let CurrencyDecode::Currency(restored) =
1248 DatabaseQueries::decode_currency(encoding, &Bytes::from(payload))
1249 else {
1250 panic!("a registered bare code decodes to a currency");
1251 };
1252
1253 assert_eq!(restored, Currency::USD());
1254 }
1255
1256 #[rstest]
1258 #[case(SerializationEncoding::Json)]
1259 #[case(SerializationEncoding::MsgPack)]
1260 fn test_invalid_record_is_classified_as_invalid(#[case] encoding: SerializationEncoding) {
1261 let record = CurrencyRecord {
1262 code: "ZZQ1".to_string(),
1263 precision: 20,
1264 iso4217: 0,
1265 name: "ZZQ1".to_string(),
1266 currency_type: CurrencyType::Crypto,
1267 };
1268 let bytes = Bytes::from(DatabaseQueries::serialize_payload(encoding, &record).unwrap());
1269
1270 assert!(matches!(
1271 DatabaseQueries::decode_currency(encoding, &bytes),
1272 CurrencyDecode::InvalidRecord(_)
1273 ));
1274 }
1275
1276 #[rstest]
1281 #[case(SerializationEncoding::Json)]
1282 #[case(SerializationEncoding::MsgPack)]
1283 fn test_record_with_an_unknown_currency_type_is_invalid(
1284 #[case] encoding: SerializationEncoding,
1285 ) {
1286 let record = serde_json::json!({
1287 "code": "ZZQ3",
1288 "precision": 6,
1289 "iso4217": 0,
1290 "name": "ZZQ3",
1291 "currency_type": "STABLECOIN",
1292 });
1293 let bytes = Bytes::from(DatabaseQueries::serialize_payload(encoding, &record).unwrap());
1294
1295 assert!(matches!(
1296 DatabaseQueries::decode_currency(encoding, &bytes),
1297 CurrencyDecode::InvalidRecord(_)
1298 ));
1299 }
1300
1301 #[rstest]
1303 #[case(SerializationEncoding::Json)]
1304 #[case(SerializationEncoding::MsgPack)]
1305 fn test_unknown_bare_code_is_classified_as_undecodable(
1306 #[case] encoding: SerializationEncoding,
1307 ) {
1308 let bytes = Bytes::from(DatabaseQueries::serialize_payload(encoding, &"ZZQ2").unwrap());
1309
1310 let CurrencyDecode::Undecodable(error) = DatabaseQueries::decode_currency(encoding, &bytes)
1311 else {
1312 panic!("an unregistered bare code is undecodable");
1313 };
1314 assert!(
1315 error.to_string().contains("ZZQ2"),
1316 "the error names the code so the operator can register it: {error}"
1317 );
1318 }
1319
1320 #[derive(Debug, Deserialize, PartialEq, Eq)]
1321 struct TimestampPayload {
1322 ts_event: UnixNanos,
1323 ts_init: UnixNanos,
1324 }
1325
1326 #[rstest]
1327 #[case(SerializationEncoding::Json)]
1328 #[case(SerializationEncoding::MsgPack)]
1329 fn test_deserialize_chrono_timestamp_payload(#[case] encoding: SerializationEncoding) {
1330 let json = include_bytes!("../../test_data/redis_cache_timestamp_chrono.json");
1331 let payload = match encoding {
1332 SerializationEncoding::Json => json.to_vec(),
1333 SerializationEncoding::MsgPack => {
1334 let value = serde_json::from_slice::<serde_json::Value>(json).unwrap();
1335 rmp_serde::to_vec(&value).unwrap()
1336 }
1337 SerializationEncoding::Sbe => unreachable!(),
1338 };
1339
1340 let result =
1341 DatabaseQueries::deserialize_payload::<TimestampPayload>(encoding, &payload).unwrap();
1342
1343 assert_eq!(
1344 result,
1345 TimestampPayload {
1346 ts_event: UnixNanos::from(1_123_456_789),
1347 ts_init: UnixNanos::from(2_987_654_321),
1348 }
1349 );
1350 }
1351
1352 #[rstest]
1353 #[case(SerializationEncoding::Json)]
1354 #[case(SerializationEncoding::MsgPack)]
1355 fn test_wallet_account_state_round_trips_unregistered_currency(
1356 #[case] encoding: SerializationEncoding,
1357 ) {
1358 let currency = Currency::new(
1359 "ENG729C",
1360 6,
1361 0,
1362 "Cache round-trip token",
1363 CurrencyType::Crypto,
1364 );
1365 let total = Money::from_mantissa_exponent(123_456_789, -6, currency);
1366 let state = AccountState::new(
1367 AccountId::new("WALLET-CACHE-001"),
1368 AccountType::Wallet,
1369 vec![AccountBalance::new(total, Money::zero(currency), total)],
1370 vec![],
1371 true,
1372 nautilus_core::UUID4::new(),
1373 UnixNanos::from(1),
1374 UnixNanos::from(2),
1375 None,
1376 );
1377 assert!(Currency::try_from_str("ENG729C").is_none());
1378
1379 let payload = DatabaseQueries::serialize_payload(encoding, &state).unwrap();
1380 let restored: AccountState =
1381 DatabaseQueries::deserialize_payload(encoding, &payload).unwrap();
1382 let restored = restored.balances[0];
1383
1384 assert_eq!(restored.total.raw(), total.raw());
1385 assert_eq!(restored.locked.raw(), 0);
1386 assert_eq!(restored.free.raw(), total.raw());
1387 assert_eq!(restored.currency.code, currency.code);
1388 assert_eq!(restored.currency.precision, currency.precision);
1389 assert_eq!(restored.currency.iso4217, currency.iso4217);
1390 assert_eq!(restored.currency.name, currency.name);
1391 assert_eq!(restored.currency.currency_type, currency.currency_type);
1392 assert!(Currency::try_from_str("ENG729C").is_none());
1393 }
1394
1395 #[rstest]
1396 #[ignore = "optimized Redis cache payload measurements without a Redis connection"]
1397 fn redis_payload_measurements() {
1398 let samples = std::env::var("NAUTILUS_BENCH_SAMPLES")
1399 .map_or(10_000, |value| value.parse::<usize>().unwrap());
1400 let trials = std::env::var("NAUTILUS_BENCH_TRIALS")
1401 .map_or(3, |value| value.parse::<usize>().unwrap());
1402
1403 for trial in 0..trials {
1404 let formats = [SerializationEncoding::Json, SerializationEncoding::MsgPack];
1405 for offset in 0..formats.len() {
1406 let encoding = formats[(offset + trial) % formats.len()];
1407
1408 let format = match encoding {
1409 SerializationEncoding::Json => "json",
1410 SerializationEncoding::MsgPack => "msgpack",
1411 SerializationEncoding::Sbe => unreachable!(),
1412 };
1413
1414 if std::env::var("NAUTILUS_BENCH_FORMAT").is_ok_and(|selected| selected != format) {
1415 continue;
1416 }
1417
1418 cache_cases(encoding, trial, samples);
1419 }
1420 }
1421 }
1422
1423 fn cache_cases(encoding: SerializationEncoding, trial: usize, samples: usize) {
1424 let mut state = AccountState::new(
1425 AccountId::from("SIM-1"),
1426 AccountType::Cash,
1427 vec![AccountBalance::new(
1428 Money::from("1000.75 USD"),
1429 Money::from("250.25 USD"),
1430 Money::from("750.50 USD"),
1431 )],
1432 vec![],
1433 true,
1434 UUID4::new(),
1435 UnixNanos::from(1_700_000_000_000_000_123),
1436 UnixNanos::from(1_700_000_000_000_000_456),
1437 Some(Currency::USD()),
1438 );
1439 let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
1440 let custom = Data::Custom(stub_custom_data(77, -88, None, None));
1441 let mut fill = OrderFilledSpec::builder()
1442 .instrument_id(instrument.id())
1443 .position_id(PositionId::from("POSITION-1"))
1444 .last_qty(Quantity::from("2.750"))
1445 .last_px(Price::from("16384.00"))
1446 .currency(Currency::USDT())
1447 .commission(Money::from("0.73 USDT"))
1448 .ts_event(UnixNanos::from(1_700_000_000_000_000_789))
1449 .ts_init(UnixNanos::from(1_700_000_000_000_000_987))
1450 .build();
1451
1452 for populated in [false, true] {
1453 state.info = populated.then(|| {
1454 serde_json::from_str(include_str!(
1455 "../../../common/test_data/message_params.json"
1456 ))
1457 .unwrap()
1458 });
1459
1460 let name = if populated {
1461 "account_state_nested"
1462 } else {
1463 "account_state_plain"
1464 };
1465
1466 measure_cache_payload(name, &state, encoding, trial, samples);
1467 }
1468
1469 measure_cache_payload("instrument", &instrument, encoding, trial, samples);
1470 measure_cache_payload("custom", &custom, encoding, trial, samples);
1471 fill.trade_id = TradeId::from("FILL-0");
1472 let mut position = Position::new(&instrument, fill.clone());
1473 for count in [1, 32, 256] {
1474 for index in position.events.len()..count {
1475 fill.event_id = UUID4::new();
1476 fill.trade_id = TradeId::new(format!("FILL-{index}"));
1477 position.apply(&fill);
1478 }
1479
1480 assert_eq!(position.events.len(), count);
1481 measure_cache_payload(
1482 &format!("position_{count}"),
1483 &position,
1484 encoding,
1485 trial,
1486 samples,
1487 );
1488 }
1489 }
1490
1491 #[allow(
1492 clippy::cast_possible_truncation,
1493 clippy::cast_precision_loss,
1494 reason = "nanosecond samples fit in u64 and timing means are approximate"
1495 )]
1496 fn measure_cache_payload<T: Serialize + DeserializeOwned>(
1497 name: &str,
1498 value: &T,
1499 encoding: SerializationEncoding,
1500 trial: usize,
1501 samples: usize,
1502 ) {
1503 if std::env::var("NAUTILUS_BENCH_CASE").is_ok_and(|selected| selected != name) {
1504 return;
1505 }
1506
1507 assert!(samples > 0);
1508 let payload = DatabaseQueries::serialize_payload(encoding, value).unwrap();
1509 let decoded: T = DatabaseQueries::deserialize_payload(encoding, &payload).unwrap();
1510 assert_eq!(
1511 serde_json::to_value(&decoded).unwrap(),
1512 serde_json::to_value(value).unwrap()
1513 );
1514
1515 let warmup = std::env::var("NAUTILUS_BENCH_WARMUP")
1516 .map_or(1_000, |value| value.parse::<usize>().unwrap());
1517
1518 for _ in 0..warmup {
1519 black_box(DatabaseQueries::serialize_payload(encoding, black_box(value)).unwrap());
1520 black_box(
1521 DatabaseQueries::deserialize_payload::<T>(encoding, black_box(&payload)).unwrap(),
1522 );
1523 }
1524
1525 if let Ok(operation) = std::env::var("NAUTILUS_BENCH_PROFILE") {
1526 for _ in 0..samples {
1527 match operation.as_str() {
1528 "encode" => {
1529 black_box(cache_encode(encoding, black_box(value)));
1530 }
1531 "decode" => {
1532 black_box(cache_decode::<T>(encoding, black_box(&payload)));
1533 }
1534 _ => panic!("profile must be encode or decode"),
1535 }
1536 }
1537
1538 println!(
1539 "{}",
1540 serde_json::json!({"case": name, "operation": operation,
1541 "profile_calls": samples, "bytes": payload.len()})
1542 );
1543 return;
1544 }
1545
1546 let mut encoded = Vec::with_capacity(samples);
1547 let mut decoded = Vec::with_capacity(samples);
1548 for _ in 0..samples {
1549 let started = Instant::now();
1550 let bytes = cache_encode(encoding, black_box(value));
1551 encoded.push(started.elapsed().as_nanos() as u64);
1552 black_box(bytes);
1553 let started = Instant::now();
1554 let value = cache_decode::<T>(encoding, black_box(&payload));
1555 decoded.push(started.elapsed().as_nanos() as u64);
1556 black_box(value);
1557 }
1558
1559 for (operation, mut times) in [("encode", encoded), ("decode", decoded)] {
1560 let mean = times.iter().map(|value| *value as f64).sum::<f64>() / samples as f64;
1561 times.sort_unstable();
1562 let percentile = |n| times[(samples - 1) * n / 1_000];
1563 println!(
1564 "{}",
1565 serde_json::json!({
1566 "case": name, "boundary": "redis_payload", "format": encoding,
1567 "trial": trial, "operation": operation, "samples": samples, "bytes": payload.len(),
1568 "mean_ns": mean, "p50_ns": percentile(500), "p95_ns": percentile(950),
1569 "p99_ns": percentile(990), "p999_ns": percentile(999), "max_ns": times.last().unwrap(),
1570 })
1571 );
1572 }
1573 }
1574
1575 #[inline(never)]
1576 fn cache_encode<T: Serialize>(encoding: SerializationEncoding, value: &T) -> Vec<u8> {
1577 DatabaseQueries::serialize_payload(encoding, value).unwrap()
1578 }
1579
1580 #[inline(never)]
1581 fn cache_decode<T: DeserializeOwned>(encoding: SerializationEncoding, payload: &[u8]) -> T {
1582 DatabaseQueries::deserialize_payload(encoding, payload).unwrap()
1583 }
1584
1585 #[rstest]
1586 #[case("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443.Arbitrum:UniswapV3")]
1587 #[case(concat!(
1588 "0xc9bc8043294146424a4e4607d8ad837d",
1589 "6a659142822bbaaabc83bb57e7447461.Arbitrum:UniswapV4",
1590 ))]
1591 fn test_parse_instrument_key_preserves_colons_in_venue(#[case] value: &str) {
1592 let prefix = "TRADER-001:instruments:";
1593 let key = format!("{prefix}{value}");
1594
1595 let result = parse_instrument_key(&key, prefix).unwrap();
1596
1597 assert_eq!(result, InstrumentId::from_str(value).unwrap());
1598 }
1599}