| <template> |
| <div> |
| <el-card shadow="never" style="border: 1px solid var(--border-color); margin-bottom: 16px;"> |
| <template #header> |
| <div style="display: flex; justify-content: space-between; align-items: center;"> |
| <span style="font-weight: 600;">可用策略</span> |
| <el-button type="primary" size="small" @click="showAddDialog = true"><el-icon><Plus /></el-icon> 添加策略</el-button> |
| </div> |
| </template> |
| <el-row :gutter="16"> |
| <el-col :span="8" v-for="s in availableStrategies" :key="s.type"> |
| <div style="padding: 16px; background: var(--bg-secondary); border-radius: 8px; border: 1px solid var(--border-color);"> |
| <div style="font-size: 16px; font-weight: 600; margin-bottom: 8px; color: var(--accent-blue);">{{ s.name }}</div> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 12px; line-height: 1.5;">{{ s.description }}</div> |
| <div style="font-size: 11px; color: var(--text-secondary);">默认参数: {{ JSON.stringify(s.default_params) }}</div> |
| </div> |
| </el-col> |
| </el-row> |
| </el-card> |
| |
| <el-card shadow="never" style="border: 1px solid var(--border-color);"> |
| <template #header> |
| <div style="display: flex; justify-content: space-between; align-items: center;"> |
| <span style="font-weight: 600;">策略实例</span> |
| <el-button size="small" text @click="loadStrategies"><el-icon><Refresh /></el-icon> 刷新</el-button> |
| </div> |
| </template> |
| <div v-if="strategies.length === 0" style="text-align: center; padding: 40px; color: var(--text-secondary);">暂无策略实例</div> |
| <el-table v-else :data="strategies" stripe style="width: 100%" size="small"> |
| <el-table-column prop="strategy_id" label="策略ID" width="160" /> |
| <el-table-column prop="strategy_type" label="类型" width="140" /> |
| <el-table-column prop="symbol" label="合约" width="120" /> |
| <el-table-column label="状态" width="100"> |
| <template #default="{ row }"> |
| <el-tag :type="row.status === 'RUNNING' ? 'success' : row.status === 'ERROR' ? 'danger' : 'info'" size="small" effect="dark"> |
| {{ row.status === 'RUNNING' ? '运行中' : row.status === 'ERROR' ? '异常' : '已停止' }} |
| </el-tag> |
| </template> |
| </el-table-column> |
| <el-table-column label="参数"> |
| <template #default="{ row }"><span style="font-size: 12px; color: var(--text-secondary);">{{ JSON.stringify(row.params) }}</span></template> |
| </el-table-column> |
| <el-table-column label="操作" width="200"> |
| <template #default="{ row }"> |
| <el-button v-if="row.status !== 'RUNNING'" size="small" type="success" text @click="startStrategy(row.strategy_id)">启动</el-button> |
| <el-button v-if="row.status === 'RUNNING'" size="small" type="warning" text @click="stopStrategy(row.strategy_id)">停止</el-button> |
| <el-button size="small" type="info" text @click="viewSignals(row.strategy_id)">信号</el-button> |
| <el-button size="small" type="danger" text @click="removeStrategy(row.strategy_id)">删除</el-button> |
| </template> |
| </el-table-column> |
| </el-table> |
| </el-card> |
| |
| <el-dialog v-model="showAddDialog" title="添加策略" width="480px"> |
| <el-form label-position="top"> |
| <el-form-item label="策略类型"> |
| <el-select v-model="newStrategy.strategy_type" style="width: 100%;" @change="onTypeChange"> |
| <el-option v-for="s in availableStrategies" :key="s.type" :label="s.name" :value="s.type" /> |
| </el-select> |
| </el-form-item> |
| <el-form-item label="分类筛选"> |
| <el-select v-model="dialogCategory" style="width: 100%;" placeholder="全部" @change="onDialogCategoryChange"> |
| <el-option label="全部" value="" /> |
| <el-option v-for="cat in store.categoryList.slice(1)" :key="cat.key" :label="cat.label" :value="cat.key" /> |
| </el-select> |
| </el-form-item> |
| <el-form-item label="合约"> |
| <el-select v-model="newStrategy.symbol" style="width: 100%;" filterable> |
| <el-option v-for="c in dialogContracts" :key="c.symbol" :label="`${c.name} (${c.exchange})`" :value="c.symbol" /> |
| </el-select> |
| </el-form-item> |
| <el-form-item v-for="(val, key) in newStrategy.params" :key="key" :label="paramLabel(key)"> |
| <el-input-number v-model="newStrategy.params[key]" :precision="typeof val === 'number' && val % 1 !== 0 ? 2 : 0" style="width: 100%;" /> |
| </el-form-item> |
| </el-form> |
| <template #footer> |
| <el-button @click="showAddDialog = false">取消</el-button> |
| <el-button type="primary" @click="addStrategy">确认添加</el-button> |
| </template> |
| </el-dialog> |
| |
| <el-dialog v-model="showSignals" title="策略信号" width="600px"> |
| <el-table :data="signals" stripe style="width: 100%" size="small" :max-height="400"> |
| <el-table-column prop="timestamp" label="时间" width="180" /> |
| <el-table-column label="类型" width="80"> |
| <template #default="{ row }"><el-tag :type="row.type === 'BUY' ? 'success' : 'danger'" size="small">{{ row.type }}</el-tag></template> |
| </el-table-column> |
| <el-table-column label="价格" width="100"> |
| <template #default="{ row }">{{ row.price?.toFixed(2) }}</template> |
| </el-table-column> |
| <el-table-column prop="reason" label="原因" /> |
| </el-table> |
| </el-dialog> |
| </div> |
| </template> |
| |
| <script setup> |
| import { ref, computed, onMounted } from 'vue' |
| import { ElMessage } from 'element-plus' |
| import { strategyApi } from '../api' |
| import { useTradingStore } from '../stores/trading' |
| |
| const store = useTradingStore() |
| const availableStrategies = ref([]) |
| const strategies = ref([]) |
| const showAddDialog = ref(false) |
| const showSignals = ref(false) |
| const signals = ref([]) |
| const dialogCategory = ref('') |
| |
| const dialogContracts = computed(() => { |
| if (!dialogCategory.value) return store.symbolList |
| return store.symbolList.filter(c => c.category === dialogCategory.value) |
| }) |
| |
| function onDialogCategoryChange() { |
| const list = dialogContracts.value |
| if (list.length > 0 && !list.find(c => c.symbol === newStrategy.value.symbol)) { |
| newStrategy.value.symbol = list[0].symbol |
| } |
| } |
| |
| const newStrategy = ref({ strategy_id: '', strategy_type: 'ma_crossover', symbol: '螺纹钢', params: { fast_period: 5, slow_period: 20, quantity: 1 } }) |
| const PARAM_LABELS = { fast_period: '快线周期', slow_period: '慢线周期', quantity: '下单数量', period: '计算周期', std_dev: '标准差倍数', lookback: '回看周期', k1: 'K1系数', k2: 'K2系数' } |
| function paramLabel(key) { return PARAM_LABELS[key] || key } |
| |
| function onTypeChange(type) { |
| const s = availableStrategies.value.find(s => s.type === type) |
| if (s) newStrategy.value.params = { ...s.default_params } |
| } |
| |
| async function loadAvailable() { try { const { data } = await strategyApi.getAvailable(); availableStrategies.value = data } catch (e) { console.error(e) } } |
| async function loadStrategies() { try { const { data } = await strategyApi.getStrategies(); strategies.value = data } catch (e) { console.error(e) } } |
| async function addStrategy() { |
| try { await strategyApi.addStrategy(newStrategy.value); ElMessage.success('策略添加成功'); showAddDialog.value = false; loadStrategies() } |
| catch (e) { ElMessage.error(e.response?.data?.detail || '添加失败') } |
| } |
| async function startStrategy(id) { try { await strategyApi.startStrategy(id); ElMessage.success('策略已启动'); loadStrategies() } catch (e) { ElMessage.error('启动失败') } } |
| async function stopStrategy(id) { try { await strategyApi.stopStrategy(id); ElMessage.success('策略已停止'); loadStrategies() } catch (e) { ElMessage.error('停止失败') } } |
| async function removeStrategy(id) { try { await strategyApi.removeStrategy(id); ElMessage.success('策略已删除'); loadStrategies() } catch (e) { ElMessage.error('删除失败') } } |
| async function viewSignals(id) { try { const { data } = await strategyApi.getSignals(id); signals.value = data; showSignals.value = true } catch (e) { console.error(e) } } |
| |
| onMounted(() => { loadAvailable(); loadStrategies(); store.fetchCategories(); store.fetchContracts() }) |
| </script> |
| |