feat: 添加前端界面和市场分析模块

- 新增 Vue 3 + Vuetify 前端界面
- 新增市场分析模块 (market/)
- 更新主服务器和路由
- 更新 MT5 EA 文件
- 添加 .gitignore 排除临时文件
This commit is contained in:
guaiwoluo2020
2026-03-10 17:38:13 +08:00
parent 0c9cf048d2
commit 51b2f30748
40 changed files with 8576 additions and 151 deletions
+412
View File
@@ -0,0 +1,412 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
转折点监控模块
实时监控价格与转折点的接近程度,并通过WebSocket推送提醒
"""
from typing import Dict, List, Optional, Set
from datetime import datetime
import threading
import asyncio
import json
from .store import MarketStore, normalize_symbol
from .pivot_detector import PivotDetector
from .pending_orders import PendingOrderManager
# 交易配置
class TradeConfig:
"""交易配置"""
_instance = None
_lock = threading.Lock()
def __init__(self):
self.enabled = True # 是否启用自动生成
# 默认配置
self.default_volume = 0.01 # 默认手数
self.default_sl_offset = 0.05 # 默认止损偏移(固定点数)
# 按品种配置: {symbol: {"volume": 0.01, "sl_offset": 0.05}}
self.symbol_config = {
"GOLD#": {"volume": 0.01, "sl_offset": 0.5},
"OILCASH#": {"volume": 0.01, "sl_offset": 0.05},
}
@classmethod
def get_instance(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
def get_symbol_config(self, symbol: str) -> Dict:
"""获取品种配置,如果未配置则返回默认值"""
symbol = symbol.upper()
if symbol in self.symbol_config:
config = self.symbol_config[symbol]
return {
"volume": config.get("volume", self.default_volume),
"sl_offset": config.get("sl_offset", self.default_sl_offset)
}
return {
"volume": self.default_volume,
"sl_offset": self.default_sl_offset
}
def to_dict(self) -> Dict:
return {
"enabled": self.enabled,
"default_volume": self.default_volume,
"default_sl_offset": self.default_sl_offset,
"symbol_config": self.symbol_config
}
def update(self, data: Dict):
if "enabled" in data:
self.enabled = bool(data["enabled"])
if "default_volume" in data:
self.default_volume = float(data["default_volume"])
if "default_sl_offset" in data:
self.default_sl_offset = float(data["default_sl_offset"])
if "symbol_config" in data:
self.symbol_config = data["symbol_config"]
class PivotMonitor:
"""转折点监控器"""
def __init__(self, store: MarketStore, detector: PivotDetector,
pending_orders: PendingOrderManager = None):
self.store = store
self.detector = detector
self.pending_orders = pending_orders
self.trade_config = TradeConfig.get_instance()
# WebSocket连接管理
self._ws_clients: Set = set()
self._ws_lock = threading.Lock()
# 已提醒的转折点(避免重复提醒)
# 结构: {(symbol, period, timestamp, price): datetime}
self._alerted_pivots: Dict[tuple, datetime] = {}
self._alert_lock = threading.Lock()
# 提醒冷却时间(秒)
self.alert_cooldown = 300 # 5分钟内同一转折点不重复提醒
print("[PivotMonitor] 转折点监控器已初始化")
def check_and_alert(self, symbol: str, current_price: float) -> List[Dict]:
"""
检查价格是否接近转折点,并发送提醒
Args:
symbol: 交易品种
current_price: 当前价格
Returns:
接近的转折点列表
"""
symbol = normalize_symbol(symbol)
# 检查是否接近转折点
near_pivots = self.detector.check_near_pivot(symbol, current_price)
if not near_pivots:
return []
# 过滤已提醒过的转折点
new_alerts = []
current_time = datetime.now()
with self._alert_lock:
for pivot in near_pivots:
key = (
pivot['symbol'],
pivot['period'],
pivot['timestamp'],
pivot['price']
)
# 检查是否已提醒过
if key in self._alerted_pivots:
last_alert = self._alerted_pivots[key]
elapsed = (current_time - last_alert).total_seconds()
# 如果在冷却时间内,跳过
if elapsed < self.alert_cooldown:
continue
# 记录提醒时间
self._alerted_pivots[key] = current_time
# 构建提醒消息
is_breakthrough = pivot.get('is_breakthrough', False)
alert_type = pivot.get('alert_type', '')
period = pivot['period']
# 根据类型生成不同的消息
if is_breakthrough:
if 'high' in alert_type:
message = f"{pivot['symbol']} {period} 已突破高点 {pivot['price']}, 当前价格 {pivot['current_price']}"
else:
message = f"{pivot['symbol']} {period} 已突破低点 {pivot['price']}, 当前价格 {pivot['current_price']}"
else:
if 'high' in alert_type:
message = f"{pivot['symbol']} {period} 接近高点 {pivot['price']}, 当前价格 {pivot['current_price']}, 距离 {pivot['distance_pct']}%"
else:
message = f"{pivot['symbol']} {period} 接近低点 {pivot['price']}, 当前价格 {pivot['current_price']}, 距离 {pivot['distance_pct']}%"
alert = {
"type": "pivot_alert",
"symbol": pivot['symbol'],
"period": period,
"direction": pivot['direction'],
"pivot_price": pivot['price'],
"current_price": pivot['current_price'],
"distance_pct": pivot['distance_pct'],
"threshold_pct": pivot['threshold_pct'],
"timestamp": current_time.isoformat(),
"alert_type": alert_type,
"is_breakthrough": is_breakthrough,
"message": message
}
# M1和M5周期接近转折点时,自动生成交易指令
pending_order = None
if period in ['M1', 'M5'] and not is_breakthrough:
pending_order = self._auto_generate_order(pivot, current_time)
# 如果生成了订单,加入通知中
if pending_order:
alert["pending_order"] = pending_order
new_alerts.append(alert)
# 异步推送WebSocket消息
self._broadcast_alert(alert)
# 清理过期的提醒记录
self._cleanup_alerted()
return new_alerts
def _auto_generate_order(self, pivot: Dict, current_time: datetime) -> Optional[Dict]:
"""
M1周期接近转折点时,自动生成交易指令
Args:
pivot: 转折点信息
current_time: 当前时间
Returns:
生成的订单信息,包含order_id
"""
if not self.pending_orders:
return None
if not self.trade_config.enabled:
return None
symbol = pivot['symbol']
current_price = pivot['current_price']
pivot_price = pivot['price']
direction = pivot['direction']
alert_type = pivot['alert_type']
# 只处理"接近"类型(near_high, near_low
if not alert_type.startswith('near_'):
return None
# 获取品种配置
config = self.trade_config.get_symbol_config(symbol)
volume = config["volume"]
sl_offset = config["sl_offset"] # 固定点数偏移
order = None
if alert_type == 'near_low':
# 接近低点 → 买入
# 止损 = 低点 - 配置的偏移
sl = pivot_price - sl_offset
# 止盈 = 最近的高点
tp = self._find_nearest_pivot_price(symbol, 'high', current_price)
if tp and tp > current_price:
order = {
"symbol": symbol,
"action": "b", # 买入
"price": current_price,
"mount": volume,
"sl": round(sl, 2),
"tp": round(tp, 2),
"reason": f"M1接近低点{pivot_price:.2f},建议买入,止损{sl:.2f},止盈{tp:.2f}",
"source": "auto_pivot_m1",
"pivot_price": pivot_price,
"generated_at": current_time.isoformat()
}
elif alert_type == 'near_high':
# 接近高点 → 卖出
# 止损 = 高点 + 配置的偏移
sl = pivot_price + sl_offset
# 止盈 = 最近的低点
tp = self._find_nearest_pivot_price(symbol, 'low', current_price)
if tp and tp < current_price:
order = {
"symbol": symbol,
"action": "s", # 卖出
"price": current_price,
"mount": volume,
"sl": round(sl, 2),
"tp": round(tp, 2),
"reason": f"M1接近高点{pivot_price:.2f},建议卖出,止损{sl:.2f},止盈{tp:.2f}",
"source": "auto_pivot_m1",
"pivot_price": pivot_price,
"generated_at": current_time.isoformat()
}
if order:
order_id = self.pending_orders.add_order(order)
print(f"[PivotMonitor] 自动生成交易指令: {order_id} - {order['action']} {symbol} @ {current_price}")
# 返回订单信息(包含order_id)
order["order_id"] = order_id
return order
return None
def _find_nearest_pivot_price(self, symbol: str, direction: str,
current_price: float) -> Optional[float]:
"""
找到离当前价格最近的转折点价格
Args:
symbol: 交易品种
direction: 'high''low'
current_price: 当前价格
Returns:
最近的转折点价格,如果没有返回None
"""
symbol = normalize_symbol(symbol)
nearest_price = None
min_distance = float('inf')
with self.detector._lock:
for period in self.detector._pivots[symbol]:
pivots = self.detector._pivots[symbol][period]
for pivot in pivots:
if pivot.direction != direction:
continue
# 对于高点,只考虑价格高于当前价的
# 对于低点,只考虑价格低于当前价的
if direction == 'high' and pivot.price <= current_price:
continue
if direction == 'low' and pivot.price >= current_price:
continue
distance = abs(pivot.price - current_price)
if distance < min_distance:
min_distance = distance
nearest_price = pivot.price
return nearest_price
def _broadcast_new_order(self, order_id: str, order: Dict) -> None:
"""广播新订单通知"""
message = json.dumps({
"type": "new_order",
"order_id": order_id,
"order": order
})
with self._ws_lock:
clients = list(self._ws_clients)
for client in clients:
try:
asyncio.create_task(self._send_to_client(client, message))
except Exception as e:
print(f"[PivotMonitor] 发送新订单通知失败: {e}")
def _cleanup_alerted(self):
"""清理过期的提醒记录"""
current_time = datetime.now()
with self._alert_lock:
keys_to_remove = []
for key, alert_time in self._alerted_pivots.items():
elapsed = (current_time - alert_time).total_seconds()
if elapsed > self.alert_cooldown * 2:
keys_to_remove.append(key)
for key in keys_to_remove:
del self._alerted_pivots[key]
def _broadcast_alert(self, alert: Dict):
"""广播提醒到所有WebSocket客户端"""
message = json.dumps(alert)
with self._ws_lock:
clients = list(self._ws_clients)
# 在事件循环中发送消息
for client in clients:
try:
asyncio.create_task(self._send_to_client(client, message))
except Exception as e:
print(f"[PivotMonitor] 发送WebSocket消息失败: {e}")
async def _send_to_client(self, client, message: str):
"""发送消息到客户端"""
try:
await client.send_text(message)
except Exception as e:
print(f"[PivotMonitor] 发送消息到客户端失败: {e}")
# 移除失效的客户端
with self._ws_lock:
self._ws_clients.discard(client)
def add_ws_client(self, client):
"""添加WebSocket客户端"""
with self._ws_lock:
self._ws_clients.add(client)
print(f"[PivotMonitor] WebSocket客户端已连接, 当前连接数: {len(self._ws_clients)}")
def remove_ws_client(self, client):
"""移除WebSocket客户端"""
with self._ws_lock:
self._ws_clients.discard(client)
print(f"[PivotMonitor] WebSocket客户端已断开, 当前连接数: {len(self._ws_clients)}")
def get_ws_client_count(self) -> int:
"""获取WebSocket客户端数量"""
with self._ws_lock:
return len(self._ws_clients)
def clear_symbol(self, symbol: str):
"""清除某个Symbol的提醒记录"""
symbol = normalize_symbol(symbol)
with self._alert_lock:
keys_to_remove = [k for k in self._alerted_pivots if k[0] == symbol]
for key in keys_to_remove:
del self._alerted_pivots[key]
def get_status(self) -> Dict:
"""获取监控状态"""
with self._alert_lock:
alerted_count = len(self._alerted_pivots)
return {
"ws_clients": self.get_ws_client_count(),
"alerted_pivots": alerted_count,
"alert_cooldown": self.alert_cooldown
}