--- name: run-trex-core-daemon description: "Launch the trex-core daemon against the real PostgreSQL + RabbitMQ infrastructure and drive it via the messaging surfaces (snapshot RPC, intent admission RPC, slot events). Use when asked to run trex-core, smoke-test the daemon, confirm a change works end-to-end, or reproduce the BAT-007 transport lifecycle." --- # Run trex-core (sync + thread daemon over RabbitMQ) The daemon (BAT-007) has **no HTTP surface**. All strategy-facing traffic goes over RabbitMQ — snapshot RPC on `trex.snapshot.req`, intent admission RPC on `trex.intent.req`, slot lifecycle events on the `trex.events` topic exchange. "Run" means: boot the daemon in the background, drive it with messages, look at the responses + the events. ## Prerequisites The daemon needs three live services. Verify before launching: ```bash .venv/bin/python - <<'PY' import os, psycopg, pika from dotenv import load_dotenv load_dotenv(".env") psycopg.connect(os.environ["TREX_POSTGRESQL_DSN"], connect_timeout=3).close() print("postgres OK") p = pika.URLParameters(os.environ["TREX_RABBITMQ_URL"]); p.socket_timeout = 3 pika.BlockingConnection(p).close() print("rabbitmq OK") PY ``` If either fails, fix the infra first — the daemon will not boot without `TREX_POSTGRESQL_DSN` and will boot silently degraded if `TREX_RABBITMQ_URL` is missing (stub `InMemoryTransport` — strategy modules can't reach it). ### Required env (`.env`, gitignored) | Variable | Required | Notes | |---|---|---| | `TREX_POSTGRESQL_DSN` | yes | psycopg DSN | | `TREX_RABBITMQ_URL` | yes for real use | e.g. `amqp://admin:admin@localhost:5672/` | | `TREX_BROKER` | no | `paper` (default) or `kis` | | `TREX_TOTAL_SLOTS` | no | default 10 | | `TREX_EXECUTION_INTERVAL_SEC` | no | default 1.0 | | `TREX_LEDGER_INTERVAL_SEC` | no | default 1.0 | | `TREX_PORTFOLIO_HUB_BASE_URL` | no | if set, real portfolio-hub ledger; else in-memory stub | The daemon preallocates slots for **UTC today** (`datetime.now(tz=timezone.utc).date()`). KST users near midnight: check `date -u` before driving the daemon — the trading_date you query must match what was preallocated. ## Launch Background, redirect log: ```bash .venv/bin/trex-core-daemon &> /tmp/trex-core.log & DAEMON_PID=$! ``` The daemon does not bind any TCP port; "ready" means **four worker threads running and all RabbitMQ topology declared**. Poll for the topology to appear: ```bash for i in {1..15}; do .venv/bin/python - <<'PY' && break import pika, sys p = pika.URLParameters("amqp://admin:admin@localhost:5672/"); p.socket_timeout = 2 c = pika.BlockingConnection(p) try: ch = c.channel() # passive=True: succeeds only if the daemon already declared it ch.queue_declare("trex.intent.req", durable=True, passive=True) ch.queue_declare("trex.snapshot.req", durable=True, passive=True) ch.exchange_declare("trex.events", exchange_type="topic", durable=True, passive=True) print("daemon ready") sys.exit(0) except Exception as exc: print(f"not ready: {exc}"); sys.exit(1) finally: c.close() PY sleep 0.5 done ``` `/tmp/trex-core.log` has the pika INFO chatter — first connection lines mean boot succeeded; absence of `ERROR`/exception traceback means topology declare worked. ## Drive The canonical end-to-end smoke is in `smoke.py` — copy/adapt it. It exercises: 1. **snapshot RPC** (view=all) — confirms the daemon answers. 2. **buy intent RPC** with a fresh `correlation_id` — admitted, slot → `pending_buy`. 3. **duplicate buy** with the *same* `correlation_id` — identical response, no double transition (intent_log dedup gate, REQ-023 AC-1). 4. **wait one execution loop tick** — paper broker fills, slot → `holding`, `slot_filled_buy.{strategy_id}.{symbol}` event published. 5. **sell intent RPC** on the holding slot — slot → `closed`, `slot_filled_sell.{strategy_id}.{symbol}` event published. Run it: ```bash .venv/bin/python .claude/skills/run-trex-core-daemon/smoke.py ``` Sample output (paper broker, `strategy_id=STRAT-A`): ``` === 1) snapshot RPC === slots=10 capacity={idle=10, holding=0, pending=0} === 2) buy intent === admitted, slot_id=b2d60fa3-… === 3) duplicate buy === identical: True === 4) wait for fill === slot status=holding qty=10 avg_price=60000.0 === 5) sell intent === admitted; event rk=slot_filled_sell.STRAT-A.005930 status=closed qty=10 DONE ``` ### Subscribing to events (timing caveat) If you subscribe to `trex.events` *after* the publish happens, the message is gone — topic exchanges drop unroutable publishes. Production strategy modules have their per-strategy queue bound from boot, so they don't miss anything; ad-hoc subscribers need to declare + bind their queue **before** triggering the intent. `smoke.py` does this correctly (declares queue, then sleeps 0.6s, then runs the RPC). ## Stop ```bash kill -TERM $DAEMON_PID # or if you've lost the PID: pkill -f trex-core-daemon ``` The daemon installs SIGINT/SIGTERM handlers that set the stop event, join all worker threads, and `close()` every RabbitMQ connection — last log line should be `Normal shutdown` from pika. Exit code 0. ## Inspecting state The daemon's source of truth is PostgreSQL `daily_slot`: ```bash .venv/bin/python - <<'PY' import os, psycopg from dotenv import load_dotenv load_dotenv(".env") with psycopg.connect(os.environ["TREX_POSTGRESQL_DSN"]) as c: with c.cursor() as cur: cur.execute(""" SELECT trading_date, status, COUNT(*) FROM daily_slot GROUP BY trading_date, status ORDER BY trading_date DESC, status """) for r in cur.fetchall(): print(r) PY ``` Ledger outbox state (BAT-006): ```bash .venv/bin/python - <<'PY' import os, psycopg from dotenv import load_dotenv load_dotenv(".env") with psycopg.connect(os.environ["TREX_POSTGRESQL_DSN"]) as c: with c.cursor() as cur: cur.execute("SELECT status, last_outcome, COUNT(*) FROM ledger_outbox GROUP BY 1,2 ORDER BY 1,2") for r in cur.fetchall(): print(r) PY ``` ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | `slots returned: 0` from snapshot | querying wrong `trading_date` | use UTC today, see `date -u` | | intent gets `rejected: no_idle_slot` | no preallocated slots for that day OR all 10 in flight | check `daily_slot` rows; restart daemon to repreallocate next UTC day | | `RpcTimeoutError` on the client | daemon not up, or consumer thread crashed | check `/tmp/trex-core.log` for exception trace | | `TransportUnavailableError` | RabbitMQ down | the daemon does NOT buffer locally (REQ-024) — fix the broker, then re-issue | | fill event missed by ad-hoc subscriber | queue bound *after* publish | bind queue + sleep ≥0.5s before triggering the intent | | import-time `ModuleNotFoundError: pika` | dev deps not installed | `.venv/bin/pip install -e ".[dev]"` | ## Related docs - `docs/interface-contract.md` — full RabbitMQ topology + message payloads - `docs/runtime-flows.md` — flow diagrams (snapshot RPC, intent RPC, events, degrade branch) - `docs/batches/bat-007_20260605_strategy-module-rabbitmq-transport/` — the batch that introduced this runtime shape