Implement BAT-006: portfolio-hub ledger integration (REQ-017~019)
- REQ-017: LedgerPort contract (application/ledger_port.py) + portfolio-hub adapter + in-memory stub (adapters/ledger/), TREX_PORTFOLIO_HUB_* config - REQ-018: fill→ledger recording in execution loop; FillStatus carries execution_id (deterministic synth) + fee; paper/kis adapters updated - REQ-019: ledger outbox + retry worker (application/ledger_outbox.py), correlation_id idempotency, outcome vocabulary; 002_create_ledger_outbox.sql - Tests: in-memory/portfolio-hub ledger, execution ledger hook, outbox, domain import sweep Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,15 @@ class KISConfig:
|
||||
redis_url: str = "" # REQ-016: optional token cache backend
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PortfolioHubConfig:
|
||||
"""portfolio-hub ledger adapter config (REQ-017)."""
|
||||
|
||||
base_url: str
|
||||
token: str = ""
|
||||
timeout: float = 5.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DaemonConfig:
|
||||
dsn: str
|
||||
@@ -40,6 +49,10 @@ class DaemonConfig:
|
||||
broker: str # 'paper' | 'kis'
|
||||
execution_interval_sec: float
|
||||
kis: Optional[KISConfig] = None
|
||||
# REQ-017/019: ledger writer config (None => in-memory stub ledger,
|
||||
# appropriate for slice-1 paper-broker integration tests).
|
||||
portfolio_hub: Optional[PortfolioHubConfig] = None
|
||||
ledger_interval_sec: float = 1.0
|
||||
|
||||
@staticmethod
|
||||
def from_env() -> "DaemonConfig":
|
||||
@@ -61,6 +74,14 @@ class DaemonConfig:
|
||||
allow_live=os.environ.get("TREX_KIS_ALLOW_LIVE", "").lower() in ("1", "true", "yes"),
|
||||
redis_url=os.environ.get("TREX_KIS_REDIS_URL", ""),
|
||||
)
|
||||
portfolio_hub: Optional[PortfolioHubConfig] = None
|
||||
pf_base = os.environ.get("TREX_PORTFOLIO_HUB_BASE_URL", "").strip()
|
||||
if pf_base:
|
||||
portfolio_hub = PortfolioHubConfig(
|
||||
base_url=pf_base,
|
||||
token=os.environ.get("TREX_PORTFOLIO_HUB_TOKEN", "").strip(),
|
||||
timeout=float(os.environ.get("TREX_PORTFOLIO_HUB_TIMEOUT", "5")),
|
||||
)
|
||||
return DaemonConfig(
|
||||
dsn=dsn,
|
||||
total_slots=int(os.environ.get("TREX_TOTAL_SLOTS", "10")),
|
||||
@@ -69,4 +90,6 @@ class DaemonConfig:
|
||||
broker=broker,
|
||||
execution_interval_sec=float(os.environ.get("TREX_EXECUTION_INTERVAL_SEC", "1.0")),
|
||||
kis=kis,
|
||||
portfolio_hub=portfolio_hub,
|
||||
ledger_interval_sec=float(os.environ.get("TREX_LEDGER_INTERVAL_SEC", "1.0")),
|
||||
)
|
||||
|
||||
@@ -16,14 +16,18 @@ from fastapi import FastAPI
|
||||
|
||||
from trex_core.adapters.api.app import create_app
|
||||
from trex_core.adapters.broker.paper import PaperBroker
|
||||
from trex_core.adapters.ledger import InMemoryLedger, PortfolioHubLedger
|
||||
from trex_core.adapters.persistence.migrate import apply_migrations
|
||||
from trex_core.adapters.persistence.postgresql import (
|
||||
PostgreSQLIntentStore,
|
||||
PostgreSQLLedgerOutbox,
|
||||
PostgreSQLSlotRepository,
|
||||
)
|
||||
from trex_core.application.broker_port import BrokerPort
|
||||
from trex_core.application.execution import ExecutionService
|
||||
from trex_core.application.intents import IntentService
|
||||
from trex_core.application.ledger_outbox import LedgerWriter
|
||||
from trex_core.application.ledger_port import LedgerPort
|
||||
from trex_core.domain.slot import SlotService
|
||||
from trex_core.runtime.config import DaemonConfig
|
||||
|
||||
@@ -46,13 +50,23 @@ def _build_broker(config: DaemonConfig) -> BrokerPort:
|
||||
return PaperBroker()
|
||||
|
||||
|
||||
def _build_ledger(config: DaemonConfig) -> LedgerPort:
|
||||
"""portfolio-hub when configured; otherwise in-memory stub (REQ-017)."""
|
||||
pf = config.portfolio_hub
|
||||
if pf is None:
|
||||
return InMemoryLedger()
|
||||
return PortfolioHubLedger(pf.base_url, token=pf.token or None, timeout=pf.timeout)
|
||||
|
||||
|
||||
def build_app(config: DaemonConfig) -> FastAPI:
|
||||
slot_repo = PostgreSQLSlotRepository(config.dsn)
|
||||
intent_store = PostgreSQLIntentStore(config.dsn)
|
||||
slot_service = SlotService(slot_repo)
|
||||
intent_service = IntentService(slot_service, slot_repo, intent_store)
|
||||
broker = _build_broker(config)
|
||||
execution = ExecutionService(slot_service, slot_repo, broker)
|
||||
outbox = PostgreSQLLedgerOutbox(config.dsn)
|
||||
execution = ExecutionService(slot_service, slot_repo, broker, ledger_outbox=outbox)
|
||||
ledger_writer = LedgerWriter(outbox, _build_ledger(config))
|
||||
|
||||
base = create_app(
|
||||
slot_service=slot_service,
|
||||
@@ -63,13 +77,20 @@ def build_app(config: DaemonConfig) -> FastAPI:
|
||||
|
||||
stop = threading.Event()
|
||||
|
||||
def _loop() -> None:
|
||||
def _execution_loop() -> None:
|
||||
while not stop.wait(config.execution_interval_sec):
|
||||
try:
|
||||
execution.run_once()
|
||||
except Exception: # noqa: BLE001 - loop must survive transient errors
|
||||
continue
|
||||
|
||||
def _ledger_loop() -> None:
|
||||
while not stop.wait(config.ledger_interval_sec):
|
||||
try:
|
||||
ledger_writer.run_once()
|
||||
except Exception: # noqa: BLE001 - loop must survive transient errors
|
||||
continue
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
apply_migrations(config.dsn)
|
||||
@@ -77,13 +98,16 @@ def build_app(config: DaemonConfig) -> FastAPI:
|
||||
datetime.now(tz=timezone.utc).date(), config.total_slots
|
||||
)
|
||||
execution.reconcile() # restart recovery before serving (REQ-014)
|
||||
worker = threading.Thread(target=_loop, name="trex-execution", daemon=True)
|
||||
worker.start()
|
||||
exec_worker = threading.Thread(target=_execution_loop, name="trex-execution", daemon=True)
|
||||
ledger_worker = threading.Thread(target=_ledger_loop, name="trex-ledger", daemon=True)
|
||||
exec_worker.start()
|
||||
ledger_worker.start()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
stop.set()
|
||||
worker.join(timeout=config.execution_interval_sec * 3)
|
||||
exec_worker.join(timeout=config.execution_interval_sec * 3)
|
||||
ledger_worker.join(timeout=config.ledger_interval_sec * 3)
|
||||
|
||||
base.router.lifespan_context = lifespan
|
||||
return base
|
||||
|
||||
Reference in New Issue
Block a user