Skip to main content

betfair_rs/
order_cache.rs

1use crate::dto::streaming::UnmatchedOrder;
2use rust_decimal::Decimal;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
6pub struct OrderCache {
7    pub market_id: String,
8    pub runners: HashMap<u64, RunnerOrders>,
9    pub last_update: i64,
10}
11
12#[derive(Debug, Clone)]
13pub struct RunnerOrders {
14    pub selection_id: u64,
15    pub handicap: Option<Decimal>,
16    pub orders: HashMap<String, UnmatchedOrder>,
17    pub matched_backs: HashMap<String, Decimal>,
18    pub matched_lays: HashMap<String, Decimal>,
19}
20
21impl OrderCache {
22    pub fn new(market_id: String) -> Self {
23        Self {
24            market_id,
25            runners: HashMap::new(),
26            last_update: 0,
27        }
28    }
29
30    pub fn update_timestamp(&mut self, timestamp: i64) {
31        self.last_update = timestamp;
32    }
33
34    pub fn get_runner(&self, selection_id: u64) -> Option<&RunnerOrders> {
35        self.runners.get(&selection_id)
36    }
37
38    pub fn get_runner_mut(&mut self, selection_id: u64) -> &mut RunnerOrders {
39        self.runners
40            .entry(selection_id)
41            .or_insert_with(|| RunnerOrders::new(selection_id))
42    }
43
44    pub fn get_all_orders(&self) -> Vec<&UnmatchedOrder> {
45        self.runners
46            .values()
47            .flat_map(|r| r.orders.values())
48            .collect()
49    }
50
51    pub fn get_active_orders(&self) -> Vec<&UnmatchedOrder> {
52        self.get_all_orders()
53            .into_iter()
54            .filter(|o| o.status == "E")
55            .collect()
56    }
57
58    pub fn clear(&mut self) {
59        self.runners.clear();
60    }
61}
62
63impl RunnerOrders {
64    pub fn new(selection_id: u64) -> Self {
65        Self {
66            selection_id,
67            handicap: None,
68            orders: HashMap::new(),
69            matched_backs: HashMap::new(),
70            matched_lays: HashMap::new(),
71        }
72    }
73
74    pub fn set_handicap(&mut self, handicap: Option<Decimal>) {
75        self.handicap = handicap;
76    }
77
78    pub fn apply_full_image(&mut self, orders: Vec<UnmatchedOrder>) {
79        self.orders.clear();
80        for order in orders {
81            self.orders.insert(order.id.clone(), order);
82        }
83    }
84
85    pub fn update_order(&mut self, order: UnmatchedOrder) {
86        if order.status == "EC" {
87            self.orders.remove(&order.id);
88        } else {
89            self.orders.insert(order.id.clone(), order);
90        }
91    }
92
93    pub fn update_matched_backs(&mut self, matched_backs: Vec<Vec<Decimal>>) {
94        for entry in matched_backs {
95            if entry.len() >= 2 {
96                let price = entry[0].to_string();
97                let size = entry[1];
98                if size.is_zero() {
99                    self.matched_backs.remove(&price);
100                } else {
101                    self.matched_backs.insert(price, size);
102                }
103            }
104        }
105    }
106
107    pub fn update_matched_lays(&mut self, matched_lays: Vec<Vec<Decimal>>) {
108        for entry in matched_lays {
109            if entry.len() >= 2 {
110                let price = entry[0].to_string();
111                let size = entry[1];
112                if size.is_zero() {
113                    self.matched_lays.remove(&price);
114                } else {
115                    self.matched_lays.insert(price, size);
116                }
117            }
118        }
119    }
120
121    pub fn clear_matched_backs(&mut self) {
122        self.matched_backs.clear();
123    }
124
125    pub fn clear_matched_lays(&mut self) {
126        self.matched_lays.clear();
127    }
128
129    pub fn get_order(&self, bet_id: &str) -> Option<&UnmatchedOrder> {
130        self.orders.get(bet_id)
131    }
132
133    pub fn get_total_back_matched(&self) -> Decimal {
134        self.matched_backs.values().sum()
135    }
136
137    pub fn get_total_lay_matched(&self) -> Decimal {
138        self.matched_lays.values().sum()
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use rust_decimal_macros::dec;
146
147    fn create_test_order(id: &str, price: Decimal, size: Decimal, status: &str) -> UnmatchedOrder {
148        UnmatchedOrder {
149            id: id.to_string(),
150            p: price,
151            s: size,
152            bsp: None,
153            side: "B".to_string(),
154            status: status.to_string(),
155            pt: "L".to_string(),
156            ot: "L".to_string(),
157            pd: 1234567890000,
158            md: None,
159            cd: None,
160            ld: None,
161            lsrc: None,
162            avp: None,
163            sm: None,
164            sr: Some(size),
165            sl: None,
166            sc: None,
167            sv: None,
168            rac: None,
169            rc: None,
170            rfo: None,
171            rfs: None,
172        }
173    }
174
175    #[test]
176    fn test_order_cache_new() {
177        let cache = OrderCache::new("1.123456".to_string());
178        assert_eq!(cache.market_id, "1.123456");
179        assert!(cache.runners.is_empty());
180        assert_eq!(cache.last_update, 0);
181    }
182
183    #[test]
184    fn test_runner_orders_new() {
185        let runner = RunnerOrders::new(12345);
186        assert_eq!(runner.selection_id, 12345);
187        assert!(runner.handicap.is_none());
188        assert!(runner.orders.is_empty());
189        assert!(runner.matched_backs.is_empty());
190        assert!(runner.matched_lays.is_empty());
191    }
192
193    #[test]
194    fn test_update_order() {
195        let mut runner = RunnerOrders::new(12345);
196        let order = create_test_order("bet1", dec!(2.0), dec!(10.0), "E");
197
198        runner.update_order(order.clone());
199        assert_eq!(runner.orders.len(), 1);
200        assert!(runner.get_order("bet1").is_some());
201    }
202
203    #[test]
204    fn test_remove_completed_order() {
205        let mut runner = RunnerOrders::new(12345);
206        let order1 = create_test_order("bet1", dec!(2.0), dec!(10.0), "E");
207        let order2 = create_test_order("bet1", dec!(2.0), dec!(10.0), "EC");
208
209        runner.update_order(order1);
210        assert_eq!(runner.orders.len(), 1);
211
212        runner.update_order(order2);
213        assert_eq!(runner.orders.len(), 0);
214    }
215
216    #[test]
217    fn test_apply_full_image() {
218        let mut runner = RunnerOrders::new(12345);
219        let order1 = create_test_order("bet1", dec!(2.0), dec!(10.0), "E");
220        let order2 = create_test_order("bet2", dec!(3.0), dec!(20.0), "E");
221
222        runner.update_order(order1.clone());
223        assert_eq!(runner.orders.len(), 1);
224
225        runner.apply_full_image(vec![order1, order2]);
226        assert_eq!(runner.orders.len(), 2);
227    }
228
229    #[test]
230    fn test_update_matched_backs() {
231        let mut runner = RunnerOrders::new(12345);
232
233        runner.update_matched_backs(vec![vec![dec!(2.0), dec!(50.0)], vec![dec!(2.5), dec!(100.0)]]);
234        assert_eq!(runner.matched_backs.len(), 2);
235
236        let keys: Vec<String> = runner.matched_backs.keys().cloned().collect();
237        assert!(keys.contains(&"2".to_string()) || keys.contains(&"2.0".to_string()),
238                "Expected key '2' or '2.0', got: {keys:?}");
239
240        let price_key = dec!(2.0).to_string();
241        assert_eq!(*runner.matched_backs.get(&price_key).unwrap(), dec!(50.0));
242
243        let price_key_2 = dec!(2.5).to_string();
244        assert_eq!(*runner.matched_backs.get(&price_key_2).unwrap(), dec!(100.0));
245    }
246
247    #[test]
248    fn test_remove_matched_backs_with_zero() {
249        let mut runner = RunnerOrders::new(12345);
250
251        runner.update_matched_backs(vec![vec![dec!(2.0), dec!(50.0)], vec![dec!(2.5), dec!(100.0)]]);
252        assert_eq!(runner.matched_backs.len(), 2);
253
254        runner.update_matched_backs(vec![vec![dec!(2.0), Decimal::ZERO]]);
255        assert_eq!(runner.matched_backs.len(), 1);
256        assert!(!runner.matched_backs.contains_key("2"));
257    }
258
259    #[test]
260    fn test_get_all_orders() {
261        let mut cache = OrderCache::new("1.123456".to_string());
262        let order1 = create_test_order("bet1", dec!(2.0), dec!(10.0), "E");
263        let order2 = create_test_order("bet2", dec!(3.0), dec!(20.0), "E");
264
265        cache.get_runner_mut(12345).update_order(order1);
266        cache.get_runner_mut(12346).update_order(order2);
267
268        let all_orders = cache.get_all_orders();
269        assert_eq!(all_orders.len(), 2);
270    }
271
272    #[test]
273    fn test_get_active_orders() {
274        let mut cache = OrderCache::new("1.123456".to_string());
275        let order1 = create_test_order("bet1", dec!(2.0), dec!(10.0), "E");
276        let order2 = create_test_order("bet2", dec!(3.0), dec!(20.0), "EC");
277
278        cache.get_runner_mut(12345).update_order(order1);
279        cache.get_runner_mut(12345).update_order(order2);
280
281        let active_orders = cache.get_active_orders();
282        assert_eq!(active_orders.len(), 1);
283        assert_eq!(active_orders[0].id, "bet1");
284    }
285
286    #[test]
287    fn test_get_total_matched() {
288        let mut runner = RunnerOrders::new(12345);
289
290        runner.update_matched_backs(vec![vec![dec!(2.0), dec!(50.0)], vec![dec!(2.5), dec!(100.0)]]);
291        runner.update_matched_lays(vec![vec![dec!(3.0), dec!(75.0)]]);
292
293        assert_eq!(runner.get_total_back_matched(), dec!(150.0));
294        assert_eq!(runner.get_total_lay_matched(), dec!(75.0));
295    }
296
297    #[test]
298    fn test_clear_cache() {
299        let mut cache = OrderCache::new("1.123456".to_string());
300        let order = create_test_order("bet1", dec!(2.0), dec!(10.0), "E");
301
302        cache.get_runner_mut(12345).update_order(order);
303        assert_eq!(cache.runners.len(), 1);
304
305        cache.clear();
306        assert_eq!(cache.runners.len(), 0);
307    }
308}