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:
OpenSquared
2026-06-23 18:00:46 +02:00
parent 70a9e2b569
commit 3b7fa35456
9 changed files with 1965 additions and 40 deletions

View File

@@ -1252,3 +1252,107 @@ export const useUnlinkReportDesk = () => {
},
})
}
// ── COT / Forward Curves / Surprise Index / Hawk-Dove — new types ─────────────
export interface CotEntry {
id: number
market_name: string
commodity: string
asset_class: string
report_date: string
mm_long: number
mm_short: number
open_interest: number
net_position: number
net_pct_oi: number
change_net: number
fetched_at: string
}
export interface ForwardCurveEntry {
id: number
asset: string
asset_class: string
front_price: number | null
far_price: number | null
slope_pct: number | null
structure: 'contango' | 'backwardation' | 'flat' | 'unknown'
months_spread: number
fetched_at: string
}
export interface TextSentimentResult {
score: number
label: string
summary: string
key_phrases: string[]
confidence: 'high' | 'medium' | 'low'
}
export interface ReportResultUpdate {
actual_value?: number | null
consensus_estimate?: number | null
text_sentiment_score?: number | null
text_sentiment_label?: string | null
text_sentiment_summary?: string | null
}
// ── COT Data ──────────────────────────────────────────────────────────────────
export function useCotData() {
return useQuery<CotEntry[]>({
queryKey: ['specialist-desks', 'cot'],
queryFn: () => api.get('/specialist-desks/cot').then(r => r.data),
staleTime: 1000 * 60 * 60, // 1h
})
}
export function useFetchCot() {
const qc = useQueryClient()
return useMutation({
mutationFn: () => api.post('/specialist-desks/cot/fetch').then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['specialist-desks', 'cot'] }),
})
}
// ── Forward Curves ────────────────────────────────────────────────────────────
export function useForwardCurves() {
return useQuery<ForwardCurveEntry[]>({
queryKey: ['specialist-desks', 'forward-curves'],
queryFn: () => api.get('/specialist-desks/forward-curves').then(r => r.data),
staleTime: 1000 * 60 * 60,
})
}
export function useFetchForwardCurves() {
const qc = useQueryClient()
return useMutation({
mutationFn: () => api.post('/specialist-desks/forward-curves/fetch').then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['specialist-desks', 'forward-curves'] }),
})
}
// ── Surprise Index ────────────────────────────────────────────────────────────
export function useUpdateReportResult() {
const qc = useQueryClient()
return useMutation({
mutationFn: ({ id, body }: { id: string; body: ReportResultUpdate }) =>
api.put(`/specialist-desks/reports/${id}/result`, body).then(r => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['specialist-reports'] })
qc.invalidateQueries({ queryKey: ['desk-configs'] })
},
})
}
// ── Hawk/Dove Text Scorer ─────────────────────────────────────────────────────
export function useScoreText() {
return useMutation({
mutationFn: (body: { text: string; report_name: string; desk: string }) =>
api.post('/specialist-desks/score-text', body).then(r => r.data) as Promise<TextSentimentResult>,
})
}