diff --git a/services/trade_executor/main.py b/services/trade_executor/main.py index abc3996..65ef5db 100644 --- a/services/trade_executor/main.py +++ b/services/trade_executor/main.py @@ -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) diff --git a/tests/services/test_trade_executor.py b/tests/services/test_trade_executor.py index bbaf84f..fc44ed5 100644 --- a/tests/services/test_trade_executor.py +++ b/tests/services/test_trade_executor.py @@ -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)