diff --git a/.DS_Store b/.DS_Store index 4b9a6fc..8504d65 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/ChanKLC.py b/ChanKLC.py index 9d4b490..d792f13 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -160,7 +160,7 @@ class ChanKLC(): klu_list.append(klc3.klus) gap = klc3.end_klu.index - klc1.start_klu.index + 1 if gap < 4: - pass + pass return gap def get_feature_data(self): features = dict() @@ -1162,269 +1162,44 @@ class ChanKLC(): return features def cal_fx_strength(self, klc_offset=2): - """ - 用self.pre和self.next实现分型强弱判断 - - 核心缠论原理: - - 强分型:出现在笔的末端,能够终结当前笔,标志着趋势转折 - - 弱分型:出现在笔的中间,是中继性质,笔还会继续延伸 - - 返回值: - 3: 极强分型(笔终结+强确认) - 2: 强分型(笔终结) - 1: 偏强分型(可能终结笔) - 0: 中性分型 - -1: 偏弱分型(中继特征明显) - -2: 弱分型(明显中继) - -3: 极弱分型(无效分型) - """ - # 检查是否为分型,且有前后K线数据 - if self.fx == Chan_FX_TYPE.UNKNOWN: + strength = 0 + if self.fx == Chan_FX_TYPE.UNKNOWN or not self.pre or not self.next: return 0 - if not self.pre or not self.next: - return 100 - # === 核心判断:分型在笔中的位置 === - - # 1. 检查这个分型是否能够终结当前笔 - is_bi_end = self._check_if_bi_ending_fx(klc_offset) - - # 2. 检查分型的后续走势确认 - post_fx_confirmation = self._check_post_fx_confirmation() - - # 3. 检查分型的标准性和强度 - fx_quality = self._check_fx_quality() - - # === 综合评分 === - base_score = 0 - - # 笔位置是最重要的判断标准 - if is_bi_end == 2: # 强烈确认笔终结 - base_score = 2 - elif is_bi_end == 1: # 可能笔终结 - base_score = 1 - elif is_bi_end == -1: # 明显中继 - base_score = -2 - elif is_bi_end == -2: # 强烈中继特征 - base_score = -3 - else: # 不确定 - base_score = 0 - - # 后续确认调整 - base_score += post_fx_confirmation - - # 分型质量调整 - base_score += fx_quality - #print(self.start_time, base_score, is_bi_end, post_fx_confirmation, fx_quality) - # 2025-06-07 08:15:00 1.5 0 0.8 0.7 - # 限制在-3到3范围内 - return base_score - - def _check_if_bi_ending_fx(self, klc_offset): - """ - 检查分型是否为笔终结分型 - 返回值: - 2: 强烈确认笔终结 - 1: 可能笔终结 - 0: 不确定 - -1: 明显中继 - -2: 强烈中继特征 - """ - # 检查是否有足够的后续数据来判断 - if not self.next or not hasattr(self.next, 'next'): - return 0 - - # 获取分型后的几根K线数据 - subsequent_klcs = [] - temp = self.next - for i in range(klc_offset): - if temp: - subsequent_klcs.append(temp) - temp = temp.next if hasattr(temp, 'next') else None + else: + if self.pre and self.next: + klc1 = self.pre + klc2 = self + klc3 = self.next + if self.bi: + if self.bi.dir == Chan_BI_DIR.UP and self.fx == Chan_FX_TYPE.BOTTOM: + return 0 + if self.bi.dir == Chan_BI_DIR.DOWN and self.fx == Chan_FX_TYPE.TOP: + return 0 + if self.bi.dir == Chan_BI_DIR.UP: + if self.klc_fx_type == Chan_KLC_FX.TOP1: + strength += self.check_bi_end(self.bi) + elif self.klc_fx_type == Chan_KLC_FX.TOP2: + strength += self.check_bi_end(self.bi) + else: + if self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2: + strength += self.check_bi_end(self.bi) + elif self.klc_fx_type == Chan_KLC_FX.BOTTOM2: + strength += self.check_bi_end(self.bi) else: - break - - if len(subsequent_klcs) < 2: - return 0 - pass - if self.fx == Chan_FX_TYPE.TOP: - return self._check_top_bi_ending(subsequent_klcs) - else: # BOTTOM - return self._check_bottom_bi_ending(subsequent_klcs) - - def _check_top_bi_ending(self, subsequent_klcs): - """检查顶分型是否为笔终结""" - # 强烈笔终结特征: - # 1. 后续K线持续下跌,且跌破关键位置 - # 2. 没有新的更高的高点出现 - - broken_key_levels = 0 - new_highs = 0 - downward_trend = 0 - - # 检查关键价位突破 - first_low = self.pre.low - middle_low = self.low - key_support = min(first_low, middle_low) - - for i, klc in enumerate(subsequent_klcs): - # 检查是否跌破关键支撑 - if klc.low < key_support: - broken_key_levels += 1 - - # 检查是否出现新高 - if klc.high > self.high: - new_highs += 1 - - # 检查下跌趋势 - if i > 0 and klc.close < subsequent_klcs[i-1].close: - downward_trend += 1 - - # 强烈笔终结:跌破关键位且无新高 - if broken_key_levels >= 1 and new_highs == 0 and downward_trend >= 1: - return 2 - - # 可能笔终结:部分条件满足 - if (broken_key_levels >= 1 and new_highs <= 1) or (new_highs == 0 and downward_trend >= 1): + return 0 + return strength + def check_bi_end(self, bi): + if bi.dir == Chan_BI_DIR.UP: return 1 - - # 明显中继:出现新高且未跌破关键位 - if new_highs >= 2 and broken_key_levels == 0: - return -2 - - # 中继倾向:出现新高 - if new_highs >= 1: - return -1 - - return 0 - - def _check_bottom_bi_ending(self, subsequent_klcs): - """检查底分型是否为笔终结""" - # 强烈笔终结特征: - # 1. 后续K线持续上涨,且突破关键位置 - # 2. 没有新的更低的低点出现 - - broken_key_levels = 0 - new_lows = 0 - upward_trend = 0 - - # 检查关键价位突破 - first_high = self.pre.high - middle_high = self.high - key_resistance = max(first_high, middle_high) - - for i, klc in enumerate(subsequent_klcs): - # 检查是否突破关键阻力 - if klc.high > key_resistance: - broken_key_levels += 1 - - # 检查是否出现新低 - if klc.low < self.low: - new_lows += 1 - - # 检查上涨趋势 - if i > 0 and klc.close > subsequent_klcs[i-1].close: - upward_trend += 1 - - # 强烈笔终结:突破关键位且无新低 - if broken_key_levels >= 1 and new_lows == 0 and upward_trend >= 1: - return 2 - - # 可能笔终结:部分条件满足 - if (broken_key_levels >= 1 and new_lows <= 1) or (new_lows == 0 and upward_trend >= 1): + else: return 1 - - # 明显中继:出现新低且未突破关键位 - if new_lows >= 2 and broken_key_levels == 0: - return -2 - - # 中继倾向:出现新低 - if new_lows >= 1: - return -1 - - return 0 - - def _check_post_fx_confirmation(self): - """ - 检查分型后的走势确认 - 返回值:-1到1的调整分数 - """ - if not self.next: - return 0 - - score = 0 + def cal_klu_strength(self, klc1, klc2, klc3): + klu_list = [] + klu_list.extend(klc1.klus) + klu_list.extend(klc2.klus) + klu_list.extend(klc3.klus) - # 检查第三根K线的确认 - third_klc = self.next - - if self.fx == Chan_FX_TYPE.TOP: - # 顶分型:第三根K线应该走弱 - middle_price = (self.high + self.low) / 2 - - if third_klc.close < middle_price: - score += 0.5 - if third_klc.low < self.pre.low: # 跌破第一根K线低点 - score += 0.5 - if third_klc.close < third_klc.open and abs(third_klc.close - third_klc.open) > abs(self.close - self.open) * 0.5: - score += 0.3 # 明显阴线 - - else: # BOTTOM - # 底分型:第三根K线应该走强 - middle_price = (self.high + self.low) / 2 - - if third_klc.close > middle_price: - score += 0.5 - if third_klc.high > self.pre.high: # 突破第一根K线高点 - score += 0.5 - if third_klc.close > third_klc.open and abs(third_klc.close - third_klc.open) > abs(self.close - self.open) * 0.5: - score += 0.3 # 明显阳线 - - return min(1, max(-1, score)) - - def _check_fx_quality(self): - """ - 检查分型本身的质量 - 返回值:-1到1的调整分数 - """ - score = 0 - - # 检查分型的标准性 - if self.fx == Chan_FX_TYPE.TOP: - # 高点突出程度 - high_diff1 = (self.high - self.pre.high) / self.high if self.high > 0 else 0 - high_diff2 = (self.high - self.next.high) / self.high if self.high > 0 else 0 - min_diff = 33*min(high_diff1, high_diff2) - - if min_diff > 0.03: # 非常突出 - score += 0.5 - elif min_diff > 0.01: # 比较突出 - score += 0.2 - elif min_diff < 0.003: # 不够突出 - score -= 0.5 - - else: # BOTTOM - # 低点突出程度 - low_diff1 = (self.pre.low - self.low) / self.pre.low if self.pre.low > 0 else 0 - low_diff2 = (self.next.low - self.low) / self.next.low if self.next.low > 0 else 0 - min_diff = 33*min(low_diff1, low_diff2) - - if min_diff > 0.03: # 非常突出 - score += 0.5 - elif min_diff > 0.01: # 比较突出 - score += 0.2 - elif min_diff < 0.003: # 不够突出 - score -= 0.5 - - # 检查量价配合 - avg_volume = self._calculate_average_volume(lookback=5) - if avg_volume > 0: - volume_ratio = self.volume / avg_volume - if volume_ratio > 1.5: - score += 0.5 - elif volume_ratio < 0.7: - score -= 0.3 - - return min(1, max(-1, score)) - + return def calculate_fx_strength(self): """ 基于专业缠论理论的分型强度评估体系 diff --git a/ChanLun.py b/ChanLun.py index daabe2f..0f407b0 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -754,7 +754,7 @@ class ChanLun(): last_top = klc #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 1") klc.set_klc_fx_type(Chan_KLC_FX.TOP1) - #print(klc.start_time, klc.fx, "一类卖点Sell 1") + print(klc.end_time, klc.fx, "一类卖点Sell 1") #klc.set_fx(fx) #klc.set_state("10") bi_list[-1].add_klc(klc) @@ -772,7 +772,7 @@ class ChanLun(): if last_top.index + 4 < klc.index and len(bi_list) > 1: pre_last_bi = bi_list[-2] last_bi = bi_list[-1] - if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.UP: + if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.UP and False: pre_last_bi.update_bi(klc) bi_list.remove(last_bi) pre_last_bi.set_next(None) @@ -784,7 +784,7 @@ class ChanLun(): #print(klc.start_time, last_bi.start_klc.start_time, "New TOP Found reset last bi") #klc.set_state("10") #print(klc.start_time, klc.fx, "笔卖点Sell 1") - klc.set_klc_fx_type(Chan_KLC_FX.TOP2) + ###klc.set_klc_fx_type(Chan_KLC_FX.TOP2) # when bi is down but the fx is top bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) else: @@ -804,7 +804,7 @@ class ChanLun(): bi.add_klc(klc) bi_list.append(bi) last_top = klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 2") + print(klc.end_time, klc.fx, bi_list[-1].dir, "Last Top Change 2") klc.set_klc_fx_type(Chan_KLC_FX.TOP2) #klc.set_state('30') bi_list[-1].add_klc(klc) @@ -892,7 +892,7 @@ class ChanLun(): if last_bottom.index + 4 < klc.index and len(bi_list) > 1: pre_last_bi = bi_list[-2] last_bi = bi_list[-1] - if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.DOWN: + if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.DOWN and False: pre_last_bi.update_bi(klc) bi_list.remove(last_bi) pre_last_bi.set_next(None) @@ -904,7 +904,7 @@ class ChanLun(): #print(klc.start_time, last_bi.start_klc.start_time, "New BOTTOM Found reset last bi") #klc.set_state("-10") #print(klc.start_time, klc.fx, "笔买点Buy 1") - klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) + ###klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) # when bi is up but the fx is bottom bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) else: diff --git a/ChanPY.py b/ChanPY.py index 65fd94a..c06d9b2 100644 --- a/ChanPY.py +++ b/ChanPY.py @@ -1,7 +1,7 @@ import sys import os -sys.path.append(os.path.abspath("/Users/jack/Documents/GitHub/chan.py")) -#sys.path.append(os.path.abspath("/Users/jack/Project/chan.py")) +#sys.path.append(os.path.abspath("/Users/jack/Documents/GitHub/chan.py")) +sys.path.append(os.path.abspath("/Users/jack/Project/chan.py")) from Chan import CChan from BuySellPoint.BS_Point import CBS_Point from ChanConfig import CChanConfig @@ -51,6 +51,7 @@ class ChanPY(): autype=AUTYPE.QFQ, ) klu_list = [] + bsps = [] chanIn = True #def __init__(self, dataframe): #self.klu_list = self.get_kl_data(dataframe) @@ -266,7 +267,7 @@ class ChanPY(): #print(klu.time, bsps[-1], updown[-1], bi_list[-1].is_sure) self.chanIn = False else: - klu = CKLine_Unit(self.create_item_dict(self.get_last_item_data(dataframe), GetColumnNameFromFieldList(fields)), autofix=True) + klu = self.get_last_klu(dataframe) if self.last_kline.time < klu.time: self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 self.last_kline = klu @@ -318,7 +319,6 @@ class ChanPY(): return bsps def get_bsp_state(self, dataframe:DataFrame): fields = "time,open,high,low,close,volume" - bsps = [] if self.chanIn: kl_data = self.get_kl_data(dataframe) bsp_list = [] @@ -337,66 +337,66 @@ class ChanPY(): #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, lst[-2].fx, bi_list[-1].dir, bi_list[-1].is_sure,klu.close) if bsp_list_pre_len > len(bsp_list): if abs(last_bsp_value) == 1: - bsps.append(1) + self.bsps.append(1) #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, 98) else: - bsps.append(99) + self.bsps.append(99) else: if bsp_list_pre_len == len(bsp_list): if klu.idx == last_bsp.klu.idx: if last_bsp.klu.idx - last_bsp_index > 3: last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy) - bsps.append(last_bsp_value) + self.bsps.append(last_bsp_value) else: - bsps.append(0) + self.bsps.append(0) last_bsp_index = last_bsp.klu.idx #if abs(last_bsp_value) == 1 or abs(last_bsp_value) == 2: #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, "Knonw") else: - bsps.append(0) + self.bsps.append(0) else: if klu.idx == last_bsp.klu.idx: if last_bsp.klu.idx - last_bsp_index > 3: last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy) - bsps.append(last_bsp_value) + self.bsps.append(last_bsp_value) else: - bsps.append(0) + self.bsps.append(0) last_bsp_index = last_bsp.klu.idx #if abs(last_bsp_value) == 1 or abs(last_bsp_value) == 2: #print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, "Knonw") else: - bsps.append(0) + self.bsps.append(0) else: - bsps.append(0) + self.bsps.append(0) bsp_list_pre_len = len(bsp_list) self.chanIn = False else: - klu = CKLine_Unit(self.create_item_dict(self.get_last_item_data(dataframe), GetColumnNameFromFieldList(fields)), autofix=True) + klu = self.get_last_klu(dataframe) if self.last_kline.time < klu.time: self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线 self.last_kline = klu bsp_list = self.chan.get_bsp() last_bsp = bsp_list[-1] if last_bsp.klu.idx == klu.idx: - bsps.append(self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy)) + self.bsps.append(self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy)) else: - bsps.append(0) - for index in range(0, len(bsps)): - if not (abs(bsps[index]) == 1 or abs(bsps[index]) == 2): - bsps[index] = 0 + self.bsps.append(0) + for index in range(0, len(self.bsps)): + if not (abs(self.bsps[index]) == 1 or abs(self.bsps[index]) == 2): + self.bsps[index] = 0 else: - if bsps[index] == 2: - bsps[index] = 10 + if self.bsps[index] == 2: + self.bsps[index] = 10 else: - if bsps[index] == -2: - bsps[index] = -10 + if self.bsps[index] == -2: + self.bsps[index] = -10 else: - if bsps[index] == 1: - bsps[index] = 1 + if self.bsps[index] == 1: + self.bsps[index] = 1 else: - if bsps[index] == -1: - bsps[index] = -1 + if self.bsps[index] == -1: + self.bsps[index] = -1 else: - bsps[index] = 0 - return bsps + self.bsps[index] = 0 + return self.bsps diff --git a/strategies/ChanLun_BTC_30.py b/strategies/ChanLun_BTC_30.py index 2e30ea7..6385dec 100644 --- a/strategies/ChanLun_BTC_30.py +++ b/strategies/ChanLun_BTC_30.py @@ -49,7 +49,7 @@ class ChanLun_BTC_30(IStrategy): "240": 0 } # 15m and 30m - minimal_roi = { + minimal_roi_1 = { "0": 0.1, "240": 0.05, "480": 0.03, @@ -61,9 +61,9 @@ class ChanLun_BTC_30(IStrategy): "2400": 0.025, "3600": 0 } - can_short = True - lev = 1.0 - stoploss = -0.3 + can_short = False + lev = 2.0 + stoploss = -0.5 trailing_stop = False trailing_stop_positive = 0.025 trailing_stop_positive_offset = 0.045 @@ -105,13 +105,22 @@ class ChanLun_BTC_30(IStrategy): dataframe_4h = self.add_indicators(dataframe_4h) dataframe_1d = self.add_indicators(dataframe_1d) #self.chan.plot_dual(dataframe_5, dataframe_30) - dataframe_5['chanpy_state'] = self.chanpy.get_bsp_state(dataframe_5) + chanpy_state = self.chanpy.get_bsp_state(dataframe_5) + dataframe_5['chanpy_state'] = chanpy_state state_list, fx_list = self.chan.get_klc_strength_list(dataframe_30) dataframe_30['state'] = state_list dataframe_30['fx'] = fx_list - + #bi_list_1 = self.chan.get_bi_list(dataframe) + #bi_list_5 = self.chan.get_bi_list(dataframe_5) + #bi_list_15 = self.chan.get_bi_list(dataframe_15) + #bi_list_30 = self.chan.get_bi_list(dataframe_30) + #bi_list_60 = self.chan.get_bi_list(dataframe_60) if self.last_time + timedelta(minutes=1) < datetime.now(): - + #self.print_bi(bi_list_1) + #self.print_bi(bi_list_5) + #self.print_bi(bi_list_15) + #self.print_bi(bi_list_30) + #self.print_bi(bi_list_60) print("-------------------------------------------------------------------------------") self.last_time = datetime.now() dataframe = resampled_merge(dataframe, dataframe_5) @@ -120,7 +129,11 @@ class ChanLun_BTC_30(IStrategy): #dataframe = resampled_merge(dataframe, dataframe_60) #dataframe = resampled_merge(dataframe, dataframe_4h) return dataframe - + def print_bi(self, bi_list): + if bi_list and len(bi_list) > 2: + bi1 = bi_list[-1] + bi2 = bi_list[-2] + print(bi1.start_time, bi1.end_time, bi1.dir, bi2.start_time, bi2.end_time, bi2.dir) def add_indicators(self, df): fast = 8 slow = 16 @@ -167,16 +180,26 @@ class ChanLun_BTC_30(IStrategy): return new_exitprice def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: datetime, entry_tag: str | None, - side: str, **kwargs) -> bool: + time_in_force: str, current_time: datetime, entry_tag: str | None, + side: str, **kwargs) -> bool: if self.last_trade: - if self.last_trade.open_date + timedelta(minutes=30) > current_time: - return False + if self.last_trade.is_short: + if side == 'short': + if self.last_trade.open_date + timedelta(minutes=30) > current_time: + return False + else: + return True + else: + if side == 'long': + if self.last_trade.open_date + timedelta(minutes=30) > current_time: + return True + else: + return False #if self.last_trade: #print(self.last_trade.open_date, current_time, self.last_trade.open_date + timedelta(minutes=self.time5)) return True - def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + def custom_exit1(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs): #dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) #last_candle = dataframe.iloc[-1].squeeze() @@ -220,8 +243,8 @@ class ChanLun_BTC_30(IStrategy): #last_candle = dataframe.iloc[-1].squeeze() klc_list = self.chan.get_klc_list(resample_to_interval(dataframe, self.get_ticker_indicator() * 30)) bi_list = self.chan.cal_bi_list(klc_list) - last_high = klc_list[-3].high - last_low = klc_list[-3].low + last_high = klc_list[-2].high + last_low = klc_list[-2].low if trade.is_short: if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side): trade.set_custom_data(key="entry_candle_high", value=last_high) @@ -234,15 +257,15 @@ class ChanLun_BTC_30(IStrategy): def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: state_str = 'resample_{}_state'.format(self.get_ticker_indicator()*self.time30) fx_str = 'resample_{}_fx'.format(self.get_ticker_indicator()*self.time30) - chanpy_state_str = 'resample_{}_chanpy_state'.format(self.get_ticker_indicator()*self.time5) - shift_time = self.time30*2 - strength = 2.2 + #chanpy_state_str = 'resample_{}_chanpy_state'.format(self.get_ticker_indicator()*self.time5) + shift_time = self.time30 + strength = 0.9 dataframe.loc[ ( #(dataframe['state'] == "-30") (dataframe[state_str].shift(shift_time) > strength) & - (dataframe[fx_str].shift(shift_time) == -1) & - (dataframe[chanpy_state_str].shift(shift_time+30) == 1) + (dataframe[fx_str].shift(shift_time) == -1) + #(dataframe[chanpy_state_str].shift(shift_time+30) == 1) #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "-10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") @@ -253,8 +276,8 @@ class ChanLun_BTC_30(IStrategy): ( #(dataframe['state'] == "-30") (dataframe[state_str].shift(shift_time) > strength) & - (dataframe[fx_str].shift(shift_time) == 1) & - (dataframe[chanpy_state_str].shift(shift_time+30) == -1) + (dataframe[fx_str].shift(shift_time) == 1) + #(dataframe[chanpy_state_str].shift(shift_time+30) == -1) #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "-10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") @@ -265,15 +288,15 @@ class ChanLun_BTC_30(IStrategy): def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: state_str = 'resample_{}_state'.format(self.get_ticker_indicator()*self.time30) fx_str = 'resample_{}_fx'.format(self.get_ticker_indicator()*self.time30) - chanpy_state_str = 'resample_{}_chanpy_state'.format(self.get_ticker_indicator()*self.time5) - shift_time = self.time30*2 - strength = 2.2 + #chanpy_state_str = 'resample_{}_chanpy_state'.format(self.get_ticker_indicator()*self.time5) + shift_time = self.time30 + strength = 0.9 dataframe.loc[ ( #(dataframe['state']== "30") (dataframe[state_str].shift(shift_time) > strength) & - (dataframe[fx_str].shift(shift_time) == 1) & - (dataframe[chanpy_state_str].shift(shift_time+30) == -1) + (dataframe[fx_str].shift(shift_time) == 1) + #(dataframe[chanpy_state_str].shift(shift_time+30) == -1) #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time60)] == "10") ), @@ -282,8 +305,8 @@ class ChanLun_BTC_30(IStrategy): ( #(dataframe['state']== "30") (dataframe[state_str].shift(shift_time) > strength) & - (dataframe[fx_str].shift(shift_time) == -1) & - (dataframe[chanpy_state_str].shift(shift_time+30) == 1) + (dataframe[fx_str].shift(shift_time) == -1) + #(dataframe[chanpy_state_str].shift(shift_time+30) == 1) #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "10") & #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time60)] == "10") ), diff --git a/web/app.py b/web/app.py index 32a4816..f5a5825 100644 --- a/web/app.py +++ b/web/app.py @@ -299,6 +299,10 @@ def add_indicators(df): # 处理Infinity和-Infinity值 df['volume_ratio'] = df['volume_ratio'].replace([float('inf'), float('-inf')], 1.0) + # 计算ATR (Average True Range) - 14周期 + df['atr'] = ta.ATR(df, timeperiod=14) + df['atr'] = df['atr'].fillna(0) + return df def calculate_macd(df): @@ -568,6 +572,8 @@ def generate_replay_data(df, client_tz, symbol=None, element_timeframe=None, sta 'middle': element_current_df['element_bb_middle'].tolist(), 'lower': element_current_df['element_bb_lower'].tolist() }, + # 添加次周期ATR数据 + 'element_atr': element_current_df['atr'].tolist(), 'element_klc_fx_info': [{ 'time': format_time_safely(point['time'], client_tz), 'price': float(point['price']), @@ -644,6 +650,8 @@ def generate_replay_data(df, client_tz, symbol=None, element_timeframe=None, sta 'middle': current_df['element_bb_middle'].tolist(), 'lower': current_df['element_bb_lower'].tolist() }, + # 添加ATR数据 + 'atr': current_df['atr'].tolist(), 'klc_fx_info': [{ 'time': format_time_safely(point['time'], client_tz), 'price': float(point['price']), @@ -680,6 +688,7 @@ def generate_replay_data(df, client_tz, symbol=None, element_timeframe=None, sta 'element_macd': {'macd': [], 'signal': [], 'histogram': []}, 'element_bollinger': {'upper': [], 'middle': [], 'lower': []}, 'element_element_bollinger': {'upper': [], 'middle': [], 'lower': []}, + 'element_atr': [], 'element_klc_fx_info': [], 'element_klu_fx_info': [] }) @@ -1116,6 +1125,8 @@ def analyze(): 'middle': df['element_bb_middle'].tolist(), 'lower': df['element_bb_lower'].tolist() }, + # 添加ATR数据 + 'atr': df['atr'].tolist(), # 添加K线分型信息 'klc_fx_info': [{ 'time': format_time_safely(point['time'], client_tz), @@ -1173,6 +1184,9 @@ def analyze(): 'lower': element_df['element_bb_lower'].tolist() } + # 添加小周期ATR数据 + result['element_atr'] = element_df['atr'].tolist() + result['element_bi_list'] = [{ 'start_time': bi.start_klc.end_time if isinstance(bi.start_klc.end_time, str) else bi.start_klc.end_time.astimezone(client_tz).isoformat(), 'end_time': (bi.end_klc.end_time if isinstance(bi.end_klc.end_time, str) else bi.end_klc.end_time.astimezone(client_tz).isoformat()) if bi.end_klc else None, diff --git a/web/templates/index.html b/web/templates/index.html index 9230ee8..2059aee 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -407,6 +407,10 @@ +
+ + +
@@ -741,6 +745,7 @@ candleSeries: null, lineSeries: null, volumeSeries: null, + atrLineSeries: null, macdLineSeries: null, signalLineSeries: null, histogramSeries: null, @@ -1387,7 +1392,7 @@ container.style.height = '100%'; // 是否显示MACD - const showMacd = true; + const showMacd = $('#showMacd').is(':checked'); const showOriginalKline = $('#showOriginalKline').is(':checked'); // 创建主图容器 @@ -1406,31 +1411,46 @@ volumeChartContainer.style.right = '0'; volumeChartContainer.style.borderTop = '1px solid #e0e0e0'; + // 添加ATR图表容器 + const atrChartContainer = document.createElement('div'); + atrChartContainer.style.width = '100%'; + atrChartContainer.style.position = 'absolute'; + atrChartContainer.style.left = '0'; + atrChartContainer.style.right = '0'; + atrChartContainer.style.borderTop = '1px solid #e0e0e0'; + // 如果需要显示MACD,创建MACD容器 let macdChartContainer = null; if (showMacd) { - // 设置各图表高度 - 为三个图表分配合理比例,主图表适度增加高度 - mainChartContainer.style.height = '55%'; // 主图占55%(约385px) - volumeChartContainer.style.top = '55%'; - volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5%(约157.5px) + // 设置各图表高度 - 为四个图表分配合理比例 + mainChartContainer.style.height = '45%'; // 主图占45% + volumeChartContainer.style.top = '45%'; + volumeChartContainer.style.height = '20%'; // 成交量图占20% + + atrChartContainer.style.top = '65%'; // ATR图从65%位置开始 + atrChartContainer.style.height = '17.5%'; // ATR图占17.5% macdChartContainer = document.createElement('div'); macdChartContainer.style.width = '100%'; - macdChartContainer.style.height = '22.5%'; // MACD图占22.5%(约157.5px) + macdChartContainer.style.height = '17.5%'; // MACD图占17.5% macdChartContainer.style.position = 'absolute'; - macdChartContainer.style.top = '77.5%'; // 从77.5%位置开始 + macdChartContainer.style.top = '82.5%'; // 从82.5%位置开始 macdChartContainer.style.left = '0'; macdChartContainer.style.right = '0'; macdChartContainer.style.borderTop = '1px solid #e0e0e0'; } else { - // 不显示MACD时的高度 - 主图和成交量图分配 - mainChartContainer.style.height = '72%'; // 主图占72%(约504px) - volumeChartContainer.style.top = '72%'; - volumeChartContainer.style.height = '28%'; // 成交量图占28%(约196px) + // 不显示MACD时的高度 - 主图、成交量图和ATR图分配 + mainChartContainer.style.height = '55%'; // 主图占55% + volumeChartContainer.style.top = '55%'; + volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5% + + atrChartContainer.style.top = '77.5%'; // ATR图从77.5%位置开始 + atrChartContainer.style.height = '22.5%'; // ATR图占22.5% } container.appendChild(mainChartContainer); container.appendChild(volumeChartContainer); + container.appendChild(atrChartContainer); if (showMacd) container.appendChild(macdChartContainer); // 防止同步过程中的无限循环 @@ -1444,6 +1464,8 @@ chartHeight = mainChartContainer.clientHeight; } else if (chartType === 'volume') { chartHeight = volumeChartContainer.clientHeight; + } else if (chartType === 'atr') { + chartHeight = atrChartContainer.clientHeight; } else if (chartType === 'macd') { chartHeight = macdChartContainer ? macdChartContainer.clientHeight : 0; } else { @@ -1556,6 +1578,8 @@ visible: showTimeScale, borderColor: '#ddd', barSpacing: symbolConfig.type === 'a_stock' ? 6 : 10, + // 确保所有图表使用相同的边距设置 + rightOffset: 12, // 移除可能影响拖动的固定边缘设置 // fixLeftEdge: true, // fixRightEdge: true, @@ -1593,6 +1617,9 @@ // 创建成交量图表 - 只显示底部的时间轴 const volumeChart = LightweightCharts.createChart(volumeChartContainer, createChartOptions(false, 'volume')); + // 创建ATR图表 + const atrChart = LightweightCharts.createChart(atrChartContainer, createChartOptions(false, 'atr')); + // 创建MACD图表(如果需要) let macdChart = null; if (showMacd) { @@ -1663,6 +1690,59 @@ volumeSeries.setData(volumes); tvWidget.series.volumeSeries = volumeSeries; + // 添加ATR图表 + const atrLineSeries = atrChart.addLineSeries({ + color: '#FF9800', + lineWidth: 2, + title: 'ATR', + lastValueVisible: false, + priceLineVisible: false, + }); + + // 准备ATR数据 + const atrData = []; + // 使用与K线数据相同的数据源来确保时间对齐 + const atrKlineDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data; + const atrDataSource = useElementPeriod ? + (currentData.element_atr || currentData.atr) : // 如果有次周期ATR数据则使用,否则使用主周期 + currentData.atr; // 主周期使用主周期ATR数据 + + console.log('ATR数据源选择:', useElementPeriod ? '次周期' : '主周期'); + console.log('ATR数据长度:', atrDataSource ? atrDataSource.length : 0); + console.log('K线数据长度:', atrKlineDataSource ? atrKlineDataSource.length : 0); + + if (atrDataSource && Array.isArray(atrDataSource) && atrKlineDataSource && Array.isArray(atrKlineDataSource)) { + // 关键修复:为每个K线时间点都创建ATR数据点,包括没有ATR值的前期数据 + for (let i = 0; i < atrKlineDataSource.length; i++) { + const kline = atrKlineDataSource[i]; + const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); + + // 为每个时间点都添加数据以保持时间轴对齐,但ATR为0时不显示 + if (atrDataSource[i] !== undefined) { + if (atrDataSource[i] > 0) { + // ATR有效值,正常显示 + atrData.push({ + time: timestamp, + value: atrDataSource[i] + }); + } else { + // ATR为0,添加时间点但不显示线条(使用undefined作为value) + atrData.push({ + time: timestamp, + value: undefined + }); + } + } + } + + console.log('处理后的ATR数据点数:', atrData.length); + console.log('ATR数据样本:', atrData.slice(0, 5)); + } + + console.log('处理后的ATR数据点数:', atrData.length); + atrLineSeries.setData(atrData); + tvWidget.series.atrLineSeries = atrLineSeries; + // 添加MACD图表 - 始终使用主K线周期的MACD数据 if (showMacd && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data)) { // 创建MACD线 @@ -1758,7 +1838,8 @@ syncInProgress = true; console.log('🚀 开始同步图表,来源:', sourceChart === mainChart ? '主图' : - sourceChart === volumeChart ? '成交量图' : 'MACD图'); + sourceChart === volumeChart ? '成交量图' : + sourceChart === atrChart ? 'ATR图' : 'MACD图'); try { if (sourceChart && sourceChart.timeScale) { @@ -1787,6 +1868,16 @@ } } + // 同步ATR图 + if (sourceChart !== atrChart && atrChart && atrChart.timeScale) { + try { + atrChart.timeScale().setVisibleLogicalRange(logicalRange); + console.log('✅ ATR图同步完成'); + } catch (e) { + console.error('❌ ATR图同步失败:', e); + } + } + // 同步MACD图 if (showMacd && macdChart && sourceChart !== macdChart && macdChart.timeScale) { try { @@ -1822,6 +1913,7 @@ let localDragStates = { main: false, volume: false, + atr: false, macd: false }; @@ -1838,10 +1930,15 @@ // 为每个图表添加事件监听 const addChartSyncEvents = (chartContainer, chart) => { - console.log('为图表添加同步事件监听:', chart === mainChart ? '主图' : chart === volumeChart ? '成交量图' : 'MACD图'); + console.log('为图表添加同步事件监听:', + chart === mainChart ? '主图' : + chart === volumeChart ? '成交量图' : + chart === atrChart ? 'ATR图' : 'MACD图'); // 确定当前图表类型 - const chartType = chart === mainChart ? 'main' : chart === volumeChart ? 'volume' : 'macd'; + const chartType = chart === mainChart ? 'main' : + chart === volumeChart ? 'volume' : + chart === atrChart ? 'atr' : 'macd'; // 使用LightweightCharts内置的时间范围变化事件(这是最可靠的方法) chart.timeScale().subscribeVisibleTimeRangeChange(() => { @@ -1897,6 +1994,7 @@ // 添加事件监听 addChartSyncEvents(mainChartContainer, mainChart); addChartSyncEvents(volumeChartContainer, volumeChart); + addChartSyncEvents(atrChartContainer, atrChart); if (showMacd && macdChart) { addChartSyncEvents(macdChartContainer, macdChart); } @@ -1915,6 +2013,12 @@ height: volumeChartContainer.clientHeight }); + // 调整ATR图大小 + atrChart.applyOptions({ + width: atrChartContainer.clientWidth, + height: atrChartContainer.clientHeight + }); + // 调整MACD图大小 if (showMacd && macdChart && macdChartContainer) { macdChart.applyOptions({ @@ -2914,6 +3018,8 @@ // 清除之前的十字线标记 const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line'); existingVolumeLines.forEach(line => line.remove()); + const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); + existingAtrLines.forEach(line => line.remove()); const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); existingMacdLines.forEach(line => line.remove()); @@ -2941,6 +3047,26 @@ document.body.appendChild(volumeLine); } + // 在ATR图上绘制垂直线 + if (atrChart && atrChartContainer) { + const atrTimeCoordinate = atrChart.timeScale().timeToCoordinate(param.time); + if (atrTimeCoordinate !== null) { + const atrChartRect = atrChartContainer.getBoundingClientRect(); + const atrLine = document.createElement('div'); + atrLine.className = 'atr-crosshair-line'; + atrLine.style.position = 'fixed'; // 改为fixed定位 + atrLine.style.left = (atrChartRect.left + atrTimeCoordinate) + 'px'; + atrLine.style.top = atrChartRect.top + 'px'; + atrLine.style.width = '1px'; + atrLine.style.height = atrChartRect.height + 'px'; + atrLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; + atrLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)'; + atrLine.style.pointerEvents = 'none'; + atrLine.style.zIndex = '1000'; + document.body.appendChild(atrLine); + } + } + // 如果有MACD图,也在MACD图上绘制垂直线 if (showMacd && macdChart && macdChartContainer) { const macdTimeCoordinate = macdChart.timeScale().timeToCoordinate(param.time); @@ -2969,6 +3095,8 @@ try { const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line'); existingVolumeLines.forEach(line => line.remove()); + const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); + existingAtrLines.forEach(line => line.remove()); const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); existingMacdLines.forEach(line => line.remove()); } catch (e) { @@ -3314,7 +3442,7 @@ // 构建显示文本,包含分型类型和强度信息 let displayText = `${fx.fx_strength.toFixed(1)}`; - if (fx.fx_strength < 2.0) { // 降低阈值,让更多分型显示 + if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示 displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 } @@ -3399,7 +3527,7 @@ let strengthColor = fx.is_bottom ? '#11116B' : '#222222'; // 底分型用珊瑚红,顶分型用薄荷绿 let displayText = `${fx.fx_strength.toFixed(1)}`; // 构建小周期分型显示文本 - if (fx.fx_strength < 2.0){ // 调整小周期阈值 + if (fx.fx_strength < 1.0){ // 调整小周期阈值 displayText = fx.fx_strength >= 0.6 ? '' : '' // 0.6以上显示点 } @@ -3544,21 +3672,82 @@ } } - // 调整所有图表以适应数据 + // 同步所有图表的时间轴配置 + const syncTimeScaleSettings = () => { + // 获取主图表的时间轴设置 + const mainTimeScale = mainChart.timeScale(); + const baseOptions = { + timeVisible: true, + secondsVisible: false, + borderColor: '#ddd', + barSpacing: symbolConfig.type === 'a_stock' ? 6 : 10, + rightOffset: 12, + lockVisibleTimeRangeOnResize: true, + // 关键:确保所有图表边缘行为完全一致 + fixLeftEdge: false, + fixRightEdge: false, + // 确保时间刻度行为一致 + ticksVisible: true, + minimumHeight: 0, + }; + + console.log('🔧 同步时间轴设置:', baseOptions); + + // 应用相同的设置到所有图表 + mainChart.timeScale().applyOptions(baseOptions); + volumeChart.timeScale().applyOptions(baseOptions); + atrChart.timeScale().applyOptions(baseOptions); + if (showMacd && macdChart) { + macdChart.timeScale().applyOptions(baseOptions); + } + }; + + // 首先同步时间轴设置 + syncTimeScaleSettings(); + + // 然后让主图表适应内容 mainChart.timeScale().fitContent(); - volumeChart.timeScale().fitContent(); - if (showMacd && macdChart) { - macdChart.timeScale().fitContent(); - } + + // 立即同步其他图表到主图表的范围 + setTimeout(() => { + const visibleRange = mainChart.timeScale().getVisibleRange(); + if (visibleRange) { + console.log('🔧 同步可见范围:', visibleRange); + volumeChart.timeScale().setVisibleRange(visibleRange); + atrChart.timeScale().setVisibleRange(visibleRange); + if (showMacd && macdChart) { + macdChart.timeScale().setVisibleRange(visibleRange); + } + console.log('🔧 时间轴同步完成'); + } + }, 50); // 保存图表对象 tvWidget.mainChart = mainChart; tvWidget.volumeChart = volumeChart; + tvWidget.atrChart = atrChart; tvWidget.macdChart = macdChart; tvWidget.state.isInitialized = true; // 绑定同步事件 - bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, mainChart, volumeChart, macdChart, showMacd); + bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, mainChart, volumeChart, atrChart, macdChart, showMacd); + + // 最终确保所有图表时间轴对齐 + setTimeout(() => { + const visibleRange = mainChart.timeScale().getVisibleRange(); + if (visibleRange) { + console.log('🔧 最终同步可见范围:', visibleRange); + + // 强制重新设置所有图表的可见范围 + volumeChart.timeScale().setVisibleRange(visibleRange); + atrChart.timeScale().setVisibleRange(visibleRange); + if (showMacd && macdChart) { + macdChart.timeScale().setVisibleRange(visibleRange); + } + + console.log('🔧 最终时间轴对齐完成'); + } + }, 150); // 只有在时间输入框都为空时才设置图表默认时间范围 if (!$('#start_time').val() && !$('#end_time').val()) { @@ -3566,7 +3755,7 @@ } // 添加买卖点提示 - setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, macdChartContainer, volumeChart, macdChart, showMacd); + setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, volumeChart, atrChart, macdChart, showMacd); // 显示买卖点 if ($('#showTradePoints').is(':checked')) { @@ -3674,8 +3863,46 @@ tvWidget.series.volumeSeries.setData(volumes); } + // 更新ATR数据 + if (tvWidget.series.atrLineSeries) { + const atrData = []; + const atrDataSource = useElementPeriod ? + (currentData.element_atr || currentData.atr) : + currentData.atr; + + if (atrDataSource && Array.isArray(atrDataSource)) { + const klineDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data; + // 修复:为每个K线时间点都创建ATR数据点,包括没有ATR值的前期数据 + for (let i = 0; i < klineDataSource.length; i++) { + const kline = klineDataSource[i]; + const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); + + // 为每个时间点都添加数据以保持时间轴对齐,但ATR为0时不显示 + if (atrDataSource[i] !== undefined) { + if (atrDataSource[i] > 0) { + // ATR有效值,正常显示 + atrData.push({ + time: timestamp, + value: atrDataSource[i] + }); + } else { + // ATR为0,添加时间点但不显示线条(使用undefined作为value) + atrData.push({ + time: timestamp, + value: undefined + }); + } + } + } + + console.log('🔄 增量更新ATR数据点数:', atrData.length); + } + + tvWidget.series.atrLineSeries.setData(atrData); + } + // 更新MACD数据 - if (true && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data) && tvWidget.series.macdLineSeries) { + if (tvWidget.series.macdLineSeries && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data)) { // 提取MACD数据 const macdData = []; const signalData = []; @@ -3714,16 +3941,20 @@ // 重新显示笔、线段和中枢等图形 redrawFractalElements(); - // 恢复之前的可视范围 + // 恢复之前的可视范围 - 优先使用visibleRange以确保时间轴对齐 if (tvWidget.mainChart) { - if (tvWidget.state.logicalRange) { - tvWidget.mainChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange); - if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange); - if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange); - } else if (tvWidget.state.visibleRange) { + if (tvWidget.state.visibleRange) { + console.log('🔄 恢复可见范围:', tvWidget.state.visibleRange); tvWidget.mainChart.timeScale().setVisibleRange(tvWidget.state.visibleRange); if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(tvWidget.state.visibleRange); + if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleRange(tvWidget.state.visibleRange); if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(tvWidget.state.visibleRange); + } else if (tvWidget.state.logicalRange) { + console.log('🔄 恢复逻辑范围:', tvWidget.state.logicalRange); + tvWidget.mainChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange); + if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange); + if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange); + if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange); } } @@ -3735,7 +3966,7 @@ } } - function bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, mainChart, volumeChart, macdChart, showMacd) { + function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, mainChart, volumeChart, atrChart, macdChart, showMacd) { // 防止同步过程中的无限循环 let syncInProgress = false; @@ -3743,6 +3974,7 @@ let localDragStates = { main: false, volume: false, + atr: false, macd: false }; @@ -3757,7 +3989,8 @@ syncInProgress = true; console.log('🚀 开始同步图表,来源:', sourceChart === mainChart ? '主图' : - sourceChart === volumeChart ? '成交量图' : 'MACD图'); + sourceChart === volumeChart ? '成交量图' : + sourceChart === atrChart ? 'ATR图' : 'MACD图'); try { if (sourceChart && sourceChart.timeScale) { @@ -3786,6 +4019,16 @@ } } + // 同步ATR图 + if (sourceChart !== atrChart && atrChart && atrChart.timeScale) { + try { + atrChart.timeScale().setVisibleLogicalRange(logicalRange); + console.log('✅ ATR图同步完成'); + } catch (e) { + console.error('❌ ATR图同步失败:', e); + } + } + // 同步MACD图 if (showMacd && macdChart && sourceChart !== macdChart && macdChart.timeScale) { try { @@ -3799,6 +4042,14 @@ // 保存当前的可见范围到全局状态 if (tvWidget && tvWidget.state) { tvWidget.state.logicalRange = logicalRange; + // 同时保存可见范围以确保精确对齐 + try { + const visibleRange = sourceChart.timeScale().getVisibleRange(); + tvWidget.state.visibleRange = visibleRange; + console.log('💾 保存状态 - 逻辑范围:', logicalRange, '可见范围:', visibleRange); + } catch (e) { + console.warn('⚠️ 保存可见范围失败:', e); + } } } else { console.warn('⚠️ 无效的逻辑范围:', logicalRange); @@ -3819,10 +4070,15 @@ // 为每个图表添加事件监听 const addChartSyncEvents = (chartContainer, chart) => { - console.log('为图表添加同步事件监听:', chart === mainChart ? '主图' : chart === volumeChart ? '成交量图' : 'MACD图'); + console.log('为图表添加同步事件监听:', + chart === mainChart ? '主图' : + chart === volumeChart ? '成交量图' : + chart === atrChart ? 'ATR图' : 'MACD图'); // 确定当前图表类型 - const chartType = chart === mainChart ? 'main' : chart === volumeChart ? 'volume' : 'macd'; + const chartType = chart === mainChart ? 'main' : + chart === volumeChart ? 'volume' : + chart === atrChart ? 'atr' : 'macd'; // 使用LightweightCharts内置的时间范围变化事件(这是最可靠的方法) chart.timeScale().subscribeVisibleTimeRangeChange(() => { @@ -3882,6 +4138,9 @@ if (volumeChartContainer && volumeChart) { addChartSyncEvents(volumeChartContainer, volumeChart); } + if (atrChartContainer && atrChart) { + addChartSyncEvents(atrChartContainer, atrChart); + } if (showMacd && macdChartContainer && macdChart) { addChartSyncEvents(macdChartContainer, macdChart); } @@ -3904,6 +4163,14 @@ }); } + // 调整ATR图大小 + if (atrChart && atrChartContainer) { + atrChart.applyOptions({ + width: atrChartContainer.clientWidth, + height: atrChartContainer.clientHeight + }); + } + // 调整MACD图大小 if (showMacd && macdChart && macdChartContainer) { macdChart.applyOptions({ @@ -3921,7 +4188,7 @@ }); } - function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, macdChartContainer, volumeChart, macdChart, showMacd) { + function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, volumeChart, atrChart, macdChart, showMacd) { // 调试变量 window.debugMode = true; @@ -3953,6 +4220,8 @@ // 清除之前的十字线标记 const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line'); existingVolumeLines.forEach(line => line.remove()); + const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); + existingAtrLines.forEach(line => line.remove()); const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); existingMacdLines.forEach(line => line.remove()); @@ -3980,6 +4249,26 @@ document.body.appendChild(volumeLine); } + // 在ATR图上绘制垂直线 + if (atrChart && atrChartContainer) { + const atrTimeCoordinate = atrChart.timeScale().timeToCoordinate(param.time); + if (atrTimeCoordinate !== null) { + const atrChartRect = atrChartContainer.getBoundingClientRect(); + const atrLine = document.createElement('div'); + atrLine.className = 'atr-crosshair-line'; + atrLine.style.position = 'fixed'; // 改为fixed定位 + atrLine.style.left = (atrChartRect.left + atrTimeCoordinate) + 'px'; + atrLine.style.top = atrChartRect.top + 'px'; + atrLine.style.width = '1px'; + atrLine.style.height = atrChartRect.height + 'px'; + atrLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; + atrLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)'; + atrLine.style.pointerEvents = 'none'; + atrLine.style.zIndex = '1000'; + document.body.appendChild(atrLine); + } + } + // 如果有MACD图,也在MACD图上绘制垂直线 if (showMacd && macdChart && macdChartContainer) { const macdTimeCoordinate = macdChart.timeScale().timeToCoordinate(param.time); @@ -4008,6 +4297,8 @@ try { const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line'); existingVolumeLines.forEach(line => line.remove()); + const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); + existingAtrLines.forEach(line => line.remove()); const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); existingMacdLines.forEach(line => line.remove()); } catch (e) { @@ -4754,12 +5045,14 @@ // 销毁主图表及其关联的线系列 tvWidget.mainChart = null; tvWidget.volumeChart = null; + tvWidget.atrChart = null; tvWidget.macdChart = null; // 重置系列数据 tvWidget.series = { candleSeries: null, lineSeries: null, volumeSeries: null, + atrLineSeries: null, macdLineSeries: null, signalLineSeries: null, histogramSeries: null, @@ -4809,6 +5102,8 @@ // 清除所有十字线延长线,防止它们跟着页面滚动 const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line'); existingVolumeLines.forEach(line => line.remove()); + const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); + existingAtrLines.forEach(line => line.remove()); const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); existingMacdLines.forEach(line => line.remove()); } catch (e) { @@ -5020,7 +5315,7 @@ const logicalRange = mainChart.timeScale().getVisibleLogicalRange(); // 获取当前图表设置 - const showMacd = true; + const showMacd = $('#showMacd').is(':checked'); const showOriginalKline = $('#showOriginalKline').is(':checked'); const showBi = $('#showMainBi').is(':checked'); const showSeg = $('#showMainSeg').is(':checked'); @@ -5264,7 +5559,7 @@ 'showMainZs': $('#showMainZs').is(':checked'), 'showMainUncompletedZs': $('#showMainUncompletedZs').is(':checked'), 'showVolume': false, - 'showMacd': true, + 'showMacd': $('#showMacd').is(':checked'), 'showKlcFxType': $('#showKlcFxType').is(':checked'), 'showKluFxType': $('#showKluFxType').is(':checked'), 'showElementKlcFxType': $('#showElementKlcFxType').is(':checked'), @@ -6335,9 +6630,11 @@ try { // 清除所有十字线延长线,防止它们跟着页面滚动 const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line'); - existingVolumeLines.forEach(line => line.remove()); - const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); - existingMacdLines.forEach(line => line.remove()); + existingVolumeLines.forEach(line => line.remove()); + const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); + existingAtrLines.forEach(line => line.remove()); + const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); + existingMacdLines.forEach(line => line.remove()); } catch (e) { console.debug('清除滚动中的十字线时出错:', e); }