feat: 添加新闻监控、持仓管理、系统日志等功能
- 新增新闻爬取和监控模块 (news_crawler, news_monitor) - 新增 LLM 分析模块 (llm_analyzer) - 新增持仓管理和交易历史存储 - 新增系统日志功能 - 新增前端页面: News, Positions, Settings, SystemLog - 更新路由和 API 接口 - 更新 .gitignore 排除敏感文件
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<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-toolbar-title>AITrader</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon>
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
@@ -44,8 +44,12 @@ export default {
|
||||
{ title: '仪表板', path: '/', icon: 'mdi-view-dashboard' },
|
||||
{ title: '交易指令', path: '/trades', icon: 'mdi-format-list-bulleted' },
|
||||
{ title: '行情分析', path: '/market', icon: 'mdi-chart-candlestick' },
|
||||
{ title: '仓位管理', path: '/positions', icon: 'mdi-chart-box' },
|
||||
{ title: '财经日历', path: '/news', icon: 'mdi-newspaper-variant-outline' },
|
||||
{ title: '统计数据', path: '/statistics', icon: 'mdi-chart-line' },
|
||||
{ title: '服务状态', path: '/status', icon: 'mdi-information' },
|
||||
{ title: '系统设置', path: '/settings', icon: 'mdi-cog' },
|
||||
{ title: '运行日志', path: '/logs', icon: 'mdi-text-box-outline' },
|
||||
]
|
||||
|
||||
return {
|
||||
|
||||
@@ -143,6 +143,92 @@ export const marketAPI = {
|
||||
async closePosition(ticket, symbol) {
|
||||
const response = await api.post('/close_position', { ticket, symbol })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ==================== 大模型分析 ====================
|
||||
|
||||
// 获取大模型分析结果
|
||||
async getLLMAnalysis(symbol = null) {
|
||||
const params = symbol ? { symbol } : {}
|
||||
const response = await api.get('/llm/analysis', { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取大模型分析器状态
|
||||
async getLLMStatus() {
|
||||
const response = await api.get('/llm/status')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取大模型配置
|
||||
async getLLMConfig() {
|
||||
const response = await api.get('/llm/config')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 手动触发大模型分析
|
||||
async triggerLLMAnalysis() {
|
||||
const response = await api.post('/llm/trigger')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 配置大模型参数
|
||||
async configureLLM(config) {
|
||||
const response = await api.post('/llm/configure', config)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取已配置品种的K线数据状态
|
||||
async getConfiguredSymbols() {
|
||||
const response = await api.get('/market/configured_symbols')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取系统运行日志
|
||||
async getSystemLogs(count = 50, eventTypes = null, symbol = null) {
|
||||
const params = { count }
|
||||
if (eventTypes && eventTypes.length > 0) {
|
||||
params.event_type = eventTypes.join(',')
|
||||
}
|
||||
if (symbol) params.symbol = symbol
|
||||
const response = await api.get('/system/logs', { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 清空系统日志
|
||||
async clearSystemLogs() {
|
||||
const response = await api.delete('/system/logs')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ==================== 仓位管理 ====================
|
||||
|
||||
// 获取持仓数据
|
||||
async getPositions(symbol = null) {
|
||||
const params = symbol ? { symbol } : {}
|
||||
const response = await api.get('/positions', { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取持仓汇总
|
||||
async getPositionsSummary(symbol = null) {
|
||||
const params = symbol ? { symbol } : {}
|
||||
const response = await api.get('/positions/summary', { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ==================== 交易历史 ====================
|
||||
|
||||
// 获取交易历史
|
||||
async getTradeHistory() {
|
||||
const response = await api.get('/trade_history')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取交易历史统计
|
||||
async getTradeHistoryStatistics() {
|
||||
const response = await api.get('/trade_history/statistics')
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import TradeOrders from '../views/TradeOrders.vue'
|
||||
import Statistics from '../views/Statistics.vue'
|
||||
import Status from '../views/Status.vue'
|
||||
import Market from '../views/Market.vue'
|
||||
import Settings from '../views/Settings.vue'
|
||||
import SystemLog from '../views/SystemLog.vue'
|
||||
import Positions from '../views/Positions.vue'
|
||||
import News from '../views/News.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -30,6 +34,26 @@ const routes = [
|
||||
path: '/market',
|
||||
name: 'Market',
|
||||
component: Market
|
||||
},
|
||||
{
|
||||
path: '/positions',
|
||||
name: 'Positions',
|
||||
component: Positions
|
||||
},
|
||||
{
|
||||
path: '/news',
|
||||
name: 'News',
|
||||
component: News
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: Settings
|
||||
},
|
||||
{
|
||||
path: '/logs',
|
||||
name: 'SystemLog',
|
||||
component: SystemLog
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
+922
-669
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,667 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<!-- 页面标题 -->
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12">
|
||||
<h2 class="text-h4">
|
||||
<v-icon large class="mr-2">mdi-newspaper-variant-outline</v-icon>
|
||||
财经日历与新闻
|
||||
</h2>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 状态卡片 -->
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" md="3">
|
||||
<v-card outlined>
|
||||
<v-card-text class="d-flex align-center">
|
||||
<v-icon large color="primary" class="mr-3">mdi-calendar-check</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-grey">日历天数</div>
|
||||
<div class="text-h5">{{ status.store_status?.calendar_dates || 0 }}</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card outlined>
|
||||
<v-card-text class="d-flex align-center">
|
||||
<v-icon large color="warning" class="mr-3">mdi-alert-circle</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-grey">重要事件</div>
|
||||
<div class="text-h5">{{ status.store_status?.calendar_events || 0 }}</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card outlined>
|
||||
<v-card-text class="d-flex align-center">
|
||||
<v-icon large color="info" class="mr-3">mdi-lightning-bolt</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-grey">快讯数量</div>
|
||||
<div class="text-h5">{{ status.store_status?.flash_news_count || 0 }}</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card outlined>
|
||||
<v-card-text class="d-flex align-center">
|
||||
<v-icon large :color="status.running ? 'success' : 'error'" class="mr-3">
|
||||
{{ status.running ? 'mdi-check-circle' : 'mdi-close-circle' }}
|
||||
</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-grey">监控状态</div>
|
||||
<div class="text-h5">{{ status.running ? '运行中' : '已停止' }}</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<v-card>
|
||||
<v-tabs v-model="activeTab" background-color="primary" dark>
|
||||
<v-tab>
|
||||
<v-icon class="mr-2">mdi-calendar</v-icon>
|
||||
财经日历
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
<v-icon class="mr-2">mdi-lightning-bolt</v-icon>
|
||||
实时快讯
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
<v-icon class="mr-2">mdi-chart-timeline-variant</v-icon>
|
||||
品种影响
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<!-- 财经日历 -->
|
||||
<v-tab-item>
|
||||
<v-card-text>
|
||||
<!-- 筛选栏 -->
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" md="4">
|
||||
<v-select
|
||||
v-model="selectedImportance"
|
||||
:items="importanceOptions"
|
||||
label="重要性筛选"
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
clearable
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-select
|
||||
v-model="selectedCountry"
|
||||
:items="countryOptions"
|
||||
label="国家筛选"
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
clearable
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-btn color="primary" @click="fetchCalendar" :loading="loading">
|
||||
<v-icon class="mr-2">mdi-refresh</v-icon>
|
||||
刷新
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 事件列表 -->
|
||||
<v-data-table
|
||||
:headers="calendarHeaders"
|
||||
:items="filteredCalendar"
|
||||
:loading="loading"
|
||||
item-key="id"
|
||||
class="elevation-1"
|
||||
:items-per-page="20"
|
||||
>
|
||||
<!-- 重要性 -->
|
||||
<template v-slot:item.importance="{ item }">
|
||||
<v-chip
|
||||
:color="getImportanceColor(item.importance)"
|
||||
small
|
||||
dark
|
||||
>
|
||||
{{ getImportanceText(item.importance) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<!-- 发布时间 -->
|
||||
<template v-slot:item.publish_time="{ item }">
|
||||
<div>
|
||||
<div class="font-weight-medium">{{ formatDate(item.publish_time) }}</div>
|
||||
<div class="text-caption text-grey">{{ formatTime(item.publish_time) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 影响品种 -->
|
||||
<template v-slot:item.symbols="{ item }">
|
||||
<v-chip
|
||||
v-for="symbol in item.symbols"
|
||||
:key="symbol"
|
||||
:color="getSymbolColor(symbol)"
|
||||
small
|
||||
class="mr-1"
|
||||
>
|
||||
{{ symbol }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<!-- 数值 -->
|
||||
<template v-slot:item.values="{ item }">
|
||||
<div class="text-caption">
|
||||
<div>预期: <span class="font-weight-medium">{{ item.forecast || '--' }}</span></div>
|
||||
<div>前值: <span class="text-grey">{{ item.previous || '--' }}</span></div>
|
||||
<div v-if="item.actual" class="success--text">
|
||||
实际: {{ item.actual }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 结果 -->
|
||||
<template v-slot:item.result="{ item }">
|
||||
<v-chip
|
||||
v-if="item.result"
|
||||
:color="getResultColor(item.result)"
|
||||
small
|
||||
dark
|
||||
>
|
||||
{{ getResultText(item.result) }}
|
||||
</v-chip>
|
||||
<span v-else class="text-grey">待发布</span>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- 实时快讯 -->
|
||||
<v-tab-item>
|
||||
<v-card-text>
|
||||
<v-btn color="primary" class="mb-4" @click="fetchFlashNews" :loading="loading">
|
||||
<v-icon class="mr-2">mdi-refresh</v-icon>
|
||||
刷新快讯
|
||||
</v-btn>
|
||||
|
||||
<v-timeline v-if="flashNews.length > 0" dense>
|
||||
<v-timeline-item
|
||||
v-for="news in flashNews"
|
||||
:key="news.id"
|
||||
:color="news.importance >= 2 ? 'error' : 'info'"
|
||||
small
|
||||
>
|
||||
<v-card outlined class="mb-2">
|
||||
<v-card-text>
|
||||
<div class="d-flex justify-space-between align-start">
|
||||
<div class="flex-grow-1">
|
||||
<!-- 讲话者标签 -->
|
||||
<v-chip
|
||||
v-if="news.speaker"
|
||||
color="primary"
|
||||
small
|
||||
class="mr-2 mb-2"
|
||||
>
|
||||
<v-icon small class="mr-1">mdi-account</v-icon>
|
||||
{{ news.speaker }}
|
||||
<span v-if="news.speaker_title" class="ml-1">({{ news.speaker_title }})</span>
|
||||
</v-chip>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="text-body-1 mb-2">{{ news.content }}</div>
|
||||
|
||||
<!-- 影响分析 -->
|
||||
<div v-if="news.impact && Object.keys(news.impact).length > 0" class="mt-2">
|
||||
<div class="text-caption text-grey mb-1">影响分析:</div>
|
||||
<v-chip
|
||||
v-for="(impact, symbol) in news.impact"
|
||||
:key="symbol"
|
||||
:color="getImpactColor(impact.direction)"
|
||||
small
|
||||
class="mr-1 mb-1"
|
||||
>
|
||||
{{ symbol }}: {{ impact.direction }}
|
||||
<span v-if="impact.reason" class="ml-1">- {{ impact.reason }}</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey ml-4">
|
||||
{{ formatDateTime(news.time) }}
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
|
||||
<v-alert v-else type="info" text>
|
||||
暂无快讯数据
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- 品种影响 -->
|
||||
<v-tab-item>
|
||||
<v-card-text>
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="selectedSymbol"
|
||||
:items="symbolOptions"
|
||||
label="选择品种"
|
||||
outlined
|
||||
@change="fetchSymbolImpact"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="symbolEvents.length > 0">
|
||||
<v-col
|
||||
v-for="event in symbolEvents"
|
||||
:key="event.id"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-card outlined class="mb-3">
|
||||
<v-card-text>
|
||||
<div class="d-flex justify-space-between align-start mb-2">
|
||||
<div>
|
||||
<v-chip
|
||||
:color="getImportanceColor(event.importance)"
|
||||
small
|
||||
dark
|
||||
class="mr-2"
|
||||
>
|
||||
{{ getImportanceText(event.importance) }}
|
||||
</v-chip>
|
||||
<span class="font-weight-medium">{{ event.name }}</span>
|
||||
</div>
|
||||
<v-chip small>{{ event.country }}</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey mb-2">
|
||||
{{ formatDateTime(event.publish_time) }}
|
||||
</div>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
<div class="text-caption text-grey">预期</div>
|
||||
<div class="font-weight-medium">{{ event.forecast || '--' }}</div>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<div class="text-caption text-grey">前值</div>
|
||||
<div class="font-weight-medium">{{ event.previous || '--' }}</div>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<div class="text-caption text-grey">实际</div>
|
||||
<div class="font-weight-medium success--text">{{ event.actual || '待发布' }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-alert v-else-if="selectedSymbol" type="info" text>
|
||||
该品种暂无即将发布的重要事件
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-tab-item>
|
||||
</v-card>
|
||||
|
||||
<!-- 新闻提醒弹窗 -->
|
||||
<v-snackbar
|
||||
v-model="snackbar.show"
|
||||
:color="snackbar.color"
|
||||
:timeout="5000"
|
||||
top
|
||||
right
|
||||
>
|
||||
<v-icon class="mr-2">{{ snackbar.icon }}</v-icon>
|
||||
{{ snackbar.message }}
|
||||
<template v-slot:action>
|
||||
<v-btn text @click="snackbar.show = false">关闭</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'News',
|
||||
setup() {
|
||||
const activeTab = ref(0)
|
||||
const loading = ref(false)
|
||||
const status = ref({
|
||||
running: false,
|
||||
ws_clients: 0,
|
||||
store_status: {
|
||||
calendar_dates: 0,
|
||||
total_events: 0,
|
||||
flash_news_count: 0
|
||||
},
|
||||
scheduled_events: 0
|
||||
})
|
||||
|
||||
const calendar = ref([])
|
||||
const flashNews = ref([])
|
||||
const symbolEvents = ref([])
|
||||
|
||||
const selectedImportance = ref(null)
|
||||
const selectedCountry = ref(null)
|
||||
const selectedSymbol = ref('GOLD')
|
||||
|
||||
const ws = ref(null)
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
color: 'info',
|
||||
icon: 'mdi-bell',
|
||||
message: ''
|
||||
})
|
||||
|
||||
const importanceOptions = [
|
||||
{ text: '高影响', value: 3 },
|
||||
{ text: '中等影响', value: 2 },
|
||||
{ text: '低影响', value: 1 }
|
||||
]
|
||||
|
||||
const countryOptions = [
|
||||
{ text: '美国 (US)', value: 'US' },
|
||||
{ text: '日本 (JP)', value: 'JP' },
|
||||
{ text: '欧洲 (EU)', value: 'EU' },
|
||||
{ text: '英国 (UK)', value: 'UK' },
|
||||
{ text: '中国 (CN)', value: 'CN' }
|
||||
]
|
||||
|
||||
const symbolOptions = [
|
||||
{ text: '黄金 (GOLD)', value: 'GOLD' },
|
||||
{ text: '原油 (OIL)', value: 'OIL' },
|
||||
{ text: '比特币 (BTC)', value: 'BTC' },
|
||||
{ text: '标普500 (SPX)', value: 'SPX' },
|
||||
{ text: '美日 (USDJPY)', value: 'USDJPY' }
|
||||
]
|
||||
|
||||
const calendarHeaders = [
|
||||
{ text: '重要性', value: 'importance', width: 100 },
|
||||
{ text: '事件', value: 'name', width: 200 },
|
||||
{ text: '国家', value: 'country', width: 80 },
|
||||
{ text: '发布时间', value: 'publish_time', width: 150 },
|
||||
{ text: '数值', value: 'values', width: 120 },
|
||||
{ text: '结果', value: 'result', width: 100 },
|
||||
{ text: '影响品种', value: 'symbols', width: 200 }
|
||||
]
|
||||
|
||||
const filteredCalendar = computed(() => {
|
||||
let items = calendar.value
|
||||
|
||||
if (selectedImportance.value) {
|
||||
items = items.filter(e => e.importance === selectedImportance.value)
|
||||
}
|
||||
|
||||
if (selectedCountry.value) {
|
||||
items = items.filter(e => e.country === selectedCountry.value)
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 获取状态
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/news/status')
|
||||
status.value = response.data.data
|
||||
} catch (error) {
|
||||
console.error('获取状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取财经日历
|
||||
const fetchCalendar = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/news/upcoming', {
|
||||
params: { hours: 168 } // 未来7天
|
||||
})
|
||||
calendar.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('获取日历失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取快讯
|
||||
const fetchFlashNews = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/news/flash', {
|
||||
params: { count: 30 }
|
||||
})
|
||||
flashNews.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('获取快讯失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取品种影响
|
||||
const fetchSymbolImpact = async () => {
|
||||
if (!selectedSymbol.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get(`/api/news/impact/${selectedSymbol.value}`)
|
||||
symbolEvents.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('获取品种影响失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket连接
|
||||
const connectWebSocket = () => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/news/ws`
|
||||
|
||||
ws.value = new WebSocket(wsUrl)
|
||||
|
||||
ws.value.onopen = () => {
|
||||
console.log('新闻WebSocket已连接')
|
||||
}
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
handleWebSocketMessage(data)
|
||||
} catch (error) {
|
||||
console.error('解析WebSocket消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
ws.value.onerror = (error) => {
|
||||
console.error('WebSocket错误:', error)
|
||||
}
|
||||
|
||||
ws.value.onclose = () => {
|
||||
console.log('新闻WebSocket已断开,5秒后重连...')
|
||||
setTimeout(connectWebSocket, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理WebSocket消息
|
||||
const handleWebSocketMessage = (data) => {
|
||||
switch (data.type) {
|
||||
case 'event_reminder':
|
||||
showNotification('warning', 'mdi-clock-alert', data.message)
|
||||
fetchCalendar()
|
||||
break
|
||||
|
||||
case 'event_result':
|
||||
showNotification('success', 'mdi-check-circle', data.message)
|
||||
fetchCalendar()
|
||||
break
|
||||
|
||||
case 'flash_news':
|
||||
showNotification('info', 'mdi-lightning-bolt', data.news?.content?.substring(0, 50) + '...')
|
||||
fetchFlashNews()
|
||||
break
|
||||
|
||||
case 'calendar_update':
|
||||
fetchCalendar()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
const showNotification = (color, icon, message) => {
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
color,
|
||||
icon,
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化函数
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '--'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
const formatTime = (dateStr) => {
|
||||
if (!dateStr) return '--'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
const formatDateTime = (dateStr) => {
|
||||
if (!dateStr) return '--'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const getImportanceColor = (importance) => {
|
||||
switch (importance) {
|
||||
case 3: return 'error'
|
||||
case 2: return 'warning'
|
||||
case 1: return 'info'
|
||||
default: return 'grey'
|
||||
}
|
||||
}
|
||||
|
||||
const getImportanceText = (importance) => {
|
||||
switch (importance) {
|
||||
case 3: return '高'
|
||||
case 2: return '中'
|
||||
case 1: return '低'
|
||||
default: return '--'
|
||||
}
|
||||
}
|
||||
|
||||
const getSymbolColor = (symbol) => {
|
||||
const colors = {
|
||||
'GOLD': 'amber',
|
||||
'OIL': 'black',
|
||||
'BTC': 'orange',
|
||||
'SPX': 'blue',
|
||||
'USDJPY': 'red'
|
||||
}
|
||||
return colors[symbol] || 'grey'
|
||||
}
|
||||
|
||||
const getResultColor = (result) => {
|
||||
switch (result) {
|
||||
case 'better': return 'success'
|
||||
case 'worse': return 'error'
|
||||
case 'in_line': return 'info'
|
||||
default: return 'grey'
|
||||
}
|
||||
}
|
||||
|
||||
const getResultText = (result) => {
|
||||
switch (result) {
|
||||
case 'better': return '好于预期'
|
||||
case 'worse': return '差于预期'
|
||||
case 'in_line': return '符合预期'
|
||||
default: return '--'
|
||||
}
|
||||
}
|
||||
|
||||
const getImpactColor = (direction) => {
|
||||
switch (direction) {
|
||||
case '利好': return 'success'
|
||||
case '利空': return 'error'
|
||||
case '中性': return 'info'
|
||||
default: return 'grey'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchStatus()
|
||||
fetchCalendar()
|
||||
fetchFlashNews()
|
||||
fetchSymbolImpact()
|
||||
connectWebSocket()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws.value) {
|
||||
ws.value.close()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
loading,
|
||||
status,
|
||||
calendar,
|
||||
flashNews,
|
||||
symbolEvents,
|
||||
selectedImportance,
|
||||
selectedCountry,
|
||||
selectedSymbol,
|
||||
importanceOptions,
|
||||
countryOptions,
|
||||
symbolOptions,
|
||||
calendarHeaders,
|
||||
filteredCalendar,
|
||||
snackbar,
|
||||
fetchStatus,
|
||||
fetchCalendar,
|
||||
fetchFlashNews,
|
||||
fetchSymbolImpact,
|
||||
formatDate,
|
||||
formatTime,
|
||||
formatDateTime,
|
||||
getImportanceColor,
|
||||
getImportanceText,
|
||||
getSymbolColor,
|
||||
getResultColor,
|
||||
getResultText,
|
||||
getImpactColor
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-timeline-item {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,497 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<h1 class="mb-4">仓位管理</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<v-card>
|
||||
<v-tabs v-model="activeTab" background-color="primary" dark>
|
||||
<v-tab>
|
||||
<v-icon class="mr-2">mdi-chart-box</v-icon>
|
||||
当前持仓
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
<v-icon class="mr-2">mdi-history</v-icon>
|
||||
历史交易
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<!-- 当前持仓 -->
|
||||
<v-tab-item>
|
||||
<!-- 汇总卡片 -->
|
||||
<v-row class="pa-4">
|
||||
<v-col cols="12" md="3">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4">{{ summary.total_count }}</div>
|
||||
<div class="text-caption grey--text">总持仓数</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4" :class="summary.total_profit >= 0 ? 'success--text' : 'error--text'">
|
||||
{{ summary.total_profit >= 0 ? '+' : '' }}{{ summary.total_profit.toFixed(2) }}
|
||||
</div>
|
||||
<div class="text-caption grey--text">总盈亏</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4 success--text">{{ summary.buy_count }}</div>
|
||||
<div class="text-caption grey--text">买单数量</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h4 error--text">{{ summary.sell_count }}</div>
|
||||
<div class="text-caption grey--text">卖单数量</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 持仓列表 -->
|
||||
<v-card-text>
|
||||
<v-btn color="primary" small class="mb-3" @click="loadPositions" :loading="loading">
|
||||
<v-icon left small>mdi-refresh</v-icon>
|
||||
刷新
|
||||
</v-btn>
|
||||
|
||||
<v-simple-table v-if="positions.length > 0">
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>品种</th>
|
||||
<th>方向</th>
|
||||
<th>手数</th>
|
||||
<th>开仓价</th>
|
||||
<th>当前盈亏</th>
|
||||
<th>止损距离</th>
|
||||
<th>止盈距离</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="pos in positions" :key="pos.ticket">
|
||||
<td>{{ pos.ticket }}</td>
|
||||
<td><strong>{{ pos.symbol }}</strong></td>
|
||||
<td>
|
||||
<v-chip x-small :color="pos.type === 'BUY' ? 'success' : 'error'">
|
||||
{{ pos.type === 'BUY' ? '买入' : '卖出' }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>{{ pos.volume }}</td>
|
||||
<td>{{ pos.price_open }}</td>
|
||||
<td :class="pos.profit >= 0 ? 'success--text' : 'error--text'">
|
||||
{{ pos.profit >= 0 ? '+' : '' }}{{ pos.profit.toFixed(2) }}
|
||||
</td>
|
||||
<td>{{ pos.distance_sl || '-' }}</td>
|
||||
<td>{{ pos.distance_tp || '-' }}</td>
|
||||
<td>{{ formatTime(pos.updated_at) }}</td>
|
||||
<td>
|
||||
<v-btn x-small color="error" outlined @click="closePosition(pos)">
|
||||
平仓
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
<div v-else class="text-center grey--text py-8">
|
||||
<v-icon large>mdi-folder-open-outline</v-icon>
|
||||
<div class="mt-2">暂无持仓</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- 历史交易 -->
|
||||
<v-tab-item>
|
||||
<v-card-text>
|
||||
<v-btn color="primary" small class="mb-3" @click="loadTradeHistory" :loading="historyLoading">
|
||||
<v-icon left small>mdi-refresh</v-icon>
|
||||
刷新
|
||||
</v-btn>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" md="2">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h5">{{ historyStats.total_count || 0 }}</div>
|
||||
<div class="text-caption grey--text">总成交数</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h5" :class="(historyStats.net_profit || 0) >= 0 ? 'success--text' : 'error--text'">
|
||||
{{ (historyStats.net_profit || 0) >= 0 ? '+' : '' }}{{ (historyStats.net_profit || 0).toFixed(2) }}
|
||||
</div>
|
||||
<div class="text-caption grey--text">净盈亏</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="6" md="1">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h6">{{ historyStats.manual_count || 0 }}</div>
|
||||
<div class="text-caption grey--text">手动</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="6" md="1">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h6 primary--text">{{ historyStats.auto_count || 0 }}</div>
|
||||
<div class="text-caption grey--text">自动</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="6" md="1">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h6 warning--text">{{ historyStats.sl_tp_count || 0 }}</div>
|
||||
<div class="text-caption grey--text">止损/止盈</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="6" md="1">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h6 error--text">{{ historyStats.so_count || 0 }}</div>
|
||||
<div class="text-caption grey--text">强制平仓</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="6" md="2">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h6">{{ (historyStats.total_commission || 0).toFixed(2) }}</div>
|
||||
<div class="text-caption grey--text">手续费</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="6" md="2">
|
||||
<v-card outlined>
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h6">{{ (historyStats.total_swap || 0).toFixed(2) }}</div>
|
||||
<div class="text-caption grey--text">库存费</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 品种分布 -->
|
||||
<v-row class="mb-4" v-if="historyStats.symbols && Object.keys(historyStats.symbols).length > 0">
|
||||
<v-col cols="12">
|
||||
<div class="text-subtitle-1 font-weight-bold mb-2">品种分布</div>
|
||||
<v-chip
|
||||
v-for="(data, symbol) in historyStats.symbols"
|
||||
:key="symbol"
|
||||
class="mr-2 mb-2"
|
||||
:color="data.profit >= 0 ? 'success' : 'error'"
|
||||
outlined
|
||||
>
|
||||
{{ symbol }}: {{ data.count }}单, 盈亏 {{ data.profit >= 0 ? '+' : '' }}{{ data.profit.toFixed(2) }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 自动单分类 -->
|
||||
<v-row class="mb-4" v-if="historyStats.auto_categories && Object.keys(historyStats.auto_categories).length > 0">
|
||||
<v-col cols="12">
|
||||
<div class="text-subtitle-1 font-weight-bold mb-2">自动单分类</div>
|
||||
<v-data-table
|
||||
:headers="categoryHeaders"
|
||||
:items="categoryItems"
|
||||
dense
|
||||
hide-default-footer
|
||||
class="elevation-1"
|
||||
>
|
||||
<template v-slot:item.profit="{ item }">
|
||||
<span :class="item.profit >= 0 ? 'success--text' : 'error--text'">
|
||||
{{ item.profit >= 0 ? '+' : '' }}{{ item.profit.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot:item.percentage="{ item }">
|
||||
<v-progress-linear
|
||||
:value="item.percentage"
|
||||
color="primary"
|
||||
height="20"
|
||||
>
|
||||
<template v-slot:default>
|
||||
{{ item.percentage }}%
|
||||
</template>
|
||||
</v-progress-linear>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 成交列表 -->
|
||||
<div class="text-subtitle-1 font-weight-bold mb-2">成交记录</div>
|
||||
<v-simple-table v-if="tradeDeals.length > 0" fixed-header height="400">
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>品种</th>
|
||||
<th>方向</th>
|
||||
<th>类型</th>
|
||||
<th>手数</th>
|
||||
<th>价格</th>
|
||||
<th>盈亏</th>
|
||||
<th>手续费</th>
|
||||
<th>时间</th>
|
||||
<th>备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="deal in tradeDeals" :key="deal.ticket">
|
||||
<td>{{ deal.ticket }}</td>
|
||||
<td><strong>{{ deal.symbol }}</strong></td>
|
||||
<td>
|
||||
<v-chip x-small :color="deal.type === 0 ? 'success' : 'error'">
|
||||
{{ deal.type_text }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>
|
||||
<v-chip x-small outlined :color="deal.entry === 1 ? 'warning' : 'info'">
|
||||
{{ deal.entry_text }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>{{ deal.volume }}</td>
|
||||
<td>{{ deal.price }}</td>
|
||||
<td :class="deal.profit >= 0 ? 'success--text' : 'error--text'">
|
||||
{{ deal.profit >= 0 ? '+' : '' }}{{ deal.profit.toFixed(2) }}
|
||||
</td>
|
||||
<td>{{ deal.commission.toFixed(2) }}</td>
|
||||
<td>{{ deal.time }}</td>
|
||||
<td>
|
||||
<v-chip v-if="deal.order_source === '自动'" x-small color="primary">
|
||||
{{ deal.comment }}
|
||||
</v-chip>
|
||||
<v-chip v-else-if="deal.order_source === '止损触发'" x-small color="error" outlined>
|
||||
{{ deal.comment }}
|
||||
</v-chip>
|
||||
<v-chip v-else-if="deal.order_source === '止盈触发'" x-small color="success" outlined>
|
||||
{{ deal.comment }}
|
||||
</v-chip>
|
||||
<v-chip v-else-if="deal.order_source === '强制平仓'" x-small color="error" dark>
|
||||
{{ deal.comment }}
|
||||
</v-chip>
|
||||
<span v-else class="grey--text">{{ deal.order_source }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
<div v-else class="text-center grey--text py-8">
|
||||
<v-icon large>mdi-history</v-icon>
|
||||
<div class="mt-2">暂无历史交易数据</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-tab-item>
|
||||
</v-card>
|
||||
|
||||
<!-- 平仓确认对话框 -->
|
||||
<v-dialog v-model="closeDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title>确认平仓</v-card-title>
|
||||
<v-card-text>
|
||||
<div v-if="selectedPosition">
|
||||
<div>订单号: {{ selectedPosition.ticket }}</div>
|
||||
<div>品种: {{ selectedPosition.symbol }}</div>
|
||||
<div>手数: {{ selectedPosition.volume }}</div>
|
||||
<div>盈亏: <span :class="selectedPosition.profit >= 0 ? 'success--text' : 'error--text'">{{ selectedPosition.profit }}</span></div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="closeDialog = false">取消</v-btn>
|
||||
<v-btn color="error" @click="confirmClosePosition" :loading="closing">确认平仓</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 提示 -->
|
||||
<v-snackbar v-model="showSnackbar" :color="snackbarColor" timeout="3000">
|
||||
{{ snackbarMessage }}
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { marketAPI } from '@/api/market'
|
||||
|
||||
export default {
|
||||
name: 'Positions',
|
||||
setup() {
|
||||
const activeTab = ref(0)
|
||||
const positions = ref([])
|
||||
const summary = ref({
|
||||
total_count: 0,
|
||||
total_profit: 0,
|
||||
buy_count: 0,
|
||||
sell_count: 0
|
||||
})
|
||||
const loading = ref(false)
|
||||
const closeDialog = ref(false)
|
||||
const selectedPosition = ref(null)
|
||||
const closing = ref(false)
|
||||
const showSnackbar = ref(false)
|
||||
const snackbarMessage = ref('')
|
||||
const snackbarColor = ref('success')
|
||||
|
||||
// 交易历史
|
||||
const tradeDeals = ref([])
|
||||
const historyStats = ref({})
|
||||
const historyLoading = ref(false)
|
||||
|
||||
let refreshInterval = null
|
||||
|
||||
const categoryHeaders = [
|
||||
{ text: '分类', value: 'category', width: 150 },
|
||||
{ text: '数量', value: 'count', width: 80 },
|
||||
{ text: '占比', value: 'percentage', width: 150 },
|
||||
{ text: '盈亏', value: 'profit', width: 100 }
|
||||
]
|
||||
|
||||
const categoryItems = computed(() => {
|
||||
if (!historyStats.value.auto_categories) return []
|
||||
return Object.entries(historyStats.value.auto_categories).map(([category, data]) => ({
|
||||
category,
|
||||
count: data.count,
|
||||
percentage: data.percentage,
|
||||
profit: data.profit
|
||||
}))
|
||||
})
|
||||
|
||||
const loadPositions = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await marketAPI.getPositionsSummary()
|
||||
if (data.status === 'ok') {
|
||||
positions.value = data.positions || []
|
||||
summary.value = {
|
||||
total_count: data.total_count || 0,
|
||||
total_profit: data.total_profit || 0,
|
||||
buy_count: data.buy_count || 0,
|
||||
sell_count: data.sell_count || 0
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载持仓失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadTradeHistory = async () => {
|
||||
historyLoading.value = true
|
||||
try {
|
||||
const data = await marketAPI.getTradeHistory()
|
||||
if (data.status === 'ok') {
|
||||
tradeDeals.value = data.deals || []
|
||||
historyStats.value = data.statistics || {}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载交易历史失败:', err)
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closePosition = (pos) => {
|
||||
selectedPosition.value = pos
|
||||
closeDialog.value = true
|
||||
}
|
||||
|
||||
const confirmClosePosition = async () => {
|
||||
if (!selectedPosition.value) return
|
||||
|
||||
closing.value = true
|
||||
try {
|
||||
const data = await marketAPI.closePosition(selectedPosition.value.ticket, selectedPosition.value.symbol)
|
||||
if (data.status === 'ok') {
|
||||
snackbarMessage.value = '平仓指令已发送'
|
||||
snackbarColor.value = 'success'
|
||||
showSnackbar.value = true
|
||||
closeDialog.value = false
|
||||
// 刷新持仓
|
||||
setTimeout(loadPositions, 1000)
|
||||
} else {
|
||||
snackbarMessage.value = data.message || '平仓失败'
|
||||
snackbarColor.value = 'error'
|
||||
showSnackbar.value = true
|
||||
}
|
||||
} catch (err) {
|
||||
snackbarMessage.value = '平仓失败: ' + err.message
|
||||
snackbarColor.value = 'error'
|
||||
showSnackbar.value = true
|
||||
} finally {
|
||||
closing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return '-'
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPositions()
|
||||
loadTradeHistory()
|
||||
// 每5秒刷新一次持仓
|
||||
refreshInterval = setInterval(loadPositions, 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
positions,
|
||||
summary,
|
||||
loading,
|
||||
closeDialog,
|
||||
selectedPosition,
|
||||
closing,
|
||||
showSnackbar,
|
||||
snackbarMessage,
|
||||
snackbarColor,
|
||||
tradeDeals,
|
||||
historyStats,
|
||||
historyLoading,
|
||||
categoryHeaders,
|
||||
categoryItems,
|
||||
loadPositions,
|
||||
loadTradeHistory,
|
||||
closePosition,
|
||||
confirmClosePosition,
|
||||
formatTime
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,574 @@
|
||||
<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">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-icon class="mr-2">mdi-cog</v-icon>
|
||||
自动交易配置
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="12">
|
||||
<v-switch
|
||||
v-model="tradeConfig.enabled"
|
||||
label="启用自动生成"
|
||||
@change="saveTradeConfig"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 品种配置表格 -->
|
||||
<div class="text-subtitle-2 mt-2 mb-2">品种配置</div>
|
||||
<v-simple-table dense>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>品种</th>
|
||||
<th>手数</th>
|
||||
<th>止损偏移(点)</th>
|
||||
<th>关键点位</th>
|
||||
<th>阈值</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(config, symbol) in tradeConfig.symbol_config" :key="symbol">
|
||||
<td>
|
||||
<strong>{{ symbol }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="config.volume"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
dense
|
||||
hide-details
|
||||
style="width: 80px"
|
||||
></v-text-field>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="config.sl_offset"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
dense
|
||||
hide-details
|
||||
style="width: 80px"
|
||||
></v-text-field>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model="config.key_levels"
|
||||
type="text"
|
||||
dense
|
||||
hide-details
|
||||
placeholder="如: 5000,5100"
|
||||
style="width: 120px"
|
||||
></v-text-field>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="config.key_level_threshold"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
min="0"
|
||||
dense
|
||||
hide-details
|
||||
style="width: 80px"
|
||||
></v-text-field>
|
||||
</td>
|
||||
<td>
|
||||
<v-btn x-small color="primary" @click="saveTradeConfig">保存</v-btn>
|
||||
<v-btn x-small color="error" outlined class="ml-1" @click="removeSymbolConfig(symbol)">删除</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
|
||||
<!-- 添加新品种配置 -->
|
||||
<v-row class="mt-3" align="center">
|
||||
<v-col cols="2">
|
||||
<v-select
|
||||
v-model="newSymbol"
|
||||
:items="availableSymbols"
|
||||
label="选择品种"
|
||||
dense
|
||||
hide-details
|
||||
@change="onSymbolSelect"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-text-field
|
||||
v-model.number="newVolume"
|
||||
label="手数"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-text-field
|
||||
v-model.number="newSlOffset"
|
||||
label="止损偏移"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-text-field
|
||||
v-model="newKeyLevels"
|
||||
label="关键点位"
|
||||
type="text"
|
||||
dense
|
||||
hide-details
|
||||
placeholder="如: 5000,5100"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-text-field
|
||||
v-model.number="newKeyLevelThreshold"
|
||||
label="阈值"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
min="0"
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-btn color="primary" small @click="addSymbolConfig">
|
||||
<v-icon left small>mdi-plus</v-icon>
|
||||
添加
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="text-caption grey--text mt-3">
|
||||
<v-icon small>mdi-information</v-icon>
|
||||
支撑压力策略: M1周期接近转折点时自动生成交易指令,止损偏移为固定点数。<br/>
|
||||
关键点位策略: 价格接近关键点位时生成反向订单。例如下降趋势接近5000时生成买单。阈值表示触发距离(默认0.0008)。
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 大模型配置 -->
|
||||
<v-row class="mt-4">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-icon class="mr-2">mdi-brain</v-icon>
|
||||
大模型配置
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="llmForm">
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="llmConfig.api_key"
|
||||
label="API Key"
|
||||
:type="showApiKey ? 'text' : 'password'"
|
||||
:append-icon="showApiKey ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
@click:append="showApiKey = !showApiKey"
|
||||
dense
|
||||
hide-details
|
||||
:placeholder="llmConfig.api_key_set ? '已设置(输入可更新)' : '请输入 API Key'"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="llmConfig.api_base"
|
||||
label="API Base URL"
|
||||
dense
|
||||
hide-details
|
||||
placeholder="https://api.openai.com/v1"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="llmConfig.model"
|
||||
label="模型名称"
|
||||
dense
|
||||
hide-details
|
||||
placeholder="gpt-4o-mini"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-2">
|
||||
<v-col cols="12">
|
||||
<v-btn color="primary" @click="saveLLMConfig" :loading="llmSaving">
|
||||
<v-icon left>mdi-content-save</v-icon>
|
||||
保存配置
|
||||
</v-btn>
|
||||
<v-chip
|
||||
class="ml-3"
|
||||
:color="llmConfig.enabled ? 'success' : 'error'"
|
||||
small
|
||||
>
|
||||
{{ llmConfig.enabled ? '已启用' : '未启用' }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
|
||||
<div class="text-caption grey--text mt-3">
|
||||
<v-icon small>mdi-information</v-icon>
|
||||
配置大模型用于生成AI趋势分析和交易建议。支持OpenAI兼容的API接口。
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 品种数据状态 -->
|
||||
<v-row class="mt-4">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-icon class="mr-2">mdi-chart-line</v-icon>
|
||||
品种数据状态
|
||||
<v-btn icon small class="ml-2" @click="loadSymbolStatus" :loading="symbolStatusLoading">
|
||||
<v-icon small>mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-simple-table dense v-if="symbolStatus.length > 0">
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>品种</th>
|
||||
<th>数据状态</th>
|
||||
<th>M1数量</th>
|
||||
<th>最新M1时间</th>
|
||||
<th>距上次更新</th>
|
||||
<th>市场状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in symbolStatus" :key="item.symbol">
|
||||
<td><strong>{{ item.symbol }}</strong></td>
|
||||
<td>
|
||||
<v-chip x-small :color="item.has_data ? 'success' : 'error'">
|
||||
{{ item.has_data ? '有数据' : '无数据' }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>{{ item.m1_count || 0 }}</td>
|
||||
<td>{{ item.latest_m1_time || '-' }}</td>
|
||||
<td>
|
||||
<span v-if="item.seconds_ago !== null">{{ item.seconds_ago }}秒前</span>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td>
|
||||
<v-chip x-small :color="getMarketStatusColor(item.market_status)">
|
||||
{{ getMarketStatusText(item.market_status) }}
|
||||
</v-chip>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
<div v-else class="text-center grey--text py-4">
|
||||
<v-icon large>mdi-database-off</v-icon>
|
||||
<div class="mt-2">暂无已配置的品种</div>
|
||||
</div>
|
||||
|
||||
<div class="text-caption grey--text mt-3">
|
||||
<v-icon small>mdi-information</v-icon>
|
||||
显示交易配置中的品种K线数据状态。M1数据超过3分钟未更新视为休市。
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<v-snackbar v-model="showError" color="error" timeout="5000">
|
||||
{{ errorMessage }}
|
||||
</v-snackbar>
|
||||
|
||||
<!-- 成功提示 -->
|
||||
<v-snackbar v-model="showSuccess" color="success" timeout="3000">
|
||||
{{ successMessage }}
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { marketAPI } from '@/api/market'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
setup() {
|
||||
// 交易配置
|
||||
const tradeConfig = ref({
|
||||
enabled: true,
|
||||
default_volume: 0.01,
|
||||
default_sl_offset: 0.05,
|
||||
symbol_config: {}
|
||||
})
|
||||
|
||||
// 添加新品种
|
||||
const newSymbol = ref('')
|
||||
const newVolume = ref(0.01)
|
||||
const newSlOffset = ref(0.05)
|
||||
const newKeyLevels = ref('')
|
||||
const newKeyLevelThreshold = ref(0.0008)
|
||||
const symbols = ref([])
|
||||
|
||||
// 提示
|
||||
const showError = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const showSuccess = ref(false)
|
||||
const successMessage = ref('')
|
||||
|
||||
// 大模型配置
|
||||
const llmConfig = ref({
|
||||
api_key: '',
|
||||
api_key_set: false,
|
||||
api_base: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4o-mini',
|
||||
enabled: false
|
||||
})
|
||||
const showApiKey = ref(false)
|
||||
const llmSaving = ref(false)
|
||||
|
||||
// 品种数据状态
|
||||
const symbolStatus = ref([])
|
||||
const symbolStatusLoading = ref(false)
|
||||
|
||||
// 可用品种列表(已连接但未配置的)
|
||||
const availableSymbols = computed(() => {
|
||||
const configured = Object.keys(tradeConfig.value.symbol_config || {})
|
||||
return symbols.value.filter(s => !configured.includes(s))
|
||||
})
|
||||
|
||||
// 加载配置
|
||||
const loadTradeConfig = async () => {
|
||||
try {
|
||||
const data = await marketAPI.getTradeConfig()
|
||||
if (data.config) {
|
||||
tradeConfig.value = {
|
||||
enabled: data.config.enabled,
|
||||
default_volume: data.config.default_volume,
|
||||
default_sl_offset: data.config.default_sl_offset,
|
||||
symbol_config: data.config.symbol_config || {}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载交易配置失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载品种列表
|
||||
const loadSymbols = async () => {
|
||||
try {
|
||||
const data = await marketAPI.getSymbols()
|
||||
symbols.value = data.symbols || []
|
||||
} catch (err) {
|
||||
console.error('加载品种列表失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const saveTradeConfig = async () => {
|
||||
try {
|
||||
const data = await marketAPI.updateTradeConfig({
|
||||
enabled: tradeConfig.value.enabled,
|
||||
default_volume: tradeConfig.value.default_volume,
|
||||
default_sl_offset: tradeConfig.value.default_sl_offset,
|
||||
symbol_config: tradeConfig.value.symbol_config
|
||||
})
|
||||
if (data.status !== 'ok') {
|
||||
errorMessage.value = data.message || '保存配置失败'
|
||||
showError.value = true
|
||||
} else {
|
||||
successMessage.value = '配置已保存'
|
||||
showSuccess.value = true
|
||||
}
|
||||
} catch (err) {
|
||||
errorMessage.value = `保存配置失败: ${err.message}`
|
||||
showError.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 添加品种配置
|
||||
const addSymbolConfig = () => {
|
||||
if (!newSymbol.value) return
|
||||
const symbol = newSymbol.value
|
||||
tradeConfig.value.symbol_config[symbol] = {
|
||||
volume: newVolume.value || 0.01,
|
||||
sl_offset: newSlOffset.value || 0.05,
|
||||
key_levels: newKeyLevels.value || '',
|
||||
key_level_threshold: newKeyLevelThreshold.value || 0.0008
|
||||
}
|
||||
saveTradeConfig()
|
||||
// 清空输入
|
||||
newSymbol.value = ''
|
||||
newVolume.value = 0.01
|
||||
newSlOffset.value = 0.05
|
||||
newKeyLevels.value = ''
|
||||
newKeyLevelThreshold.value = 0.0008
|
||||
}
|
||||
|
||||
// 删除品种配置
|
||||
const removeSymbolConfig = (symbol) => {
|
||||
delete tradeConfig.value.symbol_config[symbol]
|
||||
saveTradeConfig()
|
||||
}
|
||||
|
||||
// 选择品种时自动填充默认值
|
||||
const onSymbolSelect = (symbol) => {
|
||||
if (symbol && tradeConfig.value.symbol_config && tradeConfig.value.symbol_config[symbol]) {
|
||||
const config = tradeConfig.value.symbol_config[symbol]
|
||||
newVolume.value = config.volume || 0.01
|
||||
newSlOffset.value = config.sl_offset || 0.05
|
||||
newKeyLevels.value = config.key_levels || ''
|
||||
newKeyLevelThreshold.value = config.key_level_threshold || 0.0008
|
||||
} else {
|
||||
newVolume.value = tradeConfig.value.default_volume || 0.01
|
||||
newSlOffset.value = tradeConfig.value.default_sl_offset || 0.05
|
||||
newKeyLevels.value = ''
|
||||
newKeyLevelThreshold.value = 0.0008
|
||||
}
|
||||
}
|
||||
|
||||
// 加载大模型配置
|
||||
const loadLLMConfig = async () => {
|
||||
try {
|
||||
const data = await marketAPI.getLLMConfig()
|
||||
if (data.config) {
|
||||
llmConfig.value = {
|
||||
api_key: '', // 不显示已有key,只显示是否设置
|
||||
api_key_set: data.config.api_key_set || false,
|
||||
api_base: data.config.api_base || 'https://api.openai.com/v1',
|
||||
model: data.config.model || 'gpt-4o-mini',
|
||||
enabled: data.config.enabled || false
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载大模型配置失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存大模型配置
|
||||
const saveLLMConfig = async () => {
|
||||
llmSaving.value = true
|
||||
try {
|
||||
const updateData = {
|
||||
api_base: llmConfig.value.api_base,
|
||||
model: llmConfig.value.model
|
||||
}
|
||||
// 只有输入了新的API Key才更新
|
||||
if (llmConfig.value.api_key) {
|
||||
updateData.api_key = llmConfig.value.api_key
|
||||
}
|
||||
|
||||
const data = await marketAPI.configureLLM(updateData)
|
||||
if (data.status === 'ok') {
|
||||
successMessage.value = '大模型配置已保存'
|
||||
showSuccess.value = true
|
||||
// 重新加载配置
|
||||
await loadLLMConfig()
|
||||
} else {
|
||||
errorMessage.value = data.message || '保存配置失败'
|
||||
showError.value = true
|
||||
}
|
||||
} catch (err) {
|
||||
errorMessage.value = `保存配置失败: ${err.message}`
|
||||
showError.value = true
|
||||
} finally {
|
||||
llmSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载品种数据状态
|
||||
const loadSymbolStatus = async () => {
|
||||
symbolStatusLoading.value = true
|
||||
try {
|
||||
const data = await marketAPI.getConfiguredSymbols()
|
||||
if (data.status === 'ok') {
|
||||
symbolStatus.value = data.symbols || []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载品种状态失败:', err)
|
||||
} finally {
|
||||
symbolStatusLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取市场状态颜色
|
||||
const getMarketStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'active': return 'success'
|
||||
case 'stale': return 'warning'
|
||||
case 'closed': return 'error'
|
||||
default: return 'grey'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取市场状态文本
|
||||
const getMarketStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'active': return '活跃'
|
||||
case 'stale': return '数据过期'
|
||||
case 'closed': return '休市中'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSymbols()
|
||||
loadTradeConfig()
|
||||
loadLLMConfig()
|
||||
loadSymbolStatus()
|
||||
})
|
||||
|
||||
return {
|
||||
tradeConfig,
|
||||
newSymbol,
|
||||
newVolume,
|
||||
newSlOffset,
|
||||
newKeyLevels,
|
||||
newKeyLevelThreshold,
|
||||
availableSymbols,
|
||||
showError,
|
||||
errorMessage,
|
||||
showSuccess,
|
||||
successMessage,
|
||||
saveTradeConfig,
|
||||
addSymbolConfig,
|
||||
removeSymbolConfig,
|
||||
onSymbolSelect,
|
||||
// 大模型配置
|
||||
llmConfig,
|
||||
showApiKey,
|
||||
llmSaving,
|
||||
saveLLMConfig,
|
||||
// 品种数据状态
|
||||
symbolStatus,
|
||||
symbolStatusLoading,
|
||||
loadSymbolStatus,
|
||||
getMarketStatusColor,
|
||||
getMarketStatusText
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,470 @@
|
||||
<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">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-icon class="mr-2">mdi-text-box-outline</v-icon>
|
||||
实时日志
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon small class="mr-2" @click="loadLogs" :loading="loading">
|
||||
<v-icon small>mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
<v-btn color="error" small outlined @click="confirmClear">
|
||||
<v-icon left small>mdi-delete</v-icon>
|
||||
清空
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<!-- 过滤器 -->
|
||||
<v-row class="mb-2">
|
||||
<v-col cols="4">
|
||||
<v-select
|
||||
v-model="filterEventTypes"
|
||||
:items="eventTypes"
|
||||
label="事件类型"
|
||||
dense
|
||||
hide-details
|
||||
clearable
|
||||
multiple
|
||||
chips
|
||||
small-chips
|
||||
deletable-chips
|
||||
@change="loadLogs"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-text-field
|
||||
v-model="filterSymbol"
|
||||
label="品种"
|
||||
dense
|
||||
hide-details
|
||||
clearable
|
||||
@change="loadLogs"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-chip :color="wsConnected ? 'success' : 'error'" small>
|
||||
WebSocket: {{ wsConnected ? '已连接' : '未连接' }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<div class="log-container" ref="logContainer">
|
||||
<div v-if="logs.length === 0" class="text-center grey--text py-8">
|
||||
<v-icon large>mdi-text-box-remove-outline</v-icon>
|
||||
<div class="mt-2">暂无日志</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="(log, index) in logs"
|
||||
:key="index"
|
||||
class="log-entry"
|
||||
:class="'log-' + log.event_type"
|
||||
>
|
||||
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
|
||||
<v-chip
|
||||
x-small
|
||||
:color="getEventColor(log.event_type)"
|
||||
class="mx-2"
|
||||
>
|
||||
{{ log.event_name }}
|
||||
</v-chip>
|
||||
<span v-if="log.symbol" class="log-symbol">[{{ log.symbol }}]</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 清空确认对话框 -->
|
||||
<v-dialog v-model="clearDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title>确认清空</v-card-title>
|
||||
<v-card-text>确定要清空所有日志吗?此操作不可撤销。</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="clearDialog = false">取消</v-btn>
|
||||
<v-btn color="error" @click="clearLogs">确认清空</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { marketAPI } from '@/api/market'
|
||||
|
||||
export default {
|
||||
name: 'SystemLog',
|
||||
setup() {
|
||||
const logs = ref([])
|
||||
const loading = ref(false)
|
||||
const wsConnected = ref(false)
|
||||
const clearDialog = ref(false)
|
||||
const filterEventTypes = ref([])
|
||||
const filterSymbol = ref(null)
|
||||
const logContainer = ref(null)
|
||||
let ws = null
|
||||
|
||||
const eventTypes = [
|
||||
// 大模型相关
|
||||
{ text: '大模型分析开始', value: 'llm_analysis_start' },
|
||||
{ text: '大模型分析完成', value: 'llm_analysis_complete' },
|
||||
{ text: '大模型分析错误', value: 'llm_analysis_error' },
|
||||
// EA数据推送
|
||||
{ text: 'EA推送统计数据', value: 'ea_statistics' },
|
||||
{ text: 'EA推送全量K线', value: 'ea_kline_full' },
|
||||
{ text: 'EA推送增量K线', value: 'ea_kline_incremental' },
|
||||
{ text: 'K线数据过期', value: 'ea_kline_stale' },
|
||||
{ text: 'EA请求交易指令', value: 'ea_trade_request' },
|
||||
// MT5财经日历推送
|
||||
{ text: 'MT5财经日历上报', value: 'mt5_calendar_update' },
|
||||
{ text: 'MT5事件结果上报', value: 'mt5_event_result' },
|
||||
// 转折点相关
|
||||
{ text: '转折点检测完成', value: 'pivot_detected' },
|
||||
{ text: '转折点提醒', value: 'pivot_alert' },
|
||||
// 交易指令
|
||||
{ text: '交易指令生成', value: 'order_generated' },
|
||||
{ text: '交易指令确认', value: 'order_confirmed' },
|
||||
{ text: '交易指令拒绝', value: 'order_rejected' },
|
||||
{ text: '平仓指令', value: 'close_position' },
|
||||
// 持仓相关
|
||||
{ text: '持仓数据更新', value: 'position_update' },
|
||||
// 新闻爬虫相关
|
||||
{ text: '新闻爬虫启动', value: 'news_crawler_start' },
|
||||
{ text: '财经日历获取', value: 'news_calendar_fetch' },
|
||||
{ text: '财经日历更新', value: 'news_calendar_update' },
|
||||
{ text: '财经日历获取失败', value: 'news_calendar_fetch_error' },
|
||||
{ text: '快讯获取', value: 'news_flash_fetch' },
|
||||
{ text: '快讯获取失败', value: 'news_flash_fetch_error' },
|
||||
{ text: '事件调度创建', value: 'news_event_scheduled' },
|
||||
{ text: '事件发布前提醒', value: 'news_event_reminder' },
|
||||
{ text: '事件结果获取', value: 'news_event_result' },
|
||||
{ text: '影响分析完成', value: 'news_impact_analysis' },
|
||||
{ text: '新闻WebSocket推送', value: 'news_ws_broadcast' },
|
||||
// 系统事件
|
||||
{ text: '系统启动', value: 'system_startup' },
|
||||
{ text: '系统关闭', value: 'system_shutdown' },
|
||||
{ text: 'WebSocket连接', value: 'websocket_connect' },
|
||||
{ text: 'WebSocket断开', value: 'websocket_disconnect' },
|
||||
]
|
||||
|
||||
const loadLogs = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await marketAPI.getSystemLogs(100, filterEventTypes.value, filterSymbol.value)
|
||||
if (data.status === 'ok') {
|
||||
logs.value = data.logs
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载日志失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const connectWebSocket = () => {
|
||||
ws = new WebSocket('ws://localhost:8000/ws/market')
|
||||
|
||||
ws.onopen = () => {
|
||||
wsConnected.value = true
|
||||
console.log('[SystemLog] WebSocket已连接')
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'system_log') {
|
||||
// 新日志推送到列表顶部
|
||||
logs.value.unshift(data.data)
|
||||
// 保持最多200条
|
||||
if (logs.value.length > 200) {
|
||||
logs.value = logs.value.slice(0, 200)
|
||||
}
|
||||
// 滚动到顶部
|
||||
nextTick(() => {
|
||||
if (logContainer.value) {
|
||||
logContainer.value.scrollTop = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略非JSON消息
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[SystemLog] WebSocket错误:', error)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
wsConnected.value = false
|
||||
console.log('[SystemLog] WebSocket已断开')
|
||||
// 5秒后重连
|
||||
setTimeout(connectWebSocket, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
const confirmClear = () => {
|
||||
clearDialog.value = true
|
||||
}
|
||||
|
||||
const clearLogs = async () => {
|
||||
try {
|
||||
await marketAPI.clearSystemLogs()
|
||||
logs.value = []
|
||||
clearDialog.value = false
|
||||
} catch (err) {
|
||||
console.error('清空日志失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return ''
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const getEventColor = (eventType) => {
|
||||
const colors = {
|
||||
// 大模型相关
|
||||
'llm_analysis_start': 'info',
|
||||
'llm_analysis_complete': 'success',
|
||||
'llm_analysis_error': 'error',
|
||||
// EA数据推送
|
||||
'ea_statistics': 'grey',
|
||||
'ea_kline_full': 'primary',
|
||||
'ea_kline_incremental': 'primary',
|
||||
'ea_kline_stale': 'warning',
|
||||
'ea_trade_request': 'success',
|
||||
// MT5财经日历推送
|
||||
'mt5_calendar_update': 'primary',
|
||||
'mt5_event_result': 'success',
|
||||
// 转折点相关
|
||||
'pivot_detected': 'warning',
|
||||
'pivot_alert': 'warning',
|
||||
// 交易指令
|
||||
'order_generated': 'success',
|
||||
'order_confirmed': 'success',
|
||||
'order_rejected': 'error',
|
||||
'close_position': 'error',
|
||||
// 持仓相关
|
||||
'position_update': 'info',
|
||||
// 新闻爬虫相关
|
||||
'news_crawler_start': 'success',
|
||||
'news_calendar_fetch': 'info',
|
||||
'news_calendar_update': 'success',
|
||||
'news_calendar_fetch_error': 'error',
|
||||
'news_flash_fetch': 'info',
|
||||
'news_flash_fetch_error': 'error',
|
||||
'news_event_scheduled': 'warning',
|
||||
'news_event_reminder': 'warning',
|
||||
'news_event_result': 'success',
|
||||
'news_impact_analysis': 'primary',
|
||||
'news_ws_broadcast': 'info',
|
||||
// 系统事件
|
||||
'system_startup': 'success',
|
||||
'system_shutdown': 'error',
|
||||
'websocket_connect': 'success',
|
||||
'websocket_disconnect': 'warning',
|
||||
}
|
||||
return colors[eventType] || 'grey'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLogs()
|
||||
connectWebSocket()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws) {
|
||||
ws.close()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
logs,
|
||||
loading,
|
||||
wsConnected,
|
||||
clearDialog,
|
||||
filterEventTypes,
|
||||
filterSymbol,
|
||||
logContainer,
|
||||
eventTypes,
|
||||
loadLogs,
|
||||
confirmClear,
|
||||
clearLogs,
|
||||
formatTime,
|
||||
getEventColor
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
background-color: #1e1e1e;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #888;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.log-symbol {
|
||||
color: #64b5f6;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.log-llm_analysis_start {
|
||||
border-left: 3px solid #2196f3;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-llm_analysis_complete {
|
||||
border-left: 3px solid #4caf50;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-llm_analysis_error {
|
||||
border-left: 3px solid #f44336;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-ea_kline_full {
|
||||
border-left: 3px solid #9c27b0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-ea_kline_incremental {
|
||||
border-left: 3px solid #673ab7;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-ea_kline_stale {
|
||||
border-left: 3px solid #ff9800;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-ea_statistics {
|
||||
border-left: 3px solid #607d8b;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-pivot_alert {
|
||||
border-left: 3px solid #ff9800;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-order_generated {
|
||||
border-left: 3px solid #4caf50;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-order_confirmed {
|
||||
border-left: 3px solid #2e7d32;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-order_rejected {
|
||||
border-left: 3px solid #f44336;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-close_position {
|
||||
border-left: 3px solid #e91e63;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-position_update {
|
||||
border-left: 3px solid #00bcd4;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
/* 新闻爬虫相关样式 */
|
||||
.log-news_crawler_start {
|
||||
border-left: 3px solid #4caf50;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-news_calendar_fetch {
|
||||
border-left: 3px solid #2196f3;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-news_calendar_update {
|
||||
border-left: 3px solid #4caf50;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-news_calendar_fetch_error {
|
||||
border-left: 3px solid #f44336;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-news_flash_fetch {
|
||||
border-left: 3px solid #00bcd4;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-news_flash_fetch_error {
|
||||
border-left: 3px solid #ff5722;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-news_event_scheduled {
|
||||
border-left: 3px solid #ff9800;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-news_event_reminder {
|
||||
border-left: 3px solid #ffc107;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-news_event_result {
|
||||
border-left: 3px solid #8bc34a;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.log-news_impact_analysis {
|
||||
border-left: 3px solid #9c27b0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user