Add trailing stops and parameter sweeps
Trailing stops: strategies can pass trailing_stop_pct to enter_long/enter_short. advance_trailing_stop in app/engine/portfolio.py ratchets the stop with the price (up for longs, down for shorts) and never loosens; the engine persists the ratcheted level and the backtester applies the same logic. Position gains a trailing_stop_pct column (additive migration). momentum exposes it as a param. Parameter sweeps: app/backtest/sweep.py runs a cartesian grid through the same backtester and ranks by return minus max drawdown. POST /api/sweep plus a dashboard Parameter sweep panel.
This commit is contained in:
1 parent
d80e7b155e
commit
9dc00eba95
22 files changed
+575
-7
No files matched your search
@@ -8,6 +8,7 @@ import OrdersTable from './components/OrdersTable.vue'
|
||||
import PositionsTable from './components/PositionsTable.vue'
|
||||
import StatusCards from './components/StatusCards.vue'
|
||||
import StrategyPanel from './components/StrategyPanel.vue'
|
||||
import SweepPanel from './components/SweepPanel.vue'
|
||||
|
||||
const status = ref({
|
||||
running: false,
|
||||
@@ -196,6 +197,7 @@ onUnmounted(() => {
|
||||
/>
|
||||
|
||||
<BacktestPanel :strategies="strategies" :status="status" />
|
||||
<SweepPanel :strategies="strategies" :status="status" />
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<EquityChart :points="equityPoints" />
|
||||
|
||||
@@ -19,6 +19,9 @@ export const placeOrder = (payload) => http.post('/orders', payload).then((r) =>
|
||||
export const runBacktest = (payload) =>
|
||||
http.post('/backtest', payload, { timeout: 60000 }).then((r) => r.data)
|
||||
|
||||
export const runSweep = (payload) =>
|
||||
http.post('/sweep', payload, { timeout: 120000 }).then((r) => r.data)
|
||||
|
||||
export const getPresets = () => http.get('/presets').then((r) => r.data)
|
||||
export const createPreset = (payload) => http.post('/presets', payload).then((r) => r.data)
|
||||
export const deletePreset = (id) => http.delete(`/presets/${id}`).then((r) => r.data)
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import * as api from '../api'
|
||||
import { fmtMoney, pnlClass, signed } from '../format'
|
||||
|
||||
const props = defineProps({
|
||||
strategies: { type: Array, required: true },
|
||||
status: { type: Object, required: true },
|
||||
})
|
||||
|
||||
const running = ref(false)
|
||||
const error = ref(null)
|
||||
const result = ref(null)
|
||||
const selectedKey = ref('')
|
||||
const limit = ref(500)
|
||||
const topN = ref(20)
|
||||
const symbol = ref(props.status.symbol || 'BTC/USDT')
|
||||
const timeframe = ref(props.status.timeframe || '5m')
|
||||
// param name -> comma-separated candidate values
|
||||
const gridText = ref({})
|
||||
|
||||
const selected = computed(() => props.strategies.find((s) => s.key === selectedKey.value))
|
||||
|
||||
watch(
|
||||
() => props.strategies,
|
||||
(list) => {
|
||||
if (!selectedKey.value && list.length) selectedKey.value = list[0].key
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(selected, (strategy) => {
|
||||
gridText.value = {}
|
||||
if (strategy) {
|
||||
// Seed each numeric param with a sensible two-value grid.
|
||||
for (const p of strategy.params) {
|
||||
if (p.type === 'int' || p.type === 'float') {
|
||||
const v = Number(p.default)
|
||||
gridText.value[p.name] = `${v},${p.type === 'int' ? v * 2 : (v * 2).toFixed(4)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function buildGrid() {
|
||||
const grid = {}
|
||||
for (const [name, raw] of Object.entries(gridText.value)) {
|
||||
const values = String(raw)
|
||||
.split(',')
|
||||
.map((x) => x.trim())
|
||||
.filter((x) => x !== '')
|
||||
.map((x) => Number(x))
|
||||
.filter((x) => !Number.isNaN(x))
|
||||
if (values.length) grid[name] = values
|
||||
}
|
||||
return grid
|
||||
}
|
||||
|
||||
async function run() {
|
||||
running.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const grid = buildGrid()
|
||||
if (!Object.keys(grid).length) {
|
||||
error.value = 'Provide at least one parameter with candidate values.'
|
||||
return
|
||||
}
|
||||
result.value = await api.runSweep({
|
||||
strategy_key: selectedKey.value,
|
||||
base_params: {},
|
||||
grid,
|
||||
symbol: symbol.value,
|
||||
timeframe: timeframe.value,
|
||||
limit: Number(limit.value),
|
||||
top_n: Number(topN.value),
|
||||
})
|
||||
if (result.value.error) error.value = result.value.error
|
||||
} catch (e) {
|
||||
error.value = e.response?.data?.detail || e.message
|
||||
} finally {
|
||||
running.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="font-semibold">Parameter sweep</h2>
|
||||
<span v-if="result" class="text-xs text-slate-400">
|
||||
{{ result.combinations }} combinations · top {{ result.rows.length }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2 sm:grid-cols-4">
|
||||
<label class="space-y-1">
|
||||
<span class="label">Strategy</span>
|
||||
<select v-model="selectedKey" class="input" :disabled="running">
|
||||
<option v-for="s in strategies" :key="s.key" :value="s.key">{{ s.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="label">Symbol</span>
|
||||
<input v-model="symbol" class="input" :disabled="running" />
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="label">Timeframe</span>
|
||||
<input v-model="timeframe" class="input" :disabled="running" />
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="label">Candles</span>
|
||||
<input v-model="limit" type="number" min="20" max="1000" class="input" :disabled="running" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="selected" class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<label v-for="(raw, name) in gridText" :key="name" class="space-y-1">
|
||||
<span class="label">{{ name }} (comma-separated)</span>
|
||||
<input v-model="gridText[name]" class="input" :disabled="running" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="flex items-center gap-2 text-sm text-slate-300">
|
||||
Top N
|
||||
<input v-model="topN" type="number" min="1" max="100" class="input w-20" :disabled="running" />
|
||||
</label>
|
||||
<button class="btn-primary" :disabled="running || !selectedKey" @click="run">
|
||||
{{ running ? 'Running…' : 'Run sweep' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="rounded-lg border border-rose-500/40 bg-rose-500/10 p-3 text-sm text-rose-200">
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<div v-if="result && result.rows.length" class="overflow-x-auto">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="label">
|
||||
<tr>
|
||||
<th class="pb-2">#</th>
|
||||
<th class="pb-2">Params</th>
|
||||
<th class="pb-2 text-right">Return</th>
|
||||
<th class="pb-2 text-right">Max DD</th>
|
||||
<th class="pb-2 text-right">Trades</th>
|
||||
<th class="pb-2 text-right">Win rate</th>
|
||||
<th class="pb-2 text-right">Sharpe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in result.rows" :key="i" class="border-t border-edge/60">
|
||||
<td class="py-1.5 text-slate-400">{{ i + 1 }}</td>
|
||||
<td class="py-1.5 font-mono text-xs">
|
||||
<span v-for="(v, k) in row.params" :key="k" class="mr-2">{{ k }}={{ v }}</span>
|
||||
</td>
|
||||
<td class="py-1.5 text-right tabular-nums" :class="pnlClass(row.metrics.total_return_pct)">
|
||||
{{ signed(row.metrics.total_return_pct) }}%
|
||||
</td>
|
||||
<td class="py-1.5 text-right tabular-nums text-amber-300">
|
||||
-{{ fmtMoney(row.metrics.max_drawdown_pct) }}%
|
||||
</td>
|
||||
<td class="py-1.5 text-right tabular-nums">{{ row.metrics.num_trades }}</td>
|
||||
<td class="py-1.5 text-right tabular-nums">{{ fmtMoney(row.metrics.win_rate_pct) }}%</td>
|
||||
<td class="py-1.5 text-right tabular-nums">
|
||||
{{ row.metrics.sharpe === null ? '—' : fmtMoney(row.metrics.sharpe) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user