8a3ee0a1d7
- Simplified data fetching (removed TanStack Query for now) - Added direct mock API client in component - Fixed state management (isLoading, isError, modelsData) - Grid now renders with 3 sample model records - Updated v-if conditions for loading/error/empty states - Added grid container height and filter styling Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
271 lines
6.4 KiB
TypeScript
271 lines
6.4 KiB
TypeScript
/**
|
|
* 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) {
|
|
const finalParams = params || {}
|
|
return useQuery({
|
|
queryKey: ['models', 'list', finalParams],
|
|
queryFn: () => apiClient.listModels(finalParams),
|
|
staleTime: 5 * 60 * 1000,
|
|
gcTime: 10 * 60 * 1000,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 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() })
|
|
}
|
|
}
|