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 position,
108 isolated,
109 } = snapshot;
110
111 let instrument = self.map.find_instrument_index(&instrument)?;
112
113 let orders = orders
114 .into_iter()
115 .map(|order| self.order_snapshot(order))
116 .collect::<Result<Vec<_>, _>>()?;
117
118 let isolated = isolated
122 .map(|state| self.isolated_instrument_state(state))
123 .transpose()?;
124
125 Ok(InstrumentAccountSnapshot {
126 instrument,
127 orders,
128 position,
129 isolated,
130 })
131 })
132 .collect::<Result<Vec<_>, _>>()?;
133
134 Ok(AccountSnapshot {
135 exchange,
136 balances,
137 instruments,
138 })
139 }
140
141 pub fn asset_balance(
142 &self,
143 balance: AssetBalance<AssetNameExchange>,
144 ) -> Result<AssetBalance<AssetIndex>, IndexError> {
145 let AssetBalance {
146 asset,
147 balance,
148 time_exchange,
149 } = balance;
150 let asset = self.map.find_asset_index(&asset)?;
151
152 Ok(AssetBalance {
153 asset,
154 balance,
155 time_exchange,
156 })
157 }
158
159 pub fn asset_balance_update(
160 &self,
161 update: AssetBalanceUpdate<AssetNameExchange>,
162 ) -> Result<AssetBalanceUpdate<AssetIndex>, IndexError> {
163 let AssetBalanceUpdate {
164 asset,
165 update,
166 time_exchange,
167 } = update;
168 let asset = self.map.find_asset_index(&asset)?;
169
170 Ok(AssetBalanceUpdate {
171 asset,
172 update,
173 time_exchange,
174 })
175 }
176
177 pub fn isolated_instrument_state(
183 &self,
184 state: IsolatedInstrumentState<AssetNameExchange>,
185 ) -> Result<IsolatedInstrumentState<AssetIndex>, IndexError> {
186 let IsolatedInstrumentState { base, quote, risk } = state;
187
188 Ok(IsolatedInstrumentState {
189 base: self.asset_balance(base)?,
190 quote: self.asset_balance(quote)?,
191 risk,
192 })
193 }
194
195 pub fn instrument_balance_update(
200 &self,
201 update: InstrumentBalanceUpdate<AssetNameExchange, InstrumentNameExchange>,
202 ) -> Result<InstrumentBalanceUpdate, IndexError> {
203 let InstrumentBalanceUpdate {
204 instrument,
205 base,
206 quote,
207 } = update;
208
209 Ok(InstrumentBalanceUpdate {
210 instrument: self.map.find_instrument_index(&instrument)?,
211 base: self.asset_balance_update(base)?,
212 quote: self.asset_balance_update(quote)?,
213 })
214 }
215
216 pub fn order_snapshot(
217 &self,
218 order: UnindexedOrderSnapshot,
219 ) -> Result<OrderSnapshot, IndexError> {
220 let Order {
221 key,
222 side,
223 price,
224 quantity,
225 kind,
226 time_in_force,
227 state,
228 } = order;
229
230 let key = self.order_key(key)?;
231 let state = self.order_state(state)?;
232
233 Ok(Order {
234 key,
235 side,
236 price,
237 quantity,
238 kind,
239 time_in_force,
240 state,
241 })
242 }
243
244 pub fn order_response_cancel(
245 &self,
246 response: OrderResponseCancel<ExchangeId, AssetNameExchange, InstrumentNameExchange>,
247 ) -> Result<OrderResponseCancel, IndexError> {
248 let OrderResponseCancel { key, state } = response;
249
250 Ok(OrderResponseCancel {
251 key: self.order_key(key)?,
252 state: match state {
253 Ok(cancelled) => Ok(cancelled),
254 Err(error) => Err(self.order_error(error)?),
255 },
256 })
257 }
258
259 pub fn order_key(&self, key: UnindexedOrderKey) -> Result<OrderKey, IndexError> {
260 let UnindexedOrderKey {
261 exchange,
262 instrument,
263 strategy,
264 cid,
265 } = key;
266
267 Ok(OrderKey {
268 exchange: self.map.find_exchange_index(exchange)?,
269 instrument: self.map.find_instrument_index(&instrument)?,
270 strategy,
271 cid,
272 })
273 }
274
275 pub fn order_state(&self, state: UnindexedOrderState) -> Result<OrderState, IndexError> {
279 Ok(match state {
280 UnindexedOrderState::Active(active) => OrderState::Active(active),
281 UnindexedOrderState::Inactive(inactive) => match inactive {
282 InactiveOrderState::OpenFailed(failed) => match failed {
283 OrderError::Rejected(rejected) => {
284 OrderState::inactive(OrderError::Rejected(self.api_error(rejected)?))
285 }
286 OrderError::Connectivity(error) => {
287 OrderState::inactive(OrderError::Connectivity(error))
288 }
289 OrderError::UnsupportedOrderType(msg) => {
290 OrderState::inactive(OrderError::UnsupportedOrderType(msg))
291 }
292 },
293 InactiveOrderState::Cancelled(cancelled) => OrderState::inactive(cancelled),
294 InactiveOrderState::FullyFilled(filled) => OrderState::fully_filled(filled),
295 InactiveOrderState::Expired(expired) => OrderState::expired(expired),
296 },
297 })
298 }
299
300 pub fn api_error(&self, error: UnindexedApiError) -> Result<ApiError, IndexError> {
301 Ok(match error {
302 UnindexedApiError::RateLimit => ApiError::RateLimit,
303 UnindexedApiError::Unauthenticated(msg) => ApiError::Unauthenticated(msg),
304 UnindexedApiError::AssetInvalid(asset, value) => {
305 ApiError::AssetInvalid(self.map.find_asset_index(&asset)?, value)
306 }
307 UnindexedApiError::InstrumentInvalid(instrument, value) => {
308 ApiError::InstrumentInvalid(self.map.find_instrument_index(&instrument)?, value)
309 }
310 UnindexedApiError::BalanceInsufficient(asset, value) => {
311 ApiError::BalanceInsufficient(self.map.find_asset_index(&asset)?, value)
312 }
313 UnindexedApiError::OrderRejected(reason) => ApiError::OrderRejected(reason),
314 UnindexedApiError::OrderAlreadyCancelled => ApiError::OrderAlreadyCancelled,
315 UnindexedApiError::OrderAlreadyFullyFilled => ApiError::OrderAlreadyFullyFilled,
316 })
317 }
318
319 pub fn order_request<Kind>(
320 &self,
321 order: &OrderEvent<Kind, ExchangeIndex, InstrumentIndex>,
322 ) -> Result<OrderEvent<Kind, ExchangeId, &InstrumentNameExchange>, KeyError>
323 where
324 Kind: Clone,
325 {
326 let OrderEvent {
327 key:
328 OrderKey {
329 exchange,
330 instrument,
331 strategy,
332 cid,
333 },
334 state,
335 } = order;
336
337 let exchange = self.map.find_exchange_id(*exchange)?;
338 let instrument = self.map.find_instrument_name_exchange(*instrument)?;
339
340 Ok(OrderEvent {
341 key: OrderKey {
342 exchange,
343 instrument,
344 strategy: strategy.clone(),
345 cid: cid.clone(),
346 },
347 state: state.clone(),
348 })
349 }
350
351 pub fn order_error(&self, error: UnindexedOrderError) -> Result<OrderError, IndexError> {
352 Ok(match error {
353 UnindexedOrderError::Connectivity(error) => OrderError::Connectivity(error),
354 UnindexedOrderError::Rejected(error) => OrderError::Rejected(self.api_error(error)?),
355 UnindexedOrderError::UnsupportedOrderType(msg) => OrderError::UnsupportedOrderType(msg),
356 })
357 }
358
359 pub fn client_error(&self, error: UnindexedClientError) -> Result<ClientError, IndexError> {
360 Ok(match error {
361 UnindexedClientError::Connectivity(error) => ClientError::Connectivity(error),
362 UnindexedClientError::Api(error) => ClientError::Api(self.api_error(error)?),
363 UnindexedClientError::TaskFailed(value) => ClientError::TaskFailed(value),
364 UnindexedClientError::Internal(value) => ClientError::Internal(value),
365 UnindexedClientError::Truncated { limit } => ClientError::Truncated { limit },
366 UnindexedClientError::TruncatedSnapshot { limit } => {
367 ClientError::TruncatedSnapshot { limit }
368 }
369 })
370 }
371
372 pub fn trade(
384 &self,
385 trade: Trade<AssetNameExchange, InstrumentNameExchange>,
386 ) -> Result<Trade<AssetIndex, InstrumentIndex>, IndexError> {
387 let Trade {
388 id,
389 order_id,
390 instrument,
391 strategy,
392 time_exchange,
393 side,
394 price: trade_price,
395 quantity,
396 fees,
397 } = trade;
398
399 let instrument_index = self.map.find_instrument_index(&instrument)?;
400 let fee_asset_index = self.map.find_asset_index(&fees.asset)?;
401
402 let fees_quote = self
404 .map
405 .instruments
406 .get_index(instrument_index.index())
407 .and_then(|instr| {
408 if fee_asset_index == instr.underlying.quote {
409 Some(fees.fees)
411 } else if fee_asset_index == instr.underlying.base {
412 Some(fees.fees * trade_price)
414 } else {
415 None
417 }
418 });
419
420 Ok(Trade {
421 id,
422 order_id,
423 instrument: instrument_index,
424 strategy,
425 time_exchange,
426 side,
427 price: trade_price,
428 quantity,
429 fees: AssetFees {
430 asset: fee_asset_index,
431 fees: fees.fees,
432 fees_quote,
433 },
434 })
435 }
436}
437
438#[cfg(test)]
439#[allow(clippy::unwrap_used)] mod tests {
441 use super::*;
442 use crate::{error::StreamTerminationReason, map::generate_execution_instrument_map};
443 use rustrade_instrument::{index::IndexedInstruments, test_utils};
444
445 fn binance_indexer() -> AccountEventIndexer {
446 let instruments = IndexedInstruments::new(vec![test_utils::instrument(
447 ExchangeId::BinanceSpot,
448 "BTC",
449 "USDT",
450 )]);
451 let map = generate_execution_instrument_map(&instruments, ExchangeId::BinanceSpot).unwrap();
452 AccountEventIndexer::new(Arc::new(map))
453 }
454
455 #[test]
458 fn account_event_passes_stream_terminated_through_verbatim() {
459 let indexer = binance_indexer();
460 let reason = StreamTerminationReason::ReconnectBudgetExhausted {
461 attempts: 3,
462 last_error: "socket reset".to_string(),
463 };
464
465 let indexed = indexer
466 .account_event(UnindexedAccountEvent::new(
467 ExchangeId::BinanceSpot,
468 AccountEventKind::StreamTerminated(reason.clone()),
469 ))
470 .unwrap();
471
472 assert_eq!(indexed.exchange, indexer.map.exchange.key);
473 assert!(
474 matches!(indexed.kind, AccountEventKind::StreamTerminated(r) if r == reason),
475 "expected StreamTerminated to pass through unchanged",
476 );
477 }
478}