Skip to main content

brokapi_runtime/
reconcile.rs

1//! Bootstrap ledger reconciliation.
2
3use std::collections::{HashMap, HashSet};
4
5use brokapi_core::{ClientOrderId, OrderSnapshot, ReconcileReport};
6
7/// Compares the client's ledger of orders with the broker's active orders and sorts them into three
8/// groups.
9///
10/// - `confirmed`: in both. The broker's snapshot is returned.
11/// - `closed_offline`: in the ledger, not active at the broker. They filled, were cancelled or
12///   were rejected while the client was away. Their cids are returned. The function does not say
13///   which of these happened. Ask the broker for the order state.
14/// - `foreign`: active at the broker, not in the ledger. They were placed by someone else (another
15///   program, the broker's app). They must not be touched. Why: cancelling an order the client
16///   never created could damage someone else's strategy.
17///
18/// Broker orders whose cid is the nil UUID (no client id) are ignored. They cannot be matched with
19/// a ledger entry and are not reported as foreign. The order of the returned lists is not defined.
20///
21/// The function is pure: no I/O and no state.
22///
23/// # Examples
24///
25/// ```
26/// use std::time::Instant;
27/// use brokapi_core::{
28///     BrokerOrderId, ClientOrderId, InstrumentId, InstrumentKey, NativeId, OrderSnapshot,
29///     OrderState, OrderType, Price, Qty, Side, Timestamps, UnixNanos,
30/// };
31/// use brokapi_runtime::reconcile_ledger;
32///
33/// let sber = InstrumentId::new(
34///     InstrumentKey::new("SBER", "TQBR"),
35///     NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
36/// );
37/// let order = |cid: ClientOrderId| OrderSnapshot::new(cid, sber.clone(), OrderState::Acked { cid, broker_id: BrokerOrderId::new("1") }, Side::Buy, OrderType::Limit, Qty::from(1), Timestamps::new(UnixNanos::ZERO, Instant::now())).with_filled(Qty::from(0)).with_price(Some(Price::from(100)));
38/// let (kept, gone, other) = (ClientOrderId::new_v4(), ClientOrderId::new_v4(), ClientOrderId::new_v4());
39///
40/// // We remember `kept` and `gone`. The broker shows `kept` and one we never placed.
41/// let ledger = [order(kept), order(gone)];
42/// let at_broker = [order(kept), order(other)];
43/// let report = reconcile_ledger(&ledger, &at_broker);
44///
45/// assert_eq!(report.confirmed.len(), 1);
46/// assert_eq!(report.confirmed[0].cid, kept);
47/// assert_eq!(report.closed_offline, vec![gone]);
48/// assert_eq!(report.foreign[0].cid, other);
49/// ```
50pub fn reconcile_ledger(
51    ledger: &[OrderSnapshot],
52    broker_active: &[OrderSnapshot],
53) -> ReconcileReport {
54    let ledger_map: HashMap<ClientOrderId, &OrderSnapshot> =
55        ledger.iter().map(|o| (o.cid, o)).collect();
56    let broker_map: HashMap<ClientOrderId, &OrderSnapshot> = broker_active
57        .iter()
58        .filter(|o| !o.cid.is_nil())
59        .map(|o| (o.cid, o))
60        .collect();
61
62    let ledger_ids: HashSet<_> = ledger_map.keys().copied().collect();
63    let broker_ids: HashSet<_> = broker_map.keys().copied().collect();
64
65    let confirmed = ledger_ids
66        .intersection(&broker_ids)
67        .filter_map(|cid| broker_map.get(cid).copied().cloned())
68        .collect();
69
70    let closed_offline = ledger_ids.difference(&broker_ids).copied().collect();
71
72    let foreign = broker_ids
73        .difference(&ledger_ids)
74        .filter_map(|cid| broker_map.get(cid).copied().cloned())
75        .collect();
76
77    ReconcileReport::new(confirmed, closed_offline, foreign)
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    use brokapi_core::{
85        InstrumentId, InstrumentKey, NativeId, OrderState, OrderType, Qty, Side, Timestamps,
86        UnixNanos,
87    };
88    use std::time::Instant;
89
90    fn snapshot(cid: u8) -> OrderSnapshot {
91        OrderSnapshot::new(
92            ClientOrderId::from_bytes([cid; 16]),
93            InstrumentId::new(
94                InstrumentKey::new("SBER", "TQBR"),
95                NativeId::TickerClass {
96                    ticker: "SBER".into(),
97                    class_code: "TQBR".into(),
98                },
99            ),
100            OrderState::Acked {
101                cid: ClientOrderId::from_bytes([cid; 16]),
102                broker_id: brokapi_core::BrokerOrderId::new("1"),
103            },
104            Side::Buy,
105            OrderType::Limit,
106            Qty::from(1),
107            Timestamps::new(UnixNanos::ZERO, Instant::now()),
108        )
109        .with_filled(Qty::from(0))
110    }
111
112    #[test]
113    fn splits_confirmed_closed_foreign() {
114        let ledger = vec![snapshot(1), snapshot(2)];
115        let broker = vec![snapshot(1), snapshot(3)];
116        let report = reconcile_ledger(&ledger, &broker);
117        assert_eq!(report.confirmed.len(), 1);
118        assert_eq!(report.closed_offline.len(), 1);
119        assert_eq!(report.foreign.len(), 1);
120    }
121}