Premchan369 commited on
Commit
ff8e6b2
·
verified ·
1 Parent(s): 513693b

Add synthetic market simulation with agent-based modeling for self-play training

Browse files
Files changed (1) hide show
  1. synthetic_market_sim.py +533 -0
synthetic_market_sim.py ADDED
@@ -0,0 +1,533 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Synthetic Market Simulation — Train Your Strategies Against Themselves
2
+
3
+ Jane Street, Two Sigma, Citadel ALL run simulations.
4
+ Why? Because you need MORE data than history provides.
5
+
6
+ This module creates realistic synthetic markets with:
7
+ - Agent-based modeling (informed vs noise traders)
8
+ - Market impact propagation
9
+ - Correlated asset dynamics
10
+ - Regime switches
11
+ - News shock injection
12
+
13
+ Use this to:
14
+ 1. Train RL agents on unlimited data
15
+ 2. Stress test strategies with extreme scenarios
16
+ 3. Bootstrap confidence intervals
17
+ 4. Test strategy robustness
18
+ """
19
+ import numpy as np
20
+ import pandas as pd
21
+ from typing import Dict, List, Tuple, Optional, Callable
22
+ from dataclasses import dataclass
23
+ import warnings
24
+ warnings.filterwarnings('ignore')
25
+
26
+
27
+ @dataclass
28
+ class MarketConfig:
29
+ """Configuration for synthetic market"""
30
+ n_assets: int = 10
31
+ n_informed_traders: int = 5
32
+ n_noise_traders: int = 50
33
+ initial_price: float = 100.0
34
+ fundamental_volatility: float = 0.01
35
+ noise_trader_sigma: float = 0.02
36
+ informed_signal_quality: float = 0.7 # Probability informed trader is right
37
+ market_impact_per_lot: float = 0.0001
38
+ mean_reversion_speed: float = 0.05
39
+ correlation_matrix: Optional[np.ndarray] = None
40
+
41
+ def __post_init__(self):
42
+ if self.correlation_matrix is None:
43
+ # Generate random correlation matrix
44
+ from scipy.stats import wishart
45
+ # Use Wishart to generate positive definite correlation
46
+ df = self.n_assets + 2
47
+ scale = np.eye(self.n_assets) * 0.5 + np.ones((self.n_assets, self.n_assets)) * 0.5
48
+ cov = wishart.rvs(df=df, scale=scale, size=1)
49
+ # Convert to correlation
50
+ d = np.sqrt(np.diag(cov))
51
+ self.correlation_matrix = cov / np.outer(d, d)
52
+
53
+
54
+ class FundamentalPriceProcess:
55
+ """
56
+ Simulate fundamental (fair) value of each asset.
57
+
58
+ Follows: dF = θ(μ - F)dt + σdW
59
+
60
+ Where:
61
+ - θ = mean reversion speed
62
+ - μ = long-term mean (changes at regime switches)
63
+ - σ = fundamental volatility
64
+ """
65
+
66
+ def __init__(self, config: MarketConfig):
67
+ self.config = config
68
+ self.prices = np.ones(config.n_assets) * config.initial_price
69
+ self.long_term_means = np.ones(config.n_assets) * config.initial_price
70
+ self.regime = 'normal' # normal, boom, crash, high_vol
71
+ self.regime_duration = 0
72
+ self.regime_switches = []
73
+
74
+ def step(self, dt: float = 1.0) -> np.ndarray:
75
+ """Evolve fundamental prices one step"""
76
+ cfg = self.config
77
+
78
+ # Regime switching
79
+ self.regime_duration += 1
80
+ if self.regime_duration > np.random.poisson(100):
81
+ self._switch_regime()
82
+
83
+ # Mean reversion + random walk
84
+ theta = cfg.mean_reversion_speed
85
+ noise = np.random.multivariate_normal(
86
+ np.zeros(cfg.n_assets),
87
+ cfg.correlation_matrix * (cfg.fundamental_volatility ** 2)
88
+ )
89
+
90
+ # Regime effects
91
+ if self.regime == 'boom':
92
+ drift = 0.002
93
+ vol_mult = 1.0
94
+ elif self.regime == 'crash':
95
+ drift = -0.003
96
+ vol_mult = 2.0
97
+ elif self.regime == 'high_vol':
98
+ drift = 0.0
99
+ vol_mult = 3.0
100
+ else: # normal
101
+ drift = 0.0
102
+ vol_mult = 1.0
103
+
104
+ dprices = (
105
+ theta * (self.long_term_means - self.prices) * dt
106
+ + drift * self.prices
107
+ + noise * vol_mult * np.sqrt(dt)
108
+ )
109
+
110
+ self.prices = np.maximum(self.prices + dprices, 0.01)
111
+
112
+ return self.prices.copy()
113
+
114
+ def _switch_regime(self):
115
+ """Switch market regime"""
116
+ old_regime = self.regime
117
+ self.regime = np.random.choice(
118
+ ['normal', 'boom', 'crash', 'high_vol'],
119
+ p=[0.6, 0.15, 0.15, 0.1]
120
+ )
121
+ self.regime_duration = 0
122
+
123
+ if self.regime == 'boom':
124
+ self.long_term_means *= 1.02
125
+ elif self.regime == 'crash':
126
+ self.long_term_means *= 0.98
127
+
128
+ self.regime_switches.append({
129
+ 'step': len(self.regime_switches),
130
+ 'from': old_regime,
131
+ 'to': self.regime
132
+ })
133
+
134
+
135
+ class Trader:
136
+ """Base trader agent"""
137
+
138
+ def __init__(self, trader_id: str, capital: float = 1_000_000):
139
+ self.trader_id = trader_id
140
+ self.capital = capital
141
+ self.positions = np.zeros(0) # Will be set
142
+ self.trade_history = []
143
+
144
+ def decide(self,
145
+ market_state: Dict,
146
+ fundamental: np.ndarray) -> np.ndarray:
147
+ """
148
+ Returns trade vector: positive = buy, negative = sell.
149
+ """
150
+ raise NotImplementedError
151
+
152
+ def execute(self, trade: np.ndarray, prices: np.ndarray):
153
+ """Execute trade and update state"""
154
+ cost = np.sum(np.abs(trade) * prices)
155
+ if cost <= self.capital:
156
+ self.positions += trade
157
+ self.capital -= cost
158
+ self.trade_history.append({
159
+ 'positions': trade.copy(),
160
+ 'prices': prices.copy()
161
+ })
162
+
163
+
164
+ class InformedTrader(Trader):
165
+ """
166
+ Informed trader with private signal about future price.
167
+
168
+ Has signal_quality probability of being right.
169
+ More informed = more likely to profit, creates adverse selection.
170
+ """
171
+
172
+ def __init__(self, trader_id: str, signal_quality: float, capital: float = 5_000_000):
173
+ super().__init__(trader_id, capital)
174
+ self.signal_quality = signal_quality
175
+ self.signal_horizon = np.random.randint(5, 30)
176
+ self.aggression = np.random.uniform(0.3, 0.8)
177
+
178
+ def decide(self,
179
+ market_state: Dict,
180
+ fundamental: np.ndarray) -> np.ndarray:
181
+ """Generate trade based on private signal"""
182
+ n_assets = len(fundamental)
183
+
184
+ if len(self.positions) != n_assets:
185
+ self.positions = np.zeros(n_assets)
186
+
187
+ # Generate signal: will price go up or down?
188
+ signal = np.random.randn(n_assets)
189
+
190
+ # Correct signal with probability signal_quality
191
+ future_drift = market_state.get('future_drift', np.zeros(n_assets))
192
+ correct = np.random.rand(n_assets) < self.signal_quality
193
+ signal = np.where(correct, np.sign(future_drift), -np.sign(future_drift))
194
+
195
+ # Trade size proportional to conviction
196
+ max_trade = self.capital * self.aggression / np.mean(fundamental)
197
+ trade = signal * max_trade / n_assets
198
+
199
+ return trade
200
+
201
+
202
+ class NoiseTrader(Trader):
203
+ """
204
+ Noise trader with no information.
205
+
206
+ Trades randomly, provides liquidity, gets picked off.
207
+ Represents retail traders, uninformed flow.
208
+ """
209
+
210
+ def __init__(self, trader_id: str, sigma: float = 0.02, capital: float = 500_000):
211
+ super().__init__(trader_id, capital)
212
+ self.sigma = sigma
213
+
214
+ def decide(self,
215
+ market_state: Dict,
216
+ fundamental: np.ndarray) -> np.ndarray:
217
+ """Random trade with zero mean"""
218
+ n_assets = len(fundamental)
219
+
220
+ if len(self.positions) != n_assets:
221
+ self.positions = np.zeros(n_assets)
222
+
223
+ # Random position changes
224
+ trade_size = np.abs(np.random.randn(n_assets)) * self.sigma * self.capital
225
+ trade_size /= np.mean(fundamental)
226
+
227
+ direction = np.random.choice([-1, 1], n_assets)
228
+
229
+ return trade_size * direction
230
+
231
+
232
+ class MomentumTrader(Trader):
233
+ """
234
+ Momentum trader: buys assets going up, sells going down.
235
+
236
+ Creates and rides trends. Can cause bubbles/crashes.
237
+ """
238
+
239
+ def __init__(self, trader_id: str, lookback: int = 10,
240
+ threshold: float = 0.01, capital: float = 2_000_000):
241
+ super().__init__(trader_id, capital)
242
+ self.lookback = lookback
243
+ self.threshold = threshold
244
+ self.price_history = []
245
+
246
+ def decide(self,
247
+ market_state: Dict,
248
+ fundamental: np.ndarray) -> np.ndarray:
249
+ """Trade on momentum"""
250
+ n_assets = len(fundamental)
251
+
252
+ if len(self.positions) != n_assets:
253
+ self.positions = np.zeros(n_assets)
254
+
255
+ self.price_history.append(fundamental.copy())
256
+
257
+ if len(self.price_history) < self.lookback:
258
+ return np.zeros(n_assets)
259
+
260
+ # Calculate momentum
261
+ recent = np.array(self.price_history[-self.lookback:])
262
+ returns = (recent[-1] / recent[0]) - 1
263
+
264
+ # Trade on momentum
265
+ momentum_signals = returns / self.threshold # Normalized
266
+
267
+ # Scale by available capital
268
+ max_trade = self.capital * 0.2 / np.mean(fundamental)
269
+ trade = momentum_signals * max_trade / n_assets
270
+
271
+ # Keep history bounded
272
+ if len(self.price_history) > self.lookback * 2:
273
+ self.price_history = self.price_history[-self.lookback:]
274
+
275
+ return np.clip(trade, -max_trade, max_trade)
276
+
277
+
278
+ class SyntheticMarket:
279
+ """
280
+ Complete synthetic market simulation.
281
+
282
+ Simulates:
283
+ - Fundamental prices (with regime switches)
284
+ - Multiple trader types
285
+ - Market impact from orders
286
+ - Transaction costs
287
+ - Price discovery
288
+ """
289
+
290
+ def __init__(self, config: MarketConfig):
291
+ self.config = config
292
+ self.fundamental = FundamentalPriceProcess(config)
293
+
294
+ # Initialize traders
295
+ self.traders = []
296
+
297
+ # Informed traders
298
+ for i in range(config.n_informed_traders):
299
+ quality = np.random.uniform(0.5, 0.9)
300
+ self.traders.append(
301
+ InformedTrader(f"informed_{i}", quality)
302
+ )
303
+
304
+ # Noise traders
305
+ for i in range(config.n_noise_traders):
306
+ sigma = np.random.uniform(0.01, 0.03)
307
+ self.traders.append(
308
+ NoiseTrader(f"noise_{i}", sigma)
309
+ )
310
+
311
+ # Momentum traders
312
+ for i in range(5):
313
+ self.traders.append(
314
+ MomentumTrader(f"momentum_{i}")
315
+ )
316
+
317
+ # History
318
+ self.price_history = []
319
+ self.fundamental_history = []
320
+ self.volume_history = []
321
+ self.regime_history = []
322
+ self.order_flow_history = []
323
+
324
+ def step(self) -> Dict:
325
+ """Simulate one market step"""
326
+ cfg = self.config
327
+
328
+ # Update fundamentals
329
+ fundamental = self.fundamental.step()
330
+
331
+ # Generate future drift (for informed traders)
332
+ future_drift = np.random.randn(cfg.n_assets) * cfg.fundamental_volatility
333
+
334
+ market_state = {
335
+ 'fundamental': fundamental,
336
+ 'future_drift': future_drift,
337
+ 'regime': self.fundamental.regime,
338
+ 'prices': self.price_history[-1] if self.price_history else fundamental
339
+ }
340
+
341
+ # Collect all trades
342
+ total_orders = np.zeros(cfg.n_assets)
343
+
344
+ for trader in self.traders:
345
+ trade = trader.decide(market_state, fundamental)
346
+ total_orders += trade
347
+
348
+ # Market impact: orders move price
349
+ impact = total_orders * cfg.market_impact_per_lot
350
+
351
+ # Transaction cost decay
352
+ observed_price = fundamental + impact
353
+
354
+ # Noise
355
+ noise = np.random.randn(cfg.n_assets) * cfg.fundamental_volatility * 0.5
356
+ observed_price += noise
357
+
358
+ observed_price = np.maximum(observed_price, 0.01)
359
+
360
+ # Execute trades
361
+ for trader in self.traders:
362
+ trade = trader.decide(market_state, fundamental)
363
+ trader.execute(trade, observed_price)
364
+
365
+ # Record
366
+ self.price_history.append(observed_price.copy())
367
+ self.fundamental_history.append(fundamental.copy())
368
+ self.volume_history.append(np.sum(np.abs(total_orders)))
369
+ self.regime_history.append(self.fundamental.regime)
370
+ self.order_flow_history.append(total_orders.copy())
371
+
372
+ return {
373
+ 'prices': observed_price,
374
+ 'fundamental': fundamental,
375
+ 'impact': impact,
376
+ 'volume': np.sum(np.abs(total_orders)),
377
+ 'regime': self.fundamental.regime,
378
+ 'order_flow': total_orders
379
+ }
380
+
381
+ def run(self, n_steps: int = 1000) -> pd.DataFrame:
382
+ """Run simulation for n steps"""
383
+ print(f"Running synthetic market simulation: {n_steps} steps")
384
+ print(f" {self.config.n_assets} assets")
385
+ print(f" {len(self.traders)} traders ({self.config.n_informed_traders} informed, "
386
+ f"{self.config.n_noise_traders} noise, 5 momentum)")
387
+
388
+ results = []
389
+
390
+ for step in range(n_steps):
391
+ state = self.step()
392
+ state['step'] = step
393
+ results.append(state)
394
+
395
+ if (step + 1) % 200 == 0:
396
+ print(f" Step {step + 1}/{n_steps} — Regime: {state['regime']}")
397
+
398
+ # Build DataFrame
399
+ df = pd.DataFrame()
400
+ df['step'] = [r['step'] for r in results]
401
+ df['regime'] = [r['regime'] for r in results]
402
+ df['volume'] = [r['volume'] for r in results]
403
+
404
+ for i in range(self.config.n_assets):
405
+ df[f'price_{i}'] = [r['prices'][i] for r in results]
406
+ df[f'fundamental_{i}'] = [r['fundamental'][i] for r in results]
407
+ df[f'impact_{i}'] = [r['impact'][i] for r in results]
408
+
409
+ return df
410
+
411
+ def get_price_data(self) -> pd.DataFrame:
412
+ """Get OHLC-style price data for all assets"""
413
+ if not self.price_history:
414
+ return pd.DataFrame()
415
+
416
+ prices = np.array(self.price_history)
417
+
418
+ df = pd.DataFrame()
419
+ for i in range(self.config.n_assets):
420
+ df[f'asset_{i}_close'] = prices[:, i]
421
+ df[f'asset_{i}_return'] = np.log(prices[1:, i] / prices[:-1, i])
422
+
423
+ df['regime'] = self.regime_history
424
+ df['volume'] = self.volume_history
425
+
426
+ return df
427
+
428
+ def inject_shock(self,
429
+ asset_idx: int = 0,
430
+ shock_size: float = 0.05,
431
+ shock_type: str = 'price'):
432
+ """
433
+ Inject a price shock (simulates earnings surprise, news, etc.)
434
+ """
435
+ if shock_type == 'price':
436
+ self.fundamental.prices[asset_idx] *= (1 + shock_size)
437
+ self.fundamental.long_term_means[asset_idx] *= (1 + shock_size * 0.3)
438
+ elif shock_type == 'volatility':
439
+ # Temporarily increase volatility
440
+ pass # Would modify the fundamental process
441
+
442
+ print(f"Injected {shock_type} shock: {shock_size*100:+.1f}% on asset {asset_idx}")
443
+
444
+
445
+ def generate_training_data(n_simulations: int = 100,
446
+ steps_per_sim: int = 500,
447
+ config: Optional[MarketConfig] = None) -> List[pd.DataFrame]:
448
+ """
449
+ Generate massive synthetic training dataset.
450
+
451
+ Jane Street trains on YEARS of simulated data because:
452
+ 1. Real data is expensive/limited
453
+ 2. Simulations let you test extreme scenarios
454
+ 3. You can generate unlimited data for deep learning
455
+ """
456
+ if config is None:
457
+ config = MarketConfig()
458
+
459
+ datasets = []
460
+
461
+ print(f"Generating {n_simulations} synthetic market simulations...")
462
+ print(f" Total data: {n_simulations * steps_per_sim:,} observations")
463
+
464
+ for i in range(n_simulations):
465
+ # Vary parameters slightly each simulation
466
+ sim_config = MarketConfig(
467
+ n_assets=config.n_assets,
468
+ n_informed_traders=config.n_informed_traders,
469
+ n_noise_traders=config.n_noise_traders,
470
+ fundamental_volatility=config.fundamental_volatility * np.random.uniform(0.8, 1.2),
471
+ market_impact_per_lot=config.market_impact_per_lot * np.random.uniform(0.5, 2.0)
472
+ )
473
+
474
+ market = SyntheticMarket(sim_config)
475
+ df = market.run(steps_per_sim)
476
+ datasets.append(df)
477
+
478
+ if (i + 1) % 10 == 0:
479
+ print(f" Completed {i+1}/{n_simulations} simulations")
480
+
481
+ return datasets
482
+
483
+
484
+ if __name__ == '__main__':
485
+ print("=" * 70)
486
+ print(" SYNTHETIC MARKET SIMULATION")
487
+ print("=" * 70)
488
+
489
+ # Single simulation
490
+ config = MarketConfig(n_assets=5, n_informed_traders=3, n_noise_traders=30)
491
+ market = SyntheticMarket(config)
492
+
493
+ df = market.run(n_steps=1000)
494
+
495
+ print(f"\nSimulation Results:")
496
+ print(f" Steps: {len(df)}")
497
+ print(f" Regimes: {df['regime'].value_counts().to_dict()}")
498
+ print(f" Avg Volume: {df['volume'].mean():.0f}")
499
+ print(f" Price range (asset 0): ${df['price_0'].min():.2f} - ${df['price_0'].max():.2f}")
500
+
501
+ # Correlation structure
502
+ price_cols = [c for c in df.columns if c.startswith('price_')]
503
+ returns = np.log(df[price_cols].values[1:] / df[price_cols].values[:-1])
504
+ corr = np.corrcoef(returns.T)
505
+
506
+ print(f"\nAsset Return Correlations:")
507
+ for i in range(len(price_cols)):
508
+ for j in range(i+1, len(price_cols)):
509
+ print(f" Asset {i} ↔ Asset {j}: {corr[i,j]:.3f}")
510
+
511
+ # Shock test
512
+ print(f"\nInjecting price shock on asset 0...")
513
+ market.inject_shock(asset_idx=0, shock_size=-0.10, shock_type='price')
514
+
515
+ for _ in range(10):
516
+ market.step()
517
+
518
+ print(f" Post-shock price: ${market.price_history[-1][0]:.2f}")
519
+ print(f" Recovery: {((market.price_history[-1][0] / market.price_history[-20][0]) - 1)*100:+.1f}%")
520
+
521
+ # Massive training dataset
522
+ print(f"\nGenerating training dataset...")
523
+ datasets = generate_training_data(n_simulations=5, steps_per_sim=500, config=config)
524
+
525
+ total_rows = sum(len(d) for d in datasets)
526
+ print(f" Total synthetic observations: {total_rows:,}")
527
+
528
+ print(f"\n Use this data to:")
529
+ print(f" 1. Train RL execution agents (unlimited episodes)")
530
+ print(f" 2. Test strategy robustness across market regimes")
531
+ print(f" 3. Bootstrap confidence intervals")
532
+ print(f" 4. Generate adversarial scenarios")
533
+ print(f" 5. Calibrate risk models")