Optimal Execution Algorithms: TWAP, VWAP & Market Impact › Adaptive Execution Algorithms

Michael BrenndoerferJanuary 20, 202510 min read
Quantitative FinanceAll chapters
Part 12 of 15Market Making and High-Frequency Trading
Chapter 68Optimal Execution Algorithms: TWAP, VWAP & Market Impact › Adaptive Execution Algorithms
Reading section 6 of 10 · Adaptive Execution Algorithms · 10 minSection 6 of 10 · 10 min

Book contents

Section 6 of 10 · 10 min read

Optimal Execution Algorithms: TWAP, VWAP & Market Impact › Adaptive Execution AlgorithmsOne section · Sections in this chapter6 / 10 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

While the Almgren-Chriss framework provides optimal trajectories given known parameters, real markets present additional challenges. Volatility fluctuates, liquidity varies, and short-term price movements may signal information. Adaptive algorithms modify their behavior in response to market conditions.

Percentage of Volume (POV)

Percentage of Volume algorithms trade as a fixed fraction of observed market volume, adapting execution speed to current liquidity. The algorithm monitors real-time trading activity and participates proportionally. This approach gives you natural camouflage.

vt=α⋅Vtmarketv_t = \alpha \cdot V_t^{\text{market}}

where:

  • vtv_t: trading rate at time tt
  • α\alpha: target participation rate, typically between 5% and 20%
  • VtmarketV_t^{\text{market}}: observed market volume at time tt

The participation rate α\alpha is specified by you and represents what fraction of all market activity the algorithm should constitute. A 10% participation rate means that for every 100 shares traded in the market the algorithm trades 10.

POV offers natural camouflage. By trading proportionally with the market, your algorithm's activity is harder to distinguish from background flow. Other market participants cannot easily detect that your algorithmic execution is underway because the trading pattern mirrors overall market activity. POV also automatically slows during thin markets and accelerates during liquid periods, and gives a built-in adaptation mechanism that schedule-based algorithms lack.

In[14]:
Code
def simulate_pov_execution(
    total_shares: int,
    participation_rate: float,
    market_volumes: np.ndarray,
    max_participation: float = 0.25,  # Cap to avoid excessive impact
) -> tuple[np.ndarray, np.ndarray]:
    """
    Simulate POV execution given market volume series.

    Returns:
        executed: Shares executed in each interval
        remaining: Remaining inventory after each interval
    """
    remaining = total_shares
    executed = []
    inventory = [remaining]

    for vol in market_volumes:
        if remaining <= 0:
            executed.append(0)
            inventory.append(0)
            continue

        # Trade up to participation rate of volume, capped
        target_trade = min(participation_rate * vol, max_participation * vol)
        actual_trade = min(target_trade, remaining)

        executed.append(actual_trade)
        remaining -= actual_trade
        inventory.append(remaining)

    return np.array(executed), np.array(inventory[:-1])


import numpy as np

# Simulate varying market volume
np.random.seed(42)
num_periods = 50
base_volume = create_intraday_volume_profile(num_periods)
# Add random variation to volume
market_volumes = (
    100_000 * base_volume * (1 + 0.3 * np.random.randn(num_periods))
)
market_volumes = np.maximum(market_volumes, 1000)  # Ensure positive

# Execute with 15% POV
pov_executed, pov_remaining = simulate_pov_execution(
    total_shares=50_000, participation_rate=0.15, market_volumes=market_volumes
)
Out[15]:
Visualization
Dual-axis chart showing market volume bars and executed shares line tracking each other proportionally.
POV algorithm executing 50,000 shares at a 15% participation rate, automatically scaling execution interval by interval with realized market volume. Executed shares rise and fall proportionally with the volume bars, maintaining consistent participation while avoiding a fixed clock-based schedule.

Pegging Algorithms

Pegging algorithms dynamically adjust limit order prices to maintain a position relative to the current market. Rather than crossing the spread to execute immediately, these algorithms post passive orders that capture the spread. Common peg types include:

  • Midpoint peg: order price tracks the midpoint between bid and ask; it has price improvement to both sides
  • Primary peg: order tracks the same-side best quote, joining the queue at the bid for buys or the ask for sells
  • Passive peg: order tracks one tick behind the best quote for added price improvement at the cost of lower fill probability

These algorithms are useful when you prioritize execution price. By posting limit orders rather than crossing the spread, pegging algorithms capture the spread rather than paying it but risk non-execution if the market moves away.

