diff --git a/frontend/src/features/models/composables/useModels.ts b/frontend/src/features/models/composables/useModels.ts new file mode 100644 index 00000000..986f5976 --- /dev/null +++ b/frontend/src/features/models/composables/useModels.ts @@ -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 { + 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 { + 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() }) + } +} diff --git a/frontend/src/features/models/pages/ModelDetail.vue b/frontend/src/features/models/pages/ModelDetail.vue index a18f748a..9bcd6e84 100644 --- a/frontend/src/features/models/pages/ModelDetail.vue +++ b/frontend/src/features/models/pages/ModelDetail.vue @@ -1,8 +1,9 @@