Skip to main content

brokapi_finam/
index.rs

1//! Client order id <-> Finam order id.
2//!
3//! Finam cancels and reads orders by its own `order_id`, while the library API speaks `cid`.
4//! This index remembers the pairs seen in this process (placed here, echoed by streams, listed by
5//! `GetOrders`) and resolves the cid of any order echo.
6//!
7//! The index lives in memory. After a restart it is empty and fills again from `reconcile` and from
8//! the comments of order echoes, which carry the cid in a form that survives restarts
9//! (see [`FinamCidCodec`]).
10//!
11//! # Examples
12//!
13//! ```
14//! use brokapi_core::{CidCodec, ClientOrderId};
15//! use brokapi_finam::FinamCidCodec;
16//! use brokapi_finam::index::{OrderIndex, foreign_cid};
17//!
18//! let index = OrderIndex::new();
19//! let cid = ClientOrderId::new_v4();
20//!
21//! // We placed an order: remember the pair when the broker answers with its id.
22//! index.bind(cid, "ORD-1");
23//! assert_eq!(index.order_id(cid).as_deref(), Some("ORD-1"));
24//!
25//! // An echo whose comment carries our cid resolves to it.
26//! let comment = FinamCidCodec.encode(cid).unwrap();
27//! assert_eq!(index.resolve("ORD-1", "", comment.as_str()), cid);
28//!
29//! // An order placed by someone else gets a stable made-up cid, and `lookup` says it is not ours.
30//! assert_eq!(index.lookup("ORD-2", "set by hand"), None);
31//! assert_eq!(index.resolve("ORD-2", "", "set by hand"), foreign_cid("ORD-2"));
32//! ```
33
34use std::collections::HashMap;
35use std::sync::Mutex;
36
37use brokapi_core::ClientOrderId;
38use uuid::Uuid;
39
40use crate::cid::FinamCidCodec;
41use crate::map::order::cid_from_comment;
42
43/// Namespace for cids made up for orders this library did not place (UUID v5, so they can never
44/// collide with the client's v4 ids).
45const FOREIGN_NS: Uuid = Uuid::from_u128(0x6f5e_1c2a_9b7d_4e3f_8a10_46fa_6d1b_0c01);
46
47#[derive(Debug, Default)]
48struct Inner {
49    order_by_cid: HashMap<ClientOrderId, String>,
50    cid_by_order: HashMap<String, ClientOrderId>,
51    cid_by_tag: HashMap<String, ClientOrderId>,
52}
53
54/// Memory of which Finam order id belongs to which client order id.
55///
56/// Thread safe. The user data connector owns one and its core reads it for every order echo.
57///
58/// See the [module docs](self) for an example.
59#[derive(Debug, Default)]
60pub struct OrderIndex {
61    inner: Mutex<Inner>,
62}
63
64impl OrderIndex {
65    /// An empty index.
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// A cid this process knows about: its tag becomes a lookup key for echoes without comment.
71    pub fn know(&self, cid: ClientOrderId) {
72        let tag = FinamCidCodec.client_order_id(cid);
73        self.inner
74            .lock()
75            .expect("index poisoned")
76            .cid_by_tag
77            .insert(tag, cid);
78    }
79
80    /// Remembers that Finam order `order_id` belongs to `cid`. An empty `order_id` is ignored.
81    pub fn bind(&self, cid: ClientOrderId, order_id: &str) {
82        if order_id.is_empty() {
83            return;
84        }
85        let mut i = self.inner.lock().expect("index poisoned");
86        i.order_by_cid.insert(cid, order_id.to_string());
87        i.cid_by_order.insert(order_id.to_string(), cid);
88    }
89
90    /// Forgets everything kept for `cid`: the pair with its Finam order id and its tag. For an order that
91    /// finished long ago. A later echo of that order resolves through its comment again, or to a made-up cid.
92    pub fn forget(&self, cid: ClientOrderId) {
93        let tag = FinamCidCodec.client_order_id(cid);
94        let mut i = self.inner.lock().expect("index poisoned");
95        if let Some(order_id) = i.order_by_cid.remove(&cid) {
96            i.cid_by_order.remove(&order_id);
97        }
98        i.cid_by_tag.remove(&tag);
99    }
100
101    /// Finam order id of `cid`, if this process has seen the pair. `None` does not mean the order
102    /// does not exist: after a restart call `reconcile` first.
103    pub fn order_id(&self, cid: ClientOrderId) -> Option<String> {
104        self.inner
105            .lock()
106            .expect("index poisoned")
107            .order_by_cid
108            .get(&cid)
109            .cloned()
110    }
111
112    /// Like [`OrderIndex::resolve`] but never invents a cid: `None` for orders that are not ours.
113    pub fn lookup(&self, order_id: &str, comment: &str) -> Option<ClientOrderId> {
114        cid_from_comment(comment).or_else(|| {
115            let i = self.inner.lock().expect("index poisoned");
116            i.cid_by_order.get(order_id).copied()
117        })
118    }
119
120    /// Cid of an order echo, in order of trust: comment, known order id, known tag, otherwise a
121    /// stable made-up cid (UUID v5 of the order id). The result is bound to `order_id`.
122    pub fn resolve(&self, order_id: &str, client_order_id: &str, comment: &str) -> ClientOrderId {
123        let found = cid_from_comment(comment).or_else(|| {
124            let i = self.inner.lock().expect("index poisoned");
125            i.cid_by_order
126                .get(order_id)
127                .or_else(|| i.cid_by_tag.get(client_order_id))
128                .copied()
129        });
130        match found {
131            Some(cid) => {
132                self.bind(cid, order_id);
133                cid
134            }
135            None => {
136                // Not ours. The made-up cid is remembered one way only (cid -> order id, so that the
137                // order can be cancelled by it). The order id does not map back to it, so `lookup` keeps
138                // saying "not ours" and the fills of foreign orders carry no cid.
139                let cid = foreign_cid(order_id);
140                if !order_id.is_empty() {
141                    self.inner
142                        .lock()
143                        .expect("index poisoned")
144                        .order_by_cid
145                        .insert(cid, order_id.to_string());
146                }
147                cid
148            }
149        }
150    }
151}
152
153/// Stable cid for an order that is not ours.
154///
155/// A UUID v5 of the Finam order id, so the same foreign order always gets the same cid and it cannot
156/// collide with the v4 ids this library makes.
157pub fn foreign_cid(order_id: &str) -> ClientOrderId {
158    Uuid::new_v5(&FOREIGN_NS, order_id.as_bytes())
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use brokapi_core::CidCodec;
165
166    #[test]
167    fn comment_wins_and_binds() {
168        let idx = OrderIndex::new();
169        let cid = ClientOrderId::from_bytes([3; 16]);
170        let comment = FinamCidCodec.encode(cid).unwrap();
171        assert_eq!(idx.resolve("100", "whatever", comment.as_str()), cid);
172        assert_eq!(idx.order_id(cid).as_deref(), Some("100"));
173    }
174
175    #[test]
176    fn tag_is_the_fallback_when_comment_is_lost() {
177        let idx = OrderIndex::new();
178        let cid = ClientOrderId::from_bytes([7; 16]);
179        idx.know(cid);
180        let tag = FinamCidCodec.client_order_id(cid);
181        assert_eq!(idx.resolve("200", &tag, ""), cid);
182        // Later echoes find it by order id alone.
183        assert_eq!(idx.resolve("200", "", ""), cid);
184    }
185
186    #[test]
187    fn unknown_orders_get_a_stable_foreign_cid() {
188        let idx = OrderIndex::new();
189        let a = idx.resolve("300", "", "someone else's label");
190        let b = idx.resolve("300", "", "");
191        assert_eq!(a, b);
192        assert_eq!(a, foreign_cid("300"));
193        assert_ne!(a, foreign_cid("301"));
194        assert_eq!(a.get_version_num(), 5);
195    }
196}