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

188 lines
7.3 KiB
Python

"""Unit tests for pure quote construction — the strategy's decision core."""
from __future__ import annotations
import pytest
from polymaker.domain import Position, Regime, Side
from polymaker.strategy.quoting import (
QuoteInputs,
compute_fair_value,
construct_quotes,
round_to_tick,
)
from tests.conftest import view
def _inputs(meta, profile, **over):
base = dict(
meta=meta,
regime=Regime.QUIET,
fv=0.50,
vol_short=0.0,
toxicity=0.0,
yes_view=view(0.49, 0.51),
no_view=view(0.49, 0.51),
pos_yes=Position("yes-token"),
pos_no=Position("no-token"),
profile=profile,
now=1000.0,
)
base.update(over)
return QuoteInputs(**base)
# ── round_to_tick ──────────────────────────────────────────────────────────
def test_round_to_tick_down_and_up():
assert round_to_tick(0.5049, 0.01, 2, up=False) == 0.50
assert round_to_tick(0.5051, 0.01, 2, up=True) == 0.51
# clamps inside (0,1)
assert round_to_tick(0.0, 0.01, 2, up=False) == 0.01
assert round_to_tick(1.0, 0.01, 2, up=True) == 0.99
def test_compute_fair_value_flow_nudge():
# positive flow nudges FV up, negative down, no flow = microprice
assert compute_fair_value(0.50, 0.0, 0.01) == pytest.approx(0.50)
assert compute_fair_value(0.50, 1.0, 0.01, weight=0.5) == pytest.approx(0.505)
assert compute_fair_value(0.50, -1.0, 0.01, weight=0.5) == pytest.approx(0.495)
# ── two-sided quoting ────────────────────────────────────────────────────────
def test_quiet_market_quotes_both_sides_as_bids(meta, profile):
tq = construct_quotes(_inputs(meta, profile))
assert tq.regime == Regime.QUIET
yes = [q for q in tq.quotes if q.token_id == "yes-token"]
no = [q for q in tq.quotes if q.token_id == "no-token"]
assert yes and no
# both entry quotes are BUYs (USDC-collateralized two-sided quote)
assert all(q.side == Side.BUY for q in yes)
assert all(q.side == Side.BUY for q in no)
def test_pair_prices_sum_below_one(meta, profile):
"""BUY YES @ p and BUY NO @ q must satisfy p + q < 1 (merge edge)."""
tq = construct_quotes(_inputs(meta, profile))
top_yes = max(q.price for q in tq.quotes if q.token_id == "yes-token")
top_no = max(q.price for q in tq.quotes if q.token_id == "no-token")
assert top_yes + top_no < 1.0
def test_never_bids_through_fair_value(meta, profile):
"""No BUY should ever sit at or above FV - min_edge (YES) / (1-FV)-min_edge (NO)."""
tq = construct_quotes(_inputs(meta, profile, fv=0.50))
edge = profile.min_edge_ticks * meta.tick_size
for q in tq.quotes:
if q.side == Side.BUY and q.token_id == "yes-token":
assert q.price <= 0.50 - edge + 1e-9
if q.side == Side.BUY and q.token_id == "no-token":
assert q.price <= 0.50 - edge + 1e-9 # NO fv is also 0.50 here
def test_layers_split_size(meta, profile):
tq = construct_quotes(_inputs(meta, profile))
yes = sorted((q for q in tq.quotes if q.token_id == "yes-token" and q.side == Side.BUY),
key=lambda q: -q.price)
assert len(yes) == profile.layers
# deeper layer is at a lower price
assert yes[0].price > yes[1].price
# ── inventory skew ──────────────────────────────────────────────────────────
def test_long_yes_inventory_skews_quotes_down(meta, profile):
"""Holding YES should lower the YES bid and raise the NO bid vs flat."""
flat = construct_quotes(_inputs(meta, profile, vol_short=0.02))
longy = construct_quotes(
_inputs(meta, profile, vol_short=0.02, pos_yes=Position("yes-token", 300, 0.5))
)
def top(tq, tok):
ps = [q.price for q in tq.quotes if q.token_id == tok and q.side == Side.BUY]
return max(ps) if ps else None
# YES bid should not be higher when long YES; NO bid should not be lower
assert top(longy, "yes-token") <= top(flat, "yes-token")
assert top(longy, "no-token") >= top(flat, "no-token")
def test_reduce_only_emits_only_exits(meta, profile):
tq = construct_quotes(
_inputs(
meta, profile, regime=Regime.REDUCE_ONLY,
pos_yes=Position("yes-token", 100, 0.5),
)
)
assert all(q.side == Side.SELL for q in tq.quotes)
assert any(q.token_id == "yes-token" for q in tq.quotes)
def test_event_and_halted_pull_all_quotes(meta, profile):
for regime in (Regime.EVENT, Regime.HALTED):
tq = construct_quotes(
_inputs(meta, profile, regime=regime, pos_yes=Position("yes-token", 100, 0.5))
)
assert tq.is_empty
# ── exits ────────────────────────────────────────────────────────────────────
def test_exit_sell_priced_above_fv_when_not_urgent(meta, profile):
tq = construct_quotes(
_inputs(meta, profile, pos_yes=Position("yes-token", 100, 0.4), yes_exit_urgency=0.0)
)
sells = [q for q in tq.quotes if q.side == Side.SELL and q.token_id == "yes-token"]
assert sells
assert sells[0].price >= 0.50 # at/above FV, a passive maker exit
def test_exit_never_below_best_bid(meta, profile):
tq = construct_quotes(
_inputs(
meta, profile,
pos_yes=Position("yes-token", 100, 0.4),
yes_view=view(0.49, 0.51),
yes_exit_urgency=1.0, # maximally urgent
)
)
sells = [q for q in tq.quotes if q.side == Side.SELL and q.token_id == "yes-token"]
assert sells
assert sells[0].price >= 0.49 # still a maker order, never crosses down
def test_no_exit_when_position_is_dust(meta, profile):
tq = construct_quotes(
_inputs(meta, profile, pos_yes=Position("yes-token", 1.0, 0.4)) # below min_order_size
)
assert not [q for q in tq.quotes if q.side == Side.SELL]
# ── spread widening ──────────────────────────────────────────────────────────
def test_toxicity_widens_spread(meta, profile):
"""Higher toxicity should push the YES bid lower (wider spread)."""
calm = construct_quotes(_inputs(meta, profile, regime=Regime.TRENDING, toxicity=0.0))
toxic = construct_quotes(_inputs(meta, profile, regime=Regime.TRENDING, toxicity=0.02))
def top_yes(tq):
ps = [q.price for q in tq.quotes if q.token_id == "yes-token" and q.side == Side.BUY]
return max(ps) if ps else None
assert top_yes(toxic) < top_yes(calm)
def test_quiet_regime_clamps_spread_to_reward_band(meta, profile):
"""In QUIET, even with high vol the bid stays within the reward band of FV."""
tq = construct_quotes(_inputs(meta, profile, regime=Regime.QUIET, vol_short=0.5))
band = meta.rewards_max_spread / 100.0 # 0.03
top_yes = max(q.price for q in tq.quotes if q.token_id == "yes-token" and q.side == Side.BUY)
# bid should be within (band + a tick of rounding) of FV
assert top_yes >= 0.50 - band - meta.tick_size