1use crate::{
2 AccountEvent, AccountEventKind, AccountSnapshot, InstrumentAccountSnapshot,
3 InstrumentBalanceUpdate, IsolatedInstrumentState, UnindexedAccountEvent,
4 UnindexedAccountSnapshot,
5 balance::{AssetBalance, AssetBalanceUpdate},
6 error::{
7 ApiError, ClientError, KeyError, OrderError, UnindexedApiError, UnindexedClientError,
8 UnindexedOrderError,
9 },
10 map::ExecutionInstrumentMap,
11 order::{
12 Order, OrderEvent, OrderKey, OrderSnapshot, UnindexedOrderKey, UnindexedOrderSnapshot,
13 request::OrderResponseCancel,
14 state::{InactiveOrderState, OrderState, UnindexedOrderState},
15 },
16 trade::{AssetFees, Trade},
17};
18use derive_more::Constructor;
19use rustrade_instrument::{
20 asset::{AssetIndex, name::AssetNameExchange},
21 exchange::{ExchangeId, ExchangeIndex},
22 index::error::IndexError,
23 instrument::{InstrumentIndex, name::InstrumentNameExchange},
24};
25use rustrade_integration::{
26 collection::snapshot::Snapshot,
27 stream::ext::indexed::{IndexedStream, Indexer},
28};
29use std::sync::Arc;
30
31pub type IndexedAccountStream<St> = IndexedStream<St, AccountEventIndexer>;
32
33#[derive(Debug, Clone, Constructor)]
34pub struct AccountEventIndexer {
35 pub map: Arc<ExecutionInstrumentMap>,
36}
37
38impl Indexer for AccountEventIndexer {
39 type Unindexed = UnindexedAccountEvent;
40 type Indexed = AccountEvent;
41
42 fn index(&self, item: Self::Unindexed) -> Result<Self::Indexed, IndexError> {
43 self.account_event(item)
44 }
45}
46
47impl AccountEventIndexer {
48 pub fn account_event(&self, event: UnindexedAccountEvent) -> Result<AccountEvent, IndexError> {
49 let UnindexedAccountEvent { exchange, kind } = event;
50
51 let exchange = self.map.find_exchange_index(exchange)?;
52
53 let kind = match kind {
54 AccountEventKind::Snapshot(snapshot) => {
55 AccountEventKind::Snapshot(self.snapshot(snapshot)?)
56 }
57 AccountEventKind::BalanceSnapshot(snapshot) => {
58 AccountEventKind::BalanceSnapshot(self.asset_balance(snapshot.0).map(Snapshot)?)
59 }
60 AccountEventKind::BalanceStreamUpdate(snapshot) => {
61 AccountEventKind::BalanceStreamUpdate(
62 self.asset_balance_update(snapshot.0).map(Snapshot)?,
63 )
64 }
65 AccountEventKind::InstrumentBalanceUpdate(update) => {
66 AccountEventKind::InstrumentBalanceUpdate(self.instrument_balance_update(update)?)
67 }
68 AccountEventKind::OrderSnapshot(snapshot) => {
69 AccountEventKind::OrderSnapshot(self.order_snapshot(snapshot.0).map(Snapshot)?)
70 }
71 AccountEventKind::OrderCancelled(response) => {
72 AccountEventKind::OrderCancelled(self.order_response_cancel(response)?)
73 }
74 AccountEventKind::Trade(trade) => AccountEventKind::Trade(self.trade(trade)?),
75 AccountEventKind::StreamTerminated(reason) => {
77 AccountEventKind::StreamTerminated(reason)
78 }
79 };
80
81 Ok(AccountEvent { exchange, kind })
82 }
83
84 pub fn snapshot(
85 &self,
86 snapshot: UnindexedAccountSnapshot,
87 ) -> Result<AccountSnapshot, IndexError> {
88 let UnindexedAccountSnapshot {
89 exchange,
90 balances,
91 instruments,
92 } = snapshot;
93
94 let exchange = self.map.find_exchange_index(exchange)?;
95
96 let balances = balances
97 .into_iter()
98 .map(|balance| self.asset_balance(balance))
99 .collect::<Result<Vec<_>, _>>()?;
100
101 let instruments = instruments
102 .into_iter()
103 .map(|snapshot| {
104 let InstrumentAccountSnapshot {
105 instrument,
106 orders,
107 orders_complete,
108 position,
109 isolated,
110 } = snapshot;
111
112 let instrument = self.map.find_instrument_index(&instrument)?;
113
114 let orders = orders
115 .into_iter()
116 .map(|order| self.order_snapshot(order))
117 .collect::<Result<Vec<_>, _>>()?;
118
119 let isolated = isolated
123 .map(|state| self.isolated_instrument_state(state))
124 .transpose()?;
125
126 Ok(InstrumentAccountSnapshot {
127 instrument,
128 orders,
129 orders_complete,
130 position,
131 isolated,
132 })
133 })
134 .collect::<Result<Vec<_>, _>>()?;
135
136 Ok(AccountSnapshot {
137 exchange,
138 balances,
139 instruments,
140 })
141 }
142
143 pub fn asset_balance(
144 &self,
145 balance: AssetBalance<AssetNameExchange>,
146 ) -> Result<AssetBalance<AssetIndex>, IndexError> {
147 let AssetBalance {
148 asset,
149 balance,
150 time_exchange,
151 } = balance;
152 let asset = self.map.find_asset_index(&asset)?;
153
154 Ok(AssetBalance {
155 asset,
156 balance,
157 time_exchange,
158 })
159 }
160
161 pub fn asset_balance_update(
162 &self,
163 update: AssetBalanceUpdate<AssetNameExchange>,
164 ) -> Result<AssetBalanceUpdate<AssetIndex>, IndexError> {
165 let AssetBalanceUpdate {
166 asset,
167 update,
168 time_exchange,
169 } = update;
170 let asset = self.map.find_asset_index(&asset)?;
171
172 Ok(AssetBalanceUpdate {
173 asset,
174 update,
175 time_exchange,
176 })
177 }
178
179 pub fn isolated_instrument_state(
185 &self,
186 state: IsolatedInstrumentState<AssetNameExchange>,
187 ) -> Result<IsolatedInstrumentState<AssetIndex>, IndexError> {
188 let IsolatedInstrumentState { base, quote, risk } = state;
189
190 Ok(IsolatedInstrumentState {
191 base: self.asset_balance(base)?,
192 quote: self.asset_balance(quote)?,
193 risk,
194 })
195 }
196
197 pub fn instrument_balance_update(
202 &self,
203 update: InstrumentBalanceUpdate<AssetNameExchange, InstrumentNameExchange>,
204 ) -> Result<InstrumentBalanceUpdate, IndexError> {
205 let InstrumentBalanceUpdate {
206 instrument,
207 base,
208 quote,
209 } = update;
210
211 Ok(InstrumentBalanceUpdate {
212 instrument: self.map.find_instrument_index(&instrument)?,
213 base: self.asset_balance_update(base)?,
214 quote: self.asset_balance_update(quote)?,
215 })
216 }
217
218 pub fn order_snapshot(
219 &self,
220 order: UnindexedOrderSnapshot,
221 ) -> Result<OrderSnapshot, IndexError> {
222 let Order {
223 key,
224 side,
225 price,
226 quantity,
227 kind,
228 time_in_force,
229 state,
230 } = order;
231
232 let key = self.order_key(key)?;
233 let state = self.order_state(state)?;
234
235 Ok(Order {
236 key,
237 side,
238 price,
239 quantity,
240 kind,
241 time_in_force,
242 state,
243 })
244 }
245
246 pub fn order_response_cancel(
247 &self,
248 response: OrderResponseCancel<ExchangeId, AssetNameExchange, InstrumentNameExchange>,
249 ) -> Result<OrderResponseCancel, IndexError> {
250 let OrderResponseCancel { key, state } = response;
251
252 Ok(OrderResponseCancel {
253 key: self.order_key(key)?,
254 state: match state {
255 Ok(cancelled) => Ok(cancelled),
256 Err(error) => Err(self.order_error(error)?),
257 },
258 })
259 }
260
261 pub fn order_key(&self, key: UnindexedOrderKey) -> Result<OrderKey, IndexError> {
262 let UnindexedOrderKey {
263 exchange,
264 instrument,
265 strategy,
266 cid,
267 } = key;
268
269 Ok(OrderKey {
270 exchange: self.map.find_exchange_index(exchange)?,
271 instrument: self.map.find_instrument_index(&instrument)?,
272 strategy,
273 cid,
274 })
275 }
276
277 pub fn order_state(&self, state: UnindexedOrderState) -> Result<OrderState, IndexError> {
281 Ok(match state {
282 UnindexedOrderState::Active(active) => OrderState::Active(active),
283 UnindexedOrderState::Inactive(inactive) => match inactive {
284 InactiveOrderState::OpenFailed(failed) => match failed {
285 OrderError::Rejected(rejected) => {
286 OrderState::inactive(OrderError::Rejected(self.api_error(rejected)?))
287 }
288 OrderError::Connectivity(error) => {
289 OrderState::inactive(OrderError::Connectivity(error))
290 }
291 OrderError::UnsupportedOrderType(msg) => {
292 OrderState::inactive(OrderError::UnsupportedOrderType(msg))
293 }
294 },
295 InactiveOrderState::Cancelled(cancelled) => OrderState::inactive(cancelled),
296 InactiveOrderState::FullyFilled(filled) => OrderState::fully_filled(filled),
297 InactiveOrderState::Expired(expired) => OrderState::expired(expired),
298 },
299 })
300 }
301
302 pub fn api_error(&self, error: UnindexedApiError) -> Result<ApiError, IndexError> {
303 Ok(match error {
304 UnindexedApiError::RateLimit => ApiError::RateLimit,
305 UnindexedApiError::Unauthenticated(msg) => ApiError::Unauthenticated(msg),
306 UnindexedApiError::AssetInvalid(asset, value) => {
307 ApiError::AssetInvalid(self.map.find_asset_index(&asset)?, value)
308 }
309 UnindexedApiError::InstrumentInvalid(instrument, value) => {
310 ApiError::InstrumentInvalid(self.map.find_instrument_index(&instrument)?, value)
311 }
312 UnindexedApiError::BalanceInsufficient(asset, value) => {
313 ApiError::BalanceInsufficient(self.map.find_asset_index(&asset)?, value)
314 }
315 UnindexedApiError::OrderRejected(reason) => ApiError::OrderRejected(reason),
316 UnindexedApiError::OrderAlreadyCancelled => ApiError::OrderAlreadyCancelled,
317 UnindexedApiError::OrderAlreadyFullyFilled => ApiError::OrderAlreadyFullyFilled,
318 UnindexedApiError::OrderAlreadyExpired => ApiError::OrderAlreadyExpired,
319 })
320 }
321
322 pub fn order_request<Kind>(
323 &self,
324 order: &OrderEvent<Kind, ExchangeIndex, InstrumentIndex>,
325 ) -> Result<OrderEvent<Kind, ExchangeId, &InstrumentNameExchange>, KeyError>
326 where
327 Kind: Clone,
328 {
329 let OrderEvent {
330 key:
331 OrderKey {
332 exchange,
333 instrument,
334 strategy,
335 cid,
336 },
337 state,
338 } = order;
339
340 let exchange = self.map.find_exchange_id(*exchange)?;
341 let instrument = self.map.find_instrument_name_exchange(*instrument)?;
342
343 Ok(OrderEvent {
344 key: OrderKey {
345 exchange,
346 instrument,
347 strategy: strategy.clone(),
348 cid: cid.clone(),
349 },
350 state: state.clone(),
351 })
352 }
353
354 pub fn order_error(&self, error: UnindexedOrderError) -> Result<OrderError, IndexError> {
355 Ok(match error {
356 UnindexedOrderError::Connectivity(error) => OrderError::Connectivity(error),
357 UnindexedOrderError::Rejected(error) => OrderError::Rejected(self.api_error(error)?),
358 UnindexedOrderError::UnsupportedOrderType(msg) => OrderError::UnsupportedOrderType(msg),
359 })
360 }
361
362 pub fn client_error(&self, error: UnindexedClientError) -> Result<ClientError, IndexError> {
363 Ok(match error {
364 UnindexedClientError::Connectivity(error) => ClientError::Connectivity(error),
365 UnindexedClientError::Api(error) => ClientError::Api(self.api_error(error)?),
366 UnindexedClientError::TaskFailed(value) => ClientError::TaskFailed(value),
367 UnindexedClientError::Internal(value) => ClientError::Internal(value),
368 UnindexedClientError::Truncated { limit } => ClientError::Truncated { limit },
369 UnindexedClientError::TruncatedSnapshot { limit } => {
370 ClientError::TruncatedSnapshot { limit }
371 }
372 })
373 }
374
375 pub fn trade(
387 &self,
388 trade: Trade<AssetNameExchange, InstrumentNameExchange>,
389 ) -> Result<Trade<AssetIndex, InstrumentIndex>, IndexError> {
390 let Trade {
391 id,
392 order_id,
393 instrument,
394 strategy,
395 time_exchange,
396 side,
397 price: trade_price,
398 quantity,
399 order_filled_quantity,
400 fees,
401 } = trade;
402
403 let instrument_index = self.map.find_instrument_index(&instrument)?;
404 let fee_asset_index = self.map.find_asset_index(&fees.asset)?;
405
406 let fees_quote = self
408 .map
409 .instruments
410 .get_index(instrument_index.index())
411 .and_then(|instr| {
412 if fee_asset_index == instr.underlying.quote {
413 Some(fees.fees)
415 } else if fee_asset_index == instr.underlying.base {
416 Some(fees.fees * trade_price)
418 } else {
419 None
421 }
422 });
423
424 Ok(Trade {
425 id,
426 order_id,
427 instrument: instrument_index,
428 strategy,
429 time_exchange,
430 side,
431 price: trade_price,
432 quantity,
433 order_filled_quantity,
434 fees: AssetFees {
435 asset: fee_asset_index,
436 fees: fees.fees,
437 fees_quote,
438 },
439 })
440 }
441}
442
443#[cfg(test)]
444#[allow(clippy::unwrap_used)] mod tests {
446 use super::*;
447 use crate::{error::StreamTerminationReason, map::generate_execution_instrument_map};
448 use rustrade_instrument::{index::IndexedInstruments, test_utils};
449
450 fn binance_indexer() -> AccountEventIndexer {
451 let instruments = IndexedInstruments::new(vec![test_utils::instrument(
452 ExchangeId::BinanceSpot,
453 "BTC",
454 "USDT",
455 )]);
456 let map = generate_execution_instrument_map(&instruments, ExchangeId::BinanceSpot).unwrap();
457 AccountEventIndexer::new(Arc::new(map))
458 }
459
460 #[test]
463 fn account_event_passes_stream_terminated_through_verbatim() {
464 let indexer = binance_indexer();
465 let reason = StreamTerminationReason::ReconnectBudgetExhausted {
466 attempts: 3,
467 last_error: "socket reset".to_string(),
468 };
469
470 let indexed = indexer
471 .account_event(UnindexedAccountEvent::new(
472 ExchangeId::BinanceSpot,
473 AccountEventKind::StreamTerminated(reason.clone()),
474 ))
475 .unwrap();
476
477 assert_eq!(indexed.exchange, indexer.map.exchange.key);
478 assert!(
479 matches!(indexed.kind, AccountEventKind::StreamTerminated(r) if r == reason),
480 "expected StreamTerminated to pass through unchanged",
481 );
482 }
483}