feat: 添加新闻监控、持仓管理、系统日志等功能
- 新增新闻爬取和监控模块 (news_crawler, news_monitor) - 新增 LLM 分析模块 (llm_analyzer) - 新增持仓管理和交易历史存储 - 新增系统日志功能 - 新增前端页面: News, Positions, Settings, SystemLog - 更新路由和 API 接口 - 更新 .gitignore 排除敏感文件
This commit is contained in:
+377
-14
@@ -4,10 +4,16 @@
|
||||
EA 相关的接口路由
|
||||
"""
|
||||
|
||||
import random
|
||||
from fastapi import APIRouter, Query, Request
|
||||
from typing import Optional, List, Dict
|
||||
from models import TradeInstruction
|
||||
from server import TradingServer
|
||||
from market.system_log import get_system_log
|
||||
|
||||
|
||||
# 统计数据日志打印概率 (5%)
|
||||
STATISTICS_LOG_PROBABILITY = 0.05
|
||||
|
||||
|
||||
def create_ea_routes(server: TradingServer) -> APIRouter:
|
||||
@@ -61,9 +67,56 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
|
||||
# 添加平仓指令
|
||||
result["close_tickets"] = server.get_close_position_instructions(symbol)
|
||||
|
||||
# 打印完整返回数据用于调试
|
||||
import json
|
||||
print(f"[EA API] 返回给EA的数据: {json.dumps(result, ensure_ascii=False)}")
|
||||
# 如果结果不为空,记录到运行日志
|
||||
trades = result.get("trades", [])
|
||||
close_tickets = result.get("close_tickets", [])
|
||||
pivot_alerts = result.get("pivot_alerts", [])
|
||||
|
||||
if trades or close_tickets:
|
||||
import json
|
||||
system_log = get_system_log()
|
||||
|
||||
# 打印完整返回数据
|
||||
print(f"[EA API] 返回给EA的数据: {json.dumps(result, ensure_ascii=False)}")
|
||||
|
||||
# 记录交易指令日志
|
||||
if trades:
|
||||
for t in trades:
|
||||
action_text = '买入' if t.get('action') == 'b' else '卖出'
|
||||
system_log.add_log(
|
||||
"order_generated",
|
||||
{
|
||||
"order_id": t.get('order_id'),
|
||||
"action": t.get('action'),
|
||||
"price": t.get('price'),
|
||||
"mount": t.get('mount'),
|
||||
"sl": t.get('sl'),
|
||||
"tp": t.get('tp')
|
||||
},
|
||||
symbol=t.get('symbol'),
|
||||
message=f"{action_text} @ {t.get('price')}, 手数={t.get('mount')}"
|
||||
)
|
||||
|
||||
# 记录平仓指令日志
|
||||
if close_tickets:
|
||||
system_log.add_log(
|
||||
"close_position",
|
||||
{"tickets": close_tickets},
|
||||
symbol=symbol,
|
||||
message=f"平仓指令: {close_tickets}"
|
||||
)
|
||||
|
||||
# 记录汇总日志
|
||||
system_log.add_log(
|
||||
"ea_trade_request",
|
||||
{
|
||||
"trades_count": len(trades),
|
||||
"close_count": len(close_tickets),
|
||||
"pivot_alerts_count": len(pivot_alerts)
|
||||
},
|
||||
symbol=symbol,
|
||||
message=f"下发交易指令: {len(trades)}个开仓, {len(close_tickets)}个平仓"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -96,22 +149,33 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
|
||||
}
|
||||
```
|
||||
"""
|
||||
# 获取原始请求体用于调试
|
||||
body = await request.body()
|
||||
print(f"[DEBUG] Raw body type: {type(body)}")
|
||||
print(f"[DEBUG] Raw body: {body}")
|
||||
print(f"[DEBUG] Raw body length: {len(body)}")
|
||||
|
||||
# 尝试解析JSON
|
||||
import json
|
||||
try:
|
||||
data = await request.json()
|
||||
print(f"[DEBUG] Parsed JSON successfully: {data}")
|
||||
server.save_statistics(data)
|
||||
|
||||
# 随机打印日志 (5%概率)
|
||||
if random.random() < STATISTICS_LOG_PROBABILITY:
|
||||
symbol = data.get('symbol', 'UNKNOWN')
|
||||
system_log = get_system_log()
|
||||
system_log.add_log(
|
||||
"ea_statistics",
|
||||
{
|
||||
"tick_count": data.get('tickCount'),
|
||||
"bid": data.get('bidPrice'),
|
||||
"ask": data.get('askPrice'),
|
||||
"spread": data.get('spread'),
|
||||
"spread_points": data.get('spreadPoints'),
|
||||
"balance": data.get('balance'),
|
||||
"equity": data.get('equity')
|
||||
},
|
||||
symbol=symbol,
|
||||
message=f"Tick: {data.get('tickCount')}, Spread: {data.get('spreadPoints', 0):.1f}pts, Balance: {data.get('balance')}"
|
||||
)
|
||||
|
||||
return {"status": "ok", "message": "统计数据已保存"}
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to parse JSON: {e}")
|
||||
print(f"[ERROR] Body as string: {body.decode('utf-8', errors='ignore')}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
@router.post("/close_position")
|
||||
@@ -138,7 +202,7 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
|
||||
try:
|
||||
data = await request.json()
|
||||
ticket = data.get('ticket')
|
||||
symbol = data.get('symbol', '').upper()
|
||||
symbol = data.get('symbol', '')
|
||||
|
||||
if not ticket:
|
||||
return {"status": "error", "message": "缺少订单号"}
|
||||
@@ -147,10 +211,309 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
|
||||
server.add_close_position_instruction(symbol, ticket)
|
||||
|
||||
print(f"[EA API] 平仓指令已添加: {symbol} ticket={ticket}")
|
||||
|
||||
# 记录日志
|
||||
system_log = get_system_log()
|
||||
system_log.add_log(
|
||||
"close_position",
|
||||
{"ticket": ticket},
|
||||
symbol=symbol,
|
||||
message=f"Ticket: {ticket}"
|
||||
)
|
||||
|
||||
return {"status": "ok", "message": "平仓指令已添加"}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] close_position 异常: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
return router
|
||||
@router.post("/calendar")
|
||||
async def send_calendar(request: Request) -> Dict:
|
||||
"""
|
||||
接收 EA 发送的财经日历数据(来自MT5 API)
|
||||
|
||||
EA调用MT5的calendar_*函数获取数据后,推送到此接口
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"id": "12345",
|
||||
"name": "Nonfarm Payrolls",
|
||||
"name_en": "Nonfarm Payrolls",
|
||||
"country": "US",
|
||||
"currency": "USD",
|
||||
"importance": 3,
|
||||
"publish_time": "2026-03-16T20:30:00",
|
||||
"forecast": "200K",
|
||||
"previous": "180K",
|
||||
"actual": "",
|
||||
"unit": "K",
|
||||
"event_type": "indicator"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "财经日历已更新",
|
||||
"count": 150
|
||||
}
|
||||
```
|
||||
"""
|
||||
import json as json_module
|
||||
import re as re_module
|
||||
try:
|
||||
# 先获取原始body
|
||||
raw_body = await request.body()
|
||||
raw_text = raw_body.decode('utf-8', errors='replace')
|
||||
|
||||
print(f"[calendar] 收到请求, 数据长度: {len(raw_text)} 字节")
|
||||
|
||||
# 清理所有控制字符 (0x00-0x1F, 除了 \t \n \r)
|
||||
# 保留 tab(0x09), LF(0x0A), CR(0x0D)
|
||||
def clean_control_chars(text):
|
||||
# 使用正则表达式一次性清理所有控制字符
|
||||
# 除了 tab(0x09), LF(0x0A), CR(0x0D)
|
||||
import re
|
||||
# 匹配所有控制字符 (0x00-0x1F) 除了 \t \n \r
|
||||
pattern = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f]')
|
||||
cleaned = pattern.sub('', text)
|
||||
removed_count = len(text) - len(cleaned)
|
||||
if removed_count > 0:
|
||||
print(f"[calendar] 已移除 {removed_count} 个控制字符")
|
||||
return cleaned
|
||||
|
||||
cleaned_text = clean_control_chars(raw_text)
|
||||
|
||||
try:
|
||||
data = json_module.loads(cleaned_text)
|
||||
except json_module.JSONDecodeError as e:
|
||||
# 如果仍然失败,打印问题位置附近的数据
|
||||
print(f"[ERROR] calendar JSON解析失败: {e}")
|
||||
error_pos = e.pos if hasattr(e, 'pos') else 0
|
||||
start = max(0, error_pos - 50)
|
||||
end = min(len(cleaned_text), error_pos + 50)
|
||||
print(f"[ERROR] 问题位置附近数据[{start}:{end}]: {repr(cleaned_text[start:end])}")
|
||||
return {"status": "error", "message": f"JSON解析失败: {e}"}
|
||||
|
||||
events = data.get('events', [])
|
||||
|
||||
print(f"[calendar] 解析成功, 收到 {len(events)} 个事件")
|
||||
|
||||
if not events:
|
||||
print("[calendar] 警告: events数组为空")
|
||||
return {"status": "ok", "message": "无数据需要更新", "count": 0}
|
||||
|
||||
from market.news_store import get_news_store
|
||||
news_store = get_news_store()
|
||||
|
||||
# 更新财经日历
|
||||
updated_count = news_store.update_calendar_from_mt5(events)
|
||||
|
||||
# 记录日志 - MT5上报财经日历
|
||||
system_log = get_system_log()
|
||||
system_log.add_log(
|
||||
"mt5_calendar_update",
|
||||
{
|
||||
"events_received": len(events),
|
||||
"events_updated": updated_count,
|
||||
"total_events": news_store.get_status().get('calendar_events', 0)
|
||||
},
|
||||
message=f"MT5上报财经日历: 收到{len(events)}条, 更新{updated_count}条"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": "财经日历已更新",
|
||||
"count": updated_count
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] calendar 更新异常: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
@router.post("/calendar_event_result")
|
||||
async def send_calendar_event_result(request: Request) -> Dict:
|
||||
"""
|
||||
接收 EA 发送的事件结果(事件发布后EA获取实际值)
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{
|
||||
"event_id": "12345",
|
||||
"actual": "210K",
|
||||
"forecast": "200K",
|
||||
"previous": "180K"
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "事件结果已更新"
|
||||
}
|
||||
```
|
||||
"""
|
||||
try:
|
||||
data = await request.json()
|
||||
event_id = data.get('event_id')
|
||||
actual = data.get('actual', '')
|
||||
forecast = data.get('forecast', '')
|
||||
previous = data.get('previous', '')
|
||||
|
||||
if not event_id:
|
||||
return {"status": "error", "message": "缺少事件ID"}
|
||||
|
||||
from market.news_store import get_news_store
|
||||
news_store = get_news_store()
|
||||
|
||||
# 获取事件
|
||||
event = news_store.get_event_by_id(event_id)
|
||||
if not event:
|
||||
return {"status": "error", "message": f"未找到事件: {event_id}"}
|
||||
|
||||
# 更新事件结果
|
||||
event.actual = actual
|
||||
if forecast:
|
||||
event.forecast = forecast
|
||||
if previous:
|
||||
event.previous = previous
|
||||
|
||||
# 计算结果(好于/差于/符合预期)
|
||||
result = _calculate_event_result(actual, event.forecast)
|
||||
event.result = result
|
||||
event.analyzed = True
|
||||
|
||||
# 记录日志 - MT5上报事件结果
|
||||
system_log = get_system_log()
|
||||
system_log.add_log(
|
||||
"mt5_event_result",
|
||||
{
|
||||
"event_id": event_id,
|
||||
"event_name": event.name,
|
||||
"actual": actual,
|
||||
"forecast": event.forecast,
|
||||
"previous": previous,
|
||||
"result": result
|
||||
},
|
||||
symbol=event.currency,
|
||||
message=f"MT5事件结果: {event.name} 实际={actual} 预测={event.forecast} ({result})"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": "事件结果已更新"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] calendar_event_result 更新异常: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
@router.post("/trade_history")
|
||||
async def receive_trade_history(request: Request) -> Dict:
|
||||
"""
|
||||
接收 EA 发送的交易历史数据
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{
|
||||
"deals": [
|
||||
{
|
||||
"ticket": 123456,
|
||||
"order": 789012,
|
||||
"symbol": "GOLD#",
|
||||
"type": 0,
|
||||
"entry": 0,
|
||||
"volume": 0.1,
|
||||
"price": 2050.50,
|
||||
"profit": 0,
|
||||
"swap": 0,
|
||||
"commission": -5.0,
|
||||
"time": "2026.03.16 15:30:00",
|
||||
"comment": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "交易历史已更新",
|
||||
"count": 50
|
||||
}
|
||||
```
|
||||
"""
|
||||
import json as json_module
|
||||
try:
|
||||
data = await request.json()
|
||||
deals = data.get('deals', [])
|
||||
|
||||
print(f"[trade_history] 收到 {len(deals)} 条成交记录")
|
||||
|
||||
if not deals:
|
||||
return {"status": "ok", "message": "无数据需要更新", "count": 0}
|
||||
|
||||
from market.trade_history_store import get_trade_history_store
|
||||
store = get_trade_history_store()
|
||||
|
||||
# 更新交易历史
|
||||
new_count = store.update_from_ea(deals)
|
||||
|
||||
# 记录日志
|
||||
system_log = get_system_log()
|
||||
system_log.add_log(
|
||||
"trade_history_update",
|
||||
{
|
||||
"deals_received": len(deals),
|
||||
"deals_new": new_count,
|
||||
"total_deals": len(store.get_all_deals())
|
||||
},
|
||||
message=f"交易历史上报: 收到{len(deals)}条, 新增{new_count}条"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": "交易历史已更新",
|
||||
"count": new_count
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] trade_history 更新异常: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _calculate_event_result(actual: str, forecast: str) -> str:
|
||||
"""计算事件结果"""
|
||||
try:
|
||||
# 尝试提取数字
|
||||
import re
|
||||
actual_num = float(re.sub(r'[^\d.-]', '', actual))
|
||||
forecast_num = float(re.sub(r'[^\d.-]', '', forecast))
|
||||
|
||||
if forecast_num == 0:
|
||||
return 'unknown'
|
||||
|
||||
diff_pct = (actual_num - forecast_num) / abs(forecast_num)
|
||||
|
||||
if abs(diff_pct) < 0.05:
|
||||
return 'in_line'
|
||||
elif diff_pct > 0:
|
||||
return 'better'
|
||||
else:
|
||||
return 'worse'
|
||||
except:
|
||||
return 'unknown'
|
||||
Reference in New Issue
Block a user