#!/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 }