feat: Specialist Desks — per asset-class fundamental configs + report catalogue
- 7 pre-seeded desks (Forex, Metals, Agri, Energy, Indices, Crypto, Bonds) each with default fundamental drivers, macro regime sensitivities and price delta thresholds - Global report catalogue (specialist_reports) fully manual — add any report including non-calendar ones (e.g. Cocoa Grinding Report, ICCO) - Many-to-many report ↔ desk linking (report_desk_links table) - 12 default reports pre-seeded (COT, EIA, WASDE, FOMC, ECB, CPI, NFP…) - AI scorer injects SPECIALIST DESK context block for asset classes present in each scoring batch (upcoming reports, key drivers, regime sensitivity) - /specialist-desks page: desk sidebar + fundamentals editor + macro sensitivity tag editor + reports tab + global reports catalogue + modal to create/edit any report with desk assignment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
144
backend/routers/specialist_desks.py
Normal file
144
backend/routers/specialist_desks.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Specialist Desks — per asset-class fundamental configs + report catalogue.
|
||||
Prefix: /api/specialist-desks
|
||||
"""
|
||||
import uuid
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
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] = []
|
||||
|
||||
|
||||
# ── 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}
|
||||
Reference in New Issue
Block a user