Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
def86d7f56 | ||
|
|
b9c69c546c |
No files matched your search
+16
-3
@@ -55,6 +55,7 @@ let socket
|
|||||||
let pollTimer
|
let pollTimer
|
||||||
let statusTimer
|
let statusTimer
|
||||||
let graceTimer
|
let graceTimer
|
||||||
|
let listTimer
|
||||||
|
|
||||||
async function refreshAll() {
|
async function refreshAll() {
|
||||||
const [p, o, e, q] = await Promise.all([
|
const [p, o, e, q] = await Promise.all([
|
||||||
@@ -183,11 +184,15 @@ onMounted(() => {
|
|||||||
if (!connected.value) pollingFallback.value = true
|
if (!connected.value) pollingFallback.value = true
|
||||||
}, 6000)
|
}, 6000)
|
||||||
|
|
||||||
// Initial loads are best-effort; the intervals below keep retrying.
|
// Initial loads are best-effort.
|
||||||
api.getStatus().then((s) => (status.value = s)).catch(() => {})
|
api.getStatus().then((s) => (status.value = s)).catch(() => {})
|
||||||
refreshAll().catch(() => {})
|
refreshAll().catch(() => {})
|
||||||
loadStrategies().catch(() => {})
|
|
||||||
loadPresets().catch(() => {})
|
// The strategy list and presets rarely change, so load them once but keep
|
||||||
|
// retrying until they arrive: a single failed first request used to leave the
|
||||||
|
// strategy dropdown empty forever.
|
||||||
|
loadStrategies()
|
||||||
|
loadPresets()
|
||||||
|
|
||||||
// Poll fast enough that the UI feels live even when it is the only channel.
|
// Poll fast enough that the UI feels live even when it is the only channel.
|
||||||
pollTimer = setInterval(() => refreshAll().catch(() => {}), 3000)
|
pollTimer = setInterval(() => refreshAll().catch(() => {}), 3000)
|
||||||
@@ -195,12 +200,20 @@ onMounted(() => {
|
|||||||
() => api.getStatus().then((s) => (status.value = s)).catch(() => {}),
|
() => api.getStatus().then((s) => (status.value = s)).catch(() => {}),
|
||||||
3000,
|
3000,
|
||||||
)
|
)
|
||||||
|
// Retry the static lists until non-empty (covers a failed first load and a
|
||||||
|
// backend that was still starting up).
|
||||||
|
listTimer = setInterval(() => {
|
||||||
|
if (!strategies.value.length) loadStrategies()
|
||||||
|
if (!presets.value.length) loadPresets()
|
||||||
|
if (strategies.value.length && presets.value.length) clearInterval(listTimer)
|
||||||
|
}, 3000)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
socket?.close()
|
socket?.close()
|
||||||
clearInterval(pollTimer)
|
clearInterval(pollTimer)
|
||||||
clearInterval(statusTimer)
|
clearInterval(statusTimer)
|
||||||
|
clearInterval(listTimer)
|
||||||
clearTimeout(graceTimer)
|
clearTimeout(graceTimer)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ async function run(payload) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CollapsiblePanel title="Backtest" storage-key="backtest" :summary="summary">
|
<CollapsiblePanel title="Backtest" storage-key="backtest" :summary="summary" :default-open="false">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<StrategyForm
|
<StrategyForm
|
||||||
:strategies="strategies"
|
:strategies="strategies"
|
||||||
|
|||||||
@@ -39,8 +39,9 @@ watch(open, (value) => {
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center justify-between gap-3 text-left"
|
class="-m-2 flex w-full items-center justify-between gap-3 rounded-lg p-2 text-left transition hover:bg-slate-700/40"
|
||||||
:aria-expanded="open"
|
:aria-expanded="open"
|
||||||
|
:title="open ? 'Collapse' : 'Expand'"
|
||||||
@click="open = !open"
|
@click="open = !open"
|
||||||
>
|
>
|
||||||
<span class="flex items-center gap-2 font-semibold">
|
<span class="flex items-center gap-2 font-semibold">
|
||||||
@@ -54,6 +55,7 @@ watch(open, (value) => {
|
|||||||
<path d="M6 3.5 11 8l-5 4.5z" />
|
<path d="M6 3.5 11 8l-5 4.5z" />
|
||||||
</svg>
|
</svg>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
<span class="text-xs font-normal text-slate-500">{{ open ? 'hide' : 'show' }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!open && summary" class="truncate text-xs font-normal text-slate-400">
|
<span v-if="!open && summary" class="truncate text-xs font-normal text-slate-400">
|
||||||
{{ summary }}
|
{{ summary }}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ async function run() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CollapsiblePanel title="Parameter sweep" storage-key="sweep" :summary="summary">
|
<CollapsiblePanel title="Parameter sweep" storage-key="sweep" :summary="summary" :default-open="false">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="grid gap-2 sm:grid-cols-4">
|
<div class="grid gap-2 sm:grid-cols-4">
|
||||||
<label class="space-y-1">
|
<label class="space-y-1">
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import StrategyPanel from './components/StrategyPanel.vue'
|
||||||
|
|
||||||
|
function mountPanel() {
|
||||||
|
return mount(StrategyPanel, {
|
||||||
|
props: {
|
||||||
|
strategies: [
|
||||||
|
{
|
||||||
|
key: 'sma_cross',
|
||||||
|
name: 'SMA Crossover',
|
||||||
|
description: 'test',
|
||||||
|
params: [{ name: 'fast', type: 'int', default: 10, description: '' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
status: { running: false, symbol: 'BTC/USDT', timeframe: '5m', open_positions: 0 },
|
||||||
|
busy: false,
|
||||||
|
presets: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('StrategyPanel collapse', () => {
|
||||||
|
it('shows the strategy form by default', () => {
|
||||||
|
const wrapper = mountPanel()
|
||||||
|
expect(wrapper.find('select').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides the form when the header is clicked', async () => {
|
||||||
|
const wrapper = mountPanel()
|
||||||
|
const header = wrapper.find('button')
|
||||||
|
expect(header.attributes('aria-expanded')).toBe('true')
|
||||||
|
|
||||||
|
await header.trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.find('button').attributes('aria-expanded')).toBe('false')
|
||||||
|
// The form container is v-show-hidden, so the select is no longer visible.
|
||||||
|
const hidden = wrapper.findAll('div').some((d) => d.element.style.display === 'none')
|
||||||
|
expect(hidden).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('picks the first strategy so the dropdown is populated', () => {
|
||||||
|
const wrapper = mountPanel()
|
||||||
|
const select = wrapper.find('select')
|
||||||
|
expect(select.element.value).toBe('sma_cross')
|
||||||
|
expect(select.findAll('option')).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in new issue
Block a user