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::{
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/// Persisted representation of a [`Currency`].
45///
46/// A `Currency` serializes as its bare code, which only resolves in a process where that code is
47/// already registered, so a currency minted at runtime could not be restored. This record carries
48/// the same fields the Postgres `currency` table stores, keeping the two backends equivalent.
49#[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
85/// Outcome of decoding one persisted currency entry.
86enum CurrencyDecode {
87    Currency(Currency),
88    /// A record, or a payload that is not a bare code, that does not yield a valid currency.
89    InvalidRecord(anyhow::Error),
90    /// A bare code that is not registered, so the entry holds nothing recoverable.
91    Undecodable(anyhow::Error),
92}
93
94// Collection keys
95const 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
110// Index keys
111const 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    /// Serializes the given `payload` using the specified `encoding` to a byte vector.
128    ///
129    /// # Errors
130    ///
131    /// Returns an error if serialization to the chosen encoding fails.
132    pub fn serialize_payload<T: Serialize>(
133        encoding: SerializationEncoding,
134        payload: &T,
135    ) -> anyhow::Result<Vec<u8>> {
136        match encoding {
137            SerializationEncoding::MsgPack => {
138                let mut value = serde_json::to_value(payload)?;
139                convert_timestamps(&mut value);
140                rmp_serde::to_vec(&value)
141                    .map_err(|e| anyhow::anyhow!("Failed to serialize msgpack `payload`: {e}"))
142            }
143            SerializationEncoding::Json => {
144                let mut value = serde_json::to_value(payload)?;
145                convert_timestamps(&mut value);
146                serde_json::to_vec(&value)
147                    .map_err(|e| anyhow::anyhow!("Failed to serialize json `payload`: {e}"))
148            }
149            SerializationEncoding::Sbe => {
150                anyhow::bail!("SBE encoding is not supported for Redis cache payloads")
151            }
152        }
153    }
154
155    /// Deserializes the given byte slice `payload` into type `T` using the specified `encoding`.
156    ///
157    /// # Errors
158    ///
159    /// Returns an error if deserialization from the chosen encoding fails or converting to the target type fails.
160    pub fn deserialize_payload<T: DeserializeOwned>(
161        encoding: SerializationEncoding,
162        payload: &[u8],
163    ) -> anyhow::Result<T> {
164        let mut value = match encoding {
165            SerializationEncoding::MsgPack => rmp_serde::from_slice(payload)
166                .map_err(|e| anyhow::anyhow!("Failed to deserialize msgpack `payload`: {e}"))?,
167            SerializationEncoding::Json => serde_json::from_slice(payload)
168                .map_err(|e| anyhow::anyhow!("Failed to deserialize json `payload`: {e}"))?,
169            SerializationEncoding::Sbe => {
170                anyhow::bail!("SBE encoding is not supported for Redis cache payloads")
171            }
172        };
173
174        convert_timestamp_strings(&mut value);
175
176        serde_json::from_value(value)
177            .map_err(|e| anyhow::anyhow!("Failed to convert value to target type: {e}"))
178    }
179
180    /// Scans Redis for keys matching the given `pattern`.
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if the Redis scan operation fails.
185    pub async fn scan_keys(
186        con: &mut ConnectionManager,
187        pattern: String,
188    ) -> anyhow::Result<Vec<String>> {
189        let mut result = Vec::new();
190        let mut cursor = 0u64;
191
192        loop {
193            let scan_result: (u64, Vec<String>) = redis::cmd("SCAN")
194                .arg(cursor)
195                .arg("MATCH")
196                .arg(&pattern)
197                .arg("COUNT")
198                .arg(5000)
199                .query_async(con)
200                .await?;
201
202            let (new_cursor, keys) = scan_result;
203            result.extend(keys);
204
205            // If cursor is 0, we've completed the full scan
206            if new_cursor == 0 {
207                break;
208            }
209
210            cursor = new_cursor;
211        }
212
213        Ok(result)
214    }
215
216    /// Bulk reads multiple keys from Redis using MGET for efficiency.
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if the underlying Redis MGET operation fails.
221    pub async fn read_bulk(
222        con: &ConnectionManager,
223        keys: &[String],
224    ) -> anyhow::Result<Vec<Option<Bytes>>> {
225        if keys.is_empty() {
226            return Ok(vec![]);
227        }
228
229        let mut con = con.clone();
230
231        // Use MGET to fetch all keys in a single network operation
232        let results: Vec<Option<Vec<u8>>> =
233            redis::cmd("MGET").arg(keys).query_async(&mut con).await?;
234
235        // Convert Vec<u8> to Bytes
236        let bytes_results: Vec<Option<Bytes>> = results
237            .into_iter()
238            .map(|opt| opt.map(Bytes::from))
239            .collect();
240
241        Ok(bytes_results)
242    }
243
244    /// Bulk reads multiple keys from Redis using MGET, batched into chunks.
245    ///
246    /// Keys are batched into chunks of `batch_size` to avoid exceeding Redis
247    /// request size limits on some providers.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if `batch_size` is zero or if the underlying Redis MGET operation fails.
252    pub async fn read_bulk_batched(
253        con: &ConnectionManager,
254        keys: &[String],
255        batch_size: usize,
256    ) -> anyhow::Result<Vec<Option<Bytes>>> {
257        if batch_size == 0 {
258            anyhow::bail!("`batch_size` must be greater than zero");
259        }
260
261        if keys.is_empty() {
262            return Ok(vec![]);
263        }
264
265        let mut all_results: Vec<Option<Bytes>> = Vec::with_capacity(keys.len());
266
267        for chunk in keys.chunks(batch_size) {
268            let mut con = con.clone();
269
270            let results: Vec<Option<Vec<u8>>> =
271                redis::cmd("MGET").arg(chunk).query_async(&mut con).await?;
272
273            all_results.extend(results.into_iter().map(|opt| opt.map(Bytes::from)));
274        }
275
276        Ok(all_results)
277    }
278
279    /// Reads raw byte payloads for `key` under `trader_key` from Redis.
280    ///
281    /// # Errors
282    ///
283    /// Returns an error if the underlying Redis read operation fails or if the collection is unsupported.
284    pub async fn read(
285        con: &ConnectionManager,
286        trader_key: &str,
287        key: &str,
288    ) -> anyhow::Result<Vec<Bytes>> {
289        let collection = Self::get_collection_key(key)?;
290        let full_key = format!("{trader_key}{REDIS_DELIMITER}{key}");
291
292        let mut con = con.clone();
293
294        match collection {
295            INDEX => Self::read_index(&mut con, &full_key).await,
296            GENERAL | CURRENCIES | INSTRUMENTS | INSTRUMENT_CLOSES | SYNTHETICS | ACTORS
297            | STRATEGIES => Self::read_string(&mut con, &full_key).await,
298            ACCOUNTS | ORDERS | POSITIONS | SNAPSHOTS => Self::read_list(&mut con, &full_key).await,
299            _ => anyhow::bail!("Unsupported operation: `read` for collection '{collection}'"),
300        }
301    }
302
303    /// Loads all cache data (currencies, instruments, synthetics, accounts, orders, positions) for `trader_key`.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if loading any of the individual caches fails or combining data fails.
308    pub async fn load_all(
309        con: &ConnectionManager,
310        encoding: SerializationEncoding,
311        trader_key: &str,
312    ) -> anyhow::Result<CacheMap> {
313        // Currencies must be registered before the dependent payloads decode, because a `Money`
314        // or a `Currency` in them resolves its code through the global registry.
315        let mut currencies = Self::load_currencies(con, trader_key, encoding).await?;
316        register_loaded_currencies(&mut currencies)?;
317
318        let (instruments, instrument_closes, synthetics, accounts, orders, positions) =
319            tokio::try_join!(
320                Self::load_instruments(con, trader_key, encoding),
321                Self::load_instrument_closes(con, trader_key, encoding),
322                Self::load_synthetics(con, trader_key, encoding),
323                Self::load_accounts(con, trader_key, encoding),
324                Self::load_orders(con, trader_key, encoding),
325                Self::load_positions(con, trader_key, encoding)
326            )
327            .map_err(|e| anyhow::anyhow!("Error loading cache data: {e}"))?;
328
329        // For now, we don't load greeks and yield curves from the database
330        // This will be implemented in the future
331        let greeks = AHashMap::new();
332        let yield_curves = AHashMap::new();
333
334        Ok(CacheMap {
335            currencies,
336            instruments,
337            instrument_closes,
338            synthetics,
339            accounts,
340            orders,
341            positions,
342            greeks,
343            yield_curves,
344        })
345    }
346
347    /// Decodes a persisted currency, accepting both the record and a bare code.
348    ///
349    /// A bare-code entry holds only the code, which resolves if that code is registered; an
350    /// unregistered one is skipped by bulk loading, since the entry holds nothing recoverable. Anything
351    /// that is not a bare code is a record, and a record that does not decode or describes an
352    /// invalid currency is an error, since the intent is recoverable and dropping it would omit a
353    /// currency the dependent payloads need.
354    fn decode_currency(encoding: SerializationEncoding, value_bytes: &Bytes) -> CurrencyDecode {
355        match Self::deserialize_payload::<CurrencyRecord>(encoding, value_bytes) {
356            Ok(record) => match Currency::try_from(record) {
357                Ok(currency) => CurrencyDecode::Currency(currency),
358                Err(e) => CurrencyDecode::InvalidRecord(e),
359            },
360            Err(record_err) => match Self::deserialize_payload::<String>(encoding, value_bytes) {
361                Ok(code) => match Currency::try_from_str(&code) {
362                    Some(currency) => CurrencyDecode::Currency(currency),
363                    None => CurrencyDecode::Undecodable(anyhow::anyhow!(
364                        "bare currency code {code} is not registered"
365                    )),
366                },
367                Err(_) => CurrencyDecode::InvalidRecord(anyhow::anyhow!(
368                    "payload is not a bare code and does not decode as a currency record: {record_err}"
369                )),
370            },
371        }
372    }
373
374    /// Loads all currencies for `trader_key` using the specified `encoding`.
375    ///
376    /// # Errors
377    ///
378    /// Returns an error if scanning keys or reading currency data fails, or if a stored record
379    /// describes an invalid currency.
380    pub async fn load_currencies(
381        con: &ConnectionManager,
382        trader_key: &str,
383        encoding: SerializationEncoding,
384    ) -> anyhow::Result<AHashMap<Ustr, Currency>> {
385        let mut currencies = AHashMap::new();
386        let prefix = format!("{trader_key}{REDIS_DELIMITER}{CURRENCIES}{REDIS_DELIMITER}");
387        let pattern = format!("{prefix}*");
388        log::debug!("Loading {pattern}");
389
390        let mut con = con.clone();
391        let keys = Self::scan_keys(&mut con, pattern).await?;
392
393        if keys.is_empty() {
394            return Ok(currencies);
395        }
396
397        // Use bulk loading with MGET for efficiency
398        let bulk_values = Self::read_bulk(&con, &keys).await?;
399
400        // Process the bulk results
401        for (key, value_opt) in keys.iter().zip(bulk_values.iter()) {
402            // A code can contain the delimiter (Hyperliquid HIP-3 assets such as `xyz:TSLA`), so
403            // the code is everything after the collection prefix, not the last segment.
404            let currency_code = match key.as_str().strip_prefix(prefix.as_str()) {
405                Some(code) if !code.is_empty() => Ustr::from(code),
406                _ => {
407                    log::error!("Invalid key format: {key}");
408                    continue;
409                }
410            };
411
412            if let Some(value_bytes) = value_opt {
413                match Self::decode_currency(encoding, value_bytes) {
414                    CurrencyDecode::Currency(currency) => {
415                        // The key names the code the dependents resolve, so a record filed under
416                        // another code would register one currency and index another.
417                        if currency.code != currency_code {
418                            anyhow::bail!(
419                                "Currency record under key {currency_code} holds code {}",
420                                currency.code
421                            );
422                        }
423                        currencies.insert(currency_code, currency);
424                    }
425                    CurrencyDecode::InvalidRecord(e) => {
426                        anyhow::bail!("Invalid currency record for {currency_code}: {e}");
427                    }
428                    CurrencyDecode::Undecodable(e) => {
429                        log::error!(
430                            "Skipping currency {currency_code}: {e}; payloads denominated in it will not decode"
431                        );
432                    }
433                }
434            } else {
435                log::error!("Currency not found in Redis: {currency_code}");
436            }
437        }
438
439        log::debug!("Loaded {} currencies(s)", currencies.len());
440
441        Ok(currencies)
442    }
443
444    /// Loads all instruments for `trader_key` using the specified `encoding`.
445    ///
446    /// # Errors
447    ///
448    /// Returns an error if scanning keys or reading instrument data fails.
449    /// Loads all instruments for `trader_key` using the specified `encoding`.
450    ///
451    /// # Errors
452    ///
453    /// Returns an error if scanning keys or reading instrument data fails.
454    pub async fn load_instruments(
455        con: &ConnectionManager,
456        trader_key: &str,
457        encoding: SerializationEncoding,
458    ) -> anyhow::Result<AHashMap<InstrumentId, InstrumentAny>> {
459        let mut instruments = AHashMap::new();
460        let prefix = format!("{trader_key}{REDIS_DELIMITER}{INSTRUMENTS}{REDIS_DELIMITER}");
461        let pattern = format!("{prefix}*");
462        log::debug!("Loading {pattern}");
463
464        let mut con = con.clone();
465        let keys = Self::scan_keys(&mut con, pattern).await?;
466
467        let futures: Vec<_> = keys
468            .iter()
469            .map(|key| {
470                let con = con.clone();
471                let prefix = &prefix;
472                async move {
473                    let instrument_id = parse_instrument_key(key, prefix);
474
475                    let Ok(instrument_id) = instrument_id else {
476                        log::error!("Failed to parse InstrumentId from Redis key: {key}");
477                        return None;
478                    };
479
480                    match Self::load_instrument(&con, trader_key, &instrument_id, encoding).await {
481                        Ok(Some(instrument)) => Some((instrument_id, instrument)),
482                        Ok(None) => {
483                            log::error!("Instrument not found: {instrument_id}");
484                            None
485                        }
486                        Err(e) => {
487                            log::error!("Failed to load instrument {instrument_id}: {e}");
488                            None
489                        }
490                    }
491                }
492            })
493            .collect();
494
495        // Insert all Instrument_id (key) and Instrument (value) into the HashMap, filtering out None values.
496        instruments.extend(join_all(futures).await.into_iter().flatten());
497        log::debug!("Loaded {} instruments(s)", instruments.len());
498
499        Ok(instruments)
500    }
501
502    /// Loads all instrument closes for `trader_key`.
503    ///
504    /// Missing or invalid close data fails the load so recovery cannot silently omit a close.
505    ///
506    /// # Errors
507    ///
508    /// Returns an error if scanning, reading, parsing, or deserializing instrument closes fails.
509    pub async fn load_instrument_closes(
510        con: &ConnectionManager,
511        trader_key: &str,
512        encoding: SerializationEncoding,
513    ) -> anyhow::Result<AHashMap<InstrumentId, InstrumentClose>> {
514        let prefix = format!("{trader_key}{REDIS_DELIMITER}{INSTRUMENT_CLOSES}{REDIS_DELIMITER}");
515        let pattern = format!("{prefix}*");
516        log::debug!("Loading {pattern}");
517
518        let mut con = con.clone();
519        let keys = Self::scan_keys(&mut con, pattern).await?;
520        let values = Self::read_bulk(&con, &keys).await?;
521        let mut closes = AHashMap::with_capacity(keys.len());
522
523        for (key, value) in keys.into_iter().zip(values) {
524            let instrument_id = parse_instrument_key(&key, &prefix)?;
525            let value = value
526                .ok_or_else(|| anyhow::anyhow!("Instrument close not found in Redis: {key}"))?;
527            let close: InstrumentClose = Self::deserialize_payload(encoding, &value)?;
528            anyhow::ensure!(
529                close.instrument_id == instrument_id,
530                "Instrument close key ID {instrument_id} did not match payload ID {}",
531                close.instrument_id,
532            );
533            closes.insert(instrument_id, close);
534        }
535
536        log::debug!("Loaded {} instrument close(s)", closes.len());
537        Ok(closes)
538    }
539
540    /// Loads all synthetic instruments for `trader_key` using the specified `encoding`.
541    ///
542    /// # Errors
543    ///
544    /// Returns an error if scanning keys or reading synthetic instrument data fails.
545    pub async fn load_synthetics(
546        con: &ConnectionManager,
547        trader_key: &str,
548        encoding: SerializationEncoding,
549    ) -> anyhow::Result<AHashMap<InstrumentId, SyntheticInstrument>> {
550        let mut synthetics = AHashMap::new();
551        let pattern = format!("{trader_key}{REDIS_DELIMITER}{SYNTHETICS}*");
552        log::debug!("Loading {pattern}");
553
554        let mut con = con.clone();
555        let keys = Self::scan_keys(&mut con, pattern).await?;
556
557        let futures: Vec<_> = keys
558            .iter()
559            .map(|key| {
560                let con = con.clone();
561                async move {
562                    let instrument_id = key
563                        .as_str()
564                        .rsplit(':')
565                        .next()
566                        .ok_or_else(|| {
567                            log::error!("Invalid key format: {key}");
568                            "Invalid key format"
569                        })
570                        .and_then(|code| {
571                            InstrumentId::from_str(code).map_err(|e| {
572                                log::error!("Failed to parse InstrumentId for {key}: {e}");
573                                "Invalid instrument ID"
574                            })
575                        });
576
577                    let Ok(instrument_id) = instrument_id else {
578                        return None;
579                    };
580
581                    match Self::load_synthetic(&con, trader_key, &instrument_id, encoding).await {
582                        Ok(Some(synthetic)) => Some((instrument_id, synthetic)),
583                        Ok(None) => {
584                            log::error!("Synthetic not found: {instrument_id}");
585                            None
586                        }
587                        Err(e) => {
588                            log::error!("Failed to load synthetic {instrument_id}: {e}");
589                            None
590                        }
591                    }
592                }
593            })
594            .collect();
595
596        // Insert all Instrument_id (key) and Synthetic (value) into the HashMap, filtering out None values.
597        synthetics.extend(join_all(futures).await.into_iter().flatten());
598        log::debug!("Loaded {} synthetics(s)", synthetics.len());
599
600        Ok(synthetics)
601    }
602
603    /// Loads all accounts for `trader_key` using the specified `encoding`.
604    ///
605    /// # Errors
606    ///
607    /// Returns an error if scanning keys or reading account data fails.
608    /// Loads all accounts for `trader_key` using the specified `encoding`.
609    ///
610    /// # Errors
611    ///
612    /// Returns an error if scanning keys or reading account data fails.
613    pub async fn load_accounts(
614        con: &ConnectionManager,
615        trader_key: &str,
616        encoding: SerializationEncoding,
617    ) -> anyhow::Result<AHashMap<AccountId, AccountAny>> {
618        let mut accounts = AHashMap::new();
619        let pattern = format!("{trader_key}{REDIS_DELIMITER}{ACCOUNTS}*");
620        log::debug!("Loading {pattern}");
621
622        let mut con = con.clone();
623        let keys = Self::scan_keys(&mut con, pattern).await?;
624
625        let futures: Vec<_> = keys
626            .iter()
627            .map(|key| {
628                let con = con.clone();
629                async move {
630                    let account_id = if let Some(code) = key.as_str().rsplit(':').next() {
631                        AccountId::from(code)
632                    } else {
633                        log::error!("Invalid key format: {key}");
634                        return None;
635                    };
636
637                    match Self::load_account(&con, trader_key, &account_id, encoding).await {
638                        Ok(Some(account)) => Some((account_id, account)),
639                        Ok(None) => {
640                            log::error!("Account not found: {account_id}");
641                            None
642                        }
643                        Err(e) => {
644                            log::error!("Failed to load account {account_id}: {e}");
645                            None
646                        }
647                    }
648                }
649            })
650            .collect();
651
652        // Insert all Account_id (key) and Account (value) into the HashMap, filtering out None values.
653        accounts.extend(join_all(futures).await.into_iter().flatten());
654        log::debug!("Loaded {} accounts(s)", accounts.len());
655
656        Ok(accounts)
657    }
658
659    /// Loads all orders for `trader_key` using the specified `encoding`.
660    ///
661    /// # Errors
662    ///
663    /// Returns an error if scanning keys or reading order data fails.
664    /// Loads all orders for `trader_key` using the specified `encoding`.
665    ///
666    /// # Errors
667    ///
668    /// Returns an error if scanning keys or reading order data fails.
669    pub async fn load_orders(
670        con: &ConnectionManager,
671        trader_key: &str,
672        encoding: SerializationEncoding,
673    ) -> anyhow::Result<AHashMap<ClientOrderId, OrderAny>> {
674        let mut orders = AHashMap::new();
675        let pattern = format!("{trader_key}{REDIS_DELIMITER}{ORDERS}*");
676        log::debug!("Loading {pattern}");
677
678        let mut con = con.clone();
679        let keys = Self::scan_keys(&mut con, pattern).await?;
680
681        let futures: Vec<_> = keys
682            .iter()
683            .map(|key| {
684                let con = con.clone();
685                async move {
686                    let client_order_id = if let Some(code) = key.as_str().rsplit(':').next() {
687                        ClientOrderId::from(code)
688                    } else {
689                        log::error!("Invalid key format: {key}");
690                        return None;
691                    };
692
693                    match Self::load_order(&con, trader_key, &client_order_id, encoding).await {
694                        Ok(Some(order)) => Some((client_order_id, order)),
695                        Ok(None) => {
696                            log::error!("Order not found: {client_order_id}");
697                            None
698                        }
699                        Err(e) => {
700                            log::error!("Failed to load order {client_order_id}: {e}");
701                            None
702                        }
703                    }
704                }
705            })
706            .collect();
707
708        // Insert all Client-Order-Id (key) and Order (value) into the HashMap, filtering out None values.
709        orders.extend(join_all(futures).await.into_iter().flatten());
710        log::debug!("Loaded {} order(s)", orders.len());
711
712        Ok(orders)
713    }
714
715    /// Loads all positions for `trader_key` using the specified `encoding`.
716    ///
717    /// # Errors
718    ///
719    /// Returns an error if scanning keys or reading position data fails.
720    /// Loads all positions for `trader_key` using the specified `encoding`.
721    ///
722    /// # Errors
723    ///
724    /// Returns an error if scanning keys or reading position data fails.
725    pub async fn load_positions(
726        con: &ConnectionManager,
727        trader_key: &str,
728        encoding: SerializationEncoding,
729    ) -> anyhow::Result<AHashMap<PositionId, Position>> {
730        let mut positions = AHashMap::new();
731        let pattern = format!("{trader_key}{REDIS_DELIMITER}{POSITIONS}*");
732        log::debug!("Loading {pattern}");
733
734        let mut con = con.clone();
735        let keys = Self::scan_keys(&mut con, pattern).await?;
736
737        let futures: Vec<_> = keys
738            .iter()
739            .map(|key| {
740                let con = con.clone();
741                async move {
742                    let position_id = if let Some(code) = key.as_str().rsplit(':').next() {
743                        PositionId::from(code)
744                    } else {
745                        log::error!("Invalid key format: {key}");
746                        return None;
747                    };
748
749                    match Self::load_position(&con, trader_key, &position_id, encoding).await {
750                        Ok(Some(position)) => Some((position_id, position)),
751                        Ok(None) => {
752                            log::error!("Position not found: {position_id}");
753                            None
754                        }
755                        Err(e) => {
756                            log::error!("Failed to load position {position_id}: {e}");
757                            None
758                        }
759                    }
760                }
761            })
762            .collect();
763
764        // Insert all Position_id (key) and Position (value) into the HashMap, filtering out None values.
765        positions.extend(join_all(futures).await.into_iter().flatten());
766        log::debug!("Loaded {} position(s)", positions.len());
767
768        Ok(positions)
769    }
770
771    /// Loads the order ID to position ID index for `trader_key`.
772    ///
773    /// # Errors
774    ///
775    /// Returns an error if reading or parsing the index fails.
776    pub async fn load_index_order_position(
777        con: &ConnectionManager,
778        trader_key: &str,
779    ) -> anyhow::Result<AHashMap<ClientOrderId, PositionId>> {
780        let index = Self::read_index_hash(con, trader_key, INDEX_ORDER_POSITION).await?;
781        Ok(index
782            .into_iter()
783            .map(|(k, v)| {
784                (
785                    ClientOrderId::from(k.as_str()),
786                    PositionId::from(v.as_str()),
787                )
788            })
789            .collect())
790    }
791
792    /// Loads the order ID to execution client ID index for `trader_key`.
793    ///
794    /// # Errors
795    ///
796    /// Returns an error if reading or parsing the index fails.
797    pub async fn load_index_order_client(
798        con: &ConnectionManager,
799        trader_key: &str,
800    ) -> anyhow::Result<AHashMap<ClientOrderId, ClientId>> {
801        let index = Self::read_index_hash(con, trader_key, INDEX_ORDER_CLIENT).await?;
802        Ok(index
803            .into_iter()
804            .map(|(k, v)| (ClientOrderId::from(k.as_str()), ClientId::from(v.as_str())))
805            .collect())
806    }
807
808    async fn read_index_hash(
809        con: &ConnectionManager,
810        trader_key: &str,
811        key: &str,
812    ) -> anyhow::Result<HashMap<String, String>> {
813        let result = Self::read(con, trader_key, key).await?;
814        if result.is_empty() {
815            return Ok(HashMap::new());
816        }
817
818        serde_json::from_slice(&result[0])
819            .map_err(|e| anyhow::anyhow!("Failed to parse index hash '{key}': {e}"))
820    }
821
822    /// Loads all custom data for `trader_key` matching the given `data_type`.
823    ///
824    /// Keys are stored as `custom:<ts_init_020>:<uuid>`; value is full `CustomData` JSON.
825    /// Scans all custom keys, deserializes, filters by `type_name` (full or short), metadata,
826    /// and identifier to match SQL semantics, then sorts by `ts_init` ascending.
827    ///
828    /// # Errors
829    ///
830    /// Returns an error if scanning, bulk read, or deserialization fails.
831    pub async fn load_custom_data(
832        con: &ConnectionManager,
833        trader_key: &str,
834        data_type: &DataType,
835    ) -> anyhow::Result<Vec<CustomData>> {
836        let pattern = format!("{trader_key}{REDIS_DELIMITER}{CUSTOM}*");
837        log::debug!("Loading custom data {pattern}");
838
839        let mut con = con.clone();
840        let keys = Self::scan_keys(&mut con, pattern).await?;
841
842        if keys.is_empty() {
843            return Ok(Vec::new());
844        }
845
846        let values = Self::read_bulk(&con, &keys).await?;
847        let request_type_name = data_type.type_name();
848        let request_short = request_type_name
849            .rsplit([':', '.'])
850            .next()
851            .unwrap_or(request_type_name);
852        let request_identifier = data_type.identifier().unwrap_or("");
853
854        let mut results = Vec::new();
855
856        for value_opt in values {
857            let Some(value_bytes) = value_opt else {
858                continue;
859            };
860            let custom = match CustomData::from_json_bytes(value_bytes.as_ref()) {
861                Ok(c) => c,
862                Err(e) => {
863                    log::warn!("Failed to deserialize custom data from Redis: {e}");
864                    continue;
865                }
866            };
867            let stored_type_name = custom.data_type.type_name();
868            let type_match =
869                stored_type_name == request_type_name || stored_type_name == request_short;
870            let identifier_match =
871                custom.data_type.identifier().unwrap_or("") == request_identifier;
872            let metadata_match = match (data_type.metadata(), custom.data_type.metadata()) {
873                (None, None) => true,
874                (Some(a), Some(b)) => serde_json::to_value(a).ok() == serde_json::to_value(b).ok(),
875                _ => false,
876            };
877
878            if type_match && identifier_match && metadata_match {
879                results.push(custom);
880            }
881        }
882
883        results.sort_by_key(HasTsInit::ts_init);
884        log::debug!("Loaded {} custom data item(s)", results.len());
885        Ok(results)
886    }
887
888    /// Loads a single currency for `trader_key` and `code` using the specified `encoding`.
889    ///
890    /// # Errors
891    ///
892    /// Returns an error if the underlying read or deserialization fails.
893    pub async fn load_currency(
894        con: &ConnectionManager,
895        trader_key: &str,
896        code: &Ustr,
897        encoding: SerializationEncoding,
898    ) -> anyhow::Result<Option<Currency>> {
899        let key = format!("{CURRENCIES}{REDIS_DELIMITER}{code}");
900        let result = Self::read(con, trader_key, &key).await?;
901
902        if result.is_empty() {
903            return Ok(None);
904        }
905
906        match Self::decode_currency(encoding, &result[0]) {
907            CurrencyDecode::Currency(currency) => Ok(Some(currency)),
908            CurrencyDecode::InvalidRecord(e) | CurrencyDecode::Undecodable(e) => {
909                Err(e.context(format!("Failed to load currency {code}")))
910            }
911        }
912    }
913
914    /// Loads a single instrument for `trader_key` and `instrument_id` using the specified `encoding`.
915    ///
916    /// # Errors
917    ///
918    /// Returns an error if the underlying read or deserialization fails.
919    pub async fn load_instrument(
920        con: &ConnectionManager,
921        trader_key: &str,
922        instrument_id: &InstrumentId,
923        encoding: SerializationEncoding,
924    ) -> anyhow::Result<Option<InstrumentAny>> {
925        let key = format!("{INSTRUMENTS}{REDIS_DELIMITER}{instrument_id}");
926        let result = Self::read(con, trader_key, &key).await?;
927        if result.is_empty() {
928            return Ok(None);
929        }
930
931        let instrument: InstrumentAny = Self::deserialize_payload(encoding, &result[0])?;
932        Ok(Some(instrument))
933    }
934
935    /// Loads a single synthetic instrument for `trader_key` and `instrument_id` using the specified `encoding`.
936    ///
937    /// # Errors
938    ///
939    /// Returns an error if the underlying read or deserialization fails.
940    pub async fn load_synthetic(
941        con: &ConnectionManager,
942        trader_key: &str,
943        instrument_id: &InstrumentId,
944        encoding: SerializationEncoding,
945    ) -> anyhow::Result<Option<SyntheticInstrument>> {
946        let key = format!("{SYNTHETICS}{REDIS_DELIMITER}{instrument_id}");
947        let result = Self::read(con, trader_key, &key).await?;
948        if result.is_empty() {
949            return Ok(None);
950        }
951
952        let synthetic: SyntheticInstrument = Self::deserialize_payload(encoding, &result[0])?;
953        Ok(Some(synthetic))
954    }
955
956    /// Loads a single account for `trader_key` and `account_id` using the specified `encoding`.
957    ///
958    /// # Errors
959    ///
960    /// Returns an error if the underlying read or deserialization fails.
961    pub async fn load_account(
962        con: &ConnectionManager,
963        trader_key: &str,
964        account_id: &AccountId,
965        encoding: SerializationEncoding,
966    ) -> anyhow::Result<Option<AccountAny>> {
967        let key = format!("{ACCOUNTS}{REDIS_DELIMITER}{account_id}");
968        let result = Self::read(con, trader_key, &key).await?;
969        if result.is_empty() {
970            return Ok(None);
971        }
972
973        let events: Vec<AccountState> = result
974            .iter()
975            .map(|payload| Self::deserialize_payload(encoding, payload))
976            .collect::<anyhow::Result<_>>()?;
977        let account = AccountAny::from_events(&events)?;
978        Ok(Some(account))
979    }
980
981    /// Loads a single order for `trader_key` and `client_order_id` using the specified `encoding`.
982    ///
983    /// # Errors
984    ///
985    /// Returns an error if the underlying read or deserialization fails.
986    pub async fn load_order(
987        con: &ConnectionManager,
988        trader_key: &str,
989        client_order_id: &ClientOrderId,
990        encoding: SerializationEncoding,
991    ) -> anyhow::Result<Option<OrderAny>> {
992        let key = format!("{ORDERS}{REDIS_DELIMITER}{client_order_id}");
993        let result = Self::read(con, trader_key, &key).await?;
994        if result.is_empty() {
995            return Ok(None);
996        }
997
998        let events: Vec<OrderEventAny> = result
999            .iter()
1000            .map(|payload| Self::deserialize_payload(encoding, payload))
1001            .collect::<anyhow::Result<_>>()?;
1002        let order = OrderAny::from_events(events)?;
1003        Ok(Some(order))
1004    }
1005
1006    /// Loads a single position for `trader_key` and `position_id` using the specified `encoding`.
1007    ///
1008    /// # Errors
1009    ///
1010    /// Returns an error if the underlying read or deserialization fails.
1011    pub async fn load_position(
1012        con: &ConnectionManager,
1013        trader_key: &str,
1014        position_id: &PositionId,
1015        encoding: SerializationEncoding,
1016    ) -> anyhow::Result<Option<Position>> {
1017        let snapshot_key =
1018            format!("{SNAPSHOTS}{REDIS_DELIMITER}{POSITIONS}{REDIS_DELIMITER}{position_id}");
1019        let snapshots = Self::read(con, trader_key, &snapshot_key).await?;
1020        for payload in snapshots.iter().rev() {
1021            let snapshot: PositionSnapshot = Self::deserialize_payload(encoding, payload)?;
1022            if let Some(replay_state) = snapshot.replay_state {
1023                let position: Position = serde_json::from_value(replay_state)
1024                    .map_err(|e| anyhow::anyhow!("Failed to decode position replay state: {e}"))?;
1025                position.check_accounting()?;
1026                return Ok(Some(position));
1027            }
1028        }
1029
1030        let key = format!("{POSITIONS}{REDIS_DELIMITER}{position_id}");
1031        let result = Self::read(con, trader_key, &key).await?;
1032        if result.is_empty() {
1033            return Ok(None);
1034        }
1035
1036        let fills: Vec<OrderFilled> = result
1037            .iter()
1038            .map(|payload| Self::deserialize_payload(encoding, payload))
1039            .collect::<anyhow::Result<_>>()?;
1040        let Some((first_fill, remaining_fills)) = fills.split_first() else {
1041            return Ok(None);
1042        };
1043        let Some(instrument) =
1044            Self::load_instrument(con, trader_key, &first_fill.instrument_id, encoding).await?
1045        else {
1046            log::error!(
1047                "Instrument not found for position {position_id}: {}",
1048                first_fill.instrument_id
1049            );
1050            return Ok(None);
1051        };
1052
1053        let mut position = Position::new_checked(&instrument, first_fill.clone())?;
1054        for fill in remaining_fills {
1055            if position.trade_ids.contains(&fill.trade_id) {
1056                anyhow::bail!(
1057                    "Duplicate fill event for position {position_id}: {}",
1058                    fill.trade_id
1059                );
1060            }
1061
1062            position.try_apply(fill)?;
1063        }
1064
1065        Ok(Some(position))
1066    }
1067
1068    fn get_collection_key(key: &str) -> anyhow::Result<&str> {
1069        key.split_once(REDIS_DELIMITER)
1070            .map(|(collection, _)| collection)
1071            .ok_or_else(|| {
1072                anyhow::anyhow!("Invalid `key`, missing a '{REDIS_DELIMITER}' delimiter, was {key}")
1073            })
1074    }
1075
1076    async fn read_index(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1077        let index_key = get_index_key(key)?;
1078        match index_key {
1079            INDEX_ORDER_IDS
1080            | INDEX_ORDERS
1081            | INDEX_ORDERS_OPEN
1082            | INDEX_ORDERS_CLOSED
1083            | INDEX_ORDERS_EMULATED
1084            | INDEX_ORDERS_INFLIGHT
1085            | INDEX_POSITIONS
1086            | INDEX_POSITIONS_OPEN
1087            | INDEX_POSITIONS_CLOSED => Self::read_set(conn, key).await,
1088            INDEX_ORDER_POSITION | INDEX_ORDER_CLIENT => Self::read_hset(conn, key).await,
1089            _ => anyhow::bail!("Index unknown '{index_key}' on read"),
1090        }
1091    }
1092
1093    async fn read_string(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1094        let result: Vec<u8> = conn.get(key).await?;
1095
1096        if result.is_empty() {
1097            Ok(vec![])
1098        } else {
1099            Ok(vec![Bytes::from(result)])
1100        }
1101    }
1102
1103    async fn read_set(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1104        let result: Vec<Bytes> = conn.smembers(key).await?;
1105        Ok(result)
1106    }
1107
1108    async fn read_hset(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1109        let result: HashMap<String, String> = conn.hgetall(key).await?;
1110        let json = serde_json::to_string(&result)?;
1111        Ok(vec![Bytes::from(json.into_bytes())])
1112    }
1113
1114    async fn read_list(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
1115        let result: Vec<Bytes> = conn.lrange(key, 0, -1).await?;
1116        Ok(result)
1117    }
1118}
1119
1120fn parse_instrument_key(key: &str, prefix: &str) -> anyhow::Result<InstrumentId> {
1121    let value = key
1122        .strip_prefix(prefix)
1123        .filter(|value| !value.is_empty())
1124        .ok_or_else(|| anyhow::anyhow!("Invalid instrument key '{key}'"))?;
1125    InstrumentId::from_str(value)
1126        .map_err(|e| anyhow::anyhow!("Failed to parse instrument ID from key '{key}': {e}"))
1127}
1128
1129fn is_timestamp_field(key: &str) -> bool {
1130    let expire_match = key == "expire_time_ns";
1131    let ts_match = key.starts_with("ts_");
1132    expire_match || ts_match
1133}
1134
1135fn convert_timestamps(value: &mut Value) {
1136    match value {
1137        Value::Object(map) => {
1138            for (key, v) in map {
1139                if is_timestamp_field(key)
1140                    && let Value::Number(n) = v
1141                    && let Some(n) = n.as_u64()
1142                {
1143                    let dt = Timestamp::from_nanosecond(i128::from(n))
1144                        .expect("UnixNanos is within Jiff's timestamp range");
1145                    *v = Value::String(format!("{dt:.9}"));
1146                }
1147                convert_timestamps(v);
1148            }
1149        }
1150        Value::Array(arr) => {
1151            for item in arr {
1152                convert_timestamps(item);
1153            }
1154        }
1155        _ => {}
1156    }
1157}
1158
1159fn convert_timestamp_strings(value: &mut Value) {
1160    match value {
1161        Value::Object(map) => {
1162            for (key, v) in map {
1163                if is_timestamp_field(key)
1164                    && let Value::String(s) = v
1165                    && let Ok(dt) = s.parse::<Timestamp>()
1166                {
1167                    let nanos = u64::try_from(dt.as_nanosecond()).expect("Invalid timestamp");
1168                    *v = Value::Number(nanos.into());
1169                }
1170                convert_timestamp_strings(v);
1171            }
1172        }
1173        Value::Array(arr) => {
1174            for item in arr {
1175                convert_timestamp_strings(item);
1176            }
1177        }
1178        _ => {}
1179    }
1180}
1181
1182#[cfg(test)]
1183mod tests {
1184    use std::{hint::black_box, str::FromStr, time::Instant};
1185
1186    use bytes::Bytes;
1187    use nautilus_common::enums::SerializationEncoding;
1188    use nautilus_core::{UUID4, UnixNanos};
1189    use nautilus_model::{
1190        data::{Data, stubs::stub_custom_data},
1191        enums::{AccountType, CurrencyType},
1192        events::{AccountState, order::spec::OrderFilledSpec},
1193        identifiers::{AccountId, InstrumentId, PositionId, TradeId},
1194        instruments::{Instrument, InstrumentAny, stubs::currency_pair_btcusdt},
1195        position::Position,
1196        types::{AccountBalance, Currency, Money, Price, Quantity},
1197    };
1198    use rstest::rstest;
1199    use serde::{Deserialize, Serialize, de::DeserializeOwned};
1200
1201    use super::{CurrencyDecode, CurrencyRecord, DatabaseQueries, parse_instrument_key};
1202
1203    /// A currency the process never registered must survive the round trip.
1204    ///
1205    /// This is the whole point of the record: `Currency`'s own encoding is the bare code, which
1206    /// only resolves against the global registry.
1207    #[rstest]
1208    #[case(SerializationEncoding::Json)]
1209    #[case(SerializationEncoding::MsgPack)]
1210    fn test_currency_record_round_trips_an_unregistered_code(
1211        #[case] encoding: SerializationEncoding,
1212    ) {
1213        // A distinct code per encoding: `CURRENCY_MAP` is process-global and tests run in parallel.
1214        let code = match encoding {
1215            SerializationEncoding::MsgPack => "ZZREDISM",
1216            _ => "ZZREDISJ",
1217        };
1218        assert!(
1219            Currency::try_from_str(code).is_none(),
1220            "the fixture code must be unregistered for this test to mean anything"
1221        );
1222
1223        let currency = Currency::new(code, 3, 0, "Redis Fixture", CurrencyType::Crypto);
1224        let payload =
1225            DatabaseQueries::serialize_payload(encoding, &CurrencyRecord::from(&currency)).unwrap();
1226
1227        let CurrencyDecode::Currency(restored) =
1228            DatabaseQueries::decode_currency(encoding, &Bytes::from(payload))
1229        else {
1230            panic!("a valid record decodes to a currency");
1231        };
1232
1233        assert_eq!(restored.code.as_str(), code);
1234        assert_eq!(restored.precision, 3);
1235        assert_eq!(restored.iso4217, 0);
1236        assert_eq!(restored.name.as_str(), "Redis Fixture");
1237        assert_eq!(restored.currency_type, CurrencyType::Crypto);
1238    }
1239
1240    /// A record written before the full record existed must keep loading.
1241    #[rstest]
1242    #[case(SerializationEncoding::Json)]
1243    #[case(SerializationEncoding::MsgPack)]
1244    fn test_legacy_bare_code_still_loads(#[case] encoding: SerializationEncoding) {
1245        let payload = DatabaseQueries::serialize_payload(encoding, &Currency::USD()).unwrap();
1246
1247        let CurrencyDecode::Currency(restored) =
1248            DatabaseQueries::decode_currency(encoding, &Bytes::from(payload))
1249        else {
1250            panic!("a registered bare code decodes to a currency");
1251        };
1252
1253        assert_eq!(restored, Currency::USD());
1254    }
1255
1256    /// A record that decodes but fails validation is an error, not a skip.
1257    #[rstest]
1258    #[case(SerializationEncoding::Json)]
1259    #[case(SerializationEncoding::MsgPack)]
1260    fn test_invalid_record_is_classified_as_invalid(#[case] encoding: SerializationEncoding) {
1261        let record = CurrencyRecord {
1262            code: "ZZQ1".to_string(),
1263            precision: 20,
1264            iso4217: 0,
1265            name: "ZZQ1".to_string(),
1266            currency_type: CurrencyType::Crypto,
1267        };
1268        let bytes = Bytes::from(DatabaseQueries::serialize_payload(encoding, &record).unwrap());
1269
1270        assert!(matches!(
1271            DatabaseQueries::decode_currency(encoding, &bytes),
1272            CurrencyDecode::InvalidRecord(_)
1273        ));
1274    }
1275
1276    /// A record-shaped payload that does not decode as a record is an error, not a bare code.
1277    ///
1278    /// A record written by a build with a currency type this one lacks must fail the load rather
1279    /// than vanish, or the payloads denominated in it vanish with it.
1280    #[rstest]
1281    #[case(SerializationEncoding::Json)]
1282    #[case(SerializationEncoding::MsgPack)]
1283    fn test_record_with_an_unknown_currency_type_is_invalid(
1284        #[case] encoding: SerializationEncoding,
1285    ) {
1286        let record = serde_json::json!({
1287            "code": "ZZQ3",
1288            "precision": 6,
1289            "iso4217": 0,
1290            "name": "ZZQ3",
1291            "currency_type": "STABLECOIN",
1292        });
1293        let bytes = Bytes::from(DatabaseQueries::serialize_payload(encoding, &record).unwrap());
1294
1295        assert!(matches!(
1296            DatabaseQueries::decode_currency(encoding, &bytes),
1297            CurrencyDecode::InvalidRecord(_)
1298        ));
1299    }
1300
1301    /// An unregistered bare code is skipped by bulk loading: the entry holds nothing recoverable.
1302    #[rstest]
1303    #[case(SerializationEncoding::Json)]
1304    #[case(SerializationEncoding::MsgPack)]
1305    fn test_unknown_bare_code_is_classified_as_undecodable(
1306        #[case] encoding: SerializationEncoding,
1307    ) {
1308        let bytes = Bytes::from(DatabaseQueries::serialize_payload(encoding, &"ZZQ2").unwrap());
1309
1310        let CurrencyDecode::Undecodable(error) = DatabaseQueries::decode_currency(encoding, &bytes)
1311        else {
1312            panic!("an unregistered bare code is undecodable");
1313        };
1314        assert!(
1315            error.to_string().contains("ZZQ2"),
1316            "the error names the code so the operator can register it: {error}"
1317        );
1318    }
1319
1320    #[derive(Debug, Deserialize, PartialEq, Eq)]
1321    struct TimestampPayload {
1322        ts_event: UnixNanos,
1323        ts_init: UnixNanos,
1324    }
1325
1326    #[rstest]
1327    #[case(SerializationEncoding::Json)]
1328    #[case(SerializationEncoding::MsgPack)]
1329    fn test_deserialize_chrono_timestamp_payload(#[case] encoding: SerializationEncoding) {
1330        let json = include_bytes!("../../test_data/redis_cache_timestamp_chrono.json");
1331        let payload = match encoding {
1332            SerializationEncoding::Json => json.to_vec(),
1333            SerializationEncoding::MsgPack => {
1334                let value = serde_json::from_slice::<serde_json::Value>(json).unwrap();
1335                rmp_serde::to_vec(&value).unwrap()
1336            }
1337            SerializationEncoding::Sbe => unreachable!(),
1338        };
1339
1340        let result =
1341            DatabaseQueries::deserialize_payload::<TimestampPayload>(encoding, &payload).unwrap();
1342
1343        assert_eq!(
1344            result,
1345            TimestampPayload {
1346                ts_event: UnixNanos::from(1_123_456_789),
1347                ts_init: UnixNanos::from(2_987_654_321),
1348            }
1349        );
1350    }
1351
1352    #[rstest]
1353    #[case(SerializationEncoding::Json)]
1354    #[case(SerializationEncoding::MsgPack)]
1355    fn test_wallet_account_state_round_trips_unregistered_currency(
1356        #[case] encoding: SerializationEncoding,
1357    ) {
1358        let currency = Currency::new(
1359            "ENG729C",
1360            6,
1361            0,
1362            "Cache round-trip token",
1363            CurrencyType::Crypto,
1364        );
1365        let total = Money::from_mantissa_exponent(123_456_789, -6, currency);
1366        let state = AccountState::new(
1367            AccountId::new("WALLET-CACHE-001"),
1368            AccountType::Wallet,
1369            vec![AccountBalance::new(total, Money::zero(currency), total)],
1370            vec![],
1371            true,
1372            nautilus_core::UUID4::new(),
1373            UnixNanos::from(1),
1374            UnixNanos::from(2),
1375            None,
1376        );
1377        assert!(Currency::try_from_str("ENG729C").is_none());
1378
1379        let payload = DatabaseQueries::serialize_payload(encoding, &state).unwrap();
1380        let restored: AccountState =
1381            DatabaseQueries::deserialize_payload(encoding, &payload).unwrap();
1382        let restored = restored.balances[0];
1383
1384        assert_eq!(restored.total.raw(), total.raw());
1385        assert_eq!(restored.locked.raw(), 0);
1386        assert_eq!(restored.free.raw(), total.raw());
1387        assert_eq!(restored.currency.code, currency.code);
1388        assert_eq!(restored.currency.precision, currency.precision);
1389        assert_eq!(restored.currency.iso4217, currency.iso4217);
1390        assert_eq!(restored.currency.name, currency.name);
1391        assert_eq!(restored.currency.currency_type, currency.currency_type);
1392        assert!(Currency::try_from_str("ENG729C").is_none());
1393    }
1394
1395    #[rstest]
1396    #[ignore = "optimized Redis cache payload measurements without a Redis connection"]
1397    fn redis_payload_measurements() {
1398        let samples = std::env::var("NAUTILUS_BENCH_SAMPLES")
1399            .map_or(10_000, |value| value.parse::<usize>().unwrap());
1400        let trials = std::env::var("NAUTILUS_BENCH_TRIALS")
1401            .map_or(3, |value| value.parse::<usize>().unwrap());
1402
1403        for trial in 0..trials {
1404            let formats = [SerializationEncoding::Json, SerializationEncoding::MsgPack];
1405            for offset in 0..formats.len() {
1406                let encoding = formats[(offset + trial) % formats.len()];
1407
1408                let format = match encoding {
1409                    SerializationEncoding::Json => "json",
1410                    SerializationEncoding::MsgPack => "msgpack",
1411                    SerializationEncoding::Sbe => unreachable!(),
1412                };
1413
1414                if std::env::var("NAUTILUS_BENCH_FORMAT").is_ok_and(|selected| selected != format) {
1415                    continue;
1416                }
1417
1418                cache_cases(encoding, trial, samples);
1419            }
1420        }
1421    }
1422
1423    fn cache_cases(encoding: SerializationEncoding, trial: usize, samples: usize) {
1424        let mut state = AccountState::new(
1425            AccountId::from("SIM-1"),
1426            AccountType::Cash,
1427            vec![AccountBalance::new(
1428                Money::from("1000.75 USD"),
1429                Money::from("250.25 USD"),
1430                Money::from("750.50 USD"),
1431            )],
1432            vec![],
1433            true,
1434            UUID4::new(),
1435            UnixNanos::from(1_700_000_000_000_000_123),
1436            UnixNanos::from(1_700_000_000_000_000_456),
1437            Some(Currency::USD()),
1438        );
1439        let instrument = InstrumentAny::CurrencyPair(currency_pair_btcusdt());
1440        let custom = Data::Custom(stub_custom_data(77, -88, None, None));
1441        let mut fill = OrderFilledSpec::builder()
1442            .instrument_id(instrument.id())
1443            .position_id(PositionId::from("POSITION-1"))
1444            .last_qty(Quantity::from("2.750"))
1445            .last_px(Price::from("16384.00"))
1446            .currency(Currency::USDT())
1447            .commission(Money::from("0.73 USDT"))
1448            .ts_event(UnixNanos::from(1_700_000_000_000_000_789))
1449            .ts_init(UnixNanos::from(1_700_000_000_000_000_987))
1450            .build();
1451
1452        for populated in [false, true] {
1453            state.info = populated.then(|| {
1454                serde_json::from_str(include_str!(
1455                    "../../../common/test_data/message_params.json"
1456                ))
1457                .unwrap()
1458            });
1459
1460            let name = if populated {
1461                "account_state_nested"
1462            } else {
1463                "account_state_plain"
1464            };
1465
1466            measure_cache_payload(name, &state, encoding, trial, samples);
1467        }
1468
1469        measure_cache_payload("instrument", &instrument, encoding, trial, samples);
1470        measure_cache_payload("custom", &custom, encoding, trial, samples);
1471        fill.trade_id = TradeId::from("FILL-0");
1472        let mut position = Position::new(&instrument, fill.clone());
1473        for count in [1, 32, 256] {
1474            for index in position.events.len()..count {
1475                fill.event_id = UUID4::new();
1476                fill.trade_id = TradeId::new(format!("FILL-{index}"));
1477                position.apply(&fill);
1478            }
1479
1480            assert_eq!(position.events.len(), count);
1481            measure_cache_payload(
1482                &format!("position_{count}"),
1483                &position,
1484                encoding,
1485                trial,
1486                samples,
1487            );
1488        }
1489    }
1490
1491    #[allow(
1492        clippy::cast_possible_truncation,
1493        clippy::cast_precision_loss,
1494        reason = "nanosecond samples fit in u64 and timing means are approximate"
1495    )]
1496    fn measure_cache_payload<T: Serialize + DeserializeOwned>(
1497        name: &str,
1498        value: &T,
1499        encoding: SerializationEncoding,
1500        trial: usize,
1501        samples: usize,
1502    ) {
1503        if std::env::var("NAUTILUS_BENCH_CASE").is_ok_and(|selected| selected != name) {
1504            return;
1505        }
1506
1507        assert!(samples > 0);
1508        let payload = DatabaseQueries::serialize_payload(encoding, value).unwrap();
1509        let decoded: T = DatabaseQueries::deserialize_payload(encoding, &payload).unwrap();
1510        assert_eq!(
1511            serde_json::to_value(&decoded).unwrap(),
1512            serde_json::to_value(value).unwrap()
1513        );
1514
1515        let warmup = std::env::var("NAUTILUS_BENCH_WARMUP")
1516            .map_or(1_000, |value| value.parse::<usize>().unwrap());
1517
1518        for _ in 0..warmup {
1519            black_box(DatabaseQueries::serialize_payload(encoding, black_box(value)).unwrap());
1520            black_box(
1521                DatabaseQueries::deserialize_payload::<T>(encoding, black_box(&payload)).unwrap(),
1522            );
1523        }
1524
1525        if let Ok(operation) = std::env::var("NAUTILUS_BENCH_PROFILE") {
1526            for _ in 0..samples {
1527                match operation.as_str() {
1528                    "encode" => {
1529                        black_box(cache_encode(encoding, black_box(value)));
1530                    }
1531                    "decode" => {
1532                        black_box(cache_decode::<T>(encoding, black_box(&payload)));
1533                    }
1534                    _ => panic!("profile must be encode or decode"),
1535                }
1536            }
1537
1538            println!(
1539                "{}",
1540                serde_json::json!({"case": name, "operation": operation,
1541                "profile_calls": samples, "bytes": payload.len()})
1542            );
1543            return;
1544        }
1545
1546        let mut encoded = Vec::with_capacity(samples);
1547        let mut decoded = Vec::with_capacity(samples);
1548        for _ in 0..samples {
1549            let started = Instant::now();
1550            let bytes = cache_encode(encoding, black_box(value));
1551            encoded.push(started.elapsed().as_nanos() as u64);
1552            black_box(bytes);
1553            let started = Instant::now();
1554            let value = cache_decode::<T>(encoding, black_box(&payload));
1555            decoded.push(started.elapsed().as_nanos() as u64);
1556            black_box(value);
1557        }
1558
1559        for (operation, mut times) in [("encode", encoded), ("decode", decoded)] {
1560            let mean = times.iter().map(|value| *value as f64).sum::<f64>() / samples as f64;
1561            times.sort_unstable();
1562            let percentile = |n| times[(samples - 1) * n / 1_000];
1563            println!(
1564                "{}",
1565                serde_json::json!({
1566                    "case": name, "boundary": "redis_payload", "format": encoding,
1567                    "trial": trial, "operation": operation, "samples": samples, "bytes": payload.len(),
1568                    "mean_ns": mean, "p50_ns": percentile(500), "p95_ns": percentile(950),
1569                    "p99_ns": percentile(990), "p999_ns": percentile(999), "max_ns": times.last().unwrap(),
1570                })
1571            );
1572        }
1573    }
1574
1575    #[inline(never)]
1576    fn cache_encode<T: Serialize>(encoding: SerializationEncoding, value: &T) -> Vec<u8> {
1577        DatabaseQueries::serialize_payload(encoding, value).unwrap()
1578    }
1579
1580    #[inline(never)]
1581    fn cache_decode<T: DeserializeOwned>(encoding: SerializationEncoding, payload: &[u8]) -> T {
1582        DatabaseQueries::deserialize_payload(encoding, payload).unwrap()
1583    }
1584
1585    #[rstest]
1586    #[case("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443.Arbitrum:UniswapV3")]
1587    #[case(concat!(
1588        "0xc9bc8043294146424a4e4607d8ad837d",
1589        "6a659142822bbaaabc83bb57e7447461.Arbitrum:UniswapV4",
1590    ))]
1591    fn test_parse_instrument_key_preserves_colons_in_venue(#[case] value: &str) {
1592        let prefix = "TRADER-001:instruments:";
1593        let key = format!("{prefix}{value}");
1594
1595        let result = parse_instrument_key(&key, prefix).unwrap();
1596
1597        assert_eq!(result, InstrumentId::from_str(value).unwrap());
1598    }
1599}