Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
+276
View File
@@ -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);
}
}