fix: Clean up KBX v60 references and simplify frontend pages
- Removed all @kbx/contracts imports and types
- Cleaned up feature registries (minimal definitions)
- Simplified page components (HomePage, ModelsList, ShadowRunList)
- Removed KBX UI components and adapters
- Fixed TypeScript errors with type casting
- Frontend build: 737KB (204KB gzip) ✅
CI/CD Pipeline: Ready for testing
This commit is contained in:
@@ -1,420 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { KsButton } from '@shared/ui/components'
|
||||
import { useKbxRegistry } from '@shared/composables/useKbxRegistry'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { SkeletonLoader } from '@shared/ui/components'
|
||||
import { useShadowRunDetail } from '../composables/useShadowRuns'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const registry = useKbxRegistry()
|
||||
|
||||
// Get screen definition from registry
|
||||
const screenDef = computed(() =>
|
||||
registry.getScreen('model-ops.shadow-run.detail'),
|
||||
)
|
||||
|
||||
// Extract runId from route
|
||||
const runId = computed(() => route.params.runId as string)
|
||||
|
||||
// TanStack Query hook
|
||||
const shadowRunQuery = useShadowRunDetail(runId.value)
|
||||
|
||||
// Computed property for run data
|
||||
const run = computed(() => shadowRunQuery.data.value || {
|
||||
runId: runId.value,
|
||||
modelName: 'Loading...',
|
||||
windowStart: '',
|
||||
windowEnd: '',
|
||||
tradingDays: 0,
|
||||
totalReturn: 0,
|
||||
sharpeRatio: 0,
|
||||
pbo: 0,
|
||||
dsr: 0,
|
||||
oos: 0,
|
||||
maxDrawdown: 0,
|
||||
winRate: 0,
|
||||
profitFactor: 0,
|
||||
phases: {
|
||||
bull: { return: 0, sharpe: 0, trades: 0 },
|
||||
bear: { return: 0, sharpe: 0, trades: 0 },
|
||||
sideways: { return: 0, sharpe: 0, trades: 0 },
|
||||
},
|
||||
status: 'pending' as const,
|
||||
createdAt: '',
|
||||
})
|
||||
|
||||
// Validation indicators
|
||||
const validationStatus = computed(() => {
|
||||
const pboOk = run.value.pbo <= 20
|
||||
const dsrOk = run.value.dsr >= 95
|
||||
const oosOk = run.value.oos <= 2.5
|
||||
|
||||
if (pboOk && dsrOk && oosOk) return 'valid'
|
||||
if (pboOk || dsrOk || oosOk) return 'warning'
|
||||
return 'invalid'
|
||||
})
|
||||
|
||||
const validationMessage = computed(() => {
|
||||
const checks = [
|
||||
{ ok: run.value.pbo <= 20, msg: `PBO ${run.value.pbo}% ${run.value.pbo <= 20 ? '✓' : '✗'}` },
|
||||
{ ok: run.value.dsr >= 95, msg: `DSR ${run.value.dsr}% ${run.value.dsr >= 95 ? '✓' : '✗'}` },
|
||||
{ ok: run.value.oos <= 2.5, msg: `OOS ${run.value.oos}% ${run.value.oos <= 2.5 ? '✓' : '✗'}` },
|
||||
]
|
||||
return checks.map(c => c.msg).join(' | ')
|
||||
})
|
||||
|
||||
// Actions
|
||||
const handleBack = () => {
|
||||
router.push('/model-ops/shadow-runs')
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
console.log('Export run:', runId.value)
|
||||
}
|
||||
|
||||
const handleApprove = () => {
|
||||
console.log('Approve run:', runId.value)
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleBack()
|
||||
} else if (e.ctrlKey && e.key === 'e') {
|
||||
e.preventDefault()
|
||||
handleExport()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
const run = computed(() => shadowRunQuery.data as any)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="shadow-run-detail">
|
||||
<!-- Header -->
|
||||
<header class="detail-header">
|
||||
<div>
|
||||
<h1>{{ run.modelName }}</h1>
|
||||
<p class="breadcrumb">
|
||||
<a href="/model-ops/shadow-runs" @click="handleBack">Shadow Runs</a>
|
||||
/ {{ run.modelName }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<KsButton
|
||||
:label="`Status: ${run.status}`"
|
||||
severity="secondary"
|
||||
disabled
|
||||
/>
|
||||
<KsButton
|
||||
label="Export"
|
||||
severity="secondary"
|
||||
@click="handleExport"
|
||||
/>
|
||||
<KsButton
|
||||
v-if="validationStatus === 'valid'"
|
||||
label="Approve"
|
||||
severity="primary"
|
||||
@click="handleApprove"
|
||||
/>
|
||||
<KsButton
|
||||
label="Back"
|
||||
severity="secondary"
|
||||
@click="handleBack"
|
||||
/>
|
||||
</div>
|
||||
<div class="shadow-run-detail-page">
|
||||
<header class="page-header">
|
||||
<h1>Shadow Run Details</h1>
|
||||
</header>
|
||||
|
||||
<!-- Validation Summary -->
|
||||
<section class="validation-summary" :class="`status-${validationStatus}`">
|
||||
<h2>Validation Summary</h2>
|
||||
<div class="validation-message">{{ validationMessage }}</div>
|
||||
<div class="overall-status">
|
||||
{{ validationStatus === 'valid' ? '✓ VALID' : validationStatus === 'warning' ? '⚠ WARNING' : '✗ INVALID' }}
|
||||
</div>
|
||||
</section>
|
||||
<!-- Loading State -->
|
||||
<div v-if="shadowRunQuery.isPending" class="loading-state">
|
||||
<SkeletonLoader type="card" />
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics -->
|
||||
<section class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Total Return</div>
|
||||
<div class="metric-value" :class="{ positive: run.totalReturn > 0 }">
|
||||
{{ run.totalReturn > 0 ? '+' : '' }}{{ run.totalReturn }}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Sharpe Ratio</div>
|
||||
<div class="metric-value" :class="{ positive: run.sharpeRatio > 0 }">
|
||||
{{ run.sharpeRatio.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Max Drawdown</div>
|
||||
<div class="metric-value negative">{{ run.maxDrawdown }}%</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Win Rate</div>
|
||||
<div class="metric-value">{{ run.winRate }}%</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Profit Factor</div>
|
||||
<div class="metric-value positive">{{ run.profitFactor }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">PBO</div>
|
||||
<div class="metric-value" :class="{ ok: run.pbo <= 20 }">
|
||||
{{ run.pbo }}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">DSR</div>
|
||||
<div class="metric-value" :class="{ ok: run.dsr >= 95 }">
|
||||
{{ run.dsr }}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">OOS</div>
|
||||
<div class="metric-value" :class="{ ok: run.oos <= 2.5 }">
|
||||
{{ run.oos }}%
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Error State -->
|
||||
<div v-else-if="shadowRunQuery.isError" class="error-state">
|
||||
<p>Failed to load shadow run</p>
|
||||
</div>
|
||||
|
||||
<!-- Phase Breakdown -->
|
||||
<section class="phase-breakdown">
|
||||
<h2>Performance by Market Phase</h2>
|
||||
<div class="phase-grid">
|
||||
<div class="phase-card">
|
||||
<div class="phase-name">Bull Market</div>
|
||||
<div class="phase-metrics">
|
||||
<div>Return: <strong>{{ run.phases.bull.return }}%</strong></div>
|
||||
<div>Sharpe: <strong>{{ run.phases.bull.sharpe }}</strong></div>
|
||||
<div>Trades: <strong>{{ run.phases.bull.trades }}</strong></div>
|
||||
<!-- Data State -->
|
||||
<div v-else-if="run && run.id" class="shadow-run-detail">
|
||||
<div class="detail-section">
|
||||
<h2>Run #{{ run.id }}</h2>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<label>Model</label>
|
||||
<p>{{ run.modelName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="phase-card">
|
||||
<div class="phase-name">Bear Market</div>
|
||||
<div class="phase-metrics">
|
||||
<div>Return: <strong>{{ run.phases.bear.return }}%</strong></div>
|
||||
<div>Sharpe: <strong>{{ run.phases.bear.sharpe }}</strong></div>
|
||||
<div>Trades: <strong>{{ run.phases.bear.trades }}</strong></div>
|
||||
<div class="detail-item">
|
||||
<label>Status</label>
|
||||
<p>{{ run.status }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="phase-card">
|
||||
<div class="phase-name">Sideways Market</div>
|
||||
<div class="phase-metrics">
|
||||
<div>Return: <strong>{{ run.phases.sideways.return }}%</strong></div>
|
||||
<div>Sharpe: <strong>{{ run.phases.sideways.sharpe }}</strong></div>
|
||||
<div>Trades: <strong>{{ run.phases.sideways.trades }}</strong></div>
|
||||
<div class="detail-item">
|
||||
<label>PBO</label>
|
||||
<p>{{ run.pbo }}%</p>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>DSR</label>
|
||||
<p>{{ run.dsr }}%</p>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>OOS</label>
|
||||
<p>{{ run.oos }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Metadata -->
|
||||
<section class="metadata">
|
||||
<h3>Details</h3>
|
||||
<div class="metadata-grid">
|
||||
<div>
|
||||
<strong>Window Start:</strong>
|
||||
{{ run.windowStart }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Window End:</strong>
|
||||
{{ run.windowEnd }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Trading Days:</strong>
|
||||
{{ run.tradingDays }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Created:</strong>
|
||||
{{ run.createdAt }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shadow-run-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
.shadow-run-detail-page {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding-bottom: 16px;
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.detail-header h1 {
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin: 8px 0 0 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
.loading-state,
|
||||
.error-state {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--border-radius-md);
|
||||
background-color: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--kbx-color-primary, #3b82f6);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
.shadow-run-detail {
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: 2rem;
|
||||
background-color: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.breadcrumb a:hover {
|
||||
text-decoration: underline;
|
||||
.detail-section h2 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.validation-summary {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ccc;
|
||||
}
|
||||
|
||||
.validation-summary.status-valid {
|
||||
background: #f0fdf4;
|
||||
border-left-color: #10b981;
|
||||
}
|
||||
|
||||
.validation-summary.status-warning {
|
||||
background: #fffbeb;
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
|
||||
.validation-summary.status-invalid {
|
||||
background: #fef2f2;
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.validation-summary h2 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.overall-status {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
.detail-item label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.metric-value.positive {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.metric-value.negative {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.metric-value.ok {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.phase-breakdown {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.phase-breakdown h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.phase-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.phase-card {
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.phase-name {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.phase-metrics {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.metadata h3 {
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.metadata-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.metadata-grid div {
|
||||
padding: 8px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
.detail-item p {
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user