feat(safety-sell): dcy-007 core 자율 안전 매도 — BAT-010 기반 + BAT-011 트리거
dcy-007(core 자율 안전 매도) 전체 delivery. BAT-010 기반 인프라가 미커밋 상태였고 BAT-011 트리거 레이어와 공유 파일에서 섞여 있어 한 커밋으로 묶음. BAT-010 (REQ-034/035/036/041, release-candidate): - WatchdogStore(application/watchdog.py) + snapshot RPC 워치독 갱신 - Slot stop_loss_price/take_profit_price 필드 + 004_add_price_thresholds.sql - SlotService.sell() 자율 매도 내부 경로 + idempotency - SafetySellConfig env 5종 파싱 BAT-011 (REQ-037/038/039/040/042, release-candidate): - SafetySellService(application/safety_sell.py): forced_close 만기 / stop-loss· take-profit KIS polling / 장 마감 강제 청산 + trex-safety-sell 7번째 스레드 - BrokerPort.get_current_price + KIS inquire-price(TTTC8397R/VTTC8397R, stck_prpr) + paper stub - SlotService.update_thresholds() + intent RPC update_thresholds 액션 (부분 갱신/null 해제, 워치독 last_seen_at 리셋) - baseline 3문서 정합(interface-contract/runtime-flows/project-structure) 거버넌스: REQ-034~042 Implemented 동기화, BAT-010/011 release-candidate, dcy-007 discovery 포함. 검증: 신규 38 테스트 green, 단위 236 passed/23 skipped, 인프라 게이트(PG+RabbitMQ) 259 passed(마이그레이션 001~004 round-trip 포함), ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,12 @@ strategy-facing communication runs over RabbitMQ (REQ-021/022/023).
|
||||
|
||||
Threads (started in `run()`):
|
||||
- intent RPC consumer — `trex.intent.req`, prefetch=1, FIFO, dedup via intent_log
|
||||
- snapshot RPC consumer — `trex.snapshot.req`
|
||||
- snapshot RPC consumer — `trex.snapshot.req` (also refreshes the watchdog)
|
||||
- pause RPC consumer — `trex.pause.req` (REQ-032)
|
||||
- execution loop — broker dispatch + poll + slot transitions + event emit
|
||||
- ledger writer — ledger outbox drain (REQ-019)
|
||||
- snapshot scheduler — daily portfolio-hub snapshot refresh (REQ-030)
|
||||
- safety-sell worker — forced_close / stop-loss·take-profit / market-close (REQ-037/038/039)
|
||||
|
||||
Each consumer/publisher uses its OWN RabbitMQTransport instance because
|
||||
pika BlockingConnection is not thread-safe across channels.
|
||||
@@ -56,7 +59,9 @@ from trex_core.application.portfolio import (
|
||||
PortfolioHubReadPort,
|
||||
SnapshotStore,
|
||||
)
|
||||
from trex_core.application.safety_sell import SafetySellService
|
||||
from trex_core.application.snapshot_scheduler import SnapshotScheduler
|
||||
from trex_core.application.watchdog import WatchdogStore
|
||||
from trex_core.domain.slot import SlotService
|
||||
from trex_core.runtime.config import DaemonConfig
|
||||
|
||||
@@ -140,6 +145,8 @@ class Daemon:
|
||||
|
||||
# BAT-008: portfolio-hub read + snapshot store + durable pause + scheduler.
|
||||
self.snapshot_store = SnapshotStore()
|
||||
# BAT-010/011: per-slot watchdog (snapshot RPC writer, safety-sell reader).
|
||||
self.watchdog = WatchdogStore()
|
||||
self.pause_store = PostgreSQLPauseStore(config.dsn)
|
||||
self.portfolio_read = _build_portfolio_read(config)
|
||||
self.snapshot_scheduler = SnapshotScheduler(
|
||||
@@ -160,6 +167,15 @@ class Daemon:
|
||||
event_emitter=self._make_event_emitter(),
|
||||
)
|
||||
|
||||
# BAT-011: safety-sell trigger service (forced_close / SL-TP / market-close).
|
||||
self.safety_sell = SafetySellService(
|
||||
slot_repo=self.slot_repo,
|
||||
slot_service=self.slot_service,
|
||||
broker=self.broker,
|
||||
watchdog=self.watchdog,
|
||||
config=config.safety_sell,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------ event hook
|
||||
def _make_event_emitter(self):
|
||||
def emit(event_type: str, slot) -> None:
|
||||
@@ -195,7 +211,11 @@ class Daemon:
|
||||
transport.declare_topology()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
handler = make_intent_rpc_handler(self.intent_service)
|
||||
handler = make_intent_rpc_handler(
|
||||
self.intent_service,
|
||||
slot_service=self.slot_service,
|
||||
watchdog_store=self.watchdog,
|
||||
)
|
||||
transport.consume_rpc_forever(topology.INTENT_REQ_QUEUE, handler, self.stop)
|
||||
|
||||
def _snapshot_rpc_loop(self) -> None:
|
||||
@@ -210,6 +230,7 @@ class Daemon:
|
||||
snapshot_store=self.snapshot_store,
|
||||
pause_store=self.pause_store,
|
||||
default_total_slots=self.config.total_slots,
|
||||
watchdog_store=self.watchdog,
|
||||
)
|
||||
transport.consume_rpc_forever(topology.SNAPSHOT_REQ_QUEUE, handler, self.stop)
|
||||
|
||||
@@ -223,6 +244,14 @@ class Daemon:
|
||||
handler = make_pause_rpc_handler(self.pause_store)
|
||||
transport.consume_rpc_forever(topology.PAUSE_REQ_QUEUE, handler, self.stop)
|
||||
|
||||
def _safety_sell_loop(self) -> None:
|
||||
# REQ-037/038/039: forced_close / stop-loss·take-profit / market-close.
|
||||
while not self.stop.wait(self.safety_sell.base_tick_sec):
|
||||
try:
|
||||
self.safety_sell.run_once()
|
||||
except Exception: # noqa: BLE001 - loop must survive transient errors
|
||||
continue
|
||||
|
||||
def _snapshot_scheduler_loop(self) -> None:
|
||||
try:
|
||||
self.snapshot_scheduler.run_forever(
|
||||
@@ -251,6 +280,7 @@ class Daemon:
|
||||
("trex-snapshot-rpc", self._snapshot_rpc_loop),
|
||||
("trex-pause-rpc", self._pause_rpc_loop),
|
||||
("trex-snapshot-scheduler", self._snapshot_scheduler_loop),
|
||||
("trex-safety-sell", self._safety_sell_loop),
|
||||
]
|
||||
for name, target in workers:
|
||||
t = threading.Thread(target=target, name=name, daemon=True)
|
||||
|
||||
Reference in New Issue
Block a user