The v2 prompt produces expected_move for every ticker mention. This
commit makes KevinStrategy.evaluate_mention USE it as a hard signal
rather than just a display field.
Three new rules, all guarded by KevinStrategyConfig knobs so the
behaviour can be turned off if it over-filters:
1) SELL + non-bearish expected_move => NO_OP (require_forward_for_
bearish, default True). This is THE anti-capitulation rule —
Kevin saying "I sold" without articulating where the stock goes
next becomes NO_OP. Reactive sells stop translating into
trades.
2) AVOID + bullish expected_move => NO_OP (don't close, don't
blocklist). Same idea — if the LLM's forward call contradicts the
avoid action, treat as inconsistent and skip.
3) BUY + bearish/sideways expected_move => NO_OP (schema veto).
Catches LLM inconsistency.
4) BUY + unknown expected_move => bump min_conviction floor by
unknown_conviction_bonus (default +0.05). Forces stronger
conviction when there's no forward direction.
Tests: 6 new (one per rule above), 22 regression — total 28 GREEN.
Backtest stub _mention factory now defaults expected_move from
action (buy/sell/avoid maps) so existing backtest scenarios stay
green; the test_backtest_sell_mid_position_closes_early case was
the only one that needed the fix.
Side note: strategy is backward-compatible. If a mention has no
expected_move attribute (e.g. v1 stub from older code), it defaults
to UNKNOWN and the legacy code paths still work — just with the
stricter conviction floor on buys.
Pipeline #46 surfaced two pre-existing CI bugs once fakeredis was
installed and tests could collect:
1. test_models.py:389 asserted "DISCOVERED" in status_col.type.enums,
but the model defines KevinVideoStatus with values_callable so
.enums returns the lowercase string values, not member names.
Asserting "discovered" instead.
2. Four test files use the db_session fixture which requires a real
Postgres on localhost:5432. CI has no Postgres, so 10 tests failed
with Connect call failed (errno 111). These genuinely need a DB —
mirroring tests/integration/* which already use
@pytest.mark.integration. Adding module-level
pytestmark = pytest.mark.integration to:
- tests/shared/models/test_meet_kevin_trading.py
- tests/services/kevin_signal_bridge/test_aggregator.py
- tests/services/kevin_signal_bridge/test_audit.py
- tests/services/kevin_signal_bridge/test_exit_scanner.py
CI runs with -m "not integration" so they're now deselected.
Local pytest still picks them up by default (no marker filter).
Stateless: mention + account_state -> KevinDecision. Conviction-weighted
sizing, time_horizon-derived hold periods, hard per-ticker cap. The
bridge and the backtest mini-engine both call evaluate_mention so
behaviour cannot drift.
3 tables (kevin_signal_bridge_state, kevin_backtest_runs,
kevin_backtest_trades) all UUID-keyed for consistency with Trade/Position.
KEVIN_STRATEGY_UUID constant pinned for FK joins from Trade.strategy_id.