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,438 +0,0 @@
#!/usr/bin/env python3
"""Write the complete 68-test live e2e file. Bodies receive (k, symbol, p) where p is a float."""
import ast, os
SCENARIOS = [] # (name, code_lines)
def S(name, lines):
SCENARIOS.append((name, lines))
# ---- Original 9 ----
S("simple_entry_exit", [
"tid = f's-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
])
S("multi_leg_exit", [
"tid = f'ml-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.002, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(1)",
"_si(k, E.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)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
])
S("entry_hold_exit", [
"tid = f'h-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(3)",
"_si(k, E.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)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*1.005, 0.001); await asyncio.sleep(1)",
])
S("two_sequential_cycles", [
"t1 = f'2c1-{int(time.time()*1000)}'; t2 = f'2c2-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, t1, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
"_si(k, E.ENTER, t2, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, t2, symbol, 'SHORT', p*0.99, 0.001); await asyncio.sleep(1)",
])
S("entry_then_recover", [
"tid = f'r-{int(time.time()*1000)}'",
"_si(k, E.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)}'",
"_si(k, E.ENTER, tid, symbol, 'LONG', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.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)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
])
S("double_cancel", [
"tid = f'dc-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
])
S("cancel_then_exit", [
"tid = f'ctx-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"if not k.slot(0).is_free():",
" _si(k, E.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)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
])
S("exit_then_reentry", [
"t1 = f'er1-{int(time.time()*1000)}'; t2 = f'er2-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, t1, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.ENTER, t2, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
])
S("limit_cancel", [
"tid = f'lc-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p*0.9, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p*0.9, 0.001); await asyncio.sleep(1)",
])
# ---- X4 ----
S("x4_partial_hold_exit", [
"tid = f'ph-{int(time.time()*1000)}'; sz = 0.003",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, sz, exit_leg_ratios=(0.3,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, sz*0.3, exit_leg_ratios=(0.3,1.0)); await asyncio.sleep(1)",
"_si(k, E.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)}'; sz = 0.004",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, sz, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)",
"_si(k, E.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, E.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, E.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)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.002); await asyncio.sleep(0.5)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.002); await asyncio.sleep(0.3)",
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.993, 0.001); await asyncio.sleep(1)",
])
S("x4_rapid_three", [
"for i in range(3):",
" tid = f'r3-{i}-{int(time.time()*1000)}'",
" _si(k, E.ENTER, tid, symbol, 'SHORT', p*(1-i*0.005), 0.001); await asyncio.sleep(0.8)",
" _si(k, E.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)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"sym2 = 'BTCUSDT' if symbol != 'BTCUSDT' else 'ETHUSDT'",
"_si(k, E.EXIT, tid, sym2, 'SHORT', p, 0.001); await asyncio.sleep(0.5)",
])
S("x4_alternating", [
"t1 = f'as1-{int(time.time()*1000)}'; t2 = f'as2-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"sym2 = 'BTCUSDT' if symbol != 'BTCUSDT' else 'ETHUSDT'",
"try:",
" p2 = float(json.loads(urllib.request.urlopen('https://open-api-vst.bingx.com/openApi/swap/v2/quote/price?symbol='+sym2.replace('USDT','-USDT'), timeout=5).read())['data']['price'])",
"except: p2 = p",
"_si(k, E.ENTER, t2, sym2, 'LONG', p2, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, t1, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, t2, sym2, 'LONG', p2*1.005, 0.001); await asyncio.sleep(1)",
])
S("x4_multi_flatten", [
"tid = f'mf-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"for i in range(3):",
" if k.slot(0).is_free(): break",
" _flatten(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)}'; sz = 0.004",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, sz, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)",
"_si(k, E.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, E.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, E.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", [
"t1 = f'x4b1-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.EXIT, t1, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
"t2 = f'x4b2-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t2, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.EXIT, t2, symbol, 'SHORT', p*0.99, 0.001); await asyncio.sleep(0.5)",
"t3 = f'x4b3-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t3, symbol, 'SHORT', p*0.99, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.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)}'; sz = 0.002",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, sz, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, sz); await asyncio.sleep(0.3)",
"if not k.slot(0).is_free():",
" _si(k, E.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, E.EXIT, tid, symbol, 'SHORT', p*0.993, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)",
])
# ---- 2 sides x 2 profit x 4 patterns = 16 doubled ----
for side, side_str, ep in [("short","SHORT",0.995), ("long","LONG",1.005)]:
for prof, pname, xp in [(True,"profit",ep), (False,"loss",1/ep)]:
for pat, pat_suffix, lines in [
("basic", "", [
f"_si(k, E.ENTER, tid, symbol, '{side_str}', p, 0.001); await asyncio.sleep(0.8)",
f"_si(k, E.EXIT, tid, symbol, '{side_str}', p*{xp}, 0.001); await asyncio.sleep(0.8)",
]),
("partial", "_partial", [
"sz = 0.002",
f"_si(k, E.ENTER, tid, symbol, '{side_str}', p, sz, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)",
f"_si(k, E.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, E.EXIT, tid, symbol, '{side_str}', p*{xp}, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)",
]),
("cancel", "_cancel", [
f"_si(k, E.ENTER, tid, symbol, '{side_str}', p, 0.001); await asyncio.sleep(0.3)",
f"_si(k, E.CANCEL, tid, symbol, '{side_str}', p, 0.001); await asyncio.sleep(0.3)",
"if not k.slot(0).is_free():",
f" _si(k, E.EXIT, tid, symbol, '{side_str}', p*{xp}, 0.001); await asyncio.sleep(0.8)",
]),
("double_exit", "_double_exit", [
f"_si(k, E.ENTER, tid, symbol, '{side_str}', p, 0.001); await asyncio.sleep(0.8)",
f"_si(k, E.EXIT, tid, symbol, '{side_str}', p*{xp}, 0.001); await asyncio.sleep(0.3)",
"if not k.slot(0).is_free():",
f" _si(k, E.EXIT, tid, symbol, '{side_str}', p*{xp}*0.995, 0.001); await asyncio.sleep(0.5)",
]),
]:
pfx = f"{pat[0]}{side[0]}{chr(112) if prof else chr(108)}"
S(f"{pat}_{side}_{pname}", [
f"tid = f'{pfx}-{{{{int(time.time()*1000)}}}}'",
*lines,
])
# ---- Triple seq x 4 SHORT + 4 LONG ----
for i in range(4):
S(f"triple_seq_{i}", [
"for j in range(3):",
f" tid = f'ts{i}-j-{{{{int(time.time()*1000)}}}}'",
" _si(k, E.ENTER, tid, symbol, 'SHORT', p*(1-j*0.003), 0.001); await asyncio.sleep(0.7)",
" _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995*(1-j*0.003), 0.001); await asyncio.sleep(0.7)",
])
for i in range(4):
S(f"triple_seq_long_{i}", [
"for j in range(3):",
f" tid = f'tsl{i}-j-{{{{int(time.time()*1000)}}}}'",
" _si(k, E.ENTER, tid, symbol, 'LONG', p*(1+j*0.003), 0.001); await asyncio.sleep(0.7)",
" _si(k, E.EXIT, tid, symbol, 'LONG', p*1.005*(1+j*0.003), 0.001); await asyncio.sleep(0.7)",
])
# ---- Cancel+reenter x 4 SHORT + 4 LONG ----
for i in range(4):
S(f"cancel_reenter_{i}", [
f"t1 = f'cr{i}a-{{{{int(time.time()*1000)}}}}'; t2 = f'cr{i}b-{{{{int(time.time()*1000)}}}}'",
"_si(k, E.ENTER, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.ENTER, t2, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.8)",
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, t2, symbol, 'SHORT', p*0.99, 0.001); await asyncio.sleep(0.5)",
])
for i in range(4):
S(f"cancel_reenter_long_{i}", [
f"t1 = f'crl{i}a-{{{{int(time.time()*1000)}}}}'; t2 = f'crl{i}b-{{{{int(time.time()*1000)}}}}'",
"_si(k, E.ENTER, t1, symbol, 'LONG', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, t1, symbol, 'LONG', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.ENTER, t2, symbol, 'LONG', p*1.005, 0.001); await asyncio.sleep(0.8)",
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, t2, symbol, 'LONG', p*1.01, 0.001); await asyncio.sleep(0.5)",
])
# ---- Leg ratios x 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)
code = [f"tid = f'lr{i}-{{{{int(time.time()*1000)}}}}'; sz = 0.004",
f"_si(k, E.ENTER, tid, symbol, 'SHORT', p, sz, exit_leg_ratios=({rat_str})); await asyncio.sleep(1)"]
for leg in range(len(ratios) - 1):
r = ratios[leg]
code.append(f"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995*(1-{leg}*0.002), sz*{r}, exit_leg_ratios=({rat_str})); await asyncio.sleep(0.8)")
code.append(f"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.99, sz*{ratios[-1]}, exit_leg_ratios=({rat_str})); await asyncio.sleep(0.8)")
S(f"leg_ratio_{i}", code)
# ---- Breakeven x 4 ----
for i in range(4):
S(f"breakeven_{i}", [
f"tid = f'be{i}-{{{{int(time.time()*1000)}}}}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.8)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.8)",
])
# =====================================================================
# Assemble
# =====================================================================
HEADER = '''#!/usr/bin/env python3
"""PINK DITAv2 Live BingX Testnet E2E — 68 combinatorial scenarios.
Kernel-direct tests: bodies receive (k, symbol, p). Capital integrity
asserted. Exchange state confirmed flat.
"""
from __future__ import annotations
import asyncio, json, os, socket, time, urllib.request
import urllib.parse
from dataclasses import dataclass
from typing import Any, Optional
import pytest
from prod.bingx.http import BingxHttpClient
from prod.bingx.config import BingxExecClientConfig, BingxEnvironment
from prod.clean_arch.dita_v2.launcher import build_launcher_bundle
from prod.clean_arch.dita_v2.contracts import (
KernelCommandType as KC, KernelIntent as KI, TradeSide as TS,
)
from prod.clean_arch.ports.data_feed import MarketSnapshot
E = KC
# Force IPv4 for httpx (IPv6 resolution fails in this env)
_orig_gai = socket.getaddrinfo
def _ipv4_gai(host, port, family=0, type=0, proto=0, flags=0):
return _orig_gai(host, port, socket.AF_INET, type, proto, flags)
socket.getaddrinfo = _ipv4_gai
# ---- env gates ----
if not os.environ.get("BINGX_SMOKE_LIVE"):
pytest.skip("BINGX_SMOKE_LIVE not set", allow_module_level=True)
if not os.environ.get("BINGX_SMOKE_ALLOW_TRADE"):
pytest.skip("BINGX_SMOKE_ALLOW_TRADE not set", allow_module_level=True)
if not os.environ.get("PINK_DITA_E2E"):
pytest.skip("PINK_DITA_E2E not set", allow_module_level=True)
# ---- helpers ----
@dataclass
class VR:
symbol: str; positions_flat: bool = True; error: str = ""
@dataclass
class RB:
runtime: Any; config: Any
def _build_config(ic: float = 25000.0) -> 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_rb(ic: float = 25000.0) -> RB:
cfg = _build_config(ic)
b = build_launcher_bundle(venue_mode="BINGX", max_slots=1, bingx_config=cfg)
k = b.kernel; k.account.snapshot.capital = ic; k.account.snapshot.peak_capital = ic; k.account.snapshot.equity = ic
class Shim:
def __init__(self, k): self.kernel = k
async def connect(self, initial_capital=0): self.kernel.venue.connect()
async def disconnect(self):
try: self.kernel.venue.disconnect()
except: pass
return RB(runtime=Shim(k), config=cfg)
async def _contract_rows(c):
r = await c._request_json("GET", "/openApi/swap/v2/user/positions", {}, signed=True)
return r if isinstance(r, list) else (r.get("data") or r.get("positions") or [])
async def _pick_sym(k, c):
rs = await _contract_rows(c)
oss = {str(r.get("symbol","")).replace("-","").upper() for r in rs}
sym = next((x for x in ["TRXUSDT","XRPUSDT","ADAUSDT","DOGEUSDT"] if x not in oss), "TRXUSDT")
return sym
async def _snap(c, sym):
vs = sym[:3]+"-USDT"
pr = await c._request_json("GET", "/openApi/swap/v2/quote/price", {"symbol": vs}, signed=False)
d = pr.get("data") or pr; rp = float(d.get("price") or d.get("lastPrice") or 0)
return MarketSnapshot(timestamp=__import__("datetime").datetime.now(__import__("datetime").timezone.utc),
symbol=sym, price=rp, bid=rp*0.9995, ask=rp*1.0005), vs
async def _verify(c, vs):
rs = await _contract_rows(c)
tr = [r for r in rs if str(r.get("symbol","")).upper().replace("-","") == vs.replace("-","").upper()]
ts = sum(abs(float(r.get("positionAmt",r.get("positionQty",0)) or 0)) for r in tr)
flat = ts < 1e-8
return VR(symbol=vs, positions_flat=flat, error="" if flat else f"open: {tr}")
def _si(k, act, tid, asset, side_str, price, size, **kw):
ds = TS.SHORT if side_str.upper() == "SHORT" else TS.LONG
return k.process_intent(KI(
timestamp=__import__("datetime").datetime.now(__import__("datetime").timezone.utc),
intent_id=tid, trade_id=tid, slot_id=0, asset=asset, side=ds, action=act,
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_{act.value.lower()}"), metadata=kw))
def _flatten(k, sym, price, label):
if k.slot(0).is_free(): return
_si(k, E.EXIT, f"fl{label}-{int(time.time()*1000)}", sym, "SHORT", price, 0.001)
async def _run(bundle, client, body_fn, label, ic):
k = bundle.runtime.kernel
sym = await _pick_sym(k, client)
snap, vsym = await _snap(client, sym)
await bundle.runtime.connect(initial_capital=ic)
p = float(snap.price)
try:
_flatten(k, sym, p, f"{label}-pre")
await asyncio.sleep(0.3)
cb = k.account.snapshot.capital
await body_fn(k, sym, p)
ca = k.account.snapshot.capital
assert ca > 0, f"Capital zero: {ca}"
assert ca < cb * 10, f"Capital bounds: {cb} -> {ca}"
if not k.slot(0).is_free():
_flatten(k, sym, p*0.99, f"{label}-post")
await asyncio.sleep(1.0)
return await _verify(client, vsym)
finally:
await bundle.runtime.disconnect()
'''
lines = [HEADER]
# Scenario bodies
lines.append("\n# =====================================================================\n# Scenario bodies\n# =====================================================================\n")
for name, code_lines in SCENARIOS:
lines.append(f"async def _body_{name}(k, symbol, p):")
for cl in code_lines:
lines.append(f" {cl}")
lines.append("")
# Test functions
lines.append("\n# =====================================================================\n# Test functions\n# =====================================================================\n")
lines.append('''@pytest.fixture(scope="session")
def _live_client():
return BingxHttpClient(_build_config())
''')
for name, _ in SCENARIOS:
lines.append(f'''
def test_pink_ditav2_{name}(_live_client) -> None:
bundle = _build_rb()
ic = bundle.runtime.kernel.account.snapshot.capital
r = asyncio.run(_run(bundle, _live_client, _body_{name}, "{name}", ic))
assert r.positions_flat, name + ": " + r.error
''')
full = '\n'.join(lines)
try:
ast.parse(full)
count = full.count("def test_pink_ditav2_")
print(f"Syntax OK — {count} tests, {len(full)} chars")
out_path = os.path.join('/mnt/dolphinng5_predict', 'prod/tests/test_pink_bingx_dita_live_e2e.py')
with open(out_path, 'w') as f:
f.write(full)
print(f"Written OK ({count} tests)")
except SyntaxError as e:
print(f"Syntax error L{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]}")