Markets: A Rigorous Course

Exercise · m04a_trailing.py

ATR and trailing stops

ATR. True range TR_t = max(H_t, C_{t-1}) - min(L_t, C_{t-1}); Wilder's ATR with period n is W_n = mean(TR_1..TR_n), then W_t = ((n-1) W_{t-1} + TR_t)/n. For a continuously traded log price with daily volatility sigma and no gaps, E[H-L] ~ 2 sqrt(2/pi) sigma S (M09 Proposition 9.1), so sigma ~ ATR/(2 sqrt(2/pi) S).

Trailing stop. A long position in X_t = x0 + mu t + sigma W_t exits at the first time the drawdown from the running maximum M_t reaches d: tau = inf{t: M_t - X_t >= d}. Proposition 4A.11 (Taylor 1975; Lehoczky 1977): M_tau - x0 is exponential with mean m(d) = (exp(g d) - 1)/g, g = 2 mu / sigma^2 (m = d when mu = 0), so E[P&L] = m - d, P(loss) = 1 - exp(-d/m), E[tau] = (m - d)/mu (= d^2/sigma^2 if mu = 0), and the give-back from the peak, M_tau - X_tau, equals d under continuous monitoring.

Tests (6)

  • test_true_range_hand_fixture
  • test_atr_wilder_recursion
  • test_atr_estimates_sigma_without_gaps
  • test_trailing_exit_on_hand_path
  • test_trailing_analytic_values
  • test_trailing_simulation_vs_corrected_analytic

Theory: M04A notes. Run with Ctrl/⌘+Enter.

not run yet

Not saved: sign up to keep your code
"""M04A exercise 3: ATR and trailing stops (notes §4A.7-4A.8).

ATR. True range TR_t = max(H_t, C_{t-1}) - min(L_t, C_{t-1}); Wilder's ATR with
period n is W_n = mean(TR_1..TR_n), then W_t = ((n-1) W_{t-1} + TR_t)/n.
For a continuously traded log price with daily volatility sigma and no gaps,
E[H-L] ~ 2 sqrt(2/pi) sigma S (M09 Proposition 9.1), so sigma ~ ATR/(2 sqrt(2/pi) S).

Trailing stop. A long position in X_t = x0 + mu t + sigma W_t exits at the first
time the drawdown from the running maximum M_t reaches d: tau = inf{t: M_t - X_t >= d}.
Proposition 4A.11 (Taylor 1975; Lehoczky 1977): M_tau - x0 is exponential with mean
    m(d) = (exp(g d) - 1)/g,  g = 2 mu / sigma^2   (m = d when mu = 0),
so E[P&L] = m - d, P(loss) = 1 - exp(-d/m), E[tau] = (m - d)/mu (= d^2/sigma^2 if mu = 0),
and the give-back from the peak, M_tau - X_tau, equals d under continuous monitoring.
"""

from __future__ import annotations

import math

import numpy as np
import pandas as pd

RANGE_MEAN = 2.0 * math.sqrt(2.0 / math.pi)  # E[max W - min W] on [0,1], about 1.5958
BETA = 0.5825971579390106


def true_range(ohlc: pd.DataFrame) -> pd.Series:
    """TR_t from columns open/high/low/close (same index). The first bar has no
    previous close: use H - L."""
    raise NotImplementedError


def atr_wilder(ohlc: pd.DataFrame, n: int = 14) -> pd.Series:
    """Wilder's ATR: NaN for positions 0..n-2, mean(TR[0..n-1]) at position n-1,
    then ATR_t = ((n-1) ATR_{t-1} + TR_t)/n."""
    raise NotImplementedError


def atr_to_sigma(atr: float, price: float) -> float:
    """Daily log volatility implied by an ATR under the no-gap Brownian model: atr/(RANGE_MEAN*price)."""
    raise NotImplementedError


def trailing_stop_exit(path: np.ndarray, d: float) -> tuple[int, float, float]:
    """Exit of a long trailing stop of distance d on a monitored price path.

    path[0] is the entry price. Exit at the first index n >= 1 with
    max(path[:n+1]) - path[n] >= d, at price path[n]. If never triggered, exit at
    the last index. Returns (exit_index, exit_price, running_max_at_exit).
    """
    raise NotImplementedError


def trailing_analytic(mu: float, sigma: float, d: float) -> dict:
    """Closed forms of Proposition 4A.11 for continuous monitoring.

    Returns dict(mean_peak, mean_pnl, p_loss, mean_time) where mean_peak = E[M_tau - x0].
    Handle mu -> 0 continuously (use math.expm1).
    """
    raise NotImplementedError


def simulate_trailing(
    mu: float, sigma: float, d: float, dt: float, n_paths: int, seed: int, max_steps: int = 200_000
) -> dict:
    """Monte Carlo of a trailing stop monitored every dt on X = mu t + sigma W, x0 = 0.

    Use np.random.default_rng(seed). The running max starts at 0 and is updated on
    the grid; exit at the first grid time with max - X >= d.
    Returns dict(mean_peak, mean_pnl, p_loss, mean_time, mean_giveback, se_pnl), where
    peak is the observed running max at exit, giveback = peak - exit value, p_loss =
    fraction of exits below 0, se_pnl = std(pnl, ddof=1)/sqrt(n_paths).
    """
    raise NotImplementedError

No runs yet

The stub's functions raise NotImplementedError until you write them. Fill in one, press Run tests, and watch its tests turn green.

Reference solution

Available after your first run, with an account approved for code runs.

Create a free account

Reading is open to everyone. An account keeps your progress, answers, notes and code drafts, and gives you the study plan, the journals and the simulator. Running code needs an account approved for it.