#!/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]}")