feat: 添加前端界面和市场分析模块
- 新增 Vue 3 + Vuetify 前端界面 - 新增市场分析模块 (market/) - 更新主服务器和路由 - 更新 MT5 EA 文件 - 添加 .gitignore 排除临时文件
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user