trading/services/kevin_signal_bridge/audit.py
Viktor Barzin 35707a5c8a
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix(kevin_bridge): persist signal to signals table before audit row
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).
2026-05-26 21:23:59 +00:00

74 lines
2.8 KiB
Python

"""Audit writer: upserts a row in kevin_signal_bridge_state per processed mention."""
from __future__ import annotations
import uuid
from decimal import Decimal
from typing import Any, Callable
from sqlalchemy.dialects.postgresql import insert as pg_insert
from shared.models.meet_kevin_trading import KevinSignalBridgeState
from shared.models.trading import Signal as SignalRow
from shared.schemas.trading import TradeSignal
class AuditWriter:
def __init__(self, session_factory: Callable[..., Any]) -> None:
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(
self,
*,
mention_id: int,
bridge_status: str,
effective_conviction: Decimal | None = None,
signal_id: uuid.UUID | None = None,
trade_id: uuid.UUID | None = None,
notes: str | None = None,
) -> None:
"""Upsert one audit row (mention_id is unique)."""
async with self.session_factory() as session:
stmt = pg_insert(KevinSignalBridgeState).values(
id=uuid.uuid4(),
mention_id=mention_id,
bridge_status=bridge_status,
effective_conviction=effective_conviction,
signal_id=signal_id,
trade_id=trade_id,
notes=notes,
)
stmt = stmt.on_conflict_do_update(
index_elements=["mention_id"],
set_={
"bridge_status": stmt.excluded.bridge_status,
"effective_conviction": stmt.excluded.effective_conviction,
"signal_id": stmt.excluded.signal_id,
"trade_id": stmt.excluded.trade_id,
"notes": stmt.excluded.notes,
},
)
await session.execute(stmt)
await session.commit()