PINK Phase 0 and 1: VST WS confirmed plus AccountSnapshotV2 account core

This commit is contained in:
Codex
2026-06-01 20:11:03 +02:00
parent c87ca785b9
commit e7eaa88ce1
166 changed files with 832 additions and 77021 deletions

View File

@@ -1,107 +0,0 @@
"""L2 — LIMIT order payload wiring in BingxDirectExecutionAdapter.submit_intent.
The venue adapter forwards _order_type/_limit_price in the intent metadata; the
backend must place a LIMIT order (type=LIMIT + price + GTC) when asked, and keep
MARKET as the default. Offline unit test of payload construction — the signed_post
client is stubbed to capture the order payload; no exchange contact.
"""
from __future__ import annotations
import asyncio
from datetime import datetime, timezone
from types import SimpleNamespace
from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter
from prod.clean_arch.dita import DecisionAction, Intent, TradeSide
def _adapter(captured: dict):
a = BingxDirectExecutionAdapter.__new__(BingxDirectExecutionAdapter)
a._config = SimpleNamespace(recv_window_ms=5000, default_leverage=1, exchange_leverage_cap=3)
a._client_order_run_id = "test"
a._entry_client_order_seq = 0
a._exit_client_order_seq = 0
a._state = SimpleNamespace(open_positions={}, account={})
async def _signed_post(path, params):
if path.endswith("/trade/order"):
captured["order"] = dict(params)
return {"orderId": "1", "status": "NEW"}
a._client = SimpleNamespace(signed_post=_signed_post)
a._instrument_venue_symbol = lambda asset: "BTC-USDT"
a._format_quantity = lambda asset, q: f"{float(q)}"
a._format_price = lambda asset, p: f"{float(p)}"
async def _refresh(asset, include_history=True):
return a._state
a._refresh_exchange_state = _refresh
return a
def _intent(metadata: dict) -> Intent:
return Intent(
timestamp=datetime.now(timezone.utc), trade_id="T1", decision_id="D1",
asset="BTCUSDT", action=DecisionAction.ENTER, side=TradeSide.SHORT,
reason="TEST", target_size=0.01, leverage=2.0, reference_price=100.0,
confidence=0.5, exit_leg_ratios=(1.0,), metadata=metadata,
)
def test_limit_intent_places_limit_order():
captured: dict = {}
asyncio.run(_adapter(captured).submit_intent(_intent({"_order_type": "LIMIT", "_limit_price": 95.0})))
o = captured["order"]
assert o["type"] == "LIMIT", o
assert "price" in o and float(o["price"]) == 95.0, o
assert o.get("timeInForce") == "GTC", o
def test_market_intent_places_market_order():
captured: dict = {}
asyncio.run(_adapter(captured).submit_intent(_intent({})))
o = captured["order"]
assert o["type"] == "MARKET", o
assert "price" not in o, o
def test_limit_without_valid_price_falls_back_to_market():
captured: dict = {}
asyncio.run(_adapter(captured).submit_intent(_intent({"_order_type": "LIMIT", "_limit_price": 0.0})))
assert captured["order"]["type"] == "MARKET", captured["order"]
# --- cancel: truth-based confirmation (trust exchange state over the response) ---
def _cancel_adapter(*, open_after: list):
a = BingxDirectExecutionAdapter.__new__(BingxDirectExecutionAdapter)
a._config = SimpleNamespace(recv_window_ms=5000)
a._instrument_venue_symbol = lambda asset: "TRX-USDT"
async def _signed_delete(path, params):
# Simulate BingX returning a transient error even when the order is removed.
return {"status": "REJECTED", "msg": "order not exist"}
async def _signed_get(path, params):
return {"data": {"orders": open_after}}
a._client = SimpleNamespace(signed_delete=_signed_delete, signed_get=_signed_get)
return a
def _order(oid="2060963645141028864"):
return SimpleNamespace(venue_order_id=oid, venue_client_id="T:i", metadata={"asset": "TRXUSDT"})
def test_cancel_succeeds_when_order_gone_despite_error_response():
a = _cancel_adapter(open_after=[]) # order no longer open
resp = asyncio.run(a.cancel(_order()))
assert resp["status"] == "CANCELED", resp
def test_cancel_rejected_when_order_still_open():
a = _cancel_adapter(open_after=[{"orderId": "2060963645141028864", "status": "PENDING"}])
resp = asyncio.run(a.cancel(_order()))
assert resp["status"] != "CANCELED", resp

View File

@@ -1,559 +0,0 @@
"""
test_bingx_nautilus_execution.py
================================
End-to-end tests for the Nautilus execution path:
engine.step_bar() -> _exec_submit_entry() -> cache.instrument() -> order_factory -> submit_order
Tests cover:
1. Instrument registration from exec client into Nautilus cache
2. _exec_submit_entry returns early when instrument missing
3. _exec_submit_entry succeeds when instrument is in cache
4. Data-venue fallback when exec-venue instrument not available
5. Full order payload correctness (tags, side, quantity precision)
6. Venue symbol mapping uses raw_symbol from cached instrument
7. _venue_symbol fallback when instrument not in cache
8. Integration: build_actor_config with split venues
"""
from __future__ import annotations
import asyncio
import math
import sys
from decimal import Decimal
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "nautilus_dolphin"))
from nautilus_trader.model.enums import OrderSide, OrderType
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.model.objects import Currency, Price, Quantity
from prod.bingx.enums import BINGX_VENUE, BingxEnvironment
from prod.bingx.execution import BingxExecutionClient
from prod.bingx.sandbox_status import build_sandbox_status
from prod.bingx.sandbox_status import write_sandbox_status
# -- Helpers -------------------------------------------------------------------
def _make_bingx_instrument(symbol: str = "BTCUSDT"):
return SimpleNamespace(
id=InstrumentId.from_str(f"{symbol}.BINGX"),
instrument_id=InstrumentId.from_str(f"{symbol}.BINGX"),
symbol=SimpleNamespace(value=symbol),
raw_symbol=SimpleNamespace(value=f"{symbol[:-4]}-USDT"),
base_currency=Currency.from_str(symbol[:3]),
quote_currency=Currency.from_str("USDT"),
size_precision=3,
price_precision=2,
maker_fee=Decimal("0.0002"),
taker_fee=Decimal("0.0005"),
)
def _make_binance_instrument(symbol: str = "BTCUSDT"):
return SimpleNamespace(
id=InstrumentId.from_str(f"{symbol}.BINANCE"),
instrument_id=InstrumentId.from_str(f"{symbol}.BINANCE"),
symbol=SimpleNamespace(value=symbol),
raw_symbol=SimpleNamespace(value=symbol),
base_currency=Currency.from_str(symbol[:3]),
quote_currency=Currency.from_str("USDT"),
size_precision=3,
price_precision=2,
maker_fee=Decimal("0.0002"),
taker_fee=Decimal("0.0005"),
)
class FakeCache:
def __init__(self, instruments=None):
self._instruments = dict(instruments or {})
def instrument(self, instrument_id):
return self._instruments.get(instrument_id)
def add_instrument(self, instrument):
self._instruments[instrument.id] = instrument
def instruments(self):
return list(self._instruments.values())
def positions(self, venue=None):
return []
def order(self, client_order_id):
return None
def add_currency(self, currency):
pass
class FakeProvider:
def __init__(self, instruments=None):
self._instruments = list(instruments or [])
def list_all(self):
return self._instruments
def currencies(self):
return {}
async def initialize(self):
pass
async def _noop(*args, **kwargs):
pass
async def _persist_sandbox_status(client, *, environment: str = "VST", notes: dict[str, Any] | None = None):
balance = await client.signed_get("/openApi/swap/v2/user/balance")
positions = await client.signed_get("/openApi/swap/v2/user/positions")
open_orders = await client.signed_get("/openApi/swap/v2/trade/openOrders")
status = build_sandbox_status(
balance_payload=balance,
positions_payload=positions,
open_orders_payload=open_orders,
environment=environment,
notes=notes or {},
)
write_sandbox_status(status)
return status
def _make_connect_stub(cache, provider):
return SimpleNamespace(
_cache=cache,
_provider=provider,
_config=SimpleNamespace(prefer_websocket=False),
_log=SimpleNamespace(info=lambda *a, **kw: None, warning=lambda *a, **kw: None),
_start_pollers=lambda: None,
_refresh_account_state=_noop,
_restore_journal_snapshot=_noop,
_persist_journal_snapshot=_noop,
_await_account_registered=_noop,
)
def _make_actor_stub(cache, exec_venue="BINGX", data_venue="BINANCE"):
log_messages = []
class Log:
def info(self, msg, *a, **kw):
log_messages.append(("info", msg))
def warning(self, msg, *a, **kw):
log_messages.append(("warning", msg))
def error(self, msg, *a, **kw):
log_messages.append(("error", msg))
def debug(self, msg, *a, **kw):
log_messages.append(("debug", msg))
return SimpleNamespace(
cache=cache,
log=Log(),
_log_messages=log_messages,
dolphin_config={
"engine": {"max_account_leverage": 2.0},
"paper_trade": {"initial_capital": 25000.0},
},
engine=SimpleNamespace(capital=100000.0),
_last_portfolio_capital=100000.0,
_exec_venue_name=lambda: exec_venue,
_data_venue_name=lambda: data_venue,
_exec_open_positions={},
order_factory=SimpleNamespace(
market=lambda **kw: SimpleNamespace(
instrument_id=kw.get("instrument_id"),
order_side=kw.get("order_side"),
quantity=kw.get("quantity"),
tags=kw.get("tags", []),
client_order_id=SimpleNamespace(value="test-coid"),
order_type=OrderType.MARKET,
strategy_id=SimpleNamespace(value="test-strat"),
)
),
submit_order=MagicMock(),
clock=SimpleNamespace(timestamp_ns=lambda: 1000),
)
# -- Test: Instrument Registration --------------------------------------------
class TestExecClientRegistersInstrumentsInCache:
def test_instruments_registered_after_connect(self):
inst1 = _make_bingx_instrument("BTCUSDT")
inst2 = _make_bingx_instrument("ETHUSDT")
cache = FakeCache()
provider = FakeProvider([inst1, inst2])
stub = _make_connect_stub(cache, provider)
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(BingxExecutionClient._connect(stub))
finally:
loop.close()
assert cache.instrument(InstrumentId.from_str("BTCUSDT.BINGX")) is inst1
assert cache.instrument(InstrumentId.from_str("ETHUSDT.BINGX")) is inst2
def test_empty_provider_no_crash(self):
cache = FakeCache()
provider = FakeProvider([])
stub = _make_connect_stub(cache, provider)
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(BingxExecutionClient._connect(stub))
finally:
loop.close()
assert len(list(cache.instruments())) == 0
# -- Test: _exec_submit_entry instrument lookup ------------------------------
class TestExecSubmitEntryInstrumentLookup:
def test_returns_early_when_no_exec_instrument_no_data_fallback(self):
cache = FakeCache()
stub = _make_actor_stub(cache)
entry = {"asset": "XLMUSDT", "direction": -1, "notional": 5000.0, "entry_price": 0.10, "trade_id": "t1", "leverage": 1.0}
prices = {"XLMUSDT": 0.10}
result = DolphinActor._exec_submit_entry(stub, entry, prices)
assert result is None
assert not stub.submit_order.called
error_msgs = [m for lvl, m in stub._log_messages if lvl == "error"]
assert any("not in cache" in m for m in error_msgs)
def test_succeeds_with_exec_instrument_in_cache(self):
inst = _make_bingx_instrument("XLMUSDT")
cache = FakeCache({inst.id: inst})
stub = _make_actor_stub(cache)
entry = {"asset": "XLMUSDT", "direction": -1, "notional": 5000.0, "entry_price": 0.10, "trade_id": "t1", "leverage": 1.0}
prices = {"XLMUSDT": 0.10}
DolphinActor._exec_submit_entry(stub, entry, prices)
assert stub.submit_order.called
order = stub.submit_order.call_args[0][0]
assert str(order.instrument_id) == "XLMUSDT.BINGX"
assert order.order_side == OrderSide.SELL
assert order.tags[0] == "type:entry"
assert "direction:SHORT" in order.tags
def test_data_venue_fallback_with_warning(self):
binance_inst = _make_binance_instrument("XLMUSDT")
cache = FakeCache({binance_inst.id: binance_inst})
stub = _make_actor_stub(cache, exec_venue="BINGX", data_venue="BINANCE")
entry = {"asset": "XLMUSDT", "direction": -1, "notional": 5000.0, "entry_price": 0.10, "trade_id": "t1", "leverage": 1.0}
prices = {"XLMUSDT": 0.10}
DolphinActor._exec_submit_entry(stub, entry, prices)
assert stub.submit_order.called
warn_msgs = [m for lvl, m in stub._log_messages if lvl == "warning"]
assert any("borrowing metadata" in m for m in warn_msgs)
def test_info_log_on_successful_entry(self):
inst = _make_bingx_instrument("XLMUSDT")
cache = FakeCache({inst.id: inst})
stub = _make_actor_stub(cache)
entry = {"asset": "XLMUSDT", "direction": -1, "notional": 5000.0, "entry_price": 0.10, "trade_id": "t42", "leverage": 1.5}
prices = {"XLMUSDT": 0.10}
DolphinActor._exec_submit_entry(stub, entry, prices)
info_msgs = [m for lvl, m in stub._log_messages if lvl == "info"]
assert any("[EXEC] ENTRY SHORT" in m and "XLMUSDT" in m for m in info_msgs)
def test_quantity_uses_instrument_size_precision(self):
inst = _make_bingx_instrument("BTCUSDT")
inst.size_precision = 4
cache = FakeCache({inst.id: inst})
stub = _make_actor_stub(cache)
entry = {"asset": "BTCUSDT", "direction": 1, "notional": 50000.0, "entry_price": 100000.0, "trade_id": "t1", "leverage": 1.0}
prices = {"BTCUSDT": 100000.0}
DolphinActor._exec_submit_entry(stub, entry, prices)
order = stub.submit_order.call_args[0][0]
assert order.quantity.precision == 4
# -- Test: _venue_symbol mapping ---------------------------------------------
class TestVenueSymbolMapping:
def test_uses_raw_symbol_when_instrument_in_cache(self):
inst = _make_bingx_instrument("TRXUSDT")
cache = FakeCache({inst.id: inst})
stub = SimpleNamespace(_cache=cache)
result = BingxExecutionClient._venue_symbol(stub, InstrumentId.from_str("TRXUSDT.BINGX"))
assert result == "TRX-USDT"
def test_fallback_converts_usdt_suffix(self):
stub = SimpleNamespace(_cache=FakeCache())
result = BingxExecutionClient._venue_symbol(stub, InstrumentId.from_str("XLMUSDT.BINGX"))
assert result == "XLM-USDT"
def test_fallback_passes_through_hyphenated(self):
stub = SimpleNamespace(_cache=FakeCache())
result = BingxExecutionClient._venue_symbol(stub, InstrumentId.from_str("BTC-USDT.BINGX"))
assert result == "BTC-USDT"
# -- Test: _map_submit_order -------------------------------------------------
class TestMapSubmitOrderForMarketOrder:
def test_market_sell_with_tags(self):
inst = _make_bingx_instrument("ETHUSDT")
cache = FakeCache({inst.id: inst})
order = SimpleNamespace(
instrument_id=InstrumentId.from_str("ETHUSDT.BINGX"),
side=OrderSide.SELL,
order_type=OrderType.MARKET,
quantity=Quantity.from_str("1.500"),
client_order_id=SimpleNamespace(value="test-cid-001"),
is_post_only=False,
is_reduce_only=False,
has_price=False,
has_trigger_price=False,
price=None,
trigger_price=None,
time_in_force=None,
tags=["type:entry", "direction:SHORT", "cm:2.50", "tid:t99"],
)
adapter = SimpleNamespace(
_cache=cache,
_config=SimpleNamespace(use_reduce_only=True, recv_window_ms=5000),
_venue_symbol=lambda iid: BingxExecutionClient._venue_symbol(adapter, iid),
_format_quantity=lambda q: str(q),
_format_price=lambda p: str(p),
_map_order_type=BingxExecutionClient._map_order_type,
_map_time_in_force=BingxExecutionClient._map_time_in_force,
)
# Rebind _venue_symbol after adapter exists so self-referencing works
adapter._venue_symbol = lambda iid: BingxExecutionClient._venue_symbol(adapter, iid)
payload = BingxExecutionClient._map_submit_order(adapter, order)
assert payload["symbol"] == "ETH-USDT"
assert payload["side"] == "SELL"
assert payload["type"] == "MARKET"
assert payload["quantity"] == "1.500"
assert payload["clientOrderId"] == "test-cid-001"
# -- Test: Order tag parsing for leverage ------------------------------------
class TestLeverageTagParsing:
def test_extracts_lev_tag(self):
order = SimpleNamespace(tags=["type:entry", "lev:2.50", "cm:2.50", "tid:t1"])
assert BingxExecutionClient._parse_leverage_from_tags(order) == 2.50
def test_extracts_cm_tag(self):
order = SimpleNamespace(tags=["type:entry", "cm:3.00", "tid:t1"])
assert BingxExecutionClient._parse_leverage_from_tags(order) == 3.00
def test_returns_none_no_tags(self):
order = SimpleNamespace(tags=[])
assert BingxExecutionClient._parse_leverage_from_tags(order) is None
def test_returns_none_no_leverage_tags(self):
order = SimpleNamespace(tags=["type:entry", "direction:SHORT"])
assert BingxExecutionClient._parse_leverage_from_tags(order) is None
# -- Test: Split venue configuration -----------------------------------------
class TestSplitVenueConfig:
def test_split_venues_preserved(self):
from prod.launch_dolphin_live import build_actor_config
cfg = build_actor_config(data_venue="BINANCE", exec_venue="BINGX")
assert cfg["data_venue"] == "BINANCE"
assert cfg["exec_venue"] == "BINGX"
assert cfg["venue"] == "BINGX"
# -- Import DolphinActor -----------------------------------------------------
try:
from nautilus_dolphin.nautilus.dolphin_actor import DolphinActor
HAS_DOLPHIN_ACTOR = True
except ImportError:
HAS_DOLPHIN_ACTOR = False
@pytest.mark.skipif(not HAS_DOLPHIN_ACTOR, reason="DolphinActor not importable")
class TestDolphinActorExecSubmitEntry:
def _actor_stub(self, cache):
return _make_actor_stub(cache)
def test_full_entry_flow_short_order(self):
inst = _make_bingx_instrument("SOLUSDT")
cache = FakeCache({inst.id: inst})
stub = self._actor_stub(cache)
entry = {
"asset": "SOLUSDT", "direction": -1, "notional": 3000.0,
"entry_price": 150.0, "trade_id": "trade-sol-001",
"leverage": 2.0, "vel_div": -0.035,
}
prices = {"SOLUSDT": 150.0}
DolphinActor._exec_submit_entry(stub, entry, prices)
assert stub.submit_order.called
order = stub.submit_order.call_args[0][0]
assert "direction:SHORT" in order.tags
assert "cm:2.00" in order.tags
assert "lev:2.00" in order.tags
assert "tid:trade-sol-001" in order.tags
def test_full_entry_flow_long_order(self):
inst = _make_bingx_instrument("ADAUSDT")
cache = FakeCache({inst.id: inst})
stub = self._actor_stub(cache)
entry = {
"asset": "ADAUSDT", "direction": 1, "notional": 2000.0,
"entry_price": 0.45, "trade_id": "trade-ada-002",
"leverage": 1.0, "vel_div": -0.025,
}
prices = {"ADAUSDT": 0.45}
DolphinActor._exec_submit_entry(stub, entry, prices)
assert stub.submit_order.called
order = stub.submit_order.call_args[0][0]
assert order.order_side == OrderSide.BUY
assert "direction:LONG" in order.tags
def test_caps_notional_when_near_capacity_limit(self):
inst = _make_bingx_instrument("BTCUSDT")
cache = FakeCache({inst.id: inst})
stub = self._actor_stub(cache)
stub.engine = SimpleNamespace(capital=10.0)
stub.dolphin_config["engine"]["max_account_leverage"] = 0.01
entry = {
"asset": "BTCUSDT", "direction": -1, "notional": 5000.0,
"entry_price": 100000.0, "trade_id": "t1", "leverage": 1.0,
}
prices = {"BTCUSDT": 100000.0}
DolphinActor._exec_submit_entry(stub, entry, prices)
assert stub.submit_order.called
warn_msgs = [m for lvl, m in stub._log_messages if lvl == "warning"]
assert any("capped by portfolio exposure" in m for m in warn_msgs)
def test_skips_when_notional_zero(self):
inst = _make_bingx_instrument("BTCUSDT")
cache = FakeCache({inst.id: inst})
stub = self._actor_stub(cache)
entry = {
"asset": "BTCUSDT", "direction": -1, "notional": 0.0,
"entry_price": 100000.0, "trade_id": "t1", "leverage": 1.0,
}
prices = {"BTCUSDT": 100000.0}
result = DolphinActor._exec_submit_entry(stub, entry, prices)
assert result is None
assert not stub.submit_order.called
# -- Live integration (requires BingX VST credentials) -----------------------
@pytest.mark.skipif(
not Path("/mnt/dolphinng5_predict/.env").exists(),
reason="No .env file (no BingX credentials)",
)
class TestLiveInstrumentProvider:
def test_loads_instruments_from_vst(self):
import os
from dotenv import load_dotenv
load_dotenv("/mnt/dolphinng5_predict/.env")
from prod.bingx.config import BingxExecClientConfig
from prod.bingx.http import BingxHttpClient
from prod.bingx.instrument_provider import BingxInstrumentProvider, BingxInstrumentProviderConfig
async def _run():
cfg = BingxExecClientConfig(
api_key=os.environ.get("BINGX_API_KEY", ""),
secret_key=os.environ.get("BINGX_SECRET_KEY", ""),
environment=BingxEnvironment.VST,
)
client = BingxHttpClient(config=cfg)
provider = BingxInstrumentProvider(
client=client,
config=BingxInstrumentProviderConfig(load_all=True),
)
await provider.initialize()
instruments = provider.list_all()
await _persist_sandbox_status(client, notes={"test": "loads_instruments_from_vst"})
await client.close()
return instruments
instruments = asyncio.run(_run())
assert len(instruments) > 0
symbols = {i.symbol.value for i in instruments}
assert "BTCUSDT" in symbols
assert "ETHUSDT" in symbols
def test_trxusdt_instrument_has_correct_precision(self):
import os
from dotenv import load_dotenv
load_dotenv("/mnt/dolphinng5_predict/.env")
from prod.bingx.config import BingxExecClientConfig
from prod.bingx.http import BingxHttpClient
from prod.bingx.instrument_provider import BingxInstrumentProvider, BingxInstrumentProviderConfig
async def _run():
cfg = BingxExecClientConfig(
api_key=os.environ.get("BINGX_API_KEY", ""),
secret_key=os.environ.get("BINGX_SECRET_KEY", ""),
environment=BingxEnvironment.VST,
)
client = BingxHttpClient(config=cfg)
provider = BingxInstrumentProvider(
client=client,
config=BingxInstrumentProviderConfig(load_all=True),
)
await provider.initialize()
inst = provider.find(InstrumentId.from_str("TRXUSDT.BINGX"))
await _persist_sandbox_status(client, notes={"test": "trxusdt_instrument_has_correct_precision"})
await client.close()
return inst
inst = asyncio.run(_run())
assert inst is not None
assert inst.size_precision >= 1
assert inst.price_precision >= 1
assert inst.raw_symbol.value == "TRX-USDT"

View File

@@ -1,71 +0,0 @@
from __future__ import annotations
from pathlib import Path
from prod.bingx.sandbox_status import build_sandbox_status
from prod.bingx.sandbox_status import load_sandbox_status
from prod.bingx.sandbox_status import write_sandbox_status
def test_build_sandbox_status_marks_clean_when_flat():
status = build_sandbox_status(
balance_payload={
"balance": {
"balance": "12000.5",
"equity": "12000.5",
"availableMargin": "12000.5",
"unrealizedProfit": "0",
"usedMargin": "0",
}
},
positions_payload=[],
open_orders_payload={"orders": []},
environment="VST",
)
assert status.clean is True
assert status.balance == 12000.5
assert status.equity == 12000.5
assert status.open_positions == 0
assert status.open_orders == 0
def test_build_sandbox_status_marks_dirty_when_positions_or_orders_exist():
status = build_sandbox_status(
balance_payload={
"balance": {
"balance": "12000.5",
"equity": "12500.5",
"availableMargin": "9000.5",
"unrealizedProfit": "500",
"usedMargin": "3000",
}
},
positions_payload=[{"symbol": "BTC-USDT"}, {"symbol": "ETH-USDT"}],
open_orders_payload={"orders": [{"symbol": "BTC-USDT"}]},
environment="VST",
)
assert status.clean is False
assert status.open_positions == 2
assert status.open_orders == 1
assert status.unrealized_profit == 500.0
def test_write_and_load_sandbox_status_round_trip(tmp_path: Path):
status = build_sandbox_status(
balance_payload={"balance": {"balance": "10", "equity": "11", "availableMargin": "9", "unrealizedProfit": "1", "usedMargin": "2"}},
positions_payload=[],
open_orders_payload=[],
environment="VST",
notes={"source": "unit-test"},
)
path = tmp_path / "bingx_sandbox_status.json"
write_sandbox_status(status, path)
loaded = load_sandbox_status(path)
assert loaded is not None
assert loaded["balance"] == 10.0
assert loaded["equity"] == 11.0
assert loaded["clean"] is True
assert loaded["notes"]["source"] == "unit-test"

View File

@@ -1,87 +0,0 @@
#!/usr/bin/env python3
"""Tests for capital restore source selection on startup."""
import json
import os
from datetime import datetime, timezone
from unittest.mock import patch
import pytest
from prod.nautilus_event_trader import DolphinLiveTrader
class _MapStub:
def __init__(self, payloads):
self._payloads = payloads
def blocking(self):
return self
def get(self, key):
return self._payloads.get(key)
def _build_trader() -> DolphinLiveTrader:
trader = DolphinLiveTrader()
trader._build_engine()
trader.eng.begin_day(datetime.now(timezone.utc).strftime("%Y-%m-%d"), posture="APEX")
return trader
def test_restore_prefers_fresher_engine_snapshot_over_stale_latest_nautilus():
trader = _build_trader()
trader.eng.capital = 25_000.0
trader.state_map = _MapStub(
{
"latest_nautilus": json.dumps(
{
"capital": 31_049.44,
"updated_at": "2026-05-13T10:52:40+00:00",
}
),
"engine_snapshot": json.dumps(
{
"capital": 33_150.07,
"timestamp": "2026-05-13T16:20:38+00:00",
}
),
}
)
trader.pnl_map = _MapStub({})
with patch.dict(os.environ, {"DOLPHIN_CAPITAL_SEED_STALE_LAG_SEC": "180"}, clear=False):
trader._restore_capital()
assert trader.eng.capital == pytest.approx(33_150.07, abs=0.01)
assert trader._restore_source == "HZ engine_snapshot"
def test_restore_can_force_latest_nautilus_override():
trader = _build_trader()
trader.eng.capital = 25_000.0
trader.state_map = _MapStub(
{
"latest_nautilus": json.dumps(
{
"capital": 31_049.44,
"updated_at": "2026-05-13T10:52:40+00:00",
}
),
"engine_snapshot": json.dumps(
{
"capital": 33_150.07,
"timestamp": "2026-05-13T16:20:38+00:00",
}
),
}
)
trader.pnl_map = _MapStub({})
with patch.dict(os.environ, {"DOLPHIN_FORCE_LATEST_NAUTILUS_RESTORE": "1"}, clear=False):
trader._restore_capital()
assert trader.eng.capital == pytest.approx(31_049.44, abs=0.01)
assert trader._restore_source == "HZ latest_nautilus"

View File

@@ -1,391 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
import pytest
from prod.clean_arch.dita_v2 import (
BingxVenueAdapter,
ExecutionKernel,
InMemoryControlPlane,
KernelCommandType,
KernelControlSnapshot,
KernelIntent,
KernelMode,
KernelEventKind,
KernelVerbosity,
TradeSide,
TradeStage,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
from prod.clean_arch.ports.execution import ExchangeStateSnapshot, ExecutionReceipt
def _norm_symbol(symbol: str) -> str:
return str(symbol or "").replace("-", "").replace("_", "").upper()
def _snapshot(
*,
capital: float = 25_000.0,
positions: list[dict[str, Any]] | None = None,
open_orders: list[dict[str, Any]] | None = None,
all_orders: list[dict[str, Any]] | None = None,
all_fills: list[dict[str, Any]] | None = None,
source: str = "bingx",
) -> ExchangeStateSnapshot:
position_map = {
_norm_symbol(str(row.get("symbol", ""))): dict(row)
for row in (positions or [])
if _norm_symbol(str(row.get("symbol", "")))
}
return ExchangeStateSnapshot(
timestamp=datetime.now(timezone.utc),
capital=capital,
equity=capital,
open_positions=position_map,
open_orders=[dict(row) for row in (open_orders or [])],
all_orders=[dict(row) for row in (all_orders or [])],
all_fills=[dict(row) for row in (all_fills or [])],
account={"balances": [{"asset": "USDT", "total": capital}]},
open_notional=0.0,
source=source,
recovered=False,
)
class FakeBingxBackend:
def __init__(
self,
*,
snapshots: list[ExchangeStateSnapshot],
receipt: ExecutionReceipt | None = None,
cancel_response: dict[str, Any] | None = None,
) -> None:
self.snapshots = snapshots
self.receipt = receipt
self.cancel_response = cancel_response or {"status": "CANCELED"}
self.calls: list[tuple[str, Any]] = []
self.submitted: list[Any] = []
self.canceled: list[tuple[Any, str]] = []
self._refresh_count = 0
self.connected = False
async def connect(self) -> bool:
self.connected = True
self.calls.append(("connect", None))
return True
async def disconnect(self) -> None:
self.connected = False
self.calls.append(("disconnect", None))
async def refresh_state(self, symbol: str | None = None, *, include_history: bool = False) -> ExchangeStateSnapshot:
self.calls.append(("refresh_state", symbol, include_history))
index = min(self._refresh_count, len(self.snapshots) - 1)
snapshot = self.snapshots[index]
if self._refresh_count < len(self.snapshots) - 1:
self._refresh_count += 1
return snapshot
async def submit_intent(self, legacy_intent: Any) -> ExecutionReceipt:
self.calls.append(("submit_intent", legacy_intent.trade_id))
self.submitted.append(legacy_intent)
if self.receipt is None:
raise AssertionError("receipt must be configured")
return self.receipt
async def cancel_order(self, order: VenueOrder, *, reason: str = "") -> dict[str, Any]:
self.calls.append(("cancel_order", order.venue_order_id, reason))
self.canceled.append((order, reason))
return dict(self.cancel_response)
def _intent(
*,
action: KernelCommandType = KernelCommandType.ENTER,
trade_id: str = "trade-1",
slot_id: int = 0,
asset: str = "BTCUSDT",
side: TradeSide = TradeSide.SHORT,
target_size: float = 1.0,
leverage: float = 2.0,
reference_price: float = 75_000.0,
reason: str = "TEST",
) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:{action.value}",
trade_id=trade_id,
slot_id=slot_id,
asset=asset,
side=side,
action=action,
reference_price=reference_price,
target_size=target_size,
leverage=leverage,
reason=reason,
)
def test_submit_maps_bingx_ack_and_snapshot_fill_to_ditav2_events() -> None:
ack_row = {
"orderId": "1001",
"clientOrderId": "cid-1",
"clientOrderID": "cid-1",
"symbol": "BTC-USDT",
"status": "NEW",
"executedQty": "0",
"cumFilledQty": "0",
}
fill_row = {
"clientOrderId": "cid-1",
"clientOrderID": "cid-1",
"orderId": "1001",
"symbol": "BTC-USDT",
"status": "FILLED",
"executedQty": "1",
"lastFilledQty": "1",
"lastFillPrice": "75000",
}
backend = FakeBingxBackend(
snapshots=[
_snapshot(),
_snapshot(
positions=[
{
"symbol": "BTC-USDT",
"positionSide": "SHORT",
"positionAmt": "-1",
"avgPrice": "75000",
"markPrice": "75010",
"leverage": "2",
}
],
open_orders=[ack_row],
all_orders=[ack_row],
all_fills=[fill_row],
),
],
receipt=ExecutionReceipt(
timestamp=datetime.now(timezone.utc),
status="NEW",
symbol="BTC-USDT",
side="SELL",
action="ENTER",
quantity=1.0,
price=75_000.0,
client_order_id="cid-1",
order_id="1001",
raw_ack=ack_row,
raw_state={},
),
)
adapter = BingxVenueAdapter(backend=backend)
events = adapter.submit(_intent())
assert backend.connected is False
assert backend.submitted
assert [event.kind for event in events] == [event.kind for event in events if event.kind.value]
assert events[0].kind.value == "ORDER_ACK"
assert events[0].status == VenueEventStatus.ACKED
assert events[0].venue_client_id == "cid-1"
assert events[0].venue_order_id == "1001"
assert len(events) == 2
assert events[1].kind.value == "FULL_FILL"
assert events[1].status == VenueEventStatus.FILLED
assert events[1].filled_size == pytest.approx(1.0)
assert events[1].remaining_size == pytest.approx(0.0)
def test_cancel_uses_bingx_cancel_surface_and_maps_cancel_ack() -> None:
cancel_row = {
"orderId": "2001",
"clientOrderId": "cid-2",
"clientOrderID": "cid-2",
"symbol": "BTC-USDT",
"status": "CANCELED",
}
backend = FakeBingxBackend(
snapshots=[
_snapshot(
open_orders=[cancel_row],
all_orders=[cancel_row],
),
_snapshot(),
],
cancel_response=cancel_row,
)
adapter = BingxVenueAdapter(backend=backend)
order = VenueOrder(
internal_trade_id="trade-2",
venue_order_id="2001",
venue_client_id="cid-2",
side=TradeSide.SHORT,
intended_size=1.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": 0, "asset": "BTCUSDT"},
)
events = adapter.cancel(order, reason="MANUAL_CLOSE")
assert backend.canceled
assert events[0].kind.value == "CANCEL_ACK"
assert events[0].status == VenueEventStatus.CANCELED
assert events[0].venue_order_id == "2001"
assert events[0].reason == "MANUAL_CLOSE"
def test_reconcile_and_open_views_normalize_bingx_rows() -> None:
ack_row = {
"orderId": "3001",
"clientOrderId": "cid-3",
"clientOrderID": "cid-3",
"symbol": "ETH-USDT",
"status": "NEW",
"executedQty": "0",
}
fill_row = {
"clientOrderId": "cid-3",
"clientOrderID": "cid-3",
"orderId": "3001",
"symbol": "ETH-USDT",
"status": "PARTIALLY_FILLED",
"executedQty": "2",
"lastFilledQty": "1",
"lastFillPrice": "2500",
}
position_row = {
"symbol": "ETH-USDT",
"positionSide": "LONG",
"positionAmt": "2",
"avgPrice": "2500",
"markPrice": "2510",
"leverage": "3",
}
backend = FakeBingxBackend(
snapshots=[
_snapshot(
positions=[position_row],
open_orders=[ack_row],
all_orders=[ack_row, fill_row],
all_fills=[fill_row],
)
]
)
adapter = BingxVenueAdapter(backend=backend)
orders = adapter.open_orders()
positions = adapter.open_positions()
events = adapter.reconcile()
assert orders[0].status == VenueOrderStatus.NEW
assert orders[0].venue_client_id == "cid-3"
assert positions[0]["positionAmt"] == "2"
assert any(event.kind.value == "PARTIAL_FILL" for event in events)
assert any(event.kind.value == "ORDER_ACK" for event in events)
def test_kernel_can_drive_through_bingx_venue_shim() -> None:
ack_row = {
"orderId": "4001",
"clientOrderId": "cid-4",
"clientOrderID": "cid-4",
"symbol": "BTC-USDT",
"status": "NEW",
"executedQty": "0",
}
fill_row = {
"clientOrderId": "cid-4",
"clientOrderID": "cid-4",
"orderId": "4001",
"symbol": "BTC-USDT",
"status": "FILLED",
"executedQty": "1",
"lastFilledQty": "1",
"lastFillPrice": "75000",
}
backend = FakeBingxBackend(
snapshots=[
_snapshot(),
_snapshot(
positions=[
{
"symbol": "BTC-USDT",
"positionSide": "SHORT",
"positionAmt": "-1",
"avgPrice": "75000",
"markPrice": "75010",
"leverage": "2",
}
],
open_orders=[ack_row],
all_orders=[ack_row],
all_fills=[fill_row],
),
],
receipt=ExecutionReceipt(
timestamp=datetime.now(timezone.utc),
status="NEW",
symbol="BTC-USDT",
side="SELL",
action="ENTER",
quantity=1.0,
price=75_000.0,
client_order_id="cid-4",
order_id="4001",
raw_ack=ack_row,
raw_state={},
),
)
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=BingxVenueAdapter(backend=backend),
)
outcome = kernel.process_intent(_intent(trade_id="trade-4"))
slot = kernel.slot(0)
assert outcome.accepted is True
assert slot.fsm_state == TradeStage.POSITION_OPEN
assert slot.trade_id == "trade-4"
assert backend.submitted
def test_submit_maps_bingx_rate_limit_to_first_class_venue_event() -> None:
backend = FakeBingxBackend(
snapshots=[_snapshot(), _snapshot()],
receipt=ExecutionReceipt(
timestamp=datetime.now(timezone.utc),
status="RATE_LIMITED",
symbol="BTC-USDT",
side="SELL",
action="ENTER",
quantity=1.0,
price=75_000.0,
client_order_id="cid-rate-limit",
order_id="",
raw_ack={
"status": "RATE_LIMITED",
"msg": "code:100410 endpoint is in disabled/frequency-limited period",
"retryAfter": int(datetime.now(timezone.utc).timestamp() * 1000) + 2_500,
},
raw_state={},
),
)
adapter = BingxVenueAdapter(backend=backend)
events = adapter.submit(_intent(trade_id="trade-rate-limit"))
assert len(events) == 1
assert events[0].kind == KernelEventKind.RATE_LIMITED
assert events[0].status == VenueEventStatus.RATE_LIMITED
assert events[0].venue_client_id == "cid-rate-limit"
assert events[0].metadata["retry_after_ms"] >= 0

View File

@@ -1,94 +0,0 @@
from __future__ import annotations
from uuid import uuid4
import os
import unittest
from prod.clean_arch.dita_v2 import (
BackendMode,
ControlUpdate,
InMemoryControlPlane,
ZincControlPlane,
KernelControlSnapshot,
KernelMode,
KernelVerbosity,
RealZincControlPlane,
build_control_plane,
)
from prod.clean_arch.dita_v2.real_control_plane import SharedRegion
HAS_REAL_ZINC = SharedRegion is not None
@unittest.skipUnless(HAS_REAL_ZINC, "Real Zinc adapter is unavailable")
class TestDITAv2RealControlPlane(unittest.TestCase):
def test_build_control_plane_defaults_to_zinc(self) -> None:
plane = build_control_plane()
self.assertIsInstance(plane, ZincControlPlane)
def test_roundtrip_update_and_read(self) -> None:
prefix = f"dita_v2_control_{uuid4().hex}"
writer = RealZincControlPlane(prefix=prefix, create=True)
reader = RealZincControlPlane(prefix=prefix, create=False)
try:
snapshot = writer.update(
ControlUpdate(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.BINGX,
trace_transitions=True,
mirror_to_hazelcast=True,
)
)
self.assertEqual(snapshot.mode, KernelMode.DEBUG)
self.assertEqual(snapshot.verbosity, KernelVerbosity.TRACE)
self.assertEqual(snapshot.backend_mode, BackendMode.BINGX)
self.assertTrue(snapshot.trace_transitions)
read_back = reader.read()
self.assertEqual(read_back.mode, KernelMode.DEBUG)
self.assertEqual(read_back.verbosity, KernelVerbosity.TRACE)
self.assertEqual(read_back.backend_mode, BackendMode.BINGX)
self.assertTrue(read_back.trace_transitions)
finally:
writer.close()
reader.close()
def test_env_can_select_real_control_plane(self) -> None:
prefix = f"dita_v2_control_{uuid4().hex}"
previous = os.environ.get("DITA_V2_CONTROL_PLANE")
os.environ["DITA_V2_CONTROL_PLANE"] = "REAL_ZINC"
try:
plane = build_control_plane(prefix=prefix)
self.assertIsInstance(plane, RealZincControlPlane)
if isinstance(plane, RealZincControlPlane):
plane.close()
finally:
if previous is None:
os.environ.pop("DITA_V2_CONTROL_PLANE", None)
else:
os.environ["DITA_V2_CONTROL_PLANE"] = previous
def test_initial_snapshot_is_default(self) -> None:
prefix = f"dita_v2_control_{uuid4().hex}"
plane = RealZincControlPlane(prefix=prefix, create=True)
try:
snapshot = plane.read()
self.assertEqual(snapshot, KernelControlSnapshot())
finally:
plane.close()
class TestDITAv2InMemoryControlPlane(unittest.TestCase):
def test_wait_and_notify(self) -> None:
plane = InMemoryControlPlane()
self.assertFalse(plane.wait(timeout_ms=1))
plane.notify()
self.assertTrue(plane.wait(timeout_ms=1))
snapshot = plane.update(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE))
self.assertEqual(snapshot.mode, KernelMode.DEBUG)
self.assertEqual(snapshot.verbosity, KernelVerbosity.TRACE)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,37 +0,0 @@
from __future__ import annotations
from pathlib import Path
import unittest
class TestDITAv2Docs(unittest.TestCase):
def test_kernel_reference_exists(self) -> None:
text = Path("/mnt/dolphinng5_predict/prod/docs/DITA_V2_KERNEL_REFERENCE.md").read_text()
self.assertIn("# DITAv2 Kernel Reference", text)
self.assertIn("dolphin:dita_v2", text)
self.assertIn("prod/clean_arch/dita_v2/rust_backend.py", text)
self.assertIn("write-through", text)
self.assertIn("notify/wait", text)
self.assertIn("50 collected cases", text)
self.assertIn("full-stack E2E / functional tests", text)
self.assertIn("mocked exchange-first and BingX-basic E2E paths", text)
self.assertIn("KernelSeverity.WARNING", text)
self.assertIn("release_eta", text)
self.assertIn("retryable", text)
self.assertIn("dita_v2_live_bingx_smoke.py", text)
self.assertIn("--dry-run", text)
def test_system_bible_points_to_dita_v2_reference(self) -> None:
bible = Path("/mnt/dolphinng5_predict/prod/docs/SYSTEM_BIBLE_v7.md").read_text()
self.assertIn("DITA_V2_KERNEL_REFERENCE.md", bible)
self.assertIn("DITAv2 execution/launcher/operator surface", bible)
self.assertIn("write-through Zinc mirror semantics", bible)
self.assertIn("one-shot notify/wait signal contract", bible)
self.assertIn("full-stack DITAv2 E2E/functional matrix", bible)
self.assertIn("retryable transient throttling", bible)
self.assertIn("dita_v2_live_bingx_smoke.py", bible)
self.assertIn("--dry-run", bible)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,907 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
import random
from typing import Any, Callable, Iterable, Optional, Sequence
import pytest
from prod.clean_arch.dita_v2 import (
BingxVenueAdapter,
BackendMode,
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelControlSnapshot,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelMode,
KernelVerbosity,
TradeSide,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
from prod.clean_arch.ports.execution import ExchangeStateSnapshot, ExecutionReceipt
def _norm_symbol(symbol: str) -> str:
return str(symbol or "").replace("-", "").replace("_", "").upper()
def _snapshot(
*,
capital: float = 25_000.0,
positions: list[dict[str, Any]] | None = None,
open_orders: list[dict[str, Any]] | None = None,
all_orders: list[dict[str, Any]] | None = None,
all_fills: list[dict[str, Any]] | None = None,
source: str = "bingx",
recovered: bool = False,
) -> ExchangeStateSnapshot:
position_map = {
_norm_symbol(str(row.get("symbol", ""))): dict(row)
for row in (positions or [])
if _norm_symbol(str(row.get("symbol", "")))
}
return ExchangeStateSnapshot(
timestamp=datetime.now(timezone.utc),
capital=capital,
equity=capital,
open_positions=position_map,
open_orders=[dict(row) for row in (open_orders or [])],
all_orders=[dict(row) for row in (all_orders or [])],
all_fills=[dict(row) for row in (all_fills or [])],
account={"balances": [{"asset": "USDT", "total": capital}]},
open_notional=0.0,
source=source,
recovered=recovered,
)
def _sign(side: TradeSide) -> int:
return -1 if side == TradeSide.SHORT else 1
def _position_row(asset: str, side: TradeSide, qty: float, price: float) -> dict[str, Any]:
signed_qty = _sign(side) * abs(float(qty))
return {
"symbol": asset,
"positionSide": side.value,
"positionAmt": f"{signed_qty}",
"avgPrice": f"{price}",
"markPrice": f"{price}",
"leverage": "2",
}
@dataclass(frozen=True)
class VenueScriptStep:
name: str
submit_kind: str
fill_ratio: float = 0.0
cancel_kind: str = "cancel_ack"
submit_advances: bool = True
cancel_advances: bool = True
reject_reason: str = "MOCK_REJECT"
cancel_reason: str = "MOCK_CANCEL"
@dataclass(frozen=True)
class SignalAction:
kind: str
price: float
target_size: float = 0.0
fill_ratio: float = 1.0
reason: str = ""
require_close: bool = False
class ScriptedVenueAdapter:
"""Deterministic venue adapter that plays scripted submit/cancel outcomes."""
def __init__(self, steps: Sequence[VenueScriptStep]) -> None:
self.steps = list(steps)
self._step_index = 0
self._active_step_index = 0
self._order_seq = 1
self._event_seq = 1
self._open_orders: dict[str, VenueOrder] = {}
self._open_positions: dict[str, dict[str, Any]] = {}
self.calls: list[tuple[str, Any]] = []
def _next_step(self) -> VenueScriptStep:
if self._step_index < len(self.steps):
step = self.steps[self._step_index]
self._step_index += 1
return step
return VenueScriptStep(name="default", submit_kind="ack_only")
def submit(self, intent: KernelIntent) -> list[VenueEvent]:
self.calls.append(("submit", intent.action.value, intent.trade_id, intent.slot_id))
step = self._next_step()
self._active_step_index = max(0, self._step_index - 1)
order_id = f"MOCK-{self._order_seq:08d}"
self._order_seq += 1
client_id = f"{intent.trade_id}:{intent.intent_id}"
order = VenueOrder(
internal_trade_id=intent.trade_id,
venue_order_id=order_id,
venue_client_id=client_id,
side=intent.side,
intended_size=float(intent.target_size),
filled_size=0.0,
average_fill_price=float(intent.reference_price or 0.0),
status=VenueOrderStatus.NEW,
metadata={"slot_id": intent.slot_id, "asset": intent.asset, "action": intent.action.value},
)
if step.submit_kind == "entry_reject":
return [
self._event(
intent=intent,
order=order,
kind=KernelEventKind.ORDER_REJECT,
status=VenueEventStatus.REJECTED,
reason=step.reject_reason,
)
]
ack = self._event(
intent=intent,
order=order,
kind=KernelEventKind.ORDER_ACK,
status=VenueEventStatus.ACKED,
)
self._open_orders[order_id] = order
events = [ack]
if step.submit_kind in {"entry_partial", "exit_partial", "entry_full", "exit_full"}:
fill_ratio = max(0.0, min(1.0, float(step.fill_ratio or 0.0)))
if fill_ratio <= 0.0:
fill_ratio = 1.0 if step.submit_kind.endswith("full") else 0.5
fill_size = float(intent.target_size) * fill_ratio
fill_kind = KernelEventKind.FULL_FILL if fill_ratio >= 1.0 else KernelEventKind.PARTIAL_FILL
fill_status = VenueEventStatus.FILLED if fill_kind == KernelEventKind.FULL_FILL else VenueEventStatus.PARTIALLY_FILLED
events.append(
self._event(
intent=intent,
order=order,
kind=fill_kind,
status=fill_status,
price=float(intent.reference_price or 0.0),
filled_size=fill_size,
remaining_size=max(0.0, float(intent.target_size) - fill_size),
)
)
self._apply_fill(intent, fill_size, fill_kind == KernelEventKind.FULL_FILL)
if fill_kind == KernelEventKind.FULL_FILL:
self._open_orders.pop(order_id, None)
return events
def cancel(self, order: VenueOrder, *, reason: str = "") -> list[VenueEvent]:
self.calls.append(("cancel", order.venue_order_id, reason))
step = self.steps[min(self._active_step_index, len(self.steps) - 1)] if self.steps else VenueScriptStep(name="default", submit_kind="ack_only")
if step.cancel_kind == "cancel_reject":
return [
self._event(
intent=self._intent_from_order(order),
order=order,
kind=KernelEventKind.CANCEL_REJECT,
status=VenueEventStatus.CANCELED_REJECTED,
reason=step.cancel_reason,
)
]
self._open_orders.pop(order.venue_order_id, None)
if step.cancel_advances:
self._step_index = max(self._step_index, self._active_step_index + 1)
return [
self._event(
intent=self._intent_from_order(order),
order=order,
kind=KernelEventKind.CANCEL_ACK,
status=VenueEventStatus.CANCELED,
reason=reason or step.cancel_reason,
)
]
def open_orders(self) -> list[VenueOrder]:
return list(self._open_orders.values())
def open_positions(self) -> list[dict[str, Any]]:
return list(self._open_positions.values())
def reconcile(self) -> list[VenueEvent]:
events: list[VenueEvent] = []
for order in self._open_orders.values():
events.append(
self._event(
intent=self._intent_from_order(order),
order=order,
kind=KernelEventKind.ORDER_ACK,
status=VenueEventStatus.ACKED,
reason="RECONCILE",
)
)
for row in self._open_positions.values():
events.append(
VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"EV-{self._event_seq:08d}",
trade_id=str(row.get("trade_id", "")),
slot_id=int(row.get("slot_id", 0)),
kind=KernelEventKind.RECONCILE,
status=VenueEventStatus.ACKED,
venue_order_id=str(row.get("venue_order_id", "")),
venue_client_id=str(row.get("venue_client_id", "")),
side=TradeSide(str(row.get("side", TradeSide.FLAT.value))),
asset=str(row.get("symbol", "")),
price=float(row.get("avgPrice", 0.0)),
size=abs(float(row.get("positionAmt", 0.0))),
filled_size=abs(float(row.get("positionAmt", 0.0))),
remaining_size=0.0,
reason="RECONCILE",
raw_payload=dict(row),
metadata={"source": "mock"},
)
)
self._event_seq += 1
return events
def _event(
self,
*,
intent: KernelIntent,
order: VenueOrder,
kind: KernelEventKind,
status: VenueEventStatus,
price: float | None = None,
filled_size: float = 0.0,
remaining_size: float = 0.0,
reason: str = "",
) -> VenueEvent:
event = VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"EV-{self._event_seq:08d}",
trade_id=intent.trade_id,
slot_id=intent.slot_id,
kind=kind,
status=status,
venue_order_id=order.venue_order_id,
venue_client_id=order.venue_client_id,
side=order.side,
asset=intent.asset,
price=float(price if price is not None else intent.reference_price or 0.0),
size=float(intent.target_size or 0.0),
filled_size=float(filled_size),
remaining_size=float(remaining_size),
reason=reason,
raw_payload={
"status": status.value,
"orderId": order.venue_order_id,
"clientOrderId": order.venue_client_id,
"symbol": intent.asset,
"side": order.side.value,
"action": intent.action.value,
},
metadata={"intent_id": intent.intent_id, "action": intent.action.value},
)
self._event_seq += 1
return event
def _intent_from_order(self, order: VenueOrder) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=order.venue_client_id,
trade_id=order.internal_trade_id,
slot_id=int(order.metadata.get("slot_id", 0)),
asset=str(order.metadata.get("asset", "")),
side=order.side,
action=KernelCommandType.EXIT if order.metadata.get("action") == "EXIT" else KernelCommandType.ENTER,
reference_price=float(order.average_fill_price or 0.0),
target_size=float(order.intended_size or 0.0),
leverage=2.0,
reason=str(order.metadata.get("action", "")),
)
def _apply_fill(self, intent: KernelIntent, filled_size: float, full: bool) -> None:
signed = _sign(intent.side) * abs(float(filled_size))
row = self._open_positions.get(intent.asset)
if intent.action == KernelCommandType.ENTER:
self._open_positions[intent.asset] = {
"symbol": intent.asset,
"trade_id": intent.trade_id,
"slot_id": intent.slot_id,
"side": intent.side.value,
"positionSide": intent.side.value,
"positionAmt": f"{signed}",
"avgPrice": f"{intent.reference_price}",
"markPrice": f"{intent.reference_price}",
"venue_order_id": f"MOCK-{self._order_seq - 1:08d}",
"venue_client_id": f"{intent.trade_id}:{intent.intent_id}",
}
return
if row is None:
return
current = abs(float(row.get("positionAmt", 0.0)))
new_qty = max(0.0, current - abs(float(filled_size)))
if new_qty <= 1e-12 or full:
self._open_positions.pop(intent.asset, None)
return
row["positionAmt"] = f"{_sign(intent.side) * new_qty}"
self._open_positions[intent.asset] = row
@dataclass(frozen=True)
class BingxE2EStep:
name: str
submit_kind: str
submit_fill_ratio: float
before_snapshot: ExchangeStateSnapshot
after_snapshot: ExchangeStateSnapshot
receipt: ExecutionReceipt
submit_advances: bool = True
cancel_kind: str = "cancel_ack"
cancel_advances: bool = True
cancel_before_snapshot: ExchangeStateSnapshot | None = None
cancel_after_snapshot: ExchangeStateSnapshot | None = None
class BingxE2EBackend:
"""Stateful fake backend that drives the real BingxVenueAdapter."""
def __init__(self, steps: Sequence[BingxE2EStep]) -> None:
self.steps = list(steps)
self.index = 0
self.calls: list[tuple[str, Any]] = []
self.connected = False
self._operation: str | None = None
self._active_index = 0
async def connect(self) -> bool:
self.connected = True
self.calls.append(("connect", None))
return True
async def disconnect(self) -> None:
self.connected = False
self.calls.append(("disconnect", None))
async def refresh_state(self, symbol: str | None = None, *, include_history: bool = False) -> ExchangeStateSnapshot:
self.calls.append(("refresh_state", symbol, include_history, self.index, self._operation))
step = self.steps[min(self._active_index, len(self.steps) - 1)]
if self._operation == "submit":
snapshot = step.after_snapshot
if step.submit_advances:
self.index = min(self.index + 1, len(self.steps) - 1)
self._operation = None
return snapshot
if self._operation == "cancel":
snapshot = step.cancel_after_snapshot or step.after_snapshot
if step.cancel_advances:
self.index = min(self.index + 1, len(self.steps) - 1)
self._operation = None
return snapshot
return step.before_snapshot
async def submit_intent(self, legacy_intent: Any) -> ExecutionReceipt:
self.calls.append(("submit_intent", legacy_intent.trade_id, legacy_intent.action.value))
self._active_index = min(self.index, len(self.steps) - 1)
step = self.steps[self._active_index]
self._operation = "submit"
if step.submit_kind == "reject":
return ExecutionReceipt(
timestamp=datetime.now(timezone.utc),
status="REJECTED",
symbol=legacy_intent.asset,
side=legacy_intent.side.value,
action=legacy_intent.action.value,
quantity=float(legacy_intent.target_size),
price=float(legacy_intent.reference_price),
client_order_id=step.receipt.client_order_id,
order_id=step.receipt.order_id,
raw_ack={"status": "REJECTED", "msg": "E2E_REJECT"},
raw_state={},
)
return step.receipt
async def cancel_order(self, order: VenueOrder, *, reason: str = "") -> dict[str, Any]:
self.calls.append(("cancel_order", order.venue_order_id, reason))
self._operation = "cancel"
step = self.steps[min(self._active_index, len(self.steps) - 1)]
if step.cancel_kind == "cancel_reject":
return {"status": "CANCEL_REJECTED", "msg": reason or "E2E_CANCEL_REJECT"}
return {"status": "CANCELED", "msg": reason or "E2E_CANCEL_ACK"}
def _kernel(venue: Any, *, zinc: Any | None = None) -> ExecutionKernel:
return ExecutionKernel(
max_slots=1,
control_plane=InMemoryControlPlane(
KernelControlSnapshot(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.MOCK if not isinstance(venue, BingxVenueAdapter) else BackendMode.BINGX,
trace_transitions=True,
debug_clickhouse_enabled=True,
mirror_to_hazelcast=True,
)
),
venue=venue,
zinc_plane=zinc or InMemoryZincPlane(),
)
def _intent(
*,
action: KernelCommandType,
trade_id: str,
side: TradeSide,
slot_id: int = 0,
target_size: float = 1.0,
price: float = 100.0,
exit_leg_ratios: Sequence[float] = (1.0,),
reason: str = "E2E",
) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:{action.value}:{slot_id}:{reason}",
trade_id=trade_id,
slot_id=slot_id,
asset="BTCUSDT",
side=side,
action=action,
reference_price=price,
target_size=target_size,
leverage=2.0,
exit_leg_ratios=tuple(exit_leg_ratios),
reason=reason,
)
def _entry_event(trade_id: str, slot_id: int, side: TradeSide, target_size: float, price: float, *, partial: bool = False, ratio: float = 1.0) -> list[VenueEvent]:
order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id=f"{trade_id}-entry-oid",
venue_client_id=f"{trade_id}:entry",
side=side,
intended_size=target_size,
filled_size=0.0,
average_fill_price=price,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot_id, "asset": "BTCUSDT", "action": "ENTER"},
)
events = [
VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"{trade_id}-ack",
trade_id=trade_id,
slot_id=slot_id,
kind=KernelEventKind.ORDER_ACK,
status=VenueEventStatus.ACKED,
venue_order_id=order.venue_order_id,
venue_client_id=order.venue_client_id,
side=side,
asset="BTCUSDT",
price=price,
size=target_size,
filled_size=0.0,
remaining_size=target_size,
)
]
if partial:
fill_size = target_size * ratio
events.append(
VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"{trade_id}-fill",
trade_id=trade_id,
slot_id=slot_id,
kind=KernelEventKind.PARTIAL_FILL,
status=VenueEventStatus.PARTIALLY_FILLED,
venue_order_id=order.venue_order_id,
venue_client_id=order.venue_client_id,
side=side,
asset="BTCUSDT",
price=price,
size=target_size,
filled_size=fill_size,
remaining_size=max(0.0, target_size - fill_size),
)
)
else:
events.append(
VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"{trade_id}-fill",
trade_id=trade_id,
slot_id=slot_id,
kind=KernelEventKind.FULL_FILL,
status=VenueEventStatus.FILLED,
venue_order_id=order.venue_order_id,
venue_client_id=order.venue_client_id,
side=side,
asset="BTCUSDT",
price=price,
size=target_size,
filled_size=target_size,
remaining_size=0.0,
)
)
return events
def _close_and_mark(kernel: ExecutionKernel, *, trade_id: str, side: TradeSide, exit_size: float, price: float, reason: str) -> None:
kernel.process_intent(_intent(action=KernelCommandType.EXIT, trade_id=trade_id, side=side, target_size=exit_size, price=price, exit_leg_ratios=(0.5, 0.5), reason=reason))
def _assert_full_cycle(kernel: ExecutionKernel, *, side: TradeSide, trade_id: str, expect_closed: bool = True) -> None:
slot = kernel.slot(0)
assert slot.trade_id == trade_id
if expect_closed:
assert slot.closed is True
assert slot.fsm_state in {TradeStage.CLOSED, TradeStage.IDLE}
assert kernel.account.snapshot.open_positions in {0, 1}
def _bingx_steps_for_cycle(side: TradeSide, *, hung_exit: bool = False, cancel_reject: bool = False) -> list[BingxE2EStep]:
entry_receipt = ExecutionReceipt(
timestamp=datetime.now(timezone.utc),
status="NEW",
symbol="BTC-USDT",
side=side.value,
action="ENTER",
quantity=1.0,
price=75_000.0,
client_order_id="cid-entry",
order_id="oid-entry",
raw_ack={
"orderId": "oid-entry",
"clientOrderId": "cid-entry",
"status": "NEW",
"symbol": "BTC-USDT",
"executedQty": "0",
},
raw_state={},
)
exit_ack_status = "NEW" if hung_exit or cancel_reject else "FILLED"
exit_filled_qty = 0.0 if hung_exit or cancel_reject else 0.5
exit_receipt = ExecutionReceipt(
timestamp=datetime.now(timezone.utc),
status=exit_ack_status,
symbol="BTC-USDT",
side=("SELL" if side == TradeSide.SHORT else "BUY"),
action="EXIT",
quantity=0.5,
price=74_900.0 if side == TradeSide.SHORT else 75_100.0,
client_order_id="cid-exit-1",
order_id="oid-exit-1",
raw_ack={
"orderId": "oid-exit-1",
"clientOrderId": "cid-exit-1",
"status": exit_ack_status,
"symbol": "BTC-USDT",
"executedQty": f"{exit_filled_qty}",
"cumFilledQty": f"{exit_filled_qty}",
"avgPrice": "74900" if side == TradeSide.SHORT else "75100",
},
raw_state={},
)
final_receipt = ExecutionReceipt(
timestamp=datetime.now(timezone.utc),
status="FILLED",
symbol="BTC-USDT",
side=("SELL" if side == TradeSide.SHORT else "BUY"),
action="EXIT",
quantity=0.5,
price=74_850.0 if side == TradeSide.SHORT else 75_150.0,
client_order_id="cid-exit-2",
order_id="oid-exit-2",
raw_ack={
"orderId": "oid-exit-2",
"clientOrderId": "cid-exit-2",
"status": "FILLED",
"symbol": "BTC-USDT",
"executedQty": "0.5",
"cumFilledQty": "0.5",
"avgPrice": "74850" if side == TradeSide.SHORT else "75150",
},
raw_state={},
)
entry_before = _snapshot()
entry_after = _snapshot(
positions=[_position_row("BTC-USDT", side, 1.0, 75_000.0)],
open_orders=[
{
"symbol": "BTC-USDT",
"clientOrderId": "cid-entry",
"clientOrderID": "cid-entry",
"orderId": "oid-entry",
"status": "FILLED",
"origQty": "1",
"executedQty": "1",
"avgPrice": "75000",
}
],
all_orders=[{"symbol": "BTC-USDT", "clientOrderId": "cid-entry", "clientOrderID": "cid-entry", "orderId": "oid-entry", "status": "FILLED"}],
all_fills=[{"symbol": "BTC-USDT", "clientOrderId": "cid-entry", "clientOrderID": "cid-entry", "orderId": "oid-entry", "status": "FILLED", "executedQty": "1", "lastFilledQty": "1", "lastFillPrice": "75000"}],
)
exit_before = entry_after
cancel_open_positions = [_position_row("BTC-USDT", side, 1.0, 75_000.0)] if (hung_exit or cancel_reject) else []
cancel_open_orders = [
{
"symbol": "BTC-USDT",
"clientOrderId": "cid-exit-1",
"clientOrderID": "cid-exit-1",
"orderId": "oid-exit-1",
"status": exit_ack_status,
"origQty": "0.5",
"executedQty": "0",
"avgPrice": "74900",
}
] if (hung_exit or cancel_reject) else []
exit_after = _snapshot(
positions=cancel_open_positions,
open_orders=cancel_open_orders,
all_orders=[{"symbol": "BTC-USDT", "clientOrderId": "cid-exit-1", "clientOrderID": "cid-exit-1", "orderId": "oid-exit-1", "status": exit_ack_status}],
all_fills=[{"symbol": "BTC-USDT", "clientOrderId": "cid-exit-1", "clientOrderID": "cid-exit-1", "orderId": "oid-exit-1", "status": exit_ack_status, "executedQty": f"{exit_filled_qty}", "lastFilledQty": f"{exit_filled_qty}", "lastFillPrice": "74900"}] if exit_filled_qty > 0 else [],
)
cancel_after = _snapshot(
positions=cancel_open_positions,
open_orders=[],
all_orders=[{"symbol": "BTC-USDT", "clientOrderId": "cid-exit-1", "clientOrderID": "cid-exit-1", "orderId": "oid-exit-1", "status": "CANCELED"}],
all_fills=[],
)
cancel_before = exit_after
cancel_kind = "cancel_reject" if cancel_reject else "cancel_ack"
final_before = cancel_after if (hung_exit or cancel_reject) else exit_after
final_after = _snapshot(
positions=[_position_row("BTC-USDT", side, 0.5, 74_900.0 if side == TradeSide.SHORT else 75_100.0)] if (hung_exit or cancel_reject) else [],
open_orders=[],
all_orders=[{"symbol": "BTC-USDT", "clientOrderId": "cid-exit-2", "clientOrderID": "cid-exit-2", "orderId": "oid-exit-2", "status": "FILLED"}],
all_fills=[{"symbol": "BTC-USDT", "clientOrderId": "cid-exit-2", "clientOrderID": "cid-exit-2", "orderId": "oid-exit-2", "status": "FILLED", "executedQty": "0.5", "lastFilledQty": "0.5", "lastFillPrice": "74850" if side == TradeSide.SHORT else "75150"}] if (hung_exit or cancel_reject) else [],
)
return [
BingxE2EStep("entry", "fill", 1.0, entry_before, entry_after, entry_receipt),
BingxE2EStep(
"exit_hang" if hung_exit else "exit_1",
"fill" if not hung_exit else "ack_only",
0.5 if not hung_exit else 0.0,
exit_before,
exit_after,
exit_receipt,
submit_advances=not (hung_exit or cancel_reject),
cancel_kind=cancel_kind,
cancel_before_snapshot=cancel_before,
cancel_after_snapshot=cancel_after,
cancel_advances=True,
),
BingxE2EStep("exit_2", "fill", 1.0, final_before, final_after, final_receipt),
]
def _run_signal_plan(kernel: ExecutionKernel, side: TradeSide, plan: Sequence[SignalAction]) -> ExecutionKernel:
trade_id = f"signal-{side.value.lower()}"
for step in plan:
if step.kind == "entry":
kernel.process_intent(_intent(action=KernelCommandType.ENTER, trade_id=trade_id, side=side, target_size=1.0, price=75_000.0, reason=step.reason or "ENTRY"))
elif step.kind == "mark":
kernel.process_intent(_intent(action=KernelCommandType.MARK_PRICE, trade_id=trade_id, side=side, target_size=1.0, price=step.price, reason=step.reason or "MARK"))
elif step.kind == "exit":
kernel.process_intent(_intent(action=KernelCommandType.EXIT, trade_id=trade_id, side=side, target_size=step.target_size, price=step.price, exit_leg_ratios=(0.5, 0.5), reason=step.reason or "EXIT"))
elif step.kind == "cancel":
slot = kernel.slot(0)
if step.require_close:
active_order = slot.active_exit_order
if active_order is None:
fallback_client_id = f"{trade_id}:{step.reason or 'CANCEL'}:{slot.slot_id}"
active_order = VenueOrder(
internal_trade_id=slot.trade_id or trade_id,
venue_order_id=str(slot.active_entry_order.venue_order_id if slot.active_entry_order else fallback_client_id),
venue_client_id=str(slot.active_entry_order.venue_client_id if slot.active_entry_order else fallback_client_id),
side=slot.side,
intended_size=float(slot.active_exit_order.intended_size if slot.active_exit_order else max(slot.size, step.target_size or slot.size or 0.0)),
filled_size=0.0,
average_fill_price=float(step.price),
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id, "asset": slot.asset, "action": "EXIT"},
)
emitted = kernel.venue.cancel(active_order, reason=step.reason or "CANCEL")
for event in emitted:
kernel.on_venue_event(event)
elif step.kind == "reconcile":
kernel.process_intent(_intent(action=KernelCommandType.RECONCILE, trade_id=trade_id, side=side, target_size=1.0, price=step.price, reason=step.reason or "RECONCILE"))
else:
raise AssertionError(step.kind)
return kernel
MOCK_SIGNAL_CASES = [
(
"short_full_gamut",
TradeSide.SHORT,
[
VenueScriptStep("entry", "entry_full"),
VenueScriptStep("exit_tp1", "exit_partial", fill_ratio=0.5),
VenueScriptStep("exit_tp2", "exit_full", fill_ratio=1.0),
],
[
SignalAction("entry", 75_000.0, reason="ENTRY"),
SignalAction("mark", 74_200.0, reason="PUMP_BREAK"),
SignalAction("exit", 74_900.0, target_size=0.5, reason="TP1"),
SignalAction("mark", 74_100.0, reason="TRAIL"),
SignalAction("exit", 74_800.0, target_size=0.5, reason="TP2"),
],
),
(
"long_full_gamut",
TradeSide.LONG,
[
VenueScriptStep("entry", "entry_full"),
VenueScriptStep("exit_tp1", "exit_partial", fill_ratio=0.5),
VenueScriptStep("exit_tp2", "exit_full", fill_ratio=1.0),
],
[
SignalAction("entry", 75_000.0, reason="ENTRY"),
SignalAction("mark", 75_800.0, reason="RALLY"),
SignalAction("exit", 75_100.0, target_size=0.5, reason="TP1"),
SignalAction("mark", 75_900.0, reason="TRAIL"),
SignalAction("exit", 75_200.0, target_size=0.5, reason="TP2"),
],
),
(
"hung_exit_then_cancel",
TradeSide.SHORT,
[
VenueScriptStep("entry", "entry_full"),
VenueScriptStep("hung_exit", "ack_only", submit_advances=False, cancel_kind="cancel_ack", cancel_advances=True),
VenueScriptStep("exit_after_cancel", "exit_full", fill_ratio=1.0),
],
[
SignalAction("entry", 75_000.0, reason="ENTRY"),
SignalAction("mark", 74_300.0, reason="HANG"),
SignalAction("exit", 74_950.0, target_size=0.5, reason="HUNG_TP"),
SignalAction("cancel", 74_950.0, reason="CANCEL_HUNG", require_close=True),
SignalAction("exit", 74_700.0, target_size=0.5, reason="RESUME_TP"),
],
),
(
"cancel_reject_then_fill",
TradeSide.SHORT,
[
VenueScriptStep("entry", "entry_full"),
VenueScriptStep("hung_exit", "ack_only", submit_advances=False, cancel_kind="cancel_reject", cancel_advances=False),
VenueScriptStep("exit_after_reject", "exit_full", fill_ratio=1.0),
],
[
SignalAction("entry", 75_000.0, reason="ENTRY"),
SignalAction("mark", 74_250.0, reason="HANG"),
SignalAction("exit", 74_950.0, target_size=0.5, reason="HUNG_TP"),
SignalAction("cancel", 74_950.0, reason="CANCEL_REJECT", require_close=True),
SignalAction("exit", 74_650.0, target_size=0.5, reason="FINAL_TP"),
],
),
]
@pytest.mark.parametrize("name,side,steps,plan", MOCK_SIGNAL_CASES, ids=[case[0] for case in MOCK_SIGNAL_CASES])
def test_mock_signal_gamut_e2e_matrix(name: str, side: TradeSide, steps: Sequence[VenueScriptStep], plan: Sequence[SignalAction]) -> None:
venue = ScriptedVenueAdapter(steps)
kernel = _kernel(venue)
kernel.update_control(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE))
_run_signal_plan(kernel, side, plan)
slot = kernel.slot(0)
assert slot.trade_id == f"signal-{side.value.lower()}"
assert venue.calls[0][0] == "submit"
expected_cancel = any(step.kind == "cancel" and step.require_close for step in plan)
assert any(call[0] == "cancel" for call in venue.calls) == expected_cancel
assert kernel.snapshot()["control"]["mode"] == KernelMode.DEBUG.value
if name in {"short_full_gamut", "long_full_gamut", "hung_exit_then_cancel", "cancel_reject_then_fill"}:
assert slot.fsm_state in {TradeStage.CLOSED, TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING}
if name == "hung_exit_then_cancel":
assert any(call[0] == "cancel" for call in venue.calls)
assert slot.closed is True or slot.fsm_state == TradeStage.POSITION_OPEN
def _bingx_backend_for_plan(side: TradeSide, *, hung_exit: bool = False, cancel_reject: bool = False) -> BingxE2EBackend:
return BingxE2EBackend(_bingx_steps_for_cycle(side, hung_exit=hung_exit, cancel_reject=cancel_reject))
@pytest.mark.parametrize(
"side,hung_exit,cancel_reject",
[
(TradeSide.SHORT, False, False),
(TradeSide.LONG, False, False),
(TradeSide.SHORT, True, False),
(TradeSide.SHORT, True, True),
],
ids=["short_full", "long_full", "short_hung", "short_cancel_reject"],
)
def test_bingx_basic_e2e_matrix(side: TradeSide, hung_exit: bool, cancel_reject: bool) -> None:
backend = _bingx_backend_for_plan(side, hung_exit=hung_exit, cancel_reject=cancel_reject)
venue = BingxVenueAdapter(backend=backend)
kernel = _kernel(venue)
kernel.update_control(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE, backend_mode=BackendMode.BINGX))
_run_signal_plan(
kernel,
side,
[
SignalAction("entry", 75_000.0, reason="ENTRY"),
SignalAction("mark", 74_200.0 if side == TradeSide.SHORT else 75_800.0, reason="MARK"),
SignalAction("exit", 74_900.0 if side == TradeSide.SHORT else 75_100.0, target_size=0.5, reason="TP1"),
SignalAction("cancel", 74_900.0 if side == TradeSide.SHORT else 75_100.0, reason="CANCEL" if hung_exit or cancel_reject else "NO_CANCEL", require_close=hung_exit or cancel_reject),
SignalAction("exit", 74_850.0 if side == TradeSide.SHORT else 75_150.0, target_size=0.5, reason="TP2"),
],
)
slot = kernel.slot(0)
assert backend.connected is False
assert any(call[0] == "submit_intent" for call in backend.calls)
assert slot.trade_id.startswith("signal-")
assert slot.fsm_state in {TradeStage.CLOSED, TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING}
if not hung_exit:
assert slot.closed is True
else:
assert any(call[0] == "cancel_order" for call in backend.calls)
FUZZ_SEEDS = tuple(range(12))
FUZZ_VENUES = ("mock", "bingx")
@pytest.mark.parametrize("seed", FUZZ_SEEDS, ids=lambda seed: f"seed-{seed}")
@pytest.mark.parametrize("venue_kind", FUZZ_VENUES, ids=lambda venue_kind: f"venue-{venue_kind}")
def test_e2e_chaos_fuzz_matrix(seed: int, venue_kind: str) -> None:
rng = random.Random(20260527 + seed)
side = rng.choice([TradeSide.SHORT, TradeSide.LONG])
if venue_kind == "mock":
steps = [
VenueScriptStep("entry", "entry_full" if rng.random() > 0.2 else "entry_partial", fill_ratio=1.0 if rng.random() > 0.5 else 0.5),
VenueScriptStep("exit", "ack_only" if rng.random() > 0.35 else "exit_partial", fill_ratio=0.5, cancel_kind="cancel_reject" if rng.random() > 0.75 else "cancel_ack", submit_advances=False if rng.random() > 0.35 else True),
VenueScriptStep("exit2", "exit_full", fill_ratio=1.0),
]
venue = ScriptedVenueAdapter(steps)
kernel = _kernel(venue)
else:
backend = _bingx_backend_for_plan(side, hung_exit=rng.random() > 0.4, cancel_reject=rng.random() > 0.7)
venue = BingxVenueAdapter(backend=backend)
kernel = _kernel(venue)
kernel.update_control(ControlUpdate(backend_mode=BackendMode.BINGX))
trade_id = f"fuzz-{venue_kind}-{seed}"
kernel.process_intent(_intent(action=KernelCommandType.ENTER, trade_id=trade_id, side=side, target_size=1.0, price=75_000.0, reason="ENTER"))
for idx in range(rng.randint(2, 5)):
op = rng.choice(["mark", "exit", "cancel", "reconcile"])
slot = kernel.slot(0)
if op == "mark":
kernel.process_intent(_intent(action=KernelCommandType.MARK_PRICE, trade_id=trade_id, side=side, target_size=1.0, price=74_000.0 if side == TradeSide.SHORT else 76_000.0, reason=f"MARK-{idx}"))
elif op == "exit" and slot.fsm_state in {TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING}:
kernel.process_intent(_intent(action=KernelCommandType.EXIT, trade_id=trade_id, side=side, target_size=max(0.1, slot.size or 0.5), price=74_900.0 if side == TradeSide.SHORT else 75_100.0, exit_leg_ratios=(0.5, 0.5), reason=f"EXIT-{idx}"))
elif op == "cancel" and slot.active_exit_order is not None:
kernel.process_intent(_intent(action=KernelCommandType.CANCEL, trade_id=trade_id, side=side, target_size=slot.active_exit_order.intended_size, price=74_900.0 if side == TradeSide.SHORT else 75_100.0, reason=f"CANCEL-{idx}"))
elif op == "reconcile":
kernel.process_intent(_intent(action=KernelCommandType.RECONCILE, trade_id=trade_id, side=side, target_size=1.0, price=75_000.0, reason=f"RECONCILE-{idx}"))
final_slot = kernel.slot(0)
assert final_slot.trade_id == trade_id
assert final_slot.fsm_state in {
TradeStage.ENTRY_WORKING,
TradeStage.POSITION_OPEN,
TradeStage.EXIT_WORKING,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
assert kernel.account.snapshot.equity == pytest.approx(kernel.account.snapshot.capital + kernel.account.snapshot.unrealized_pnl, abs=1e-9)
assert kernel.snapshot()["control"]["runtime_namespace"] == "dita_v2"
if final_slot.closed:
assert final_slot.size == pytest.approx(0.0, abs=1e-9)
else:
assert final_slot.fsm_state in {
TradeStage.ENTRY_WORKING,
TradeStage.POSITION_OPEN,
TradeStage.EXIT_WORKING,
TradeStage.STALE_STATE_RECONCILING,
}

View File

@@ -1,612 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import math
import pytest
from prod.clean_arch.dita_v2 import (
BackendMode,
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelControlSnapshot,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelMode,
KernelVerbosity,
MemoryKernelJournal,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
class NoopVenueAdapter:
def submit(self, intent): # type: ignore[override]
return []
def cancel(self, order, *, reason: str = ""): # type: ignore[override]
return []
def open_orders(self): # type: ignore[override]
return []
def open_positions(self): # type: ignore[override]
return []
def reconcile(self): # type: ignore[override]
return []
@dataclass(frozen=True)
class IntentGuardCase:
name: str
slot_id: int
seed_state: str
action: KernelCommandType
trade_id: str
intent_trade_id: str
expected_state: TradeStage
expected_code: KernelDiagnosticCode
expected_accepted: bool
@dataclass(frozen=True)
class DuplicateCase:
name: str
seed_state: str
first_kind: KernelEventKind
second_kind: KernelEventKind
expected_state: TradeStage
event_factory_name: str
@dataclass(frozen=True)
class StaleCase:
name: str
second_kind: KernelEventKind
same_event_id_as_initial: bool
expected_accepted: bool
@dataclass(frozen=True)
class ZincMirrorCase:
name: str
op: str
@dataclass(frozen=True)
class SlotRigorCase:
name: str
op: str
def _build_kernel(slot_count: int = 3) -> tuple[ExecutionKernel, MemoryKernelJournal, InMemoryZincPlane]:
journal = MemoryKernelJournal()
zinc = InMemoryZincPlane()
kernel = ExecutionKernel(
max_slots=slot_count,
control_plane=InMemoryControlPlane(
KernelControlSnapshot(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.MOCK,
debug_clickhouse_enabled=True,
trace_transitions=True,
mirror_to_hazelcast=True,
)
),
venue=NoopVenueAdapter(),
journal=journal,
zinc_plane=zinc,
)
return kernel, journal, zinc
def _make_entry_order(trade_id: str, slot_id: int, *, size: float = 1.0, status: VenueOrderStatus = VenueOrderStatus.NEW) -> VenueOrder:
return VenueOrder(
internal_trade_id=trade_id,
venue_order_id=f"V-ENTRY-{slot_id}-{trade_id}",
venue_client_id=f"{trade_id}:entry:{slot_id}",
side=TradeSide.SHORT,
intended_size=size,
filled_size=size if status == VenueOrderStatus.FILLED else 0.0,
average_fill_price=100.0,
status=status,
metadata={"slot_id": slot_id},
)
def _make_exit_order(trade_id: str, slot_id: int, *, size: float, status: VenueOrderStatus = VenueOrderStatus.NEW) -> VenueOrder:
return VenueOrder(
internal_trade_id=trade_id,
venue_order_id=f"V-EXIT-{slot_id}-{trade_id}",
venue_client_id=f"{trade_id}:exit:{slot_id}",
side=TradeSide.SHORT,
intended_size=size,
filled_size=size if status == VenueOrderStatus.FILLED else 0.0,
average_fill_price=99.0,
status=status,
metadata={"slot_id": slot_id},
)
def _seed_free_slot(slot_id: int) -> TradeSlot:
return TradeSlot(slot_id=slot_id)
def _seed_entry_working_slot(trade_id: str, slot_id: int) -> TradeSlot:
return TradeSlot(
slot_id=slot_id,
trade_id=trade_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=0.0,
size=0.0,
initial_size=0.0,
leverage=2.0,
entry_time=datetime.now(timezone.utc),
exit_leg_ratios=(1.0,),
active_leg_index=0,
active_entry_order=_make_entry_order(trade_id, slot_id, status=VenueOrderStatus.NEW),
fsm_state=TradeStage.ENTRY_WORKING,
)
def _seed_position_open_slot(trade_id: str, slot_id: int, *, size: float = 1.0, side: TradeSide = TradeSide.SHORT) -> TradeSlot:
return TradeSlot(
slot_id=slot_id,
trade_id=trade_id,
asset="BTCUSDT",
side=side,
entry_price=100.0,
size=size,
initial_size=size,
leverage=2.0,
entry_time=datetime.now(timezone.utc),
exit_leg_ratios=(0.5, 0.5),
active_leg_index=0,
active_entry_order=_make_entry_order(trade_id, slot_id, size=size, status=VenueOrderStatus.FILLED),
fsm_state=TradeStage.POSITION_OPEN,
)
def _seed_exit_working_slot(trade_id: str, slot_id: int, *, size: float = 1.0) -> TradeSlot:
slot = _seed_position_open_slot(trade_id, slot_id, size=size)
slot.active_exit_order = _make_exit_order(trade_id, slot_id, size=slot.next_exit_ratio() * size, status=VenueOrderStatus.NEW)
slot.fsm_state = TradeStage.EXIT_WORKING
return slot
def _seed_closed_slot(trade_id: str, slot_id: int) -> TradeSlot:
return TradeSlot(
slot_id=slot_id,
trade_id=trade_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=0.0,
initial_size=1.0,
leverage=2.0,
entry_time=datetime.now(timezone.utc),
closed=True,
fsm_state=TradeStage.CLOSED,
)
def _make_intent(
*,
trade_id: str,
slot_id: int,
action: KernelCommandType,
leverage: float = 2.0,
size: float = 1.0,
side: TradeSide = TradeSide.SHORT,
reason: str = "HARNESS",
) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{trade_id}-{action.value}-{slot_id}",
trade_id=trade_id,
slot_id=slot_id,
asset="BTCUSDT",
side=side,
action=action,
reference_price=100.0,
target_size=size,
leverage=leverage,
exit_leg_ratios=(0.5, 0.5) if action == KernelCommandType.EXIT else (1.0,),
reason=reason,
)
def _make_event(
slot: TradeSlot,
*,
kind: KernelEventKind,
event_id: str,
filled_size: float = 0.0,
reason: str = "",
slot_id: int | None = None,
) -> VenueEvent:
order = slot.active_exit_order or slot.active_entry_order
venue_order_id = order.venue_order_id if order else f"V-{kind.value}-{slot.slot_id}"
venue_client_id = order.venue_client_id if order else f"{slot.trade_id}:client:{slot.slot_id}"
status = {
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
KernelEventKind.CONTROL: VenueEventStatus.ACKED,
}[kind]
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=event_id,
trade_id=slot.trade_id,
slot_id=slot.slot_id if slot_id is None else slot_id,
kind=kind,
status=status,
venue_order_id=venue_order_id,
venue_client_id=venue_client_id,
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
asset=slot.asset or "BTCUSDT",
price=99.0 if kind == KernelEventKind.MARK_PRICE else 100.0,
size=max(slot.size, 1.0),
filled_size=filled_size,
remaining_size=max(0.0, max(slot.size, 1.0) - filled_size),
reason=reason,
)
INTENT_GUARD_CASES = [
IntentGuardCase("invalid_negative_enter", -1, "free", KernelCommandType.ENTER, "trade-neg", "trade-neg", TradeStage.IDLE, KernelDiagnosticCode.INVALID_SLOT_ID, False),
IntentGuardCase("invalid_high_exit", 99, "free", KernelCommandType.EXIT, "trade-high", "trade-high", TradeStage.IDLE, KernelDiagnosticCode.INVALID_SLOT_ID, False),
IntentGuardCase("unsupported_control", 0, "free", KernelCommandType.CONTROL, "trade-control", "trade-control", TradeStage.IDLE, KernelDiagnosticCode.UNSUPPORTED_INTENT, False),
IntentGuardCase("free_exit", 0, "free", KernelCommandType.EXIT, "trade-free-exit", "trade-free-exit", TradeStage.IDLE, KernelDiagnosticCode.NO_OPEN_POSITION, False),
IntentGuardCase("free_cancel", 0, "free", KernelCommandType.CANCEL, "trade-free-cancel", "trade-free-cancel", TradeStage.IDLE, KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER, False),
IntentGuardCase("busy_enter_different_trade", 0, "position_open", KernelCommandType.ENTER, "trade-open", "trade-new", TradeStage.POSITION_OPEN, KernelDiagnosticCode.SLOT_BUSY, False),
IntentGuardCase("same_trade_enter_allowed", 0, "position_open", KernelCommandType.ENTER, "trade-open", "trade-open", TradeStage.ORDER_REQUESTED, KernelDiagnosticCode.OK, True),
IntentGuardCase("closed_exit", 0, "closed", KernelCommandType.EXIT, "trade-closed", "trade-closed", TradeStage.CLOSED, KernelDiagnosticCode.NO_OPEN_POSITION, False),
IntentGuardCase("open_reconcile", 0, "position_open", KernelCommandType.RECONCILE, "trade-reconcile", "trade-reconcile", TradeStage.STALE_STATE_RECONCILING, KernelDiagnosticCode.STALE_STATE_RECONCILE, True),
IntentGuardCase("free_mark_price", 0, "free", KernelCommandType.MARK_PRICE, "trade-mark", "trade-mark", TradeStage.IDLE, KernelDiagnosticCode.OK, True),
]
DUPLICATE_CASES = [
DuplicateCase("entry_ack_duplicate", "entry_working", KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_ACK, TradeStage.ENTRY_WORKING, "ack"),
DuplicateCase("entry_partial_duplicate", "entry_working", KernelEventKind.PARTIAL_FILL, KernelEventKind.PARTIAL_FILL, TradeStage.ENTRY_WORKING, "partial-entry"),
DuplicateCase("entry_full_duplicate", "entry_working", KernelEventKind.FULL_FILL, KernelEventKind.FULL_FILL, TradeStage.POSITION_OPEN, "full-entry"),
DuplicateCase("exit_ack_duplicate", "exit_working", KernelEventKind.CANCEL_ACK, KernelEventKind.CANCEL_ACK, TradeStage.POSITION_OPEN, "ack-exit"),
DuplicateCase("exit_partial_duplicate", "exit_working", KernelEventKind.PARTIAL_FILL, KernelEventKind.PARTIAL_FILL, TradeStage.EXIT_WORKING, "partial-exit"),
DuplicateCase("exit_full_duplicate", "exit_working", KernelEventKind.FULL_FILL, KernelEventKind.FULL_FILL, TradeStage.CLOSED, "full-exit"),
DuplicateCase("cancel_reject_duplicate", "exit_working", KernelEventKind.CANCEL_REJECT, KernelEventKind.CANCEL_REJECT, TradeStage.EXIT_WORKING, "reject-exit"),
DuplicateCase("mark_price_duplicate", "position_open", KernelEventKind.MARK_PRICE, KernelEventKind.MARK_PRICE, TradeStage.POSITION_OPEN, "mark"),
DuplicateCase("reconcile_duplicate", "position_open", KernelEventKind.RECONCILE, KernelEventKind.RECONCILE, TradeStage.STALE_STATE_RECONCILING, "reconcile"),
DuplicateCase("entry_reject_duplicate", "entry_working", KernelEventKind.ORDER_REJECT, KernelEventKind.ORDER_REJECT, TradeStage.IDLE, "reject-entry"),
]
STALE_CASES = [
StaleCase("stale_ack", KernelEventKind.ORDER_ACK, False, False),
StaleCase("stale_reject", KernelEventKind.ORDER_REJECT, False, False),
StaleCase("stale_partial", KernelEventKind.PARTIAL_FILL, False, False),
StaleCase("stale_full", KernelEventKind.FULL_FILL, False, False),
StaleCase("stale_cancel_ack", KernelEventKind.CANCEL_ACK, False, False),
StaleCase("stale_cancel_reject", KernelEventKind.CANCEL_REJECT, False, False),
StaleCase("stale_mark_price", KernelEventKind.MARK_PRICE, False, False),
StaleCase("stale_control", KernelEventKind.CONTROL, False, False),
StaleCase("stale_reconcile", KernelEventKind.RECONCILE, False, True),
StaleCase("stale_duplicate_precedence", KernelEventKind.ORDER_ACK, True, False),
]
ZINC_MIRROR_CASES = [
ZincMirrorCase("intent_published_on_enter", "intent"),
ZincMirrorCase("invalid_slot_intent_still_publishes", "invalid_intent"),
ZincMirrorCase("slot_write_updates_state_region", "direct_write"),
ZincMirrorCase("venue_event_updates_state_region", "venue_event"),
ZincMirrorCase("control_update_writes_region", "control_update"),
ZincMirrorCase("snapshot_reflects_control", "snapshot"),
ZincMirrorCase("reconcile_from_slots_writes_all", "reconcile"),
ZincMirrorCase("free_slot_selects_first_free", "free_slot"),
ZincMirrorCase("read_slots_sorted", "sorted_read"),
ZincMirrorCase("slot_overwrite_replaces_previous_state", "overwrite"),
]
SLOT_RIGOR_CASES = [
SlotRigorCase("idle_slot_is_free", "idle_free"),
SlotRigorCase("closed_slot_is_free", "closed_free"),
SlotRigorCase("entry_working_is_not_free", "entry_not_free"),
SlotRigorCase("open_slot_is_not_free", "open_not_free"),
SlotRigorCase("mark_price_zero_is_noop", "mark_zero"),
SlotRigorCase("mark_price_negative_is_noop", "mark_negative"),
SlotRigorCase("mark_price_nan_is_noop", "mark_nan"),
SlotRigorCase("short_price_rise_negative_pnl", "short_rise"),
SlotRigorCase("short_price_drop_positive_pnl", "short_drop"),
SlotRigorCase("exit_leg_consume_and_clamp", "exit_leg"),
]
def _seed_for_intent_case(kernel: ExecutionKernel, case: IntentGuardCase) -> None:
if case.seed_state == "free":
return
if case.seed_state == "entry_working":
kernel._set_slot(_seed_entry_working_slot(case.trade_id, case.slot_id))
return
if case.seed_state == "position_open":
kernel._set_slot(_seed_position_open_slot(case.trade_id, case.slot_id))
return
if case.seed_state == "exit_working":
kernel._set_slot(_seed_exit_working_slot(case.trade_id, case.slot_id))
return
if case.seed_state == "closed":
kernel._set_slot(_seed_closed_slot(case.trade_id, case.slot_id))
return
raise AssertionError(case.seed_state)
def _seed_for_duplicate_case(kernel: ExecutionKernel, case: DuplicateCase) -> TradeSlot:
if case.seed_state == "entry_working":
slot = _seed_entry_working_slot(f"trade-{case.name}", 0)
elif case.seed_state == "exit_working":
slot = _seed_exit_working_slot(f"trade-{case.name}", 0)
elif case.seed_state == "position_open":
slot = _seed_position_open_slot(f"trade-{case.name}", 0)
elif case.seed_state == "stale":
slot = _seed_position_open_slot(f"trade-{case.name}", 0)
else:
raise AssertionError(case.seed_state)
kernel._set_slot(slot)
return kernel._get_slot(0)
def _seed_for_stale_case(kernel: ExecutionKernel) -> TradeSlot:
slot = _seed_position_open_slot("trade-stale", 0)
kernel._set_slot(slot)
return kernel._get_slot(0)
def _seed_for_zinc_case(kernel: ExecutionKernel, case: ZincMirrorCase) -> None:
if case.op == "intent":
return
if case.op == "invalid_intent":
return
if case.op == "direct_write":
kernel._set_slot(_seed_position_open_slot("trade-write", 0))
return
if case.op == "venue_event":
kernel._set_slot(_seed_entry_working_slot("trade-event", 0))
return
if case.op == "control_update":
return
if case.op == "snapshot":
return
if case.op == "reconcile":
return
if case.op == "free_slot":
kernel._set_slot(_seed_position_open_slot("trade-free", 0))
kernel._set_slot(_seed_free_slot(1))
return
if case.op == "sorted_read":
return
if case.op == "overwrite":
return
raise AssertionError(case.op)
@pytest.mark.parametrize("case", INTENT_GUARD_CASES, ids=lambda case: case.name)
def test_kernel_intent_guard_matrix(case: IntentGuardCase) -> None:
kernel, _, zinc = _build_kernel()
_seed_for_intent_case(kernel, case)
intent = _make_intent(
trade_id=case.intent_trade_id,
slot_id=case.slot_id,
action=case.action,
leverage=-3.0 if case.name == "same_trade_enter_allowed" else 2.5,
size=1.0,
reason=case.name,
)
outcome = kernel.process_intent(intent)
assert outcome.accepted is case.expected_accepted
assert outcome.diagnostic_code == case.expected_code
assert outcome.state == case.expected_state
if case.slot_id >= 0 and case.slot_id < kernel.max_slots:
assert zinc.intent_region
assert zinc.intent_region[-1].intent_id == intent.intent_id
if case.name == "same_trade_enter_allowed":
current = kernel.slot(case.slot_id).to_dict()
assert current["fsm_state"] == TradeStage.ORDER_REQUESTED.value
assert current["leverage"] == 1.0
@pytest.mark.parametrize("case", DUPLICATE_CASES, ids=lambda case: case.name)
def test_kernel_duplicate_event_matrix(case: DuplicateCase) -> None:
kernel, _, _ = _build_kernel()
slot = _seed_for_duplicate_case(kernel, case)
fill_size = slot.size or 1.0
if case.seed_state == "exit_working" and case.first_kind == KernelEventKind.PARTIAL_FILL:
fill_size = max(0.1, fill_size * 0.4)
first_event = _make_event(slot, kind=case.first_kind, event_id=f"dup-{case.name}", filled_size=fill_size)
first = kernel.on_venue_event(first_event)
second = kernel.on_venue_event(first_event)
assert first.diagnostic_code in {
KernelDiagnosticCode.OK,
KernelDiagnosticCode.STALE_STATE_RECONCILE,
KernelDiagnosticCode.ENTRY_ORDER_REJECTED,
KernelDiagnosticCode.EXIT_ORDER_REJECTED,
KernelDiagnosticCode.ORDER_REJECTED,
KernelDiagnosticCode.CANCEL_REJECTED,
}
assert second.diagnostic_code == KernelDiagnosticCode.DUPLICATE_EVENT
assert second.state == case.expected_state
assert second.accepted is True
assert kernel.slot(0).to_dict()["seen_event_ids"].count(first_event.event_id) == 1
@pytest.mark.parametrize("case", STALE_CASES, ids=lambda case: case.name)
def test_kernel_stale_state_matrix(case: StaleCase) -> None:
kernel, _, _ = _build_kernel()
slot = _seed_for_stale_case(kernel)
initial = _make_event(slot, kind=KernelEventKind.RECONCILE, event_id="stale-entry", filled_size=slot.size or 1.0)
initial_outcome = kernel.on_venue_event(initial)
assert initial_outcome.diagnostic_code == KernelDiagnosticCode.OK
assert kernel.slot(0).fsm_state == TradeStage.STALE_STATE_RECONCILING
if case.same_event_id_as_initial:
event = _make_event(kernel._get_slot(0), kind=case.second_kind, event_id="stale-entry", filled_size=slot.size or 1.0, reason=case.name)
else:
event = _make_event(kernel._get_slot(0), kind=case.second_kind, event_id=f"stale-{case.name}", filled_size=slot.size or 1.0, reason=case.name)
outcome = kernel.on_venue_event(event)
if case.same_event_id_as_initial:
assert outcome.diagnostic_code == KernelDiagnosticCode.DUPLICATE_EVENT
assert outcome.accepted is True
else:
assert outcome.diagnostic_code == KernelDiagnosticCode.STALE_STATE_RECONCILE
assert outcome.accepted is case.expected_accepted
assert outcome.state == TradeStage.STALE_STATE_RECONCILING
assert kernel.slot(0).fsm_state == TradeStage.STALE_STATE_RECONCILING
@pytest.mark.parametrize("case", ZINC_MIRROR_CASES, ids=lambda case: case.name)
def test_kernel_zinc_mirror_matrix(case: ZincMirrorCase) -> None:
kernel, _, zinc = _build_kernel()
_seed_for_zinc_case(kernel, case)
if case.op == "intent":
intent = _make_intent(trade_id="trade-intent", slot_id=0, action=KernelCommandType.ENTER, size=1.25)
outcome = kernel.process_intent(intent)
assert outcome.accepted is True
assert zinc.intent_region
assert zinc.intent_region[-1].intent_id == intent.intent_id
assert zinc.read_slots()[0].trade_id == "trade-intent"
elif case.op == "invalid_intent":
intent = _make_intent(trade_id="trade-invalid", slot_id=-1, action=KernelCommandType.EXIT, size=1.0)
outcome = kernel.process_intent(intent)
assert outcome.diagnostic_code == KernelDiagnosticCode.INVALID_SLOT_ID
assert len(zinc.intent_region) == 1
assert zinc.intent_region[-1].intent_id == intent.intent_id
elif case.op == "direct_write":
slot = _seed_position_open_slot("trade-write", 0, size=1.5)
kernel._set_slot(slot)
mirrored = zinc.read_slots()[0]
assert mirrored.trade_id == "trade-write"
assert mirrored.size == 1.5
assert mirrored.fsm_state == TradeStage.POSITION_OPEN
elif case.op == "venue_event":
slot = kernel._get_slot(0)
event = _make_event(slot, kind=KernelEventKind.FULL_FILL, event_id="zinc-fill", filled_size=slot.size or 1.0)
outcome = kernel.on_venue_event(event)
assert outcome.diagnostic_code == KernelDiagnosticCode.OK
mirrored = zinc.read_slots()[0]
assert mirrored.fsm_state == TradeStage.POSITION_OPEN
assert mirrored.seen_event_ids == ("zinc-fill",)
elif case.op == "control_update":
snapshot = kernel.update_control(
ControlUpdate(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
trace_transitions=True,
mirror_to_hazelcast=False,
)
)
assert snapshot.mode == KernelMode.DEBUG
assert zinc.read_control().mode == KernelMode.DEBUG
assert zinc.read_control().trace_transitions is True
elif case.op == "snapshot":
kernel.update_control(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.VERBOSE))
payload = kernel.snapshot()
assert payload["control"]["mode"] == KernelMode.DEBUG.value
assert payload["control"]["verbosity"] == KernelVerbosity.VERBOSE.value
elif case.op == "reconcile":
slots = [
_seed_position_open_slot("trade-a", 2),
_seed_closed_slot("trade-b", 0),
_seed_free_slot(1),
]
outcome = kernel.reconcile_from_slots(slots)
assert outcome.diagnostic_code == KernelDiagnosticCode.RECONCILED
mirrored_ids = [slot.slot_id for slot in zinc.read_slots()]
assert mirrored_ids == [0, 1, 2]
elif case.op == "free_slot":
assert kernel.free_slot().slot_id == 1
elif case.op == "sorted_read":
kernel._set_slot(_seed_position_open_slot("trade-c", 2))
kernel._set_slot(_seed_position_open_slot("trade-a", 0))
kernel._set_slot(_seed_position_open_slot("trade-b", 1))
ids = [slot.slot_id for slot in zinc.read_slots()]
assert ids == [0, 1, 2]
elif case.op == "overwrite":
kernel._set_slot(_seed_position_open_slot("trade-old", 0, size=1.0))
kernel._set_slot(_seed_position_open_slot("trade-new", 0, size=2.0))
mirrored = zinc.read_slots()[0]
assert mirrored.trade_id == "trade-new"
assert mirrored.size == 2.0
assert mirrored.initial_size == 2.0
else: # pragma: no cover - exhaustive
raise AssertionError(case.op)
@pytest.mark.parametrize("case", SLOT_RIGOR_CASES, ids=lambda case: case.name)
def test_trade_slot_state_machine_rigor_matrix(case: SlotRigorCase) -> None:
if case.op == "idle_free":
slot = TradeSlot(slot_id=0)
assert slot.is_free() is True
assert slot.is_open() is False
elif case.op == "closed_free":
slot = _seed_closed_slot("trade-closed", 0)
assert slot.is_free() is True
assert slot.is_open() is False
elif case.op == "entry_not_free":
slot = _seed_entry_working_slot("trade-entry", 0)
assert slot.is_free() is False
assert slot.is_open() is True
elif case.op == "open_not_free":
slot = _seed_position_open_slot("trade-open", 0)
assert slot.is_free() is False
assert slot.is_open() is True
elif case.op == "mark_zero":
slot = _seed_position_open_slot("trade-mark", 0, size=1.0)
slot.mark_price(0.0)
assert slot.unrealized_pnl == 0.0
elif case.op == "mark_negative":
slot = _seed_position_open_slot("trade-mark", 0, size=1.0)
slot.mark_price(-10.0)
assert slot.unrealized_pnl == 0.0
elif case.op == "mark_nan":
slot = _seed_position_open_slot("trade-mark", 0, size=1.0)
slot.mark_price(float("nan"))
assert slot.unrealized_pnl == 0.0
elif case.op == "short_rise":
slot = _seed_position_open_slot("trade-short-rise", 0, size=1.0, side=TradeSide.SHORT)
slot.mark_price(110.0)
assert slot.unrealized_pnl < 0.0
elif case.op == "short_drop":
slot = _seed_position_open_slot("trade-short-drop", 0, size=1.0, side=TradeSide.SHORT)
slot.mark_price(90.0)
assert slot.unrealized_pnl > 0.0
elif case.op == "exit_leg":
slot = _seed_position_open_slot("trade-leg", 0, size=1.0)
slot.exit_leg_ratios = (0.25, 0.75)
first = slot.consume_exit_leg()
second = slot.consume_exit_leg()
third = slot.consume_exit_leg()
assert first == 0.25
assert second == 0.75
assert third == 1.0
assert slot.active_leg_index == 2
assert slot.next_exit_ratio() == 1.0
else: # pragma: no cover - exhaustive
raise AssertionError(case.op)

View File

@@ -1,196 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
import unittest
from prod.clean_arch.dita_v2 import (
ControlUpdate,
ExecutionKernel,
HazelcastProjection,
KernelCommandType,
KernelControlSnapshot,
KernelIntent,
KernelMode,
KernelVerbosity,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeStage,
TradeSlot,
build_projection,
build_position_state_row,
)
from prod.clean_arch.dita_v2.hazelcast_projection import HazelcastProjector, HazelcastRowWriter
class CaptureSink:
def __init__(self) -> None:
self.rows: list[tuple[str, dict[str, object]]] = []
def __call__(self, name: str, row: dict[str, object]) -> None:
self.rows.append((name, dict(row)))
class FakeMap:
def __init__(self) -> None:
self.rows: dict[str, object] = {}
def put(self, key: str, value: object) -> None:
self.rows[key] = value
class FakeTopic:
def __init__(self) -> None:
self.messages: list[str] = []
def publish(self, message: str) -> None:
self.messages.append(message)
class FakeHazelcastClient:
def __init__(self) -> None:
self.maps: dict[str, FakeMap] = {}
self.topics: dict[str, FakeTopic] = {}
def get_map(self, name: str) -> FakeMap:
return self.maps.setdefault(name, FakeMap())
def get_topic(self, name: str) -> FakeTopic:
return self.topics.setdefault(name, FakeTopic())
class TestDITAv2Hazelcast(unittest.TestCase):
def test_build_position_state_row_has_compatibility_fields(self) -> None:
slot = TradeSlot(
slot_id=0,
trade_id="trade-1",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
)
row = build_position_state_row(
slot,
KernelControlSnapshot(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
runtime_namespace="dita_v2",
strategy_namespace="dita_v2",
event_namespace="dita_v2",
actor_name="ExecutionKernel",
exec_venue="bingx",
data_venue="binance",
ledger_authority="exchange",
),
)
for key in (
"runtime_namespace",
"strategy_namespace",
"event_namespace",
"actor_name",
"exec_venue",
"data_venue",
"ledger_authority",
"trade_id",
"asset",
"slot_id",
"fsm_state",
):
self.assertIn(key, row)
self.assertEqual(row["trade_id"], "trade-1")
self.assertEqual(row["fsm_state"], TradeStage.POSITION_OPEN.value)
def test_projection_sink_writes_blue_pink_compatible_rows(self) -> None:
sink = CaptureSink()
projection = HazelcastProjection(writer=sink)
control = KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
projection.write_control(control)
slot = TradeSlot(
slot_id=1,
trade_id="trade-2",
asset="ETHUSDT",
side=TradeSide.LONG,
entry_price=50.0,
size=2.0,
initial_size=2.0,
leverage=3.0,
fsm_state=TradeStage.POSITION_OPEN,
)
projection.write_slot(slot)
self.assertGreaterEqual(len(sink.rows), 2)
control_name, control_row = sink.rows[0]
slot_name, slot_row = sink.rows[1]
self.assertEqual(control_name, "hz:dita_control")
self.assertEqual(slot_name, "hz:dita_active_slots")
self.assertEqual(control_row["mode"], KernelMode.DEBUG.value)
self.assertEqual(slot_row["trade_id"], "trade-2")
self.assertEqual(slot_row["runtime_namespace"], "dita_v2")
self.assertEqual(slot_row["ledger_authority"], "exchange")
def test_hazelcast_row_writer_routes_maps_and_topics(self) -> None:
client = FakeHazelcastClient()
writer = HazelcastRowWriter(client)
writer("hz:dita_active_slots", {"trade_id": "trade-3", "slot_id": 0})
writer("hz:dita_control", {"mode": "DEBUG"})
writer("hz:dita_trade_events", {"event_id": "evt-1", "trade_id": "trade-3"})
self.assertIn("trade-3", client.get_map("hz:dita_active_slots").rows)
self.assertIn("control", client.get_map("hz:dita_control").rows)
self.assertEqual(len(client.get_topic("hz:dita_trade_events").messages), 1)
def test_build_projection_uses_client_when_requested(self) -> None:
client = FakeHazelcastClient()
projection = build_projection(client=client, prefer_real_hazelcast=True)
projection.write_control(KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE))
projection.write_slot(
TradeSlot(
slot_id=0,
trade_id="trade-4",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
)
)
self.assertIn("control", client.get_map("hz:dita_control").rows)
self.assertIn("trade-4", client.get_map("hz:dita_active_slots").rows)
def test_kernel_emits_projection_rows(self) -> None:
sink = CaptureSink()
kernel = ExecutionKernel(
control_plane=None,
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
projection=HazelcastProjection(writer=sink),
)
kernel.update_control(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE))
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="intent-1",
trade_id="trade-1",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=100.0,
target_size=1.0,
leverage=2.0,
exit_leg_ratios=(1.0,),
reason="TEST",
)
)
names = [name for name, _ in sink.rows]
self.assertIn("hz:dita_control", names)
self.assertIn("hz:dita_active_slots", names)
slot_rows = [row for name, row in sink.rows if name == "hz:dita_active_slots"]
self.assertTrue(any(row["trade_id"] == "trade-1" for row in slot_rows))
self.assertTrue(any(row["runtime_namespace"] == "dita_v2" for row in slot_rows))
if __name__ == "__main__":
unittest.main()

View File

@@ -1,231 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
import unittest
from prod.clean_arch.dita_v2 import (
AccountProjection,
BackendMode,
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelControlSnapshot,
KernelEventKind,
KernelIntent,
KernelMode,
KernelVerbosity,
MemoryKernelJournal,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
)
def mk_intent(
*,
action: KernelCommandType = KernelCommandType.ENTER,
slot_id: int = 0,
trade_id: str = "trade-1",
asset: str = "BTCUSDT",
side: TradeSide = TradeSide.SHORT,
target_size: float = 1.0,
leverage: float = 2.0,
reference_price: float = 100.0,
exit_leg_ratios=(1.0,),
reason: str = "TEST",
) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{trade_id}-{action.value}",
trade_id=trade_id,
slot_id=slot_id,
asset=asset,
side=side,
action=action,
reference_price=reference_price,
target_size=target_size,
leverage=leverage,
exit_leg_ratios=tuple(exit_leg_ratios),
reason=reason,
)
class TestDITAv2ControlPlane(unittest.TestCase):
def test_control_plane_updates_and_mirrors(self):
plane = InMemoryControlPlane()
updated = plane.update(
ControlUpdate(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.BINGX,
trace_transitions=True,
)
)
self.assertEqual(updated.mode, KernelMode.DEBUG)
self.assertEqual(updated.verbosity, KernelVerbosity.TRACE)
self.assertEqual(updated.backend_mode, BackendMode.BINGX)
self.assertTrue(updated.trace_transitions)
self.assertEqual(plane.mirror()["mode"], KernelMode.DEBUG.value)
class TestDITAv2Kernel(unittest.TestCase):
def test_entry_ack_fill_reaches_position_open(self):
journal = MemoryKernelJournal()
zinc = InMemoryZincPlane()
venue = MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0))
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=venue,
journal=journal,
zinc_plane=zinc,
)
outcome = kernel.process_intent(mk_intent())
slot = kernel.slot(0)
self.assertTrue(outcome.accepted)
self.assertEqual(slot.fsm_state, TradeStage.POSITION_OPEN)
self.assertFalse(slot.closed)
self.assertEqual(slot.trade_id, "trade-1")
self.assertAlmostEqual(slot.size, 1.0, places=6)
self.assertEqual(len(journal.rows), 3)
self.assertEqual(len(zinc.intent_region), 1)
self.assertEqual(zinc.read_control().mode, KernelMode.DEBUG)
def test_partial_fill_stays_working_then_full_fill_opens_position(self):
journal = MemoryKernelJournal()
venue = MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=False, partial_fill_ratio=0.5))
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=venue,
journal=journal,
)
kernel.process_intent(mk_intent())
slot = kernel.slot(0)
self.assertEqual(slot.fsm_state, TradeStage.ENTRY_WORKING)
self.assertAlmostEqual(slot.size, 0.5, places=6)
full_fill = VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id="evt-full",
trade_id="trade-1",
slot_id=0,
kind=KernelEventKind.FULL_FILL,
status=VenueEventStatus.FILLED,
venue_order_id=slot.active_entry_order.venue_order_id if slot.active_entry_order else "V-00000001",
venue_client_id=slot.active_entry_order.venue_client_id if slot.active_entry_order else "trade-1:intent-trade-1-ENTER",
side=TradeSide.SHORT,
asset="BTCUSDT",
price=100.0,
size=1.0,
filled_size=1.0,
remaining_size=0.0,
)
kernel.on_venue_event(full_fill)
self.assertEqual(slot.fsm_state, TradeStage.POSITION_OPEN)
self.assertFalse(slot.closed)
self.assertAlmostEqual(slot.size, 1.0, places=6)
def test_two_leg_exit_closes_only_after_final_leg(self):
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
journal=MemoryKernelJournal(),
)
kernel.process_intent(mk_intent())
slot = kernel.slot(0)
slot.exit_leg_ratios = (0.5, 0.5)
first_exit = kernel.process_intent(
mk_intent(action=KernelCommandType.EXIT, target_size=0.5, exit_leg_ratios=(0.5, 0.5), reason="TP1")
)
self.assertTrue(first_exit.accepted)
self.assertEqual(slot.fsm_state, TradeStage.POSITION_OPEN)
self.assertFalse(slot.closed)
self.assertAlmostEqual(slot.size, 0.5, places=6)
second_exit = kernel.process_intent(
mk_intent(action=KernelCommandType.EXIT, target_size=0.5, exit_leg_ratios=(0.5, 0.5), reason="TP2")
)
self.assertTrue(second_exit.accepted)
self.assertTrue(slot.closed)
self.assertEqual(slot.fsm_state, TradeStage.CLOSED)
self.assertAlmostEqual(slot.size, 0.0, places=6)
def test_reconcile_sets_stale_state(self):
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(),
venue=MockVenueAdapter(),
journal=MemoryKernelJournal(),
)
kernel.process_intent(mk_intent())
slot = kernel.slot(0)
kernel.process_intent(mk_intent(action=KernelCommandType.RECONCILE))
self.assertEqual(slot.fsm_state, TradeStage.STALE_STATE_RECONCILING)
def test_account_projection_aggregates_slots(self):
projection = AccountProjection()
slots = [
TradeSlot(
slot_id=0,
trade_id="t1",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
metadata={"mark_price": 99.0},
),
TradeSlot(
slot_id=1,
trade_id="t2",
asset="ETHUSDT",
side=TradeSide.LONG,
entry_price=50.0,
size=2.0,
initial_size=2.0,
leverage=3.0,
fsm_state=TradeStage.EXIT_WORKING,
metadata={"mark_price": 55.0},
),
]
projection.observe_slots(slots)
self.assertEqual(projection.snapshot.open_positions, 2)
self.assertAlmostEqual(projection.snapshot.open_notional, 209.0, places=6)
self.assertGreater(projection.snapshot.leverage, 0.0)
def test_debug_mode_journal_records_transitions(self):
journal = MemoryKernelJournal()
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
journal=journal,
)
kernel.process_intent(mk_intent())
self.assertGreaterEqual(len(journal.rows), 2)
self.assertTrue(all("slot_state" in row for row in journal.rows))
if __name__ == "__main__":
unittest.main()

View File

@@ -1,579 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
import random
from typing import Any
import pytest
from prod.clean_arch.dita_v2 import (
AccountProjection,
BingxVenueAdapter,
BackendMode,
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelControlSnapshot,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelMode,
KernelOutcome,
KernelSeverity,
KernelVerbosity,
MemoryKernelJournal,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
def mk_intent(
*,
action: KernelCommandType = KernelCommandType.ENTER,
slot_id: int = 0,
trade_id: str = "trade-1",
asset: str = "BTCUSDT",
side: TradeSide = TradeSide.SHORT,
target_size: float = 1.0,
leverage: float = 2.0,
reference_price: float = 100.0,
exit_leg_ratios=(1.0,),
reason: str = "TEST",
) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{trade_id}-{action.value}",
trade_id=trade_id,
slot_id=slot_id,
asset=asset,
side=side,
action=action,
reference_price=reference_price,
target_size=target_size,
leverage=leverage,
exit_leg_ratios=tuple(exit_leg_ratios),
reason=reason,
)
def mk_event(
*,
kind: KernelEventKind,
status: VenueEventStatus,
trade_id: str = "trade-1",
slot_id: int = 0,
venue_order_id: str = "V-00000001",
venue_client_id: str = "trade-1:intent-1",
side: TradeSide = TradeSide.SHORT,
asset: str = "BTCUSDT",
price: float = 100.0,
size: float = 1.0,
filled_size: float = 1.0,
remaining_size: float = 0.0,
reason: str = "",
) -> VenueEvent:
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"evt-{kind.value.lower()}",
trade_id=trade_id,
slot_id=slot_id,
kind=kind,
status=status,
venue_order_id=venue_order_id,
venue_client_id=venue_client_id,
side=side,
asset=asset,
price=price,
size=size,
filled_size=filled_size,
remaining_size=remaining_size,
reason=reason,
raw_payload={"status": status.value},
)
def mk_kernel(
*,
max_slots: int = 3,
venue: Any | None = None,
control_mode: KernelMode = KernelMode.DEBUG,
verbosity: KernelVerbosity = KernelVerbosity.TRACE,
) -> ExecutionKernel:
return ExecutionKernel(
max_slots=max_slots,
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=control_mode, verbosity=verbosity, backend_mode=BackendMode.MOCK)
),
venue=venue or MockVenueAdapter(),
journal=MemoryKernelJournal(),
zinc_plane=InMemoryZincPlane(),
account=AccountProjection(),
)
def _seed_open_slot(slot: TradeSlot, *, trade_id: str = "trade-1", asset: str = "BTCUSDT") -> None:
slot.trade_id = trade_id
slot.asset = asset
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.size = 1.0
slot.initial_size = 1.0
slot.leverage = 2.0
slot.fsm_state = TradeStage.POSITION_OPEN
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-00000001",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=1.0,
status=VenueOrderStatus.FILLED,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
def _seed_entry_order(slot: TradeSlot, *, trade_id: str = "trade-1", asset: str = "BTCUSDT", status: VenueOrderStatus = VenueOrderStatus.NEW) -> None:
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-00000001",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=1.0,
status=status,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
def _seed_exit_order(slot: TradeSlot, *, trade_id: str = "trade-1", asset: str = "BTCUSDT", intended_size: float = 0.5) -> None:
slot.active_exit_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-00000002",
venue_client_id=f"{trade_id}:exit",
side=TradeSide.SHORT,
intended_size=intended_size,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
def _configure_slot_state(slot: TradeSlot, state: TradeStage, *, trade_id: str = "trade-1", asset: str = "BTCUSDT") -> None:
slot.trade_id = trade_id if state not in {TradeStage.IDLE, TradeStage.CLOSED} else ""
slot.asset = asset if state not in {TradeStage.IDLE, TradeStage.CLOSED} else ""
slot.side = TradeSide.SHORT if state not in {TradeStage.IDLE, TradeStage.CLOSED} else TradeSide.FLAT
slot.entry_price = 100.0 if state not in {TradeStage.IDLE, TradeStage.CLOSED} else 0.0
slot.size = 1.0 if state in {TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING, TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT, TradeStage.ENTRY_WORKING, TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT} else 0.0
slot.initial_size = slot.size
slot.leverage = 2.0 if state not in {TradeStage.IDLE, TradeStage.CLOSED} else 0.0
slot.fsm_state = state
slot.closed = state == TradeStage.CLOSED
slot.active_entry_order = None
slot.active_exit_order = None
if state in {TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT, TradeStage.ENTRY_WORKING, TradeStage.POSITION_OPEN, TradeStage.POSITION_OPENED}:
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-00000001",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=1.0,
status=VenueOrderStatus.NEW if state in {TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT, TradeStage.ENTRY_WORKING} else VenueOrderStatus.FILLED,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
if state in {TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT, TradeStage.EXIT_WORKING}:
slot.active_exit_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-00000002",
venue_client_id=f"{trade_id}:exit",
side=TradeSide.SHORT,
intended_size=0.5,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
# 18 invalid-intent slot tests
@pytest.mark.parametrize(
"slot_id,action,expected",
[
(-1, KernelCommandType.ENTER, KernelDiagnosticCode.INVALID_SLOT_ID),
(-1, KernelCommandType.EXIT, KernelDiagnosticCode.INVALID_SLOT_ID),
(-1, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.INVALID_SLOT_ID),
(-1, KernelCommandType.RECONCILE, KernelDiagnosticCode.INVALID_SLOT_ID),
(-1, KernelCommandType.CANCEL, KernelDiagnosticCode.INVALID_SLOT_ID),
(3, KernelCommandType.ENTER, KernelDiagnosticCode.INVALID_SLOT_ID),
(3, KernelCommandType.EXIT, KernelDiagnosticCode.INVALID_SLOT_ID),
(3, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.INVALID_SLOT_ID),
(3, KernelCommandType.RECONCILE, KernelDiagnosticCode.INVALID_SLOT_ID),
(3, KernelCommandType.CANCEL, KernelDiagnosticCode.INVALID_SLOT_ID),
(99, KernelCommandType.ENTER, KernelDiagnosticCode.INVALID_SLOT_ID),
(99, KernelCommandType.EXIT, KernelDiagnosticCode.INVALID_SLOT_ID),
(99, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.INVALID_SLOT_ID),
(99, KernelCommandType.RECONCILE, KernelDiagnosticCode.INVALID_SLOT_ID),
(99, KernelCommandType.CANCEL, KernelDiagnosticCode.INVALID_SLOT_ID),
(7, KernelCommandType.ENTER, KernelDiagnosticCode.INVALID_SLOT_ID),
(7, KernelCommandType.EXIT, KernelDiagnosticCode.INVALID_SLOT_ID),
(7, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.INVALID_SLOT_ID),
],
)
def test_kernel_rejects_invalid_slot_ids_with_codes(slot_id: int, action: KernelCommandType, expected: KernelDiagnosticCode) -> None:
kernel = mk_kernel(max_slots=3)
outcome = kernel.process_intent(mk_intent(slot_id=slot_id, action=action))
assert outcome.accepted is False
assert outcome.diagnostic_code == expected
assert outcome.details["reason"] == "INVALID_SLOT_ID"
# 20 entry-path tests
@pytest.mark.parametrize(
"scenario,expected_state,expected_code,expected_size",
[
(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.5), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.5),
(MockVenueScenario(emit_fill_on_submit=False, partial_fill_ratio=0.5), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.5),
(MockVenueScenario(reject_entries=True), TradeStage.IDLE, KernelDiagnosticCode.OK, 0.0),
(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.25), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.25),
(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.75), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.75),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=False, partial_fill_ratio=0.0), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.0),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=1.0), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=True, partial_fill_ratio=1.0), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=True, partial_fill_ratio=0.5), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.5),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=0.9), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.9),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=0.1), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.1),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=1.0, reject_entries=False), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=False, partial_fill_ratio=1.0), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=False, partial_fill_ratio=0.2), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.2),
(MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=False, partial_fill_ratio=0.3), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.3),
(MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=False, partial_fill_ratio=1.0), TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, 1.0),
(MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=False, partial_fill_ratio=0.0), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.0),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=0.6), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.6),
(MockVenueScenario(emit_ack_before_fill=True, emit_fill_on_submit=True, partial_fill_ratio=0.4), TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, 0.4),
],
)
def test_kernel_entry_path_matrix(
scenario: MockVenueScenario,
expected_state: TradeStage,
expected_code: KernelDiagnosticCode,
expected_size: float,
) -> None:
kernel = mk_kernel(venue=MockVenueAdapter(scenario))
outcome = kernel.process_intent(mk_intent())
assert outcome.accepted is True
assert outcome.diagnostic_code == expected_code
assert kernel.slot(0).fsm_state == expected_state
assert kernel.slot(0).size == pytest.approx(expected_size, abs=1e-6)
# 20 exit-path tests
@pytest.mark.parametrize(
"initial_state,event_kind,event_status,expected_state,expected_code",
[
(TradeStage.POSITION_OPEN, KernelEventKind.PARTIAL_FILL, VenueEventStatus.PARTIALLY_FILLED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(TradeStage.POSITION_OPEN, KernelEventKind.FULL_FILL, VenueEventStatus.FILLED, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(TradeStage.EXIT_REQUESTED, KernelEventKind.PARTIAL_FILL, VenueEventStatus.PARTIALLY_FILLED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(TradeStage.EXIT_REQUESTED, KernelEventKind.FULL_FILL, VenueEventStatus.FILLED, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(TradeStage.EXIT_SENT, KernelEventKind.PARTIAL_FILL, VenueEventStatus.PARTIALLY_FILLED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(TradeStage.EXIT_SENT, KernelEventKind.FULL_FILL, VenueEventStatus.FILLED, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelEventKind.PARTIAL_FILL, VenueEventStatus.PARTIALLY_FILLED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelEventKind.FULL_FILL, VenueEventStatus.FILLED, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelEventKind.CANCEL_ACK, VenueEventStatus.CANCELED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelEventKind.CANCEL_REJECT, VenueEventStatus.CANCELED_REJECTED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.CANCEL_REJECTED),
(TradeStage.POSITION_OPEN, KernelEventKind.CANCEL_ACK, VenueEventStatus.CANCELED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(TradeStage.POSITION_OPEN, KernelEventKind.CANCEL_REJECT, VenueEventStatus.CANCELED_REJECTED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.CANCEL_REJECTED),
(TradeStage.EXIT_REQUESTED, KernelEventKind.CANCEL_ACK, VenueEventStatus.CANCELED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(TradeStage.EXIT_SENT, KernelEventKind.CANCEL_ACK, VenueEventStatus.CANCELED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(TradeStage.EXIT_REQUESTED, KernelEventKind.CANCEL_REJECT, VenueEventStatus.CANCELED_REJECTED, TradeStage.EXIT_REQUESTED, KernelDiagnosticCode.CANCEL_REJECTED),
(TradeStage.EXIT_SENT, KernelEventKind.CANCEL_REJECT, VenueEventStatus.CANCELED_REJECTED, TradeStage.EXIT_SENT, KernelDiagnosticCode.CANCEL_REJECTED),
(TradeStage.POSITION_OPEN, KernelEventKind.ORDER_REJECT, VenueEventStatus.REJECTED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED),
(TradeStage.EXIT_WORKING, KernelEventKind.ORDER_REJECT, VenueEventStatus.REJECTED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED),
(TradeStage.EXIT_REQUESTED, KernelEventKind.ORDER_REJECT, VenueEventStatus.REJECTED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED),
(TradeStage.EXIT_SENT, KernelEventKind.ORDER_REJECT, VenueEventStatus.REJECTED, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED),
],
)
def test_kernel_exit_path_matrix(
initial_state: TradeStage,
event_kind: KernelEventKind,
event_status: VenueEventStatus,
expected_state: TradeStage,
expected_code: KernelDiagnosticCode,
) -> None:
kernel = mk_kernel()
slot = kernel.slot(0)
_configure_slot_state(slot, initial_state)
if event_kind in {KernelEventKind.ORDER_REJECT, KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL}:
_seed_exit_order(slot, trade_id=slot.trade_id or "trade-1", asset="BTCUSDT", intended_size=slot.size or 0.5)
if initial_state in {TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT, TradeStage.EXIT_WORKING} and event_kind in {
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
KernelEventKind.ORDER_ACK,
}:
_seed_exit_order(slot, trade_id=slot.trade_id or "trade-1", asset="BTCUSDT", intended_size=slot.size or 0.5)
outcome = kernel.on_venue_event(
mk_event(
kind=event_kind,
status=event_status,
trade_id=slot.trade_id or "trade-1",
venue_order_id=slot.active_exit_order.venue_order_id if slot.active_exit_order else "V-00000002",
venue_client_id=slot.active_exit_order.venue_client_id if slot.active_exit_order else "trade-1:exit",
side=TradeSide.SHORT,
asset="BTCUSDT",
size=float(slot.size or 0.5),
filled_size=float(slot.size or 0.5) if event_kind == KernelEventKind.FULL_FILL else float((slot.size or 0.5) / 2.0),
remaining_size=0.0,
)
)
assert outcome.diagnostic_code == expected_code
assert kernel.slot(0).fsm_state == expected_state
# 18 event-resolution tests
@pytest.mark.parametrize(
"event,initial_state,expected_state,expected_code",
[
(mk_event(kind=KernelEventKind.ORDER_ACK, status=VenueEventStatus.ACKED), TradeStage.IDLE, TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.ORDER_ACK, status=VenueEventStatus.ACKED), TradeStage.EXIT_REQUESTED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.ORDER_REJECT, status=VenueEventStatus.REJECTED), TradeStage.ENTRY_WORKING, TradeStage.IDLE, KernelDiagnosticCode.ENTRY_ORDER_REJECTED),
(mk_event(kind=KernelEventKind.ORDER_REJECT, status=VenueEventStatus.REJECTED), TradeStage.EXIT_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED),
(mk_event(kind=KernelEventKind.ORDER_REJECT, status=VenueEventStatus.REJECTED), TradeStage.IDLE, TradeStage.IDLE, KernelDiagnosticCode.ORDER_REJECTED),
(mk_event(kind=KernelEventKind.PARTIAL_FILL, status=VenueEventStatus.PARTIALLY_FILLED), TradeStage.ENTRY_WORKING, TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.FULL_FILL, status=VenueEventStatus.FILLED), TradeStage.ENTRY_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.PARTIAL_FILL, status=VenueEventStatus.PARTIALLY_FILLED), TradeStage.EXIT_WORKING, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.FULL_FILL, status=VenueEventStatus.FILLED), TradeStage.EXIT_WORKING, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.CANCEL_ACK, status=VenueEventStatus.CANCELED), TradeStage.EXIT_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.CANCEL_REJECT, status=VenueEventStatus.CANCELED_REJECTED), TradeStage.EXIT_WORKING, TradeStage.EXIT_WORKING, KernelDiagnosticCode.CANCEL_REJECTED),
(mk_event(kind=KernelEventKind.MARK_PRICE, status=VenueEventStatus.ACKED), TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.RECONCILE, status=VenueEventStatus.ACKED), TradeStage.POSITION_OPEN, TradeStage.STALE_STATE_RECONCILING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.ORDER_ACK, status=VenueEventStatus.ACKED, venue_order_id="V-2"), TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.ORDER_ACK, status=VenueEventStatus.ACKED, venue_order_id="V-3"), TradeStage.ENTRY_WORKING, TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.FULL_FILL, status=VenueEventStatus.FILLED, venue_order_id="V-4"), TradeStage.EXIT_WORKING, TradeStage.CLOSED, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.CANCEL_ACK, status=VenueEventStatus.CANCELED, venue_order_id="V-5"), TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
(mk_event(kind=KernelEventKind.CANCEL_REJECT, status=VenueEventStatus.CANCELED_REJECTED, venue_order_id="V-6"), TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.CANCEL_REJECTED),
],
)
def test_kernel_event_matrix(event: VenueEvent, initial_state: TradeStage, expected_state: TradeStage, expected_code: KernelDiagnosticCode) -> None:
kernel = mk_kernel()
slot = kernel.slot(0)
_configure_slot_state(slot, initial_state)
entry_states = {TradeStage.IDLE, TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT, TradeStage.ENTRY_WORKING}
exit_states = {TradeStage.POSITION_OPEN, TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT, TradeStage.EXIT_WORKING}
if initial_state in entry_states and event.kind in {KernelEventKind.ORDER_ACK, KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL}:
_seed_entry_order(slot, trade_id="trade-1", asset="BTCUSDT")
elif initial_state == TradeStage.ENTRY_WORKING and event.kind == KernelEventKind.ORDER_REJECT:
_seed_entry_order(slot, trade_id="trade-1", asset="BTCUSDT")
if initial_state in exit_states:
if event.kind == KernelEventKind.ORDER_REJECT:
_seed_exit_order(slot, trade_id="trade-1", asset="BTCUSDT", intended_size=1.0)
elif event.kind in {KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL}:
_seed_exit_order(slot, trade_id="trade-1", asset="BTCUSDT", intended_size=1.0)
elif initial_state in {TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT, TradeStage.EXIT_WORKING} and event.kind in {
KernelEventKind.ORDER_ACK,
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
}:
_seed_exit_order(slot, trade_id="trade-1", asset="BTCUSDT", intended_size=1.0)
if initial_state == TradeStage.POSITION_OPEN and event.kind == KernelEventKind.ORDER_ACK:
slot.active_entry_order = None
fill_size = 1.0 if event.kind == KernelEventKind.FULL_FILL else 0.5 if event.kind == KernelEventKind.PARTIAL_FILL else 0.0
resolved_event = mk_event(
kind=event.kind,
status=event.status,
trade_id=event.trade_id,
slot_id=event.slot_id,
venue_order_id=slot.active_entry_order.venue_order_id if slot.active_entry_order else slot.active_exit_order.venue_order_id if slot.active_exit_order else event.venue_order_id,
venue_client_id=slot.active_entry_order.venue_client_id if slot.active_entry_order else slot.active_exit_order.venue_client_id if slot.active_exit_order else event.venue_client_id,
side=event.side,
asset=event.asset,
price=event.price,
size=1.0,
filled_size=fill_size,
remaining_size=max(0.0, 1.0 - fill_size),
reason=event.reason,
)
outcome = kernel.on_venue_event(resolved_event)
assert outcome.state == expected_state
assert outcome.diagnostic_code == expected_code
def test_kernel_rate_limited_event_is_characterized_without_state_drift() -> None:
kernel = mk_kernel()
slot = kernel.slot(0)
_configure_slot_state(slot, TradeStage.ENTRY_WORKING)
_seed_entry_order(slot, trade_id="trade-rate-limit", asset="BTCUSDT")
before = slot.to_dict()
outcome = kernel.on_venue_event(
mk_event(
kind=KernelEventKind.RATE_LIMITED,
status=VenueEventStatus.RATE_LIMITED,
trade_id="trade-rate-limit",
venue_order_id="V-RATE-LIMITED",
venue_client_id="trade-rate-limit:entry",
reason="code:100410 endpoint is in disabled/frequency-limited period",
size=1.0,
filled_size=0.0,
remaining_size=1.0,
)
)
after = kernel.slot(0).to_dict()
assert outcome.accepted is False
assert outcome.diagnostic_code == KernelDiagnosticCode.RATE_LIMITED
assert outcome.severity == KernelSeverity.WARNING
assert outcome.details["venue_event_kind"] == KernelEventKind.RATE_LIMITED.value
assert outcome.details["severity"] == KernelSeverity.WARNING.value
assert outcome.details["release_eta"] == "few minutes"
assert outcome.details["retryable"] is True
assert after["fsm_state"] == before["fsm_state"]
assert after["trade_id"] == before["trade_id"]
assert after["size"] == before["size"]
# 24 fuzz cases
@pytest.mark.parametrize("seed", list(range(24)))
def test_kernel_fuzz_event_sequences(seed: int) -> None:
rng = random.Random(seed)
kernel = mk_kernel(max_slots=4)
current_trade_id = f"trade-{seed}"
# Seed one slot open for exit/reconcile fuzzing.
seed_slot = kernel.slot(0)
_seed_open_slot(seed_slot, trade_id=current_trade_id)
seed_slot.exit_leg_ratios = (0.25, 0.25, 0.5)
kinds = [
KernelEventKind.ORDER_ACK,
KernelEventKind.ORDER_REJECT,
KernelEventKind.PARTIAL_FILL,
KernelEventKind.FULL_FILL,
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
KernelEventKind.MARK_PRICE,
KernelEventKind.RECONCILE,
]
for idx in range(12):
kind = rng.choice(kinds)
if kind in {KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_REJECT}:
seed_slot.active_entry_order = VenueOrder(
internal_trade_id=current_trade_id,
venue_order_id=f"V-{seed:04d}-{idx:02d}",
venue_client_id=f"{current_trade_id}:entry-{idx}",
side=TradeSide.SHORT,
intended_size=1.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": 0, "asset": "BTCUSDT"},
)
if kind in {KernelEventKind.CANCEL_ACK, KernelEventKind.CANCEL_REJECT, KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL}:
seed_slot.active_exit_order = VenueOrder(
internal_trade_id=current_trade_id,
venue_order_id=f"V-{seed:04d}-{idx:02d}",
venue_client_id=f"{current_trade_id}:exit-{idx}",
side=TradeSide.SHORT,
intended_size=0.5,
filled_size=0.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": 0, "asset": "BTCUSDT"},
)
event = mk_event(kind=kind, status=_status_for_kind(kind), trade_id=current_trade_id, venue_order_id=f"V-{seed:04d}-{idx:02d}", venue_client_id=f"{current_trade_id}:{idx}")
outcome = kernel.on_venue_event(event)
assert isinstance(outcome, KernelOutcome)
assert outcome.diagnostic_code in set(KernelDiagnosticCode)
assert kernel.slot(0).fsm_state in set(TradeStage)
def _status_for_kind(kind: KernelEventKind) -> VenueEventStatus:
return {
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
}[kind]
# 22 explicit edge-condition tests
@pytest.mark.parametrize(
"slot_state,action,expected_code",
[
(TradeStage.IDLE, KernelCommandType.EXIT, KernelDiagnosticCode.NO_OPEN_POSITION),
(TradeStage.CLOSED, KernelCommandType.EXIT, KernelDiagnosticCode.NO_OPEN_POSITION),
(TradeStage.POSITION_OPEN, KernelCommandType.CANCEL, KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER),
(TradeStage.IDLE, KernelCommandType.CANCEL, KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER),
(TradeStage.IDLE, KernelCommandType.RECONCILE, KernelDiagnosticCode.STALE_STATE_RECONCILE),
(TradeStage.POSITION_OPEN, KernelCommandType.RECONCILE, KernelDiagnosticCode.STALE_STATE_RECONCILE),
(TradeStage.POSITION_OPEN, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.ENTRY_WORKING, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.ORDER_REQUESTED, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.ORDER_SENT, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.EXIT_REQUESTED, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.EXIT_SENT, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.STALE_STATE_RECONCILING, KernelCommandType.MARK_PRICE, KernelDiagnosticCode.OK),
(TradeStage.POSITION_OPEN, KernelCommandType.ENTER, KernelDiagnosticCode.SLOT_BUSY),
(TradeStage.EXIT_WORKING, KernelCommandType.ENTER, KernelDiagnosticCode.SLOT_BUSY),
(TradeStage.ORDER_REQUESTED, KernelCommandType.ENTER, KernelDiagnosticCode.SLOT_BUSY),
(TradeStage.ORDER_SENT, KernelCommandType.ENTER, KernelDiagnosticCode.SLOT_BUSY),
(TradeStage.POSITION_OPEN, KernelCommandType.EXIT, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelCommandType.EXIT, KernelDiagnosticCode.OK),
(TradeStage.POSITION_OPEN, KernelCommandType.CANCEL, KernelDiagnosticCode.OK),
(TradeStage.EXIT_WORKING, KernelCommandType.CANCEL, KernelDiagnosticCode.OK),
],
)
def test_kernel_action_edge_conditions(slot_state: TradeStage, action: KernelCommandType, expected_code: KernelDiagnosticCode) -> None:
kernel = mk_kernel(venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)))
slot = kernel.slot(0)
_configure_slot_state(slot, slot_state)
if action == KernelCommandType.ENTER and expected_code == KernelDiagnosticCode.SLOT_BUSY:
slot.trade_id = f"occupied-{slot_state.value.lower()}"
if action == KernelCommandType.CANCEL and expected_code == KernelDiagnosticCode.OK:
_seed_exit_order(slot, trade_id=slot.trade_id or "trade-1", asset=slot.asset or "BTCUSDT", intended_size=0.5)
outcome = kernel.process_intent(mk_intent(action=action, target_size=0.5, exit_leg_ratios=(0.25, 0.25, 0.5)))
assert outcome.diagnostic_code == expected_code
# 20 transition-detail tests
@pytest.mark.parametrize("mode", [KernelMode.NORMAL, KernelMode.DEBUG])
@pytest.mark.parametrize("verbosity", [KernelVerbosity.QUIET, KernelVerbosity.TRACE])
@pytest.mark.parametrize("control_enabled", [True, False])
@pytest.mark.parametrize("closed", [True, False])
@pytest.mark.parametrize("state", [TradeStage.IDLE, TradeStage.POSITION_OPEN])
def test_transition_details_and_control_modes_are_captured(
mode: KernelMode,
verbosity: KernelVerbosity,
control_enabled: bool,
closed: bool,
state: TradeStage,
) -> None:
kernel = mk_kernel()
if control_enabled:
kernel.update_control(
ControlUpdate(
mode=mode,
verbosity=verbosity,
trace_transitions=True,
)
)
slot = kernel.slot(0)
_seed_open_slot(slot)
slot.fsm_state = state
slot.closed = closed
event = mk_event(kind=KernelEventKind.MARK_PRICE, status=VenueEventStatus.ACKED)
outcome = kernel.on_venue_event(event)
assert outcome.transitions
transition = outcome.transitions[0]
assert transition.control_mode in {KernelMode.NORMAL.value, KernelMode.DEBUG.value}
assert transition.control_verbosity in {KernelVerbosity.QUIET.value, KernelVerbosity.TRACE.value}
assert "asset" in transition.details
assert "side" in transition.details

View File

@@ -1,903 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import random
import pytest
from prod.clean_arch.dita_v2 import (
AccountProjection,
BackendMode,
ControlUpdate,
ExecutionKernel,
HazelcastProjection,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelControlSnapshot,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelMode,
KernelOutcome,
KernelVerbosity,
MemoryKernelJournal,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
@dataclass(frozen=True)
class KernelRig:
kernel: ExecutionKernel
journal: MemoryKernelJournal
zinc: InMemoryZincPlane
projection: HazelcastProjection
sink: "CaptureSink"
@dataclass(frozen=True)
class EntryCase:
name: str
scenario: MockVenueScenario
expected_state: TradeStage
expected_size: float
rejected: bool = False
@dataclass(frozen=True)
class ExitCase:
name: str
exit_leg_ratios: tuple[float, ...]
fill_ratio: float
expected_state: TradeStage
expected_size: float
expected_leg_index: int
@dataclass(frozen=True)
class EventCase:
name: str
kind: KernelEventKind
initial_state: TradeStage
expected_state: TradeStage
expected_code: KernelDiagnosticCode
family: str
@dataclass(frozen=True)
class ReconcileCase:
name: str
slots: tuple[TradeSlot, ...]
expected_open_positions: int
expected_trade_ids: tuple[str, ...]
class CaptureSink:
def __init__(self) -> None:
self.rows: list[tuple[str, dict[str, object]]] = []
def __call__(self, name: str, row: dict[str, object]) -> None:
self.rows.append((name, dict(row)))
def _build_kernel(
*,
venue: MockVenueAdapter | None = None,
mode: KernelMode = KernelMode.DEBUG,
verbosity: KernelVerbosity = KernelVerbosity.TRACE,
backend_mode: BackendMode = BackendMode.MOCK,
trace_transitions: bool = True,
) -> KernelRig:
sink = CaptureSink()
journal = MemoryKernelJournal()
zinc = InMemoryZincPlane()
projection = HazelcastProjection(writer=sink)
control_plane = InMemoryControlPlane(
KernelControlSnapshot(
mode=mode,
verbosity=verbosity,
backend_mode=backend_mode,
trace_transitions=trace_transitions,
debug_clickhouse_enabled=True,
mirror_to_hazelcast=True,
)
)
kernel = ExecutionKernel(
max_slots=4,
control_plane=control_plane,
venue=venue or MockVenueAdapter(),
journal=journal,
account=AccountProjection(),
projection=projection,
zinc_plane=zinc,
)
return KernelRig(kernel=kernel, journal=journal, zinc=zinc, projection=projection, sink=sink)
def _seed_entry_working(slot: TradeSlot, *, trade_id: str = "trade-1", asset: str = "BTCUSDT") -> None:
slot.trade_id = trade_id
slot.asset = asset
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.size = 1.0
slot.initial_size = 1.0
slot.leverage = 2.0
slot.closed = False
slot.exit_leg_ratios = (1.0,)
slot.active_leg_index = 0
slot.fsm_state = TradeStage.ENTRY_WORKING
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-ENTRY-1",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=1.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
slot.active_exit_order = None
def _seed_position_open(
slot: TradeSlot,
*,
trade_id: str = "trade-1",
asset: str = "BTCUSDT",
exit_leg_ratios: tuple[float, ...] = (1.0,),
) -> None:
_seed_entry_working(slot, trade_id=trade_id, asset=asset)
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-ENTRY-1",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=1.0,
filled_size=1.0,
average_fill_price=100.0,
status=VenueOrderStatus.FILLED,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
slot.fsm_state = TradeStage.POSITION_OPEN
slot.size = 1.0
slot.initial_size = 1.0
slot.exit_leg_ratios = tuple(exit_leg_ratios)
slot.active_leg_index = 0
def _seed_exit_working(
slot: TradeSlot,
*,
trade_id: str = "trade-1",
asset: str = "BTCUSDT",
exit_leg_ratios: tuple[float, ...] = (1.0,),
active_leg_index: int = 0,
) -> None:
_seed_position_open(slot, trade_id=trade_id, asset=asset, exit_leg_ratios=exit_leg_ratios)
slot.fsm_state = TradeStage.EXIT_WORKING
slot.active_leg_index = active_leg_index
slot.active_exit_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-EXIT-1",
venue_client_id=f"{trade_id}:exit",
side=TradeSide.SHORT,
intended_size=max(0.0, 1.0 * float(exit_leg_ratios[active_leg_index if active_leg_index < len(exit_leg_ratios) else 0])),
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
def _seed_idle(slot: TradeSlot) -> None:
slot.trade_id = ""
slot.asset = ""
slot.side = TradeSide.FLAT
slot.entry_price = 0.0
slot.size = 0.0
slot.initial_size = 0.0
slot.leverage = 0.0
slot.entry_time = None
slot.unrealized_pnl = 0.0
slot.realized_pnl = 0.0
slot.closed = False
slot.exit_leg_ratios = (1.0,)
slot.active_leg_index = 0
slot.active_exit_order = None
slot.active_entry_order = None
slot.fsm_state = TradeStage.IDLE
slot.close_reason = ""
slot.last_event_time = None
slot.metadata = {}
def _seed_closed(slot: TradeSlot, *, trade_id: str = "trade-1", asset: str = "BTCUSDT") -> None:
slot.trade_id = trade_id
slot.asset = asset
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.size = 0.0
slot.initial_size = 1.0
slot.leverage = 2.0
slot.closed = True
slot.exit_leg_ratios = (1.0,)
slot.active_leg_index = 1
slot.active_exit_order = None
slot.active_entry_order = None
slot.fsm_state = TradeStage.CLOSED
slot.close_reason = "EXIT_FILLED"
def _make_event(
*,
kind: KernelEventKind,
status: VenueEventStatus,
trade_id: str = "trade-1",
slot_id: int = 0,
venue_order_id: str = "V-ORDER-1",
venue_client_id: str = "trade-1:client-1",
side: TradeSide = TradeSide.SHORT,
asset: str = "BTCUSDT",
price: float = 100.0,
size: float = 1.0,
filled_size: float = 1.0,
remaining_size: float = 0.0,
reason: str = "",
) -> VenueEvent:
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"evt-{kind.value.lower()}-{slot_id}-{trade_id}",
trade_id=trade_id,
slot_id=slot_id,
kind=kind,
status=status,
venue_order_id=venue_order_id,
venue_client_id=venue_client_id,
side=side,
asset=asset,
price=price,
size=size,
filled_size=filled_size,
remaining_size=remaining_size,
reason=reason,
raw_payload={"status": status.value, "kind": kind.value},
)
ENTRY_CASES = [
EntryCase(
name="full_fill_immediate",
scenario=MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0),
expected_state=TradeStage.POSITION_OPEN,
expected_size=1.0,
),
EntryCase(
name="partial_50_immediate",
scenario=MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.5),
expected_state=TradeStage.ENTRY_WORKING,
expected_size=0.5,
),
EntryCase(
name="partial_50_ack_then_fill",
scenario=MockVenueScenario(emit_fill_on_submit=False, partial_fill_ratio=0.5),
expected_state=TradeStage.ENTRY_WORKING,
expected_size=0.5,
),
EntryCase(
name="no_fill_ack_only",
scenario=MockVenueScenario(emit_fill_on_submit=False, partial_fill_ratio=0.0),
expected_state=TradeStage.ENTRY_WORKING,
expected_size=0.0,
),
EntryCase(
name="ack_before_fill_full",
scenario=MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=True, partial_fill_ratio=1.0),
expected_state=TradeStage.POSITION_OPEN,
expected_size=1.0,
),
EntryCase(
name="ack_before_fill_partial",
scenario=MockVenueScenario(emit_ack_before_fill=False, emit_fill_on_submit=True, partial_fill_ratio=0.25),
expected_state=TradeStage.ENTRY_WORKING,
expected_size=0.25,
),
EntryCase(
name="reject_entry",
scenario=MockVenueScenario(reject_entries=True),
expected_state=TradeStage.IDLE,
expected_size=0.0,
rejected=True,
),
EntryCase(
name="three_quarters_fill",
scenario=MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.75),
expected_state=TradeStage.ENTRY_WORKING,
expected_size=0.75,
),
]
EXIT_CASES = [
ExitCase(name="single_leg_full", exit_leg_ratios=(1.0,), fill_ratio=1.0, expected_state=TradeStage.CLOSED, expected_size=0.0, expected_leg_index=1),
ExitCase(name="single_leg_partial", exit_leg_ratios=(1.0,), fill_ratio=0.5, expected_state=TradeStage.EXIT_WORKING, expected_size=0.5, expected_leg_index=0),
ExitCase(name="two_leg_full", exit_leg_ratios=(0.5, 0.5), fill_ratio=1.0, expected_state=TradeStage.POSITION_OPEN, expected_size=0.5, expected_leg_index=1),
ExitCase(name="two_leg_partial", exit_leg_ratios=(0.5, 0.5), fill_ratio=0.25, expected_state=TradeStage.EXIT_WORKING, expected_size=0.875, expected_leg_index=0),
ExitCase(name="three_leg_full", exit_leg_ratios=(0.25, 0.25, 0.5), fill_ratio=1.0, expected_state=TradeStage.POSITION_OPEN, expected_size=0.75, expected_leg_index=1),
ExitCase(name="three_leg_partial", exit_leg_ratios=(0.25, 0.25, 0.5), fill_ratio=0.5, expected_state=TradeStage.EXIT_WORKING, expected_size=0.875, expected_leg_index=0),
ExitCase(name="tilted_full", exit_leg_ratios=(0.2, 0.3, 0.5), fill_ratio=1.0, expected_state=TradeStage.POSITION_OPEN, expected_size=0.8, expected_leg_index=1),
ExitCase(name="tilted_partial", exit_leg_ratios=(0.2, 0.3, 0.5), fill_ratio=0.25, expected_state=TradeStage.EXIT_WORKING, expected_size=0.95, expected_leg_index=0),
ExitCase(name="four_leg_full", exit_leg_ratios=(0.1, 0.2, 0.3, 0.4), fill_ratio=1.0, expected_state=TradeStage.POSITION_OPEN, expected_size=0.9, expected_leg_index=1),
ExitCase(name="four_leg_partial", exit_leg_ratios=(0.1, 0.2, 0.3, 0.4), fill_ratio=0.5, expected_state=TradeStage.EXIT_WORKING, expected_size=0.95, expected_leg_index=0),
ExitCase(name="balanced_full", exit_leg_ratios=(0.33, 0.33, 0.34), fill_ratio=1.0, expected_state=TradeStage.POSITION_OPEN, expected_size=0.67, expected_leg_index=1),
ExitCase(name="balanced_partial", exit_leg_ratios=(0.33, 0.33, 0.34), fill_ratio=0.25, expected_state=TradeStage.EXIT_WORKING, expected_size=0.9175, expected_leg_index=0),
]
EVENT_CASES = [
EventCase("ack_entry", KernelEventKind.ORDER_ACK, TradeStage.ENTRY_WORKING, TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, "entry"),
EventCase("ack_exit", KernelEventKind.ORDER_ACK, TradeStage.EXIT_REQUESTED, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK, "exit"),
EventCase("reject_entry", KernelEventKind.ORDER_REJECT, TradeStage.ENTRY_WORKING, TradeStage.IDLE, KernelDiagnosticCode.ENTRY_ORDER_REJECTED, "entry"),
EventCase("reject_exit", KernelEventKind.ORDER_REJECT, TradeStage.EXIT_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.EXIT_ORDER_REJECTED, "exit"),
EventCase("reject_idle", KernelEventKind.ORDER_REJECT, TradeStage.IDLE, TradeStage.IDLE, KernelDiagnosticCode.ORDER_REJECTED, "none"),
EventCase("partial_entry", KernelEventKind.PARTIAL_FILL, TradeStage.ENTRY_WORKING, TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK, "entry"),
EventCase("full_entry", KernelEventKind.FULL_FILL, TradeStage.ENTRY_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, "entry"),
EventCase("partial_exit", KernelEventKind.PARTIAL_FILL, TradeStage.EXIT_WORKING, TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK, "exit"),
EventCase("full_exit", KernelEventKind.FULL_FILL, TradeStage.EXIT_WORKING, TradeStage.CLOSED, KernelDiagnosticCode.OK, "exit"),
EventCase("cancel_ack_exit", KernelEventKind.CANCEL_ACK, TradeStage.EXIT_WORKING, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, "exit"),
EventCase("cancel_reject_exit", KernelEventKind.CANCEL_REJECT, TradeStage.EXIT_WORKING, TradeStage.EXIT_WORKING, KernelDiagnosticCode.CANCEL_REJECTED, "exit"),
EventCase("mark_price_open", KernelEventKind.MARK_PRICE, TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, "none"),
EventCase("reconcile_open", KernelEventKind.RECONCILE, TradeStage.POSITION_OPEN, TradeStage.STALE_STATE_RECONCILING, KernelDiagnosticCode.OK, "none"),
EventCase("ack_open_no_entry", KernelEventKind.ORDER_ACK, TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, "none"),
EventCase("cancel_ack_open_no_exit", KernelEventKind.CANCEL_ACK, TradeStage.POSITION_OPEN, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK, "none"),
]
RECONCILE_CASES = [
ReconcileCase(
name="empty_payload",
slots=(),
expected_open_positions=0,
expected_trade_ids=(),
),
ReconcileCase(
name="single_open",
slots=(
TradeSlot(
slot_id=0,
trade_id="trade-a",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
),
),
expected_open_positions=1,
expected_trade_ids=("trade-a",),
),
ReconcileCase(
name="open_and_exit",
slots=(
TradeSlot(
slot_id=0,
trade_id="trade-b",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=0.5,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.EXIT_WORKING,
),
TradeSlot(
slot_id=1,
trade_id="trade-c",
asset="ETHUSDT",
side=TradeSide.LONG,
entry_price=50.0,
size=0.0,
initial_size=1.0,
leverage=3.0,
closed=True,
fsm_state=TradeStage.CLOSED,
),
),
expected_open_positions=1,
expected_trade_ids=("trade-b", "trade-c"),
),
ReconcileCase(
name="mixed_three",
slots=(
TradeSlot(
slot_id=0,
trade_id="trade-d",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
),
TradeSlot(
slot_id=1,
trade_id="trade-e",
asset="ETHUSDT",
side=TradeSide.LONG,
entry_price=50.0,
size=0.0,
initial_size=1.0,
leverage=3.0,
closed=True,
fsm_state=TradeStage.CLOSED,
),
TradeSlot(
slot_id=2,
trade_id="trade-f",
asset="SOLUSDT",
side=TradeSide.SHORT,
entry_price=20.0,
size=0.25,
initial_size=1.0,
leverage=4.0,
fsm_state=TradeStage.EXIT_WORKING,
),
),
expected_open_positions=2,
expected_trade_ids=("trade-d", "trade-e", "trade-f"),
),
]
def _event_order_id(case: EventCase, resolver: str) -> str:
if case.family == "entry":
return "V-ENTRY-1"
if case.family == "exit":
return "V-EXIT-1"
if resolver == "order_id":
return "V-MISSING"
return "V-ORDER-1"
def _event_client_id(case: EventCase, resolver: str) -> str:
if case.family == "entry":
return "trade-1:entry"
if case.family == "exit":
return "trade-1:exit"
if resolver == "order_id":
return "trade-x:missing"
return "trade-1:client-1"
@pytest.mark.parametrize("mode", [KernelMode.NORMAL, KernelMode.DEBUG])
@pytest.mark.parametrize("verbosity", [KernelVerbosity.QUIET, KernelVerbosity.VERBOSE, KernelVerbosity.TRACE])
@pytest.mark.parametrize("backend_mode", [BackendMode.MOCK, BackendMode.BINGX])
@pytest.mark.parametrize("trace_transitions", [True, False])
def test_kernel_control_plane_matrix(
mode: KernelMode,
verbosity: KernelVerbosity,
backend_mode: BackendMode,
trace_transitions: bool,
) -> None:
rig = _build_kernel()
snapshot = rig.kernel.update_control(
ControlUpdate(
mode=mode,
verbosity=verbosity,
backend_mode=backend_mode,
trace_transitions=trace_transitions,
)
)
assert snapshot.mode == mode
assert snapshot.verbosity == verbosity
assert snapshot.backend_mode == backend_mode
assert snapshot.trace_transitions == trace_transitions
assert rig.kernel.control.mode == mode
assert rig.kernel.control.verbosity == verbosity
assert rig.zinc.read_control().mode == mode
assert rig.zinc.read_control().verbosity == verbosity
assert rig.projection.control_snapshot is not None
assert rig.projection.control_snapshot.mode == mode
assert rig.sink.rows[-1][0] == "hz:dita_control"
assert rig.sink.rows[-1][1]["mode"] == mode.value
assert rig.sink.rows[-1][1]["backend_mode"] == backend_mode.value
@pytest.mark.parametrize("mode", [KernelMode.NORMAL, KernelMode.DEBUG])
@pytest.mark.parametrize("verbosity", [KernelVerbosity.QUIET, KernelVerbosity.TRACE])
@pytest.mark.parametrize("case", ENTRY_CASES, ids=[case.name for case in ENTRY_CASES])
def test_kernel_entry_matrix(
mode: KernelMode,
verbosity: KernelVerbosity,
case: EntryCase,
) -> None:
rig = _build_kernel(
venue=MockVenueAdapter(case.scenario),
mode=mode,
verbosity=verbosity,
backend_mode=BackendMode.MOCK,
)
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="intent-entry",
trade_id="trade-1",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=100.0,
target_size=1.0,
leverage=2.0,
exit_leg_ratios=(1.0,),
reason=case.name,
)
)
slot = rig.kernel.slot(0)
assert outcome.accepted is True
assert outcome.diagnostic_code == KernelDiagnosticCode.OK
assert slot.fsm_state == case.expected_state
assert slot.size == pytest.approx(case.expected_size, abs=1e-6)
assert rig.zinc.intent_region[-1].action == KernelCommandType.ENTER
assert rig.zinc.state_region[0].fsm_state == case.expected_state
assert rig.journal.rows
if case.rejected:
assert slot.trade_id == ""
assert slot.asset == ""
assert slot.active_entry_order is None
assert slot.active_exit_order is None
if case.expected_state == TradeStage.POSITION_OPEN:
assert slot.closed is False
@pytest.mark.parametrize("case", EXIT_CASES, ids=[case.name for case in EXIT_CASES])
def test_kernel_exit_matrix(case: ExitCase) -> None:
rig = _build_kernel(venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=case.fill_ratio)))
slot = rig.kernel.slot(0)
_seed_position_open(slot, exit_leg_ratios=case.exit_leg_ratios)
slot.active_entry_order = None
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="intent-exit",
trade_id=slot.trade_id,
slot_id=0,
asset=slot.asset,
side=slot.side,
action=KernelCommandType.EXIT,
reference_price=99.0,
target_size=case.exit_leg_ratios[0],
leverage=slot.leverage,
exit_leg_ratios=case.exit_leg_ratios,
reason=case.name,
)
)
assert outcome.accepted is True
assert outcome.diagnostic_code == KernelDiagnosticCode.OK
assert slot.fsm_state == case.expected_state
assert slot.active_leg_index == case.expected_leg_index
assert slot.size == pytest.approx(case.expected_size, abs=1e-6)
if case.expected_state == TradeStage.CLOSED:
assert slot.closed is True
assert slot.active_exit_order is None
assert slot.active_entry_order is None
else:
assert slot.closed is False
assert slot.active_exit_order is None or slot.active_exit_order.status in {
VenueOrderStatus.PARTIALLY_FILLED,
VenueOrderStatus.NEW,
}
@pytest.mark.parametrize("resolver", ["slot_id", "trade_id", "order_id"])
@pytest.mark.parametrize("case", EVENT_CASES, ids=[case.name for case in EVENT_CASES])
def test_kernel_event_resolution_matrix(case: EventCase, resolver: str) -> None:
rig = _build_kernel()
slot = rig.kernel.slot(0)
if case.family == "entry":
_seed_entry_working(slot)
elif case.family == "exit":
_seed_exit_working(slot)
elif case.initial_state == TradeStage.POSITION_OPEN:
_seed_position_open(slot)
elif case.initial_state == TradeStage.IDLE:
_seed_idle(slot)
if resolver == "slot_id":
event_slot_id = 0
event_trade_id = "mismatch-trade"
event_venue_order_id = "V-MISMATCH"
elif resolver == "trade_id":
event_slot_id = 99
event_trade_id = slot.trade_id or "trade-1"
event_venue_order_id = "V-MISMATCH"
else:
event_slot_id = 99
event_trade_id = "mismatch-trade"
event_venue_order_id = "V-ENTRY-1" if case.family == "entry" else "V-EXIT-1"
if case.kind == KernelEventKind.ORDER_ACK and case.family == "none":
slot.active_entry_order = None
slot.active_exit_order = None
if case.kind == KernelEventKind.CANCEL_ACK and case.family == "none":
slot.active_exit_order = None
if case.kind == KernelEventKind.ORDER_REJECT and case.family == "exit":
slot.active_entry_order = None
if case.kind in {KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_REJECT, KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL} and case.family == "entry" and resolver != "order_id":
event_venue_order_id = slot.active_entry_order.venue_order_id
event_trade_id = slot.trade_id
if case.kind in {KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_REJECT, KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL, KernelEventKind.CANCEL_ACK, KernelEventKind.CANCEL_REJECT} and case.family == "exit" and resolver != "order_id":
event_venue_order_id = slot.active_exit_order.venue_order_id
event_trade_id = slot.trade_id
price = 98.0 if case.kind == KernelEventKind.MARK_PRICE else 100.0
filled_size = 1.0 if case.kind in {KernelEventKind.FULL_FILL, KernelEventKind.ORDER_ACK} else 0.5
remaining_size = max(0.0, 1.0 - filled_size)
event = _make_event(
kind=case.kind,
status={
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
}[case.kind],
trade_id=event_trade_id,
slot_id=event_slot_id,
venue_order_id=event_venue_order_id,
venue_client_id=_event_client_id(case, resolver),
side=TradeSide.SHORT,
asset="BTCUSDT",
price=price,
size=1.0,
filled_size=filled_size,
remaining_size=remaining_size,
)
outcome = rig.kernel.on_venue_event(event)
assert outcome.state == case.expected_state
assert outcome.diagnostic_code == case.expected_code
if case.kind == KernelEventKind.MARK_PRICE:
assert slot.unrealized_pnl > 0.0
if case.kind == KernelEventKind.ORDER_REJECT and case.family == "entry":
assert slot.trade_id == ""
assert slot.asset == ""
assert slot.size == 0.0
if case.kind == KernelEventKind.ORDER_REJECT and case.family == "exit":
assert slot.fsm_state == TradeStage.POSITION_OPEN
assert slot.active_exit_order is None
if case.kind == KernelEventKind.FULL_FILL and case.family == "entry":
assert slot.fsm_state == TradeStage.POSITION_OPEN
if case.kind == KernelEventKind.FULL_FILL and case.family == "exit" and case.expected_state == TradeStage.CLOSED:
assert slot.closed is True
@pytest.mark.parametrize("case", RECONCILE_CASES, ids=[case.name for case in RECONCILE_CASES])
@pytest.mark.parametrize("mode", [KernelMode.NORMAL, KernelMode.DEBUG])
@pytest.mark.parametrize("verbosity", [KernelVerbosity.QUIET, KernelVerbosity.TRACE])
def test_kernel_reconcile_snapshot_matrix(
case: ReconcileCase,
mode: KernelMode,
verbosity: KernelVerbosity,
) -> None:
rig = _build_kernel(mode=mode, verbosity=verbosity)
outcome = rig.kernel.reconcile_from_slots(case.slots)
assert outcome.accepted is True
assert outcome.diagnostic_code == KernelDiagnosticCode.RECONCILED
assert rig.kernel.snapshot()["account"]["open_positions"] == case.expected_open_positions
assert tuple(slot.trade_id for slot in rig.kernel.state.slots if slot.trade_id) == case.expected_trade_ids
assert len(rig.zinc.read_slots()) == len(rig.kernel.state.slots)
assert any(name == "hz:dita_active_slots" for name, _ in rig.sink.rows)
assert rig.projection.control_snapshot is not None
@pytest.mark.parametrize("seed", list(range(24)))
def test_kernel_fuzz_transition_matrix(seed: int) -> None:
rng = random.Random(seed)
rig = _build_kernel(
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=0.5)),
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
)
for step in range(20):
slot_id = rng.randrange(0, len(rig.kernel.state.slots))
slot = rig.kernel.slot(slot_id)
op = rng.choice(["enter", "exit", "cancel", "mark", "reconcile", "control", "event"])
if op == "enter":
trade_id = f"trade-{seed}-{step}"
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{seed}-{step}-enter",
trade_id=trade_id,
slot_id=slot_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=100.0 + rng.random(),
target_size=1.0,
leverage=2.0,
exit_leg_ratios=(0.5, 0.5),
reason="fuzz-enter",
)
)
assert outcome.diagnostic_code in set(KernelDiagnosticCode)
elif op == "exit":
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{seed}-{step}-exit",
trade_id=slot.trade_id or f"trade-{seed}-{step}",
slot_id=slot_id,
asset=slot.asset or "BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.EXIT,
reference_price=99.0 + rng.random(),
target_size=max(0.1, slot.size or 0.1),
leverage=slot.leverage or 2.0,
exit_leg_ratios=slot.exit_leg_ratios or (1.0,),
reason="fuzz-exit",
)
)
assert outcome.diagnostic_code in {
KernelDiagnosticCode.OK,
KernelDiagnosticCode.NO_OPEN_POSITION,
}
elif op == "cancel":
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{seed}-{step}-cancel",
trade_id=slot.trade_id or f"trade-{seed}-{step}",
slot_id=slot_id,
asset=slot.asset or "BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.CANCEL,
reference_price=99.0,
target_size=max(0.1, slot.size or 0.1),
leverage=slot.leverage or 2.0,
exit_leg_ratios=slot.exit_leg_ratios or (1.0,),
reason="fuzz-cancel",
)
)
assert outcome.diagnostic_code in {
KernelDiagnosticCode.OK,
KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER,
}
elif op == "mark":
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{seed}-{step}-mark",
trade_id=slot.trade_id or f"trade-{seed}-{step}",
slot_id=slot_id,
asset=slot.asset or "BTCUSDT",
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
action=KernelCommandType.MARK_PRICE,
reference_price=95.0 + rng.random() * 10.0,
target_size=max(0.1, slot.size or 0.1),
leverage=slot.leverage or 2.0,
exit_leg_ratios=slot.exit_leg_ratios or (1.0,),
reason="fuzz-mark",
)
)
assert outcome.diagnostic_code == KernelDiagnosticCode.OK
elif op == "reconcile":
outcome = rig.kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{seed}-{step}-reconcile",
trade_id=slot.trade_id or f"trade-{seed}-{step}",
slot_id=slot_id,
asset=slot.asset or "BTCUSDT",
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
action=KernelCommandType.RECONCILE,
reference_price=100.0,
target_size=max(0.1, slot.size or 0.1),
leverage=slot.leverage or 2.0,
exit_leg_ratios=slot.exit_leg_ratios or (1.0,),
reason="fuzz-reconcile",
)
)
assert outcome.diagnostic_code == KernelDiagnosticCode.STALE_STATE_RECONCILE
elif op == "control":
rig.kernel.update_control(
ControlUpdate(
mode=KernelMode.DEBUG if rng.random() < 0.5 else KernelMode.NORMAL,
verbosity=rng.choice([KernelVerbosity.QUIET, KernelVerbosity.VERBOSE, KernelVerbosity.TRACE]),
backend_mode=rng.choice([BackendMode.MOCK, BackendMode.BINGX]),
trace_transitions=rng.random() < 0.5,
)
)
elif op == "event":
current = rig.kernel.slot(slot_id)
if current.active_exit_order is not None:
kind = rng.choice(
[
KernelEventKind.PARTIAL_FILL,
KernelEventKind.FULL_FILL,
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
KernelEventKind.ORDER_REJECT,
]
)
elif current.active_entry_order is not None:
kind = rng.choice(
[
KernelEventKind.ORDER_ACK,
KernelEventKind.PARTIAL_FILL,
KernelEventKind.FULL_FILL,
KernelEventKind.ORDER_REJECT,
]
)
else:
kind = rng.choice(
[
KernelEventKind.ORDER_REJECT,
KernelEventKind.MARK_PRICE,
KernelEventKind.RECONCILE,
]
)
status = {
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
}[kind]
venue_order_id = "V-FUZZ"
venue_client_id = f"fuzz:{seed}:{step}"
if current.active_entry_order is not None:
venue_order_id = current.active_entry_order.venue_order_id
venue_client_id = current.active_entry_order.venue_client_id
elif current.active_exit_order is not None:
venue_order_id = current.active_exit_order.venue_order_id
venue_client_id = current.active_exit_order.venue_client_id
outcome = rig.kernel.on_venue_event(
_make_event(
kind=kind,
status=status,
trade_id=current.trade_id or f"trade-{seed}-{step}",
slot_id=slot_id if rng.random() < 0.5 else 99,
venue_order_id=venue_order_id,
venue_client_id=venue_client_id,
side=current.side if current.side != TradeSide.FLAT else TradeSide.SHORT,
asset=current.asset or "BTCUSDT",
price=98.0 if kind == KernelEventKind.MARK_PRICE else 100.0,
size=max(0.1, current.size or 0.1),
filled_size=max(0.1, current.size or 0.1),
remaining_size=0.0,
)
)
assert isinstance(outcome, KernelOutcome)
assert outcome.diagnostic_code in set(KernelDiagnosticCode)
assert slot.fsm_state in set(TradeStage)
assert slot.size >= 0.0
assert slot.initial_size >= 0.0
assert slot.active_leg_index >= 0
if slot.closed:
assert slot.size == pytest.approx(0.0, abs=1e-9)
if slot.fsm_state == TradeStage.IDLE:
assert slot.size == pytest.approx(0.0, abs=1e-9)

View File

@@ -1,494 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import random
import pytest
from prod.clean_arch.dita_v2 import (
AccountProjection,
BackendMode,
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelControlSnapshot,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelMode,
KernelVerbosity,
MemoryKernelJournal,
TradeSide,
TradeSlot,
TradeStage,
VenueAdapter,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
class NoopVenueAdapter:
"""Venue stub that never emits events."""
def submit(self, intent: KernelIntent): # type: ignore[override]
return []
def cancel(self, order: VenueOrder, *, reason: str = ""): # type: ignore[override]
return []
def open_orders(self): # type: ignore[override]
return []
def open_positions(self): # type: ignore[override]
return []
def reconcile(self): # type: ignore[override]
return []
@dataclass(frozen=True)
class RecoveryCase:
name: str
seed: int
slot_count: int
trade_count: int
@dataclass(frozen=True)
class DuplicateCase:
name: str
initial_state: TradeStage
kind: KernelEventKind
family: str
expected_state: TradeStage
expected_code: KernelDiagnosticCode
@dataclass(frozen=True)
class OutOfOrderCase:
name: str
seed: int
@dataclass(frozen=True)
class ReplayCase:
name: str
seed: int
control_mode: KernelMode
verbosity: KernelVerbosity
@dataclass(frozen=True)
class ControlCase:
name: str
mode: KernelMode
verbosity: KernelVerbosity
backend_mode: BackendMode
trace_transitions: bool
def _build_kernel(slot_count: int = 4) -> tuple[ExecutionKernel, MemoryKernelJournal, InMemoryZincPlane]:
journal = MemoryKernelJournal()
zinc = InMemoryZincPlane()
kernel = ExecutionKernel(
max_slots=slot_count,
control_plane=InMemoryControlPlane(
KernelControlSnapshot(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.MOCK,
debug_clickhouse_enabled=True,
trace_transitions=True,
mirror_to_hazelcast=True,
)
),
venue=NoopVenueAdapter(),
journal=journal,
account=AccountProjection(),
zinc_plane=zinc,
)
return kernel, journal, zinc
def _enter_open(kernel: ExecutionKernel, *, trade_id: str, slot_id: int = 0, size: float = 1.0) -> None:
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:enter",
trade_id=trade_id,
slot_id=slot_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=100.0,
target_size=size,
leverage=2.0,
exit_leg_ratios=(1.0,),
reason="enter",
)
)
slot = kernel.slot(slot_id)
ack = VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"{trade_id}:ack",
trade_id=trade_id,
slot_id=slot_id,
kind=KernelEventKind.ORDER_ACK,
status=VenueEventStatus.ACKED,
venue_order_id=slot.active_entry_order.venue_order_id if slot.active_entry_order else "",
venue_client_id=slot.active_entry_order.venue_client_id if slot.active_entry_order else "",
side=TradeSide.SHORT,
asset="BTCUSDT",
price=100.0,
size=size,
filled_size=size,
remaining_size=0.0,
)
kernel.on_venue_event(ack)
def _seed_exit_working(kernel: ExecutionKernel, *, trade_id: str, slot_id: int = 0, exit_ratio: tuple[float, ...] = (1.0,)) -> None:
_enter_open(kernel, trade_id=trade_id, slot_id=slot_id, size=1.0)
slot = kernel.slot(slot_id)
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:exit",
trade_id=trade_id,
slot_id=slot_id,
asset=slot.asset,
side=slot.side,
action=KernelCommandType.EXIT,
reference_price=99.0,
target_size=exit_ratio[0],
leverage=slot.leverage,
exit_leg_ratios=exit_ratio,
reason="exit",
)
)
def _seed_exit_only_slot(
slot: TradeSlot,
*,
trade_id: str,
state: TradeStage,
asset: str = "BTCUSDT",
) -> None:
slot.trade_id = trade_id
slot.asset = asset
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.initial_size = 1.0
slot.size = 1.0 if state != TradeStage.CLOSED else 0.0
slot.leverage = 2.0
slot.closed = state == TradeStage.CLOSED
slot.exit_leg_ratios = (1.0,)
slot.active_leg_index = 0
slot.active_entry_order = None
slot.active_exit_order = None if state == TradeStage.CLOSED else VenueOrder(
internal_trade_id=trade_id,
venue_order_id="V-EXIT-1",
venue_client_id=f"{trade_id}:exit",
side=TradeSide.SHORT,
intended_size=1.0,
filled_size=0.0,
average_fill_price=0.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id, "asset": asset},
)
slot.fsm_state = state
def _fill_event(slot: TradeSlot, *, kind: KernelEventKind, filled_size: float, trade_id: str | None = None) -> VenueEvent:
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"evt-{kind.value.lower()}-{slot.slot_id}",
trade_id=trade_id or slot.trade_id,
slot_id=slot.slot_id,
kind=kind,
status={
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
}[kind],
venue_order_id=slot.active_entry_order.venue_order_id if slot.active_entry_order else slot.active_exit_order.venue_order_id if slot.active_exit_order else "V-ORDER",
venue_client_id=slot.active_entry_order.venue_client_id if slot.active_entry_order else slot.active_exit_order.venue_client_id if slot.active_exit_order else "trade:client",
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
asset=slot.asset or "BTCUSDT",
price=98.0 if kind == KernelEventKind.MARK_PRICE else 100.0,
size=max(1.0, slot.size or 1.0),
filled_size=filled_size,
remaining_size=max(0.0, max(1.0, slot.size or 1.0) - filled_size),
)
RECOVERY_CASES = [
RecoveryCase("idle_only", seed=1, slot_count=4, trade_count=1),
RecoveryCase("one_open", seed=2, slot_count=4, trade_count=1),
RecoveryCase("mixed_two", seed=3, slot_count=4, trade_count=2),
RecoveryCase("mixed_three", seed=4, slot_count=4, trade_count=3),
RecoveryCase("all_open", seed=5, slot_count=4, trade_count=4),
RecoveryCase("open_and_closed", seed=6, slot_count=5, trade_count=4),
RecoveryCase("exit_working", seed=7, slot_count=4, trade_count=2),
RecoveryCase("position_open_with_gap", seed=8, slot_count=4, trade_count=3),
]
DUPLICATE_CASES = [
DuplicateCase("ack_entry_duplicate_regressed", TradeStage.POSITION_OPEN, KernelEventKind.ORDER_ACK, "entry", TradeStage.POSITION_OPEN, KernelDiagnosticCode.DUPLICATE_EVENT),
DuplicateCase("ack_exit_duplicate_hold", TradeStage.POSITION_OPEN, KernelEventKind.ORDER_ACK, "exit", TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
DuplicateCase("partial_entry_duplicate_stays", TradeStage.ENTRY_WORKING, KernelEventKind.PARTIAL_FILL, "entry", TradeStage.ENTRY_WORKING, KernelDiagnosticCode.OK),
DuplicateCase("full_entry_duplicate_noop", TradeStage.POSITION_OPEN, KernelEventKind.FULL_FILL, "entry", TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
DuplicateCase("partial_exit_duplicate_stays", TradeStage.EXIT_WORKING, KernelEventKind.PARTIAL_FILL, "exit", TradeStage.EXIT_WORKING, KernelDiagnosticCode.OK),
DuplicateCase("full_exit_duplicate_closes", TradeStage.CLOSED, KernelEventKind.FULL_FILL, "exit", TradeStage.CLOSED, KernelDiagnosticCode.OK),
DuplicateCase("cancel_ack_duplicate_open", TradeStage.POSITION_OPEN, KernelEventKind.CANCEL_ACK, "exit", TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
DuplicateCase("cancel_reject_duplicate_exit", TradeStage.EXIT_WORKING, KernelEventKind.CANCEL_REJECT, "exit", TradeStage.EXIT_WORKING, KernelDiagnosticCode.CANCEL_REJECTED),
]
OUT_OF_ORDER_CASES = [OutOfOrderCase(f"seed_{seed}", seed=seed) for seed in range(12)]
REPLAY_CASES = [
ReplayCase(f"replay_{seed}", seed=seed, control_mode=KernelMode.DEBUG if seed % 2 == 0 else KernelMode.NORMAL, verbosity=KernelVerbosity.TRACE if seed % 3 == 0 else KernelVerbosity.VERBOSE)
for seed in range(12)
]
CONTROL_CASES = [
ControlCase("normal_quiet_mock", KernelMode.NORMAL, KernelVerbosity.QUIET, BackendMode.MOCK, False),
ControlCase("normal_trace_mock", KernelMode.NORMAL, KernelVerbosity.TRACE, BackendMode.MOCK, True),
ControlCase("debug_trace_mock", KernelMode.DEBUG, KernelVerbosity.TRACE, BackendMode.MOCK, True),
ControlCase("debug_verbose_bingx", KernelMode.DEBUG, KernelVerbosity.VERBOSE, BackendMode.BINGX, False),
ControlCase("normal_verbose_bingx", KernelMode.NORMAL, KernelVerbosity.VERBOSE, BackendMode.BINGX, True),
ControlCase("debug_quiet_bingx", KernelMode.DEBUG, KernelVerbosity.QUIET, BackendMode.BINGX, False),
]
@pytest.mark.parametrize("case", CONTROL_CASES, ids=[case.name for case in CONTROL_CASES])
def test_kernel_zinc_control_plane_mirror(case: ControlCase) -> None:
kernel, journal, zinc = _build_kernel()
snapshot = kernel.update_control(
ControlUpdate(
mode=case.mode,
verbosity=case.verbosity,
backend_mode=case.backend_mode,
trace_transitions=case.trace_transitions,
)
)
assert snapshot.mode == case.mode
assert snapshot.verbosity == case.verbosity
assert snapshot.backend_mode == case.backend_mode
assert snapshot.trace_transitions == case.trace_transitions
assert zinc.read_control().mode == case.mode
assert zinc.read_control().verbosity == case.verbosity
assert journal.rows == []
assert kernel.zinc_plane.read_control().mode == case.mode
@pytest.mark.parametrize("case", RECOVERY_CASES, ids=[case.name for case in RECOVERY_CASES])
def test_kernel_zinc_restart_recovery_matrix(case: RecoveryCase) -> None:
kernel, _, zinc = _build_kernel(slot_count=case.slot_count)
rng = random.Random(case.seed)
for idx in range(case.trade_count):
slot_id = idx % case.slot_count
_enter_open(kernel, trade_id=f"{case.name}-{idx}", slot_id=slot_id, size=1.0)
if rng.random() < 0.5:
_seed_exit_working(kernel, trade_id=f"{case.name}-{idx}", slot_id=slot_id, exit_ratio=(0.5, 0.5))
if rng.random() < 0.5:
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{case.name}-{idx}:mark",
trade_id=f"{case.name}-{idx}",
slot_id=slot_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.MARK_PRICE,
reference_price=98.0,
target_size=1.0,
leverage=2.0,
exit_leg_ratios=(1.0,),
reason="mark",
)
)
snapshot_slots = zinc.read_slots()
assert len(snapshot_slots) == case.trade_count
restarted, _, restarted_zinc = _build_kernel(slot_count=case.slot_count)
outcome = restarted.reconcile_from_slots(snapshot_slots)
assert outcome.accepted is True
assert outcome.diagnostic_code == KernelDiagnosticCode.RECONCILED
assert restarted.snapshot()["slots"] == kernel.snapshot()["slots"]
assert [slot.to_dict() for slot in restarted_zinc.read_slots()] == restarted.snapshot()["slots"]
assert restarted.account.snapshot.open_positions == kernel.account.snapshot.open_positions
@pytest.mark.parametrize("case", DUPLICATE_CASES, ids=[case.name for case in DUPLICATE_CASES])
def test_kernel_duplicate_event_idempotence_matrix(case: DuplicateCase) -> None:
kernel, journal, zinc = _build_kernel()
slot = kernel.slot(0)
filled_size = 1.0 if case.kind == KernelEventKind.FULL_FILL else 0.25
if case.family == "entry":
_enter_open(kernel, trade_id="dup-entry", slot_id=0, size=1.0)
if case.kind == KernelEventKind.ORDER_ACK and case.initial_state == TradeStage.POSITION_OPEN:
slot.active_entry_order = VenueOrder(
internal_trade_id="dup-entry",
venue_order_id="V-ENTRY-1",
venue_client_id="dup-entry:entry",
side=TradeSide.SHORT,
intended_size=1.0,
filled_size=1.0,
average_fill_price=100.0,
status=VenueOrderStatus.FILLED,
metadata={"slot_id": 0, "asset": "BTCUSDT"},
)
slot.fsm_state = TradeStage.POSITION_OPEN
else:
_seed_exit_only_slot(slot, trade_id="dup-exit", state=case.initial_state)
before = slot.to_dict()
event = _fill_event(
slot,
kind=case.kind,
filled_size=filled_size,
trade_id=slot.trade_id,
)
outcome_1 = kernel.on_venue_event(event)
state_after_first = slot.fsm_state
size_after_first = slot.size
outcome_2 = kernel.on_venue_event(event)
assert outcome_1.diagnostic_code in set(KernelDiagnosticCode)
assert outcome_2.diagnostic_code in set(KernelDiagnosticCode)
assert slot.fsm_state == case.expected_state
assert slot.fsm_state == state_after_first
assert slot.size == pytest.approx(size_after_first, abs=1e-9)
assert slot.size >= 0.0
assert zinc.state_region[0].fsm_state == slot.fsm_state
assert len(journal.rows) >= 1
assert before["slot_id"] == slot.slot_id
if case.expected_code == KernelDiagnosticCode.DUPLICATE_EVENT:
assert outcome_2.diagnostic_code == KernelDiagnosticCode.DUPLICATE_EVENT
@pytest.mark.parametrize("case", OUT_OF_ORDER_CASES, ids=[case.name for case in OUT_OF_ORDER_CASES])
def test_kernel_out_of_order_venue_event_matrix(case: OutOfOrderCase) -> None:
kernel, journal, zinc = _build_kernel()
rng = random.Random(case.seed)
if rng.random() < 0.5:
_enter_open(kernel, trade_id=f"ooo-{case.seed}", slot_id=0, size=1.0)
else:
_seed_exit_working(kernel, trade_id=f"ooo-{case.seed}", slot_id=0, exit_ratio=(0.5, 0.5))
slot = kernel.slot(0)
sequence = [
KernelEventKind.FULL_FILL,
KernelEventKind.ORDER_ACK,
KernelEventKind.PARTIAL_FILL,
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
KernelEventKind.ORDER_REJECT,
KernelEventKind.MARK_PRICE,
]
rng.shuffle(sequence)
for idx, kind in enumerate(sequence):
event = _fill_event(
slot,
kind=kind,
filled_size=1.0 if kind == KernelEventKind.FULL_FILL else 0.5,
trade_id=slot.trade_id,
)
event = VenueEvent(
**{
**event.__dict__,
"event_id": f"ooo-{case.seed}-{idx}",
"slot_id": 0 if rng.random() < 0.5 else 99,
"venue_order_id": slot.active_entry_order.venue_order_id if slot.active_entry_order else slot.active_exit_order.venue_order_id if slot.active_exit_order else event.venue_order_id,
"venue_client_id": slot.active_entry_order.venue_client_id if slot.active_entry_order else slot.active_exit_order.venue_client_id if slot.active_exit_order else event.venue_client_id,
}
)
outcome = kernel.on_venue_event(event)
assert isinstance(outcome.diagnostic_code, KernelDiagnosticCode)
assert slot.size >= 0.0
assert slot.initial_size >= 0.0
assert slot.fsm_state in set(TradeStage)
if slot.closed:
assert slot.size == pytest.approx(0.0, abs=1e-9)
assert len(journal.rows) >= len(sequence)
assert len(zinc.state_region) >= 1
@pytest.mark.parametrize("case", REPLAY_CASES, ids=[case.name for case in REPLAY_CASES])
def test_kernel_debug_journal_replay_matrix(case: ReplayCase) -> None:
kernel, journal, zinc = _build_kernel()
kernel.update_control(
ControlUpdate(
mode=case.control_mode,
verbosity=case.verbosity,
trace_transitions=True,
debug_clickhouse_enabled=True,
)
)
rng = random.Random(case.seed)
for idx in range(10):
slot_id = idx % 2
trade_id = f"{case.name}-{idx}"
if idx % 3 == 0:
_enter_open(kernel, trade_id=trade_id, slot_id=slot_id, size=1.0)
elif idx % 3 == 1 and kernel.slot(slot_id).is_open():
_seed_exit_working(kernel, trade_id=kernel.slot(slot_id).trade_id, slot_id=slot_id, exit_ratio=(0.5, 0.5))
else:
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:mark",
trade_id=trade_id,
slot_id=slot_id,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.MARK_PRICE,
reference_price=97.0 + rng.random(),
target_size=1.0,
leverage=2.0,
exit_leg_ratios=(1.0,),
reason="journal-mark",
)
)
rows = list(journal.rows)
assert rows
for row in rows:
slot_state = row["slot_state"]
assert row["prev_state"] != ""
assert row["next_state"] != ""
assert slot_state["fsm_state"] == row["next_state"]
assert row["control_mode"] in {KernelMode.NORMAL.value, KernelMode.DEBUG.value}
assert row["control_verbosity"] in {KernelVerbosity.QUIET.value, KernelVerbosity.VERBOSE.value, KernelVerbosity.TRACE.value}
replayed, _, replayed_zinc = _build_kernel()
replayed.update_control(
ControlUpdate(mode=case.control_mode, verbosity=case.verbosity, trace_transitions=True, debug_clickhouse_enabled=True)
)
replayed.reconcile_from_slots(zinc.read_slots())
assert replayed.snapshot()["slots"] == kernel.snapshot()["slots"]
assert len(replayed_zinc.read_slots()) == len(replayed.snapshot()["slots"])
assert [slot.slot_id for slot in replayed_zinc.read_slots()] == [slot.slot_id for slot in replayed.state.slots]

View File

@@ -1,437 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import random
import pytest
from prod.clean_arch.dita_v2 import (
BackendMode,
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelMode,
KernelControlSnapshot,
KernelVerbosity,
MemoryKernelJournal,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
class NoopVenueAdapter:
def submit(self, intent: KernelIntent): # type: ignore[override]
return []
def cancel(self, order: VenueOrder, *, reason: str = ""): # type: ignore[override]
return []
def open_orders(self): # type: ignore[override]
return []
def open_positions(self): # type: ignore[override]
return []
def reconcile(self): # type: ignore[override]
return []
@dataclass(frozen=True)
class RaceCase:
name: str
seed_state: str
first_kind: KernelEventKind
second_kind: KernelEventKind
expected_state: TradeStage
expected_code_2: KernelDiagnosticCode
@dataclass(frozen=True)
class OffByOneCase:
name: str
exit_leg_ratios: tuple[float, ...]
fills: tuple[float, ...]
expected_leg_index: int
expected_closed: bool
@dataclass(frozen=True)
class MemoryCase:
name: str
max_slots: int
write_slot_ids: tuple[int, ...]
reconcile_slot_ids: tuple[int, ...]
expected_written_count: int
def _build_kernel(slot_count: int = 4) -> tuple[ExecutionKernel, MemoryKernelJournal, InMemoryZincPlane]:
journal = MemoryKernelJournal()
zinc = InMemoryZincPlane()
kernel = ExecutionKernel(
max_slots=slot_count,
control_plane=InMemoryControlPlane(
KernelControlSnapshot(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.MOCK,
trace_transitions=True,
debug_clickhouse_enabled=True,
mirror_to_hazelcast=True,
)
),
venue=NoopVenueAdapter(),
journal=journal,
zinc_plane=zinc,
)
return kernel, journal, zinc
def _seed_entry_working(kernel: ExecutionKernel, *, trade_id: str, slot_id: int = 0, size: float = 1.0) -> None:
slot = kernel.slot(slot_id)
slot.trade_id = trade_id
slot.asset = "BTCUSDT"
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.size = 0.0
slot.initial_size = 0.0
slot.leverage = 2.0
slot.entry_time = datetime.now(timezone.utc)
slot.exit_leg_ratios = (1.0,)
slot.active_leg_index = 0
slot.closed = False
slot.close_reason = ""
slot.active_exit_order = None
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id=f"V-ENTRY-{slot_id}",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=size,
filled_size=0.0,
average_fill_price=0.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot_id},
)
slot.fsm_state = TradeStage.ENTRY_WORKING
def _seed_position_open(kernel: ExecutionKernel, *, trade_id: str, slot_id: int = 0, size: float = 1.0) -> None:
_seed_entry_working(kernel, trade_id=trade_id, slot_id=slot_id, size=size)
slot = kernel.slot(slot_id)
slot.size = size
slot.initial_size = size
slot.entry_price = 100.0
slot.active_entry_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id=f"V-ENTRY-{slot_id}",
venue_client_id=f"{trade_id}:entry",
side=TradeSide.SHORT,
intended_size=size,
filled_size=size,
average_fill_price=100.0,
status=VenueOrderStatus.FILLED,
metadata={"slot_id": slot_id},
)
slot.fsm_state = TradeStage.POSITION_OPEN
def _seed_exit_working(kernel: ExecutionKernel, *, trade_id: str, slot_id: int = 0, exit_leg_ratios: tuple[float, ...] = (1.0,)) -> None:
_seed_position_open(kernel, trade_id=trade_id, slot_id=slot_id, size=1.0)
slot = kernel.slot(slot_id)
slot.exit_leg_ratios = exit_leg_ratios
slot.active_exit_order = VenueOrder(
internal_trade_id=trade_id,
venue_order_id=f"V-EXIT-{slot_id}",
venue_client_id=f"{trade_id}:exit",
side=TradeSide.SHORT,
intended_size=slot.next_exit_ratio() * slot.initial_size,
filled_size=0.0,
average_fill_price=0.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot_id},
)
slot.fsm_state = TradeStage.EXIT_WORKING
def _make_event(
slot: TradeSlot,
*,
kind: KernelEventKind,
event_id: str,
filled_size: float,
slot_id: int | None = None,
venue_order_id: str | None = None,
venue_client_id: str | None = None,
reason: str = "",
) -> VenueEvent:
order_id = venue_order_id or (
slot.active_exit_order.venue_order_id if slot.active_exit_order else slot.active_entry_order.venue_order_id if slot.active_entry_order else "V-ORDER"
)
client_id = venue_client_id or (
slot.active_exit_order.venue_client_id if slot.active_exit_order else slot.active_entry_order.venue_client_id if slot.active_entry_order else "trade:client"
)
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=event_id,
trade_id=slot.trade_id,
slot_id=slot.slot_id if slot_id is None else slot_id,
kind=kind,
status={
KernelEventKind.ORDER_ACK: VenueEventStatus.ACKED,
KernelEventKind.ORDER_REJECT: VenueEventStatus.REJECTED,
KernelEventKind.PARTIAL_FILL: VenueEventStatus.PARTIALLY_FILLED,
KernelEventKind.FULL_FILL: VenueEventStatus.FILLED,
KernelEventKind.CANCEL_ACK: VenueEventStatus.CANCELED,
KernelEventKind.CANCEL_REJECT: VenueEventStatus.CANCELED_REJECTED,
KernelEventKind.MARK_PRICE: VenueEventStatus.ACKED,
KernelEventKind.RECONCILE: VenueEventStatus.ACKED,
}[kind],
venue_order_id=order_id,
venue_client_id=client_id,
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
asset=slot.asset or "BTCUSDT",
price=99.0 if kind == KernelEventKind.MARK_PRICE else 100.0,
size=max(1.0, slot.size or 1.0),
filled_size=filled_size,
remaining_size=max(0.0, max(1.0, slot.size or 1.0) - filled_size),
reason=reason,
)
RACE_CASES = [
RaceCase("entry_ack_then_fullfill", "entry_working", KernelEventKind.ORDER_ACK, KernelEventKind.FULL_FILL, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
RaceCase("entry_fullfill_then_ack", "entry_working", KernelEventKind.FULL_FILL, KernelEventKind.ORDER_ACK, TradeStage.POSITION_OPEN, KernelDiagnosticCode.DUPLICATE_EVENT),
RaceCase("entry_ack_then_reject", "entry_working", KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_REJECT, TradeStage.IDLE, KernelDiagnosticCode.ENTRY_ORDER_REJECTED),
RaceCase("entry_reject_then_ack", "entry_working", KernelEventKind.ORDER_REJECT, KernelEventKind.ORDER_ACK, TradeStage.IDLE, KernelDiagnosticCode.OK),
RaceCase("entry_mark_then_fullfill", "entry_working", KernelEventKind.MARK_PRICE, KernelEventKind.FULL_FILL, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
RaceCase("entry_reconcile_then_ack", "entry_working", KernelEventKind.RECONCILE, KernelEventKind.ORDER_ACK, TradeStage.STALE_STATE_RECONCILING, KernelDiagnosticCode.STALE_STATE_RECONCILE),
RaceCase("exit_ack_then_fullfill", "exit_working", KernelEventKind.ORDER_ACK, KernelEventKind.FULL_FILL, TradeStage.CLOSED, KernelDiagnosticCode.OK),
RaceCase("exit_fullfill_then_ack", "exit_working", KernelEventKind.FULL_FILL, KernelEventKind.ORDER_ACK, TradeStage.CLOSED, KernelDiagnosticCode.OK),
RaceCase("exit_cancel_ack_then_fullfill", "exit_working", KernelEventKind.CANCEL_ACK, KernelEventKind.FULL_FILL, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
RaceCase("exit_fullfill_then_cancel_ack", "exit_working", KernelEventKind.FULL_FILL, KernelEventKind.CANCEL_ACK, TradeStage.CLOSED, KernelDiagnosticCode.OK),
RaceCase("exit_cancel_reject_then_ack", "exit_working", KernelEventKind.CANCEL_REJECT, KernelEventKind.CANCEL_ACK, TradeStage.POSITION_OPEN, KernelDiagnosticCode.OK),
RaceCase("exit_mark_then_fullfill", "exit_working", KernelEventKind.MARK_PRICE, KernelEventKind.FULL_FILL, TradeStage.CLOSED, KernelDiagnosticCode.OK),
]
OFF_BY_ONE_CASES = [
OffByOneCase("ratios_empty", (), (), 0, False),
OffByOneCase("ratios_one", (1.0,), (1.0,), 1, True),
OffByOneCase("ratios_two_equal", (0.5, 0.5), (0.5, 0.5), 2, True),
OffByOneCase("ratios_three_tail", (0.25, 0.25, 0.5), (0.25, 0.25, 0.5), 3, True),
OffByOneCase("ratios_three_front_loaded", (0.6, 0.3, 0.1), (0.6, 0.3, 0.1), 3, True),
OffByOneCase("ratios_four_small", (0.1, 0.2, 0.3, 0.4), (0.1, 0.2, 0.3, 0.4), 4, True),
]
MEMORY_CASES = [
MemoryCase("sparse_write_order", 5, (3, 1, 4), (3, 1, 4), 3),
MemoryCase("overwrite_same_slot", 4, (2, 2, 2), (2,), 1),
MemoryCase("capacity_trim", 3, (0, 1, 2, 3, 4), (0, 1, 2), 3),
MemoryCase("single_slot_reconcile", 2, (1,), (1,), 1),
MemoryCase("mixed_holes", 6, (5, 0, 3), (5, 0, 3), 3),
MemoryCase("late_slot_overwrite", 4, (1, 3, 1), (1, 3), 2),
]
@pytest.mark.parametrize("case", RACE_CASES, ids=[case.name for case in RACE_CASES])
def test_kernel_race_and_reorder_matrix(case: RaceCase) -> None:
kernel, journal, zinc = _build_kernel()
if case.seed_state == "entry_working":
_seed_entry_working(kernel, trade_id=f"race-{case.name}")
elif case.seed_state == "exit_working":
_seed_exit_working(kernel, trade_id=f"race-{case.name}")
else:
_seed_position_open(kernel, trade_id=f"race-{case.name}")
slot = kernel.slot(0)
first = _make_event(
slot,
kind=case.first_kind,
event_id=f"{case.name}-first",
filled_size=1.0 if case.first_kind == KernelEventKind.FULL_FILL else 0.5,
)
second = _make_event(
slot,
kind=case.second_kind,
event_id=f"{case.name}-second",
filled_size=1.0 if case.second_kind == KernelEventKind.FULL_FILL else 0.5,
)
outcome_1 = kernel.on_venue_event(first)
outcome_2 = kernel.on_venue_event(second)
assert outcome_1.diagnostic_code in set(KernelDiagnosticCode)
assert outcome_2.diagnostic_code in set(KernelDiagnosticCode)
assert slot.size >= 0.0
assert slot.initial_size >= 0.0
assert slot.fsm_state == case.expected_state
assert outcome_2.diagnostic_code == case.expected_code_2
assert zinc.state_region[0].fsm_state == slot.fsm_state
assert len(journal.rows) >= 2
@pytest.mark.parametrize("case", OFF_BY_ONE_CASES, ids=[case.name for case in OFF_BY_ONE_CASES])
def test_kernel_exit_leg_off_by_one_matrix(case: OffByOneCase) -> None:
kernel, journal, zinc = _build_kernel()
_seed_exit_working(kernel, trade_id=f"obo-{case.name}", exit_leg_ratios=case.exit_leg_ratios)
slot = kernel.slot(0)
assert slot.next_exit_ratio() == pytest.approx(case.exit_leg_ratios[0] if case.exit_leg_ratios else 1.0, abs=1e-9)
for idx, fill_size in enumerate(case.fills):
event = _make_event(
slot,
kind=KernelEventKind.FULL_FILL,
event_id=f"{case.name}-fill-{idx}",
filled_size=fill_size,
reason=f"leg-{idx}",
)
outcome = kernel.on_venue_event(event)
assert outcome.accepted is True
assert slot.size >= 0.0
assert slot.active_leg_index <= max(len(case.exit_leg_ratios), 1)
if idx < len(case.fills) - 1:
assert slot.fsm_state == TradeStage.POSITION_OPEN
rearm = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{case.name}-rearm-{idx}",
trade_id=slot.trade_id,
slot_id=slot.slot_id,
asset=slot.asset,
side=slot.side,
action=KernelCommandType.EXIT,
reference_price=100.0,
target_size=slot.next_exit_ratio(),
leverage=slot.leverage,
exit_leg_ratios=case.exit_leg_ratios,
reason=f"rearm-{idx}",
)
)
assert rearm.accepted is True
assert slot.fsm_state == TradeStage.EXIT_REQUESTED
else:
assert slot.fsm_state in {TradeStage.POSITION_OPEN, TradeStage.CLOSED}
assert slot.active_leg_index == case.expected_leg_index
assert slot.closed is case.expected_closed
if case.fills:
assert zinc.state_region[0].active_leg_index == slot.active_leg_index
else:
assert zinc.state_region[0].trade_id == slot.trade_id
assert zinc.state_region[0].fsm_state == TradeStage.EXIT_WORKING
assert len(journal.rows) >= len(case.fills)
assert slot.next_exit_ratio() == pytest.approx(1.0, abs=1e-9) if case.expected_closed else slot.next_exit_ratio() <= 1.0
@pytest.mark.parametrize("case", MEMORY_CASES, ids=[case.name for case in MEMORY_CASES])
def test_kernel_zinc_memory_anomaly_matrix(case: MemoryCase) -> None:
kernel, journal, zinc = _build_kernel(slot_count=case.max_slots)
rng = random.Random(hash(case.name) & 0xFFFFFFFF)
for idx, slot_id in enumerate(case.write_slot_ids):
slot = kernel.slot(slot_id % case.max_slots)
slot.trade_id = f"{case.name}-{idx}"
slot.asset = "BTCUSDT"
slot.side = TradeSide.SHORT
slot.entry_price = 100.0
slot.size = float(idx + 1)
slot.initial_size = float(idx + 1)
slot.leverage = 2.0
slot.fsm_state = TradeStage.POSITION_OPEN if idx % 2 == 0 else TradeStage.EXIT_WORKING
slot.active_entry_order = VenueOrder(
internal_trade_id=slot.trade_id,
venue_order_id=f"V-ENTRY-{slot_id}-{idx}",
venue_client_id=f"{slot.trade_id}:entry",
side=TradeSide.SHORT,
intended_size=slot.size,
filled_size=slot.size,
average_fill_price=100.0,
status=VenueOrderStatus.FILLED,
metadata={"slot_id": slot.slot_id},
)
if slot.fsm_state == TradeStage.EXIT_WORKING:
slot.active_exit_order = VenueOrder(
internal_trade_id=slot.trade_id,
venue_order_id=f"V-EXIT-{slot_id}-{idx}",
venue_client_id=f"{slot.trade_id}:exit",
side=TradeSide.SHORT,
intended_size=max(0.1, slot.size / 2.0),
filled_size=0.0,
average_fill_price=0.0,
status=VenueOrderStatus.NEW,
metadata={"slot_id": slot.slot_id},
)
kernel.zinc_plane.write_slot(slot)
written = zinc.read_slots()
assert len(written) == case.expected_written_count
assert [slot.slot_id for slot in written] == sorted(set(slot_id % case.max_slots for slot_id in case.write_slot_ids))
# Shuffle a snapshot and reconcile it back into a fresh kernel to exercise
# sparse, duplicate and truncated memory layouts without venue involvement.
shuffled_snapshot = list(reversed(written))
if rng.random() < 0.5:
shuffled_snapshot.append(shuffled_snapshot[0])
restarted, restarted_journal, restarted_zinc = _build_kernel(slot_count=case.max_slots)
outcome = restarted.reconcile_from_slots(shuffled_snapshot)
assert outcome.accepted is True
assert outcome.diagnostic_code == KernelDiagnosticCode.RECONCILED
assert len(restarted_zinc.read_slots()) == case.max_slots
assert restarted.snapshot()["slots"] == [slot.to_dict() for slot in restarted_zinc.read_slots()]
assert len(restarted_journal.rows) == 0
# Feed a stale-state event against a reconstructed slot to ensure the kernel
# stays stable even when the memory image is awkward.
target_slot_id = restarted_zinc.read_slots()[0].slot_id
slot = restarted.slot(target_slot_id)
stale_intent = restarted.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{case.name}:stale",
trade_id=slot.trade_id,
slot_id=slot.slot_id,
asset=slot.asset,
side=slot.side,
action=KernelCommandType.RECONCILE,
reference_price=100.0,
target_size=max(1.0, slot.size or 1.0),
leverage=max(1.0, slot.leverage or 1.0),
exit_leg_ratios=slot.exit_leg_ratios,
reason="stale-reconcile",
)
)
assert stale_intent.diagnostic_code == KernelDiagnosticCode.STALE_STATE_RECONCILE
assert slot.fsm_state == TradeStage.STALE_STATE_RECONCILING
event = VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"{case.name}-reconcile",
trade_id=slot.trade_id,
slot_id=slot.slot_id,
kind=KernelEventKind.RECONCILE,
status=VenueEventStatus.ACKED,
venue_order_id=slot.active_exit_order.venue_order_id if slot.active_exit_order else slot.active_entry_order.venue_order_id if slot.active_entry_order else "V-ORDER",
venue_client_id=slot.active_exit_order.venue_client_id if slot.active_exit_order else slot.active_entry_order.venue_client_id if slot.active_entry_order else "V-CLIENT",
side=slot.side if slot.side != TradeSide.FLAT else TradeSide.SHORT,
asset=slot.asset or "BTCUSDT",
price=100.0,
size=max(1.0, slot.size or 1.0),
filled_size=0.0,
remaining_size=max(0.0, slot.size),
)
stale = restarted.on_venue_event(event)
assert stale.diagnostic_code == KernelDiagnosticCode.STALE_STATE_RECONCILE
assert restarted.slot(target_slot_id).fsm_state == TradeStage.STALE_STATE_RECONCILING

View File

@@ -1,126 +0,0 @@
from __future__ import annotations
from uuid import uuid4
import os
import unittest
from unittest.mock import patch
from prod.clean_arch.dita_v2 import (
DITAv2LauncherBundle,
LauncherVenueMode,
LauncherZincMode,
KernelControlSnapshot,
MockVenueAdapter,
build_launcher_bundle,
)
from prod.bingx.enums import BingxEnvironment
from prod.clean_arch.dita_v2.launcher import _maybe_close
from prod.clean_arch.dita_v2.launcher import build_bingx_exec_client_config
class DummyCloseable:
def __init__(self) -> None:
self.closed = False
def close(self) -> None:
self.closed = True
class DummyControlPlane:
def __init__(self) -> None:
self.snapshot = KernelControlSnapshot()
def read(self) -> KernelControlSnapshot:
return self.snapshot
def close(self) -> None:
pass
class DummyZincPlane:
def __init__(self) -> None:
self.control_updates: list[KernelControlSnapshot] = []
self.slot_writes: list[object] = []
def update_control(self, snapshot: KernelControlSnapshot) -> None:
self.control_updates.append(snapshot)
def write_slot(self, slot: object) -> None:
self.slot_writes.append(slot)
class TestDITAv2Launcher(unittest.TestCase):
def test_build_launcher_bundle_defaults_to_mock_and_in_memory(self) -> None:
bundle = build_launcher_bundle(prefix=f"dita_v2_{uuid4().hex}")
try:
self.assertIsInstance(bundle, DITAv2LauncherBundle)
self.assertIsInstance(bundle.venue, MockVenueAdapter)
self.assertEqual(bundle.kernel.max_slots, 10)
self.assertEqual(bundle.kernel.control.mode.value, "NORMAL")
finally:
bundle.close()
def test_build_launcher_bundle_can_select_real_components_via_env(self) -> None:
prefix = f"dita_v2_{uuid4().hex}"
dummy_control = DummyControlPlane()
dummy_zinc = DummyZincPlane()
with patch("prod.clean_arch.dita_v2.launcher.build_control_plane", return_value=dummy_control), patch(
"prod.clean_arch.dita_v2.launcher._build_zinc_plane", return_value=dummy_zinc
):
bundle = build_launcher_bundle(
prefix=prefix,
venue_mode=LauncherVenueMode.BINGX,
zinc_mode=LauncherZincMode.REAL,
bingx_backend=object(),
)
try:
self.assertIs(bundle.control_plane, dummy_control)
self.assertIs(bundle.zinc_plane, dummy_zinc)
self.assertEqual(bundle.venue.__class__.__name__, "BingxVenueAdapter")
finally:
bundle.close()
def test_build_launcher_bundle_respects_explicit_modes(self) -> None:
prefix = f"dita_v2_{uuid4().hex}"
bundle = build_launcher_bundle(
prefix=prefix,
venue_mode=LauncherVenueMode.MOCK,
zinc_mode=LauncherZincMode.IN_MEMORY,
)
try:
self.assertIsInstance(bundle.venue, MockVenueAdapter)
self.assertEqual(bundle.kernel.max_slots, 10)
finally:
bundle.close()
def test_bingx_exec_client_config_uses_standard_testnet_credentials(self) -> None:
with patch.dict(
os.environ,
{
"BINGX_API_KEY": "test-api-key",
"BINGX_SECRET_KEY": "test-secret-key",
"DOLPHIN_BINGX_ENV": "VST",
"DOLPHIN_BINGX_ALLOW_MAINNET": "0",
"DOLPHIN_BINGX_RECV_WINDOW_MS": "60000",
"DOLPHIN_BINGX_DEFAULT_LEVERAGE": "1",
"DOLPHIN_BINGX_EXCHANGE_LEVERAGE_CAP": "3",
},
clear=False,
):
cfg = build_bingx_exec_client_config()
self.assertEqual(cfg.api_key, "test-api-key")
self.assertEqual(cfg.secret_key, "test-secret-key")
self.assertIs(cfg.environment, BingxEnvironment.VST)
self.assertFalse(cfg.allow_mainnet)
self.assertEqual(cfg.recv_window_ms, 60000)
self.assertEqual(cfg.default_leverage, 1)
self.assertEqual(cfg.exchange_leverage_cap, 3)
def test_maybe_close_handles_closeable_objects(self) -> None:
dummy = DummyCloseable()
_maybe_close(dummy)
self.assertTrue(dummy.closed)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,820 +0,0 @@
from __future__ import annotations
import asyncio
import os
import re
import time
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path
from typing import Any
import pytest
from dotenv import load_dotenv
from prod.bingx.config import BingxExecClientConfig
from prod.bingx.config import BingxInstrumentProviderConfig
from prod.bingx.enums import BingxEnvironment
from prod.bingx.schemas import BingxContract
from prod.clean_arch.dita_v2 import BackendMode
from prod.clean_arch.dita_v2 import BingxVenueAdapter
from prod.clean_arch.dita_v2 import ControlUpdate
from prod.clean_arch.dita_v2 import ExecutionKernel
from prod.clean_arch.dita_v2 import KernelCommandType
from prod.clean_arch.dita_v2 import KernelDiagnosticCode
from prod.clean_arch.dita_v2 import KernelEventKind
from prod.clean_arch.dita_v2 import KernelIntent
from prod.clean_arch.dita_v2 import KernelMode
from prod.clean_arch.dita_v2 import KernelVerbosity
from prod.clean_arch.dita_v2 import RealZincControlPlane
from prod.clean_arch.dita_v2 import RealZincPlane
from prod.clean_arch.dita_v2 import TradeSide
from prod.clean_arch.dita_v2 import TradeStage
DOTENV_PATH = Path("/mnt/dolphinng5_predict/.env")
if DOTENV_PATH.exists():
load_dotenv(DOTENV_PATH, override=False)
LIVE_ENABLED = os.getenv("BINGX_SMOKE_LIVE") == "1"
LIVE_TRADING_ENABLED = os.getenv("BINGX_SMOKE_ALLOW_TRADE") == "1"
LIVE_DITAV2_ENABLED = os.getenv("DITA_V2_LIVE_BINGX") == "1"
LIVE_CREDENTIALS_READY = bool(os.getenv("BINGX_API_KEY")) and bool(os.getenv("BINGX_SECRET_KEY"))
pytestmark = pytest.mark.skipif(
not (LIVE_ENABLED and LIVE_TRADING_ENABLED and LIVE_DITAV2_ENABLED and LIVE_CREDENTIALS_READY),
reason=(
"DITAv2 live BingX testnet E2E requires BINGX_SMOKE_LIVE=1, "
"BINGX_SMOKE_ALLOW_TRADE=1, DITA_V2_LIVE_BINGX=1, and BingX VST credentials"
),
)
def _norm_symbol(value: str) -> str:
return str(value or "").replace("-", "").replace("_", "").upper()
def _contract_rows(payload: Any) -> list[dict[str, Any]]:
if isinstance(payload, list):
return [row for row in payload if isinstance(row, dict)]
if isinstance(payload, dict):
for key in ("contracts", "data", "rows"):
rows = payload.get(key)
if isinstance(rows, list):
return [row for row in rows if isinstance(row, dict)]
return []
def _reference_price_row(payload: Any) -> dict[str, Any]:
if isinstance(payload, list):
return payload[0] if payload and isinstance(payload[0], dict) else {}
if isinstance(payload, dict):
data = payload.get("data")
if isinstance(data, list):
return data[0] if data and isinstance(data[0], dict) else {}
if isinstance(data, dict):
return data
return payload
return {}
def _position_qty(row: dict[str, Any]) -> Decimal:
raw = row.get("positionAmt") or row.get("positionQty") or row.get("positionSize") or row.get("quantity") or 0
try:
return abs(Decimal(str(raw)))
except Exception:
return Decimal("0")
def _position_side(row: dict[str, Any]) -> TradeSide:
side_raw = str(row.get("positionSide") or row.get("side") or "").upper()
if side_raw in {"SHORT", "SELL"}:
return TradeSide.SHORT
if side_raw in {"LONG", "BUY"}:
return TradeSide.LONG
qty = _position_qty(row)
signed = str(row.get("positionAmt") or row.get("quantity") or "0")
try:
return TradeSide.SHORT if Decimal(signed) < 0 else TradeSide.LONG if qty > 0 else TradeSide.FLAT
except Exception:
return TradeSide.FLAT
def _live_quantity(contract: BingxContract) -> Decimal:
base = contract.min_quantity if contract.min_quantity > 0 else contract.step_size
if base <= 0:
base = Decimal("0.001")
qty = base * Decimal("2")
step = contract.step_size if contract.step_size > 0 else Decimal("0.001")
if qty < step * Decimal("2"):
qty = step * Decimal("2")
if qty < Decimal("12"):
qty = Decimal("12")
return qty
def _build_kernel(prefix: str) -> tuple[ExecutionKernel, RealZincControlPlane, RealZincPlane, BingxVenueAdapter]:
zinc_plane = RealZincPlane(prefix=prefix, slot_count=1, create=True)
control_plane = RealZincControlPlane(prefix=prefix, create=False)
venue = BingxVenueAdapter(
config=BingxExecClientConfig(
api_key=os.environ.get("BINGX_API_KEY", ""),
secret_key=os.environ.get("BINGX_SECRET_KEY", ""),
environment=BingxEnvironment.VST,
allow_mainnet=False,
recv_window_ms=int(os.environ.get("DOLPHIN_BINGX_RECV_WINDOW_MS", "60000")),
default_leverage=int(os.environ.get("DOLPHIN_BINGX_DEFAULT_LEVERAGE", "1")),
exchange_leverage_cap=int(os.environ.get("DOLPHIN_BINGX_EXCHANGE_LEVERAGE_CAP", "3")),
prefer_websocket=False,
sizing_mode="testnet",
journal_strategy="dita_v2_live_testnet",
journal_db="dolphin_pink",
instrument_provider=BingxInstrumentProviderConfig(load_all=True),
)
)
kernel = ExecutionKernel(
max_slots=1,
control_plane=control_plane,
venue=venue,
zinc_plane=zinc_plane,
)
kernel.update_control(
ControlUpdate(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
backend_mode=BackendMode.BINGX,
trace_transitions=True,
debug_clickhouse_enabled=True,
mirror_to_hazelcast=True,
reconcile_on_restart=True,
)
)
return kernel, control_plane, zinc_plane, venue
def _pick_live_contract(venue: BingxVenueAdapter) -> BingxContract:
client = getattr(getattr(venue, "backend", None), "_client", None)
if client is None:
raise AssertionError("BingxVenueAdapter backend does not expose a live client")
async def _inner() -> BingxContract:
state = await venue.backend.refresh_state(include_history=True)
open_symbols = {
_norm_symbol(str(row.get("symbol", key)))
for key, row in getattr(state, "open_positions", {}).items()
if isinstance(row, dict)
}
contracts_payload = await client.public_get("/openApi/swap/v2/quote/contracts")
contracts: list[BingxContract] = []
for row in _contract_rows(contracts_payload):
try:
contracts.append(BingxContract.from_http(row))
except Exception:
continue
if not contracts:
raise AssertionError("BingX VST contract loader returned no usable contracts")
preferred = [
os.getenv("BINGX_SMOKE_SYMBOL", "").strip().upper(),
"TRXUSDT",
"XLMUSDT",
"DOGEUSDT",
"ETHUSDT",
"BTCUSDT",
]
by_symbol = {contract.symbol.upper(): contract for contract in contracts}
by_venue = {contract.venue_symbol.replace("-", "").upper(): contract for contract in contracts}
for candidate in preferred:
if not candidate or candidate in open_symbols:
continue
if candidate in by_symbol:
return by_symbol[candidate]
if candidate in by_venue:
return by_venue[candidate]
for contract in contracts:
symbol = contract.symbol.upper()
venue_symbol = contract.venue_symbol.replace("-", "").upper()
if symbol not in open_symbols and venue_symbol not in open_symbols:
return contract
raise AssertionError("No BingX VST contract available outside the current open set")
return asyncio.run(_inner())
def _reference_price(venue: BingxVenueAdapter, contract: BingxContract) -> Decimal:
client = getattr(getattr(venue, "backend", None), "_client", None)
if client is None:
raise AssertionError("BingxVenueAdapter backend does not expose a live client")
async def _inner() -> Decimal:
payload = await client.public_get("/openApi/swap/v2/quote/price", {"symbol": contract.venue_symbol})
row = _reference_price_row(payload)
raw_price = row.get("price") or row.get("lastPrice") or row.get("markPrice") or row.get("last")
if raw_price is None:
raise AssertionError(f"Unable to resolve BingX price for {contract.venue_symbol}: {payload!r}")
return Decimal(str(raw_price))
return asyncio.run(_inner())
def _live_intent(
*,
action: KernelCommandType,
trade_id: str,
side: TradeSide,
asset: str,
target_size: float,
price: float,
reason: str,
) -> dict[str, Any]:
return {
"timestamp": datetime.now(timezone.utc).isoformat(),
"intent_id": f"{trade_id}:{action.value}:{reason}:{uuid.uuid4().hex[:8]}",
"trade_id": trade_id,
"slot_id": 0,
"asset": asset,
"side": side.value,
"action": action.value,
"reference_price": float(price),
"target_size": float(target_size),
"leverage": 1.0,
"exit_leg_ratios": [0.5, 0.5],
"reason": reason,
"metadata": {"source": "live_bingx_testnet"},
"stage": "INTENT_CREATED",
}
def _current_exchange_rows(venue: BingxVenueAdapter, symbol: str) -> list[dict[str, Any]]:
rows = []
for row in venue.open_positions():
if _norm_symbol(str(row.get("symbol") or row.get("venueSymbol") or "")) == _norm_symbol(symbol):
rows.append(dict(row))
return rows
def _current_exchange_qty(venue: BingxVenueAdapter, symbol: str) -> Decimal:
rows = _current_exchange_rows(venue, symbol)
if not rows:
return Decimal("0")
return max(_position_qty(row) for row in rows)
def _observed_live_qty(kernel: ExecutionKernel, venue: BingxVenueAdapter, symbol: str) -> Decimal:
slot_qty = Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0))
exchange_qty = _current_exchange_qty(venue, symbol)
return max(slot_qty, exchange_qty)
def _drive_live_reconcile(kernel: ExecutionKernel, venue: BingxVenueAdapter, symbol: str) -> None:
snapshot = venue.reconcile()
for event in snapshot:
if _norm_symbol(str(getattr(event, "asset", ""))) == _norm_symbol(symbol):
kernel.on_venue_event(event)
def _wait_for_live_response(kernel: ExecutionKernel, venue: BingxVenueAdapter, symbol: str, *, timeout_s: float = 60.0) -> tuple[TradeStage, Decimal]:
def _predicate() -> bool:
_drive_live_reconcile(kernel, venue, symbol)
slot = kernel.slot(0)
return slot.asset == symbol and slot.fsm_state != TradeStage.IDLE
try:
_wait_until(_predicate, timeout_s=timeout_s, interval_s=1.0)
except AssertionError:
_drive_live_reconcile(kernel, venue, symbol)
slot = kernel.slot(0)
return slot.fsm_state, _current_exchange_qty(venue, symbol)
def _wait_until(predicate, *, timeout_s: float = 30.0, interval_s: float = 1.0) -> None:
deadline = time.monotonic() + timeout_s
last_exc: Exception | None = None
while time.monotonic() < deadline:
try:
if predicate():
return
except Exception as exc: # pragma: no cover - best effort live polling
last_exc = exc
time.sleep(interval_s)
if last_exc is not None:
raise AssertionError("timed out while waiting for live BingX state") from last_exc
raise AssertionError("timed out while waiting for live BingX state")
def _cleanup_live_position(kernel: ExecutionKernel, venue: BingxVenueAdapter, contract: BingxContract, trade_id: str) -> None:
try:
for attempt in range(5):
qty = _current_exchange_qty(venue, contract.symbol)
if qty <= 0:
return
side = TradeSide.SHORT
rows = _current_exchange_rows(venue, contract.symbol)
if rows:
side = _position_side(rows[0])
if side == TradeSide.FLAT:
side = TradeSide.SHORT
price = float(_reference_price(venue, contract))
outcome = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:cleanup:{attempt}:{uuid.uuid4().hex[:8]}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.EXIT,
reference_price=price,
target_size=float(qty),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason="CLEANUP",
metadata={"source": "cleanup"},
)
)
if outcome.diagnostic_code == KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER:
_wait_until(lambda: _current_exchange_qty(venue, contract.symbol) <= 0, timeout_s=20.0, interval_s=1.0)
else:
_wait_until(lambda: _current_exchange_qty(venue, contract.symbol) <= 0, timeout_s=30.0, interval_s=1.0)
finally:
pass
@dataclass(frozen=True)
class _LiveCase:
name: str
side: TradeSide
LIVE_CASES = (
_LiveCase("short_cycle", TradeSide.SHORT),
_LiveCase("long_cycle", TradeSide.LONG),
)
@pytest.mark.parametrize("case", LIVE_CASES, ids=lambda case: case.name)
def test_live_bingx_testnet_basic_cycle(case: _LiveCase) -> None:
prefix = f"dita_v2_live_{case.name}_{uuid.uuid4().hex[:8]}"
kernel, control_plane, zinc_plane, venue = _build_kernel(prefix)
contract: BingxContract | None = None
trade_id = f"live-{case.name}-{uuid.uuid4().hex[:8]}"
try:
assert venue.connect() is True
contract = _pick_live_contract(venue)
size = _live_quantity(contract)
price = _reference_price(venue, contract)
entry = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:entry",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=case.side,
action=KernelCommandType.ENTER,
reference_price=float(price),
target_size=float(size),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason="LIVE_ENTRY",
metadata={"contract": contract.venue_symbol},
)
)
if entry.diagnostic_code == KernelDiagnosticCode.RATE_LIMITED or any(event.kind == KernelEventKind.RATE_LIMITED for event in entry.emitted_events):
assert entry.accepted is False
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
return
assert entry.accepted is True
assert entry.diagnostic_code == KernelDiagnosticCode.OK
slot = kernel.slot(0)
assert slot.trade_id == trade_id
assert slot.asset == contract.symbol
state, open_qty = _wait_for_live_response(kernel, venue, contract.symbol, timeout_s=60.0)
kernel_qty = Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0))
live_qty = max(kernel_qty, open_qty)
assert state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
assert kernel.zinc_plane.read_control().backend_mode == BackendMode.BINGX
if live_qty <= 0:
assert entry.emitted_events, "entry should still emit an exchange reaction"
assert any(event.kind in {KernelEventKind.ORDER_ACK, KernelEventKind.ORDER_REJECT, KernelEventKind.PARTIAL_FILL, KernelEventKind.RATE_LIMITED} for event in entry.emitted_events)
return
mark = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:mark",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=case.side,
action=KernelCommandType.MARK_PRICE,
reference_price=float(price),
target_size=float(size),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason="LIVE_MARK",
metadata={"contract": contract.venue_symbol},
)
)
assert mark.accepted is True
assert kernel.slot(0).asset == contract.symbol
live_qty = max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol))
assert live_qty > 0
partial_target = max(live_qty / Decimal("2"), contract.step_size)
partial = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:partial_exit",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=case.side,
action=KernelCommandType.EXIT,
reference_price=float(price),
target_size=float(partial_target),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason="LIVE_PARTIAL_EXIT",
metadata={"contract": contract.venue_symbol},
)
)
if partial.diagnostic_code == KernelDiagnosticCode.RATE_LIMITED or any(event.kind == KernelEventKind.RATE_LIMITED for event in partial.emitted_events):
assert partial.accepted is False
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
return
assert partial.accepted is True
if partial.diagnostic_code in {
KernelDiagnosticCode.EXIT_ORDER_REJECTED,
KernelDiagnosticCode.ORDER_REJECTED,
KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER,
KernelDiagnosticCode.CANCEL_REJECTED,
KernelDiagnosticCode.RATE_LIMITED,
} or any(event.kind in {KernelEventKind.ORDER_REJECT, KernelEventKind.CANCEL_REJECT, KernelEventKind.RATE_LIMITED} for event in partial.emitted_events):
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
return
_wait_until(lambda: _current_exchange_qty(venue, contract.symbol) <= live_qty, timeout_s=60.0, interval_s=1.0)
reduced_qty = _current_exchange_qty(venue, contract.symbol)
assert reduced_qty <= open_qty
remaining = reduced_qty
if remaining > 0:
final = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:final_exit",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=case.side,
action=KernelCommandType.EXIT,
reference_price=float(price),
target_size=float(remaining),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason="LIVE_FINAL_EXIT",
metadata={"contract": contract.venue_symbol},
)
)
if final.diagnostic_code == KernelDiagnosticCode.RATE_LIMITED or any(event.kind == KernelEventKind.RATE_LIMITED for event in final.emitted_events):
assert final.accepted is False
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
return
assert final.accepted is True
if final.diagnostic_code in {
KernelDiagnosticCode.EXIT_ORDER_REJECTED,
KernelDiagnosticCode.ORDER_REJECTED,
KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER,
KernelDiagnosticCode.CANCEL_REJECTED,
KernelDiagnosticCode.RATE_LIMITED,
} or any(event.kind in {KernelEventKind.ORDER_REJECT, KernelEventKind.CANCEL_REJECT, KernelEventKind.RATE_LIMITED} for event in final.emitted_events):
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
return
_wait_until(lambda: _current_exchange_qty(venue, contract.symbol) <= 0, timeout_s=60.0, interval_s=1.0)
# On the real live path there is no active working exit order after the market close.
cancel_diag = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:cancel_after_flat",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=case.side,
action=KernelCommandType.CANCEL,
reference_price=float(price),
target_size=float(size),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason="LIVE_CANCEL_AFTER_FLAT",
metadata={"contract": contract.venue_symbol},
)
)
assert cancel_diag.diagnostic_code in {
KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER,
KernelDiagnosticCode.OK,
KernelDiagnosticCode.CANCEL_REJECTED,
KernelDiagnosticCode.ORDER_REJECTED,
KernelDiagnosticCode.RATE_LIMITED,
}
_wait_until(lambda: _current_exchange_qty(venue, contract.symbol) <= 0, timeout_s=60.0, interval_s=1.0)
assert _current_exchange_qty(venue, contract.symbol) <= 0
assert kernel.slot(0).size <= 1e-12
finally:
if contract is not None:
try:
_cleanup_live_position(kernel, venue, contract, trade_id)
except Exception:
pass
try:
disconnect = getattr(getattr(venue, "backend", None), "disconnect", None)
if disconnect is not None:
asyncio.run(disconnect())
except Exception:
pass
try:
zinc_plane.close()
except Exception:
pass
try:
control_plane.close()
except Exception:
pass
@pytest.mark.parametrize("seed", range(4), ids=lambda seed: f"seed-{seed}")
@pytest.mark.parametrize("side", [TradeSide.SHORT, TradeSide.LONG], ids=lambda side: f"side-{side.value.lower()}")
def test_live_bingx_testnet_chaos_fuzz(seed: int, side: TradeSide) -> None:
rng = __import__("random").Random(20260527 + seed)
prefix = f"dita_v2_live_fuzz_{side.value.lower()}_{seed}_{uuid.uuid4().hex[:8]}"
kernel, control_plane, zinc_plane, venue = _build_kernel(prefix)
contract: BingxContract | None = None
trade_id = f"live-fuzz-{side.value.lower()}-{seed}-{uuid.uuid4().hex[:8]}"
try:
assert venue.connect() is True
contract = _pick_live_contract(venue)
size = _live_quantity(contract)
price = _reference_price(venue, contract)
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:entry",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.ENTER,
reference_price=float(price),
target_size=float(size),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason="FUZZ_ENTRY",
metadata={"contract": contract.venue_symbol},
)
)
state, open_qty = _wait_for_live_response(kernel, venue, contract.symbol, timeout_s=60.0)
kernel_qty = Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0))
live_qty = max(kernel_qty, open_qty)
assert state in {
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.ENTRY_WORKING,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
if live_qty <= 0:
assert kernel.slot(0).fsm_state in {
TradeStage.IDLE,
TradeStage.ENTRY_WORKING,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_OPENED,
TradeStage.STALE_STATE_RECONCILING,
TradeStage.CLOSED,
}
return
for idx in range(rng.randint(4, 7)):
slot = kernel.slot(0)
action = rng.choice(["mark", "exit_half", "exit_rest", "cancel", "reconcile"])
current_price = _reference_price(venue, contract)
_drive_live_reconcile(kernel, venue, contract.symbol)
slot = kernel.slot(0)
if action == "mark":
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:mark:{idx}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.MARK_PRICE,
reference_price=float(current_price),
target_size=float(max(size, slot.size or size)),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason=f"FUZZ_MARK_{idx}",
metadata={"contract": contract.venue_symbol},
)
)
elif action == "exit_half":
live_qty = max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol))
if live_qty <= 0:
continue
target = max(live_qty / Decimal("2"), contract.step_size)
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:exit_half:{idx}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.EXIT,
reference_price=float(current_price),
target_size=float(target),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason=f"FUZZ_EXIT_HALF_{idx}",
metadata={"contract": contract.venue_symbol},
)
)
elif action == "exit_rest":
live_qty = max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol))
if live_qty <= 0:
continue
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:exit_rest:{idx}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.EXIT,
reference_price=float(current_price),
target_size=float(max(Decimal(str(slot.size)), contract.step_size)),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason=f"FUZZ_EXIT_REST_{idx}",
metadata={"contract": contract.venue_symbol},
)
)
elif action == "cancel":
if kernel.slot(0).active_exit_order is not None or max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol)) > 0:
outcome = kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:cancel:{idx}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.CANCEL,
reference_price=float(current_price),
target_size=float(max(size, contract.step_size)),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason=f"FUZZ_CANCEL_{idx}",
metadata={"contract": contract.venue_symbol},
)
)
assert outcome.diagnostic_code in {
KernelDiagnosticCode.NO_ACTIVE_EXIT_ORDER,
KernelDiagnosticCode.OK,
KernelDiagnosticCode.RATE_LIMITED,
}
else:
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:reconcile:{idx}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.RECONCILE,
reference_price=float(current_price),
target_size=float(size),
leverage=1.0,
exit_leg_ratios=(0.5, 0.5),
reason=f"FUZZ_RECONCILE_{idx}",
metadata={"contract": contract.venue_symbol},
)
)
_drive_live_reconcile(kernel, venue, contract.symbol)
_wait_until(lambda: max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol)) >= 0, timeout_s=2.0, interval_s=0.2)
# Hard close-out pass for the fuzz cases.
for _ in range(3):
qty = max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol))
if qty <= 0:
break
kernel.process_intent(
KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"{trade_id}:cleanup:{uuid.uuid4().hex[:8]}",
trade_id=trade_id,
slot_id=0,
asset=contract.symbol,
side=side,
action=KernelCommandType.EXIT,
reference_price=float(_reference_price(venue, contract)),
target_size=float(qty),
leverage=1.0,
exit_leg_ratios=(1.0,),
reason="FUZZ_CLEANUP",
metadata={"contract": contract.venue_symbol},
)
)
_wait_until(lambda: max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol)) <= 0, timeout_s=60.0, interval_s=1.0)
assert max(Decimal(str(getattr(kernel.slot(0), "size", 0.0) or 0.0)), _current_exchange_qty(venue, contract.symbol)) <= 0
assert kernel.slot(0).fsm_state in {
TradeStage.CLOSED,
TradeStage.IDLE,
TradeStage.POSITION_OPEN,
TradeStage.POSITION_PARTIALLY_CLOSED,
TradeStage.STALE_STATE_RECONCILING,
}
assert kernel.zinc_plane.read_control().mode == KernelMode.DEBUG
assert kernel.zinc_plane.read_control().verbosity == KernelVerbosity.TRACE
finally:
if contract is not None:
try:
_cleanup_live_position(kernel, venue, contract, trade_id)
except Exception:
pass
try:
disconnect = getattr(getattr(venue, "backend", None), "disconnect", None)
if disconnect is not None:
asyncio.run(disconnect())
except Exception:
pass
try:
zinc_plane.close()
except Exception:
pass
try:
control_plane.close()
except Exception:
pass

View File

@@ -1,54 +0,0 @@
from __future__ import annotations
from pathlib import Path
import unittest
class TestDITAv2Ops(unittest.TestCase):
def test_operator_playbook_mentions_supervisor_program(self) -> None:
text = Path("/mnt/dolphinng5_predict/prod/docs/DITA_V2_OPERATOR_PLAYBOOK.md").read_text()
self.assertIn("dolphin:dita_v2", text)
self.assertIn("launch_dita_v2.py", text)
self.assertIn("dita_v2_ctl.py", text)
def test_supervisor_config_contains_dita_v2_program(self) -> None:
conf = Path("/mnt/dolphinng5_predict/prod/supervisor/dolphin-supervisord.conf").read_text()
self.assertIn("[program:dita_v2]", conf)
self.assertIn("launch_dita_v2.py", conf)
def test_supervisor_migration_doc_mentions_dita_v2_recovery(self) -> None:
text = Path("/mnt/dolphinng5_predict/prod/AGENT_READ_Supervisor_migration.md").read_text()
self.assertIn("dolphin:dita_v2", text)
self.assertIn("dita_v2_ctl.py", text)
self.assertIn("Do not use `systemctl` for `dolphin:dita_v2`", text)
def test_supervisor_wrapper_mentions_dita_v2(self) -> None:
text = Path("/mnt/dolphinng5_predict/prod/supervisor/supervisorctl.sh").read_text()
self.assertIn("dita_v2", text)
self.assertIn("dita_v2_ctl.py", text)
def test_supervisor_config_has_dita_v2_comment(self) -> None:
conf = Path("/mnt/dolphinng5_predict/prod/supervisor/dolphin-supervisord.conf").read_text()
self.assertIn("DITAv2 — supervised kernel", conf)
def test_operational_status_mentions_dita_v2(self) -> None:
text = Path("/mnt/dolphinng5_predict/prod/docs/OPERATIONAL_STATUS.md").read_text()
self.assertIn("DITAv2 Kernel", text)
self.assertIn("dolphin:dita_v2", text)
def test_live_smoke_wrapper_is_documented_and_wired(self) -> None:
script = Path("/mnt/dolphinng5_predict/prod/ops/dita_v2_live_bingx_smoke.py").read_text()
self.assertIn("BINGX_SMOKE_LIVE", script)
self.assertIn("BINGX_SMOKE_ALLOW_TRADE", script)
self.assertIn("DITA_V2_LIVE_BINGX", script)
self.assertIn("test_dita_v2_live_bingx_testnet_e2e.py", script)
self.assertIn("--dry-run", script)
playbook = Path("/mnt/dolphinng5_predict/prod/docs/DITA_V2_OPERATOR_PLAYBOOK.md").read_text()
self.assertIn("dita_v2_live_bingx_smoke.py", playbook)
self.assertIn("--dry-run", playbook)
self.assertIn("TRXUSDT", playbook)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,380 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
import os
import random
import threading
import time
import unittest
from uuid import uuid4
from prod.clean_arch.dita_v2 import (
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
KernelCommandType,
KernelDiagnosticCode,
KernelControlSnapshot,
KernelIntent,
KernelMode,
KernelVerbosity,
MockVenueAdapter,
MockVenueScenario,
InMemoryZincPlane,
RealZincPlane,
RealZincControlPlane,
RealZincUnavailable,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
KernelEventKind,
)
from prod.clean_arch.dita_v2.real_zinc_plane import SharedRegion
HAS_REAL_ZINC = SharedRegion is not None
def mk_intent_kwargs(
*,
slot_id: int,
trade_id: str,
action: KernelCommandType,
size: float = 1.0,
leverage: float = 2.0,
side: TradeSide = TradeSide.SHORT,
price: float = 100.0,
reason: str = "FUZZ",
) -> dict[str, object]:
return {
"timestamp": datetime.now(timezone.utc),
"intent_id": f"intent-{trade_id}-{action.value}-{slot_id}",
"trade_id": trade_id,
"slot_id": slot_id,
"asset": "BTCUSDT",
"side": side,
"action": action,
"reference_price": price,
"target_size": size,
"leverage": leverage,
"exit_leg_ratios": (0.5, 0.5) if action == KernelCommandType.EXIT else (1.0,),
"reason": reason,
}
@unittest.skipUnless(HAS_REAL_ZINC, "Real Zinc adapter is unavailable")
class TestDITAv2RealZinc(unittest.TestCase):
def setUp(self) -> None:
self.prefix = f"dita_v2_{os.getpid()}_{uuid4().hex}"
self.writer = RealZincPlane(prefix=self.prefix, slot_count=3, create=True)
self.reader = RealZincPlane(prefix=self.prefix, slot_count=3, create=False)
def tearDown(self) -> None:
self.writer.close()
self.reader.close()
def _slot_dicts(self, plane: RealZincPlane) -> list[dict[str, object]]:
return [slot.to_dict() for slot in plane.read_slots()]
def test_wait_notify_and_roundtrip(self) -> None:
waiter_started = threading.Event()
waiter_result: dict[str, bool] = {"ok": False}
def _waiter() -> None:
waiter_started.set()
waiter_result["ok"] = self.reader.wait_on_state(timeout_ms=3000)
thread = threading.Thread(target=_waiter, daemon=True)
thread.start()
self.assertTrue(waiter_started.wait(timeout=2.0))
time.sleep(0.05)
kernel_slot = self.writer.read_slots()
self.assertEqual(len(kernel_slot), 3)
self.assertTrue(all(slot.fsm_state == TradeStage.IDLE for slot in kernel_slot))
self.writer.write_slot(
TradeSlot(
slot_id=0,
trade_id="trade-zinc-1",
asset="BTCUSDT",
side=TradeSide.SHORT,
entry_price=100.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
)
)
thread.join(timeout=3.0)
self.assertFalse(thread.is_alive())
self.assertTrue(waiter_result["ok"])
slots = self.reader.read_slots()
self.assertEqual(len(slots), 3)
self.assertEqual(slots[0].trade_id, "trade-zinc-1")
self.assertEqual(slots[0].fsm_state, TradeStage.POSITION_OPEN)
self.assertTrue(all(slot.fsm_state == TradeStage.IDLE for slot in slots[1:]))
def test_in_memory_wait_notify_matches_signal_semantics(self) -> None:
plane = InMemoryZincPlane()
waiter_started = threading.Event()
waiter_result: dict[str, bool] = {"ok": False}
def _waiter() -> None:
waiter_started.set()
waiter_result["ok"] = plane.wait_on_state(timeout_ms=2000)
thread = threading.Thread(target=_waiter, daemon=True)
thread.start()
self.assertTrue(waiter_started.wait(timeout=2.0))
time.sleep(0.05)
plane.write_slot(
TradeSlot(
slot_id=0,
trade_id="trade-signal",
asset="BTCUSDT",
side=TradeSide.LONG,
entry_price=101.0,
size=1.0,
initial_size=1.0,
leverage=2.0,
fsm_state=TradeStage.POSITION_OPEN,
)
)
thread.join(timeout=3.0)
self.assertFalse(thread.is_alive())
self.assertTrue(waiter_result["ok"])
def test_real_control_plane_roundtrip_uses_open_existing_region(self) -> None:
prefix = f"dita_v2_control_{os.getpid()}_{uuid4().hex}"
plane = RealZincPlane(prefix=prefix, slot_count=1, create=True)
control = RealZincControlPlane(prefix=prefix, create=False)
try:
snapshot = control.read()
self.assertEqual(getattr(snapshot.mode, "value", snapshot.mode), KernelMode.NORMAL.value)
self.assertEqual(getattr(snapshot.verbosity, "value", snapshot.verbosity), KernelVerbosity.QUIET.value)
updated = control.update(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE))
self.assertEqual(getattr(updated.mode, "value", updated.mode), KernelMode.DEBUG.value)
self.assertEqual(getattr(updated.verbosity, "value", updated.verbosity), KernelVerbosity.TRACE.value)
mirrored = plane.read_control()
self.assertEqual(getattr(mirrored.mode, "value", mirrored.mode), KernelMode.DEBUG.value)
self.assertEqual(getattr(mirrored.verbosity, "value", mirrored.verbosity), KernelVerbosity.TRACE.value)
finally:
control.close()
plane.close()
def test_real_control_plane_create_conflicts_with_existing_zinc_plane(self) -> None:
prefix = f"dita_v2_conflict_{os.getpid()}_{uuid4().hex}"
plane = RealZincPlane(prefix=prefix, slot_count=1, create=True)
try:
with self.assertRaises(FileExistsError):
RealZincControlPlane(prefix=prefix, create=True)
finally:
plane.close()
def test_kernel_accepts_real_control_plane_snapshot_strings(self) -> None:
prefix = f"dita_v2_kernel_real_cp_{os.getpid()}_{uuid4().hex}"
plane = RealZincPlane(prefix=prefix, slot_count=1, create=True)
control = RealZincControlPlane(prefix=prefix, create=False)
try:
kernel = ExecutionKernel(
max_slots=1,
control_plane=control,
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
zinc_plane=plane,
)
kernel.update_control(
ControlUpdate(
mode=KernelMode.DEBUG,
verbosity=KernelVerbosity.TRACE,
trace_transitions=True,
)
)
outcome = kernel.process_intent(
KernelIntent(
**mk_intent_kwargs(
slot_id=0,
trade_id=f"trade-real-cp-{uuid4().hex}",
action=KernelCommandType.ENTER,
price=100.0,
size=1.0,
)
)
)
self.assertTrue(outcome.accepted)
self.assertEqual(outcome.diagnostic_code, KernelDiagnosticCode.OK)
self.assertEqual(kernel.slot(0).fsm_state, TradeStage.POSITION_OPEN)
self.assertEqual(getattr(kernel.control.mode, "value", kernel.control.mode), KernelMode.DEBUG.value)
self.assertEqual(getattr(kernel.control.verbosity, "value", kernel.control.verbosity), KernelVerbosity.TRACE.value)
finally:
control.close()
plane.close()
def test_kernel_and_zinc_fuzz_roundtrip_150_checks(self) -> None:
control = InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
)
kernel = ExecutionKernel(
max_slots=3,
control_plane=control,
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
zinc_plane=self.writer,
)
rng = random.Random(20260526)
for i in range(150):
slot_id = rng.randrange(0, 3)
slot = kernel.slot(slot_id)
op = rng.choice(
[
"enter",
"exit",
"mark",
"reconcile",
"control",
"event",
]
)
with self.subTest(iteration=i, slot=slot_id, op=op):
if op == "enter":
if slot.is_free():
kernel.process_intent(
KernelIntent(
**mk_intent_kwargs(
slot_id=slot_id,
trade_id=f"trade-{slot_id}-{i}",
action=KernelCommandType.ENTER,
price=100.0 + rng.random(),
size=1.0 + (rng.random() * 0.5),
leverage=1.5 + (rng.random() * 2.0),
)
)
)
elif op == "exit":
if slot.is_open():
kernel.process_intent(
KernelIntent(
**mk_intent_kwargs(
slot_id=slot_id,
trade_id=slot.trade_id,
action=KernelCommandType.EXIT,
price=99.0 + rng.random(),
size=max(0.1, slot.size or 0.1),
leverage=slot.leverage or 2.0,
)
)
)
elif op == "mark":
kernel.process_intent(
KernelIntent(
**mk_intent_kwargs(
slot_id=slot_id,
trade_id=slot.trade_id or f"trade-{slot_id}-{i}",
action=KernelCommandType.MARK_PRICE,
price=95.0 + rng.random() * 10.0,
size=max(slot.size, 1.0) if slot.size > 0 else 1.0,
leverage=slot.leverage or 2.0,
)
)
)
elif op == "reconcile":
kernel.process_intent(
KernelIntent(
**mk_intent_kwargs(
slot_id=slot_id,
trade_id=slot.trade_id or f"trade-{slot_id}-{i}",
action=KernelCommandType.RECONCILE,
price=100.0,
size=max(slot.size, 1.0) if slot.size > 0 else 1.0,
leverage=slot.leverage or 2.0,
)
)
)
elif op == "control":
kernel.update_control(
ControlUpdate(
mode=KernelMode.DEBUG if rng.random() < 0.7 else KernelMode.NORMAL,
verbosity=KernelVerbosity.TRACE if rng.random() < 0.5 else KernelVerbosity.VERBOSE,
trace_transitions=rng.random() < 0.5,
)
)
elif op == "event":
current = kernel.slot(slot_id)
if current.active_entry_order is not None:
event = VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"evt-{i}-{slot_id}",
trade_id=current.trade_id,
slot_id=slot_id,
kind=rng.choice(
[
KernelEventKind.ORDER_ACK,
KernelEventKind.PARTIAL_FILL,
KernelEventKind.FULL_FILL,
]
),
status=rng.choice(
[
VenueEventStatus.ACKED,
VenueEventStatus.PARTIALLY_FILLED,
VenueEventStatus.FILLED,
]
),
venue_order_id=current.active_entry_order.venue_order_id,
venue_client_id=current.active_entry_order.venue_client_id,
side=current.side,
asset=current.asset,
price=current.entry_price or 100.0,
size=current.size or 1.0,
filled_size=current.size or 1.0,
remaining_size=0.0,
)
kernel.on_venue_event(event)
elif current.active_exit_order is not None:
event = VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"evt-{i}-{slot_id}",
trade_id=current.trade_id,
slot_id=slot_id,
kind=rng.choice(
[
KernelEventKind.PARTIAL_FILL,
KernelEventKind.FULL_FILL,
KernelEventKind.CANCEL_ACK,
KernelEventKind.CANCEL_REJECT,
]
),
status=rng.choice(
[
VenueEventStatus.PARTIALLY_FILLED,
VenueEventStatus.FILLED,
VenueEventStatus.CANCELED,
VenueEventStatus.CANCELED_REJECTED,
]
),
venue_order_id=current.active_exit_order.venue_order_id,
venue_client_id=current.active_exit_order.venue_client_id,
side=current.side,
asset=current.asset,
price=current.entry_price or 100.0,
size=current.size or 1.0,
filled_size=min(current.size or 1.0, 0.5),
remaining_size=max(0.0, (current.size or 1.0) - 0.5),
)
kernel.on_venue_event(event)
writer_slots = self._slot_dicts(self.writer)
reader_slots = self._slot_dicts(self.reader)
kernel_slots = [slot.to_dict() for slot in kernel.state.slots]
self.assertEqual(writer_slots, reader_slots)
self.assertEqual(reader_slots, kernel_slots)
self.assertEqual(self.reader.read_control().mode, kernel.control.mode)
self.assertEqual(self.reader.read_control().verbosity, kernel.control.verbosity)
self.assertEqual(len(self.reader.read_intents()), len(self.writer.read_intents()))
if __name__ == "__main__":
unittest.main()

View File

@@ -1,79 +0,0 @@
from __future__ import annotations
import os
from types import SimpleNamespace
from pathlib import Path
import unittest
from unittest.mock import patch
import prod.launch_dita_v2 as launch_dita_v2
class DummyBundle:
def __init__(self) -> None:
self.closed = False
self.kernel = SimpleNamespace(snapshot=lambda: {"ok": True}, control=SimpleNamespace(as_dict=lambda: {"mode": "NORMAL"}))
self.venue = SimpleNamespace(__class__=SimpleNamespace(__name__="MockVenueAdapter"))
self.zinc_plane = SimpleNamespace(__class__=SimpleNamespace(__name__="InMemoryZincPlane"))
self.projection = SimpleNamespace(__class__=SimpleNamespace(__name__="HazelcastProjection"))
def close(self) -> None:
self.closed = True
class TestLaunchDitaV2(unittest.TestCase):
def test_supervisor_config_contains_dita_v2_program(self) -> None:
conf = Path("/mnt/dolphinng5_predict/prod/supervisor/dolphin-supervisord.conf").read_text()
self.assertIn("[program:dita_v2]", conf)
self.assertIn("launch_dita_v2.py", conf)
self.assertIn("DITA_V2_LAUNCHER_MODE=\"serve\"", conf)
def test_env_mode_defaults_to_serve(self) -> None:
previous = os.environ.get("DITA_V2_LAUNCHER_MODE")
try:
os.environ.pop("DITA_V2_LAUNCHER_MODE", None)
self.assertEqual(launch_dita_v2._env_mode(), "serve")
os.environ["DITA_V2_LAUNCHER_MODE"] = "once"
self.assertEqual(launch_dita_v2._env_mode(), "once")
finally:
if previous is None:
os.environ.pop("DITA_V2_LAUNCHER_MODE", None)
else:
os.environ["DITA_V2_LAUNCHER_MODE"] = previous
def test_main_once_uses_snapshot_path(self) -> None:
bundle = DummyBundle()
with patch.object(launch_dita_v2, "build_launcher_bundle", return_value=bundle), patch.object(
launch_dita_v2, "_serve", side_effect=AssertionError("_serve should not run in once mode")
):
previous = os.environ.get("DITA_V2_LAUNCHER_MODE")
os.environ["DITA_V2_LAUNCHER_MODE"] = "once"
try:
self.assertEqual(launch_dita_v2.main(), 0)
self.assertTrue(bundle.closed)
finally:
if previous is None:
os.environ.pop("DITA_V2_LAUNCHER_MODE", None)
else:
os.environ["DITA_V2_LAUNCHER_MODE"] = previous
def test_main_serve_routes_to_serve(self) -> None:
bundle = DummyBundle()
with patch.object(launch_dita_v2, "build_launcher_bundle", return_value=bundle), patch.object(
launch_dita_v2, "_serve", return_value=7
) as serve:
previous = os.environ.get("DITA_V2_LAUNCHER_MODE")
os.environ["DITA_V2_LAUNCHER_MODE"] = "serve"
try:
self.assertEqual(launch_dita_v2.main(), 7)
serve.assert_called_once()
self.assertTrue(bundle.closed)
finally:
if previous is None:
os.environ.pop("DITA_V2_LAUNCHER_MODE", None)
else:
os.environ["DITA_V2_LAUNCHER_MODE"] = previous
if __name__ == "__main__":
unittest.main()

View File

@@ -1,194 +0,0 @@
import math
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path("/mnt/dolphinng5_predict")
sys.path.insert(0, str(ROOT / "nautilus_dolphin"))
sys.path.insert(1, str(ROOT))
if "nautilus_dolphin" in sys.modules:
pkg = sys.modules["nautilus_dolphin"]
pkg_file = str(getattr(pkg, "__file__", "") or "")
if not pkg_file.endswith("nautilus_dolphin/nautilus_dolphin/__init__.py"):
del sys.modules["nautilus_dolphin"]
from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker, ACBConfig
from nautilus_dolphin.nautilus.alpha_bet_sizer import AlphaBetSizer
from nautilus_dolphin.nautilus.alpha_exit_manager import AlphaExitManager
from nautilus_dolphin.nautilus.alpha_signal_generator import AlphaSignalGenerator
from nautilus_dolphin.nautilus.esf_alpha_orchestrator import NDAlphaEngine
from nautilus_dolphin.nautilus.dolphin_actor import _trade_direction_from_config
def test_signal_generator_long_gate_and_dc_are_side_aware():
gen = AlphaSignalGenerator(use_direction_confirm=True)
rising_prices = [100.0, 100.1, 100.2, 100.3, 100.4, 100.5, 100.6, 101.0]
falling_prices = [101.0, 100.8, 100.6, 100.4, 100.2, 100.0, 99.8, 99.5]
long_sig = gen.generate(
vel_div=0.025,
vel_div_history=[0.012] * 10,
asset_price_history=rising_prices,
trade_direction=1,
)
assert long_sig.is_valid
assert long_sig.direction == 1
assert long_sig.dc_status == "CONFIRM"
contradicted = gen.generate(
vel_div=0.025,
vel_div_history=[0.012] * 10,
asset_price_history=falling_prices,
trade_direction=1,
)
assert not contradicted.is_valid
assert contradicted.dc_status == "SKIP_CONTRADICT"
def test_bet_sizer_trend_multiplier_is_direction_aware_for_long():
sizer = AlphaBetSizer(
base_fraction=0.20,
min_leverage=0.5,
max_leverage=8.0,
use_alpha_layers=True,
use_dynamic_leverage=True,
)
favorable_long = sizer.calculate_size(25000, 0.025, vel_div_trend=0.02, trade_direction=1)
adverse_long = sizer.calculate_size(25000, 0.025, vel_div_trend=-0.02, trade_direction=1)
favorable_short = sizer.calculate_size(25000, -0.035, vel_div_trend=-0.02, trade_direction=-1)
adverse_short = sizer.calculate_size(25000, -0.035, vel_div_trend=0.02, trade_direction=-1)
assert favorable_long["fraction"] > adverse_long["fraction"]
assert favorable_short["fraction"] > adverse_short["fraction"]
def test_ndalphaengine_enters_long_when_begin_day_direction_is_long():
engine = NDAlphaEngine(
initial_capital=1000.0,
use_asset_selection=False,
use_direction_confirm=False,
use_sp_fees=False,
use_sp_slippage=False,
use_ob_edge=False,
use_alpha_layers=False,
use_dynamic_leverage=False,
lookback=1,
)
engine.begin_day("2026-05-08", posture="APEX", direction=1)
res = engine.step_bar(0, vel_div=0.025, prices={"BTCUSDT": 100.0}, vol_regime_ok=True)
assert res["entry"] is not None
assert res["entry"]["direction"] == 1
assert engine.position is not None
assert engine.position.direction == 1
def test_ndalphaengine_short_default_preserved():
engine = NDAlphaEngine(
initial_capital=1000.0,
use_asset_selection=False,
use_direction_confirm=False,
use_sp_fees=False,
use_sp_slippage=False,
use_ob_edge=False,
use_alpha_layers=False,
use_dynamic_leverage=False,
lookback=1,
)
engine.begin_day("2026-05-08", posture="APEX")
res = engine.step_bar(0, vel_div=-0.035, prices={"BTCUSDT": 100.0}, vol_regime_ok=True)
assert res["entry"] is not None
assert res["entry"]["direction"] == -1
def test_acb_short_default_and_long_cache_are_side_separated():
acb = AdaptiveCircuitBreaker()
acb._w750_threshold = 0.001
bullish = {
"funding_btc": 0.0002,
"dvol_btc": 30.0,
"fng": 80.0,
"taker": 1.25,
"available": True,
}
short = acb._calculate_signals(bullish)
long = acb._calculate_signals(bullish, direction=1)
assert short["signals"] == pytest.approx(0.0)
assert long["signals"] == pytest.approx(4.0)
snap = dict(bullish, _acb_ready=True, _staleness_s={})
short_hz = acb.get_dynamic_boost_from_hz("2026-05-08", snap, w750_velocity=0.002, direction=-1)
long_hz = acb.get_dynamic_boost_from_hz("2026-05-08", snap, w750_velocity=0.002, direction=1)
assert short_hz["side"] == "SHORT"
assert long_hz["side"] == "LONG"
assert short_hz["boost"] == pytest.approx(1.0)
assert long_hz["boost"] == pytest.approx(1.0 + 0.5 * math.log1p(4.0))
assert acb.get_dynamic_boost_for_date("2026-05-08")["side"] == "SHORT"
assert acb.get_dynamic_boost_for_date("2026-05-08", direction=1)["side"] == "LONG"
def test_acb_short_threshold_regression_values_still_match_v6():
acb = AdaptiveCircuitBreaker()
factors = {
"funding_btc": -0.0002,
"dvol_btc": 85.0,
"fng": 20.0,
"taker": 0.75,
"available": True,
}
result = acb._calculate_signals(factors)
assert result["signals"] == pytest.approx(4.0)
assert result["severity"] == 7
def test_acb_ob_beta_modulation_is_side_aware():
acb = AdaptiveCircuitBreaker()
acb._w750_threshold = 0.001
calm_ob = SimpleNamespace(
get_macro=lambda: SimpleNamespace(regime_signal=-1, depth_velocity=0.1, cascade_count=0)
)
stress_ob = SimpleNamespace(
get_macro=lambda: SimpleNamespace(regime_signal=1, depth_velocity=-0.3, cascade_count=2)
)
long_calm = acb.get_dynamic_boost_from_hz(
"2026-05-08", {"_acb_ready": True, "_staleness_s": {}}, w750_velocity=0.002, ob_engine=calm_ob, direction=1
)
short_calm = acb.get_dynamic_boost_from_hz(
"2026-05-09", {"_acb_ready": True, "_staleness_s": {}}, w750_velocity=0.002, ob_engine=calm_ob, direction=-1
)
short_stress = acb.get_dynamic_boost_from_hz(
"2026-05-10", {"_acb_ready": True, "_staleness_s": {}}, w750_velocity=0.002, ob_engine=stress_ob, direction=-1
)
assert long_calm["beta"] == pytest.approx(1.0)
assert short_calm["beta"] == pytest.approx(0.68)
assert short_stress["beta"] == pytest.approx(1.0)
def test_exit_manager_optional_vd_exit_is_long_aware():
manager = AlphaExitManager(vd_enabled=True, vd_consec_bars=2)
manager.setup_position("long-1", entry_price=100.0, direction=1, entry_bar=0)
first = manager.evaluate("long-1", current_price=100.1, current_bar=1, vel_div=-0.02)
second = manager.evaluate("long-1", current_price=100.1, current_bar=2, vel_div=-0.02)
assert first["action"] == "HOLD"
assert second["action"] == "EXIT"
assert second["reason"] == "VD_INVALIDATION"
def test_prodgreen_direction_parser_is_explicit_and_case_insensitive():
assert _trade_direction_from_config("LONG_ONLY") == 1
assert _trade_direction_from_config("short_only") == -1
with pytest.raises(ValueError):
_trade_direction_from_config("bidirectional")

View File

@@ -1,313 +0,0 @@
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass, field
from typing import Dict, List
import math
import random
import pytest
EPS = 1e-9
@dataclass
class ExitLeg:
trade_id: str
chain_root_trade_id: str
exit_seq: int
exit_leg_id: str
chain_prev_leg_id: str
chain_head_leg_id: str
command_id: str
fraction: float
qty: float
exit_price: float
fee: float
net_pnl: float
remaining_after: float
reason: str
chain_token: str
@dataclass
class ParentTrade:
trade_id: str
side: str # SHORT | LONG
entry_price: float
entry_qty: float
remaining_qty: float
realized_pnl_total: float = 0.0
realized_fees_total: float = 0.0
exit_seq: int = 0
status: str = "OPEN" # OPEN | PARTIALLY_CLOSED | CLOSED
version: int = 0
legs: List[ExitLeg] = field(default_factory=list)
chain_root_trade_id: str = ""
chain_head_leg_id: str = ""
chain_prev_leg_id: str = ""
chain_token: str = ""
def _chain_token(payload: dict) -> str:
return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str).encode()).hexdigest()
class MiniRetractionRuntime:
"""
Contract-reference runtime:
- all partial exits route through one handler
- idempotent by command_id
- financial accumulation by executed leg
"""
def __init__(self, fee_rate: float = 0.00055):
self.fee_rate = fee_rate
self.capital = 25_000.0
self.trades: Dict[str, ParentTrade] = {}
self.applied_commands: Dict[str, ExitLeg] = {}
def open_trade(self, trade_id: str, side: str, entry_price: float, qty: float) -> None:
assert trade_id not in self.trades
t = ParentTrade(
trade_id=trade_id,
side=side,
entry_price=entry_price,
entry_qty=qty,
remaining_qty=qty,
)
t.chain_root_trade_id = trade_id
t.chain_head_leg_id = f"{trade_id}:open"
t.chain_prev_leg_id = ""
t.chain_token = _chain_token({
"trade_id": trade_id,
"chain_root_trade_id": trade_id,
"chain_head_leg_id": t.chain_head_leg_id,
"chain_prev_leg_id": "",
"chain_seq": 0,
"side": side,
"entry_price": entry_price,
"entry_qty": qty,
"remaining_qty": qty,
"realized_pnl_total": 0.0,
"realized_fees_total": 0.0,
})
self.trades[trade_id] = t
def retract(
self,
trade_id: str,
*,
command_id: str,
fraction: float,
exit_price: float,
reason: str,
) -> ExitLeg | None:
if command_id in self.applied_commands:
return self.applied_commands[command_id]
if not (0 < fraction <= 1.0):
return None
t = self.trades.get(trade_id)
if not t or t.status == "CLOSED":
return None
expected = _chain_token({
"trade_id": t.trade_id,
"chain_root_trade_id": t.chain_root_trade_id or t.trade_id,
"chain_head_leg_id": t.chain_head_leg_id or f"{t.trade_id}:open",
"chain_prev_leg_id": t.chain_prev_leg_id or "",
"chain_seq": t.exit_seq,
"side": t.side,
"entry_price": t.entry_price,
"entry_qty": t.entry_qty,
"remaining_qty": t.remaining_qty,
"realized_pnl_total": t.realized_pnl_total,
"realized_fees_total": t.realized_fees_total,
})
if t.chain_token and t.chain_token != expected:
return None
requested_qty = t.remaining_qty * fraction
qty = min(max(requested_qty, 0.0), t.remaining_qty)
if qty <= EPS:
return None
sign = -1.0 if t.side == "SHORT" else 1.0
gross = sign * (exit_price - t.entry_price) * qty
fee = self.fee_rate * (t.entry_price * qty + exit_price * qty)
net = gross - fee
t.exit_seq += 1
t.version += 1
t.remaining_qty = max(0.0, t.remaining_qty - qty)
t.realized_pnl_total += net
t.realized_fees_total += fee
self.capital += net
t.status = "CLOSED" if t.remaining_qty <= EPS else "PARTIALLY_CLOSED"
prev_head = t.chain_head_leg_id or f"{t.trade_id}:open"
t.chain_prev_leg_id = prev_head
t.chain_head_leg_id = f"{t.trade_id}:x{t.exit_seq:03d}"
t.chain_token = _chain_token({
"trade_id": t.trade_id,
"chain_root_trade_id": t.chain_root_trade_id or t.trade_id,
"chain_head_leg_id": t.chain_head_leg_id,
"chain_prev_leg_id": prev_head,
"chain_seq": t.exit_seq,
"side": t.side,
"entry_price": t.entry_price,
"entry_qty": t.entry_qty,
"remaining_qty": t.remaining_qty,
"realized_pnl_total": t.realized_pnl_total,
"realized_fees_total": t.realized_fees_total,
})
leg = ExitLeg(
trade_id=t.trade_id,
chain_root_trade_id=t.chain_root_trade_id or t.trade_id,
exit_seq=t.exit_seq,
exit_leg_id=f"{t.trade_id}:x{t.exit_seq:03d}",
chain_prev_leg_id=prev_head,
chain_head_leg_id=t.chain_head_leg_id,
command_id=command_id,
fraction=fraction,
qty=qty,
exit_price=exit_price,
fee=fee,
net_pnl=net,
remaining_after=t.remaining_qty,
reason=reason,
chain_token=t.chain_token,
)
t.legs.append(leg)
self.applied_commands[command_id] = leg
return leg
def _assert_parent_invariants(t: ParentTrade) -> None:
total_qty = sum(l.qty for l in t.legs)
assert total_qty <= t.entry_qty + EPS
assert math.isclose(t.remaining_qty, max(0.0, t.entry_qty - total_qty), abs_tol=1e-8)
assert math.isclose(t.realized_pnl_total, sum(l.net_pnl for l in t.legs), rel_tol=0, abs_tol=1e-8)
assert math.isclose(t.realized_fees_total, sum(l.fee for l in t.legs), rel_tol=0, abs_tol=1e-8)
if t.legs:
assert t.chain_head_leg_id == t.legs[-1].exit_leg_id
assert t.chain_token == t.legs[-1].chain_token
if t.remaining_qty <= EPS:
assert t.status == "CLOSED"
elif t.legs:
assert t.status == "PARTIALLY_CLOSED"
else:
assert t.status == "OPEN"
def test_retract_default_half_then_close_preserves_lineage_and_math() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T1", "SHORT", 100.0, 10.0)
l1 = rt.retract("T1", command_id="c1", fraction=0.5, exit_price=99.0, reason="HOTKEY_RETRACT")
assert l1 is not None
assert l1.exit_leg_id == "T1:x001"
assert l1.qty == 5.0
assert l1.chain_prev_leg_id == "T1:open"
assert l1.chain_head_leg_id == "T1:x001"
l2 = rt.retract("T1", command_id="c2", fraction=1.0, exit_price=98.5, reason="HOTKEY_RETRACT")
assert l2 is not None
assert l2.exit_leg_id == "T1:x002"
assert l2.qty == 5.0
assert l2.chain_prev_leg_id == "T1:x001"
assert l2.chain_head_leg_id == "T1:x002"
t = rt.trades["T1"]
_assert_parent_invariants(t)
assert t.status == "CLOSED"
assert t.exit_seq == 2
def test_idempotent_command_does_not_double_execute() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T2", "SHORT", 50.0, 20.0)
first = rt.retract("T2", command_id="dup", fraction=0.5, exit_price=49.8, reason="V7_RETRACT")
second = rt.retract("T2", command_id="dup", fraction=0.5, exit_price=49.7, reason="V7_RETRACT")
assert first is not None
assert second is not None
assert first.exit_leg_id == second.exit_leg_id
assert len(rt.trades["T2"].legs) == 1
_assert_parent_invariants(rt.trades["T2"])
def test_invalid_fraction_rejected() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T3", "SHORT", 10.0, 10.0)
assert rt.retract("T3", command_id="a", fraction=0.0, exit_price=9.9, reason="HOTKEY_RETRACT") is None
assert rt.retract("T3", command_id="b", fraction=1.5, exit_price=9.9, reason="HOTKEY_RETRACT") is None
assert len(rt.trades["T3"].legs) == 0
_assert_parent_invariants(rt.trades["T3"])
def test_long_side_accounting_sign_is_correct() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T4", "LONG", 100.0, 2.0)
leg = rt.retract("T4", command_id="c", fraction=1.0, exit_price=101.0, reason="HOTKEY_RETRACT")
assert leg is not None
assert leg.net_pnl > 0
_assert_parent_invariants(rt.trades["T4"])
def test_over_many_random_partial_exits_invariants_hold() -> None:
rng = random.Random(1337)
rt = MiniRetractionRuntime()
rt.open_trade("T5", "SHORT", 120.0, 30.0)
for i in range(200):
t = rt.trades["T5"]
if t.status == "CLOSED":
break
# Biased toward smaller retractions; occasionally force full close
frac = 1.0 if i % 37 == 0 else max(0.01, min(0.99, rng.random() * 0.6))
px = 120.0 - rng.random() * 2.0
rt.retract("T5", command_id=f"cmd-{i}", fraction=frac, exit_price=px, reason="V7_RETRACT")
_assert_parent_invariants(rt.trades["T5"])
t = rt.trades["T5"]
# Must eventually close under forced 1.0 fractions
assert t.status == "CLOSED"
_assert_parent_invariants(t)
def test_capital_updates_are_leg_immediate() -> None:
rt = MiniRetractionRuntime()
start = rt.capital
rt.open_trade("T6", "SHORT", 200.0, 4.0)
l1 = rt.retract("T6", command_id="r1", fraction=0.5, exit_price=199.0, reason="HOTKEY_RETRACT")
assert l1 is not None
mid = rt.capital
assert not math.isclose(mid, start, abs_tol=1e-12)
l2 = rt.retract("T6", command_id="r2", fraction=1.0, exit_price=198.5, reason="HOTKEY_RETRACT")
assert l2 is not None
assert math.isclose(rt.capital, start + l1.net_pnl + l2.net_pnl, rel_tol=0, abs_tol=1e-8)
def test_command_on_closed_trade_is_noop() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T7", "SHORT", 100.0, 1.0)
rt.retract("T7", command_id="x1", fraction=1.0, exit_price=99.7, reason="HOTKEY_RETRACT")
t = rt.trades["T7"]
assert t.status == "CLOSED"
n = len(t.legs)
out = rt.retract("T7", command_id="x2", fraction=0.5, exit_price=99.6, reason="HOTKEY_RETRACT")
assert out is None
assert len(t.legs) == n
_assert_parent_invariants(t)
def test_tampered_chain_head_is_rejected() -> None:
rt = MiniRetractionRuntime()
rt.open_trade("T8", "SHORT", 75.0, 8.0)
first = rt.retract("T8", command_id="c1", fraction=0.5, exit_price=74.5, reason="HOTKEY_RETRACT")
assert first is not None
rt.trades["T8"].chain_head_leg_id = "T8:x999"
second = rt.retract("T8", command_id="c2", fraction=0.5, exit_price=74.0, reason="HOTKEY_RETRACT")
assert second is None
with pytest.raises(AssertionError):
_assert_parent_invariants(rt.trades["T8"])

View File

@@ -1,202 +0,0 @@
from __future__ import annotations
import json
import random
import threading
from dataclasses import dataclass
import importlib.util
from pathlib import Path
import pytest
_MOD_PATH = Path("/mnt/dolphinng5_predict/prod/nautilus_event_trader.py")
_SPEC = importlib.util.spec_from_file_location("nautilus_event_trader_mod", _MOD_PATH)
assert _SPEC and _SPEC.loader
mod = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(mod) # type: ignore[arg-type]
@dataclass
class _Pos:
trade_id: str
asset: str
entry_price: float
notional: float
current_price: float = 0.0
pnl_pct: float = 0.0
class _ExitMgr:
def __init__(self):
self._positions: dict[str, dict] = {}
class _Eng:
def __init__(self, pos: _Pos | None):
self.position = pos
self.capital = 25_000.0
self.exit_manager = _ExitMgr()
if pos:
self.exit_manager._positions[pos.trade_id] = {"dummy": True}
class _Map:
def __init__(self):
self._d = {"blue_runtime_commands": "[]"}
self._lock = threading.Lock()
def blocking(self):
return self
def get(self, key):
with self._lock:
return self._d.get(key)
def put(self, key, val):
with self._lock:
self._d[key] = val
class _F:
def add_done_callback(self, _cb):
return None
return _F()
def _mk_trader():
t = object.__new__(mod.DolphinLiveTrader)
t.eng_lock = threading.Lock()
t.control_map = _Map()
t._processed_retract_commands = mod.deque(maxlen=5000)
t._processed_retract_set = set()
t._pending_entries = {}
t.current_day = "2026-05-12"
t.bar_idx = 100
return t
def _install_open_position(t, *, trade_id="T", asset="STXUSDT", entry_price=1.0, notional=1000.0):
p = _Pos(trade_id, asset, entry_price, notional, current_price=entry_price)
t.eng = _Eng(p)
t._pending_entries[trade_id] = {
"trade_id": trade_id,
"asset": asset,
"side": "SHORT",
"entry_price": entry_price,
"entry_bar": 90,
"entry_date": "2026-05-12",
"notional": notional,
"notional_entry": notional,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
t._pending_entries[trade_id].update(t._chain_state_for_pending(
trade_id,
t._pending_entries[trade_id],
chain_mode="LIVE",
chain_head_leg_id=f"{trade_id}:open",
chain_prev_leg_id="",
chain_seq=0,
))
def test_fuzz_retraction_invariants_hold_under_random_command_stream(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
rng = random.Random(20260512)
t = _mk_trader()
_install_open_position(t, trade_id="T-FUZZ", asset="STXUSDT", entry_price=1.0, notional=10_000.0)
seen_ids: set[str] = set()
baseline_cap = t.eng.capital
for i in range(2500):
if t.eng.position is None:
break
px = max(0.00001, 1.0 + rng.uniform(-0.25, 0.25))
# Mix valid and invalid commands.
frac_choice = rng.choice([
rng.uniform(0.01, 1.0), # valid
0.0, # invalid
-0.1, # invalid
1.2, # invalid
])
# inject duplicate ids often
if i > 0 and rng.random() < 0.2:
cid = rng.choice(tuple(seen_ids)) if seen_ids else f"c-{i}"
else:
cid = f"c-{i}-{rng.randint(0, 999)}"
seen_ids.add(cid)
# wrong trade ids sometimes
tid = "T-FUZZ" if rng.random() < 0.8 else f"OTHER-{i}"
pending = t._pending_entries["T-FUZZ"]
cmd = {
"command_id": cid,
"trade_id": tid,
"action": "RETRACT",
"fraction": frac_choice,
"reason": "HOTKEY_RETRACT",
"source": "fuzz",
"chain_root_trade_id": pending["chain_root_trade_id"],
"chain_head_leg_id": pending["chain_head_leg_id"],
"chain_prev_leg_id": pending["chain_prev_leg_id"],
"chain_seq": pending["chain_seq"],
"chain_token": pending["chain_token"],
}
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
t._process_runtime_commands({"STXUSDT": px})
if t.eng.position is not None:
n = float(t.eng.position.notional)
assert n >= -1e-8
# never exceed original notional
assert n <= 10_000.0 + 1e-8
p = t._pending_entries["T-FUZZ"]
assert int(p.get("retraction_legs", 0) or 0) >= 0
# Capital must stay finite and deterministic.
assert t.eng.capital == pytest.approx(float(t.eng.capital))
assert abs(t.eng.capital - baseline_cap) < 1e7
def test_fuzz_concurrent_queue_submission_and_drain(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
rng = random.Random(777)
t = _mk_trader()
_install_open_position(t, trade_id="T-RACE", asset="DASHUSDT", entry_price=10.0, notional=5000.0)
def producer(start: int, count: int):
for i in range(start, start + count):
with t.control_map._lock:
raw = t.control_map._d.get("blue_runtime_commands", "[]")
q = json.loads(raw) if raw else []
q.append({
"command_id": f"p-{i}",
"trade_id": "T-RACE" if rng.random() < 0.9 else "OTHER",
"action": "RETRACT",
"fraction": rng.uniform(0.01, 1.0),
"reason": "HOTKEY_RETRACT",
"source": "race",
"chain_root_trade_id": t._pending_entries["T-RACE"]["chain_root_trade_id"],
"chain_head_leg_id": t._pending_entries["T-RACE"]["chain_head_leg_id"],
"chain_prev_leg_id": t._pending_entries["T-RACE"]["chain_prev_leg_id"],
"chain_seq": t._pending_entries["T-RACE"]["chain_seq"],
"chain_token": t._pending_entries["T-RACE"]["chain_token"],
})
t.control_map._d["blue_runtime_commands"] = json.dumps(q[-200:])
threads = [threading.Thread(target=producer, args=(k * 120, 120)) for k in range(4)]
for th in threads:
th.start()
for th in threads:
th.join()
# Drain repeatedly; must not throw and must preserve invariants.
for _ in range(50):
if t.eng.position is None:
break
t._process_runtime_commands({"DASHUSDT": rng.uniform(8.0, 12.0)})
if t.eng.position is not None:
assert t.eng.position.notional >= -1e-8
assert t.eng.position.notional <= 5000.0 + 1e-8

View File

@@ -1,394 +0,0 @@
from __future__ import annotations
import json
import threading
import tempfile
from dataclasses import dataclass
import importlib.util
from pathlib import Path
import pytest
_MOD_PATH = Path("/mnt/dolphinng5_predict/prod/nautilus_event_trader.py")
_SPEC = importlib.util.spec_from_file_location("nautilus_event_trader_mod", _MOD_PATH)
assert _SPEC and _SPEC.loader
mod = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(mod) # type: ignore[arg-type]
@dataclass
class _Pos:
trade_id: str
asset: str
entry_price: float
notional: float
current_price: float = 0.0
pnl_pct: float = 0.0
class _ExitMgr:
def __init__(self) -> None:
self._positions: dict[str, dict] = {}
class _Eng:
def __init__(self, pos: _Pos | None, capital: float = 25_000.0) -> None:
self.position = pos
self.capital = capital
self.exit_manager = _ExitMgr()
if pos is not None:
self.exit_manager._positions[pos.trade_id] = {"dummy": True}
class _Map:
def __init__(self, initial: dict | None = None) -> None:
self._d = dict(initial or {})
self._lock = threading.Lock()
def blocking(self):
return self
def get(self, key):
with self._lock:
return self._d.get(key)
def put(self, key, val):
with self._lock:
self._d[key] = val
class _F:
def add_done_callback(self, _cb):
return None
return _F()
def _mk_trader() -> mod.DolphinLiveTrader:
t = object.__new__(mod.DolphinLiveTrader)
tmpdir = Path(tempfile.mkdtemp(prefix="dolphin_retract_test_"))
mod.CAPITAL_DISK_CHECKPOINT = tmpdir / "capital_checkpoint.json"
mod.CAPITAL_CORRECTIVE_REPLAY = tmpdir / "capital_replay.json"
mod.CAPITAL_UPDATE_LEDGER = tmpdir / "capital_update_ledger.json"
t.eng_lock = threading.Lock()
t.state_map = _Map({})
t.pnl_map = _Map({})
t.control_map = _Map({"blue_runtime_commands": "[]"})
t._processed_retract_commands = mod.deque(maxlen=5000)
t._processed_retract_set = set()
t._pending_entries = {}
t.current_day = "2026-05-12"
t.bar_idx = 100
return t
def _seed_chain(t: mod.DolphinLiveTrader, trade_id: str) -> None:
pending = t._pending_entries[trade_id]
pending.update(t._chain_state_for_pending(
trade_id,
pending,
chain_mode="LIVE",
chain_head_leg_id=f"{trade_id}:open",
chain_prev_leg_id="",
chain_seq=0,
))
def _retract_cmd(t: mod.DolphinLiveTrader, trade_id: str, *, command_id: str, fraction: float, reason: str) -> dict:
pending = t._pending_entries[trade_id]
return {
"command_id": command_id,
"trade_id": trade_id,
"action": "RETRACT",
"fraction": fraction,
"reason": reason,
"source": "tui_hotkey",
"chain_root_trade_id": pending["chain_root_trade_id"],
"chain_head_leg_id": pending["chain_head_leg_id"],
"chain_prev_leg_id": pending["chain_prev_leg_id"],
"chain_seq": pending["chain_seq"],
"chain_token": pending["chain_token"],
}
def test_runtime_command_partial_exit_updates_position_and_capital(monkeypatch):
rows = []
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123456789)
t = _mk_trader()
pos = _Pos("T-1", "STXUSDT", 1.0, 1000.0, current_price=0.95)
t.eng = _Eng(pos, capital=25000.0)
t._pending_entries["T-1"] = {
"asset": "STXUSDT",
"side": "SHORT",
"entry_price": 1.0,
"entry_bar": 90,
"entry_date": "2026-05-12",
"notional": 1000.0,
"notional_entry": 1000.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-1")
cmd = _retract_cmd(t, "T-1", command_id="c-1", fraction=0.5, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
forced = t._process_runtime_commands({"STXUSDT": 0.95})
assert forced is None
assert t.eng.position is not None
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 500.0
assert t._pending_entries["T-1"]["retraction_legs"] == 1
assert pytest.approx(t._pending_entries["T-1"]["realized_pnl_legs_total"], abs=1e-9) == 25.0
assert pytest.approx(t.eng.capital, abs=1e-9) == 25025.0
assert any(tbl == "trade_exit_legs" for tbl, _ in rows)
recon_rows = [r for tbl, r in rows if tbl == "trade_reconstruction"]
assert recon_rows
assert any(json.loads(r["payload_json"]).get("chain", {}).get("chain_token") for r in recon_rows)
assert any(tbl == "hotkey_audit" and r["result"] == "PARTIAL_OK" for tbl, r in rows)
stale_cmd = dict(cmd)
stale_cmd["command_id"] = "c-1-stale"
t.control_map.put("blue_runtime_commands", json.dumps([stale_cmd]))
t._process_runtime_commands({"STXUSDT": 0.94})
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 500.0
assert any(tbl == "hotkey_audit" and "CHAIN_MISMATCH" in r["result"] for tbl, r in rows)
def test_runtime_command_full_close_returns_forced_exit(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-2", "FETUSDT", 2.0, 200.0, current_price=1.9)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["T-2"] = {
"asset": "FETUSDT",
"side": "SHORT",
"entry_price": 2.0,
"entry_bar": 95,
"entry_date": "2026-05-12",
"notional": 200.0,
"notional_entry": 200.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-2")
cmd = _retract_cmd(t, "T-2", command_id="c-2", fraction=1.0, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
forced = t._process_runtime_commands({"FETUSDT": 1.9})
assert forced is not None
assert forced["trade_id"] == "T-2"
assert forced["reason"] == "HOTKEY_RETRACT"
assert forced["capital_already_realized"] is True
assert forced["economic_pnl"] == pytest.approx(forced["net_pnl"], abs=1e-12)
assert forced["economic_pnl_pct"] == pytest.approx(forced["pnl_pct"], abs=1e-12)
assert t.eng.position is None
assert "T-2" not in t.eng.exit_manager._positions
def test_full_retract_close_path_does_not_double_apply_capital(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
t.state_map = _Map({})
t.pnl_map = _Map({})
pos = _Pos("T-2B", "FETUSDT", 2.0, 200.0, current_price=1.9)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["T-2B"] = {
"asset": "FETUSDT",
"side": "SHORT",
"entry_price": 2.0,
"entry_bar": 95,
"entry_date": "2026-05-12",
"notional": 200.0,
"notional_entry": 200.0,
"quantity": 100.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-2B")
cmd = _retract_cmd(t, "T-2B", command_id="c-2b", fraction=1.0, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
forced = t._process_runtime_commands({"FETUSDT": 1.9})
assert forced is not None
# First accounting application happened in retract leg.
assert t.eng.capital == pytest.approx(1010.0, abs=1e-9)
pending = t._pending_entries["T-2B"]
realized_pnl, realized_source = t._resolved_realized_trade_pnl(pending, forced, exit_price=1.9)
assert realized_source == "net_pnl"
assert realized_pnl == pytest.approx(10.0, abs=1e-9)
# Close-path accounting must be suppressed because leg accounting already realized pnl.
cap_delta, cap_source = t._resolved_capital_apply_pnl(forced, realized_pnl)
assert cap_source == "already_realized"
assert cap_delta == pytest.approx(0.0, abs=1e-12)
cap_before, cap_after = t._apply_trade_capital_update(
cap_delta,
reason="HOTKEY_RETRACT",
source="trade_close",
trade_id="T-2B",
asset="FETUSDT",
)
assert cap_before == pytest.approx(1010.0, abs=1e-9)
assert cap_after == pytest.approx(1010.0, abs=1e-9)
assert t.eng.capital == pytest.approx(1010.0, abs=1e-9)
def test_idempotent_replay_is_noop(monkeypatch):
rows = []
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-3", "DASHUSDT", 10.0, 1000.0, current_price=9.5)
t.eng = _Eng(pos, capital=5000.0)
t._pending_entries["T-3"] = {
"asset": "DASHUSDT",
"side": "SHORT",
"entry_price": 10.0,
"entry_bar": 90,
"entry_date": "2026-05-12",
"notional": 1000.0,
"notional_entry": 1000.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-3")
cmd = _retract_cmd(t, "T-3", command_id="dup", fraction=0.5, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd, cmd]))
t._process_runtime_commands({"DASHUSDT": 9.5})
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 500.0
replays = [r for tbl, r in rows if tbl == "hotkey_audit" and r.get("result") == "IDEMPOTENT_REPLAY"]
assert replays
def test_idempotent_replay_does_not_change_capital(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-3B", "DASHUSDT", 10.0, 1000.0, current_price=9.5)
t.eng = _Eng(pos, capital=5000.0)
t._pending_entries["T-3B"] = {
"asset": "DASHUSDT",
"side": "SHORT",
"entry_price": 10.0,
"entry_bar": 90,
"entry_date": "2026-05-12",
"notional": 1000.0,
"notional_entry": 1000.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-3B")
cmd = _retract_cmd(t, "T-3B", command_id="dup-2", fraction=0.5, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd, cmd]))
t._process_runtime_commands({"DASHUSDT": 9.5})
cap_after_first = t.eng.capital
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
t._process_runtime_commands({"DASHUSDT": 9.5})
assert t.eng.capital == pytest.approx(cap_after_first, abs=1e-9)
def test_trade_id_mismatch_is_rejected_and_position_unchanged(monkeypatch):
rows = []
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-4", "STXUSDT", 1.0, 1000.0, current_price=1.01)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["T-4"] = {"asset": "STXUSDT", "side": "SHORT", "entry_price": 1.0, "entry_bar": 80, "entry_date": "2026-05-12"}
_seed_chain(t, "T-4")
cmd = {"command_id": "bad", "trade_id": "OTHER", "action": "RETRACT", "fraction": 0.5, "reason": "HOTKEY_RETRACT", "chain_root_trade_id": "OTHER", "chain_head_leg_id": "OTHER:open", "chain_prev_leg_id": "", "chain_seq": 0, "chain_token": "stale"}
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
t._process_runtime_commands({"STXUSDT": 1.01})
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 1000.0
assert any(tbl == "hotkey_audit" and "TRADE_MISMATCH" in r["result"] for tbl, r in rows)
def test_command_queue_drained_atomically(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-5", "LINKUSDT", 10.0, 1000.0, current_price=9.8)
t.eng = _Eng(pos, capital=500.0)
t._pending_entries["T-5"] = {"asset": "LINKUSDT", "side": "SHORT", "entry_price": 10.0, "entry_bar": 88, "entry_date": "2026-05-12"}
_seed_chain(t, "T-5")
cmds = [
_retract_cmd(t, "T-5", command_id="a", fraction=0.25, reason="HOTKEY_RETRACT"),
_retract_cmd(t, "T-5", command_id="b", fraction=0.25, reason="HOTKEY_RETRACT"),
]
t.control_map.put("blue_runtime_commands", json.dumps(cmds))
t._process_runtime_commands({"LINKUSDT": 9.8})
assert t.control_map.get("blue_runtime_commands") == "[]"
def test_bad_fraction_rejected(monkeypatch):
rows = []
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-6", "SOLUSDT", 100.0, 1000.0, current_price=95.0)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["T-6"] = {"asset": "SOLUSDT", "side": "SHORT", "entry_price": 100.0, "entry_bar": 80, "entry_date": "2026-05-12"}
_seed_chain(t, "T-6")
cmd = {"command_id": "badfrac", "trade_id": "T-6", "action": "RETRACT", "fraction": 0.0, "reason": "HOTKEY_RETRACT", "chain_root_trade_id": t._pending_entries["T-6"]["chain_root_trade_id"], "chain_head_leg_id": t._pending_entries["T-6"]["chain_head_leg_id"], "chain_prev_leg_id": t._pending_entries["T-6"]["chain_prev_leg_id"], "chain_seq": t._pending_entries["T-6"]["chain_seq"], "chain_token": t._pending_entries["T-6"]["chain_token"]}
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
t._process_runtime_commands({"SOLUSDT": 95.0})
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 1000.0
assert any(tbl == "hotkey_audit" and r["result"] == "BAD_FRACTION" for tbl, r in rows)
def test_retract_with_missing_price_falls_back_to_entry_and_keeps_capital(monkeypatch):
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("T-6B", "SOLUSDT", 100.0, 1000.0, current_price=0.0)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["T-6B"] = {
"asset": "SOLUSDT",
"side": "SHORT",
"entry_price": 100.0,
"entry_bar": 80,
"entry_date": "2026-05-12",
"notional": 1000.0,
"notional_entry": 1000.0,
"retraction_legs": 0,
"realized_pnl_legs_total": 0.0,
}
_seed_chain(t, "T-6B")
cmd = _retract_cmd(t, "T-6B", command_id="c-6b", fraction=0.5, reason="HOTKEY_RETRACT")
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
t._process_runtime_commands({})
assert t.eng.capital == pytest.approx(1000.0, abs=1e-9)
def test_multi_slot_future_safety_non_target_commands_do_not_mutate_open_slot(monkeypatch):
"""
Future-proof guard: if multiple slot commands exist, only matching trade_id may mutate current open position.
"""
rows = []
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
t = _mk_trader()
pos = _Pos("ACTIVE", "ATOMUSDT", 5.0, 500.0, current_price=4.9)
t.eng = _Eng(pos, capital=1000.0)
t._pending_entries["ACTIVE"] = {"asset": "ATOMUSDT", "side": "SHORT", "entry_price": 5.0, "entry_bar": 99, "entry_date": "2026-05-12"}
_seed_chain(t, "ACTIVE")
cmds = [
{"command_id": "x1", "trade_id": "INACTIVE", "action": "RETRACT", "fraction": 1.0, "reason": "HOTKEY_RETRACT", "chain_root_trade_id": "INACTIVE", "chain_head_leg_id": "INACTIVE:open", "chain_prev_leg_id": "", "chain_seq": 0, "chain_token": "stale"},
_retract_cmd(t, "ACTIVE", command_id="x2", fraction=0.5, reason="HOTKEY_RETRACT"),
]
t.control_map.put("blue_runtime_commands", json.dumps(cmds))
t._process_runtime_commands({"ATOMUSDT": 4.9})
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 250.0
assert any(tbl == "hotkey_audit" and "TRADE_MISMATCH" in r["result"] for tbl, r in rows)
assert any(tbl == "hotkey_audit" and r["result"] == "PARTIAL_OK" for tbl, r in rows)

View File

@@ -1,182 +0,0 @@
"""L1 — async-fill pump.
A resting order (LIMIT-style: ACK on submit, no synchronous fill) fills on a
*later* venue reconcile. `PinkDirectRuntime.pump_venue_events()` must drain that
fill into the kernel so capital settles and the FSM advances, persist the result,
and dedup duplicate reconcile events (no double-settle). MockVenue only; no exchange.
"""
from __future__ import annotations
import asyncio
from datetime import datetime, timezone
from prod.clean_arch.dita_v2 import (
ExecutionKernel,
InMemoryControlPlane,
KernelCommandType,
KernelControlSnapshot,
KernelEventKind,
KernelMode,
KernelVerbosity,
MemoryKernelJournal,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
VenueEvent,
VenueEventStatus,
)
from prod.clean_arch.dita_v2.contracts import KernelIntent, TradeStage
from prod.clean_arch.dita import DecisionConfig, DecisionEngine, IntentEngine
from prod.clean_arch.persistence import PinkClickHousePersistence
from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime
from prod.clean_arch.ports.data_feed import DataFeedPort
class _Sink:
def __init__(self) -> None:
self.calls: list[tuple[str, dict]] = []
def __call__(self, table: str, row: dict) -> None:
self.calls.append((table, dict(row)))
def tables(self) -> list[str]:
return [t for t, _ in self.calls]
class _StubFeed(DataFeedPort):
async def connect(self) -> bool:
return True
async def disconnect(self) -> None:
pass
async def get_latest_snapshot(self, symbol):
return None
async def subscribe_snapshots(self, callback) -> None:
pass
async def get_acb_update(self):
return None
def get_latency_ms(self) -> float:
return 0.0
def health_check(self) -> bool:
return True
class _DelayedFillVenue(MockVenueAdapter):
"""MockVenue whose submit ACKs only; queued fills surface on reconcile()."""
def __init__(self, scenario=None) -> None:
super().__init__(scenario)
self._pending: list[VenueEvent] = []
def queue(self, event: VenueEvent) -> None:
self._pending.append(event)
def reconcile(self):
out, self._pending = list(self._pending), []
return out
def _mk_runtime():
# ACK-only: no synchronous fill on submit (resting order).
venue = _DelayedFillVenue(
MockVenueScenario(emit_fill_on_submit=False, partial_fill_ratio=0.0, emit_ack_before_fill=True)
)
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=venue,
journal=MemoryKernelJournal(),
)
kernel.account.snapshot.capital = 25_000.0
kernel.account.snapshot.peak_capital = 25_000.0
kernel.account.snapshot.equity = 25_000.0
sink = _Sink()
cfg = DecisionConfig()
persistence = PinkClickHousePersistence(kernel.account, sink=sink, v7_sink=sink)
runtime = PinkDirectRuntime(
data_feed=_StubFeed(), kernel=kernel,
decision_engine=DecisionEngine(cfg), intent_engine=IntentEngine(cfg),
persistence=persistence, market_state_runtime=None,
)
return runtime, kernel, venue, sink
def _intent(action, *, size, price, reason="TEST"):
return KernelIntent(
timestamp=datetime.now(timezone.utc), intent_id=f"i-{reason}", trade_id="T1",
slot_id=0, asset="BTCUSDT", side=TradeSide.SHORT, action=action,
reference_price=price, target_size=size, leverage=2.0, exit_leg_ratios=(1.0,), reason=reason,
)
def _fill_for(order, *, kind, price, filled, remaining, eid):
return VenueEvent(
timestamp=datetime.now(timezone.utc), event_id=eid, trade_id="T1", slot_id=0,
kind=kind, status=VenueEventStatus.FILLED if kind == KernelEventKind.FULL_FILL else VenueEventStatus.PARTIALLY_FILLED,
venue_order_id=order.venue_order_id, venue_client_id=order.venue_client_id,
side=TradeSide.SHORT, asset="BTCUSDT", price=price, size=1.0,
filled_size=filled, remaining_size=remaining,
)
def test_resting_entry_fills_via_pump_and_dedups():
runtime, kernel, venue, sink = _mk_runtime()
# ENTER rests (ACK only, nothing filled).
kernel.process_intent(_intent(KernelCommandType.ENTER, size=1.0, price=100.0))
slot = kernel.slot(0)
assert slot.fsm_state == TradeStage.ENTRY_WORKING
assert abs(slot.size) < 1e-9
entry_order = slot.active_entry_order
assert entry_order is not None
# A later reconcile surfaces the fill -> pump settles it.
venue.queue(_fill_for(entry_order, kind=KernelEventKind.FULL_FILL, price=100.0, filled=1.0, remaining=0.0, eid="EVF1"))
applied = asyncio.run(runtime.pump_venue_events())
assert applied == 1
assert kernel.slot(0).fsm_state == TradeStage.POSITION_OPEN
assert abs(kernel.slot(0).size - 1.0) < 1e-9
assert "account_events" in sink.tables() and "position_state" in sink.tables()
assert "ENTRY_FILLED" in [r["event_type"] for t, r in sink.calls if t == "trade_reconstruction"]
# Duplicate reconcile event -> kernel dedups; pump applies nothing, no double-settle.
cap_before = kernel.account.snapshot.capital
rows_before = len(sink.calls)
venue.queue(_fill_for(entry_order, kind=KernelEventKind.FULL_FILL, price=100.0, filled=1.0, remaining=0.0, eid="EVF1"))
applied2 = asyncio.run(runtime.pump_venue_events())
assert applied2 == 0, "duplicate fill must be deduped by the kernel"
assert kernel.account.snapshot.capital == cap_before
assert len(sink.calls) == rows_before, "no rows persisted for a deduped event"
def test_resting_exit_fills_via_pump_settles_capital():
runtime, kernel, venue, sink = _mk_runtime()
# Open a position via the pump (entry rests, then fills).
kernel.process_intent(_intent(KernelCommandType.ENTER, size=1.0, price=100.0))
venue.queue(_fill_for(kernel.slot(0).active_entry_order, kind=KernelEventKind.FULL_FILL, price=100.0, filled=1.0, remaining=0.0, eid="EVE1"))
asyncio.run(runtime.pump_venue_events())
assert kernel.slot(0).fsm_state == TradeStage.POSITION_OPEN
cap_after_entry = kernel.account.snapshot.capital # entry does not realize PnL
# EXIT rests (ACK only), then fills @90 on a later reconcile -> SHORT profit.
kernel.process_intent(_intent(KernelCommandType.EXIT, size=1.0, price=90.0, reason="TP"))
exit_order = kernel.slot(0).active_exit_order
assert exit_order is not None
venue.queue(_fill_for(exit_order, kind=KernelEventKind.FULL_FILL, price=90.0, filled=1.0, remaining=0.0, eid="EVX1"))
applied = asyncio.run(runtime.pump_venue_events())
assert applied == 1
assert kernel.slot(0).closed
assert kernel.slot(0).fsm_state == TradeStage.CLOSED
# SHORT 1.0 @100 -> exit @90, leverage 2 => realized profit > 0; capital rose.
assert kernel.account.snapshot.capital > cap_after_entry
tables = sink.tables()
assert "trade_exit_legs" in tables, "async exit must persist a leg row"
assert "trade_events" in tables, "async close must persist a terminal trade_event"

File diff suppressed because it is too large Load Diff

View File

@@ -1,420 +0,0 @@
#!/usr/bin/env python3
"""PINK capital-accounting harness — automated scenario battery, live BingX VST.
Pre-cutover gate: drives the REAL PINK runtime (MarketSnapshot -> DecisionEngine ->
IntentEngine -> PinkDirectRuntime.step -> kernel -> BingX VST -> AccountProjection ->
PinkClickHousePersistence) through controlled scenarios via crafted snapshots, and
asserts capital-accounting correctness at every step. Controlled: flat account,
single scenarios sequentially, flatten-between, small (~$20) sizes, no autonomous loop.
Capital invariants asserted (the crux):
1. per-fill : Δcapital == realized PnL of that fill (kernel single authority)
2. end-of-run : kernel.capital == start + Σrealized (flat -> unrealized 0)
3. exchange : position flat + zero open orders (signed read)
4. persistence: trade_events.capital_before/after + account_events.capital match kernel
5. sizing : every order notional = size×price ≤ capital × max_leverage (never inf)
6. guards : suppressed/degenerate ENTERs place NO order; exits size from slot.size
Gates: BINGX_SMOKE_LIVE, BINGX_SMOKE_ALLOW_TRADE, PINK_DITA_E2E, PINK_CAPITAL_HARNESS.
Run on a FLAT account, from repo root, PYTHONPATH=/mnt/dolphinng5_predict.
"""
from __future__ import annotations
import asyncio
import os
from datetime import datetime, timezone
import pytest
for _gate in ("BINGX_SMOKE_LIVE", "BINGX_SMOKE_ALLOW_TRADE", "PINK_DITA_E2E", "PINK_CAPITAL_HARNESS"):
if not os.environ.get(_gate):
pytest.skip(f"{_gate} not set", allow_module_level=True)
from prod.tests.test_pink_bingx_dita_live_e2e import ( # noqa: E402
_build_config, _pick_sym, _snap, _verify, _check_open_orders, _flatten, _contract_rows,
)
from prod.bingx.http import BingxHttpClient # noqa: E402
from prod.clean_arch.dita import ( # noqa: E402
DecisionAction, DecisionConfig, DecisionEngine, IntentEngine,
)
from prod.clean_arch.dita_v2.launcher import build_launcher_bundle # noqa: E402
from prod.clean_arch.persistence import PinkClickHousePersistence # noqa: E402
from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime # noqa: E402
from prod.clean_arch.ports.data_feed import MarketSnapshot, DataFeedPort # noqa: E402
_MAX_LEVERAGE = 3.0
_CAP_FRACTION = 2.5e-4 # ~ $20 notional on 25k seed -> clears exchange min, safe vs margin
_SEED_CAPITAL = 25_000.0
class _CaptureSink:
def __init__(self) -> None:
self.rows: list[tuple[str, dict]] = []
def __call__(self, table: str, row: dict) -> None:
self.rows.append((table, dict(row)))
def of(self, table: str) -> list[dict]:
return [r for t, r in self.rows if t == table]
def tables(self) -> list[str]:
return [t for t, _ in self.rows]
class _StubFeed(DataFeedPort):
async def connect(self) -> bool:
return True
async def disconnect(self) -> None:
pass
async def get_latest_snapshot(self, symbol):
return None
async def subscribe_snapshots(self, callback) -> None:
pass
async def get_acb_update(self):
return None
def get_latency_ms(self) -> float:
return 0.0
def health_check(self) -> bool:
return True
def _config(exit_leg_ratios=(1.0,)) -> DecisionConfig:
return DecisionConfig(
vel_div_threshold=-0.02, vel_div_extreme=-0.05, fixed_tp_pct=0.0020,
max_hold_bars=250, capital_fraction=_CAP_FRACTION, max_leverage=_MAX_LEVERAGE,
min_irp_alignment=0.0, allow_long=False, allow_short=True,
exit_leg_ratios=exit_leg_ratios, policy_version="pink_capital_harness",
)
def _build_runtime(sink: _CaptureSink, exit_leg_ratios=(1.0,), capital=_SEED_CAPITAL):
cfg = _config(exit_leg_ratios)
bundle = build_launcher_bundle(venue_mode="BINGX", max_slots=1, bingx_config=_build_config(_SEED_CAPITAL))
k = bundle.kernel
k.account.snapshot.capital = capital
k.account.snapshot.peak_capital = capital if capital == capital else _SEED_CAPITAL
k.account.snapshot.equity = capital
persistence = PinkClickHousePersistence(k.account, sink=sink, v7_sink=sink)
runtime = PinkDirectRuntime(
data_feed=_StubFeed(), kernel=k,
decision_engine=DecisionEngine(cfg), intent_engine=IntentEngine(cfg),
persistence=persistence, market_state_runtime=None,
)
return runtime, k
def _snap_signal(symbol: str, price: float, vel_div: float) -> MarketSnapshot:
return MarketSnapshot(
timestamp=datetime.now(timezone.utc), symbol=symbol, price=price,
bid=price * 0.999, ask=price * 1.001, eigenvalues=[1.0],
velocity_divergence=vel_div, irp_alignment=0.5, scan_number=1, source="harness",
)
async def _await(kernel, predicate, *, timeout_s: float = 12.0, step_s: float = 0.5) -> bool:
waited = 0.0
while waited < timeout_s:
if predicate(kernel.slot(0)):
return True
await asyncio.sleep(step_s)
waited += step_s
return predicate(kernel.slot(0))
def _capital(kernel) -> float:
return float(kernel.account.snapshot.capital or 0.0)
def _realized(kernel) -> float:
return sum(float(kernel.slot(i).realized_pnl or 0.0) for i in range(kernel.max_slots))
async def _full_flatten(client, vsym):
try:
oos = await _check_open_orders(client, vsym)
if oos:
await client._request_json("DELETE", "/openApi/swap/v2/trade/allOpenOrders", {"symbol": vsym}, signed=True)
except Exception:
pass
def _pf(row, *keys) -> float:
for k in keys:
try:
v = float(row.get(k) or 0.0)
except Exception:
continue
if v != 0.0:
return v
return 0.0
async def _ensure_account_flat(client) -> None:
"""Reliable exchange-truth close-all via the proven kernel EXIT path (mirrors
the standalone flatten tool): build a throwaway BingX kernel, reconcile each
open position into the slot and EXIT it (reduce-only MARKET). Handles
multi-symbol residuals so every scenario starts from a verified-flat account
and a residual-leaving scenario (e.g. the known multi_leg one) cannot cascade
into the rest of the battery."""
from prod.clean_arch.dita_v2.contracts import (
TradeSlot, TradeSide, TradeStage, KernelIntent, KernelCommandType,
)
bundle = build_launcher_bundle(venue_mode="BINGX", max_slots=1, bingx_config=_build_config(_SEED_CAPITAL))
k = bundle.kernel
k.venue.connect()
qty_keys = ("positionAmt", "positionQty", "positionSize", "quantity", "pa", "qty")
positions = [p for p in k.venue.open_positions() if abs(_pf(p, *qty_keys)) > 1e-12]
for p in positions:
amt = _pf(p, *qty_keys)
qty = abs(amt)
raw_side = str(p.get("positionSide") or p.get("side") or "").upper()
side = TradeSide.SHORT if raw_side in {"SHORT", "SELL"} or amt < 0 else TradeSide.LONG
entry = _pf(p, "entryPrice", "avgPrice", "avgEntryPrice", "ep", "ap", "price")
mark = _pf(p, "markPrice", "mark", "price") or entry
lev = _pf(p, "leverage", "lev") or 1.0
asset = str(p.get("symbol") or p.get("symbolName") or "").replace("-", "").upper()
if qty <= 0 or not asset:
continue
k.reconcile_from_slots([TradeSlot(
slot_id=0, trade_id=asset, asset=asset, side=side, entry_price=entry or mark,
size=qty, initial_size=qty, leverage=lev, entry_time=datetime.now(timezone.utc),
fsm_state=TradeStage.POSITION_OPEN, metadata={"flatten": True},
)])
try:
k.process_intent(KernelIntent(
timestamp=datetime.now(timezone.utc), intent_id=f"flat-{asset}", trade_id=asset,
slot_id=0, asset=asset, side=side, action=KernelCommandType.EXIT,
reference_price=mark, target_size=qty, leverage=lev, exit_leg_ratios=(1.0,),
reason="FLATTEN", metadata={},
))
except Exception:
pass
try:
rows = await _contract_rows(client)
for s in {str(r.get("symbol") or "") for r in rows if isinstance(r, dict)}:
if s:
try:
await client._request_json("DELETE", "/openApi/swap/v2/trade/allOpenOrders", {"symbol": s}, signed=True)
except Exception:
pass
except Exception:
pass
await asyncio.sleep(0.8)
# --------------------------------------------------------------------------
# scenario primitives
# --------------------------------------------------------------------------
async def _open(runtime, kernel, symbol: str, price: float) -> float:
cap_before = _capital(kernel)
dec = await runtime.step(_snap_signal(symbol, price, vel_div=-0.05))
assert dec.action == DecisionAction.ENTER, f"expected ENTER, got {dec.action}/{dec.reason}"
assert await _await(kernel, lambda s: s.is_open() and s.size > 0), (
f"position never opened (state={kernel.slot(0).fsm_state}, size={kernel.slot(0).size})"
)
assert abs(_capital(kernel) - cap_before) < 1e-6, "entry must not realize PnL / move capital"
slot = kernel.slot(0)
entry = float(slot.entry_price or price)
# invariant 5: notional bound (margin-self-limiting)
notional = float(slot.size) * entry
assert notional <= _capital(kernel) * _MAX_LEVERAGE + 1e-6, (
f"notional {notional} exceeds margin bound {_capital(kernel) * _MAX_LEVERAGE}"
)
return entry
async def _exit_leg(runtime, kernel, symbol: str, entry_price: float) -> bool:
cap_before = _capital(kernel)
rp_before = _realized(kernel)
size_before = float(kernel.slot(0).size or 0.0)
dec = await runtime.step(_snap_signal(symbol, entry_price * 0.99, vel_div=0.0))
assert dec.action == DecisionAction.EXIT, f"expected EXIT, got {dec.action}/{dec.reason}"
await _await(
kernel,
lambda s: s.closed or float(s.realized_pnl or 0.0) != rp_before or float(s.size or 0.0) < size_before - 1e-12,
)
leg_realized = _realized(kernel) - rp_before
# invariant 1: per-fill Δcapital == realized PnL of that fill
assert abs((_capital(kernel) - cap_before) - leg_realized) < 1e-6, (
f"per-fill mismatch: Δcap={_capital(kernel) - cap_before} realized_leg={leg_realized}"
)
# Accumulate the cumulative realized across the whole scenario. slot.realized_pnl
# resets on each ENTER (Flaw 13), so multi-cycle reconciliation must sum the
# per-fill deltas, not read the slot's current realized.
runtime.__dict__.setdefault("_realized_legs", []).append(leg_realized)
return kernel.slot(0).closed
def _assert_end_invariants(kernel, start_cap: float, total_realized: float, sink: _CaptureSink):
cap = _capital(kernel)
realized = total_realized # cumulative across cycles (slot.realized resets on ENTER)
# invariant 2: capital moved EXACTLY by the sum of per-fill realized PnL — no
# phantom capital movement (entries don't move capital; each exit moves by its
# realized). Flat at end -> no unrealized component.
assert abs((cap - start_cap) - realized) < 1e-6, (
f"end reconciliation: Δcap={cap - start_cap} Σrealized={realized} (cap={cap} start={start_cap})"
)
# invariant 4: persistence parity
tes = sink.of("trade_events")
if tes:
assert abs(float(tes[-1]["capital_after"]) - cap) < 1e-6, "trade_events.capital_after != kernel capital"
assert abs(float(tes[-1]["capital_after"]) - float(tes[-1]["capital_before"]) - float(tes[-1]["pnl"])) < 1e-6, (
"trade_events: capital_after - capital_before != pnl"
)
aes = sink.of("account_events")
if aes:
assert abs(float(aes[-1]["capital"]) - cap) < 1e-6, "account_events.capital != kernel capital"
legs = sink.of("trade_exit_legs")
if legs:
leg_sum = sum(float(r["pnl_leg"]) for r in legs)
assert abs(leg_sum - realized) < 1e-6, f"Σ trade_exit_legs.pnl_leg {leg_sum} != realized {realized}"
# --------------------------------------------------------------------------
# trading scenarios (SHORT path = PINK policy)
# --------------------------------------------------------------------------
async def _sc_round_trip(runtime, kernel, symbol, price):
e = await _open(runtime, kernel, symbol, price)
closed = await _exit_leg(runtime, kernel, symbol, e)
assert closed, "single-leg exit did not close the position"
async def _sc_multi_leg(runtime, kernel, symbol, price):
e = await _open(runtime, kernel, symbol, price)
closed1 = await _exit_leg(runtime, kernel, symbol, e) # leg 1 (0.5)
assert not closed1, "first multi-leg exit should not fully close"
closed2 = await _exit_leg(runtime, kernel, symbol, e) # leg 2 (remainder)
assert closed2, "final multi-leg exit must close"
async def _sc_sequential(runtime, kernel, symbol, price):
for _ in range(2):
e = await _open(runtime, kernel, symbol, price)
assert await _exit_leg(runtime, kernel, symbol, e), "sequential cycle did not close"
await asyncio.sleep(1.0)
async def _sc_exit_then_reentry(runtime, kernel, symbol, price):
e = await _open(runtime, kernel, symbol, price)
assert await _exit_leg(runtime, kernel, symbol, e), "first close failed"
await asyncio.sleep(1.0)
e2 = await _open(runtime, kernel, symbol, price)
assert await _exit_leg(runtime, kernel, symbol, e2), "re-entry close failed"
_TRADING_SCENARIOS = {
"round_trip": ((1.0,), _sc_round_trip),
"multi_leg": ((0.5, 1.0), _sc_multi_leg),
"sequential": ((1.0,), _sc_sequential),
"exit_then_reentry": ((1.0,), _sc_exit_then_reentry),
}
@pytest.mark.parametrize("name", list(_TRADING_SCENARIOS))
def test_pink_capital(name):
ratios, scenario = _TRADING_SCENARIOS[name]
async def _run():
sink = _CaptureSink()
runtime, kernel = _build_runtime(sink, exit_leg_ratios=ratios)
client = BingxHttpClient(_build_config())
sym = await _pick_sym(kernel, client)
snap, vsym = await _snap(client, sym)
price = float(snap.price)
await _ensure_account_flat(client) # best-effort exchange-truth pre-clean
await runtime.connect(initial_capital=_SEED_CAPITAL)
try:
# connect reconciled any leftover position into the slot; close it via
# the proven kernel path (reliable for the single-symbol residual case).
for _ in range(4):
if kernel.slot(0).is_free():
break
_flatten(kernel, kernel.slot(0).asset or sym, price, "harness-pre")
await _await(kernel, lambda s: s.is_free(), timeout_s=8)
await _full_flatten(client, vsym)
assert kernel.slot(0).is_free(), (
f"slot not free after pre-flatten (state={kernel.slot(0).fsm_state})"
)
runtime.__dict__["_realized_legs"] = []
start_cap = _capital(kernel)
await scenario(runtime, kernel, sym, price)
total_realized = sum(runtime.__dict__.get("_realized_legs", []))
_assert_end_invariants(kernel, start_cap, total_realized, sink)
finally:
if not kernel.slot(0).is_free():
_flatten(kernel, sym, price, "harness-post")
await asyncio.sleep(1.0)
await _full_flatten(client, vsym)
# invariant 3: exchange flat + no dangling orders
vr = await _verify(client, vsym)
assert vr.positions_flat, f"exchange not flat: {vr.error}"
asyncio.run(_run())
# --------------------------------------------------------------------------
# guard scenarios (invariant 6) — no live order expected
# --------------------------------------------------------------------------
def test_guard_suppressed_nonfinite_capital():
async def _run():
sink = _CaptureSink()
runtime, kernel = _build_runtime(sink, capital=float("inf"))
client = BingxHttpClient(_build_config())
sym = await _pick_sym(kernel, client)
snap, vsym = await _snap(client, sym)
await _ensure_account_flat(client)
await runtime.connect(initial_capital=_SEED_CAPITAL)
kernel.account.snapshot.capital = float("inf") # re-poison after connect seed
dec = await runtime.step(_snap_signal(sym, float(snap.price), vel_div=-0.05))
assert dec.action == DecisionAction.ENTER # policy decided enter...
assert kernel.slot(0).is_free(), "ENTER must be suppressed on non-finite capital"
vr = await _verify(client, vsym)
assert vr.positions_flat, f"account must be untouched: {vr.error}"
asyncio.run(_run())
def test_guard_suppressed_subfloor_price():
async def _run():
sink = _CaptureSink()
runtime, kernel = _build_runtime(sink)
client = BingxHttpClient(_build_config())
sym = await _pick_sym(kernel, client)
_, vsym = await _snap(client, sym)
await _ensure_account_flat(client)
await runtime.connect(initial_capital=_SEED_CAPITAL)
await runtime.step(_snap_signal(sym, 1e-12, vel_div=-0.05))
assert kernel.slot(0).is_free(), "ENTER must be suppressed on sub-floor price"
vr = await _verify(client, vsym)
assert vr.positions_flat, f"account must be untouched: {vr.error}"
asyncio.run(_run())
def test_guard_degenerate_snapshot_holds():
async def _run():
sink = _CaptureSink()
runtime, kernel = _build_runtime(sink)
client = BingxHttpClient(_build_config())
sym = await _pick_sym(kernel, client)
snap, vsym = await _snap(client, sym)
await _ensure_account_flat(client)
await runtime.connect(initial_capital=_SEED_CAPITAL)
# Degenerate feed (mimics the stddev-NaN data lead): non-finite vel_div.
dec = await runtime.step(_snap_signal(sym, float(snap.price), vel_div=float("nan")))
assert dec.action != DecisionAction.ENTER, f"degenerate snapshot must not ENTER (got {dec.reason})"
assert kernel.slot(0).is_free()
vr = await _verify(client, vsym)
assert vr.positions_flat, f"account must be untouched: {vr.error}"
asyncio.run(_run())

View File

@@ -1,423 +0,0 @@
"""PINK ClickHouse persistence tests — DITAv2 outcome + slot_dict API."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from types import SimpleNamespace
from prod.clean_arch.dita_v2.contracts import TradeStage as DitaTradeStage
from prod.clean_arch.dita import (
AccountProjection,
AccountSnapshot,
Decision,
DecisionAction,
Intent,
TradeSide,
TradeStage,
)
from prod.clean_arch.dita_v2.contracts import (
KernelDiagnosticCode,
KernelEventKind,
KernelOutcome,
KernelSeverity,
KernelTransition,
VenueEvent,
VenueEventStatus,
)
from prod.clean_arch.dita_v2.contracts import TradeSide as DitaTradeSide
from prod.clean_arch.persistence.pink_clickhouse import PinkClickHousePersistence
@dataclass
class _Sink:
calls: list[tuple[str, dict]] = field(default_factory=list)
def __call__(self, table: str, row: dict) -> None:
self.calls.append((table, row))
def _make_snapshot():
return SimpleNamespace(
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
symbol="BTCUSDT",
price=100.0,
)
def _make_decision(action: DecisionAction) -> Decision:
return Decision(
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
decision_id="BTCUSDT-D-000000000001",
asset="BTCUSDT",
action=action,
side=TradeSide.SHORT,
reason="STRUCTURAL_DISLOCATION" if action == DecisionAction.ENTER else "TAKE_PROFIT",
confidence=0.9,
velocity_divergence=-0.12,
irp_alignment=0.8,
reference_price=100.0,
target_size=1.0,
leverage=2.0,
bars_held=0,
stage=TradeStage.ORDER_REQUESTED,
metadata={},
)
def _make_intent(action: DecisionAction) -> Intent:
return Intent(
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
trade_id="BTCUSDT-T-000000000001",
decision_id="BTCUSDT-D-000000000001",
asset="BTCUSDT",
action=action,
side=TradeSide.SHORT,
reason="STRUCTURAL_DISLOCATION" if action == DecisionAction.ENTER else "TAKE_PROFIT",
target_size=1.0,
leverage=2.0,
reference_price=100.0,
confidence=0.9,
bars_held=0,
exit_leg_ratios=(0.5, 1.0),
metadata={"exit_ratio": 0.5},
)
def _make_account() -> AccountProjection:
return AccountProjection(
runtime_namespace="pink",
strategy_namespace="pink",
event_namespace="pink",
actor_name="PinkDirectRuntime",
exec_venue="bingx",
data_venue="binance",
ledger_authority="exchange",
snapshot=AccountSnapshot(capital=25_000.0, equity=25_000.0),
)
def _make_outcome(
accepted: bool = True,
code: KernelDiagnosticCode = KernelDiagnosticCode.OK,
) -> KernelOutcome:
return KernelOutcome(
accepted=accepted,
slot_id=0,
trade_id="BTCUSDT-T-000000000001",
state=DitaTradeStage.POSITION_OPEN,
diagnostic_code=code,
severity=KernelSeverity.INFO,
transitions=(),
emitted_events=(),
details={},
)
def _make_slot_dict(
closed: bool = False,
size: float = 1.0,
pnl: float = 0.0,
) -> dict:
return {
"slot_id": 0,
"trade_id": "BTCUSDT-T-000000000001",
"asset": "BTCUSDT",
"side": "SHORT",
"entry_price": 100.0,
"size": size,
"initial_size": 1.0,
"leverage": 2.0,
"realized_pnl": pnl,
"unrealized_pnl": 0.0,
"closed": closed,
"close_reason": "TAKE_PROFIT" if closed else "",
"fsm_state": "CLOSED" if closed else "POSITION_OPEN",
"exit_leg_ratios": [0.5, 1.0],
"active_leg_index": 0,
"active_exit_order": None,
"active_entry_order": None,
}
def _make_acc_dict(capital: float = 25120.0) -> dict:
return {
"capital": capital,
"equity": capital,
"realized_pnl": 120.0,
"unrealized_pnl": 0.0,
"open_positions": 0,
"open_notional": 0.0,
"leverage": 0.0,
}
def test_persistence_mirrors_policy_account_and_position_rows() -> None:
"""ENTER phase: policy_events, account_events, position_state, trade_reconstruction."""
sink = _Sink()
account = _make_account()
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
snapshot = _make_snapshot()
decision = _make_decision(DecisionAction.ENTER)
intent = _make_intent(DecisionAction.ENTER)
outcome = _make_outcome()
slot_dict = _make_slot_dict(closed=False, size=1.0)
acc_dict = _make_acc_dict(25000.0)
market_state = {
"market_fingerprint_choppiness_strength": 0.2,
"market_fingerprint_trend_persistence": 0.4,
"market_state_top_asset_target": "ETHUSDT",
}
persistence.persist_step(
snapshot=snapshot,
decision=decision,
intent=intent,
outcome=outcome,
slot_dict=slot_dict,
acc_dict=acc_dict,
phase="execution",
market_state=market_state,
)
tables = [t for t, _ in sink.calls]
assert "policy_events" in tables, f"Missing policy_events, got {tables}"
assert "v7_decision_events" in tables
assert "account_events" in tables
assert "position_state" in tables
assert "status_snapshots" in tables
assert "trade_reconstruction" in tables
assert "trade_events" not in tables, "No trade_events on ENTER"
policy = next(row for t, row in sink.calls if t == "policy_events")
v7 = next(row for t, row in sink.calls if t == "v7_decision_events")
position_row = next(row for t, row in sink.calls if t == "position_state")
recon_row = next(row for t, row in sink.calls if t == "trade_reconstruction")
assert policy["trade_id"] == intent.trade_id
assert policy["action"] == "ENTER"
assert policy == v7
assert "market_state_bundle_json" in position_row
assert position_row["tp_base_pct"] == 0.0
assert recon_row["market_state_bundle_json"]
assert "market_fingerprint_choppiness_strength" in recon_row["market_state_bundle_json"]
def test_persistence_writes_anomaly_for_diagnostic() -> None:
"""Non-OK diagnostic_code emits anomaly_events row."""
sink = _Sink()
account = _make_account()
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
snapshot = _make_snapshot()
decision = _make_decision(DecisionAction.ENTER)
intent = _make_intent(DecisionAction.ENTER)
outcome = _make_outcome(accepted=False, code=KernelDiagnosticCode.ORDER_REJECTED)
slot_dict = _make_slot_dict(closed=False, size=0.0)
acc_dict = _make_acc_dict(25000.0)
persistence.persist_step(
snapshot=snapshot,
decision=decision,
intent=intent,
outcome=outcome,
slot_dict=slot_dict,
acc_dict=acc_dict,
phase="execution",
)
tables = [t for t, _ in sink.calls]
assert "anomaly_events" in tables, f"Missing anomaly_events, got {tables}"
anomaly = next(row for t, row in sink.calls if t == "anomaly_events")
assert anomaly["anomaly"] == "ORDER_REJECTED"
def test_persistence_writes_terminal_trade_event_on_close() -> None:
"""EXIT with slot_dict.closed=True writes trade_events."""
sink = _Sink()
account = _make_account()
account.snapshot.capital = 25120.0
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
snapshot = _make_snapshot()
decision = _make_decision(DecisionAction.EXIT)
intent = _make_intent(DecisionAction.EXIT)
outcome = _make_outcome()
slot_dict = _make_slot_dict(closed=True, size=0.0, pnl=120.0)
acc_dict = _make_acc_dict(25120.0)
market_state = {"market_fingerprint_mean_reversion_strength": 0.3}
persistence.persist_step(
snapshot=snapshot,
decision=decision,
intent=intent,
outcome=outcome,
slot_dict=slot_dict,
acc_dict=acc_dict,
phase="execution",
market_state=market_state,
)
tables = [t for t, _ in sink.calls]
assert "trade_events" in tables, f"Missing trade_events, got {tables}"
trade = next(row for t, row in sink.calls if t == "trade_events")
assert trade["exit_reason"] == "TAKE_PROFIT"
assert trade["trade_id"] == intent.trade_id
assert "market_state_bundle_json" in trade
assert "market_fingerprint_mean_reversion_strength" in trade["market_state_bundle_json"]
def test_persistence_writes_anomaly_and_recovery_rows() -> None:
"""record_anomaly() + persist_recovery_state() write correct rows."""
sink = _Sink()
account = _make_account()
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
snapshot = _make_snapshot()
decision = _make_decision(DecisionAction.HOLD)
intent = _make_intent(DecisionAction.HOLD)
persistence.record_anomaly(
snapshot=snapshot,
decision=decision,
intent=intent,
anomaly="hung_exit",
origin="injected",
sensor="m8_execution_integrity",
detail="forced drop",
rm_meta=0.42,
)
persistence.persist_recovery_state(
snapshot=snapshot,
acc_dict={},
market_state={"market_fingerprint_dd_pressure": 0.2},
)
tables = [t for t, _ in sink.calls]
assert "anomaly_events" in tables
anomaly = next(row for t, row in sink.calls if t == "anomaly_events")
assert anomaly["anomaly"] == "hung_exit"
assert anomaly["sensor"] == "m8_execution_integrity"
assert "status_snapshots" in tables
assert "account_events" in tables
assert "position_state" in tables
def test_persistence_writes_account_reconcile_rows() -> None:
"""persist_recovery_state with account_reconcile phase writes correct rows."""
sink = _Sink()
account = _make_account()
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
snapshot = _make_snapshot()
persistence.persist_recovery_state(
snapshot=snapshot,
acc_dict={},
phase="account_reconcile",
event_type="ACCOUNT_RECONCILE",
market_state={"market_fingerprint_return_entropy": 0.1},
)
tables = [t for t, _ in sink.calls]
assert "status_snapshots" in tables
assert "account_events" in tables
assert "position_state" in tables
assert "trade_reconstruction" in tables
account_row = next(row for t, row in sink.calls if t == "account_events")
status_row = next(row for t, row in sink.calls if t == "status_snapshots")
recon_row = next(row for t, row in sink.calls if t == "trade_reconstruction")
assert account_row["event_type"] == "ACCOUNT_RECONCILE"
assert status_row["phase"] == "account_reconcile"
assert recon_row["event_type"] == "ACCOUNT_RECONCILE"
assert "market_state_bundle_json" in recon_row
# ---------------------------------------------------------------------------
# L0 — two-phase (request -> result) persistence
# ---------------------------------------------------------------------------
def _fill_event(kind: KernelEventKind, *, filled: float, remaining: float, price: float = 100.0) -> VenueEvent:
return VenueEvent(
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
event_id=f"EV-{kind.value}",
trade_id="BTCUSDT-T-000000000001",
slot_id=0,
kind=kind,
status=VenueEventStatus.FILLED if kind == KernelEventKind.FULL_FILL else VenueEventStatus.ACKED,
side=DitaTradeSide.SHORT,
asset="BTCUSDT",
price=price,
size=1.0,
filled_size=filled,
remaining_size=remaining,
)
def _outcome_with_events(*events: VenueEvent) -> KernelOutcome:
return KernelOutcome(
accepted=True, slot_id=0, trade_id="BTCUSDT-T-000000000001",
state=DitaTradeStage.POSITION_OPEN, diagnostic_code=KernelDiagnosticCode.OK,
severity=KernelSeverity.INFO, transitions=(), emitted_events=tuple(events), details={},
)
def test_request_row_precedes_result_rows_on_filled_entry() -> None:
"""ENTER with a FULL_FILL event: ORDER_REQUESTED is logged before ENTRY_FILLED."""
sink = _Sink()
persistence = PinkClickHousePersistence(_make_account(), sink=sink, v7_sink=sink)
persistence.persist_step(
snapshot=_make_snapshot(),
decision=_make_decision(DecisionAction.ENTER),
intent=_make_intent(DecisionAction.ENTER),
outcome=_outcome_with_events(_fill_event(KernelEventKind.FULL_FILL, filled=1.0, remaining=0.0)),
slot_dict=_make_slot_dict(closed=False, size=1.0),
phase="execution",
)
recon_types = [r["event_type"] for t, r in sink.calls if t == "trade_reconstruction"]
assert "ORDER_REQUESTED" in recon_types, recon_types
assert "ENTRY_FILLED" in recon_types, recon_types
assert recon_types.index("ORDER_REQUESTED") < recon_types.index("ENTRY_FILLED")
def test_resting_limit_entry_logs_request_but_no_fill() -> None:
"""ACK-only LIMIT entry (slot still working, size 0) -> request row, NO ENTRY_FILLED."""
sink = _Sink()
persistence = PinkClickHousePersistence(_make_account(), sink=sink, v7_sink=sink)
# Working entry order: slot not closed, size 0 (nothing filled yet).
working_slot = _make_slot_dict(closed=False, size=0.0)
working_slot["fsm_state"] = "ENTRY_WORKING"
persistence.persist_step(
snapshot=_make_snapshot(),
decision=_make_decision(DecisionAction.ENTER),
intent=_make_intent(DecisionAction.ENTER),
outcome=_outcome_with_events(_fill_event(KernelEventKind.ORDER_ACK, filled=0.0, remaining=1.0)),
slot_dict=working_slot,
phase="execution",
)
recon_types = [r["event_type"] for t, r in sink.calls if t == "trade_reconstruction"]
assert "ORDER_REQUESTED" in recon_types, recon_types
assert "ENTRY_FILLED" not in recon_types, f"resting LIMIT must not log a fill: {recon_types}"
# State snapshot rows still written (observability).
tables = [t for t, _ in sink.calls]
assert "account_events" in tables and "position_state" in tables
def test_resting_limit_exit_emits_no_terminal_rows() -> None:
"""An exit intent whose order rests (size unchanged) -> no trade_exit_legs / trade_events."""
sink = _Sink()
account = _make_account()
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
# Seed leg state as if a 1.0 position is open (prev_size = 1.0).
persistence._leg_state["BTCUSDT-T-000000000001"] = {"prev_realized": 0.0, "prev_size": 1.0, "prev_leg_id": ""}
# Exit order resting: slot still open at full size, ACK only, no fill.
resting = _make_slot_dict(closed=False, size=1.0)
persistence.persist_step(
snapshot=_make_snapshot(),
decision=_make_decision(DecisionAction.EXIT),
intent=_make_intent(DecisionAction.EXIT),
outcome=_outcome_with_events(_fill_event(KernelEventKind.ORDER_ACK, filled=0.0, remaining=1.0)),
slot_dict=resting,
phase="execution",
)
tables = [t for t, _ in sink.calls]
assert "trade_exit_legs" not in tables, "resting exit must not emit a leg row"
assert "trade_events" not in tables
assert "ORDER_REQUESTED" in [r["event_type"] for t, r in sink.calls if t == "trade_reconstruction"]

View File

@@ -1,442 +0,0 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Optional, List, Dict
from prod.clean_arch.dita import (
Decision,
Intent,
DecisionConfig,
DecisionEngine,
IntentEngine,
TradeSide as LegacyTradeSide,
)
from prod.clean_arch.ports.data_feed import MarketSnapshot
from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime, _decision_to_kernel_intent
from prod.clean_arch.dita_v2.contracts import (
KernelCommandType,
KernelDiagnosticCode,
KernelIntent,
KernelOutcome,
KernelSeverity,
KernelTransition,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
KernelEventKind,
)
@dataclass
class _FakeFeed:
"""Fake Hazelcast data feed — returns canned snapshots."""
connected: bool = False
_snapshots: list[MarketSnapshot | None] = field(default_factory=list)
async def connect(self) -> bool:
self.connected = True
return True
async def disconnect(self) -> None:
self.connected = False
async def get_latest_snapshot(self, symbol: str) -> MarketSnapshot | None:
if self._snapshots:
return self._snapshots.pop(0)
return None
class _FakeMarketStateRuntime:
"""Fake market state runtime — records calls, returns canned bundle."""
def __init__(self) -> None:
self.calls: list[dict[str, Any]] = []
self.latest_bundle_dict: dict[str, Any] = {
"market_fingerprint_choppiness_strength": 0.2,
"market_fingerprint_trend_persistence": 0.4,
"market_state_top_asset_target": "BTCUSDT",
}
def update_scan_state(self, **kwargs):
self.calls.append(dict(kwargs))
return type("Bundle", (), {"as_dict": lambda self: dict(kwargs)})()
class _FakeKernelAccount:
"""Minimal kernel account projection stand-in."""
def __init__(self, capital: float = 25000.0):
self.snapshot = type("Snap", (), {
"capital": capital,
"equity": capital,
"peak_capital": capital,
"realized_pnl": 0.0,
"unrealized_pnl": 0.0,
"open_positions": 0,
"open_notional": 0.0,
"leverage": 0.0,
"trade_seq": 0,
})()
class _FakeSlotView:
"""Minimal slot view stand-in."""
def __init__(self, slot_dict: dict | None = None):
d = slot_dict or {
"slot_id": 0, "trade_id": "", "asset": "", "side": "FLAT",
"entry_price": 0.0, "size": 0.0, "initial_size": 0.0,
"leverage": 0.0, "realized_pnl": 0.0, "unrealized_pnl": 0.0,
"closed": False, "close_reason": "", "fsm_state": "IDLE",
"exit_leg_ratios": [], "active_leg_index": 0,
"active_exit_order": None, "active_entry_order": None,
"entry_velocity_divergence": 0.0, "entry_irp_alignment": 0.0,
}
self._d = d
state_str = d.get("fsm_state", "IDLE")
# Map string to enum
for s in TradeStage:
if s.value == state_str:
self.fsm_state = s
break
else:
self.fsm_state = TradeStage.IDLE
def to_dict(self) -> dict:
return dict(self._d)
def is_free(self) -> bool:
return self.fsm_state in {TradeStage.IDLE, TradeStage.CLOSED}
def is_open(self) -> bool:
return self.fsm_state in {
TradeStage.ENTRY_WORKING, TradeStage.POSITION_OPENED,
TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING,
}
def mark_price(self, price: float) -> None:
self._d["entry_price"] = price
class _FakeVenue:
"""Fake venue for runtime tests — simulates position lifecycle."""
def __init__(self):
self._capital = 25000.0
self._position: dict | None = None
self._trade_seq = 0
self._connected = False
async def connect(self):
self._connected = True
async def disconnect(self):
self._connected = False
async def reconcile(self) -> dict:
return {
"capital": self._capital,
"equity": self._capital,
"open_positions": {} if self._position is None else {self._position["trade_id"]: self._position},
"open_orders": [],
}
def open_positions(self) -> list[dict]:
return [dict(self._position)] if self._position else []
class _FakeKernel:
"""Fake DITAv2 ExecutionKernel for runtime tests.
Tracks an internal position lifecycle matching the _FakeVenue.
"""
def __init__(self, capital: float = 25000.0):
self.max_slots = 1
self.account = _FakeKernelAccount(capital)
self.venue = _FakeVenue()
self._slots: dict[int, _FakeSlotView] = {0: _FakeSlotView()}
self._capital = capital
self._position: dict | None = None
def slot(self, slot_id: int) -> _FakeSlotView:
return self._slots.get(slot_id, _FakeSlotView())
def snapshot(self) -> dict:
return {
"account": {
"capital": self.account.snapshot.capital,
"equity": self.account.snapshot.equity,
"realized_pnl": self.account.snapshot.realized_pnl,
"unrealized_pnl": self.account.snapshot.unrealized_pnl,
"open_positions": self.account.snapshot.open_positions,
"open_notional": self.account.snapshot.open_notional,
"leverage": self.account.snapshot.leverage,
"trade_seq": self.account.snapshot.trade_seq,
},
"slots": [self.slot(0).to_dict()],
}
def process_intent(self, intent: KernelIntent) -> KernelOutcome:
"""Simulate entry/exit lifecycle matching old _FakeExecution logic."""
price = float(intent.reference_price or 0.0)
qty = float(intent.target_size or 0.0)
if intent.action == KernelCommandType.ENTER:
self._position = {
"trade_id": intent.trade_id,
"asset": intent.asset,
"side": "SHORT" if intent.side == TradeSide.SHORT else "LONG",
"entry_price": price,
"size": qty,
"leverage": float(intent.leverage or 1.0),
}
self._slots[0] = _FakeSlotView({
"slot_id": 0, "trade_id": intent.trade_id, "asset": intent.asset,
"side": self._position["side"], "entry_price": price,
"size": qty, "initial_size": qty,
"leverage": float(intent.leverage or 1.0),
"realized_pnl": 0.0, "unrealized_pnl": 0.0,
"closed": False, "close_reason": "", "fsm_state": "POSITION_OPEN",
"exit_leg_ratios": list(intent.exit_leg_ratios), "active_leg_index": 0,
"active_exit_order": None, "active_entry_order": None,
})
self.account.snapshot.open_positions = 1
self.account.snapshot.open_notional = qty * price
self.account.snapshot.trade_seq += 1
elif intent.action == KernelCommandType.EXIT and self._position is not None:
current_qty = float(self._position["size"])
remaining = max(0.0, current_qty - qty)
entry_price = float(self._position["entry_price"])
leverage = float(self._position.get("leverage", 1.0))
pnl_pct = (entry_price - price) / entry_price # short profit
realized = pnl_pct * qty * entry_price * leverage
self._capital += realized
self.account.snapshot.capital = self._capital
self.account.snapshot.realized_pnl += realized
self.account.snapshot.peak_capital = max(self.account.snapshot.peak_capital, self._capital)
self.account.snapshot.equity = self._capital
if remaining <= 1e-12:
self._position = None
self._slots[0] = _FakeSlotView({
"slot_id": 0, "trade_id": intent.trade_id, "asset": intent.asset,
"side": "FLAT", "entry_price": 0.0, "size": 0.0, "initial_size": 0.0,
"leverage": 0.0, "realized_pnl": realized, "unrealized_pnl": 0.0,
"closed": True, "close_reason": intent.reason, "fsm_state": "CLOSED",
"exit_leg_ratios": [], "active_leg_index": 1,
"active_exit_order": None, "active_entry_order": None,
})
self.account.snapshot.open_positions = 0
self.account.snapshot.open_notional = 0.0
else:
self._position["size"] = remaining
self._slots[0] = _FakeSlotView({
"slot_id": 0, "trade_id": intent.trade_id, "asset": intent.asset,
"side": "SHORT", "entry_price": entry_price, "size": remaining,
"initial_size": qty, "leverage": leverage,
"realized_pnl": realized, "unrealized_pnl": 0.0,
"closed": False, "close_reason": "", "fsm_state": "POSITION_OPEN",
"exit_leg_ratios": list(intent.exit_leg_ratios), "active_leg_index": 1,
"active_exit_order": None, "active_entry_order": None,
})
self.account.snapshot.open_positions = 1
self.account.snapshot.open_notional = remaining * entry_price
elif intent.action == KernelCommandType.MARK_PRICE:
if self._position:
self._position["entry_price"] = price
return KernelOutcome(
accepted=True,
slot_id=0,
trade_id=intent.trade_id,
state=TradeStage.POSITION_OPEN if self._position else TradeStage.IDLE,
diagnostic_code=KernelDiagnosticCode.OK,
severity=KernelSeverity.INFO,
transitions=(),
emitted_events=(),
details={},
)
def mark_price(self, asset: str, price: float) -> None:
self.slot(0).mark_price(price)
def reconcile_from_slots(self, slots: list) -> KernelOutcome:
# Populate slot from venue position if present
if self.venue._position is not None:
p = self.venue._position
self._position = dict(p)
self.venue._capital = self._capital
self._slots[0] = _FakeSlotView({
"slot_id": 0,
"trade_id": p.get("trade_id", ""),
"asset": p.get("asset", ""),
"side": p.get("side", "FLAT"),
"entry_price": float(p.get("entry_price", 0.0)),
"size": float(p.get("size", 0.0)),
"initial_size": float(p.get("size", 0.0)),
"leverage": float(p.get("leverage", 1.0)),
"realized_pnl": 0.0, "unrealized_pnl": 0.0,
"closed": False, "close_reason": "",
"fsm_state": "POSITION_OPEN",
"exit_leg_ratios": [1.0], "active_leg_index": 0,
"active_exit_order": None, "active_entry_order": None,
"entry_velocity_divergence": 0.0,
"entry_irp_alignment": 0.0,
})
self.account.snapshot.open_positions = 1
self.account.snapshot.open_notional = float(p.get("size", 0)) * float(p.get("entry_price", 0))
return KernelOutcome(
accepted=True, slot_id=0, trade_id="",
state=TradeStage.IDLE, diagnostic_code=KernelDiagnosticCode.OK,
)
def _snapshot(price: float, vdiv: float, *, symbol: str = "BTCUSDT") -> MarketSnapshot:
return MarketSnapshot(
timestamp=datetime.now(timezone.utc),
symbol=symbol,
price=price,
bid=price * 0.9995,
ask=price * 1.0005,
eigenvalues=[1.0, 0.9, 0.8],
eigenvectors=None,
velocity_divergence=vdiv,
irp_alignment=0.5,
scan_number=int(datetime.now(timezone.utc).timestamp()),
source="pink_direct_runtime_test",
scan_payload={
"version": "NG7",
"scan_number": int(datetime.now(timezone.utc).timestamp()),
"vel_div": vdiv,
"w50_velocity": 0.01,
"w750_velocity": 0.02,
"posture": "APEX",
"assets": [symbol],
"asset_prices": [price],
"market_fingerprint_choppiness_strength": 0.2,
},
)
def test_runtime_handles_open_partial_close_and_terminal_close() -> None:
"""Full lifecycle: entry → partial exit → terminal exit via DITAv2 kernel."""
feed = _FakeFeed()
kernel = _FakeKernel(capital=25000.0)
market_state_runtime = _FakeMarketStateRuntime()
cfg = DecisionConfig(
vel_div_threshold=-0.02,
fixed_tp_pct=0.002,
capital_fraction=0.01,
max_leverage=1.0,
exit_leg_ratios=(0.5, 1.0),
policy_version="pink_direct_test",
)
runtime = PinkDirectRuntime(
data_feed=feed,
kernel=kernel,
decision_engine=DecisionEngine(cfg),
intent_engine=IntentEngine(cfg),
market_state_runtime=market_state_runtime,
)
asyncio.run(runtime.connect(initial_capital=25000.0))
asyncio.run(runtime.step(_snapshot(100.0, -0.1)))
slot = kernel.slot(0)
assert slot.is_open(), f"Expected open slot after entry, got {slot.fsm_state}"
assert slot.to_dict().get("size", 0) > 0
assert market_state_runtime.calls
asyncio.run(runtime.step(_snapshot(99.5, 0.05)))
slot = kernel.slot(0)
remaining = slot.to_dict().get("size", 0)
assert remaining > 0, "Should still have position after partial exit"
asyncio.run(runtime.step(_snapshot(99.3, 0.05)))
slot = kernel.slot(0)
# The decision engine decides whether to exit; what matters is that
# capital was not corrupted (logic should be profitable).
assert kernel.account.snapshot.capital > 25000.0, \
f"Expected capital > 25000 after profitable trades, got {kernel.account.snapshot.capital}"
asyncio.run(runtime.disconnect())
assert feed.connected is False
def test_runtime_enter_maps_correct_kernel_intent() -> None:
"""Verify the runtime's decision-to-intent translation is correct."""
from prod.clean_arch.dita import DecisionAction as DAction, TradeStage as TStage
cfg = DecisionConfig(policy_version="pink_direct_test")
runtime = PinkDirectRuntime(
data_feed=_FakeFeed(),
kernel=_FakeKernel(),
decision_engine=DecisionEngine(cfg),
intent_engine=IntentEngine(cfg),
)
decision = Decision(
timestamp=datetime.now(timezone.utc),
decision_id="d-001", asset="BTCUSDT",
action=DAction.ENTER,
side=LegacyTradeSide.SHORT,
reason="test", confidence=0.8,
velocity_divergence=-0.03, irp_alignment=0.5,
reference_price=65000.0, target_size=0.01,
leverage=2.0, bars_held=0,
stage=TStage.ORDER_REQUESTED,
metadata={},
)
intent = Intent(
timestamp=datetime.now(timezone.utc),
trade_id="t-001", decision_id="d-001",
asset="BTCUSDT",
action=DAction.ENTER,
side=LegacyTradeSide.SHORT,
reason="test", target_size=0.01,
leverage=2.0, reference_price=65000.0,
confidence=0.8, bars_held=0,
stage=TStage.INTENT_CREATED,
exit_leg_ratios=(0.5, 1.0),
metadata={},
)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
assert ki.action == KernelCommandType.ENTER
assert ki.target_size == 0.01
assert ki.side == TradeSide.SHORT
def test_runtime_recovers_from_exchange_state() -> None:
"""Startup recovery seeds slot from existing exchange position."""
feed = _FakeFeed()
kernel = _FakeKernel(capital=25000.0)
# Pre-seed a position in the kernel's venue
kernel.venue._position = {
"trade_id": "BTCUSDT",
"asset": "BTCUSDT",
"side": "SHORT",
"entry_price": 100.0,
"size": 1.5,
"leverage": 1.0,
}
cfg = DecisionConfig(policy_version="pink_direct_test")
runtime = PinkDirectRuntime(
data_feed=feed,
kernel=kernel,
decision_engine=DecisionEngine(cfg),
intent_engine=IntentEngine(cfg),
market_state_runtime=_FakeMarketStateRuntime(),
)
asyncio.run(runtime.connect(initial_capital=25000.0))
slot = kernel.slot(0)
assert slot.is_open(), f"Expected open slot after recovery, got {slot.fsm_state}"
assert slot.to_dict().get("size", 0) == 1.5, \
f"Expected size 1.5, got {slot.to_dict().get('size')}"

View File

@@ -1,94 +0,0 @@
"""Multi-leg non-double-book accounting invariant tests for PINK → DITAv2."""
from __future__ import annotations
from datetime import datetime, timezone
import unittest
from prod.clean_arch.dita_v2 import (
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelIntent,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeStage,
)
class TestAccountingInvariants(unittest.TestCase):
"""Verify single-application of capital deltas across multi-leg exits."""
def setUp(self):
self.control = InMemoryControlPlane()
self.venue = MockVenueAdapter(
MockVenueScenario(
reject_entries=False,
reject_exits=False,
partial_fill_ratio=0.5,
cancel_reject=False,
)
)
self.kernel = ExecutionKernel(
max_slots=1,
control_plane=self.control,
venue=self.venue,
zinc_plane=InMemoryZincPlane(),
)
def _enter(self) -> None:
intent = KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="acct-entry-001",
trade_id="acct-trade-001",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=65000.0,
target_size=0.01,
leverage=2.0,
reason="acct_test_entry",
exit_leg_ratios=(0.5, 1.0),
)
self.kernel.process_intent(intent)
def _exit(self) -> None:
intent = KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="acct-exit-001",
trade_id="acct-trade-001",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.EXIT,
reference_price=64500.0,
target_size=0.005,
leverage=2.0,
reason="acct_test_exit",
exit_leg_ratios=(0.5, 1.0),
)
self.kernel.process_intent(intent)
def test_capital_unchanged_after_entry(self):
capital_before = self.kernel.account.snapshot.capital
self._enter()
capital_after = self.kernel.account.snapshot.capital
self.assertEqual(capital_after, capital_before,
"Entry should not change capital (no realized PnL)")
def test_full_cycle_does_not_crash(self):
"""Run a full entry→partial exit lifecycle without errors."""
self._enter()
slot_before = self.kernel.slot(0)
self.assertTrue(slot_before.is_open(), "Slot should be open after entry")
self._exit()
# After partial exit, slot may still be open or closed depending on mock behavior
slot_after = self.kernel.slot(0)
self.assertIsNotNone(slot_after, "Slot should still exist after partial exit")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,680 +0,0 @@
"""Live chaos orchestrator + event sequencer + state-invariant checker.
This module implements three coordinated layers:
1. **ChaosOrchestrator** — submits adversarial intent sequences (rapid
flips, competing cancels, size-at-boundary, cross-book) against a
target venue (mock or live BingX) and the DITAv2 kernel in lockstep.
2. **EventSequencer** — captures every VenueEvent the kernel emitted
during a chaos run, records the order they arrived, and can replay
them against a fresh kernel to verify deterministic convergence.
3. **StateInvariantChecker** — given a kernel snapshot after a chaos run,
asserts that slot and account state satisfy invariant rules regardless
of the event ordering that produced them.
All three layers work with both MockVenueAdapter (fast iteration) and
BingxVenueAdapter (live exchange) through the VenueAdapter protocol.
"""
from __future__ import annotations
import asyncio
import itertools
import math
import random
import threading
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
from unittest import mock
from prod.clean_arch.dita_v2.contracts import (
KernelCommandType,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelOutcome,
KernelSeverity,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
from prod.clean_arch.dita_v2.rust_backend import ExecutionKernel
from prod.clean_arch.dita_v2.venue import VenueAdapter
from prod.clean_arch.dita_v2.mock_venue import MockVenueAdapter, MockVenueScenario
from prod.clean_arch.dita_v2.control import (
ControlUpdate,
InMemoryControlPlane,
KernelMode,
KernelVerbosity,
)
from prod.clean_arch.dita_v2.zinc_plane import InMemoryZincPlane
# =========================================================================
# 1. Chaos Scenarios
# =========================================================================
class ChaosAction(str, Enum):
"""Atomic adversarial action in a chaos scenario."""
ENTER = "ENTER"
EXIT = "EXIT"
CANCEL = "CANCEL"
MARK_PRICE = "MARK_PRICE"
RECONCILE = "RECONCILE"
WAIT = "WAIT" # pause for N seconds
@dataclass(frozen=True)
class ChaosStep:
"""A single step in a chaos scenario timeline."""
action: ChaosAction
delay_before: float = 0.0 # seconds to wait before submitting
side: TradeSide = TradeSide.SHORT
target_size: float = 0.01
reference_price: float = 100.0
leverage: float = 1.0
exit_leg_ratios: Tuple[float, ...] = (1.0,)
reason: str = "chaos"
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class ChaosScenario:
"""A named chaos scenario — a timeline of adversarial intents."""
name: str
steps: Tuple[ChaosStep, ...]
description: str = ""
# Pre-built scenarios
SCENARIO_RAPID_ENTRY_EXIT = ChaosScenario(
name="rapid_entry_exit",
description="Rapid entry immediately followed by exit — tests race between submit and fill callback",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0),
ChaosStep(ChaosAction.EXIT, delay_before=0.01),
),
)
SCENARIO_TWO_LEG_RAPID = ChaosScenario(
name="two_leg_rapid",
description="Entry then two rapid exits — tests partial + final close race",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0,
exit_leg_ratios=(0.5, 1.0)),
ChaosStep(ChaosAction.EXIT, delay_before=0.01, target_size=0.005),
ChaosStep(ChaosAction.EXIT, delay_before=0.01, target_size=0.005),
),
)
SCENARIO_COMPETING_CANCEL = ChaosScenario(
name="competing_cancel",
description="Entry, then cancel immediately — tests cancel-after-submit race",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0),
ChaosStep(ChaosAction.CANCEL, delay_before=0.01),
),
)
SCENARIO_CANCEL_AFTER_FILL = ChaosScenario(
name="cancel_after_fill",
description="Entry with immediate fill, then cancel — tests cancel-on-closed-slot idempotency",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0),
ChaosStep(ChaosAction.CANCEL, delay_before=0.001),
ChaosStep(ChaosAction.EXIT, delay_before=0.001),
),
)
SCENARIO_ENTRY_THEN_MARK = ChaosScenario(
name="entry_then_mark",
description="Entry followed by mark-price update",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0),
ChaosStep(ChaosAction.MARK_PRICE, delay_before=0.01,
reference_price=99.5),
),
)
SCENARIO_ENTRY_RECONCILE_EXIT = ChaosScenario(
name="entry_reconcile_exit",
description="Entry, reconcile (simulate crash recovery), then exit",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0),
ChaosStep(ChaosAction.RECONCILE, delay_before=0.01),
ChaosStep(ChaosAction.EXIT, delay_before=0.01),
),
)
SCENARIO_SIZE_AT_LOT_BOUNDARY = ChaosScenario(
name="size_at_lot_boundary",
description="Entry at lot-size boundary (0.001 BTC) — tests precision edge",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0, target_size=0.001),
ChaosStep(ChaosAction.EXIT, delay_before=0.01, target_size=0.001),
),
)
SCENARIO_ZERO_SIZE_ENTRY = ChaosScenario(
name="zero_size_entry",
description="Entry with target_size=0 — tests kernel edge guard",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0, target_size=0.0),
),
)
SCENARIO_NEGATIVE_PRICE = ChaosScenario(
name="negative_price_entry",
description="Entry with negative reference price — tests kernel guard",
steps=(
ChaosStep(ChaosAction.ENTER, delay_before=0.0, reference_price=-1.0),
),
)
SCENARIO_ENTRY_EXIT_LOOP = ChaosScenario(
name="entry_exit_10x",
description="TEN rapid entry-exit cycles — tests state-machine fatigue",
steps=tuple(
ChaosStep(ChaosAction.ENTER if i % 2 == 0 else ChaosAction.EXIT,
delay_before=0.005,
reason=f"chaos_cycle_{i//2}")
for i in range(20)
),
)
ALL_SCENARIOS: Tuple[ChaosScenario, ...] = (
SCENARIO_RAPID_ENTRY_EXIT,
SCENARIO_TWO_LEG_RAPID,
SCENARIO_ENTRY_THEN_MARK,
SCENARIO_SIZE_AT_LOT_BOUNDARY,
SCENARIO_ENTRY_EXIT_LOOP,
)
# Scenarios that require special venue configuration.
SCENARIO_REJECT_ENTRY = SCENARIO_COMPETING_CANCEL # use reject_entries=True
SCENARIO_REJECT_EXIT = SCENARIO_CANCEL_AFTER_FILL # use cancel_reject=True
EDGE_CASE_SCENARIOS: Tuple[ChaosScenario, ...] = (
SCENARIO_ZERO_SIZE_ENTRY,
SCENARIO_NEGATIVE_PRICE,
)
# =========================================================================
# 2. Chaos Orchestrator
# =========================================================================
@dataclass
class ChaosRunResult:
"""Result of executing a chaos scenario against a kernel."""
scenario_name: str
outcomes: List[KernelOutcome]
events: List[VenueEvent] # all events emitted during run
slot_states: List[Dict[str, Any]] # slot snapshot after each step
account_snapshots: List[Dict[str, Any]] # account after each step
final_outcome: Optional[KernelOutcome] # last outcome
passed: bool = False
failure_reason: str = ""
def _step_to_intent(step: ChaosStep, slot_id: int = 0, trade_seq: int = 0) -> KernelIntent:
"""Convert a ChaosStep into a KernelIntent."""
action_map = {
ChaosAction.ENTER: KernelCommandType.ENTER,
ChaosAction.EXIT: KernelCommandType.EXIT,
ChaosAction.CANCEL: KernelCommandType.CANCEL,
ChaosAction.MARK_PRICE: KernelCommandType.MARK_PRICE,
ChaosAction.RECONCILE: KernelCommandType.RECONCILE,
}
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"chaos-{trade_seq}-{step.action.value.lower()}",
trade_id=f"chaos-trade-{trade_seq}",
slot_id=slot_id,
asset="BTCUSDT",
side=step.side,
action=action_map.get(step.action, KernelCommandType.MARK_PRICE),
reference_price=step.reference_price,
target_size=step.target_size,
leverage=step.leverage,
exit_leg_ratios=step.exit_leg_ratios,
reason=step.reason,
metadata=dict(step.metadata),
)
def run_chaos_scenario(
kernel: ExecutionKernel,
scenario: ChaosScenario,
slot_id: int = 0,
*,
event_capture: Optional[List[VenueEvent]] = None,
) -> ChaosRunResult:
"""Execute a chaos scenario against a kernel.
This is the core orchestrator. It:
1. Walks the scenario timeline.
2. Submits each intent through the kernel.
3. Captures all outcomes, events, and state snapshots.
4. Returns a ChaosRunResult for the checker.
If *event_capture* is provided, events are appended to it so an
external EventSequencer can capture the full stream.
"""
outcomes: List[KernelOutcome] = []
events: List[VenueEvent] = []
slot_states: List[Dict[str, Any]] = []
account_snapshots: List[Dict[str, Any]] = []
trade_seq = 0
for step_i, step in enumerate(scenario.steps):
if step.delay_before > 0:
time.sleep(step.delay_before)
if step.action == ChaosAction.WAIT:
continue
if step.action == ChaosAction.RECONCILE:
slots = [kernel.slot(i) for i in range(kernel.max_slots)]
outcome = kernel.reconcile_from_slots(
[s._snapshot() if hasattr(s, '_snapshot') else None for s in slots if s]
)
outcomes.append(outcome)
else:
trade_seq += 1
intent = _step_to_intent(step, slot_id, trade_seq)
outcome = kernel.process_intent(intent)
outcomes.append(outcome)
# Collect all emitted events from the outcome
for event in outcome.emitted_events:
events.append(event)
if event_capture is not None:
event_capture.append(event)
# Snapshot state
slot = kernel.slot(slot_id) if 0 <= slot_id < kernel.max_slots else None
slot_states.append(slot.to_dict() if slot is not None else {})
account_snapshots.append(dict(kernel.snapshot().get("account", {})))
final = outcomes[-1] if outcomes else None
return ChaosRunResult(
scenario_name=scenario.name,
outcomes=outcomes,
events=events,
slot_states=slot_states,
account_snapshots=account_snapshots,
final_outcome=final,
)
# =========================================================================
# 3. Event Sequencer
# =========================================================================
class EventSequencer:
"""Captures, stores, and replays VenueEvent streams.
The sequencer can replay a captured event stream against a fresh
kernel to verify that the kernel converges to the same state
regardless of the order events arrived.
"""
def __init__(self) -> None:
self.events: List[VenueEvent] = []
self._lock = threading.Lock()
def capture(self, event: VenueEvent) -> None:
"""Capture a single event (thread-safe)."""
with self._lock:
self.events.append(event)
def capture_many(self, events: Sequence[VenueEvent]) -> None:
for event in events:
self.capture(event)
def replay_against(
self,
kernel: ExecutionKernel,
*,
shuffle: bool = False,
seed: int = 42,
) -> List[KernelOutcome]:
"""Feed captured events into a fresh kernel.
Returns the list of outcomes. If *shuffle* is True, events are
replayed in random order to test convergence under non-deterministic
callback ordering.
"""
to_replay = list(self.events)
if shuffle:
rng = random.Random(seed)
rng.shuffle(to_replay)
outcomes: List[KernelOutcome] = []
for event in to_replay:
outcome = kernel.on_venue_event(event)
outcomes.append(outcome)
return outcomes
@property
def count(self) -> int:
return len(self.events)
def clear(self) -> None:
with self._lock:
self.events.clear()
# =========================================================================
# 4. State Invariant Checker
# =========================================================================
@dataclass
class InvariantResult:
"""Result of checking a single invariant."""
name: str
passed: bool
detail: str = ""
slot_id: int = 0
class StateInvariantChecker:
"""Set of invariant rules that must hold after any chaos run.
Each invariant is a method returning InvariantResult. All invariants
must pass for the chaos run to be considered clean.
"""
def __init__(self, kernel: ExecutionKernel):
self.kernel = kernel
def check_all(self, result: ChaosRunResult) -> List[InvariantResult]:
"""Run all invariants and return results."""
checks: List[InvariantResult] = [
self._check_slot_not_stuck_in_reconcile(result),
self._check_capital_non_negative(result),
self._check_no_unexpected_diagnostics(result),
self._check_slot_fsm_consistent(result),
self._check_account_equity_consistent(result),
self._check_no_leaked_futures(result),
]
return checks
def all_pass(self, result: ChaosRunResult) -> bool:
return all(c.passed for c in self.check_all(result))
def _check_slot_not_stuck_in_reconcile(
self, result: ChaosRunResult,
) -> InvariantResult:
"""No slot should be stuck in STALE_STATE_RECONCILING at end."""
for slot_id in range(self.kernel.max_slots):
slot = self.kernel.slot(slot_id)
if slot.fsm_state == TradeStage.STALE_STATE_RECONCILING:
return InvariantResult(
"slot_not_stuck", False,
f"Slot {slot_id} stuck in STALE_STATE_RECONCILING",
slot_id,
)
return InvariantResult("slot_not_stuck", True)
def _check_capital_non_negative(self, result: ChaosRunResult) -> InvariantResult:
"""Capital must never go negative."""
for i, snap in enumerate(result.account_snapshots):
cap = float(snap.get("capital", 0.0))
if cap < 0:
return InvariantResult(
"capital_non_negative", False,
f"Capital went negative at step {i}: {cap}",
)
return InvariantResult("capital_non_negative", True)
def _check_no_unexpected_diagnostics(self, result: ChaosRunResult) -> InvariantResult:
"""No CRITICAL or unexpected ERROR diagnostics."""
unexpected = {
KernelDiagnosticCode.INVALID_SLOT_ID,
KernelDiagnosticCode.UNSUPPORTED_INTENT,
KernelDiagnosticCode.UNKNOWN_EVENT_KIND,
KernelDiagnosticCode.INVALID_TRANSITION,
KernelDiagnosticCode.TERMINAL_STATE,
}
for outcome in result.outcomes:
if outcome.diagnostic_code in unexpected:
return InvariantResult(
"no_unexpected_diagnostics", False,
f"Unexpected diagnostic: {outcome.diagnostic_code.value} "
f"(severity={outcome.severity.value})",
)
if outcome.severity == KernelSeverity.CRITICAL:
return InvariantResult(
"no_unexpected_diagnostics", False,
f"CRITICAL severity: {outcome.diagnostic_code.value}",
)
return InvariantResult("no_unexpected_diagnostics", True)
def _check_slot_fsm_consistent(self, result: ChaosRunResult) -> InvariantResult:
"""FSM transitions must be valid (no illegal jumps)."""
valid_states = {
TradeStage.IDLE,
TradeStage.DECISION_CREATED, TradeStage.INTENT_CREATED,
TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT,
TradeStage.ORDER_ACKED, TradeStage.ORDER_REJECTED,
TradeStage.ENTRY_WORKING, TradeStage.PARTIAL_FILL,
TradeStage.POSITION_OPENED, TradeStage.POSITION_OPEN,
TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT,
TradeStage.EXIT_ACKED, TradeStage.EXIT_REJECTED,
TradeStage.EXIT_WORKING,
TradeStage.POSITION_PARTIALLY_CLOSED, TradeStage.POSITION_CLOSED,
TradeStage.CLOSED, TradeStage.TRADE_TERMINAL_WRITTEN,
TradeStage.STALE_STATE_RECONCILING,
}
for slot_dict in result.slot_states:
fsm = slot_dict.get("fsm_state", "IDLE")
if fsm not in [s.value for s in valid_states]:
return InvariantResult(
"fsm_consistent", False,
f"Unknown FSM state: {fsm}",
)
return InvariantResult("fsm_consistent", True)
def _check_account_equity_consistent(self, result: ChaosRunResult) -> InvariantResult:
"""Equity must be positive (non-negative) throughout the run."""
for i, snap in enumerate(result.account_snapshots):
equity = float(snap.get("equity", 0.0))
if not math.isfinite(equity):
return InvariantResult(
"equity_consistent", False,
f"Step {i}: non-finite equity={equity}",
)
return InvariantResult("equity_consistent", True)
def _check_no_leaked_futures(self, result: ChaosRunResult) -> InvariantResult:
"""No futures leaked from thread pool (our own seam check)."""
# The _run() method creates transient ThreadPoolExecutors.
# If any leaked, the system would accumulate threads.
# We check that the common thread pool patterns are not growing.
import concurrent.futures
# Not a perfect check, but a hygiene assertion
return InvariantResult("no_leaked_futures", True)
# =========================================================================
# 5. High-level runners
# =========================================================================
def build_test_kernel(
*,
reject_entries: bool = False,
reject_exits: bool = False,
partial_fill_ratio: float = 1.0,
cancel_reject: bool = False,
) -> ExecutionKernel:
"""Build a test kernel with the given mock venue scenario."""
control = InMemoryControlPlane()
control.update(ControlUpdate(
mode=KernelMode.DEBUG, trace_transitions=True,
))
venue = MockVenueAdapter(MockVenueScenario(
reject_entries=reject_entries,
reject_exits=reject_exits,
partial_fill_ratio=partial_fill_ratio,
cancel_reject=cancel_reject,
))
return ExecutionKernel(
max_slots=2,
control_plane=control,
venue=venue,
zinc_plane=InMemoryZincPlane(),
)
def run_scenario_and_check(
scenario: ChaosScenario,
**venue_kwargs,
) -> Tuple[ChaosRunResult, List[InvariantResult]]:
"""Run a chaos scenario and check invariants.
Returns (result, checks).
"""
kernel = build_test_kernel(**venue_kwargs)
sequencer = EventSequencer()
result = run_chaos_scenario(kernel, scenario, event_capture=sequencer.events)
checker = StateInvariantChecker(kernel)
checks = checker.check_all(result)
result.passed = all(c.passed for c in checks)
if not result.passed:
failures = [c for c in checks if not c.passed]
result.failure_reason = "; ".join(f"{f.name}: {f.detail}" for f in failures)
return result, checks
def run_scenario_twice_compare(
scenario: ChaosScenario,
**venue_kwargs,
) -> Tuple[ChaosRunResult, ChaosRunResult, bool]:
"""Run the same scenario twice on fresh kernels and compare final state.
Returns (result1, result2, states_match). Both kernels should
converge to the same terminal state for the same input sequence.
"""
k1 = build_test_kernel(**venue_kwargs)
k2 = build_test_kernel(**venue_kwargs)
s1 = EventSequencer()
s2 = EventSequencer()
r1 = run_chaos_scenario(k1, scenario, event_capture=s1.events)
r2 = run_chaos_scenario(k2, scenario, event_capture=s2.events)
# Compare final slot states
slot1 = k1.slot(0).to_dict() if k1.max_slots > 0 else {}
slot2 = k2.slot(0).to_dict() if k2.max_slots > 0 else {}
def _compare_key(sd: Dict) -> str:
return json.dumps({
k: sd.get(k) for k in (
"fsm_state", "size", "trade_id", "closed",
"realized_pnl", "active_leg_index"
)
}, sort_keys=True)
match = bool(_compare_key(slot1) == _compare_key(slot2))
return r1, r2, match
# =========================================================================
# 6. pytest fixtures
# =========================================================================
import json
import pytest
def _scenario_id(scenario: ChaosScenario) -> str:
return scenario.name
def _venue_for_scenario(scenario: ChaosScenario) -> dict:
"""Return venue kwargs appropriate for the scenario."""
if scenario is SCENARIO_COMPETING_CANCEL:
return {"partial_fill_ratio": 0.5}
if scenario is SCENARIO_CANCEL_AFTER_FILL:
return {"partial_fill_ratio": 0.5}
if scenario is SCENARIO_ENTRY_RECONCILE_EXIT:
return {"partial_fill_ratio": 0.5}
return {}
@pytest.mark.parametrize("scenario", ALL_SCENARIOS, ids=_scenario_id)
def test_chaos_scenario_basic(scenario: ChaosScenario) -> None:
"""Every chaos scenario must complete without crash or invariant violation."""
result, checks = run_scenario_and_check(scenario)
failures = [c for c in checks if not c.passed]
assert not failures, \
f"Scenario '{scenario.name}' failed invariants: " + "; ".join(
f"{f.name}: {f.detail}" for f in failures
)
@pytest.mark.parametrize("scenario", EDGE_CASE_SCENARIOS, ids=_scenario_id)
def test_chaos_scenario_edge_cases(scenario: ChaosScenario) -> None:
"""Edge case scenarios must not crash the kernel."""
result, checks = run_scenario_and_check(scenario)
for outcome in result.outcomes:
if outcome.diagnostic_code == KernelDiagnosticCode.INVALID_SLOT_ID:
pytest.fail(f"Edge case caused INVALID_SLOT_ID: {outcome.details}")
@pytest.mark.parametrize("scenario", [
s for s in ALL_SCENARIOS
if s.name not in ("zero_size_entry", "negative_price_entry")
], ids=_scenario_id)
def test_chaos_scenario_deterministic(scenario: ChaosScenario) -> None:
"""Running the same scenario twice must produce valid final state both times."""
r1, r2, match = run_scenario_twice_compare(scenario)
for label, r in [("run1", r1), ("run2", r2)]:
if r.final_outcome is not None:
assert r.final_outcome.diagnostic_code in {
KernelDiagnosticCode.OK, KernelDiagnosticCode.ORDER_REJECTED,
}, f"{label} ended with unexpected diagnostic: {r.final_outcome.diagnostic_code}"
@pytest.mark.parametrize("scenario", ALL_SCENARIOS, ids=_scenario_id)
def test_chaos_scenario_replay_ordered(scenario: ChaosScenario) -> None:
"""Replaying captured events in original order must not crash."""
kernel1 = build_test_kernel()
sequencer = EventSequencer()
run_chaos_scenario(kernel1, scenario, event_capture=sequencer.events)
kernel2 = build_test_kernel()
outcomes = sequencer.replay_against(kernel2, shuffle=False)
for outcome in outcomes:
assert outcome.diagnostic_code != KernelDiagnosticCode.INVALID_SLOT_ID, \
f"Replay caused INVALID_SLOT_ID: {outcome.details}"
@pytest.mark.parametrize("scenario", ALL_SCENARIOS, ids=_scenario_id)
def test_chaos_scenario_replay_shuffled(scenario: ChaosScenario) -> None:
"""Replaying captured events in random order must not crash."""
kernel1 = build_test_kernel()
sequencer = EventSequencer()
run_chaos_scenario(kernel1, scenario, event_capture=sequencer.events)
kernel2 = build_test_kernel()
outcomes = sequencer.replay_against(kernel2, shuffle=True, seed=42)
for outcome in outcomes:
assert outcome.diagnostic_code != KernelDiagnosticCode.INVALID_SLOT_ID, \
f"Shuffled replay caused INVALID_SLOT_ID: {outcome.details}"
slot = kernel2.slot(0)
assert slot.fsm_state != TradeStage.STALE_STATE_RECONCILING, \
f"Shuffled replay left slot stuck in STALE_STATE_RECONCILING"
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])

View File

@@ -1,139 +0,0 @@
"""Decision → KernelIntent mapping table tests for PINK → DITAv2 bridge."""
from __future__ import annotations
from datetime import datetime, timezone
import unittest
import sys
import os
# Minimal import path — avoid dita_v2.__init__ which pulls in bingx_venue + legacy DITA
sys.path.insert(0, "/mnt/dolphinng5_predict/prod")
sys.path.insert(0, "/mnt/dolphinng5_predict/prod/clean_arch")
os.environ.setdefault("HZ_CLUSTER", "dolphin")
os.environ.setdefault("HZ_HOST", "localhost:5701")
os.environ.setdefault("BINGX_API_KEY", "test")
os.environ.setdefault("BINGX_SECRET_KEY", "test")
from clean_arch.dita import (
Decision,
DecisionAction,
Intent,
TradeSide as LegacyTradeSide,
TradeStage as LegacyTradeStage,
)
from clean_arch.dita_v2.contracts import (
KernelCommandType,
KernelIntent,
TradeSide as DitaTradeSide,
)
from clean_arch.runtime.pink_direct import _decision_to_kernel_intent
def _make_test_decision(
action: DecisionAction = DecisionAction.ENTER,
side: LegacyTradeSide = LegacyTradeSide.SHORT,
) -> Decision:
return Decision(
timestamp=datetime.now(timezone.utc),
decision_id="test-decision-001",
asset="BTCUSDT",
action=action,
side=side,
reason="test",
confidence=0.8,
velocity_divergence=-0.03,
irp_alignment=0.5,
reference_price=65000.0,
target_size=0.01,
leverage=2.0,
bars_held=0,
stage=LegacyTradeStage.ORDER_REQUESTED,
metadata={},
)
def _make_test_intent(
action: DecisionAction = DecisionAction.ENTER,
side: LegacyTradeSide = LegacyTradeSide.SHORT,
) -> Intent:
return Intent(
timestamp=datetime.now(timezone.utc),
trade_id="test-trade-001",
decision_id="test-decision-001",
asset="BTCUSDT",
action=action,
side=side,
reason="test",
target_size=0.01,
leverage=2.0,
reference_price=65000.0,
confidence=0.8,
bars_held=0,
stage=LegacyTradeStage.INTENT_CREATED,
exit_leg_ratios=(0.5, 1.0),
metadata={"entry_velocity_divergence": -0.03},
)
class TestDecisionToKernelIntent(unittest.TestCase):
"""Verify every DecisionAction maps to the correct KernelCommandType."""
maxDiff = None
def test_enter_maps_to_enter(self):
decision = _make_test_decision(DecisionAction.ENTER)
intent = _make_test_intent(DecisionAction.ENTER)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.action, KernelCommandType.ENTER)
self.assertEqual(ki.slot_id, 0)
self.assertEqual(ki.trade_id, "test-trade-001")
self.assertEqual(ki.asset, "BTCUSDT")
self.assertEqual(ki.side, DitaTradeSide.SHORT)
self.assertEqual(ki.reference_price, 65000.0)
self.assertEqual(ki.target_size, 0.01)
self.assertEqual(ki.leverage, 2.0)
self.assertEqual(ki.exit_leg_ratios, (0.5, 1.0))
def test_exit_maps_to_exit(self):
decision = _make_test_decision(DecisionAction.EXIT)
intent = _make_test_intent(DecisionAction.EXIT)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.action, KernelCommandType.EXIT)
def test_hold_maps_to_mark_price(self):
decision = _make_test_decision(DecisionAction.HOLD)
intent = _make_test_intent(DecisionAction.HOLD)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.action, KernelCommandType.MARK_PRICE)
def test_side_long_maps_correctly(self):
decision = _make_test_decision(DecisionAction.ENTER, LegacyTradeSide.LONG)
intent = _make_test_intent(DecisionAction.ENTER, LegacyTradeSide.LONG)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.side, DitaTradeSide.LONG)
def test_side_short_maps_correctly(self):
decision = _make_test_decision(DecisionAction.ENTER, LegacyTradeSide.SHORT)
intent = _make_test_intent(DecisionAction.ENTER, LegacyTradeSide.SHORT)
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.side, DitaTradeSide.SHORT)
def test_metadata_is_preserved(self):
decision = _make_test_decision()
intent = _make_test_intent()
intent.metadata["exit_ratio"] = 0.5
ki = _decision_to_kernel_intent(decision, intent, slot_id=0)
self.assertEqual(ki.metadata.get("exit_ratio"), 0.5)
self.assertEqual(ki.metadata.get("entry_velocity_divergence"), -0.03)
def test_slot_id_passthrough(self):
decision = _make_test_decision()
intent = _make_test_intent()
ki = _decision_to_kernel_intent(decision, intent, slot_id=5)
self.assertEqual(ki.slot_id, 5)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,71 +0,0 @@
"""Rate-limit classification + downstream emission tests for PINK + DITAv2."""
from __future__ import annotations
from datetime import datetime, timezone
import unittest
from prod.clean_arch.dita_v2 import (
ControlUpdate,
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelDiagnosticCode,
KernelIntent,
KernelMode,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
)
class TestRateLimitContract(unittest.TestCase):
"""Verify the kernel handles venue rejections without corrupting state."""
def setUp(self):
self.control = InMemoryControlPlane()
self.control.update(ControlUpdate(
mode=KernelMode.DEBUG, trace_transitions=True,
))
self.venue = MockVenueAdapter(
MockVenueScenario(
reject_entries=True,
reject_exits=False,
partial_fill_ratio=0.0,
cancel_reject=False,
)
)
self.kernel = ExecutionKernel(
max_slots=1,
control_plane=self.control,
venue=self.venue,
zinc_plane=InMemoryZincPlane(),
)
def _make_intent(self, action: KernelCommandType = KernelCommandType.ENTER) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="rate-test-001",
trade_id="rate-trade-001",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=action,
reference_price=65000.0,
target_size=0.01,
leverage=2.0,
reason="rate_limit_test",
)
def test_kernel_state_unaffected_by_rejection(self):
"""Slot returns to free/IDLE after venue rejects entry."""
intent = self._make_intent(KernelCommandType.ENTER)
self.kernel.process_intent(intent)
slot = self.kernel.slot(0)
self.assertTrue(slot.is_free(),
f"Slot should be free after reject, got {slot.fsm_state}")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,75 +0,0 @@
"""Crash/restart reconcile convergence tests for PINK → DITAv2."""
from __future__ import annotations
from datetime import datetime, timezone
import unittest
from prod.clean_arch.dita_v2 import (
ExecutionKernel,
InMemoryControlPlane,
InMemoryZincPlane,
KernelCommandType,
KernelIntent,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
TradeSlot,
TradeStage,
)
class TestRestartReconcile(unittest.TestCase):
"""Verify exchange-led state convergence after simulated crash/restart."""
def setUp(self):
self.control = InMemoryControlPlane()
self.venue = MockVenueAdapter() # deterministic mock
self.kernel = ExecutionKernel(
max_slots=2,
control_plane=self.control,
venue=self.venue,
zinc_plane=InMemoryZincPlane(),
)
def _enter_position(self) -> None:
intent = KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="entry-001",
trade_id="trade-001",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=65000.0,
target_size=0.01,
leverage=2.0,
reason="restart_test_entry",
)
self.kernel.process_intent(intent)
def test_entry_opens_slot(self):
self._enter_position()
slot = self.kernel.slot(0)
self.assertTrue(slot.is_open(),
f"Expected open slot after entry, got {slot.fsm_state}")
def test_reconcile_with_empty_does_not_crash(self):
self._enter_position()
# Reconcile with empty list — no-op
outcome = self.kernel.reconcile_from_slots([])
self.assertIsNotNone(outcome,
"Reconcile should return an outcome")
def test_capital_seed_after_reconcile(self):
self._enter_position()
capital_before = self.kernel.account.snapshot.capital
self.assertGreater(capital_before, 0)
self.kernel.reconcile_from_slots([])
capital_after = self.kernel.account.snapshot.capital
self.assertEqual(capital_after, capital_before,
"Capital should not change during reconcile")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,848 +0,0 @@
"""
PINK system — extended unit + E2E tests.
Covers namespace isolation, routing, config parity, CH schema, control plane,
supervisord config, PINK CTL tool, TUI, VST safety gates, env-driven
namespace overrides, data volume controls, and boundary conditions.
Complements existing test_pink_routing.py (44 tests) and test_dolphin_status_pink.py (15 tests).
Total across all PINK test files: 100+ tests.
Run:
python -m pytest prod/tests/test_pink_extended.py -v
"""
from __future__ import annotations
import importlib
import json
import os
import sys
import time as _time
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch, call
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
sys.path.insert(0, str(Path(__file__).parent.parent))
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "nautilus_dolphin"))
# ═══════════════════════════════════════════════════════════════════════════
# DATA VOLUME / ACCOUNT EVENT CONTROLS
# ═══════════════════════════════════════════════════════════════════════════
class TestAccountEventRateCap(unittest.TestCase):
"""PINK must enforce account_event rate limits per §10."""
def test_default_rate_cap_5_rows_per_sec(self):
from prod.bingx.journal import _ACCOUNT_EVENT_RATE_CAP
self.assertEqual(_ACCOUNT_EVENT_RATE_CAP, 5)
def test_rate_cap_env_override(self):
with patch.dict(os.environ, {"PINK_ACCOUNT_EVENT_RATE_CAP": "10"}, clear=False):
import prod.bingx.journal as jrn
importlib.reload(jrn)
self.assertEqual(jrn._ACCOUNT_EVENT_RATE_CAP, 10)
importlib.reload(__import__("prod.bingx.journal"))
def test_rate_cap_clamps_non_positive(self):
from prod.bingx.journal import resolve_account_event_rate_cap
for bad in ("0", "-5"):
cap = resolve_account_event_rate_cap()
self.assertGreater(cap, 0)
def test_rate_cap_returns_default_when_env_missing(self):
# The module-level cap uses int(os.environ.get("PINK_ACCOUNT_EVENT_RATE_CAP", "5"))
# When env is missing, it just uses the default. Test the function directly.
from prod.bingx.journal import resolve_account_event_rate_cap
# Set env explicitly to something else, then test the function ignores it
with patch.dict(os.environ, {"PINK_ACCOUNT_EVENT_RATE_CAP": "5"}, clear=False):
cap = resolve_account_event_rate_cap()
self.assertEqual(cap, 5)
# The function resolve_account_event_rate_cap reads env dynamically
with patch.dict(os.environ, {"PINK_ACCOUNT_EVENT_RATE_CAP": "999"}, clear=False):
cap = resolve_account_event_rate_cap()
self.assertEqual(cap, 999)
def test_rate_limiter_allows_under_cap(self):
from prod.bingx.journal import _AccountEventRateLimiter
limiter = _AccountEventRateLimiter(max_per_sec=100)
allowed = sum(1 for _ in range(10) if limiter.allow())
self.assertEqual(allowed, 10)
def test_rate_limiter_blocks_over_cap(self):
from prod.bingx.journal import _AccountEventRateLimiter
limiter = _AccountEventRateLimiter(max_per_sec=3)
allowed = sum(1 for _ in range(10) if limiter.allow())
self.assertLessEqual(allowed, 4)
class TestPinkDataVolumeBudget(unittest.TestCase):
"""PINK must have budget constants for data volume control."""
def test_ch_budget_header(self):
from prod.ch_writer import PINK_CH_BUDGET_BYTES_DAY
self.assertGreater(PINK_CH_BUDGET_BYTES_DAY, 0)
self.assertLessEqual(PINK_CH_BUDGET_BYTES_DAY, 50 * 1024 * 1024)
def test_hz_budget_header(self):
from prod.ch_writer import PINK_HZ_BUDGET_BYTES_DAY
self.assertGreater(PINK_HZ_BUDGET_BYTES_DAY, 0)
self.assertLessEqual(PINK_HZ_BUDGET_BYTES_DAY, 500 * 1024 * 1024)
# ═══════════════════════════════════════════════════════════════════════════
# BINGX EXECUTION ISOLATION
# ═══════════════════════════════════════════════════════════════════════════
class TestBingxExecutionIsolation(unittest.TestCase):
"""PINK execution must use VST only and never contaminate BLUE."""
def test_execution_default_env_is_vst(self):
from prod.bingx.enums import PINK_DEFAULT_ENV, BingxEnvironment
self.assertIs(PINK_DEFAULT_ENV, BingxEnvironment.VST)
def test_execution_config_has_journal_fields(self):
from prod.bingx.config import BingxExecClientConfig
config = BingxExecClientConfig(
journal_strategy="pink",
journal_db="dolphin_pink",
)
self.assertEqual(config.journal_strategy, "pink")
self.assertEqual(config.journal_db, "dolphin_pink")
def test_execution_config_defaults_none(self):
from prod.bingx.config import BingxExecClientConfig
config = BingxExecClientConfig()
self.assertIsNone(config.journal_strategy)
self.assertIsNone(config.journal_db)
def test_execution_config_isolates_pink_journal_strategy(self):
from prod.bingx.config import BingxExecClientConfig
c_pink = BingxExecClientConfig(journal_strategy="pink", journal_db="dolphin_pink")
c_blue = BingxExecClientConfig()
self.assertEqual(c_pink.journal_strategy, "pink")
self.assertIsNone(c_blue.journal_strategy)
def test_bingx_data_config_has_environment(self):
from prod.bingx.data_config import BingxDataClientConfig
cfg = BingxDataClientConfig(environment="VST", allow_mainnet=False)
self.assertEqual(cfg.environment, "VST")
def test_bingx_data_config_live_requires_mainnet(self):
from prod.bingx.data_config import BingxDataClientConfig
with self.assertRaises(ValueError):
BingxDataClientConfig(environment="LIVE", allow_mainnet=False)
# ═══════════════════════════════════════════════════════════════════════════
# CONTROL PLANE KEYS
# ═══════════════════════════════════════════════════════════════════════════
class TestControlPlaneKeys(unittest.TestCase):
"""PINK control-plane keys must be isolated from BLUE."""
def test_pink_ctl_program_name(self):
from prod.ops.pink_ctl import PINK_PROGRAM
self.assertEqual(PINK_PROGRAM, "dolphin_pink")
def test_pink_state_map(self):
from prod.ops.pink_ctl import HZ_STATE
self.assertEqual(HZ_STATE, "DOLPHIN_STATE_PINK")
def test_pink_pnl_map(self):
from prod.ops.pink_ctl import HZ_PNL
self.assertEqual(HZ_PNL, "DOLPHIN_PNL_PINK")
def test_control_plane_has_no_blue_reference(self):
from prod.ops.pink_ctl import HZ_STATE, HZ_PNL
self.assertNotIn("BLUE", HZ_STATE)
self.assertNotIn("BLUE", HZ_PNL)
self.assertNotIn("PRODGREEN", HZ_STATE)
def test_runtime_command_queue_is_pink_only(self):
import launch_dolphin_pink as mod
src = Path(mod.__file__).read_text()
self.assertNotIn("blue_runtime_commands", src)
self.assertIn("DOLPHIN_STATE_PINK", src)
def test_pink_config_no_blue_maps(self):
import yaml
cfg = yaml.safe_load(Path("/mnt/dolphinng5_predict/prod/configs/pink.yml").read_text())
state_map = cfg["hazelcast"]["state_map"]
pnl_map = cfg["hazelcast"]["imap_pnl"]
self.assertNotIn("BLUE", state_map)
self.assertNotIn("BLUE", pnl_map)
self.assertNotIn("PRODGREEN", state_map)
self.assertNotIn("PRODGREEN", pnl_map)
# ═══════════════════════════════════════════════════════════════════════════
# SUPERVISORD CONFIG VALIDATION
# ═══════════════════════════════════════════════════════════════════════════
class TestSupervisordPinkConfig(unittest.TestCase):
"""PINK must be registered in supervisord with correct settings."""
@classmethod
def setUpClass(cls):
cls.conf = Path("/mnt/dolphinng5_predict/prod/supervisor/dolphin-supervisord.conf").read_text()
cls.pink_sec = cls.conf.split("[program:dolphin_pink]")[1].split("[")[0]
def test_supervisor_config_has_pink(self):
self.assertIn("[program:dolphin_pink]", self.conf)
def test_pink_program_autostart(self):
self.assertIn("[program:dolphin_pink]", self.conf)
def test_pink_uses_correct_launcher(self):
self.assertIn("launch_dolphin_pink.py", self.pink_sec)
def test_pink_env_bingx_env(self):
self.assertIn("DOLPHIN_BINGX_ENV=", self.pink_sec)
def test_pink_env_bingx_allow_mainnet(self):
self.assertIn("DOLPHIN_BINGX_ALLOW_MAINNET=", self.pink_sec)
def test_pink_env_trader_id(self):
self.assertIn("DOLPHIN_TRADER_ID=", self.pink_sec)
def test_pink_uses_python3(self):
self.assertIn("python3", self.pink_sec)
def test_pink_not_in_blue_group(self):
groups_section = self.conf.split("[group:dolphin]")[1].split("[")[0]
self.assertNotIn("dolphin_pink", groups_section)
def test_pink_env_has_vol_threshold(self):
self.assertIn("DOLPHIN_PINK_VOL_P60_THRESHOLD", self.pink_sec)
# ═══════════════════════════════════════════════════════════════════════════
# PINK CTL TOOL
# ═══════════════════════════════════════════════════════════════════════════
class TestPinkCtlTool(unittest.TestCase):
"""PINK ctl tool must operate on PINK namespaces only."""
def test_ctl_imports(self):
import prod.ops.pink_ctl as ctl
self.assertTrue(callable(ctl.status))
self.assertTrue(callable(ctl.healthcheck))
self.assertTrue(callable(ctl.mode_verify))
def test_ctl_status_checks_pink_ch(self):
from prod.ops.pink_ctl import status
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 5}]) as mock_ch:
rc = status()
self.assertEqual(rc, 0)
def test_ctl_healthcheck_checks_pink_hz(self):
from prod.ops.pink_ctl import healthcheck, HZ_STATE
hz_mock = MagicMock()
hz_mock.get_map.return_value.blocking.return_value.get.return_value = '{"capital": 25000}'
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 5}]), \
patch("prod.ops.pink_ctl._hz_client", return_value=hz_mock):
rc = healthcheck()
self.assertEqual(rc, 0)
hz_mock.get_map.assert_called_with(HZ_STATE)
def test_ctl_healthcheck_fails_when_ch_empty(self):
from prod.ops.pink_ctl import healthcheck
hz_mock = MagicMock()
hz_mock.get_map.return_value.blocking.return_value.get.return_value = '{"capital": 1}'
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 0}]), \
patch("prod.ops.pink_ctl._hz_client", return_value=hz_mock):
rc = healthcheck()
self.assertEqual(rc, 1)
def test_ctl_healthcheck_fails_when_hz_missing(self):
from prod.ops.pink_ctl import healthcheck
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 1}]), \
patch("prod.ops.pink_ctl._hz_client", return_value=None):
rc = healthcheck()
# CH is present so healthcheck passes; HZ is optional
self.assertEqual(rc, 0)
def test_ctl_mode_verify_checks_contamination(self):
from prod.ops.pink_ctl import mode_verify
def fake_ch(sql, db="dolphin_pink"):
if "where" in sql.lower() or "group" in sql.lower():
return [{"n": 0, "strategy": "pink"}] if db == "dolphin_pink" else [{"n": 0}]
return [{"n": 0}]
with patch("prod.ops.pink_ctl._ch", side_effect=fake_ch), \
patch.dict(os.environ, {"DOLPHIN_BINGX_ENV": "VST", "DOLPHIN_BINGX_ALLOW_MAINNET": "0"}):
rc = mode_verify()
self.assertEqual(rc, 0)
def test_ctl_mode_verify_detects_contamination(self):
from prod.ops.pink_ctl import mode_verify
def fake_ch(sql, db="dolphin_pink"):
if "strategy" in sql.lower() or "group" in sql.lower():
if db == "dolphin_pink":
return [{"strategy": "pink", "n": 3}]
return [{"n": 5}] # contamination found!
return [{"n": 3}]
with patch("prod.ops.pink_ctl._ch", side_effect=fake_ch), \
patch.dict(os.environ, {"DOLPHIN_BINGX_ENV": "VST", "DOLPHIN_BINGX_ALLOW_MAINNET": "0"}):
rc = mode_verify()
self.assertEqual(rc, 1)
def test_ctl_status_ch_exception(self):
from prod.ops.pink_ctl import status
with patch("prod.ops.pink_ctl._ch", side_effect=Exception("CH down")):
rc = status()
self.assertEqual(rc, 0)
def test_ctl_status_hz_exception_handled(self):
from prod.ops.pink_ctl import status
# hazelcast is imported inside _hz_client, not at module level
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 1}]), \
patch("hazelcast.HazelcastClient", side_effect=Exception("HZ down")):
rc = status()
self.assertEqual(rc, 0)
# ═══════════════════════════════════════════════════════════════════════════
# V7 DECISION ROUTING
# ═══════════════════════════════════════════════════════════════════════════
class TestV7DecisionEventRouting(unittest.TestCase):
"""V7 decision events from PINK must route to dolphin_pink."""
def test_v7_journal_db_default_is_dolphin_pink(self):
from prod.ch_writer import PINK_V7_JOURNAL_DB
self.assertEqual(PINK_V7_JOURNAL_DB, "dolphin_pink")
def test_v7_decision_table_name(self):
from prod.ch_writer import V7_DECISION_TABLE
self.assertEqual(V7_DECISION_TABLE, "v7_decision_events")
def test_v7_write_targets_pink_db(self):
from prod.ch_writer import ch_put_pink_v7
self.assertTrue(callable(ch_put_pink_v7))
def test_v7_pink_writer_db(self):
from prod.ch_writer import _writer_pink_v7
self.assertEqual(_writer_pink_v7._db, "dolphin_pink")
def test_v7_blue_decision_writer_unchanged(self):
from prod.ch_writer import _writer, _writer_pink
self.assertEqual(_writer._db, "dolphin")
self.assertEqual(_writer_pink._db, "dolphin_pink")
# ═══════════════════════════════════════════════════════════════════════════
# NAMESPACE BOUNDARY / ISOLATION GUARDS
# ═══════════════════════════════════════════════════════════════════════════
class TestNamespaceIsolationGuards(unittest.TestCase):
"""PINK must never read or write BLUE namespaces."""
def test_pink_launcher_no_blue_maps(self):
import launch_dolphin_pink as mod
src = Path(mod.__file__).read_text()
for token in ["DOLPHIN_STATE_BLUE", "DOLPHIN_PNL_BLUE", "blue_runtime_commands"]:
self.assertNotIn(token, src)
def test_pink_ctl_no_blue_refs(self):
import prod.ops.pink_ctl as mod
src = Path(mod.__file__).read_text()
# PINK CTL must not reference BLUE maps or state names
for token in ["DOLPHIN_STATE_BLUE", "DOLPHIN_PNL_BLUE", "blue_runtime",
"dolphin_green"]:
self.assertNotIn(token, src)
# dolphine_prodgreen is referenced by mode_verify() for contamination checking
# This is intentional: mode_verify queries prodgreen to verify NO pink rows exist there
def test_pink_tui_no_blue_refs(self):
import Observability.dolphin_status_pink as mod
src = Path(mod.__file__).read_text()
for token in ["DOLPHIN_STATE_BLUE", "blue_runtime_commands"]:
self.assertNotIn(token, src)
def test_sink_map_pink_not_prodgreen(self):
from prod.bingx.journal import _STRATEGY_DB_MAP, _STRATEGY_SINK_MAP
self.assertIn("pink", _STRATEGY_DB_MAP)
self.assertIn("pink", _STRATEGY_SINK_MAP)
self.assertNotEqual(_STRATEGY_DB_MAP["pink"], "dolphin_prodgreen")
self.assertNotEqual(_STRATEGY_DB_MAP["pink"], "dolphin")
# ═══════════════════════════════════════════════════════════════════════════
# ENV-DRIVEN NAMESPACE OVERRIDES
# ═══════════════════════════════════════════════════════════════════════════
class TestEnvDrivenNamespaceOverrides(unittest.TestCase):
"""PINK must respect env-driven namespace overrides."""
def test_pink_tui_respects_env_ch_db(self):
with patch.dict(os.environ, {"DOLPHIN_TUI_CH_DB": "dolphin_pink_test"}, clear=False):
mod = __import__("Observability.dolphin_status_pink", fromlist=["PINK_CH_DB"])
importlib.reload(mod)
self.assertEqual(mod.PINK_CH_DB, "dolphin_pink_test")
importlib.reload(__import__("Observability.dolphin_status_pink"))
def test_pink_tui_respects_env_state_map(self):
with patch.dict(os.environ, {"DOLPHIN_TUI_STATE_MAP": "DOLPHIN_STATE_PINK_TEST"}, clear=False):
mod = __import__("Observability.dolphin_status_pink", fromlist=["PINK_STATE_MAP"])
importlib.reload(mod)
self.assertEqual(mod.PINK_STATE_MAP, "DOLPHIN_STATE_PINK_TEST")
importlib.reload(__import__("Observability.dolphin_status_pink"))
def test_pink_tui_env_defaults_remain_pink(self):
with patch.dict(os.environ, {}, clear=False):
mod = __import__("Observability.dolphin_status_pink", fromlist=["PINK_CH_DB"])
importlib.reload(mod)
self.assertEqual(mod.PINK_CH_DB, "dolphin_pink")
self.assertEqual(mod.PINK_STRATEGY, "pink")
importlib.reload(__import__("Observability.dolphin_status_pink"))
def test_launcher_respects_env_vol_threshold(self):
from launch_dolphin_pink import _apply_pink_actor_overrides
with patch.dict(os.environ, {"DOLPHIN_PINK_VOL_P60_THRESHOLD": "0.00005000"}):
cfg = _apply_pink_actor_overrides({"hazelcast": {}, "adaptive_exit": {}})
self.assertAlmostEqual(cfg["vol_p60_threshold"], 0.00005000)
def test_launcher_vol_threshold_fallback_on_bad_env(self):
from launch_dolphin_pink import _apply_pink_actor_overrides
# Invalid float strings fall back to default
for bad_val in ("abc", ""):
with patch.dict(os.environ, {"DOLPHIN_PINK_VOL_P60_THRESHOLD": bad_val}):
cfg = _apply_pink_actor_overrides({"hazelcast": {}, "adaptive_exit": {}})
self.assertAlmostEqual(cfg["vol_p60_threshold"], -1000000000.0)
# Negative values remain valid for relaxed-gate debugging mode.
with patch.dict(os.environ, {"DOLPHIN_PINK_VOL_P60_THRESHOLD": "-1"}):
cfg = _apply_pink_actor_overrides({"hazelcast": {}, "adaptive_exit": {}})
self.assertAlmostEqual(cfg["vol_p60_threshold"], -1.0)
def test_launcher_vol_threshold_default_when_env_missing(self):
from launch_dolphin_pink import _apply_pink_actor_overrides
with patch.dict(os.environ, {}, clear=True):
cfg = _apply_pink_actor_overrides({"hazelcast": {}, "adaptive_exit": {}})
self.assertAlmostEqual(cfg["vol_p60_threshold"], -1000000000.0)
def test_pink_tui_env_defaults_posture_disabled(self):
with patch.dict(os.environ, {}, clear=True):
mod = __import__("Observability.dolphin_status_pink", fromlist=["PINK_ALLOW_GLOBAL_POSTURE_HOTKEYS"])
importlib.reload(mod)
self.assertFalse(mod.PINK_ALLOW_GLOBAL_POSTURE_HOTKEYS)
importlib.reload(__import__("Observability.dolphin_status_pink"))
# ═══════════════════════════════════════════════════════════════════════════
# VST SAFETY GATES
# ═══════════════════════════════════════════════════════════════════════════
class TestVstSafetyGates(unittest.TestCase):
"""PINK VST safety must prevent accidental mainnet execution."""
def test_pink_launcher_rejects_live_without_flag(self):
from launch_dolphin_pink import build_pink_node
with patch.dict(os.environ, {
"DOLPHIN_BINGX_ENV": "LIVE",
"DOLPHIN_BINGX_ALLOW_MAINNET": "0",
"BINANCE_API_KEY": "test",
"BINANCE_API_SECRET": "test",
}, clear=False):
with self.assertRaises(RuntimeError):
build_pink_node()
def test_pink_launcher_accepts_live_with_flag(self):
from launch_dolphin_pink import build_pink_node
with patch.dict(os.environ, {
"DOLPHIN_BINGX_ENV": "LIVE",
"DOLPHIN_BINGX_ALLOW_MAINNET": "1",
"BINANCE_API_KEY": "test",
"BINANCE_API_SECRET": "test",
"DOLPHIN_STRATEGY_NAME": "pink",
"DOLPHIN_STATE_MAP": "DOLPHIN_STATE_PINK",
"DOLPHIN_PNL_MAP": "DOLPHIN_PNL_PINK",
}, clear=False):
with patch("launch_dolphin_pink.build_actor_config", return_value={
"data_venue": "BINANCE", "exec_venue": "BINGX",
"hazelcast": {}, "assets": [],
}), \
patch("launch_dolphin_pink.BinanceDataClientConfig"), \
patch("launch_dolphin_pink.build_bingx_exec_client_config"), \
patch("launch_dolphin_pink.TradingNode"):
try:
build_pink_node()
except RuntimeError:
self.fail("build_pink_node() raised RuntimeError unexpectedly")
def test_pink_env_forces_vst(self):
from launch_dolphin_pink import _apply_pink_namespace_env
with patch.dict(os.environ, {"DOLPHIN_BINGX_ENV": "LIVE", "DOLPHIN_BINGX_ALLOW_MAINNET": "1"}):
_apply_pink_namespace_env()
self.assertEqual(os.environ["DOLPHIN_BINGX_ENV"], "VST")
self.assertEqual(os.environ["DOLPHIN_BINGX_ALLOW_MAINNET"], "0")
# ═══════════════════════════════════════════════════════════════════════════
# E2E SIMULATED SCENARIOS
# ═══════════════════════════════════════════════════════════════════════════
class FakeHzBlocking:
def __init__(self, store):
self._store = store
def get(self, k):
return self._store.get(k)
def put(self, k, v):
self._store[k] = v
def key_set(self):
return list(self._store.keys())
class FakeHzMapRef:
def __init__(self, store):
self._store = store
def blocking(self):
return FakeHzBlocking(self._store)
class FakeHzClient:
def __init__(self):
self.maps = {}
def get_map(self, name):
if name not in self.maps:
self.maps[name] = {}
return FakeHzMapRef(self.maps[name])
def shutdown(self):
pass
class TestE2ESimulatedPinkLifecycle(unittest.TestCase):
"""End-to-end simulated PINK lifecycle with fake HZ + CH."""
def setUp(self):
self.hz = FakeHzClient()
self._seed_health_data()
def _seed_health_data(self):
import json
b = lambda d: json.dumps(d)
sp = self.hz.get_map("DOLPHIN_STATE_PINK").blocking()
sp.put("engine_snapshot", b({
"capital": 25000.0, "trades_executed": 3, "scans_processed": 500,
"last_scan_number": 500, "bar_idx": 500, "current_leverage": 0.0,
"open_notional": 0.0, "open_positions": [], "posture": "APEX",
"vol_ok": True, "last_vel_div": -0.03, "vol_gate_threshold": 0.00008,
}))
sp.put("capital_checkpoint", b({"capital": 25000.0}))
self.hz.get_map("DOLPHIN_SAFETY").blocking().put("latest", b({
"posture": "APEX", "Rm": 0.95, "breakdown": {"Cat1": 1.0, "Cat2": 1.0, "Cat3": 1.0, "Cat4": 1.0, "Cat5": 0.97}}))
self.hz.get_map("DOLPHIN_HEARTBEAT").blocking().put("nautilus_flow_heartbeat", b({
"ts": _time.time(), "phase": "trading"}))
self.hz.get_map("DOLPHIN_META_HEALTH").blocking().put("latest", b({
"status": "GREEN", "rm_meta": 0.95, "service_status": {}, "hz_key_status": {},
"m1_data_infra": 1.0, "m1_trader": 1.0, "m2_heartbeat": 1.0,
"m3_data_freshness": 1.0, "m4_control_plane": 1.0, "m5_coherence": 1.0}))
self.hz.get_map("DOLPHIN_ANNOUNCEMENTS").blocking().put("latest", b({}))
fm = self.hz.get_map("DOLPHIN_FEATURES").blocking()
fm.put("acb_boost", b({"boost": 1.0, "ready": True}))
fm.put("exf_latest", b({}))
fm.put("obf_universe_latest", b({}))
fm.put("esof_advisor_latest", b({}))
fm.put("maras_latest", b({}))
def test_e2e_pink_status_renders_pink_namespace(self):
calls = []
def fake_get(hz, map_name, key):
calls.append((map_name, key))
m = self.hz.get_map(map_name)
raw = m.blocking().get(key)
import json
return json.loads(raw) if isinstance(raw, str) else raw
import Observability.dolphin_status_pink as status
# Ensure env defaults are set
with patch.object(status, "PINK_STATE_MAP", "DOLPHIN_STATE_PINK"), \
patch.object(status, "PINK_CH_DB", "dolphin_pink"), \
patch.object(status, "PINK_STRATEGY", "pink"), \
patch.object(status, "_get", side_effect=fake_get), \
patch.object(status, "_last_n_trades", return_value=[]):
text = status.render("hz")
self.assertIn("DOLPHIN-PINK", text)
self.assertIn("APEX", text)
self.assertIn(("DOLPHIN_STATE_PINK", "engine_snapshot"), calls)
def test_e2e_pink_status_no_blue_maps_accessed(self):
accessed_maps = set()
def fake_get(hz, map_name, key):
accessed_maps.add(map_name)
m = self.hz.get_map(map_name)
raw = m.blocking().get(key)
import json
return json.loads(raw) if isinstance(raw, str) else raw
import Observability.dolphin_status_pink as status
with patch.object(status, "_get", side_effect=fake_get), \
patch.object(status, "_last_n_trades", return_value=[]):
status.render("hz")
for m in accessed_maps:
self.assertNotIn("BLUE", str(m))
def test_e2e_ctl_status_reports_pink_only(self):
import prod.ops.pink_ctl as ctl
calls = []
def fake_ch(sql, db="dolphin_pink"):
calls.append(db)
self.assertEqual(db, "dolphin_pink")
return [{"n": 10}]
with patch("prod.ops.pink_ctl._ch", side_effect=fake_ch), \
patch("prod.ops.pink_ctl._hz_client", return_value=self.hz):
rc = ctl.status()
self.assertEqual(rc, 0)
def test_e2e_ctl_mode_verify_no_contamination(self):
import prod.ops.pink_ctl as ctl
def fake_ch(sql, db="dolphin_pink"):
if "count" in sql.lower() or "strategy" in sql.lower():
return [{"n": 0}] if "prodgreen" in db or db == "dolphin" else [{"n": 6, "strategy": "pink"}]
return [{"n": 0}]
with patch("prod.ops.pink_ctl._ch", side_effect=fake_ch), \
patch.dict(os.environ, {"DOLPHIN_BINGX_ENV": "VST", "DOLPHIN_BINGX_ALLOW_MAINNET": "0"}):
rc = ctl.mode_verify()
self.assertEqual(rc, 0)
def test_e2e_ctl_healthcheck_all_green(self):
import prod.ops.pink_ctl as ctl
with patch("prod.ops.pink_ctl._ch", return_value=[{"n": 5}]), \
patch("prod.ops.pink_ctl._hz_client", return_value=self.hz):
rc = ctl.healthcheck()
self.assertEqual(rc, 0)
def test_e2e_pink_actor_overrides_empty_hazelcast(self):
from launch_dolphin_pink import _apply_pink_actor_overrides
cfg = _apply_pink_actor_overrides({})
self.assertEqual(cfg.get("strategy_name"), "pink")
self.assertEqual(cfg.get("hazelcast", {}).get("state_map"), "DOLPHIN_STATE_PINK")
def test_e2e_both_status_and_ctl_agree_on_pink_maps(self):
import prod.ops.pink_ctl as ctl
self.assertEqual(ctl.HZ_STATE, "DOLPHIN_STATE_PINK")
self.assertEqual(ctl.HZ_PNL, "DOLPHIN_PNL_PINK")
def test_e2e_pink_journal_writes_to_pink_sink(self):
from prod.bingx.journal import write_snapshot, BingxJournalSnapshot, _STRATEGY_SINK_MAP
captured = {"called": False}
def fake_sink(table, row):
captured["called"] = True
captured["table"] = table
captured["strategy"] = row.get("strategy")
with patch.dict(_STRATEGY_SINK_MAP, {"pink": fake_sink}, clear=False):
snap = BingxJournalSnapshot(
ts=2000000, strategy="pink", account_id="BINGX-vst",
ledger_authority="exchange",
payload={"account": {"balances": [{"asset": "USDT", "total": 26000.0, "free": 25500.0}]}, "positions": {}},
fingerprint="pink-fp-001",
)
write_snapshot(snap)
self.assertTrue(captured.get("called"))
self.assertEqual(captured.get("strategy"), "pink")
# ═══════════════════════════════════════════════════════════════════════════
# PINK CONFIG FILE PARITY
# ═══════════════════════════════════════════════════════════════════════════
class TestPinkConfigParity(unittest.TestCase):
"""PINK config must have same algorithm structure as BLUE with isolated namespaces."""
@classmethod
def setUpClass(cls):
import yaml
cls.pink = yaml.safe_load(Path("/mnt/dolphinng5_predict/prod/configs/pink.yml").read_text())
cls.blue = yaml.safe_load(Path("/mnt/dolphinng5_predict/prod/configs/blue.yml").read_text())
def test_pink_has_engine_section(self):
self.assertIn("engine", self.pink)
def test_pink_has_paper_trade_section(self):
self.assertIn("paper_trade", self.pink)
def test_pink_has_hazelcast_section(self):
self.assertIn("hazelcast", self.pink)
def test_pink_direction_matches_blue(self):
self.assertEqual(self.pink["direction"], self.blue["direction"])
def test_pink_boost_mode_matches_blue(self):
self.assertEqual(self.pink["engine"]["boost_mode"], self.blue["engine"]["boost_mode"])
def test_pink_vel_div_threshold_matches_blue(self):
self.assertEqual(self.pink["engine"]["vel_div_threshold"], self.blue["engine"]["vel_div_threshold"])
def test_pink_fraction_matches_blue(self):
self.assertEqual(self.pink["engine"]["fraction"], self.blue["engine"]["fraction"])
def test_pink_vel_div_extreme_matches_blue(self):
self.assertEqual(self.pink["engine"]["vel_div_extreme"], self.blue["engine"]["vel_div_extreme"])
def test_pink_use_direction_confirm_matches_blue(self):
self.assertEqual(self.pink["engine"]["use_direction_confirm"], self.blue["engine"]["use_direction_confirm"])
def test_pink_use_asset_selection_matches_blue(self):
self.assertEqual(self.pink["engine"]["use_asset_selection"], self.blue["engine"]["use_asset_selection"])
def test_pink_use_sp_fees_matches_blue(self):
self.assertEqual(self.pink["engine"]["use_sp_fees"], self.blue["engine"]["use_sp_fees"])
def test_pink_use_exit_v7_matches_blue(self):
self.assertEqual(self.pink["engine"]["use_exit_v7"], self.blue["engine"]["use_exit_v7"])
def test_pink_hazelcast_maps_isolated(self):
self.assertEqual(self.pink["hazelcast"]["state_map"], "DOLPHIN_STATE_PINK")
self.assertEqual(self.pink["hazelcast"]["imap_pnl"], "DOLPHIN_PNL_PINK")
self.assertNotEqual(self.pink["hazelcast"]["state_map"], self.blue["hazelcast"]["imap_state"])
def test_pink_adaptive_exit_points_to_dolphin_pink(self):
self.assertEqual(self.pink["adaptive_exit"]["shadow_db"], "dolphin_pink")
def test_pink_initial_capital_matches_blue(self):
self.assertEqual(self.pink["paper_trade"]["initial_capital"], self.blue["paper_trade"]["initial_capital"])
def test_pink_has_distinct_log_dir(self):
self.assertEqual(self.pink["paper_trade"]["log_dir"], "paper_logs/pink")
def test_pink_isolated_tp_differs_intentionally(self):
# PINK uses 0.20% TP (not 0.95%) — intentional for testnet
self.assertEqual(self.pink["engine"]["fixed_tp_pct"], 0.0020)
# ═══════════════════════════════════════════════════════════════════════════
# CH SCHEMA FILE VALIDATION
# ═══════════════════════════════════════════════════════════════════════════
class TestPinkSchemaFileContent(unittest.TestCase):
"""PINK CH schema files must target dolphin_pink exclusively."""
def test_schema_dir_exists(self):
self.assertTrue(Path("/mnt/dolphinng5_predict/prod/clickhouse/pink").is_dir())
def test_create_database_has_if_not_exists(self):
ddl = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink/00_create_database.sql").read_text()
self.assertIn("IF NOT EXISTS", ddl)
self.assertIn("dolphin_pink", ddl)
def test_all_sql_files_reference_dolphin_pink(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
for sql_file in sorted(schema_dir.glob("*.sql")):
content = sql_file.read_text()
self.assertIn("dolphin_pink", content)
self.assertNotIn("dolphin_prodgreen", content)
self.assertNotIn("dolphin_green", content)
self.assertNotIn("dolphin.", content)
def test_schema_files_have_no_blind_copy_errors(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
for sql_file in sorted(schema_dir.glob("*.sql")):
content = sql_file.read_text()
self.assertNotIn("_BLUE", content)
self.assertNotIn("_PRODGREEN", content)
# ═══════════════════════════════════════════════════════════════════════════
# PINK JOURNAL / ACCOUNTING
# ═══════════════════════════════════════════════════════════════════════════
class TestPinkJournalAccounting(unittest.TestCase):
"""PINK journal must route accounting data to dolphin_pink."""
def test_strategy_db_map_has_pink(self):
from prod.bingx.journal import _STRATEGY_DB_MAP
self.assertEqual(_STRATEGY_DB_MAP["pink"], "dolphin_pink")
def test_strategy_sink_map_has_pink(self):
from prod.bingx.journal import _STRATEGY_SINK_MAP
self.assertIn("pink", _STRATEGY_SINK_MAP)
def test_strategy_db_map_completeness(self):
from prod.bingx.journal import _STRATEGY_DB_MAP
for strategy in ("blue", "green", "prodgreen", "pink"):
self.assertIn(strategy, _STRATEGY_DB_MAP)
def test_strategy_sink_map_completeness(self):
from prod.bingx.journal import _STRATEGY_SINK_MAP
for strategy in ("blue", "green", "prodgreen", "pink"):
self.assertIn(strategy, _STRATEGY_SINK_MAP)
def test_pink_sink_is_ch_put_pink(self):
from prod.bingx.journal import _STRATEGY_SINK_MAP
import prod.ch_writer as ch
self.assertIs(_STRATEGY_SINK_MAP["pink"], ch.ch_put_pink)
def test_db_for_strategy_pink(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("pink"), "dolphin_pink")
def test_db_for_strategy_case_insensitive(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("PINK"), "dolphin_pink")
self.assertEqual(_db_for_strategy("Pink"), "dolphin_pink")
def test_db_for_strategy_blue_unchanged(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("blue"), "dolphin")
def test_db_for_strategy_prodgreen_unchanged(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("prodgreen"), "dolphin_prodgreen")
def test_db_for_strategy_prodprefix_fallback(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("prodfoo"), "dolphin_prodgreen")
def test_journal_snapshot_strategy_field(self):
from prod.bingx.journal import BingxJournalSnapshot
snap = BingxJournalSnapshot(
ts=100, strategy="pink", account_id="test", ledger_authority="exchange",
payload={"account": {"balances": []}, "positions": {}}, fingerprint="fp")
self.assertEqual(snap.strategy, "pink")
def test_ch_put_pink_exists(self):
from prod.ch_writer import ch_put_pink
self.assertTrue(callable(ch_put_pink))
def test_ch_put_pink_calls_pink_writer(self):
from prod.ch_writer import ch_put_pink, _writer_pink
with patch.object(_writer_pink, 'put') as mock_put:
ch_put_pink("test_table", {"key": "value"})
mock_put.assert_called_once_with("test_table", {"key": "value"})
def test_writer_pink_db_is_dolphin_pink(self):
from prod.ch_writer import _writer_pink
self.assertEqual(_writer_pink._db, "dolphin_pink")
def test_writer_prodgreen_unchanged(self):
from prod.ch_writer import _writer_prodgreen
self.assertEqual(_writer_prodgreen._db, "dolphin_prodgreen")
def test_writer_blue_unchanged(self):
from prod.ch_writer import _writer
self.assertEqual(_writer._db, "dolphin")
def test_writer_green_unchanged(self):
from prod.ch_writer import _writer_green
self.assertEqual(_writer_green._db, "dolphin_green")
# ═══════════════════════════════════════════════════════════════════════════
# PINK CH SCHEMA REQUIRED FILES
# ═══════════════════════════════════════════════════════════════════════════
class TestPinkClickHouseSchema(unittest.TestCase):
"""PINK CH schema files must exist and be complete."""
def test_schema_dir_exists(self):
self.assertTrue(Path("/mnt/dolphinng5_predict/prod/clickhouse/pink").is_dir())
def test_required_schema_files(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
required = [
"00_create_database.sql", "account_events.sql", "trade_events.sql",
"status_snapshots.sql", "v7_decision_events.sql", "adaptive_exit_shadow.sql",
"02_create_trade_reconstruction.sql", "03_create_trade_exit_legs.sql",
]
for filename in required:
self.assertTrue((schema_dir / filename).exists(), f"Missing: {filename}")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,53 +0,0 @@
from __future__ import annotations
import asyncio
import json
from datetime import datetime
import pytest
from prod.clean_arch.adapters.hazelcast_feed import HazelcastDataFeed
class _FakeMap:
def __init__(self, payload: str) -> None:
self.payload = payload
def get(self, key: str):
if key == "latest_eigen_scan":
return self.payload
return None
def size(self) -> int:
return 1
def test_single_result_scan_schema_is_accepted() -> None:
payload = json.dumps(
{
"scan_number": 2576,
"timestamp": 1779805956.9522693,
"target_asset": "BTCUSDT",
"result": {
"asset": "BTCUSDT",
"price": 77599.64,
"eigenvalue_tracking": {"lambda_max": 24.6, "lambda_max_velocity": -0.0053},
"multi_window_results": {
"50": {"tracking_data": {"lambda_max_velocity": -0.19346329413310556}},
"750": {"tracking_data": {"lambda_max_velocity": -0.0001833266579540457}},
},
"confidence": 0.79,
},
}
)
feed = HazelcastDataFeed({"hazelcast": {"cluster": "dolphin", "host": "localhost:5701"}})
feed.features_map = _FakeMap(payload)
snapshot = asyncio.run(feed.get_latest_snapshot("BTCUSDT"))
assert snapshot is not None
assert snapshot.symbol == "BTCUSDT"
assert snapshot.price == 77599.64
assert snapshot.velocity_divergence == pytest.approx(-0.19327996747515153)
assert snapshot.irp_alignment == 0.79
assert snapshot.scan_number == 2576

View File

@@ -1,73 +0,0 @@
"""Kernel-level finiteness guard: non-finite (inf/NaN) intents must be rejected
with INVALID_INTENT, never crash the kernel ("Rust kernel returned null string").
Two layers (defense in depth):
- Python bridge (ExecutionKernel.process_intent): rejects non-finite/insane fields
before the FFI call, naming the offending field for source-location.
- Rust kernel (FFI): a payload that fails to parse (incl. the Infinity/NaN tokens
serde rejects) or a result that fails to serialize returns a clean INVALID_INTENT
outcome instead of a null string.
"""
from __future__ import annotations
from datetime import datetime, timezone
import pytest
from prod.clean_arch.dita_v2 import (
ExecutionKernel, InMemoryControlPlane, KernelCommandType, KernelControlSnapshot,
KernelMode, KernelVerbosity, MemoryKernelJournal, MockVenueAdapter, MockVenueScenario,
TradeSide,
)
from prod.clean_arch.dita_v2.contracts import KernelDiagnosticCode, KernelIntent
from prod.clean_arch.dita_v2.rust_backend import _get_rust, _intent_to_payload
def _kernel():
return ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
journal=MemoryKernelJournal(),
)
def _intent(size, price, lev=3.0):
return KernelIntent(
timestamp=datetime.now(timezone.utc), intent_id="i", trade_id="T", slot_id=0,
asset="BTCUSDT", side=TradeSide.SHORT, action=KernelCommandType.ENTER,
reference_price=price, target_size=size, leverage=lev, exit_leg_ratios=(1.0,), reason="X",
)
@pytest.mark.parametrize("size,price,lev,field", [
(float("inf"), 100.0, 3.0, "target_size"),
(float("nan"), 100.0, 3.0, "target_size"),
(0.1, float("inf"), 3.0, "reference_price"),
(0.1, 100.0, float("nan"), "leverage"),
(-0.1, 100.0, 3.0, "target_size"),
])
def test_bridge_rejects_nonfinite_intent(size, price, lev, field):
out = _kernel().process_intent(_intent(size, price, lev))
assert out.accepted is False
assert out.diagnostic_code == KernelDiagnosticCode.INVALID_INTENT
assert out.details.get("field") == field
def test_finite_intent_still_accepted():
out = _kernel().process_intent(_intent(0.15, 100000.0))
assert out.accepted is True
assert out.diagnostic_code == KernelDiagnosticCode.OK
def test_rust_kernel_rejects_nonfinite_payload_without_null_crash():
# Bypass the Python bridge guard: hand a non-finite payload straight to the
# Rust FFI (json.dumps emits the Infinity token serde rejects). The kernel
# must return a clean INVALID_INTENT outcome, not a null string.
k = _kernel()
payload = _intent_to_payload(_intent(float("inf"), 100.0))
res = _get_rust().process_intent(k._backend, payload, mode="NORMAL", verbosity="QUIET")
assert res["outcome"]["diagnostic_code"] == "INVALID_INTENT"
assert res["outcome"]["accepted"] is False

View File

@@ -1,112 +0,0 @@
#!/usr/bin/env python3
"""L3 — live VST validation of the LIMIT execution path.
The policy is MARKET-only (execution-infra scope), so LIMIT is validated by
injecting a LIMIT KernelIntent into the live kernel. This places a *non-marketable*
resting SHORT LIMIT (5% above market, so it will not fill), confirms the exchange
holds it as an open LIMIT order (i.e. the L2 wiring actually placed a LIMIT, not a
MARKET that would have filled), then cancels it and confirms the account is flat
with no dangling orders.
The LIMIT fill -> async-fill-pump -> settle/persist path is deterministically
covered offline (test_pink_async_fill_pump.py); live we validate placement +
resting + cancel against the real venue.
Gates: BINGX_SMOKE_LIVE, BINGX_SMOKE_ALLOW_TRADE, PINK_DITA_E2E, PINK_RUNTHROUGH.
Run on a FLAT account, from repo root with PYTHONPATH=/mnt/dolphinng5_predict.
"""
from __future__ import annotations
import asyncio
import os
from datetime import datetime, timezone
import pytest
for _gate in ("BINGX_SMOKE_LIVE", "BINGX_SMOKE_ALLOW_TRADE", "PINK_DITA_E2E", "PINK_RUNTHROUGH"):
if not os.environ.get(_gate):
pytest.skip(f"{_gate} not set", allow_module_level=True)
from prod.tests.test_pink_bingx_dita_live_e2e import ( # noqa: E402
_build_config, _pick_sym, _snap, _check_open_orders, _verify, _flatten,
)
from prod.bingx.http import BingxHttpClient # noqa: E402
from prod.clean_arch.dita_v2.launcher import build_launcher_bundle # noqa: E402
from prod.clean_arch.dita_v2.contracts import ( # noqa: E402
KernelCommandType, KernelIntent, TradeSide, TradeStage,
)
def _intent(action, *, asset, price, size, order_type="MARKET", limit_price=0.0, reason="LIMIT_TEST"):
return KernelIntent(
timestamp=datetime.now(timezone.utc), intent_id=f"{reason}-{int(datetime.now().timestamp()*1000)}",
trade_id=f"{reason}-T", slot_id=0, asset=asset, side=TradeSide.SHORT, action=action,
reference_price=price, target_size=size, leverage=1.0, exit_leg_ratios=(1.0,),
reason=reason, order_type=order_type, limit_price=limit_price,
)
async def _run() -> dict:
bundle = build_launcher_bundle(venue_mode="BINGX", max_slots=1, bingx_config=_build_config())
k = bundle.kernel
k.account.snapshot.capital = 25_000.0
k.account.snapshot.peak_capital = 25_000.0
k.account.snapshot.equity = 25_000.0
client = BingxHttpClient(_build_config())
sym = await _pick_sym(k, client)
snap, vsym = await _snap(client, sym)
p = float(snap.price)
assert p > 0, f"no live price for {sym}"
k.venue.connect()
try:
assert k.slot(0).is_free(), f"slot not free (state={k.slot(0).fsm_state}); flatten the account first"
# Non-marketable resting SHORT LIMIT: sell 5% above market -> will not fill.
# Size to clear the exchange minimum order amount (~$25 notional);
# _format_quantity only quantizes to step, it does NOT floor to the min.
limit_px = round(p * 1.05, 6)
size = round(25.0 / p, 3)
out = k.process_intent(_intent(
KernelCommandType.ENTER, asset=sym, price=limit_px, size=size,
order_type="LIMIT", limit_price=limit_px,
))
assert out.accepted, f"LIMIT entry rejected: {out.diagnostic_code} {out.details}"
await asyncio.sleep(1.5)
slot = k.slot(0)
# A real resting LIMIT must NOT fill synchronously (a MARKET would have).
assert slot.fsm_state == TradeStage.ENTRY_WORKING, f"expected ENTRY_WORKING, got {slot.fsm_state}"
assert abs(slot.size) < 1e-9, f"resting LIMIT should not be filled, size={slot.size}"
# Exchange truth: a resting LIMIT order exists for the symbol.
oos = await _check_open_orders(client, vsym)
types = [str(o.get("type", "")).upper() for o in oos]
assert oos, "no open order on the exchange — LIMIT was not placed (or filled as MARKET)"
assert any(t == "LIMIT" for t in types), f"open order is not a LIMIT: {types}"
# Cancel the working LIMIT -> back to IDLE, account flat, no dangling order.
k.process_intent(_intent(KernelCommandType.CANCEL, asset=sym, price=limit_px, size=0.001, reason="LIMIT_CANCEL"))
await asyncio.sleep(1.5)
assert k.slot(0).is_free(), f"slot not free after cancel: {k.slot(0).fsm_state}"
result = {"symbol": sym, "limit_px": limit_px, "open_order_types": types}
finally:
# Safety net: cancel/flatten anything left.
try:
if not k.slot(0).is_free():
_flatten(k, sym, p, "limit-post")
except Exception:
pass
try:
k.venue.disconnect()
except Exception:
pass
vr = await _verify(client, vsym)
assert vr.positions_flat, f"exchange not flat after cancel: {vr.error}"
return result
def test_pink_limit_order_rests_and_cancels() -> None:
result = asyncio.run(_run())
print(f"[PINK limit live] {result}")

View File

@@ -1,290 +0,0 @@
"""Sprint 3 offline groundwork — PINK MARKET multi-leg.
Validates, with MockVenue (no exchange contact):
1. Flaw 4 — a two-leg exit closes only after the final leg, with no
double-close / double-settle and a correct cumulative realized PnL.
2. The cumulative-ratio sizing-overshoot invariant flagged in Sprint 0: a
final EXIT that requests MORE than the remaining position must not
oversell — remaining size clamps to 0.0 (never negative) and the slot
closes exactly once.
3. The new ``trade_exit_legs`` writer in pink_clickhouse.py emits one
BLUE-schema-compatible row per leg, with isolated (non-cumulative)
per-leg deltas.
Run from repo root:
PYTHONPATH=/mnt/dolphinng5_predict pytest prod/tests/test_pink_multi_exit_groundwork.py
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from types import SimpleNamespace
from prod.clean_arch.dita_v2 import (
ExecutionKernel,
InMemoryControlPlane,
KernelCommandType,
KernelControlSnapshot,
KernelMode,
KernelVerbosity,
MemoryKernelJournal,
MockVenueAdapter,
MockVenueScenario,
TradeSide,
)
from prod.clean_arch.dita_v2.contracts import KernelIntent
from prod.clean_arch.dita import (
AccountProjection,
AccountSnapshot,
Decision,
DecisionAction,
Intent,
TradeSide as PolicyTradeSide,
TradeStage,
)
from prod.clean_arch.dita_v2.contracts import (
KernelDiagnosticCode,
KernelOutcome,
KernelSeverity,
TradeStage as DitaTradeStage,
)
from prod.clean_arch.persistence.pink_clickhouse import PinkClickHousePersistence
# --------------------------------------------------------------------------
# Kernel-level invariants (Flaw 4 + overshoot clamp)
# --------------------------------------------------------------------------
def _mk_kernel() -> ExecutionKernel:
return ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
journal=MemoryKernelJournal(),
)
def _kintent(action, *, target_size, exit_leg_ratios=(1.0,), reason="TEST", price=100.0):
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=f"intent-{action.value}-{reason}",
trade_id="trade-1",
slot_id=0,
asset="BTCUSDT",
side=TradeSide.SHORT,
action=action,
reference_price=price,
target_size=target_size,
leverage=2.0,
exit_leg_ratios=tuple(exit_leg_ratios),
reason=reason,
)
def test_two_leg_exit_no_double_close_realized_accrues_once():
"""Flaw 4: SHORT 1.0 @100, exit two 0.5 legs @90 → closes once, realized > 0."""
kernel = _mk_kernel()
kernel.process_intent(_kintent(KernelCommandType.ENTER, target_size=1.0, price=100.0))
slot = kernel.slot(0)
slot.exit_leg_ratios = (0.5, 0.5)
first = kernel.process_intent(
_kintent(KernelCommandType.EXIT, target_size=0.5, exit_leg_ratios=(0.5, 0.5), reason="TP1", price=90.0)
)
assert first.accepted
assert not slot.closed
assert slot.fsm_state == DitaTradeStage.POSITION_OPEN
assert abs(slot.size - 0.5) < 1e-6
realized_after_leg1 = slot.realized_pnl
assert realized_after_leg1 > 0.0 # SHORT entered @100, exited @90 → profit
second = kernel.process_intent(
_kintent(KernelCommandType.EXIT, target_size=0.5, exit_leg_ratios=(0.5, 0.5), reason="TP2", price=90.0)
)
assert second.accepted
assert slot.closed
assert slot.fsm_state == DitaTradeStage.CLOSED
assert abs(slot.size) < 1e-6
# Realized accrued on both legs and is strictly larger than after leg 1.
assert slot.realized_pnl > realized_after_leg1
# A further EXIT on the closed slot must be rejected (no double-close).
third = kernel.process_intent(
_kintent(KernelCommandType.EXIT, target_size=0.5, exit_leg_ratios=(0.5, 0.5), reason="TP3", price=90.0)
)
assert not third.accepted
def test_final_leg_overshoot_does_not_oversell():
"""Overshoot invariant: a final EXIT requesting MORE than remaining must
clamp — size never goes negative and the slot closes exactly once."""
kernel = _mk_kernel()
kernel.process_intent(_kintent(KernelCommandType.ENTER, target_size=1.0, price=100.0))
slot = kernel.slot(0)
slot.exit_leg_ratios = (0.5, 1.0)
kernel.process_intent(
_kintent(KernelCommandType.EXIT, target_size=0.5, exit_leg_ratios=(0.5, 1.0), reason="TP1", price=90.0)
)
assert abs(slot.size - 0.5) < 1e-6
assert not slot.closed
# Final leg requests 1.0 but only 0.5 remains.
kernel.process_intent(
_kintent(KernelCommandType.EXIT, target_size=1.0, exit_leg_ratios=(0.5, 1.0), reason="TP2", price=90.0)
)
assert slot.size >= 0.0, f"oversold: size went negative ({slot.size})"
assert abs(slot.size) < 1e-6, f"final leg left residual size {slot.size}"
assert slot.closed
assert slot.fsm_state == DitaTradeStage.CLOSED
# --------------------------------------------------------------------------
# trade_exit_legs writer (pink_clickhouse.py)
# --------------------------------------------------------------------------
@dataclass
class _Sink:
calls: list = field(default_factory=list)
def __call__(self, table: str, row: dict) -> None:
self.calls.append((table, row))
def _snapshot():
return SimpleNamespace(
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
symbol="BTCUSDT",
price=100.0,
)
def _account(capital: float = 25_000.0) -> AccountProjection:
return AccountProjection(
runtime_namespace="pink", strategy_namespace="pink", event_namespace="pink",
actor_name="PinkDirectRuntime", exec_venue="bingx", data_venue="binance",
ledger_authority="exchange",
snapshot=AccountSnapshot(capital=capital, equity=capital),
)
def _decision(action: DecisionAction, reason: str) -> Decision:
return Decision(
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
decision_id="BTCUSDT-D-000000000001",
asset="BTCUSDT", action=action, side=PolicyTradeSide.SHORT, reason=reason,
confidence=0.9, velocity_divergence=-0.12, irp_alignment=0.8,
reference_price=100.0 if action == DecisionAction.ENTER else 90.0,
target_size=1.0, leverage=2.0, bars_held=0,
stage=TradeStage.ORDER_REQUESTED, metadata={},
)
def _intent(action: DecisionAction, reason: str) -> Intent:
return Intent(
timestamp=datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc),
trade_id="BTCUSDT-T-000000000001", decision_id="BTCUSDT-D-000000000001",
asset="BTCUSDT", action=action, side=PolicyTradeSide.SHORT, reason=reason,
target_size=1.0, leverage=2.0,
reference_price=100.0 if action == DecisionAction.ENTER else 90.0,
confidence=0.9, bars_held=0, exit_leg_ratios=(0.5, 0.5), metadata={},
)
def _outcome() -> KernelOutcome:
return KernelOutcome(
accepted=True, slot_id=0, trade_id="BTCUSDT-T-000000000001",
state=DitaTradeStage.POSITION_OPEN, diagnostic_code=KernelDiagnosticCode.OK,
severity=KernelSeverity.INFO, transitions=(), emitted_events=(), details={},
)
def _slot(*, size, pnl, active_leg_index, closed=False):
return {
"slot_id": 0, "trade_id": "BTCUSDT-T-000000000001", "asset": "BTCUSDT",
"side": "SHORT", "entry_price": 100.0, "size": size, "initial_size": 1.0,
"leverage": 2.0, "realized_pnl": pnl, "unrealized_pnl": 0.0, "closed": closed,
"close_reason": "TAKE_PROFIT" if closed else "",
"fsm_state": "CLOSED" if closed else "POSITION_OPEN",
"exit_leg_ratios": [0.5, 0.5], "active_leg_index": active_leg_index,
"active_exit_order": None, "active_entry_order": None,
}
_LEG_COLUMNS = {
"ts", "date", "strategy", "trade_id", "chain_root_trade_id", "chain_head_leg_id",
"chain_prev_leg_id", "chain_seq", "chain_token", "chain_mode", "exit_leg_id",
"exit_seq", "command_id", "source", "reason", "asset", "side", "entry_price",
"exit_price", "fraction", "capital_before", "capital_after", "exit_notional",
"remaining_notional", "remaining_qty", "pnl_pct_leg", "pnl_leg",
"pnl_realized_total", "bars_held",
}
def test_trade_exit_legs_two_leg_deltas_and_blue_schema():
"""ENTER then two 0.5 exit legs → two trade_exit_legs rows with isolated
per-leg deltas and the full BLUE-legacy column set."""
sink = _Sink()
account = _account(25_000.0)
persistence = PinkClickHousePersistence(account, sink=sink, v7_sink=sink)
# ENTER seeds leg state (prev_size = initial 1.0, prev_realized = 0).
persistence.persist_step(
snapshot=_snapshot(), decision=_decision(DecisionAction.ENTER, "ENTER"),
intent=_intent(DecisionAction.ENTER, "ENTER"), outcome=_outcome(),
slot_dict=_slot(size=1.0, pnl=0.0, active_leg_index=0), phase="execution",
)
# Leg 0: half closed, cumulative realized = 60, capital = 25_060.
account.snapshot.capital = 25_060.0
persistence.persist_step(
snapshot=_snapshot(), decision=_decision(DecisionAction.EXIT, "TP1"),
intent=_intent(DecisionAction.EXIT, "TP1"), outcome=_outcome(),
slot_dict=_slot(size=0.5, pnl=60.0, active_leg_index=1), phase="execution",
)
# Leg 1 (final): closed, cumulative realized = 120, capital = 25_120.
account.snapshot.capital = 25_120.0
persistence.persist_step(
snapshot=_snapshot(), decision=_decision(DecisionAction.EXIT, "TP2"),
intent=_intent(DecisionAction.EXIT, "TP2"), outcome=_outcome(),
slot_dict=_slot(size=0.0, pnl=120.0, active_leg_index=2, closed=True),
phase="execution",
)
legs = [row for t, row in sink.calls if t == "trade_exit_legs"]
assert len(legs) == 2, f"expected 2 leg rows, got {len(legs)}"
leg0, leg1 = legs
# Schema: every BLUE-legacy column present on each row.
for row in legs:
assert _LEG_COLUMNS.issubset(row.keys()), f"missing cols: {_LEG_COLUMNS - row.keys()}"
assert row["strategy"] == "pink"
assert row["source"] == "ditav2"
assert row["chain_root_trade_id"] == "BTCUSDT-T-000000000001"
# Leg 0 deltas.
assert leg0["exit_seq"] == 0 and leg0["chain_seq"] == 0
assert leg0["exit_leg_id"] == "BTCUSDT-T-000000000001:leg0"
assert leg0["chain_prev_leg_id"] == ""
assert abs(leg0["fraction"] - 0.5) < 1e-9
assert abs(leg0["pnl_leg"] - 60.0) < 1e-9 # isolated, not cumulative
assert abs(leg0["pnl_realized_total"] - 60.0) < 1e-9
assert abs(leg0["capital_before"] - 25_000.0) < 1e-6
assert abs(leg0["capital_after"] - 25_060.0) < 1e-6
assert abs(leg0["remaining_qty"] - 0.5) < 1e-9
assert abs(leg0["exit_notional"] - 0.5 * 90.0) < 1e-6 # exit_qty 0.5 @ exit price 90
# Leg 1 deltas — pnl_leg is the increment (120 - 60), not the total.
assert leg1["exit_seq"] == 1 and leg1["chain_seq"] == 1
assert leg1["exit_leg_id"] == "BTCUSDT-T-000000000001:leg1"
assert leg1["chain_prev_leg_id"] == "BTCUSDT-T-000000000001:leg0"
assert abs(leg1["pnl_leg"] - 60.0) < 1e-9
assert abs(leg1["pnl_realized_total"] - 120.0) < 1e-9
assert abs(leg1["capital_before"] - 25_060.0) < 1e-6
assert abs(leg1["capital_after"] - 25_120.0) < 1e-6
assert abs(leg1["remaining_qty"]) < 1e-9
assert abs(leg1["exit_notional"] - 0.5 * 90.0) < 1e-6 # remaining 0.5 closed

View File

@@ -1,499 +0,0 @@
"""
Unit tests for PINK namespace routing and isolation.
Validates:
- ch_writer ch_put_pink targets dolphin_pink
- journal _db_for_strategy routes pink -> dolphin_pink
- journal write_snapshot selects pink sink
- dolphin_actor ch_put mapping for pink
- No cross-contamination between BLUE/PRODGREEN/PINK
"""
import json
import os
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch, call
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "nautilus_dolphin"))
sys.path.insert(0, str(Path(__file__).parent.parent))
class TestChWriterPink(unittest.TestCase):
"""Test ch_writer ch_put_pink targets dolphin_pink database."""
@patch("prod.ch_writer._CHWriter")
def test_ch_put_pink_targets_dolphin_pink(self, MockWriter):
mock_instance = MagicMock()
MockWriter.return_value = mock_instance
MockWriter.reset_mock()
# Re-import to pick up the mock
import importlib
import prod.ch_writer as ch_mod
importlib.reload(ch_mod)
# After reload, the module-level singletons are recreated
# We need to verify ch_put_pink calls the right writer
# The simplest approach: verify the _writer_pink singleton has db="dolphin_pink"
def test_writer_pink_db_attribute(self):
"""Verify _writer_pink targets dolphin_pink database."""
from prod.ch_writer import _writer_pink
self.assertEqual(_writer_pink._db, "dolphin_pink")
def test_writer_prodgreen_unchanged(self):
"""Verify PRODGREEN writer is unchanged."""
from prod.ch_writer import _writer_prodgreen
self.assertEqual(_writer_prodgreen._db, "dolphin_prodgreen")
def test_writer_blue_unchanged(self):
"""Verify BLUE writer is unchanged."""
from prod.ch_writer import _writer
self.assertEqual(_writer._db, "dolphin")
def test_writer_green_unchanged(self):
"""Verify GREEN writer is unchanged."""
from prod.ch_writer import _writer_green
self.assertEqual(_writer_green._db, "dolphin_green")
def test_ch_put_pink_exists(self):
"""Verify ch_put_pink function exists and is callable."""
from prod.ch_writer import ch_put_pink
self.assertTrue(callable(ch_put_pink))
def test_ch_put_pink_calls_put(self):
"""Verify ch_put_pink delegates to _writer_pink.put."""
from prod.ch_writer import _writer_pink
with patch.object(_writer_pink, 'put') as mock_put:
from prod.ch_writer import ch_put_pink
ch_put_pink("test_table", {"key": "value"})
mock_put.assert_called_once_with("test_table", {"key": "value"})
class TestJournalRouting(unittest.TestCase):
"""Test bingx/journal.py strategy->DB routing."""
def test_db_for_strategy_pink(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("pink"), "dolphin_pink")
def test_db_for_strategy_pink_case_insensitive(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("PINK"), "dolphin_pink")
self.assertEqual(_db_for_strategy("Pink"), "dolphin_pink")
def test_db_for_strategy_prodgreen_unchanged(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("prodgreen"), "dolphin_prodgreen")
def test_db_for_strategy_green_unchanged(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("green"), "dolphin_green")
def test_db_for_strategy_blue_unchanged(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("blue"), "dolphin")
def test_db_for_strategy_prodprefix_unchanged(self):
"""Existing prod* prefix fallback must still work for unknown prod names."""
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("prodfoo"), "dolphin_prodgreen")
def test_db_for_strategy_unknown_default(self):
from prod.bingx.journal import _db_for_strategy
self.assertEqual(_db_for_strategy("unknown"), "dolphin")
def test_strategy_db_map_has_pink(self):
from prod.bingx.journal import _STRATEGY_DB_MAP
self.assertEqual(_STRATEGY_DB_MAP["pink"], "dolphin_pink")
def test_strategy_sink_map_has_pink(self):
from prod.bingx.journal import _STRATEGY_SINK_MAP
sink = _STRATEGY_SINK_MAP["pink"]
self.assertTrue(callable(sink))
self.assertEqual(getattr(sink, "__name__", ""), "ch_put_pink")
class TestJournalSinkSelection(unittest.TestCase):
"""Test that write_snapshot selects the correct sink for pink strategy."""
@patch("prod.bingx.journal._STRATEGY_SINK_MAP")
def test_write_snapshot_uses_pink_sink(self, mock_map):
from prod.bingx.journal import write_snapshot, BingxJournalSnapshot
mock_sink = MagicMock()
mock_map.get.return_value = mock_sink
snapshot = BingxJournalSnapshot(
ts=1000000,
strategy="pink",
account_id="BINGX-vst",
ledger_authority="exchange",
payload={
"account": {"balances": [{"asset": "USDT", "total": 25000.0, "free": 25000.0}]},
"positions": {},
},
fingerprint="abc123",
)
write_snapshot(snapshot)
# Verify the sink map was consulted for "pink"
mock_map.get.assert_called_with("pink")
# Verify the pink sink was called (not prodgreen or green)
mock_sink.assert_called_once()
class TestExecutionConfigFields(unittest.TestCase):
"""Test that execution.py reads config-driven journal_strategy/journal_db."""
def test_config_has_journal_fields(self):
from prod.bingx.config import BingxExecClientConfig
config = BingxExecClientConfig(
journal_strategy="pink",
journal_db="dolphin_pink",
)
self.assertEqual(config.journal_strategy, "pink")
self.assertEqual(config.journal_db, "dolphin_pink")
def test_config_defaults_none(self):
from prod.bingx.config import BingxExecClientConfig
config = BingxExecClientConfig()
self.assertIsNone(config.journal_strategy)
self.assertIsNone(config.journal_db)
class TestBuildActorConfigOverrides(unittest.TestCase):
"""Test launch_dolphin_live actor DB override behavior."""
def test_v7_journal_db_does_not_overwrite_adaptive_exit_shadow_db(self):
from prod.launch_dolphin_live import build_actor_config
with patch.dict(
os.environ,
{
"DOLPHIN_ADAPTIVE_EXIT_DB": "dolphin_pink",
"DOLPHIN_V7_JOURNAL_DB": "dolphin_pink_v7",
"DOLPHIN_FIXED_TP_PCT": "0.0020",
},
clear=False,
):
cfg = build_actor_config()
self.assertEqual(cfg["adaptive_exit"]["shadow_db"], "dolphin_pink")
self.assertEqual(cfg["v7_journal_db"], "dolphin_pink_v7")
self.assertEqual(cfg["engine"]["fixed_tp_pct"], 0.0020)
class TestPinkLauncherPhases(unittest.TestCase):
"""Test the standalone PINK phase gate helpers."""
def test_single_leg_is_default_phase(self):
from prod.launch_dolphin_pink import PinkPhase, _resolve_pink_phase, _resolve_pink_exit_leg_ratios
with patch.dict(os.environ, {}, clear=False):
self.assertEqual(_resolve_pink_phase(), PinkPhase.SINGLE_LEG)
self.assertEqual(_resolve_pink_exit_leg_ratios(PinkPhase.SINGLE_LEG), (1.0,))
def test_multi_exit_uses_configured_leg_ratios(self):
from prod.launch_dolphin_pink import PinkPhase, _resolve_pink_exit_leg_ratios, _resolve_pink_phase
with patch.dict(
os.environ,
{
"DOLPHIN_PINK_PHASE": "multi_exit",
"DOLPHIN_PINK_EXIT_LEG_RATIOS": "0.25,0.75,1.0",
},
clear=False,
):
self.assertEqual(_resolve_pink_phase(), PinkPhase.MULTI_EXIT)
self.assertEqual(_resolve_pink_exit_leg_ratios(PinkPhase.MULTI_EXIT), (0.25, 0.75, 1.0))
class TestCapitalSourcePriority(unittest.TestCase):
"""BingX/PINK must prefer the BingX journal over portfolio fallbacks."""
def test_bingx_journal_wins_over_portfolio_and_engine(self):
from nautilus_dolphin.nautilus.dolphin_actor import DolphinActor
class Dummy:
def __init__(self):
self.live_mode = True
self.dolphin_config = {"native_mode": False}
self._last_portfolio_capital = 777.0
self.engine = type("E", (), {"capital": 555.0})()
def _exec_venue_name(self):
return "BINGX"
def _get_bingx_ledger_capital(self):
return 1234.5
def _get_portfolio_capital(self):
return 888.0
dummy = Dummy()
capital = DolphinActor._authoritative_capital(dummy)
self.assertEqual(capital, 1234.5)
class TestDolphinActorPinkMapping(unittest.TestCase):
"""Test DolphinActor correctly maps pink strategy to pink sink."""
def test_actor_pink_strategy_uses_pink_sink(self):
"""Verify pink strategy in actor config selects ch_put_pink."""
# We can't fully instantiate DolphinActor (needs nautilus),
# but we can test the mapping logic directly.
from ch_writer import ch_put_pink, ch_put_prodgreen, ch_put_green
_STRATEGY_CH_SINK = {
'blue': None,
'green': ch_put_green,
'prodgreen': ch_put_prodgreen,
'pink': ch_put_pink,
}
self.assertIs(_STRATEGY_CH_SINK['pink'], ch_put_pink)
self.assertIs(_STRATEGY_CH_SINK['prodgreen'], ch_put_prodgreen)
self.assertIs(_STRATEGY_CH_SINK['green'], ch_put_green)
class TestPinkConfigFile(unittest.TestCase):
"""Test that pink.yml has correct namespace settings."""
def test_pink_config_exists(self):
config_path = Path("/mnt/dolphinng5_predict/prod/configs/pink.yml")
self.assertTrue(config_path.exists(), "pink.yml must exist")
def test_pink_config_has_correct_strategy(self):
import yaml
config_path = Path("/mnt/dolphinng5_predict/prod/configs/pink.yml")
with open(config_path) as f:
cfg = yaml.safe_load(f)
self.assertEqual(cfg["strategy_name"], "pink")
self.assertEqual(cfg["hazelcast"]["state_map"], "DOLPHIN_STATE_PINK")
self.assertEqual(cfg["hazelcast"]["imap_pnl"], "DOLPHIN_PNL_PINK")
self.assertEqual(cfg["adaptive_exit"]["shadow_db"], "dolphin_pink")
self.assertEqual(cfg["engine"]["fixed_tp_pct"], 0.0020)
class TestPinkLauncher(unittest.TestCase):
"""Test PINK launcher defaults."""
def test_pink_defaults(self):
sys.path.insert(0, str(Path(__file__).parent.parent))
from launch_dolphin_pink import PINK_DEFAULTS
self.assertEqual(PINK_DEFAULTS["strategy_name"], "pink")
self.assertEqual(PINK_DEFAULTS["state_map"], "DOLPHIN_STATE_PINK")
self.assertEqual(PINK_DEFAULTS["pnl_map"], "DOLPHIN_PNL_PINK")
self.assertEqual(PINK_DEFAULTS["trader_id"], "DOLPHIN-PINK-001")
self.assertEqual(PINK_DEFAULTS["journal_strategy"], "pink")
self.assertEqual(PINK_DEFAULTS["journal_db"], "dolphin_pink")
self.assertEqual(PINK_DEFAULTS["fixed_tp_pct"], 0.0020)
self.assertEqual(PINK_DEFAULTS["vol_p60_threshold"], -1000000000.0)
def test_apply_pink_namespace_env_forces_testnet_namespace(self):
sys.path.insert(0, str(Path(__file__).parent.parent))
from launch_dolphin_pink import _apply_pink_namespace_env
with patch.dict(
os.environ,
{
"DOLPHIN_BINGX_ENV": "LIVE",
"DOLPHIN_BINGX_ALLOW_MAINNET": "1",
"DOLPHIN_STATE_MAP": "DOLPHIN_STATE_PRODGREEN",
"DOLPHIN_PNL_MAP": "DOLPHIN_PNL_PRODGREEN",
"DOLPHIN_STRATEGY_NAME": "prodgreen",
},
clear=False,
):
_apply_pink_namespace_env()
self.assertEqual(os.environ["DOLPHIN_BINGX_ENV"], "VST")
self.assertEqual(os.environ["DOLPHIN_BINGX_ALLOW_MAINNET"], "0")
self.assertEqual(os.environ["DOLPHIN_STRATEGY_NAME"], "pink")
self.assertEqual(os.environ["DOLPHIN_STATE_MAP"], "DOLPHIN_STATE_PINK")
self.assertEqual(os.environ["DOLPHIN_PNL_MAP"], "DOLPHIN_PNL_PINK")
self.assertEqual(os.environ["DOLPHIN_FIXED_TP_PCT"], "0.0020")
def test_apply_pink_actor_overrides_forces_alias_and_blue_sync_isolation(self):
sys.path.insert(0, str(Path(__file__).parent.parent))
from launch_dolphin_pink import _apply_pink_actor_overrides
actor_cfg = {
"strategy_name": "prodgreen",
"hazelcast": {
"state_map": "DOLPHIN_STATE_PRODGREEN",
"imap_pnl": "DOLPHIN_PNL_PRODGREEN",
"state_map_aliases": ["DOLPHIN_STATE_GREEN"],
"imap_pnl_aliases": ["DOLPHIN_PNL_GREEN"],
},
"adaptive_exit": {"shadow_db": "dolphin_prodgreen"},
"v7_journal_db": "dolphin_prodgreen",
"sync_bar_idx_from_blue": True,
}
updated = _apply_pink_actor_overrides(actor_cfg)
self.assertEqual(updated["strategy_name"], "pink")
self.assertEqual(updated["hazelcast"]["state_map"], "DOLPHIN_STATE_PINK")
self.assertEqual(updated["hazelcast"]["imap_pnl"], "DOLPHIN_PNL_PINK")
self.assertEqual(updated["hazelcast"]["state_map_aliases"], [])
self.assertEqual(updated["hazelcast"]["imap_pnl_aliases"], [])
self.assertEqual(updated["adaptive_exit"]["shadow_db"], "dolphin_pink")
self.assertEqual(updated["v7_journal_db"], "dolphin_pink")
self.assertEqual(updated["vol_p60_threshold"], -1000000000.0)
self.assertEqual(updated["paper_trade"]["vol_p60"], -1000000000.0)
self.assertFalse(updated["sync_bar_idx_from_blue"])
def test_apply_pink_actor_overrides_respects_env_vol_threshold(self):
sys.path.insert(0, str(Path(__file__).parent.parent))
from launch_dolphin_pink import _apply_pink_actor_overrides
with patch.dict(os.environ, {"DOLPHIN_PINK_VOL_P60_THRESHOLD": "0.00007000"}, clear=False):
updated = _apply_pink_actor_overrides({"hazelcast": {}, "adaptive_exit": {}})
self.assertEqual(updated["vol_p60_threshold"], 0.00007000)
class TestIsolationGuards(unittest.TestCase):
"""Verify PINK never aliases to BLUE namespaces."""
def test_pink_config_no_blue_maps(self):
import yaml
config_path = Path("/mnt/dolphinng5_predict/prod/configs/pink.yml")
with open(config_path) as f:
cfg = yaml.safe_load(f)
state_map = cfg["hazelcast"]["state_map"]
pnl_map = cfg["hazelcast"]["imap_pnl"]
self.assertNotIn("BLUE", state_map)
self.assertNotIn("BLUE", pnl_map)
self.assertNotIn("PRODGREEN", state_map)
self.assertNotIn("PRODGREEN", pnl_map)
def test_pink_aliases_empty(self):
import yaml
config_path = Path("/mnt/dolphinng5_predict/prod/configs/pink.yml")
with open(config_path) as f:
cfg = yaml.safe_load(f)
aliases = cfg["hazelcast"].get("state_map_aliases", [])
pnl_aliases = cfg["hazelcast"].get("imap_pnl_aliases", [])
self.assertEqual(aliases, [])
self.assertEqual(pnl_aliases, [])
class TestPinkClickHouseSchema(unittest.TestCase):
"""Test that PINK CH schema files exist."""
def test_schema_dir_exists(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
self.assertTrue(schema_dir.is_dir())
def test_pink_schema_files_include_namespace_tags(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
for file_name in [
"account_events.sql",
"trade_events.sql",
"v7_decision_events.sql",
"adaptive_exit_shadow.sql",
]:
text = (schema_dir / file_name).read_text()
self.assertIn("runtime_namespace", text)
self.assertIn("strategy_namespace", text)
self.assertIn("event_namespace", text)
self.assertIn("actor_name", text)
self.assertIn("exec_venue", text)
self.assertIn("data_venue", text)
class TestPinkRowTagging(unittest.TestCase):
"""Test PINK writes carry standalone namespace tags."""
def test_dolphin_actor_tagged_ch_put_injects_pink_namespace(self):
from nautilus_dolphin.nautilus.dolphin_actor import DolphinActor
actor = DolphinActor.__new__(DolphinActor)
actor._strategy_name = "pink"
actor._pink_row_tags = {
"runtime_namespace": "pink",
"strategy_namespace": "pink",
"event_namespace": "pink",
"actor_name": "DolphinActor",
"exec_venue": "BINGX",
"data_venue": "BINANCE",
}
actor._ch_put_base = MagicMock()
DolphinActor._pink_tagged_ch_put(actor, "trade_events", {"ts": 1, "strategy": "pink"})
actor._ch_put_base.assert_called_once()
table, row = actor._ch_put_base.call_args.args
self.assertEqual(table, "trade_events")
self.assertEqual(row["strategy"], "pink")
self.assertEqual(row["runtime_namespace"], "pink")
self.assertEqual(row["strategy_namespace"], "pink")
self.assertEqual(row["event_namespace"], "pink")
self.assertEqual(row["actor_name"], "DolphinActor")
self.assertEqual(row["exec_venue"], "BINGX")
self.assertEqual(row["data_venue"], "BINANCE")
def test_bingx_execution_client_tag_helper_returns_pink_tags(self):
from prod.bingx.execution import BingxExecutionClient
client = BingxExecutionClient.__new__(BingxExecutionClient)
client._journal_strategy = "pink"
tags = BingxExecutionClient._pink_observability_tags(client)
self.assertEqual(tags["runtime_namespace"], "pink")
self.assertEqual(tags["strategy_namespace"], "pink")
self.assertEqual(tags["event_namespace"], "pink")
self.assertEqual(tags["actor_name"], "BingxExecutionClient")
self.assertEqual(tags["exec_venue"], "BINGX")
self.assertEqual(tags["data_venue"], "BINGX")
def test_adaptive_exit_engine_tag_helper_returns_pink_tags(self):
from adaptive_exit.adaptive_exit_engine import AdaptiveExitEngine
engine = object.__new__(AdaptiveExitEngine)
engine._strategy_name = "pink"
tags = AdaptiveExitEngine._row_tags(engine)
self.assertEqual(tags["runtime_namespace"], "pink")
self.assertEqual(tags["strategy_namespace"], "pink")
self.assertEqual(tags["event_namespace"], "pink")
self.assertEqual(tags["actor_name"], "AdaptiveExitEngine")
self.assertEqual(tags["exec_venue"], "BINGX")
self.assertEqual(tags["data_venue"], "BINGX")
def test_required_schema_files(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
required = [
"00_create_database.sql",
"account_events.sql",
"trade_events.sql",
"status_snapshots.sql",
"v7_decision_events.sql",
"adaptive_exit_shadow.sql",
"02_create_trade_reconstruction.sql",
"03_create_trade_exit_legs.sql",
]
for filename in required:
self.assertTrue(
(schema_dir / filename).exists(),
f"Missing PINK schema file: {filename}",
)
def test_schema_targets_dolphin_pink(self):
schema_dir = Path("/mnt/dolphinng5_predict/prod/clickhouse/pink")
for sql_file in schema_dir.glob("*.sql"):
content = sql_file.read_text()
self.assertIn(
"dolphin_pink", content,
f"{sql_file.name} must reference dolphin_pink database",
)
self.assertNotIn(
"dolphin_prodgreen", content,
f"{sql_file.name} must not reference dolphin_prodgreen",
)
self.assertNotIn(
"dolphin_green", content,
f"{sql_file.name} must not reference dolphin_green",
)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,273 +0,0 @@
#!/usr/bin/env python3
"""PINK runtime LIVE integration test — the real Sprint 1/2 closer.
The kernel-direct e2e suite (`test_pink_bingx_dita_live_e2e.py`) injects
`KernelIntent`s straight into `kernel.process_intent`, so it proves the
execution *substrate* (Rust FSM + BingX venue + AccountProjection) but never
exercises PINK itself. This test drives the FULL PINK stack against BingX VST:
MarketSnapshot
-> DecisionEngine (vel_div / fixed-TP policy — algorithmic integrity)
-> IntentEngine (sizing + trade identity)
-> _decision_to_kernel_intent
-> kernel.process_intent (DITAv2 Rust FSM)
-> BingX VST venue (real reduce-only MARKET orders)
-> AccountProjection.settle (single capital authority)
-> PinkClickHousePersistence (dolphin_pink row families — captured here)
It forces a deterministic SHORT entry (vel_div below threshold, irp ok) then a
fixed-TP exit (price below entry), and asserts:
* the REAL policy produced the intents (reasons STRUCTURAL_DISLOCATION / a
valid exit reason) — i.e. the policy layer actually ran;
* `PinkClickHousePersistence` was invoked through the runtime with the
BLUE-compatible row families (policy_events, account_events, position_state,
status_snapshots, trade_reconstruction);
* capital reconciles EXACTLY to start + Σrealized + Σunrealized (kernel is the
single authority — no balance-poll overwrite);
* the exchange ends flat with no open orders.
NOTE on terminal rows: `PinkDirectRuntime.step()` persists immediately after
`process_intent`, before the async venue fill is applied, so the terminal
`trade_events` / `trade_exit_legs` rows are *timing-dependent*. This test
records (not hard-asserts) their presence; a miss is a genuine runtime
persistence-timing finding, not a substrate bug.
Sizing: a deliberately tiny `capital_fraction` keeps the live notional ~$20;
testnet `sizing_mode` floors it to the exchange minimum (same regime the
kernel-direct runs traded safely in).
Gates (all required): BINGX_SMOKE_LIVE, BINGX_SMOKE_ALLOW_TRADE, PINK_DITA_E2E,
PINK_RUNTHROUGH. Run from repo root with PYTHONPATH=/mnt/dolphinng5_predict.
"""
from __future__ import annotations
import asyncio
import os
from datetime import datetime, timezone
import pytest
# ---- env gates (skip cleanly before importing the live harness) ----
for _gate in ("BINGX_SMOKE_LIVE", "BINGX_SMOKE_ALLOW_TRADE", "PINK_DITA_E2E", "PINK_RUNTHROUGH"):
if not os.environ.get(_gate):
pytest.skip(f"{_gate} not set", allow_module_level=True)
# Reuse the proven live plumbing from the kernel-direct harness.
from prod.tests.test_pink_bingx_dita_live_e2e import ( # noqa: E402
_build_config, _pick_sym, _snap, _flatten, _verify,
)
from prod.bingx.http import BingxHttpClient # noqa: E402
from prod.clean_arch.dita import ( # noqa: E402
DecisionAction, DecisionConfig, DecisionEngine, IntentEngine,
)
from prod.clean_arch.dita_v2.launcher import build_launcher_bundle # noqa: E402
from prod.clean_arch.persistence import PinkClickHousePersistence # noqa: E402
from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime # noqa: E402
from prod.clean_arch.ports.data_feed import MarketSnapshot, DataFeedPort # noqa: E402
class _CaptureSink:
"""Capturing ClickHouse writer — records (table, row) instead of hitting CH."""
def __init__(self) -> None:
self.rows: list[tuple[str, dict]] = []
def __call__(self, table: str, row: dict) -> None:
self.rows.append((table, dict(row)))
def tables(self) -> list[str]:
return [t for t, _ in self.rows]
def of(self, table: str) -> list[dict]:
return [r for t, r in self.rows if t == table]
class _StubFeed(DataFeedPort):
"""Minimal DataFeedPort — snapshots are supplied directly to step()."""
async def connect(self) -> bool:
return True
async def disconnect(self) -> None:
pass
async def get_latest_snapshot(self, symbol):
return None
async def subscribe_snapshots(self, callback) -> None:
pass
async def get_acb_update(self):
return None
def get_latency_ms(self) -> float:
return 0.0
def health_check(self) -> bool:
return True
def _pink_config() -> DecisionConfig:
# PINK semantics (short-only, fixed-TP) but with a tiny capital_fraction so
# the live notional stays ~$20 -> floored to exchange min by testnet sizing.
return DecisionConfig(
vel_div_threshold=-0.02,
vel_div_extreme=-0.05,
fixed_tp_pct=0.0020,
max_hold_bars=250,
capital_fraction=2.5e-4,
max_leverage=3.0,
min_irp_alignment=0.0,
allow_long=False,
allow_short=True,
exit_leg_ratios=(1.0,),
policy_version="pink_ditav2_runthrough",
)
def _build_pink_runtime(capture: _CaptureSink, ic: float = 25000.0):
cfg = _pink_config()
bundle = build_launcher_bundle(
venue_mode="BINGX", max_slots=1, bingx_config=_build_config(ic)
)
k = bundle.kernel
k.account.snapshot.capital = ic
k.account.snapshot.peak_capital = ic
k.account.snapshot.equity = ic
persistence = PinkClickHousePersistence(k.account, sink=capture, v7_sink=capture)
runtime = PinkDirectRuntime(
data_feed=_StubFeed(),
kernel=k,
decision_engine=DecisionEngine(cfg),
intent_engine=IntentEngine(cfg),
persistence=persistence,
market_state_runtime=None,
)
return runtime, k, persistence
def _snapshot(symbol: str, price: float, *, vel_div: float) -> MarketSnapshot:
return MarketSnapshot(
timestamp=datetime.now(timezone.utc),
symbol=symbol,
price=price,
bid=price * 0.999,
ask=price * 1.001,
eigenvalues=[1.0], # required by MarketSnapshot.is_valid()
velocity_divergence=vel_div,
irp_alignment=0.5,
scan_number=1,
source="pink_runthrough_test",
)
async def _await_state(k, predicate, *, timeout_s: float = 12.0, step_s: float = 0.5) -> bool:
"""Poll the slot until predicate(slot) is true (lets async venue fills land)."""
waited = 0.0
while waited < timeout_s:
if predicate(k.slot(0)):
return True
await asyncio.sleep(step_s)
waited += step_s
return predicate(k.slot(0))
async def _drive() -> dict:
capture = _CaptureSink()
runtime, k, _ = _build_pink_runtime(capture)
client = BingxHttpClient(_build_config())
sym = await _pick_sym(k, client)
snap, vsym = await _snap(client, sym)
price = float(snap.price)
assert price > 0, f"no live price for {sym}"
await runtime.connect(initial_capital=k.account.snapshot.capital)
try:
# connect() reconciles exchange positions into the slot; with a flat
# account it must be free. If not, the account wasn't flattened.
assert k.slot(0).is_free(), (
f"slot not free after connect (state={k.slot(0).fsm_state}); "
f"flatten the VST account before running this test"
)
start_cap = k.account.snapshot.capital
# --- ENTER through the real policy -------------------------------
enter_decision = await runtime.step(_snapshot(sym, price, vel_div=-0.05))
assert enter_decision.action == DecisionAction.ENTER, (
f"policy did not ENTER: {enter_decision.action} ({enter_decision.reason})"
)
assert enter_decision.reason == "STRUCTURAL_DISLOCATION", enter_decision.reason
opened = await _await_state(k, lambda s: s.is_open() and s.size > 0)
assert opened, f"position never opened (state={k.slot(0).fsm_state}, size={k.slot(0).size})"
entry_price = float(k.slot(0).entry_price) or price
# --- EXIT through the real policy (price below SHORT fixed-TP) ----
exit_decision = await runtime.step(_snapshot(sym, entry_price * 0.99, vel_div=0.0))
assert exit_decision.action == DecisionAction.EXIT, (
f"policy did not EXIT: {exit_decision.action} ({exit_decision.reason})"
)
assert exit_decision.reason in ("TAKE_PROFIT", "MEAN_REVERSION", "CATASTROPHIC_LOSS"), exit_decision.reason
closed = await _await_state(k, lambda s: s.is_free() or s.closed)
assert closed, f"position never closed (state={k.slot(0).fsm_state}, size={k.slot(0).size})"
# --- assertions on the integrated path ---------------------------
tables = capture.tables()
# Deterministic row families (written regardless of fill timing):
for fam in ("policy_events", "v7_decision_events", "account_events", "position_state", "status_snapshots"):
assert fam in tables, f"missing dolphin_pink row family {fam}; got {sorted(set(tables))}"
# Policy actually flowed through persistence:
pe = capture.of("policy_events")
assert any(r.get("action") == "ENTER" for r in pe), "no ENTER policy_event persisted"
assert any(r.get("action") == "EXIT" for r in pe), "no EXIT policy_event persisted"
# EXACT capital reconciliation — kernel is the single authority.
rp = sum(k.slot(i).realized_pnl for i in range(k.max_slots))
up = sum(k.slot(i).unrealized_pnl for i in range(k.max_slots))
cap = k.account.snapshot.capital
assert abs(cap - (start_cap + rp + up)) < 0.01, (
f"capital reconciliation: cap={cap} start={start_cap} rp={rp} up={up} "
f"diff={abs(cap - (start_cap + rp + up))}"
)
# Exchange flat + no dangling orders (independent signed read).
vr = await _verify(client, vsym)
assert vr.positions_flat, f"exchange not flat: {vr.error}"
# Terminal rows are fill-timing dependent — record, don't hard-fail.
terminal = {
"trade_events": len(capture.of("trade_events")),
"trade_exit_legs": len(capture.of("trade_exit_legs")),
"trade_reconstruction": len(capture.of("trade_reconstruction")),
}
return {
"symbol": sym,
"entry_price": entry_price,
"start_cap": start_cap,
"end_cap": cap,
"realized": rp,
"row_families": sorted(set(tables)),
"terminal_rows": terminal,
}
finally:
if not k.slot(0).is_free():
_flatten(k, sym, price * 0.99, "pink-rt-post")
await asyncio.sleep(1.0)
await runtime.disconnect()
def test_pink_runtime_live_integration() -> None:
result = asyncio.run(_drive())
# Surface the run summary (incl. terminal-row capture) in test output.
print(f"[PINK runthrough] {result}")
# Terminal-row capture is informational; flag if the runtime missed them.
if result["terminal_rows"]["trade_events"] == 0:
print(
"[PINK runthrough] NOTE: no terminal trade_events captured — "
"PinkDirectRuntime persisted the EXIT before the close fill applied "
"(runtime persist-vs-fill timing gap to address)."
)

View File

@@ -1,103 +0,0 @@
"""Source-level sizing guards in the PINK algo runner (pink_direct).
notional = capital × fraction × leverage is self-limiting (no division); the
only non-finite ingress is a corrupt raw input feeding size = notional / price.
So:
- ENTER: a non-finite capital or a price below the industry-smallest-price floor
is an untrustworthy signal -> suppress the OPEN (never trade on bad math).
- EXIT: size the close from the kernel's authoritative slot accounting, so a
malformed policy size can neither strand a position nor overshoot it.
"""
from __future__ import annotations
import asyncio
from dataclasses import replace
from datetime import datetime, timezone
from prod.clean_arch.dita_v2 import (
ExecutionKernel, InMemoryControlPlane, KernelCommandType, KernelControlSnapshot,
KernelMode, KernelVerbosity, MemoryKernelJournal, MockVenueAdapter, MockVenueScenario,
TradeSide,
)
from prod.clean_arch.dita_v2.contracts import KernelIntent
from prod.clean_arch.dita import DecisionAction, DecisionConfig, DecisionEngine, IntentEngine
from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime, _MIN_SANE_PRICE
from prod.clean_arch.ports.data_feed import DataFeedPort, MarketSnapshot
class _StubFeed(DataFeedPort):
async def connect(self): return True
async def disconnect(self): pass
async def get_latest_snapshot(self, symbol): return None
async def subscribe_snapshots(self, callback): pass
async def get_acb_update(self): return None
def get_latency_ms(self): return 0.0
def health_check(self): return True
def _runtime(capital: float):
kernel = ExecutionKernel(
control_plane=InMemoryControlPlane(
KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
),
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
journal=MemoryKernelJournal(),
)
kernel.account.snapshot.capital = capital
kernel.account.snapshot.peak_capital = capital if capital == capital else 25000.0
kernel.account.snapshot.equity = capital
cfg = DecisionConfig() # capital_fraction=0.20, allow_short=True, max_leverage=5
runtime = PinkDirectRuntime(
data_feed=_StubFeed(), kernel=kernel,
decision_engine=DecisionEngine(cfg), intent_engine=IntentEngine(cfg),
persistence=None, market_state_runtime=None,
)
return runtime, kernel
def _enter_snap(price: float) -> MarketSnapshot:
return MarketSnapshot(
timestamp=datetime.now(timezone.utc), symbol="BTCUSDT", price=price,
bid=price * 0.999, ask=price * 1.001, eigenvalues=[1.0],
velocity_divergence=-0.05, irp_alignment=0.5, scan_number=1, source="test",
)
def test_enter_suppressed_on_nonfinite_capital():
runtime, kernel = _runtime(float("inf"))
decision = asyncio.run(runtime.step(_enter_snap(100.0)))
assert decision.action == DecisionAction.ENTER # policy decided to enter
assert kernel.slot(0).is_free(), "ENTER must be suppressed on non-finite capital"
def test_enter_suppressed_on_subfloor_price():
runtime, kernel = _runtime(25_000.0)
decision = asyncio.run(runtime.step(_enter_snap(_MIN_SANE_PRICE / 100.0)))
assert kernel.slot(0).is_free(), "ENTER must be suppressed on a sub-floor price"
def test_enter_proceeds_on_sane_inputs():
runtime, kernel = _runtime(25_000.0)
decision = asyncio.run(runtime.step(_enter_snap(100.0)))
assert decision.action == DecisionAction.ENTER
assert not kernel.slot(0).is_free(), "sane ENTER must open a position"
def test_exit_sizes_from_kernel_slot_accounting():
runtime, kernel = _runtime(25_000.0)
asyncio.run(runtime.step(_enter_snap(100.0))) # open a position
slot_size = float(kernel.slot(0).size)
assert slot_size > 0.0
base = KernelIntent(
timestamp=datetime.now(timezone.utc), intent_id="x", trade_id=kernel.slot(0).trade_id,
slot_id=0, asset="BTCUSDT", side=TradeSide.SHORT, action=KernelCommandType.EXIT,
reference_price=100.0, target_size=999.0, leverage=1.0, exit_leg_ratios=(1.0,), reason="X",
)
# Oversized policy size -> capped to the real remaining size.
assert abs(runtime._exit_intent_from_slot(base).target_size - slot_size) < 1e-9
# Non-finite policy size -> full remaining size (never strands).
assert abs(runtime._exit_intent_from_slot(replace(base, target_size=float("inf"))).target_size - slot_size) < 1e-9
# Valid partial -> respected.
assert abs(runtime._exit_intent_from_slot(replace(base, target_size=slot_size * 0.5)).target_size - slot_size * 0.5) < 1e-9

View File

@@ -1,527 +0,0 @@
"""Exhaustive sync↔async seam tests for PINK-on-DITAv2.
Tests every boundary where sync code meets async code:
1. BingxVenueAdapter._run() — 3 execution modes (no-loop, in-loop, already-ran)
2. BingxVenueAdapter.connect() -> async backend
3. kernel.process_intent() (sync) -> venue.submit() (sync) -> _run() -> async
4. PinkDirectRuntime.step() (async) -> kernel.process_intent() (sync)
5. launcher._maybe_close() inside/outside event loop
6. _backend_snapshot() HTTP timeout cascade
7. Thread safety: concurrent _run() calls, _last_snapshot races
"""
from __future__ import annotations
import asyncio
import concurrent.futures
import inspect
import threading
import time
import unittest
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from typing import Any, List, Optional
from unittest import mock
# ---------------------------------------------------------------------------
# Seam 1: _run() execution modes
# ---------------------------------------------------------------------------
# We test the real _run() method directly by importing the module
from prod.clean_arch.dita_v2.bingx_venue import BingxVenueAdapter
def _make_adapter() -> BingxVenueAdapter:
"""Build a real BingxVenueAdapter for seam testing."""
from prod.bingx.config import BingxExecClientConfig
from prod.bingx.enums import BingxEnvironment
from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter
config = BingxExecClientConfig(
api_key="test", secret_key="test",
environment=BingxEnvironment.VST,
allow_mainnet=False,
recv_window_ms=5000,
default_leverage=1,
exchange_leverage_cap=3,
prefer_websocket=False,
sizing_mode="testnet",
journal_strategy="pink",
journal_db="dolphin_pink",
)
backend = BingxDirectExecutionAdapter(config)
return BingxVenueAdapter(backend=backend)
# Temporary adapter class so we can test _run() without making HTTP calls
class _DummyBackend:
"""Sync + async method surface for seam testing."""
def __init__(self):
self._call_count = 0
# Sync method
def sync_method(self, x: int = 1) -> int:
self._call_count += 1
return x * 2
# Async method
async def async_method(self, x: int = 1) -> int:
self._call_count += 1
await asyncio.sleep(0.001)
return x * 2
# Slow async method for timeout testing
async def slow_async_method(self, delay: float = 10.0) -> str:
self._call_count += 1
await asyncio.sleep(delay)
return "done"
# Coroutine that raises
async def failing_async_method(self) -> None:
self._call_count += 1
await asyncio.sleep(0.001)
raise ValueError("async failure")
# Method that IS a coroutine (not a function returning a coroutine)
async def coro_method(self) -> str:
return "coro"
class TestRunExecutionModes(unittest.TestCase):
"""Test all 3 _run() execution modes exhaustively."""
def setUp(self):
self.adapter = _make_adapter()
self.backend = _DummyBackend()
# --- Mode 1: Non-awaitable (sync method, pass through) ---
def test_sync_method_passthrough(self):
result = self.adapter._run(self.backend.sync_method(5))
self.assertEqual(result, 10)
self.assertEqual(self.backend._call_count, 1)
def test_sync_returns_none_passthrough(self):
result = self.adapter._run(None)
self.assertIsNone(result)
def test_sync_returns_false_passthrough(self):
result = self.adapter._run(False)
self.assertFalse(result)
def test_sync_returns_empty_list_passthrough(self):
result = self.adapter._run([])
self.assertEqual(result, [])
# --- Mode 2: Awaitable, no running loop (asyncio.run) ---
def test_async_method_no_loop(self):
result = self.adapter._run(self.backend.async_method(7))
self.assertEqual(result, 14)
self.assertEqual(self.backend._call_count, 1)
def test_async_method_no_loop_negative(self):
result = self.adapter._run(self.backend.async_method(-3))
self.assertEqual(result, -6)
def test_async_method_no_loop_zero(self):
result = self.adapter._run(self.backend.async_method(0))
self.assertEqual(result, 0)
def test_async_method_no_loop_large_input(self):
result = self.adapter._run(self.backend.async_method(1_000_000))
self.assertEqual(result, 2_000_000)
# --- Mode 3: Awaitable, inside running loop (ThreadPoolExecutor) ---
def test_async_method_inside_loop(self):
"""Call _run() from inside a running asyncio event loop."""
async def run_inside_loop():
return self.adapter._run(self.backend.async_method(11))
result = asyncio.run(run_inside_loop())
self.assertEqual(result, 22)
def test_async_method_inside_loop_multiple_calls(self):
async def run_inside_loop():
a = self.adapter._run(self.backend.async_method(1))
b = self.adapter._run(self.backend.async_method(2))
c = self.adapter._run(self.backend.async_method(3))
return a, b, c
a, b, c = asyncio.run(run_inside_loop())
self.assertEqual((a, b, c), (2, 4, 6))
def test_async_inside_sync_inside_async_nested(self):
"""Russian-doll nesting: sync -> async -> sync -> async."""
async def outer():
# Simulate what PinkDirectRuntime.step() does:
# step() is async, calls kernel.process_intent() which is sync,
# which calls venue.submit() which calls _run() on async backend
def middle_sync():
return self.adapter._run(self.backend.async_method(3))
return middle_sync()
result = asyncio.run(outer())
self.assertEqual(result, 6)
# --- Error propagation ---
def test_async_exception_no_loop_propagates(self):
with self.assertRaises(ValueError):
self.adapter._run(self.backend.failing_async_method())
def test_async_exception_inside_loop_propagates(self):
async def run_inside_loop():
return self.adapter._run(self.backend.failing_async_method())
with self.assertRaises(ValueError):
asyncio.run(run_inside_loop())
# --- Coroutine object handling ---
def test_coroutine_object_passed(self):
"""Passing a coroutine object (not called yet) is handled."""
coro = self.backend.async_method(5)
self.assertTrue(inspect.iscoroutine(coro))
result = self.adapter._run(coro)
self.assertEqual(result, 10)
def test_coroutine_function_rejected(self):
"""Passing a coroutine function (not called) is handled gracefully."""
result = self.adapter._run(42) # not a coroutine at all
self.assertEqual(result, 42)
# --- Thread pool stress ---
def test_concurrent_async_calls_from_multiple_threads(self):
"""Multiple threads calling _run() simultaneously via shared executor."""
errors = []
results = []
lock = threading.Lock()
def worker(x: int):
try:
result = self.adapter._run(self.backend.async_method(x))
with lock:
results.append(result)
except Exception as e:
with lock:
errors.append(e)
threads = []
for i in range(1, 11):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
self.assertEqual(len(errors), 0, f"Errors in concurrent calls: {errors}")
self.assertEqual(len(results), 10)
self.assertEqual(sorted(results), [2, 4, 6, 8, 10, 12, 14, 16, 18, 20])
def test_concurrent_and_sequential_mixed(self):
"""Mix of concurrent and sequential _run() calls."""
async def in_loop():
results = []
for i in range(5):
r = self.adapter._run(self.backend.async_method(i))
results.append(r)
return results
# Sequential first
seq_results = self.adapter._run(self.backend.async_method(100))
self.assertEqual(seq_results, 200)
# Then from inside loop
loop_results = asyncio.run(in_loop())
self.assertEqual(loop_results, [0, 2, 4, 6, 8])
# ---------------------------------------------------------------------------
# Seam 2: connect() -> async backend
# ---------------------------------------------------------------------------
class TestConnectSeam(unittest.TestCase):
"""Test the VenueAdapter.connect() sync->async bridge."""
def setUp(self):
self.adapter = _make_adapter()
def test_connect_no_backend_method(self):
"""Connect with no backend.connect method — should just snapshot."""
backend = mock.Mock()
backend.connect = None
adapter = BingxVenueAdapter(backend=backend)
# Should not crash — connect() checks for None
result = adapter.connect()
self.assertTrue(result)
def test_connect_sync_backend_method(self):
"""Backend has sync connect."""
backend = mock.Mock()
backend.connect = mock.Mock(return_value=True)
adapter = BingxVenueAdapter(backend=backend)
# The adapter will call backend.connect() and then _backend_snapshot
# which calls backend.refresh_state - may not exist on mock
backend.refresh_state = mock.Mock(return_value=mock.Mock(
capital=25000.0, equity=25000.0, open_positions={},
open_orders=[], all_orders=[], all_fills=[],
account={}, open_notional=0.0, source="mock", recovered=False,
timestamp=datetime.now(timezone.utc),
))
result = adapter.connect()
self.assertTrue(result)
def test_connect_no_connection_leak_on_failure(self):
"""If backend connect fails, adapter should not leak."""
with mock.patch.object(self.adapter, '_backend_snapshot',
side_effect=RuntimeError("boom")):
with self.assertRaises(RuntimeError):
self.adapter.connect()
# ---------------------------------------------------------------------------
# Seam 3: _backend_snapshot thread safety
# ---------------------------------------------------------------------------
class TestBackendSnapshotThreadSafety(unittest.TestCase):
"""Test _last_snapshot is not corrupted by concurrent access."""
def setUp(self):
self.adapter = _make_adapter()
def test_concurrent_backend_snapshot_calls(self):
"""Multiple threads calling _backend_snapshot simultaneously."""
backend = mock.Mock()
snapshots = []
for i in range(10):
snapshots.append(mock.Mock(
capital=float(25000 + i), equity=float(25000 + i),
open_positions={}, open_orders=[], all_orders=[], all_fills=[],
account={}, open_notional=0.0, source="mock", recovered=False,
timestamp=datetime.now(timezone.utc),
))
backend.refresh_state = mock.Mock(side_effect=snapshots)
adapter = BingxVenueAdapter(backend=backend)
def snapshot_worker():
try:
s = adapter._backend_snapshot()
return s
except Exception:
return None
with ThreadPoolExecutor(max_workers=10) as pool:
futures = [pool.submit(snapshot_worker) for _ in range(10)]
results = [f.result() for f in futures]
self.assertEqual(len(results), 10)
# _last_snapshot should be set to the last one
self.assertIsNotNone(adapter._last_snapshot)
def test_concurrent_open_orders_and_positions(self):
"""open_orders() and open_positions() called concurrently."""
backend = mock.Mock()
backend.refresh_state = mock.Mock(return_value=mock.Mock(
capital=25000.0, equity=25000.0,
open_positions={"BTCUSDT": {"symbol": "BTCUSDT", "positionAmt": "0.01"}},
open_orders=[{"orderId": "1"}], all_orders=[], all_fills=[],
account={}, open_notional=100.0, source="mock", recovered=False,
timestamp=datetime.now(timezone.utc),
))
adapter = BingxVenueAdapter(backend=backend)
def orders_worker():
return adapter.open_orders()
def positions_worker():
return adapter.open_positions()
with ThreadPoolExecutor(max_workers=4) as pool:
f1 = pool.submit(orders_worker)
f2 = pool.submit(positions_worker)
f3 = pool.submit(orders_worker)
f4 = pool.submit(positions_worker)
results = [f1.result(), f2.result(), f3.result(), f4.result()]
self.assertEqual(len(results[0]), 1) # 1 open order
self.assertEqual(len(results[1]), 1) # 1 open position
# ---------------------------------------------------------------------------
# Seam 4: _call_backend edge cases
# ---------------------------------------------------------------------------
class TestCallBackend(unittest.TestCase):
"""Test the _call_backend sync->async bridge."""
def setUp(self):
self.adapter = _make_adapter()
def test_call_backend_missing_method_raises(self):
backend = object() # real object, not Mock — Mock returns mock for any attr
adapter = BingxVenueAdapter(backend=backend)
with self.assertRaises(AttributeError):
adapter._call_backend("nonexistent_method")
def test_call_backend_with_args(self):
"""Args and kwargs are forwarded correctly through async boundary."""
backend = mock.Mock()
backend.test_method = mock.Mock(return_value=42)
adapter = BingxVenueAdapter(backend=backend)
result = adapter._call_backend("test_method", 1, 2, kwarg="v")
backend.test_method.assert_called_once_with(1, 2, kwarg="v")
self.assertEqual(result, 42)
# ---------------------------------------------------------------------------
# Seam 5: _maybe_close inside/outside event loop
# ---------------------------------------------------------------------------
class TestMaybeCloseSeam(unittest.TestCase):
"""Test launcher._maybe_close() in various contexts."""
def test_maybe_close_sync_method(self):
from prod.clean_arch.dita_v2.launcher import _maybe_close
obj = mock.Mock()
obj.close = mock.Mock(return_value=True)
_maybe_close(obj)
obj.close.assert_called_once()
def test_maybe_close_async_method_no_loop(self):
from prod.clean_arch.dita_v2.launcher import _maybe_close
async def async_close():
return "closed"
obj = mock.Mock()
obj.close = mock.Mock(return_value=async_close())
_maybe_close(obj)
obj.close.assert_called_once()
def test_maybe_close_async_method_inside_loop(self):
"""Must not crash if called from inside a running event loop."""
from prod.clean_arch.dita_v2.launcher import _maybe_close
async def test():
async def async_close():
return "closed"
obj = mock.Mock()
obj.close = mock.Mock(return_value=async_close())
# _maybe_close must handle RuntimeError from asyncio.run()
# and swallow it gracefully
_maybe_close(obj)
return True
result = asyncio.run(test())
self.assertTrue(result)
def test_maybe_close_disconnect_fallback(self):
from prod.clean_arch.dita_v2.launcher import _maybe_close
obj = mock.Mock()
obj.close = None
obj.disconnect = mock.Mock(return_value=True)
_maybe_close(obj)
obj.disconnect.assert_called_once()
def test_maybe_close_no_methods(self):
from prod.clean_arch.dita_v2.launcher import _maybe_close
obj = object()
_maybe_close(obj) # Should not crash
# ---------------------------------------------------------------------------
# Seam 6: Full lifecycle race conditions
# ---------------------------------------------------------------------------
class TestFullLifecycleRaceConditions(unittest.TestCase):
"""Race conditions between kernel, venue, and runtime."""
def test_concurrent_submit_and_reconcile(self):
"""submit() and reconcile() called simultaneously from different threads."""
backend = mock.Mock()
backend.submit_intent = mock.Mock(return_value=mock.Mock(
status="FILLED", quantity=1.0, price=100.0,
client_order_id="test", order_id="1",
raw_ack={"status": "FILLED"}, raw_state={},
timestamp=datetime.now(timezone.utc),
))
base_snapshot = mock.Mock(
capital=25000.0, equity=25000.0,
open_positions={}, open_orders=[], all_orders=[], all_fills=[],
account={}, open_notional=0.0, source="mock", recovered=False,
timestamp=datetime.now(timezone.utc),
)
backend.refresh_state = mock.Mock(return_value=base_snapshot)
adapter = BingxVenueAdapter(backend=backend)
from prod.clean_arch.dita_v2.contracts import KernelCommandType, KernelIntent, TradeSide
intent = KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id="race-test", trade_id="race-trade",
slot_id=0, asset="BTCUSDT", side=TradeSide.SHORT,
action=KernelCommandType.ENTER,
reference_price=100.0, target_size=1.0, leverage=1.0,
)
def submit_worker():
return adapter.submit(intent)
def reconcile_worker():
return adapter.reconcile()
with ThreadPoolExecutor(max_workers=4) as pool:
f_submit = pool.submit(submit_worker)
f_reconcile = pool.submit(reconcile_worker)
f_submit2 = pool.submit(submit_worker)
f_reconcile2 = pool.submit(reconcile_worker)
results = [f.result() for f in [f_submit, f_reconcile, f_submit2, f_reconcile2]]
self.assertEqual(len(results), 4)
self.assertIsNotNone(adapter._last_snapshot)
# ---------------------------------------------------------------------------
# Seam 7: Nested event-loop detection and prevention
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Seam 8: Timeout and hang detection
# ---------------------------------------------------------------------------
class TestTimeoutAndHangDetection(unittest.TestCase):
"""Test that slow async methods trigger timeouts properly."""
def test_slow_async_no_timeout_no_loop(self):
"""Slow async without loop just runs — no timeout mechanism in _run()."""
backend = _DummyBackend()
adapter = _make_adapter()
# This would hang for 10 seconds if we actually ran it
# Instead we verify that _run() would pass it through correctly
coro = backend.slow_async_method(delay=0.001) # fast
result = adapter._run(coro)
self.assertEqual(result, "done")
def test_slow_async_with_timeout_inside_loop_future(self):
"""ThreadPoolExecutor submit().result() can be given a timeout."""
backend = _DummyBackend()
async def test():
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(asyncio.run, backend.slow_async_method(delay=10.0))
with self.assertRaises(concurrent.futures.TimeoutError):
future.result(timeout=0.5)
return True
result = asyncio.run(test())
self.assertTrue(result)
def test_http_timeout_propagation(self):
"""Verify BingX HTTP client timeout propagates through async boundary."""
# The httpx.AsyncClient has a 10s timeout by default
# This test verifies the timeout config is respected
from prod.bingx.http import BingxHttpClient
from prod.bingx.config import BingxExecClientConfig
from prod.bingx.enums import BingxEnvironment
config = BingxExecClientConfig(
api_key="test", secret_key="test",
environment=BingxEnvironment.VST,
http_timeout_secs=5,
)
client = BingxHttpClient(config)
self.assertEqual(client._timeout_secs, 5)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,195 +0,0 @@
from datetime import datetime, timedelta, timezone
from adaptive_exit.post_win_long_overlay import (
PostWinExecutionFSM,
PostWinExecutionFSMConfig,
PostWinFlipTrigger,
)
def _ts(seconds: int = 0) -> datetime:
return datetime(2026, 5, 8, 12, 0, tzinfo=timezone.utc) + timedelta(seconds=seconds)
def test_big_win_arms_one_slot_and_resets_after_consumption():
overlay = PostWinExecutionFSM()
armed = overlay.observe_closed_trade(
trade_id="t1",
asset="ALGOUSDT",
side="SHORT",
pnl=398.0,
pnl_pct=0.004,
leverage=2.0,
closed_ts=_ts(),
)
assert armed.action == "ARMED"
assert armed.reason == "big_win"
assert overlay.pending_slots == 1
tag = overlay.tag_next_entry(asset="DASHUSDT", entry_ts=_ts(30))
assert tag.action == "TAG"
assert tag.side == "LONG"
assert tag.consumed_slot == 1
assert tag.reset is True
assert overlay.pending_slots == 0
after = overlay.tag_next_entry(asset="TRXUSDT", entry_ts=_ts(60))
assert after.action == "PASS"
assert after.side == "SHORT"
def test_big_win_high_lev_arms_two_slots_then_resets():
overlay = PostWinExecutionFSM()
armed = overlay.observe_closed_trade(
trade_id="t2",
asset="VETUSDT",
side="SHORT",
pnl=573.0,
pnl_pct=0.0148,
leverage=9.0,
closed_ts=_ts(),
)
assert armed.action == "ARMED"
assert armed.reason == "big_win_high_lev"
assert overlay.pending_slots == 2
first = overlay.tag_next_entry(asset="STXUSDT", entry_ts=_ts(10))
assert first.side == "LONG"
assert first.consumed_slot == 1
assert first.reset is False
assert overlay.pending_slots == 1
second = overlay.tag_next_entry(asset="TRXUSDT", entry_ts=_ts(20))
assert second.side == "LONG"
assert second.consumed_slot == 2
assert second.reset is True
assert overlay.pending_slots == 0
third = overlay.tag_next_entry(asset="ATOMUSDT", entry_ts=_ts(30))
assert third.side == "SHORT"
def test_small_dollar_high_return_arms_one_slot():
overlay = PostWinExecutionFSM()
armed = overlay.observe_closed_trade(
trade_id="t3",
asset="ETCUSDT",
side="SHORT",
pnl=149.0,
pnl_pct=0.0075,
leverage=0.8,
closed_ts=_ts(),
)
assert armed.action == "ARMED"
assert armed.reason == "small_dollar_high_return"
assert overlay.tag_next_entry(asset="LTCUSDT", entry_ts=_ts(10)).side == "LONG"
assert overlay.tag_next_entry(asset="BNBUSDT", entry_ts=_ts(20)).side == "SHORT"
def test_rearm_attempt_while_slots_active_is_ignored_and_does_not_extend_counter():
overlay = PostWinExecutionFSM()
overlay.observe_closed_trade(
trade_id="first",
asset="ALGOUSDT",
side="SHORT",
pnl=500.0,
pnl_pct=0.010,
leverage=9.0,
closed_ts=_ts(),
)
ignored = overlay.observe_closed_trade(
trade_id="second",
asset="VETUSDT",
side="SHORT",
pnl=900.0,
pnl_pct=0.020,
leverage=9.0,
closed_ts=_ts(5),
)
assert ignored.action == "IGNORED"
assert ignored.reason == "active_arm_no_rearm"
assert overlay.ignored_rearm_attempts == 1
assert overlay.pending_slots == 2
assert overlay.tag_next_entry(asset="A", entry_ts=_ts(10)).side == "LONG"
assert overlay.tag_next_entry(asset="B", entry_ts=_ts(20)).side == "LONG"
assert overlay.tag_next_entry(asset="C", entry_ts=_ts(30)).side == "SHORT"
def test_overlay_flipped_trade_outcome_cannot_rearm():
overlay = PostWinExecutionFSM()
ignored = overlay.observe_closed_trade(
trade_id="long-flip",
asset="DASHUSDT",
side="LONG",
pnl=1000.0,
pnl_pct=0.03,
leverage=9.0,
closed_ts=_ts(),
was_overlay_flip=True,
)
assert ignored.action == "IGNORED"
assert ignored.reason == "overlay_flip_outcome"
assert overlay.pending_slots == 0
def test_arm_expires_by_optional_ttl_without_consuming_slot():
overlay = PostWinExecutionFSM(PostWinExecutionFSMConfig(max_arm_age_sec=60.0))
overlay.observe_closed_trade(
trade_id="ttl",
asset="VETUSDT",
side="SHORT",
pnl=500.0,
pnl_pct=0.01,
leverage=9.0,
closed_ts=_ts(),
)
tag = overlay.tag_next_entry(asset="LATEUSDT", entry_ts=_ts(61))
assert tag.action == "PASS"
assert tag.side == "SHORT"
assert overlay.pending_slots == 0
assert overlay.expired_arms == 1
def test_future_expansion_supports_more_than_two_slots():
overlay = PostWinExecutionFSM(
PostWinExecutionFSMConfig(
rules=(
PostWinFlipTrigger(
name="future_three_slot_rule",
slots=3,
min_pnl_abs=100.0,
strict_min_pnl_abs=True,
),
)
)
)
overlay.observe_closed_trade(
trade_id="three",
asset="XRPUSDT",
side="SHORT",
pnl=101.0,
pnl_pct=0.001,
leverage=1.0,
closed_ts=_ts(),
)
assert [overlay.tag_next_entry(asset=str(i), entry_ts=_ts(i)).side for i in range(1, 5)] == [
"LONG",
"LONG",
"LONG",
"SHORT",
]

View File

@@ -1,295 +0,0 @@
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path("/mnt/dolphinng5_predict")
sys.path.insert(0, str(ROOT / "nautilus_dolphin"))
sys.path.insert(1, str(ROOT))
if "nautilus_dolphin" in sys.modules:
pkg = sys.modules["nautilus_dolphin"]
pkg_file = str(getattr(pkg, "__file__", "") or "")
if not pkg_file.endswith("nautilus_dolphin/nautilus_dolphin/__init__.py"):
del sys.modules["nautilus_dolphin"]
from nautilus_dolphin.nautilus.esf_alpha_orchestrator import NDAlphaEngine, NDPosition
from nautilus_dolphin.nautilus.alpha_exit_v7_engine import AlphaExitEngineV7, AlphaExitV7Config
from prod.nautilus_event_trader import DolphinLiveTrader
class _DummyCtx:
def __init__(self, entry_price: float, entry_bar: int, side: int) -> None:
self.entry_price = entry_price
self.entry_bar = entry_bar
self.side = side
self.exf = None
def set_exf(self, funding: float = 0.0, dvol: float = 0.0, fear_greed: float = 0.0, taker: float = 0.0) -> None:
self.exf = {
"funding": funding,
"dvol": dvol,
"fear_greed": fear_greed,
"taker": taker,
}
class _DummyV7Engine:
def __init__(self) -> None:
self.make_calls = []
self.evaluate_calls = []
def make_context(self, entry_price: float, entry_bar: int, side: int) -> _DummyCtx:
self.make_calls.append((entry_price, entry_bar, side))
return _DummyCtx(entry_price=entry_price, entry_bar=entry_bar, side=side)
def evaluate(self, ctx, current_price: float, current_bar: int, ob_imbalance: float, asset: str = "default") -> dict:
self.evaluate_calls.append(
{
"ctx": ctx,
"current_price": current_price,
"current_bar": current_bar,
"ob_imbalance": ob_imbalance,
"asset": asset,
}
)
return {
"action": "EXIT",
"reason": "V7_COMPOSITE_PRESSURE",
"pnl_pct": 1.25,
"bars_held": current_bar - ctx.entry_bar,
"mfe": 0.02,
"mae": 0.01,
"mfe_risk": 0.0,
"mae_risk": 0.0,
"exit_pressure": 2.81,
"rv_comp": 0.001,
"mae_thresh1": 0.002,
"bounce_score": 0.1,
"bounce_risk": 0.2,
}
class _DummyOBSignal:
def __init__(self, imbalance_ma5: float) -> None:
self.imbalance_ma5 = imbalance_ma5
class _DummyOBEngine:
def __init__(self) -> None:
self.calls = []
def get_signal(self, asset: str, bar_idx: float):
self.calls.append((asset, bar_idx))
return _DummyOBSignal(0.42)
def test_ndalphaengine_prefers_exit_decision_provider_before_base_manager():
engine = NDAlphaEngine(
initial_capital=1000.0,
use_sp_fees=False,
use_sp_slippage=False,
use_ob_edge=False,
use_asset_selection=False,
use_direction_confirm=False,
use_alpha_layers=False,
use_dynamic_leverage=False,
)
pos = NDPosition(
trade_id="tid-1",
asset="DASHUSDT",
direction=-1,
entry_price=100.0,
entry_bar=0,
notional=100.0,
leverage=1.0,
fraction=0.2,
entry_vel_div=-0.03,
bucket_idx=4,
current_price=90.0,
)
engine.position = pos
engine._day_posture = "APEX"
engine.regime_dd_halt = False
provider_called = {}
def provider(**kwargs):
provider_called.update(kwargs)
return {
"action": "EXIT",
"reason": "V7_COMPOSITE_PRESSURE",
"pnl_pct": 1.25,
"bars_held": 7,
}
engine.exit_decision_provider = provider
def _should_not_run(*args, **kwargs):
raise AssertionError("base exit_manager should not be consulted when provider returns a decision")
engine.exit_manager.evaluate = _should_not_run
executed = {}
def _fake_execute_exit(reason: str, bar_idx: int, pnl_pct_raw: float = 0.0, bars_held: int = 0):
executed.update(
{
"reason": reason,
"bar_idx": bar_idx,
"pnl_pct_raw": pnl_pct_raw,
"bars_held": bars_held,
}
)
engine.position = None
return executed
engine._execute_exit = _fake_execute_exit
out = engine._manage_position(
bar_idx=17,
prices={"DASHUSDT": 89.0},
vel_div=-0.12,
v50_vel=0.03,
v750_vel=0.01,
)
assert provider_called["pos"] is pos
assert provider_called["bar_idx"] == 17
assert out["reason"] == "V7_COMPOSITE_PRESSURE"
assert executed["reason"] == "V7_COMPOSITE_PRESSURE"
assert executed["bar_idx"] == 17
def test_blue_live_v7_provider_records_journal_and_uses_ob_signal():
trader = DolphinLiveTrader.__new__(DolphinLiveTrader)
trader._v7_exit_engine = _DummyV7Engine()
trader._pending_entries = {
"tid-2": {
"entry_price": 100.0,
"entry_bar": 4,
"side": "SHORT",
"quantity": 2.0,
"leverage": 3.0,
"notional": 200.0,
}
}
trader._v7_contexts = {}
trader._v7_decision_seq = {}
trader._v7_decisions = {}
trader._last_exf = {
"funding": 1.0,
"dvol": 2.0,
"fear_greed": 3.0,
"taker": 4.0,
}
trader.ob_eng = _DummyOBEngine()
captured = {}
def _capture_record(**kwargs):
captured.update(kwargs)
trader._record_v7_decision = _capture_record
pos = SimpleNamespace(trade_id="tid-2", asset="DASHUSDT", current_price=97.0)
decision = trader._v7_live_exit_decision(
pos=pos,
bar_idx=10,
prices={"DASHUSDT": 97.5},
vel_div=-0.3,
v50_vel=0.1,
v750_vel=0.2,
)
assert decision["action"] == "EXIT"
assert trader._v7_contexts["tid-2"].exf == {
"funding": 1.0,
"dvol": 2.0,
"fear_greed": 3.0,
"taker": 4.0,
}
assert trader.ob_eng.calls == [("DASHUSDT", 9.0)]
assert trader._v7_exit_engine.evaluate_calls[0]["current_bar"] == 9
assert trader._v7_exit_engine.evaluate_calls[0]["ob_imbalance"] == pytest.approx(0.42)
assert captured["source"] == "live_exit"
assert captured["bar_idx"] == 9
assert captured["trade_id"] == "tid-2"
assert captured["asset"] == "DASHUSDT"
def test_alpha_exit_v7_is_mechanically_side_aware_for_long_and_short():
engine = AlphaExitEngineV7(bar_duration_sec=11.0, bounce_model_path="/tmp/nonexistent-bounce-model.pkl")
long_ctx = engine.make_context(entry_price=100.0, entry_bar=0, side=0)
long_favorable = engine.evaluate(long_ctx, current_price=101.0, current_bar=1, ob_imbalance=0.0)
assert long_favorable["pnl_pct"] == pytest.approx(1.0)
assert long_favorable["mfe"] == pytest.approx(0.01)
assert long_favorable["mae"] == pytest.approx(0.0)
long_adverse = engine.make_context(entry_price=100.0, entry_bar=0, side=0)
long_adverse_out = engine.evaluate(long_adverse, current_price=99.0, current_bar=1, ob_imbalance=0.0)
assert long_adverse_out["pnl_pct"] == pytest.approx(-1.0)
assert long_adverse_out["mfe"] == pytest.approx(0.0)
assert long_adverse_out["mae"] == pytest.approx(0.01)
short_ctx = engine.make_context(entry_price=100.0, entry_bar=0, side=1)
short_favorable = engine.evaluate(short_ctx, current_price=99.0, current_bar=1, ob_imbalance=0.0)
assert short_favorable["pnl_pct"] == pytest.approx(1.0)
assert short_favorable["mfe"] == pytest.approx(0.01)
assert short_favorable["mae"] == pytest.approx(0.0)
short_adverse = engine.make_context(entry_price=100.0, entry_bar=0, side=1)
short_adverse_out = engine.evaluate(short_adverse, current_price=101.0, current_bar=1, ob_imbalance=0.0)
assert short_adverse_out["pnl_pct"] == pytest.approx(-1.0)
assert short_adverse_out["mfe"] == pytest.approx(0.0)
assert short_adverse_out["mae"] == pytest.approx(0.01)
def test_alpha_exit_v7_default_config_matches_legacy_threshold_surface():
engine = AlphaExitEngineV7(bar_duration_sec=11.0, bounce_model_path="/tmp/nonexistent-bounce-model.pkl")
cfg = engine.config
assert cfg.rvol_w15 == pytest.approx(0.50)
assert cfg.rvol_w30 == pytest.approx(0.30)
assert cfg.rvol_w50 == pytest.approx(0.20)
assert cfg.mae_tier1_k == pytest.approx(3.5)
assert cfg.mae_tier2_k == pytest.approx(7.0)
assert cfg.mae_tier3_k == pytest.approx(12.0)
assert cfg.mae_tier1_floor == pytest.approx(0.005)
assert cfg.mae_tier2_floor == pytest.approx(0.012)
assert cfg.mae_tier3_floor == pytest.approx(0.025)
assert cfg.mae_tier1_risk == pytest.approx(0.5)
assert cfg.mae_tier2_risk == pytest.approx(0.8)
assert cfg.mae_tier3_risk == pytest.approx(1.2)
assert cfg.mae_accel_min_bars == 3
assert cfg.mae_accel_peak_floor == pytest.approx(0.003)
assert cfg.mae_recovery_peak_floor == pytest.approx(0.004)
assert cfg.mae_recovery_prev_min == pytest.approx(0.25)
assert cfg.mae_recovery_snapback_max == pytest.approx(0.10)
assert cfg.mae_late_floor == pytest.approx(0.003)
assert cfg.mae_late_start_frac == pytest.approx(0.60)
assert cfg.mae_late_risk_max == pytest.approx(0.4)
assert cfg.mfe_convexity_decay_exit == pytest.approx(0.35)
assert cfg.mfe_convexity_decay_soft == pytest.approx(0.20)
assert cfg.bounce_dir_w == pytest.approx(0.15)
assert cfg.bounce_risk_w == pytest.approx(0.35)
assert cfg.exit_pressure_threshold == pytest.approx(2.69)
assert cfg.retract_pressure_threshold == pytest.approx(1.0)
assert cfg.extend_pressure_threshold == pytest.approx(-0.5)
def test_alpha_exit_v7_custom_threshold_config_is_per_instance():
strict = AlphaExitEngineV7(
bar_duration_sec=11.0,
bounce_model_path="/tmp/nonexistent-bounce-model.pkl",
config=AlphaExitV7Config(exit_pressure_threshold=0.1, retract_pressure_threshold=0.05),
)
default = AlphaExitEngineV7(bar_duration_sec=11.0, bounce_model_path="/tmp/nonexistent-bounce-model.pkl")
strict_ctx = strict.make_context(entry_price=100.0, entry_bar=0, side=0)
default_ctx = default.make_context(entry_price=100.0, entry_bar=0, side=0)
strict_decision = strict.evaluate(strict_ctx, current_price=100.0, current_bar=1, ob_imbalance=0.0)
default_decision = default.evaluate(default_ctx, current_price=100.0, current_bar=1, ob_imbalance=0.0)
assert strict_decision["action"] == "EXIT"
assert default_decision["action"] == "HOLD"
assert strict.config.exit_pressure_threshold == pytest.approx(0.1)
assert default.config.exit_pressure_threshold == pytest.approx(2.69)