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::u64("ts_event", false),
35 JsonFieldSpec::u64("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(
47 metadata: &HashMap<String, String>,
48 data: &[Self],
49 ) -> Result<RecordBatch, ArrowError> {
50 encode_batch("AccountState", metadata, data, ACCOUNT_STATE_FIELDS)
51 }
52
53 fn metadata(&self) -> HashMap<String, String> {
54 metadata_for_type("AccountState")
55 }
56}
57
58impl DecodeTypedFromRecordBatch for AccountState {
59 fn decode_typed_batch(
60 metadata: &HashMap<String, String>,
61 record_batch: RecordBatch,
62 ) -> Result<Vec<Self>, EncodingError> {
63 let fields = if record_batch.schema().index_of("info").is_ok() {
64 ACCOUNT_STATE_FIELDS
65 } else {
66 &ACCOUNT_STATE_FIELDS[..ACCOUNT_STATE_FIELDS.len() - 1]
67 };
68 decode_batch(metadata, &record_batch, fields, Some("AccountState"))
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use nautilus_core::Params;
75 use nautilus_model::events::account::stubs::cash_account_state;
76 use rstest::rstest;
77 use serde_json::json;
78
79 use super::*;
80
81 #[rstest]
82 fn test_account_state_round_trip(cash_account_state: AccountState) {
83 let mut info = Params::new();
84 info.insert(
85 "total_wallet_balance".to_string(),
86 json!("1525000.00000001"),
87 );
88 info.insert("can_trade".to_string(), json!(true));
89 let state = cash_account_state.with_info(Some(info));
90 let metadata = state.metadata();
91 let batch = AccountState::encode_batch(&metadata, std::slice::from_ref(&state)).unwrap();
92 let decoded = AccountState::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
93
94 assert_eq!(decoded.len(), 1);
95 assert_eq!(decoded[0].account_id, state.account_id);
96 assert_eq!(decoded[0].balances, state.balances);
97 assert_eq!(decoded[0].margins, state.margins);
98 assert_eq!(decoded[0].base_currency, state.base_currency);
99 assert_eq!(decoded[0].info, state.info);
100 }
101
102 #[rstest]
103 fn test_account_state_decodes_legacy_batch_without_info(cash_account_state: AccountState) {
104 let metadata = cash_account_state.metadata();
105 let legacy_fields = &ACCOUNT_STATE_FIELDS[..ACCOUNT_STATE_FIELDS.len() - 1];
106 let batch = encode_batch(
107 "AccountState",
108 &metadata,
109 std::slice::from_ref(&cash_account_state),
110 legacy_fields,
111 )
112 .unwrap();
113 let decoded = AccountState::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
114
115 assert_eq!(decoded.len(), 1);
116 assert!(decoded[0].info.is_none());
117 }
118}