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:
@@ -14,7 +14,7 @@ Cycle steps:
|
||||
import logging
|
||||
import threading
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -551,6 +551,16 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
except Exception as _cbe:
|
||||
logger.warning(f"[Cycle] Convergence block build failed (non-blocking): {_cbe}")
|
||||
|
||||
# ── Build institutional reports block ──────────────────────────
|
||||
_institutional_block = ""
|
||||
try:
|
||||
from services.ai_analyzer import build_institutional_block
|
||||
_institutional_block = build_institutional_block(days=7)
|
||||
if _institutional_block:
|
||||
logger.info(f"[Cycle {run_id[:16]}] Institutional block injected")
|
||||
except Exception as _ibe:
|
||||
logger.warning(f"[Cycle] Institutional block failed (non-blocking): {_ibe}")
|
||||
|
||||
suggestions = suggest_patterns_from_market_context(
|
||||
news, quotes, calendar, macro_regime=macro_regime, geo_score=geo_score_obj,
|
||||
portfolio_lessons=portfolio_lessons,
|
||||
@@ -705,6 +715,7 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
fred_block=_fred_block,
|
||||
price_discovery_block=_price_discovery_block,
|
||||
portfolio_context_block=_portfolio_block,
|
||||
institutional_block=_institutional_block,
|
||||
run_id=run_id,
|
||||
)
|
||||
scored_with_id = [s for s in scored if s.get("pattern_id")]
|
||||
@@ -991,6 +1002,56 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
_current_status["last_run_at"] = datetime.utcnow().isoformat()
|
||||
logger.info(f"[Cycle {run_id[:16]}] Completed — {added_count} new patterns, {len(scored)} scored")
|
||||
|
||||
# ── Step 6.1: Institutional report absorption tracking ───────────────
|
||||
try:
|
||||
from services.database import get_conn as _get_conn
|
||||
import json as _json
|
||||
_commentary_text = ""
|
||||
if commentary:
|
||||
try:
|
||||
_c = _json.loads(commentary) if isinstance(commentary, str) else commentary
|
||||
_commentary_text = " ".join([
|
||||
str(_c.get("narrative", "")),
|
||||
str(_c.get("key_catalyst", "")),
|
||||
" ".join(str(x) for x in (_c.get("risks", []) or [])),
|
||||
]).lower()
|
||||
except Exception:
|
||||
_commentary_text = str(commentary).lower()
|
||||
|
||||
if _commentary_text:
|
||||
_conn = _get_conn()
|
||||
try:
|
||||
cutoff = (datetime.utcnow() - timedelta(days=14)).strftime("%Y-%m-%d")
|
||||
_inst_rows = _conn.execute(
|
||||
"SELECT id, key_points_json FROM institutional_reports "
|
||||
"WHERE report_date >= ? AND (absorbed_score IS NULL OR absorbed_score = 0)",
|
||||
(cutoff,),
|
||||
).fetchall()
|
||||
for _ir in _inst_rows:
|
||||
try:
|
||||
_kps = _json.loads(_ir["key_points_json"] or "[]")
|
||||
_kp_words = set(
|
||||
w for kp in _kps for w in kp.lower().split()
|
||||
if len(w) > 4
|
||||
)
|
||||
if not _kp_words:
|
||||
continue
|
||||
_overlap = sum(1 for w in _kp_words if w in _commentary_text)
|
||||
_score = min(1.0, _overlap / max(len(_kp_words), 1))
|
||||
if _score > 0:
|
||||
_conn.execute(
|
||||
"UPDATE institutional_reports SET absorbed_score=?, injected_in_cycle_id=? WHERE id=?",
|
||||
(round(_score, 3), run_id, _ir["id"]),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
_conn.commit()
|
||||
logger.info(f"[Cycle {run_id[:16]}] Absorption tracked for {len(_inst_rows)} institutional reports")
|
||||
finally:
|
||||
_conn.close()
|
||||
except Exception as _abs_e:
|
||||
logger.warning(f"[Cycle] Absorption tracking failed (non-blocking): {_abs_e}")
|
||||
|
||||
# ── Step 6.5: Generate full cycle report ─────────────────────────────
|
||||
try:
|
||||
_cycle_report = _generate_cycle_report(
|
||||
|
||||
Reference in New Issue
Block a user