#!/usr/bin/env python3
"""OFI filter (#A HALO): compute, mild widen, strong cancel, gate, logging fields."""
import json
import sys
import tempfile
import time
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from src.strategy_policy import (
    OFI_GATE_N,
    OFI_HALF_ADD_BPS,
    OFI_MILD,
    OFI_STRONG,
    MarkoutTracker,
    compute_ofi,
    is_ofi_adverse,
    ofi_policy,
)
from src.market_data import LocalBook, BookTop
from src.config import QuotingCfg
from src.quoter import build_ladder
from src.live_engine import PLACE_GAP_S, MAX_STALE_KEEP_BPS, POST_PLACE_GRACE_S

# frozen constraints still hold
assert PLACE_GAP_S == 2.0
assert MAX_STALE_KEEP_BPS == 22.0
assert POST_PLACE_GRACE_S == 30.0

# --- compute_ofi L1-L3 ---
assert abs(compute_ofi([10, 10, 10], [10, 10, 10]) - 0.0) < 1e-9
# ask-heavy
o = compute_ofi([1, 1, 1], [10, 10, 10])
assert o is not None and o < -0.25, o
# bid-heavy
o = compute_ofi([10, 10, 10], [1, 1, 1])
assert o is not None and o > 0.25, o
# L1-only fallback when depth < 2
assert abs(compute_ofi([8], [2]) - (6 / 10)) < 1e-9
assert compute_ofi([], [1]) is None
print("OK compute_ofi")

# --- LocalBook.ofi ---
bk = LocalBook()
bk.bids = {"100.0": 5.0, "99.9": 4.0, "99.8": 3.0}
bk.asks = {"100.1": 1.0, "100.2": 1.0, "100.3": 1.0}
ofi_b = bk.ofi(3)
assert ofi_b is not None and ofi_b > 0.25, ofi_b
bk2 = LocalBook()
bk2.bids = {"100.0": 1.0}
bk2.asks = {"100.1": 9.0}
assert bk2.ofi(3) is not None and bk2.ofi(3) < -0.25
print("OK LocalBook.ofi")

# --- adverse helper ---
assert is_ofi_adverse("buy", -0.3)
assert not is_ofi_adverse("buy", 0.3)
assert is_ofi_adverse("sell", 0.3)
assert not is_ofi_adverse("sell", -0.3)
assert not is_ofi_adverse("buy", None)
print("OK is_ofi_adverse")

# --- policy log-only when gate closed ---
p_off = ofi_policy(-0.5, enabled=False)
assert p_off.allow_bid and p_off.allow_ask
assert p_off.half_bid_add_bps == 0.0
assert p_off.reason == "ofi_log_only"

# --- mild: half +0.4, keep both sides ---
p_m = ofi_policy(-0.30, enabled=True)
assert p_m.allow_bid and p_m.allow_ask
assert abs(p_m.half_bid_add_bps - OFI_HALF_ADD_BPS) < 1e-9
assert p_m.half_ask_add_bps == 0.0
p_m2 = ofi_policy(+0.30, enabled=True)
assert abs(p_m2.half_ask_add_bps - OFI_HALF_ADD_BPS) < 1e-9
assert p_m2.allow_ask and p_m2.allow_bid

# --- strong: cancel accumulating only ---
p_s = ofi_policy(-0.45, enabled=True)
assert p_s.allow_bid is False and p_s.allow_ask is True
p_s2 = ofi_policy(+0.45, enabled=True)
assert p_s2.allow_ask is False and p_s2.allow_bid is True
# neutral
p_n = ofi_policy(0.1, enabled=True)
assert p_n.allow_bid and p_n.allow_ask and p_n.half_bid_add_bps == 0.0
print("OK ofi_policy mild/strong/log_only")

# --- ladder: mild add widens bid side; strong drops bids ---
cfg = QuotingCfg(
    lot_usd_min=400.0, lot_usd_max=400.0, half_spread_bps=1.3,
    n_bid_levels=8, n_ask_levels=8, level_spacing_bps=0.35,
    inventory_skew_bps_per_btc=2200.0, max_inventory_btc=0.25,
)
top = BookTop(bid=80000.0, ask=80000.1, mid=80000.05, ts=0.0)
lad_m = build_ladder(
    top, 0.0, cfg, 1, 5, 0.00001, 1.0,
    half_spread_bps_override=1.3, join_extra_ticks=0, depth_sizing=True,
    half_bid_add_bps=0.4,
)
assert lad_m is not None and lad_m.n_bids() == 8 and lad_m.n_asks() == 8
lad_s = build_ladder(
    top, 0.0, cfg, 1, 5, 0.00001, 1.0,
    allow_bid=False, allow_ask=True,
    half_spread_bps_override=1.3, join_extra_ticks=0, depth_sizing=True,
)
assert lad_s is not None and lad_s.n_bids() == 0 and lad_s.n_asks() == 8
print("OK ladder half_add + strong one-sided")

# --- MarkoutTracker: ofi_at_fill logged + gate ---
with tempfile.TemporaryDirectory() as td:
    path = Path(td) / "markout.jsonl"
    mk = MarkoutTracker(path=path, half_bps_ref=1.2, window=20, grace_s=30.0)
    assert mk.ofi_actions_enabled() is False
    t0 = time.time() - 20.0
    # 30 adverse buy fills (ofi=-0.4) with mid drop → bad markout
    for i in range(OFI_GATE_N):
        mk.on_fill("buy", 80000.0, t0 + i * 0.01, ofi=-0.40)
    # resolve at lower mid (~ -2.5 bp for buys = adverse)
    mk.on_mid(80000.0 * (1 - 2.5 / 10_000), time.time())
    assert len(mk.adverse_ofi_m15) == OFI_GATE_N, len(mk.adverse_ofi_m15)
    assert mk.last_median_adverse_ofi_15 < -1.0 * 1.2
    assert mk.ofi_actions_enabled() is True
    # log rows include ofi_at_fill + markout_15s
    lines = path.read_text().strip().splitlines()
    assert len(lines) >= OFI_GATE_N
    row = json.loads(lines[-1])
    assert "ofi_at_fill" in row and row["ofi_at_fill"] == -0.40
    assert "markout_15s" in row and row["markout_15s"] is not None
    assert row.get("ofi_adverse") is True
    snap = mk.snapshot()
    assert snap["ofi_gate_open"] is True
    assert snap["n_adverse_ofi"] == OFI_GATE_N
print("OK MarkoutTracker ofi_at_fill + gate")

# gate stays closed with good markouts even if ofi adverse
mk2 = MarkoutTracker(path=None, half_bps_ref=1.2)
t0 = time.time() - 20.0
for i in range(OFI_GATE_N):
    mk2.on_fill("buy", 80000.0, t0 + i * 0.01, ofi=-0.40)
# mid UP = good for buy maker
mk2.on_mid(80000.0 * (1 + 2.0 / 10_000), time.time())
assert mk2.ofi_actions_enabled() is False
print("OK gate stays closed when adverse-OFI markout not bad")

assert OFI_MILD == 0.25 and OFI_STRONG == 0.40
print("ALL test_ofi_filter OK")
