Initial commit
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user