Skip to main content

polyoxide_clob/
utils.rs

1use rand::Rng;
2use rust_decimal::prelude::ToPrimitive;
3
4use crate::{
5    api::markets::OrderLevel,
6    types::{OrderSide, TickSize},
7};
8
9/// Calculate maker and taker amounts for an order using f64 arithmetic.
10///
11/// # Arguments
12///
13/// * `price` - Order price (0.0 to 1.0)
14/// * `size` - Order size in shares
15/// * `side` - Buy or Sell
16/// * `tick_size` - Minimum price increment for rounding
17///
18/// # Returns
19///
20/// A tuple of (maker_amount, taker_amount) as strings suitable for the CLOB API.
21pub fn calculate_order_amounts(
22    price: f64,
23    size: f64,
24    side: OrderSide,
25    tick_size: TickSize,
26) -> (String, String) {
27    const SIZE_DECIMALS: u32 = 6;
28    let tick_decimals = tick_size.decimals();
29
30    let price_rounded = round_bankers(price, tick_decimals);
31    let size_rounded = round_bankers(size, SIZE_DECIMALS);
32
33    let cost = price_rounded * size_rounded;
34    let cost_rounded = round_bankers(cost, SIZE_DECIMALS);
35
36    let share_amount = to_raw_amount(size_rounded, SIZE_DECIMALS);
37    let cost_amount = to_raw_amount(cost_rounded, SIZE_DECIMALS);
38
39    match side {
40        OrderSide::Buy => (cost_amount, share_amount),
41        OrderSide::Sell => (share_amount, cost_amount),
42    }
43}
44
45/// Calculate maker and taker amounts for a MARKET order.
46pub fn calculate_market_order_amounts(
47    amount: f64,
48    price: f64,
49    side: OrderSide,
50    tick_size: TickSize,
51) -> (String, String) {
52    const SIZE_DECIMALS: u32 = 6;
53    let tick_decimals = tick_size.decimals();
54
55    let price_rounded = round_bankers(price, tick_decimals);
56    let amount_rounded = round_bankers(amount, SIZE_DECIMALS); // Input amount (USDC or Shares)
57
58    if price_rounded == 0.0 {
59        return ("0".to_string(), "0".to_string());
60    }
61
62    match side {
63        OrderSide::Buy => {
64            // Market BUY: amount is USDC (maker amount).
65            // Taker (shares) = usdc / price
66            let maker_amount = amount_rounded;
67            let taker_amount_raw = maker_amount / price_rounded;
68            let taker_amount = round_to_zero(taker_amount_raw, SIZE_DECIMALS); // Round down/to-zero for shares?
69                                                                               // Existing logic used round_dp_with_strategy(ToZero).
70
71            (
72                to_raw_amount(maker_amount, SIZE_DECIMALS),
73                to_raw_amount(taker_amount, SIZE_DECIMALS),
74            )
75        }
76        OrderSide::Sell => {
77            // Market SELL: amount is Shares (maker amount)
78            // Taker (USDC) = shares * price
79            let maker_amount = round_to_zero(amount, SIZE_DECIMALS); // Shares input usually rounded?
80            let taker_amount_raw = maker_amount * price_rounded;
81            let taker_amount = round_bankers(taker_amount_raw, SIZE_DECIMALS);
82
83            (
84                to_raw_amount(maker_amount, SIZE_DECIMALS),
85                to_raw_amount(taker_amount, SIZE_DECIMALS),
86            )
87        }
88    }
89}
90
91/// Calculate the worst price needed to fill the requested amount from the orderbook.
92pub fn calculate_market_price(levels: &[OrderLevel], amount: f64, side: OrderSide) -> Option<f64> {
93    if levels.is_empty() {
94        return None;
95    }
96
97    let mut sum = 0.0;
98
99    for level in levels {
100        let p = level.price.to_f64()?;
101        let s = level.size.to_f64()?;
102
103        match side {
104            OrderSide::Buy => {
105                sum += p * s;
106            }
107            OrderSide::Sell => {
108                sum += s;
109            }
110        }
111
112        if sum >= amount {
113            return Some(p);
114        }
115    }
116
117    // Not enough liquidity to fill the requested amount
118    None
119}
120
121/// Convert f64 to raw integer string by multiplying by 10^decimals
122fn to_raw_amount(val: f64, decimals: u32) -> String {
123    let factor = 10f64.powi(decimals as i32);
124    // Use matching rounding? Usually if we already rounded 'val', we just multiply and round to int.
125    let raw = (val * factor).round();
126    // Handle potential overflow if needed, but f64 goes up to 10^308. u128 is 10^38.
127    // We assume amounts fit in u128.
128    format!("{:.0}", raw)
129}
130
131/// Generate random salt for orders
132pub fn generate_salt() -> String {
133    rand::rng().random::<u128>().to_string()
134}
135
136// Helpers for rounding
137
138/// Round half to even (Banker's rounding)
139fn round_bankers(val: f64, decimals: u32) -> f64 {
140    let factor = 10f64.powi(decimals as i32);
141    let v = val * factor;
142    let r = v.round();
143    let diff = (v - r).abs();
144
145    if (diff - 0.5).abs() < 1e-10 {
146        // Half-way case
147        if r % 2.0 != 0.0 {
148            // Odd, so move to even.
149            // if v was 1.5, round() gives 2. 2 is even. ok.
150            // if v was 2.5, round() gives 3. 3 is odd. We want 2.
151            // if v was 0.5, round() gives 1. We want 0.
152
153            // Wait, round() rounds away from zero for .5.
154            // 0.5 -> 1.0. 1.5 -> 2.0. 2.5 -> 3.0.
155            // We want 2.5 -> 2.0.
156            if v > 0.0 {
157                return (r - 1.0) / factor;
158            } else {
159                return (r + 1.0) / factor;
160            }
161        }
162    }
163    r / factor
164}
165
166/// Round towards zero (Truncate)
167fn round_to_zero(val: f64, decimals: u32) -> f64 {
168    let factor = 10f64.powi(decimals as i32);
169    (val * factor).trunc() / factor
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_calculate_order_amounts_buy() {
178        let (maker, taker) =
179            calculate_order_amounts(0.52, 100.0, OrderSide::Buy, TickSize::Hundredth);
180        assert_eq!(maker, "52000000");
181        assert_eq!(taker, "100000000");
182    }
183
184    #[test]
185    fn test_calculate_order_amounts_sell() {
186        let (maker, taker) =
187            calculate_order_amounts(0.52, 100.0, OrderSide::Sell, TickSize::Hundredth);
188        assert_eq!(maker, "100000000");
189        assert_eq!(taker, "52000000");
190    }
191
192    #[test]
193    fn test_round_bankers() {
194        assert_eq!(round_bankers(0.5, 0), 0.0);
195        assert_eq!(round_bankers(1.5, 0), 2.0);
196        assert_eq!(round_bankers(2.5, 0), 2.0);
197        assert_eq!(round_bankers(3.5, 0), 4.0);
198    }
199
200    #[test]
201    fn test_calculate_market_order_amounts_buy() {
202        // 100 USDC, 0.50 price.
203        // Maker = 100 * 10^6. Taker = 200 * 10^6.
204        let (maker, taker) =
205            calculate_market_order_amounts(100.0, 0.50, OrderSide::Buy, TickSize::Hundredth);
206        assert_eq!(maker, "100000000");
207        assert_eq!(taker, "200000000");
208    }
209
210    #[test]
211    fn test_calculate_market_price_buy_simple() {
212        use rust_decimal_macros::dec;
213        // Should find match at 0.50
214        let levels = vec![OrderLevel {
215            price: dec!(0.50),
216            size: dec!(1000),
217        }];
218        let price = calculate_market_price(&levels, 100.0, OrderSide::Buy);
219        assert_eq!(price, Some(0.50));
220    }
221
222    #[test]
223    fn test_calculate_market_price_insufficient_liquidity() {
224        use rust_decimal_macros::dec;
225        // Only 10 shares available at 0.50, but we want 1000 USDC worth
226        let levels = vec![OrderLevel {
227            price: dec!(0.50),
228            size: dec!(10),
229        }];
230        // Buy: sum += price * size = 0.50 * 10 = 5.0, which is < 1000.0
231        let price = calculate_market_price(&levels, 1000.0, OrderSide::Buy);
232        assert_eq!(
233            price, None,
234            "Should return None when liquidity is insufficient"
235        );
236    }
237
238    #[test]
239    fn test_calculate_market_price_empty_levels() {
240        let price = calculate_market_price(&[], 100.0, OrderSide::Buy);
241        assert_eq!(price, None);
242    }
243
244    #[test]
245    fn test_calculate_market_price_sell_insufficient() {
246        use rust_decimal_macros::dec;
247        let levels = vec![OrderLevel {
248            price: dec!(0.50),
249            size: dec!(10),
250        }];
251        // Sell: sum += size = 10, which is < 100
252        let price = calculate_market_price(&levels, 100.0, OrderSide::Sell);
253        assert_eq!(
254            price, None,
255            "Should return None when sell liquidity is insufficient"
256        );
257    }
258
259    #[test]
260    fn test_generate_salt_large_range() {
261        // Salt should be a valid u128 string (can be very large)
262        let salt = generate_salt();
263        let _parsed: u128 = salt.parse().expect("Salt should parse as u128");
264        // Two random salts should (almost certainly) differ
265        let salt2 = generate_salt();
266        assert_ne!(salt, salt2, "Two random salts should differ");
267    }
268
269    // ── calculate_market_order_amounts ──
270
271    #[test]
272    fn test_calculate_market_order_amounts_sell() {
273        // 100 shares at 0.50 price → maker=shares, taker=USDC
274        let (maker, taker) =
275            calculate_market_order_amounts(100.0, 0.50, OrderSide::Sell, TickSize::Hundredth);
276        assert_eq!(maker, "100000000"); // 100 shares * 10^6
277        assert_eq!(taker, "50000000"); // 100 * 0.50 = 50 USDC * 10^6
278    }
279
280    #[test]
281    fn test_calculate_market_order_amounts_zero_price() {
282        let (maker, taker) =
283            calculate_market_order_amounts(100.0, 0.0, OrderSide::Buy, TickSize::Hundredth);
284        assert_eq!(maker, "0");
285        assert_eq!(taker, "0");
286    }
287
288    #[test]
289    fn test_calculate_market_order_amounts_sell_zero_price() {
290        let (maker, taker) =
291            calculate_market_order_amounts(100.0, 0.0, OrderSide::Sell, TickSize::Hundredth);
292        assert_eq!(maker, "0");
293        assert_eq!(taker, "0");
294    }
295
296    #[test]
297    fn test_calculate_market_order_amounts_tenth_tick() {
298        // With Tenth tick size, price rounds to 1 decimal
299        let (maker, taker) =
300            calculate_market_order_amounts(100.0, 0.5, OrderSide::Buy, TickSize::Tenth);
301        assert_eq!(maker, "100000000");
302        assert_eq!(taker, "200000000");
303    }
304
305    #[test]
306    fn test_calculate_market_order_amounts_thousandth_tick() {
307        let (maker, taker) =
308            calculate_market_order_amounts(100.0, 0.555, OrderSide::Buy, TickSize::Thousandth);
309        assert_eq!(maker, "100000000");
310        // taker = 100 / 0.555 = 180.180180... truncated to 6 decimals
311        let taker_val: u64 = taker.parse().unwrap();
312        assert!(taker_val > 180_000_000); // ~180.18 shares
313    }
314
315    #[test]
316    fn test_calculate_order_amounts_tenth_tick() {
317        let (maker, taker) = calculate_order_amounts(0.5, 100.0, OrderSide::Buy, TickSize::Tenth);
318        assert_eq!(maker, "50000000");
319        assert_eq!(taker, "100000000");
320    }
321
322    #[test]
323    fn test_calculate_order_amounts_thousandth_tick() {
324        let (maker, taker) =
325            calculate_order_amounts(0.555, 100.0, OrderSide::Buy, TickSize::Thousandth);
326        assert_eq!(maker, "55500000");
327        assert_eq!(taker, "100000000");
328    }
329
330    // ── calculate_market_price ──
331
332    #[test]
333    fn test_calculate_market_price_sell_simple() {
334        use rust_decimal_macros::dec;
335        let levels = vec![OrderLevel {
336            price: dec!(0.50),
337            size: dec!(200),
338        }];
339        // Sell: sum += size. 200 >= 100 → price = 0.50
340        let price = calculate_market_price(&levels, 100.0, OrderSide::Sell);
341        assert_eq!(price, Some(0.50));
342    }
343
344    #[test]
345    fn test_calculate_market_price_buy_multiple_levels() {
346        use rust_decimal_macros::dec;
347        let levels = vec![
348            OrderLevel {
349                price: dec!(0.50),
350                size: dec!(100),
351            }, // sum = 50
352            OrderLevel {
353                price: dec!(0.55),
354                size: dec!(100),
355            }, // sum = 105
356            OrderLevel {
357                price: dec!(0.60),
358                size: dec!(100),
359            }, // sum = 165
360        ];
361        // Buy: sum += price*size. Need 100 USDC.
362        // Level 1: 0.50*100=50 (sum=50 < 100)
363        // Level 2: 0.55*100=55 (sum=105 >= 100) → price = 0.55
364        let price = calculate_market_price(&levels, 100.0, OrderSide::Buy);
365        assert_eq!(price, Some(0.55));
366    }
367
368    // ── rounding helpers ──
369
370    #[test]
371    fn test_round_to_zero() {
372        assert_eq!(round_to_zero(1.999999, 6), 1.999999);
373        assert_eq!(round_to_zero(1.9999999, 6), 1.999999);
374        assert_eq!(round_to_zero(-1.9999999, 6), -1.999999);
375        assert_eq!(round_to_zero(0.0, 6), 0.0);
376    }
377
378    #[test]
379    fn test_round_bankers_decimals() {
380        // 2 decimal places — f64 representation means epsilon-based half
381        // detection treats these as half-way cases, rounding to even digit
382        assert_eq!(round_bankers(1.235, 2), 1.24); // 124 is even
383        assert_eq!(round_bankers(1.245, 2), 1.24); // 124 is even
384        assert_eq!(round_bankers(1.265, 2), 1.26); // 126 is even
385    }
386
387    #[test]
388    fn test_round_bankers_negative() {
389        assert_eq!(round_bankers(-0.5, 0), 0.0);
390        assert_eq!(round_bankers(-1.5, 0), -2.0);
391        assert_eq!(round_bankers(-2.5, 0), -2.0);
392    }
393
394    #[test]
395    fn test_to_raw_amount() {
396        assert_eq!(to_raw_amount(1.0, 6), "1000000");
397        assert_eq!(to_raw_amount(0.5, 6), "500000");
398        assert_eq!(to_raw_amount(0.0, 6), "0");
399        assert_eq!(to_raw_amount(123.456789, 6), "123456789");
400    }
401
402    #[test]
403    fn test_calculate_market_order_amounts_negative_price_treated_as_zero() {
404        // Negative price rounds to negative, which != 0.0, so division proceeds
405        // This documents the current behavior (no explicit rejection of negatives)
406        let (maker, taker) =
407            calculate_market_order_amounts(100.0, -0.5, OrderSide::Buy, TickSize::Hundredth);
408        // -0.5 rounds to -0.5, not zero, so division: 100 / -0.5 = -200
409        let taker_val: i64 = taker.parse().unwrap();
410        assert!(
411            taker_val < 0,
412            "Negative price produces negative taker amount"
413        );
414        // This reveals that callers MUST validate price > 0 before calling
415        assert_eq!(maker, "100000000");
416    }
417
418    #[test]
419    fn test_calculate_order_amounts_small_fractional_size() {
420        // Very small size to test precision isn't lost
421        let (maker, taker) =
422            calculate_order_amounts(0.50, 0.000001, OrderSide::Buy, TickSize::Hundredth);
423        // cost = 0.50 * 0.000001 = 0.0000005 → rounds to 0 at 6 decimals
424        assert_eq!(taker, "1"); // 0.000001 * 10^6 = 1
425        assert_eq!(maker, "0"); // 0.0000005 rounds to 0 at 6 decimals → 0
426    }
427
428    #[test]
429    fn test_calculate_market_price_exact_boundary() {
430        use rust_decimal_macros::dec;
431        // Amount exactly matches the sum at a level boundary
432        let levels = vec![
433            OrderLevel {
434                price: dec!(0.50),
435                size: dec!(100),
436            },
437            OrderLevel {
438                price: dec!(0.60),
439                size: dec!(100),
440            },
441        ];
442        // Buy: level 1 sum = 0.50*100 = 50. Exactly 50 requested → price = 0.50
443        let price = calculate_market_price(&levels, 50.0, OrderSide::Buy);
444        assert_eq!(price, Some(0.50));
445    }
446}