trading/tests/services/kevin_signal_bridge/test_main.py
Viktor Barzin 14407d37dc feat(kevin-exec): size from target_dollars, propagate price, bracket entries
Kevin signals never placed orders: the executor sized only from
sentiment_context["current_price"] (None for Kevin) so qty=0, and orders
were always built SIMPLE (stop/take pcts ignored).

- TradeSignal gains `current_price`; the bridge now sets it on publish
- risk_manager honors `target_dollars` directly (no strength re-scale) and
  resolves price from current_price then sentiment_context
- executor builds BRACKET orders for LONG entries carrying stop/take pcts;
  EXIT/SELL signals stay SIMPLE (the bridge sets pcts even on exits)

[ci skip]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:56:59 +00:00

234 lines
7.5 KiB
Python

"""Smoke-level orchestrator tests for the Kevin signal bridge."""
from decimal import Decimal
from unittest.mock import AsyncMock, MagicMock
from services.kevin_signal_bridge.main import KevinBridge
async def test_bridge_dry_run_writes_audit_does_not_publish():
"""When TRADING_KEVIN_ENABLE_TRADING=false the bridge writes an audit row
and never calls publisher.publish."""
config = MagicMock(kevin_enable_trading=False, kevin_stop_loss_pct=0.08, kevin_take_profit_pct=0.20, kevin_avoid_blocks_days=7)
cursor = AsyncMock()
cursor.last_seen_id.return_value = 0
publisher = AsyncMock()
aggregator = AsyncMock()
aggregator.fetch_pending.return_value = [
MagicMock(
id=1,
symbol="NVDA",
action=MagicMock(value="buy"),
conviction=Decimal("0.8"),
time_horizon=MagicMock(value="weeks"),
),
]
strategy = AsyncMock()
strategy.evaluate_mention.return_value = MagicMock(
decision=MagicMock(value="open_long", name="OPEN_LONG"),
symbol="NVDA",
target_dollars=Decimal("3000"),
holding_days=10,
effective_conviction=Decimal("0.8"),
rationale="ok",
)
# Make the .decision attribute compare against KevinDecisionType.OPEN_LONG
from shared.schemas.kevin import KevinDecisionType
strategy.evaluate_mention.return_value.decision = KevinDecisionType.OPEN_LONG
audit_writer = AsyncMock()
broker = AsyncMock()
broker.is_asset_tradable.return_value = True
broker.get_latest_price.return_value = Decimal("100")
broker.get_account.return_value = MagicMock(
equity=Decimal("100000"), cash=Decimal("100000")
)
broker.get_positions.return_value = []
bridge = KevinBridge(
config=config,
cursor=cursor,
publisher=publisher,
aggregator=aggregator,
strategy=strategy,
audit_writer=audit_writer,
broker=broker,
)
await bridge.process_one_pass()
publisher.publish.assert_not_called()
audit_writer.write.assert_awaited()
args = audit_writer.write.call_args
assert "dry_run" in str(args).lower()
async def test_bridge_kill_switch_on_publishes_to_stream():
config = MagicMock(
kevin_enable_trading=True,
kevin_stop_loss_pct=0.08,
kevin_take_profit_pct=0.20,
kevin_avoid_blocks_days=7,
)
cursor = AsyncMock()
cursor.last_seen_id.return_value = 0
publisher = AsyncMock()
publisher.publish.return_value = "1234-0"
aggregator = AsyncMock()
aggregator.fetch_pending.return_value = [
MagicMock(
id=1,
symbol="NVDA",
action=MagicMock(value="buy"),
conviction=Decimal("0.8"),
effective_conviction=Decimal("0.8"),
time_horizon=MagicMock(value="weeks"),
),
]
strategy = AsyncMock()
from shared.schemas.kevin import KevinDecisionType
strategy.evaluate_mention.return_value = MagicMock(
decision=KevinDecisionType.OPEN_LONG,
symbol="NVDA",
target_dollars=Decimal("3000"),
holding_days=10,
effective_conviction=Decimal("0.8"),
rationale="ok",
)
audit_writer = AsyncMock()
broker = AsyncMock()
broker.is_asset_tradable.return_value = True
broker.get_latest_price.return_value = Decimal("100")
broker.get_account.return_value = MagicMock(
equity=Decimal("100000"), cash=Decimal("100000")
)
broker.get_positions.return_value = []
bridge = KevinBridge(
config=config,
cursor=cursor,
publisher=publisher,
aggregator=aggregator,
strategy=strategy,
audit_writer=audit_writer,
broker=broker,
)
await bridge.process_one_pass()
publisher.publish.assert_awaited_once()
cursor.advance.assert_awaited_with(1)
async def test_bridge_attaches_current_price_to_signal():
"""The bridge must propagate broker.get_latest_price onto the published
TradeSignal so the executor can size the position."""
config = MagicMock(
kevin_enable_trading=True,
kevin_stop_loss_pct=0.08,
kevin_take_profit_pct=0.20,
kevin_avoid_blocks_days=7,
)
cursor = AsyncMock()
cursor.last_seen_id.return_value = 0
publisher = AsyncMock()
publisher.publish.return_value = "1234-0"
aggregator = AsyncMock()
aggregator.fetch_pending.return_value = [
MagicMock(
id=1,
symbol="NVDA",
action=MagicMock(value="buy"),
conviction=Decimal("0.8"),
effective_conviction=Decimal("0.8"),
time_horizon=MagicMock(value="weeks"),
),
]
strategy = AsyncMock()
from shared.schemas.kevin import KevinDecisionType
strategy.evaluate_mention.return_value = MagicMock(
decision=KevinDecisionType.OPEN_LONG,
symbol="NVDA",
target_dollars=Decimal("3000"),
holding_days=10,
effective_conviction=Decimal("0.8"),
rationale="ok",
)
audit_writer = AsyncMock()
broker = AsyncMock()
broker.is_asset_tradable.return_value = True
broker.get_latest_price.return_value = Decimal("123.45")
broker.get_account.return_value = MagicMock(
equity=Decimal("100000"), cash=Decimal("100000")
)
broker.get_positions.return_value = []
bridge = KevinBridge(
config=config,
cursor=cursor,
publisher=publisher,
aggregator=aggregator,
strategy=strategy,
audit_writer=audit_writer,
broker=broker,
)
await bridge.process_one_pass()
publisher.publish.assert_awaited_once()
published_signal = publisher.publish.call_args[0][0]
assert published_signal.current_price == Decimal("123.45")
async def test_bridge_advances_cursor_only_after_publish():
"""Race-condition guard: cursor must NOT advance if publish raises."""
config = MagicMock(
kevin_enable_trading=True,
kevin_stop_loss_pct=0.08,
kevin_take_profit_pct=0.20,
kevin_avoid_blocks_days=7,
)
cursor = AsyncMock()
cursor.last_seen_id.return_value = 0
publisher = AsyncMock()
publisher.publish.side_effect = RuntimeError("XADD failed")
aggregator = AsyncMock()
aggregator.fetch_pending.return_value = [
MagicMock(
id=1,
symbol="NVDA",
action=MagicMock(value="buy"),
conviction=Decimal("0.8"),
effective_conviction=Decimal("0.8"),
time_horizon=MagicMock(value="weeks"),
),
]
strategy = AsyncMock()
from shared.schemas.kevin import KevinDecisionType
strategy.evaluate_mention.return_value = MagicMock(
decision=KevinDecisionType.OPEN_LONG,
symbol="NVDA",
target_dollars=Decimal("3000"),
holding_days=10,
effective_conviction=Decimal("0.8"),
rationale="ok",
)
audit_writer = AsyncMock()
broker = AsyncMock()
broker.is_asset_tradable.return_value = True
broker.get_latest_price.return_value = Decimal("100")
broker.get_account.return_value = MagicMock(
equity=Decimal("100000"), cash=Decimal("100000")
)
broker.get_positions.return_value = []
bridge = KevinBridge(
config=config,
cursor=cursor,
publisher=publisher,
aggregator=aggregator,
strategy=strategy,
audit_writer=audit_writer,
broker=broker,
)
await bridge.process_one_pass()
# cursor.advance must NOT have been called for mention 1
cursor.advance.assert_not_called()