Files
PuGa/tests/test_history.py
dodoxandClaude Sonnet 5 c4bcc8f42c sell.py: optimal-ask model via empirical first-passage, not random walk
A driftless-random-walk closed form was tried first and backtested wrong
against 3 years of real exchange history (overstated touch probability
2-4x, confounded by discrete daily-VWAP sampling and real mean-reversion).
Replaced with puga.history.empirical_touch(): a non-parametric estimator
measuring first-passage directly from a ticker's own history, conditioned
on current state (trailing-60d z-score) since mean reversion's effect
depends on where price is now, not just move size. Queue-depth (existing
resting quantity) folds in as a deterministic shift of the hitting time,
not a max, ranking by the resulting hit_factor rather than a point
estimate of median days (which created a false cliff at the 50%-touched
threshold). Optional fixed-horizon right-censoring avoids recency bias
in "never reached" samples.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-10 23:06:34 +02:00

73 lines
4.0 KiB
Python

import datetime, math
from puga.history import empirical_touch, _trailing_zscores
def _series(jump_idx, n=20, jump_price=220.0, base_price=100.0):
base = datetime.date(2026, 1, 1)
return [(base + datetime.timedelta(days=i), jump_price if i >= jump_idx else base_price) for i in range(n)]
def test_hit_factor_has_no_cliff_at_the_50pct_touched_threshold():
"""median_days flips from a number to None the instant touched_frac crosses 50% - that's
fine as a reported diagnostic, but ranking by it (as sell.py's --tiers used to) creates a
fake cliff: a tier touched 47% of the time would be valued at exactly 0, one touched 53% of
the time at near-full value, even though hit_factor (the real E[e^-rho*tau]) moves smoothly
between them. This is what tools/sell.py's --tiers ranks by instead."""
y = math.log(2.0)
below = empirical_touch(_series(jump_idx=9), y, rate=0.05, condition_on_state=False)
above = empirical_touch(_series(jump_idx=10), y, rate=0.05, condition_on_state=False)
assert below["median_days"] is None and above["median_days"] is not None
assert below["touched_frac"] < 0.5 <= above["touched_frac"]
# hit_factor must not have fallen off a cliff just because median_days did
assert below["hit_factor"] > 0.5 * above["hit_factor"]
def test_empirical_touch_trivial_for_zero_move():
r = empirical_touch(_series(jump_idx=5), 0.0, rate=0.05)
assert r == {"hit_factor": 1.0, "touched_frac": 1.0, "median_days": 0.0, "n": 20}
def test_conditioning_on_state_uses_only_similar_regime_days():
"""Mean reversion means the answer should depend on whether today looks like the ticker's
readily-rising early regime or its since-flattened plateau - not blend both in unconditionally.
Series: 20 flat days, then a steady climb, then 10 more flat days at the new high (today is
in the plateau, not the climb)."""
base = datetime.date(2026, 1, 1)
prices = [100] * 20 + [100 + i * 5 for i in range(1, 11)] + [150] * 10
series = [(base + datetime.timedelta(days=i), float(p)) for i, p in enumerate(prices)]
y = math.log(1.1)
uncond = empirical_touch(series, y, rate=0.05, condition_on_state=False)
cond = empirical_touch(series, y, rate=0.05, condition_on_state=True)
assert uncond["touched_frac"] > 0.5 # blends in the climb regime, looks like it moves readily
assert cond["touched_frac"] < uncond["touched_frac"] # today's plateau regime moves less
def test_trailing_zscores_flat_window_is_zero_not_none():
logp = [math.log(100.0)] * 15
z = _trailing_zscores(logp, trail=60, min_window=10)
assert z[:9] == [None] * 9 # not enough window yet
assert z[9:] == [0.0] * 6 # flat price is exactly at its own mean - a real, defined state
def test_horizon_removes_recency_bias_from_right_censoring():
"""Without a fixed horizon, start days near the end of history get an unfairly short (or
long) remaining window to register a touch, purely from running out of data. A flat series
with a late jump looks like it "touches" readily only because the jump happens to sit right
next to the end-of-history start days; a fixed horizon gives every sample the same window."""
base = datetime.date(2026, 1, 1)
prices = [100.0] * 30 + [220.0] * 5
series = [(base + datetime.timedelta(days=i), p) for i, p in enumerate(prices)]
y = math.log(2.0)
uncapped = empirical_touch(series, y, rate=0.05, condition_on_state=False)
capped = empirical_touch(series, y, rate=0.05, condition_on_state=False, horizon=5)
assert capped["touched_frac"] < uncapped["touched_frac"]
assert capped["n"] < uncapped["n"] # the last `horizon` days of start days are dropped
def test_empirical_touch_never_reached_is_zero_not_excluded():
# a move far beyond anything the series ever does should look like "basically never"
r = empirical_touch(_series(jump_idx=19, jump_price=101.0), math.log(50.0), rate=0.05)
assert r["touched_frac"] == 0.0
assert r["hit_factor"] == 0.0
assert r["median_days"] is None