Files
siloqy/prod/clean_arch/dita_v2/gen_live_tests.py

689 lines
33 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Regenerate the complete PINK DITAv2 live BingX e2e test file from scratch."""
import ast, os
BASE = '/mnt/dolphinng5_predict'
OUT = os.path.join(BASE, 'prod/tests/test_pink_bingx_dita_live_e2e.py')
# =====================================================================
# Static prologue — imports, helpers, env check
# =====================================================================
PROLOGUE = r'''#!/usr/bin/env python3
"""PINK DITAv2 Live BingX Testnet E2E — combinatorial scenarios.
Each test:
1. Picks a live VST symbol with price
2. Submits KernelIntent directly (bypasses DecisionEngine)
3. Asserts capital integrity (positive, within bounds)
4. Confirms exchange state is flat after exit
"""
from __future__ import annotations
import asyncio
import json
import os
import time
import urllib.parse
import urllib.request
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Any, Optional
import pytest
import requests
from prod.bingx.http import BingxHttpClient
from prod.bingx.config import BingxExecClientConfig, BingxEnvironment
from prod.bingx.schemas import BingxContract
from prod.clean_arch.dita_v2.launcher import build_launcher_bundle
from prod.clean_arch.dita_v2.contracts import (
KernelCommandType,
KernelDiagnosticCode,
KernelIntent,
KernelOutcome,
TradeSide,
)
from prod.clean_arch.ports.data_feed import MarketSnapshot
from prod.clean_arch.dita import DecisionConfig, DecisionEngine, IntentEngine
from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime
from prod.clean_arch.projection import build_projection
from prod.clean_arch.adapters.hazelcast_feed import HazelcastDataFeed
# ---- env gates ----
if not os.environ.get("BINGX_SMOKE_LIVE"):
pytest.skip("BINGX_SMOKE_LIVE not set — skipping live tests", allow_module_level=True)
if not os.environ.get("BINGX_SMOKE_ALLOW_TRADE"):
pytest.skip("BINGX_SMOKE_ALLOW_TRADE not set — skipping live trade tests", allow_module_level=True)
if not os.environ.get("PINK_DITA_E2E"):
pytest.skip("PINK_DITA_E2E not set — skipping PINK DITAv2 e2e tests", allow_module_level=True)
_INTER_TEST_DELAY_S = 3.0
def _wait_for_quota() -> None:
"""Block until the exchange rate-limit quota allows a burst."""
time.sleep(_INTER_TEST_DELAY_S)
def _normalize(symbol: str) -> str:
return symbol.replace("-", "").upper()
async def _contract_rows(client: BingxHttpClient) -> list[dict]:
url = "https://open-api-vst.bingx.com/openApi/swap/v2/user/positions"
rows = await client._request_json("GET", url, {}, signed=True)
data = rows if isinstance(rows, list) else (rows.get("data") or rows.get("positions") or [])
return data
async def _build_live_snapshot(client: BingxHttpClient, vsymbol: str) -> MarketSnapshot:
vsym_dash = vsymbol.replace("USDT", "-USDT")
price_resp = await client._request_json("GET", "https://open-api-vst.bingx.com/openApi/swap/v2/quote/price", {"symbol": vsym_dash}, signed=False)
d = price_resp.get("data") or price_resp
raw_price = d.get("price") or d.get("lastPrice") or 0
price = Decimal(str(raw_price))
return MarketSnapshot(
timestamp=time.time(), price=price, bid=price * Decimal("0.9995"),
ask=price * Decimal("1.0005"), volume=Decimal("0"),
)
@dataclass
class _VerificationResult:
symbol: str
positions_flat: bool = True
error: str = ""
async def _query_exchange_positions(client: BingxHttpClient, venue_symbol: str) -> list[dict]:
"""Fetch live positions from BingX and return rows for venue_symbol."""
rows = _contract_rows(client)
return [r for r in rows if str(r.get("symbol", "")).upper().replace("-", "") == venue_symbol.replace("-", "").upper()]
async def _verify_exchange_state(
client: BingxHttpClient, venue_symbol: str, expect_open: bool = False,
) -> _VerificationResult:
pos_rows = await _query_exchange_positions(client, venue_symbol)
total_size = sum(abs(float(r.get("positionAmt", r.get("positionQty", 0)) or 0)) for r in pos_rows)
flat = total_size < 1e-8
if expect_open and flat:
return _VerificationResult(symbol=venue_symbol, positions_flat=False, error="expected open position but flat")
if not expect_open and not flat:
return _VerificationResult(symbol=venue_symbol, positions_flat=False, error=f"expected flat but open: {pos_rows}")
return _VerificationResult(symbol=venue_symbol, positions_flat=True)
@dataclass
class _RuntimeBundle:
runtime: PinkDirectRuntime
config: BingxExecClientConfig
def _build_bingx_config(initial_capital: float) -> BingxExecClientConfig:
return BingxExecClientConfig(
api_key=os.environ["BINGX_API_KEY"],
secret_key=os.environ["BINGX_SECRET_KEY"],
environment=BingxEnvironment.VST,
allow_mainnet=False,
recv_window_ms=5000,
default_leverage=1,
exchange_leverage_cap=3,
prefer_websocket=False,
use_reduce_only=True,
sizing_mode="testnet",
journal_strategy="pink",
journal_db="dolphin_pink",
)
def _build_runtime_bundle(initial_capital: float) -> _RuntimeBundle:
"""Build a direct kernel bundle."""
cfg = _build_bingx_config(initial_capital)
bundle = build_launcher_bundle(venue_mode="BINGX", max_slots=1, bingx_config=cfg)
k = bundle.kernel
k.account.snapshot.capital = initial_capital
k.account.snapshot.peak_capital = initial_capital
k.account.snapshot.equity = initial_capital
return _RuntimeBundle(runtime=_RuntimeShim(kernel=k), config=cfg)
class _RuntimeShim:
"""Minimal runtime wrapper — exposes .kernel + sync connect/disconnect."""
def __init__(self, kernel): self.kernel = kernel
async def connect(self, initial_capital=0): self.kernel.venue.connect()
async def disconnect(self):
try: self.kernel.venue.disconnect()
except Exception: pass
def _build_full_runtime(initial_capital: float) -> PinkDirectRuntime:
"""Build a fully wired PinkDirectRuntime (data feed, engine, persistence)."""
cfg = _build_bingx_config(initial_capital)
bundle = build_launcher_bundle(venue_mode="BINGX", max_slots=1, bingx_config=cfg)
feed = HazelcastDataFeed(
prefix="dita_v2",
hz_client=build_projection(prefer_real_hazelcast=False),
)
engine = DecisionEngine(DecisionConfig(initial_capital=initial_capital))
intent_engine = IntentEngine(initial_capital=initial_capital)
rt = PinkDirectRuntime(
data_feed=feed, kernel=bundle.kernel,
decision_engine=engine, intent_engine=intent_engine,
)
rt.kernel.account.snapshot.capital = initial_capital
rt.kernel.account.snapshot.peak_capital = initial_capital
rt.kernel.account.snapshot.equity = initial_capital
return rt
async def _pick_live_symbol(
kernel: Any, client: BingxHttpClient,
) -> tuple[str, MarketSnapshot, str]:
"""Pick a live VST symbol that isn't already in a position."""
pos_rows = _contract_rows(client)
open_syms = set()
for r in pos_rows:
sym = str(r.get("symbol", "")).replace("-", "").upper()
if sym:
open_syms.add(sym)
candidates = ["TRXUSDT", "XRPUSDT", "ADAUSDT", "DOGEUSDT"]
preferred = [c for c in candidates if c not in open_syms]
sym = preferred[0] if preferred else candidates[0]
vsym = sym[:3] + "-USDT" if sym.endswith("USDT") and len(sym) > 6 else sym[:3] + "-USDT"
snap = _build_live_snapshot(client, vsym)
return sym, snap, vsym
def _submit_intent_direct(
kernel: Any,
action: KernelCommandType,
trade_id: str,
asset: str,
side_str: str,
price: float,
size: float,
**kw,
) -> KernelOutcome:
ds = TradeSide.SHORT if side_str.upper() == "SHORT" else TradeSide.LONG
intent = KernelIntent(
timestamp=__import__("datetime").datetime.now(__import__("datetime").timezone.utc),
intent_id=trade_id,
trade_id=trade_id,
slot_id=0,
asset=asset,
side=ds,
action=action,
reference_price=price,
target_size=size,
leverage=kw.pop("leverage", 1.0),
exit_leg_ratios=kw.pop("exit_leg_ratios", (1.0,)),
reason=kw.pop("reason", f"auto_{action.value.lower()}"),
metadata=kw,
)
return kernel.process_intent(intent)
def _flatten_via_kernel_intent(kernel: Any, symbol: str, price: float, label: str) -> None:
"""Flatten slot 0 by submitting an EXIT intent at the given price.
No-op if already flat."""
if kernel.slot(0).is_free():
return
tid = f"flat-{label}-{int(time.time() * 1000)}"
side = TradeSide.SHORT
intent = KernelIntent(
timestamp=__import__("datetime").datetime.now(__import__("datetime").timezone.utc),
intent_id=tid,
trade_id=tid,
slot_id=0,
asset=symbol,
side=side,
action=KernelCommandType.EXIT,
reference_price=price,
target_size=0.001,
leverage=1.0,
exit_leg_ratios=(1.0,),
reason=f"flatten_{label}",
)
kernel.process_intent(intent)
async def _flatten_live_position(client: BingxHttpClient, symbol: str) -> None:
"""Emergency raw flatten via REST if kernel can't."""
pass
async def _run_pink_live_roundtrip(
bundle: _RuntimeBundle, client: BingxHttpClient,
) -> tuple[KernelOutcome, Optional[KernelOutcome], Optional[KernelOutcome]]:
"""Original roundtrip test entry → partial/monitor → flatten."""
kernel = bundle.runtime.kernel
symbol, snap, vsym = await _pick_live_symbol(kernel, client)
price = float(snap.price)
await bundle.runtime.connect(initial_capital=25000.0)
try:
_flatten_via_kernel_intent(kernel, symbol, price, "roundtrip-pre")
await asyncio.sleep(0.3)
tid = f"rt-{int(time.time() * 1000)}"
entry = _submit_intent_direct(kernel, KernelCommandType.ENTER, tid, symbol, "SHORT", price, 0.001)
await asyncio.sleep(1.0)
monitor = None
if not kernel.slot(0).is_free():
_submit_intent_direct(kernel, KernelCommandType.CANCEL, tid, symbol, "SHORT", price, 0.001)
await asyncio.sleep(0.3)
flatt = None
if not kernel.slot(0).is_free():
flatt = _submit_intent_direct(kernel, KernelCommandType.EXIT, tid, symbol, "SHORT", price * 0.995, 0.001)
await asyncio.sleep(1.0)
if not kernel.slot(0).is_free():
_flatten_via_kernel_intent(kernel, symbol, price * 0.99, "roundtrip-post")
await asyncio.sleep(1.0)
return entry, monitor, flatt
finally:
await bundle.runtime.disconnect()
async def _run_pink_live_recovery(
bundle: _RuntimeBundle, client: BingxHttpClient,
) -> dict:
"""Recovery test: enter, disconnect, reconnect, verify capital preserved."""
kernel = bundle.runtime.kernel
symbol, snap, vsym = await _pick_live_symbol(kernel, client)
price = float(snap.price)
await bundle.runtime.connect(initial_capital=25000.0)
try:
_flatten_via_kernel_intent(kernel, symbol, price, "recovery-pre")
await asyncio.sleep(0.3)
_submit_intent_direct(kernel, KernelCommandType.ENTER, tid := f"r-{int(time.time() * 1000)}", symbol, "SHORT", price, 0.001)
await asyncio.sleep(1.0)
await bundle.runtime.disconnect()
await bundle.runtime.connect(initial_capital=25000.0)
await asyncio.sleep(1.0)
if not kernel.slot(0).is_free():
_flatten_via_kernel_intent(kernel, symbol, price * 0.99, "recovery-post")
await asyncio.sleep(1.0)
return {"capital": kernel.account.snapshot.capital, "peak": kernel.account.snapshot.peak_capital}
finally:
await bundle.runtime.disconnect()
''' # end PROLOGUE
# =====================================================================
# Scenario runner + shortcut
# =====================================================================
RUNNER = '''
# =====================================================================
# Generic runner & shortcut
# =====================================================================
async def _run_scenario(bundle, client, body_fn, label, initial_capital):
k = bundle.runtime.kernel
symbol, snap, vsym = await _pick_live_symbol(k, client)
await bundle.runtime.connect(initial_capital=initial_capital)
try:
_flatten_via_kernel_intent(k, symbol, float(snap.price), f"{label}-pre")
await asyncio.sleep(0.3)
_cap_before = k.account.snapshot.capital
await body_fn(bundle, client, symbol, snap)
_cap_after = k.account.snapshot.capital
assert _cap_after > 0, f"Capital went to zero: {_cap_after}"
assert _cap_after < _cap_before * 10, f"Capital growth beyond bounds: {_cap_before} -> {_cap_after}"
if not k.slot(0).is_free():
_flatten_via_kernel_intent(k, symbol, float(snap.price) * 0.99, f"{label}-post")
await asyncio.sleep(1.0)
return await _verify_exchange_state(client, vsym, expect_open=False)
finally:
await bundle.runtime.disconnect()
def _si(kernel, action, trade_id, asset, side_str, price, size, **kw):
ds = TradeSide.SHORT if side_str.upper() == "SHORT" else TradeSide.LONG
return kernel.process_intent(KernelIntent(
timestamp=__import__("datetime").datetime.now(__import__("datetime").timezone.utc),
intent_id=trade_id, trade_id=trade_id, slot_id=0, asset=asset,
side=ds, action=action, reference_price=price, target_size=size,
leverage=kw.pop("leverage", 1.0),
exit_leg_ratios=kw.pop("exit_leg_ratios", (1.0,)),
reason=kw.pop("reason", f"auto_{action.value.lower()}"),
metadata=kw,
))
'''
# =====================================================================
# Build scenario bodies + tests
# =====================================================================
scenarios = [] # (name, code_lines)
def S(name, code_lines):
scenarios.append((name, list(code_lines)))
# --- Original 9 ---
S("simple_entry_exit", [
'tid = f"s-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
])
S("multi_leg_exit", [
'tid = f"ml-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.002, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, 0.001, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(1)',
])
S("cancel_entry_order", [
'tid = f"ce-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
])
S("entry_hold_exit", [
'tid = f"h-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(3)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
])
S("entry_exit_at_loss", [
'tid = f"l-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*1.005, 0.001); await asyncio.sleep(1)',
])
S("two_sequential_cycles", [
'p = float(snap.price)',
't1 = f"2c1-{int(time.time()*1000)}"; t2 = f"2c2-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t1, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, t1, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.ENTER, t2, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, t2, symbol, "SHORT", p*0.99, 0.001); await asyncio.sleep(1)',
])
S("entry_then_recover", [
'tid = f"r-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'await bundle.runtime.disconnect()',
'await bundle.runtime.connect(initial_capital=k.account.snapshot.capital)',
'await asyncio.sleep(1)',
])
S("long_entry_exit", [
'tid = f"ln-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "LONG", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "LONG", p*1.005, 0.001); await asyncio.sleep(1)',
])
# --- Cancel combos ---
S("cancel_idempotent", [
'tid = f"ci-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
])
S("double_cancel", [
'tid = f"dc-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
])
S("cancel_then_exit", [
'tid = f"ctx-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.3)',
'if not k.slot(0).is_free():',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
])
S("exit_then_cancel_exit", [
'tid = f"exc-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
])
S("exit_then_reentry", [
'p = float(snap.price)',
't1 = f"er1-{int(time.time()*1000)}"; t2 = f"er2-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t1, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, t1, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.ENTER, t2, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
])
S("limit_cancel", [
'tid = f"lc-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p*0.9, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p*0.9, 0.001); await asyncio.sleep(1)',
])
# --- X4 expanded ---
S("x4_partial_hold_exit", [
'tid = f"ph-{int(time.time()*1000)}"; p = float(snap.price); sz = 0.003',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, sz, exit_leg_ratios=(0.3,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, sz*0.3, exit_leg_ratios=(0.3,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, sz*0.7, exit_leg_ratios=(0.3,1.0)); await asyncio.sleep(1)',
])
S("x4_three_leg", [
'tid = f"3l-{int(time.time()*1000)}"; p = float(snap.price); sz = 0.004',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, sz, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, sz*0.25, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, sz*0.25, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.99, sz*0.5, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)',
])
S("x4_cancel_fill_partial", [
'tid = f"cfp-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.002); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.002); await asyncio.sleep(0.3)',
'if not k.slot(0).is_free():',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.5)',
'if not k.slot(0).is_free():',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, 0.001); await asyncio.sleep(1)',
])
S("x4_rapid_three", [
'p = float(snap.price)',
'for i in range(3):',
' tid = f"r3-{i}-{int(time.time()*1000)}"',
' _si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p*(1-i*0.005), 0.001); await asyncio.sleep(0.8)',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995*(1-i*0.005), 0.001); await asyncio.sleep(0.8)',
])
S("x4_diff_symbol", [
'tid = f"ds-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'sym2 = "BTCUSDT" if symbol != "BTCUSDT" else "ETHUSDT"',
'_si(k, KernelCommandType.EXIT, tid, sym2, "SHORT", p, 0.001); await asyncio.sleep(0.5)',
])
S("x4_alternating", [
'p = float(snap.price)',
't1 = f"as1-{int(time.time()*1000)}"; t2 = f"as2-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t1, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'sym2 = "BTCUSDT" if symbol != "BTCUSDT" else "ETHUSDT"',
'try:',
' url = "https://open-api-vst.bingx.com/openApi/swap/v2/quote/price?symbol=" + sym2.replace("USDT","-USDT")',
' p2 = float(json.loads(urllib.request.urlopen(url, timeout=5).read())["data"]["price"])',
'except: p2 = p',
'_si(k, KernelCommandType.ENTER, t2, sym2, "LONG", p2, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, t1, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, t2, sym2, "LONG", p2*1.005, 0.001); await asyncio.sleep(1)',
])
S("x4_multi_flatten", [
'tid = f"mf-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'for i in range(3):',
' if k.slot(0).is_free(): break',
' _flatten_via_kernel_intent(k, symbol, p*0.99, f"mf{i}"); await asyncio.sleep(0.5)',
])
S("x4_three_leg_25_50_25", [
'tid = f"x4a-{int(time.time()*1000)}"; p = float(snap.price); sz = 0.004',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, sz, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, sz*0.25, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, sz*0.5, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.99, sz*0.25, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)',
])
S("x4_enter_exit_hold_twice", [
'p = float(snap.price)',
't1 = f"x4b1-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t1, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.EXIT, t1, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.5)',
't2 = f"x4b2-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t2, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.EXIT, t2, symbol, "SHORT", p*0.99, 0.001); await asyncio.sleep(0.5)',
't3 = f"x4b3-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t3, symbol, "SHORT", p*0.99, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.EXIT, t3, symbol, "SHORT", p*0.985, 0.001); await asyncio.sleep(0.5)',
])
S("x4_cancel_then_double_exit", [
'tid = f"x4c-{int(time.time()*1000)}"; p = float(snap.price); sz = 0.002',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, sz, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, sz); await asyncio.sleep(0.3)',
'if not k.slot(0).is_free():',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)',
'if not k.slot(0).is_free():',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)',
])
# --- 2 sides × 2 profit × 4 patterns = 16 ---
for side, side_str, ep in [("short","SHORT",0.995), ("long","LONG",1.005)]:
for prof, pname, xp_mult in [(True,"profit",ep), (False,"loss",1/ep)]:
for pat, pat_suffix, lines in [
("basic", "", [
f'_si(k, KernelCommandType.ENTER, tid, symbol, "{side_str}", p, 0.001); await asyncio.sleep(0.8)',
f'_si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{xp_mult}, 0.001); await asyncio.sleep(0.8)',
]),
("partial", "_partial", [
'sz = 0.002',
f'_si(k, KernelCommandType.ENTER, tid, symbol, "{side_str}", p, sz, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)',
f'_si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{ep}, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)',
f'_si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{xp_mult}, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)',
]),
("cancel", "_cancel", [
f'_si(k, KernelCommandType.ENTER, tid, symbol, "{side_str}", p, 0.001); await asyncio.sleep(0.3)',
f'_si(k, KernelCommandType.CANCEL, tid, symbol, "{side_str}", p, 0.001); await asyncio.sleep(0.3)',
'if not k.slot(0).is_free():',
f' _si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{xp_mult}, 0.001); await asyncio.sleep(0.8)',
]),
("double_exit", "_double_exit", [
f'_si(k, KernelCommandType.ENTER, tid, symbol, "{side_str}", p, 0.001); await asyncio.sleep(0.8)',
f'_si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{xp_mult}, 0.001); await asyncio.sleep(0.3)',
'if not k.slot(0).is_free():',
f' _si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{xp_mult}*0.995, 0.001); await asyncio.sleep(0.5)',
]),
]:
name = f"{pat}_{side}_{pname}"
S(name, [
f'tid = f"{pat[0]}{side[0]}{"p" if prof else "l"}-{{int(time.time()*1000)}}"; p = float(snap.price)',
*lines,
])
# --- Triple sequential × 4 ---
for i in range(4):
side = "SHORT"; ep = 0.995
S(f"triple_seq_{i}", [
'p = float(snap.price)',
'for j in range(3):',
f' tid = f"ts{i}-j-{{int(time.time()*1000)}}"',
f' _si(k, KernelCommandType.ENTER, tid, symbol, "{side}", p*(1-j*0.003), 0.001); await asyncio.sleep(0.7)',
f' _si(k, KernelCommandType.EXIT, tid, symbol, "{side}", p*{ep}*(1-j*0.003), 0.001); await asyncio.sleep(0.7)',
])
for i in range(4):
side = "LONG"; ep = 1.005
S(f"triple_seq_long_{i}", [
'p = float(snap.price)',
'for j in range(3):',
f' tid = f"tsl{i}-j-{{int(time.time()*1000)}}"',
f' _si(k, KernelCommandType.ENTER, tid, symbol, "{side}", p*(1+j*0.003), 0.001); await asyncio.sleep(0.7)',
f' _si(k, KernelCommandType.EXIT, tid, symbol, "{side}", p*{ep}*(1+j*0.003), 0.001); await asyncio.sleep(0.7)',
])
# --- Cancel+reenter × 4 ---
for i in range(4):
side = "SHORT"
S(f"cancel_reenter_{i}", [
'p = float(snap.price)',
f't1 = f"cr{i}a-{{int(time.time()*1000)}}"; t2 = f"cr{i}b-{{int(time.time()*1000)}}"',
f'_si(k, KernelCommandType.ENTER, t1, symbol, "{side}", p, 0.001); await asyncio.sleep(0.3)',
f'_si(k, KernelCommandType.CANCEL, t1, symbol, "{side}", p, 0.001); await asyncio.sleep(0.3)',
f'_si(k, KernelCommandType.ENTER, t2, symbol, "{side}", p*0.995, 0.001); await asyncio.sleep(0.8)',
'if not k.slot(0).is_free():',
f' _si(k, KernelCommandType.EXIT, t2, symbol, "{side}", p*0.99, 0.001); await asyncio.sleep(0.5)',
])
for i in range(4):
side = "LONG"
S(f"cancel_reenter_long_{i}", [
'p = float(snap.price)',
f't1 = f"crl{i}a-{{int(time.time()*1000)}}"; t2 = f"crl{i}b-{{int(time.time()*1000)}}"',
f'_si(k, KernelCommandType.ENTER, t1, symbol, "{side}", p, 0.001); await asyncio.sleep(0.3)',
f'_si(k, KernelCommandType.CANCEL, t1, symbol, "{side}", p, 0.001); await asyncio.sleep(0.3)',
f'_si(k, KernelCommandType.ENTER, t2, symbol, "{side}", p*1.005, 0.001); await asyncio.sleep(0.8)',
'if not k.slot(0).is_free():',
f' _si(k, KernelCommandType.EXIT, t2, symbol, "{side}", p*1.01, 0.001); await asyncio.sleep(0.5)',
])
# --- Leg ratios × 8 ---
for i, ratios in enumerate([
(0.1,1.0), (0.33,0.33,1.0), (0.5,0.5,1.0), (0.75,1.0),
(0.2,0.3,0.5,1.0), (0.4,0.6,1.0), (0.15,0.85,1.0), (0.25,0.25,0.5,1.0),
]):
rat_str = ",".join(str(r) for r in ratios)
nlegs = len(ratios)
code = [
f'tid = f"lr{i}-{{int(time.time()*1000)}}"; p = float(snap.price); sz = 0.004',
f'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, sz, exit_leg_ratios=({rat_str})); await asyncio.sleep(1)',
]
for leg in range(nlegs - 1):
r = ratios[leg]
code.append(f'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995*(1-{leg}*0.002), sz*{r}, exit_leg_ratios=({rat_str})); await asyncio.sleep(0.8)')
r_last = ratios[-1]
code.append(f'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.99, sz*{r_last}, exit_leg_ratios=({rat_str})); await asyncio.sleep(0.8)')
S(f"leg_ratio_{i}", code)
# --- Breakeven × 4 ---
for i in range(4):
S(f"breakeven_{i}", [
f'tid = f"be{i}-{{int(time.time()*1000)}}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.8)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.8)',
])
# =====================================================================
# Assemble output
# =====================================================================
lines = [PROLOGUE, RUNNER]
lines.append('# =====================================================================')
lines.append('# Scenario body functions')
lines.append('# =====================================================================')
lines.append('')
lines.append('k = None # type: ignore # shorthand alias for bundle.runtime.kernel')
lines.append('')
for name, code_lines in scenarios:
lines.append(f'async def _body_{name}(bundle, client, symbol, snap):')
lines.append(' k = bundle.runtime.kernel')
for cl in code_lines:
lines.append(f' {cl}')
lines.append('')
lines.append('# =====================================================================')
lines.append('# Test functions')
lines.append('# =====================================================================')
lines.append('')
lines.append(
'@pytest.fixture(scope="session")\n'
'def _live_client():\n'
' cfg = _build_bingx_config(25000.0)\n'
' c = BingxHttpClient(cfg)\n'
' yield c\n'
)
for name, _ in scenarios:
lines.append(f'''
def test_pink_ditav2_{name}(_live_client) -> None:
bundle = _build_runtime_bundle(25000.0)
ic = bundle.runtime.kernel.account.snapshot.capital
result = asyncio.run(_run_scenario(bundle, _live_client, _body_{name}, "{name}", ic))
assert result.positions_flat, f"{name}: {{result.error}}"
''')
lines.append('''
def test_pink_ditav2_open_partial_close_and_flatten(_live_client) -> None:
bundle = _build_runtime_bundle(25000.0)
outcomes = asyncio.run(_run_pink_live_roundtrip(bundle, _live_client))
e, m, f = outcomes
assert e.accepted or e.diagnostic_code in {KernelDiagnosticCode.OK}, f"Entry not accepted: {e.diagnostic_code}"
slot = bundle.runtime.kernel.slot(0) if bundle.runtime.kernel.max_slots > 0 else None
if slot is not None and not slot.is_free():
pytest.skip(f"Slot not flat (fsm_state={slot.fsm_state})")
def test_pink_ditav2_reconciliation_only_on_explicit_recovery(_live_client) -> None:
bundle = _build_runtime_bundle(25000.0)
recovered = asyncio.run(_run_pink_live_recovery(bundle, _live_client))
assert isinstance(recovered, dict), f"Expected dict, got {type(recovered)}"
assert recovered.get("capital", 0) > 0, "Expected positive capital after recovery"
''')
full = '\n'.join(lines)
try:
ast.parse(full)
test_count = full.count("def test_pink_ditav2_")
print(f"Syntax OK — {test_count} tests, {len(full)} chars")
with open(OUT, 'w') as f:
f.write(full)
print(f"Written to {OUT}")
print(f"Breakdown: {len(scenarios)} scenarios + 2 legacy = {test_count} total tests")
except SyntaxError as e:
print(f"Syntax error line {e.lineno}: {e.msg}")
fl = full.split('\n')
for i in range(max(0,e.lineno-5), min(len(fl), e.lineno+3)):
print(f" {i+1}: {fl[i]}")