feat: 添加前端界面和市场分析模块
- 新增 Vue 3 + Vuetify 前端界面 - 新增市场分析模块 (market/) - 更新主服务器和路由 - 更新 MT5 EA 文件 - 添加 .gitignore 排除临时文件
This commit is contained in:
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user