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 UnindexedApiError::RequestRejected(reason) => ApiError::RequestRejected(reason),
320 })
321 }
322
323 pub fn order_request<Kind>(
324 &self,
325 order: &OrderEvent<Kind, ExchangeIndex, InstrumentIndex>,
326 ) -> Result<OrderEvent<Kind, ExchangeId, &InstrumentNameExchange>, KeyError>
327 where
328 Kind: Clone,
329 {
330 let OrderEvent {
331 key:
332 OrderKey {
333 exchange,
334 instrument,
335 strategy,
336 cid,
337 },
338 state,
339 } = order;
340
341 let exchange = self.map.find_exchange_id(*exchange)?;
342 let instrument = self.map.find_instrument_name_exchange(*instrument)?;
343
344 Ok(OrderEvent {
345 key: OrderKey {
346 exchange,
347 instrument,
348 strategy: strategy.clone(),
349 cid: cid.clone(),
350 },
351 state: state.clone(),
352 })
353 }
354
355 pub fn order_error(&self, error: UnindexedOrderError) -> Result<OrderError, IndexError> {
356 Ok(match error {
357 UnindexedOrderError::Connectivity(error) => OrderError::Connectivity(error),
358 UnindexedOrderError::Rejected(error) => OrderError::Rejected(self.api_error(error)?),
359 UnindexedOrderError::UnsupportedOrderType(msg) => OrderError::UnsupportedOrderType(msg),
360 })
361 }
362
363 pub fn client_error(&self, error: UnindexedClientError) -> Result<ClientError, IndexError> {
364 Ok(match error {
365 UnindexedClientError::Connectivity(error) => ClientError::Connectivity(error),
366 UnindexedClientError::Api(error) => ClientError::Api(self.api_error(error)?),
367 UnindexedClientError::TaskFailed(value) => ClientError::TaskFailed(value),
368 UnindexedClientError::Internal(value) => ClientError::Internal(value),
369 UnindexedClientError::Truncated { limit } => ClientError::Truncated { limit },
370 UnindexedClientError::TruncatedSnapshot { limit } => {
371 ClientError::TruncatedSnapshot { limit }
372 }
373 })
374 }
375
376 pub fn trade(
388 &self,
389 trade: Trade<AssetNameExchange, InstrumentNameExchange>,
390 ) -> Result<Trade<AssetIndex, InstrumentIndex>, IndexError> {
391 let Trade {
392 id,
393 order_id,
394 instrument,
395 strategy,
396 time_exchange,
397 side,
398 price: trade_price,
399 quantity,
400 order_filled_quantity,
401 fees,
402 } = trade;
403
404 let instrument_index = self.map.find_instrument_index(&instrument)?;
405 let fee_asset_index = self.map.find_asset_index(&fees.asset)?;
406
407 let fees_quote = self
409 .map
410 .instruments
411 .get_index(instrument_index.index())
412 .and_then(|instr| {
413 if fee_asset_index == instr.underlying.quote {
414 Some(fees.fees)
416 } else if fee_asset_index == instr.underlying.base {
417 Some(fees.fees * trade_price)
419 } else {
420 None
422 }
423 });
424
425 Ok(Trade {
426 id,
427 order_id,
428 instrument: instrument_index,
429 strategy,
430 time_exchange,
431 side,
432 price: trade_price,
433 quantity,
434 order_filled_quantity,
435 fees: AssetFees {
436 asset: fee_asset_index,
437 fees: fees.fees,
438 fees_quote,
439 },
440 })
441 }
442}
443
444#[cfg(test)]
445#[allow(clippy::unwrap_used)] mod tests {
447 use super::*;
448 use crate::{error::StreamTerminationReason, map::generate_execution_instrument_map};
449 use rustrade_instrument::{index::IndexedInstruments, test_utils};
450
451 fn binance_indexer() -> AccountEventIndexer {
452 let instruments = IndexedInstruments::new(vec![test_utils::instrument(
453 ExchangeId::BinanceSpot,
454 "BTC",
455 "USDT",
456 )]);
457 let map = generate_execution_instrument_map(&instruments, ExchangeId::BinanceSpot).unwrap();
458 AccountEventIndexer::new(Arc::new(map))
459 }
460
461 #[test]
464 fn account_event_passes_stream_terminated_through_verbatim() {
465 let indexer = binance_indexer();
466 let reason = StreamTerminationReason::ReconnectBudgetExhausted {
467 attempts: 3,
468 last_error: "socket reset".to_string(),
469 };
470
471 let indexed = indexer
472 .account_event(UnindexedAccountEvent::new(
473 ExchangeId::BinanceSpot,
474 AccountEventKind::StreamTerminated(reason.clone()),
475 ))
476 .unwrap();
477
478 assert_eq!(indexed.exchange, indexer.map.exchange.key);
479 assert!(
480 matches!(indexed.kind, AccountEventKind::StreamTerminated(r) if r == reason),
481 "expected StreamTerminated to pass through unchanged",
482 );
483 }
484}