In[16]:
Code
def simulate_midpoint_peg(
    total_shares: int,
    num_periods: int,
    fill_probability: float = 0.3,  # Probability of fill each period when pegged
    price_volatility: float = 0.01,
) -> dict:
    """
    Simulate midpoint pegging execution.

    Lower fill probability reflects the uncertainty of passive execution.
    """
    np.random.seed(123)

    remaining = total_shares
    period_size = total_shares // num_periods

    executed = []
    midpoints = [100.0]  # Starting midpoint
    execution_prices = []

    for i in range(num_periods):
        # Random midpoint movement
        midpoint = midpoints[-1] * (1 + price_volatility * np.random.randn())
        midpoints.append(midpoint)

        if remaining <= 0:
            executed.append(0)
            continue

        # Probabilistic fill at midpoint
        if np.random.random() < fill_probability:
            fill_amount = min(period_size, remaining)
            executed.append(fill_amount)
            execution_prices.extend([midpoint] * int(fill_amount // 100))
            remaining -= fill_amount
        else:
            executed.append(0)

    return {
        "executed": np.array(executed),
        "midpoints": np.array(midpoints[1:]),
        "remaining": remaining,
        "avg_price": np.mean(execution_prices) if execution_prices else None,
    }


import numpy as np

peg_target = 50_000
peg_result = simulate_midpoint_peg(peg_target, 50, fill_probability=0.35)
Out[17]:
Console
Midpoint Pegging Results:
Total executed: 17,000 / 50,000 shares (34.0%)
Remaining: 33,000 shares
Average execution price: $101.3119
Out[18]:
Visualization
Dual-axis chart showing a noisy midpoint price path in blue with red scatter dots marking individual fills, and a grey cumulative execution curve rising in steps.
Midpoint pegging execution simulation for 50,000 shares over 50 periods with 35% fill probability per period. Executed fills reach approximately 17,500 shares, revealing how midpoint pegging captures bid-ask spread savings at the cost of execution uncertainty. This strategy is optimal when price improvement matters more than completing the full order.

Adaptive Shortfall Algorithms

Modern implementation shortfall algorithms combine the theoretical foundation of Almgren-Chriss with real-time adaptation. Rather than committing to a fixed trajectory computed at the start of execution, these algorithms continuously revise their plans based on incoming market data. Key features include:

  • Intraday volatility estimation: adjusting urgency based on current versus expected volatility
  • Spread monitoring: slowing execution when spreads widen beyond normal levels
  • Momentum detection: accelerating or decelerating based on short-term price trends
  • Volume surprise: adjusting participation when actual volume differs from forecast

These algorithms maintain a target trajectory as a baseline but allow deviations when market conditions diverge significantly from expectations.

In[19]:
Code
class AdaptiveISAlgorithm:
    """
    Adaptive Implementation Shortfall algorithm that adjusts
    execution based on real-time market conditions.
    """

    def __init__(
        self,
        total_shares: int,
        num_periods: int,
        base_urgency: float = 0.5,
        volatility_sensitivity: float = 0.5,
        spread_sensitivity: float = 0.3,
    ):
        self.total_shares = total_shares
        self.num_periods = num_periods
        self.base_urgency = base_urgency
        self.vol_sens = volatility_sensitivity
        self.spread_sens = spread_sensitivity

        # Generate base schedule
        self.base_schedule = implementation_shortfall_schedule(
            total_shares, num_periods, base_urgency
        )
        self.remaining = total_shares
        self.executed_history = []

    def compute_execution(
        self,
        period: int,
        realized_volatility: float,
        expected_volatility: float,
        current_spread: float,
        normal_spread: float,
    ) -> float:
        """Compute adaptive execution for current period."""

        if self.remaining <= 0:
            return 0

        # Base execution from schedule
        base_exec = self.base_schedule[period]

        # Volatility adjustment: execute faster when vol is high
        vol_ratio = realized_volatility / expected_volatility
        vol_adjustment = 1 + self.vol_sens * (vol_ratio - 1)

        # Spread adjustment: slow down when spreads are wide
        spread_ratio = current_spread / normal_spread
        spread_adjustment = 1 - self.spread_sens * (spread_ratio - 1)
        spread_adjustment = max(0.3, min(1.5, spread_adjustment))  # Bounds

        # Combined adjustment
        adjusted_exec = base_exec * vol_adjustment * spread_adjustment

        # Cap at remaining inventory
        actual_exec = min(adjusted_exec, self.remaining)

        self.remaining -= actual_exec
        self.executed_history.append(actual_exec)

        return actual_exec


import numpy as np

# Simulate adaptive execution
np.random.seed(456)

algo = AdaptiveISAlgorithm(
    total_shares=100_000, num_periods=40, base_urgency=0.5
)

# Simulate market conditions
expected_vol = 0.015
normal_spread = 0.02

adaptive_executions = []
market_conditions = []

for period in range(40):
    # Varying market conditions
    if 10 <= period <= 20:  # High volatility regime
        realized_vol = expected_vol * (1.5 + 0.3 * np.random.randn())
        current_spread = normal_spread * (1.3 + 0.2 * np.random.randn())
    else:  # Normal regime
        realized_vol = expected_vol * (1.0 + 0.2 * np.random.randn())
        current_spread = normal_spread * (1.0 + 0.1 * np.random.randn())

    realized_vol = max(0.005, realized_vol)
    current_spread = max(0.005, current_spread)

    execution = algo.compute_execution(
        period, realized_vol, expected_vol, current_spread, normal_spread
    )

    adaptive_executions.append(execution)
    market_conditions.append(
        {"volatility": realized_vol, "spread": current_spread}
    )

adaptive_executions = np.array(adaptive_executions)
Out[20]:
Visualization
Line chart of realized volatility over 40 periods with an expected-volatility reference line and periods 10 through 20 shaded as the high-volatility regime.
Realized volatility across the 40-period execution window. Volatility rises above its expected level during periods 10 to 20, creating additional timing risk.
Line chart of the bid-ask spread over 40 periods with a normal-spread reference line and periods 10 through 20 shaded as the widened-spread regime.
Bid-ask spread across the execution window. The spread widens during periods 10 to 20, increasing the immediate cost of accelerated trading.
Bar chart of shares executed by period for the adaptive algorithm, overlaid with an outlined baseline execution schedule.
Adaptive implementation shortfall execution compared with the baseline schedule. The algorithm accelerates during the volatile regime while accounting for the simultaneous spread increase.

The adaptive algorithm increases execution during the high-volatility regime (periods 10-20) to reduce timing risk, while the widened spreads partially counteract this acceleration. This dynamic adjustment is impossible with static schedules like TWAP or pre-computed VWAP.