Implement FastAPI backend and Vue 3 frontend for Lowkey Backtest UI
- Added FastAPI backend with core API endpoints for strategies, backtests, and data management. - Introduced Vue 3 frontend with a dark theme, enabling users to run backtests, adjust parameters, and compare results. - Implemented Pydantic schemas for request/response validation and SQLAlchemy models for database interactions. - Enhanced project structure with dedicated modules for services, routers, and components. - Updated dependencies in `pyproject.toml` and `frontend/package.json` to include FastAPI, SQLAlchemy, and Vue-related packages. - Improved `.gitignore` to exclude unnecessary files and directories.
This commit is contained in:
141
frontend/src/components/RunHistory.vue
Normal file
141
frontend/src/components/RunHistory.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useBacktest } from '@/composables/useBacktest'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const {
|
||||
runs,
|
||||
currentResult,
|
||||
selectedRuns,
|
||||
refreshRuns,
|
||||
loadRun,
|
||||
removeRun,
|
||||
toggleRunSelection
|
||||
} = useBacktest()
|
||||
|
||||
onMounted(() => {
|
||||
refreshRuns()
|
||||
})
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
return d.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
function formatReturn(val: number): string {
|
||||
return (val >= 0 ? '+' : '') + val.toFixed(2) + '%'
|
||||
}
|
||||
|
||||
async function handleClick(runId: string) {
|
||||
await loadRun(runId)
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
function handleCheckbox(e: Event, runId: string) {
|
||||
e.stopPropagation()
|
||||
toggleRunSelection(runId)
|
||||
}
|
||||
|
||||
function handleDelete(e: Event, runId: string) {
|
||||
e.stopPropagation()
|
||||
if (confirm('Delete this run?')) {
|
||||
removeRun(runId)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b border-border">
|
||||
<h2 class="text-sm font-semibold text-text-secondary uppercase tracking-wide">
|
||||
Run History
|
||||
</h2>
|
||||
<p class="text-xs text-text-muted mt-1">
|
||||
{{ runs.length }} runs | {{ selectedRuns.length }} selected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Run List -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div
|
||||
v-for="run in runs"
|
||||
:key="run.run_id"
|
||||
@click="handleClick(run.run_id)"
|
||||
class="p-3 border-b border-border-muted cursor-pointer hover:bg-bg-hover transition-colors"
|
||||
:class="{ 'bg-bg-tertiary': currentResult?.run_id === run.run_id }"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<!-- Checkbox for comparison -->
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedRuns.includes(run.run_id)"
|
||||
@click="handleCheckbox($event, run.run_id)"
|
||||
class="mt-1 w-4 h-4 rounded border-border bg-bg-tertiary"
|
||||
/>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Strategy & Symbol -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm truncate">{{ run.strategy }}</span>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-bg-tertiary text-text-secondary">
|
||||
{{ run.symbol }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Metrics -->
|
||||
<div class="flex items-center gap-3 mt-1">
|
||||
<span
|
||||
class="text-sm font-mono"
|
||||
:class="run.total_return >= 0 ? 'profit' : 'loss'"
|
||||
>
|
||||
{{ formatReturn(run.total_return) }}
|
||||
</span>
|
||||
<span class="text-xs text-text-muted">
|
||||
SR {{ run.sharpe_ratio.toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Date -->
|
||||
<div class="text-xs text-text-muted mt-1">
|
||||
{{ formatDate(run.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete button -->
|
||||
<button
|
||||
@click="handleDelete($event, run.run_id)"
|
||||
class="p-1 rounded hover:bg-loss/20 text-text-muted hover:text-loss transition-colors"
|
||||
title="Delete run"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-if="runs.length === 0" class="p-8 text-center text-text-muted">
|
||||
<p>No runs yet.</p>
|
||||
<p class="text-xs mt-1">Run a backtest to see results here.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compare Button -->
|
||||
<div v-if="selectedRuns.length >= 2" class="p-4 border-t border-border">
|
||||
<router-link
|
||||
to="/compare"
|
||||
class="btn btn-primary w-full"
|
||||
>
|
||||
Compare {{ selectedRuns.length }} Runs
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user