Files
poly-maker/tests/test_estimators.py
Daniel Sapkota c7b686116e Getting there
2026-07-05 17:50:04 -04:00

84 lines
2.4 KiB
Python

"""Unit tests for the online estimators (vol, flow, markout/toxicity)."""
from __future__ import annotations
import pytest
from polymaker.domain import Side
from polymaker.strategy.estimators import (
Ewma,
FlowEstimator,
MarkoutTracker,
VolEstimator,
)
def test_ewma_seeds_then_decays():
e = Ewma(halflife_s=10.0)
assert not e.ready
e.update(1.0, ts=0.0)
assert e.value == 1.0
# after exactly one half-life, a 0 observation pulls the mean halfway
e.update(0.0, ts=10.0)
assert e.value == pytest.approx(0.5, abs=1e-9)
def test_ewma_decay_to_ages_value():
e = Ewma(halflife_s=10.0)
e.update(1.0, ts=0.0)
e.decay_to(ts=10.0) # one half-life of silence
assert e.value == pytest.approx(0.5, abs=1e-9)
def test_vol_estimator_rises_with_movement():
v = VolEstimator(short_halflife_s=5.0, long_halflife_s=100.0)
# quiet: tiny moves
fv, t = 0.5, 0.0
for _ in range(20):
t += 1.0
v.update(fv, t) # no change -> zero vol
assert v.short == pytest.approx(0.0, abs=1e-6)
# sudden jumps -> short vol jumps, ratio > 1
for step in (0.05, -0.04, 0.06):
t += 1.0
fv += step
v.update(fv, t)
assert v.short > 0.01
assert v.ratio > 1.0
def test_flow_estimator_sign_and_z():
f = FlowEstimator(halflife_s=10.0)
t = 0.0
for _ in range(5):
t += 1.0
f.update(Side.BUY, 100, t) # persistent buying
assert f.signed > 0
assert f.z > 0.5 # strongly one-sided
# now heavy selling flips the sign over time
for _ in range(10):
t += 1.0
f.update(Side.SELL, 200, t)
assert f.signed < 0
assert f.z < 0
def test_markout_toxicity_from_adverse_fills():
mt = MarkoutTracker(horizon_s=30.0, ewma_halflife_s=100.0)
# we BUY at fv=0.50; price then falls to 0.45 after the horizon -> adverse
mt.record_fill(Side.BUY, fv_at_fill=0.50, ts=0.0)
mt.evaluate(fv_now=0.50, ts=10.0) # before horizon: nothing resolves
assert mt.markout == 0.0
mt.evaluate(fv_now=0.45, ts=31.0) # after horizon: -0.05 markout
assert mt.markout < 0
assert mt.toxicity > 0
def test_markout_benign_fills_are_not_toxic():
mt = MarkoutTracker(horizon_s=30.0, ewma_halflife_s=100.0)
# we BUY at 0.50; price rises to 0.55 -> favorable, not toxic
mt.record_fill(Side.BUY, fv_at_fill=0.50, ts=0.0)
mt.evaluate(fv_now=0.55, ts=31.0)
assert mt.markout > 0
assert mt.toxicity == 0.0