689 lines
33 KiB
Python
689 lines
33 KiB
Python
|
|
#!/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]}")
|