From 6139af98c33a950959fdfe082060273b878e9e4f Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Sat, 27 Sep 2025 01:14:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0ema52=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChanLun.py | 87 +++++++++++----- TF_DF.py | 26 +++-- strategies/ChanLun_BTC.py | 107 +++++++++++--------- web/app.py | 54 ++++++++-- web/templates/index.html | 203 +++++++++++++++++++++++++++++++++++++- 5 files changed, 389 insertions(+), 88 deletions(-) diff --git a/ChanLun.py b/ChanLun.py index 6ac7686..30f832e 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -21,29 +21,61 @@ from ChanMACD import ChanMACD from TF_DF import TF_DF class ChanLun(): - time1 = 1 - time3 = 3 - time5 = 5 - time10 = 10 - time15 = 15 - time30 = 30 - time60 = 60 - time2h = 120 - time4h = 240 - time6h = 360 - time8h = 480 - time12h = 720 - time1d = 1440 - timeframes = [time1, time3, time5, time10, time15, time30, time60] - tf_df_dict = {} - def init_data(self, dataframe, ticker_indicator): - for timeframe in self.timeframes: - self.tf_df_dict[timeframe] = TF_DF(timeframe, dataframe, ticker_indicator) - def cal_bsp(self, dataframe, ticker_indicator): - # 初始化多周期数据 - self.init_data(dataframe, ticker_indicator) - - + def __init__(self): + self.time3m = 3 + self.time5m = 5 + self.time10m = 10 + self.time15m = 15 + self.time30m = 30 + self.time_m_intervals = [3, 5, 10, 15, 30] + self.time_m_symbols = ['3m', '5m', '10m', '15m', '30m'] + self.time2h = 2*60 + self.time4h = 4*60 + self.time6h = 6*60 + self.time8h = 8*60 + self.time12h = 12*60 + self.time16h = 16*60 + self.time_h_intervals = [2*60, 4*60, 6*60, 8*60, 12*60, 16*60] + self.time_h_symbols = ['2h', '4h', '6h', '8h', '12h', '16h'] + self.time2d = 2*24*60 + self.time3d = 3*24*60 + self.time1w = 7*24*60 + self.time2w = 14*24*60 + self.time_d_intervals = [2*24*60, 3*24*60, 7*24*60, 14*24*60] + self.time_d_symbols = ['2d', '3d', '1w', '2w'] + self.time2M = 2*30*24*60 + self.time3M = 3*30*24*60 + self.time6M = 6*30*24*60 + self.time1y = 12*30*24*60 + self.time_M_intervals = [2*30*24*60, 3*30*24*60, 6*30*24*60, 12*30*24*60] + self.time_M_symbols = ['2M', '3M', '6M', '1y'] + self.time_symbols = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d', '1w', '2w', '1M', '3M', '6M', '1y'] + self.tf_df_dict = {} + self.ema_symbols = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d', '1w', '2w'] + def init_data(self, dataframe, intervals, timeframes): + for index in range(0, len(intervals)): + timeframe = timeframes[index] + interval = intervals[index] + self.tf_df_dict[timeframe] = TF_DF(dataframe, interval, timeframe) + def init_dataframes(self, dataframe_m, dataframe_h, dataframe_d, dataframe_M): + self.tf_df_dict['1m'] = TF_DF(dataframe_m, 1, '1m') + self.init_data(dataframe_m, self.time_m_intervals, self.time_m_symbols) + self.tf_df_dict['1h'] = TF_DF(dataframe_h, 1, '1h') + self.init_data(dataframe_h, self.time_h_intervals, self.time_h_symbols) + self.tf_df_dict['1d'] = TF_DF(dataframe_d, 1, '1d') + self.init_data(dataframe_d, self.time_d_intervals, self.time_d_symbols) + self.tf_df_dict['1M'] = TF_DF(dataframe_M, 1, '1M') + self.init_data(dataframe_M, self.time_M_intervals, self.time_M_symbols) + def get_ema52_dict(self): + if len(self.tf_df_dict) > 0: + return {key: self.tf_df_dict[key].get_ema52() for key in self.ema_symbols} + return None + def get_ema24_dict(self): + if len(self.tf_df_dict) > 0: + return {key: self.tf_df_dict[key].get_ema24() for key in self.ema_symbols} + return None + def cal_bsp(self): + return def check_fx(self, klc): if klc.pre and klc.next: if klc.high > klc.pre.high and klc.high > klc.next.high: @@ -444,13 +476,14 @@ class ChanLun(): hist = getattr(klc, 'macdhist', 0) if getattr(klc, 'macdhist', None) is not None else 0 rsi = getattr(klc, 'rsi', None) trend = Chan_PRICE_TREND.UNKNOWN + score = 0 try: # 有效性 price_valid = price is not None and price != 0 ema24_valid = ema24 is not None and ema24 != 0 ema52_valid = ema52 is not None and ema52 != 0 # 多因子投票 - score = 0 + # 1) 均线结构 + 价位 if ema24_valid and ema52_valid: score += 1 if ema24 > ema52 else -1 @@ -542,9 +575,9 @@ class ChanLun(): setattr(klc, 'trend', trend) last_trend = trend price_diff = klc.close - klc.pre.close if klc.pre else 0 - if klc.index > len(klc_list) - 10: - print(klc.start_time, klc.end_time, klc.close, klc.ema24, klc.ema52, klc.macd, klc.signal, klc.macdhist, klc.trend, price_diff) - #print(klc.start_time, klc.end_time, klc.trend, price_diff) + #if klc.index > len(klc_list) - 10: + #print(klc.start_time, klc.end_time, klc.close, klc.ema24, klc.ema52, klc.macd, klc.signal, klc.macdhist, klc.trend, price_diff, score) + #print(klc.start_time, klc.end_time, klc.trend, price_diff, score) return klc_list def cal_bi_list(self, klc_list): bi_list = [] diff --git a/TF_DF.py b/TF_DF.py index 97229d6..a195ade 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -20,11 +20,11 @@ import numpy as np from ChanMACD import ChanMACD class TF_DF(): - def __init__(self, timeframe, df, ticker_indicator): + def __init__(self, df, interval, timeframe): self.timeframe = timeframe - self.dataframe = resample_to_interval(df, ticker_indicator*timeframe) + self.interval = interval + self.dataframe = resample_to_interval(df, interval) self.dataframe = self.add_indicators(self.dataframe) - self.ticker_indicator = ticker_indicator self.klu_list = [] self.klc_list = [] self.bi_list = [] @@ -40,6 +40,22 @@ class TF_DF(): self.zs_list = self.cal_zs_list(self.bi_list, self.seg_list) self.chanmacd = ChanMACD(self.klu_list) self.klu_list = self.chanmacd.cal_macd_state() + def get_ema52(self): + if self.klu_list: + ema52_value = self.klu_list[-1].ema52 + # 处理NaN值 + if pd.isna(ema52_value) or ema52_value is None: + return None + return float(ema52_value) + return None + def get_ema24(self): + if self.klu_list: + ema24_value = self.klu_list[-1].ema24 + # 处理NaN值 + if pd.isna(ema24_value) or ema24_value is None: + return None + return float(ema24_value) + return None def add_indicators(self, df): fast = 12 slow = 26 @@ -223,7 +239,7 @@ class TF_DF(): setattr(klc, 'trend', trend) last_trend = trend price_diff = klc.close - klc.pre.close if klc.pre else 0 - print(klc.start_time, klc.end_time, klc.close, klc.ema24, klc.ema52, klc.macd, klc.signal, klc.macdhist, klc.trend, price_diff) + #print(klc.start_time, klc.end_time, klc.close, klc.ema24, klc.ema52, klc.macd, klc.signal, klc.macdhist, klc.trend, price_diff) #print(klc.start_time, klc.end_time, klc.trend, price_diff) return klc_list def cal_kl_data(self, dataframe:DataFrame): @@ -627,7 +643,6 @@ class TF_DF(): bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) #klc.set_klc_fx_type(Chan_KLC_FX.TOP3) - klc.set_last_top_klu(last_top) #print(klc.start_time, klc.fx, "二类卖点Sell 1") else: # A new top found @@ -752,7 +767,6 @@ class TF_DF(): bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3) - klc.set_last_bottom_klc(last_bottom) #print(last_bottom.start_time, last_bottom.end_time, "--------------------------------1") #print(klc.start_time, klc.fx, "二类买点Buy 1") else: diff --git a/strategies/ChanLun_BTC.py b/strategies/ChanLun_BTC.py index 02ccea1..b2e78f5 100644 --- a/strategies/ChanLun_BTC.py +++ b/strategies/ChanLun_BTC.py @@ -23,6 +23,7 @@ logger = logging.getLogger(__name__) # freqtrade trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies # freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies --timerange=20250901- # freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json -t 1m --pairs BTC/USDT:USDT --timerange=20250405- +# freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json -t 1m 1h 1d 1M --pairs BTC/USDT --timerange=20170101- # freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_30.json -e 200 --timerange=20250201-20250901 # freqtrade edge -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 # freqtrade plot-dataframe -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 @@ -76,36 +77,56 @@ class ChanLun_BTC(IStrategy): # 关闭分批止盈/仓位调整 position_adjustment_enable = False - startup_candle_count = 2880 - time3 = 3 - time5 = 5 - time15 = 15 - time30 = 30 - time60 = 60 - time2h = 120 - time4h = 240 - time1d = 1440 + startup_candle_count = 1600 + time3m = 3 + time5m = 5 + time10m = 10 + time15m = 15 + time30m = 30 + time_m = [3, 5, 10, 15, 30] + + time1h = 60 + time2h = 2 + time4h = 4 + time6h = 6 + time8h = 8 + time12h = 12 + time16h = 16 + time_h = [2, 4, 6, 8, 12, 16] + + time2d = 2 + time3d = 3 + time1w = 7 + time_d = [2, 3, 7] + + time2M = 2 + time3M = 3 + time_M = [2, 3] + last_time = datetime.now() chan = ChanLun() last_order = None last_trade = None - + pair = 'BTC/USDT:USDT' + def informative_pairs(self): + timeframes = ['1h', '1d', '1M'] + informative_pairs = [(self.pair, tf) for tf in timeframes] + return informative_pairs def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - - dataframe = self.add_indicators(dataframe) - # 仅保留15m(用于BSP)与60m(用于ATR过滤/止损)两个重采样 - dataframe_15 = resample_to_interval(dataframe, self.get_ticker_indicator() * 15) - dataframe_60 = resample_to_interval(dataframe, self.get_ticker_indicator() * 60) - # 计算多周期BSP(以15m为基准),并合并到15m数据上 - # 先给重采样帧补指标 - dataframe_15 = self.add_indicators(dataframe_15) - dataframe_60 = self.add_indicators(dataframe_60) - # 计算15m BSP - bsp_15 = self.chan.cal_bsp(dataframe, self.get_ticker_indicator()) - # 合并15m与60m到主DF,生成 resample_*_* 列 - dataframe = resampled_merge(dataframe, dataframe_15) - dataframe = resampled_merge(dataframe, dataframe_60) + self.init_dataframes(dataframe) return dataframe + def init_dataframes(self, dataframe_m): + dataframe_1h = self.dp.get_pair_dataframe(pair=self.pair, timeframe='1h') + dataframe_1d = self.dp.get_pair_dataframe(pair=self.pair, timeframe='1d') + dataframe_1M = self.dp.get_pair_dataframe(pair=self.pair, timeframe='1M') + self.chan.init_dataframes(dataframe_m, dataframe_1h, dataframe_1d, dataframe_1M) + self.print_all_ema52() + def print_all_ema52(self): + for key, value in self.chan.get_ema52_dict().items(): + print(key, value) + def print_all_ema24(self): + for key, value in self.chan.get_ema24_dict().items(): + print(key, value) def add_indicators(self, df): fast = 12 slow = 26 @@ -212,9 +233,9 @@ class ChanLun_BTC(IStrategy): return -0.05 dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) last_candle = dataframe.iloc[-1].squeeze() - ema52_str = 'resample_{}_ema52'.format(self.get_ticker_indicator()*self.time15) + ema52_str = 'resample_{}_ema52'.format(self.time15m) ema52_val = float(last_candle.get(ema52_str, 0) or 0) - close_str = 'resample_{}_close'.format(self.get_ticker_indicator()*self.time15) + close_str = 'resample_{}_close'.format(self.time15m) close_val = float(last_candle.get(close_str, 0) or 0) if close_val < ema52_val: return -0.01 @@ -240,7 +261,7 @@ class ChanLun_BTC(IStrategy): if dataframe is None or len(dataframe) == 0: return False last = dataframe.iloc[-1] - atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator()*self.time60) + atr_str = 'resample_{}_atr'.format(self.time1h) atr_val = float(last.get(atr_str, 0) or 0) if atr_val < 0.001: #logger.info(f"ATR过滤:atr={atr_val:.2f} < 100, 拒绝进场 {pair}") @@ -263,7 +284,7 @@ class ChanLun_BTC(IStrategy): # Obtain pair dataframe (just to show how to access it) dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) last_candle = dataframe.iloc[-1].squeeze() - atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator()*self.time15) + atr_str = 'resample_{}_atr'.format(elf.time15) # 保存开仓时的ATR值用于止损计算 if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side): entry_atr = last_candle[atr_str] * 4 @@ -271,13 +292,13 @@ class ChanLun_BTC(IStrategy): #logger.info(f"保存开仓时ATR值: {entry_atr}") return None def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - shift15 = self.time15 - shift60 = self.time60 - bsp_col = 'resample_{}_bsp_mtf'.format(self.get_ticker_indicator()*shift15) - score_col = 'resample_{}_mtf_score'.format(self.get_ticker_indicator()*shift15) - macdh_col = 'resample_{}_macdhist'.format(self.get_ticker_indicator()*shift15) - c60_col = 'resample_{}_close'.format(self.get_ticker_indicator()*shift60) - e60_col = 'resample_{}_ema52'.format(self.get_ticker_indicator()*shift60) + shift15 = self.time15m + shift60 = self.time1h + bsp_col = 'resample_{}_bsp_mtf'.format(shift15) + score_col = 'resample_{}_mtf_score'.format(shift15) + macdh_col = 'resample_{}_macdhist'.format(shift15) + c60_col = 'resample_{}_close'.format(shift60) + e60_col = 'resample_{}_ema52'.format(shift60) # 强化过滤:15m BSP + 分数阈值 + 60m 趋势同向 + 15m MACD柱同向 if all(col in dataframe.columns for col in [bsp_col, score_col, macdh_col, c60_col, e60_col]): dataframe.loc[ @@ -298,12 +319,12 @@ class ChanLun_BTC(IStrategy): ['enter_short', 'enter_tag']] = (1, 'short_bsp15_v2') return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - shift15 = self.time15 - shift60 = self.time60 - bsp_col = 'resample_{}_bsp_mtf'.format(self.get_ticker_indicator()*shift15) - score_col = 'resample_{}_mtf_score'.format(self.get_ticker_indicator()*shift15) - c60_col = 'resample_{}_close'.format(self.get_ticker_indicator()*shift60) - e60_col = 'resample_{}_ema52'.format(self.get_ticker_indicator()*shift60) + shift15 = self.time15m + shift60 = self.time1h + bsp_col = 'resample_{}_bsp_mtf'.format(shift15) + score_col = 'resample_{}_mtf_score'.format(shift15) + c60_col = 'resample_{}_close'.format(shift60) + e60_col = 'resample_{}_ema52'.format(shift60) # 反向强信号或60m趋势反向时平仓 if all(col in dataframe.columns for col in [bsp_col, score_col, c60_col, e60_col]): dataframe.loc[ @@ -323,6 +344,4 @@ class ChanLun_BTC(IStrategy): proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, **kwargs) -> float: return self.lev - - def get_ticker_indicator(self): - return int(self.timeframe[:-1]) \ No newline at end of file + \ No newline at end of file diff --git a/web/app.py b/web/app.py index 231f9a2..92ac231 100644 --- a/web/app.py +++ b/web/app.py @@ -346,10 +346,43 @@ def calculate_macd(df): 'histogram': histogram.tolist() } -def analyze_chan(df): +def analyze_chan(df, symbol=None, timeframe=None): """进行缠论分析""" chan = ChanLun() + # 初始化多时间周期数据以获取EMA52 + ema52_dict = None + if symbol and timeframe: + try: + # 获取不同时间周期的数据用于初始化 + df_1h = get_kl_data(symbol, '1h', limit=800) if timeframe != '1h' else df + df_1d = get_kl_data(symbol, '1d', limit=800) if timeframe != '1d' else df + df_1M = get_kl_data(symbol, '1M', limit=800) if timeframe != '1M' else df + + # 添加指标 + if df_1h is not None and len(df_1h) > 0: + df_1h = add_indicators(df_1h) + if df_1d is not None and len(df_1d) > 0: + df_1d = add_indicators(df_1d) + if df_1M is not None and len(df_1M) > 0: + df_1M = add_indicators(df_1M) + + # 初始化多时间周期数据 + chan.init_dataframes(df, df_1h, df_1d, df_1M) + + # 获取EMA52数据 + ema52_dict = chan.get_ema52_dict() + # 处理NaN值 + if ema52_dict: + for key, value in ema52_dict.items(): + if pd.isna(value) or value is None: + ema52_dict[key] = None + else: + ema52_dict[key] = float(value) + except Exception as e: + print(f"获取多时间周期EMA52数据失败: {e}") + ema52_dict = None + # 获取分析结果 klc_list = chan.get_klc_list(df) bi_list = chan.cal_bi_list(klc_list) @@ -546,7 +579,8 @@ def analyze_chan(df): 'trade_points': buy_sell_points, 'klc_fx_info': klc_fx_info, # KLC分型信息 'klu_fx_info': klu_fx_info, # 添加KLU分型信息 - 'chan_macd': chan_macd_data # 添加ChanMACD分析数据 + 'chan_macd': chan_macd_data, # 添加ChanMACD分析数据 + 'ema52_dict': ema52_dict # 添加多时间周期EMA52数据 } def generate_replay_data(df, client_tz, symbol=None, element_timeframe=None, start_time=None, end_time=None): @@ -572,7 +606,7 @@ def generate_replay_data(df, client_tz, symbol=None, element_timeframe=None, sta current_df = add_indicators(current_df) # 进行缠论分析 - analysis_result = analyze_chan(current_df) + analysis_result = analyze_chan(current_df, symbol, timeframe) # 计算MACD macd_data = calculate_macd(current_df) @@ -590,7 +624,7 @@ def generate_replay_data(df, client_tz, symbol=None, element_timeframe=None, sta if len(element_current_df) > 0: # 重新对当前时间范围的次周期数据进行缠论分析 # 这样可以确保数据的准确性,避免时间筛选的复杂性 - element_current_analysis = analyze_chan(element_current_df) + element_current_analysis = analyze_chan(element_current_df, symbol, element_timeframe) # 直接使用分析结果,无需复杂的时间筛选 filtered_bi_list = element_current_analysis['bi_list'] @@ -1614,7 +1648,7 @@ def analyze(): df = add_indicators(df) # 进行缠论分析 - analysis_result = analyze_chan(df) + analysis_result = analyze_chan(df, symbol, timeframe) # 计算MACD macd_data = calculate_macd(df) @@ -1717,7 +1751,9 @@ def analyze(): 'fx_confirmed': bool(point['fx_confirmed']) # 分型是否确认 } for point in analysis_result['klu_fx_info']], # 添加ChanMACD分析数据 - 'chan_macd': serialize_chan_macd_data(analysis_result.get('chan_macd', {}), client_tz) + 'chan_macd': serialize_chan_macd_data(analysis_result.get('chan_macd', {}), client_tz), + # 添加多时间周期EMA52数据 + 'ema52_dict': analysis_result.get('ema52_dict', {}) }) # 如果生成了回放数据,添加到返回结果中 @@ -1734,7 +1770,7 @@ def analyze(): element_df = add_indicators(element_df) # 对小周期数据进行缠论分析 - element_analysis = analyze_chan(element_df) + element_analysis = analyze_chan(element_df, symbol, element_timeframe) # 计算小周期MACD数据 element_macd_data = calculate_macd(element_df) @@ -1936,7 +1972,7 @@ def test_element_data(): # 分析次周期数据 element_df = add_indicators(element_df) - element_analysis = analyze_chan(element_df) + element_analysis = analyze_chan(element_df, symbol, element_timeframe) return jsonify({ 'main_data_count': len(main_df), @@ -2123,7 +2159,7 @@ def filter_stocks(): continue # 进行缠论分析 - analysis_result = analyze_chan(df) + analysis_result = analyze_chan(df, symbol, timeframe) if not analysis_result or 'klc_fx_info' not in analysis_result: continue diff --git a/web/templates/index.html b/web/templates/index.html index f884264..d5764d8 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1788,6 +1788,7 @@ elementBollingerSeries: [], maSeries: [], // 添加均线系列 bbSeries: [], // 添加布林带系列 + ema52Series: [], // 添加EMA52系列数组 chanMacdLineSeries: null, // ChanMACD线 chanMacdSignalSeries: null, // ChanMACD信号线 chanMacdHistSeries: null, // ChanMACD柱状图 @@ -2426,7 +2427,8 @@ mainBollingerSeries: [], elementBollingerSeries: [], maSeries: [], // 添加均线系列数组 - bbSeries: [] // 添加布林带系列数组 + bbSeries: [], // 添加布林带系列数组 + ema52Series: [] // 添加EMA52系列数组 }, state: { isInitialized: false, @@ -5680,6 +5682,11 @@ displayTradePoints(); } + // 更新EMA52显示 + if (currentData) { + updateEMA52Display(currentData); + } + console.log('图表初始化完成'); } catch (e) { console.error('图表初始化错误:', e); @@ -5912,6 +5919,9 @@ // 重新显示笔、线段和中枢等图形 redrawFractalElements(); + // 更新EMA52显示 + updateEMA52Display(currentData); + // 恢复之前的可视范围 - 优先使用visibleRange以确保时间轴对齐 if (tvWidget.mainChart) { if (tvWidget.state.visibleRange) { @@ -7121,6 +7131,8 @@ // 先销毁现有图表实例 if (tvWidget.mainChart) { try { + // 清理EMA52系列 + clearEMA52Series(); // 销毁主图表及其关联的线系列 tvWidget.mainChart = null; tvWidget.volumeChart = null; @@ -7147,7 +7159,8 @@ mainBollingerSeries: [], elementBollingerSeries: [], maSeries: [], // 添加均线系列 - bbSeries: [] // 添加布林带系列 + bbSeries: [], // 添加布林带系列 + ema52Series: [] // 添加EMA52系列数组 }; } catch (e) { console.error('销毁图表错误:', e); @@ -7576,6 +7589,12 @@ // 更新表格数据 updateTables(data); + // 只有在图表已初始化且调用了updateTradingViewData时才不需要重复调用EMA52显示 + // 如果图表未初始化,调用了initTradingView,那么EMA52显示已经在initTradingView中处理了 + // 但为了确保在所有情况下都能正确显示,这里统一调用一次 + if (currentData && currentData.ema52_dict) { + updateEMA52Display(currentData); + } } @@ -8653,6 +8672,186 @@ let bollingerBands = []; // 存储所有布林带配置 let bbIdCounter = 0; // 布林带ID计数器 + // 清理EMA52系列 + function clearEMA52Series() { + // 清理所有EMA52相关系列(包括线系列和标记系列) + if (tvWidget && tvWidget.series && tvWidget.series.ema52Series) { + tvWidget.series.ema52Series.forEach(series => { + try { + if (tvWidget.mainChart) { + tvWidget.mainChart.removeSeries(series); + } + } catch (e) { + console.warn('移除EMA52系列失败:', e); + } + }); + tvWidget.series.ema52Series = []; + } + + console.log('✅ EMA52系列已清理'); + } + + // 存储上次的EMA52数据,用于比较 + let lastEMA52Data = null; + + // 更新EMA52显示 + function updateEMA52Display(data) { + console.log('更新EMA52显示', data.ema52_dict); + + // 检查是否有EMA52数据 + if (!data.ema52_dict || Object.keys(data.ema52_dict).length === 0) { + // 即使没有数据也要清理之前的系列 + clearEMA52Series(); + lastEMA52Data = null; + return; + } + + // 检查数据是否与上次相同,如果相同则跳过更新 + const currentDataStr = JSON.stringify(data.ema52_dict); + if (lastEMA52Data === currentDataStr) { + console.log('EMA52数据未变化,跳过更新'); + return; + } + + // 清除之前的EMA52线系列 + clearEMA52Series(); + + // 保存当前数据 + lastEMA52Data = currentDataStr; + + // 定义时间周期的显示顺序和颜色 + const timeframeColors = { + '1m': '#FF0000', // 红色 + '3m': '#FF6600', // 橙红色 + '5m': '#FF9900', // 橙色 + '15m': '#FFCC00', // 黄色 + '30m': '#99FF00', // 黄绿色 + '1h': '#00FF00', // 绿色 + '2h': '#00FF99', // 青绿色 + '4h': '#00FFFF', // 青色 + '6h': '#0099FF', // 蓝青色 + '8h': '#0066FF', // 蓝色 + '12h': '#3300FF', // 蓝紫色 + '16h': '#6600FF', // 紫色 + '1d': '#9900FF', // 紫红色 + '2d': '#CC00FF', // 品红色 + '3d': '#FF00CC', // 粉红色 + '1w': '#FF0099', // 玫瑰色 + '2w': '#FF3366' // 深粉色 + }; + + // 按照预定义顺序排列时间周期 + const orderedTimeframes = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d', '1w', '2w']; + + // 获取主图表容器 + const chartContainer = document.getElementById('tradingview_chart'); + if (!chartContainer || !tvWidget.mainChart) { + return; + } + + orderedTimeframes.forEach(timeframe => { + if (data.ema52_dict[timeframe] !== undefined && data.ema52_dict[timeframe] !== null) { + const value = data.ema52_dict[timeframe]; + const color = timeframeColors[timeframe] || '#800080'; + + // 添加虚线到主图表 + if (tvWidget.mainChart) { + try { + // 使用LightweightCharts的addLineSeries创建虚线 + const lineSeries = tvWidget.mainChart.addLineSeries({ + color: color, + lineWidth: 1, + lineStyle: 2, // 虚线样式 + title: `EMA52-${timeframe}`, + lastValueVisible: false, // 不在价格标尺显示数值 + priceLineVisible: false, // 不显示默认价格线 + crosshairMarkerVisible: false, + priceFormat: { + type: 'price', + precision: 2, + minMove: 0.01, + }, + priceScaleId: 'right', + }); + + // 创建横线数据(使用K线数据的时间范围) + if (data.kline_data && data.kline_data.length > 0) { + const firstKline = data.kline_data[0]; + const lastKline = data.kline_data[data.kline_data.length - 1]; + + const startTime = Math.floor(new Date(firstKline.date).getTime() / 1000); + const endTime = Math.floor(new Date(lastKline.date).getTime() / 1000); + + const lineData = [ + { time: startTime, value: value }, + { time: endTime, value: value } + ]; + lineSeries.setData(lineData); + + // 使用标记在K线右侧显示文字 + if (data.kline_data && data.kline_data.length > 0) { + const lastKline = data.kline_data[data.kline_data.length - 1]; + const lastTime = Math.floor(new Date(lastKline.date).getTime() / 1000); + + // 计算时间间隔(用于右偏移) + let timeInterval = 60; // 默认1分钟 + if (data.kline_data.length > 1) { + const secondLastKline = data.kline_data[data.kline_data.length - 2]; + const secondLastTime = Math.floor(new Date(secondLastKline.date).getTime() / 1000); + timeInterval = lastTime - secondLastTime; + } + + // 创建右偏移的时间点(在最后一根K线右边) + const rightOffsetTime = lastTime + timeInterval * 0.3; + + // 创建一个新的线系列用于显示文字标记 + const markerSeries = tvWidget.mainChart.addLineSeries({ + color: 'transparent', + lineWidth: 0, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + priceScaleId: 'right', + }); + + // 添加一个透明的数据点在右偏移位置 + markerSeries.setData([{ + time: rightOffsetTime, + value: value + }]); + + // 在右偏移位置添加文字标记 + markerSeries.setMarkers([{ + time: rightOffsetTime, + position: 'inBar', + color: color, + shape: 'square', + text: `${timeframe} ${value.toFixed(2)}`, + size: 1, + }]); + + // 保存标记系列引用 + if (!tvWidget.series.ema52Series) { + tvWidget.series.ema52Series = []; + } + tvWidget.series.ema52Series.push(markerSeries); + } + } + + // 保存系列引用以便后续清理 + if (!tvWidget.series.ema52Series) { + tvWidget.series.ema52Series = []; + } + tvWidget.series.ema52Series.push(lineSeries); + + } catch (e) { + console.warn('添加EMA52虚线失败:', timeframe, e); + } + } + } + }); + } + // 获取随机颜色 function getRandomColor() { const colors = ['#2962FF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',