market-aware AI cadence + incremental log updates

Two changes that together cut OpenRouter spend ~50% and give the daily
log temporal awareness.

1. CadencePolicy (app/services/cadence.py): expensive AI jobs only
   fire hourly during the EU/US active window (Mon-Fri 07-21 UTC).
   Off-hours weekdays throttle to every 4h; weekends to every 12h.
   ai_log_job and indicator_summary_job both consult the policy before
   doing real work; market/news/portfolio ingest jobs stay hourly
   (cheap, no API cost). Skipped runs land in job_runs with status
   'skipped' and the throttle reason in error.

2. Update mode for ai_log_job: when an earlier log exists for the
   current UTC day, it's passed to the model as 'Earlier log from
   today (generated HH:MM UTC)'. The system prompt grows an Update
   mode section instructing the model to revise — not restart — and
   anchor on what has CHANGED since the earlier draft. The TL;DR
   leads with intra-day change when meaningful, the watch list evolves
   rather than restarts. PROMPT_VERSION bumped to 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-16 10:17:39 +01:00
parent 2f223b75a3
commit 40cfb50e37
4 changed files with 157 additions and 6 deletions

View file

@ -20,7 +20,7 @@ OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
# Bump when the composed prompt changes meaningfully. Stored on every
# StrategicLog row so historical logs can be linked to the prompt that produced
# them.
PROMPT_VERSION = 4
PROMPT_VERSION = 5
# --- Core: invariant across tone/analysis settings ----------------------------
@ -99,7 +99,23 @@ Close the log with a single sentence on a line of its own, formatted exactly:
This is the line a reader who only sees the watch list scrolls down to. Make \
it earn its place: cite real signals (HY OAS, breadth, VIX, valuation, real \
yields), not vibes."""
yields), not vibes.
# Update mode (when an earlier log from today is provided)
If the user message includes a section labelled "Earlier log from today \
(generated HH:MM UTC)", treat that as YOUR OWN earlier draft. You are \
UPDATING it for the current data, not starting from scratch.
- Don't restate context that hasn't changed. Anchor on what's moved SINCE \
that timestamp: confirmations, refutations, new emergent patterns.
- The TL;DR should lead with the move since the earlier read when there \
was a meaningful intra-day change ("Since this morning's read, …") \
otherwise stay regime-level.
- The watch list should evolve: drop items that triggered or settled, add \
items that emerged. Keep items still load-bearing.
- Preserve any insights from the earlier draft that remain valid; sharpen \
or revise the ones that don't. Avoid contradicting yourself silently — if \
you change a stance, name it briefly ("Earlier I read X; with Y now, the \
read shifts to Z")."""
# --- Tone: audience-shaping block --------------------------------------------
@ -312,8 +328,11 @@ def build_user_prompt(
quotes_by_group: dict[str, list[dict]],
headlines_by_bucket: dict[str, list[dict]],
reference_line: str | None = None,
previous_log: object | None = None,
) -> str:
"""Assemble the user message from already-fetched-and-persisted data."""
"""Assemble the user message from already-fetched-and-persisted data.
If `previous_log` is a StrategicLog from earlier today, it's included
as 'Update mode' context the model will revise rather than restart."""
parts = [f"# Strategic log request — {today.strftime('%Y-%m-%d')}"]
if anchor:
parts.append(f"Anchor reference date: {anchor}")
@ -322,6 +341,20 @@ def build_user_prompt(
"\n## Reference snapshot (when the macro thesis was authored)"
f"\n{reference_line}\nCompare live readings against it."
)
if previous_log is not None:
gen = getattr(previous_log, "generated_at", None)
ts = gen.strftime("%H:%M UTC") if gen else "earlier today"
parts.append(
f"\n## Earlier log from today (generated {ts})\n"
"Treat this as YOUR OWN earlier draft for today. Update it for\n"
"the current data — don't restate unchanged context. See the\n"
"'Update mode' section of the system prompt for how to handle it.\n"
"```markdown\n"
f"{previous_log.content}\n"
"```"
)
parts.append("\n## Live market data (per group)")
parts.append("```json\n" + json.dumps(quotes_by_group, indent=2, default=str) + "\n```")
parts.append("\n## News flow (last 24h, filtered by bucket)")
@ -331,11 +364,20 @@ def build_user_prompt(
parts.append(f"\n### {label.upper()}")
for h in items[:30]:
parts.append(f"- [{h['when'][:16].replace('T',' ')}] [{h['source']}] {h['title']}")
parts.append(
task_line = (
"\n## Task\nWrite the daily strategic log in ~800 words, following "
"the discipline in the system prompt. No preamble; begin directly "
"with the date header."
)
if previous_log is not None:
task_line = (
"\n## Task\nUpdate the earlier log above for the current data. "
"Keep the same structure (date header, TL;DR, sections, watch "
"list, system temperature) but anchor on what has CHANGED since "
"the earlier draft's timestamp. ~800 words. No preamble."
)
parts.append(task_line)
return "\n".join(parts)