Skip to main content

nautilus_infrastructure/redis/
queries.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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
40// Collection keys
41const 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
55// Index keys
56const 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    /// Serializes the given `payload` using the specified `encoding` to a byte vector.
73    ///
74    /// # Errors
75    ///
76    /// Returns an error if serialization to the chosen encoding fails.
77    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    /// Deserializes the given byte slice `payload` into type `T` using the specified `encoding`.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if deserialization from the chosen encoding fails or converting to the target type fails.
108    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    /// Scans Redis for keys matching the given `pattern`.
132    ///
133    /// # Errors
134    ///
135    /// Returns an error if the Redis scan operation fails.
136    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 cursor is 0, we've completed the full scan
157            if new_cursor == 0 {
158                break;
159            }
160
161            cursor = new_cursor;
162        }
163
164        Ok(result)
165    }
166
167    /// Bulk reads multiple keys from Redis using MGET for efficiency.
168    ///
169    /// # Errors
170    ///
171    /// Returns an error if the underlying Redis MGET operation fails.
172    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        // Use MGET to fetch all keys in a single network operation
183        let results: Vec<Option<Vec<u8>>> =
184            redis::cmd("MGET").arg(keys).query_async(&mut con).await?;
185
186        // Convert Vec<u8> to Bytes
187        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    /// Bulk reads multiple keys from Redis using MGET, batched into chunks.
196    ///
197    /// Keys are batched into chunks of `batch_size` to avoid exceeding Redis
198    /// request size limits on some providers.
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if `batch_size` is zero or if the underlying Redis MGET operation fails.
203    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    /// Reads raw byte payloads for `key` under `trader_key` from Redis.
231    ///
232    /// # Errors
233    ///
234    /// Returns an error if the underlying Redis read operation fails or if the collection is unsupported.
235    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    /// Loads all cache data (currencies, instruments, synthetics, accounts, orders, positions) for `trader_key`.
256    ///
257    /// # Errors
258    ///
259    /// Returns an error if loading any of the individual caches fails or combining data fails.
260    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        // For now, we don't load greeks and yield curves from the database
276        // This will be implemented in the future
277        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    /// Loads all currencies for `trader_key` using the specified `encoding`.
293    ///
294    /// # Errors
295    ///
296    /// Returns an error if scanning keys or reading currency data fails.
297    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        // Use bulk loading with MGET for efficiency
314        let bulk_values = Self::read_bulk(&con, &keys).await?;
315
316        // Process the bulk results
317        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    /// Loads all instruments for `trader_key` using the specified `encoding`.
345    ///
346    /// # Errors
347    ///
348    /// Returns an error if scanning keys or reading instrument data fails.
349    /// Loads all instruments for `trader_key` using the specified `encoding`.
350    ///
351    /// # Errors
352    ///
353    /// Returns an error if scanning keys or reading instrument data fails.
354    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        // Insert all Instrument_id (key) and Instrument (value) into the HashMap, filtering out None values.
396        instruments.extend(join_all(futures).await.into_iter().flatten());
397        log::debug!("Loaded {} instruments(s)", instruments.len());
398
399        Ok(instruments)
400    }
401
402    /// Loads all synthetic instruments for `trader_key` using the specified `encoding`.
403    ///
404    /// # Errors
405    ///
406    /// Returns an error if scanning keys or reading synthetic instrument data fails.
407    /// Loads all synthetic instruments for `trader_key` using the specified `encoding`.
408    ///
409    /// # Errors
410    ///
411    /// Returns an error if scanning keys or reading synthetic instrument data fails.
412    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        // Insert all Instrument_id (key) and Synthetic (value) into the HashMap, filtering out None values.
464        synthetics.extend(join_all(futures).await.into_iter().flatten());
465        log::debug!("Loaded {} synthetics(s)", synthetics.len());
466
467        Ok(synthetics)
468    }
469
470    /// Loads all accounts for `trader_key` using the specified `encoding`.
471    ///
472    /// # Errors
473    ///
474    /// Returns an error if scanning keys or reading account data fails.
475    /// Loads all accounts for `trader_key` using the specified `encoding`.
476    ///
477    /// # Errors
478    ///
479    /// Returns an error if scanning keys or reading account data fails.
480    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        // Insert all Account_id (key) and Account (value) into the HashMap, filtering out None values.
520        accounts.extend(join_all(futures).await.into_iter().flatten());
521        log::debug!("Loaded {} accounts(s)", accounts.len());
522
523        Ok(accounts)
524    }
525
526    /// Loads all orders for `trader_key` using the specified `encoding`.
527    ///
528    /// # Errors
529    ///
530    /// Returns an error if scanning keys or reading order data fails.
531    /// Loads all orders for `trader_key` using the specified `encoding`.
532    ///
533    /// # Errors
534    ///
535    /// Returns an error if scanning keys or reading order data fails.
536    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        // Insert all Client-Order-Id (key) and Order (value) into the HashMap, filtering out None values.
576        orders.extend(join_all(futures).await.into_iter().flatten());
577        log::debug!("Loaded {} order(s)", orders.len());
578
579        Ok(orders)
580    }
581
582    /// Loads all positions for `trader_key` using the specified `encoding`.
583    ///
584    /// # Errors
585    ///
586    /// Returns an error if scanning keys or reading position data fails.
587    /// Loads all positions for `trader_key` using the specified `encoding`.
588    ///
589    /// # Errors
590    ///
591    /// Returns an error if scanning keys or reading position data fails.
592    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        // Insert all Position_id (key) and Position (value) into the HashMap, filtering out None values.
632        positions.extend(join_all(futures).await.into_iter().flatten());
633        log::debug!("Loaded {} position(s)", positions.len());
634
635        Ok(positions)
636    }
637
638    /// Loads the order ID to position ID index for `trader_key`.
639    ///
640    /// # Errors
641    ///
642    /// Returns an error if reading or parsing the index fails.
643    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    /// Loads the order ID to execution client ID index for `trader_key`.
660    ///
661    /// # Errors
662    ///
663    /// Returns an error if reading or parsing the index fails.
664    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    /// Loads all custom data for `trader_key` matching the given `data_type`.
690    ///
691    /// Keys are stored as `custom:<ts_init_020>:<uuid>`; value is full `CustomData` JSON.
692    /// Scans all custom keys, deserializes, filters by `type_name` (full or short), metadata,
693    /// and identifier to match SQL semantics, then sorts by `ts_init` ascending.
694    ///
695    /// # Errors
696    ///
697    /// Returns an error if scanning, bulk read, or deserialization fails.
698    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    /// Loads a single currency for `trader_key` and `code` using the specified `encoding`.
756    ///
757    /// # Errors
758    ///
759    /// Returns an error if the underlying read or deserialization fails.
760    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    /// Loads a single instrument for `trader_key` and `instrument_id` using the specified `encoding`.
778    ///
779    /// # Errors
780    ///
781    /// Returns an error if the underlying read or deserialization fails.
782    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    /// Loads a single synthetic instrument for `trader_key` and `instrument_id` using the specified `encoding`.
799    ///
800    /// # Errors
801    ///
802    /// Returns an error if the underlying read or deserialization fails.
803    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    /// Loads a single account for `trader_key` and `account_id` using the specified `encoding`.
820    ///
821    /// # Errors
822    ///
823    /// Returns an error if the underlying read or deserialization fails.
824    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    /// Loads a single order for `trader_key` and `client_order_id` using the specified `encoding`.
845    ///
846    /// # Errors
847    ///
848    /// Returns an error if the underlying read or deserialization fails.
849    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    /// Loads a single position for `trader_key` and `position_id` using the specified `encoding`.
870    ///
871    /// # Errors
872    ///
873    /// Returns an error if the underlying read or deserialization fails.
874    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}