277 lines
7.5 KiB
Rust
277 lines
7.5 KiB
Rust
//! Market simulation.
|
|
//!
|
|
//! Generates price paths and handles order execution.
|
|
|
|
use algoarena_shared::{
|
|
AssetConfig, Execution, MarketIndicators, MarketState, Order, OrderSide, OrderType,
|
|
};
|
|
|
|
/// Market simulator.
|
|
#[derive(Debug)]
|
|
pub struct MarketSimulator {
|
|
/// Asset configurations.
|
|
assets: Vec<AssetConfig>,
|
|
/// Current prices.
|
|
prices: Vec<f64>,
|
|
/// Price history.
|
|
price_history: Vec<Vec<f64>>,
|
|
/// Current step.
|
|
step: usize,
|
|
/// RNG state.
|
|
rng: SimpleRng,
|
|
/// Transaction cost (basis points).
|
|
transaction_cost_bps: f64,
|
|
}
|
|
|
|
impl MarketSimulator {
|
|
/// Create a new market simulator.
|
|
#[must_use]
|
|
pub fn new(assets: Vec<AssetConfig>, transaction_cost_bps: f64, seed: u64) -> Self {
|
|
let prices: Vec<f64> = assets.iter().map(|a| a.initial_price).collect();
|
|
let price_history: Vec<Vec<f64>> = assets.iter().map(|a| vec![a.initial_price]).collect();
|
|
|
|
Self {
|
|
assets,
|
|
prices,
|
|
price_history,
|
|
step: 0,
|
|
rng: SimpleRng::new(seed),
|
|
transaction_cost_bps,
|
|
}
|
|
}
|
|
|
|
/// Advance one time step.
|
|
pub fn step(&mut self) {
|
|
self.step += 1;
|
|
|
|
for (i, asset) in self.assets.iter().enumerate() {
|
|
// Geometric Brownian Motion
|
|
let z = self.rng.normal();
|
|
let return_val = asset.drift + asset.volatility * z;
|
|
|
|
self.prices[i] *= 1.0 + return_val;
|
|
self.price_history[i].push(self.prices[i]);
|
|
}
|
|
}
|
|
|
|
/// Get current market state.
|
|
#[must_use]
|
|
pub fn get_state(&self) -> MarketState {
|
|
let volumes: Vec<f64> = self.prices.iter().map(|p| p * 1_000_000.0).collect();
|
|
|
|
// Calculate market indicators
|
|
let market_return = if self.step > 0 {
|
|
let initial: f64 = self.price_history.iter().map(|h| h[0]).sum();
|
|
let current: f64 = self.prices.iter().sum();
|
|
(current - initial) / initial * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
let volatility = self.calculate_realized_volatility();
|
|
let trend = self.calculate_trend();
|
|
|
|
MarketState {
|
|
step: self.step,
|
|
prices: self.prices.clone(),
|
|
price_history: self.price_history.clone(),
|
|
volumes,
|
|
indicators: MarketIndicators {
|
|
market_return,
|
|
volatility,
|
|
trend,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Execute an order.
|
|
#[must_use]
|
|
pub fn execute_order(&mut self, order: &Order) -> Execution {
|
|
let price = self.prices[order.asset_idx];
|
|
|
|
// Slippage based on order size (simplified)
|
|
let slippage_pct = 0.0005 * order.quantity / 100.0;
|
|
let slippage = price * slippage_pct;
|
|
|
|
let fill_price = match order.side {
|
|
OrderSide::Buy => price + slippage,
|
|
OrderSide::Sell => price - slippage,
|
|
};
|
|
|
|
// Check limit orders
|
|
let is_filled = match order.order_type {
|
|
OrderType::Market => true,
|
|
OrderType::Limit => match order.side {
|
|
OrderSide::Buy => order.limit_price.is_none_or(|limit| fill_price <= limit),
|
|
OrderSide::Sell => order.limit_price.is_none_or(|limit| fill_price >= limit),
|
|
},
|
|
};
|
|
|
|
let filled_quantity = if is_filled { order.quantity } else { 0.0 };
|
|
let transaction_cost = filled_quantity * fill_price * self.transaction_cost_bps / 10_000.0;
|
|
|
|
Execution {
|
|
order: order.clone(),
|
|
filled_quantity,
|
|
fill_price,
|
|
transaction_cost,
|
|
slippage,
|
|
is_filled,
|
|
}
|
|
}
|
|
|
|
/// Get current prices.
|
|
#[must_use]
|
|
pub fn prices(&self) -> &[f64] {
|
|
&self.prices
|
|
}
|
|
|
|
/// Get price history.
|
|
#[must_use]
|
|
pub fn price_history(&self) -> &[Vec<f64>] {
|
|
&self.price_history
|
|
}
|
|
|
|
/// Calculate realized volatility.
|
|
fn calculate_realized_volatility(&self) -> f64 {
|
|
if self.step < 2 {
|
|
return 0.0;
|
|
}
|
|
|
|
let lookback = self.step.min(20);
|
|
let mut returns = Vec::new();
|
|
|
|
for i in (self.step - lookback + 1)..=self.step {
|
|
let ret = (self.price_history[0][i] / self.price_history[0][i - 1]).ln();
|
|
returns.push(ret);
|
|
}
|
|
|
|
if returns.is_empty() {
|
|
return 0.0;
|
|
}
|
|
|
|
let mean: f64 = returns.iter().sum::<f64>() / returns.len() as f64;
|
|
let variance: f64 =
|
|
returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / returns.len() as f64;
|
|
|
|
variance.sqrt() * (252.0_f64).sqrt() * 100.0 // Annualized %
|
|
}
|
|
|
|
/// Calculate market trend.
|
|
fn calculate_trend(&self) -> f64 {
|
|
if self.step < 10 {
|
|
return 0.0;
|
|
}
|
|
|
|
// Short MA vs Long MA
|
|
let short_period = 10.min(self.step);
|
|
let long_period = 50.min(self.step);
|
|
|
|
let short_ma: f64 = self.price_history[0][(self.step - short_period + 1)..=self.step]
|
|
.iter()
|
|
.sum::<f64>()
|
|
/ short_period as f64;
|
|
|
|
let long_ma: f64 = self.price_history[0][(self.step - long_period + 1)..=self.step]
|
|
.iter()
|
|
.sum::<f64>()
|
|
/ long_period as f64;
|
|
|
|
(short_ma - long_ma) / long_ma * 100.0
|
|
}
|
|
}
|
|
|
|
/// Simple pseudo-random number generator.
|
|
struct SimpleRng {
|
|
state: u64,
|
|
}
|
|
|
|
impl std::fmt::Debug for SimpleRng {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("SimpleRng").finish()
|
|
}
|
|
}
|
|
|
|
impl SimpleRng {
|
|
fn new(seed: u64) -> Self {
|
|
Self { state: seed }
|
|
}
|
|
|
|
fn next(&mut self) -> u64 {
|
|
self.state = self
|
|
.state
|
|
.wrapping_mul(6364136223846793005)
|
|
.wrapping_add(1442695040888963407);
|
|
self.state
|
|
}
|
|
|
|
fn uniform(&mut self) -> f64 {
|
|
(self.next() >> 11) as f64 / (1u64 << 53) as f64
|
|
}
|
|
|
|
fn normal(&mut self) -> f64 {
|
|
let u1 = self.uniform() + 1e-10;
|
|
let u2 = self.uniform();
|
|
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use algoarena_shared::default_assets;
|
|
|
|
#[test]
|
|
fn test_market_creation() {
|
|
let assets = default_assets();
|
|
let market = MarketSimulator::new(assets.clone(), 10.0, 42);
|
|
|
|
assert_eq!(market.prices().len(), assets.len());
|
|
}
|
|
|
|
#[test]
|
|
fn test_market_step() {
|
|
let assets = default_assets();
|
|
let mut market = MarketSimulator::new(assets, 10.0, 42);
|
|
|
|
let initial_price = market.prices()[0];
|
|
market.step();
|
|
|
|
// Price should have changed
|
|
assert_ne!(market.prices()[0], initial_price);
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_state() {
|
|
let assets = default_assets();
|
|
let mut market = MarketSimulator::new(assets, 10.0, 42);
|
|
|
|
for _ in 0..10 {
|
|
market.step();
|
|
}
|
|
|
|
let state = market.get_state();
|
|
assert_eq!(state.step, 10);
|
|
assert_eq!(state.price_history[0].len(), 11); // Initial + 10 steps
|
|
}
|
|
|
|
#[test]
|
|
fn test_execute_order() {
|
|
let assets = default_assets();
|
|
let mut market = MarketSimulator::new(assets, 10.0, 42);
|
|
|
|
let order = Order {
|
|
agent_id: "test".to_string(),
|
|
asset_idx: 0,
|
|
side: OrderSide::Buy,
|
|
order_type: OrderType::Market,
|
|
quantity: 10.0,
|
|
limit_price: None,
|
|
};
|
|
|
|
let execution = market.execute_order(&order);
|
|
assert!(execution.is_filled);
|
|
assert_eq!(execution.filled_quantity, 10.0);
|
|
}
|
|
}
|