feat: 添加前端界面和市场分析模块

- 新增 Vue 3 + Vuetify 前端界面
- 新增市场分析模块 (market/)
- 更新主服务器和路由
- 更新 MT5 EA 文件
- 添加 .gitignore 排除临时文件
This commit is contained in:
guaiwoluo2020
2026-03-10 17:38:13 +08:00
parent 0c9cf048d2
commit 51b2f30748
40 changed files with 8576 additions and 151 deletions
+63
View File
@@ -0,0 +1,63 @@
<template>
<v-app>
<v-app-bar app color="primary" dark>
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title>量化交易系统</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>mdi-refresh</v-icon>
</v-btn>
</v-app-bar>
<v-navigation-drawer v-model="drawer" app>
<v-list>
<v-list-item
v-for="item in menuItems"
:key="item.title"
:to="item.path"
link
>
<v-list-item-icon>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
const drawer = ref(false)
const menuItems = [
{ title: '仪表板', path: '/', icon: 'mdi-view-dashboard' },
{ title: '交易指令', path: '/trades', icon: 'mdi-format-list-bulleted' },
{ title: '行情分析', path: '/market', icon: 'mdi-chart-candlestick' },
{ title: '统计数据', path: '/statistics', icon: 'mdi-chart-line' },
{ title: '服务状态', path: '/status', icon: 'mdi-information' },
]
return {
drawer,
menuItems,
}
},
}
</script>
<style scoped>
.v-app-bar {
z-index: 1000;
}
</style>
+149
View File
@@ -0,0 +1,149 @@
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:8000',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
export const marketAPI = {
// 获取所有symbol列表
async getSymbols() {
const response = await api.get('/market/symbols')
return response.data
},
// 获取K线数据
async getKlines(symbol, period = 'M5', count = 100) {
const encodedSymbol = encodeURIComponent(symbol)
const response = await api.get(`/market/kline/${encodedSymbol}`, {
params: { period, count }
})
return response.data
},
// 获取转折点数据
async getPivots(symbol, period = null, direction = null, count = 50) {
const params = { count }
if (period) params.period = period
if (direction) params.direction = direction
const encodedSymbol = encodeURIComponent(symbol)
const response = await api.get(`/market/pivots/${encodedSymbol}`, { params })
return response.data
},
// 获取行情状态
async getStatus() {
const response = await api.get('/market/status')
return response.data
},
// 获取阈值配置
async getThresholds() {
const response = await api.get('/market/thresholds')
return response.data
},
// 创建WebSocket连接
createWebSocket(onMessage, onError, onOpen, onClose) {
const ws = new WebSocket('ws://localhost:8000/ws/market')
ws.onopen = () => {
console.log('WebSocket 连接成功')
if (onOpen) onOpen()
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
if (onMessage) onMessage(data)
} catch (e) {
console.error('WebSocket 消息解析错误:', e)
}
}
ws.onerror = (error) => {
console.error('WebSocket 错误:', error)
if (onError) onError(error)
}
ws.onclose = () => {
console.log('WebSocket 连接关闭')
if (onClose) onClose()
}
return ws
},
// 发送心跳
sendPing(ws) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }))
}
},
// 获取趋势分析
async getTrend(symbol) {
const response = await api.get(`/trend/${encodeURIComponent(symbol)}`)
return response.data
},
// 生成交易建议
async generateTradeOrder(symbol) {
const response = await api.post(`/trend/generate_order/${encodeURIComponent(symbol)}`)
return response.data
},
// 获取待确认订单
async getPendingOrders(symbol = null) {
const params = symbol ? { symbol } : {}
const response = await api.get('/pending_orders', { params })
return response.data
},
// 确认订单
async confirmOrder(orderId) {
const response = await api.post(`/pending_orders/${orderId}/confirm`)
return response.data
},
// 确认订单并更新参数
async confirmOrderWithUpdate(orderId, updateData) {
const response = await api.post(`/pending_orders/${orderId}/confirm`, updateData)
return response.data
},
// 拒绝订单
async rejectOrder(orderId) {
const response = await api.post(`/pending_orders/${orderId}/reject`)
return response.data
},
// 获取交易配置
async getTradeConfig() {
const response = await api.get('/trade_config')
return response.data
},
// 更新交易配置
async updateTradeConfig(config) {
const response = await api.post('/trade_config', config)
return response.data
},
// 获取统计数据(包含持仓)
async getStatistics(count = 1) {
const response = await api.get('/query_statistics', { params: { count } })
return response.data
},
// 平仓
async closePosition(ticket, symbol) {
const response = await api.post('/close_position', { ticket, symbol })
return response.data
}
}
export default api
+71
View File
@@ -0,0 +1,71 @@
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:8000',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 可以在这里添加认证token等
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response
},
(error) => {
console.error('API Error:', error)
return Promise.reject(error)
}
)
export const tradingAPI = {
// 健康检查
async health() {
const response = await api.get('/health')
return response.data
},
// 获取服务状态
async getStatus() {
const response = await api.get('/status')
return response.data
},
// 发送交易指令
async sendTradeInstructions(instructions) {
const response = await api.post('/send_trade_instructions', instructions)
return response.data
},
// 查询待执行指令
async getPendingTrades() {
const response = await api.get('/query_pending_trades')
return response.data
},
// 查询统计数据
async getStatistics() {
const response = await api.get('/query_statistics')
return response.data
},
// 清空指令
async clearTrades() {
const response = await api.delete('/clear_trades')
return response.data
},
}
export default api
+13
View File
@@ -0,0 +1,13 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify'
import './style.css'
const app = createApp(App)
app.use(router)
app.use(vuetify)
app.mount('#app')
+34
View File
@@ -0,0 +1,34 @@
// plugins/vuetify.js
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { aliases, mdi } from 'vuetify/iconsets/mdi'
export default createVuetify({
components,
directives,
icons: {
defaultSet: 'mdi',
aliases,
sets: {
mdi,
},
},
theme: {
defaultTheme: 'light',
themes: {
light: {
colors: {
primary: '#1976D2',
secondary: '#424242',
accent: '#82B1FF',
error: '#FF5252',
info: '#2196F3',
success: '#4CAF50',
warning: '#FFC107',
},
},
},
},
})
+41
View File
@@ -0,0 +1,41 @@
import { createRouter, createWebHistory } from 'vue-router'
import Dashboard from '../views/Dashboard.vue'
import TradeOrders from '../views/TradeOrders.vue'
import Statistics from '../views/Statistics.vue'
import Status from '../views/Status.vue'
import Market from '../views/Market.vue'
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard
},
{
path: '/trades',
name: 'TradeOrders',
component: TradeOrders
},
{
path: '/statistics',
name: 'Statistics',
component: Statistics
},
{
path: '/status',
name: 'Status',
component: Status
},
{
path: '/market',
name: 'Market',
component: Market
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
+11
View File
@@ -0,0 +1,11 @@
/* style.css */
#app {
font-family: 'Roboto', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
padding: 0;
}
+136
View File
@@ -0,0 +1,136 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<h1 class="mb-4">仪表板</h1>
</v-col>
</v-row>
<!-- 状态卡片 -->
<v-row>
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-heart</v-icon>
服务状态
</v-card-title>
<v-card-text>
<v-chip
:color="status.ok ? 'success' : 'error'"
variant="flat"
>
{{ status.ok ? '正常' : '异常' }}
</v-chip>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-format-list-bulleted</v-icon>
待执行指令
</v-card-title>
<v-card-text>
<div class="text-h4">{{ pendingTradesCount }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-chart-line</v-icon>
统计记录
</v-card-title>
<v-card-text>
<div class="text-h4">{{ statisticsCount }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-currency-usd</v-icon>
活跃品种
</v-card-title>
<v-card-text>
<div class="text-h4">{{ activeSymbolsCount }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 错误信息 -->
<v-row v-if="error">
<v-col cols="12">
<v-alert type="error" dismissible>
{{ error }}
</v-alert>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { ref, onMounted } from 'vue'
import { tradingAPI } from '@/api/trading'
export default {
name: 'Dashboard',
setup() {
const status = ref({ ok: false })
const pendingTradesCount = ref(0)
const statisticsCount = ref(0)
const activeSymbolsCount = ref(0)
const error = ref('')
const loadData = async () => {
try {
error.value = ''
// 获取服务状态
const statusData = await tradingAPI.getStatus()
status.value = { ok: statusData.status === 'ok' }
// 获取待执行指令数量
const pendingTrades = await tradingAPI.getPendingTrades()
pendingTradesCount.value = pendingTrades.length || 0
// 获取统计数据数量
const statistics = await tradingAPI.getStatistics()
statisticsCount.value = (statistics.statistics || []).length
// 计算活跃品种数量
const symbols = new Set()
if (pendingTrades && pendingTrades.length > 0) {
pendingTrades.forEach(trade => {
if (trade.symbol) symbols.add(trade.symbol)
})
}
activeSymbolsCount.value = symbols.size
} catch (err) {
error.value = `加载数据失败: ${err.message}`
console.error('Dashboard error:', err)
}
}
onMounted(() => {
loadData()
// 每30秒自动刷新
setInterval(loadData, 30000)
})
return {
status,
pendingTradesCount,
statisticsCount,
activeSymbolsCount,
error,
loadData,
}
},
}
</script>
File diff suppressed because it is too large Load Diff
+248
View File
@@ -0,0 +1,248 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<h1 class="mb-4">统计数据分析</h1>
</v-col>
</v-row>
<!-- 汇总统计 -->
<v-row>
<v-col cols="12" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-counter</v-icon>
总记录数
</v-card-title>
<v-card-text>
<div class="text-h4">{{ totalRecords }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-trending-up</v-icon>
平均价格
</v-card-title>
<v-card-text>
<div class="text-h4">{{ averagePrice.toFixed(5) }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-chart-line</v-icon>
最高价格
</v-card-title>
<v-card-text>
<div class="text-h4">{{ maxPrice.toFixed(5) }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-chart-line-variant</v-icon>
最低价格
</v-card-title>
<v-card-text>
<div class="text-h4">{{ minPrice.toFixed(5) }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 图表 -->
<v-row>
<v-col cols="12">
<v-card>
<v-card-title>价格趋势图</v-card-title>
<v-card-text>
<div ref="chartContainer" style="width: 100%; height: 400px;"></div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 数据表格 -->
<v-row>
<v-col cols="12">
<v-card>
<v-card-title>详细数据</v-card-title>
<v-card-text>
<v-data-table
:headers="tableHeaders"
:items="statistics"
:loading="loading"
:items-per-page="itemsPerPage"
no-data-text="暂无统计数据"
density="compact"
>
<template v-slot:item.bidPrice="{ item }">
{{ item.bidPrice.toFixed(2) }}
</template>
<template v-slot:item.askPrice="{ item }">
{{ item.askPrice.toFixed(2) }}
</template>
<template v-slot:item.balance="{ item }">
{{ item.balance.toFixed(2) }}
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 错误信息 -->
<v-row v-if="error">
<v-col cols="12">
<v-alert type="error" dismissible>
{{ error }}
</v-alert>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { ref, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { tradingAPI } from '@/api/trading'
export default {
name: 'Statistics',
setup() {
const chartContainer = ref(null)
const chart = ref(null)
const loading = ref(false)
const error = ref('')
const statistics = ref([])
const itemsPerPage = ref(10)
const totalRecords = ref(0)
const averagePrice = ref(0)
const maxPrice = ref(0)
const minPrice = ref(0)
const tableHeaders = [
{ title: '时间', key: 'timestamp', width: '20%' },
{ title: '品种', key: 'symbol', width: '15%' },
{ title: '买价', key: 'bidPrice', width: '15%' },
{ title: '卖价', key: 'askPrice', width: '15%' },
{ title: 'Tick数', key: 'tickCount', width: '15%' },
{ title: '余额', key: 'balance', width: '20%' },
]
const loadStatistics = async () => {
try {
loading.value = true
error.value = ''
const data = await tradingAPI.getStatistics()
statistics.value = data.statistics || []
// 计算统计信息
if (statistics.value.length > 0) {
totalRecords.value = statistics.value.length
const prices = statistics.value.map(item => (item.bidPrice + item.askPrice) / 2)
averagePrice.value = prices.reduce((a, b) => a + b, 0) / prices.length
maxPrice.value = Math.max(...prices)
minPrice.value = Math.min(...prices)
} else {
totalRecords.value = 0
averagePrice.value = 0
maxPrice.value = 0
minPrice.value = 0
}
// 更新图表
updateChart()
} catch (err) {
error.value = `加载统计数据失败: ${err.message}`
console.error('Load statistics error:', err)
} finally {
loading.value = false
}
}
const updateChart = async () => {
await nextTick()
if (!chartContainer.value) return
if (chart.value) {
chart.value.dispose()
}
chart.value = echarts.init(chartContainer.value)
const option = {
title: {
text: '价格趋势'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: statistics.value.map(item => item.timestamp)
},
yAxis: {
type: 'value',
name: '价格'
},
series: [{
name: '买价',
type: 'line',
data: statistics.value.map(item => item.bidPrice),
smooth: true,
lineStyle: {
color: '#1976D2'
}
}, {
name: '卖价',
type: 'line',
data: statistics.value.map(item => item.askPrice),
smooth: true,
lineStyle: {
color: '#4CAF50'
}
}]
}
chart.value.setOption(option)
}
const formatTime = (timestamp) => {
if (!timestamp) return ''
return new Date(timestamp * 1000).toLocaleString('zh-CN')
}
onMounted(() => {
loadStatistics()
// 每30秒自动刷新
setInterval(loadStatistics, 30000)
})
return {
chartContainer,
loading,
error,
statistics,
itemsPerPage,
totalRecords,
averagePrice,
maxPrice,
minPrice,
tableHeaders,
loadStatistics,
formatTime,
}
},
}
</script>
+250
View File
@@ -0,0 +1,250 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<h1 class="mb-4">服务状态监控</h1>
</v-col>
</v-row>
<!-- 健康状态 -->
<v-row>
<v-col cols="12" md="6">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2" :color="healthStatus.ok ? 'success' : 'error'">
mdi-heart
</v-icon>
服务健康状态
</v-card-title>
<v-card-text>
<v-chip
:color="healthStatus.ok ? 'success' : 'error'"
variant="flat"
size="large"
>
{{ healthStatus.ok ? '服务正常' : '服务异常' }}
</v-chip>
<div class="mt-2 text-caption">
最后检查: {{ lastCheckTime }}
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-information</v-icon>
系统信息
</v-card-title>
<v-card-text>
<div class="d-flex flex-column ga-2">
<div><strong>版本:</strong> {{ systemInfo.version || '未知' }}</div>
<div><strong>运行时间:</strong> {{ formatUptime(systemInfo.uptime) }}</div>
<div><strong>内存使用:</strong> {{ formatMemory(systemInfo.memory) }}</div>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 详细指标 -->
<v-row>
<v-col cols="12">
<v-card>
<v-card-title>详细服务指标</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" sm="6" md="3">
<v-card variant="outlined">
<v-card-text class="text-center">
<div class="text-h4">{{ serviceMetrics.pendingTrades }}</div>
<div class="text-caption">待执行指令</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card variant="outlined">
<v-card-text class="text-center">
<div class="text-h4">{{ serviceMetrics.totalTrades }}</div>
<div class="text-caption">总交易次数</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card variant="outlined">
<v-card-text class="text-center">
<div class="text-h4">{{ serviceMetrics.activeSymbols }}</div>
<div class="text-caption">活跃品种</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card variant="outlined">
<v-card-text class="text-center">
<div class="text-h4">{{ serviceMetrics.successRate }}%</div>
<div class="text-caption">成功率</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 连接状态 -->
<v-row>
<v-col cols="12">
<v-card>
<v-card-title>连接状态</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex align-center">
<v-icon
:color="connectionStatus.backend ? 'success' : 'error'"
class="me-2"
>
mdi-server
</v-icon>
<div>
<div class="font-weight-bold">后端服务</div>
<div class="text-caption">
{{ connectionStatus.backend ? '已连接' : '未连接' }}
</div>
</div>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex align-center">
<v-icon
:color="connectionStatus.mt5 ? 'success' : 'warning'"
class="me-2"
>
mdi-chart-line
</v-icon>
<div>
<div class="font-weight-bold">MT5 连接</div>
<div class="text-caption">
{{ connectionStatus.mt5 ? '已连接' : '未连接' }}
</div>
</div>
</div>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 错误信息 -->
<v-row v-if="error">
<v-col cols="12">
<v-alert type="error" dismissible>
{{ error }}
</v-alert>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { ref, onMounted } from 'vue'
import { tradingAPI } from '@/api/trading'
export default {
name: 'Status',
setup() {
const healthStatus = ref({ ok: false })
const systemInfo = ref({})
const serviceMetrics = ref({
pendingTrades: 0,
totalTrades: 0,
activeSymbols: 0,
successRate: 0,
})
const connectionStatus = ref({
backend: false,
mt5: false,
})
const lastCheckTime = ref('')
const error = ref('')
const checkStatus = async () => {
try {
error.value = ''
// 检查健康状态
const health = await tradingAPI.health()
healthStatus.value = health
// 获取详细状态
const status = await tradingAPI.getStatus()
systemInfo.value = status.system || {}
// 适配实际API响应结构
serviceMetrics.value = {
pendingTrades: status.pending_instructions || 0,
totalTrades: status.statistics_records || 0,
activeSymbols: status.symbols?.length || 0,
successRate: status.success_rate || 0,
}
// 检查连接状态
connectionStatus.value = {
backend: health.ok,
mt5: status.mt5_connected || false,
}
lastCheckTime.value = new Date().toLocaleString('zh-CN')
} catch (err) {
error.value = `检查状态失败: ${err.message}`
healthStatus.value = { ok: false }
connectionStatus.value = { backend: false, mt5: false }
console.error('Status check error:', err)
}
}
const formatUptime = (seconds) => {
if (!seconds) return '未知'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
return `${hours}小时 ${minutes}分钟`
}
const formatMemory = (bytes) => {
if (!bytes) return '未知'
const mb = (bytes / 1024 / 1024).toFixed(1)
return `${mb} MB`
}
onMounted(() => {
checkStatus()
// 每5秒自动检查
setInterval(checkStatus, 5000)
})
return {
healthStatus,
systemInfo,
serviceMetrics,
connectionStatus,
lastCheckTime,
error,
checkStatus,
formatUptime,
formatMemory,
}
},
}
</script>
+308
View File
@@ -0,0 +1,308 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<h1 class="mb-4">交易指令管理</h1>
</v-col>
</v-row>
<!-- 发送交易指令表单 -->
<v-row>
<v-col cols="12" md="6">
<v-card>
<v-card-title>发送交易指令</v-card-title>
<v-card-text>
<v-form ref="form" v-model="formValid">
<v-select
v-model="tradeForm.symbol"
:items="symbols"
label="交易品种"
required
:rules="[v => !!v || '请选择交易品种']"
></v-select>
<v-select
v-model="tradeForm.direction"
:items="directions"
label="买卖方向"
required
:rules="[v => !!v || '请选择买卖方向']"
></v-select>
<v-text-field
v-model.number="tradeForm.volume"
label="手数"
type="number"
step="0.01"
required
:rules="[v => v > 0 || '手数必须大于0']"
></v-text-field>
<v-text-field
v-model.number="tradeForm.price"
label="执行价格"
type="number"
step="0.00001"
required
:rules="[v => v > 0 || '执行价格必须大于0']"
></v-text-field>
<v-text-field
v-model.number="tradeForm.sl"
label="止损价格"
type="number"
step="0.00001"
:rules="[v => !v || v > 0 || '止损价格必须大于0']"
></v-text-field>
<v-text-field
v-model.number="tradeForm.tp"
label="止盈价格"
type="number"
step="0.00001"
:rules="[v => !v || v > 0 || '止盈价格必须大于0']"
></v-text-field>
<v-btn
color="primary"
:disabled="!formValid"
:loading="sending"
@click="sendTrade"
block
class="mt-4"
>
发送交易指令
</v-btn>
</v-form>
</v-card-text>
</v-card>
</v-col>
<!-- 待执行指令列表 -->
<v-col cols="12" md="6">
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
待执行指令
<v-btn
color="error"
size="small"
:loading="clearing"
@click="clearAllTrades"
>
清空全部
</v-btn>
</v-card-title>
<v-card-text>
<v-data-table
:headers="tradeHeaders"
:items="pendingTrades"
:loading="loadingTrades"
no-data-text="暂无待执行指令"
density="compact"
>
<template v-slot:item.direction="{ item }">
<v-chip
:color="item.direction === 'BUY' ? 'success' : 'error'"
size="small"
>
{{ item.direction === 'BUY' ? '买入' : '卖出' }}
</v-chip>
</template>
<template v-slot:item.timestamp="{ item }">
{{ formatTime(item.timestamp) }}
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 错误信息 -->
<v-row v-if="error">
<v-col cols="12">
<v-alert type="error" dismissible>
{{ error }}
</v-alert>
</v-col>
</v-row>
<!-- 成功信息 -->
<v-row v-if="success">
<v-col cols="12">
<v-alert type="success" dismissible>
{{ success }}
</v-alert>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { ref, onMounted } from 'vue'
import { tradingAPI } from '@/api/trading'
import { marketAPI } from '@/api/market'
export default {
name: 'TradeOrders',
setup() {
const form = ref(null)
const formValid = ref(false)
const sending = ref(false)
const loadingTrades = ref(false)
const clearing = ref(false)
const error = ref('')
const success = ref('')
const tradeForm = ref({
symbol: '',
direction: '',
volume: 0.01,
price: 0,
sl: 0,
tp: 0,
})
const symbols = ref([])
const directions = [
{ title: '买入', value: 'BUY' },
{ title: '卖出', value: 'SELL' },
]
const tradeHeaders = [
{ title: '品种', key: 'symbol', width: '20%' },
{ title: '方向', key: 'direction', width: '15%' },
{ title: '手数', key: 'volume', width: '15%' },
{ title: '价格', key: 'price', width: '20%' },
{ title: '时间', key: 'timestamp', width: '30%' },
]
const pendingTrades = ref([])
const loadPendingTrades = async () => {
try {
loadingTrades.value = true
error.value = ''
const data = await tradingAPI.getPendingTrades()
// 将对象格式转换为数组格式
const tradesObj = data.pending_trades || {}
const tradesArray = []
Object.keys(tradesObj).forEach(symbol => {
tradesObj[symbol].forEach(trade => {
tradesArray.push({
...trade,
direction: trade.action === 'b' ? 'BUY' : 'SELL',
volume: trade.mount
})
})
})
pendingTrades.value = tradesArray
} catch (err) {
error.value = `加载指令失败: ${err.message}`
console.error('Load trades error:', err)
} finally {
loadingTrades.value = false
}
}
const sendTrade = async () => {
try {
sending.value = true
error.value = ''
success.value = ''
// 价格验证
if (tradeForm.value.sl > 0 && tradeForm.value.tp > 0) {
if (tradeForm.value.direction === 'BUY' && !(tradeForm.value.sl < tradeForm.value.price && tradeForm.value.price < tradeForm.value.tp)) {
error.value = '买入指令必须满足: 止损 < 执行价格 < 止盈'
return
}
if (tradeForm.value.direction === 'SELL' && !(tradeForm.value.tp < tradeForm.value.price && tradeForm.value.price < tradeForm.value.sl)) {
error.value = '卖出指令必须满足: 止盈 < 执行价格 < 止损'
return
}
}
// 转换数据格式
const instruction = {
symbol: tradeForm.value.symbol,
action: tradeForm.value.direction === 'BUY' ? 'b' : 's',
mount: tradeForm.value.volume,
price: tradeForm.value.price,
sl: tradeForm.value.sl || 0,
tp: tradeForm.value.tp || 0
}
await tradingAPI.sendTradeInstructions([instruction])
success.value = '交易指令发送成功!'
form.value.reset()
await loadPendingTrades()
} catch (err) {
error.value = `发送指令失败: ${err.message}`
console.error('Send trade error:', err)
} finally {
sending.value = false
}
}
const clearAllTrades = async () => {
if (!confirm('确定要清空所有待执行指令吗?')) return
try {
clearing.value = true
error.value = ''
success.value = ''
await tradingAPI.clearTrades()
success.value = '已清空所有指令!'
await loadPendingTrades()
} catch (err) {
error.value = `清空指令失败: ${err.message}`
console.error('Clear trades error:', err)
} finally {
clearing.value = false
}
}
const formatTime = (timestamp) => {
if (!timestamp) return ''
return new Date(timestamp * 1000).toLocaleString('zh-CN')
}
const loadSymbols = async () => {
try {
const data = await marketAPI.getSymbols()
symbols.value = data.symbols || []
} catch (err) {
console.error('加载品种列表失败:', err)
}
}
onMounted(() => {
loadSymbols()
loadPendingTrades()
// 每10秒自动刷新
setInterval(loadPendingTrades, 10000)
})
return {
form,
formValid,
sending,
loadingTrades,
clearing,
error,
success,
tradeForm,
symbols,
directions,
tradeHeaders,
pendingTrades,
loadPendingTrades,
sendTrade,
clearAllTrades,
formatTime,
}
},
}
</script>