Files
siloqy/prod/tests/test_pink_sizing_guards.py

104 lines
4.5 KiB
Python
Raw Normal View History

PINK DITAv2: source-level sizing guards (bounded size, close-always) Located the source of the cutover non-finite: target_size = capital × fraction × leverage / price. notional (capital×fraction×leverage) is self-limiting (no division, bounded by capital), so a non-finite size can only come from a corrupt raw input — non-finite capital, or a price below the industry floor that overflows the division. Guards in the PINK algo runner (pink_direct), per design review: - _MIN_SANE_PRICE = 1e-8 industry-smallest-price floor. - ENTER: _unsafe_entry_reason() rejects the OPEN (logs provenance, no trade) when capital/leverage/size are non-finite/non-positive or price < floor. A corrupt sizing input is an untrustworthy signal — don't open (nothing to strand). - EXIT: _exit_intent_from_slot() sizes the close from the kernel's authoritative slot.size (cap to remaining; full remaining if policy size malformed) — a bad-math exit can never strand or overshoot a position. Falls back to policy size only when the kernel reports no/unknown remaining size. size semantics confirmed: base-asset QUANTITY; notional = size×price; margin = notional/ leverage ≈ 0.2×capital (already margin-bounded by construction — no extra clamp needed). Tests: test_pink_sizing_guards.py (4) green; full offline suite 25 green (no regression). Complements the kernel INVALID_INTENT guard (9168cf0): source refuses to produce bad sizes; kernel rejects any that slip through. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 14:18:14 +02:00
"""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