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}