- 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.
187 lines
6.5 KiB
Vue
187 lines
6.5 KiB
Vue
<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>
|