---
title: "Optimal Stopping"
description: "Longstaff-Schwartz regression-based optimal stopping for American-style exit decisions in prediction markets."
icon: "hand"
---

<Note>
  **Pro Feature.** Requires a Pro or Ultra subscription. [Get started at api.mathematicalcompany.com](https://api.mathematicalcompany.com)
</Note>

<Tip>
  **What is this?** When should you exit a position? Hold too long and you give back profits; exit too early and you leave money on the table. The Longstaff-Schwartz algorithm solves this as an optimal stopping problem, computing an exercise boundary that tells you exactly when the expected value of holding drops below the value of exiting.
</Tip>

# Optimal Stopping

When should you exit a prediction market position? Holding too long risks giving back profits; exiting too early leaves money on the table. The Longstaff-Schwartz algorithm solves this as an optimal stopping problem: at each time step, compare the immediate exit value to the expected value of continuing. Horizon implements the full backward-induction algorithm in Rust, including path generation and basis-function regression.

<CardGroup cols={2}>
  <Card title="Path Generation" icon="route">
    `hz.generate_gbm_paths()` simulates geometric Brownian motion paths for Monte Carlo valuation.
  </Card>
  <Card title="Longstaff-Schwartz" icon="backward">
    `hz.longstaff_schwartz()` computes the optimal stopping policy via backward regression on simulated paths.
  </Card>
  <Card title="Live Decision" icon="hand">
    `hz.should_exit()` evaluates the learned policy against current state to produce a hold/exit signal.
  </Card>
  <Card title="Pipeline Integration" icon="plug">
    `hz.exit_optimizer()` runs the stopping policy each cycle and injects exit signals into your strategy context.
  </Card>
</CardGroup>

---

## hz.generate_gbm_paths

Generate simulated price paths using geometric Brownian motion for use in the Longstaff-Schwartz algorithm.

```python
import horizon as hz

paths = hz.generate_gbm_paths(
    s0=0.55,          # initial price
    mu=0.0,           # drift (annualized)
    sigma=0.30,       # volatility (annualized)
    t=30.0,           # time horizon in days
    n_steps=100,      # time steps per path
    n_paths=10000,    # number of simulated paths
    seed=42,          # optional RNG seed for reproducibility
)
# paths is a list of lists: n_paths x (n_steps + 1)
print(f"Generated {len(paths)} paths, each with {len(paths[0])} steps")
print(f"First path final price: {paths[0][-1]:.4f}")
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `s0` | `float` | Initial price (e.g., current market probability) |
| `mu` | `float` | Annualized drift rate |
| `sigma` | `float` | Annualized volatility |
| `t` | `float` | Time horizon in days |
| `n_steps` | `int` | Number of time steps per path |
| `n_paths` | `int` | Number of Monte Carlo paths to simulate |
| `seed` | `int` or `None` | Optional RNG seed. If `None`, uses entropy-based seed |

Returns `list[list[float]]`: matrix of shape (n_paths, n_steps + 1) with simulated prices. Prices are clamped to [0.01, 0.99] to respect prediction market bounds.

---

## hz.longstaff_schwartz

Compute the optimal stopping policy using backward regression on simulated paths. At each time step, the algorithm regresses the continuation value against polynomial basis functions of the current state, then compares the immediate exercise value to the fitted continuation value.

```python
import horizon as hz

paths = hz.generate_gbm_paths(
    s0=0.55, mu=0.0, sigma=0.30,
    t=30.0, n_steps=100, n_paths=10000, seed=42,
)

policy = hz.longstaff_schwartz(
    paths=paths,
    entry_price=0.50,      # price you entered at
    transaction_cost=0.02, # round-trip cost (spread + fees)
    discount_rate=0.0,     # daily discount rate (0 for prediction markets)
    basis_degree=3,        # polynomial degree for regression
)

print(f"Optimal exit threshold at t=0: {policy.thresholds[0]:.4f}")
print(f"Expected value (hold): {policy.expected_value:.4f}")
print(f"Expected value (exit now): {policy.immediate_value:.4f}")
print(f"Recommendation: {policy.action}")  # "hold" or "exit"
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `paths` | `list[list[float]]` | Simulated price paths from `generate_gbm_paths()` |
| `entry_price` | `float` | Price at which you entered the position |
| `transaction_cost` | `float` | Round-trip transaction cost (spread + fees) |
| `discount_rate` | `float` | Daily discount rate. Use 0.0 for prediction markets (no time value of money) |
| `basis_degree` | `int` | Polynomial degree for the regression basis (2 or 3 recommended) |

### StopPolicy Type

| Field | Type | Description |
|-------|------|-------------|
| `thresholds` | `list[float]` | Optimal exit threshold at each time step. Exit when price >= threshold[t] |
| `expected_value` | `float` | Expected value of following the optimal policy from t=0 |
| `immediate_value` | `float` | Value of exiting immediately at current price |
| `action` | `str` | `"hold"` or `"exit"` based on current state vs. threshold |
| `confidence` | `float` | Confidence in the recommendation (0.0 to 1.0), based on how far the current price is from the threshold |
| `coefficients` | `list[list[float]]` | Regression coefficients at each time step (for advanced inspection) |

<Note>
  The `thresholds` list has one entry per time step. The threshold typically decreases over time: as expiry approaches, the option to wait becomes less valuable, so you become willing to exit at lower prices.
</Note>

---

## hz.should_exit

Evaluate the learned stopping policy against a current price and time fraction to produce a hold/exit signal. This is the lightweight evaluation function for use in live trading.

```python
import horizon as hz

# Compute policy once (expensive)
policy = hz.longstaff_schwartz(paths, entry_price=0.50, transaction_cost=0.02)

# Evaluate many times (cheap)
signal = hz.should_exit(
    policy=policy,
    current_price=0.62,
    time_fraction=0.3,  # 30% of the way to expiry
)

print(signal.action)      # "hold" or "exit"
print(signal.confidence)  # 0.0 to 1.0
print(signal.threshold)   # exit threshold at this time
print(signal.edge)        # current_price - threshold (positive = above threshold)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `policy` | `StopPolicy` | Learned stopping policy from `longstaff_schwartz()` |
| `current_price` | `float` | Current market price |
| `time_fraction` | `float` | Fraction of time elapsed (0.0 = start, 1.0 = expiry) |

| Field | Type | Description |
|-------|------|-------------|
| `action` | `str` | `"hold"` or `"exit"` |
| `confidence` | `float` | Distance from threshold normalized to [0, 1] |
| `threshold` | `float` | Exit threshold at this time step |
| `edge` | `float` | current_price - threshold. Positive means price is above the exit threshold |

---

## Pipeline Integration

The `hz.exit_optimizer()` pipeline function evaluates the stopping policy each cycle and injects exit signals into `ctx.params["exit_signal"]`.

```python
import horizon as hz

def model(ctx):
    exit_sig = ctx.params.get("exit_signal")
    if exit_sig is None:
        return []

    if exit_sig.action == "exit" and exit_sig.confidence > 0.7:
        # Strong exit signal: close the position
        return hz.close_position(ctx, market="election")

    # Otherwise, continue market making
    return hz.quotes(fair=ctx.feeds["poly"].price, spread=0.04, size=10)

hz.run(
    name="optimal-exit",
    markets=["election"],
    pipeline=[
        hz.exit_optimizer(
            feed="poly",
            entry_price=0.50,
            transaction_cost=0.02,
            horizon_days=30.0,
            n_paths=10000,
            recompute_every=500,  # recompute policy every 500 cycles
        ),
        model,
    ],
    feeds={"poly": hz.PolymarketBook(token_id="0x123...")},
    interval=5.0,
)
```

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `feed` | `str` | required | Feed name to read current price from |
| `entry_price` | `float` | required | Price at which the position was entered |
| `transaction_cost` | `float` | `0.02` | Round-trip transaction cost |
| `horizon_days` | `float` | `30.0` | Time horizon in days until market resolution |
| `n_paths` | `int` | `10000` | Number of Monte Carlo paths for policy computation |
| `recompute_every` | `int` | `500` | Recompute the policy every N cycles (uses updated volatility estimate) |

---

## Mathematical Background

<AccordionGroup>
  <Accordion title="Longstaff-Schwartz Algorithm">
    The Longstaff-Schwartz (2001) algorithm solves the American option stopping problem via backward induction on simulated paths:

    1. Generate N price paths using Monte Carlo simulation.
    2. At the final time step T, the exercise value is known: max(S_T - K, 0) for a call (or simply S_T - entry for a position exit).
    3. Moving backward from T-1 to 0: for paths where immediate exercise has positive value, regress the discounted future continuation value against polynomial basis functions of the current price.
    4. At each step, exercise if the immediate value exceeds the fitted continuation value.
    5. The optimal stopping time for each path is recorded, and the policy is the set of regression coefficients.

    For prediction markets, the "exercise value" is the profit from closing the position (current_price - entry_price - transaction_cost).
  </Accordion>
  <Accordion title="Why Not a Fixed Threshold?">
    A fixed take-profit threshold ignores the time dimension. Early in a market's life, a position at 0.60 (entered at 0.50) might be worth holding because there is time for further appreciation. Near expiry, the same position should be exited because the remaining optionality is small. The Longstaff-Schwartz approach produces a time-varying threshold that accounts for this.
  </Accordion>
  <Accordion title="Basis Functions">
    The regression uses polynomial basis functions of the current price: 1, S, S^2, S^3, and so on. A degree of 3 is usually sufficient. Higher degrees can overfit to Monte Carlo noise. The Rust implementation uses Householder QR decomposition for numerically stable least-squares regression.
  </Accordion>
</AccordionGroup>

<Warning>
  The quality of the stopping policy depends on the accuracy of the volatility estimate and the number of simulated paths. Use at least 5000 paths for reliable results. Recompute the policy periodically as market conditions change (the `recompute_every` parameter in the pipeline handles this automatically).
</Warning>
