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:
186
frontend/src/components/BacktestConfig.vue
Normal file
186
frontend/src/components/BacktestConfig.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useBacktest } from '@/composables/useBacktest'
|
||||
import type { BacktestRequest } from '@/api/types'
|
||||
|
||||
const { strategies, symbols, loading, init, executeBacktest } = useBacktest()
|
||||
|
||||
// Form state
|
||||
const selectedStrategy = ref('')
|
||||
const selectedSymbol = ref('')
|
||||
const selectedMarket = ref('perpetual')
|
||||
const timeframe = ref('1h')
|
||||
const initCash = ref(10000)
|
||||
const leverage = ref<number | null>(null)
|
||||
const slStop = ref<number | null>(null)
|
||||
const tpStop = ref<number | null>(null)
|
||||
const params = ref<Record<string, number | boolean>>({})
|
||||
|
||||
// Initialize
|
||||
onMounted(async () => {
|
||||
await init()
|
||||
if (strategies.value.length > 0 && strategies.value[0]) {
|
||||
selectedStrategy.value = strategies.value[0].name
|
||||
}
|
||||
})
|
||||
|
||||
// Get current strategy config
|
||||
const currentStrategy = computed(() =>
|
||||
strategies.value.find(s => s.name === selectedStrategy.value)
|
||||
)
|
||||
|
||||
// Filter symbols by market type
|
||||
const filteredSymbols = computed(() =>
|
||||
symbols.value.filter(s => s.market_type === selectedMarket.value)
|
||||
)
|
||||
|
||||
// Update params when strategy changes
|
||||
watch(selectedStrategy, (name) => {
|
||||
const strategy = strategies.value.find(s => s.name === name)
|
||||
if (strategy) {
|
||||
params.value = { ...strategy.default_params } as Record<string, number | boolean>
|
||||
selectedMarket.value = strategy.market_type
|
||||
leverage.value = strategy.default_leverage > 1 ? strategy.default_leverage : null
|
||||
}
|
||||
})
|
||||
|
||||
// Update symbol when market changes
|
||||
watch([filteredSymbols, selectedMarket], () => {
|
||||
const firstSymbol = filteredSymbols.value[0]
|
||||
if (filteredSymbols.value.length > 0 && firstSymbol && !filteredSymbols.value.find(s => s.symbol === selectedSymbol.value)) {
|
||||
selectedSymbol.value = firstSymbol.symbol
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!selectedStrategy.value || !selectedSymbol.value) return
|
||||
|
||||
const request: BacktestRequest = {
|
||||
strategy: selectedStrategy.value,
|
||||
symbol: selectedSymbol.value,
|
||||
market_type: selectedMarket.value,
|
||||
timeframe: timeframe.value,
|
||||
init_cash: initCash.value,
|
||||
leverage: leverage.value,
|
||||
sl_stop: slStop.value,
|
||||
tp_stop: tpStop.value,
|
||||
params: params.value,
|
||||
}
|
||||
|
||||
await executeBacktest(request)
|
||||
}
|
||||
|
||||
function getParamType(value: unknown): 'number' | 'boolean' | 'unknown' {
|
||||
if (typeof value === 'boolean') return 'boolean'
|
||||
if (typeof value === 'number') return 'number'
|
||||
return 'unknown'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<h2 class="text-lg font-semibold mb-4">Backtest Configuration</h2>
|
||||
|
||||
<form @submit.prevent="handleSubmit" class="space-y-4">
|
||||
<!-- Strategy -->
|
||||
<div>
|
||||
<label class="block text-xs text-text-secondary uppercase mb-1">Strategy</label>
|
||||
<select v-model="selectedStrategy" class="w-full">
|
||||
<option v-for="s in strategies" :key="s.name" :value="s.name">
|
||||
{{ s.display_name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Market Type & Symbol -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs text-text-secondary uppercase mb-1">Market</label>
|
||||
<select v-model="selectedMarket" class="w-full">
|
||||
<option value="spot">Spot</option>
|
||||
<option value="perpetual">Perpetual</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-text-secondary uppercase mb-1">Symbol</label>
|
||||
<select v-model="selectedSymbol" class="w-full">
|
||||
<option v-for="s in filteredSymbols" :key="s.symbol" :value="s.symbol">
|
||||
{{ s.symbol }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeframe & Cash -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs text-text-secondary uppercase mb-1">Timeframe</label>
|
||||
<select v-model="timeframe" class="w-full">
|
||||
<option value="1h">1 Hour</option>
|
||||
<option value="4h">4 Hours</option>
|
||||
<option value="1d">1 Day</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-text-secondary uppercase mb-1">Initial Cash</label>
|
||||
<input type="number" v-model.number="initCash" class="w-full" min="100" step="100" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leverage (perpetual only) -->
|
||||
<div v-if="selectedMarket === 'perpetual'" class="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs text-text-secondary uppercase mb-1">Leverage</label>
|
||||
<input type="number" v-model.number="leverage" class="w-full" min="1" max="100" placeholder="1" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-text-secondary uppercase mb-1">Stop Loss %</label>
|
||||
<input type="number" v-model.number="slStop" class="w-full" min="0" max="100" step="0.1" placeholder="None" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-text-secondary uppercase mb-1">Take Profit %</label>
|
||||
<input type="number" v-model.number="tpStop" class="w-full" min="0" max="100" step="0.1" placeholder="None" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Strategy Parameters -->
|
||||
<div v-if="currentStrategy && Object.keys(params).length > 0">
|
||||
<h3 class="text-sm font-medium text-text-secondary mb-2">Strategy Parameters</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div v-for="(value, key) in params" :key="key">
|
||||
<label class="block text-xs text-text-secondary uppercase mb-1">
|
||||
{{ String(key).replace(/_/g, ' ') }}
|
||||
</label>
|
||||
<template v-if="getParamType(value) === 'boolean'">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="Boolean(value)"
|
||||
@change="params[key] = ($event.target as HTMLInputElement).checked"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input
|
||||
type="number"
|
||||
:value="value"
|
||||
@input="params[key] = parseFloat(($event.target as HTMLInputElement).value)"
|
||||
class="w-full"
|
||||
step="any"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-full"
|
||||
:disabled="loading || !selectedStrategy || !selectedSymbol"
|
||||
>
|
||||
<span v-if="loading" class="spinner"></span>
|
||||
<span v-else>Run Backtest</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user