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

@ -10,7 +10,7 @@ no Redis publish — strictly observational.
Env vars (or pass via flags):
TRADING_ANTHROPIC_OAUTH_TOKEN required (matches what the pod uses)
TRADING_MEET_KEVIN_LLM_MODEL default: claude-haiku-4-5-20251001
TRADING_MEET_KEVIN_PROMPT_VERSION default: v1
TRADING_MEET_KEVIN_PROMPT_VERSION default: v2
"""
from __future__ import annotations
@ -94,8 +94,8 @@ def print_analysis(video_id: str, captions, result) -> None:
return
print(f"\n TICKERS ({len(a.tickers)}):")
print(f" {'SYMBOL':<8} {'ACTION':<7} {'CONV':>6} {'HORIZON':<11} {'RATIONALE'}")
print(f" {'-'*8} {'-'*7} {'-'*6} {'-'*11} {'-'*60}")
print(f" {'SYMBOL':<8} {'ACTION':<7} {'EXPECTED':<12} {'CONV':>6} {'HORIZON':<11} {'RATIONALE'}")
print(f" {'-'*8} {'-'*7} {'-'*12} {'-'*6} {'-'*11} {'-'*60}")
# sort by action priority (buy/sell first), then conviction desc
action_rank = {"buy": 0, "sell": 1, "hold": 2, "watch": 3, "avoid": 4}
sorted_t = sorted(
@ -107,9 +107,12 @@ def print_analysis(video_id: str, captions, result) -> None:
)
for t in sorted_t:
rationale = (t.rationale_quote or "").replace("\n", " ")[:60]
expected = getattr(t, "expected_move", None)
expected_str = expected.value if expected is not None else ""
print(
f" {t.symbol:<8} "
f"{t.action.value:<7} "
f"{expected_str:<12} "
f"{fmt_pct(t.conviction):>6} "
f"{t.time_horizon.value:<11} "
f"{rationale}"
@ -127,7 +130,7 @@ async def main() -> None:
)
parser.add_argument(
"--prompt-version",
default=os.environ.get("TRADING_MEET_KEVIN_PROMPT_VERSION", "v1"),
default=os.environ.get("TRADING_MEET_KEVIN_PROMPT_VERSION", "v2"),
)
args = parser.parse_args()