feat(kevin-exec): attribute trades to strategy_id (record + dashboard)

The executor wrote trades with strategy_id=None, so Kevin trades were unattributed and invisible to the dashboard, which filters by strategy_id. Set strategy_id=signal.strategy_id on both the persisted Trade row and the published TradeExecution.

[ci skip]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-04 22:01:44 +00:00
parent 14407d37dc
commit a8b0d33bd1
2 changed files with 38 additions and 1 deletions

View file

@ -183,7 +183,7 @@ async def process_signal(
price=result.filled_price or 0.0,
status=result.status,
signal_id=signal.signal_id,
strategy_id=None,
strategy_id=signal.strategy_id,
strategy_sources=signal.strategy_sources,
timestamp=result.timestamp,
)
@ -210,6 +210,7 @@ async def process_signal(
price=result.filled_price or 0.0,
timestamp=str(result.timestamp),
signal_id=signal.signal_id,
strategy_id=signal.strategy_id,
status=status_map.get(result.status, TradeStatusModel.PENDING),
)
session.add(db_trade)

View file

@ -16,6 +16,7 @@ import pytest
from services.trade_executor.config import TradeExecutorConfig
from services.trade_executor.main import process_signal
from services.trade_executor.risk_manager import RiskManager
from shared.constants.kevin import KEVIN_STRATEGY_UUID
from shared.schemas.trading import (
AccountInfo,
OrderResult,
@ -73,6 +74,7 @@ def _make_kevin_signal(
target_dollars: Decimal | None = Decimal("2000"),
stop_loss_pct: Decimal | None = Decimal("0.08"),
take_profit_pct: Decimal | None = Decimal("0.20"),
strategy_id: UUID | None = KEVIN_STRATEGY_UUID,
) -> TradeSignal:
"""A Kevin-style signal: price on the new ``current_price`` field,
pre-computed ``target_dollars``, and stop/take percentages but NO
@ -86,6 +88,7 @@ def _make_kevin_signal(
target_dollars=target_dollars,
stop_loss_pct=stop_loss_pct,
take_profit_pct=take_profit_pct,
strategy_id=strategy_id,
timestamp=datetime.now(timezone.utc),
)
@ -734,3 +737,36 @@ class TestExecutorDBPersistence:
# Verify signal_id is a UUID
from uuid import UUID
assert isinstance(signal.signal_id, UUID)
class TestExecutorStrategyAttribution:
"""Kevin trades must carry strategy_id so they are recorded as Kevin
trades and surface on the dashboard (which filters by strategy_id)."""
@pytest.mark.asyncio
async def test_strategy_id_persisted_and_published(self):
config = _make_config()
broker = _mock_broker(positions=[], account=_make_account(100_000))
publisher = AsyncMock()
publisher.publish = AsyncMock(return_value=b"1-0")
counters = {
"trades_executed": MagicMock(),
"rejections": MagicMock(),
"fill_latency": MagicMock(),
}
signal = _make_kevin_signal()
mock_session = AsyncMock()
mock_session.add = MagicMock()
mock_session.commit = AsyncMock()
db_factory = _make_mock_db_session_factory(mock_session)
with patch.object(RiskManager, "check_risk", return_value=(True, "approved")):
await process_signal(
signal, RiskManager(config, broker), broker, publisher, counters, db_factory
)
trade_obj = mock_session.add.call_args[0][0]
assert trade_obj.strategy_id == KEVIN_STRATEGY_UUID
published = publisher.publish.call_args[0][0]
assert published["strategy_id"] == str(KEVIN_STRATEGY_UUID)