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 SerializationEncoding::Capnp => {
153 anyhow::bail!("Cap'n Proto encoding is not supported for Redis cache payloads")
154 }
155 }
156 }
157
158 pub fn deserialize_payload<T: DeserializeOwned>(
164 encoding: SerializationEncoding,
165 payload: &[u8],
166 ) -> anyhow::Result<T> {
167 let mut value = match encoding {
168 SerializationEncoding::MsgPack => rmp_serde::from_slice(payload)
169 .map_err(|e| anyhow::anyhow!("Failed to deserialize msgpack `payload`: {e}"))?,
170 SerializationEncoding::Json => serde_json::from_slice(payload)
171 .map_err(|e| anyhow::anyhow!("Failed to deserialize json `payload`: {e}"))?,
172 SerializationEncoding::Sbe => {
173 anyhow::bail!("SBE encoding is not supported for Redis cache payloads")
174 }
175 SerializationEncoding::Capnp => {
176 anyhow::bail!("Cap'n Proto encoding is not supported for Redis cache payloads")
177 }
178 };
179
180 convert_timestamp_strings(&mut value);
181
182 serde_json::from_value(value)
183 .map_err(|e| anyhow::anyhow!("Failed to convert value to target type: {e}"))
184 }
185
186 pub async fn scan_keys(
192 con: &mut ConnectionManager,
193 pattern: String,
194 ) -> anyhow::Result<Vec<String>> {
195 let mut result = Vec::new();
196 let mut cursor = 0u64;
197
198 loop {
199 let scan_result: (u64, Vec<String>) = redis::cmd("SCAN")
200 .arg(cursor)
201 .arg("MATCH")
202 .arg(&pattern)
203 .arg("COUNT")
204 .arg(5000)
205 .query_async(con)
206 .await?;
207
208 let (new_cursor, keys) = scan_result;
209 result.extend(keys);
210
211 if new_cursor == 0 {
213 break;
214 }
215
216 cursor = new_cursor;
217 }
218
219 Ok(result)
220 }
221
222 pub async fn read_bulk(
228 con: &ConnectionManager,
229 keys: &[String],
230 ) -> anyhow::Result<Vec<Option<Bytes>>> {
231 if keys.is_empty() {
232 return Ok(vec![]);
233 }
234
235 let mut con = con.clone();
236
237 let results: Vec<Option<Vec<u8>>> =
239 redis::cmd("MGET").arg(keys).query_async(&mut con).await?;
240
241 let bytes_results: Vec<Option<Bytes>> = results
243 .into_iter()
244 .map(|opt| opt.map(Bytes::from))
245 .collect();
246
247 Ok(bytes_results)
248 }
249
250 pub async fn read_bulk_batched(
259 con: &ConnectionManager,
260 keys: &[String],
261 batch_size: usize,
262 ) -> anyhow::Result<Vec<Option<Bytes>>> {
263 if batch_size == 0 {
264 anyhow::bail!("`batch_size` must be greater than zero");
265 }
266
267 if keys.is_empty() {
268 return Ok(vec![]);
269 }
270
271 let mut all_results: Vec<Option<Bytes>> = Vec::with_capacity(keys.len());
272
273 for chunk in keys.chunks(batch_size) {
274 let mut con = con.clone();
275
276 let results: Vec<Option<Vec<u8>>> =
277 redis::cmd("MGET").arg(chunk).query_async(&mut con).await?;
278
279 all_results.extend(results.into_iter().map(|opt| opt.map(Bytes::from)));
280 }
281
282 Ok(all_results)
283 }
284
285 pub async fn read(
291 con: &ConnectionManager,
292 trader_key: &str,
293 key: &str,
294 ) -> anyhow::Result<Vec<Bytes>> {
295 let collection = Self::get_collection_key(key)?;
296 let full_key = format!("{trader_key}{REDIS_DELIMITER}{key}");
297
298 let mut con = con.clone();
299
300 match collection {
301 INDEX => Self::read_index(&mut con, &full_key).await,
302 GENERAL | CURRENCIES | INSTRUMENTS | INSTRUMENT_CLOSES | SYNTHETICS | ACTORS
303 | STRATEGIES => Self::read_string(&mut con, &full_key).await,
304 ACCOUNTS | ORDERS | POSITIONS | SNAPSHOTS => Self::read_list(&mut con, &full_key).await,
305 _ => anyhow::bail!("Unsupported operation: `read` for collection '{collection}'"),
306 }
307 }
308
309 pub async fn load_all(
315 con: &ConnectionManager,
316 encoding: SerializationEncoding,
317 trader_key: &str,
318 ) -> anyhow::Result<CacheMap> {
319 let mut currencies = Self::load_currencies(con, trader_key, encoding).await?;
322 register_loaded_currencies(&mut currencies)?;
323
324 let (instruments, instrument_closes, synthetics, accounts, orders, positions) =
325 tokio::try_join!(
326 Self::load_instruments(con, trader_key, encoding),
327 Self::load_instrument_closes(con, trader_key, encoding),
328 Self::load_synthetics(con, trader_key, encoding),
329 Self::load_accounts(con, trader_key, encoding),
330 Self::load_orders(con, trader_key, encoding),
331 Self::load_positions(con, trader_key, encoding)
332 )
333 .map_err(|e| anyhow::anyhow!("Error loading cache data: {e}"))?;
334
335 let greeks = AHashMap::new();
338 let yield_curves = AHashMap::new();
339
340 Ok(CacheMap {
341 currencies,
342 instruments,
343 instrument_closes,
344 synthetics,
345 accounts,
346 orders,
347 positions,
348 greeks,
349 yield_curves,
350 })
351 }
352
353 fn decode_currency(encoding: SerializationEncoding, value_bytes: &Bytes) -> CurrencyDecode {
361 match Self::deserialize_payload::<CurrencyRecord>(encoding, value_bytes) {
362 Ok(record) => match Currency::try_from(record) {
363 Ok(currency) => CurrencyDecode::Currency(currency),
364 Err(e) => CurrencyDecode::InvalidRecord(e),
365 },
366 Err(record_err) => match Self::deserialize_payload::<String>(encoding, value_bytes) {
367 Ok(code) => match Currency::try_from_str(&code) {
368 Some(currency) => CurrencyDecode::Currency(currency),
369 None => CurrencyDecode::Undecodable(anyhow::anyhow!(
370 "bare currency code {code} is not registered"
371 )),
372 },
373 Err(_) => CurrencyDecode::InvalidRecord(anyhow::anyhow!(
374 "payload is not a bare code and does not decode as a currency record: {record_err}"
375 )),
376 },
377 }
378 }
379
380 pub async fn load_currencies(
387 con: &ConnectionManager,
388 trader_key: &str,
389 encoding: SerializationEncoding,
390 ) -> anyhow::Result<AHashMap<Ustr, Currency>> {
391 let mut currencies = AHashMap::new();
392 let prefix = format!("{trader_key}{REDIS_DELIMITER}{CURRENCIES}{REDIS_DELIMITER}");
393 let pattern = format!("{prefix}*");
394 log::debug!("Loading {pattern}");
395
396 let mut con = con.clone();
397 let keys = Self::scan_keys(&mut con, pattern).await?;
398
399 if keys.is_empty() {
400 return Ok(currencies);
401 }
402
403 let bulk_values = Self::read_bulk(&con, &keys).await?;
405
406 for (key, value_opt) in keys.iter().zip(bulk_values.iter()) {
408 let currency_code = match key.as_str().strip_prefix(prefix.as_str()) {
411 Some(code) if !code.is_empty() => Ustr::from(code),
412 _ => {
413 log::error!("Invalid key format: {key}");
414 continue;
415 }
416 };
417
418 if let Some(value_bytes) = value_opt {
419 match Self::decode_currency(encoding, value_bytes) {
420 CurrencyDecode::Currency(currency) => {
421 if currency.code != currency_code {
424 anyhow::bail!(
425 "Currency record under key {currency_code} holds code {}",
426 currency.code
427 );
428 }
429 currencies.insert(currency_code, currency);
430 }
431 CurrencyDecode::InvalidRecord(e) => {
432 anyhow::bail!("Invalid currency record for {currency_code}: {e}");
433 }
434 CurrencyDecode::Undecodable(e) => {
435 log::error!(
436 "Skipping currency {currency_code}: {e}; payloads denominated in it will not decode"
437 );
438 }
439 }
440 } else {
441 log::error!("Currency not found in Redis: {currency_code}");
442 }
443 }
444
445 log::debug!("Loaded {} currencies(s)", currencies.len());
446
447 Ok(currencies)
448 }
449
450 pub async fn load_instruments(
461 con: &ConnectionManager,
462 trader_key: &str,
463 encoding: SerializationEncoding,
464 ) -> anyhow::Result<AHashMap<InstrumentId, InstrumentAny>> {
465 let mut instruments = AHashMap::new();
466 let prefix = format!("{trader_key}{REDIS_DELIMITER}{INSTRUMENTS}{REDIS_DELIMITER}");
467 let pattern = format!("{prefix}*");
468 log::debug!("Loading {pattern}");
469
470 let mut con = con.clone();
471 let keys = Self::scan_keys(&mut con, pattern).await?;
472
473 let futures: Vec<_> = keys
474 .iter()
475 .map(|key| {
476 let con = con.clone();
477 let prefix = &prefix;
478 async move {
479 let instrument_id = parse_instrument_key(key, prefix);
480
481 let Ok(instrument_id) = instrument_id else {
482 log::error!("Failed to parse InstrumentId from Redis key: {key}");
483 return None;
484 };
485
486 match Self::load_instrument(&con, trader_key, &instrument_id, encoding).await {
487 Ok(Some(instrument)) => Some((instrument_id, instrument)),
488 Ok(None) => {
489 log::error!("Instrument not found: {instrument_id}");
490 None
491 }
492 Err(e) => {
493 log::error!("Failed to load instrument {instrument_id}: {e}");
494 None
495 }
496 }
497 }
498 })
499 .collect();
500
501 instruments.extend(join_all(futures).await.into_iter().flatten());
503 log::debug!("Loaded {} instruments(s)", instruments.len());
504
505 Ok(instruments)
506 }
507
508 pub async fn load_instrument_closes(
516 con: &ConnectionManager,
517 trader_key: &str,
518 encoding: SerializationEncoding,
519 ) -> anyhow::Result<AHashMap<InstrumentId, InstrumentClose>> {
520 let prefix = format!("{trader_key}{REDIS_DELIMITER}{INSTRUMENT_CLOSES}{REDIS_DELIMITER}");
521 let pattern = format!("{prefix}*");
522 log::debug!("Loading {pattern}");
523
524 let mut con = con.clone();
525 let keys = Self::scan_keys(&mut con, pattern).await?;
526 let values = Self::read_bulk(&con, &keys).await?;
527 let mut closes = AHashMap::with_capacity(keys.len());
528
529 for (key, value) in keys.into_iter().zip(values) {
530 let instrument_id = parse_instrument_key(&key, &prefix)?;
531 let value = value
532 .ok_or_else(|| anyhow::anyhow!("Instrument close not found in Redis: {key}"))?;
533 let close: InstrumentClose = Self::deserialize_payload(encoding, &value)?;
534 anyhow::ensure!(
535 close.instrument_id == instrument_id,
536 "Instrument close key ID {instrument_id} did not match payload ID {}",
537 close.instrument_id,
538 );
539 closes.insert(instrument_id, close);
540 }
541
542 log::debug!("Loaded {} instrument close(s)", closes.len());
543 Ok(closes)
544 }
545
546 pub async fn load_synthetics(
552 con: &ConnectionManager,
553 trader_key: &str,
554 encoding: SerializationEncoding,
555 ) -> anyhow::Result<AHashMap<InstrumentId, SyntheticInstrument>> {
556 let mut synthetics = AHashMap::new();
557 let pattern = format!("{trader_key}{REDIS_DELIMITER}{SYNTHETICS}*");
558 log::debug!("Loading {pattern}");
559
560 let mut con = con.clone();
561 let keys = Self::scan_keys(&mut con, pattern).await?;
562
563 let futures: Vec<_> = keys
564 .iter()
565 .map(|key| {
566 let con = con.clone();
567 async move {
568 let instrument_id = key
569 .as_str()
570 .rsplit(':')
571 .next()
572 .ok_or_else(|| {
573 log::error!("Invalid key format: {key}");
574 "Invalid key format"
575 })
576 .and_then(|code| {
577 InstrumentId::from_str(code).map_err(|e| {
578 log::error!("Failed to parse InstrumentId for {key}: {e}");
579 "Invalid instrument ID"
580 })
581 });
582
583 let Ok(instrument_id) = instrument_id else {
584 return None;
585 };
586
587 match Self::load_synthetic(&con, trader_key, &instrument_id, encoding).await {
588 Ok(Some(synthetic)) => Some((instrument_id, synthetic)),
589 Ok(None) => {
590 log::error!("Synthetic not found: {instrument_id}");
591 None
592 }
593 Err(e) => {
594 log::error!("Failed to load synthetic {instrument_id}: {e}");
595 None
596 }
597 }
598 }
599 })
600 .collect();
601
602 synthetics.extend(join_all(futures).await.into_iter().flatten());
604 log::debug!("Loaded {} synthetics(s)", synthetics.len());
605
606 Ok(synthetics)
607 }
608
609 pub async fn load_accounts(
620 con: &ConnectionManager,
621 trader_key: &str,
622 encoding: SerializationEncoding,
623 ) -> anyhow::Result<AHashMap<AccountId, AccountAny>> {
624 let mut accounts = AHashMap::new();
625 let pattern = format!("{trader_key}{REDIS_DELIMITER}{ACCOUNTS}*");
626 log::debug!("Loading {pattern}");
627
628 let mut con = con.clone();
629 let keys = Self::scan_keys(&mut con, pattern).await?;
630
631 let futures: Vec<_> = keys
632 .iter()
633 .map(|key| {
634 let con = con.clone();
635 async move {
636 let account_id = if let Some(code) = key.as_str().rsplit(':').next() {
637 AccountId::from(code)
638 } else {
639 log::error!("Invalid key format: {key}");
640 return None;
641 };
642
643 match Self::load_account(&con, trader_key, &account_id, encoding).await {
644 Ok(Some(account)) => Some((account_id, account)),
645 Ok(None) => {
646 log::error!("Account not found: {account_id}");
647 None
648 }
649 Err(e) => {
650 log::error!("Failed to load account {account_id}: {e}");
651 None
652 }
653 }
654 }
655 })
656 .collect();
657
658 accounts.extend(join_all(futures).await.into_iter().flatten());
660 log::debug!("Loaded {} accounts(s)", accounts.len());
661
662 Ok(accounts)
663 }
664
665 pub async fn load_orders(
676 con: &ConnectionManager,
677 trader_key: &str,
678 encoding: SerializationEncoding,
679 ) -> anyhow::Result<AHashMap<ClientOrderId, OrderAny>> {
680 let mut orders = AHashMap::new();
681 let pattern = format!("{trader_key}{REDIS_DELIMITER}{ORDERS}*");
682 log::debug!("Loading {pattern}");
683
684 let mut con = con.clone();
685 let keys = Self::scan_keys(&mut con, pattern).await?;
686
687 let futures: Vec<_> = keys
688 .iter()
689 .map(|key| {
690 let con = con.clone();
691 async move {
692 let client_order_id = if let Some(code) = key.as_str().rsplit(':').next() {
693 ClientOrderId::from(code)
694 } else {
695 log::error!("Invalid key format: {key}");
696 return None;
697 };
698
699 match Self::load_order(&con, trader_key, &client_order_id, encoding).await {
700 Ok(Some(order)) => Some((client_order_id, order)),
701 Ok(None) => {
702 log::error!("Order not found: {client_order_id}");
703 None
704 }
705 Err(e) => {
706 log::error!("Failed to load order {client_order_id}: {e}");
707 None
708 }
709 }
710 }
711 })
712 .collect();
713
714 orders.extend(join_all(futures).await.into_iter().flatten());
716 log::debug!("Loaded {} order(s)", orders.len());
717
718 Ok(orders)
719 }
720
721 pub async fn load_positions(
732 con: &ConnectionManager,
733 trader_key: &str,
734 encoding: SerializationEncoding,
735 ) -> anyhow::Result<AHashMap<PositionId, Position>> {
736 let mut positions = AHashMap::new();
737 let pattern = format!("{trader_key}{REDIS_DELIMITER}{POSITIONS}*");
738 log::debug!("Loading {pattern}");
739
740 let mut con = con.clone();
741 let keys = Self::scan_keys(&mut con, pattern).await?;
742
743 let futures: Vec<_> = keys
744 .iter()
745 .map(|key| {
746 let con = con.clone();
747 async move {
748 let position_id = if let Some(code) = key.as_str().rsplit(':').next() {
749 PositionId::from(code)
750 } else {
751 log::error!("Invalid key format: {key}");
752 return None;
753 };
754
755 match Self::load_position(&con, trader_key, &position_id, encoding).await {
756 Ok(Some(position)) => Some((position_id, position)),
757 Ok(None) => {
758 log::error!("Position not found: {position_id}");
759 None
760 }
761 Err(e) => {
762 log::error!("Failed to load position {position_id}: {e}");
763 None
764 }
765 }
766 }
767 })
768 .collect();
769
770 positions.extend(join_all(futures).await.into_iter().flatten());
772 log::debug!("Loaded {} position(s)", positions.len());
773
774 Ok(positions)
775 }
776
777 pub async fn load_index_order_position(
783 con: &ConnectionManager,
784 trader_key: &str,
785 ) -> anyhow::Result<AHashMap<ClientOrderId, PositionId>> {
786 let index = Self::read_index_hash(con, trader_key, INDEX_ORDER_POSITION).await?;
787 Ok(index
788 .into_iter()
789 .map(|(k, v)| {
790 (
791 ClientOrderId::from(k.as_str()),
792 PositionId::from(v.as_str()),
793 )
794 })
795 .collect())
796 }
797
798 pub async fn load_index_order_client(
804 con: &ConnectionManager,
805 trader_key: &str,
806 ) -> anyhow::Result<AHashMap<ClientOrderId, ClientId>> {
807 let index = Self::read_index_hash(con, trader_key, INDEX_ORDER_CLIENT).await?;
808 Ok(index
809 .into_iter()
810 .map(|(k, v)| (ClientOrderId::from(k.as_str()), ClientId::from(v.as_str())))
811 .collect())
812 }
813
814 async fn read_index_hash(
815 con: &ConnectionManager,
816 trader_key: &str,
817 key: &str,
818 ) -> anyhow::Result<HashMap<String, String>> {
819 let result = Self::read(con, trader_key, key).await?;
820 if result.is_empty() {
821 return Ok(HashMap::new());
822 }
823
824 serde_json::from_slice(&result[0])
825 .map_err(|e| anyhow::anyhow!("Failed to parse index hash '{key}': {e}"))
826 }
827
828 pub async fn load_custom_data(
838 con: &ConnectionManager,
839 trader_key: &str,
840 data_type: &DataType,
841 ) -> anyhow::Result<Vec<CustomData>> {
842 let pattern = format!("{trader_key}{REDIS_DELIMITER}{CUSTOM}*");
843 log::debug!("Loading custom data {pattern}");
844
845 let mut con = con.clone();
846 let keys = Self::scan_keys(&mut con, pattern).await?;
847
848 if keys.is_empty() {
849 return Ok(Vec::new());
850 }
851
852 let values = Self::read_bulk(&con, &keys).await?;
853 let request_type_name = data_type.type_name();
854 let request_short = request_type_name
855 .rsplit([':', '.'])
856 .next()
857 .unwrap_or(request_type_name);
858 let request_identifier = data_type.identifier().unwrap_or("");
859
860 let mut results = Vec::new();
861
862 for value_opt in values {
863 let Some(value_bytes) = value_opt else {
864 continue;
865 };
866 let custom = match CustomData::from_json_bytes(value_bytes.as_ref()) {
867 Ok(c) => c,
868 Err(e) => {
869 log::warn!("Failed to deserialize custom data from Redis: {e}");
870 continue;
871 }
872 };
873 let stored_type_name = custom.data_type.type_name();
874 let type_match =
875 stored_type_name == request_type_name || stored_type_name == request_short;
876 let identifier_match =
877 custom.data_type.identifier().unwrap_or("") == request_identifier;
878 let metadata_match = match (data_type.metadata(), custom.data_type.metadata()) {
879 (None, None) => true,
880 (Some(a), Some(b)) => serde_json::to_value(a).ok() == serde_json::to_value(b).ok(),
881 _ => false,
882 };
883
884 if type_match && identifier_match && metadata_match {
885 results.push(custom);
886 }
887 }
888
889 results.sort_by_key(HasTsInit::ts_init);
890 log::debug!("Loaded {} custom data item(s)", results.len());
891 Ok(results)
892 }
893
894 pub async fn load_currency(
900 con: &ConnectionManager,
901 trader_key: &str,
902 code: &Ustr,
903 encoding: SerializationEncoding,
904 ) -> anyhow::Result<Option<Currency>> {
905 let key = format!("{CURRENCIES}{REDIS_DELIMITER}{code}");
906 let result = Self::read(con, trader_key, &key).await?;
907
908 if result.is_empty() {
909 return Ok(None);
910 }
911
912 match Self::decode_currency(encoding, &result[0]) {
913 CurrencyDecode::Currency(currency) => Ok(Some(currency)),
914 CurrencyDecode::InvalidRecord(e) | CurrencyDecode::Undecodable(e) => {
915 Err(e.context(format!("Failed to load currency {code}")))
916 }
917 }
918 }
919
920 pub async fn load_instrument(
926 con: &ConnectionManager,
927 trader_key: &str,
928 instrument_id: &InstrumentId,
929 encoding: SerializationEncoding,
930 ) -> anyhow::Result<Option<InstrumentAny>> {
931 let key = format!("{INSTRUMENTS}{REDIS_DELIMITER}{instrument_id}");
932 let result = Self::read(con, trader_key, &key).await?;
933 if result.is_empty() {
934 return Ok(None);
935 }
936
937 let instrument: InstrumentAny = Self::deserialize_payload(encoding, &result[0])?;
938 Ok(Some(instrument))
939 }
940
941 pub async fn load_synthetic(
947 con: &ConnectionManager,
948 trader_key: &str,
949 instrument_id: &InstrumentId,
950 encoding: SerializationEncoding,
951 ) -> anyhow::Result<Option<SyntheticInstrument>> {
952 let key = format!("{SYNTHETICS}{REDIS_DELIMITER}{instrument_id}");
953 let result = Self::read(con, trader_key, &key).await?;
954 if result.is_empty() {
955 return Ok(None);
956 }
957
958 let synthetic: SyntheticInstrument = Self::deserialize_payload(encoding, &result[0])?;
959 Ok(Some(synthetic))
960 }
961
962 pub async fn load_account(
968 con: &ConnectionManager,
969 trader_key: &str,
970 account_id: &AccountId,
971 encoding: SerializationEncoding,
972 ) -> anyhow::Result<Option<AccountAny>> {
973 let key = format!("{ACCOUNTS}{REDIS_DELIMITER}{account_id}");
974 let result = Self::read(con, trader_key, &key).await?;
975 if result.is_empty() {
976 return Ok(None);
977 }
978
979 let events: Vec<AccountState> = result
980 .iter()
981 .map(|payload| Self::deserialize_payload(encoding, payload))
982 .collect::<anyhow::Result<_>>()?;
983 let account = AccountAny::from_events(&events)?;
984 Ok(Some(account))
985 }
986
987 pub async fn load_order(
993 con: &ConnectionManager,
994 trader_key: &str,
995 client_order_id: &ClientOrderId,
996 encoding: SerializationEncoding,
997 ) -> anyhow::Result<Option<OrderAny>> {
998 let key = format!("{ORDERS}{REDIS_DELIMITER}{client_order_id}");
999 let result = Self::read(con, trader_key, &key).await?;
1000 if result.is_empty() {
1001 return Ok(None);
1002 }
1003
1004 let events: Vec<OrderEventAny> = result
1005 .iter()
1006 .map(|payload| Self::deserialize_payload(encoding, payload))
1007 .collect::<anyhow::Result<_>>()?;
1008 let order = OrderAny::from_events(events)?;
1009 Ok(Some(order))
1010 }
1011
1012 pub async fn load_position(
1018 con: &ConnectionManager,
1019 trader_key: &str,
1020 position_id: &PositionId,
1021 encoding: SerializationEncoding,
1022 ) -> anyhow::Result<Option<Position>> {
1023 let snapshot_key =
1024 format!("{SNAPSHOTS}{REDIS_DELIMITER}{POSITIONS}{REDIS_DELIMITER}{position_id}");
1025 let snapshots = Self::read(con, trader_key, &snapshot_key).await?;
1026 for payload in snapshots.iter().rev() {
1027 let snapshot: PositionSnapshot = Self::deserialize_payload(encoding, payload)?;
1028 if let Some(replay_state) = snapshot.replay_state {
1029 return serde_json::from_value(replay_state)
1030 .map(Some)
1031 .map_err(|e| anyhow::anyhow!("Failed to decode position replay state: {e}"));
1032 }
1033 }
1034
1035 let key = format!("{POSITIONS}{REDIS_DELIMITER}{position_id}");
1036 let result = Self::read(con, trader_key, &key).await?;
1037 if result.is_empty() {
1038 return Ok(None);
1039 }
1040
1041 let fills: Vec<OrderFilled> = result
1042 .iter()
1043 .map(|payload| Self::deserialize_payload(encoding, payload))
1044 .collect::<anyhow::Result<_>>()?;
1045 let Some((first_fill, remaining_fills)) = fills.split_first() else {
1046 return Ok(None);
1047 };
1048 let Some(instrument) =
1049 Self::load_instrument(con, trader_key, &first_fill.instrument_id, encoding).await?
1050 else {
1051 log::error!(
1052 "Instrument not found for position {position_id}: {}",
1053 first_fill.instrument_id
1054 );
1055 return Ok(None);
1056 };
1057
1058 let mut position = Position::new(&instrument, first_fill.clone());
1059 for fill in remaining_fills {
1060 if position.trade_ids.contains(&fill.trade_id) {
1061 anyhow::bail!(
1062 "Duplicate fill event for position {position_id}: {}",
1063 fill.trade_id
1064 );
1065 }
1066 position.apply(fill);
1067 }
1068
1069 Ok(Some(position))
1070 }
1071
1072 fn get_collection_key(key: &str) -> anyhow::Result<&str> {
1073 key.split_once(REDIS_DELIMITER)
1074 .map(|(collection, _)| collection)
1075 .ok_or_else(|| {
1076 anyhow::anyhow!("Invalid `key`, missing a '{REDIS_DELIMITER}' delimiter, was {key}")
1077 })
1078 }
1079
1080 async fn read_index(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1081 let index_key = get_index_key(key)?;
1082 match index_key {
1083 INDEX_ORDER_IDS
1084 | INDEX_ORDERS
1085 | INDEX_ORDERS_OPEN
1086 | INDEX_ORDERS_CLOSED
1087 | INDEX_ORDERS_EMULATED
1088 | INDEX_ORDERS_INFLIGHT
1089 | INDEX_POSITIONS
1090 | INDEX_POSITIONS_OPEN
1091 | INDEX_POSITIONS_CLOSED => Self::read_set(conn, key).await,
1092 INDEX_ORDER_POSITION | INDEX_ORDER_CLIENT => Self::read_hset(conn, key).await,
1093 _ => anyhow::bail!("Index unknown '{index_key}' on read"),
1094 }
1095 }
1096
1097 async fn read_string(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1098 let result: Vec<u8> = conn.get(key).await?;
1099
1100 if result.is_empty() {
1101 Ok(vec![])
1102 } else {
1103 Ok(vec![Bytes::from(result)])
1104 }
1105 }
1106
1107 async fn read_set(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1108 let result: Vec<Bytes> = conn.smembers(key).await?;
1109 Ok(result)
1110 }
1111
1112 async fn read_hset(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1113 let result: HashMap<String, String> = conn.hgetall(key).await?;
1114 let json = serde_json::to_string(&result)?;
1115 Ok(vec![Bytes::from(json.into_bytes())])
1116 }
1117
1118 async fn read_list(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1119 let result: Vec<Bytes> = conn.lrange(key, 0, -1).await?;
1120 Ok(result)
1121 }
1122}
1123
1124fn parse_instrument_key(key: &str, prefix: &str) -> anyhow::Result<InstrumentId> {
1125 let value = key
1126 .strip_prefix(prefix)
1127 .filter(|value| !value.is_empty())
1128 .ok_or_else(|| anyhow::anyhow!("Invalid instrument key '{key}'"))?;
1129 InstrumentId::from_str(value)
1130 .map_err(|e| anyhow::anyhow!("Failed to parse instrument ID from key '{key}': {e}"))
1131}
1132
1133fn is_timestamp_field(key: &str) -> bool {
1134 let expire_match = key == "expire_time_ns";
1135 let ts_match = key.starts_with("ts_");
1136 expire_match || ts_match
1137}
1138
1139fn convert_timestamps(value: &mut Value) {
1140 match value {
1141 Value::Object(map) => {
1142 for (key, v) in map {
1143 if is_timestamp_field(key)
1144 && let Value::Number(n) = v
1145 && let Some(n) = n.as_u64()
1146 {
1147 let dt = Timestamp::from_nanosecond(i128::from(n))
1148 .expect("UnixNanos is within Jiff's timestamp range");
1149 *v = Value::String(format!("{dt:.9}"));
1150 }
1151 convert_timestamps(v);
1152 }
1153 }
1154 Value::Array(arr) => {
1155 for item in arr {
1156 convert_timestamps(item);
1157 }
1158 }
1159 _ => {}
1160 }
1161}
1162
1163fn convert_timestamp_strings(value: &mut Value) {
1164 match value {
1165 Value::Object(map) => {
1166 for (key, v) in map {
1167 if is_timestamp_field(key)
1168 && let Value::String(s) = v
1169 && let Ok(dt) = s.parse::<Timestamp>()
1170 {
1171 let nanos = u64::try_from(dt.as_nanosecond()).expect("Invalid timestamp");
1172 *v = Value::Number(nanos.into());
1173 }
1174 convert_timestamp_strings(v);
1175 }
1176 }
1177 Value::Array(arr) => {
1178 for item in arr {
1179 convert_timestamp_strings(item);
1180 }
1181 }
1182 _ => {}
1183 }
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188 use std::str::FromStr;
1189
1190 use bytes::Bytes;
1191 use nautilus_common::enums::SerializationEncoding;
1192 use nautilus_core::UnixNanos;
1193 use nautilus_model::{
1194 enums::{AccountType, CurrencyType},
1195 events::AccountState,
1196 identifiers::{AccountId, InstrumentId},
1197 types::{AccountBalance, Currency, Money},
1198 };
1199 use rstest::rstest;
1200 use serde::Deserialize;
1201
1202 use super::{CurrencyDecode, CurrencyRecord, DatabaseQueries, parse_instrument_key};
1203
1204 #[rstest]
1209 #[case(SerializationEncoding::Json)]
1210 #[case(SerializationEncoding::MsgPack)]
1211 fn test_currency_record_round_trips_an_unregistered_code(
1212 #[case] encoding: SerializationEncoding,
1213 ) {
1214 let code = match encoding {
1216 SerializationEncoding::MsgPack => "ZZREDISM",
1217 _ => "ZZREDISJ",
1218 };
1219 assert!(
1220 Currency::try_from_str(code).is_none(),
1221 "the fixture code must be unregistered for this test to mean anything"
1222 );
1223
1224 let currency = Currency::new(code, 3, 0, "Redis Fixture", CurrencyType::Crypto);
1225 let payload =
1226 DatabaseQueries::serialize_payload(encoding, &CurrencyRecord::from(¤cy)).unwrap();
1227
1228 let CurrencyDecode::Currency(restored) =
1229 DatabaseQueries::decode_currency(encoding, &Bytes::from(payload))
1230 else {
1231 panic!("a valid record decodes to a currency");
1232 };
1233
1234 assert_eq!(restored.code.as_str(), code);
1235 assert_eq!(restored.precision, 3);
1236 assert_eq!(restored.iso4217, 0);
1237 assert_eq!(restored.name.as_str(), "Redis Fixture");
1238 assert_eq!(restored.currency_type, CurrencyType::Crypto);
1239 }
1240
1241 #[rstest]
1243 #[case(SerializationEncoding::Json)]
1244 #[case(SerializationEncoding::MsgPack)]
1245 fn test_legacy_bare_code_still_loads(#[case] encoding: SerializationEncoding) {
1246 let payload = DatabaseQueries::serialize_payload(encoding, &Currency::USD()).unwrap();
1247
1248 let CurrencyDecode::Currency(restored) =
1249 DatabaseQueries::decode_currency(encoding, &Bytes::from(payload))
1250 else {
1251 panic!("a registered bare code decodes to a currency");
1252 };
1253
1254 assert_eq!(restored, Currency::USD());
1255 }
1256
1257 #[rstest]
1259 #[case(SerializationEncoding::Json)]
1260 #[case(SerializationEncoding::MsgPack)]
1261 fn test_invalid_record_is_classified_as_invalid(#[case] encoding: SerializationEncoding) {
1262 let record = CurrencyRecord {
1263 code: "ZZQ1".to_string(),
1264 precision: 20,
1265 iso4217: 0,
1266 name: "ZZQ1".to_string(),
1267 currency_type: CurrencyType::Crypto,
1268 };
1269 let bytes = Bytes::from(DatabaseQueries::serialize_payload(encoding, &record).unwrap());
1270
1271 assert!(matches!(
1272 DatabaseQueries::decode_currency(encoding, &bytes),
1273 CurrencyDecode::InvalidRecord(_)
1274 ));
1275 }
1276
1277 #[rstest]
1282 #[case(SerializationEncoding::Json)]
1283 #[case(SerializationEncoding::MsgPack)]
1284 fn test_record_with_an_unknown_currency_type_is_invalid(
1285 #[case] encoding: SerializationEncoding,
1286 ) {
1287 let record = serde_json::json!({
1288 "code": "ZZQ3",
1289 "precision": 6,
1290 "iso4217": 0,
1291 "name": "ZZQ3",
1292 "currency_type": "STABLECOIN",
1293 });
1294 let bytes = Bytes::from(DatabaseQueries::serialize_payload(encoding, &record).unwrap());
1295
1296 assert!(matches!(
1297 DatabaseQueries::decode_currency(encoding, &bytes),
1298 CurrencyDecode::InvalidRecord(_)
1299 ));
1300 }
1301
1302 #[rstest]
1304 #[case(SerializationEncoding::Json)]
1305 #[case(SerializationEncoding::MsgPack)]
1306 fn test_unknown_bare_code_is_classified_as_undecodable(
1307 #[case] encoding: SerializationEncoding,
1308 ) {
1309 let bytes = Bytes::from(DatabaseQueries::serialize_payload(encoding, &"ZZQ2").unwrap());
1310
1311 let CurrencyDecode::Undecodable(error) = DatabaseQueries::decode_currency(encoding, &bytes)
1312 else {
1313 panic!("an unregistered bare code is undecodable");
1314 };
1315 assert!(
1316 error.to_string().contains("ZZQ2"),
1317 "the error names the code so the operator can register it: {error}"
1318 );
1319 }
1320
1321 #[derive(Debug, Deserialize, PartialEq, Eq)]
1322 struct TimestampPayload {
1323 ts_event: UnixNanos,
1324 ts_init: UnixNanos,
1325 }
1326
1327 #[rstest]
1328 #[case(SerializationEncoding::Json)]
1329 #[case(SerializationEncoding::MsgPack)]
1330 fn test_deserialize_chrono_timestamp_payload(#[case] encoding: SerializationEncoding) {
1331 let json = include_bytes!("../../test_data/redis_cache_timestamp_chrono.json");
1332 let payload = match encoding {
1333 SerializationEncoding::Json => json.to_vec(),
1334 SerializationEncoding::MsgPack => {
1335 let value = serde_json::from_slice::<serde_json::Value>(json).unwrap();
1336 rmp_serde::to_vec(&value).unwrap()
1337 }
1338 _ => unreachable!(),
1339 };
1340
1341 let result =
1342 DatabaseQueries::deserialize_payload::<TimestampPayload>(encoding, &payload).unwrap();
1343
1344 assert_eq!(
1345 result,
1346 TimestampPayload {
1347 ts_event: UnixNanos::from(1_123_456_789),
1348 ts_init: UnixNanos::from(2_987_654_321),
1349 }
1350 );
1351 }
1352
1353 #[rstest]
1354 #[case(SerializationEncoding::Json)]
1355 #[case(SerializationEncoding::MsgPack)]
1356 fn test_wallet_account_state_round_trips_unregistered_currency(
1357 #[case] encoding: SerializationEncoding,
1358 ) {
1359 let currency = Currency::new(
1360 "ENG729C",
1361 6,
1362 0,
1363 "Cache round-trip token",
1364 CurrencyType::Crypto,
1365 );
1366 let total = Money::from_mantissa_exponent(123_456_789, -6, currency);
1367 let state = AccountState::new(
1368 AccountId::new("WALLET-CACHE-001"),
1369 AccountType::Wallet,
1370 vec![AccountBalance::new(total, Money::zero(currency), total)],
1371 vec![],
1372 true,
1373 nautilus_core::UUID4::new(),
1374 UnixNanos::from(1),
1375 UnixNanos::from(2),
1376 None,
1377 );
1378 assert!(Currency::try_from_str("ENG729C").is_none());
1379
1380 let payload = DatabaseQueries::serialize_payload(encoding, &state).unwrap();
1381 let restored: AccountState =
1382 DatabaseQueries::deserialize_payload(encoding, &payload).unwrap();
1383 let restored = restored.balances[0];
1384
1385 assert_eq!(restored.total.raw(), total.raw());
1386 assert_eq!(restored.locked.raw(), 0);
1387 assert_eq!(restored.free.raw(), total.raw());
1388 assert_eq!(restored.currency.code, currency.code);
1389 assert_eq!(restored.currency.precision, currency.precision);
1390 assert_eq!(restored.currency.iso4217, currency.iso4217);
1391 assert_eq!(restored.currency.name, currency.name);
1392 assert_eq!(restored.currency.currency_type, currency.currency_type);
1393 assert!(Currency::try_from_str("ENG729C").is_none());
1394 }
1395
1396 #[rstest]
1397 #[case("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443.Arbitrum:UniswapV3")]
1398 #[case(concat!(
1399 "0xc9bc8043294146424a4e4607d8ad837d",
1400 "6a659142822bbaaabc83bb57e7447461.Arbitrum:UniswapV4",
1401 ))]
1402 fn test_parse_instrument_key_preserves_colons_in_venue(#[case] value: &str) {
1403 let prefix = "TRADER-001:instruments:";
1404 let key = format!("{prefix}{value}");
1405
1406 let result = parse_instrument_key(&key, prefix).unwrap();
1407
1408 assert_eq!(result, InstrumentId::from_str(value).unwrap());
1409 }
1410}