feat: add portfolio snapshot read layer
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user