Files
DOLPHIN/prod/tests/test_signal_to_fill.py

1452 lines
56 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
test_signal_to_fill.py
======================
Prod-path signal-to-fill latency harness for DolphinLiveTrader.
Uses the EXACT production codepath: DolphinLiveTrader on_scan()
NDAlphaEngine (D_LIQ_GOLD config) ACBv6 OBF (HZOBProvider live).
No engine mocking. External I/O (HZ state-push, trade log file) is
stubbed to avoid prod side-effects.
Latency segments measured (wall-clock via time.perf_counter):
pre_engine_ms parse + price-dict + vol_ok + OBF.step_live (pre-lock)
lock_wait_ms time blocked acquiring eng_lock
engine_ms step_bar() compute inside lock
post_engine_ms result handling + state bookkeeping
e2e_ms full on_scan() wall time (signal available step_bar done)
Performance budgets (P99 assertions):
engine_ms P99 < 5 ms (numba-compiled kernels; typically < 0.5 ms)
pre_engine P99 < 20 ms (OBF step_live is the bottleneck: HZ loopback)
e2e_ms P99 < 30 ms (headroom: bar cadence is 5 000 ms)
Test classes:
TestSignalFiringCorrectness fires iff vel_div < 0.02 + all gates
TestSignalLatency 500-scan timing distribution + budget asserts
TestBarIdxIntegrity never skips / doubles; resets on day rollover
TestDeduplication file_mtime guard prevents double-processing
TestConcurrentScans lock contention under burst; no corruption
TestOBFLiveIntegration OBF wired once; step_live called per scan
TestLiveHZInjection push real HZ event; measure listener latency
"""
import json
import sys
import threading
import time
import unittest
from collections import deque
from datetime import datetime, timezone
from typing import List
from unittest.mock import MagicMock, patch
import numpy as np
sys.path.insert(0, '/mnt/dolphinng5_predict')
sys.path.insert(0, '/mnt/dolphinng5_predict/prod')
sys.path.insert(0, '/mnt/dolphinng5_predict/nautilus_dolphin')
import math
from nautilus_event_trader import (
DolphinLiveTrader,
ENGINE_KWARGS,
VOL_P60_THRESHOLD,
BTC_VOL_WINDOW,
_STABLECOIN_SYMBOLS,
)
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
ASSETS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT", "XRPUSDT"]
BASE_PRICES = [84_230.5, 2_143.2, 612.4, 145.8, 2.41]
VEL_DIV_THRESHOLD = ENGINE_KWARGS['vel_div_threshold'] # -0.02
LOOKBACK = ENGINE_KWARGS['lookback'] # 100
# P99 budget targets (milliseconds)
BUDGET_ENGINE_P99 = 5.0
BUDGET_PRE_ENGINE_P99 = 20.0
BUDGET_E2E_P99 = 30.0
BUDGET_HZ_LISTENER_P95 = 500.0 # measured P95 ~301ms; HZ Python client async dispatch overhead
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_scan(scan_number: int, vel_div: float,
file_mtime: float | None = None,
assets: list = ASSETS,
prices: list = BASE_PRICES,
v50: float = -0.025,
v750: float = -0.005) -> dict:
"""Build a scan dict matching the NG5/7 schema consumed by on_scan()."""
ts = time.time()
return {
"scan_number": scan_number,
"timestamp_ns": int(ts * 1e9),
"timestamp_iso": datetime.now(timezone.utc).isoformat(),
"schema_version": "5.0.0",
"vel_div": vel_div,
"w50_velocity": v50,
"w750_velocity": v750,
"instability_50": max(0.0, v50 - v750),
"assets": list(assets),
"asset_prices": list(prices),
"asset_loadings": [1.0 / len(assets)] * len(assets),
"file_mtime": file_mtime if file_mtime is not None else ts,
"bridge_ts": datetime.now(timezone.utc).isoformat(),
"data_quality_score": 1.0,
}
def _make_event(scan: dict) -> MagicMock:
ev = MagicMock()
ev.value = json.dumps(scan)
return ev
def _volatile_btc_prices(n: int = BTC_VOL_WINDOW + 5,
base: float = 84_230.0,
sigma: float = 300.0) -> list:
"""Return BTC price series with std high enough to pass vol gate."""
rng = np.random.default_rng(42)
prices = [base]
for _ in range(n - 1):
prices.append(prices[-1] + rng.normal(0, sigma))
return prices
def _flat_btc_prices(n: int = BTC_VOL_WINDOW + 5,
base: float = 84_230.0) -> list:
"""Return constant BTC price series → vol_ok = False."""
return [base] * n
def _build_trader(suppress_pushes: bool = True,
connect_hz: bool = False) -> DolphinLiveTrader:
"""Instantiate and build engine; stub HZ write calls.
on_scan() is made synchronous for deterministic testing: the
ThreadPoolExecutor dispatch is bypassed so assertions can be made
immediately after the call without sleeping or draining the queue.
Tests that specifically exercise threading call _process_scan directly.
"""
trader = DolphinLiveTrader()
trader._build_engine()
# Keep posture cache hot → no live HZ read for posture
trader.cached_posture = "APEX"
trader.posture_cache_time = time.time() + 3600
if suppress_pushes:
trader._push_state = MagicMock()
trader._save_capital = MagicMock() # also suppress HZ capital checkpoint writes
# Synchronous on_scan for deterministic test assertions
_orig_process = trader._process_scan
trader.on_scan = lambda ev: _orig_process(ev, time.time())
if connect_hz:
trader._connect_hz()
return trader
def _warmup(trader: DolphinLiveTrader,
n_bars: int = LOOKBACK + 5,
vol: str = "volatile") -> int:
"""
Feed n_bars warm-up scans so engine reaches _bar_count >= LOOKBACK.
Returns next available scan_number.
"""
prices = _volatile_btc_prices() if vol == "volatile" else _flat_btc_prices()
trader.btc_prices = deque(prices, maxlen=BTC_VOL_WINDOW + 2)
# Ensure begin_day has been called
today = datetime.now(timezone.utc).strftime('%Y-%m-%d')
with trader.eng_lock:
trader.eng.begin_day(today, posture="APEX")
trader.bar_idx = 0
trader.current_day = today
# Stub OBF so warmup is fast (wired later in live tests)
trader.ob_assets = list(ASSETS) # prevents _wire_obf from firing again
trader.ob_eng = MagicMock()
trader.ob_eng.step_live = MagicMock()
trader.eng.set_ob_engine(MagicMock())
mtime_base = time.time() - n_bars * 5
for i in range(n_bars):
scan = _make_scan(
scan_number=i + 1,
vel_div=-0.01, # below threshold → no entry
file_mtime=mtime_base + i * 5,
)
ev = _make_event(scan)
trader.on_scan(ev)
return n_bars + 1
# ---------------------------------------------------------------------------
# Timing wrapper — installed without touching prod code
# ---------------------------------------------------------------------------
class _TimingInterceptor:
"""
Wraps eng.step_bar to record per-call timing.
Install before test run, uninstall after.
"""
def __init__(self, eng):
self._eng = eng
self._orig = eng.step_bar
self.samples: List[float] = [] # milliseconds per step_bar call
def install(self):
orig = self._orig
samples = self.samples
def _timed(*args, **kwargs):
t0 = time.perf_counter()
result = orig(*args, **kwargs)
samples.append((time.perf_counter() - t0) * 1_000)
return result
self._eng.step_bar = _timed
def uninstall(self):
self._eng.step_bar = self._orig
def percentile(self, pct: float) -> float:
return float(np.percentile(self.samples, pct)) if self.samples else 0.0
# ---------------------------------------------------------------------------
# 1. Signal Firing Correctness
# ---------------------------------------------------------------------------
class TestSignalFiringCorrectness(unittest.TestCase):
"""
Verify signal fires iff vel_div < VEL_DIV_THRESHOLD (0.02)
and all upstream gates pass.
"""
@classmethod
def setUpClass(cls):
cls.trader = _build_trader()
cls.next_scan = _warmup(cls.trader)
def _fire(self, vel_div: float, vol: str = "volatile") -> dict:
"""Fire one scan and return step_bar result."""
if vol == "volatile":
prices = list(_volatile_btc_prices())
self.trader.btc_prices = deque(prices, maxlen=BTC_VOL_WINDOW + 2)
scan = _make_scan(
scan_number=self.next_scan,
vel_div=vel_div,
file_mtime=time.time(),
)
self.__class__.next_scan += 1
results = []
orig = self.trader.eng.step_bar
def capture(*a, **kw):
r = orig(*a, **kw)
results.append(r)
return r
self.trader.eng.step_bar = capture
try:
self.trader.on_scan(_make_event(scan))
finally:
self.trader.eng.step_bar = orig
return results[0] if results else {}
def test_strong_signal_fires(self):
"""vel_div = 0.05 (extreme zone) → entry generated."""
# Close any open position first
with self.trader.eng_lock:
self.trader.eng.position = None
result = self._fire(-0.05)
# Engine may or may not produce entry depending on ACB/OB state;
# what we assert is that step_bar ran and returned a valid dict.
self.assertIn('entry', result)
self.assertIn('exit', result)
def test_just_past_threshold_fires(self):
"""vel_div = 0.021 (1 bps past threshold) → entry attempted."""
with self.trader.eng_lock:
self.trader.eng.position = None
result = self._fire(-0.021)
self.assertIn('entry', result)
def test_at_threshold_no_fire(self):
"""vel_div = 0.020 (exactly at threshold) → no entry (strict <)."""
with self.trader.eng_lock:
self.trader.eng.position = None
result = self._fire(-0.020)
self.assertIsNone(result.get('entry'),
"vel_div == threshold must NOT produce an entry (strict < check)")
def test_above_threshold_no_fire(self):
"""vel_div = 0.01 → no entry."""
with self.trader.eng_lock:
self.trader.eng.position = None
result = self._fire(-0.01)
self.assertIsNone(result.get('entry'))
def test_positive_vel_div_no_fire(self):
"""vel_div = +0.05 → no entry."""
with self.trader.eng_lock:
self.trader.eng.position = None
result = self._fire(+0.05)
self.assertIsNone(result.get('entry'))
def test_no_entry_when_position_open(self):
"""If position is open, no new entry should fire."""
fake_pos = MagicMock() # duck-type: any truthy non-None value
fake_pos.asset = "BTCUSDT"
with self.trader.eng_lock:
self.trader.eng.position = fake_pos
result = self._fire(-0.05)
self.assertIsNone(result.get('entry'),
"Engine must not open a second position while one is open")
with self.trader.eng_lock:
self.trader.eng.position = None # restore
def test_vol_gate_blocks_entry(self):
"""Flat BTC prices → vol_ok=False → no entry even with strong signal."""
flat = _flat_btc_prices()
self.trader.btc_prices = deque(flat, maxlen=BTC_VOL_WINDOW + 2)
with self.trader.eng_lock:
self.trader.eng.position = None
result = self._fire(-0.05, vol="flat")
self.assertIsNone(result.get('entry'),
"vol_ok=False must suppress entry")
def test_duplicate_mtime_skipped(self):
"""Same file_mtime → on_scan returns early; step_bar NOT called."""
mtime = time.time()
self.trader.last_file_mtime = mtime
calls = []
orig = self.trader.eng.step_bar
self.trader.eng.step_bar = lambda *a, **kw: calls.append(1) or orig(*a, **kw)
try:
scan = _make_scan(self.next_scan, -0.05, file_mtime=mtime)
self.trader.on_scan(_make_event(scan))
finally:
self.trader.eng.step_bar = orig
self.assertEqual(len(calls), 0,
"Duplicate mtime must be dropped before reaching step_bar")
# ---------------------------------------------------------------------------
# 2. Signal-to-Fill Latency
# ---------------------------------------------------------------------------
class TestSignalLatency(unittest.TestCase):
"""
500-scan run over the EXACT prod path (DolphinLiveTrader.on_scan).
Measures pre_engine, engine, post_engine, e2e latency distributions.
Asserts P99 budget targets.
"""
N_SCANS = 500
WARMUP_N = LOOKBACK + 5
@classmethod
def setUpClass(cls):
cls.trader = _build_trader()
cls.next_scan = _warmup(cls.trader)
cls.pre_engine_ms: List[float] = []
cls.engine_ms: List[float] = []
cls.e2e_ms: List[float] = []
interceptor = _TimingInterceptor(cls.trader.eng)
interceptor.install()
cls._interceptor = interceptor
# Volatile BTC for vol_ok=True throughout
prices = _volatile_btc_prices()
cls.trader.btc_prices = deque(prices, maxlen=BTC_VOL_WINDOW + 2)
mtime_base = time.time()
for i in range(cls.N_SCANS):
vel_div = -0.04 if (i % 20 == 0) else -0.01
scan = _make_scan(
scan_number=cls.next_scan + i,
vel_div=vel_div,
file_mtime=mtime_base + i * 0.001, # 1 ms apart → all distinct
)
ev = _make_event(scan)
# Time full on_scan
t0 = time.perf_counter()
cls.trader.on_scan(ev)
t1 = time.perf_counter()
cls.e2e_ms.append((t1 - t0) * 1_000)
interceptor.uninstall()
cls.next_scan += cls.N_SCANS
def test_engine_p99_within_budget(self):
p99 = self._interceptor.percentile(99)
self.assertLess(p99, BUDGET_ENGINE_P99,
f"engine step_bar P99={p99:.2f}ms exceeds budget {BUDGET_ENGINE_P99}ms")
def test_e2e_p99_within_budget(self):
p99 = float(np.percentile(self.e2e_ms, 99))
self.assertLess(p99, BUDGET_E2E_P99,
f"on_scan E2E P99={p99:.2f}ms exceeds budget {BUDGET_E2E_P99}ms")
def test_engine_p50_sub_millisecond(self):
p50 = self._interceptor.percentile(50)
self.assertLess(p50, 1.0,
f"engine step_bar P50={p50:.3f}ms should be sub-millisecond")
def test_no_on_scan_exceptions(self):
"""All 500 scans processed without exceptions (scans_processed incremented)."""
# Each valid (non-dup) scan increments scans_processed
self.assertGreaterEqual(self.trader.scans_processed, self.N_SCANS)
def test_engine_samples_count(self):
"""step_bar called exactly once per non-dup scan."""
self.assertEqual(len(self._interceptor.samples), self.N_SCANS)
def test_print_latency_report(self):
"""Print latency report (informational, always passes)."""
engine_samples = self._interceptor.samples
e2e = self.e2e_ms
print(f"\n{'='*60}")
print(f" Signal-to-Fill Latency Report ({self.N_SCANS} scans)")
print(f"{'='*60}")
print(f" step_bar(): P50={np.percentile(engine_samples,50):.3f}ms"
f" P95={np.percentile(engine_samples,95):.3f}ms"
f" P99={np.percentile(engine_samples,99):.3f}ms"
f" max={max(engine_samples):.3f}ms")
print(f" on_scan E2E: P50={np.percentile(e2e,50):.2f}ms"
f" P95={np.percentile(e2e,95):.2f}ms"
f" P99={np.percentile(e2e,99):.2f}ms"
f" max={max(e2e):.2f}ms")
print(f" budget: engine<{BUDGET_ENGINE_P99}ms"
f" e2e<{BUDGET_E2E_P99}ms")
print(f"{'='*60}")
self.assertTrue(True)
# ---------------------------------------------------------------------------
# 3. bar_idx Integrity
# ---------------------------------------------------------------------------
class TestBarIdxIntegrity(unittest.TestCase):
def setUp(self):
self.trader = _build_trader()
self.next_scan = _warmup(self.trader)
def test_bar_idx_increments_by_one_per_scan(self):
"""Each valid scan increments bar_idx by exactly 1."""
start = self.trader.bar_idx
n = 10
mtime = time.time()
for i in range(n):
scan = _make_scan(self.next_scan + i, -0.01,
file_mtime=mtime + i * 0.1)
self.trader.on_scan(_make_event(scan))
self.assertEqual(self.trader.bar_idx, start + n,
f"Expected bar_idx={start+n}, got {self.trader.bar_idx}")
def test_duplicate_scan_does_not_increment_bar_idx(self):
"""Duplicate mtime → bar_idx unchanged."""
mtime = time.time()
self.trader.last_file_mtime = mtime
before = self.trader.bar_idx
scan = _make_scan(self.next_scan, -0.05, file_mtime=mtime)
self.trader.on_scan(_make_event(scan))
self.assertEqual(self.trader.bar_idx, before)
def test_bar_idx_resets_on_day_rollover(self):
"""Day rollover resets bar_idx to 0."""
# Force tomorrow's date
import datetime as dt
tomorrow = (dt.date.today() + dt.timedelta(days=1)).isoformat()
self.trader.current_day = tomorrow # lie about current day
# Fire a scan with today's actual date → triggers rollover
self.trader.current_day = "1970-01-01" # force mismatch
scan = _make_scan(self.next_scan, -0.01, file_mtime=time.time() + 1000)
self.trader.on_scan(_make_event(scan))
self.assertEqual(self.trader.bar_idx, 1,
"bar_idx must be 1 (post-rollover reset to 0, then incremented by scan)")
def test_bar_idx_passed_correctly_to_step_bar(self):
"""bar_idx passed to step_bar equals trader.bar_idx before the call."""
received_bar_idx = []
orig = self.trader.eng.step_bar
def capturing(*args, **kwargs):
received_bar_idx.append(kwargs.get('bar_idx', args[0] if args else None))
return orig(*args, **kwargs)
self.trader.eng.step_bar = capturing
before = self.trader.bar_idx
scan = _make_scan(self.next_scan, -0.01, file_mtime=time.time() + 500)
self.trader.on_scan(_make_event(scan))
self.trader.eng.step_bar = orig
self.assertEqual(len(received_bar_idx), 1)
self.assertEqual(received_bar_idx[0], before,
f"step_bar received bar_idx={received_bar_idx[0]}, expected {before}")
# ---------------------------------------------------------------------------
# 4. Deduplication
# ---------------------------------------------------------------------------
class TestDeduplication(unittest.TestCase):
def setUp(self):
self.trader = _build_trader()
_warmup(self.trader)
def _count_step_bar_calls(self, events) -> int:
calls = []
orig = self.trader.eng.step_bar
self.trader.eng.step_bar = lambda *a, **kw: calls.append(1) or orig(*a, **kw)
for ev in events:
self.trader.on_scan(ev)
self.trader.eng.step_bar = orig
return len(calls)
def test_same_mtime_rejected(self):
mtime = time.time()
s1 = _make_scan(999, -0.01, file_mtime=mtime)
s2 = _make_scan(1000, -0.01, file_mtime=mtime) # same mtime
calls = self._count_step_bar_calls([
_make_event(s1), _make_event(s2)
])
self.assertEqual(calls, 1, "Second scan with same mtime must be dropped")
def test_older_mtime_rejected(self):
now = time.time()
s1 = _make_scan(999, -0.01, file_mtime=now)
s2 = _make_scan(1000, -0.01, file_mtime=now - 1.0) # older
calls = self._count_step_bar_calls([
_make_event(s1), _make_event(s2)
])
self.assertEqual(calls, 1, "Older mtime must be dropped")
def test_newer_mtime_accepted(self):
now = time.time()
s1 = _make_scan(999, -0.01, file_mtime=now)
s2 = _make_scan(1000, -0.01, file_mtime=now + 0.001) # 1ms newer
calls = self._count_step_bar_calls([
_make_event(s1), _make_event(s2)
])
self.assertEqual(calls, 2, "Newer mtime must be accepted")
def test_100_sequential_scans_all_accepted(self):
"""100 scans with monotonically increasing mtime — all reach step_bar."""
base = time.time()
events = [_make_event(_make_scan(i, -0.01, file_mtime=base + i * 0.01))
for i in range(100)]
calls = self._count_step_bar_calls(events)
self.assertEqual(calls, 100)
# ---------------------------------------------------------------------------
# 5. Concurrent Scans (lock contention + race conditions)
# ---------------------------------------------------------------------------
class TestConcurrentScans(unittest.TestCase):
def setUp(self):
self.trader = _build_trader()
_warmup(self.trader)
def test_burst_10_threads_bar_idx_exact(self):
"""
10 threads burst simultaneously via Barrier, each with a distinct mtime.
Invariants verified:
1. bar_idx increments by AT LEAST 1 (some scans processed)
2. bar_idx increments by AT MOST N (no double-counting TOCTOU guard)
3. last_file_mtime ends up as the maximum mtime of any accepted scan
(monotone without _dedup_lock this could be overwritten with a
lower value, allowing stale re-processing)
4. No thread errors or crashes
Why "exactly N" is NOT the correct assertion here:
With strict Barrier burst and monotone dedup, threads execute in random
scheduler order. If thread-9 (mtime=base+0.009) acquires _dedup_lock
before thread-0 (mtime=base+0.000), thread-0 is correctly rejected
(its mtime < last). The invariant that matters is no double-counting and
correct monotone state not that all N were accepted (which requires
ascending execution order, unguaranteed with threads).
The TOCTOU bug (pre-fix, no lock): thread-B could overwrite
last_file_mtime with a LOWER value after thread-A set a higher one,
allowing future stale scans to be incorrectly accepted. _dedup_lock
makes check+set atomic, preserving the monotone invariant.
"""
N = 10
base_mtime = time.time() + 1000
scans = [_make_scan(9000 + i, -0.01, file_mtime=base_mtime + i * 0.001)
for i in range(N)]
barrier = threading.Barrier(N)
errors = []
def fire(ev):
try:
barrier.wait()
self.trader.on_scan(ev)
except Exception as e:
errors.append(e)
before = self.trader.bar_idx
mtime_before = self.trader.last_file_mtime
threads = [threading.Thread(target=fire, args=(_make_event(s),))
for s in scans]
for t in threads: t.start()
for t in threads: t.join(timeout=10)
increment = self.trader.bar_idx - before
max_scan_mtime = base_mtime + (N - 1) * 0.001
self.assertEqual(errors, [], f"Thread errors: {errors}")
# 1+2: at least one processed, never double-counted
self.assertGreaterEqual(increment, 1,
"No scan was processed at all — total deadlock?")
self.assertLessEqual(increment, N,
f"bar_idx over-incremented: {increment} > {N}. Double-counting race!")
# 3: last_file_mtime must be monotonically increasing (key invariant)
self.assertGreater(self.trader.last_file_mtime, mtime_before,
"last_file_mtime did not advance — no scan was accepted")
self.assertLessEqual(self.trader.last_file_mtime, max_scan_mtime + 1e-6,
"last_file_mtime beyond max injected mtime — impossible")
def test_concurrent_scans_no_engine_state_corruption(self):
"""
50 rapid sequential scans (no sleep) engine state must remain
self-consistent: capital >= 0, no position with None asset.
"""
base = time.time() + 2000
for i in range(50):
scan = _make_scan(8000 + i, -0.01, file_mtime=base + i * 0.0001)
self.trader.on_scan(_make_event(scan))
with self.trader.eng_lock:
capital = getattr(self.trader.eng, 'capital', 25000.0)
pos = getattr(self.trader.eng, 'position', None)
self.assertGreater(capital, 0, "Capital must remain positive")
if pos is not None:
self.assertIsNotNone(getattr(pos, 'asset', None),
"Open position must have a non-None asset")
def test_lock_always_released_on_exception(self):
"""
Even if step_bar raises, eng_lock must be released so
subsequent scans can proceed.
"""
orig = self.trader.eng.step_bar
explode_once = [True]
def maybe_raise(*a, **kw):
if explode_once[0]:
explode_once[0] = False
raise RuntimeError("injected test failure")
return orig(*a, **kw)
self.trader.eng.step_bar = maybe_raise
base = time.time() + 3000
# Fire 3 scans: first will raise, others must still run
results = []
for i in range(3):
try:
self.trader.on_scan(
_make_event(_make_scan(7000 + i, -0.01,
file_mtime=base + i * 0.1)))
results.append('ok')
except Exception:
results.append('exc')
self.trader.eng.step_bar = orig
# Lock must not be held after the exception
acquired = self.trader.eng_lock.acquire(blocking=False)
if acquired:
self.trader.eng_lock.release()
self.assertTrue(acquired, "eng_lock is stuck — not released after exception in on_scan")
# Scans 2 and 3 must have reached step_bar
self.assertGreaterEqual(results.count('ok'), 2,
"Scans after an exception must still process normally")
def test_nan_vel_div_capital_stays_finite(self):
"""
A scan with vel_div=NaN must not poison capital.
Root cause of 2026-03-30 incident: NaN vel_div NaN notional
capital += NaN all subsequent trades broken.
"""
trader = _build_trader()
trader.eng.begin_day(datetime.now(timezone.utc).strftime('%Y-%m-%d'), posture='APEX')
capital_before = trader.eng.capital
base = time.time() + 9000
# Inject NaN vel_div — must be clamped, not passed to engine
nan_scan = _make_scan(9001, float('nan'), file_mtime=base)
nan_scan['w50_velocity'] = float('nan')
nan_scan['w750_velocity'] = float('nan')
trader._process_scan(_make_event(nan_scan), base)
with trader.eng_lock:
capital_after = trader.eng.capital
self.assertTrue(math.isfinite(capital_after),
f"Capital became non-finite ({capital_after}) after NaN vel_div scan — "
"NaN guard in _process_scan must clamp before step_bar")
def test_stablecoin_never_selected_as_trade_asset(self):
"""
Stablecoin symbols (USDCUSDT et al.) must be stripped from prices_dict
before reaching the engine picker, even when present in scan data.
Eigen algo retains them for correlation purity; the PICKER hard-blocks them.
Root cause of 2026-03-30 incident: USDCUSDT shorted at 9x leverage.
"""
trader = _build_trader()
trader.eng.begin_day(datetime.now(timezone.utc).strftime('%Y-%m-%d'), posture='APEX')
# Warmup so engine can fire entries
trader.btc_prices = deque(_volatile_btc_prices(), maxlen=BTC_VOL_WINDOW + 2)
base = time.time() + 10000
for i in range(LOOKBACK + 5):
trader._process_scan(_make_event(_make_scan(10000 + i, -0.005,
file_mtime=base + i * 0.001)), base)
# Now inject a strong signal scan that includes stablecoins
stable_assets = list(ASSETS) + ['USDCUSDT', 'BUSDUSDT', 'FDUSDUSDT']
stable_prices = list(BASE_PRICES) + [1.0001, 0.9999, 1.0002]
captured_prices_dicts = []
orig_step = trader.eng.step_bar
def capture_step(*a, **kw):
captured_prices_dicts.append(kw.get('prices') or (a[2] if len(a) > 2 else {}))
return orig_step(*a, **kw)
trader.eng.step_bar = capture_step
trigger = _make_scan(10999, -0.08, file_mtime=base + 99999,
assets=stable_assets, prices=stable_prices)
trader._process_scan(_make_event(trigger), base + 99999)
trader.eng.step_bar = orig_step
if captured_prices_dicts:
pd = captured_prices_dicts[-1]
for sym in _STABLECOIN_SYMBOLS:
self.assertNotIn(sym, pd,
f"{sym} reached engine prices_dict — picker must hard-block stablecoins")
def test_capital_checkpoint_save_restore(self):
"""Capital saved to HZ checkpoint must be recoverable on trader restart."""
trader = _build_trader()
trader.eng.begin_day(datetime.now(timezone.utc).strftime('%Y-%m-%d'), posture='APEX')
trader.eng.capital = 31_500.00 # simulate P&L growth
# Mock state_map for save
saved = {}
mock_map = MagicMock()
mock_map.blocking.return_value.put = lambda k, v: saved.update({k: v})
mock_map.blocking.return_value.get = lambda k: saved.get(k)
trader.state_map = mock_map
# Call real _save_capital (instance has it mocked in _build_trader)
DolphinLiveTrader._save_capital(trader)
self.assertIn('capital_checkpoint', saved, "checkpoint key not written")
# New trader restores from checkpoint
trader2 = _build_trader()
trader2.eng.begin_day(datetime.now(timezone.utc).strftime('%Y-%m-%d'), posture='APEX')
trader2.state_map = mock_map
DolphinLiveTrader._restore_capital(trader2)
self.assertAlmostEqual(trader2.eng.capital, 31_500.00, delta=0.01,
msg="Restored capital does not match saved checkpoint")
# ---------------------------------------------------------------------------
# 6. OBF Live Integration
# ---------------------------------------------------------------------------
class TestOBFLiveIntegration(unittest.TestCase):
"""
Verifies OBF (HZOBProvider) wiring and step_live cadence using
the actual live HZ connection.
"""
@classmethod
def setUpClass(cls):
# Build trader, suppress HZ state pushes
cls.trader = _build_trader(suppress_pushes=True, connect_hz=False)
# Warmup with mock OBF so warmup is fast
cls.trader.ob_assets = list(ASSETS)
cls.trader.ob_eng = MagicMock()
cls.trader.ob_eng.step_live = MagicMock()
cls.trader.eng.set_ob_engine(MagicMock())
_warmup(cls.trader)
# Reset OBF state — next scan will trigger real _wire_obf
cls.trader.ob_assets = []
cls.trader.ob_eng = None
cls.trader.eng.set_ob_engine(None)
# Track wiring calls
cls._wired_calls = []
orig_wire = cls.trader._wire_obf
def spy_wire(assets):
cls._wired_calls.append(list(assets))
orig_wire(assets)
cls.trader._wire_obf = spy_wire
# Fire first scan — triggers _wire_obf once
scan = _make_scan(5000, -0.01, file_mtime=time.time() + 5000)
cls.trader.on_scan(_make_event(scan))
# Restore _wire_obf (spy no longer needed)
cls.trader._wire_obf = orig_wire
def test_obf_wired_on_first_scan(self):
"""_wire_obf called exactly once on first scan; BTCUSDT in asset list."""
self.assertEqual(len(self._wired_calls), 1,
"OBF must be wired exactly once")
self.assertIn('BTCUSDT', self._wired_calls[0],
"BTCUSDT must be in wired assets")
def test_obf_not_wired_twice(self):
"""Second scan must NOT re-wire OBF (ob_assets already set)."""
# ob_assets was populated by the first scan in setUpClass
self.assertNotEqual(self.trader.ob_assets, [],
"ob_assets should be set after first scan")
extra_wires = []
orig_wire = self.trader._wire_obf
self.trader._wire_obf = lambda a: extra_wires.append(a)
scan = _make_scan(5001, -0.01, file_mtime=time.time() + 6000)
self.trader.on_scan(_make_event(scan))
self.trader._wire_obf = orig_wire
self.assertEqual(len(extra_wires), 0,
"_wire_obf must not be called again once ob_assets is populated")
def test_step_live_called_per_scan(self):
"""ob_eng.step_live() called on every scan (provides fresh OBF data)."""
if self.trader.ob_eng is None:
self.skipTest("OBF not wired (HZ unavailable?)")
step_live_calls = []
orig_sl = self.trader.ob_eng.step_live
def spy_sl(assets, bar_idx):
step_live_calls.append(bar_idx)
return orig_sl(assets, bar_idx)
self.trader.ob_eng.step_live = spy_sl
base = time.time() + 7000
for i in range(5):
scan = _make_scan(5100 + i, -0.01, file_mtime=base + i * 0.1)
self.trader.on_scan(_make_event(scan))
self.trader.ob_eng.step_live = orig_sl
self.assertEqual(len(step_live_calls), 5,
"step_live must be called once per scan")
for j in range(1, len(step_live_calls)):
self.assertGreater(step_live_calls[j], step_live_calls[j - 1],
"bar_idx passed to step_live must increase each scan")
# ---------------------------------------------------------------------------
# 7. Live HZ Injection (round-trip listener latency)
# ---------------------------------------------------------------------------
class TestLiveHZInjection(unittest.TestCase):
"""
Inject a synthetic scan directly into DOLPHIN_FEATURES['latest_eigen_scan']
via HZ, then measure how long it takes the listener to fire on_scan().
This tests the FULL signal path: HZ publish event queue listener
thread on_scan() step_bar() signal available.
"""
@classmethod
def setUpClass(cls):
try:
import hazelcast
cls.hz = hazelcast.HazelcastClient(
cluster_name="dolphin",
cluster_members=["127.0.0.1:5701"],
)
cls.features_map = cls.hz.get_map("DOLPHIN_FEATURES")
cls.hz_available = True
except Exception:
cls.hz_available = False
@classmethod
def tearDownClass(cls):
if getattr(cls, 'hz_available', False):
try:
cls.hz.shutdown()
except Exception:
pass
def setUp(self):
if not self.hz_available:
self.skipTest("Hazelcast unavailable — skipping live injection tests")
def _make_live_trader_with_listener(self):
"""Build trader, connect HZ, register listener."""
trader = _build_trader(suppress_pushes=True)
trader._connect_hz()
# Warmup engine state
today = datetime.now(timezone.utc).strftime('%Y-%m-%d')
with trader.eng_lock:
trader.eng.begin_day(today, posture="APEX")
trader.bar_idx = 0
trader.current_day = today
# Suppress OBF wiring in listener path (live OBF tested separately)
trader.ob_assets = list(ASSETS)
trader.ob_eng = MagicMock()
trader.ob_eng.step_live = MagicMock()
trader.eng.set_ob_engine(MagicMock())
# Warm vol buffer
prices = _volatile_btc_prices()
trader.btc_prices = deque(prices, maxlen=BTC_VOL_WINDOW + 2)
# Advance past lookback barrier
mtime_base = time.time() - (LOOKBACK + 10) * 5
for i in range(LOOKBACK + 5):
scan = _make_scan(i + 1, -0.01, file_mtime=mtime_base + i * 5)
trader.on_scan(_make_event(scan))
return trader
def test_listener_fires_within_budget(self):
"""
Inject scan to HZ, register listener, verify on_scan fires within
BUDGET_HZ_LISTENER_P95 ms.
"""
trader = self._make_live_trader_with_listener()
fired = threading.Event()
fire_times: List[float] = []
orig_on_scan = trader.on_scan
def instrumented_on_scan(event):
fire_times.append(time.perf_counter())
orig_on_scan(event)
fired.set()
trader.on_scan = instrumented_on_scan
# Register listener on the trader's own HZ client
listener_handle = trader.features_map.add_entry_listener(
key='latest_eigen_scan',
include_value=True,
updated_func=instrumented_on_scan,
added_func=instrumented_on_scan,
)
time.sleep(0.2) # allow listener registration to propagate in HZ
# Push via the same client — guarantees same-cluster observation
push_scan_number = 999_001
scan = _make_scan(push_scan_number, -0.03,
file_mtime=time.time() + 99_000)
t_push = time.perf_counter()
trader.features_map.put('latest_eigen_scan', json.dumps(scan))
# Wait for listener to fire (up to 2 seconds)
fired_ok = fired.wait(timeout=2.0)
# Unregister listener
trader.features_map.remove_entry_listener(listener_handle)
if trader.hz_client:
trader.hz_client.shutdown()
self.assertTrue(fired_ok, "HZ listener did NOT fire within 2s of publish")
if fire_times:
latency_ms = (fire_times[0] - t_push) * 1_000
print(f"\n HZ listener latency: {latency_ms:.2f} ms")
self.assertLess(latency_ms, BUDGET_HZ_LISTENER_P95,
f"HZ publish→listener latency {latency_ms:.2f}ms > "
f"budget {BUDGET_HZ_LISTENER_P95}ms")
def test_10_injections_latency_distribution(self):
"""
Inject 10 scans sequentially, collect listener latencies.
Report distribution; assert P95 within budget.
"""
trader = self._make_live_trader_with_listener()
latencies_ms: List[float] = []
lock = threading.Lock()
push_times: List[float] = []
orig_on_scan = trader.on_scan
call_count = [0]
def instrumented(event):
with lock:
if call_count[0] < len(push_times):
latencies_ms.append(
(time.perf_counter() - push_times[call_count[0]]) * 1_000
)
call_count[0] += 1
orig_on_scan(event)
listener_handle = trader.features_map.add_entry_listener(
key='latest_eigen_scan',
include_value=True,
updated_func=instrumented,
added_func=instrumented,
)
N = 10
for i in range(N):
scan = _make_scan(998_000 + i, -0.02 - i * 0.001,
file_mtime=time.time() + 98_000 + i)
push_times.append(time.perf_counter())
self.features_map.put('latest_eigen_scan', json.dumps(scan))
time.sleep(0.3) # space injections
time.sleep(1.0) # drain last events
trader.features_map.remove_entry_listener(listener_handle)
if trader.hz_client:
trader.hz_client.shutdown()
self.assertGreaterEqual(len(latencies_ms), N // 2,
f"Expected ≥{N//2} listener fires, got {len(latencies_ms)}")
if latencies_ms:
p50 = float(np.percentile(latencies_ms, 50))
p95 = float(np.percentile(latencies_ms, 95))
print(f"\n HZ injection latency over {len(latencies_ms)} samples:")
print(f" P50={p50:.1f}ms P95={p95:.1f}ms max={max(latencies_ms):.1f}ms")
self.assertLess(p95, BUDGET_HZ_LISTENER_P95,
f"P95 HZ listener latency {p95:.1f}ms > budget {BUDGET_HZ_LISTENER_P95}ms")
# ---------------------------------------------------------------------------
# 8. Scan Data Integrity
# ---------------------------------------------------------------------------
class TestScanDataIntegrity(unittest.TestCase):
"""
Guards against silent data corruption at the scanengine boundary.
zip() truncation, null fields, and ordering bugs can trigger wrong trades
without raising exceptions.
"""
def setUp(self):
self.trader = _build_trader()
self.next_scan = _warmup(self.trader)
def _step_bar_calls(self, events):
calls = []
orig = self.trader.eng.step_bar
self.trader.eng.step_bar = lambda *a, **kw: calls.append(1) or orig(*a, **kw)
for ev in events:
self.trader.on_scan(ev)
self.trader.eng.step_bar = orig
return len(calls)
def _captured_prices(self, scan):
received = {}
orig = self.trader.eng.step_bar
def cap(*a, **kw):
received.update(kw.get('prices', {}))
return orig(*a, **kw)
self.trader.eng.step_bar = cap
self.trader.on_scan(_make_event(scan))
self.trader.eng.step_bar = orig
return received
def test_assets_prices_length_mismatch_dropped(self):
"""zip() truncation = silent wrong prices → trade on garbage data. Must be dropped."""
scan = _make_scan(self.next_scan, -0.05, file_mtime=time.time() + 10_000)
scan['asset_prices'] = scan['asset_prices'][:-1] # len mismatch
self.assertEqual(self._step_bar_calls([_make_event(scan)]), 0,
"Length-mismatched scan must be dropped before step_bar")
def test_all_assets_mapped_to_correct_prices(self):
"""asset[i] must map to price[i] — no rotation or shuffle."""
scan = _make_scan(self.next_scan + 1, -0.01, file_mtime=time.time() + 11_000,
assets=ASSETS, prices=BASE_PRICES)
received = self._captured_prices(scan)
for asset, price in zip(ASSETS, BASE_PRICES):
self.assertAlmostEqual(received.get(asset, -1), price, places=2,
msg=f"{asset} price mapping incorrect")
def test_btcusdt_present_in_prices_dict(self):
"""BTCUSDT must reach step_bar — vol gate and engine both depend on it."""
scan = _make_scan(self.next_scan + 2, -0.01, file_mtime=time.time() + 12_000)
received = self._captured_prices(scan)
self.assertIn('BTCUSDT', received)
def test_null_assets_does_not_crash(self):
"""assets: null must not raise — or [] guard converts to empty, engine handles gracefully."""
scan = _make_scan(self.next_scan + 3, -0.05, file_mtime=time.time() + 13_000)
scan['assets'] = None
scan['asset_prices'] = None
try:
self.trader.on_scan(_make_event(scan))
except Exception as e:
self.fail(f"Null assets crashed on_scan: {e}")
def test_missing_vel_div_defaults_to_zero(self):
"""Missing vel_div key → 0.0 (no entry triggered, no crash)."""
vd_seen = []
orig = self.trader.eng.step_bar
def cap(*a, **kw):
vd_seen.append(kw.get('vel_div'))
return orig(*a, **kw)
self.trader.eng.step_bar = cap
scan = _make_scan(self.next_scan + 4, -0.01, file_mtime=time.time() + 14_000)
del scan['vel_div']
self.trader.on_scan(_make_event(scan))
self.trader.eng.step_bar = orig
self.assertEqual(len(vd_seen), 1)
self.assertEqual(vd_seen[0], 0.0)
def test_extended_universe_all_prices_preserved(self):
"""Extra assets beyond base universe must all reach step_bar."""
extra_assets = ASSETS + ["AVAXUSDT", "DOGEUSDT"]
extra_prices = BASE_PRICES + [28.5, 0.18]
scan = _make_scan(self.next_scan + 5, -0.01, file_mtime=time.time() + 15_000,
assets=extra_assets, prices=extra_prices)
received = self._captured_prices(scan)
for a in ["AVAXUSDT", "DOGEUSDT"]:
self.assertIn(a, received, f"{a} missing from prices_dict — universe truncated")
def test_prices_with_zero_not_dropped(self):
"""A price of 0.0 is suspicious but must not be silently dropped by `or []` guards."""
prices_with_zero = list(BASE_PRICES)
prices_with_zero[1] = 0.0 # ETHUSDT = 0
scan = _make_scan(self.next_scan + 6, -0.01, file_mtime=time.time() + 16_000,
assets=ASSETS, prices=prices_with_zero)
received = self._captured_prices(scan)
self.assertIn('ETHUSDT', received)
self.assertEqual(received['ETHUSDT'], 0.0)
# ---------------------------------------------------------------------------
# 9. Rollover Safety
# ---------------------------------------------------------------------------
class TestRolloverSafety(unittest.TestCase):
"""
begin_day() must fire EXACTLY once at day boundary even under concurrent load.
Double-begin_day resets bar_idx mid-day silent desync from backtest.
"""
def test_concurrent_rollover_begin_day_called_once(self):
trader = _build_trader()
trader.current_day = "1970-01-01"
calls = []
orig = trader.eng.begin_day
trader.eng.begin_day = lambda *a, **kw: calls.append(1) or orig(*a, **kw)
N = 20
barrier = threading.Barrier(N)
errors = []
def go():
try:
barrier.wait()
trader._rollover_day()
except Exception as e:
errors.append(e)
threads = [threading.Thread(target=go) for _ in range(N)]
for t in threads: t.start()
for t in threads: t.join(timeout=5)
trader.eng.begin_day = orig
self.assertEqual(errors, [])
self.assertEqual(len(calls), 1,
f"begin_day called {len(calls)}× — double rollover resets bar_idx mid-day")
def test_current_day_updated_inside_lock(self):
"""
current_day must be set inside eng_lock so a second thread sees the
updated value before it can acquire the lock and re-call begin_day.
"""
trader = _build_trader()
trader.current_day = "1970-01-01"
current_day_during_begin = []
orig = trader.eng.begin_day
def slow_begin(*a, **kw):
current_day_during_begin.append(trader.current_day)
time.sleep(0.02)
return orig(*a, **kw)
trader.eng.begin_day = slow_begin
t1_done = threading.Event()
t2_calls = []
def t1():
trader._rollover_day()
t1_done.set()
def t2():
t1_done.wait()
orig2 = trader.eng.begin_day
trader.eng.begin_day = lambda *a, **kw: t2_calls.append(1) or orig2(*a, **kw)
trader._rollover_day()
th1 = threading.Thread(target=t1)
th2 = threading.Thread(target=t2)
th1.start(); th2.start()
th1.join(5); th2.join(5)
trader.eng.begin_day = orig
self.assertEqual(len(t2_calls), 0,
"current_day set outside lock lets t2 see stale value and re-call begin_day")
def test_bar_idx_is_zero_after_rollover_before_first_scan(self):
trader = _build_trader()
trader.current_day = "1970-01-01"
trader._rollover_day()
self.assertEqual(trader.bar_idx, 0)
def test_rollover_is_idempotent(self):
"""Calling _rollover_day twice on same day must not call begin_day twice."""
trader = _build_trader()
calls = []
orig = trader.eng.begin_day
trader.eng.begin_day = lambda *a, **kw: calls.append(1) or orig(*a, **kw)
trader.current_day = "1970-01-01"
trader._rollover_day()
trader._rollover_day() # same day now
trader.eng.begin_day = orig
self.assertEqual(len(calls), 1)
# ---------------------------------------------------------------------------
# 10. Backtest Parity
# ---------------------------------------------------------------------------
class TestBacktestParity(unittest.TestCase):
"""
Every ENGINE_KWARGS value is an assertion against the gold-spec backtest.
Any drift here is silent algorithmic deviation the live trader is no
longer running the strategy that was validated.
"""
def test_vel_div_threshold(self):
self.assertEqual(ENGINE_KWARGS['vel_div_threshold'], -0.02)
def test_vel_div_extreme(self):
self.assertEqual(ENGINE_KWARGS['vel_div_extreme'], -0.05)
def test_max_leverage_gold_spec(self):
self.assertEqual(ENGINE_KWARGS['max_leverage'], 8.0)
def test_min_leverage(self):
self.assertEqual(ENGINE_KWARGS['min_leverage'], 0.5)
def test_max_hold_bars_gold_spec(self):
self.assertEqual(ENGINE_KWARGS['max_hold_bars'], 250)
def test_tp_pct_gold_spec(self):
self.assertEqual(ENGINE_KWARGS['fixed_tp_pct'], 0.0095)
def test_fraction_gold_spec(self):
self.assertEqual(ENGINE_KWARGS['fraction'], 0.20)
def test_vol_threshold_5yr_calibration(self):
self.assertAlmostEqual(VOL_P60_THRESHOLD, 0.00026414, places=8)
def test_direction_confirm_params(self):
self.assertTrue(ENGINE_KWARGS['use_direction_confirm'])
self.assertEqual(ENGINE_KWARGS['dc_lookback_bars'], 7)
self.assertAlmostEqual(ENGINE_KWARGS['dc_min_magnitude_bps'], 0.75)
self.assertTrue(ENGINE_KWARGS['dc_skip_contradicts'])
def test_fees_and_slippage_on(self):
self.assertTrue(ENGINE_KWARGS['use_sp_fees'])
self.assertTrue(ENGINE_KWARGS['use_sp_slippage'])
def test_maker_rates(self):
self.assertAlmostEqual(ENGINE_KWARGS['sp_maker_entry_rate'], 0.62)
self.assertAlmostEqual(ENGINE_KWARGS['sp_maker_exit_rate'], 0.50)
def test_ob_edge_params(self):
self.assertTrue(ENGINE_KWARGS['use_ob_edge'])
self.assertAlmostEqual(ENGINE_KWARGS['ob_edge_bps'], 5.0)
self.assertAlmostEqual(ENGINE_KWARGS['ob_confirm_rate'], 0.40)
def test_irp_filter_disabled(self):
self.assertEqual(ENGINE_KWARGS['min_irp_alignment'], 0.0)
def test_lookback(self):
self.assertEqual(ENGINE_KWARGS['lookback'], 100)
def test_alpha_layers_and_dynamic_leverage_on(self):
self.assertTrue(ENGINE_KWARGS['use_alpha_layers'])
self.assertTrue(ENGINE_KWARGS['use_dynamic_leverage'])
def test_asset_selection_on(self):
self.assertTrue(ENGINE_KWARGS['use_asset_selection'])
def test_bar_idx_resets_on_rollover_documented_deviation(self):
"""
Live resets bar_idx=0 per day; backtest uses continuous bar_idx.
This is a known, accepted deviation documented here so it's explicit.
Any change to this behaviour must be intentional.
"""
trader = _build_trader()
today = datetime.now(timezone.utc).strftime('%Y-%m-%d')
with trader.eng_lock:
trader.eng.begin_day(today, posture="APEX")
trader.bar_idx = 50
trader.current_day = today
trader.current_day = "1970-01-01"
trader.on_scan(_make_event(_make_scan(1, -0.01, file_mtime=time.time() + 99_000)))
self.assertEqual(trader.bar_idx, 1,
"bar_idx resets to 0 on rollover — known live/backtest deviation")
# ---------------------------------------------------------------------------
# 11. Slippage & Missed-Trade Cost Model
# ---------------------------------------------------------------------------
class TestSlippageCostModel(unittest.TestCase):
"""
Quantifies not prevents the financial cost of latency failures.
Gold spec: T=2155, ROI=+189.48%, DD=21.31%, 5yr, capital=$25k, leverage=8x.
All monetary assertions use delta tolerances so a round-number change
in spec parameters breaks loudly.
"""
C = ENGINE_KWARGS['initial_capital'] # 25_000
LEV = ENGINE_KWARGS['max_leverage'] # 8.0
TP = ENGINE_KWARGS['fixed_tp_pct'] # 0.0095
T = 2155
ROI = 1.8948
BAR_S = 5
@property
def max_pos(self): return self.C * self.LEV # $200k
@property
def max_gross(self): return self.max_pos * self.TP # $1,900
@property
def avg_net(self): return (self.C * self.ROI) / self.T
def test_max_position_size(self):
self.assertAlmostEqual(self.max_pos, 200_000, delta=1)
def test_max_gross_profit_per_trade(self):
self.assertAlmostEqual(self.max_gross, 1_900, delta=1)
print(f"\n Max gross profit/trade: ${self.max_gross:,.0f}")
def test_avg_net_profit_per_trade_positive(self):
self.assertGreater(self.avg_net, 0,
"Negative avg net = no edge. Backtest ROI or trade count changed?")
print(f" Avg net profit/trade (5yr gold): ${self.avg_net:,.2f}")
def test_cost_of_1bar_delayed_entry(self):
"""
1-bar (5s) late entry: BTC moves against position.
Typical = 10bps × $200k = $200. Volatile = 50bps × $200k = $1,000.
Both must be < max_gross (otherwise latency budget breaks strategy viability).
"""
typical = self.max_pos * 0.0010 # 10bps
volatile = self.max_pos * 0.0050 # 50bps
print(f" 1-bar delay cost: typical=${typical:,.0f} volatile=${volatile:,.0f}")
self.assertLess(volatile, self.max_gross,
"Worst-case 1-bar delay exceeds full TP — latency budget is strategy-critical")
def test_cost_of_missed_entry_upper_bound(self):
"""Upper bound on missed entry = max_gross. Expected ≈ avg_net."""
print(f" Missed entry cost: upper=${self.max_gross:,.0f} "
f"expected≈${self.avg_net:,.2f}")
self.assertGreater(self.max_gross, self.avg_net)
def test_max_drawdown_gold_spec(self):
"""DD=21.31% → $5,327 max observed drawdown over 5yr."""
max_dd = self.C * 0.2131
max_hold_s = ENGINE_KWARGS['max_hold_bars'] * self.BAR_S
print(f" Max drawdown (gold): ${max_dd:,.0f} | "
f"Max hold: {max_hold_s}s ({max_hold_s/60:.1f}min)")
self.assertAlmostEqual(max_dd, 5327.50, delta=100)
self.assertLessEqual(max_hold_s, 1300)
def test_1ms_latency_cost_per_trade(self):
"""
Cost of 1ms additional latency per trade (entry slip only).
Avg hold ~50 bars; 1ms BTC move 0.002bps; 50 bars × 0.002bps × $200k = $2/trade.
Over T=2155 trades: $4,310 total real but not strategy-killing.
The 30ms E2E budget already ensures we're well inside this.
"""
avg_hold_bars = 50
btc_move_per_ms_bps = 0.002 / 100 # 0.002bps expressed as fraction
cost_per_trade = self.max_pos * btc_move_per_ms_bps * avg_hold_bars
total_5yr = cost_per_trade * self.T
print(f" 1ms/trade latency cost: ${cost_per_trade:.2f}/trade "
f"${total_5yr:,.0f} over {self.T} trades")
# Informational — we assert it's positive and bounded below max_gross
self.assertGreater(cost_per_trade, 0)
self.assertLess(cost_per_trade, self.max_gross)
def test_stablecoin_trade_financial_loss(self):
"""
Shorting a stablecoin (e.g. USDCUSDT $1.000) produces near-zero
gross P&L regardless of leverage it cannot meaningfully contribute to
ROI and adds unnecessary fee drag. Any stablecoin trade at gold-spec
9x abs_max leverage would cost at least one round-trip fee.
Picker hard-block prevents this entire class of loss.
"""
stable_price = 1.0001 # realistic USDC perp price
max_lev = 9.0 # abs_max_leverage
notional = self.C * ENGINE_KWARGS['fraction'] * max_lev # $22,500
round_trip_fee = notional * 0.0004 * 2 # 0.04% each side
max_move_bps = 1.0 # stablecoin max realistic move
gross = notional * (max_move_bps / 10_000)
print(f"\n Stablecoin short: notional=${notional:,.0f} "
f"max_gross=${gross:.2f} fee_drag=${round_trip_fee:.2f}")
# The fee drag exceeds any realistic gross profit on a stablecoin
self.assertGreater(round_trip_fee, gross,
"Fee drag on stablecoin trade exceeds gross P&L — "
"confirms picker hard-block is financially correct, not just cosmetic")
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
if __name__ == '__main__':
unittest.main(verbosity=2)