Deliver BAT-002: slot-core daemon + HTTP intent API + PostgreSQL

Implement REQ-005~009 (dcy-002 slice 1) as an onion stack on top of the
BAT-001 pure domain:
- application: IntentService (admission + single-writer transition +
  intent_id idempotency via durable intent_log)
- adapters/persistence: PostgreSQLSlotRepository + PostgreSQLIntentStore +
  trex-core-owned migrations (daily_slot per Appendix A, daily_symbol_entries,
  slot_transition_log, intent_log) + in-memory intent store
- adapters/api: FastAPI /v1 (healthz, intents/buy|sell|{id}, slots, capacity)
- runtime: trex-core-daemon entrypoint with lifespan migrate+preallocate

Verification (against local PostgreSQL 18): 74 passed, ruff clean, domain
import sweep shows zero web/db deps (NFR-1), 20-thread concurrency admits
exactly the idle count (NFR-2), idempotent resubmit causes no double
transition (NFR-3), EXPECTED_SLOT_FIELDS<->information_schema drift test
(realizes dcy-001 OQ-2's deferred DB-side check), and a daemon E2E smoke.

Align all four baseline docs to the new daemon/API/persistence layers.
BAT-002 -> release-candidate; REQ-005~009 -> Implemented. Full async outbox,
broker execution, safety floor, and strategy daemons remain deferred to a
follow-up Discovery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 13:46:08 +09:00
co-authored by Claude Opus 4.8
parent a44f9fddb7
commit a4e263c8e7
41 changed files with 1778 additions and 38 deletions
@@ -0,0 +1,74 @@
-- BAT-002 / REQ-006: trex-core-owned slot-core schema.
--
-- Authored fresh for trex-core, keyed to the Slot column contract in
-- dcy-001 Appendix A (orbit-v2 DDL used only as reference). Scope: slice 1.
-- Excludes daily_slot_archive / retention prune (out of scope, dcy-002).
-- daily_slot — current lifecycle state (single-writer source of truth).
-- Columns mirror dcy-001 Appendix A (16 columns) and trex_core EXPECTED_SLOT_FIELDS.
CREATE TABLE IF NOT EXISTS daily_slot (
slot_id UUID PRIMARY KEY,
trading_date DATE NOT NULL,
status TEXT NOT NULL CHECK (
status IN ('idle','pending_buy','holding','pending_sell','closed','error')
),
symbol TEXT,
qty NUMERIC(18, 4),
avg_price NUMERIC(18, 4),
entry_price NUMERIC(18, 4),
allocated_capital NUMERIC(18, 2),
order_id TEXT,
strategy_id TEXT,
broker_order_id TEXT,
portfolio_id TEXT,
sell_strategy_id TEXT,
forced_close_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'),
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now() AT TIME ZONE 'UTC')
);
CREATE INDEX IF NOT EXISTS idx_daily_slot_trading_date_status
ON daily_slot (trading_date, status);
-- daily_symbol_entries — per-day re-entry counter (decision input).
CREATE TABLE IF NOT EXISTS daily_symbol_entries (
trading_date DATE NOT NULL,
symbol TEXT NOT NULL,
entry_count INTEGER NOT NULL DEFAULT 0,
last_entry_at TIMESTAMPTZ NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'),
PRIMARY KEY (trading_date, symbol)
);
-- slot_transition_log — append-only audit of every transition.
CREATE TABLE IF NOT EXISTS slot_transition_log (
log_id BIGSERIAL PRIMARY KEY,
slot_id UUID NOT NULL,
from_state TEXT NOT NULL,
to_state TEXT NOT NULL,
ts TIMESTAMPTZ NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'),
reason TEXT NOT NULL,
actor TEXT NOT NULL,
extra JSONB NOT NULL DEFAULT '{}'::jsonb
);
CREATE INDEX IF NOT EXISTS idx_slot_transition_log_slot_id_ts
ON slot_transition_log (slot_id, ts);
-- intent_log — durable idempotency store + audit for accepted intents
-- (dcy-002 OQ-1). intent_id UNIQUE is the dedup gate that prevents double
-- slot transitions on at-least-once retransmission. Precursor to a full
-- outbox (status column reserved); async consumer loop deferred to the
-- broker-execution Discovery.
CREATE TABLE IF NOT EXISTS intent_log (
intent_id TEXT PRIMARY KEY,
intent_type TEXT NOT NULL CHECK (intent_type IN ('buy','sell')),
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
status TEXT NOT NULL DEFAULT 'processing'
CHECK (status IN ('processing','admitted','rejected')),
slot_id UUID,
reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'),
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now() AT TIME ZONE 'UTC')
);
CREATE INDEX IF NOT EXISTS idx_intent_log_status ON intent_log (status);