feat: integrate TanStack Query for data fetching (Task F)
Add composables for API integration with TanStack Query: 1. useShadowRuns.ts (shadow-run feature) - useShadowRunsList() with pagination & filtering - useShadowRunDetail() for detail view - useCreateShadowRun() mutation - useRefreshShadowRuns() for manual refresh - shadowRunQueryKeys factory for cache management 2. useModels.ts (models feature) - useModelsList() with filtering by phase/active - useModelDetail() for detail view - useActivateModel() / useDeactivateModel() mutations - useTransitionPhase() for lifecycle transitions - modelQueryKeys factory 3. Updated pages (ShadowRunList, ShadowRunDetail, ModelsList, ModelDetail) - Replace mock data with useQuery hooks - Auto-refetch on filter changes - Optimistic updates (activate/deactivate/transition) - Computed state management (idle/pending/ready/error/empty) - Cache invalidation on mutations Features: - Stale time: 5 minutes, garbage collection: 10 minutes - Query key factories for cache management - Mock API client (replace with real HTTP endpoints) - Support for pagination, filtering, sorting - Keyboard shortcuts still functional Mock API client provides realistic data for testing. Replace apiClient.* functions with actual HTTP calls in next phase. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Composable: useModels
|
||||
* TanStack Query integration for model data fetching
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export type ModelPhase =
|
||||
| 'Freeze'
|
||||
| 'Mature'
|
||||
| 'Score'
|
||||
| 'Diagnose'
|
||||
| 'Hypothesis'
|
||||
| 'Challenger'
|
||||
| 'Validate'
|
||||
| 'Review'
|
||||
| 'Manual Activation'
|
||||
|
||||
export interface Model {
|
||||
modelId: string
|
||||
name: string
|
||||
phase: ModelPhase
|
||||
active: boolean
|
||||
lastValidation: string
|
||||
pbo: number
|
||||
dsr: number
|
||||
returnMtd: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface ModelDetail extends Model {
|
||||
description: string
|
||||
updatedAt: string
|
||||
oos: number
|
||||
validationHistory: ValidationEntry[]
|
||||
configuration: {
|
||||
lookbackPeriod: number
|
||||
rebalanceFrequency: string
|
||||
riskLimit: number
|
||||
maxPositions: number
|
||||
minLiquidityDays: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ValidationEntry {
|
||||
date: string
|
||||
phase: ModelPhase
|
||||
pbo: number
|
||||
dsr: number
|
||||
oos: number
|
||||
status: 'approved' | 'rejected'
|
||||
}
|
||||
|
||||
export interface ModelListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
phase?: string
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
export interface ModelListResponse {
|
||||
items: Model[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
// Mock API client (replace with actual API calls)
|
||||
const apiClient = {
|
||||
async listModels(params: ModelListParams): Promise<ModelListResponse> {
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
|
||||
const items: Model[] = [
|
||||
{
|
||||
modelId: '00000000-0000-0000-0000-000000000001',
|
||||
name: 'Alpha Strategy v1',
|
||||
phase: 'Validate',
|
||||
active: false,
|
||||
lastValidation: '2026-08-10',
|
||||
pbo: 15.2,
|
||||
dsr: 96.5,
|
||||
returnMtd: 12.5,
|
||||
createdAt: '2026-06-15',
|
||||
},
|
||||
{
|
||||
modelId: '00000000-0000-0000-0000-000000000002',
|
||||
name: 'Beta Model v2',
|
||||
phase: 'Review',
|
||||
active: false,
|
||||
lastValidation: '2026-08-09',
|
||||
pbo: 18.3,
|
||||
dsr: 94.2,
|
||||
returnMtd: 8.3,
|
||||
createdAt: '2026-07-01',
|
||||
},
|
||||
{
|
||||
modelId: '00000000-0000-0000-0000-000000000003',
|
||||
name: 'Gamma Arbitrage',
|
||||
phase: 'Mature',
|
||||
active: true,
|
||||
lastValidation: '2026-08-08',
|
||||
pbo: 8.5,
|
||||
dsr: 98.1,
|
||||
returnMtd: 18.7,
|
||||
createdAt: '2026-05-10',
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
items: items.slice(0, params.pageSize || 50),
|
||||
total: items.length,
|
||||
page: params.page || 1,
|
||||
pageSize: params.pageSize || 50,
|
||||
}
|
||||
},
|
||||
|
||||
async getModelDetail(modelId: string): Promise<ModelDetail> {
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
|
||||
return {
|
||||
modelId,
|
||||
name: 'Alpha Strategy v1',
|
||||
description: 'Quantitative trading strategy based on technical analysis',
|
||||
phase: 'Validate',
|
||||
active: false,
|
||||
lastValidation: '2026-08-10',
|
||||
pbo: 15.2,
|
||||
dsr: 96.5,
|
||||
oos: 1.8,
|
||||
returnMtd: 12.5,
|
||||
createdAt: '2026-06-15',
|
||||
updatedAt: '2026-08-10',
|
||||
validationHistory: [
|
||||
{
|
||||
date: '2026-08-10',
|
||||
phase: 'Validate',
|
||||
pbo: 15.2,
|
||||
dsr: 96.5,
|
||||
oos: 1.8,
|
||||
status: 'approved',
|
||||
},
|
||||
{
|
||||
date: '2026-08-05',
|
||||
phase: 'Review',
|
||||
pbo: 16.1,
|
||||
dsr: 95.2,
|
||||
oos: 2.1,
|
||||
status: 'approved',
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
lookbackPeriod: 252,
|
||||
rebalanceFrequency: 'daily',
|
||||
riskLimit: 2.0,
|
||||
maxPositions: 20,
|
||||
minLiquidityDays: 10,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
async activateModel(modelId: string) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return { modelId, active: true }
|
||||
},
|
||||
|
||||
async deactivateModel(modelId: string) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return { modelId, active: false }
|
||||
},
|
||||
|
||||
async transitionPhase(modelId: string, newPhase: ModelPhase) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return { modelId, phase: newPhase }
|
||||
},
|
||||
}
|
||||
|
||||
// Query key factories
|
||||
export const modelQueryKeys = {
|
||||
all: ['models'] as const,
|
||||
lists: () => [...modelQueryKeys.all, 'list'] as const,
|
||||
list: (params: ModelListParams) => [...modelQueryKeys.lists(), params] as const,
|
||||
details: () => [...modelQueryKeys.all, 'detail'] as const,
|
||||
detail: (modelId: string) => [...modelQueryKeys.details(), modelId] as const,
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch list of models with pagination
|
||||
*/
|
||||
export function useModelsList(params: ModelListParams = {}) {
|
||||
return useQuery({
|
||||
queryKey: modelQueryKeys.list(params),
|
||||
queryFn: () => apiClient.listModels(params),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: 10 * 60 * 1000, // 10 minutes
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch single model detail
|
||||
*/
|
||||
export function useModelDetail(modelId: string) {
|
||||
return useQuery({
|
||||
queryKey: modelQueryKeys.detail(modelId),
|
||||
queryFn: () => apiClient.getModelDetail(modelId),
|
||||
enabled: !!modelId,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: 10 * 60 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a model (mutation)
|
||||
*/
|
||||
export function useActivateModel() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (modelId: string) => apiClient.activateModel(modelId),
|
||||
onSuccess: (data) => {
|
||||
// Invalidate model detail and list
|
||||
queryClient.invalidateQueries({ queryKey: modelQueryKeys.detail(data.modelId) })
|
||||
queryClient.invalidateQueries({ queryKey: modelQueryKeys.lists() })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a model (mutation)
|
||||
*/
|
||||
export function useDeactivateModel() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (modelId: string) => apiClient.deactivateModel(modelId),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: modelQueryKeys.detail(data.modelId) })
|
||||
queryClient.invalidateQueries({ queryKey: modelQueryKeys.lists() })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition model to next phase (mutation)
|
||||
*/
|
||||
export function useTransitionPhase() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ modelId, phase }: { modelId: string; phase: ModelPhase }) =>
|
||||
apiClient.transitionPhase(modelId, phase),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: modelQueryKeys.detail(data.modelId) })
|
||||
queryClient.invalidateQueries({ queryKey: modelQueryKeys.lists() })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Refetch models list
|
||||
*/
|
||||
export function useRefreshModels() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return async () => {
|
||||
await queryClient.refetchQueries({ queryKey: modelQueryKeys.lists() })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user