brokapi_runtime/
price_grid.rs1use std::collections::HashMap;
4use std::sync::{Arc, RwLock};
5
6use brokapi_core::{Error, InstrumentKey, Price, Qty, TradingParams, check_price_step};
7
8#[derive(Debug, Clone, Default)]
43pub struct PriceGrid {
44 steps: Arc<RwLock<HashMap<InstrumentKey, Price>>>,
45 lots: Arc<RwLock<HashMap<InstrumentKey, Qty>>>,
46}
47
48impl PriceGrid {
49 pub fn set(&self, key: InstrumentKey, step: Price) {
51 let mut steps = self.steps.write().unwrap_or_else(|e| e.into_inner());
52 if step.is_positive() {
53 steps.insert(key, step);
54 } else {
55 steps.remove(&key);
56 }
57 }
58
59 pub fn learn(&self, params: &TradingParams) {
61 self.set(params.instrument.key.clone(), params.price_step);
62 self.set_lot(params.instrument.key.clone(), params.lot_size);
63 }
64
65 pub fn set_lot(&self, key: InstrumentKey, lot: Qty) {
67 let mut lots = self.lots.write().unwrap_or_else(|e| e.into_inner());
68 if lot.is_positive() {
69 lots.insert(key, lot);
70 } else {
71 lots.remove(&key);
72 }
73 }
74
75 pub fn lot(&self, key: &InstrumentKey) -> Option<Qty> {
77 self.lots
78 .read()
79 .unwrap_or_else(|e| e.into_inner())
80 .get(key)
81 .copied()
82 }
83
84 pub fn check_qty(&self, key: &InstrumentKey, qty: Qty) -> Result<(), Error> {
91 if !qty.is_positive() {
92 return Err(Error::Rejected {
93 code: "invalid_qty".into(),
94 message: format!("quantity must be positive, got {qty}"),
95 });
96 }
97 match self.lot(key) {
98 Some(lot) if !qty.is_on_step(lot) => Err(Error::Rejected {
99 code: "invalid_qty".into(),
100 message: format!("quantity {qty} is not a whole number of lots of {lot}"),
101 }),
102 _ => Ok(()),
103 }
104 }
105
106 pub fn step(&self, key: &InstrumentKey) -> Option<Price> {
108 self.steps
109 .read()
110 .unwrap_or_else(|e| e.into_inner())
111 .get(key)
112 .copied()
113 }
114
115 pub fn check(&self, key: &InstrumentKey, price: Price) -> Result<(), Error> {
122 match self.step(key) {
123 Some(step) => check_price_step(price, step),
124 None => Ok(()),
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use brokapi_core::{Decimal, dec};
133
134 #[test]
135 fn unknown_step_passes_and_zero_forgets() {
136 let grid = PriceGrid::default();
137 let k = InstrumentKey::new("SBER", "TQBR");
138 assert!(grid.check(&k, Price::new(dec!(1.234))).is_ok());
139 grid.set(k.clone(), Price::new(dec!(0.05)));
140 assert!(grid.check(&k, Price::new(dec!(1.23))).is_err());
141 grid.set(k.clone(), Price::ZERO);
142 assert!(grid.check(&k, Price::new(dec!(1.23))).is_ok());
143 }
144
145 #[test]
146 fn quantity_is_checked_against_the_lot_when_it_is_known() {
147 let grid = PriceGrid::default();
148 let k = InstrumentKey::new("SBER", "TQBR");
149 assert!(grid.check_qty(&k, Qty::from(3)).is_ok());
151 assert!(grid.check_qty(&k, Qty::ZERO).is_err());
152 assert!(grid.check_qty(&k, Qty::from(-1)).is_err());
153 grid.set_lot(k.clone(), Qty::from(10));
154 assert_eq!(grid.lot(&k), Some(Qty::from(10)));
155 assert!(grid.check_qty(&k, Qty::from(30)).is_ok());
156 let err = grid.check_qty(&k, Qty::from(25)).unwrap_err();
157 assert!(matches!(err, Error::Rejected { ref code, .. } if code == "invalid_qty"));
158 assert!(grid.check_qty(&k, Qty::new(dec!(0.5))).is_err());
159 grid.set_lot(k.clone(), Qty::ZERO);
160 assert_eq!(grid.lot(&k), None);
161 assert!(grid.check_qty(&k, Qty::from(25)).is_ok());
162 }
163
164 #[test]
165 fn learn_takes_step_and_lot_from_the_trading_params() {
166 use brokapi_core::{InstrumentId, Money, NativeId, TradingParams};
167 let k = InstrumentKey::new("SBER", "TQBR");
168 let params = TradingParams {
169 instrument: InstrumentId::new(
170 k.clone(),
171 NativeId::TickerClass {
172 ticker: "SBER".into(),
173 class_code: "TQBR".into(),
174 },
175 ),
176 lot_size: Qty::from(10),
177 min_qty: Qty::from(10),
178 price_step: Price::new(dec!(0.01)),
179 price_step_value: Money::new(dec!(0.01)),
180 currency: "RUB".into(),
181 };
182 let grid = PriceGrid::default();
183 grid.learn(¶ms);
184 assert_eq!(grid.step(&k), Some(Price::new(dec!(0.01))));
185 assert_eq!(grid.lot(&k), Some(Qty::from(10)));
186 }
187
188 #[test]
189 fn clones_share_one_grid() {
190 let a = PriceGrid::default();
191 let b = a.clone();
192 let k = InstrumentKey::new("GAZP", "TQBR");
193 b.set(k.clone(), Price::new(dec!(0.01)));
194 assert_eq!(a.step(&k), Some(Price::new(dec!(0.01))));
195 }
196}