feat: institutional reports — CFTC COT + EIA petroleum weekly
- New institutional_reports table (DB) with importance, signals per asset class, key points, absorption tracking
- cot_fetcher.py: CFTC Socrata API (6dca-aqww), 7 instruments (Gold/Silver/Copper/WTI/NatGas/SP500/EURUSD), net positioning + 52-week z-score
- eia_fetcher.py: EIA API v2, 4 series (crude/Cushing/gasoline/distillates), WoW surprise detection
- institutional.py router: GET /reports, GET /reports/{id}, POST /refresh, GET /stats
- institutional_scheduler.py: weekly auto-fetch (COT Saturdays, EIA Wednesday afternoons)
- ai_analyzer.py: build_institutional_block() + institutional_block param injected into AI scoring prompt
- auto_cycle.py: inject institutional block into suggestion + scoring, absorption tracking via keyword overlap after each cycle commentary
- InstitutionalReports.tsx: full page with filter bar (type/category/importance/period), cards with key point bullets, EXTREME alerts highlighted, signal badges, absorption badge, trading implications, expandable detail
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
104
backend/services/institutional_scheduler.py
Normal file
104
backend/services/institutional_scheduler.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Weekly scheduler for institutional reports:
|
||||
- CFTC COT: released every Friday ~15:30 ET → fetch Saturday UTC
|
||||
- EIA Petroleum Weekly: released every Wednesday ~10:30 ET → fetch Wednesday afternoon UTC
|
||||
Checks once per hour; only fetches when the day matches and report not yet fetched this week.
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_stop_event = threading.Event()
|
||||
_thread: Optional[threading.Thread] = None
|
||||
_CHECK_INTERVAL_S = 3600 # check every hour
|
||||
|
||||
|
||||
def _should_fetch_cot() -> bool:
|
||||
"""Saturday UTC = day after COT release."""
|
||||
now = datetime.utcnow()
|
||||
return now.weekday() == 5 # Saturday
|
||||
|
||||
|
||||
def _should_fetch_eia() -> bool:
|
||||
"""Wednesday afternoon UTC."""
|
||||
now = datetime.utcnow()
|
||||
return now.weekday() == 2 and now.hour >= 16 # Wednesday ≥16:00 UTC
|
||||
|
||||
|
||||
def _last_fetch_date(report_type: str) -> Optional[str]:
|
||||
try:
|
||||
from services.database import get_conn
|
||||
conn = get_conn()
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT MAX(fetch_date) FROM institutional_reports WHERE report_type=?",
|
||||
(report_type,),
|
||||
).fetchone()
|
||||
return (row[0] or "")[:10] if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _run_loop():
|
||||
logger.info("[InstitutionalScheduler] Started")
|
||||
while not _stop_event.is_set():
|
||||
try:
|
||||
today = datetime.utcnow().strftime("%Y-%m-%d")
|
||||
|
||||
if _should_fetch_cot():
|
||||
last = _last_fetch_date("cot")
|
||||
if last != today:
|
||||
logger.info("[InstitutionalScheduler] Fetching COT...")
|
||||
try:
|
||||
from services.cot_fetcher import fetch_cot_report
|
||||
from routers.institutional import save_institutional_report
|
||||
report = fetch_cot_report()
|
||||
if report:
|
||||
save_institutional_report(report)
|
||||
logger.info(f"[InstitutionalScheduler] COT saved: {report['report_date']}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[InstitutionalScheduler] COT fetch failed: {e}")
|
||||
|
||||
if _should_fetch_eia():
|
||||
last = _last_fetch_date("eia")
|
||||
if last != today:
|
||||
logger.info("[InstitutionalScheduler] Fetching EIA...")
|
||||
try:
|
||||
from services.database import get_config
|
||||
from services.eia_fetcher import fetch_eia_report
|
||||
from routers.institutional import save_institutional_report
|
||||
key = get_config("eia_api_key") or ""
|
||||
if key:
|
||||
report = fetch_eia_report(key)
|
||||
if report:
|
||||
save_institutional_report(report)
|
||||
logger.info(f"[InstitutionalScheduler] EIA saved: {report['report_date']}")
|
||||
else:
|
||||
logger.info("[InstitutionalScheduler] EIA skipped — no API key configured")
|
||||
except Exception as e:
|
||||
logger.warning(f"[InstitutionalScheduler] EIA fetch failed: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[InstitutionalScheduler] Loop error: {e}")
|
||||
|
||||
_stop_event.wait(_CHECK_INTERVAL_S)
|
||||
|
||||
logger.info("[InstitutionalScheduler] Stopped")
|
||||
|
||||
|
||||
def start_institutional_scheduler():
|
||||
global _thread
|
||||
_stop_event.clear()
|
||||
_thread = threading.Thread(target=_run_loop, daemon=True, name="institutional-scheduler")
|
||||
_thread.start()
|
||||
|
||||
|
||||
def stop_institutional_scheduler():
|
||||
_stop_event.set()
|
||||
if _thread and _thread.is_alive():
|
||||
_thread.join(timeout=5)
|
||||
Reference in New Issue
Block a user