File size: 6,709 Bytes
b54319d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# backend/app/execution_algo.py

"""
Institutional Algorithmic Execution Engine & Market Microstructure Friction Engine.
Implements:
1. Almgren-Chriss Optimal Execution Model (Trajectory calculation balancing market impact vs inventory risk).
2. Square-Root Market Impact Friction Model (Kyle's Lambda / Almgren et al. empirical impact).
3. Time-Weighted Average Price (TWAP) Slicing Engine.
4. Volume-Weighted Average Price (VWAP) Slicing Engine with intraday volume profile modeling.
"""

import numpy as np
import pandas as pd
from typing import Dict, List, Tuple

class ExecutionAlgoEngine:
    def __init__(self, daily_volatility: float = 0.02, avg_daily_volume: float = 1_000_000, permanent_impact_gamma: float = 2.5e-7, temporary_impact_eta: float = 2.5e-6):
        """
        Parameters:
        - daily_volatility (sigma): Asset daily return volatility.
        - avg_daily_volume (V): Average daily volume of the target asset.
        - permanent_impact_gamma: Gamma coefficient for permanent price impact.
        - temporary_impact_eta: Eta coefficient for temporary price impact.
        """
        self.sigma = daily_volatility
        self.V = avg_daily_volume
        self.gamma = permanent_impact_gamma
        self.eta = temporary_impact_eta

    def square_root_market_impact(self, trade_size: int, current_price: float, interval_volume: float = None) -> Tuple[float, float]:
        """
        Calculates market impact using the empirical Square-Root Law:
        Impact_pct = eta * sigma * sqrt(Q / V)
        Returns:
            temp_impact_dollars: float (temporary impact per share)
            perm_impact_dollars: float (permanent impact per share)
        """
        if interval_volume is None or interval_volume <= 0:
            interval_volume = self.V / 390.0 # Default 1-minute volume (390 trading minutes)
        
        participation_rate = trade_size / max(interval_volume, 1.0)
        temp_impact_pct = self.eta * self.sigma * np.sqrt(max(trade_size, 0.0) / max(interval_volume, 1.0))
        perm_impact_pct = self.gamma * (trade_size / max(self.V, 1.0))

        temp_impact_dollars = current_price * temp_impact_pct
        perm_impact_dollars = current_price * perm_impact_pct
        return temp_impact_dollars, perm_impact_dollars

    def generate_twap_schedule(self, total_shares: int, num_slices: int) -> List[int]:
        """
        Splits total_shares evenly across num_slices execution intervals.
        """
        if num_slices <= 0:
            return [total_shares]
        base_slice = total_shares // num_slices
        remainder = total_shares % num_slices
        
        schedule = [base_slice] * num_slices
        for i in range(remainder):
            schedule[i] += 1
        return schedule

    def generate_vwap_schedule(self, total_shares: int, intraday_volume_profile: List[float] = None) -> List[int]:
        """
        Splits total_shares proportional to expected intraday U-shaped volume profile.
        """
        if intraday_volume_profile is None or len(intraday_volume_profile) == 0:
            # Default 13 half-hour buckets U-shaped volume curve for US Equity market (09:30 - 16:00)
            u_shape = np.array([0.18, 0.10, 0.07, 0.05, 0.04, 0.04, 0.04, 0.04, 0.05, 0.06, 0.08, 0.11, 0.14])
            intraday_volume_profile = u_shape / u_shape.sum()

        profile = np.array(intraday_volume_profile)
        profile = profile / profile.sum()
        
        raw_schedule = profile * total_shares
        schedule = np.floor(raw_schedule).astype(int)
        remainder = total_shares - schedule.sum()
        
        # Distribute remainder to highest fractional parts
        fractional = raw_schedule - schedule
        top_indices = np.argsort(fractional)[::-1][:remainder]
        for idx in top_indices:
            schedule[idx] += 1
            
        return schedule.tolist()

    def almgren_chriss_optimal_trajectory(self, total_shares: int, total_time_intervals: int, risk_aversion_lambda: float = 1e-5) -> Tuple[np.ndarray, np.ndarray, float]:
        """
        Computes Almgren-Chriss Optimal Execution Trajectory.
        Solves: min E[Total Cost] + lambda * Var[Total Cost]
        
        Returns:
            x: np.ndarray (inventory schedule remaining at each step)
            n: np.ndarray (trade size executed at each interval)
            expected_total_cost: float
        """
        N = total_time_intervals
        tau = 1.0 / N # normalized interval
        
        # Almgren-Chriss parameters
        # kappa^2 ~ (lambda * sigma^2 / eta) * tau
        kappa_sq = (risk_aversion_lambda * (self.sigma ** 2) / max(self.eta, 1e-9)) * tau
        kappa = np.sqrt(max(kappa_sq, 1e-8))
        
        t = np.arange(N + 1)
        
        # Closed-form Almgren-Chriss inventory trajectory:
        # x_j = sinh(kappa * (T - t_j)) / sinh(kappa * T) * X_0
        T = 1.0 # normalized total time
        t_j = t * tau
        
        sinh_kappa_T = np.sinh(kappa * T)
        if np.isinf(sinh_kappa_T) or sinh_kappa_T == 0:
            # Fallback to linear TWAP if numerical overflow occurs
            x = total_shares * (1.0 - t_j / T)
        else:
            x = total_shares * np.sinh(kappa * (T - t_j)) / sinh_kappa_T
            
        x = np.maximum(x, 0.0)
        x[0] = total_shares
        x[-1] = 0.0
        
        # Trade sizes at each step
        n = -np.diff(x)
        
        # Compute Expected Total Cost (Permanent Impact + Temporary Impact)
        perm_cost = 0.5 * self.gamma * (total_shares ** 2)
        temp_cost = self.eta * np.sum(n ** 2) / tau
        expected_cost = perm_cost + temp_cost
        
        return np.round(x, 2), np.round(n, 2), float(expected_cost)

if __name__ == "__main__":
    print("Testing ExecutionAlgoEngine...")
    algo = ExecutionAlgoEngine(daily_volatility=0.02, avg_daily_volume=5_000_000)
    
    # 1. Market Impact Test
    temp_i, perm_i = algo.square_root_market_impact(trade_size=10_000, current_price=150.0, interval_volume=50_000)
    print(f"Square-Root Impact for 10k shares at $150: Temp = ${temp_i:.4f}/sh, Perm = ${perm_i:.4f}/sh")

    # 2. TWAP / VWAP Slicing Test
    twap = algo.generate_twap_schedule(100_000, 10)
    vwap = algo.generate_vwap_schedule(100_000)
    print(f"TWAP (10 slices): {twap}")
    print(f"VWAP (13 slices): {vwap}")

    # 3. Almgren-Chriss Trajectory Test
    x_traj, n_trades, cost = algo.almgren_chriss_optimal_trajectory(total_shares=100_000, total_time_intervals=10, risk_aversion_lambda=1e-5)
    print(f"Almgren-Chriss Trade Slices: {n_trades}")
    print(f"Expected Implementation Shortfall Cost: ${cost:.2f}")
    print("[+] ExecutionAlgoEngine operational.")