diff --git a/frontend/src/views/Market.vue b/frontend/src/views/Market.vue index 575305b..83aa126 100644 --- a/frontend/src/views/Market.vue +++ b/frontend/src/views/Market.vue @@ -41,6 +41,55 @@ + + + + +
+ mdi-calendar-alert + + 重要性 {{ topCalendarEvent.importance }} + + + {{ topCalendarEvent.currency }} + + {{ topCalendarEvent.name }} + + + {{ formatEventTime(topCalendarEvent.publish_time) }} + +
+ +
+ + 预测: {{ topCalendarEvent.forecast }} + + + 前值: {{ topCalendarEvent.previous }} + + + 实际: {{ topCalendarEvent.actual }} + +
+ +
+ + {{ getEventResultLabel(topCalendarEvent.result) }} + +
+
+
+
+ @@ -748,6 +797,9 @@ export default { const latestFlashNews = ref(null) const newsWs = ref(null) + // 最高等级财经事件 + const topCalendarEvent = ref(null) + // 计算属性 const highPivots = computed(() => { return allPivots.value @@ -1079,6 +1131,73 @@ export default { return 'info' } + // 获取最高重要性财经事件 + const fetchTopCalendarEvent = async () => { + try { + const response = await fetch('/api/news/upcoming?hours=24') + const data = await response.json() + if (data.status === 'ok' && data.data && data.data.length > 0) { + // 按重要性排序,选择最高重要性的事件 + const sortedEvents = data.data + .filter(e => e.name && e.name.length > 2) // 过滤无效名称 + .sort((a, b) => { + // 先按重要性排序(高到低) + if (b.importance !== a.importance) return b.importance - a.importance + // 同重要性按时间排序(近到远) + return new Date(a.publish_time) - new Date(b.publish_time) + }) + if (sortedEvents.length > 0) { + topCalendarEvent.value = sortedEvents[0] + } + } + } catch (err) { + console.error('获取财经事件失败:', err) + } + } + + // 格式化事件时间 + const formatEventTime = (timeStr) => { + if (!timeStr) return '' + const date = new Date(timeStr) + return date.toLocaleString('zh-CN', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } + + // 获取货币颜色 + const getCurrencyColor = (currency) => { + const colors = { + 'USD': 'green', + 'EUR': 'blue', + 'GBP': 'purple', + 'JPY': 'red', + 'AUD': 'orange', + 'CAD': 'teal', + 'CHF': 'indigo', + 'CNY': 'deep-orange' + } + return colors[currency] || 'grey' + } + + // 获取事件结果颜色 + const getEventResultColor = (result) => { + if (result === 'better') return 'success' + if (result === 'worse') return 'error' + if (result === 'in_line') return 'info' + return 'grey' + } + + // 获取事件结果标签 + const getEventResultLabel = (result) => { + if (result === 'better') return '好于预期' + if (result === 'worse') return '差于预期' + if (result === 'in_line') return '符合预期' + return '未知' + } + const getTrendChipColor = (trend) => { if (!trend) return 'grey' const trendLower = trend.toLowerCase() @@ -1291,6 +1410,8 @@ export default { // 快讯相关 fetchLatestFlashNews() connectNewsWebSocket() + // 财经事件 + fetchTopCalendarEvent() // 定时刷新 const statusInterval = setInterval(() => { @@ -1366,7 +1487,13 @@ export default { // 快讯相关 latestFlashNews, formatNewsTime, - getImpactColor + getImpactColor, + // 财经事件相关 + topCalendarEvent, + formatEventTime, + getCurrencyColor, + getEventResultColor, + getEventResultLabel } } } diff --git a/frontend/src/views/News.vue b/frontend/src/views/News.vue index 9faa81d..02f56f7 100644 --- a/frontend/src/views/News.vue +++ b/frontend/src/views/News.vue @@ -87,6 +87,8 @@ v-model="selectedImportance" :items="importanceOptions" label="重要性筛选" + item-title="text" + item-value="value" outlined dense hide-details @@ -95,9 +97,11 @@ {{ event.name }} - {{ event.country }} + {{ event.currency }}
@@ -356,7 +360,7 @@ export default { const symbolEvents = ref([]) const selectedImportance = ref(null) - const selectedCountry = ref(null) + const selectedCurrency = ref(null) const selectedSymbol = ref('GOLD') const ws = ref(null) @@ -373,13 +377,27 @@ export default { { 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 currencyOptions = computed(() => { + const currencies = new Set() + calendar.value.forEach(e => { + if (e.currency) currencies.add(e.currency) + }) + const currencyNames = { + 'USD': '美元 (USD)', + 'EUR': '欧元 (EUR)', + 'GBP': '英镑 (GBP)', + 'JPY': '日元 (JPY)', + 'AUD': '澳元 (AUD)', + 'CAD': '加元 (CAD)', + 'CHF': '瑞郎 (CHF)', + 'NZD': '纽元 (NZD)' + } + return Array.from(currencies).sort().map(c => ({ + text: currencyNames[c] || c, + value: c + })) + }) const symbolOptions = [ { text: '黄金 (GOLD)', value: 'GOLD' }, @@ -392,7 +410,7 @@ export default { const calendarHeaders = [ { text: '重要性', value: 'importance', width: 100 }, { text: '事件', value: 'name', width: 200 }, - { text: '国家', value: 'country', width: 80 }, + { text: '货币', value: 'currency', width: 80 }, { text: '发布时间', value: 'publish_time', width: 150 }, { text: '数值', value: 'values', width: 120 }, { text: '结果', value: 'result', width: 100 }, @@ -406,8 +424,8 @@ export default { items = items.filter(e => e.importance === selectedImportance.value) } - if (selectedCountry.value) { - items = items.filter(e => e.country === selectedCountry.value) + if (selectedCurrency.value) { + items = items.filter(e => e.currency === selectedCurrency.value) } return items @@ -634,10 +652,10 @@ export default { flashNews, symbolEvents, selectedImportance, - selectedCountry, + selectedCurrency, selectedSymbol, importanceOptions, - countryOptions, + currencyOptions, symbolOptions, calendarHeaders, filteredCalendar, diff --git a/market/news_store.py b/market/news_store.py index 9076bb1..59ea0ce 100644 --- a/market/news_store.py +++ b/market/news_store.py @@ -10,6 +10,66 @@ from datetime import datetime, timedelta from typing import List, Dict, Optional import threading from dataclasses import dataclass, field +import re + + +# MT5中表示无效值的特殊数值 +MT5_INVALID_VALUE = -9223372036854775808.0 + + +def _clean_invalid_value(value: str) -> str: + """清理MT5返回的无效值""" + if not value: + return "" + try: + # 检查是否是无效值 + num = float(value) + if num == MT5_INVALID_VALUE or num < -1e15: + return "" + # 格式化有效数值 + if num == int(num): + return str(int(num)) + return value + except (ValueError, TypeError): + return value + + +def _clean_text(text: str) -> str: + """清理文本中的控制字符和无效Unicode""" + if not text: + return "" + # 移除控制字符 (0x00-0x1F 和 0x7F) + cleaned = re.sub(r'[\x00-\x1f\x7f]', '', text) + # 移除不可打印字符(保留ASCII、中文等常用字符) + # 保留:ASCII (0x20-0x7E), 中文 (0x4E00-0x9FFF), 其他常用Unicode + result = [] + for char in cleaned: + code = ord(char) + if (0x20 <= code <= 0x7E or # ASCII可打印字符 + 0x4E00 <= code <= 0x9FFF or # CJK统一汉字 + 0x3000 <= code <= 0x303F or # CJK标点 + 0xFF00 <= code <= 0xFFEF or # 全角字符 + code > 0x9FFF): # 其他Unicode字符 + result.append(char) + return ''.join(result) + + +def _is_valid_name(name: str) -> bool: + """检查名称是否有效(不是乱码)""" + if not name or len(name) < 2: + return False + # 计算可打印字符的比例 + printable_count = 0 + for char in name: + code = ord(char) + if (0x20 <= code <= 0x7E or # ASCII可打印字符 + 0x4E00 <= code <= 0x9FFF or # CJK统一汉字 + 0x3000 <= code <= 0x303F or # CJK标点 + 0xFF00 <= code <= 0xFFEF): # 全角字符 + printable_count += 1 + # 如果可打印字符比例低于70%,认为是乱码 + ratio = printable_count / len(name) if name else 0 + return ratio >= 0.7 @dataclass @@ -167,18 +227,24 @@ class NewsStore: if publish_time < expiry_threshold: continue + # 清理并检查名称有效性 + cleaned_name = _clean_text(event_data.get('name', '')) + if not _is_valid_name(cleaned_name): + # 名称无效(乱码),跳过此事件 + continue + # 创建事件对象 event = CalendarEvent( id=event_id, - name=event_data.get('name', ''), - name_en=event_data.get('name_en', ''), - country=event_data.get('country', ''), + name=cleaned_name, + name_en=_clean_text(event_data.get('name_en', '')), + country=_clean_text(event_data.get('country', '')), currency=event_data.get('currency', ''), importance=event_data.get('importance', 0), publish_time=publish_time, - forecast=event_data.get('forecast', ''), - previous=event_data.get('previous', ''), - actual=event_data.get('actual', ''), + forecast=_clean_invalid_value(event_data.get('forecast', '')), + previous=_clean_invalid_value(event_data.get('previous', '')), + actual=_clean_invalid_value(event_data.get('actual', '')), unit=event_data.get('unit', ''), symbols=event_data.get('symbols', []), event_type=event_data.get('event_type', '') diff --git a/routes_ea.py b/routes_ea.py index 4006446..0b86737 100644 --- a/routes_ea.py +++ b/routes_ea.py @@ -274,14 +274,14 @@ def create_ea_routes(server: TradingServer) -> APIRouter: print(f"[calendar] 收到请求, 数据长度: {len(raw_text)} 字节") - # 清理所有控制字符 (0x00-0x1F, 除了 \t \n \r) - # 保留 tab(0x09), LF(0x0A), CR(0x0D) + # 清理所有控制字符 (0x00-0x1F 和 0x7F) + # JSON字符串值中不能包含任何原始控制字符 def clean_control_chars(text): # 使用正则表达式一次性清理所有控制字符 - # 除了 tab(0x09), LF(0x0A), CR(0x0D) + # 包括 TAB(0x09), LF(0x0A), CR(0x0D), DEL(0x7F) 在内 + # 因为 JSON 字符串值中这些字符必须转义,不能直接出现 import re - # 匹配所有控制字符 (0x00-0x1F) 除了 \t \n \r - pattern = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f]') + pattern = re.compile(r'[\x00-\x1f\x7f]') cleaned = pattern.sub('', text) removed_count = len(text) - len(cleaned) if removed_count > 0: diff --git a/wangxxGold.mq5 b/wangxxGold.mq5 index affc062..3587336 100644 --- a/wangxxGold.mq5 +++ b/wangxxGold.mq5 @@ -1525,7 +1525,8 @@ void SendCalendarToPython() //+------------------------------------------------------------------+ //| JSON字符串转义 | -//| 转义所有JSON无效的控制字符(0x00-0x1F) | +//| 转义所有JSON无效的控制字符(0x00-0x1F)和非ASCII字符 | +//| 非ASCII字符使用\uXXXX格式转义,确保Unicode正确传递 | //+------------------------------------------------------------------+ string EscapeJsonString(string str) { @@ -1544,15 +1545,22 @@ string EscapeJsonString(string str) case 0x08: result += "\\b"; break; // backspace case 0x0C: result += "\\f"; break; // form feed default: - // 转义所有其他控制字符 (0x00-0x1F) - if(ch < 32) + // 转义所有控制字符 (0x00-0x1F) 和 DEL (0x7F) + if(ch < 32 || ch == 0x7F) { // 使用 \uXXXX 格式转义 result += "\\u" + StringFormat("%04X", ch); } + else if(ch < 128) + { + // ASCII 字符直接添加 + result += CharToString((uchar)ch); + } else { - result += CharToString((uchar)ch); + // 非ASCII字符(如中文)使用 \uXXXX 格式转义 + // 这样可以避免 StringToCharArray 的编码问题 + result += "\\u" + StringFormat("%04X", ch); } } }