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:
@@ -377,10 +377,21 @@ SCORE DISTRIBUTION RULE (MANDATORY):
|
||||
|
||||
|
||||
def _build_specialist_context_block(asset_classes: set) -> str:
|
||||
"""Build a SPECIALIST DESK CONTEXT block for the given asset classes."""
|
||||
"""Build SPECIALIST DESK CONTEXT block including COT, forward curves, surprise scores."""
|
||||
try:
|
||||
from services.database import get_asset_class_config, get_desk_reports
|
||||
from services.database import get_asset_class_config, get_desk_reports, get_latest_cot_data, get_latest_forward_curves
|
||||
from datetime import datetime as _dt
|
||||
|
||||
# Fetch COT and forward curve data once
|
||||
try:
|
||||
all_cot = get_latest_cot_data()
|
||||
except Exception:
|
||||
all_cot = []
|
||||
try:
|
||||
all_curves = get_latest_forward_curves()
|
||||
except Exception:
|
||||
all_curves = []
|
||||
|
||||
blocks = []
|
||||
for ac in sorted(asset_classes):
|
||||
cfg = get_asset_class_config(ac)
|
||||
@@ -388,16 +399,19 @@ def _build_specialist_context_block(asset_classes: set) -> str:
|
||||
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','?')}"
|
||||
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:
|
||||
@@ -405,7 +419,30 @@ def _build_specialist_context_block(asset_classes: set) -> str:
|
||||
f"Price thresholds: significant_week={thresh.get('significant_week','?')}% "
|
||||
f"extreme_week={thresh.get('extreme_week','?')}%"
|
||||
)
|
||||
# Upcoming reports
|
||||
|
||||
# COT positioning for this asset class
|
||||
cot_ac = [c for c in all_cot if c.get("asset_class") == ac]
|
||||
if cot_ac:
|
||||
lines.append("COT Positioning (Money Managers net, % of OI):")
|
||||
for c in cot_ac[:5]:
|
||||
net_pct = c.get("net_pct_oi", 0)
|
||||
chg = c.get("change_net", 0)
|
||||
direction = "LONG" if net_pct > 5 else "SHORT" if net_pct < -5 else "NEUTRAL"
|
||||
chg_str = f" (chg {'+' if chg >= 0 else ''}{chg:,} wk)" if chg else ""
|
||||
lines.append(f" - {c['commodity']}: net={net_pct:+.1f}% OI [{direction}]{chg_str} -- {c.get('report_date','?')}")
|
||||
|
||||
# Forward curves for this asset class
|
||||
curves_ac = [c for c in all_curves if c.get("asset_class") == ac]
|
||||
if curves_ac:
|
||||
lines.append("Forward Curve Structure (spot vs +3M):")
|
||||
for curve in curves_ac[:5]:
|
||||
struct = curve.get("structure", "unknown")
|
||||
slope = curve.get("slope_pct")
|
||||
slope_str = f" ({slope:+.1f}%)" if slope is not None else ""
|
||||
struct_label = struct.upper()
|
||||
lines.append(f" - {curve['asset']}: {struct_label}{slope_str}")
|
||||
|
||||
# Recent reports with surprise scores
|
||||
today = _dt.utcnow().strftime("%Y-%m-%d")
|
||||
upcoming = sorted(
|
||||
[r for r in reports if r.get("next_date") and r["next_date"] >= today],
|
||||
@@ -414,16 +451,85 @@ def _build_specialist_context_block(asset_classes: set) -> str:
|
||||
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 "")
|
||||
)
|
||||
line = f" - {r['name']} ({r['source']}) -- {r['next_date']} [{r['cadence']}]"
|
||||
if r.get("importance", 1) >= 3:
|
||||
line += " ***"
|
||||
if r.get("consensus_estimate") is not None:
|
||||
line += f" [consensus: {r['consensus_estimate']}]"
|
||||
lines.append(line)
|
||||
|
||||
# Recent releases with surprise scores
|
||||
recent_releases = sorted(
|
||||
[r for r in reports if r.get("last_date") and r.get("surprise_score") is not None],
|
||||
key=lambda r: r.get("last_date", ""),
|
||||
reverse=True
|
||||
)[:3]
|
||||
if recent_releases:
|
||||
lines.append("Recent releases (surprise index = actual - consensus):")
|
||||
for r in recent_releases:
|
||||
surprise = r["surprise_score"]
|
||||
label = r.get("text_sentiment_label", "")
|
||||
label_str = f" [{label}]" if label else ""
|
||||
sign = "+" if surprise >= 0 else ""
|
||||
lines.append(f" - {r['name']}: surprise={sign}{surprise:.2f}{label_str} -- {r.get('last_date','?')}")
|
||||
|
||||
blocks.append("\n".join(lines))
|
||||
return "\n".join(blocks) + "\n" if blocks else ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def score_report_text(text: str, report_name: str = "", desk: str = "forex") -> Dict:
|
||||
"""Score a report/statement excerpt. Returns score (-1 to +1), label, summary, key_phrases."""
|
||||
if desk in ("forex", "bonds"):
|
||||
dimension = "monetary policy stance: -1 = very dovish (rate cuts expected), 0 = neutral, +1 = very hawkish (rate hikes expected)"
|
||||
examples = "Hawkish: 'inflation remains persistent', 'further tightening may be appropriate'. Dovish: 'inflation returning to target', 'downside risks dominate', 'easing cycle beginning'."
|
||||
elif desk in ("energy", "metals", "agri"):
|
||||
dimension = "commodity market outlook: -1 = very bearish (oversupply, demand weakness), 0 = neutral, +1 = very bullish (deficit, demand surge)"
|
||||
examples = "Bullish: 'output cuts extended', 'crop failure', 'stockpiles at multi-year low'. Bearish: 'surplus expected', 'demand revision lower', 'record production'."
|
||||
else:
|
||||
dimension = "market sentiment: -1 = very bearish, 0 = neutral, +1 = very bullish"
|
||||
examples = ""
|
||||
|
||||
prompt = f"""You are a macro trading analyst. Analyze the following text and return ONLY valid JSON.
|
||||
|
||||
Report: {report_name or 'Unknown'}
|
||||
Desk: {desk}
|
||||
Score dimension: {dimension}
|
||||
{examples}
|
||||
|
||||
TEXT TO ANALYZE:
|
||||
{text[:3000]}
|
||||
|
||||
Return JSON with exactly these fields:
|
||||
{{
|
||||
"score": <float from -1.0 to +1.0>,
|
||||
"label": <"very_hawkish" | "hawkish" | "neutral" | "dovish" | "very_dovish"> (for forex/bonds)
|
||||
or <"very_bullish" | "bullish" | "neutral" | "bearish" | "very_bearish"> (for others),
|
||||
"summary": <1-2 sentence summary of the key signal>,
|
||||
"key_phrases": [<up to 5 most significant phrases that drove the score>],
|
||||
"confidence": <"high" | "medium" | "low">
|
||||
}}"""
|
||||
|
||||
try:
|
||||
import openai as _oai
|
||||
from services.database import get_config as _gc
|
||||
api_key = _gc("openai_api_key") or ""
|
||||
if not api_key:
|
||||
return {"score": 0, "label": "neutral", "summary": "No API key configured", "key_phrases": [], "confidence": "low"}
|
||||
client = _oai.OpenAI(api_key=api_key)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.1,
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
import json
|
||||
return json.loads(response.choices[0].message.content)
|
||||
except Exception as e:
|
||||
return {"score": 0, "label": "neutral", "summary": f"Error: {str(e)}", "key_phrases": [], "confidence": "low"}
|
||||
|
||||
|
||||
def score_patterns_with_context(
|
||||
patterns: List[Dict],
|
||||
recent_news: List[Dict],
|
||||
|
||||
Reference in New Issue
Block a user