Skip to main content

rustrade_execution/
indexer.rs

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::StreamError(msg) => AccountEventKind::StreamError(msg),
76        };
77
78        Ok(AccountEvent { exchange, kind })
79    }
80
81    pub fn snapshot(
82        &self,
83        snapshot: UnindexedAccountSnapshot,
84    ) -> Result<AccountSnapshot, IndexError> {
85        let UnindexedAccountSnapshot {
86            exchange,
87            balances,
88            instruments,
89        } = snapshot;
90
91        let exchange = self.map.find_exchange_index(exchange)?;
92
93        let balances = balances
94            .into_iter()
95            .map(|balance| self.asset_balance(balance))
96            .collect::<Result<Vec<_>, _>>()?;
97
98        let instruments = instruments
99            .into_iter()
100            .map(|snapshot| {
101                let InstrumentAccountSnapshot {
102                    instrument,
103                    orders,
104                    position,
105                    isolated,
106                } = snapshot;
107
108                let instrument = self.map.find_instrument_index(&instrument)?;
109
110                let orders = orders
111                    .into_iter()
112                    .map(|order| self.order_snapshot(order))
113                    .collect::<Result<Vec<_>, _>>()?;
114
115                // Per-pair isolated balances are generic over `AssetKey`, so (unlike `position`)
116                // their base/quote asset names must be mapped to indices. A pair whose asset is
117                // unregistered fails the snapshot index, matching top-level-balance behaviour.
118                let isolated = isolated
119                    .map(|state| self.isolated_instrument_state(state))
120                    .transpose()?;
121
122                Ok(InstrumentAccountSnapshot {
123                    instrument,
124                    orders,
125                    position,
126                    isolated,
127                })
128            })
129            .collect::<Result<Vec<_>, _>>()?;
130
131        Ok(AccountSnapshot {
132            exchange,
133            balances,
134            instruments,
135        })
136    }
137
138    pub fn asset_balance(
139        &self,
140        balance: AssetBalance<AssetNameExchange>,
141    ) -> Result<AssetBalance<AssetIndex>, IndexError> {
142        let AssetBalance {
143            asset,
144            balance,
145            time_exchange,
146        } = balance;
147        let asset = self.map.find_asset_index(&asset)?;
148
149        Ok(AssetBalance {
150            asset,
151            balance,
152            time_exchange,
153        })
154    }
155
156    pub fn asset_balance_update(
157        &self,
158        update: AssetBalanceUpdate<AssetNameExchange>,
159    ) -> Result<AssetBalanceUpdate<AssetIndex>, IndexError> {
160        let AssetBalanceUpdate {
161            asset,
162            update,
163            time_exchange,
164        } = update;
165        let asset = self.map.find_asset_index(&asset)?;
166
167        Ok(AssetBalanceUpdate {
168            asset,
169            update,
170            time_exchange,
171        })
172    }
173
174    /// Index the per-pair isolated state's `base`/`quote` asset names to indices.
175    ///
176    /// # Errors
177    /// Returns `IndexError` if either the base or quote asset is not registered in the map —
178    /// matching the fail-fast behaviour of top-level [`Self::asset_balance`].
179    pub fn isolated_instrument_state(
180        &self,
181        state: IsolatedInstrumentState<AssetNameExchange>,
182    ) -> Result<IsolatedInstrumentState<AssetIndex>, IndexError> {
183        let IsolatedInstrumentState { base, quote, risk } = state;
184
185        Ok(IsolatedInstrumentState {
186            base: self.asset_balance(base)?,
187            quote: self.asset_balance(quote)?,
188            risk,
189        })
190    }
191
192    /// Index an [`InstrumentBalanceUpdate`]'s instrument and `base`/`quote` asset keys.
193    ///
194    /// # Errors
195    /// Returns `IndexError` if the instrument or either asset is not registered in the map.
196    pub fn instrument_balance_update(
197        &self,
198        update: InstrumentBalanceUpdate<AssetNameExchange, InstrumentNameExchange>,
199    ) -> Result<InstrumentBalanceUpdate, IndexError> {
200        let InstrumentBalanceUpdate {
201            instrument,
202            base,
203            quote,
204        } = update;
205
206        Ok(InstrumentBalanceUpdate {
207            instrument: self.map.find_instrument_index(&instrument)?,
208            base: self.asset_balance_update(base)?,
209            quote: self.asset_balance_update(quote)?,
210        })
211    }
212
213    pub fn order_snapshot(
214        &self,
215        order: UnindexedOrderSnapshot,
216    ) -> Result<OrderSnapshot, IndexError> {
217        let Order {
218            key,
219            side,
220            price,
221            quantity,
222            kind,
223            time_in_force,
224            state,
225        } = order;
226
227        let key = self.order_key(key)?;
228        let state = self.order_state(state)?;
229
230        Ok(Order {
231            key,
232            side,
233            price,
234            quantity,
235            kind,
236            time_in_force,
237            state,
238        })
239    }
240
241    pub fn order_response_cancel(
242        &self,
243        response: OrderResponseCancel<ExchangeId, AssetNameExchange, InstrumentNameExchange>,
244    ) -> Result<OrderResponseCancel, IndexError> {
245        let OrderResponseCancel { key, state } = response;
246
247        Ok(OrderResponseCancel {
248            key: self.order_key(key)?,
249            state: match state {
250                Ok(cancelled) => Ok(cancelled),
251                Err(error) => Err(self.order_error(error)?),
252            },
253        })
254    }
255
256    pub fn order_key(&self, key: UnindexedOrderKey) -> Result<OrderKey, IndexError> {
257        let UnindexedOrderKey {
258            exchange,
259            instrument,
260            strategy,
261            cid,
262        } = key;
263
264        Ok(OrderKey {
265            exchange: self.map.find_exchange_index(exchange)?,
266            instrument: self.map.find_instrument_index(&instrument)?,
267            strategy,
268            cid,
269        })
270    }
271
272    /// Index an [`UnindexedOrderState`] to an [`OrderState`].
273    ///
274    /// Used by `ExecutionManager` to index `open_order` responses.
275    pub fn order_state(&self, state: UnindexedOrderState) -> Result<OrderState, IndexError> {
276        Ok(match state {
277            UnindexedOrderState::Active(active) => OrderState::Active(active),
278            UnindexedOrderState::Inactive(inactive) => match inactive {
279                InactiveOrderState::OpenFailed(failed) => match failed {
280                    OrderError::Rejected(rejected) => {
281                        OrderState::inactive(OrderError::Rejected(self.api_error(rejected)?))
282                    }
283                    OrderError::Connectivity(error) => {
284                        OrderState::inactive(OrderError::Connectivity(error))
285                    }
286                    OrderError::UnsupportedOrderType(msg) => {
287                        OrderState::inactive(OrderError::UnsupportedOrderType(msg))
288                    }
289                },
290                InactiveOrderState::Cancelled(cancelled) => OrderState::inactive(cancelled),
291                InactiveOrderState::FullyFilled(filled) => OrderState::fully_filled(filled),
292                InactiveOrderState::Expired(expired) => OrderState::expired(expired),
293            },
294        })
295    }
296
297    pub fn api_error(&self, error: UnindexedApiError) -> Result<ApiError, IndexError> {
298        Ok(match error {
299            UnindexedApiError::RateLimit => ApiError::RateLimit,
300            UnindexedApiError::Unauthenticated(msg) => ApiError::Unauthenticated(msg),
301            UnindexedApiError::AssetInvalid(asset, value) => {
302                ApiError::AssetInvalid(self.map.find_asset_index(&asset)?, value)
303            }
304            UnindexedApiError::InstrumentInvalid(instrument, value) => {
305                ApiError::InstrumentInvalid(self.map.find_instrument_index(&instrument)?, value)
306            }
307            UnindexedApiError::BalanceInsufficient(asset, value) => {
308                ApiError::BalanceInsufficient(self.map.find_asset_index(&asset)?, value)
309            }
310            UnindexedApiError::OrderRejected(reason) => ApiError::OrderRejected(reason),
311            UnindexedApiError::OrderAlreadyCancelled => ApiError::OrderAlreadyCancelled,
312            UnindexedApiError::OrderAlreadyFullyFilled => ApiError::OrderAlreadyFullyFilled,
313        })
314    }
315
316    pub fn order_request<Kind>(
317        &self,
318        order: &OrderEvent<Kind, ExchangeIndex, InstrumentIndex>,
319    ) -> Result<OrderEvent<Kind, ExchangeId, &InstrumentNameExchange>, KeyError>
320    where
321        Kind: Clone,
322    {
323        let OrderEvent {
324            key:
325                OrderKey {
326                    exchange,
327                    instrument,
328                    strategy,
329                    cid,
330                },
331            state,
332        } = order;
333
334        let exchange = self.map.find_exchange_id(*exchange)?;
335        let instrument = self.map.find_instrument_name_exchange(*instrument)?;
336
337        Ok(OrderEvent {
338            key: OrderKey {
339                exchange,
340                instrument,
341                strategy: strategy.clone(),
342                cid: cid.clone(),
343            },
344            state: state.clone(),
345        })
346    }
347
348    pub fn order_error(&self, error: UnindexedOrderError) -> Result<OrderError, IndexError> {
349        Ok(match error {
350            UnindexedOrderError::Connectivity(error) => OrderError::Connectivity(error),
351            UnindexedOrderError::Rejected(error) => OrderError::Rejected(self.api_error(error)?),
352            UnindexedOrderError::UnsupportedOrderType(msg) => OrderError::UnsupportedOrderType(msg),
353        })
354    }
355
356    pub fn client_error(&self, error: UnindexedClientError) -> Result<ClientError, IndexError> {
357        Ok(match error {
358            UnindexedClientError::Connectivity(error) => ClientError::Connectivity(error),
359            UnindexedClientError::Api(error) => ClientError::Api(self.api_error(error)?),
360            UnindexedClientError::TaskFailed(value) => ClientError::TaskFailed(value),
361            UnindexedClientError::Internal(value) => ClientError::Internal(value),
362            UnindexedClientError::Truncated { limit } => ClientError::Truncated { limit },
363            UnindexedClientError::TruncatedSnapshot { limit } => {
364                ClientError::TruncatedSnapshot { limit }
365            }
366        })
367    }
368
369    /// Index a trade, converting fee asset and computing `fees_quote`.
370    ///
371    /// Computes `fees_quote` based on fee asset relationship to instrument:
372    /// - Fee in quote asset: `fees_quote = Some(fees)`
373    /// - Fee in base asset: `fees_quote = Some(fees * price)`
374    /// - Fee in third-party asset (e.g., BNB): `fees_quote = None`
375    ///
376    /// # Errors
377    /// Returns `IndexError` if fee asset is not in the map. Some integrations use
378    /// "UNKNOWN" as a placeholder when fee data is unavailable (e.g., IBKR `fetch_trades`,
379    /// Binance when API omits `commission_asset`). These trades will fail indexing.
380    pub fn trade(
381        &self,
382        trade: Trade<AssetNameExchange, InstrumentNameExchange>,
383    ) -> Result<Trade<AssetIndex, InstrumentIndex>, IndexError> {
384        let Trade {
385            id,
386            order_id,
387            instrument,
388            strategy,
389            time_exchange,
390            side,
391            price: trade_price,
392            quantity,
393            fees,
394        } = trade;
395
396        let instrument_index = self.map.find_instrument_index(&instrument)?;
397        let fee_asset_index = self.map.find_asset_index(&fees.asset)?;
398
399        // Compute fees_quote based on fee asset relationship to instrument
400        let fees_quote = self
401            .map
402            .instruments
403            .get_index(instrument_index.index())
404            .and_then(|instr| {
405                if fee_asset_index == instr.underlying.quote {
406                    // Fee is in quote asset — no conversion needed
407                    Some(fees.fees)
408                } else if fee_asset_index == instr.underlying.base {
409                    // Fee is in base asset — convert using trade price
410                    Some(fees.fees * trade_price)
411                } else {
412                    // Fee is in third-party asset (e.g., BNB) — needs external price
413                    None
414                }
415            });
416
417        Ok(Trade {
418            id,
419            order_id,
420            instrument: instrument_index,
421            strategy,
422            time_exchange,
423            side,
424            price: trade_price,
425            quantity,
426            fees: AssetFees {
427                asset: fee_asset_index,
428                fees: fees.fees,
429                fees_quote,
430            },
431        })
432    }
433}