Created
October 8, 2025 12:07
-
-
Save yenw0d/bfeedf3702e579fe10c1b6b3d4a71767 to your computer and use it in GitHub Desktop.
Generator for https://highload.fun/tasks/24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Simulation parameters | |
| const ACCOUNTS_TARGET: usize = 20_000; | |
| const INSTRUMENTS_TARGET: usize = 15; | |
| // number of events to stdout before the final line | |
| const TICKS_GENERATED_COUNT: u64 = 1_000_000 - 1; | |
| // Trading constraints | |
| const TRADE_MAX_SZ: u64 = 1_000; | |
| const TRADE_MAX_PX: u64 = 1_000_000; | |
| const TRADE_MIN_PX: u64 = 100; | |
| const MAINTENANCE_MARGIN_PERCENTAGE: i64 = 1; | |
| // Ensure arithmetic won't overflow for the chosen params | |
| const _: () = { | |
| let worst_case_number: u128 = (TRADE_MAX_SZ as u128) // iterated max size trades | |
| .saturating_mul(TICKS_GENERATED_COUNT as u128) // one per tick | |
| .saturating_mul(TRADE_MAX_PX as u128) // notional exposure at max price | |
| .saturating_mul(100 as u128) // we multiply by 100 in margin calcs | |
| .saturating_mul(2 as u128); // for good measure | |
| assert!(worst_case_number < i64::MAX as u128, "fits in i64"); | |
| }; | |
| fn main() { | |
| let (accounts_target, instruments_target, ticks_generated_count) = parse_cli_overrides(); | |
| run(accounts_target, instruments_target, ticks_generated_count); | |
| } | |
| fn run(accounts_target: usize, instruments_target: usize, ticks_generated_count: u64) { | |
| let mut g = Generator { | |
| prng: rand::Prng::new_from_urandom(), | |
| state: solve::State::new(), | |
| ticks_generated_count, | |
| ticks: 0, | |
| }; | |
| const MAX_ITERATIONS: u32 = 10_000_000; | |
| for _ in 0..MAX_ITERATIONS { | |
| if g.ticks >= g.ticks_generated_count { | |
| break; | |
| } | |
| // add new accounts and instruments probabilistically | |
| if g.chance(g.state.accounts.len(), accounts_target) { | |
| g.add_account(); | |
| continue; | |
| } | |
| if g.chance(g.state.prices.len(), instruments_target) { | |
| g.add_instrument(); | |
| continue; | |
| } | |
| // need both accounts and instruments to trade | |
| if g.state.prices.is_empty() || g.state.accounts.is_empty() { | |
| continue; | |
| } | |
| // execute trades and update prices for traded instruments | |
| while g.ratio(1, 10) { | |
| let instrument = (g.prng.rand() * g.prng.rand() * g.state.prices.len() as f64) as usize; | |
| let mut num_trades = 0; | |
| while g.ratio(1, 3) { | |
| if g.execute_trade(instrument) { | |
| num_trades += 1; | |
| } | |
| } | |
| if g.ratio(num_trades.min(4), 5) { | |
| g.update_price(0.001, instrument); | |
| } | |
| } | |
| // fairly often, move the prices on random instruments | |
| if g.ratio(1, 10) { | |
| for instrument in 0..g.state.prices.len() { | |
| if g.ratio(1, 10) { | |
| g.update_price(0.001, instrument); | |
| } | |
| } | |
| } | |
| // very rarely, "jump" the prices on a lot of instruments | |
| if g.ratio(1, 50_000) { | |
| for instrument in 0..g.state.prices.len() { | |
| if g.ratio(1, 5) { | |
| g.update_price(0.05, instrument); | |
| } | |
| } | |
| } | |
| } | |
| // pick a random account, print its equity and notional | |
| let idx = (g.prng.rand() * g.state.accounts.len() as f64) as usize; | |
| println!("{}", idx); | |
| let account = &g.state.accounts[idx]; | |
| let (equity, notional) = account.equity_and_notional(); | |
| eprintln!("{} {}", equity, notional); | |
| } | |
| struct Generator { | |
| prng: rand::Prng, | |
| state: solve::State, | |
| ticks_generated_count: u64, | |
| ticks: u64, | |
| } | |
| impl Generator { | |
| pub fn add_account(&mut self) { | |
| let balance = self.prng.norm(1_000_000.0, 200_000.0).max(10_000.0) as i64; | |
| self.state.add_account(balance); | |
| println!("a {}", balance); | |
| self.ticks += 1; | |
| } | |
| pub fn add_instrument(&mut self) { | |
| let price = self.prng.norm(10_000.0, 5_000.0).max(1_000.0) as u64; | |
| let instrument = self.state.add_instrument(price); | |
| println!("p {} {}", instrument, price); | |
| self.ticks += 1; | |
| } | |
| pub fn update_price(&mut self, volatility: f64, instrument: usize) { | |
| let old_price = self.state.prices[instrument]; | |
| let returns = self.prng.norm(0.0, volatility); | |
| let new_price = (((old_price as f64) * (1.0 + returns)) as u64) | |
| .max(TRADE_MIN_PX) | |
| .min(TRADE_MAX_PX); | |
| self.state.update_price(instrument, new_price); | |
| println!("p {} {}", instrument, new_price); | |
| self.ticks += 1; | |
| // process any liquidations triggered by price change | |
| for (account_idx, equity, notional) in self.state.liquidatable(instrument) { | |
| eprintln!("liquidate {} {} {}", account_idx, equity, notional); | |
| for h in self.state.holders.iter_mut() { | |
| h.remove(&account_idx); | |
| } | |
| self.state.accounts[account_idx] = solve::Account::new(0); | |
| } | |
| } | |
| pub fn execute_trade(&mut self, instrument: usize) -> bool { | |
| let mut account_idx = (self.prng.rand() * self.state.accounts.len() as f64) as usize; | |
| // try up to 10 accounts to find one that can trade | |
| for _ in 0..10 { | |
| account_idx = (account_idx + 1) % self.state.accounts.len(); | |
| if let Some(trade_size) = self.calculate_trade_size(account_idx, instrument) { | |
| println!("t {} {} {}", account_idx, instrument, trade_size); | |
| self.state.trade(account_idx, instrument, trade_size); | |
| self.ticks += 1; | |
| return true; | |
| } | |
| } | |
| false | |
| } | |
| fn calculate_trade_size(&mut self, account_idx: usize, instrument: usize) -> Option<i64> { | |
| let account = &mut self.state.accounts[account_idx]; | |
| account | |
| .positions | |
| .resize_with(self.state.prices.len(), || solve::Position::default()); | |
| // calculate maximum affordable trade size | |
| let (equity, notional) = account.equity_and_notional(); | |
| let free_collateral = equity - (notional * MAINTENANCE_MARGIN_PERCENTAGE / 100); | |
| let trade_ntl_max = free_collateral * 100 / MAINTENANCE_MARGIN_PERCENTAGE; | |
| let trade_sz_max = (trade_ntl_max / self.state.prices[instrument] as i64) | |
| .max(0) | |
| .min(TRADE_MAX_SZ as i64) as u64; | |
| // if the instrument is too expensive, see if we have an opposing position to reduce | |
| if trade_sz_max == 0 { | |
| let pos_size = account | |
| .positions | |
| .get(instrument) | |
| .map(|p| p.size) | |
| .unwrap_or(0); | |
| if pos_size != 0 { | |
| return Some(-pos_size); | |
| } | |
| return None; | |
| } | |
| // Generate random trade size with cubic distribution (favors smaller trades) | |
| let size_pct = (self.prng.rand() * self.prng.rand() * self.prng.rand() * 100.0) as u64; | |
| let direction = if self.prng.ratio(1, 2) { -1 } else { 1 }; | |
| let size = (trade_sz_max * size_pct / 100) as i64 * direction; | |
| Some(size) | |
| } | |
| fn chance(&mut self, have: usize, target: usize) -> bool { | |
| self.ratio(1, if have < target { 2 } else { 10_000 }) | |
| } | |
| fn ratio(&mut self, numerator: u64, denominator: u64) -> bool { | |
| self.prng.ratio(numerator, denominator) && self.ticks < self.ticks_generated_count | |
| } | |
| } | |
| fn parse_cli_overrides() -> (usize, usize, u64) { | |
| fn usage_and_exit() -> ! { | |
| eprintln!( | |
| "Usage: gen [ACCOUNTS INSTRUMENTS TICKS]\n\nDefaults (no args): accounts={} instruments={} ticks={}\n", | |
| ACCOUNTS_TARGET, INSTRUMENTS_TARGET, TICKS_GENERATED_COUNT | |
| ); | |
| std::process::exit(1) | |
| } | |
| let args = std::env::args().skip(1).collect::<Vec<String>>(); | |
| if args.is_empty() { | |
| return (ACCOUNTS_TARGET, INSTRUMENTS_TARGET, TICKS_GENERATED_COUNT); | |
| } | |
| if args.len() == 3 { | |
| let a = args[0] | |
| .parse::<usize>() | |
| .unwrap_or_else(|_| usage_and_exit()); | |
| let i = args[1] | |
| .parse::<usize>() | |
| .unwrap_or_else(|_| usage_and_exit()); | |
| let t = args[2].parse::<u64>().unwrap_or_else(|_| usage_and_exit()); | |
| return (a, i, t); | |
| } | |
| usage_and_exit() | |
| } | |
| mod rand { | |
| /// A simple pseudorandom number generator using Xorshift64* | |
| pub struct Prng { | |
| seed: u64, | |
| gauss_cached: Option<f64>, | |
| } | |
| impl Prng { | |
| pub fn new(seed: u64) -> Self { | |
| let seed = if seed == 0 { 1 } else { seed }; | |
| Self { | |
| seed, | |
| gauss_cached: None, | |
| } | |
| } | |
| pub fn new_from_urandom() -> Self { | |
| let mut seed_bytes = [0u8; 8]; | |
| std::fs::File::open("/dev/urandom") | |
| .and_then(|mut f| std::io::Read::read_exact(&mut f, &mut seed_bytes)) | |
| .expect("Failed to get random seed from OS"); | |
| let seed = u64::from_ne_bytes(seed_bytes); | |
| Self::new(seed) | |
| } | |
| /// Generate next u64 using Xorshift64* | |
| pub fn next_u64(&mut self) -> u64 { | |
| let mut x = self.seed; | |
| x ^= x >> 12; | |
| x ^= x << 25; | |
| x ^= x >> 27; | |
| self.seed = x; | |
| x.wrapping_mul(0x2545F4914F6CDD1D) | |
| } | |
| /// Generate f64 in [0, 1) | |
| pub fn rand(&mut self) -> f64 { | |
| const SCALE: f64 = (1u64 << 53) as f64; | |
| let r = self.next_u64() >> 11; | |
| (r as f64) / SCALE | |
| } | |
| /// Generate normal distribution using Box-Muller transform | |
| pub fn norm(&mut self, mean: f64, stddev: f64) -> f64 { | |
| assert!(stddev >= 0.0); | |
| if stddev == 0.0 { | |
| return mean; | |
| } | |
| // Use cached deviate if available | |
| if let Some(z1) = self.gauss_cached.take() { | |
| return mean + stddev * z1; | |
| } | |
| // Generate two independent standard normals; cache one | |
| loop { | |
| let u = 2.0 * self.rand() - 1.0; | |
| let v = 2.0 * self.rand() - 1.0; | |
| let s = u * u + v * v; | |
| if s == 0.0 || s >= 1.0 { | |
| continue; | |
| } | |
| let m = (-2.0 * s.ln() / s).sqrt(); | |
| let z0 = u * m; | |
| let z1 = v * m; | |
| self.gauss_cached = Some(z1); | |
| return mean + stddev * z0; | |
| } | |
| } | |
| /// Return true with probability numerator/denominator | |
| pub fn ratio(&mut self, numerator: u64, denominator: u64) -> bool { | |
| assert!(denominator > 0); | |
| assert!(numerator < denominator); | |
| let r = self.next_u64() as u128; | |
| let threshold = ((numerator as u128) << 64) / (denominator as u128); | |
| r < threshold | |
| } | |
| } | |
| } | |
| mod solve { | |
| use std::collections::HashSet; | |
| /// Trading simulation state | |
| pub struct State { | |
| pub accounts: Vec<Account>, | |
| pub prices: Vec<u64>, | |
| /// accound ids indexed by instrument | |
| pub holders: Vec<HashSet<usize>>, | |
| } | |
| pub struct Account { | |
| pub cash_value: i64, | |
| pub pos_value: i64, | |
| pub positions: Vec<Position>, | |
| pub cached_notional: i64, | |
| } | |
| #[derive(Default)] | |
| pub struct Position { | |
| pub size: i64, | |
| pub total_paid: i64, | |
| pub price: u64, | |
| } | |
| impl State { | |
| pub fn new() -> Self { | |
| Self { | |
| accounts: Vec::new(), | |
| prices: Vec::new(), | |
| holders: Vec::new(), | |
| } | |
| } | |
| pub fn add_account(&mut self, balance: i64) { | |
| self.accounts.push(Account::new(balance)); | |
| } | |
| pub fn add_instrument(&mut self, price: u64) -> usize { | |
| self.prices.push(price); | |
| self.holders.push(HashSet::new()); | |
| self.prices.len() - 1 | |
| } | |
| pub fn update_price(&mut self, instrument: usize, price: u64) { | |
| self.prices[instrument] = price; | |
| let holders = &mut self.holders[instrument]; | |
| for account_idx in holders.iter() { | |
| self.accounts[*account_idx].mark_to_market(&self.prices, instrument); | |
| } | |
| } | |
| /// Find accounts that need liquidation (equity < 1% of total notional) | |
| pub fn liquidatable(&self, instrument: usize) -> Vec<(usize, i64, i64)> { | |
| let mut results = Vec::new(); | |
| for account_idx in self.holders[instrument].iter() { | |
| let account = &self.accounts[*account_idx]; | |
| let (eq, nt) = account.equity_and_notional(); | |
| if eq < (nt / 100) { | |
| results.push((*account_idx, eq, nt)); | |
| } | |
| } | |
| // sort by notional (desc), then by account index (desc) as tie-breaker | |
| results.sort_by(|a, b| { | |
| let (_, _, notional_a) = a; | |
| let (_, _, notional_b) = b; | |
| let (idx_a, _, _) = a; | |
| let (idx_b, _, _) = b; | |
| match notional_b.cmp(notional_a) { | |
| std::cmp::Ordering::Equal => idx_b.cmp(idx_a), | |
| other => other, | |
| } | |
| }); | |
| results | |
| } | |
| pub fn trade(&mut self, account: usize, instrument: usize, size: i64) { | |
| let positions = &mut self.accounts[account].positions; | |
| positions.resize_with(self.prices.len(), || Position::default()); | |
| let pos = &mut positions[instrument]; | |
| let px = self.prices[instrument]; | |
| let old_ntl = pos.size * px as i64; | |
| let paid = size * px as i64; | |
| pos.size += size; | |
| pos.total_paid += paid; | |
| pos.price = px; | |
| let new_ntl = (pos.size * px as i64).abs(); | |
| if new_ntl == 0 { | |
| self.holders[instrument].remove(&account); | |
| } else { | |
| self.holders[instrument].insert(account); | |
| } | |
| let acc = &mut self.accounts[account]; | |
| acc.cached_notional -= old_ntl.abs(); | |
| acc.cached_notional += new_ntl.abs(); | |
| acc.cash_value -= paid; | |
| acc.pos_value += paid; | |
| } | |
| } | |
| impl Account { | |
| pub fn new(balance: i64) -> Self { | |
| Self { | |
| cash_value: balance, | |
| positions: Vec::new(), | |
| pos_value: 0, | |
| cached_notional: 0, | |
| } | |
| } | |
| pub fn mark_to_market(&mut self, prices: &[u64], instrument: usize) { | |
| if let Some(pos) = self.positions.get_mut(instrument) { | |
| let new_px = prices[instrument]; | |
| let old_ntl = pos.size * pos.price as i64; | |
| let new_ntl = pos.size * new_px as i64; | |
| self.cached_notional -= old_ntl.abs(); | |
| self.cached_notional += new_ntl.abs(); | |
| let old_value = pos.size * pos.price as i64; | |
| let new_value = pos.size * new_px as i64; | |
| self.pos_value -= old_value; | |
| self.pos_value += new_value; | |
| pos.price = new_px; | |
| } | |
| } | |
| pub fn equity_and_notional(&self) -> (i64, i64) { | |
| (self.cash_value + self.pos_value, self.cached_notional) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment