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:
parent
2f223b75a3
commit
40cfb50e37
4 changed files with 157 additions and 6 deletions
66
app/services/cadence.py
Normal file
66
app/services/cadence.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""When should expensive AI jobs fire?
|
||||
|
||||
Markets matter. The scheduler wakes every hour, but there's no point spending
|
||||
OpenRouter tokens at 03:00 UTC on a Sunday when nothing has moved. This module
|
||||
encodes a single policy: weekday active hours (LSE open through NYSE close,
|
||||
roughly 07:00-21:00 UTC) get the full hourly cadence; off-hours and weekends
|
||||
get throttled.
|
||||
|
||||
Used by ai_log_job and indicator_summary_job to decide whether to run NOW or
|
||||
skip until enough time has passed since the last successful run. Market /
|
||||
news / portfolio ingestion jobs keep running hourly — they're cheap.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CadencePolicy:
|
||||
# Active window in UTC. LSE opens 07:00 BST → 07:00 UTC summer, 08:00 UTC
|
||||
# winter. NYSE closes 16:00 ET → 21:00 UTC summer, 21:00 UTC winter. The
|
||||
# combined EU/US trading window is well covered by 07:00-21:00 UTC.
|
||||
active_start_hour: int = 7
|
||||
active_end_hour: int = 21
|
||||
# Minimum gap between successful runs outside the active window.
|
||||
off_hours_gap_h: float = 4.0
|
||||
weekend_gap_h: float = 12.0
|
||||
|
||||
def is_active_window(self, now: datetime | None = None) -> bool:
|
||||
now = now or datetime.now(timezone.utc)
|
||||
if now.weekday() >= 5: # Saturday / Sunday
|
||||
return False
|
||||
return self.active_start_hour <= now.hour < self.active_end_hour
|
||||
|
||||
def min_gap_hours(self, now: datetime | None = None) -> float:
|
||||
now = now or datetime.now(timezone.utc)
|
||||
if now.weekday() >= 5:
|
||||
return self.weekend_gap_h
|
||||
if self.is_active_window(now):
|
||||
return 0.0 # always run during the active window
|
||||
return self.off_hours_gap_h
|
||||
|
||||
def should_run(
|
||||
self,
|
||||
last_success_at: datetime | None,
|
||||
now: datetime | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""Returns (should_run, reason). The reason is human-readable for logs
|
||||
and the job_runs.error column when a run is skipped."""
|
||||
now = now or datetime.now(timezone.utc)
|
||||
if self.is_active_window(now):
|
||||
return True, "active window"
|
||||
min_gap = self.min_gap_hours(now)
|
||||
if last_success_at is None:
|
||||
return True, "no prior successful run"
|
||||
# Normalise tz; DB returns naive but we treat it as UTC.
|
||||
if last_success_at.tzinfo is None:
|
||||
last_success_at = last_success_at.replace(tzinfo=timezone.utc)
|
||||
age_h = (now - last_success_at).total_seconds() / 3600.0
|
||||
if age_h >= min_gap:
|
||||
return True, f"off-hours but last run {age_h:.1f}h ago (≥ {min_gap}h)"
|
||||
return False, f"off-hours throttled — last run {age_h:.1f}h ago (< {min_gap}h)"
|
||||
|
||||
|
||||
DEFAULT_POLICY = CadencePolicy()
|
||||
Loading…
Add table
Add a link
Reference in a new issue