feat: Specialist Desks v2 — COT, Forward Curves, Surprise Index, Hawk/Dove scorer
- COT Positioning: CFTC disaggregated + financial futures (19 markets) via Socrata free API net MM position % OI + weekly change stored in cot_data table - Forward Curves: yfinance front-month vs +3M slope (8 commodities) contango/backwardation/flat stored in forward_curve_data table - Surprise Index: consensus_estimate + actual_value on specialist_reports auto-computes surprise_score = actual - consensus on save - Hawk/Dove Text Scorer: GPT-4o-mini endpoint for CB statements score -1..+1, label, summary, key_phrases (forex/bonds: hawk/dove; commodities: bull/bear) - AI context injection: COT net positioning, forward curve structure, surprise scores, upcoming consensus estimates injected into all desk blocks - Frontend: COT panel (net% bars), Forward Curves panel, SurpriseInput on report cards, Hawk/Dove scorer in forex/bonds config tab - auto_cycle.py: non-blocking COT + curve refresh before each cycle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,13 @@ Specialist Desks — per asset-class fundamental configs + report catalogue.
|
||||
Prefix: /api/specialist-desks
|
||||
"""
|
||||
import uuid
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/specialist-desks", tags=["specialist-desks"])
|
||||
|
||||
|
||||
@@ -45,6 +48,20 @@ class ReportUpsert(BaseModel):
|
||||
desks: List[str] = []
|
||||
|
||||
|
||||
class ReportResultUpdate(BaseModel):
|
||||
actual_value: Optional[float] = None
|
||||
consensus_estimate: Optional[float] = None
|
||||
text_sentiment_score: Optional[float] = None
|
||||
text_sentiment_label: Optional[str] = None
|
||||
text_sentiment_summary: Optional[str] = None
|
||||
|
||||
|
||||
class ScoreTextRequest(BaseModel):
|
||||
text: str
|
||||
report_name: str = ""
|
||||
desk: str = "forex"
|
||||
|
||||
|
||||
# ── Configs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/configs")
|
||||
@@ -142,3 +159,79 @@ def unlink_from_desk(report_id: str, asset_class: str):
|
||||
from services.database import unlink_report_desk
|
||||
unlink_report_desk(report_id, asset_class)
|
||||
return {"unlinked": report_id, "desk": asset_class}
|
||||
|
||||
|
||||
# ── Surprise Index ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.put("/reports/{report_id}/result")
|
||||
def update_report_result(report_id: str, body: ReportResultUpdate):
|
||||
from services.database import update_report_result as _upd
|
||||
_upd(
|
||||
report_id=report_id,
|
||||
actual_value=body.actual_value,
|
||||
consensus_estimate=body.consensus_estimate,
|
||||
text_sentiment_score=body.text_sentiment_score,
|
||||
text_sentiment_label=body.text_sentiment_label,
|
||||
text_sentiment_summary=body.text_sentiment_summary,
|
||||
)
|
||||
return {"saved": report_id}
|
||||
|
||||
|
||||
# ── COT Data ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/cot")
|
||||
def get_cot_data():
|
||||
from services.database import get_latest_cot_data
|
||||
return get_latest_cot_data()
|
||||
|
||||
|
||||
@router.post("/cot/fetch")
|
||||
def fetch_cot():
|
||||
from services.cot_fetcher import fetch_all_cot
|
||||
from services.database import save_cot_data
|
||||
try:
|
||||
data = fetch_all_cot()
|
||||
if data:
|
||||
saved = save_cot_data(data)
|
||||
return {"fetched": len(data), "saved": saved}
|
||||
return {"fetched": 0, "saved": 0, "warning": "No COT data returned"}
|
||||
except Exception as e:
|
||||
logger.error(f"COT fetch error: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# ── Forward Curves ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/forward-curves")
|
||||
def get_forward_curves():
|
||||
from services.database import get_latest_forward_curves
|
||||
return get_latest_forward_curves()
|
||||
|
||||
|
||||
@router.post("/forward-curves/fetch")
|
||||
def fetch_forward_curves_endpoint():
|
||||
from services.forward_curve import fetch_forward_curves
|
||||
from services.database import save_forward_curves
|
||||
try:
|
||||
data = fetch_forward_curves()
|
||||
if data:
|
||||
saved = save_forward_curves(data)
|
||||
return {"fetched": len(data), "saved": saved}
|
||||
return {"fetched": 0, "saved": 0, "warning": "No curve data returned"}
|
||||
except Exception as e:
|
||||
logger.error(f"Forward curve fetch error: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# ── Hawk / Dove Text Scorer ────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/score-text")
|
||||
def score_text(body: ScoreTextRequest):
|
||||
"""Score a central bank / commodity report excerpt via GPT-4o-mini."""
|
||||
try:
|
||||
from services.ai_analyzer import score_report_text
|
||||
result = score_report_text(body.text, body.report_name, body.desk)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Text scoring error: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
Reference in New Issue
Block a user