feat: 添加前端界面和市场分析模块
- 新增 Vue 3 + Vuetify 前端界面 - 新增市场分析模块 (market/) - 更新主服务器和路由 - 更新 MT5 EA 文件 - 添加 .gitignore 排除临时文件
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# 量化交易系统 - Vue 前端
|
||||
|
||||
基于 Vue 3 + Vuetify 的现代化量化交易前端界面
|
||||
|
||||
## 🚀 技术栈
|
||||
|
||||
- **Vue 3** - 渐进式 JavaScript 框架
|
||||
- **Vuetify 3** - Material Design 组件库
|
||||
- **Vue Router 4** - 官方路由管理器
|
||||
- **Axios** - HTTP 客户端
|
||||
- **ECharts** - 数据可视化图表
|
||||
- **Vite** - 快速构建工具
|
||||
|
||||
## 📦 安装依赖
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
## 🏃♂️ 开发运行
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 或直接使用 npx
|
||||
npx vite --host 0.0.0.0 --port 3000
|
||||
```
|
||||
|
||||
## 🏗️ 生产构建
|
||||
|
||||
```bash
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 预览构建结果
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 🌐 访问地址
|
||||
|
||||
- **开发环境**: http://localhost:3001
|
||||
- **生产环境**: 根据部署配置
|
||||
|
||||
## 📱 功能特性
|
||||
|
||||
### 1. 仪表板 (Dashboard)
|
||||
- 📊 实时服务状态监控
|
||||
- 📈 关键指标展示 (待执行指令、统计记录、活跃品种)
|
||||
- 🔄 自动刷新数据
|
||||
|
||||
### 2. 交易指令管理 (Trade Orders)
|
||||
- ➕ 发送新的交易指令
|
||||
- ✅ **智能价格验证**: 自动检查买入/卖出指令的价格合理性
|
||||
- 📋 查看所有待执行指令
|
||||
- 🗑️ 批量清空指令
|
||||
|
||||
### 3. 统计数据分析 (Statistics)
|
||||
- 📊 历史数据表格展示
|
||||
- 📈 价格趋势图表
|
||||
- 📋 汇总统计指标
|
||||
|
||||
### 4. 服务状态监控 (Status)
|
||||
- ❤️ 健康检查状态
|
||||
- 📊 详细的服务指标
|
||||
- 🔄 实时自动刷新 (每5秒)
|
||||
|
||||
## 🔧 项目结构
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── public/ # 静态资源
|
||||
├── src/
|
||||
│ ├── api/ # API 接口
|
||||
│ │ └── trading.js # 交易 API
|
||||
│ ├── components/ # 组件 (预留)
|
||||
│ ├── plugins/ # 插件配置
|
||||
│ │ └── vuetify.js # Vuetify 配置
|
||||
│ ├── router/ # 路由配置
|
||||
│ │ └── index.js # 路由定义
|
||||
│ ├── views/ # 页面视图
|
||||
│ │ ├── Dashboard.vue # 仪表板
|
||||
│ │ ├── TradeOrders.vue # 交易指令
|
||||
│ │ ├── Statistics.vue # 统计数据
|
||||
│ │ └── Status.vue # 服务状态
|
||||
│ ├── App.vue # 根组件
|
||||
│ ├── main.js # 入口文件
|
||||
│ └── style.css # 全局样式
|
||||
├── package.json # 项目配置
|
||||
├── vite.config.js # Vite 配置
|
||||
└── index.html # HTML 模板
|
||||
```
|
||||
|
||||
## 🔗 API 集成
|
||||
|
||||
前端通过代理自动连接到后端 API:
|
||||
|
||||
- **后端地址**: http://localhost:8000
|
||||
- **代理路径**: `/api/*` → `http://localhost:8000/*`
|
||||
|
||||
## 🎨 UI 设计
|
||||
|
||||
- **Material Design**: 使用 Google Material Design 规范
|
||||
- **响应式布局**: 支持桌面和移动设备
|
||||
- **深色主题**: 支持亮色/暗色主题切换
|
||||
- **中文界面**: 完全本地化的中文界面
|
||||
|
||||
## 🔒 安全特性
|
||||
|
||||
### 价格验证规则
|
||||
- **买入指令**: 必须满足 `sl < price < tp`
|
||||
- **卖出指令**: 必须满足 `tp < price < sl`
|
||||
- **自动拒绝**: 不符合规则的指令会被服务器拒绝
|
||||
|
||||
## 📊 数据可视化
|
||||
|
||||
- **ECharts 图表**: 价格趋势线图
|
||||
- **实时更新**: 图表数据自动刷新
|
||||
- **交互式**: 支持缩放、拖拽等交互
|
||||
|
||||
## 🚀 性能优化
|
||||
|
||||
- **Vite 构建**: 快速的冷启动和热重载
|
||||
- **代码分割**: 自动路由级代码分割
|
||||
- **懒加载**: 组件按需加载
|
||||
- **缓存优化**: 浏览器缓存策略
|
||||
|
||||
## 🛠️ 开发工具
|
||||
|
||||
- **ESLint**: 代码规范检查
|
||||
- **Vue DevTools**: Vue 开发调试工具
|
||||
- **热重载**: 修改代码即时预览
|
||||
|
||||
## 📝 开发指南
|
||||
|
||||
### 添加新页面
|
||||
1. 在 `src/views/` 创建 Vue 组件
|
||||
2. 在 `src/router/index.js` 添加路由配置
|
||||
3. 在 `src/App.vue` 的菜单中添加导航项
|
||||
|
||||
### API 调用
|
||||
```javascript
|
||||
import { tradingAPI } from '@/api/trading'
|
||||
|
||||
// 发送交易指令
|
||||
await tradingAPI.sendTradeInstructions(instructions)
|
||||
|
||||
// 查询统计数据
|
||||
const stats = await tradingAPI.getStatistics()
|
||||
```
|
||||
|
||||
### 组件开发
|
||||
```vue
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>我的组件</v-card-title>
|
||||
<v-card-text>
|
||||
<!-- 组件内容 -->
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MyComponent',
|
||||
setup() {
|
||||
// 组件逻辑
|
||||
return {
|
||||
// 响应式数据
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 🔄 与 React 版本对比
|
||||
|
||||
| 特性 | Vue 版本 | React 版本 |
|
||||
|------|----------|------------|
|
||||
| 框架 | Vue 3 + Composition API | React 18 + Hooks |
|
||||
| UI库 | Vuetify 3 | Material-UI 5 |
|
||||
| 路由 | Vue Router 4 | React Router 6 |
|
||||
| 图表 | ECharts | Recharts |
|
||||
| 构建 | Vite | Create React App |
|
||||
| 学习曲线 | 较平缓 | 较陡峭 |
|
||||
| 性能 | 优秀 | 优秀 |
|
||||
| 生态 | 成熟 | 庞大 |
|
||||
|
||||
## 🎯 优势特点
|
||||
|
||||
1. **Vue 生态**: 你熟悉的 Vue 框架和语法
|
||||
2. **Vuetify**: 功能完整的 Material Design 组件库
|
||||
3. **TypeScript 支持**: 可选的 TypeScript 支持
|
||||
4. **开发体验**: 优秀的开发工具和热重载
|
||||
5. **性能优化**: Vue 3 的优秀性能表现
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如果遇到问题,请检查:
|
||||
1. 浏览器开发者工具的错误信息
|
||||
2. 终端的日志输出
|
||||
3. 确保后端服务正在运行 (http://localhost:8000)
|
||||
|
||||
---
|
||||
|
||||
**🌟 享受 Vue 开发的乐趣!**
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>量化交易系统</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900&display=swap">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1628
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "lianghua-trading-frontend-vue",
|
||||
"version": "1.0.0",
|
||||
"description": "Vue 3 前端界面 for 高性能交易服务",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"axios": "^1.6.0",
|
||||
"vuetify": "^3.5.0",
|
||||
"echarts": "^5.4.0",
|
||||
"vue-echarts": "^6.6.0",
|
||||
"@mdi/font": "^7.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Vue 前端启动脚本
|
||||
|
||||
echo "🚀 启动量化交易系统 - Vue 前端"
|
||||
echo "================================="
|
||||
|
||||
# 检查是否在正确的目录
|
||||
if [ ! -f "package.json" ]; then
|
||||
echo "❌ 错误: 请在 frontend 目录下运行此脚本"
|
||||
echo " cd frontend && ./start_vue.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查依赖是否已安装
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "📦 安装依赖..."
|
||||
npm install
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ 依赖安装失败"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "🌐 启动 Vue 开发服务器..."
|
||||
echo " 前端地址: http://localhost:3001"
|
||||
echo " 后端地址: http://localhost:8000"
|
||||
echo ""
|
||||
echo "按 Ctrl+C 停止服务器"
|
||||
echo ""
|
||||
|
||||
# 启动开发服务器
|
||||
npx vite --host 0.0.0.0 --port 3001
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user