Skip to main content

trader_bot/simulator/
market.rs

1//! Market state and price simulation.
2
3use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use std::collections::HashMap;
6
7use crate::domain::Money;
8use crate::exchange::{Instrument, InstrumentType, MarketData, MarketStatus};
9
10/// Simulated market state for a single instrument.
11#[derive(Debug, Clone)]
12pub struct MarketState {
13    /// Current price.
14    pub price: Decimal,
15    /// Bid-ask spread as a percentage.
16    pub spread_percent: Decimal,
17    /// 24h volume.
18    pub volume: Decimal,
19    /// Current market status.
20    pub status: MarketStatus,
21    /// Last update time.
22    pub last_update: DateTime<Utc>,
23}
24
25impl MarketState {
26    /// Creates a new market state.
27    #[must_use]
28    pub fn new(price: Decimal) -> Self {
29        Self {
30            price,
31            spread_percent: Decimal::new(1, 1), // 0.1% default spread
32            volume: Decimal::new(1_000_000, 0),
33            status: MarketStatus::Open,
34            last_update: Utc::now(),
35        }
36    }
37
38    /// Sets the spread percentage.
39    #[must_use]
40    pub const fn with_spread(mut self, spread_percent: Decimal) -> Self {
41        self.spread_percent = spread_percent;
42        self
43    }
44
45    /// Sets the market status.
46    #[must_use]
47    pub const fn with_status(mut self, status: MarketStatus) -> Self {
48        self.status = status;
49        self
50    }
51
52    /// Calculates the bid price.
53    #[must_use]
54    pub fn bid(&self) -> Decimal {
55        let half_spread = self.spread_percent / Decimal::from(200);
56        self.price * (Decimal::ONE - half_spread)
57    }
58
59    /// Calculates the ask price.
60    #[must_use]
61    pub fn ask(&self) -> Decimal {
62        let half_spread = self.spread_percent / Decimal::from(200);
63        self.price * (Decimal::ONE + half_spread)
64    }
65
66    /// Updates the price.
67    pub fn set_price(&mut self, price: Decimal) {
68        self.price = price;
69        self.last_update = Utc::now();
70    }
71
72    /// Converts to MarketData.
73    #[must_use]
74    pub fn to_market_data(&self, symbol: &str, currency: &str) -> MarketData {
75        MarketData {
76            symbol: symbol.to_string(),
77            last_price: Money::new(self.price, currency),
78            bid: Some(Money::new(self.bid(), currency)),
79            ask: Some(Money::new(self.ask(), currency)),
80            volume_24h: Some(self.volume),
81            high_24h: Some(Money::new(self.price * Decimal::new(102, 2), currency)),
82            low_24h: Some(Money::new(self.price * Decimal::new(98, 2), currency)),
83            change_24h: None,
84            change_percent_24h: None,
85            market_status: self.status,
86            timestamp: self.last_update,
87        }
88    }
89}
90
91/// Price model for simulation.
92#[derive(Debug, Clone, Copy)]
93pub enum PriceModel {
94    /// Static price (no changes).
95    Static,
96    /// Random walk with given volatility (daily % std dev).
97    RandomWalk {
98        /// Volatility as percentage standard deviation per tick.
99        volatility: Decimal,
100    },
101    /// Mean reverting with given parameters.
102    MeanReverting {
103        /// Long-term mean price to revert to.
104        mean: Decimal,
105        /// Speed of reversion (0-1, higher = faster reversion).
106        reversion_speed: Decimal,
107        /// Random volatility component.
108        volatility: Decimal,
109    },
110    /// Trending with given drift.
111    Trending {
112        /// Drift rate per tick (positive = upward trend).
113        drift: Decimal,
114        /// Random volatility component.
115        volatility: Decimal,
116    },
117}
118
119/// Price generator for simulating market movements.
120#[derive(Debug)]
121pub struct PriceGenerator {
122    model: PriceModel,
123    rng_seed: u64,
124    step_count: u64,
125}
126
127impl PriceGenerator {
128    /// Creates a new price generator.
129    #[must_use]
130    pub const fn new(model: PriceModel) -> Self {
131        Self {
132            model,
133            rng_seed: 42,
134            step_count: 0,
135        }
136    }
137
138    /// Sets the random seed.
139    #[must_use]
140    pub const fn with_seed(mut self, seed: u64) -> Self {
141        self.rng_seed = seed;
142        self
143    }
144
145    /// Generates the next price given the current price.
146    #[must_use]
147    pub fn next_price(&mut self, current: Decimal) -> Decimal {
148        self.step_count += 1;
149
150        match self.model {
151            PriceModel::Static => current,
152            PriceModel::RandomWalk { volatility } => {
153                let random = self.pseudo_random();
154                let change = volatility * random / Decimal::from(100);
155                current * (Decimal::ONE + change)
156            }
157            PriceModel::MeanReverting {
158                mean,
159                reversion_speed,
160                volatility,
161            } => {
162                let random = self.pseudo_random();
163                let noise = volatility * random / Decimal::from(100);
164                let reversion = reversion_speed * (mean - current) / Decimal::from(100);
165                current + reversion + current * noise
166            }
167            PriceModel::Trending { drift, volatility } => {
168                let random = self.pseudo_random();
169                let noise = volatility * random / Decimal::from(100);
170                let trend = drift / Decimal::from(100);
171                current * (Decimal::ONE + trend + noise)
172            }
173        }
174    }
175
176    /// Simple pseudo-random number generator (-1 to 1).
177    fn pseudo_random(&self) -> Decimal {
178        // Simple LCG-based PRNG
179        let seed = self
180            .rng_seed
181            .wrapping_mul(self.step_count)
182            .wrapping_add(12345);
183        let value = (seed % 2000) as i64 - 1000;
184        Decimal::new(value, 3) // -1.000 to 1.000
185    }
186}
187
188/// Manager for multiple market states.
189#[derive(Debug, Default)]
190pub struct MarketManager {
191    states: HashMap<String, MarketState>,
192    instruments: HashMap<String, Instrument>,
193    generators: HashMap<String, PriceGenerator>,
194    base_currency: String,
195}
196
197impl MarketManager {
198    /// Creates a new market manager.
199    #[must_use]
200    pub fn new(base_currency: impl Into<String>) -> Self {
201        Self {
202            states: HashMap::new(),
203            instruments: HashMap::new(),
204            generators: HashMap::new(),
205            base_currency: base_currency.into(),
206        }
207    }
208
209    /// Adds an instrument with initial price.
210    pub fn add_instrument(
211        &mut self,
212        symbol: impl Into<String>,
213        name: impl Into<String>,
214        price: Decimal,
215        lot_size: u32,
216    ) {
217        let symbol = symbol.into();
218        let name = name.into();
219
220        let instrument = Instrument {
221            symbol: symbol.clone(),
222            name,
223            exchange_id: format!("SIM_{symbol}"),
224            exchange: "SIMULATOR".to_string(),
225            instrument_type: InstrumentType::Stock,
226            currency: self.base_currency.clone(),
227            lot_size,
228            min_price_increment: Decimal::new(1, 2),
229            min_quantity: 1,
230            tradeable: true,
231            margin_available: false,
232            short_available: false,
233        };
234
235        let state = MarketState::new(price);
236
237        self.instruments.insert(symbol.clone(), instrument);
238        self.states.insert(symbol.clone(), state);
239        self.generators
240            .insert(symbol, PriceGenerator::new(PriceModel::Static));
241    }
242
243    /// Sets the price model for an instrument.
244    pub fn set_price_model(&mut self, symbol: &str, model: PriceModel) {
245        self.generators
246            .insert(symbol.to_string(), PriceGenerator::new(model));
247    }
248
249    /// Gets the current market state.
250    #[must_use]
251    pub fn get_state(&self, symbol: &str) -> Option<&MarketState> {
252        self.states.get(symbol)
253    }
254
255    /// Gets an instrument.
256    #[must_use]
257    pub fn get_instrument(&self, symbol: &str) -> Option<&Instrument> {
258        self.instruments.get(symbol)
259    }
260
261    /// Gets market data for an instrument.
262    #[must_use]
263    pub fn get_market_data(&self, symbol: &str) -> Option<MarketData> {
264        let state = self.states.get(symbol)?;
265        Some(state.to_market_data(symbol, &self.base_currency))
266    }
267
268    /// Lists all instruments.
269    #[must_use]
270    pub fn list_instruments(&self) -> Vec<Instrument> {
271        self.instruments.values().cloned().collect()
272    }
273
274    /// Updates prices by one time step.
275    pub fn tick(&mut self) {
276        let symbols: Vec<String> = self.states.keys().cloned().collect();
277        for symbol in symbols {
278            if let Some(generator) = self.generators.get_mut(&symbol) {
279                if let Some(state) = self.states.get_mut(&symbol) {
280                    let new_price = generator.next_price(state.price);
281                    state.set_price(new_price);
282                }
283            }
284        }
285    }
286
287    /// Sets the market status for all instruments.
288    pub fn set_market_status(&mut self, status: MarketStatus) {
289        for state in self.states.values_mut() {
290            state.status = status;
291        }
292    }
293
294    /// Sets the price for a specific instrument.
295    pub fn set_price(&mut self, symbol: &str, price: Decimal) {
296        if let Some(state) = self.states.get_mut(symbol) {
297            state.set_price(price);
298        }
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use rust_decimal_macros::dec;
306
307    #[test]
308    fn test_market_state_spread() {
309        let state = MarketState::new(dec!(100)).with_spread(dec!(0.2)); // 0.2% spread
310
311        let bid = state.bid();
312        let ask = state.ask();
313
314        assert!(bid < dec!(100));
315        assert!(ask > dec!(100));
316        assert!((ask - bid) / dec!(100) * dec!(100) < dec!(0.21)); // Spread is ~0.2%
317    }
318
319    #[test]
320    fn test_price_generator_static() {
321        let mut gen = PriceGenerator::new(PriceModel::Static);
322        let price = dec!(100);
323
324        for _ in 0..10 {
325            let next = gen.next_price(price);
326            assert_eq!(next, price);
327        }
328    }
329
330    #[test]
331    fn test_price_generator_random_walk() {
332        let mut gen = PriceGenerator::new(PriceModel::RandomWalk {
333            volatility: dec!(2),
334        });
335        let mut price = dec!(100);
336
337        // Price should change
338        let mut changed = false;
339        for _ in 0..10 {
340            let next = gen.next_price(price);
341            if next != price {
342                changed = true;
343            }
344            price = next;
345        }
346        assert!(changed);
347    }
348
349    #[test]
350    fn test_market_manager() {
351        let mut manager = MarketManager::new("USD");
352        manager.add_instrument("AAPL", "Apple Inc.", dec!(150), 1);
353        manager.add_instrument("GOOGL", "Alphabet Inc.", dec!(2800), 1);
354
355        assert_eq!(manager.list_instruments().len(), 2);
356
357        let state = manager.get_state("AAPL").unwrap();
358        assert_eq!(state.price, dec!(150));
359
360        let data = manager.get_market_data("AAPL").unwrap();
361        assert_eq!(data.symbol, "AAPL");
362        assert_eq!(data.last_price.amount(), dec!(150));
363    }
364
365    #[test]
366    fn test_market_manager_tick() {
367        let mut manager = MarketManager::new("USD");
368        manager.add_instrument("TEST", "Test Stock", dec!(100), 1);
369        manager.set_price_model(
370            "TEST",
371            PriceModel::RandomWalk {
372                volatility: dec!(5),
373            },
374        );
375
376        let _initial = manager.get_state("TEST").unwrap().price;
377        manager.tick();
378        manager.tick();
379        manager.tick();
380
381        // Price should have changed (with high probability)
382        let final_price = manager.get_state("TEST").unwrap().price;
383        // Note: With the deterministic PRNG, we can't guarantee the price changed
384        // but we can verify the mechanism works
385        assert!(final_price > Decimal::ZERO);
386    }
387}