添加笔中枢,删除了很多没有用的方法

This commit is contained in:
jackyu66git
2025-10-24 23:31:43 +08:00
parent cb621c3d87
commit dce2099ba1
9 changed files with 519 additions and 1266 deletions
+12 -1
View File
@@ -19,6 +19,9 @@ class ChanBI():
self.start_time = klc.start_time
self.macd_hist = 0
self.macd_div = 0
self.seg = None
def set_seg(self, seg):
self.seg = seg
def set_macdhist(self, macd_hist):
self.macd_hist = macd_hist
def set_macd_div(self, macd_div):
@@ -39,8 +42,16 @@ class ChanBI():
self.macd_hist += klu.macdhist
if self.dir == Chan_BI_DIR.DOWN and klu.macdhist < 0:
self.macd_hist -= klu.macdhist
def check_overlap(self):
def check_bi_zs_overlap(self):
if self.next and self.next.next:
if self.dir == Chan_BI_DIR.UP:
return self.low < self.next.next.high
else:
return self.high > self.next.next.low
else:
return False
def check_overlap(self):
if self.next and self.next.next and self.next.next.is_sure:
if self.dir == Chan_BI_DIR.UP:
return self.high > self.next.low and self.high < self.next.next.high
else:
+14 -29
View File
@@ -7,49 +7,34 @@ class ChanBIZS():
self.start_time = self.start_klc.start_time
self.end_time = None
self.index = index
self.next = None
self.pre = None
self.start_bi = start_bi
self.bi_list = []
self.bi_list.append(start_bi)
self.end_bi = None
self.last_bi_in = None
self.bi_out = None
self.is_sure = False
self.zg = 0
self.zd = 0
self.gg = 0
self.dd = 0
self.dir = ddir
self.sure_time = None
self.end_klc = None
self.bi_out_count = 0
self.bi_out_list = []
def set_last_bi_in(self, last_bi_in):
self.last_bi_in = last_bi_in
def set_bi_out(self, bi_out):
if bi_out:
#print(bi_out.start_klc.start_time, bi_out.sure_time, bi_out.dir, bi_out_seg.dir, len(self.bi_out_list))
if len(self.bi_out_list) > 0:
last_bi = self.bi_out_list[-1]
if last_bi.index != bi_out.index:
self.bi_out_list.append(bi_out)
else:
self.bi_out_list.append(bi_out)
self.bi_out = bi_out
def set_end_klc(self, end_klc, sure_time, bi_out_count):
self.end_klc = end_klc
self.set_end_time(end_klc.end_time)
def set_end_bi(self, end_bi, sure_bi):
self.end_bi = end_bi
self.set_end_time(end_bi.end_klc.end_time)
self.is_sure = True
self.sure_time = sure_time
self.bi_out_count = bi_out_count
def set_pre(self, pre):
self.pre = pre
def set_next(self, next):
self.next = next
self.sure_time = sure_bi.sure_time
def set_end_time(self, end_time):
self.end_time = end_time
def add_klc(self, klc):
self.klc_list.append(klc)
def set_zg(self, zg):
self.zg = zg
def set_zd(self, zd):
self.zd = zd
self.zd = zd
def set_gg(self, gg):
self.gg = gg
def set_dd(self, dd):
self.dd = dd
def add_bi(self, bi: ChanBI):
if bi:
self.bi_list.append(bi)
+1
View File
@@ -48,6 +48,7 @@ class ChanKLC():
self.ema52 = klu.ema52
self.ema24 = klu.ema24
self.trend = Chan_PRICE_TREND.UNKNOWN
self.exception = klu.exception
def set_trend(self, trend):
self.trend = trend
def to_string(self):
+6
View File
@@ -36,6 +36,8 @@ class ChanKLU:
self.body_ratio = self.body / self.range
self.upper_shadow_ratio = self.upper_shadow / self.body
self.lower_shadow_ratio = self.lower_shadow / self.body
self.exception = False
self.cal_exception()
self.candle_dir = Chan_K_DIR.CROSS if self.close == self.open else Chan_K_DIR.BULL if self.close > self.open else Chan_K_DIR.BEAR
self.continue_div = 0
@@ -65,6 +67,10 @@ class ChanKLU:
#print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength)
def set_macd_state(self, state):
self.macd_state = state
def cal_exception(self):
if self.upper_shadow_ratio > 5 or self.lower_shadow_ratio > 5:
self.exception = True
#print(self.time, self.upper_shadow_ratio, self.lower_shadow_ratio, self.body, self.lower_shadow, self.upper_shadow, self.high, self.low, self.close, self.open)
def set_trend(self, trend):
self.trend = trend
def set_next(self, next):
+35 -13
View File
@@ -246,7 +246,7 @@ class ChanLun():
if last_down_sbi.has_fx_gap:
look_for_bottom = True
last_seg.pre_set_end_bi(bi_list[last_down_sbi.start_bi.index - 1])
seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN)
seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN, bi)
seg_list.append(seg)
last_seg.set_next(seg)
seg.set_pre(last_seg)
@@ -272,7 +272,7 @@ class ChanLun():
#print(bi.start_time, look_for_top, "UP 3")
else:
last_seg.set_end_bi(bi_list[last_down_sbi.start_bi.index - 1], bi)
seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN)
seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN, bi)
seg_list.append(seg)
last_seg.set_next(seg)
seg.set_pre(last_seg)
@@ -340,7 +340,7 @@ class ChanLun():
if last_up_sbi.has_fx_gap:
look_for_top = True
last_seg.pre_set_end_bi(bi_list[last_up_sbi.start_bi.index - 1])
seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP)
seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP, bi)
seg_list.append(seg)
last_seg.set_next(seg)
seg.set_pre(last_seg)
@@ -366,7 +366,7 @@ class ChanLun():
#print(bi.start_time, look_for_top, "DOWN 3")
else:
last_seg.set_end_bi(bi_list[last_up_sbi.start_bi.index - 1], bi)
seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP)
seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP, bi)
#print(last_up_sbi.start_bi.start_time)
last_seg.set_next(seg)
seg.set_pre(last_seg)
@@ -414,14 +414,14 @@ class ChanLun():
else:
if bi.check_overlap():
if bi.dir == Chan_BI_DIR.UP:
seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.UP)
seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.UP, bi)
last_up_bi = bi
last_up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir)
seg_list.append(seg)
last_seg = seg
#print(bi.start_time, 'Create first UP SEG')
else:
seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.DOWN)
seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.DOWN, bi)
last_down_bi = bi
last_down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir)
seg_list.append(seg)
@@ -448,7 +448,7 @@ class ChanLun():
# The confirmed
print("Last UP seg is broken, create a new seg. 1")
seg.pre_set_end_bi(bi_list[i])
seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.DOWN)
seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.DOWN, bi)
seg_list.append(seg)
last_seg = seg_list[-2]
if len(last_seg.bi_list) > 3:
@@ -460,7 +460,7 @@ class ChanLun():
if bi_list[i].low < last_seg_peak:
print("Last DOWN seg is broken, create a new seg. 1")
seg.pre_set_end_bi(bi_list[i])
seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.UP)
seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.UP, bi)
seg_list.append(seg)
last_seg = seg_list[-2]
if len(last_seg.bi_list) > 3:
@@ -478,12 +478,13 @@ class ChanLun():
if bi_list[i].high > last_seg_peak:
print("Last seg is broken, create a new seg. 2")
last_seg.pre_set_end_bi(bi_list[i-1])
seg = ChanSEG(bi_list[i], len(seg_list), Chan_SEG_DIR.UP)
seg = ChanSEG(bi_list[i], len(seg_list), Chan_SEG_DIR.UP, bi)
seg_list.append(seg)
last_seg = seg
last_seg_bi = bi_list[i]
break
"""
self.cal_bi_zs(seg_list)
return seg_list
def cal_trend(self, klc_list):
"""
@@ -1170,7 +1171,7 @@ class ChanLun():
# SEG is not in ZS
if seg.is_sure:
if ((seg.low > last_zs.zg and seg.high > last_zs.zg) or (seg.high < last_zs.zd and seg.low < last_zs.zd)):
last_zs.set_end_klc(seg.pre.pre.end_bi.end_klc, seg.sure_time, bi_out_count, seg)
last_zs.set_end_klc(seg.pre.end_bi.end_klc, seg.sure_time, bi_out_count, seg.pre)
bi_out_count = 0
#print(seg.start_bi.start_klc.start_time)
first_bi_out = None
@@ -1285,6 +1286,13 @@ class ChanLun():
bsp_list.append(bsp)
#self.print_zs(zs_list)
return zs_list
def cal_bi_zs(self, seg_list):
bi_zs_list = []
for seg in seg_list:
zs_list = seg.cal_bi_zs()
if len(zs_list) > 0:
bi_zs_list.append(zs_list)
return bi_zs_list
def get_decimal(self, value):
return Decimal("{:.2f}".format(value))
@@ -1297,19 +1305,33 @@ class ChanLun():
for klu in klu_list:
if len(klc_list) > 0:
last_klc = klc_list[-1]
included = last_klc.check_klu_included(klu)
if not included:
if klu.exception:
ddir = Chan_KLINE_DIR.DOWN
if last_klc.high < klu.high:
ddir = Chan_KLINE_DIR.UP
klc = ChanKLC(klu, index=len(klc_list), ddir=ddir)
klc.high = klu.close if klu.close > klu.open else klu.open
klc.low = klu.open if klu.close > klu.open else klu.close
klc_list.append(klc)
last_klc.set_next(klc)
klc.set_pre(last_klc)
last_klc.set_end_klu(last_klu)
klc.set_pre_fx()
#print(klu.time, klu.high, klu.low, klu.close, klu.open, klu.exception)
else:
last_klc.add_klu(klu)
included = last_klc.check_klu_included(klu)
if not included:
ddir = Chan_KLINE_DIR.DOWN
if last_klc.high < klu.high:
ddir = Chan_KLINE_DIR.UP
klc = ChanKLC(klu, index=len(klc_list), ddir=ddir)
klc_list.append(klc)
last_klc.set_next(klc)
klc.set_pre(last_klc)
last_klc.set_end_klu(last_klu)
klc.set_pre_fx()
else:
last_klc.add_klu(klu)
else:
ddir = Chan_KLINE_DIR.UP
if klu.open > klu.close:
+107 -6
View File
@@ -1,12 +1,12 @@
import copy
from typing import Dict, Optional
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR
import ChanKLU
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR, Chan_BI_DIR, Chan_ZS_DIR
import ChanCTime
from ChanBI import ChanBI
from ChanBIZS import ChanBIZS
class ChanSEG():
def __init__(self, start_bi: ChanBI, index, ddir=Chan_SEG_DIR.UP):
def __init__(self, start_bi: ChanBI, index, ddir=Chan_SEG_DIR.UP, pre_end_bi: ChanBI = None):
self.start_bi = start_bi
self.start_time = start_bi.start_time
self.end_time = None
@@ -28,6 +28,17 @@ class ChanSEG():
self.sure_time = None
self.macd_hist = 0
self.macd_div = 0
self.start_bi.set_seg(self)
self.pre_end_bi = pre_end_bi
if self.pre_end_bi:
self.ini_seg()
def ini_seg(self):
next_bi = self.start_bi.next
for index in range(self.start_bi.index+1, self.pre_end_bi.index):
if next_bi:
self.bi_list.append(next_bi)
next_bi.set_seg(self)
next_bi = next_bi.next
def set_macdhist(self, macd_hist):
self.macd_hist = macd_hist
def set_macd_div(self, macd_div):
@@ -63,14 +74,104 @@ class ChanSEG():
self.is_sure = True
self.format_bi_list()
def format_bi_list(self):
bi_list = []
bi_list.append(self.start_bi)
self.bi_list = []
self.bi_list.append(self.start_bi)
if self.end_bi:
next_bi = self.start_bi.next
for i in range(self.start_bi.index, self.end_bi.index):
if next_bi:
self.bi_list.append(next_bi)
next_bi.set_seg(self)
next_bi = next_bi.next
def add_bi(self, bi: ChanBI):
if len(self.bi_list) > 0:
self.bi_list.append(bi)
self.bi_list.append(bi)
bi.set_seg(self)
def cal_bi_zs(self):
zs_list = []
if len(self.bi_list) > 3:
last_zs = None
zs_count = 0
if self.dir == Chan_SEG_DIR.UP:
for index in range(1, len(self.bi_list)):
bi = self.bi_list[index]
if len(zs_list) == 0 or (last_zs and last_zs.is_sure):
if bi.next and bi.next.next and bi.next.next.is_sure and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN:
zg = min(bi.high, bi.next.high, bi.next.next.high)
zd = max(bi.low, bi.next.low, bi.next.next.low)
gg = max(bi.high, bi.next.high, bi.next.next.high)
dd = min(bi.low, bi.next.low, bi.next.next.low)
zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.UP)
zs.set_zg(zg)
zs.set_zd(zd)
zs.set_gg(gg)
zs.set_dd(dd)
zs.add_bi(bi.next)
zs.add_bi(bi.next.next)
zs_list.append(zs)
last_zs = zs
else:
if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.DOWN and bi.is_sure:
if bi.low < last_zs.zg:
last_zs.add_bi(bi.pre)
last_zs.add_bi(bi)
else:
last_zs.set_end_bi(last_zs.bi_list[-1], bi)
if bi.next and bi.next.next and bi.next.next.is_sure and bi.next.next.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN:
zg = min(bi.high, bi.next.high, bi.next.next.high)
zd = max(bi.low, bi.next.low, bi.next.next.low)
gg = max(bi.high, bi.next.high, bi.next.next.high)
dd = min(bi.low, bi.next.low, bi.next.next.low)
zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.UP)
zs.set_zg(zg)
zs.set_zd(zd)
zs.set_gg(gg)
zs.set_dd(dd)
zs.add_bi(bi.next)
zs.add_bi(bi.next.next)
zs_list.append(zs)
last_zs = zs
else:
for index in range(1, len(self.bi_list)):
bi = self.bi_list[index]
if len(zs_list) == 0 or (last_zs and last_zs.is_sure):
if bi.next and bi.next.next and bi.next.next.is_sure and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP:
zg = min(bi.high, bi.next.high, bi.next.next.high)
zd = max(bi.low, bi.next.low, bi.next.next.low)
gg = max(bi.high, bi.next.high, bi.next.next.high)
dd = min(bi.low, bi.next.low, bi.next.next.low)
zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.DOWN)
zs.set_zg(zg)
zs.set_zd(zd)
zs.set_gg(gg)
zs.set_dd(dd)
zs.add_bi(bi.next)
zs.add_bi(bi.next.next)
zs_list.append(zs)
last_zs = zs
else:
if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.UP and bi.is_sure:
if bi.high > last_zs.zd:
last_zs.add_bi(bi.pre)
last_zs.add_bi(bi)
else:
last_zs.set_end_bi(last_zs.bi_list[-1], bi)
if bi.next and bi.next.next and bi.next.next.is_sure and bi.next.next.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP:
zg = min(bi.high, bi.next.high, bi.next.next.high)
zd = max(bi.low, bi.next.low, bi.next.next.low)
gg = max(bi.high, bi.next.high, bi.next.next.high)
dd = min(bi.low, bi.next.low, bi.next.next.low)
zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.DOWN)
zs.set_zg(zg)
zs.set_zd(zd)
zs.set_gg(gg)
zs.set_dd(dd)
zs.add_bi(bi.next)
zs.add_bi(bi.next.next)
zs_list.append(zs)
last_zs = zs
if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure:
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1])
#print(self.start_time, len(zs_list))
print(self.bi_list[-1].end_time, "end_bi")
return zs_list
+68 -697
View File
@@ -125,7 +125,7 @@ def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=
# 添加请求计数和最大限制
request_count = 0
max_requests = 50 # 最大请求次数,防止无限循环
max_requests = 300 # 最大请求次数,防止无限循环
# 分页加载数据
while request_count < max_requests:
@@ -188,8 +188,8 @@ def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=
# 限制数据条数的逻辑 - 优先考虑时间范围
if start_time and end_time:
# 如果指定了明确的时间范围,返回该时间范围内的所有数据
if len(df) > 10000: # 防止数据量过大,设置一个合理的上限
df = df.tail(10000).reset_index(drop=True)
if len(df) > 100000: # 防止数据量过大,设置一个合理的上限
df = df.tail(100000).reset_index(drop=True)
elif limit and len(df) > limit:
# 如果没有指定明确时间范围,使用默认的limit限制
df = df.tail(limit).reset_index(drop=True)
@@ -391,8 +391,13 @@ def analyze_chan(df, symbol=None, timeframe=None):
#print(bi_list[index].start_time, bi_list[index].start_klc.end_time, bi_list[index].dir)
seg_list = chan.get_seg_list(bi_list)
zs_list = chan.calculate_zs(bi_list, seg_list)
# 计算笔中枢(BI中枢)并拍平成列表
try:
bi_zs_nested = chan.cal_bi_zs(seg_list)
bi_zs_list = [zs for group in bi_zs_nested for zs in (group or [])] if bi_zs_nested else []
except Exception:
bi_zs_list = []
# 添加买卖点识别
buy_sell_points = identify_trade_points(bi_list, seg_list, zs_list)
for bi in bi_list:
bi.cal_macdhist()
for bi in bi_list:
@@ -513,63 +518,6 @@ def analyze_chan(df, symbol=None, timeframe=None):
'is_strong_fx': False
})
# 提取KLU分型信息
klu_fx_info = []
for klu in klu_list:
if hasattr(klu, 'fx_type') and klu.fx_type != Chan_FX_TYPE.UNKNOWN:
try:
# 计算分型强度
fx_strength = 0
fx_strength_level = ""
is_strong_fx = False
# 尝试调用分型强度计算方法
if hasattr(klu, 'calculate_realtime_fx_strength'):
fx_strength = klu.calculate_realtime_fx_strength()
elif hasattr(klu, 'fx_strength'):
fx_strength = klu.fx_strength
# 尝试获取分型强度等级 - 基于强度值生成等级
if fx_strength >= 2:
fx_strength_level = ""
is_strong_fx = True
elif fx_strength >= 1:
fx_strength_level = ""
is_strong_fx = False
elif fx_strength >= 0:
fx_strength_level = ""
is_strong_fx = False
else:
fx_strength_level = "极弱"
is_strong_fx = False
# 确保分型确认状态
is_confirmed = getattr(klu, 'fx_confirmed', True)
klu_fx_info.append({
'time': klu.time,
'price': klu.low if klu.fx_type == Chan_FX_TYPE.BOTTOM else klu.high,
'fx_type': str(klu.fx_type).replace("Chan_FX_TYPE.", ""),
'is_bottom': klu.fx_type == Chan_FX_TYPE.BOTTOM,
'fx_strength': fx_strength, # 分型强度分数
'fx_strength_level': fx_strength_level, # 分型强度等级
'is_strong_fx': is_strong_fx, # 是否为强分型
'fx_confirmed': is_confirmed # 分型是否确认
})
except Exception as e:
# 如果出错,仍然添加基本信息,但分型强度为0
klu_fx_info.append({
'time': klu.time,
'price': klu.low if klu.fx_type == Chan_FX_TYPE.BOTTOM else klu.high,
'fx_type': str(klu.fx_type).replace("Chan_FX_TYPE.", ""),
'is_bottom': klu.fx_type == Chan_FX_TYPE.BOTTOM,
'fx_strength': 0,
'fx_strength_level': "",
'is_strong_fx': False,
'fx_confirmed': False
})
return {
'klc_list': klc_list,
@@ -577,474 +525,12 @@ def analyze_chan(df, symbol=None, timeframe=None):
'bi_list': bi_list,
'seg_list': seg_list,
'zs_list': zs_list,
'trade_points': buy_sell_points,
'bi_zs_list': bi_zs_list, # 添加BI中枢列表
'klc_fx_info': klc_fx_info, # KLC分型信息
'klu_fx_info': klu_fx_info, # 添加KLU分型信息
'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):
"""生成逐步计算的回放数据"""
replay_data = {}
# 预先获取完整的次周期数据(避免重复数据获取)
element_full_data = None
if element_timeframe and symbol:
# 一次性获取完整的次周期数据
element_full_data = get_kl_data(symbol, element_timeframe, start_time=start_time, end_time=end_time)
if element_full_data is not None and len(element_full_data) > 0:
# 一次性添加技术指标
element_full_data = add_indicators(element_full_data)
# 为每个K线索引计算分析结果
for i in range(1, len(df) + 1): # 从1开始,至少需要1根K线
try:
# 截取到当前索引的数据
current_df = df.iloc[:i].copy()
# 添加技术指标
current_df = add_indicators(current_df)
# 进行缠论分析
analysis_result = analyze_chan(current_df, symbol, timeframe)
# 计算MACD
macd_data = calculate_macd(current_df)
# 如果有次周期数据,筛选对应时间范围的数据
element_step_data = {}
if element_full_data is not None:
# 获取当前主周期时间范围
current_end_time = current_df['timestamp'].iloc[-1] if len(current_df) > 0 else None
if current_end_time:
# 筛选次周期数据:只取时间戳小于等于当前主周期结束时间的数据
element_current_df = element_full_data[element_full_data['timestamp'] <= current_end_time].copy()
if len(element_current_df) > 0:
# 重新对当前时间范围的次周期数据进行缠论分析
# 这样可以确保数据的准确性,避免时间筛选的复杂性
element_current_analysis = analyze_chan(element_current_df, symbol, element_timeframe)
# 直接使用分析结果,无需复杂的时间筛选
filtered_bi_list = element_current_analysis['bi_list']
filtered_seg_list = element_current_analysis['seg_list']
filtered_zs_list = element_current_analysis['zs_list']
filtered_trade_points = element_current_analysis['trade_points']
filtered_klc_fx = element_current_analysis['klc_fx_info']
filtered_klu_fx = element_current_analysis['klu_fx_info']
# 计算当前时间范围的MACD
element_macd_data = calculate_macd(element_current_df)
element_step_data = {
'element_kline_data': clean_dataframe_for_json(element_current_df).to_dict('records'),
'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,
'sure_time': format_time_safely(bi.sure_time, client_tz) if bi.sure_time else None,
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
'direction': convert_direction(bi.dir),
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
} for bi in filtered_bi_list if bi.end_klc],
'element_uncompleted_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': None, # 未完成笔没有结束时间
'sure_time': format_time_safely(bi.sure_time, client_tz) if bi.sure_time else None,
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
'end_price': None, # 未完成笔没有结束价格
'direction': convert_direction(bi.dir),
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
} for bi in filtered_bi_list if not bi.end_klc],
'element_seg_list': [{
'start_time': seg.start_bi.start_klc.end_time if isinstance(seg.start_bi.start_klc.end_time, str) else seg.start_bi.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None,
'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None,
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
'direction': convert_direction(seg.dir)
} for seg in filtered_seg_list if seg.is_sure],
'element_uncompleted_seg_list': get_uncompleted_seg_list(filtered_seg_list, client_tz),
'element_zs_list': [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None,
'zg': zs.zg,
'zd': zs.zd,
'gg': zs.gg,
'dd': zs.dd,
'is_sure': zs.is_sure
} for zs in filtered_zs_list if zs.end_klc],
'element_uncompleted_zs_list': [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': None,
'zg': zs.zg,
'zd': zs.zd,
'gg': zs.gg,
'dd': zs.dd,
'is_sure': zs.is_sure
} for zs in filtered_zs_list if not zs.is_sure],
'element_trade_points': [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in filtered_trade_points],
'element_macd': element_macd_data,
'element_bollinger': {
'upper': element_current_df['bb_upper'].tolist(),
'middle': element_current_df['bb_middle'].tolist(),
'lower': element_current_df['bb_lower'].tolist()
},
'element_element_bollinger': {
'upper': element_current_df['element_bb_upper'].tolist(),
'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']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']),
'fx_strength_level': str(point['fx_strength_level']),
'is_strong_fx': bool(point['is_strong_fx'])
} for point in filtered_klc_fx],
'element_klu_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']),
'fx_strength_level': str(point['fx_strength_level']),
'is_strong_fx': bool(point['is_strong_fx']),
'fx_confirmed': bool(point['fx_confirmed'])
} for point in filtered_klu_fx]
}
# 构建该索引对应的分析结果
step_data = {
'step_index': i-1, # 当前步骤索引
'total_steps': len(df), # 总步骤数
'has_element_data': element_timeframe is not None and len(element_step_data) > 0, # 是否包含次周期数据
'element_timeframe': element_timeframe, # 次周期时间框架
'kline_data': clean_dataframe_for_json(current_df).to_dict('records'),
'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,
'sure_time': format_time_safely(bi.sure_time, client_tz) if bi.sure_time else None,
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
'direction': convert_direction(bi.dir),
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
} for bi in analysis_result['bi_list'] if bi.end_klc],
'uncompleted_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': None, # 未完成笔没有结束时间
'sure_time': format_time_safely(bi.sure_time, client_tz) if bi.sure_time else None,
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
'end_price': None, # 未完成笔没有结束价格
'direction': convert_direction(bi.dir),
'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0
} for bi in analysis_result['bi_list'] if not bi.end_klc],
'seg_list': [{
'start_time': seg.start_bi.start_klc.end_time if isinstance(seg.start_bi.start_klc.end_time, str) else seg.start_bi.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None,
'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None,
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
'direction': convert_direction(seg.dir)
} for seg in analysis_result['seg_list'] if seg.is_sure],
'uncompleted_seg_list': get_uncompleted_seg_list(analysis_result['seg_list'], client_tz),
'zs_list': [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None,
'zg': zs.zg,
'zd': zs.zd,
'gg': zs.gg,
'dd': zs.dd,
'is_sure': zs.is_sure
} for zs in analysis_result['zs_list'] if zs.end_klc],
'uncompleted_zs_list': [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': None,
'zg': zs.zg,
'zd': zs.zd,
'gg': zs.gg,
'dd': zs.dd,
'is_sure': zs.is_sure
} for zs in analysis_result['zs_list'] if not zs.is_sure],
'trade_points': [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in analysis_result['trade_points']],
'macd': macd_data,
'bollinger': {
'upper': current_df['bb_upper'].tolist(),
'middle': current_df['bb_middle'].tolist(),
'lower': current_df['bb_lower'].tolist()
},
'element_bollinger': {
'upper': current_df['element_bb_upper'].tolist(),
'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']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型
} for point in analysis_result['klc_fx_info']],
'klu_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']), # 是否为强分型
'fx_confirmed': bool(point['fx_confirmed']) # 分型是否确认
} for point in analysis_result['klu_fx_info']]
}
# 合并次周期数据到step_data中,如果没有次周期数据则提供空的占位符
if element_step_data:
step_data.update(element_step_data)
else:
# 提供空的次周期数据结构,确保前端可以统一处理
step_data.update({
'element_kline_data': [],
'element_bi_list': [],
'element_uncompleted_bi_list': [],
'element_seg_list': [],
'element_uncompleted_seg_list': [],
'element_zs_list': [],
'element_uncompleted_zs_list': [],
'element_trade_points': [],
'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': []
})
replay_data[i-1] = step_data # 使用0-based索引
except Exception as e:
continue
return replay_data
def identify_trade_points(bi_list, seg_list, zs_list):
"""识别缠论买卖点 - 多级别识别,减少滞后性"""
trade_points = []
# 1. 基于笔的二三类买卖点识别(更及时)
trade_points.extend(identify_bi_trade_points(bi_list, zs_list))
# 2. 基于线段的一类买卖点识别(传统方法)
trade_points.extend(identify_seg_trade_points(seg_list))
# 3. 基于分型强度的预警点识别(最及时)
trade_points.extend(identify_fx_warning_points(bi_list))
# 4. 基于MACD背驰的买卖点识别
trade_points.extend(identify_macd_divergence_points(bi_list))
# 按时间排序
trade_points.sort(key=lambda x: x['time'])
return trade_points
def identify_bi_trade_points(bi_list, zs_list):
"""基于笔识别二三类买卖点 - 更及时的信号"""
trade_points = []
if len(bi_list) < 3:
return trade_points
# 构建中枢映射,便于快速查找
zs_map = {}
for zs in zs_list:
if zs.is_sure: # 只考虑已确认的中枢
zs_map[zs.start_klc.end_time] = zs
for i in range(2, len(bi_list)):
current_bi = bi_list[i]
prev_bi = bi_list[i-1]
prev_prev_bi = bi_list[i-2]
# 确保笔已完成
if not current_bi.end_klc or not prev_bi.end_klc or not prev_prev_bi.end_klc:
continue
# 二类买点:向下笔后的向上笔,且不创新低
if (convert_direction(prev_bi.dir) == -1 and
convert_direction(current_bi.dir) == 1):
prev_low = prev_bi.end_klc.low
current_end_price = current_bi.end_klc.high
# 检查是否不创新低(相对于前面的低点)
if i >= 4: # 至少需要5个笔来判断
earlier_lows = [bi.end_klc.low for bi in bi_list[max(0, i-4):i-1]
if convert_direction(bi.dir) == -1 and bi.end_klc]
if earlier_lows and prev_low > min(earlier_lows):
trade_points.append({
'type': TRADE_POINT_TYPE.BUY2,
'time': current_bi.end_klc.end_time,
'price': current_end_price,
'desc': '二类买点(笔)'
})
# 二类卖点:向上笔后的向下笔,且不创新高
if (convert_direction(prev_bi.dir) == 1 and
convert_direction(current_bi.dir) == -1):
prev_high = prev_bi.end_klc.high
current_end_price = current_bi.end_klc.low
# 检查是否不创新高(相对于前面的高点)
if i >= 4: # 至少需要5个笔来判断
earlier_highs = [bi.end_klc.high for bi in bi_list[max(0, i-4):i-1]
if convert_direction(bi.dir) == 1 and bi.end_klc]
if earlier_highs and prev_high < max(earlier_highs):
trade_points.append({
'type': TRADE_POINT_TYPE.SELL2,
'time': current_bi.end_klc.end_time,
'price': current_end_price,
'desc': '二类卖点(笔)'
})
return trade_points
def identify_seg_trade_points(seg_list):
"""基于线段识别一类买卖点 - 传统方法"""
trade_points = []
if len(seg_list) >= 3:
for i in range(2, len(seg_list)):
# 确保线段已完成
if seg_list[i].end_bi and seg_list[i-1].end_bi and seg_list[i-2].end_bi:
# 一类买点:向下-向上-向下的底分型
if (convert_direction(seg_list[i-2].dir) == -1 and
convert_direction(seg_list[i-1].dir) == 1 and
convert_direction(seg_list[i].dir) == -1):
trade_points.append({
'type': TRADE_POINT_TYPE.BUY1,
'time': seg_list[i].end_bi.end_klc.end_time,
'price': seg_list[i].end_bi.end_klc.low,
'desc': '一类买点(线段)'
})
# 一类卖点:向上-向下-向上的顶分型
if (convert_direction(seg_list[i-2].dir) == 1 and
convert_direction(seg_list[i-1].dir) == -1 and
convert_direction(seg_list[i].dir) == 1):
trade_points.append({
'type': TRADE_POINT_TYPE.SELL1,
'time': seg_list[i].end_bi.end_klc.end_time,
'price': seg_list[i].end_bi.end_klc.high,
'desc': '一类卖点(线段)'
})
return trade_points
def identify_fx_warning_points(bi_list):
"""基于分型强度识别预警点 - 最及时的信号"""
trade_points = []
if len(bi_list) < 2:
return trade_points
# 检查最近的几个笔
recent_bis = bi_list[-3:] if len(bi_list) >= 3 else bi_list
for bi in recent_bis:
if not bi.end_klc:
continue
# 获取分型强度(如果有的话)
fx_strength = 0
if hasattr(bi.end_klc, 'cal_fx_strength'):
try:
fx_strength = bi.end_klc.cal_fx_strength(5)
except:
fx_strength = 0
# 强分型预警(分型强度>=2
if fx_strength >= 2:
if convert_direction(bi.dir) == -1: # 向下笔结束,可能的底部
trade_points.append({
'type': TRADE_POINT_TYPE.BUY3,
'time': bi.end_klc.end_time,
'price': bi.end_klc.low,
'desc': f'强分型预警-买点(强度:{fx_strength})'
})
elif convert_direction(bi.dir) == 1: # 向上笔结束,可能的顶部
trade_points.append({
'type': TRADE_POINT_TYPE.SELL3,
'time': bi.end_klc.end_time,
'price': bi.end_klc.high,
'desc': f'强分型预警-卖点(强度:{fx_strength})'
})
return trade_points
def identify_macd_divergence_points(bi_list):
"""基于MACD背驰识别买卖点"""
trade_points = []
if len(bi_list) < 4:
return trade_points
# 检查最近的笔是否有背驰
for i in range(2, len(bi_list)):
current_bi = bi_list[i]
if not current_bi.end_klc or not hasattr(current_bi, 'macd_div'):
continue
# MACD背驰阈值
divergence_threshold = 0.3
# 向下笔的底背驰 -> 买点
if (convert_direction(current_bi.dir) == -1 and
hasattr(current_bi, 'macd_div') and
current_bi.macd_div > divergence_threshold):
trade_points.append({
'type': TRADE_POINT_TYPE.BUY2,
'time': current_bi.end_klc.end_time,
'price': current_bi.end_klc.low,
'desc': f'MACD底背驰买点(背驰度:{current_bi.macd_div:.2f})'
})
# 向上笔的顶背驰 -> 卖点
elif (convert_direction(current_bi.dir) == 1 and
hasattr(current_bi, 'macd_div') and
current_bi.macd_div > divergence_threshold):
trade_points.append({
'type': TRADE_POINT_TYPE.SELL2,
'time': current_bi.end_klc.end_time,
'price': current_bi.end_klc.high,
'desc': f'MACD顶背驰卖点(背驰度:{current_bi.macd_div:.2f})'
})
return trade_points
# 辅助函数,转换缠论方向枚举为整数
def convert_direction(direction):
"""转换方向枚举为数字"""
@@ -1719,6 +1205,20 @@ def analyze():
'dd': zs.dd,
'is_sure': zs.is_sure # 添加中枢是否完成的标志
} for zs in analysis_result['zs_list'] if zs.end_klc],
# 添加主周期BI中枢列表(已完成)
'bi_zs_list': [{
'start_time': (
(zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat())
if getattr(zs.start_klc, 'end_time', None) else
(zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())
),
'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None,
'zg': zs.zg,
'zd': zs.zd,
'gg': zs.gg,
'dd': zs.dd,
'is_sure': bool(getattr(zs, 'is_sure', False))
} for zs in analysis_result.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)],
# 添加未完成中枢列表
'uncompleted_zs_list': [{
'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(),
@@ -1729,12 +1229,21 @@ def analyze():
'dd': zs.dd,
'is_sure': zs.is_sure # 未完成中枢的is_sure为False
} for zs in analysis_result['zs_list'] if not zs.is_sure],
'trade_points': [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in analysis_result['trade_points']],
# 添加未完成BI中枢列表
'uncompleted_bi_zs_list': [{
'start_time': (
(zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat())
if getattr(zs.start_klc, 'end_time', None) else
(zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())
),
'end_time': None,
'zg': zs.zg,
'zd': zs.zd,
'gg': zs.gg,
'dd': zs.dd,
'is_sure': bool(getattr(zs, 'is_sure', False))
} for zs in analysis_result.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)],
'macd': macd_data,
# 添加布林带数据
'bollinger': {
@@ -1759,16 +1268,6 @@ def analyze():
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型
} for point in analysis_result['klc_fx_info']],
'klu_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']), # 是否为强分型
'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),
# 添加多时间周期EMA52数据
@@ -1885,13 +1384,35 @@ def analyze():
'dd': zs.dd,
'is_sure': zs.is_sure # 未完成中枢的is_sure为False
} for zs in element_analysis['zs_list'] if not zs.is_sure]
# 添加次周期 BI 中枢(已完成/未完成)
result['element_bi_zs_list'] = [{
'start_time': (
(zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat())
if getattr(zs.start_klc, 'end_time', None) else
(zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())
),
'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None,
'zg': zs.zg,
'zd': zs.zd,
'gg': zs.gg,
'dd': zs.dd,
'is_sure': bool(getattr(zs, 'is_sure', False))
} for zs in element_analysis.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)]
result['element_uncompleted_bi_zs_list'] = [{
'start_time': (
(zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat())
if getattr(zs.start_klc, 'end_time', None) else
(zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())
),
'end_time': None,
'zg': zs.zg,
'zd': zs.zd,
'gg': zs.gg,
'dd': zs.dd,
'is_sure': bool(getattr(zs, 'is_sure', False))
} for zs in element_analysis.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)]
result['element_trade_points'] = [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in element_analysis['trade_points']]
# 添加小周期分型信息
result['element_klc_fx_info'] = [{
@@ -1903,18 +1424,7 @@ def analyze():
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型
} for point in element_analysis['klc_fx_info']]
result['element_klu_fx_info'] = [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']), # 分型强度分数
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
'is_strong_fx': bool(point['is_strong_fx']), # 是否为强分型
'fx_confirmed': bool(point['fx_confirmed']) # 分型是否确认
} for point in element_analysis['klu_fx_info']]
# 添加次周期ChanMACD分析数据
result['element_chan_macd'] = serialize_chan_macd_data(element_analysis.get('chan_macd', {}), client_tz)
@@ -1990,145 +1500,6 @@ def search_stock():
except Exception as e:
return jsonify({'error': str(e)})
@app.route('/api/test_element_data')
def test_element_data():
"""测试次周期数据是否正确生成"""
try:
symbol = request.args.get('symbol', 'SOL/USDT:USDT')
timeframe = request.args.get('timeframe', '1h')
element_timeframe = request.args.get('element_timeframe', '15m')
# 获取主周期数据
main_df = get_kl_data(symbol, timeframe, limit=3)
if main_df is None or len(main_df) == 0:
return jsonify({'error': '无法获取主周期数据'})
# 获取次周期数据
element_df = get_kl_data(symbol, element_timeframe,
start_time=main_df['timestamp'].iloc[0],
end_time=main_df['timestamp'].iloc[-1])
if element_df is None or len(element_df) == 0:
return jsonify({'error': '无法获取次周期数据'})
# 分析次周期数据
element_df = add_indicators(element_df)
element_analysis = analyze_chan(element_df, symbol, element_timeframe)
return jsonify({
'main_data_count': len(main_df),
'element_data_count': len(element_df),
'element_analysis': {
'bi_count': len(element_analysis['bi_list']),
'seg_count': len(element_analysis['seg_list']),
'zs_count': len(element_analysis['zs_list']),
'klc_fx_count': len(element_analysis['klc_fx_info']),
'klu_fx_count': len(element_analysis['klu_fx_info']),
'trade_points_count': len(element_analysis['trade_points'])
},
'sample_bi': [{'has_end_klc': bi.end_klc is not None,
'direction': convert_direction(bi.dir)}
for bi in element_analysis['bi_list'][:2]] if len(element_analysis['bi_list']) > 0 else [],
'sample_klc_fx': element_analysis['klc_fx_info'][:3] if len(element_analysis['klc_fx_info']) > 0 else [],
'sample_klu_fx': element_analysis['klu_fx_info'][:3] if len(element_analysis['klu_fx_info']) > 0 else []
})
except Exception as e:
import traceback
return jsonify({'error': str(e), 'traceback': traceback.format_exc()})
@app.route('/api/debug_replay_sample')
def debug_replay_sample():
"""调试接口:返回回放数据样本,方便前端调试"""
try:
symbol = request.args.get('symbol', 'SOL/USDT:USDT')
timeframe = request.args.get('timeframe', '1h')
element_timeframe = request.args.get('element_timeframe', '15m')
step = int(request.args.get('step', 2)) # 返回第几步的数据
# 获取少量数据进行测试
df = get_kl_data(symbol, timeframe, limit=5)
if df is None or len(df) == 0:
return jsonify({'error': '无法获取测试数据'})
# 生成回放数据
client_tz = timezone('Asia/Shanghai')
replay_data = generate_replay_data(
df, client_tz, symbol, element_timeframe,
start_time=None, end_time=None
)
if step not in replay_data:
return jsonify({'error': f'步骤 {step} 不存在,可用步骤:{list(replay_data.keys())}'})
# 返回指定步骤的完整数据
step_data = replay_data[step]
return jsonify({
'step': step,
'data': step_data,
'summary': {
'has_element_data': step_data.get('has_element_data', False),
'element_timeframe': step_data.get('element_timeframe'),
'main_bi_count': len(step_data.get('bi_list', [])),
'main_klc_fx_count': len(step_data.get('klc_fx_info', [])),
'main_klu_fx_count': len(step_data.get('klu_fx_info', [])),
'element_bi_count': len(step_data.get('element_bi_list', [])),
'element_klc_fx_count': len(step_data.get('element_klc_fx_info', [])),
'element_klu_fx_count': len(step_data.get('element_klu_fx_info', [])),
'element_kline_count': len(step_data.get('element_kline_data', []))
}
})
except Exception as e:
return jsonify({'error': str(e)})
@app.route('/api/debug_replay_structure')
def debug_replay_structure():
"""调试接口:检查回放数据结构"""
try:
# 获取一个简单的测试案例
symbol = request.args.get('symbol', 'SOL/USDT:USDT')
timeframe = request.args.get('timeframe', '1h')
element_timeframe = request.args.get('element_timeframe', '15m')
# 获取少量数据进行测试
df = get_kl_data(symbol, timeframe, limit=5) # 只取5根K线
if df is None or len(df) == 0:
return jsonify({'error': '无法获取测试数据'})
# 生成回放数据
client_tz = timezone('Asia/Shanghai')
replay_data = generate_replay_data(
df, client_tz, symbol, element_timeframe,
start_time=None, end_time=None
)
# 返回结构信息
result = {
'total_steps': len(replay_data),
'sample_step_keys': list(replay_data[0].keys()) if len(replay_data) > 0 else [],
'has_element_data_in_steps': [],
'element_data_counts': {}
}
# 检查每个步骤的次周期数据
for step_idx, step_data in replay_data.items():
has_element = step_data.get('has_element_data', False)
result['has_element_data_in_steps'].append({
'step': step_idx,
'has_element_data': has_element,
'element_bi_count': len(step_data.get('element_bi_list', [])),
'element_klc_fx_count': len(step_data.get('element_klc_fx_info', [])),
'element_klu_fx_count': len(step_data.get('element_klu_fx_info', []))
})
return jsonify(result)
except Exception as e:
return jsonify({'error': str(e)})
# 已移除:/api/filter_stocks 路由
def get_uncompleted_seg_list(seg_list, client_tz):
"""获取未完成线段列表,正确处理倒数第二个和最后一个未完成线段"""
+1 -1
View File
@@ -122,7 +122,7 @@ table.dataTable tbody tr:hover {
}
.chart-container {
height: 400px;
height: 800px;
}
.controls .row {
+275 -519
View File
@@ -48,9 +48,10 @@
margin-bottom: 6px;
color: #495057;
}
/*主图大小*/
.chart-container {
width: 100%;
height: 900px;
height: 1200px;
margin-top: 10px;
border: 1px solid #e9ecef;
border-radius: 8px;
@@ -635,9 +636,6 @@
margin-right: 4px;
display: inline-block;
}
.bb-config-modal {
position: fixed;
top: 0;
@@ -877,22 +875,22 @@
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainZs">
<label class="form-check-label" for="showMainZs">中枢</label>
<label class="form-check-label" for="showMainZs">SEG中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainUncompletedZs">
<label class="form-check-label" for="showMainUncompletedZs">未完成中枢</label>
<input class="form-check-input" type="checkbox" id="showMainBiZs">
<label class="form-check-label" for="showMainBiZs">BI中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showKlcFxType" checked>
<label class="form-check-label" for="showKlcFxType">KLC分型</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainTrend" checked>
<input class="form-check-input" type="checkbox" id="showMainTrend">
<label class="form-check-label" for="showMainTrend">Trend</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="toggleUOnMain" checked>
<input class="form-check-input" type="checkbox" id="toggleUOnMain">
<label class="form-check-label" for="toggleUOnMain">显示U</label>
</div>
</div>
@@ -915,11 +913,11 @@
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementZs">
<label class="form-check-label" for="showElementZs">中枢</label>
<label class="form-check-label" for="showElementZs">SEG中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementUncompletedZs">
<label class="form-check-label" for="showElementUncompletedZs">未完成中枢</label>
<input class="form-check-input" type="checkbox" id="showElementBiZs">
<label class="form-check-label" for="showElementBiZs">BI中枢</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementKlcFxType">
@@ -1712,9 +1710,10 @@
$('#showMainZs').change(function() {
updateChartDisplay();
});
// 添加未完成中枢复选框变更事件
$('#showMainUncompletedZs').change(function() {
// 添加主周期BI中枢复选框变更事件(委托绑定,避免DOM更新后失效)
console.log('初始化BI中枢事件绑定');
$(document).on('change', '#showMainBiZs', function() {
console.log('主BI中枢切换为:', $('#showMainBiZs').is(':checked'));
updateChartDisplay();
});
@@ -1740,9 +1739,9 @@
$('#showElementZs').change(function() {
updateChartDisplay();
});
// 添加未完成中枢复选框变更事件
$('#showElementUncompletedZs').change(function() {
// 添加次周期BI中枢复选框变更事件(委托绑定,避免DOM更新后失效)
$(document).on('change', '#showElementBiZs', function() {
console.log('次BI中枢切换为:', $('#showElementBiZs').is(':checked'));
updateChartDisplay();
});
@@ -1843,11 +1842,16 @@
}
console.log('更新图表显示');
console.log('BI中枢输入状态:', {
main: $('#showMainBiZs').is(':checked'),
element: $('#showElementBiZs').is(':checked')
});
console.log('- 显示K线:', $('#showOriginalKline').is(':checked'));
console.log('- 显示笔:', $('#showMainBi').is(':checked'));
console.log('- 显示线段:', $('#showMainSeg').is(':checked'));
console.log('- 显示中枢:', $('#showMainZs').is(':checked'));
console.log('- 显示未完成中枢:', $('#showMainUncompletedZs').is(':checked'));
console.log('- 显示BI中枢(主):', $('#showMainBiZs').is(':checked'));
console.log('- 显示BI中枢(次):', $('#showElementBiZs').is(':checked'));
console.log('- 显示MACD:', true);
console.log('- 显示成交量:', false);
console.log('- 显示分型类型:', $('#showKlcFxType').is(':checked'));
@@ -2033,6 +2037,11 @@
// 检查和记录服务器返回的时区
console.log('服务器返回的时区:', data.timezone || '未指定');
// BI中枢调试输出
console.log('主周期BI中枢(完成):', Array.isArray(data.bi_zs_list) ? data.bi_zs_list.length : 0);
console.log('主周期BI中枢(未完成):', Array.isArray(data.uncompleted_bi_zs_list) ? data.uncompleted_bi_zs_list.length : 0);
console.log('次周期BI中枢(完成):', Array.isArray(data.element_bi_zs_list) ? data.element_bi_zs_list.length : 0);
console.log('次周期BI中枢(未完成):', Array.isArray(data.element_uncompleted_bi_zs_list) ? data.element_uncompleted_bi_zs_list.length : 0);
// 刷新图表
refreshChart(data);
@@ -2077,7 +2086,6 @@
} catch (e) {
console.warn('释放旧图表资源失败(可忽略):', e);
}
console.log('初始化TradingView图表:', symbol, timeframe);
// 获取当前交易对的配置
@@ -2624,7 +2632,6 @@
console.log('处理后的ATR数据点数:', atrData.length);
console.log('ATR数据样本:', atrData.slice(0, 5));
}
console.log('处理后的ATR数据点数:', atrData.length);
atrLineSeries.setData(atrData);
tvWidget.series.atrLineSeries = atrLineSeries;
@@ -3851,7 +3858,7 @@
} else {
console.log('绘制线段 - 已禁用');
}
// 显示中枢的绘制 - 分别处理主周期和次周期
// 显示中枢的绘制 - 分别处理主周期和次周期(包含BI中枢,沿用同样样式与开关)
if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked')) {
console.log('绘制中枢 - 已启用');
@@ -4133,12 +4140,199 @@
} else {
console.log('绘制中枢 - 已禁用');
}
// BI中枢(已完成)- 使用独立的BI开关
if ($('#showMainBiZs').is(':checked') && currentData.bi_zs_list && currentData.bi_zs_list.length > 0) {
try {
console.log(`绘制主周期BI中枢数据,共${currentData.bi_zs_list.length}条`);
} catch (e) {}
currentData.bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#F1C40F';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('主周期BI中枢处理出错:', e); }
});
}
if ($('#showElementBiZs').is(':checked') && currentData.element_bi_zs_list && currentData.element_bi_zs_list.length > 0) {
try {
console.log(`绘制次周期BI中枢数据,共${currentData.element_bi_zs_list.length}条`);
} catch (e) {}
currentData.element_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#3f51b5';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('次周期BI中枢处理出错:', e); }
});
}
// 显示未完成中枢 - 分别处理主周期和次周期
if ($('#showMainUncompletedZs').is(':checked') || $('#showElementUncompletedZs').is(':checked')) {
if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked')) {
console.log('绘制未完成中枢 - 已启用');
// 显示BI中枢绘制(沿用中枢样式)
if ($('#showMainBiZs').is(':checked') || $('#showElementBiZs').is(':checked')) {
console.log('绘制BI中枢 - 已启用');
// 主周期 BI 中枢
console.log('主BI开关:', $('#showMainBiZs').is(':checked'), '数据长度:', currentData.bi_zs_list ? currentData.bi_zs_list.length : 0);
if ($('#showMainBiZs').is(':checked') && currentData.bi_zs_list && currentData.bi_zs_list.length > 0) {
console.log(`绘制主周期BI中枢数据,共${currentData.bi_zs_list.length}条`);
currentData.bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#9C27B0'; // 主周期BI中枢颜色(紫色)
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('主周期BI中枢处理出错:', e); }
});
}
// 主周期 未完成 BI 中枢
console.log('主未完成BI长度:', currentData.uncompleted_bi_zs_list ? currentData.uncompleted_bi_zs_list.length : 0);
if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) {
console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`);
currentData.uncompleted_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#9C27B0';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('主周期未完成BI中枢处理出错:', e); }
});
}
// 次周期 BI 中枢
console.log('次BI开关:', $('#showElementBiZs').is(':checked'), '数据长度:', currentData.element_bi_zs_list ? currentData.element_bi_zs_list.length : 0);
if ($('#showElementBiZs').is(':checked') && currentData.element_bi_zs_list && currentData.element_bi_zs_list.length > 0) {
console.log(`绘制次周期BI中枢数据,共${currentData.element_bi_zs_list.length}条`);
currentData.element_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#8BC34A'; // 次周期BI中枢颜色(绿)
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('次周期BI中枢处理出错:', e); }
});
}
// 次周期 未完成 BI 中枢
console.log('次未完成BI长度:', currentData.element_uncompleted_bi_zs_list ? currentData.element_uncompleted_bi_zs_list.length : 0);
if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) {
console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`);
currentData.element_uncompleted_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#8BC34A';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('次周期未完成BI中枢处理出错:', e); }
});
}
}
// 主周期未完成中枢
if ($('#showMainUncompletedZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) {
if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) {
console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`);
currentData.uncompleted_zs_list.forEach(function(zs) {
@@ -4281,7 +4475,7 @@
}
// 次周期未完成中枢
if ($('#showElementUncompletedZs').is(':checked') && currentData.element_uncompleted_zs_list && currentData.element_uncompleted_zs_list.length > 0) {
if ($('#showElementZs').is(':checked') && currentData.element_uncompleted_zs_list && currentData.element_uncompleted_zs_list.length > 0) {
console.log(`绘制次周期未完成中枢数据,共${currentData.element_uncompleted_zs_list.length}条`);
currentData.element_uncompleted_zs_list.forEach(function(zs) {
@@ -4425,6 +4619,62 @@
} else {
console.log('绘制未完成中枢 - 已禁用');
}
// 未完成BI中枢 - 使用独立的BI开关
if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) {
try { console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`); } catch (e) {}
currentData.uncompleted_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#F1C40F';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('主周期未完成BI中枢处理出错:', e); }
});
}
// 次周期 未完成 BI 中枢
if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) {
try { console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`); } catch (e) {}
currentData.element_uncompleted_bi_zs_list.forEach(function(zs) {
try {
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
if (isNaN(startTime) || isNaN(endTime)) { return; }
const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
if (isNaN(zg) || isNaN(zd)) { return; }
const color = '#3f51b5';
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
if (!isNaN(gg) && gg > 0) {
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
}
if (!isNaN(dd) && dd > 0) {
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
}
} catch (e) { console.error('次周期未完成BI中枢处理出错:', e); }
});
}
// 添加买卖点标记
if ($('#showTradePoints').is(':checked')) {
console.log('绘制买卖点 - 已启用');
@@ -5044,7 +5294,6 @@
if (currentData.klu_fx_info && currentData.klu_fx_info.length > 0) {
console.log('前3个klu分型数据样本:', currentData.klu_fx_info.slice(0, 3));
}
// 收集所有主周期分型标记
const allMainFxMarkers = [];
const mainFxMarkers = []; // 用于tooltip支持
@@ -5674,17 +5923,6 @@
setDefaultTimeRange();
}
// 添加买卖点提示
// 初始化 tooltip 与 U 显示状态
window.showUOnMain = $('#toggleUOnMain').is(':checked');
window.showUOnElement = $('#toggleUOnElement').is(':checked');
setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, volumeChart, atrChart, macdChart, chanMacdChart, showMacd);
// 显示买卖点
if ($('#showTradePoints').is(':checked')) {
displayTradePoints();
}
// 更新EMA52显示
if (currentData) {
updateEMA52Display(currentData);
@@ -5962,7 +6200,6 @@
initTradingView($('#symbol').val(), $('#timeframe').val());
}
}
function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, mainChart, volumeChart, atrChart, macdChart, chanMacdChart, showMacd) {
// 防止同步过程中的无限循环
let syncInProgress = false;
@@ -6210,266 +6447,7 @@
}, 200);
});
}
function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, volumeChart, atrChart, macdChart, chanMacdChart, showMacd) {
// 调试变量
window.debugMode = true;
// 初始化 U 显示状态(主/次周期分开控制)
const isShowUMain = $('#toggleUOnMain').is(':checked');
const isShowUElement = $('#toggleUOnElement').is(':checked');
window.showUOnMain = isShowUMain;
window.showUOnElement = isShowUElement;
if (!isShowUMain && !isShowUElement) {
// 隐藏时清空子图上的 U 标记
if (tvWidget.series && tvWidget.series.chanMacdLineSeries) {
try { tvWidget.series.chanMacdLineSeries.setMarkers([]); } catch (e) {}
}
if (tvWidget.series && tvWidget.series.chanMacdSignalSeries) {
try { tvWidget.series.chanMacdSignalSeries.setMarkers([]); } catch (e) {}
}
}
// 添加买卖点悬浮提示元素
const tooltipElement = document.createElement('div');
tooltipElement.className = 'point-tooltip';
// document.body.appendChild(tooltipElement);
// 添加自定义十字线信息显示
const crosshairTooltip = document.createElement('div');
crosshairTooltip.className = 'crosshair-tooltip';
crosshairTooltip.style.position = 'absolute';
crosshairTooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
crosshairTooltip.style.color = 'white';
crosshairTooltip.style.padding = '5px 10px';
crosshairTooltip.style.borderRadius = '4px';
crosshairTooltip.style.fontSize = '12px';
crosshairTooltip.style.zIndex = '1000';
crosshairTooltip.style.pointerEvents = 'none';
crosshairTooltip.style.display = 'none';
// document.body.appendChild(crosshairTooltip);
// 添加鼠标悬停事件显示提示
if (mainChart) {
mainChart.subscribeCrosshairMove(param => {
// 十字线同步到其他图表 - 通过DOM元素绘制垂直线实现虚线延长效果
if (param.time && param.point && volumeChart) {
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());
const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
existingChanMacdLines.forEach(line => line.remove());
// 获取时间对应的坐标位置
const mainTimeCoordinate = mainChart.timeScale().timeToCoordinate(param.time);
if (mainTimeCoordinate !== null) {
// 获取主图容器的位置
const mainChartRect = mainChartContainer.getBoundingClientRect();
// 在交易量图上绘制垂直线
const volumeTimeCoordinate = volumeChart.timeScale().timeToCoordinate(param.time);
if (volumeTimeCoordinate !== null) {
const volumeChartRect = volumeChartContainer.getBoundingClientRect();
const volumeLine = document.createElement('div');
volumeLine.className = 'volume-crosshair-line';
volumeLine.style.position = 'fixed'; // 改为fixed定位
volumeLine.style.left = (volumeChartRect.left + volumeTimeCoordinate) + 'px';
volumeLine.style.top = volumeChartRect.top + 'px';
volumeLine.style.width = '1px';
volumeLine.style.height = volumeChartRect.height + 'px';
volumeLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
volumeLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
volumeLine.style.pointerEvents = 'none';
volumeLine.style.zIndex = '1000';
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);
if (macdTimeCoordinate !== null) {
const macdChartRect = macdChartContainer.getBoundingClientRect();
const macdLine = document.createElement('div');
macdLine.className = 'macd-crosshair-line';
macdLine.style.position = 'fixed'; // 改为fixed定位
macdLine.style.left = (macdChartRect.left + macdTimeCoordinate) + 'px';
macdLine.style.top = macdChartRect.top + 'px';
macdLine.style.width = '1px';
macdLine.style.height = macdChartRect.height + 'px';
macdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
macdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
macdLine.style.pointerEvents = 'none';
macdLine.style.zIndex = '1000';
document.body.appendChild(macdLine);
}
}
// 如果有ChanMACD图,也在ChanMACD图上绘制垂直线
if (showMacd && chanMacdChart && chanMacdChartContainer) {
const chanMacdTimeCoordinate = chanMacdChart.timeScale().timeToCoordinate(param.time);
if (chanMacdTimeCoordinate !== null) {
const chanMacdChartRect = chanMacdChartContainer.getBoundingClientRect();
console.log('ChanMACD图表位置(第二个位置):', {
left: chanMacdChartRect.left,
top: chanMacdChartRect.top,
width: chanMacdChartRect.width,
height: chanMacdChartRect.height,
timeCoordinate: chanMacdTimeCoordinate
});
const chanMacdLine = document.createElement('div');
chanMacdLine.className = 'chanmacd-crosshair-line';
chanMacdLine.style.position = 'fixed';
chanMacdLine.style.left = (chanMacdChartRect.left + chanMacdTimeCoordinate) + 'px';
chanMacdLine.style.top = chanMacdChartRect.top + 'px';
chanMacdLine.style.width = '1px';
chanMacdLine.style.height = chanMacdChartRect.height + 'px';
chanMacdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
chanMacdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
chanMacdLine.style.pointerEvents = 'none';
chanMacdLine.style.zIndex = '1000';
document.body.appendChild(chanMacdLine);
console.log('ChanMACD垂直线已创建(第二个位置),位置:', chanMacdLine.style.left, chanMacdLine.style.top);
} else {
console.log('ChanMACD时间坐标为空(第二个位置)');
}
} else {
console.log('ChanMACD图表条件不满足(第二个位置):', {
showMacd: showMacd,
hasChanMacdChart: !!chanMacdChart,
hasChanMacdChartContainer: !!chanMacdChartContainer
});
}
}
} catch (e) {
console.debug('十字线同步出错:', e);
}
} else {
// 当十字线离开时,清除垂直线
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());
const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
existingChanMacdLines.forEach(line => line.remove());
} catch (e) {
console.debug('清除十字线时出错:', e);
}
}
if (param.time && param.point) {
const timeStr = param.time;
const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr);
// 同时检查分型标记
const fxMarkers = (window.fxMarkers || []).filter(m => m.time === timeStr);
const allMarkers = [...markers, ...fxMarkers];
// 显示时区调试信息
if (window.debugMode) {
const timezone = $('#timezone').val();
const formattedTime = formatTimeWithTimezone(timeStr * 1000, timezone);
// 获取当前价格 - 通过param.seriesPrices获取
let priceInfo = '';
if (param.seriesPrices && param.seriesPrices.size > 0) {
// 依次从当前可能的主系列中获取价格
if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) {
const price = param.seriesPrices.get(tvWidget.series.candleSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.renkoSeries && param.seriesPrices.get(tvWidget.series.renkoSeries)) {
const price = param.seriesPrices.get(tvWidget.series.renkoSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.heikinSeries && param.seriesPrices.get(tvWidget.series.heikinSeries)) {
const price = param.seriesPrices.get(tvWidget.series.heikinSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.barSeries && param.seriesPrices.get(tvWidget.series.barSeries)) {
const price = param.seriesPrices.get(tvWidget.series.barSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) {
const price = param.seriesPrices.get(tvWidget.series.lineSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) {
const price = param.seriesPrices.get(tvWidget.series.areaSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
} else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) {
const price = param.seriesPrices.get(tvWidget.series.baselineSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
}
// 如果没有蜡烛图系列价格,尝试从区域图系列获取
else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) {
const price = param.seriesPrices.get(tvWidget.series.areaSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
}
// 如果没有蜡烛图系列价格,尝试从基线图系列获取
else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) {
const price = param.seriesPrices.get(tvWidget.series.baselineSeries);
priceInfo = `价格: ${price.toFixed(2)}`;
}
}
// 仅记录最简短的调试信息
console.debug(`十字线: ${timeStr} -> ${formattedTime} (${timezone})`);
// 显示自定义时区工具提示,包含价格信息
crosshairTooltip.innerHTML = `<div style="font-weight:bold">时间: ${formattedTime}</div>` +
(priceInfo ? `<div>${priceInfo}</div>` : '');
crosshairTooltip.style.display = 'block';
crosshairTooltip.style.left = (param.point.x + 15) + 'px';
crosshairTooltip.style.top = (param.point.y - 30) + 'px';
}
if (allMarkers.length > 0) {
// 有买卖点或分型标记,显示自定义提示
const tooltips = allMarkers.map(m => m.tooltip).join('<br><hr style="margin: 5px 0;">');
tooltipElement.innerHTML = tooltips;
tooltipElement.style.display = 'block';
tooltipElement.style.left = (param.point.x + 15) + 'px';
tooltipElement.style.top = (param.point.y + 15) + 'px';
} else {
// 隐藏提示
tooltipElement.style.display = 'none';
}
} else {
// 隐藏提示
tooltipElement.style.display = 'none';
crosshairTooltip.style.display = 'none';
}
});
// 处理图表缩放、平移等事件,隐藏提示
mainChart.timeScale().subscribeVisibleTimeRangeChange(() => {
tooltipElement.style.display = 'none';
crosshairTooltip.style.display = 'none';
});
}
}
// 辅助函数:使用指定时区格式化时间戳
function formatTimeWithTimezone(timestamp, timezone) {
try {
@@ -6487,220 +6465,6 @@
return new Date(timestamp).toLocaleString();
}
}
function displayTradePoints() {
// 优先使用小周期数据,如果不存在则使用主周期数据
const tradePointsData = currentData.element_trade_points || currentData.trade_points;
console.log(`绘制${currentData.element_trade_points ? '元素周期' : '主周期'}买卖点数据,共${tradePointsData ? tradePointsData.length : 0}条`);
// 调试信息 - 输出完整的买卖点数据
if (tradePointsData && tradePointsData.length > 0) {
console.log("买卖点数据样例:", tradePointsData[0]);
// 检查数据格式,如果time不是标准格式,进行格式化处理
const checkDataFormat = () => {
for (let i = 0; i < tradePointsData.length; i++) {
if (tradePointsData[i].time) {
// 确保时间是标准格式
try {
const timeValue = new Date(tradePointsData[i].time);
if (isNaN(timeValue.getTime())) {
console.error(`买卖点 #${i} 时间格式无效:`, tradePointsData[i].time);
}
} catch (e) {
console.error(`买卖点 #${i} 时间格式异常:`, e);
}
} else {
console.error(`买卖点 #${i} 缺少时间属性`);
}
}
};
// 执行格式检查
checkDataFormat();
// 对买卖点按时间排序,用于后续优化显示
const sortedPoints = [...tradePointsData].sort((a, b) => {
return new Date(a.time) - new Date(b.time);
});
// 记录已处理的时间点 - 按类型分开计数
const processedTimes = {};
// 创建买卖点标记系列
const buyMarkers = [];
const sellMarkers = [];
// 计数器,追踪成功和失败的处理次数
let successCount = 0;
let errorCount = 0;
sortedPoints.forEach(function(point, index) {
try {
// 检查所有必要的属性是否存在且有效
if (!point.time || !point.price || point.type === undefined) {
console.error(`买卖点 #${index} 数据不完整:`, point);
errorCount++;
return;
}
const time = Math.floor(new Date(point.time).getTime() / 1000);
const price = parseFloat(point.price);
const type = parseInt(point.type);
if (isNaN(time) || isNaN(price) || isNaN(type)) {
console.error(`买卖点 #${index} 数据格式错误:`,
{ time: isNaN(time), price: isNaN(price), type: isNaN(type) }, point);
errorCount++;
return;
}
// 获取买卖点样式
const style = TRADE_POINT_STYLE[type] || {
color: '#999999',
shape: 'circle',
text: '?',
size: 1
};
// 初始化该时间点的类型计数器
if (!processedTimes[time]) {
processedTimes[time] = {};
}
// 优化:检查是否有相同时间点和相同类型的标记,如果有,进行类型内的偏移
let stackIndex = 0;
if (processedTimes[time][type]) {
// 已经有相同时间和类型的标记,记录堆叠索引
stackIndex = processedTimes[time][type];
processedTimes[time][type]++;
} else {
// 第一次出现这个时间点的这个类型
processedTimes[time][type] = 1;
}
// 为不同类型的买卖点获取基础垂直偏移系数
const baseOffset = TRADE_POINT_OFFSET[type] || 0;
// 创建标记对象,包含额外的信息用于悬停提示
const marker = {
time: time,
position: 'inBar', // 改为在K线内部显示,不影响数据
color: style.color,
shape: style.shape,
text: style.text,
size: style.size,
// 记录堆叠索引
stackIndex: stackIndex,
// 添加悬停提示的数据
tooltip: `<span class="${type > 0 ? 'buy-point' : 'sell-point'}">${point.desc || (type > 0 ? '买点' : '卖点')}</span><br>
时间: ${formatTime(point.time)}<br>
价格: ${price.toFixed(2)}`,
// 额外添加基础类型偏移
baseOffset: baseOffset,
// 添加边框
borderColor: 'white',
borderWidth: 1,
// 添加价格偏移系数
pricePercentOffset: PRICE_PERCENT_OFFSET[type] || 0,
// 保存实际价格用于计算
price: price,
// 保存类型
type: type
};
// 区分买卖点
if (type > 0) {
buyMarkers.push(marker);
} else {
sellMarkers.push(marker);
}
successCount++;
} catch (e) {
console.error(`处理买卖点 #${index} 出错:`, e, point);
errorCount++;
}
});
console.log(`买卖点处理完成: 成功=${successCount}, 失败=${errorCount}, 买点=${buyMarkers.length}, 卖点=${sellMarkers.length}`);
// 分别添加买卖点标记
if (buyMarkers.length > 0) {
const buyMarkersSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineVisible: false,
title: '买点'
});
// 设置临时的数据点
buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: 0 }]);
try {
// 设置买点标记,并添加偏移
buyMarkersSeries.setMarkers(
buyMarkers.map(marker => {
// 使用固定偏移而非百分比
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个买点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return {
...marker,
position: 'aboveBar', // 显示在K线上方
// 使用原始价格
price: marker.price
};
})
);
console.log(`成功添加 ${buyMarkers.length} 个买点标记`);
} catch (e) {
console.error("设置买点标记时出错:", e);
}
}
if (sellMarkers.length > 0) {
const sellMarkersSeries = mainChart.addLineSeries({
lastValueVisible: false,
priceLineVisible: false,
lineVisible: false,
title: '卖点'
});
// 设置临时的数据点
sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: 0 }]);
try {
// 设置卖点标记,并添加偏移
sellMarkersSeries.setMarkers(
sellMarkers.map(marker => {
// 使用固定偏移而非百分比
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个卖点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return {
...marker,
position: 'belowBar', // 显示在K线下方
// 使用原始价格
price: marker.price
};
})
);
console.log(`成功添加 ${sellMarkers.length} 个卖点标记`);
} catch (e) {
console.error("设置卖点标记时出错:", e);
}
}
}
}
function updateTables(currentData) {
// 检查数据有效性
if (!currentData) {
@@ -7334,7 +7098,6 @@
const timeStr = nextRefreshTime.toLocaleTimeString();
$('#nextRefreshTime').text(`下次刷新: ${timeStr}`);
}
// 倒计时定时器
let countdownTimer = null;
@@ -7475,7 +7238,6 @@
console.log('重绘分形元素 - 完成');
}
// 只更新分形元素(笔、线段、中枢)的表格数据
function updateFractalTables() {
if (!currentData) return;
@@ -7682,7 +7444,6 @@
'showMainBi': $('#showMainBi').is(':checked'),
'showMainSeg': $('#showMainSeg').is(':checked'),
'showMainZs': $('#showMainZs').is(':checked'),
'showMainUncompletedZs': $('#showMainUncompletedZs').is(':checked'),
'showVolume': false,
'showMacd': $('#showMacd').is(':checked'),
'showKlcFxType': $('#showKlcFxType').is(':checked'),
@@ -7908,7 +7669,6 @@
console.log('✅ EMA52系列已清理');
}
// 存储上次的EMA52数据,用于比较
let lastEMA52Data = null;
@@ -8264,7 +8024,6 @@
return result;
}
// 更新技术指标面板(统一面板)
function updateIndicatorPanel() {
const panel = $('#indicatorPanel');
@@ -8547,7 +8306,6 @@
try { updateIndicatorPanel(); } catch(e) {}
try { if ($('#maConfigModal').is(':visible')) { hideMAConfig(); } } catch(e) {}
}
// 获取当前K线数据的辅助函数
function getCurrentCandleData() {
if (!currentData || !currentData.kline_data) {
@@ -9063,7 +8821,6 @@
// 监听配置变化以更新预览
$(document).on('change', '#maColor, #maLineWidth, #maLineStyle', updateLinePreview);
$(document).on('change', '#bbUpperColor, #bbMiddleColor, #bbLowerColor, #bbLineWidth, #bbLineStyle', updateBBLinePreview);
// 点击弹窗外部关闭
$(document).on('click', '#bbConfigModal', function(e) {
if (e.target === this) {
@@ -9159,7 +8916,6 @@
// 清空全局UnitTF标记,避免旧数据残留影响主图合并
window.unittfMarkers = [];
}
// 添加所有ChanMACD标记
function addAllChanMacdMarkers(segList, unittfList, histsetList, stateMarkers) {
const macdMarkers = [];