feat(meet-kevin): prompt v2 — forward-looking action + expected_move field
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled

User reported that the old prompt could emit 'sell' on backward-looking
capitulation ('Kevin sold after a 20% drop') — exactly the false signal
to avoid. v2 reframes every per-ticker field as forward-looking and
adds an explicit expected_move enum for the trading bot to weight.

Changes:
- New ExpectedMove enum (up_strong/up_mild/sideways/down_mild/
  down_strong/unknown) in shared/schemas + shared/models, with
  matching kevin_expected_move Postgres enum + column on
  kevin_stock_mentions (migration e5f6a7b8c9d0). NOT NULL with
  server_default 'unknown' so existing rows backfill cleanly.
- SYSTEM_PROMPT rewritten: action semantics now require a FORWARD
  view; reactive sells get downgraded to 'watch' or skipped; the
  rationale_quote must contain forward reasoning. Quality
  checklist updated.
- _ANALYSIS_TOOL JSON schema gains expected_move (required).
- prompt_version v1 → v2 in config + infra + ad-hoc CLI default.
- pipeline.py persists ticker.expected_move into the new column.

Migration safety: the column is NOT NULL DEFAULT 'unknown' so 96
existing mentions auto-fill with 'unknown' (no forward call known
for backward analyses) without breaking any reads.

Cost to backfill the 27 existing analyses with v2 prompt: ~$3 LLM
spend. A follow-up reanalyze script will replay them after this
ships.
This commit is contained in:
Viktor Barzin 2026-05-28 21:40:07 +00:00
parent 658c4d3221
commit 41ab95ec4d
7 changed files with 164 additions and 18 deletions

View file

@ -75,6 +75,21 @@ class KevinTimeHorizon(str, enum.Enum):
UNSPECIFIED = "unspecified"
class KevinExpectedMove(str, enum.Enum):
"""Forward-looking directional view over the mention's time_horizon.
Added in prompt_version v2 (2026-05-28) captures the LLM's prediction
of where the stock is going, separate from Kevin's reactive action.
"""
UP_STRONG = "up_strong"
UP_MILD = "up_mild"
SIDEWAYS = "sideways"
DOWN_MILD = "down_mild"
DOWN_STRONG = "down_strong"
UNKNOWN = "unknown"
# ---------------------------------------------------------------------------
# Models
# ---------------------------------------------------------------------------
@ -232,6 +247,15 @@ class KevinStockMention(Base):
video_timestamp_seconds: Mapped[int | None] = mapped_column(
Integer, nullable=True
)
expected_move: Mapped[KevinExpectedMove] = mapped_column(
SAEnum(
KevinExpectedMove,
name="kevin_expected_move",
values_callable=lambda x: [e.value for e in x],
),
nullable=False,
server_default=KevinExpectedMove.UNKNOWN.value,
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)

View file

@ -47,6 +47,23 @@ class MarketOutlook(str, Enum):
MIXED = "mixed"
class ExpectedMove(str, Enum):
"""Forward-looking directional view on a ticker over its time_horizon.
Independent of `action` action records what Kevin recommends doing,
expected_move records where the LLM thinks the stock is going next.
This is the field the bridge should weight when deciding whether to
paper-trade.
"""
UP_STRONG = "up_strong" # >= +5% over the horizon
UP_MILD = "up_mild" # +1% to +5%
SIDEWAYS = "sideways" # -1% to +1%
DOWN_MILD = "down_mild" # -5% to -1%
DOWN_STRONG = "down_strong" # <= -5%
UNKNOWN = "unknown" # Kevin made no directional call
class VideoStatus(str, Enum):
"""Status of a video in the processing pipeline."""
@ -92,6 +109,10 @@ class MeetKevinTickerMention(BaseModel):
video_timestamp_seconds: int | None = Field(
default=None, description="Timestamp for deep-link target"
)
expected_move: ExpectedMove = Field(
default=ExpectedMove.UNKNOWN,
description="Forward-looking directional view over time_horizon",
)
@field_validator("symbol")
@classmethod