Files
trex-core/.claude/skills/run-trex-core-daemon/SKILL.md
T
genichinandClaude Opus 4.8 7d727824dc feat(slot): BAT-030 — 자산 중심 cutover 완성 (dcy-020 BAT-δ, REQ-056·057·062·064·065)
M1 literal inactive 전면화: SlotState 5-state(idle/closed 별칭 제거),
마이그레이션 010(CHECK 교체+행 rewrite), ops_checks 를 transition_log 기반 재작성.
M2 집행 매도 수량 브로커 정본(D3): dispatch 매도 qty=broker hldg_qty,
미러 fallback 없음, 브로커 0 → skip+WARN, 리스트 기반 dispatch(head-block 방지).
M3 컬럼 drop + legacy pool 제거: 마이그레이션 011(trading_date·entry_count DROP),
repo 계약 instance 스코프 개편(집행 큐 포함), preallocation 폐기(데몬 7스레드),
admission 자산 중심 단일 경로(slot_id 정본), capacity 재정의(available=M−active),
daily_symbol_entries 코드 경로 제거.
M4 cutover 도구(D1·D4): trex-core-cutover(전체 rebuild, preflight/브로커 carry-over/
--allow-unmapped/--promote-unique/dry-run) + PG 리허설 테스트 8건.
M5 baseline 마감: data-model/runtime-flows/project-structure/interface-contract
자산 중심 최종 갱신 + run-trex-core-daemon 스킬 정합.

검증: 단위 367 passed + ruff clean; PG-gated 29 passed(010/011 round-trip·드리프트
가드·cutover 리허설); RabbitMQ 21 passed; run-the-app e2e(test DB+실 MQ, M=1):
reconcile 슬롯 생성→slot_id 매수→dedup→capacity_full→fill→already_active→
매도→inactive(자산 바인딩 보존)→영구 슬롯 재활성 전 구간 green.
실 cutover 는 운영자 실행(REL 단계, D4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:16:15 +09:00

8.4 KiB

name, description
name description
run-trex-core-daemon 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:

.venv/bin/python - <<'PY'
import os, psycopg, pika
from dotenv import load_dotenv
load_dotenv("config/.env")  # BAT-026: .env lives in the config dir
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 (config/.env, gitignored) + per-instance TOML

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_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 universe+ledger; else stub universe (pf-005930/pf-000660) + in-memory ledger

--config <name> (required since BAT-013) resolves config/<name>.toml: instance_key (queue names: trex.intent.req.<key> etc.; default keeps the literal names) and max_active_slots (M cap; unset = no cap).

BAT-030 (asset-centric): there is NO daily preallocation. Permanent asset slots (one per universe portfolio, status inactive) are created by the snapshot scheduler's universe reconcile right after boot. Slot states are inactive / pending_buy / holding / pending_sell / error — idle/closed no longer exist.

Launch

Background, redirect log (smoke against the test DB: point TREX_POSTGRESQL_DSN at trex_core_test via --env-file, never the live DB):

.venv/bin/trex-core-daemon --config <instance> &> /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:

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) — daemon answers; slots[] holds the permanent asset slots from the universe reconcile (status inactive).
  2. buy intent RPC keyed by slot_id (REQ-061 canonical; portfolio_id dual-accept) — admitted, slot → pending_buy. Rejections: unknown_slot, already_active, capacity_full (M cap).
  3. duplicate buy with the same intent_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.
  5. sell intent RPC on the holding slot — fill returns the slot to inactive (occupancy cleared, asset binding kept — NOT closed). Sell dispatch quantity is the broker balance (REQ-062); broker flat → the sell is skipped with a WARN.

Run it (instance key optional, default default):

.venv/bin/python .claude/skills/run-trex-core-daemon/smoke.py [instance_key]

Sample output (paper broker, stub universe):

=== 1) snapshot RPC ===
assets=1 slots=2 available_slots=1
target slot_id=8a160254-… portfolio_id=pf-005930
=== 2) buy by slot_id ===
-> admitted reason=None
=== 3) duplicate buy (same intent_id) ===
identical: True
=== 4) wait for fill ===
slot status=holding qty=10.0 avg_price=1000.0
=== 5) sell intent ===
-> admitted
slot status=inactive qty=None portfolio_id=pf-005930 (asset binding kept)
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

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:

.venv/bin/python - <<'PY'
import os, psycopg
from dotenv import load_dotenv
load_dotenv("config/.env")
with psycopg.connect(os.environ["TREX_POSTGRESQL_DSN"]) as c:
    with c.cursor() as cur:
        cur.execute("""
          SELECT instance_key, status, COUNT(*)
          FROM daily_slot GROUP BY instance_key, status ORDER BY 1, 2
        """)
        for r in cur.fetchall(): print(r)
PY

Ledger outbox state (BAT-006):

.venv/bin/python - <<'PY'
import os, psycopg
from dotenv import load_dotenv
load_dotenv("config/.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 universe reconcile hasn't run (portfolio-hub down / boot race) check the scheduler log line slot reconcile: {'created': N, ...}; with no hub config the stub universe gives 2 slots
intent gets rejected: unknown_slot / unknown_portfolio slot_id/portfolio_id not in this instance's slot set read slots[] from a snapshot RPC first and reuse its slot_id
intent gets rejected: capacity_full max_active_slots (M) reached — FCFS, no eviction sell something first or raise M in the instance TOML
sell never dispatches, WARN sell dispatch skipped broker balance 0 for the symbol (REQ-062 divergence) reconcile the divergence (operator); no mirror fallback by design
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]"
  • 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