PINK: flat_and_start_pink.py — flatten BingX VST + async startup check
CLI: python flat_and_start_pink.py [--flatten] [--no-start]
--flatten : cancel all orders + MARKET-close all positions (correct side
by positionAmt sign, not abs()); verifies account flat after
--no-start: flatten only, skip roundtrip
(no flags): startup roundtrip only — ENTER/EXIT via process_intent_async()
Startup roundtrip exercises the full N2/N3/N4 async hot path:
process_intent_async → submit_async → await backend.submit_intent →
BingX POST → on_venue_event(ORDER_ACK+FULL_FILL) → POSITION_OPEN → CLOSED
Min-order detection: queries /quote/contracts for tradeMinQty/minOrderQuantity;
fallback 10 units. Fixes the 0.001-TRX rejection that BingX returned.
Bugs fixed in flatten:
- positionAmt sign was lost via abs(); SHORT positions now correctly use BUY
(positionAmt < 0) vs SELL for LONG (positionAmt > 0) with reduceOnly=true
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
361
prod/clean_arch/dita_v2/flat_and_start_pink.py
Normal file
361
prod/clean_arch/dita_v2/flat_and_start_pink.py
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Optionally flatten BingX VST, then run a PINK roundtrip startup check.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# Start PINK only (no flatten):
|
||||||
|
PYTHONPATH=/mnt/dolphinng5_predict python flat_and_start_pink.py
|
||||||
|
|
||||||
|
# Flatten first, then start PINK:
|
||||||
|
PYTHONPATH=/mnt/dolphinng5_predict python flat_and_start_pink.py --flatten
|
||||||
|
|
||||||
|
# Flatten only (no PINK startup):
|
||||||
|
PYTHONPATH=/mnt/dolphinng5_predict python flat_and_start_pink.py --flatten --no-start
|
||||||
|
|
||||||
|
Steps (with --flatten):
|
||||||
|
1. Load creds from .env
|
||||||
|
2. Fetch open positions + orders
|
||||||
|
3. Cancel all open orders per symbol
|
||||||
|
4. Close all open positions (MARKET reduceOnly)
|
||||||
|
5. Verify account is flat
|
||||||
|
|
||||||
|
Steps (startup, always):
|
||||||
|
6. Run simple_entry_exit roundtrip via PINK kernel to confirm async path works
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ── path / env ────────────────────────────────────────────────────────────────
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv(ROOT / ".env")
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s %(levelname)-7s %(name)s — %(message)s",
|
||||||
|
datefmt="%H:%M:%S",
|
||||||
|
)
|
||||||
|
log = logging.getLogger("flat_pink")
|
||||||
|
|
||||||
|
# ── imports after path setup ──────────────────────────────────────────────────
|
||||||
|
from prod.bingx.config import BingxExecClientConfig
|
||||||
|
from prod.bingx.enums import BingxEnvironment
|
||||||
|
from prod.bingx.http import BingxHttpClient
|
||||||
|
from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter
|
||||||
|
from prod.clean_arch.dita_v2.contracts import (
|
||||||
|
KernelCommandType as E,
|
||||||
|
KernelIntent as KI,
|
||||||
|
TradeSide as TS,
|
||||||
|
)
|
||||||
|
from prod.clean_arch.dita_v2.launcher import build_launcher_bundle
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
# ── config ────────────────────────────────────────────────────────────────────
|
||||||
|
def _cfg() -> 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",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── step 1: flatten BingX ─────────────────────────────────────────────────────
|
||||||
|
async def flatten_exchange(client: BingxHttpClient, adapter: BingxDirectExecutionAdapter) -> bool:
|
||||||
|
"""Cancel all orders and close all positions. Returns True if account is flat."""
|
||||||
|
log.info("── FLATTEN: fetching open positions + orders ──────────────────")
|
||||||
|
snap = await adapter.refresh_state(None, include_history=False)
|
||||||
|
|
||||||
|
positions = snap.open_positions # {symbol: row_dict}
|
||||||
|
open_orders = snap.open_orders # [row_dict]
|
||||||
|
capital = snap.capital
|
||||||
|
|
||||||
|
log.info(" capital=%.2f open_positions=%d open_orders=%d",
|
||||||
|
capital, len(positions), len(open_orders))
|
||||||
|
|
||||||
|
if not positions and not open_orders:
|
||||||
|
log.info(" Account already flat — nothing to do.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Cancel all open orders (per symbol)
|
||||||
|
cancelled_symbols: set[str] = set()
|
||||||
|
for order in open_orders:
|
||||||
|
sym = str(order.get("symbol") or order.get("symbolName") or "")
|
||||||
|
if not sym or sym in cancelled_symbols:
|
||||||
|
continue
|
||||||
|
log.info(" Cancelling all orders for %s …", sym)
|
||||||
|
try:
|
||||||
|
await client.signed_delete(
|
||||||
|
"/openApi/swap/v2/trade/allOpenOrders",
|
||||||
|
{"symbol": sym},
|
||||||
|
)
|
||||||
|
cancelled_symbols.add(sym)
|
||||||
|
log.info(" ✓ Orders cancelled for %s", sym)
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning(" ✗ Cancel orders failed for %s: %s", sym, exc)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# Close all open positions
|
||||||
|
for sym_key, pos in positions.items():
|
||||||
|
# positionAmt: negative = SHORT (one-way mode), positive = LONG
|
||||||
|
pos_amt = float(pos.get("positionAmt") or pos.get("positionQty") or pos.get("positionSize") or 0.0)
|
||||||
|
if abs(pos_amt) < 1e-9:
|
||||||
|
continue
|
||||||
|
venue_sym = str(pos.get("symbol") or pos.get("symbolName") or sym_key)
|
||||||
|
pos_side = str(pos.get("positionSide") or "BOTH").upper()
|
||||||
|
qty = abs(pos_amt)
|
||||||
|
|
||||||
|
# BingX one-way mode: positionAmt < 0 → SHORT → close with BUY
|
||||||
|
# positionAmt > 0 → LONG → close with SELL
|
||||||
|
if pos_amt < 0 or pos_side == "SHORT":
|
||||||
|
close_side = "BUY"
|
||||||
|
else:
|
||||||
|
close_side = "SELL"
|
||||||
|
|
||||||
|
log.info(" Closing %s qty=%.6f via MARKET %s reduceOnly …", venue_sym, abs(qty), close_side)
|
||||||
|
try:
|
||||||
|
resp = await client.signed_post(
|
||||||
|
"/openApi/swap/v2/trade/order",
|
||||||
|
{
|
||||||
|
"symbol": venue_sym,
|
||||||
|
"side": close_side,
|
||||||
|
"positionSide": "BOTH",
|
||||||
|
"type": "MARKET",
|
||||||
|
"quantity": str(abs(qty)),
|
||||||
|
"reduceOnly": "true",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
log.info(" ✓ Close submitted: %s", resp)
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning(" ✗ Close failed for %s: %s", venue_sym, exc)
|
||||||
|
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
# Verify flat
|
||||||
|
snap2 = await adapter.refresh_state(None, include_history=False)
|
||||||
|
remaining = {s: r for s, r in snap2.open_positions.items()
|
||||||
|
if abs(float(r.get("positionAmt") or r.get("positionQty") or 0)) > 1e-8}
|
||||||
|
remaining_orders = snap2.open_orders
|
||||||
|
|
||||||
|
if remaining:
|
||||||
|
log.error(" ✗ STILL OPEN positions after flatten: %s", list(remaining.keys()))
|
||||||
|
return False
|
||||||
|
if remaining_orders:
|
||||||
|
log.warning(" ⚠ Still %d open orders after cancel — continuing anyway", len(remaining_orders))
|
||||||
|
|
||||||
|
log.info(" ✓ Account FLAT. capital=%.2f", snap2.capital)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ── step 2: PINK startup roundtrip ────────────────────────────────────────────
|
||||||
|
async def pink_startup_roundtrip(adapter: BingxDirectExecutionAdapter, capital: float) -> bool:
|
||||||
|
"""Connect kernel to BingX VST, run simple SHORT roundtrip, verify flat."""
|
||||||
|
log.info("")
|
||||||
|
log.info("── PINK STARTUP: building kernel bundle ──────────────────────")
|
||||||
|
|
||||||
|
from prod.clean_arch.dita_v2.launcher import build_launcher_bundle, build_bingx_exec_client_config
|
||||||
|
|
||||||
|
cfg = _cfg()
|
||||||
|
bundle = build_launcher_bundle(venue_mode="BINGX", max_slots=1, bingx_config=cfg)
|
||||||
|
k = bundle.kernel
|
||||||
|
k.account.snapshot.capital = capital
|
||||||
|
k.account.snapshot.peak_capital = capital
|
||||||
|
k.account.snapshot.equity = capital
|
||||||
|
|
||||||
|
log.info(" Kernel built. max_slots=%d capital=%.2f", k.max_slots, capital)
|
||||||
|
|
||||||
|
# Connect venue (sync call via _run)
|
||||||
|
try:
|
||||||
|
k.venue.connect()
|
||||||
|
log.info(" Venue connected.")
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning(" Venue connect warning (may be ok): %s", exc)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# Pick a live symbol
|
||||||
|
client = adapter._client
|
||||||
|
try:
|
||||||
|
snap = await client.signed_get("/openApi/swap/v2/user/positions")
|
||||||
|
# Just get first available contract from contract list
|
||||||
|
contracts = await client.public_get("/openApi/swap/v2/quote/contracts")
|
||||||
|
rows = []
|
||||||
|
if isinstance(contracts, dict):
|
||||||
|
rows = contracts.get("data") or contracts.get("contracts") or []
|
||||||
|
elif isinstance(contracts, list):
|
||||||
|
rows = contracts
|
||||||
|
# Pick TRXUSDT-PERP as default (small size, low price)
|
||||||
|
symbol = "TRX-USDT"
|
||||||
|
vsymbol = "TRX-USDT"
|
||||||
|
price = 0.085 # fallback price
|
||||||
|
for row in rows:
|
||||||
|
sym = str(row.get("symbol") or row.get("contractId") or "")
|
||||||
|
if "TRX" in sym.upper() and "USDT" in sym.upper():
|
||||||
|
vsymbol = sym
|
||||||
|
break
|
||||||
|
# Get live price
|
||||||
|
try:
|
||||||
|
ticker = await client.public_get("/openApi/swap/v2/quote/price", {"symbol": vsymbol})
|
||||||
|
if isinstance(ticker, dict):
|
||||||
|
price_raw = ticker.get("price") or (ticker.get("data") or {}).get("price") or price
|
||||||
|
price = float(price_raw)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
log.info(" Symbol: %s price=%.6f", vsymbol, price)
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning(" Could not pick live symbol: %s — using TRX-USDT @ 0.085", exc)
|
||||||
|
vsymbol = "TRX-USDT"
|
||||||
|
price = 0.085
|
||||||
|
|
||||||
|
asset = vsymbol.replace("-PERP", "").replace("-SWAP", "")
|
||||||
|
|
||||||
|
# Derive minimum viable size from contract info (fallback 10.0)
|
||||||
|
size = 10.0
|
||||||
|
try:
|
||||||
|
for row in rows:
|
||||||
|
sym_r = str(row.get("symbol") or row.get("contractId") or "")
|
||||||
|
if sym_r == vsymbol:
|
||||||
|
min_qty = float(
|
||||||
|
row.get("tradeMinQty") or row.get("minOrderQuantity") or
|
||||||
|
row.get("minQty") or row.get("minOrderQty") or 0.0
|
||||||
|
)
|
||||||
|
size = max(min_qty, 1.0) if min_qty > 0 else 10.0
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log.info("")
|
||||||
|
log.info("── PINK STARTUP: simple SHORT roundtrip ──────────────────────")
|
||||||
|
|
||||||
|
tid = f"startup-{int(time.time() * 1000)}"
|
||||||
|
|
||||||
|
# Patch submit_async to log events before returning
|
||||||
|
log.info(" ENTER SHORT: asset=%s size=%s price=%.6f", asset, size, price)
|
||||||
|
t0 = time.time()
|
||||||
|
enter_outcome = await k.process_intent_async(KI(
|
||||||
|
timestamp=datetime.now(timezone.utc),
|
||||||
|
intent_id=tid, trade_id=tid, slot_id=0,
|
||||||
|
asset=asset, side=TS.SHORT, action=E.ENTER,
|
||||||
|
reference_price=price, target_size=size, leverage=1.0,
|
||||||
|
exit_leg_ratios=(1.0,), reason="startup_check", metadata={},
|
||||||
|
))
|
||||||
|
dt_enter = time.time() - t0
|
||||||
|
log.info(" ENTER result: accepted=%s state=%s diag=%s (%.2fs)",
|
||||||
|
enter_outcome.accepted, enter_outcome.state, enter_outcome.diagnostic_code, dt_enter)
|
||||||
|
|
||||||
|
if not enter_outcome.accepted:
|
||||||
|
log.error(" ✗ ENTER rejected — PINK startup FAILED")
|
||||||
|
bundle.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.info(" Slot after ENTER: %s size=%s", k.slot(0).fsm_state, k.slot(0).size)
|
||||||
|
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
|
||||||
|
log.info(" EXIT SHORT: price=%.6f (0.5%% below entry)", price * 0.995)
|
||||||
|
t0 = time.time()
|
||||||
|
exit_outcome = await k.process_intent_async(KI(
|
||||||
|
timestamp=datetime.now(timezone.utc),
|
||||||
|
intent_id=tid, trade_id=tid, slot_id=0,
|
||||||
|
asset=asset, side=TS.SHORT, action=E.EXIT,
|
||||||
|
reference_price=price * 0.995, target_size=size, leverage=1.0,
|
||||||
|
exit_leg_ratios=(1.0,), reason="startup_check_exit", metadata={},
|
||||||
|
))
|
||||||
|
dt_exit = time.time() - t0
|
||||||
|
log.info(" EXIT result: accepted=%s state=%s diag=%s (%.2fs)",
|
||||||
|
exit_outcome.accepted, exit_outcome.state, exit_outcome.diagnostic_code, dt_exit)
|
||||||
|
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
|
||||||
|
# Final account state
|
||||||
|
acc = k.snapshot()["account"]
|
||||||
|
slot = k.slot(0).to_dict()
|
||||||
|
log.info(" Slot final: %s closed=%s realized_pnl=%.6f",
|
||||||
|
slot.get("fsm_state"), slot.get("closed"), slot.get("realized_pnl", 0))
|
||||||
|
log.info(" Account: capital=%.4f realized_pnl=%.6f",
|
||||||
|
acc.get("capital", 0), acc.get("realized_pnl", 0))
|
||||||
|
|
||||||
|
# Verify exchange flat
|
||||||
|
snap_final = await adapter.refresh_state(None, include_history=False)
|
||||||
|
open_pos = {s: r for s, r in snap_final.open_positions.items()
|
||||||
|
if abs(float(r.get("positionAmt") or r.get("positionQty") or 0)) > 1e-8}
|
||||||
|
if open_pos:
|
||||||
|
log.warning(" ⚠ Exchange still shows open positions: %s", list(open_pos.keys()))
|
||||||
|
else:
|
||||||
|
log.info(" ✓ Exchange FLAT after exit")
|
||||||
|
|
||||||
|
bundle.close()
|
||||||
|
|
||||||
|
success = exit_outcome.accepted and not open_pos
|
||||||
|
if success:
|
||||||
|
log.info("")
|
||||||
|
log.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||||
|
log.info(" ✓ PINK STARTUP OK — async path functional")
|
||||||
|
log.info(" ENTER latency: %.2fs EXIT latency: %.2fs", dt_enter, dt_exit)
|
||||||
|
log.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||||
|
else:
|
||||||
|
log.error(" ✗ PINK STARTUP FAILED")
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
|
# ── main ──────────────────────────────────────────────────────────────────────
|
||||||
|
async def main(do_flatten: bool, do_start: bool) -> None:
|
||||||
|
cfg = _cfg()
|
||||||
|
adapter = BingxDirectExecutionAdapter(cfg)
|
||||||
|
client = adapter._client
|
||||||
|
capital = 25000.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
if do_flatten:
|
||||||
|
flat = await flatten_exchange(client, adapter)
|
||||||
|
if not flat:
|
||||||
|
log.error("Flatten FAILED — aborting")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
log.info("── SKIP FLATTEN (no --flatten flag) ─────────────────────────")
|
||||||
|
|
||||||
|
# Read current capital from exchange
|
||||||
|
try:
|
||||||
|
snap = await adapter.refresh_state(None, include_history=False)
|
||||||
|
capital = snap.capital or 25000.0
|
||||||
|
log.info("Exchange capital: %.2f open_positions=%d",
|
||||||
|
capital, len(snap.open_positions))
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning("Could not read capital: %s — using 25000.0", exc)
|
||||||
|
|
||||||
|
if not do_start:
|
||||||
|
log.info("── SKIP PINK STARTUP (--no-start flag) ──────────────────────")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
ok = await pink_startup_roundtrip(adapter, capital)
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.info("Interrupted.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = argparse.ArgumentParser(description="Flatten BingX VST and/or start PINK roundtrip check")
|
||||||
|
p.add_argument("--flatten", action="store_true",
|
||||||
|
help="Cancel all orders and close all positions before starting PINK")
|
||||||
|
p.add_argument("--no-start", action="store_true",
|
||||||
|
help="Skip the PINK startup roundtrip (flatten only)")
|
||||||
|
args = p.parse_args()
|
||||||
|
asyncio.run(main(do_flatten=args.flatten, do_start=not args.no_start))
|
||||||
Reference in New Issue
Block a user