feat: add kill-switch control flow
This commit is contained in:
@@ -28,6 +28,7 @@ import pika
|
||||
import pika.exceptions
|
||||
|
||||
from trex_core.adapters.messaging.topology import (
|
||||
CONTROL_REQ_QUEUE,
|
||||
EVENTS_DLQ_EXCHANGE,
|
||||
EVENTS_DLQ_QUEUE,
|
||||
EVENTS_EXCHANGE,
|
||||
@@ -142,6 +143,12 @@ class RabbitMQTransport:
|
||||
durable=True,
|
||||
arguments={"x-dead-letter-exchange": EVENTS_DLQ_EXCHANGE},
|
||||
)
|
||||
# BAT-012 / REQ-043: operator control channel (action dispatch).
|
||||
ch.queue_declare(
|
||||
CONTROL_REQ_QUEUE,
|
||||
durable=True,
|
||||
arguments={"x-dead-letter-exchange": EVENTS_DLQ_EXCHANGE},
|
||||
)
|
||||
|
||||
def declare_strategy_queue(self, queue: str, binding_pattern: str) -> None:
|
||||
"""Per-strategy durable queue (REQ-023 AC-4) bound to events exchange."""
|
||||
|
||||
@@ -15,7 +15,9 @@ from __future__ import annotations
|
||||
# durable for simplicity since the queue itself is shared) ---
|
||||
SNAPSHOT_REQ_QUEUE = "trex.snapshot.req"
|
||||
INTENT_REQ_QUEUE = "trex.intent.req"
|
||||
PAUSE_REQ_QUEUE = "trex.pause.req" # BAT-008 / REQ-032 control RPC
|
||||
PAUSE_REQ_QUEUE = "trex.pause.req" # BAT-008 / REQ-032 (legacy alias of control)
|
||||
CONTROL_REQ_QUEUE = "trex.control.req" # BAT-012 / REQ-043 operator control channel
|
||||
# (action: pause|resume|list|kill_switch|resume_kill_switch)
|
||||
|
||||
# --- Events ---
|
||||
EVENTS_EXCHANGE = "trex.events" # topic, durable
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- BAT-012 / REQ-044: day-scoped kill-switch exclusion overlay.
|
||||
-- Distinct from snapshot_pause (permanent, resume-until). A row excludes a
|
||||
-- portfolio (or '*' for global) from strategy execution for ONE trading_date;
|
||||
-- the next trading day is automatically clear because reads filter by
|
||||
-- trading_date. Manual resume (REQ-046) deletes the row.
|
||||
CREATE TABLE IF NOT EXISTS killswitch_exclusion (
|
||||
portfolio_id TEXT NOT NULL, -- '*' = global exclusion sentinel
|
||||
trading_date DATE NOT NULL,
|
||||
actor TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'),
|
||||
PRIMARY KEY (portfolio_id, trading_date)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_killswitch_exclusion_trading_date
|
||||
ON killswitch_exclusion (trading_date);
|
||||
@@ -0,0 +1,52 @@
|
||||
"""PostgreSQL-backed day-scoped kill-switch exclusion store (REQ-044 / BAT-012).
|
||||
|
||||
Distinct from `PostgreSQLPauseStore`: pause is permanent (resume-until),
|
||||
kill-switch exclusion is day-scoped — keyed by `(portfolio_id, trading_date)`
|
||||
so the next trading day is automatically clear (reads filter by trading_date).
|
||||
`portfolio_id == "*"` is the global-exclusion sentinel.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from typing import Any
|
||||
|
||||
from trex_core.adapters.persistence.postgresql import _require_psycopg
|
||||
from trex_core.application.portfolio import GLOBAL_KILLSWITCH # noqa: F401 (re-export)
|
||||
|
||||
|
||||
class PostgreSQLKillSwitchStore:
|
||||
"""`killswitch_exclusion` table CRUD. (portfolio_id, trading_date) PK."""
|
||||
|
||||
def __init__(self, dsn: str, *, psycopg_module: Any = None) -> None:
|
||||
self._psycopg = _require_psycopg(psycopg_module)
|
||||
self._dsn = dsn
|
||||
|
||||
def exclude(self, portfolio_id: str, trading_date: date, *, actor: str = "kill-switch") -> None:
|
||||
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO killswitch_exclusion (portfolio_id, trading_date, actor) "
|
||||
"VALUES (%s, %s, %s) "
|
||||
"ON CONFLICT (portfolio_id, trading_date) DO NOTHING",
|
||||
(portfolio_id, trading_date, actor),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def clear(self, portfolio_id: str, trading_date: date) -> None:
|
||||
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM killswitch_exclusion "
|
||||
"WHERE portfolio_id = %s AND trading_date = %s",
|
||||
(portfolio_id, trading_date),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def list_excluded(self, trading_date: date) -> set[str]:
|
||||
with self._psycopg.connect(self._dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT portfolio_id FROM killswitch_exclusion WHERE trading_date = %s",
|
||||
(trading_date,),
|
||||
)
|
||||
return {row[0] for row in cur.fetchall()}
|
||||
Reference in New Issue
Block a user