nautilus_serialization/arrow/
account_state.rs1use std::collections::HashMap;
17
18use arrow::{datatypes::Schema, error::ArrowError, record_batch::RecordBatch};
19use nautilus_model::events::AccountState;
20
21use super::{
22 ArrowSchemaProvider, DecodeTypedFromRecordBatch, EncodeToRecordBatch, EncodingError,
23 json::{JsonFieldSpec, decode_batch, encode_batch, metadata_for_type, schema_for_type},
24};
25
26const ACCOUNT_STATE_FIELDS: &[JsonFieldSpec] = &[
27 JsonFieldSpec::utf8("account_id", false),
28 JsonFieldSpec::utf8("account_type", false),
29 JsonFieldSpec::utf8("base_currency", true),
30 JsonFieldSpec::utf8_json("balances", false),
31 JsonFieldSpec::utf8_json("margins", false),
32 JsonFieldSpec::boolean("is_reported", false),
33 JsonFieldSpec::utf8("event_id", false),
34 JsonFieldSpec::timestamp("ts_event", false),
35 JsonFieldSpec::timestamp("ts_init", false),
36 JsonFieldSpec::utf8_json("info", true),
37];
38
39impl ArrowSchemaProvider for AccountState {
40 fn get_schema(metadata: Option<HashMap<String, String>>) -> Schema {
41 schema_for_type("AccountState", metadata, ACCOUNT_STATE_FIELDS)
42 }
43}
44
45impl EncodeToRecordBatch for AccountState {
46 fn encode_batch<T>(
47 metadata: &HashMap<String, String>,
48 data: &[T],
49 ) -> Result<RecordBatch, ArrowError>
50 where
51 T: std::borrow::Borrow<Self>,
52 {
53 encode_batch(
54 "AccountState",
55 metadata,
56 data.iter().map(std::borrow::Borrow::borrow),
57 ACCOUNT_STATE_FIELDS,
58 )
59 }
60
61 fn metadata(&self) -> HashMap<String, String> {
62 metadata_for_type("AccountState")
63 }
64}
65
66impl DecodeTypedFromRecordBatch for AccountState {
67 fn decode_typed_batch(
68 metadata: &HashMap<String, String>,
69 record_batch: RecordBatch,
70 ) -> Result<Vec<Self>, EncodingError> {
71 let fields = if record_batch.schema().index_of("info").is_ok() {
72 ACCOUNT_STATE_FIELDS
73 } else {
74 &ACCOUNT_STATE_FIELDS[..ACCOUNT_STATE_FIELDS.len() - 1]
75 };
76 decode_batch(metadata, &record_batch, fields, Some("AccountState"))
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use nautilus_core::Params;
83 use nautilus_model::{
84 events::account::stubs::cash_account_state,
85 types::{AccountBalance, Money},
86 };
87 use rstest::rstest;
88 use serde_json::json;
89
90 use super::*;
91
92 #[rstest]
93 fn test_account_state_round_trip(cash_account_state: AccountState) {
94 let info = Params::new(
95 [
96 (
97 "total_wallet_balance".to_string(),
98 json!("1525000.00000001"),
99 ),
100 ("can_trade".to_string(), json!(true)),
101 ]
102 .into(),
103 );
104 let state = cash_account_state.with_info(Some(info));
105 let metadata = state.metadata();
106 let batch = AccountState::encode_batch(&metadata, std::slice::from_ref(&state)).unwrap();
107 let decoded = AccountState::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
108
109 assert_eq!(decoded.len(), 1);
110 assert_eq!(decoded[0].account_id, state.account_id);
111 assert_eq!(decoded[0].balances, state.balances);
112 assert_eq!(decoded[0].margins, state.margins);
113 assert_eq!(decoded[0].base_currency, state.base_currency);
114 assert_eq!(decoded[0].info, state.info);
115 }
116
117 #[rstest]
118 fn test_account_state_exact_usd_round_trip(mut cash_account_state: AccountState) {
119 cash_account_state.balances = vec![AccountBalance::new(
120 Money::from_str_exact("1.0001 USD").unwrap(),
121 Money::from_str_exact("0.000001 USD").unwrap(),
122 Money::from_str_exact("1.000099 USD").unwrap(),
123 )];
124 let metadata = cash_account_state.metadata();
125 let batch =
126 AccountState::encode_batch(&metadata, std::slice::from_ref(&cash_account_state))
127 .unwrap();
128 let decoded = AccountState::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
129
130 assert_eq!(decoded.len(), 1);
131 assert_eq!(decoded[0].balances, cash_account_state.balances);
132 assert_eq!(
133 serde_json::to_value(&decoded[0]).unwrap(),
134 serde_json::to_value(&cash_account_state).unwrap(),
135 );
136 }
137
138 #[rstest]
139 fn test_account_state_decodes_legacy_batch_without_info(cash_account_state: AccountState) {
140 let metadata = cash_account_state.metadata();
141 let legacy_fields = &ACCOUNT_STATE_FIELDS[..ACCOUNT_STATE_FIELDS.len() - 1];
142 let batch = encode_batch(
143 "AccountState",
144 &metadata,
145 std::slice::from_ref(&cash_account_state),
146 legacy_fields,
147 )
148 .unwrap();
149 let decoded = AccountState::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
150
151 assert_eq!(decoded.len(), 1);
152 assert!(decoded[0].info.is_none());
153 }
154}