feat(kevin): correct exits, realized P&L, wire exit scanner
- executor: EXIT/SELL signals close the FULL held broker position (not a target_dollars-sized fresh order) and skip when flat - executor: book realized P&L on the closing trade ((fill - avg_entry)*qty) so the dashboard P&L + win-rate populate; entries leave pnl=None - exit scanner: wired into the bridge run loop on kevin_bridge_exit_scan_cron (daily ET gate; croniter intentionally not a dependency) plus an offsetting-SELL guard so it only emits exits for currently-held tickers [ci skip] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a8b0d33bd1
commit
52b3c76482
7 changed files with 587 additions and 15 deletions
|
|
@ -7,8 +7,6 @@ from unittest.mock import AsyncMock, MagicMock
|
|||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
from services.kevin_signal_bridge.exit_scanner import ExitScanner
|
||||
from shared.constants.kevin import KEVIN_STRATEGY_UUID
|
||||
from shared.models.meet_kevin import (
|
||||
|
|
@ -23,6 +21,9 @@ from shared.models.meet_kevin import (
|
|||
)
|
||||
from shared.models.meet_kevin_trading import BridgeStatus, KevinSignalBridgeState
|
||||
from shared.models.trading import Trade, TradeSide, TradeStatus
|
||||
from shared.schemas.trading import PositionInfo
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
def _factory(session: AsyncSession):
|
||||
|
|
@ -39,6 +40,25 @@ def _factory(session: AsyncSession):
|
|||
return factory
|
||||
|
||||
|
||||
def _broker_holding(*tickers: str):
|
||||
"""Mock broker reporting the given tickers as currently held."""
|
||||
broker = AsyncMock()
|
||||
broker.get_positions = AsyncMock(
|
||||
return_value=[
|
||||
PositionInfo(
|
||||
ticker=t,
|
||||
qty=10.0,
|
||||
avg_entry=100.0,
|
||||
current_price=110.0,
|
||||
unrealized_pnl=100.0,
|
||||
market_value=1100.0,
|
||||
)
|
||||
for t in tickers
|
||||
]
|
||||
)
|
||||
return broker
|
||||
|
||||
|
||||
async def _seed_video_and_mention(session: AsyncSession) -> int:
|
||||
channel = KevinChannel(youtube_channel_id="UCex", title="t")
|
||||
session.add(channel)
|
||||
|
|
@ -111,7 +131,10 @@ async def test_exit_scanner_emits_on_elapsed_hold(db_session: AsyncSession):
|
|||
config = MagicMock(kevin_hold_days={"unspecified": 10})
|
||||
|
||||
scanner = ExitScanner(
|
||||
session_factory=_factory(db_session), publisher=publisher, config=config
|
||||
session_factory=_factory(db_session),
|
||||
publisher=publisher,
|
||||
config=config,
|
||||
broker=_broker_holding("NVDA"),
|
||||
)
|
||||
emitted = await scanner.scan_and_emit_exits()
|
||||
assert emitted == 1
|
||||
|
|
@ -149,7 +172,10 @@ async def test_exit_scanner_does_not_emit_when_hold_not_elapsed(
|
|||
config = MagicMock(kevin_hold_days={"unspecified": 10})
|
||||
|
||||
scanner = ExitScanner(
|
||||
session_factory=_factory(db_session), publisher=publisher, config=config
|
||||
session_factory=_factory(db_session),
|
||||
publisher=publisher,
|
||||
config=config,
|
||||
broker=_broker_holding("NVDA"),
|
||||
)
|
||||
emitted = await scanner.scan_and_emit_exits()
|
||||
assert emitted == 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue