Premchan369 commited on
Commit
b6c23e5
·
verified ·
1 Parent(s): 5e1f1d1

Add execution algorithms: TWAP, VWAP, Smart Order Router with market impact model

Browse files
Files changed (1) hide show
  1. execution_algorithms.py +497 -0
execution_algorithms.py ADDED
@@ -0,0 +1,497 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Execution Algorithms: TWAP, VWAP, Smart Order Routing
2
+
3
+ What separates retail execution from institutional execution:
4
+ - Retail: Market orders, immediate execution, pay spread
5
+ - Institutional: TWAP/VWAP, slice orders across time, minimize market impact
6
+
7
+ Market impact model: Price moves against you proportional to order size / daily volume
8
+ """
9
+ import numpy as np
10
+ import pandas as pd
11
+ from typing import Dict, List, Optional, Tuple
12
+ from dataclasses import dataclass
13
+ import warnings
14
+ warnings.filterwarnings('ignore')
15
+
16
+
17
+ @dataclass
18
+ class Order:
19
+ """Single order specification"""
20
+ symbol: str
21
+ side: str # 'buy' or 'sell'
22
+ quantity: int
23
+ order_type: str # 'market', 'limit', 'twap', 'vwap'
24
+ limit_price: Optional[float] = None
25
+
26
+ def __post_init__(self):
27
+ self.side = self.side.lower()
28
+ self.order_type = self.order_type.lower()
29
+
30
+
31
+ class MarketImpactModel:
32
+ """
33
+ Square-root market impact model (Almgren-Chriss, 1999).
34
+
35
+ Market impact = σ * sqrt(Q / V)
36
+ Where:
37
+ - σ = daily volatility
38
+ - Q = order quantity
39
+ - V = daily volume
40
+
41
+ Temporary impact: decays within minutes
42
+ Permanent impact: persists
43
+ """
44
+
45
+ def __init__(self,
46
+ temp_impact_coef: float = 0.5,
47
+ perm_impact_coef: float = 0.1,
48
+ decay_halflife: int = 10):
49
+ self.temp_impact_coef = temp_impact_coef
50
+ self.perm_impact_coef = perm_impact_coef
51
+ self.decay_halflife = decay_halflife
52
+
53
+ def temporary_impact(self, order_size: int, daily_volume: int,
54
+ volatility: float) -> float:
55
+ """Temporary price impact (bps)"""
56
+ participation = order_size / max(daily_volume, 1)
57
+ return self.temp_impact_coef * volatility * np.sqrt(participation)
58
+
59
+ def permanent_impact(self, order_size: int, daily_volume: int,
60
+ volatility: float) -> float:
61
+ """Permanent price impact (bps)"""
62
+ participation = order_size / max(daily_volume, 1)
63
+ return self.perm_impact_coef * volatility * participation
64
+
65
+
66
+ class TWAPScheduler:
67
+ """
68
+ Time-Weighted Average Price execution.
69
+
70
+ Slices parent order into N child orders, equally distributed in time.
71
+
72
+ When to use: When you want to minimize timing risk and have no view
73
+ on intraday price direction. Simple, predictable, low market impact.
74
+
75
+ Formula: Child qty = Total qty / N buckets
76
+ """
77
+
78
+ def __init__(self,
79
+ n_buckets: int = 20,
80
+ bucket_duration_minutes: int = 15):
81
+ self.n_buckets = n_buckets
82
+ self.bucket_duration = bucket_duration_minutes
83
+
84
+ def schedule(self, order: Order,
85
+ start_time: pd.Timestamp,
86
+ end_time: Optional[pd.Timestamp] = None) -> pd.DataFrame:
87
+ """
88
+ Create TWAP execution schedule.
89
+
90
+ Returns DataFrame with bucket_start, bucket_end, target_qty
91
+ """
92
+ if end_time is None:
93
+ end_time = start_time + pd.Timedelta(
94
+ minutes=self.n_buckets * self.bucket_duration
95
+ )
96
+
97
+ # Time buckets
98
+ buckets = pd.date_range(
99
+ start=start_time,
100
+ end=end_time,
101
+ periods=self.n_buckets + 1
102
+ )
103
+
104
+ # Equal quantity per bucket
105
+ qty_per_bucket = order.quantity // self.n_buckets
106
+ remainder = order.quantity % self.n_buckets
107
+
108
+ quantities = [qty_per_bucket] * self.n_buckets
109
+ # Add remainder to first buckets
110
+ for i in range(remainder):
111
+ quantities[i] += 1
112
+
113
+ schedule = pd.DataFrame({
114
+ 'bucket_start': buckets[:-1],
115
+ 'bucket_end': buckets[1:],
116
+ 'target_qty': quantities,
117
+ 'fraction': 1.0 / self.n_buckets,
118
+ 'algorithm': 'TWAP',
119
+ 'symbol': order.symbol,
120
+ 'side': order.side
121
+ })
122
+
123
+ return schedule
124
+
125
+ def execute(self, schedule: pd.DataFrame,
126
+ market_prices: pd.Series,
127
+ impact_model: Optional[MarketImpactModel] = None,
128
+ daily_volume: int = 1000000,
129
+ volatility: float = 0.02) -> Dict:
130
+ """
131
+ Simulate TWAP execution with market impact.
132
+
133
+ Returns execution statistics.
134
+ """
135
+ if impact_model is None:
136
+ impact_model = MarketImpactModel()
137
+
138
+ executed_qty = 0
139
+ total_cost = 0
140
+ prices = []
141
+ impacts = []
142
+
143
+ for _, row in schedule.iterrows():
144
+ qty = row['target_qty']
145
+
146
+ # Get price at bucket start (approximation)
147
+ mask = market_prices.index >= row['bucket_start']
148
+ if mask.any():
149
+ price = market_prices[mask].iloc[0]
150
+ else:
151
+ price = market_prices.iloc[-1]
152
+
153
+ # Market impact
154
+ impact_bps = impact_model.temporary_impact(
155
+ qty, daily_volume, volatility
156
+ )
157
+ impact_price = price * (1 + impact_bps / 10000)
158
+
159
+ # Cost
160
+ cost = qty * impact_price
161
+ total_cost += cost
162
+ executed_qty += qty
163
+
164
+ prices.append(price)
165
+ impacts.append(impact_bps)
166
+
167
+ # VWAP benchmark
168
+ vwap = total_cost / executed_qty if executed_qty > 0 else 0
169
+
170
+ # Metrics
171
+ avg_impact = np.mean(impacts)
172
+ max_impact = np.max(impacts)
173
+
174
+ return {
175
+ 'algorithm': 'TWAP',
176
+ 'total_qty': executed_qty,
177
+ 'total_cost': total_cost,
178
+ 'avg_price': vwap,
179
+ 'avg_impact_bps': avg_impact,
180
+ 'max_impact_bps': max_impact,
181
+ 'slippage_bps': avg_impact,
182
+ 'n_child_orders': len(schedule)
183
+ }
184
+
185
+
186
+ class VWAPScheduler:
187
+ """
188
+ Volume-Weighted Average Price execution.
189
+
190
+ Slices parent order proportionally to historical volume profile.
191
+ Executes more in high-volume periods (typically open, close, mid-day lull).
192
+
193
+ When to use: When you want to match the market VWAP.
194
+ Institutional benchmark: Did my execution VWAP match the market VWAP?
195
+
196
+ Formula: Child qty_i = Total qty * (Volume_i / Total_Volume)
197
+ """
198
+
199
+ def __init__(self,
200
+ n_buckets: int = 20,
201
+ default_profile: Optional[Dict[int, float]] = None):
202
+ self.n_buckets = n_buckets
203
+
204
+ # Default intraday volume profile (U-shape: high at open/close)
205
+ if default_profile is None:
206
+ # Hour of day -> volume fraction (simplified)
207
+ self.default_profile = {
208
+ 9: 0.08, # 9-10 AM: High
209
+ 10: 0.06,
210
+ 11: 0.05,
211
+ 12: 0.04, # Mid-day lull
212
+ 13: 0.04,
213
+ 14: 0.05,
214
+ 15: 0.07,
215
+ 16: 0.10, # 3-4 PM: High (close)
216
+ }
217
+ else:
218
+ self.default_profile = default_profile
219
+
220
+ def estimate_volume_profile(self,
221
+ trade_data: pd.DataFrame,
222
+ bucket_size: str = '30min') -> pd.Series:
223
+ """
224
+ Estimate intraday volume profile from historical trade data.
225
+
226
+ trade_data columns: timestamp, volume
227
+ """
228
+ trade_data = trade_data.copy()
229
+ trade_data['time'] = pd.to_datetime(trade_data.index).time
230
+
231
+ # Resample
232
+ profile = trade_data.resample(bucket_size)['volume'].mean()
233
+
234
+ # Normalize to fractions
235
+ profile = profile / profile.sum()
236
+
237
+ return profile
238
+
239
+ def schedule(self, order: Order,
240
+ start_time: pd.Timestamp,
241
+ end_time: Optional[pd.Timestamp] = None,
242
+ volume_profile: Optional[pd.Series] = None) -> pd.DataFrame:
243
+ """Create VWAP execution schedule"""
244
+ if end_time is None:
245
+ end_time = start_time + pd.Timedelta(hours=6)
246
+
247
+ # Generate time buckets
248
+ n_buckets = self.n_buckets
249
+ buckets = pd.date_range(start=start_time, end=end_time, periods=n_buckets + 1)
250
+
251
+ # Get volume fractions for each bucket
252
+ if volume_profile is not None:
253
+ # Map buckets to volume profile
254
+ fractions = []
255
+ for i in range(n_buckets):
256
+ bucket_start = buckets[i]
257
+ hour = bucket_start.hour
258
+ frac = volume_profile.get(hour, 1.0 / n_buckets)
259
+ fractions.append(frac)
260
+
261
+ # Normalize
262
+ fractions = np.array(fractions)
263
+ fractions = fractions / fractions.sum()
264
+ else:
265
+ fractions = np.ones(n_buckets) / n_buckets
266
+
267
+ # Allocate quantities
268
+ quantities = (fractions * order.quantity).astype(int)
269
+
270
+ # Handle rounding
271
+ remainder = order.quantity - quantities.sum()
272
+ quantities[0] += remainder
273
+
274
+ schedule = pd.DataFrame({
275
+ 'bucket_start': buckets[:-1],
276
+ 'bucket_end': buckets[1:],
277
+ 'target_qty': quantities,
278
+ 'fraction': fractions,
279
+ 'algorithm': 'VWAP',
280
+ 'symbol': order.symbol,
281
+ 'side': order.side
282
+ })
283
+
284
+ return schedule
285
+
286
+ def execute(self, schedule: pd.DataFrame,
287
+ market_prices: pd.Series,
288
+ market_volumes: pd.Series,
289
+ impact_model: Optional[MarketImpactModel] = None) -> Dict:
290
+ """Simulate VWAP execution"""
291
+ if impact_model is None:
292
+ impact_model = MarketImpactModel()
293
+
294
+ executed_qty = 0
295
+ total_cost = 0
296
+ prices = []
297
+ impacts = []
298
+
299
+ for _, row in schedule.iterrows():
300
+ qty = row['target_qty']
301
+ if qty <= 0:
302
+ continue
303
+
304
+ mask = market_prices.index >= row['bucket_start']
305
+ if mask.any():
306
+ price = market_prices[mask].iloc[0]
307
+ vol = market_volumes[mask].iloc[0] if len(market_volumes[mask]) > 0 else 1000000
308
+ else:
309
+ price = market_prices.iloc[-1]
310
+ vol = 1000000
311
+
312
+ # Impact proportional to participation
313
+ impact_bps = impact_model.temporary_impact(qty, vol, 0.02)
314
+ impact_price = price * (1 + impact_bps / 10000)
315
+
316
+ cost = qty * impact_price
317
+ total_cost += cost
318
+ executed_qty += qty
319
+
320
+ prices.append(price)
321
+ impacts.append(impact_bps)
322
+
323
+ vwap = total_cost / executed_qty if executed_qty > 0 else 0
324
+
325
+ # Market VWAP (what we tried to match)
326
+ market_vwap = (market_prices * market_volumes).sum() / market_volumes.sum()
327
+
328
+ return {
329
+ 'algorithm': 'VWAP',
330
+ 'total_qty': executed_qty,
331
+ 'total_cost': total_cost,
332
+ 'avg_price': vwap,
333
+ 'market_vwap': market_vwap,
334
+ 'vwap_deviation_bps': abs(vwap - market_vwap) / market_vwap * 10000 if market_vwap > 0 else 0,
335
+ 'avg_impact_bps': np.mean(impacts) if impacts else 0,
336
+ 'n_child_orders': len(schedule)
337
+ }
338
+
339
+
340
+ class SmartOrderRouter:
341
+ """
342
+ Smart Order Routing: Select optimal venue/algorithm based on order characteristics.
343
+
344
+ Decision tree:
345
+ - Small orders (< 1% ADV): Market/limit, single venue
346
+ - Medium orders (1-10% ADV): TWAP over 1-2 hours
347
+ - Large orders (> 10% ADV): VWAP over full day, possibly dark pools
348
+ - Urgent: Market order, accept impact
349
+ - Patient: TWAP/VWAP, minimize impact
350
+ """
351
+
352
+ def __init__(self, impact_model: Optional[MarketImpactModel] = None):
353
+ self.impact_model = impact_model or MarketImpactModel()
354
+ self.twap = TWAPScheduler()
355
+ self.vwap = VWAPScheduler()
356
+
357
+ def route_order(self, order: Order,
358
+ avg_daily_volume: int,
359
+ urgency: str = 'normal',
360
+ volatility: float = 0.02) -> Dict:
361
+ """
362
+ Route order to optimal execution strategy.
363
+
364
+ Args:
365
+ order: Order specification
366
+ avg_daily_volume: Average daily volume of the symbol
367
+ urgency: 'urgent', 'normal', 'patient'
368
+ volatility: Daily volatility
369
+
370
+ Returns:
371
+ Dict with routing decision and execution schedule
372
+ """
373
+ participation = order.quantity / max(avg_daily_volume, 1)
374
+
375
+ # Decision logic
376
+ if urgency == 'urgent' or participation < 0.01:
377
+ # Small or urgent: Single market/limit order
378
+ strategy = 'market'
379
+ expected_impact = self.impact_model.temporary_impact(
380
+ order.quantity, avg_daily_volume, volatility
381
+ )
382
+ schedule = pd.DataFrame({
383
+ 'bucket_start': [pd.Timestamp.now()],
384
+ 'bucket_end': [pd.Timestamp.now()],
385
+ 'target_qty': [order.quantity],
386
+ 'fraction': [1.0],
387
+ 'algorithm': 'MARKET',
388
+ 'symbol': [order.symbol],
389
+ 'side': [order.side]
390
+ })
391
+
392
+ elif participation < 0.05:
393
+ # Medium: TWAP over 2 hours
394
+ strategy = 'twap'
395
+ schedule = self.twap.schedule(
396
+ order, pd.Timestamp.now(),
397
+ end_time=pd.Timestamp.now() + pd.Timedelta(hours=2)
398
+ )
399
+ expected_impact = self.impact_model.temporary_impact(
400
+ order.quantity // len(schedule), avg_daily_volume, volatility
401
+ )
402
+
403
+ else:
404
+ # Large: VWAP over full day
405
+ strategy = 'vwap'
406
+ schedule = self.vwap.schedule(
407
+ order, pd.Timestamp.now(),
408
+ end_time=pd.Timestamp.now() + pd.Timedelta(hours=6)
409
+ )
410
+ expected_impact = self.impact_model.temporary_impact(
411
+ order.quantity // len(schedule), avg_daily_volume, volatility
412
+ )
413
+
414
+ return {
415
+ 'order': order,
416
+ 'strategy': strategy,
417
+ 'participation_rate': participation,
418
+ 'expected_impact_bps': expected_impact,
419
+ 'schedule': schedule,
420
+ 'urgency': urgency
421
+ }
422
+
423
+
424
+ def benchmark_execution_algorithms():
425
+ """Compare TWAP vs VWAP vs Market order on synthetic data"""
426
+ np.random.seed(42)
427
+
428
+ # Generate synthetic intraday data
429
+ n_minutes = 390 # Trading day minutes (9:30 - 16:00)
430
+ times = pd.date_range('2024-01-01 09:30', periods=n_minutes, freq='1min')
431
+
432
+ # Price: random walk with slight drift
433
+ price = 100.0
434
+ prices = [price]
435
+ for _ in range(n_minutes - 1):
436
+ price *= (1 + np.random.randn() * 0.001)
437
+ prices.append(price)
438
+
439
+ # Volume: U-shaped intraday pattern
440
+ base_vol = 1000
441
+ hours = np.arange(n_minutes) / 60
442
+ vol_pattern = 0.5 + 2.0 * np.exp(-((hours - 0.5) ** 2) / 0.1) + \
443
+ 0.5 * np.sin(hours * np.pi)
444
+ volumes = (base_vol * vol_pattern * (1 + np.random.randn(n_minutes) * 0.2)).astype(int)
445
+ volumes = np.maximum(volumes, 100)
446
+
447
+ price_series = pd.Series(prices, index=times)
448
+ volume_series = pd.Series(volumes, index=times)
449
+
450
+ # Create order
451
+ order = Order(symbol='AAPL', side='buy', quantity=50000, order_type='twap')
452
+
453
+ # TWAP
454
+ twap = TWAPScheduler(n_buckets=20)
455
+ twap_schedule = twap.schedule(order, times[0])
456
+ twap_result = twap.execute(twap_schedule, price_series,
457
+ daily_volume=volumes.sum(), volatility=0.02)
458
+
459
+ # VWAP
460
+ vwap = VWAPScheduler(n_buckets=20)
461
+ vwap_schedule = vwap.schedule(order, times[0],
462
+ volume_profile=None)
463
+ vwap_result = vwap.execute(vwap_schedule, price_series, volume_series)
464
+
465
+ # Market (single order)
466
+ market_impact = MarketImpactModel()
467
+ market_price = price_series.iloc[0]
468
+ impact = market_impact.temporary_impact(50000, volumes.sum(), 0.02)
469
+ market_cost = 50000 * market_price * (1 + impact / 10000)
470
+
471
+ print("=" * 60)
472
+ print("EXECUTION ALGORITHM BENCHMARK")
473
+ print("=" * 60)
474
+ print(f"\nOrder: Buy 50,000 AAPL shares")
475
+ print(f"ADV: {volumes.sum():,} | Participation: {50000/volumes.sum()*100:.1f}%")
476
+ print()
477
+ print(f"MARKET ORDER:")
478
+ print(f" Cost: ${market_cost:,.2f} | Impact: {impact:.1f} bps | Slippage: {impact:.1f} bps")
479
+ print()
480
+ print(f"TWAP:")
481
+ print(f" Cost: ${twap_result['total_cost']:,.2f} | Impact: {twap_result['avg_impact_bps']:.1f} bps")
482
+ print(f" Avg Price: ${twap_result['avg_price']:.2f} | Child Orders: {twap_result['n_child_orders']}")
483
+ print()
484
+ print(f"VWAP:")
485
+ print(f" Cost: ${vwap_result['total_cost']:,.2f} | Impact: {vwap_result['avg_impact_bps']:.1f} bps")
486
+ print(f" Avg Price: ${vwap_result['avg_price']:.2f}")
487
+ print(f" Market VWAP: ${vwap_result['market_vwap']:.2f} | Deviation: {vwap_result['vwap_deviation_bps']:.1f} bps")
488
+ print()
489
+
490
+ savings_twap = (market_cost - twap_result['total_cost']) / market_cost * 100
491
+ savings_vwap = (market_cost - vwap_result['total_cost']) / market_cost * 100
492
+ print(f"Savings vs Market Order:")
493
+ print(f" TWAP: {savings_twap:.2f}% | VWAP: {savings_vwap:.2f}%")
494
+
495
+
496
+ if __name__ == '__main__':
497
+ benchmark_execution_algorithms()