- 新增新闻爬取和监控模块 (news_crawler, news_monitor) - 新增 LLM 分析模块 (llm_analyzer) - 新增持仓管理和交易历史存储 - 新增系统日志功能 - 新增前端页面: News, Positions, Settings, SystemLog - 更新路由和 API 接口 - 更新 .gitignore 排除敏感文件
1049 lines
38 KiB
Python
1049 lines
38 KiB
Python
#!/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
|
||
import os
|
||
|
||
from .store import MarketStore
|
||
from .pivot_detector import PivotDetector
|
||
from .pending_orders import PendingOrderManager
|
||
|
||
|
||
# 配置文件路径
|
||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'trade_config.json')
|
||
|
||
|
||
# 交易配置
|
||
class TradeConfig:
|
||
"""交易配置"""
|
||
_instance = None
|
||
_lock = threading.Lock()
|
||
|
||
def __init__(self):
|
||
self.enabled = True # 是否启用自动生成
|
||
|
||
# 默认配置
|
||
self.default_volume = 0.01 # 默认手数
|
||
self.default_sl_offset = 0.05 # 默认止损偏移(固定点数)
|
||
|
||
# MT5服务器时区偏移(单位:小时)
|
||
# 正数表示MT5时间比本地时间快,负数表示比本地时间慢
|
||
# 例如:MT5服务器时间是GMT+2,本地时间是GMT+8,则偏移为 -6
|
||
self.mt5_timezone_offset = 0
|
||
|
||
# 按品种配置: {symbol: {"volume": 0.01, "sl_offset": 0.05, "key_levels": "5000,5100", "key_level_threshold": 0.0008}}
|
||
self.symbol_config = {
|
||
"GOLD#": {"volume": 0.01, "sl_offset": 0.5},
|
||
"OILCASH#": {"volume": 0.01, "sl_offset": 0.05},
|
||
}
|
||
|
||
# 启动时自动加载配置文件
|
||
self._load_from_file()
|
||
|
||
def _load_from_file(self):
|
||
"""从配置文件加载配置"""
|
||
try:
|
||
if os.path.exists(CONFIG_FILE):
|
||
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
self.update(data)
|
||
print(f"[TradeConfig] 已从配置文件加载: mt5_timezone_offset={self.mt5_timezone_offset}")
|
||
else:
|
||
print(f"[TradeConfig] 配置文件不存在: {CONFIG_FILE},使用默认配置")
|
||
except Exception as e:
|
||
print(f"[TradeConfig] 加载配置文件失败: {e},使用默认配置")
|
||
|
||
def save_to_file(self):
|
||
"""保存配置到文件"""
|
||
try:
|
||
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
|
||
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||
json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)
|
||
print(f"[TradeConfig] 配置已保存到: {CONFIG_FILE}")
|
||
return True
|
||
except Exception as e:
|
||
print(f"[TradeConfig] 保存配置文件失败: {e}")
|
||
return False
|
||
|
||
@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:
|
||
"""获取品种配置,如果未配置则返回默认值"""
|
||
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),
|
||
"key_levels": config.get("key_levels", ""),
|
||
"key_level_threshold": config.get("key_level_threshold", 0.0008)
|
||
}
|
||
return {
|
||
"volume": self.default_volume,
|
||
"sl_offset": self.default_sl_offset,
|
||
"key_levels": "",
|
||
"key_level_threshold": 0.0008
|
||
}
|
||
|
||
def get_key_levels(self, symbol: str) -> List[float]:
|
||
"""
|
||
获取品种的关键点位列表
|
||
|
||
Args:
|
||
symbol: 品种名称
|
||
|
||
Returns:
|
||
关键点位列表,如 [5000, 5100, 5200]
|
||
"""
|
||
config = self.get_symbol_config(symbol)
|
||
key_levels_str = config.get("key_levels", "")
|
||
if not key_levels_str:
|
||
return []
|
||
|
||
levels = []
|
||
for level_str in key_levels_str.split(","):
|
||
level_str = level_str.strip()
|
||
if level_str:
|
||
try:
|
||
levels.append(float(level_str))
|
||
except ValueError:
|
||
continue
|
||
return sorted(levels)
|
||
|
||
def to_dict(self) -> Dict:
|
||
return {
|
||
"enabled": self.enabled,
|
||
"default_volume": self.default_volume,
|
||
"default_sl_offset": self.default_sl_offset,
|
||
"mt5_timezone_offset": self.mt5_timezone_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 "mt5_timezone_offset" in data:
|
||
self.mt5_timezone_offset = float(data["mt5_timezone_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, llm_analyzer=None):
|
||
self.store = store
|
||
self.detector = detector
|
||
self.pending_orders = pending_orders
|
||
self.trade_config = TradeConfig.get_instance()
|
||
self.llm_analyzer = llm_analyzer
|
||
|
||
# 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()
|
||
|
||
# AI入场价提醒冷却(避免重复提醒)
|
||
self._alerted_ai_entries: Dict[str, datetime] = {}
|
||
|
||
# 关键点位订单冷却(避免重复生成订单)
|
||
# 结构: {symbol_key: datetime}
|
||
self._alerted_key_levels: Dict[str, datetime] = {}
|
||
|
||
# 主事件循环引用(在FastAPI启动时设置)
|
||
self._main_loop = None
|
||
|
||
# 提醒冷却时间(秒)
|
||
self.alert_cooldown = 300 # 5分钟内同一转折点不重复提醒
|
||
|
||
# 关键点位订单冷却时间(秒)- 与订单超时时间一致
|
||
self.key_level_cooldown = 180 # 3分钟
|
||
|
||
print("[PivotMonitor] 转折点监控器已初始化")
|
||
|
||
def set_event_loop(self, loop):
|
||
"""设置主事件循环引用"""
|
||
self._main_loop = loop
|
||
print(f"[PivotMonitor] 已设置主事件循环")
|
||
|
||
def set_statistics_history(self, statistics_history):
|
||
"""设置统计数据历史引用(用于获取价差)"""
|
||
self._statistics_history = statistics_history
|
||
|
||
def _get_symbol_spread(self, symbol: str) -> Optional[float]:
|
||
"""
|
||
获取指定品种的最新价差
|
||
|
||
Args:
|
||
symbol: 品种名称
|
||
|
||
Returns:
|
||
价差(金额),如果没有返回None
|
||
"""
|
||
if not hasattr(self, '_statistics_history') or not self._statistics_history:
|
||
return None
|
||
|
||
symbol_normalized = symbol.replace('#', '')
|
||
|
||
# 从最新的统计数据中查找该品种的价差
|
||
for stat in reversed(list(self._statistics_history)):
|
||
stat_symbol = stat.get('symbol', '')
|
||
stat_normalized = stat_symbol.replace('#', '')
|
||
if stat_normalized == symbol_normalized:
|
||
spread = stat.get('spread')
|
||
if spread is not None and spread > 0:
|
||
return spread
|
||
|
||
return None
|
||
|
||
def _calculate_take_profit(self, action: str, entry_price: float, sl: float, tp: float = None) -> Optional[float]:
|
||
"""
|
||
计算并修正止盈价格
|
||
|
||
规则:
|
||
1. 止盈方向必须正确(买入止盈>入场价,卖出止盈<入场价)
|
||
2. 风险回报比至少为1(止盈距离 >= 止损距离)
|
||
3. 如果不满足,按照风险回报比=1重新计算
|
||
|
||
Args:
|
||
action: 'b' 买入 或 's' 卖出
|
||
entry_price: 入场价格
|
||
sl: 止损价格
|
||
tp: 原始止盈价格(可能为None)
|
||
|
||
Returns:
|
||
修正后的止盈价格,如果止损设置有问题返回None
|
||
"""
|
||
if action == 'b':
|
||
# 买入:止损应该 < 入场价
|
||
risk = entry_price - sl
|
||
if risk <= 0:
|
||
# 止损设置有问题(止损高于入场价),不生成订单
|
||
print(f"[PivotMonitor] 警告: 买入止损{sl}高于入场价{entry_price},跳过订单")
|
||
return None
|
||
|
||
# 计算最小止盈(风险回报比=1)
|
||
min_tp = entry_price + risk
|
||
|
||
# 如果没有止盈,或者止盈不满足条件,使用最小止盈
|
||
if tp is None or tp <= entry_price or (tp - entry_price) < risk:
|
||
print(f"[PivotMonitor] 修正买入止盈: 原{tp} -> 新{min_tp:.2f} (风险={risk:.2f})")
|
||
return round(min_tp, 2)
|
||
return round(tp, 2)
|
||
|
||
else: # action == 's'
|
||
# 卖出:止损应该 > 入场价
|
||
risk = sl - entry_price
|
||
if risk <= 0:
|
||
# 止损设置有问题(止损低于入场价),不生成订单
|
||
print(f"[PivotMonitor] 警告: 卖出止损{sl}低于入场价{entry_price},跳过订单")
|
||
return None
|
||
|
||
# 计算最小止盈(风险回报比=1)
|
||
min_tp = entry_price - risk
|
||
|
||
# 如果没有止盈,或者止盈不满足条件,使用最小止盈
|
||
if tp is None or tp >= entry_price or (entry_price - tp) < risk:
|
||
print(f"[PivotMonitor] 修正卖出止盈: 原{tp} -> 新{min_tp:.2f} (风险={risk:.2f})")
|
||
return round(min_tp, 2)
|
||
return round(tp, 2)
|
||
|
||
def _get_auto_key_levels(self, symbol: str, current_price: float) -> List[float]:
|
||
"""
|
||
根据品种价格位数自动计算关键点位
|
||
|
||
规则:
|
||
- 一位数价格:能被1整除
|
||
- 两位数价格:能被5整除
|
||
- 三位数价格:能被10整除
|
||
- 四位数价格:能被100整除
|
||
- 五位数或六位数价格:能被1000整除
|
||
|
||
Args:
|
||
symbol: 品种名称
|
||
current_price: 当前价格
|
||
|
||
Returns:
|
||
关键点位列表(当前价格上下各3个)
|
||
"""
|
||
if current_price <= 0:
|
||
return []
|
||
|
||
# 计算整数部分位数
|
||
int_part = int(current_price)
|
||
num_digits = len(str(int_part)) if int_part > 0 else 1
|
||
|
||
# 根据位数确定步长
|
||
if num_digits == 1:
|
||
step = 1
|
||
elif num_digits == 2:
|
||
step = 5
|
||
elif num_digits == 3:
|
||
step = 10
|
||
elif num_digits == 4:
|
||
step = 100
|
||
else: # 5位数或6位数
|
||
step = 1000
|
||
|
||
# 计算当前价格所在的基础点位
|
||
base_level = int(current_price / step) * step
|
||
|
||
# 生成上下各3个关键点位
|
||
levels = []
|
||
for i in range(-3, 4):
|
||
level = base_level + i * step
|
||
if level > 0: # 确保价格为正
|
||
levels.append(float(level))
|
||
|
||
return sorted(levels)
|
||
|
||
def check_key_levels(self, symbol: str, current_price: float) -> Optional[Dict]:
|
||
"""
|
||
检查价格是否接近关键点位,并生成交易指令
|
||
|
||
策略逻辑:
|
||
- 向下走接近关键点位 → 买入(支撑位)
|
||
- 向上走接近关键点位 → 卖出(压力位)
|
||
|
||
如果没有配置关键点位,则自动计算关键点位
|
||
|
||
Args:
|
||
symbol: 交易品种
|
||
current_price: 当前价格
|
||
|
||
Returns:
|
||
交易指令或None
|
||
"""
|
||
if not self.trade_config.enabled:
|
||
return None
|
||
|
||
# 获取关键点位配置
|
||
key_levels = self.trade_config.get_key_levels(symbol)
|
||
|
||
# 如果没有配置关键点位,自动计算
|
||
if not key_levels:
|
||
key_levels = self._get_auto_key_levels(symbol, current_price)
|
||
|
||
if not key_levels:
|
||
return None
|
||
|
||
threshold = self.trade_config.get_symbol_config(symbol).get("key_level_threshold", 0.0008)
|
||
|
||
# 找到最近的关键点位
|
||
nearest_level = None
|
||
min_distance = float('inf')
|
||
|
||
for level in key_levels:
|
||
distance_pct = abs(current_price - level) / current_price
|
||
if distance_pct < min_distance:
|
||
min_distance = distance_pct
|
||
nearest_level = level
|
||
|
||
if nearest_level is None:
|
||
return None
|
||
|
||
# 判断是否在阈值范围内
|
||
distance_pct = abs(current_price - nearest_level) / current_price
|
||
if distance_pct > threshold:
|
||
return None
|
||
|
||
# 检查是否已经为该关键点位生成过订单(在冷却时间内)
|
||
current_time = datetime.now()
|
||
key_level_key = f"{symbol}_{nearest_level}"
|
||
if key_level_key in self._alerted_key_levels:
|
||
last_alert = self._alerted_key_levels[key_level_key]
|
||
elapsed = (current_time - last_alert).total_seconds()
|
||
if elapsed < self.key_level_cooldown:
|
||
# 还在冷却时间内,跳过
|
||
return None
|
||
|
||
# 记录提醒时间
|
||
self._alerted_key_levels[key_level_key] = current_time
|
||
|
||
# 判断走势方向:通过价格相对于关键点位的位置
|
||
|
||
# 获取品种配置
|
||
config = self.trade_config.get_symbol_config(symbol)
|
||
volume = config["volume"]
|
||
|
||
# 获取价差
|
||
spread = self._get_symbol_spread(symbol)
|
||
|
||
# 根据价格与关键点位的关系判断方向
|
||
if current_price > nearest_level:
|
||
# 价格在关键点位上方,向下接近 → 买入(支撑位)
|
||
action = 'b'
|
||
sl = nearest_level - (nearest_level * 0.006) # 关键点位下方万分之六
|
||
if spread:
|
||
sl -= spread # 买入止损需要更低
|
||
# 止盈:1.5倍风险回报比
|
||
risk = current_price - sl
|
||
tp = current_price + risk * 1.5
|
||
if spread:
|
||
tp -= spread # 买入止盈需要更低
|
||
reason = f"关键点位策略: 价格向下接近 {nearest_level}(支撑位)"
|
||
else:
|
||
# 价格在关键点位下方,向上接近 → 卖出(压力位)
|
||
action = 's'
|
||
sl = nearest_level + (nearest_level * 0.006) # 关键点位上方万分之六
|
||
if spread:
|
||
sl += spread # 卖出止损需要更高
|
||
# 止盈:1.5倍风险回报比
|
||
risk = sl - current_price
|
||
tp = current_price - risk * 1.5
|
||
if spread:
|
||
tp += spread # 卖出止盈需要更高
|
||
reason = f"关键点位策略: 价格向上接近 {nearest_level}(压力位)"
|
||
|
||
# 验证并修正止盈
|
||
tp = self._calculate_take_profit(action, current_price, sl, tp)
|
||
if tp is None:
|
||
# 止损设置有问题,不生成订单
|
||
return None
|
||
|
||
# 获取各周期的AI建议方向
|
||
ai_directions = self._get_ai_directions_by_period(symbol)
|
||
key_level_direction_text = '买入' if action == 'b' else '卖出'
|
||
|
||
# 判断方向一致性并生成建议
|
||
direction_analysis = self._analyze_direction_consistency(action, ai_directions)
|
||
|
||
# 创建订单
|
||
order = {
|
||
"symbol": symbol,
|
||
"action": action,
|
||
"price": current_price,
|
||
"mount": volume,
|
||
"sl": round(sl, 2),
|
||
"tp": tp,
|
||
"reason": reason,
|
||
"description": "Key Level Strategy",
|
||
"source": "key_level",
|
||
"key_level": nearest_level,
|
||
"distance_pct": round(distance_pct * 100, 4),
|
||
"generated_at": current_time.isoformat(),
|
||
# 新增AI方向对比字段
|
||
"ai_directions": ai_directions, # 各周期AI方向
|
||
"key_level_direction_text": key_level_direction_text,
|
||
"direction_consistent": direction_analysis['is_consistent'],
|
||
"consistent_periods": direction_analysis['consistent_periods'],
|
||
"inconsistent_periods": direction_analysis['inconsistent_periods'],
|
||
"recommendation": direction_analysis['recommendation'],
|
||
"recommendation_color": direction_analysis['recommendation_color']
|
||
}
|
||
|
||
# 添加到待确认订单
|
||
if self.pending_orders:
|
||
order_id = self.pending_orders.add_order(order)
|
||
order["order_id"] = order_id
|
||
|
||
print(f"[PivotMonitor] 关键点位策略生成订单: {order_id} - {action} {symbol} @ {current_price}, 关键位={nearest_level}, SL={sl:.2f}, TP={tp:.2f}")
|
||
print(f"[PivotMonitor] AI各周期方向: {ai_directions}, 关键点位方向: {key_level_direction_text}, 一致周期: {direction_analysis['consistent_periods']}, 建议: {direction_analysis['recommendation']}")
|
||
|
||
# 推送关键点位订单通知到前端
|
||
self._broadcast_key_level_order(order)
|
||
|
||
return order
|
||
|
||
return None
|
||
|
||
def _get_ai_directions_by_period(self, symbol: str) -> Dict[str, Dict]:
|
||
"""
|
||
获取AI各周期的交易建议方向
|
||
|
||
Args:
|
||
symbol: 交易品种
|
||
|
||
Returns:
|
||
{period: {'direction': 'buy'/'sell', 'text': '买入'/'卖出', 'entry_price': xxx}}
|
||
"""
|
||
if not self.llm_analyzer:
|
||
return {}
|
||
|
||
result = {}
|
||
try:
|
||
analysis = self.llm_analyzer.get_analysis(symbol)
|
||
if not analysis:
|
||
return {}
|
||
|
||
# 从交易建议中获取各周期方向
|
||
analysis_data = analysis.get('analysis', {})
|
||
trade_suggestions = analysis_data.get('trade_suggestions', [])
|
||
|
||
for suggestion in trade_suggestions:
|
||
period = suggestion.get('period', '')
|
||
direction = suggestion.get('direction', '')
|
||
entry_price = suggestion.get('entry_price')
|
||
|
||
if period and direction:
|
||
# 标准化方向
|
||
direction_lower = direction.lower().strip()
|
||
if direction_lower in ['buy', '买入', '多头']:
|
||
direction_normalized = 'buy'
|
||
direction_text = '买入'
|
||
elif direction_lower in ['sell', '卖出', '空头']:
|
||
direction_normalized = 'sell'
|
||
direction_text = '卖出'
|
||
else:
|
||
continue
|
||
|
||
result[period] = {
|
||
'direction': direction_normalized,
|
||
'text': direction_text,
|
||
'entry_price': entry_price
|
||
}
|
||
|
||
return result
|
||
except Exception as e:
|
||
print(f"[PivotMonitor] 获取AI各周期方向失败: {e}")
|
||
return {}
|
||
|
||
def _analyze_direction_consistency(self, key_level_action: str, ai_directions: Dict[str, Dict]) -> Dict:
|
||
"""
|
||
分析关键点位方向与AI各周期方向的一致性
|
||
|
||
Args:
|
||
key_level_action: 'b' 或 's'
|
||
ai_directions: {period: {'direction': 'buy'/'sell', ...}}
|
||
|
||
Returns:
|
||
{
|
||
'is_consistent': bool, # 是否有任一周期一致
|
||
'consistent_periods': [], # 一致的周期列表
|
||
'inconsistent_periods': [], # 不一致的周期列表
|
||
'recommendation': str, # 建议文本
|
||
'recommendation_color': str # 建议颜色
|
||
}
|
||
"""
|
||
if not ai_directions:
|
||
return {
|
||
'is_consistent': False,
|
||
'consistent_periods': [],
|
||
'inconsistent_periods': [],
|
||
'recommendation': 'AI暂无建议,请谨慎操作',
|
||
'recommendation_color': 'warning'
|
||
}
|
||
|
||
consistent_periods = []
|
||
inconsistent_periods = []
|
||
|
||
for period, dir_info in ai_directions.items():
|
||
ai_dir = dir_info.get('direction', '')
|
||
|
||
# b = buy, s = sell
|
||
if (key_level_action == 'b' and ai_dir == 'buy') or \
|
||
(key_level_action == 's' and ai_dir == 'sell'):
|
||
consistent_periods.append(period)
|
||
else:
|
||
inconsistent_periods.append(period)
|
||
|
||
# 判断整体一致性
|
||
is_consistent = len(consistent_periods) > 0 and len(inconsistent_periods) == 0
|
||
|
||
# 生成建议
|
||
if len(consistent_periods) == len(ai_directions):
|
||
# 全部一致
|
||
recommendation = f"AI各周期方向一致,建议下单"
|
||
recommendation_color = "success"
|
||
elif len(consistent_periods) > 0:
|
||
# 部分一致
|
||
recommendation = f"AI部分周期一致({','.join(consistent_periods)}),建议谨慎"
|
||
recommendation_color = "warning"
|
||
else:
|
||
# 全部不一致
|
||
recommendation = f"AI方向不一致,建议慎重"
|
||
recommendation_color = "error"
|
||
|
||
return {
|
||
'is_consistent': is_consistent,
|
||
'consistent_periods': consistent_periods,
|
||
'inconsistent_periods': inconsistent_periods,
|
||
'recommendation': recommendation,
|
||
'recommendation_color': recommendation_color
|
||
}
|
||
|
||
def check_ai_entry(self, symbol: str, current_price: float) -> List[Dict]:
|
||
"""
|
||
检查价格是否接近AI建议的入场价,并生成交易指令
|
||
|
||
Args:
|
||
symbol: 交易品种
|
||
current_price: 当前价格
|
||
|
||
Returns:
|
||
AI入场价提醒列表
|
||
"""
|
||
if not self.llm_analyzer:
|
||
return []
|
||
|
||
if not self.trade_config.enabled:
|
||
return []
|
||
|
||
# 检查AI入场价
|
||
ai_matches = self.llm_analyzer.check_entry_price_nearby(symbol, current_price, threshold=0.0001)
|
||
|
||
ai_entry_alerts = []
|
||
current_time = datetime.now()
|
||
|
||
# 获取价差
|
||
spread = self._get_symbol_spread(symbol)
|
||
|
||
for match in ai_matches:
|
||
# 生成待确认订单
|
||
action = 'b' if match['direction'] == 'buy' else 's'
|
||
|
||
# 检查是否已经提醒过这个AI入场价(5分钟内不重复)
|
||
ai_key = f"{symbol}_{match['period']}_{match['entry_price']}_{match['direction']}"
|
||
if ai_key in self._alerted_ai_entries:
|
||
last_alert = self._alerted_ai_entries[ai_key]
|
||
elapsed = (current_time - last_alert).total_seconds()
|
||
if elapsed < self.alert_cooldown:
|
||
continue
|
||
|
||
# 记录提醒时间
|
||
self._alerted_ai_entries[ai_key] = current_time
|
||
|
||
# 根据方向调整止损止盈(考虑价差)
|
||
sl = match['stop_loss']
|
||
tp = match['take_profit']
|
||
if spread:
|
||
if action == 'b':
|
||
# 买入:止损需要更低,止盈需要更低
|
||
sl -= spread
|
||
tp -= spread
|
||
else:
|
||
# 卖出:止损需要更高,止盈需要更高
|
||
sl += spread
|
||
tp += spread
|
||
|
||
# 验证并修正止盈
|
||
tp = self._calculate_take_profit(action, current_price, sl, tp)
|
||
if tp is None:
|
||
# 止损设置有问题,跳过此订单
|
||
continue
|
||
|
||
order = {
|
||
"symbol": symbol,
|
||
"action": action,
|
||
"price": current_price,
|
||
"mount": self.trade_config.get_symbol_config(symbol).get("volume", 0.01),
|
||
"sl": round(sl, 2) if sl else None,
|
||
"tp": tp,
|
||
"reason": f"AI建议入场: {match['reason']}",
|
||
"description": "AI Trend Strategy",
|
||
"source": "ai_entry_nearby",
|
||
"ai_period": match['period'],
|
||
"ai_entry_price": match['entry_price'],
|
||
"ai_direction": match['direction'],
|
||
"generated_at": current_time.isoformat()
|
||
}
|
||
|
||
# 添加到待确认订单
|
||
if self.pending_orders:
|
||
order_id = self.pending_orders.add_order(order)
|
||
order["order_id"] = order_id
|
||
|
||
# 构建提醒
|
||
alert = {
|
||
"type": "ai_entry_alert",
|
||
"symbol": symbol,
|
||
"period": match['period'],
|
||
"direction": match['direction'],
|
||
"entry_price": match['entry_price'],
|
||
"current_price": current_price,
|
||
"price_diff_pct": match['price_diff_pct'],
|
||
"stop_loss": sl,
|
||
"take_profit": tp,
|
||
"reason": match['reason'],
|
||
"pending_order": order,
|
||
"timestamp": current_time.isoformat()
|
||
}
|
||
ai_entry_alerts.append(alert)
|
||
|
||
print(f"[PivotMonitor] AI趋势策略生成订单: {order_id} - {action} {symbol} @ {current_price}, AI入场价={match['entry_price']}")
|
||
|
||
# 广播AI入场价提醒
|
||
self._broadcast_alert(alert)
|
||
|
||
return ai_entry_alerts
|
||
|
||
def check_and_alert(self, symbol: str, current_price: float) -> List[Dict]:
|
||
"""
|
||
检查价格是否接近转折点,并发送提醒
|
||
同时检测关键点位策略
|
||
|
||
Args:
|
||
symbol: 交易品种
|
||
current_price: 当前价格
|
||
|
||
Returns:
|
||
接近的转折点列表
|
||
"""
|
||
# 检查关键点位策略
|
||
self.check_key_levels(symbol, current_price)
|
||
|
||
# 检查AI趋势策略
|
||
self.check_ai_entry(symbol, current_price)
|
||
|
||
# 检查是否接近转折点
|
||
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"] # 固定点数偏移
|
||
|
||
# 获取价差
|
||
spread = self._get_symbol_spread(symbol)
|
||
|
||
order = None
|
||
|
||
if alert_type == 'near_low':
|
||
# 接近低点 → 买入
|
||
# 止损 = 低点 - 配置的偏移
|
||
sl = pivot_price - sl_offset
|
||
if spread:
|
||
sl -= spread # 买入止损需要更低
|
||
# 止盈 = 最近的高点
|
||
tp = self._find_nearest_pivot_price(symbol, 'high', current_price)
|
||
|
||
# 验证并修正止盈
|
||
tp = self._calculate_take_profit('b', current_price, sl, tp)
|
||
if tp is not None:
|
||
order = {
|
||
"symbol": symbol,
|
||
"action": "b", # 买入
|
||
"price": current_price,
|
||
"mount": volume,
|
||
"sl": round(sl, 2),
|
||
"tp": tp,
|
||
"reason": f"M1接近低点{pivot_price:.2f},建议买入,止损{sl:.2f},止盈{tp:.2f}",
|
||
"description": "Pivot Strategy",
|
||
"source": "auto_pivot_m1",
|
||
"pivot_price": pivot_price,
|
||
"generated_at": current_time.isoformat()
|
||
}
|
||
|
||
elif alert_type == 'near_high':
|
||
# 接近高点 → 卖出
|
||
# 止损 = 高点 + 配置的偏移
|
||
sl = pivot_price + sl_offset
|
||
if spread:
|
||
sl += spread # 卖出止损需要更高
|
||
# 止盈 = 最近的低点
|
||
tp = self._find_nearest_pivot_price(symbol, 'low', current_price)
|
||
|
||
# 验证并修正止盈
|
||
tp = self._calculate_take_profit('s', current_price, sl, tp)
|
||
if tp is not None:
|
||
order = {
|
||
"symbol": symbol,
|
||
"action": "s", # 卖出
|
||
"price": current_price,
|
||
"mount": volume,
|
||
"sl": round(sl, 2),
|
||
"tp": tp,
|
||
"reason": f"M1接近高点{pivot_price:.2f},建议卖出,止损{sl:.2f},止盈{tp:.2f}",
|
||
"description": "Pivot Strategy",
|
||
"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
|
||
"""
|
||
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)
|
||
|
||
if not clients:
|
||
return
|
||
|
||
# 使用保存的主事件循环
|
||
if self._main_loop and self._main_loop.is_running():
|
||
for client in clients:
|
||
try:
|
||
asyncio.run_coroutine_threadsafe(
|
||
self._send_to_client(client, message),
|
||
self._main_loop
|
||
)
|
||
except Exception as e:
|
||
print(f"[PivotMonitor] 发送WebSocket消息失败: {e}")
|
||
else:
|
||
# 如果事件循环未就绪,尝试直接创建任务
|
||
try:
|
||
for client in clients:
|
||
asyncio.create_task(self._send_to_client(client, message))
|
||
except Exception as e:
|
||
print(f"[PivotMonitor] 广播消息失败: {e}")
|
||
|
||
def _broadcast_key_level_order(self, order: Dict):
|
||
"""广播关键点位订单通知到前端"""
|
||
action_text = '买入' if order['action'] == 'b' else '卖出'
|
||
alert = {
|
||
"type": "key_level_alert",
|
||
"symbol": order['symbol'],
|
||
"action": order['action'],
|
||
"action_text": action_text,
|
||
"price": order['price'],
|
||
"sl": order['sl'],
|
||
"tp": order['tp'],
|
||
"key_level": order['key_level'],
|
||
"distance_pct": order['distance_pct'],
|
||
"reason": order['reason'],
|
||
"pending_order": order,
|
||
"message": f"{order['symbol']} 关键点位策略: {action_text} @ {order['price']}, 关键位={order['key_level']}"
|
||
}
|
||
self._broadcast_alert(alert)
|
||
|
||
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的提醒记录"""
|
||
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
|
||
} |