Skip to main content
Pro Feature. Requires a Pro or Ultra subscription. Get started at api.mathematicalcompany.com
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.

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.

Path Generation

hz.generate_gbm_paths() simulates geometric Brownian motion paths for Monte Carlo valuation.

Longstaff-Schwartz

hz.longstaff_schwartz() computes the optimal stopping policy via backward regression on simulated paths.

Live Decision

hz.should_exit() evaluates the learned policy against current state to produce a hold/exit signal.

Pipeline Integration

hz.exit_optimizer() runs the stopping policy each cycle and injects exit signals into your strategy context.

hz.generate_gbm_paths

Generate simulated price paths using geometric Brownian motion for use in the Longstaff-Schwartz algorithm.
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}")
ParameterTypeDescription
s0floatInitial price (e.g., current market probability)
mufloatAnnualized drift rate
sigmafloatAnnualized volatility
tfloatTime horizon in days
n_stepsintNumber of time steps per path
n_pathsintNumber of Monte Carlo paths to simulate
seedint or NoneOptional 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.
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"
ParameterTypeDescription
pathslist[list[float]]Simulated price paths from generate_gbm_paths()
entry_pricefloatPrice at which you entered the position
transaction_costfloatRound-trip transaction cost (spread + fees)
discount_ratefloatDaily discount rate. Use 0.0 for prediction markets (no time value of money)
basis_degreeintPolynomial degree for the regression basis (2 or 3 recommended)

StopPolicy Type

FieldTypeDescription
thresholdslist[float]Optimal exit threshold at each time step. Exit when price >= threshold[t]
expected_valuefloatExpected value of following the optimal policy from t=0
immediate_valuefloatValue of exiting immediately at current price
actionstr"hold" or "exit" based on current state vs. threshold
confidencefloatConfidence in the recommendation (0.0 to 1.0), based on how far the current price is from the threshold
coefficientslist[list[float]]Regression coefficients at each time step (for advanced inspection)
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.

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.
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)
ParameterTypeDescription
policyStopPolicyLearned stopping policy from longstaff_schwartz()
current_pricefloatCurrent market price
time_fractionfloatFraction of time elapsed (0.0 = start, 1.0 = expiry)
FieldTypeDescription
actionstr"hold" or "exit"
confidencefloatDistance from threshold normalized to [0, 1]
thresholdfloatExit threshold at this time step
edgefloatcurrent_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"].
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

ParameterTypeDefaultDescription
feedstrrequiredFeed name to read current price from
entry_pricefloatrequiredPrice at which the position was entered
transaction_costfloat0.02Round-trip transaction cost
horizon_daysfloat30.0Time horizon in days until market resolution
n_pathsint10000Number of Monte Carlo paths for policy computation
recompute_everyint500Recompute the policy every N cycles (uses updated volatility estimate)

Mathematical Background

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).
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.
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.
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).