84 lines
2.4 KiB
Python
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
|