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:
OpenSquared
2026-06-23 09:57:18 +02:00
parent f2dd859ba0
commit 2fb683eec5
8 changed files with 1252 additions and 2 deletions

View File

@@ -376,6 +376,54 @@ SCORE DISTRIBUTION RULE (MANDATORY):
- A pattern with 3+ relevant news AND favorable macro can reach 70-85/100"""
def _build_specialist_context_block(asset_classes: set) -> str:
"""Build a SPECIALIST DESK CONTEXT block for the given asset classes."""
try:
from services.database import get_asset_class_config, get_desk_reports
from datetime import datetime as _dt
blocks = []
for ac in sorted(asset_classes):
cfg = get_asset_class_config(ac)
if not cfg:
continue
reports = get_desk_reports(ac)
lines = [f"\n## SPECIALIST DESK — {cfg['display_name'].upper()} {cfg.get('icon','')}"]
if cfg.get("fundamentals"):
lines.append(f"Key drivers: {cfg['fundamentals'][:300]}")
# Macro sensitivity
sensitivities = cfg.get("macro_sensitivity") or []
if sensitivities:
sens_str = " | ".join(
f"{s.get('regime','?')}{s.get('effect','?')}"
for s in sensitivities[:4]
)
lines.append(f"Regime sensitivity: {sens_str}")
# Price thresholds
thresh = cfg.get("price_delta_thresholds") or {}
if thresh:
lines.append(
f"Price thresholds: significant_week={thresh.get('significant_week','?')}% "
f"extreme_week={thresh.get('extreme_week','?')}%"
)
# Upcoming reports
today = _dt.utcnow().strftime("%Y-%m-%d")
upcoming = sorted(
[r for r in reports if r.get("next_date") and r["next_date"] >= today],
key=lambda r: r["next_date"]
)[:5]
if upcoming:
lines.append("Upcoming reports:")
for r in upcoming:
lines.append(
f"{r['name']} ({r['source']}) — {r['next_date']} [{r['cadence']}]"
+ (f" ⭐×{r['importance']}" if r.get("importance", 1) >= 3 else "")
)
blocks.append("\n".join(lines))
return "\n".join(blocks) + "\n" if blocks else ""
except Exception:
return ""
def score_patterns_with_context(
patterns: List[Dict],
recent_news: List[Dict],
@@ -783,12 +831,18 @@ Lessons: {' | '.join(str(l)[:80] for l in lessons[:3])}
"""
# Build specialist-desk blocks for asset classes present in this pattern set
_all_pattern_asset_classes = {
p.get("asset_class", "") for p in patterns if p.get("asset_class")
}
specialist_block = _build_specialist_context_block(_all_pattern_asset_classes)
# Build the per-batch prompt template (static parts)
prompt_header = f"""GLOBAL CONTEXT:
- Geopolitical risk score: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')})
- Top risks: {geo_score.get('top_risks', [])}
{macro_section}{lessons_header}{iv_context}
{risk_context}
{risk_context}{specialist_block}
SCORING TEMPLATE:
{scoring_template}