/** * Typed TanStack Query hooks for all API domains. * Requirements: 13.1, 13.2 */ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { apiGet, apiPost, apiPut } from './client'; import type { ApiBase } from './client'; // --------------------------------------------------------------------------- // Generic helpers // --------------------------------------------------------------------------- function useGet(key: unknown[], api: ApiBase, path: string, enabled = true) { return useQuery({ queryKey: key, queryFn: () => apiGet(api, path), enabled }); } // --------------------------------------------------------------------------- // Companies (Query API) // --------------------------------------------------------------------------- export interface Company { id: string; ticker: string; legal_name: string; exchange: string | null; sector: string | null; industry: string | null; market_cap_bucket: string | null; active: boolean; created_at?: string; updated_at?: string; aliases?: Alias[]; active_source_count?: number; } export interface Alias { id: string; alias: string; alias_type: string; } export interface Source { id: string; source_type: string; source_name: string; config?: Record; credibility_score: number; retention_days?: number; access_policy?: string; active: boolean; } export function useCompanies(params?: { active?: boolean; sector?: string; ticker?: string }) { const qs = new URLSearchParams(); if (params?.active !== undefined) qs.set('active', String(params.active)); if (params?.sector) qs.set('sector', params.sector); if (params?.ticker) qs.set('ticker', params.ticker); const path = `/api/companies${qs.toString() ? '?' + qs : ''}`; return useGet(['companies', params], 'query', path); } export function useCompany(id: string | undefined) { return useGet(['company', id], 'query', `/api/companies/${id}`, !!id); } export function useCompanySources(companyId: string | undefined) { return useGet(['company-sources', companyId], 'query', `/api/companies/${companyId}/sources`, !!companyId); } // --------------------------------------------------------------------------- // Symbol Registry (CRUD) // --------------------------------------------------------------------------- export function useRegistryCompanies(active = true) { return useGet(['registry-companies', active], 'registry', `/companies?active=${active}`); } export function useCreateCompany() { const qc = useQueryClient(); return useMutation({ mutationFn: (body: { ticker: string; legal_name: string; sector?: string; industry?: string; exchange?: string; market_cap_bucket?: string }) => apiPost('registry', '/companies', body), onSuccess: () => qc.invalidateQueries({ queryKey: ['companies'] }), }); } export function useCreateSource(companyId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (body: { source_type: string; source_name: string; config?: Record; credibility_score?: number; retention_days?: number; access_policy?: string }) => apiPost('registry', `/companies/${companyId}/sources`, body), onSuccess: () => qc.invalidateQueries({ queryKey: ['company-sources', companyId] }), }); } export function useCreateAlias(companyId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: (body: { alias: string; alias_type?: string }) => apiPost('registry', `/companies/${companyId}/aliases`, body), onSuccess: () => qc.invalidateQueries({ queryKey: ['company', companyId] }), }); } // --------------------------------------------------------------------------- // Watchlists // --------------------------------------------------------------------------- export interface Watchlist { id: string; name: string; description: string | null; active: boolean; } export function useWatchlists() { return useGet(['watchlists'], 'registry', '/watchlists'); } export function useCreateWatchlist() { const qc = useQueryClient(); return useMutation({ mutationFn: (body: { name: string; description?: string }) => apiPost('registry', '/watchlists', body), onSuccess: () => qc.invalidateQueries({ queryKey: ['watchlists'] }), }); } export function useWatchlistMembers(watchlistId: string | undefined) { return useGet(['watchlist-members', watchlistId], 'registry', `/watchlists/${watchlistId}/members`, !!watchlistId); } // --------------------------------------------------------------------------- // Documents // --------------------------------------------------------------------------- export interface Document { id: string; document_type: string; source_type: string; publisher: string | null; url: string | null; title: string | null; published_at: string | null; retrieved_at: string | null; language: string | null; content_hash: string | null; parse_quality_score: number | null; parse_confidence: string | null; status: string; created_at: string; } export interface DocumentDetail extends Document { canonical_url: string | null; raw_storage_ref: string | null; normalized_storage_ref: string | null; company_mentions: Array<{ company_id: string; ticker: string; mention_type: string; confidence: number; legal_name: string }>; intelligence: DocumentIntelligence | null; } export interface DocumentIntelligence { id: string; summary: string | null; macro_themes: string[] | null; novelty_score: number | null; source_credibility: number | null; extraction_warnings: string[] | null; confidence: number | null; model_provider: string | null; model_name: string | null; prompt_version: string | null; schema_version: string | null; validation_status: string | null; company_impacts?: CompanyImpact[]; } export interface CompanyImpact { company_id: string; ticker: string; legal_name: string; relevance: number; sentiment: string; impact_score: number; impact_horizon: string; catalyst_type: string; key_facts: string[] | null; risks: string[] | null; evidence_spans: string[] | null; } export function useDocuments(params?: { ticker?: string; document_type?: string; status?: string; since?: string; limit?: number; offset?: number }) { const qs = new URLSearchParams(); if (params?.ticker) qs.set('ticker', params.ticker); if (params?.document_type) qs.set('document_type', params.document_type); if (params?.status) qs.set('status', params.status); if (params?.since) qs.set('since', params.since); if (params?.limit) qs.set('limit', String(params.limit)); if (params?.offset) qs.set('offset', String(params.offset)); const path = `/api/documents${qs.toString() ? '?' + qs : ''}`; return useGet(['documents', params], 'query', path); } export function useDocument(id: string | undefined) { return useGet(['document', id], 'query', `/api/documents/${id}`, !!id); } // --------------------------------------------------------------------------- // Trends // --------------------------------------------------------------------------- export interface TrendSummary { id: string; entity_type: string; entity_id: string; window: string; trend_direction: string; trend_strength: number; confidence: number; top_supporting_evidence: string[] | null; top_opposing_evidence: string[] | null; dominant_catalysts: string[] | null; material_risks: string[] | null; contradiction_score: number; market_context: Record | null; generated_at: string; } export function useTrends(params?: { ticker?: string; window?: string; limit?: number; offset?: number }) { const qs = new URLSearchParams(); if (params?.ticker) qs.set('ticker', params.ticker); if (params?.window) qs.set('window', params.window); if (params?.limit) qs.set('limit', String(params.limit)); if (params?.offset) qs.set('offset', String(params.offset)); const path = `/api/trends${qs.toString() ? '?' + qs : ''}`; return useGet(['trends', params], 'query', path); } export function useTrend(id: string | undefined) { return useGet(['trend', id], 'query', `/api/trends/${id}`, !!id); } export function useTrendEvidence(id: string | undefined) { return useGet<{ trend: TrendSummary; evidence: unknown[] }>(['trend-evidence', id], 'query', `/api/trends/${id}/evidence`, !!id); } // --------------------------------------------------------------------------- // Recommendations // --------------------------------------------------------------------------- export interface Recommendation { id: string; ticker: string; action: string; mode: string; confidence: number; time_horizon: string; thesis: string | null; invalidation_conditions: string[] | null; portfolio_pct: number | null; max_loss_pct: number | null; model_version: string | null; risk_classification: string | null; generated_at: string; } export interface RecommendationDetail extends Recommendation { company_id: string | null; evidence: Array<{ id: string; document_id: string; intelligence_id: string; evidence_type: string; weight: number; title: string; document_type: string; source_type: string; publisher: string; url: string; published_at: string }>; risk_evaluation: { id: string; eligible: boolean; allowed_mode: string; rejection_reasons: string[] | null; risk_checks: Record | null; evaluated_at: string } | null; } export function useRecommendations(params?: { ticker?: string; action?: string; mode?: string; since?: string; limit?: number; offset?: number }) { const qs = new URLSearchParams(); if (params?.ticker) qs.set('ticker', params.ticker); if (params?.action) qs.set('action', params.action); if (params?.mode) qs.set('mode', params.mode); if (params?.since) qs.set('since', params.since); if (params?.limit) qs.set('limit', String(params.limit)); if (params?.offset) qs.set('offset', String(params.offset)); const path = `/api/recommendations${qs.toString() ? '?' + qs : ''}`; return useGet(['recommendations', params], 'query', path); } export function useRecommendation(id: string | undefined) { return useGet(['recommendation', id], 'query', `/api/recommendations/${id}`, !!id); } // --------------------------------------------------------------------------- // Orders // --------------------------------------------------------------------------- export interface Order { id: string; recommendation_id: string | null; broker_account_id: string | null; ticker: string; side: string; order_type: string; quantity: number; limit_price: number | null; stop_price: number | null; status: string; broker_order_id: string | null; submitted_at: string | null; fill_price: number | null; fill_quantity: number | null; created_at: string; } export interface OrderDetail extends Order { idempotency_key: string | null; decision_trace: Record | null; events: Array<{ id: string; event_type: string; data: unknown; broker_timestamp: string | null; created_at: string }>; audit_trail: unknown[]; } export function useOrders(params?: { ticker?: string; status?: string; side?: string; since?: string; limit?: number; offset?: number }) { const qs = new URLSearchParams(); if (params?.ticker) qs.set('ticker', params.ticker); if (params?.status) qs.set('status', params.status); if (params?.side) qs.set('side', params.side); if (params?.since) qs.set('since', params.since); if (params?.limit) qs.set('limit', String(params.limit)); if (params?.offset) qs.set('offset', String(params.offset)); const path = `/api/orders${qs.toString() ? '?' + qs : ''}`; return useGet(['orders', params], 'query', path); } export function useOrder(id: string | undefined) { return useGet(['order', id], 'query', `/api/orders/${id}`, !!id); } // --------------------------------------------------------------------------- // Positions // --------------------------------------------------------------------------- export interface Position { id: string; broker_account_id: string | null; ticker: string; quantity: number; avg_entry_price: number; current_price: number | null; unrealized_pnl: number | null; realized_pnl: number | null; updated_at: string; } export function usePositions(ticker?: string) { const path = ticker ? `/api/positions?ticker=${ticker}` : '/api/positions'; return useGet(['positions', ticker], 'query', path); } // --------------------------------------------------------------------------- // Admin: Trading // --------------------------------------------------------------------------- export interface TradingConfig { id?: string; name?: string; trading_mode: string; config: Record; } export interface Approval { id: string; order_job: unknown; recommendation_id: string | null; ticker: string; side: string; quantity: number; estimated_value: number | null; status: string; requested_at: string; expires_at: string | null; } export interface Lockout { id: string; ticker: string; lockout_type: string; reason: string; expires_at: string; created_at: string; } export function useTradingConfig() { return useGet(['trading-config'], 'query', '/api/admin/trading/config'); } export function useSetTradingMode() { const qc = useQueryClient(); return useMutation({ mutationFn: (mode: string) => apiPut('query', `/api/admin/trading/mode?mode=${mode}`, {}), onSuccess: () => qc.invalidateQueries({ queryKey: ['trading-config'] }), }); } export function usePendingApprovals() { return useGet(['pending-approvals'], 'query', '/api/admin/trading/approvals'); } export function useReviewApproval() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ id, approved, review_note }: { id: string; approved: boolean; review_note?: string }) => apiPut('query', `/api/admin/trading/approvals/${id}?approved=${approved}&reviewed_by=operator&review_note=${encodeURIComponent(review_note ?? '')}`, {}), onSuccess: () => qc.invalidateQueries({ queryKey: ['pending-approvals'] }), }); } export function useActiveLockouts() { return useGet(['lockouts'], 'query', '/api/admin/trading/lockouts'); } // --------------------------------------------------------------------------- // Admin: Sources // --------------------------------------------------------------------------- export interface SourceHealth { source_id: string; source_type: string; source_name: string; credibility_score: number; active: boolean; ticker: string; legal_name: string; company_id: string; last_run_status: string | null; last_run_at: string | null; last_error: string | null; total_runs_24h: number; failed_runs_24h: number; total_items_24h: number; } export function useSourceHealth(params?: { source_type?: string; company_id?: string }) { const qs = new URLSearchParams(); if (params?.source_type) qs.set('source_type', params.source_type); if (params?.company_id) qs.set('company_id', params.company_id); const path = `/api/admin/sources/health${qs.toString() ? '?' + qs : ''}`; return useGet(['source-health', params], 'query', path); } // --------------------------------------------------------------------------- // Ops: Pipeline, Ingestion, Model, Coverage // --------------------------------------------------------------------------- export function usePipelineHealth(hours = 24) { return useGet>(['pipeline-health', hours], 'query', `/api/ops/pipeline/health?hours=${hours}`); } export function useIngestionSummary(hours = 24) { return useGet>(['ingestion-summary', hours], 'query', `/api/ops/ingestion/summary?hours=${hours}`); } export function useIngestionThroughput(hours = 24, bucket = '1h') { return useGet(['ingestion-throughput', hours, bucket], 'query', `/api/ops/ingestion/throughput?hours=${hours}&bucket=${bucket}`); } export function useModelPerformance(hours = 24) { return useGet>(['model-performance', hours], 'query', `/api/ops/model/performance?hours=${hours}`); } export function useModelFailures(hours = 24) { return useGet(['model-failures', hours], 'query', `/api/ops/model/failures?hours=${hours}`); } export function useCoverageGaps() { return useGet<{ missing_source_types: unknown[]; stale_sources: unknown[] }>(['coverage-gaps'], 'query', '/api/ops/sources/coverage-gaps'); } export function useSymbolCoverage() { return useGet(['symbol-coverage'], 'query', '/api/admin/companies/coverage'); } // --------------------------------------------------------------------------- // Competitors (Symbol Registry) // --------------------------------------------------------------------------- export interface CompetitorRelationship { id: string; company_a_id: string; company_b_id: string; relationship_type: string; strength: number; bidirectional: boolean; source: string; active: boolean; created_at: string; updated_at: string; // Enriched fields from API ticker?: string; legal_name?: string; } export function useCompanyCompetitors(companyId: string | undefined) { return useGet( ['company-competitors', companyId], 'registry', `/companies/${companyId}/competitors`, !!companyId, ); } export function useInferCompetitors(companyId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: () => apiPost('registry', `/companies/${companyId}/competitors/infer`, {}), onSuccess: () => qc.invalidateQueries({ queryKey: ['company-competitors', companyId] }), }); } // --------------------------------------------------------------------------- // Historical Patterns (Query API) // --------------------------------------------------------------------------- export interface HistoricalPattern { source_ticker: string; target_ticker: string; catalyst_type: string; time_horizon: string; sample_count: number; bullish_pct: number; bearish_pct: number; avg_strength: number; avg_time_to_resolution: number; pattern_confidence: number; data_start: string; data_end: string; tier: string; insufficient_data: boolean; } export function useHistoricalPatterns(ticker: string | undefined, params?: { catalyst_type?: string; time_horizon?: string }) { const qs = new URLSearchParams(); if (params?.catalyst_type) qs.set('catalyst_type', params.catalyst_type); if (params?.time_horizon) qs.set('time_horizon', params.time_horizon); const path = `/api/patterns/${ticker}${qs.toString() ? '?' + qs : ''}`; return useGet(['historical-patterns', ticker, params], 'query', path, !!ticker); } // --------------------------------------------------------------------------- // Competitive Signals (Query API) // --------------------------------------------------------------------------- export interface CompetitiveSignal { id: string; source_document_id: string; source_ticker: string; target_ticker: string; catalyst_type: string; pattern_confidence: number; signal_direction: string; signal_strength: number; relationship_strength: number; computed_at: string; } export function useCompetitiveSignals(ticker: string | undefined) { return useGet( ['competitive-signals', ticker], 'query', `/api/patterns/${ticker}/competitive-signals`, !!ticker, ); } // --------------------------------------------------------------------------- // Corporate Decisions (Query API) // --------------------------------------------------------------------------- export interface CorporateDecision { catalyst_type: string; date: string; summary: string; trend_direction: string; trend_strength: number; sample_count: number; pattern_confidence: number; document_id?: string; } export function useCorporateDecisions(ticker: string | undefined) { return useGet( ['corporate-decisions', ticker], 'query', `/api/patterns/${ticker}/decisions`, !!ticker, ); } // --------------------------------------------------------------------------- // Competitive Layer Toggle (Query API) // --------------------------------------------------------------------------- export interface CompetitiveStatus { enabled?: boolean; competitive_enabled?: boolean; toggled_at: string | null; toggled_by: string | null; source?: string; } export function useCompetitiveStatus() { return useGet(['competitive-status'], 'query', '/api/admin/competitive/status'); } export function useToggleCompetitive() { const qc = useQueryClient(); return useMutation({ mutationFn: (enabled: boolean) => apiPut('query', '/api/admin/competitive/toggle', { enabled }), onSuccess: () => qc.invalidateQueries({ queryKey: ['competitive-status'] }), }); } // --------------------------------------------------------------------------- // Macro: Global Events (Task 17.1, 17.2) // --------------------------------------------------------------------------- export interface GlobalEvent { id: string; event_types: string[]; severity: string; affected_regions: string[]; affected_sectors: string[]; affected_commodities: string[]; summary: string; key_facts: string[]; estimated_duration: string; confidence: number; source_document_id: string | null; model_provider: string | null; model_name: string | null; created_at: string; } export interface MacroImpactRecord { id: string; event_id: string; company_id: string; ticker: string; macro_impact_score: number; impact_direction: string; contributing_factors: string[]; confidence: number; computed_at: string; } export interface GlobalEventDetail extends GlobalEvent { impacts: MacroImpactRecord[]; } export interface ExposureProfile { id: string; company_id: string; geographic_revenue_mix: Record; supply_chain_regions: string[]; key_input_commodities: string[]; regulatory_jurisdictions: string[]; market_position_tier: string; export_dependency_pct: number; source: string; confidence: number; version: number; active: boolean; created_at: string; updated_at: string; } export interface CompanyMacroImpacts { exposure_profile: ExposureProfile | null; impacts: MacroImpactRecord[]; } export function useGlobalEvents(params?: { severity?: string; region?: string; sector?: string; limit?: number }) { const qs = new URLSearchParams(); if (params?.severity) qs.set('severity', params.severity); if (params?.region) qs.set('region', params.region); if (params?.sector) qs.set('sector', params.sector); if (params?.limit) qs.set('limit', String(params.limit)); const path = `/api/macro/events${qs.toString() ? '?' + qs : ''}`; return useGet(['global-events', params], 'query', path); } export function useGlobalEvent(id: string | undefined) { return useGet(['global-event', id], 'query', `/api/macro/events/${id}`, !!id); } // --------------------------------------------------------------------------- // Macro: Company Impacts (Task 17.3) // --------------------------------------------------------------------------- export function useCompanyMacroImpacts(ticker: string | undefined) { return useGet(['company-macro-impacts', ticker], 'query', `/api/macro/impacts/${ticker}`, !!ticker); } // --------------------------------------------------------------------------- // Macro: Trend Projection (Task 17.5) // --------------------------------------------------------------------------- export interface TrendProjection { id: string; trend_window_id: string; projected_direction: string; projected_strength: number; projected_confidence: number; projection_horizon: string; driving_factors: string[]; macro_contribution_pct: number; diverges_from_current: boolean; computed_at: string; } export function useTrendProjection(trendId: string | undefined) { return useGet(['trend-projection', trendId], 'query', `/api/trends/${trendId}/projection`, !!trendId); } // --------------------------------------------------------------------------- // System: Rate Limits // --------------------------------------------------------------------------- export interface RateLimitInfo { polygon_global_limit: number; market_api: { rate_per_minute: number; cadence_seconds: number; max_tickers_per_cycle: number; active_sources: number; }; } export function useRateLimits() { return useGet(['rate-limits'], 'query', '/api/system/rate-limits'); } // --------------------------------------------------------------------------- // Macro: Admin Toggle (Task 17.6) // --------------------------------------------------------------------------- export interface MacroStatus { enabled?: boolean; macro_enabled?: boolean; toggled_at: string | null; toggled_by: string | null; source?: string; } export function useMacroStatus() { return useGet(['macro-status'], 'query', '/api/admin/macro/status'); } export function useToggleMacro() { const qc = useQueryClient(); return useMutation({ mutationFn: (enabled: boolean) => apiPut('query', `/api/admin/macro/toggle`, { enabled }), onSuccess: () => qc.invalidateQueries({ queryKey: ['macro-status'] }), }); }