bc077bfcc8
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
- Migration 038: trading_reports table + report-summarizer agent seed
- 6 reporting modules: models, collector, sections, validator, summarizer, generator
- API endpoints: GET /api/reports (paginated, filterable), GET /api/reports/{id}
- Frontend hooks: useReports, useReport with TanStack Query
- Scheduler: daily (after 16:30 ET) and weekly (Saturday) report triggers
- Redis queue consumer for async report generation with retry/dedup
- 5 property-based tests (chunking, serialization, validation, accuracy, deltas)
- 109 unit/integration tests across all modules
- 6 frontend hook tests with MSW mocks
1095 lines
36 KiB
TypeScript
1095 lines
36 KiB
TypeScript
/**
|
|
* 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, apiDelete } from './client';
|
|
import type { ApiBase } from './client';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Generic helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function useGet<T>(key: unknown[], api: ApiBase, path: string, enabled = true) {
|
|
return useQuery<T>({ queryKey: key, queryFn: () => apiGet<T>(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<string, unknown>;
|
|
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<Company[]>(['companies', params], 'query', path);
|
|
}
|
|
|
|
export function useCompany(id: string | undefined) {
|
|
return useGet<Company>(['company', id], 'query', `/api/companies/${id}`, !!id);
|
|
}
|
|
|
|
export function useCompanySources(companyId: string | undefined) {
|
|
return useGet<Source[]>(['company-sources', companyId], 'query', `/api/companies/${companyId}/sources`, !!companyId);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Symbol Registry (CRUD)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function useRegistryCompanies(active = true) {
|
|
return useGet<Company[]>(['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<Company>('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<string, unknown>; credibility_score?: number; retention_days?: number; access_policy?: string }) =>
|
|
apiPost<Source>('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<Alias>('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<Watchlist[]>(['watchlists'], 'registry', '/watchlists');
|
|
}
|
|
|
|
export function useCreateWatchlist() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (body: { name: string; description?: string }) =>
|
|
apiPost<Watchlist>('registry', '/watchlists', body),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['watchlists'] }),
|
|
});
|
|
}
|
|
|
|
export function useWatchlistMembers(watchlistId: string | undefined) {
|
|
return useGet<Company[]>(['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<Document[]>(['documents', params], 'query', path);
|
|
}
|
|
|
|
export function useDocument(id: string | undefined) {
|
|
return useGet<DocumentDetail>(['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<string, unknown> | 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<TrendSummary[]>(['trends', params], 'query', path);
|
|
}
|
|
|
|
export function useTrendHistory(params?: { ticker?: string; window?: string; limit?: 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 ?? 200));
|
|
const path = `/api/trends/history${qs.toString() ? '?' + qs : ''}`;
|
|
return useGet<TrendSummary[]>(['trend-history', params], 'query', path);
|
|
}
|
|
|
|
export interface MarketPrice {
|
|
ticker: string;
|
|
close: number;
|
|
open: number;
|
|
high: number;
|
|
low: number;
|
|
volume: number;
|
|
bar_timestamp: number;
|
|
captured_at: string;
|
|
}
|
|
|
|
export interface MarketPriceResponse {
|
|
bars: MarketPrice[];
|
|
range_90d: { low: number | null; high: number | null };
|
|
}
|
|
|
|
export function useMarketPrices(ticker: string | undefined, limit = 200) {
|
|
return useGet<MarketPriceResponse>(
|
|
['market-prices', ticker, limit],
|
|
'query',
|
|
`/api/market/prices/${ticker}?limit=${limit}`,
|
|
!!ticker,
|
|
);
|
|
}
|
|
|
|
/** Backfill 90 days of daily bars from Polygon for a single ticker. */
|
|
export function useBackfillMarketPrices() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (ticker: string) =>
|
|
apiPost<{ ticker: string; inserted: number; total_bars: number }>('query', `/api/market/backfill/${ticker}`, {}),
|
|
onSuccess: (_data, ticker) => {
|
|
qc.invalidateQueries({ queryKey: ['market-prices', ticker] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useTrend(id: string | undefined) {
|
|
return useGet<TrendSummary>(['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<string, unknown> | null; evaluated_at: string } | null;
|
|
}
|
|
|
|
export function useRecommendations(params?: { ticker?: string; action?: string; mode?: string; since?: string; min_confidence?: number; 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?.min_confidence != null) qs.set('min_confidence', String(params.min_confidence));
|
|
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<Recommendation[]>(['recommendations', params], 'query', path);
|
|
}
|
|
|
|
export function useRecommendation(id: string | undefined) {
|
|
return useGet<RecommendationDetail>(['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<string, unknown> | 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<Order[]>(['orders', params], 'query', path);
|
|
}
|
|
|
|
export function useOrder(id: string | undefined) {
|
|
return useGet<OrderDetail>(['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;
|
|
polygon_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<Position[]>(['positions', ticker], 'query', path);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Admin: Trading
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface TradingConfig {
|
|
id?: string;
|
|
name?: string;
|
|
trading_mode: string;
|
|
config: Record<string, unknown>;
|
|
}
|
|
|
|
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<TradingConfig>(['trading-config'], 'query', '/api/admin/trading/config');
|
|
}
|
|
|
|
export function useSetTradingMode() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (mode: string) => apiPut<unknown>('query', `/api/admin/trading/mode?mode=${mode}`, {}),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['trading-config'] }),
|
|
});
|
|
}
|
|
|
|
export function usePendingApprovals() {
|
|
return useGet<Approval[]>(['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<unknown>('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<Lockout[]>(['lockouts'], 'query', '/api/admin/trading/lockouts');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Admin: Approval Config
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface ApprovalConfig {
|
|
auto_approve_paper: boolean;
|
|
require_approval_for_live: boolean;
|
|
approval_timeout_minutes: number;
|
|
}
|
|
|
|
export function useApprovalConfig() {
|
|
return useGet<ApprovalConfig>(['approval-config'], 'query', '/api/admin/trading/approval-config');
|
|
}
|
|
|
|
export function useUpdateApprovalConfig() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (body: Partial<ApprovalConfig>) =>
|
|
apiPut<ApprovalConfig>('query', '/api/admin/trading/approval-config', body),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ['approval-config'] });
|
|
qc.invalidateQueries({ queryKey: ['trading-config'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useCreateLockout() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (body: { ticker: string; reason: string; duration_minutes: number }) =>
|
|
apiPost<Lockout>('query', '/api/admin/trading/lockouts', body),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['lockouts'] }),
|
|
});
|
|
}
|
|
|
|
export function useDeleteLockout() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (id: string) =>
|
|
apiDelete<unknown>('query', `/api/admin/trading/lockouts/${id}`),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['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<SourceHealth[]>(['source-health', params], 'query', path);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Ops: Pipeline, Ingestion, Model, Coverage
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function usePipelineHealth(hours = 24) {
|
|
return useGet<Record<string, unknown>>(['pipeline-health', hours], 'query', `/api/ops/pipeline/health?hours=${hours}`);
|
|
}
|
|
|
|
export function useRetryFailedExtractions() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: () => apiPost<{ retried: number; message: string }>('query', '/api/ops/pipeline/retry-failed', {}),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['pipeline-health'] }),
|
|
});
|
|
}
|
|
|
|
export function usePipelineToggle() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (enabled: boolean) => apiPost<{ pipeline_enabled: boolean }>('query', '/api/ops/pipeline/toggle', { enabled }),
|
|
onMutate: async (enabled) => {
|
|
await qc.cancelQueries({ queryKey: ['pipeline-health'] });
|
|
qc.setQueriesData<Record<string, unknown>>({ queryKey: ['pipeline-health'] }, (old) =>
|
|
old ? { ...old, pipeline_enabled: enabled } : old,
|
|
);
|
|
},
|
|
onSettled: () => qc.invalidateQueries({ queryKey: ['pipeline-health'] }),
|
|
});
|
|
}
|
|
|
|
export function useIngestionSummary(hours = 24) {
|
|
return useGet<Record<string, unknown>>(['ingestion-summary', hours], 'query', `/api/ops/ingestion/summary?hours=${hours}`);
|
|
}
|
|
|
|
export function useIngestionThroughput(hours = 24, bucket = '1h') {
|
|
return useGet<unknown[]>(['ingestion-throughput', hours, bucket], 'query', `/api/ops/ingestion/throughput?hours=${hours}&bucket=${bucket}`);
|
|
}
|
|
|
|
export function useModelPerformance(hours = 24) {
|
|
return useGet<Record<string, unknown>>(['model-performance', hours], 'query', `/api/ops/model/performance?hours=${hours}`);
|
|
}
|
|
|
|
export function useModelFailures(hours = 24) {
|
|
return useGet<unknown[]>(['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<unknown[]>(['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<CompetitorRelationship[]>(
|
|
['company-competitors', companyId],
|
|
'registry',
|
|
`/companies/${companyId}/competitors`,
|
|
!!companyId,
|
|
);
|
|
}
|
|
|
|
export function useInferCompetitors(companyId: string) {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: () => apiPost<CompetitorRelationship[]>('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 useQuery<HistoricalPattern[]>({
|
|
queryKey: ['historical-patterns', ticker, params],
|
|
queryFn: async () => {
|
|
const resp = await apiGet<{ patterns: HistoricalPattern[] }>('query', path);
|
|
return resp.patterns ?? [];
|
|
},
|
|
enabled: !!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) {
|
|
const result = useGet<CompetitiveSignal[] | { competitive_signals: CompetitiveSignal[] }>(
|
|
['competitive-signals', ticker],
|
|
'query',
|
|
`/api/patterns/${ticker}/competitive-signals`,
|
|
!!ticker,
|
|
);
|
|
// API returns { competitive_signals: [...] } wrapper — extract the array
|
|
const data = result.data;
|
|
const signals: CompetitiveSignal[] | undefined = data
|
|
? (Array.isArray(data) ? data : (data as { competitive_signals: CompetitiveSignal[] }).competitive_signals ?? [])
|
|
: undefined;
|
|
return { ...result, data: signals };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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<CorporateDecision[]>(
|
|
['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<CompetitiveStatus>(['competitive-status'], 'query', '/api/admin/competitive/status');
|
|
}
|
|
|
|
export function useToggleCompetitive() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (enabled: boolean) => apiPut<unknown>('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<string, number>;
|
|
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<GlobalEvent[]>(['global-events', params], 'query', path);
|
|
}
|
|
|
|
export function useGlobalEvent(id: string | undefined) {
|
|
return useGet<GlobalEventDetail>(['global-event', id], 'query', `/api/macro/events/${id}`, !!id);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Macro: Company Impacts (Task 17.3)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function useCompanyMacroImpacts(ticker: string | undefined) {
|
|
return useGet<CompanyMacroImpacts>(['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<TrendProjection>(['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<RateLimitInfo>(['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<MacroStatus>(['macro-status'], 'query', '/api/admin/macro/status');
|
|
}
|
|
|
|
export function useToggleMacro() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (enabled: boolean) => apiPut<unknown>('query', `/api/admin/macro/toggle`, { enabled }),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['macro-status'] }),
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Validation: Model Quality & Calibration (Requirements 12.1, 12.2, 12.3, 12.7)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface ModelMetricSnapshot {
|
|
id: string;
|
|
generated_at: string;
|
|
lookback_window: string;
|
|
horizon: string;
|
|
prediction_count: number;
|
|
win_rate: number | null;
|
|
directional_accuracy: number | null;
|
|
information_coefficient: number | null;
|
|
rank_information_coefficient: number | null;
|
|
avg_return: number | null;
|
|
avg_excess_return_vs_spy: number | null;
|
|
avg_excess_return_vs_sector: number | null;
|
|
calibration_error: number | null;
|
|
brier_score: number | null;
|
|
buy_win_rate: number | null;
|
|
sell_win_rate: number | null;
|
|
hold_win_rate: number | null;
|
|
metadata: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface ValidationSummary {
|
|
snapshot: ModelMetricSnapshot | null;
|
|
gate_status: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface CalibrationBucket {
|
|
bucket_low: number;
|
|
bucket_high: number;
|
|
avg_confidence: number;
|
|
observed_win_rate: number;
|
|
prediction_count: number;
|
|
miscalibrated: boolean;
|
|
}
|
|
|
|
export interface ValidationCalibration {
|
|
buckets: CalibrationBucket[];
|
|
lookback: string;
|
|
horizon: string;
|
|
}
|
|
|
|
export interface ICByHorizonEntry {
|
|
horizon: string;
|
|
information_coefficient: number | null;
|
|
rank_information_coefficient: number | null;
|
|
prediction_count: number;
|
|
generated_at: string | null;
|
|
}
|
|
|
|
export interface ValidationICByHorizon {
|
|
horizons: ICByHorizonEntry[];
|
|
lookback: string;
|
|
}
|
|
|
|
export interface ValidationGateStatus {
|
|
gate_status: Record<string, unknown> | null;
|
|
updated_at?: string | null;
|
|
message?: string;
|
|
}
|
|
|
|
export function useValidationSummary(lookback = '30d', horizon = '7d') {
|
|
const qs = new URLSearchParams();
|
|
if (lookback) qs.set('lookback', lookback);
|
|
if (horizon) qs.set('horizon', horizon);
|
|
const path = `/api/validation/summary${qs.toString() ? '?' + qs : ''}`;
|
|
return useGet<ValidationSummary>(['validation-summary', lookback, horizon], 'query', path);
|
|
}
|
|
|
|
export function useValidationCalibration(lookback = '30d', horizon = '7d') {
|
|
const qs = new URLSearchParams();
|
|
if (lookback) qs.set('lookback', lookback);
|
|
if (horizon) qs.set('horizon', horizon);
|
|
const path = `/api/validation/calibration${qs.toString() ? '?' + qs : ''}`;
|
|
return useGet<ValidationCalibration>(['validation-calibration', lookback, horizon], 'query', path);
|
|
}
|
|
|
|
export function useValidationICByHorizon(lookback = '30d') {
|
|
const qs = new URLSearchParams();
|
|
if (lookback) qs.set('lookback', lookback);
|
|
const path = `/api/validation/ic-by-horizon${qs.toString() ? '?' + qs : ''}`;
|
|
return useGet<ValidationICByHorizon>(['validation-ic-by-horizon', lookback], 'query', path);
|
|
}
|
|
|
|
export function useValidationGateStatus() {
|
|
return useGet<ValidationGateStatus>(['validation-gate-status'], 'query', '/api/validation/gate-status');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Validation: Attribution — Sources, Catalysts, Layers (Requirements 12.4, 12.5, 12.6)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface SourceAttribution {
|
|
source: string;
|
|
source_type: string;
|
|
prediction_count: number;
|
|
avg_weight: number;
|
|
avg_contribution_score: number;
|
|
win_rate: number;
|
|
avg_future_return: number;
|
|
avg_excess_return_vs_spy: number;
|
|
information_coefficient: number | null;
|
|
duplicate_rate: number;
|
|
}
|
|
|
|
export interface SourceAttributionResponse {
|
|
sources: SourceAttribution[];
|
|
lookback: string;
|
|
horizon: string;
|
|
}
|
|
|
|
export interface CatalystAttribution {
|
|
catalyst_type: string;
|
|
prediction_count: number;
|
|
win_rate: number;
|
|
avg_future_return: number;
|
|
avg_excess_return_vs_spy: number;
|
|
information_coefficient: number | null;
|
|
}
|
|
|
|
export interface CatalystAttributionResponse {
|
|
catalysts: CatalystAttribution[];
|
|
lookback: string;
|
|
horizon: string;
|
|
}
|
|
|
|
export interface LayerAttribution {
|
|
layer: string;
|
|
avg_contribution_pct: number;
|
|
dominant_win_rate: number;
|
|
dominant_ic: number | null;
|
|
}
|
|
|
|
export interface LayerAttributionResponse {
|
|
layers: LayerAttribution[];
|
|
lookback: string;
|
|
horizon: string;
|
|
}
|
|
|
|
export function useValidationAttributionSources(lookback = '30d', horizon = '7d') {
|
|
const qs = new URLSearchParams();
|
|
if (lookback) qs.set('lookback', lookback);
|
|
if (horizon) qs.set('horizon', horizon);
|
|
const path = `/api/validation/attribution/sources${qs.toString() ? '?' + qs : ''}`;
|
|
return useGet<SourceAttributionResponse>(['validation-attribution-sources', lookback, horizon], 'query', path);
|
|
}
|
|
|
|
export function useValidationAttributionCatalysts(lookback = '30d', horizon = '7d') {
|
|
const qs = new URLSearchParams();
|
|
if (lookback) qs.set('lookback', lookback);
|
|
if (horizon) qs.set('horizon', horizon);
|
|
const path = `/api/validation/attribution/catalysts${qs.toString() ? '?' + qs : ''}`;
|
|
return useGet<CatalystAttributionResponse>(['validation-attribution-catalysts', lookback, horizon], 'query', path);
|
|
}
|
|
|
|
export function useValidationAttributionLayers(lookback = '30d', horizon = '7d') {
|
|
const qs = new URLSearchParams();
|
|
if (lookback) qs.set('lookback', lookback);
|
|
if (horizon) qs.set('horizon', horizon);
|
|
const path = `/api/validation/attribution/layers${qs.toString() ? '?' + qs : ''}`;
|
|
return useGet<LayerAttributionResponse>(['validation-attribution-layers', lookback, horizon], 'query', path);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Trading Reports
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface ReportListItem {
|
|
id: string;
|
|
report_type: string;
|
|
period_start: string;
|
|
period_end: string;
|
|
validation_status: string;
|
|
generated_at: string;
|
|
}
|
|
|
|
export interface ReportDetail extends ReportListItem {
|
|
report_data: Record<string, unknown>;
|
|
created_at: string;
|
|
}
|
|
|
|
export function useReports(params?: {
|
|
report_type?: string;
|
|
start_date?: string;
|
|
end_date?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}) {
|
|
const qs = new URLSearchParams();
|
|
if (params?.report_type) qs.set('report_type', params.report_type);
|
|
if (params?.start_date) qs.set('start_date', params.start_date);
|
|
if (params?.end_date) qs.set('end_date', params.end_date);
|
|
if (params?.limit) qs.set('limit', String(params.limit));
|
|
if (params?.offset) qs.set('offset', String(params.offset));
|
|
const path = `/api/reports${qs.toString() ? '?' + qs : ''}`;
|
|
return useGet<ReportListItem[]>(['reports', params], 'query', path);
|
|
}
|
|
|
|
export function useReport(id: string | undefined) {
|
|
return useGet<ReportDetail>(
|
|
['report', id], 'query', `/api/reports/${id}`, !!id
|
|
);
|
|
}
|