use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use std::collections::HashMap;
use crate::domain::Money;
use crate::exchange::{Instrument, InstrumentType, MarketData, MarketStatus};
#[derive(Debug, Clone)]
pub struct MarketState {
pub price: Decimal,
pub spread_percent: Decimal,
pub volume: Decimal,
pub status: MarketStatus,
pub last_update: DateTime<Utc>,
}
impl MarketState {
#[must_use]
pub fn new(price: Decimal) -> Self {
Self {
price,
spread_percent: Decimal::new(1, 1), volume: Decimal::new(1_000_000, 0),
status: MarketStatus::Open,
last_update: Utc::now(),
}
}
#[must_use]
pub const fn with_spread(mut self, spread_percent: Decimal) -> Self {
self.spread_percent = spread_percent;
self
}
#[must_use]
pub const fn with_status(mut self, status: MarketStatus) -> Self {
self.status = status;
self
}
#[must_use]
pub fn bid(&self) -> Decimal {
let half_spread = self.spread_percent / Decimal::from(200);
self.price * (Decimal::ONE - half_spread)
}
#[must_use]
pub fn ask(&self) -> Decimal {
let half_spread = self.spread_percent / Decimal::from(200);
self.price * (Decimal::ONE + half_spread)
}
pub fn set_price(&mut self, price: Decimal) {
self.price = price;
self.last_update = Utc::now();
}
#[must_use]
pub fn to_market_data(&self, symbol: &str, currency: &str) -> MarketData {
MarketData {
symbol: symbol.to_string(),
last_price: Money::new(self.price, currency),
bid: Some(Money::new(self.bid(), currency)),
ask: Some(Money::new(self.ask(), currency)),
volume_24h: Some(self.volume),
high_24h: Some(Money::new(self.price * Decimal::new(102, 2), currency)),
low_24h: Some(Money::new(self.price * Decimal::new(98, 2), currency)),
change_24h: None,
change_percent_24h: None,
market_status: self.status,
timestamp: self.last_update,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum PriceModel {
Static,
RandomWalk {
volatility: Decimal,
},
MeanReverting {
mean: Decimal,
reversion_speed: Decimal,
volatility: Decimal,
},
Trending {
drift: Decimal,
volatility: Decimal,
},
}
#[derive(Debug)]
pub struct PriceGenerator {
model: PriceModel,
rng_seed: u64,
step_count: u64,
}
impl PriceGenerator {
#[must_use]
pub const fn new(model: PriceModel) -> Self {
Self {
model,
rng_seed: 42,
step_count: 0,
}
}
#[must_use]
pub const fn with_seed(mut self, seed: u64) -> Self {
self.rng_seed = seed;
self
}
#[must_use]
pub fn next_price(&mut self, current: Decimal) -> Decimal {
self.step_count += 1;
match self.model {
PriceModel::Static => current,
PriceModel::RandomWalk { volatility } => {
let random = self.pseudo_random();
let change = volatility * random / Decimal::from(100);
current * (Decimal::ONE + change)
}
PriceModel::MeanReverting {
mean,
reversion_speed,
volatility,
} => {
let random = self.pseudo_random();
let noise = volatility * random / Decimal::from(100);
let reversion = reversion_speed * (mean - current) / Decimal::from(100);
current + reversion + current * noise
}
PriceModel::Trending { drift, volatility } => {
let random = self.pseudo_random();
let noise = volatility * random / Decimal::from(100);
let trend = drift / Decimal::from(100);
current * (Decimal::ONE + trend + noise)
}
}
}
fn pseudo_random(&self) -> Decimal {
let seed = self
.rng_seed
.wrapping_mul(self.step_count)
.wrapping_add(12345);
let value = (seed % 2000) as i64 - 1000;
Decimal::new(value, 3) }
}
#[derive(Debug, Default)]
pub struct MarketManager {
states: HashMap<String, MarketState>,
instruments: HashMap<String, Instrument>,
generators: HashMap<String, PriceGenerator>,
base_currency: String,
}
impl MarketManager {
#[must_use]
pub fn new(base_currency: impl Into<String>) -> Self {
Self {
states: HashMap::new(),
instruments: HashMap::new(),
generators: HashMap::new(),
base_currency: base_currency.into(),
}
}
pub fn add_instrument(
&mut self,
symbol: impl Into<String>,
name: impl Into<String>,
price: Decimal,
lot_size: u32,
) {
let symbol = symbol.into();
let name = name.into();
let instrument = Instrument {
symbol: symbol.clone(),
name,
exchange_id: format!("SIM_{symbol}"),
exchange: "SIMULATOR".to_string(),
instrument_type: InstrumentType::Stock,
currency: self.base_currency.clone(),
lot_size,
min_price_increment: Decimal::new(1, 2),
min_quantity: 1,
tradeable: true,
margin_available: false,
short_available: false,
};
let state = MarketState::new(price);
self.instruments.insert(symbol.clone(), instrument);
self.states.insert(symbol.clone(), state);
self.generators
.insert(symbol, PriceGenerator::new(PriceModel::Static));
}
pub fn set_price_model(&mut self, symbol: &str, model: PriceModel) {
self.generators
.insert(symbol.to_string(), PriceGenerator::new(model));
}
#[must_use]
pub fn get_state(&self, symbol: &str) -> Option<&MarketState> {
self.states.get(symbol)
}
#[must_use]
pub fn get_instrument(&self, symbol: &str) -> Option<&Instrument> {
self.instruments.get(symbol)
}
#[must_use]
pub fn get_market_data(&self, symbol: &str) -> Option<MarketData> {
let state = self.states.get(symbol)?;
Some(state.to_market_data(symbol, &self.base_currency))
}
#[must_use]
pub fn list_instruments(&self) -> Vec<Instrument> {
self.instruments.values().cloned().collect()
}
pub fn tick(&mut self) {
let symbols: Vec<String> = self.states.keys().cloned().collect();
for symbol in symbols {
if let Some(generator) = self.generators.get_mut(&symbol) {
if let Some(state) = self.states.get_mut(&symbol) {
let new_price = generator.next_price(state.price);
state.set_price(new_price);
}
}
}
}
pub fn set_market_status(&mut self, status: MarketStatus) {
for state in self.states.values_mut() {
state.status = status;
}
}
pub fn set_price(&mut self, symbol: &str, price: Decimal) {
if let Some(state) = self.states.get_mut(symbol) {
state.set_price(price);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_market_state_spread() {
let state = MarketState::new(dec!(100)).with_spread(dec!(0.2));
let bid = state.bid();
let ask = state.ask();
assert!(bid < dec!(100));
assert!(ask > dec!(100));
assert!((ask - bid) / dec!(100) * dec!(100) < dec!(0.21)); }
#[test]
fn test_price_generator_static() {
let mut gen = PriceGenerator::new(PriceModel::Static);
let price = dec!(100);
for _ in 0..10 {
let next = gen.next_price(price);
assert_eq!(next, price);
}
}
#[test]
fn test_price_generator_random_walk() {
let mut gen = PriceGenerator::new(PriceModel::RandomWalk {
volatility: dec!(2),
});
let mut price = dec!(100);
let mut changed = false;
for _ in 0..10 {
let next = gen.next_price(price);
if next != price {
changed = true;
}
price = next;
}
assert!(changed);
}
#[test]
fn test_market_manager() {
let mut manager = MarketManager::new("USD");
manager.add_instrument("AAPL", "Apple Inc.", dec!(150), 1);
manager.add_instrument("GOOGL", "Alphabet Inc.", dec!(2800), 1);
assert_eq!(manager.list_instruments().len(), 2);
let state = manager.get_state("AAPL").unwrap();
assert_eq!(state.price, dec!(150));
let data = manager.get_market_data("AAPL").unwrap();
assert_eq!(data.symbol, "AAPL");
assert_eq!(data.last_price.amount(), dec!(150));
}
#[test]
fn test_market_manager_tick() {
let mut manager = MarketManager::new("USD");
manager.add_instrument("TEST", "Test Stock", dec!(100), 1);
manager.set_price_model(
"TEST",
PriceModel::RandomWalk {
volatility: dec!(5),
},
);
let _initial = manager.get_state("TEST").unwrap().price;
manager.tick();
manager.tick();
manager.tick();
let final_price = manager.get_state("TEST").unwrap().price;
assert!(final_price > Decimal::ZERO);
}
}