1use std::{collections::HashMap, str::FromStr};
17
18use ahash::AHashMap;
19use bytes::Bytes;
20use futures::future::join_all;
21use jiff::Timestamp;
22use nautilus_common::{cache::database::CacheMap, enums::SerializationEncoding};
23use nautilus_model::{
24 accounts::AccountAny,
25 data::{CustomData, DataType, HasTsInit},
26 events::{AccountState, OrderEventAny, OrderFilled, PositionSnapshot},
27 identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, PositionId},
28 instruments::{InstrumentAny, SyntheticInstrument},
29 orders::OrderAny,
30 position::Position,
31 types::Currency,
32};
33use redis::{AsyncCommands, aio::ConnectionManager};
34use serde::{Serialize, de::DeserializeOwned};
35use serde_json::Value;
36use ustr::Ustr;
37
38use super::get_index_key;
39
40const INDEX: &str = "index";
42const GENERAL: &str = "general";
43const CURRENCIES: &str = "currencies";
44const INSTRUMENTS: &str = "instruments";
45const SYNTHETICS: &str = "synthetics";
46const ACCOUNTS: &str = "accounts";
47const ORDERS: &str = "orders";
48const POSITIONS: &str = "positions";
49const SNAPSHOTS: &str = "snapshots";
50const ACTORS: &str = "actors";
51const STRATEGIES: &str = "strategies";
52const CUSTOM: &str = "custom";
53const REDIS_DELIMITER: char = ':';
54
55const INDEX_ORDER_IDS: &str = "index:order_ids";
57const INDEX_ORDER_POSITION: &str = "index:order_position";
58const INDEX_ORDER_CLIENT: &str = "index:order_client";
59const INDEX_ORDERS: &str = "index:orders";
60const INDEX_ORDERS_OPEN: &str = "index:orders_open";
61const INDEX_ORDERS_CLOSED: &str = "index:orders_closed";
62const INDEX_ORDERS_EMULATED: &str = "index:orders_emulated";
63const INDEX_ORDERS_INFLIGHT: &str = "index:orders_inflight";
64const INDEX_POSITIONS: &str = "index:positions";
65const INDEX_POSITIONS_OPEN: &str = "index:positions_open";
66const INDEX_POSITIONS_CLOSED: &str = "index:positions_closed";
67
68#[derive(Debug)]
69pub struct DatabaseQueries;
70
71impl DatabaseQueries {
72 pub fn serialize_payload<T: Serialize>(
78 encoding: SerializationEncoding,
79 payload: &T,
80 ) -> anyhow::Result<Vec<u8>> {
81 match encoding {
82 SerializationEncoding::MsgPack => {
83 let mut value = serde_json::to_value(payload)?;
84 convert_timestamps(&mut value);
85 rmp_serde::to_vec(&value)
86 .map_err(|e| anyhow::anyhow!("Failed to serialize msgpack `payload`: {e}"))
87 }
88 SerializationEncoding::Json => {
89 let mut value = serde_json::to_value(payload)?;
90 convert_timestamps(&mut value);
91 serde_json::to_vec(&value)
92 .map_err(|e| anyhow::anyhow!("Failed to serialize json `payload`: {e}"))
93 }
94 SerializationEncoding::Sbe => {
95 anyhow::bail!("SBE encoding is not supported for Redis cache payloads")
96 }
97 SerializationEncoding::Capnp => {
98 anyhow::bail!("Cap'n Proto encoding is not supported for Redis cache payloads")
99 }
100 }
101 }
102
103 pub fn deserialize_payload<T: DeserializeOwned>(
109 encoding: SerializationEncoding,
110 payload: &[u8],
111 ) -> anyhow::Result<T> {
112 let mut value = match encoding {
113 SerializationEncoding::MsgPack => rmp_serde::from_slice(payload)
114 .map_err(|e| anyhow::anyhow!("Failed to deserialize msgpack `payload`: {e}"))?,
115 SerializationEncoding::Json => serde_json::from_slice(payload)
116 .map_err(|e| anyhow::anyhow!("Failed to deserialize json `payload`: {e}"))?,
117 SerializationEncoding::Sbe => {
118 anyhow::bail!("SBE encoding is not supported for Redis cache payloads")
119 }
120 SerializationEncoding::Capnp => {
121 anyhow::bail!("Cap'n Proto encoding is not supported for Redis cache payloads")
122 }
123 };
124
125 convert_timestamp_strings(&mut value);
126
127 serde_json::from_value(value)
128 .map_err(|e| anyhow::anyhow!("Failed to convert value to target type: {e}"))
129 }
130
131 pub async fn scan_keys(
137 con: &mut ConnectionManager,
138 pattern: String,
139 ) -> anyhow::Result<Vec<String>> {
140 let mut result = Vec::new();
141 let mut cursor = 0u64;
142
143 loop {
144 let scan_result: (u64, Vec<String>) = redis::cmd("SCAN")
145 .arg(cursor)
146 .arg("MATCH")
147 .arg(&pattern)
148 .arg("COUNT")
149 .arg(5000)
150 .query_async(con)
151 .await?;
152
153 let (new_cursor, keys) = scan_result;
154 result.extend(keys);
155
156 if new_cursor == 0 {
158 break;
159 }
160
161 cursor = new_cursor;
162 }
163
164 Ok(result)
165 }
166
167 pub async fn read_bulk(
173 con: &ConnectionManager,
174 keys: &[String],
175 ) -> anyhow::Result<Vec<Option<Bytes>>> {
176 if keys.is_empty() {
177 return Ok(vec![]);
178 }
179
180 let mut con = con.clone();
181
182 let results: Vec<Option<Vec<u8>>> =
184 redis::cmd("MGET").arg(keys).query_async(&mut con).await?;
185
186 let bytes_results: Vec<Option<Bytes>> = results
188 .into_iter()
189 .map(|opt| opt.map(Bytes::from))
190 .collect();
191
192 Ok(bytes_results)
193 }
194
195 pub async fn read_bulk_batched(
204 con: &ConnectionManager,
205 keys: &[String],
206 batch_size: usize,
207 ) -> anyhow::Result<Vec<Option<Bytes>>> {
208 if batch_size == 0 {
209 anyhow::bail!("`batch_size` must be greater than zero");
210 }
211
212 if keys.is_empty() {
213 return Ok(vec![]);
214 }
215
216 let mut all_results: Vec<Option<Bytes>> = Vec::with_capacity(keys.len());
217
218 for chunk in keys.chunks(batch_size) {
219 let mut con = con.clone();
220
221 let results: Vec<Option<Vec<u8>>> =
222 redis::cmd("MGET").arg(chunk).query_async(&mut con).await?;
223
224 all_results.extend(results.into_iter().map(|opt| opt.map(Bytes::from)));
225 }
226
227 Ok(all_results)
228 }
229
230 pub async fn read(
236 con: &ConnectionManager,
237 trader_key: &str,
238 key: &str,
239 ) -> anyhow::Result<Vec<Bytes>> {
240 let collection = Self::get_collection_key(key)?;
241 let full_key = format!("{trader_key}{REDIS_DELIMITER}{key}");
242
243 let mut con = con.clone();
244
245 match collection {
246 INDEX => Self::read_index(&mut con, &full_key).await,
247 GENERAL | CURRENCIES | INSTRUMENTS | SYNTHETICS | ACTORS | STRATEGIES => {
248 Self::read_string(&mut con, &full_key).await
249 }
250 ACCOUNTS | ORDERS | POSITIONS | SNAPSHOTS => Self::read_list(&mut con, &full_key).await,
251 _ => anyhow::bail!("Unsupported operation: `read` for collection '{collection}'"),
252 }
253 }
254
255 pub async fn load_all(
261 con: &ConnectionManager,
262 encoding: SerializationEncoding,
263 trader_key: &str,
264 ) -> anyhow::Result<CacheMap> {
265 let (currencies, instruments, synthetics, accounts, orders, positions) = tokio::try_join!(
266 Self::load_currencies(con, trader_key, encoding),
267 Self::load_instruments(con, trader_key, encoding),
268 Self::load_synthetics(con, trader_key, encoding),
269 Self::load_accounts(con, trader_key, encoding),
270 Self::load_orders(con, trader_key, encoding),
271 Self::load_positions(con, trader_key, encoding)
272 )
273 .map_err(|e| anyhow::anyhow!("Error loading cache data: {e}"))?;
274
275 let greeks = AHashMap::new();
278 let yield_curves = AHashMap::new();
279
280 Ok(CacheMap {
281 currencies,
282 instruments,
283 synthetics,
284 accounts,
285 orders,
286 positions,
287 greeks,
288 yield_curves,
289 })
290 }
291
292 pub async fn load_currencies(
298 con: &ConnectionManager,
299 trader_key: &str,
300 encoding: SerializationEncoding,
301 ) -> anyhow::Result<AHashMap<Ustr, Currency>> {
302 let mut currencies = AHashMap::new();
303 let pattern = format!("{trader_key}{REDIS_DELIMITER}{CURRENCIES}*");
304 log::debug!("Loading {pattern}");
305
306 let mut con = con.clone();
307 let keys = Self::scan_keys(&mut con, pattern).await?;
308
309 if keys.is_empty() {
310 return Ok(currencies);
311 }
312
313 let bulk_values = Self::read_bulk(&con, &keys).await?;
315
316 for (key, value_opt) in keys.iter().zip(bulk_values.iter()) {
318 let currency_code = if let Some(code) = key.as_str().rsplit(':').next() {
319 Ustr::from(code)
320 } else {
321 log::error!("Invalid key format: {key}");
322 continue;
323 };
324
325 if let Some(value_bytes) = value_opt {
326 match Self::deserialize_payload(encoding, value_bytes) {
327 Ok(currency) => {
328 currencies.insert(currency_code, currency);
329 }
330 Err(e) => {
331 log::error!("Failed to deserialize currency {currency_code}: {e}");
332 }
333 }
334 } else {
335 log::error!("Currency not found in Redis: {currency_code}");
336 }
337 }
338
339 log::debug!("Loaded {} currencies(s)", currencies.len());
340
341 Ok(currencies)
342 }
343
344 pub async fn load_instruments(
355 con: &ConnectionManager,
356 trader_key: &str,
357 encoding: SerializationEncoding,
358 ) -> anyhow::Result<AHashMap<InstrumentId, InstrumentAny>> {
359 let mut instruments = AHashMap::new();
360 let prefix = format!("{trader_key}{REDIS_DELIMITER}{INSTRUMENTS}{REDIS_DELIMITER}");
361 let pattern = format!("{prefix}*");
362 log::debug!("Loading {pattern}");
363
364 let mut con = con.clone();
365 let keys = Self::scan_keys(&mut con, pattern).await?;
366
367 let futures: Vec<_> = keys
368 .iter()
369 .map(|key| {
370 let con = con.clone();
371 let prefix = &prefix;
372 async move {
373 let instrument_id = parse_instrument_key(key, prefix);
374
375 let Ok(instrument_id) = instrument_id else {
376 log::error!("Failed to parse InstrumentId from Redis key: {key}");
377 return None;
378 };
379
380 match Self::load_instrument(&con, trader_key, &instrument_id, encoding).await {
381 Ok(Some(instrument)) => Some((instrument_id, instrument)),
382 Ok(None) => {
383 log::error!("Instrument not found: {instrument_id}");
384 None
385 }
386 Err(e) => {
387 log::error!("Failed to load instrument {instrument_id}: {e}");
388 None
389 }
390 }
391 }
392 })
393 .collect();
394
395 instruments.extend(join_all(futures).await.into_iter().flatten());
397 log::debug!("Loaded {} instruments(s)", instruments.len());
398
399 Ok(instruments)
400 }
401
402 pub async fn load_synthetics(
413 con: &ConnectionManager,
414 trader_key: &str,
415 encoding: SerializationEncoding,
416 ) -> anyhow::Result<AHashMap<InstrumentId, SyntheticInstrument>> {
417 let mut synthetics = AHashMap::new();
418 let pattern = format!("{trader_key}{REDIS_DELIMITER}{SYNTHETICS}*");
419 log::debug!("Loading {pattern}");
420
421 let mut con = con.clone();
422 let keys = Self::scan_keys(&mut con, pattern).await?;
423
424 let futures: Vec<_> = keys
425 .iter()
426 .map(|key| {
427 let con = con.clone();
428 async move {
429 let instrument_id = key
430 .as_str()
431 .rsplit(':')
432 .next()
433 .ok_or_else(|| {
434 log::error!("Invalid key format: {key}");
435 "Invalid key format"
436 })
437 .and_then(|code| {
438 InstrumentId::from_str(code).map_err(|e| {
439 log::error!("Failed to parse InstrumentId for {key}: {e}");
440 "Invalid instrument ID"
441 })
442 });
443
444 let Ok(instrument_id) = instrument_id else {
445 return None;
446 };
447
448 match Self::load_synthetic(&con, trader_key, &instrument_id, encoding).await {
449 Ok(Some(synthetic)) => Some((instrument_id, synthetic)),
450 Ok(None) => {
451 log::error!("Synthetic not found: {instrument_id}");
452 None
453 }
454 Err(e) => {
455 log::error!("Failed to load synthetic {instrument_id}: {e}");
456 None
457 }
458 }
459 }
460 })
461 .collect();
462
463 synthetics.extend(join_all(futures).await.into_iter().flatten());
465 log::debug!("Loaded {} synthetics(s)", synthetics.len());
466
467 Ok(synthetics)
468 }
469
470 pub async fn load_accounts(
481 con: &ConnectionManager,
482 trader_key: &str,
483 encoding: SerializationEncoding,
484 ) -> anyhow::Result<AHashMap<AccountId, AccountAny>> {
485 let mut accounts = AHashMap::new();
486 let pattern = format!("{trader_key}{REDIS_DELIMITER}{ACCOUNTS}*");
487 log::debug!("Loading {pattern}");
488
489 let mut con = con.clone();
490 let keys = Self::scan_keys(&mut con, pattern).await?;
491
492 let futures: Vec<_> = keys
493 .iter()
494 .map(|key| {
495 let con = con.clone();
496 async move {
497 let account_id = if let Some(code) = key.as_str().rsplit(':').next() {
498 AccountId::from(code)
499 } else {
500 log::error!("Invalid key format: {key}");
501 return None;
502 };
503
504 match Self::load_account(&con, trader_key, &account_id, encoding).await {
505 Ok(Some(account)) => Some((account_id, account)),
506 Ok(None) => {
507 log::error!("Account not found: {account_id}");
508 None
509 }
510 Err(e) => {
511 log::error!("Failed to load account {account_id}: {e}");
512 None
513 }
514 }
515 }
516 })
517 .collect();
518
519 accounts.extend(join_all(futures).await.into_iter().flatten());
521 log::debug!("Loaded {} accounts(s)", accounts.len());
522
523 Ok(accounts)
524 }
525
526 pub async fn load_orders(
537 con: &ConnectionManager,
538 trader_key: &str,
539 encoding: SerializationEncoding,
540 ) -> anyhow::Result<AHashMap<ClientOrderId, OrderAny>> {
541 let mut orders = AHashMap::new();
542 let pattern = format!("{trader_key}{REDIS_DELIMITER}{ORDERS}*");
543 log::debug!("Loading {pattern}");
544
545 let mut con = con.clone();
546 let keys = Self::scan_keys(&mut con, pattern).await?;
547
548 let futures: Vec<_> = keys
549 .iter()
550 .map(|key| {
551 let con = con.clone();
552 async move {
553 let client_order_id = if let Some(code) = key.as_str().rsplit(':').next() {
554 ClientOrderId::from(code)
555 } else {
556 log::error!("Invalid key format: {key}");
557 return None;
558 };
559
560 match Self::load_order(&con, trader_key, &client_order_id, encoding).await {
561 Ok(Some(order)) => Some((client_order_id, order)),
562 Ok(None) => {
563 log::error!("Order not found: {client_order_id}");
564 None
565 }
566 Err(e) => {
567 log::error!("Failed to load order {client_order_id}: {e}");
568 None
569 }
570 }
571 }
572 })
573 .collect();
574
575 orders.extend(join_all(futures).await.into_iter().flatten());
577 log::debug!("Loaded {} order(s)", orders.len());
578
579 Ok(orders)
580 }
581
582 pub async fn load_positions(
593 con: &ConnectionManager,
594 trader_key: &str,
595 encoding: SerializationEncoding,
596 ) -> anyhow::Result<AHashMap<PositionId, Position>> {
597 let mut positions = AHashMap::new();
598 let pattern = format!("{trader_key}{REDIS_DELIMITER}{POSITIONS}*");
599 log::debug!("Loading {pattern}");
600
601 let mut con = con.clone();
602 let keys = Self::scan_keys(&mut con, pattern).await?;
603
604 let futures: Vec<_> = keys
605 .iter()
606 .map(|key| {
607 let con = con.clone();
608 async move {
609 let position_id = if let Some(code) = key.as_str().rsplit(':').next() {
610 PositionId::from(code)
611 } else {
612 log::error!("Invalid key format: {key}");
613 return None;
614 };
615
616 match Self::load_position(&con, trader_key, &position_id, encoding).await {
617 Ok(Some(position)) => Some((position_id, position)),
618 Ok(None) => {
619 log::error!("Position not found: {position_id}");
620 None
621 }
622 Err(e) => {
623 log::error!("Failed to load position {position_id}: {e}");
624 None
625 }
626 }
627 }
628 })
629 .collect();
630
631 positions.extend(join_all(futures).await.into_iter().flatten());
633 log::debug!("Loaded {} position(s)", positions.len());
634
635 Ok(positions)
636 }
637
638 pub async fn load_index_order_position(
644 con: &ConnectionManager,
645 trader_key: &str,
646 ) -> anyhow::Result<AHashMap<ClientOrderId, PositionId>> {
647 let index = Self::read_index_hash(con, trader_key, INDEX_ORDER_POSITION).await?;
648 Ok(index
649 .into_iter()
650 .map(|(k, v)| {
651 (
652 ClientOrderId::from(k.as_str()),
653 PositionId::from(v.as_str()),
654 )
655 })
656 .collect())
657 }
658
659 pub async fn load_index_order_client(
665 con: &ConnectionManager,
666 trader_key: &str,
667 ) -> anyhow::Result<AHashMap<ClientOrderId, ClientId>> {
668 let index = Self::read_index_hash(con, trader_key, INDEX_ORDER_CLIENT).await?;
669 Ok(index
670 .into_iter()
671 .map(|(k, v)| (ClientOrderId::from(k.as_str()), ClientId::from(v.as_str())))
672 .collect())
673 }
674
675 async fn read_index_hash(
676 con: &ConnectionManager,
677 trader_key: &str,
678 key: &str,
679 ) -> anyhow::Result<HashMap<String, String>> {
680 let result = Self::read(con, trader_key, key).await?;
681 if result.is_empty() {
682 return Ok(HashMap::new());
683 }
684
685 serde_json::from_slice(&result[0])
686 .map_err(|e| anyhow::anyhow!("Failed to parse index hash '{key}': {e}"))
687 }
688
689 pub async fn load_custom_data(
699 con: &ConnectionManager,
700 trader_key: &str,
701 data_type: &DataType,
702 ) -> anyhow::Result<Vec<CustomData>> {
703 let pattern = format!("{trader_key}{REDIS_DELIMITER}{CUSTOM}*");
704 log::debug!("Loading custom data {pattern}");
705
706 let mut con = con.clone();
707 let keys = Self::scan_keys(&mut con, pattern).await?;
708
709 if keys.is_empty() {
710 return Ok(Vec::new());
711 }
712
713 let values = Self::read_bulk(&con, &keys).await?;
714 let request_type_name = data_type.type_name();
715 let request_short = request_type_name
716 .rsplit([':', '.'])
717 .next()
718 .unwrap_or(request_type_name);
719 let request_identifier = data_type.identifier().unwrap_or("");
720
721 let mut results = Vec::new();
722
723 for value_opt in values {
724 let Some(value_bytes) = value_opt else {
725 continue;
726 };
727 let custom = match CustomData::from_json_bytes(value_bytes.as_ref()) {
728 Ok(c) => c,
729 Err(e) => {
730 log::warn!("Failed to deserialize custom data from Redis: {e}");
731 continue;
732 }
733 };
734 let stored_type_name = custom.data_type.type_name();
735 let type_match =
736 stored_type_name == request_type_name || stored_type_name == request_short;
737 let identifier_match =
738 custom.data_type.identifier().unwrap_or("") == request_identifier;
739 let metadata_match = match (data_type.metadata(), custom.data_type.metadata()) {
740 (None, None) => true,
741 (Some(a), Some(b)) => serde_json::to_value(a).ok() == serde_json::to_value(b).ok(),
742 _ => false,
743 };
744
745 if type_match && identifier_match && metadata_match {
746 results.push(custom);
747 }
748 }
749
750 results.sort_by_key(HasTsInit::ts_init);
751 log::debug!("Loaded {} custom data item(s)", results.len());
752 Ok(results)
753 }
754
755 pub async fn load_currency(
761 con: &ConnectionManager,
762 trader_key: &str,
763 code: &Ustr,
764 encoding: SerializationEncoding,
765 ) -> anyhow::Result<Option<Currency>> {
766 let key = format!("{CURRENCIES}{REDIS_DELIMITER}{code}");
767 let result = Self::read(con, trader_key, &key).await?;
768
769 if result.is_empty() {
770 return Ok(None);
771 }
772
773 let currency = Self::deserialize_payload(encoding, &result[0])?;
774 Ok(currency)
775 }
776
777 pub async fn load_instrument(
783 con: &ConnectionManager,
784 trader_key: &str,
785 instrument_id: &InstrumentId,
786 encoding: SerializationEncoding,
787 ) -> anyhow::Result<Option<InstrumentAny>> {
788 let key = format!("{INSTRUMENTS}{REDIS_DELIMITER}{instrument_id}");
789 let result = Self::read(con, trader_key, &key).await?;
790 if result.is_empty() {
791 return Ok(None);
792 }
793
794 let instrument: InstrumentAny = Self::deserialize_payload(encoding, &result[0])?;
795 Ok(Some(instrument))
796 }
797
798 pub async fn load_synthetic(
804 con: &ConnectionManager,
805 trader_key: &str,
806 instrument_id: &InstrumentId,
807 encoding: SerializationEncoding,
808 ) -> anyhow::Result<Option<SyntheticInstrument>> {
809 let key = format!("{SYNTHETICS}{REDIS_DELIMITER}{instrument_id}");
810 let result = Self::read(con, trader_key, &key).await?;
811 if result.is_empty() {
812 return Ok(None);
813 }
814
815 let synthetic: SyntheticInstrument = Self::deserialize_payload(encoding, &result[0])?;
816 Ok(Some(synthetic))
817 }
818
819 pub async fn load_account(
825 con: &ConnectionManager,
826 trader_key: &str,
827 account_id: &AccountId,
828 encoding: SerializationEncoding,
829 ) -> anyhow::Result<Option<AccountAny>> {
830 let key = format!("{ACCOUNTS}{REDIS_DELIMITER}{account_id}");
831 let result = Self::read(con, trader_key, &key).await?;
832 if result.is_empty() {
833 return Ok(None);
834 }
835
836 let events: Vec<AccountState> = result
837 .iter()
838 .map(|payload| Self::deserialize_payload(encoding, payload))
839 .collect::<anyhow::Result<_>>()?;
840 let account = AccountAny::from_events(&events)?;
841 Ok(Some(account))
842 }
843
844 pub async fn load_order(
850 con: &ConnectionManager,
851 trader_key: &str,
852 client_order_id: &ClientOrderId,
853 encoding: SerializationEncoding,
854 ) -> anyhow::Result<Option<OrderAny>> {
855 let key = format!("{ORDERS}{REDIS_DELIMITER}{client_order_id}");
856 let result = Self::read(con, trader_key, &key).await?;
857 if result.is_empty() {
858 return Ok(None);
859 }
860
861 let events: Vec<OrderEventAny> = result
862 .iter()
863 .map(|payload| Self::deserialize_payload(encoding, payload))
864 .collect::<anyhow::Result<_>>()?;
865 let order = OrderAny::from_events(events)?;
866 Ok(Some(order))
867 }
868
869 pub async fn load_position(
875 con: &ConnectionManager,
876 trader_key: &str,
877 position_id: &PositionId,
878 encoding: SerializationEncoding,
879 ) -> anyhow::Result<Option<Position>> {
880 let snapshot_key =
881 format!("{SNAPSHOTS}{REDIS_DELIMITER}{POSITIONS}{REDIS_DELIMITER}{position_id}");
882 let snapshots = Self::read(con, trader_key, &snapshot_key).await?;
883 for payload in snapshots.iter().rev() {
884 let snapshot: PositionSnapshot = Self::deserialize_payload(encoding, payload)?;
885 if let Some(replay_state) = snapshot.replay_state {
886 return serde_json::from_value(replay_state)
887 .map(Some)
888 .map_err(|e| anyhow::anyhow!("Failed to decode position replay state: {e}"));
889 }
890 }
891
892 let key = format!("{POSITIONS}{REDIS_DELIMITER}{position_id}");
893 let result = Self::read(con, trader_key, &key).await?;
894 if result.is_empty() {
895 return Ok(None);
896 }
897
898 let fills: Vec<OrderFilled> = result
899 .iter()
900 .map(|payload| Self::deserialize_payload(encoding, payload))
901 .collect::<anyhow::Result<_>>()?;
902 let Some((first_fill, remaining_fills)) = fills.split_first() else {
903 return Ok(None);
904 };
905 let Some(instrument) =
906 Self::load_instrument(con, trader_key, &first_fill.instrument_id, encoding).await?
907 else {
908 log::error!(
909 "Instrument not found for position {position_id}: {}",
910 first_fill.instrument_id
911 );
912 return Ok(None);
913 };
914
915 let mut position = Position::new(&instrument, first_fill.clone());
916 for fill in remaining_fills {
917 if position.trade_ids().contains(&fill.trade_id) {
918 anyhow::bail!(
919 "Duplicate fill event for position {position_id}: {}",
920 fill.trade_id
921 );
922 }
923 position.apply(fill);
924 }
925
926 Ok(Some(position))
927 }
928
929 fn get_collection_key(key: &str) -> anyhow::Result<&str> {
930 key.split_once(REDIS_DELIMITER)
931 .map(|(collection, _)| collection)
932 .ok_or_else(|| {
933 anyhow::anyhow!("Invalid `key`, missing a '{REDIS_DELIMITER}' delimiter, was {key}")
934 })
935 }
936
937 async fn read_index(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
938 let index_key = get_index_key(key)?;
939 match index_key {
940 INDEX_ORDER_IDS
941 | INDEX_ORDERS
942 | INDEX_ORDERS_OPEN
943 | INDEX_ORDERS_CLOSED
944 | INDEX_ORDERS_EMULATED
945 | INDEX_ORDERS_INFLIGHT
946 | INDEX_POSITIONS
947 | INDEX_POSITIONS_OPEN
948 | INDEX_POSITIONS_CLOSED => Self::read_set(conn, key).await,
949 INDEX_ORDER_POSITION | INDEX_ORDER_CLIENT => Self::read_hset(conn, key).await,
950 _ => anyhow::bail!("Index unknown '{index_key}' on read"),
951 }
952 }
953
954 async fn read_string(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
955 let result: Vec<u8> = conn.get(key).await?;
956
957 if result.is_empty() {
958 Ok(vec![])
959 } else {
960 Ok(vec![Bytes::from(result)])
961 }
962 }
963
964 async fn read_set(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
965 let result: Vec<Bytes> = conn.smembers(key).await?;
966 Ok(result)
967 }
968
969 async fn read_hset(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
970 let result: HashMap<String, String> = conn.hgetall(key).await?;
971 let json = serde_json::to_string(&result)?;
972 Ok(vec![Bytes::from(json.into_bytes())])
973 }
974
975 async fn read_list(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
976 let result: Vec<Bytes> = conn.lrange(key, 0, -1).await?;
977 Ok(result)
978 }
979}
980
981fn parse_instrument_key(key: &str, prefix: &str) -> anyhow::Result<InstrumentId> {
982 let value = key
983 .strip_prefix(prefix)
984 .filter(|value| !value.is_empty())
985 .ok_or_else(|| anyhow::anyhow!("Invalid instrument key '{key}'"))?;
986 InstrumentId::from_str(value)
987 .map_err(|e| anyhow::anyhow!("Failed to parse instrument ID from key '{key}': {e}"))
988}
989
990fn is_timestamp_field(key: &str) -> bool {
991 let expire_match = key == "expire_time_ns";
992 let ts_match = key.starts_with("ts_");
993 expire_match || ts_match
994}
995
996fn convert_timestamps(value: &mut Value) {
997 match value {
998 Value::Object(map) => {
999 for (key, v) in map {
1000 if is_timestamp_field(key)
1001 && let Value::Number(n) = v
1002 && let Some(n) = n.as_u64()
1003 {
1004 let dt = Timestamp::from_nanosecond(i128::from(n))
1005 .expect("UnixNanos is within Jiff's timestamp range");
1006 *v = Value::String(format!("{dt:.9}"));
1007 }
1008 convert_timestamps(v);
1009 }
1010 }
1011 Value::Array(arr) => {
1012 for item in arr {
1013 convert_timestamps(item);
1014 }
1015 }
1016 _ => {}
1017 }
1018}
1019
1020fn convert_timestamp_strings(value: &mut Value) {
1021 match value {
1022 Value::Object(map) => {
1023 for (key, v) in map {
1024 if is_timestamp_field(key)
1025 && let Value::String(s) = v
1026 && let Ok(dt) = s.parse::<Timestamp>()
1027 {
1028 let nanos = u64::try_from(dt.as_nanosecond()).expect("Invalid timestamp");
1029 *v = Value::Number(nanos.into());
1030 }
1031 convert_timestamp_strings(v);
1032 }
1033 }
1034 Value::Array(arr) => {
1035 for item in arr {
1036 convert_timestamp_strings(item);
1037 }
1038 }
1039 _ => {}
1040 }
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use std::str::FromStr;
1046
1047 use nautilus_common::enums::SerializationEncoding;
1048 use nautilus_core::UnixNanos;
1049 use nautilus_model::{
1050 enums::{AccountType, CurrencyType},
1051 events::AccountState,
1052 identifiers::{AccountId, InstrumentId},
1053 types::{AccountBalance, Currency, Money},
1054 };
1055 use rstest::rstest;
1056 use serde::Deserialize;
1057
1058 use super::{DatabaseQueries, parse_instrument_key};
1059
1060 #[derive(Debug, Deserialize, PartialEq, Eq)]
1061 struct TimestampPayload {
1062 ts_event: UnixNanos,
1063 ts_init: UnixNanos,
1064 }
1065
1066 #[rstest]
1067 #[case(SerializationEncoding::Json)]
1068 #[case(SerializationEncoding::MsgPack)]
1069 fn test_deserialize_chrono_timestamp_payload(#[case] encoding: SerializationEncoding) {
1070 let json = include_bytes!("../../test_data/redis_cache_timestamp_chrono.json");
1071 let payload = match encoding {
1072 SerializationEncoding::Json => json.to_vec(),
1073 SerializationEncoding::MsgPack => {
1074 let value = serde_json::from_slice::<serde_json::Value>(json).unwrap();
1075 rmp_serde::to_vec(&value).unwrap()
1076 }
1077 _ => unreachable!(),
1078 };
1079
1080 let result =
1081 DatabaseQueries::deserialize_payload::<TimestampPayload>(encoding, &payload).unwrap();
1082
1083 assert_eq!(
1084 result,
1085 TimestampPayload {
1086 ts_event: UnixNanos::from(1_123_456_789),
1087 ts_init: UnixNanos::from(2_987_654_321),
1088 }
1089 );
1090 }
1091
1092 #[rstest]
1093 #[case(SerializationEncoding::Json)]
1094 #[case(SerializationEncoding::MsgPack)]
1095 fn test_wallet_account_state_round_trips_unregistered_currency(
1096 #[case] encoding: SerializationEncoding,
1097 ) {
1098 let currency = Currency::new(
1099 "ENG729C",
1100 6,
1101 0,
1102 "Cache round-trip token",
1103 CurrencyType::Crypto,
1104 );
1105 let total = Money::from_mantissa_exponent(123_456_789, -6, currency);
1106 let state = AccountState::new(
1107 AccountId::new("WALLET-CACHE-001"),
1108 AccountType::Wallet,
1109 vec![AccountBalance::new(total, Money::zero(currency), total)],
1110 vec![],
1111 true,
1112 nautilus_core::UUID4::new(),
1113 UnixNanos::from(1),
1114 UnixNanos::from(2),
1115 None,
1116 );
1117 assert!(Currency::try_from_str("ENG729C").is_none());
1118
1119 let payload = DatabaseQueries::serialize_payload(encoding, &state).unwrap();
1120 let restored: AccountState =
1121 DatabaseQueries::deserialize_payload(encoding, &payload).unwrap();
1122 let restored = restored.balances[0];
1123
1124 assert_eq!(restored.total.raw, total.raw);
1125 assert_eq!(restored.locked.raw, 0);
1126 assert_eq!(restored.free.raw, total.raw);
1127 assert_eq!(restored.currency.code, currency.code);
1128 assert_eq!(restored.currency.precision, currency.precision);
1129 assert_eq!(restored.currency.iso4217, currency.iso4217);
1130 assert_eq!(restored.currency.name, currency.name);
1131 assert_eq!(restored.currency.currency_type, currency.currency_type);
1132 assert!(Currency::try_from_str("ENG729C").is_none());
1133 }
1134
1135 #[rstest]
1136 #[case("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443.Arbitrum:UniswapV3")]
1137 #[case(concat!(
1138 "0xc9bc8043294146424a4e4607d8ad837d",
1139 "6a659142822bbaaabc83bb57e7447461.Arbitrum:UniswapV4",
1140 ))]
1141 fn test_parse_instrument_key_preserves_colons_in_venue(#[case] value: &str) {
1142 let prefix = "TRADER-001:instruments:";
1143 let key = format!("{prefix}{value}");
1144
1145 let result = parse_instrument_key(&key, prefix).unwrap();
1146
1147 assert_eq!(result, InstrumentId::from_str(value).unwrap());
1148 }
1149}