Files
OpenFin/backend/routers/specialist_desks.py
OpenSquared 3b7fa35456 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>
2026-06-23 18:00:46 +02:00

238 lines
8.5 KiB
Python

"""
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"])
# ── Pydantic models ────────────────────────────────────────────────────────────
class MacroSensitivity(BaseModel):
regime: str
effect: str
class PriceDeltaThresholds(BaseModel):
significant_day: float = 1.0
extreme_day: float = 3.0
significant_week: float = 2.0
extreme_week: float = 5.0
class DeskConfigUpdate(BaseModel):
display_name: str
icon: str = ""
fundamentals: str = ""
macro_sensitivity: List[MacroSensitivity] = []
price_delta_thresholds: PriceDeltaThresholds = PriceDeltaThresholds()
notes: str = ""
class ReportUpsert(BaseModel):
name: str
source: str = ""
cadence: str = "monthly"
next_date: Optional[str] = None
last_date: Optional[str] = None
importance: int = 2
notes: str = ""
url: str = ""
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")
def list_configs():
from services.database import get_all_asset_class_configs
return get_all_asset_class_configs()
@router.get("/configs/{asset_class}")
def get_config(asset_class: str):
from services.database import get_asset_class_config, get_desk_reports
cfg = get_asset_class_config(asset_class)
if not cfg:
raise HTTPException(404, f"Desk '{asset_class}' not found")
cfg["reports"] = get_desk_reports(asset_class)
return cfg
@router.put("/configs/{asset_class}")
def update_config(asset_class: str, body: DeskConfigUpdate):
from services.database import upsert_asset_class_config
upsert_asset_class_config(
asset_class=asset_class,
display_name=body.display_name,
icon=body.icon,
fundamentals=body.fundamentals,
macro_sensitivity=[s.dict() for s in body.macro_sensitivity],
price_delta_thresholds=body.price_delta_thresholds.dict(),
notes=body.notes,
)
return {"saved": asset_class}
# ── Reports ────────────────────────────────────────────────────────────────────
@router.get("/reports")
def list_reports():
from services.database import get_all_specialist_reports
return get_all_specialist_reports()
@router.post("/reports")
def create_report(body: ReportUpsert):
from services.database import upsert_specialist_report, link_report_desk
report_id = "RPT_" + uuid.uuid4().hex[:8].upper()
upsert_specialist_report(
report_id=report_id,
name=body.name, source=body.source, cadence=body.cadence,
next_date=body.next_date, last_date=body.last_date,
importance=body.importance, notes=body.notes, url=body.url,
)
for ac in body.desks:
link_report_desk(report_id, ac)
return {"id": report_id}
@router.put("/reports/{report_id}")
def update_report(report_id: str, body: ReportUpsert):
from services.database import upsert_specialist_report, link_report_desk, unlink_report_desk, get_all_specialist_reports
# Check exists
all_reports = get_all_specialist_reports()
if not any(r["id"] == report_id for r in all_reports):
raise HTTPException(404, "Report not found")
upsert_specialist_report(
report_id=report_id,
name=body.name, source=body.source, cadence=body.cadence,
next_date=body.next_date, last_date=body.last_date,
importance=body.importance, notes=body.notes, url=body.url,
)
# Sync desk links: remove all, re-add
existing = next(r for r in all_reports if r["id"] == report_id)
for old_ac in existing.get("desks", []):
unlink_report_desk(report_id, old_ac)
for new_ac in body.desks:
link_report_desk(report_id, new_ac)
return {"saved": report_id}
@router.delete("/reports/{report_id}")
def delete_report(report_id: str):
from services.database import delete_specialist_report
delete_specialist_report(report_id)
return {"deleted": report_id}
@router.post("/reports/{report_id}/link/{asset_class}")
def link_to_desk(report_id: str, asset_class: str):
from services.database import link_report_desk
link_report_desk(report_id, asset_class)
return {"linked": report_id, "desk": asset_class}
@router.delete("/reports/{report_id}/link/{asset_class}")
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))