fix(kevin_bridge): persist signal to signals table before audit row
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
End-to-end Phase 2 verification surfaced a FK violation: the bridge publishes a TradeSignal to the Redis stream and writes kevin_signal_bridge_state with signal_id, but signal_id has a FK to the signals table — which was never populated for Kevin-emitted signals (only the news+sentiment path wrote there). AuditWriter.persist_signal() inserts the TradeSignal into the signals table idempotently (on_conflict_do_nothing on the UUID PK) before the bridge publishes to Redis. Bridge calls it as a new step right before the XADD, so: 1. Signal row exists in signals table 2. XADD to signals:generated 3. Audit row with signal_id FK now resolves Verified live: mention #84 (synthetic NVDA buy, conviction 0.85) emitted a signal, trade-executor consumed and correctly rejected with outside_market_hours (market was closed at the time).
This commit is contained in:
parent
bcd0857729
commit
35707a5c8a
2 changed files with 29 additions and 0 deletions
|
|
@ -9,12 +9,36 @@ from typing import Any, Callable
|
||||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||||
|
|
||||||
from shared.models.meet_kevin_trading import KevinSignalBridgeState
|
from shared.models.meet_kevin_trading import KevinSignalBridgeState
|
||||||
|
from shared.models.trading import Signal as SignalRow
|
||||||
|
from shared.schemas.trading import TradeSignal
|
||||||
|
|
||||||
|
|
||||||
class AuditWriter:
|
class AuditWriter:
|
||||||
def __init__(self, session_factory: Callable[..., Any]) -> None:
|
def __init__(self, session_factory: Callable[..., Any]) -> None:
|
||||||
self.session_factory = session_factory
|
self.session_factory = session_factory
|
||||||
|
|
||||||
|
async def persist_signal(self, signal: TradeSignal) -> None:
|
||||||
|
"""Persist a TradeSignal to the `signals` table so downstream
|
||||||
|
FKs (kevin_signal_bridge_state.signal_id, trades.signal_id)
|
||||||
|
resolve. Idempotent on the signal UUID — caller should call
|
||||||
|
once per published signal before writing the audit row.
|
||||||
|
"""
|
||||||
|
async with self.session_factory() as session:
|
||||||
|
stmt = pg_insert(SignalRow).values(
|
||||||
|
id=signal.signal_id,
|
||||||
|
ticker=signal.ticker,
|
||||||
|
direction=signal.direction,
|
||||||
|
strength=signal.strength,
|
||||||
|
strategy_sources={"sources": signal.strategy_sources},
|
||||||
|
strategy_id=signal.strategy_id,
|
||||||
|
acted_on=False,
|
||||||
|
)
|
||||||
|
# Idempotent — if the row already exists (e.g. retry path)
|
||||||
|
# leave it alone.
|
||||||
|
stmt = stmt.on_conflict_do_nothing(index_elements=["id"])
|
||||||
|
await session.execute(stmt)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
async def write(
|
async def write(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,11 @@ class KevinBridge:
|
||||||
take_profit_pct=Decimal(str(self.config.kevin_take_profit_pct)),
|
take_profit_pct=Decimal(str(self.config.kevin_take_profit_pct)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Persist to signals table FIRST so downstream FK
|
||||||
|
# (kevin_signal_bridge_state.signal_id → signals.id) resolves.
|
||||||
|
# On retry the on_conflict_do_nothing makes this idempotent.
|
||||||
|
await self.audit_writer.persist_signal(signal)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stream_id = await self.publisher.publish(signal)
|
stream_id = await self.publisher.publish(signal)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue