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:
OpenSquared
2026-06-22 13:45:07 +02:00
parent 4423ad91db
commit 3edbd6b0b7
12 changed files with 1228 additions and 2 deletions

View File

@@ -393,6 +393,7 @@ def score_patterns_with_context(
fred_block: str = "",
price_discovery_block: str = "",
portfolio_context_block: str = "",
institutional_block: str = "",
run_id: str = "",
) -> List[Dict[str, Any]]:
"""Score all patterns with rich context (news, prices, IV, risk clusters) using GPT-4o."""
@@ -616,6 +617,7 @@ Scoring instructions:
_fred_sc_section = f"\n{fred_block}\n" if fred_block else ""
_pd_sc_section = f"\n{price_discovery_block}\n" if price_discovery_block else ""
_portfolio_sc_section = f"\n{portfolio_context_block}\n" if portfolio_context_block else ""
_inst_sc_section = f"\n{institutional_block}\n" if institutional_block else ""
user = f"""GLOBAL CONTEXT:
- Geopolitical risk score: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')})
@@ -625,6 +627,7 @@ Scoring instructions:
{_fred_sc_section}
{_pd_sc_section}
{_tech_sc_section}
{_inst_sc_section}
{_portfolio_sc_section}
SCORING TEMPLATE:
{scoring_template}
@@ -1194,6 +1197,51 @@ def _build_temporal_news_block(partitioned: Dict[str, List], cycle_meta: Dict) -
return block
def build_institutional_block(days: int = 7) -> str:
"""Build a concise institutional reports block for injection into AI prompts."""
try:
from services.database import get_conn
conn = get_conn()
try:
from datetime import datetime as _dt, timedelta as _td
cutoff = (_dt.utcnow() - _td(days=days)).strftime("%Y-%m-%d")
rows = conn.execute(
"SELECT report_type, report_date, key_points_json, trading_implications, "
"signal_energy, signal_metals, signal_indices, signal_forex, importance "
"FROM institutional_reports WHERE report_date >= ? ORDER BY report_date DESC LIMIT 6",
(cutoff,),
).fetchall()
finally:
conn.close()
if not rows:
return ""
import json as _json
lines = ["## INSTITUTIONAL REPORTS (CFTC COT + EIA — last 7 days)"]
for r in rows:
rtype = r["report_type"].upper()
rdate = r["report_date"]
importance_str = "★★★" if r["importance"] == 3 else "★★" if r["importance"] == 2 else ""
signals = (
f"Energy={r['signal_energy']} | Metals={r['signal_metals']} | "
f"Indices={r['signal_indices']} | Forex={r['signal_forex']}"
)
lines.append(f"\n### {rtype} {rdate} {importance_str}{signals}")
try:
kps = _json.loads(r["key_points_json"] or "[]")
for kp in kps[:4]:
lines.append(f"{kp}")
except Exception:
pass
if r["trading_implications"]:
lines.append(f" → Implications: {r['trading_implications'][:200]}")
return "\n".join(lines)
except Exception as _e:
return ""
def suggest_patterns_from_market_context(
news: List[Dict],
quotes_by_class: Dict[str, List[Dict]],