PINK DITAv2: Hz writes + vol_ok gate + leverage logging + 8 new tests (94/94 green)

This commit is contained in:
Codex
2026-06-03 13:26:36 +02:00
parent 0f2d3f556d
commit 8d85d75ded
6 changed files with 1570 additions and 6 deletions

View File

@@ -12,10 +12,12 @@ from __future__ import annotations
import asyncio
import inspect
import json
import logging
import math
from dataclasses import dataclass, field, replace
from datetime import datetime, timezone
from pathlib import Path
from types import SimpleNamespace
from typing import Any, Callable, Optional
@@ -75,6 +77,8 @@ def _slot_to_position_dict(slot) -> dict[str, Any]:
# overflows to inf as price -> 0. Any real perp quote is far above this floor,
# so a price below it (or non-finite) signals corrupt market data, not a trade.
_MIN_SANE_PRICE = 1e-8
# Path for kernel state persistence (crash recovery + session continuity).
_KERNEL_STATE_PATH = Path("/tmp/.pink_kernel_state.json")
def _decision_to_kernel_intent(
@@ -116,6 +120,46 @@ def _decision_to_kernel_intent(
)
def _persist_kernel_snapshot(kernel, log: logging.Logger) -> None:
"""Write full kernel state to disk after each settled fill (G5 snapshot-on-fill)."""
try:
state_json = kernel.save_state()
_KERNEL_STATE_PATH.write_text(state_json, encoding="utf-8")
except Exception as exc:
log.warning("kernel snapshot persist failed (non-fatal): %s", exc)
def _restore_kernel_snapshot(kernel, log: logging.Logger) -> bool:
"""On startup, restore kernel state from disk if account is flat (no open positions).
Returns True if a snapshot was found and successfully restored.
"""
if not _KERNEL_STATE_PATH.exists():
return False
try:
state_json = _KERNEL_STATE_PATH.read_text(encoding="utf-8")
meta = json.loads(state_json)
# Sanity check: only restore if the saved snapshot had no open trades.
saved_slots = meta.get("slots", [])
open_at_save = [s for s in saved_slots if s.get("fsm_state") not in (None, "", "IDLE", "CLOSED")]
if open_at_save:
log.warning(
"kernel snapshot has %d open slot(s) at save time — "
"skipping restore (must be flat for safe handoff)",
len(open_at_save),
)
return False
ok = kernel.restore_state(state_json)
if ok:
log.info("kernel state restored from %s (fee_calibration + account preserved)", _KERNEL_STATE_PATH)
else:
log.warning("kernel restore_state rejected snapshot (version or slot mismatch)")
return ok
except Exception as exc:
log.warning("kernel snapshot restore failed (non-fatal): %s", exc)
return False
def _reconcile_position_slot(
kernel: ExecutionKernel,
exchange_balance_capital: float,
@@ -221,11 +265,15 @@ class PinkDirectRuntime:
market_state_runtime: Any = None
event_sink: Optional[Callable[[dict[str, Any]], None]] = None
logger: Any = LOGGER
# Non-blocking Hz state writer (None = Hz unavailable; PINK trades regardless)
hz_state_writer: Any = field(default=None, repr=False, compare=False)
# Account stream state — managed by connect/disconnect, not init args
_account_stream_task: Optional[asyncio.Task] = field(
default=None, init=False, repr=False, compare=False
)
_enter_frozen: bool = field(default=False, init=False, repr=False, compare=False)
# Last known posture — carried into Hz writes for TUI/algo monitoring
_last_posture: str = field(default="APEX", init=False, repr=False, compare=False)
async def connect(self, initial_capital: float = 25000.0) -> None:
"""Connect data feed, venue, seed capital from exchange, start WS stream."""
@@ -246,6 +294,11 @@ class PinkDirectRuntime:
self.kernel.set_seed_capital(initial_capital)
await self._seed_account_from_exchange()
# Restore fee calibration + account state from the previous session if the
# kernel was flat at save time. Must be AFTER set_seed_capital and reconcile
# so the snapshot can override our fresh seed with the last-known calibration.
_restore_kernel_snapshot(self.kernel, self.logger)
# Start WS account stream (primary); poll failover handled inside stream.
self._account_stream_task = asyncio.create_task(
self._run_account_stream(), name="pink_account_stream"
@@ -269,13 +322,13 @@ class PinkDirectRuntime:
# BingX VST/LIVE taker fee schedule. These are the current published rates.
# Override via set_exchange_config() if the exchange adjusts them.
_BINGX_FEE_CONFIG: dict = {
_BINGX_FEE_CONFIG: dict = field(default_factory=lambda: {
"taker_rate": 0.0005, # 0.05% market orders
"maker_rate": 0.0002, # 0.02% limit resting
"lot_step": 0.001,
"tick_size": 0.0001,
"funding_interval_secs": 28_800, # 8 h BingX perps
}
})
async def _seed_account_from_exchange(self) -> None:
"""
@@ -347,7 +400,9 @@ class PinkDirectRuntime:
if fill_price <= 0 or fill_qty <= 0 or actual_fee <= 0:
self.logger.info("Fee calibration: fill row missing price/qty/fee — skipping")
return
report = self.kernel.calibrate_fee(fill_price, fill_qty, actual_fee)
order_type = str(row.get("orderType") or row.get("type") or "MARKET").upper()
is_maker = order_type == "LIMIT"
report = self.kernel.calibrate_fee(fill_price, fill_qty, actual_fee, is_maker=is_maker)
status = report.get("calibration_status", "?")
log = self.logger.error if status == "ERROR" else self.logger.info
log(
@@ -395,14 +450,20 @@ class PinkDirectRuntime:
"fill_price": event.fill_price,
"fill_qty": event.fill_qty,
"realized_pnl": event.realized_pnl,
"is_maker": event.is_maker,
})
# Also fold actual fee if WS delivered it
if event.fee > 0:
# Fold actual fee if WS delivered it (replaces prediction)
if event.fee != 0:
self.kernel.on_account_event({
"kind": "FILL_SETTLED",
"event_id": event.event_id,
"realized_pnl": 0.0, # already folded above
"fee": event.fee,
"fee": event.fee, # negative = rebate
"is_maker": event.is_maker,
})
# Persist full kernel state after every settled fill for
# crash recovery + session-to-session calibration continuity.
_persist_kernel_snapshot(self.kernel, self.logger)
elif event.kind == ExchangeEventKind.ACCOUNT_UPDATE:
result = self.kernel.on_account_event({
"kind": "ACCOUNT_UPDATE",
@@ -421,12 +482,20 @@ class PinkDirectRuntime:
result.get("reconcile_explanation", ""),
)
self._enter_frozen = True
# Hz write: capital_frozen state changed
_slot = self.kernel.slot(0).to_dict() if self.kernel.max_slots > 0 else {}
_acc = self.kernel.snapshot().get("account") or {}
self._hz_publish(_slot, _acc)
else:
if self._enter_frozen:
self.logger.info(
"Account reconcile %s — unfreezing ENTERs.", status
)
self._enter_frozen = False
# Hz write: unfreeze is also a state change
_slot = self.kernel.slot(0).to_dict() if self.kernel.max_slots > 0 else {}
_acc = self.kernel.snapshot().get("account") or {}
self._hz_publish(_slot, _acc)
elif event.kind == ExchangeEventKind.FUNDING_FEE:
self.kernel.on_account_event({
"kind": "FUNDING_FEE",
@@ -524,12 +593,35 @@ class PinkDirectRuntime:
if isinstance(scan_payload.get("esof_payload"), dict)
else None,
)
# Track posture for Hz writes
self._last_posture = str(scan_payload.get("posture") or "APEX")
return dict(
getattr(runtime, "latest_bundle_dict", {}) or bundle.as_dict()
)
except Exception:
return {}
def _hz_publish(self, slot_dict: dict, acc: dict) -> None:
"""Fire-and-forget Hz write after any kernel state change.
Computes system leverage (our_leverage = notional/capital) for the Hz
snapshot — this is the PINK/BLUE dual-leverage invariant: system leverage
reflects real margin utilisation; exchange leverage (1-3x cap) is set at
the BingX API level and never touches this path.
"""
if self.hz_state_writer is None:
return
try:
size = float(slot_dict.get("size") or 0.0)
ep = float(slot_dict.get("entry_price") or 0.0)
capital = float(acc.get("capital") or 0.0)
our_leverage = (size * ep / capital) if capital > 1e-10 else 0.0
self.hz_state_writer.write_engine_snapshot(
slot_dict, acc, posture=self._last_posture, our_leverage=our_leverage
)
except Exception:
pass
async def pump_venue_events(
self, snapshot: Any | None = None, *, market_state: Any = None
) -> int:
@@ -586,6 +678,9 @@ class PinkDirectRuntime:
slot_dict=slot_dict,
market_state=market_state or {},
)
# Hz write after fills settle — slot FSM and capital may have changed
acc = self.kernel.snapshot().get("account") or {}
self._hz_publish(slot_dict, acc)
return len(applied)
def _unsafe_entry_reason(self, kernel_intent: KernelIntent, context: Any) -> Optional[str]:
@@ -785,6 +880,20 @@ class PinkDirectRuntime:
phase="execution",
market_state=market_state,
)
# Hz write: ENTER/EXIT changed slot FSM — publish updated state
self._hz_publish(slot_dict, acc)
# On trade close, write daily PnL row
if (
self.hz_state_writer is not None
and slot_dict.get("closed")
):
try:
self.hz_state_writer.write_daily_pnl(acc, posture=self._last_posture)
except Exception:
pass
else:
# HOLD / no-op: update mark price in kernel.
if snapshot.price and snapshot.price > 0: