187 lines
6.8 KiB
Python
187 lines
6.8 KiB
Python
#!/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())
|