# -*- coding: utf-8 -*- """ 波浪理论分析模块 (Wave Analysis Module) - v3 基于艾略特波浪理论 · 冯雷《股票波浪理论揭秘》 核心修正(v3): - 严格区分「5浪推动」与「调整区(A/B/C…)」 - 5浪推动 = 一个完整的中期趋势结构(涨跌均可) - 调整区 = 5浪完成后的独立修正阶段,不属于推动结构 - 禁止输出1-2-3-4-5-A-B-C的8浪封闭循环 - 支持周线主级别 / 日线子级别的嵌套框架 """ import numpy as np import pandas as pd from scipy.signal import argrelextrema class WaveAnalyzer: """ 单周期浪型分析器 架构: impulse_phase(推动浪):浪1-浪5,构成一个完整的趋势结构 corrective_phase(调整区):A浪/B浪/C浪/…,5浪完成后的修正阶段 """ PHASE_IMPULSE = 'impulse' # 推动浪阶段(1-5浪) PHASE_CORRECTIVE = 'corrective' # 调整区阶段(A/B/C/…) def __init__(self, df): self.df = df.copy() if 'timestamps' in self.df.columns and not isinstance(self.df.index, pd.DatetimeIndex): self.df.set_index('timestamps', inplace=True) self.close = self.df['close'].values.astype(float) self.high = self.df['high'].values.astype(float) self.low = self.df['low'].values.astype(float) self.volume = self.df['volume'].values.astype(float) if 'volume' in self.df.columns else np.zeros(len(self.close)) self.n = len(self.close) self.dates = self.df.index def find_pivots(self, order=5): peaks_idx = argrelextrema(self.high, np.greater_equal, order=order)[0] valleys_idx = argrelextrema(self.low, np.less_equal, order=order)[0] return list(peaks_idx), list(valleys_idx) def refine_pivots(self, peaks, valleys, min_swing_pct=0.02): all_pivots = [(idx, 'peak', self.high[idx]) for idx in peaks] + \ [(idx, 'valley', self.low[idx]) for idx in valleys] all_pivots.sort(key=lambda x: x[0]) filtered = [] for i, (idx, ptype, price) in enumerate(all_pivots): if i == 0 or i == len(all_pivots) - 1: filtered.append((idx, ptype, price)) continue if filtered: swing_pct = abs(price - filtered[-1][2]) / filtered[-1][2] if swing_pct < min_swing_pct: continue filtered.append((idx, ptype, price)) alternating = [] for idx, ptype, price in filtered: if not alternating: alternating.append((idx, ptype, price)) elif ptype != alternating[-1][1]: alternating.append((idx, ptype, price)) else: if ptype == 'peak' and price > alternating[-1][2]: alternating[-1] = (idx, ptype, price) elif ptype == 'valley' and price < alternating[-1][2]: alternating[-1] = (idx, ptype, price) return [p[0] for p in alternating if p[1] == 'peak'], \ [p[0] for p in alternating if p[1] == 'valley'] def build_swing_points(self, peaks, valleys): all_pivots = [(idx, 'high', self.high[idx]) for idx in peaks] + \ [(idx, 'low', self.low[idx]) for idx in valleys] all_pivots.sort(key=lambda x: x[0]) result = [] for idx, ptype, price in all_pivots: if not result: result.append((idx, ptype, price)) elif ptype != result[-1][1]: result.append((idx, ptype, price)) else: if ptype == 'high' and price > result[-1][2]: result[-1] = (idx, ptype, price) elif ptype == 'low' and price < result[-1][2]: result[-1] = (idx, ptype, price) return result def identify_waves(self, order=5, min_swing_pct=0.02): """ 主入口:识别浪型结构 返回 { 'waves': [...], # 浪型列表(推动+调整区分开) 'impulse_waves': [...], # 推动浪 1-5 'corrective_waves': [...], # 调整区 A/B/C 'phase': 'impulse'|'corrective'|'unknown', 'swing_points': [...], 'fib_levels': {...}, 'buy_signals': [...], 'sell_signals': [...], 'characteristics': {...}, 'current_wave': {...}, 'wave_count': int, 'is_complete_5wave': bool, 'iron_rule_checks': {...}, } """ peaks, valleys = self.find_pivots(order=order) peaks, valleys = self.refine_pivots(peaks, valleys, min_swing_pct) swings = self.build_swing_points(peaks, valleys) if len(swings) < 3: return self._empty_result() # 判断总体趋势方向 trend = self._detect_trend(swings) # 阶段1: 严格匹配5浪推动 strict_match = self._find_5wave_strict(swings, trend) # 阶段2: 宽松匹配 loose_match = self._find_5wave_loose(swings, trend) best_match = None is_strict = False if strict_match and (not loose_match or strict_match[1] >= loose_match[1]): best_match = strict_match[0] is_strict = True elif loose_match: best_match = loose_match[0] is_strict = False if best_match: waves = self._build_waves(swings, best_match) fib_levels = self._calc_fib_levels(waves) buy_signals, sell_signals = self._identify_signals(waves) characteristics = self._analyze_characteristics(waves, is_strict) iron_rules = self._check_iron_rules(waves) else: waves = self._label_partial_waves(swings, trend) fib_levels = {} buy_signals, sell_signals = self._identify_signals_simple(swings) characteristics = {} iron_rules = {} # 分离推动浪和调整区 impulse_waves = [w for w in waves if w['phase'] == self.PHASE_IMPULSE] corrective_waves = [w for w in waves if w['phase'] == self.PHASE_CORRECTIVE] # 判断当前处于哪个阶段 if corrective_waves: phase = self.PHASE_CORRECTIVE elif impulse_waves and len(impulse_waves) >= 5: phase = self.PHASE_CORRECTIVE # 5浪完成后默认进入调整区 elif impulse_waves: phase = self.PHASE_IMPULSE else: phase = 'unknown' return { 'waves': waves, 'impulse_waves': impulse_waves, 'corrective_waves': corrective_waves, 'phase': phase, 'swing_points': [(int(s[0]), s[1], round(float(s[2]), 2)) for s in swings], 'fib_levels': fib_levels, 'buy_signals': buy_signals, 'sell_signals': sell_signals, 'characteristics': characteristics, 'current_wave': self._current_wave_position(waves, phase), 'wave_count': len(waves), 'is_complete_5wave': best_match is not None, 'iron_rule_checks': iron_rules, } def _detect_trend(self, swings): """检测总体趋势方向(用于确定推动浪是上涨还是下跌)""" if len(swings) < 2: return 'up' first_price = swings[0][2] last_price = swings[-1][2] # 用中间点判断趋势 mid_idx = len(swings) // 2 mid_price = swings[mid_idx][2] if last_price > first_price and mid_price > first_price: return 'up' elif last_price < first_price and mid_price < first_price: return 'down' # 用涨跌比例判断 up_sum = 0 down_sum = 0 for i in range(len(swings) - 1): diff = swings[i + 1][2] - swings[i][2] if diff > 0: up_sum += diff else: down_sum += abs(diff) return 'up' if up_sum >= down_sum else 'down' def _find_5wave_strict(self, swings, trend): """严格匹配5浪推动:铁律1浪4底>浪1顶,铁律2浪3非最短""" if len(swings) < 6: return None best_score = -1 best = None for s in range(len(swings)): if swings[s][1] != 'low' or s + 5 >= len(swings): continue # 上涨趋势: low-high-low-high-low-high # 下跌趋势: low-high-low-high-low-high(结构不变,幅度判断方向) exp = ['low', 'high', 'low', 'high', 'low', 'high'] if any(swings[s + k][1] != exp[k] for k in range(6)): continue p = [swings[s + k][2] for k in range(6)] a1, a3, a5 = p[1] - p[0], p[3] - p[2], p[5] - p[4] if trend == 'up': if a1 <= 0 or a3 <= 0 or a5 <= 0: continue else: # 下跌趋势中,也可以看为反弹结构 if a1 <= 0 or a3 <= 0: continue if p[4] <= p[1]: # 铁律1:浪4底不得进入浪1价格区域 continue if a3 < a1 and a3 < a5: # 铁律2:浪3非最短 continue score = self._score_5wave(p, a1, a3, a5, swings, s, trend) if score > best_score: best_score = score best = (s, s + 1, s + 2, s + 3, s + 4, s + 5) return (best, best_score) if best and best_score >= 3 else None def _find_5wave_loose(self, swings, trend): """宽松匹配:铁律违反时扣分但不排除""" if len(swings) < 6: return None best_score = -1 best = None for s in range(len(swings)): if swings[s][1] != 'low' or s + 5 >= len(swings): continue exp = ['low', 'high', 'low', 'high', 'low', 'high'] if any(swings[s + k][1] != exp[k] for k in range(6)): continue p = [swings[s + k][2] for k in range(6)] a1, a3, a5 = p[1] - p[0], p[3] - p[2], p[5] - p[4] if trend == 'up' and (a1 <= 0 or a3 <= 0 or a5 <= 0): continue elif trend == 'down' and (a1 <= 0 or a3 <= 0): continue score = self._score_5wave(p, a1, a3, a5, swings, s, trend) # 铁律软扣分 if p[4] <= p[1]: score -= 3 if a3 < a1 and a3 < a5: score -= 3 if score > best_score: best_score = score best = (s, s + 1, s + 2, s + 3, s + 4, s + 5) return (best, best_score) if best and best_score >= 5 else None def _score_5wave(self, p, a1, a3, a5, swings, s, trend): """5浪结构评分""" score = 0 # 浪3/浪1比率接近1.618 if a1 > 0: r3 = a3 / a1 if 0.8 <= r3 <= 2.618: score += 3 if 1.1 <= r3 <= 2.2: score += 2 # 浪2回调 if a1 > 0: r2 = (p[1] - p[2]) / a1 if 0.15 <= r2 <= 0.75: score += 2 if 0.3 <= r2 <= 0.7: score += 1 # 浪4回调 r4_denom = p[3] - p[2] if r4_denom > 0: r4 = (p[3] - p[4]) / r4_denom if 0.1 <= r4 <= 0.75: score += 2 # 浪5/浪1 if a1 > 0: r5 = a5 / a1 if 0.5 <= r5 <= 3.5: score += 1 # 覆盖度 coverage = (swings[s + 5][0] - swings[s][0]) / self.n score += coverage * 3 # 递增趋势(上涨推动) if p[5] > p[3] > p[1]: score += 2 if a3 >= a1 and a3 >= a5: score += 1 return score def _build_waves(self, swings, indices): """ 构建5浪推动 + 调整区(非8浪合并) 关键区别:浪5之后的修正标记为「调整区」而非第6/7/8浪。 一个完整的中期趋势 = 推动浪(1-5) + 调整区(A/B/C...), 其中推动浪是独立完整的结构,调整区是5浪完成后的修正阶段。 """ i1, i2, i3, i4, i5, i6 = indices start_idx = max(0, swings[i1][0] - 1) waves = [] # ========== 推动浪阶段(浪1-浪5)========== impulse_defs = [ (start_idx, swings[i2][0], '1', self.PHASE_IMPULSE, 'impulse'), (swings[i2][0], swings[i3][0], '2', self.PHASE_IMPULSE, 'corrective'), (swings[i3][0], swings[i4][0], '3', self.PHASE_IMPULSE, 'impulse'), (swings[i4][0], swings[i5][0], '4', self.PHASE_IMPULSE, 'corrective'), (swings[i5][0], swings[i6][0], '5', self.PHASE_IMPULSE, 'impulse'), ] for si, ei, label, phase, wtype in impulse_defs: amp = self.close[ei] - self.close[si] waves.append({ 'index': int(label), 'label': label, 'phase': phase, 'type': wtype, 'start_idx': int(si), 'end_idx': int(ei), 'start_price': round(float(self.close[si]), 2), 'end_price': round(float(self.close[ei]), 2), 'amplitude': round(float(amp), 2), 'amplitude_pct': round(float(amp / self.close[si] * 100), 2) if self.close[si] > 0 else 0, }) # ========== 调整区(A/B/C...)========== # 5浪完成后的修正阶段,独立于推动浪结构 remaining = swings[i6 + 1:] if i6 + 1 < len(swings) else [] if len(remaining) >= 2: for k in range(min(5, len(remaining) - 1)): si = remaining[k][0] ei = remaining[k + 1][0] if k + 1 < len(remaining) else self.n - 1 amp = self.close[ei] - self.close[si] label = chr(ord('A') + k) waves.append({ 'index': label, 'label': label, 'phase': self.PHASE_CORRECTIVE, 'type': 'corrective', 'start_idx': int(si), 'end_idx': int(ei), 'start_price': round(float(self.close[si]), 2), 'end_price': round(float(self.close[ei]), 2), 'amplitude': round(float(amp), 2), 'amplitude_pct': round(float(amp / self.close[si] * 100), 2) if self.close[si] > 0 else 0, }) return waves def _label_partial_waves(self, swings, trend): """ 局部标注(无法匹配完整5浪时) 核心规则: - 推动浪阶段只标注1-5 - 当满足切换条件时转入调整区,标注A/B/C - 严禁在一个结构中同时标注推动浪编号和调整区字母 """ waves = [] phase = self.PHASE_IMPULSE impulse_count = 0 # 推动浪内已标注的编号数 corrective_count = 0 # 调整区内已标注的字母序号 for i in range(len(swings) - 1): si = swings[i][0] ei = swings[i + 1][0] amp = self.close[ei] - self.close[si] rel = amp / self.close[si] * 100 if self.close[si] > 0 else 0 is_up = amp > 0 if phase == self.PHASE_IMPULSE: if is_up: # 推动浪中的上涨段:奇数浪(1,3,5) impulse_count += 1 label = str(min(impulse_count, 5)) else: # 推动浪中的回调段:偶数浪(2,4) impulse_count += 1 label = str(min(impulse_count, 5)) # 判断是否应切换到调整区 # 条件:已标注至少3个上涨段+至少2个回调段 → 5浪可能完成 up_in_impulse = sum(1 for j in range(len(waves)) if waves[j]['amplitude'] > 0) down_in_impulse = sum(1 for j in range(len(waves)) if waves[j]['amplitude'] <= 0) if up_in_impulse >= 3 and down_in_impulse >= 2 and impulse_count >= 5: phase = self.PHASE_CORRECTIVE corrective_count = 0 # 当前这一段重新作为调整区A浪 label = 'A' corrective_count = 1 wtype = 'impulse' if is_up else 'corrective' waves.append({ 'index': label, 'label': label, 'phase': phase if label != 'A' else self.PHASE_CORRECTIVE, 'type': wtype, 'start_idx': int(si), 'end_idx': int(ei), 'start_price': round(float(self.close[si]), 2), 'end_price': round(float(self.close[ei]), 2), 'amplitude': round(float(amp), 2), 'amplitude_pct': round(float(rel), 2), }) else: # 调整区阶段 corrective_count += 1 label = chr(ord('A') + min(corrective_count - 1, 4)) # A-E waves.append({ 'index': label, 'label': label, 'phase': self.PHASE_CORRECTIVE, 'type': 'corrective' if not is_up else 'impulse', 'start_idx': int(si), 'end_idx': int(ei), 'start_price': round(float(self.close[si]), 2), 'end_price': round(float(self.close[ei]), 2), 'amplitude': round(float(amp), 2), 'amplitude_pct': round(float(rel), 2), }) return waves def _check_iron_rules(self, waves): """检查波浪铁律""" imp = [w for w in waves if w['phase'] == self.PHASE_IMPULSE and w['label'].isdigit()] checks = {} if len(imp) >= 5: w1, w2, w3, w4, w5 = imp[0], imp[1], imp[2], imp[3], imp[4] # 铁律1:浪4底 > 浪1顶(浪4不得进入浪1价格区域) rule1_ok = w4['end_price'] > w1['end_price'] checks['rule1_w4_above_w1'] = { 'rule': '浪4底 > 浪1顶', 'w4_bottom': w4['end_price'], 'w1_top': w1['end_price'], 'pass': rule1_ok, 'detail': f'浪4底部({w4["end_price"]:.2f}) {">" if rule1_ok else "≤"} 浪1顶部({w1["end_price"]:.2f})' } # 铁律2:浪3不是三个推进浪中最短的 a1 = abs(w1['amplitude']) a3 = abs(w3['amplitude']) a5 = abs(w5['amplitude']) rule2_ok = not (a3 < a1 and a3 < a5) checks['rule2_w3_not_shortest'] = { 'rule': '浪3非最短', 'amps': {'w1': round(a1, 2), 'w3': round(a3, 2), 'w5': round(a5, 2)}, 'pass': rule2_ok, 'detail': f'浪1:{a1:.1f} 浪3:{a3:.1f} 浪5:{a5:.1f} → {"浪3非最短" if rule2_ok else "浪3最短(违反!)"}' } # 浪2底 > 浪1起点(可选检查) rule3_ok = w2['end_price'] > w1['start_price'] checks['rule2_above_w1_start'] = { 'rule': '浪2底 > 浪1起点', 'pass': rule3_ok, 'detail': f'浪2底部({w2["end_price"]:.2f}) {">" if rule3_ok else "≤"} 浪1起点({w1["start_price"]:.2f})' } return checks def _calc_fib_levels(self, waves): imp = [w for w in waves if w['phase'] == self.PHASE_IMPULSE and w['label'].isdigit()] fib = {} if len(imp) >= 2: w1 = imp[0] w1a = w1['amplitude'] if w1a > 0: cor = [w for w in imp if w['type'] == 'corrective'] if cor: fib['wave3_target'] = round(cor[0]['end_price'] + w1a * 1.618, 2) fib['wave5_low'] = round(w1['start_price'], 2) fib['wave5_high'] = round(w1['end_price'] + w1a * 3.236, 2) last = imp[-1] ta = last['end_price'] - last['start_price'] if ta > 0: fib['correction_levels'] = { '0.236': round(last['end_price'] - ta * 0.236, 2), '0.382': round(last['end_price'] - ta * 0.382, 2), '0.500': round(last['end_price'] - ta * 0.5, 2), '0.618': round(last['end_price'] - ta * 0.618, 2), } return fib def _identify_signals(self, waves): """根据浪型阶段生成交易信号""" buy_signals = [] sell_signals = [] for i, w in enumerate(waves): ei = w['end_idx'] if ei >= self.n - 1: continue label = w['label'] ep = w['end_price'] phase = w.get('phase', self.PHASE_IMPULSE) if phase == self.PHASE_IMPULSE: if label == '2': if i >= 1: w1 = waves[i - 1] r = (w1['end_price'] - ep) / w1['amplitude'] if w1['amplitude'] > 0 else 0 if 0.15 <= r <= 0.85: buy_signals.append({ 'date': str(self.dates[ei])[:10], 'price': ep, 'index': int(ei), 'type': 'buy', 'label': '第2浪低点买入', 'wave': '2', 'phase': 'impulse', 'strength': 85 if 0.35 <= r <= 0.65 else 60, 'reason': f'回调{(r * 100):.1f}%,黄金分割回调带' }) elif label == '4': if i >= 3: w1 = waves[i - 3] strength = 75 if ep > w1['end_price'] else 50 reason = f'第4浪底部({"铁律满足" if ep > w1["end_price"] else "注意风险"})' buy_signals.append({ 'date': str(self.dates[ei])[:10], 'price': ep, 'index': int(ei), 'type': 'buy', 'label': '第4浪低点买入', 'wave': '4', 'phase': 'impulse', 'strength': strength, 'reason': reason }) elif label == '3': sell_signals.append({ 'date': str(self.dates[ei])[:10], 'price': ep, 'index': int(ei), 'type': 'sell', 'label': '第3浪高点卖出', 'wave': '3', 'phase': 'impulse', 'strength': 90, 'reason': '最具爆炸性上涨后' }) elif label == '5': strength = 95 if self._check_vol_div(waves, i) == 'bearish_div' else 80 sell_signals.append({ 'date': str(self.dates[ei])[:10], 'price': ep, 'index': int(ei), 'type': 'sell', 'label': '第5浪高点卖出', 'wave': '5', 'phase': 'impulse', 'strength': strength, 'reason': '第5浪末端' }) elif phase == self.PHASE_CORRECTIVE: # 调整区信号:C浪末端可买入(等待新一轮推动浪) if label == 'C': buy_signals.append({ 'date': str(self.dates[ei])[:10], 'price': ep, 'index': int(ei), 'type': 'buy', 'label': '调整区C浪末端', 'wave': 'C', 'phase': 'corrective', 'strength': 80, 'reason': '调整区C浪结束,等待新推动浪' }) elif label == 'B': sell_signals.append({ 'date': str(self.dates[ei])[:10], 'price': ep, 'index': int(ei), 'type': 'sell', 'label': '调整区B浪高点', 'wave': 'B', 'phase': 'corrective', 'strength': 85, 'reason': '调整区B浪反弹高点' }) return buy_signals, sell_signals def _identify_signals_simple(self, swings): """局部浪型信号(无明确5浪匹配时)""" buy_signals = [] sell_signals = [] for i in range(1, len(swings) - 1): idx = swings[i][0] if idx >= self.n - 1: continue if swings[i][1] == 'low': prev = swings[i - 1][2] pct = (prev - swings[i][2]) / prev * 100 if pct > 3: buy_signals.append({ 'date': str(self.dates[idx])[:10], 'price': round(float(swings[i][2]), 2), 'index': int(idx), 'type': 'buy', 'label': '波谷买入', 'wave': '?', 'phase': 'unknown', 'strength': min(85, int(pct * 4)), 'reason': f'回调{pct:.1f}%后的波谷' }) elif swings[i][1] == 'high': prev = swings[i - 1][2] pct = (swings[i][2] - prev) / prev * 100 if pct > 3: sell_signals.append({ 'date': str(self.dates[idx])[:10], 'price': round(float(swings[i][2]), 2), 'index': int(idx), 'type': 'sell', 'label': '波峰卖出', 'wave': '?', 'phase': 'unknown', 'strength': min(85, int(pct * 4)), 'reason': f'上涨{pct:.1f}%后的波峰' }) return buy_signals, sell_signals def _analyze_characteristics(self, waves, is_strict=True): chars = {} impulse_traits = { '1': '底部营造浪,量能温和放大', '2': '强势回调0.382/0.618', '3': '最具爆炸性,成交量最大,主升浪核心', '4': '与浪2交替,常为三角形/平台整理', '5': '末升浪,常伴随量价背离' } corrective_traits = { 'A': '5浪确认后的第一波下跌', 'B': '反弹修正(多头陷阱)', 'C': '杀伤力最强的下跌段', 'D': '下跌中继(三角形)', 'E': '末端下跌' } for w in waves: if w['phase'] == self.PHASE_IMPULSE: chars[w['label']] = {'description': impulse_traits.get(w['label'], ''), 'amplitude': w['amplitude_pct']} else: chars[w['label']] = {'description': corrective_traits.get(w['label'], ''), 'amplitude': w['amplitude_pct']} imp = [w for w in waves if w['label'].isdigit() and w['phase'] == self.PHASE_IMPULSE] if len(imp) >= 4: chars['iron_rule_1'] = f'浪4底({imp[3]["end_price"]:.2f}) {">" if imp[3]["end_price"] > imp[0]["end_price"] else "≤"} 浪1顶({imp[0]["end_price"]:.2f})' if len(imp) >= 3: amps = [abs(imp[i]['amplitude']) for i in range(min(3, len(imp)))] chars['iron_rule_2'] = f'浪3({"非最短" if len(amps) >= 3 and amps[1] != min(amps) else "最短!"}) 1:{amps[0]:.0f} 3:{amps[1]:.0f} 5:{amps[2] if len(amps) > 2 else "?":.0f}' chars['is_strict_match'] = is_strict return chars def _current_wave_position(self, waves, phase): """判断当前价格所处的浪型位置""" if not waves: return {'wave': 'unknown', 'position': 'unknown', 'label': '无法判断', 'phase': 'unknown'} lw = waves[-1] label = lw['label'] wphase = lw.get('phase', phase) is_corr = label.isalpha() # A/B/C/D/E 是调整区 prefix = '调整' if is_corr else '第' if lw['end_idx'] >= self.n - 3: phase_cn = '推动浪' if wphase == self.PHASE_IMPULSE else '调整区' return { 'wave': label, 'position': 'late', 'label': f'处于{prefix}{label}浪尾段({phase_cn}阶段)', 'phase': wphase } else: phase_cn = '推动浪' if wphase == self.PHASE_IMPULSE else '调整区' return { 'wave': label, 'position': 'after', 'label': f'{prefix}{label}浪之后({phase_cn}阶段)', 'phase': wphase } def _check_vol_div(self, waves, wi): w = waves[wi] s = max(0, w['start_idx']) e = min(self.n - 1, w['end_idx']) if e <= s or np.sum(self.volume[s:e + 1]) == 0: return 'unknown' mid = (s + e) // 2 vf = np.mean(self.volume[s:mid + 1]) if mid > s else np.mean(self.volume[s:e + 1]) vs = np.mean(self.volume[mid + 1:e + 1]) if mid < e else vf return 'bearish_div' if self.close[e] > self.close[s] and vs < vf * 0.8 else 'normal' def get_chart_annotations(self): result = self.identify_waves() annotations = [] shapes = [] dates_iso = [pd.Timestamp(d).isoformat() if not isinstance(d, str) else d for d in self.dates] # 推动浪和调整区使用不同配色 impulse_colors = {'1': '#FFD700', '2': '#87CEEB', '3': '#FF6B6B', '4': '#87CEEB', '5': '#FFD700'} corrective_colors = {'A': '#69f0ae', 'B': '#DDA0DD', 'C': '#FF8A65', 'D': '#B0BEC5', 'E': '#90CAF9'} for w in result['waves']: mid = (w['start_idx'] + w['end_idx']) // 2 if mid < self.n: if w.get('phase') == self.PHASE_CORRECTIVE: c = corrective_colors.get(w['label'], '#B0BEC5') prefix = '调整' else: c = impulse_colors.get(w['label'], '#FFFFFF') prefix = '第' y = self.high[max(0, mid - 2):min(self.n, mid + 3)].max() * 1.025 text = f"{prefix}{w['label']}浪" annotations.append({ 'x': dates_iso[mid], 'y': y, 'text': text, 'showarrow': True, 'arrowhead': 0, 'arrowcolor': c, 'font': {'size': 13, 'color': c, 'family': 'Microsoft YaHei, sans-serif'}, 'bordercolor': c, 'borderwidth': 1, 'borderpad': 2 }) # 浪型分界线 drawn = set() for w in result['waves']: si = w['start_idx'] if si not in drawn and 0 < si < self.n - 1: # 推动浪与调整区之间的分界线用不同样式 phase = w.get('phase', self.PHASE_IMPULSE) if phase == self.PHASE_CORRECTIVE: dash = 'dash' color = 'rgba(105, 240, 174, 0.25)' else: dash = 'dot' color = 'rgba(255, 255, 255, 0.12)' shapes.append({ 'type': 'line', 'x0': dates_iso[si], 'x1': dates_iso[si], 'y0': 0, 'y1': 1, 'yref': 'paper', 'line': {'color': color, 'width': 1, 'dash': dash} }) drawn.add(si) # 买卖信号 for sig in result.get('buy_signals', []): idx = sig['index'] if idx < self.n: y = self.low[max(0, idx - 1):min(self.n, idx + 2)].min() * 0.975 annotations.append({ 'x': dates_iso[idx], 'y': y, 'text': '▲买', 'showarrow': True, 'arrowhead': 2, 'arrowcolor': '#EF5350', 'font': {'size': 12, 'color': '#EF5350', 'family': 'Microsoft YaHei, sans-serif'}, 'bgcolor': 'rgba(239,83,80,0.15)' }) for sig in result.get('sell_signals', []): idx = sig['index'] if idx < self.n: y = self.high[max(0, idx - 1):min(self.n, idx + 2)].max() * 1.025 annotations.append({ 'x': dates_iso[idx], 'y': y, 'text': '▼卖', 'showarrow': True, 'arrowhead': 2, 'arrowcolor': '#66bb6a', 'font': {'size': 12, 'color': '#66bb6a', 'family': 'Microsoft YaHei, sans-serif'}, 'bgcolor': 'rgba(102,187,106,0.15)' }) # 斐波那契水平线 fib = result.get('fib_levels', {}) for ratio, price in fib.get('correction_levels', {}).items(): if self.low.min() <= price <= self.high.max(): shapes.append({ 'type': 'line', 'x0': dates_iso[0], 'x1': dates_iso[-1], 'y0': price, 'y1': price, 'line': {'color': 'rgba(255,165,0,0.3)', 'width': 1, 'dash': 'dashdot'} }) annotations.append({ 'x': dates_iso[-1], 'y': price, 'text': f'F{ratio}={price}', 'showarrow': False, 'font': {'size': 9, 'color': 'rgba(255,165,0,0.7)'}, 'xanchor': 'left' }) return {'annotations': annotations, 'shapes': shapes, 'analysis': result} def _empty_result(self): return { 'waves': [], 'impulse_waves': [], 'corrective_waves': [], 'phase': 'unknown', 'swing_points': [], 'fib_levels': {}, 'buy_signals': [], 'sell_signals': [], 'characteristics': {}, 'current_wave': {'wave': 'unknown', 'position': 'unknown', 'label': '数据不足', 'phase': 'unknown'}, 'wave_count': 0, 'is_complete_5wave': False, 'iron_rule_checks': {} } def analyze_wave_patterns(df, order=5): return WaveAnalyzer(df).get_chart_annotations()