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

Add market making engine with Avellaneda-Stoikov quoting, inventory management, adverse selection detection

Browse files
Files changed (1) hide show
  1. market_making.py +541 -0
market_making.py ADDED
@@ -0,0 +1,541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Market Making Engine — What Jane Street Actually Does
2
+
3
+ Jane Street is primarily a MARKET MAKER, not a directional trader.
4
+ They quote bid/ask on options, ETFs, bonds — make money on spread + volume.
5
+
6
+ Key challenges:
7
+ 1. Adverse selection: informed traders pick off your quotes
8
+ 2. Inventory risk: holding positions you don't want
9
+ 3. Spread optimization: too wide = no volume, too tight = get run over
10
+ 4. Regulatory constraints: Reg NMS, MiFID II
11
+
12
+ Based on:
13
+ - Avellaneda & Stoikov (2008): "High-frequency trading in a limit order book"
14
+ - Guéant et al. (2012): "Dealing with the inventory risk"
15
+ - Cartea & Jaimungal (2013): "Modeling asset prices for algorithmic trading"
16
+ """
17
+ import numpy as np
18
+ import pandas as pd
19
+ from typing import Dict, List, Tuple, Optional, Callable
20
+ from dataclasses import dataclass
21
+ import warnings
22
+ warnings.filterwarnings('ignore')
23
+
24
+
25
+ @dataclass
26
+ class InventoryState:
27
+ """Current market maker position"""
28
+ position: float = 0.0 # Net position
29
+ cash: float = 0.0 # Cash balance
30
+ pnl_realized: float = 0.0
31
+ pnl_unrealized: float = 0.0
32
+ trades_executed: int = 0
33
+ quotes_submitted: int = 0
34
+ quotes_filled: int = 0
35
+
36
+ def total_pnl(self, mark_price: float) -> float:
37
+ return self.pnl_realized + self.position * mark_price + self.cash
38
+
39
+
40
+ class MarketMakerQuote:
41
+ """Single market maker quote"""
42
+ def __init__(self, side: str, price: float, quantity: int,
43
+ aggression: str = 'passive'):
44
+ self.side = side # 'bid' or 'ask'
45
+ self.price = price
46
+ self.quantity = quantity
47
+ self.aggression = aggression # 'passive' (resting) or 'aggressive' (crossing)
48
+ self.fill_probability = 0.0
49
+ self.expected_profit = 0.0
50
+
51
+
52
+ class AvellanedaStoikovMarketMaker:
53
+ """
54
+ Avellaneda-Stoikov (2008) market making model.
55
+
56
+ Key insight: Quote prices should DEPEND on current inventory.
57
+
58
+ Reservation price (where you're indifferent to trade):
59
+ r = s - q * γ * σ² * (T - t)
60
+
61
+ Spread:
62
+ δ^a + δ^b = γ * σ² * (T - t) + (2/γ) * ln(1 + γ/κ)
63
+
64
+ Where:
65
+ - s = mid price
66
+ - q = inventory position
67
+ - γ = risk aversion
68
+ - σ = volatility
69
+ - T-t = time remaining
70
+ - κ = order arrival intensity
71
+
72
+ As inventory grows positive → skew quotes DOWN (want to sell)
73
+ As inventory grows negative → skew quotes UP (want to buy)
74
+ """
75
+
76
+ def __init__(self,
77
+ gamma: float = 0.1, # Risk aversion
78
+ sigma: float = 0.02, # Volatility (per period)
79
+ kappa: float = 1.5, # Order arrival rate
80
+ max_position: float = 1000.0, # Position limit
81
+ min_spread_bps: float = 1.0, # Minimum spread in bps
82
+ max_spread_bps: float = 50.0, # Maximum spread
83
+ inventory_skew_factor: float = 2.0): # How much to skew
84
+
85
+ self.gamma = gamma
86
+ self.sigma = sigma
87
+ self.kappa = kappa
88
+ self.max_position = max_position
89
+ self.min_spread_bps = min_spread_bps / 10000.0 # Convert to decimal
90
+ self.max_spread_bps = max_spread_bps / 10000.0
91
+ self.inventory_skew_factor = inventory_skew_factor
92
+
93
+ self.state = InventoryState()
94
+ self.quote_history = []
95
+ self.pnl_history = []
96
+
97
+ def reset(self):
98
+ """Reset state"""
99
+ self.state = InventoryState()
100
+ self.quote_history = []
101
+ self.pnl_history = []
102
+
103
+ def calculate_quotes(self,
104
+ mid_price: float,
105
+ time_to_end: float = 1.0,
106
+ current_inventory: Optional[float] = None) -> Tuple[MarketMakerQuote, MarketMakerQuote]:
107
+ """
108
+ Calculate optimal bid and ask quotes.
109
+
110
+ Returns: (bid_quote, ask_quote)
111
+ """
112
+ if current_inventory is None:
113
+ current_inventory = self.state.position
114
+
115
+ # Reservation price (inventory-adjusted mid)
116
+ reservation_price = mid_price - current_inventory * self.gamma * (self.sigma ** 2) * time_to_end
117
+
118
+ # Optimal spread
119
+ optimal_spread = self.gamma * (self.sigma ** 2) * time_to_end + \
120
+ (2.0 / self.gamma) * np.log(1 + self.gamma / self.kappa)
121
+
122
+ # Apply min/max spread constraints
123
+ spread_decimal = max(optimal_spread, self.min_spread_bps * mid_price)
124
+ spread_decimal = min(spread_decimal, self.max_spread_bps * mid_price)
125
+
126
+ # Inventory skewing
127
+ # If long (q > 0), make ask more attractive (lower ask), bid less attractive
128
+ # If short (q < 0), make bid more attractive (higher bid), ask less attractive
129
+ skew = np.tanh(current_inventory / self.max_position * self.inventory_skew_factor)
130
+
131
+ half_spread = spread_decimal / 2
132
+
133
+ # Skew: shift quotes away from reservation price
134
+ bid_offset = half_spread * (1 + skew) # Higher bid when short
135
+ ask_offset = half_spread * (1 - skew) # Lower ask when long
136
+
137
+ bid_price = reservation_price - bid_offset
138
+ ask_price = reservation_price + ask_offset
139
+
140
+ # Ensure bid < ask
141
+ if bid_price >= ask_price:
142
+ # Emergency: force minimum spread
143
+ avg = (bid_price + ask_price) / 2
144
+ bid_price = avg - self.min_spread_bps * mid_price / 2
145
+ ask_price = avg + self.min_spread_bps * mid_price / 2
146
+
147
+ # Quantity sizing: larger when inventory is neutral, smaller when extreme
148
+ inventory_ratio = abs(current_inventory) / self.max_position
149
+ qty_multiplier = 1.0 - 0.7 * inventory_ratio # Reduce size as inventory grows
150
+ base_qty = 100
151
+
152
+ bid_qty = int(base_qty * qty_multiplier)
153
+ ask_qty = int(base_qty * qty_multiplier)
154
+
155
+ # If extremely long, don't quote on ask (or tiny qty)
156
+ if current_inventory > self.max_position * 0.9:
157
+ ask_qty = 0
158
+ # If extremely short, don't quote on bid
159
+ if current_inventory < -self.max_position * 0.9:
160
+ bid_qty = 0
161
+
162
+ bid_quote = MarketMakerQuote('bid', bid_price, bid_qty, 'passive')
163
+ ask_quote = MarketMakerQuote('ask', ask_price, ask_qty, 'passive')
164
+
165
+ # Expected fill probability (simplified)
166
+ bid_quote.fill_probability = np.exp(-self.kappa * bid_offset)
167
+ ask_quote.fill_probability = np.exp(-self.kappa * ask_offset)
168
+
169
+ # Expected profit per trade = half spread (simplified)
170
+ bid_quote.expected_profit = bid_offset
171
+ ask_quote.expected_profit = ask_offset
172
+
173
+ return bid_quote, ask_quote
174
+
175
+ def process_fill(self, quote: MarketMakerQuote,
176
+ fill_qty: int,
177
+ fill_price: float,
178
+ is_aggressive_side: bool):
179
+ """
180
+ Process a quote fill.
181
+
182
+ is_aggressive_side: True if WE were aggressive (market order),
183
+ False if counterparty hit our resting quote
184
+ """
185
+ if quote.side == 'bid':
186
+ # We bought
187
+ self.state.position += fill_qty
188
+ self.state.cash -= fill_qty * fill_price
189
+ self.state.trades_executed += 1
190
+ else:
191
+ # We sold
192
+ self.state.position -= fill_qty
193
+ self.state.cash += fill_qty * fill_price
194
+ self.state.trades_executed += 1
195
+
196
+ self.state.quotes_filled += 1
197
+
198
+ # Track
199
+ self.quote_history.append({
200
+ 'side': quote.side,
201
+ 'quote_price': quote.price,
202
+ 'fill_price': fill_price,
203
+ 'quantity': fill_qty,
204
+ 'position_after': self.state.position,
205
+ 'cash_after': self.state.cash
206
+ })
207
+
208
+ def update_mark_price(self, mark_price: float):
209
+ """Update unrealized PnL with current mark"""
210
+ self.state.pnl_unrealized = self.state.position * mark_price + self.state.cash
211
+ self.pnl_history.append({
212
+ 'mark_price': mark_price,
213
+ 'position': self.state.position,
214
+ 'cash': self.state.cash,
215
+ 'unrealized_pnl': self.state.pnl_unrealized
216
+ })
217
+
218
+ def get_summary(self) -> Dict:
219
+ """Get current market maker summary"""
220
+ return {
221
+ 'position': self.state.position,
222
+ 'cash': self.state.cash,
223
+ 'trades': self.state.trades_executed,
224
+ 'quotes_filled': self.state.quotes_filled,
225
+ 'pnl_realized': self.state.pnl_realized,
226
+ 'pnl_unrealized': self.state.pnl_unrealized,
227
+ 'inventory_ratio': abs(self.state.position) / self.max_position
228
+ }
229
+
230
+
231
+ class InventoryRiskManager:
232
+ """
233
+ Advanced inventory risk management for market making.
234
+
235
+ When inventory exceeds limits:
236
+ 1. Hedge via correlated instruments
237
+ 2. Cross the spread (aggressive unwind)
238
+ 3. Reduce quote sizes
239
+ 4. Stop quoting on the bad side entirely
240
+ """
241
+
242
+ def __init__(self,
243
+ max_inventory: float = 1000,
244
+ hedge_threshold: float = 0.6, # Hedge at 60% of max
245
+ stop_threshold: float = 0.9, # Stop quoting at 90%
246
+ aggressive_unwind_threshold: float = 0.95): # Market order at 95%
247
+
248
+ self.max_inventory = max_inventory
249
+ self.hedge_threshold = hedge_threshold
250
+ self.stop_threshold = stop_threshold
251
+ self.aggressive_unwind_threshold = aggressive_unwind_threshold
252
+
253
+ def check_inventory(self, position: float) -> Dict:
254
+ """Determine actions needed based on inventory"""
255
+ ratio = abs(position) / self.max_inventory
256
+
257
+ actions = {
258
+ 'hedge': False,
259
+ 'stop_quoting_bad_side': False,
260
+ 'aggressive_unwind': False,
261
+ 'reduce_size': 1.0, # Size multiplier
262
+ 'status': 'normal'
263
+ }
264
+
265
+ if ratio >= self.aggressive_unwind_threshold:
266
+ actions['aggressive_unwind'] = True
267
+ actions['stop_quoting_bad_side'] = True
268
+ actions['reduce_size'] = 0.0
269
+ actions['status'] = 'CRITICAL'
270
+
271
+ elif ratio >= self.stop_threshold:
272
+ actions['stop_quoting_bad_side'] = True
273
+ actions['reduce_size'] = 0.1
274
+ actions['status'] = 'SEVERE'
275
+
276
+ elif ratio >= self.hedge_threshold:
277
+ actions['hedge'] = True
278
+ actions['reduce_size'] = 0.5
279
+ actions['status'] = 'WARNING'
280
+
281
+ elif ratio >= 0.5:
282
+ actions['reduce_size'] = 0.8
283
+ actions['status'] = 'MODERATE'
284
+
285
+ return actions
286
+
287
+ def hedge_recommendation(self,
288
+ position: float,
289
+ correlated_assets: Dict[str, float]) -> Optional[Dict]:
290
+ """
291
+ Recommend hedge position in correlated assets.
292
+
293
+ correlated_assets: {symbol: correlation_with_primary}
294
+ """
295
+ if abs(position) < self.max_inventory * self.hedge_threshold:
296
+ return None
297
+
298
+ # Find best hedge: highest absolute correlation
299
+ best_hedge = None
300
+ best_corr = 0
301
+
302
+ for symbol, corr in correlated_assets.items():
303
+ if abs(corr) > best_corr:
304
+ best_corr = abs(corr)
305
+ best_hedge = symbol
306
+
307
+ if best_hedge is None:
308
+ return None
309
+
310
+ # Hedge amount: offset position in primary
311
+ hedge_direction = -np.sign(position)
312
+ hedge_size = abs(position) * abs(correlated_assets[best_hedge])
313
+
314
+ return {
315
+ 'hedge_symbol': best_hedge,
316
+ 'direction': 'buy' if hedge_direction > 0 else 'sell',
317
+ 'quantity': hedge_size,
318
+ 'correlation': correlated_assets[best_hedge],
319
+ 'expected_hedge_effectiveness': best_corr ** 2 # R²
320
+ }
321
+
322
+
323
+ class AdverseSelectionDetector:
324
+ """
325
+ Detect and respond to adverse selection.
326
+
327
+ Adverse selection: Informed traders know something you don't.
328
+ When they buy from you, price drops. When they sell to you, price rises.
329
+
330
+ Detection methods:
331
+ 1. Post-trade price movement
332
+ 2. Order flow toxicity (VPIN)
333
+ 3. Large order detection
334
+ 4. Timing patterns (orders arrive in clusters before news)
335
+ """
336
+
337
+ def __init__(self,
338
+ lookback_window: int = 20,
339
+ toxicity_threshold: float = 0.6):
340
+ self.lookback_window = lookback_window
341
+ self.toxicity_threshold = toxicity_threshold
342
+ self.trade_history = []
343
+ self.toxicity_score = 0.0
344
+
345
+ def record_trade(self,
346
+ side: str, # Which side WE filled
347
+ our_price: float, # Price we got
348
+ post_prices: List[float], # Prices after trade (1min, 5min, 15min)
349
+ quantity: int,
350
+ counterparty: Optional[str] = None):
351
+ """Record a trade for adverse selection analysis"""
352
+
353
+ # Calculate post-trade drift
354
+ drift = 0
355
+ if post_prices and len(post_prices) >= 1:
356
+ # If we SOLD and price went UP → bad (gave away value)
357
+ # If we BOUGHT and price went DOWN → bad (overpaid)
358
+ if side == 'ask': # We sold
359
+ drift = post_prices[0] - our_price
360
+ else: # We bought
361
+ drift = our_price - post_prices[0]
362
+
363
+ self.trade_history.append({
364
+ 'side': side,
365
+ 'our_price': our_price,
366
+ 'post_drift': drift,
367
+ 'quantity': quantity,
368
+ 'counterparty': counterparty,
369
+ 'adverse': drift > 0 # True if trade was bad for us
370
+ })
371
+
372
+ # Keep only recent trades
373
+ if len(self.trade_history) > self.lookback_window:
374
+ self.trade_history.pop(0)
375
+
376
+ def get_toxicity_score(self) -> float:
377
+ """Current toxicity score (0-1, higher = more adverse selection)"""
378
+ if len(self.trade_history) < 5:
379
+ return 0.0
380
+
381
+ adverse_count = sum(1 for t in self.trade_history if t['adverse'])
382
+ self.toxicity_score = adverse_count / len(self.trade_history)
383
+
384
+ return self.toxicity_score
385
+
386
+ def should_widen_spread(self) -> Tuple[bool, float]:
387
+ """Should we widen spread due to adverse selection?"""
388
+ toxicity = self.get_toxicity_score()
389
+
390
+ if toxicity > self.toxicity_threshold:
391
+ # Widen spread proportionally
392
+ widen_factor = 1.0 + (toxicity - self.toxicity_threshold) * 2
393
+ return True, min(widen_factor, 3.0) # Max 3x wider
394
+
395
+ return False, 1.0
396
+
397
+ def get_recent_pnl(self) -> Dict:
398
+ """P&L attribution from adverse selection"""
399
+ if not self.trade_history:
400
+ return {}
401
+
402
+ adverse_trades = [t for t in self.trade_history if t['adverse']]
403
+ good_trades = [t for t in self.trade_history if not t['adverse']]
404
+
405
+ adverse_drift = sum(t['post_drift'] * t['quantity'] for t in adverse_trades)
406
+ good_drift = sum(t['post_drift'] * t['quantity'] for t in good_trades)
407
+
408
+ return {
409
+ 'total_trades': len(self.trade_history),
410
+ 'adverse_trades': len(adverse_trades),
411
+ 'adverse_pct': len(adverse_trades) / len(self.trade_history) * 100,
412
+ 'total_adverse_cost': adverse_drift,
413
+ 'total_good_gain': -good_drift,
414
+ 'net_selection_cost': adverse_drift + good_drift
415
+ }
416
+
417
+
418
+ def simulate_market_making(n_steps: int = 1000,
419
+ price_drift: float = 0.0001,
420
+ volatility: float = 0.01,
421
+ arrival_rate: float = 0.3) -> pd.DataFrame:
422
+ """
423
+ Simulate a market maker in a random walk market.
424
+
425
+ Generates synthetic tick data and lets the market maker quote and fill.
426
+ """
427
+ np.random.seed(42)
428
+
429
+ # Initialize
430
+ mm = AvellanedaStoikovMarketMaker(
431
+ gamma=0.1,
432
+ sigma=volatility,
433
+ kappa=1.5,
434
+ max_position=1000
435
+ )
436
+
437
+ detector = AdverseSelectionDetector(lookback_window=20)
438
+ risk_mgr = InventoryRiskManager()
439
+
440
+ # Price process
441
+ price = 100.0
442
+ prices = [price]
443
+
444
+ results = []
445
+
446
+ for step in range(n_steps):
447
+ # Update price
448
+ price_change = np.random.randn() * volatility * price + price_drift * price
449
+ price += price_change
450
+ price = max(price, 0.01)
451
+ prices.append(price)
452
+
453
+ # Calculate quotes
454
+ bid_quote, ask_quote = mm.calculate_quotes(price, time_to_end=1.0)
455
+
456
+ # Check inventory risk
457
+ inventory_actions = risk_mgr.check_inventory(mm.state.position)
458
+
459
+ # Check adverse selection
460
+ widen, widen_factor = detector.should_widen_spread()
461
+ if widen:
462
+ # Widen spread
463
+ spread_adj = (widen_factor - 1.0) * (ask_quote.price - bid_quote.price) / 2
464
+ bid_quote.price -= spread_adj
465
+ ask_quote.price += spread_adj
466
+
467
+ # Simulate order arrivals
468
+ if np.random.rand() < arrival_rate:
469
+ # Someone hits our bid
470
+ if np.random.rand() < bid_quote.fill_probability:
471
+ fill_qty = np.random.randint(10, bid_quote.quantity + 1)
472
+ mm.process_fill(bid_quote, fill_qty, bid_quote.price, False)
473
+
474
+ # Record for adverse selection
475
+ future_prices = prices[-5:] if len(prices) >= 5 else prices
476
+ detector.record_trade('bid', bid_quote.price, future_prices, fill_qty)
477
+
478
+ if np.random.rand() < arrival_rate:
479
+ # Someone lifts our ask
480
+ if np.random.rand() < ask_quote.fill_probability:
481
+ fill_qty = np.random.randint(10, ask_quote.quantity + 1)
482
+ mm.process_fill(ask_quote, fill_qty, ask_quote.price, False)
483
+
484
+ future_prices = prices[-5:] if len(prices) >= 5 else prices
485
+ detector.record_trade('ask', ask_quote.price, future_prices, fill_qty)
486
+
487
+ # Mark to market
488
+ mm.update_mark_price(price)
489
+
490
+ # Record
491
+ summary = mm.get_summary()
492
+ results.append({
493
+ 'step': step,
494
+ 'price': price,
495
+ 'bid': bid_quote.price,
496
+ 'ask': ask_quote.price,
497
+ 'spread_bps': (ask_quote.price - bid_quote.price) / price * 10000,
498
+ 'position': summary['position'],
499
+ 'cash': summary['cash'],
500
+ 'inventory_ratio': summary['inventory_ratio'],
501
+ 'unrealized_pnl': summary['pnl_unrealized'],
502
+ 'toxicity': detector.get_toxicity_score(),
503
+ 'status': inventory_actions['status']
504
+ })
505
+
506
+ return pd.DataFrame(results)
507
+
508
+
509
+ if __name__ == '__main__':
510
+ print("=" * 70)
511
+ print(" MARKET MAKING ENGINE SIMULATION")
512
+ print("=" * 70)
513
+
514
+ results = simulate_market_making(n_steps=5000)
515
+
516
+ # Summary
517
+ final = results.iloc[-1]
518
+
519
+ print(f"\nSimulation: 5000 steps, random walk market")
520
+ print(f" Initial Price: $100.00")
521
+ print(f" Final Price: ${final['price']:.2f}")
522
+ print(f" Final Position: {final['position']:.0f}")
523
+ print(f" Final Cash: ${final['cash']:.2f}")
524
+ print(f" Unrealized PnL: ${final['unrealized_pnl']:.2f}")
525
+ print(f" Avg Spread: {results['spread_bps'].mean():.1f} bps")
526
+ print(f" Avg Position: {abs(results['position']).mean():.0f}")
527
+ print(f" Max Position: {results['position'].abs().max():.0f}")
528
+ print(f" Avg Toxicity: {results['toxicity'].mean():.3f}")
529
+
530
+ # Strategy attribution
531
+ pnl_from_spread = results['spread_bps'].mean() / 10000 * 2 * 100 # Simplified
532
+
533
+ print(f"\n PnL Attribution:")
534
+ print(f" Spread capture: ~${pnl_from_spread * 50:.0f} (per 100 trades)")
535
+ print(f" Inventory risk: ${final['unrealized_pnl'] - pnl_from_spread * 50:.0f}")
536
+
537
+ print(f"\n This is how Jane Street makes money:")
538
+ print(f" 1. Quote tight spreads 1000s of times per day")
539
+ print(f" 2. Inventory management keeps risk bounded")
540
+ print(f" 3. Adverse selection detection widens when toxic flow arrives")
541
+ print(f" 4. Volume × Small spread margin = Big PnL")