#!/usr/bin/env python3 """End-to-end smoke for trex-core (BAT-007). Assumes the daemon is already running (`.venv/bin/trex-core-daemon &`). Drives it through every RabbitMQ surface: 1. snapshot RPC (view=all) — daemon answers 2. buy intent RPC — admission + slot transition 3. duplicate buy (same correlation_id) — intent_log dedup gate (REQ-023 AC-1) 4. wait for execution tick + fill event 5. sell intent RPC — slot → closed + sell event Trading_date defaults to UTC today (matches what the daemon preallocates). """ from __future__ import annotations import json import os import sys import threading import time import uuid from datetime import date, datetime, timezone from dotenv import load_dotenv load_dotenv(".env") import pika # noqa: E402 from trex_core.adapters.messaging import topology # noqa: E402 from trex_core.adapters.messaging.rabbitmq import RabbitMQTransport # noqa: E402 URL = os.environ.get("TREX_RABBITMQ_URL", "amqp://admin:admin@localhost:5672/") TODAY = datetime.now(tz=timezone.utc).date() STRATEGY = "STRAT-A" SYMBOL = "005930" def _subscribe_events(received: list, stop: threading.Event, queue: str) -> threading.Thread: """Background event consumer bound BEFORE we trigger intents.""" def loop() -> None: t = RabbitMQTransport(URL, timeout=5.0) try: t.consume_events_forever( queue, topology.strategy_event_binding_pattern(STRATEGY), lambda rk, p: received.append((rk, p)), stop, ) finally: t.close() th = threading.Thread(target=loop, name="smoke-events", daemon=True) th.start() return th def _cleanup_queue(queue: str) -> None: try: c = pika.BlockingConnection(pika.URLParameters(URL)) try: c.channel().queue_delete(queue) finally: c.close() except Exception: pass def _print_slots(slots: list) -> None: for s in slots: print(f" slot {s['slot_id'][:8]}.. status={s['status']:<12} " f"symbol={s['symbol']} strategy_id={s['strategy_id']} qty={s['qty']} " f"avg_price={s['avg_price']}") def main() -> int: ev_queue = f"trex.events.strategy.{STRATEGY}.smoke.{uuid.uuid4().hex[:6]}" ev_received: list[tuple[str, dict]] = [] ev_stop = threading.Event() ev_thread = _subscribe_events(ev_received, ev_stop, ev_queue) time.sleep(0.6) # let queue bind before triggering events client = RabbitMQTransport(URL, timeout=5.0) try: # 1) snapshot RPC --------------------------------------------------- print(f"=== 1) snapshot RPC (view=all, day={TODAY.isoformat()}) ===") resp = client.call_rpc( topology.SNAPSHOT_REQ_QUEUE, {"view": "all", "trading_date": TODAY.isoformat()}, correlation_id=f"snap-{uuid.uuid4().hex[:8]}", timeout=5.0, ) print(f"slots={len(resp['slots'])} capacity={resp['capacity']}") if resp["capacity"]["idle_count"] == 0: print("\n!! no idle slots for today — restart the daemon to preallocate, " "or check `date -u`") return 2 # 2) buy intent RPC ------------------------------------------------ print(f"\n=== 2) buy intent RPC ({SYMBOL}, qty=10@60000, " f"strategy={STRATEGY}, portfolio=PF-smoke) ===") cid = f"smoke-buy-{uuid.uuid4().hex[:8]}" resp = client.call_rpc( topology.INTENT_REQ_QUEUE, { "intent_type": "buy", "intent_id": cid, "trading_date": TODAY.isoformat(), "symbol": SYMBOL, "qty": 10, "entry_price": 60000, "strategy_id": STRATEGY, "portfolio_id": "PF-smoke", }, correlation_id=cid, timeout=5.0, ) print(f"admission: {json.dumps(resp, ensure_ascii=False)}") if resp["status"] != "admitted": print("\n!! buy not admitted — aborting"); return 3 # 3) duplicate ----------------------------------------------------- print(f"\n=== 3) duplicate buy (same correlation_id={cid[-8:]}…) ===") resp_dup = client.call_rpc( topology.INTENT_REQ_QUEUE, { "intent_type": "buy", "intent_id": cid, "trading_date": TODAY.isoformat(), "symbol": SYMBOL, "qty": 10, "strategy_id": STRATEGY, "portfolio_id": "PF-smoke", }, correlation_id=cid, timeout=5.0, ) print(f"identical: {resp == resp_dup} (REQ-023 AC-1 intent_log dedup)") # 4) wait for fill + event ----------------------------------------- print(f"\n=== 4) wait for execution tick + fill event (paper broker) ===") deadline = time.monotonic() + 10 while time.monotonic() < deadline: resp = client.call_rpc( topology.SNAPSHOT_REQ_QUEUE, {"view": "open", "trading_date": TODAY.isoformat()}, correlation_id=f"snap-{uuid.uuid4().hex[:8]}", timeout=5.0, ) holdings = [s for s in resp["slots"] if s["status"] == "holding"] if holdings: _print_slots(holdings) break time.sleep(0.5) else: print("\n!! no holding slot appeared in 10s — execution loop stuck?"); return 4 print(f"buy events captured so far: " f"{[rk for rk, _ in ev_received if rk.startswith('slot_filled_buy.')]}") # 5) sell intent --------------------------------------------------- sid = holdings[0]["slot_id"] scid = f"smoke-sell-{uuid.uuid4().hex[:8]}" print(f"\n=== 5) sell intent on slot {sid[:8]}.. ===") resp_sell = client.call_rpc( topology.INTENT_REQ_QUEUE, {"intent_type": "sell", "intent_id": scid, "slot_id": sid}, correlation_id=scid, timeout=5.0, ) print(f"admission: {json.dumps(resp_sell, ensure_ascii=False)}") # Wait for the sell fill event deadline = time.monotonic() + 10 while time.monotonic() < deadline and not any( rk.startswith("slot_filled_sell.") for rk, _ in ev_received ): time.sleep(0.3) print(f"\nall events captured ({len(ev_received)}):") for rk, payload in ev_received: print(f" rk={rk} status={payload.get('status'):<8} " f"qty={payload.get('qty')} sequence_at={payload.get('sequence_at')}") print("\nDONE") return 0 finally: ev_stop.set() try: client.close() except Exception: pass ev_thread.join(timeout=2.0) _cleanup_queue(ev_queue) if __name__ == "__main__": sys.exit(main())