108 lines
3.9 KiB
Python
108 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""End-to-end smoke for trex-core (BAT-030 asset-centric).
|
|
|
|
Assumes the daemon is already running with `--config <toml>` (see SKILL.md).
|
|
Drives every RabbitMQ surface of the asset-centric model:
|
|
|
|
1. snapshot RPC (view=all) — daemon answers; slots[] = permanent
|
|
asset slots created by the universe
|
|
reconcile (status `inactive`)
|
|
2. buy intent RPC keyed by slot_id — admission + slot → pending_buy
|
|
(REQ-061 canonical key; portfolio_id is the legacy dual-accept)
|
|
3. duplicate buy (same intent_id) — intent_log dedup gate (REQ-023 AC-1)
|
|
4. wait for execution tick + fill — slot → holding
|
|
5. sell intent RPC — slot → pending_sell → fill →
|
|
`inactive` (occupancy cleared,
|
|
asset binding kept; NOT 'closed')
|
|
|
|
Pass the instance key as argv[1] (default: "default"). Env from config/.env.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import uuid
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv("config/.env")
|
|
|
|
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/")
|
|
INST = sys.argv[1] if len(sys.argv) > 1 else "default"
|
|
|
|
|
|
def main() -> int:
|
|
mq = RabbitMQTransport(URL, timeout=10.0)
|
|
snap_q = topology.slots_queue(INST)
|
|
intent_q = topology.intent_queue(INST)
|
|
|
|
def snap():
|
|
return mq.call_rpc(
|
|
snap_q,
|
|
{"view": "all", "actor": "operator", "touch_watchdog": False},
|
|
correlation_id=str(uuid.uuid4()), timeout=8.0,
|
|
)
|
|
|
|
def intent(payload):
|
|
cid = payload.get("intent_id") or str(uuid.uuid4())
|
|
return mq.call_rpc(intent_q, payload, correlation_id=cid, timeout=8.0)
|
|
|
|
print("=== 1) snapshot RPC ===")
|
|
s = snap()
|
|
slots = s["slots"]
|
|
available = [x for x in slots if x["status"] == "inactive" and not x["universe_detached"]]
|
|
print(f"slots={len(slots)} available_slots={len(available)}")
|
|
inactive = [x for x in slots if x["status"] == "inactive"
|
|
and not x["universe_detached"]]
|
|
if not inactive:
|
|
print("no admittable (inactive) asset slot — universe reconcile not "
|
|
"done yet, or all assets active")
|
|
return 1
|
|
target = inactive[0]
|
|
sid = target["slot_id"]
|
|
print(f"target slot_id={sid} portfolio_id={target['portfolio_id']}")
|
|
|
|
print("=== 2) buy by slot_id ===")
|
|
iid = str(uuid.uuid4())
|
|
d = intent({"intent_type": "buy", "intent_id": iid, "slot_id": sid,
|
|
"qty": 10, "entry_price": 1000, "strategy_id": "STRAT-A"})
|
|
print(f"-> {d['status']} reason={d['reason']}")
|
|
if d["status"] != "admitted":
|
|
return 1
|
|
|
|
print("=== 3) duplicate buy (same intent_id) ===")
|
|
d2 = intent({"intent_type": "buy", "intent_id": iid, "slot_id": sid, "qty": 10})
|
|
print(f"identical: {d2 == d}")
|
|
|
|
print("=== 4) wait for fill ===")
|
|
for _ in range(20):
|
|
time.sleep(0.5)
|
|
st = next(x for x in snap()["slots"] if x["slot_id"] == sid)
|
|
if st["status"] == "holding":
|
|
break
|
|
print(f"slot status={st['status']} qty={st['qty']} avg_price={st['avg_price']}")
|
|
|
|
print("=== 5) sell intent ===")
|
|
d3 = intent({"intent_type": "sell", "intent_id": str(uuid.uuid4()),
|
|
"slot_id": sid})
|
|
print(f"-> {d3['status']}")
|
|
for _ in range(20):
|
|
time.sleep(0.5)
|
|
st = next(x for x in snap()["slots"] if x["slot_id"] == sid)
|
|
if st["status"] == "inactive":
|
|
break
|
|
print(f"slot status={st['status']} qty={st['qty']} "
|
|
f"portfolio_id={st['portfolio_id']} (asset binding kept)")
|
|
|
|
mq.close()
|
|
print("DONE")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|