diff --git a/ChanKLC.py b/ChanKLC.py index 7634009..2c97c58 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -61,8 +61,9 @@ class ChanKLC(): # 向后兼容:保留 ema52_status 和 ema52_pos self.ema52_status = 0 self.ema52_pos = Chan_EMA_POS.UNKNOWN - self.cal_all_ema_status() - + self.bb2633upper = klu.bb2633upper + self.bb2633lower = klu.bb2633lower + self.bb2633middle = klu.bb2633middle # ==================== EMA 通用计算方法 ==================== @staticmethod @@ -278,13 +279,19 @@ class ChanKLC(): # 向后兼容 self.ema52_pos = self.ema_status['ema52']['pos'] self.ema52_status = ChanKLC.semantic_to_int(self.ema_status['ema52']['semantic']) - def get_ema_pos(self, ema_name): """获取指定EMA的客观位置,如 klc.get_ema_pos('ema24')""" if ema_name in self.ema_status: return self.ema_status[ema_name]['pos'] return Chan_EMA_POS.UNKNOWN - + def check_ema_pos(self): + if len(self.ema_status) > 0: + for ema_name, pos in self.ema_status.items(): + #print(self.end_time, ema_name, pos['pos']) + if ((self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2) and pos['pos'] == Chan_EMA_POS.CROSS_CLOSE_BELOW) or ((self.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc_fx_type == Chan_KLC_FX.BOTTOM2) and pos['pos'] == Chan_EMA_POS.CROSS_CLOSE_ABOVE): + #print("---------------------") + return ema_name + return None def get_ema_semantic(self, ema_name): """获取指定EMA的语义状态,如 klc.get_ema_semantic('ema52')""" if ema_name in self.ema_status: @@ -303,7 +310,12 @@ class ChanKLC(): #print(self.start_time, klc_fx_type, self.get_feature_data()['klu_macd'], self.get_feature_data()['klu_macdhist'], self.get_feature_data()['klu_rsi']) self.klc_fx_type = klc_fx_type #self.cal_fx() - self.cal_bb_out() + ema_name = self.check_ema_pos() + hist_div = abs(self.macdhist - self.next.macdhist) + #print(self.end_time, self.dir, abs(self.macdhist), hist_div) + #if ema_name: + #print(self.end_time, ema_name, self.ema_status[ema_name]['semantic'], hist_div) + #self.cal_bb_out() def add_klu(self, klu): self.klu_list.append(klu) def set_end_klu(self, klu): @@ -323,6 +335,7 @@ class ChanKLC(): klu.set_klc(self) self.klc_dir = Chan_KLINE_DIR.UP if self.close > self.open else Chan_KLINE_DIR.DOWN self.cal_indicators() + self.cal_all_ema_status() def cal_fx(self): if self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2: #print(self.end_time, self.fx, self.macd, self.macdhist, len(self.klu_list)) @@ -368,6 +381,9 @@ class ChanKLC(): self.ema104 += self.klu_list[index].ema104 self.ema156 += self.klu_list[index].ema156 self.ema208 += self.klu_list[index].ema208 + self.bb2633upper += self.klu_list[index].bb2633upper + self.bb2633lower += self.klu_list[index].bb2633lower + self.bb2633middle += self.klu_list[index].bb2633middle if self.ema_dir != self.klu_list[index].ema_dir: self.ema_dir = 0 n = len(self.klu_list) @@ -380,6 +396,9 @@ class ChanKLC(): self.ema104 = self.ema104 / n self.ema156 = self.ema156 / n self.ema208 = self.ema208 / n + self.bb2633upper = self.bb2633upper / n + self.bb2633lower = self.bb2633lower / n + self.bb2633middle = self.bb2633middle / n if len(self.klu_list) > 0: self.macd = self.klu_list[-1].macd self.signal = self.klu_list[-1].signal diff --git a/ChanKLU.py b/ChanKLU.py index 117a3dd..96ef47b 100644 --- a/ChanKLU.py +++ b/ChanKLU.py @@ -71,6 +71,9 @@ class ChanKLU: self.div_score = 0.0 # 背离强度(0-100) self.ema_dir = 0 self.get_ema_dir() + self.bb2633upper = 0 + self.bb2633lower = 0 + self.bb2633middle = 0 #print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength) def set_macd_state(self, state): self.macd_state = state @@ -90,9 +93,37 @@ class ChanKLU: self.trend = trend def set_separate_div(self, separate_div): self.separate_div = separate_div + bb2633_status = self.check_bb2633() if self.klc and self.klc.pre and self.klc.next: - if self.klc.klc_fx_type != Chan_KLC_FX.UNKNOWN or self.klc.pre.klc_fx_type != Chan_KLC_FX.UNKNOWN or self.klc.next.klc_fx_type != Chan_KLC_FX.UNKNOWN: - self.separate_div = 99 + fx = self.check_fx_dir(self.klc.pre, self.klc.next) + if fx == Chan_FX_TYPE.TOP: + if self.macdhist > 0: + self.separate_div = separate_div + else: + self.separate_div = 0 + elif fx == Chan_FX_TYPE.BOTTOM: + if self.macdhist < 0: + self.separate_div = separate_div + else: + self.separate_div = 0 + if bb2633_status == 0: + self.separate_div = 0 + def check_bb2633(self, threadhold=300): + #print(self.time, self.high, self.bb2633upper, self.low, self.bb2633lower) + if abs(self.high - self.bb2633upper) < threadhold: + print(self.time, self.high, self.bb2633upper) + return 1 + if abs(self.low - self.bb2633lower) < threadhold: + print(self.time, self.low, self.bb2633lower) + return -1 + return 0 + def check_fx_dir(self, pre, next): + fx = Chan_FX_TYPE.UNKNOWN + if pre.klc_fx_type == Chan_KLC_FX.TOP1 or pre.klc_fx_type == Chan_KLC_FX.TOP2 or next.klc_fx_type == Chan_KLC_FX.TOP1 or next.klc_fx_type == Chan_KLC_FX.TOP2 or self.klc.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc.klc_fx_type == Chan_KLC_FX.TOP2: + fx = Chan_FX_TYPE.TOP + elif pre.klc_fx_type == Chan_KLC_FX.BOTTOM1 or pre.klc_fx_type == Chan_KLC_FX.BOTTOM2 or next.klc_fx_type == Chan_KLC_FX.BOTTOM1 or next.klc_fx_type == Chan_KLC_FX.BOTTOM2 or self.klc.klc_fx_type == Chan_KLC_FX.BOTTOM1 or self.klc.klc_fx_type == Chan_KLC_FX.BOTTOM2: + fx = Chan_FX_TYPE.BOTTOM + return fx def set_next(self, next): self.next = next #if self.fx_type != Chan_FX_TYPE.UNKNOWN and self.fx_strength > 1: @@ -151,7 +182,9 @@ class ChanKLU: self.volume_ratio = float(item['volume_ratio']) if 'volume_ratio' in item and item['volume_ratio'] else 0 self.bb52upper = float(item['bb52upper']) if 'bb52upper' in item and item['bb52upper'] else 0 self.bb52lower = float(item['bb52lower']) if 'bb52lower' in item and item['bb52lower'] else 0 - + self.bb2633upper = float(item['bb2633upper']) if 'bb2633upper' in item and item['bb2633upper'] else 0 + self.bb2633lower = float(item['bb2633lower']) if 'bb2633lower' in item and item['bb2633lower'] else 0 + self.bb2633middle = float(item['bb2633middle']) if 'bb2633middle' in item and item['bb2633middle'] else 0 def cal_macd_state(self): # 按定义精简实现:优先级 CROSS0 > 位置(HIGH/HE/RETURN_ZERO) > NEAR0 > UNKNOWN # 首条或缺前一根 diff --git a/ChanLun.py b/ChanLun.py index ffd7f52..493e6f5 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -151,8 +151,6 @@ class ChanLun(): return self.tf_df.check_bottom_fx(last_top, klc) def cal_bi_list(self, klc_list): return self.tf_df.cal_bi_list(klc_list) - def cal_bi_list_chanlun(self, klc_list): - return self.tf_df.cal_bi_list_chanlun(klc_list) def find_first_bsp(self, bi_list, bi_zs_list): return self.tf_df.find_first_bsp(bi_list, bi_zs_list) def find_second_bsp(self, bi_list, first_bsp_list): diff --git a/TF_DF.py b/TF_DF.py index 3a031cd..8f21198 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -81,6 +81,7 @@ class TF_DF(): bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0) bb30 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) bb302 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) + bb2633 = ta.BBANDS(df, timeperiod=26, nbdevup=3.0, nbdevdn=3.0, matype=0) # 计算布林带中轨(移动平均线) bb30_middle = ta.SMA(df, timeperiod=90) @@ -90,6 +91,11 @@ class TF_DF(): bbp120 = (df['close'] - bb120['lowerband']) / (bb120['upperband'] - bb120['lowerband']) bbp30 = (df['close'] - bb30['lowerband']) / (bb30['upperband'] - bb30['lowerband']) bbp302 = (df['close'] - bb302['lowerband']) / (bb302['upperband'] - bb302['lowerband']) + bbp2633 = (df['close'] - bb2633['lowerband']) / (bb2633['upperband'] - bb2633['lowerband']) + df['bb2633upper'] = bb2633['upperband'] + df['bb2633lower'] = bb2633['lowerband'] + df['bbp2633'] = bbp2633 + df['bb2633middle'] = bb2633['middleband'] df['atr'] = ta.ATR(df, timeperiod=14) df['bbup365'] = bb365['upperband'] df['bblow365'] = bb365['lowerband'] @@ -114,6 +120,7 @@ class TF_DF(): df['ema104'] = ta.EMA(df, timeperiod=104) df['ema156'] = ta.EMA(df, timeperiod=156) df['ema208'] = ta.EMA(df, timeperiod=208) + df['ema26'] = ta.EMA(df, timeperiod=26) df['rsi'] = ta.RSI(df, timeperiod=14) df['volume_ratio'] = self.cal_volume_ratio(df) return df @@ -872,11 +879,11 @@ class TF_DF(): last_bottom = None for klc in klc_list: fx = self.check_fx(klc) - if fx == Chan_FX_TYPE.TOP and False: + if fx == Chan_FX_TYPE.TOP: if last_bottom: if self.check_top_fx(last_bottom, klc) == False: fx = Chan_FX_TYPE.UNKNOWN - if fx == Chan_FX_TYPE.BOTTOM and False: + if fx == Chan_FX_TYPE.BOTTOM: if last_top: if self.check_bottom_fx(last_top, klc) == False: #print(klc.end_time, last_top.end_time, "---") @@ -1198,200 +1205,6 @@ class TF_DF(): return False return True - def check_fx_chanlun(self, klc): - """标准缠论分型:仅用高低点,不用 MACD,不要求整根 K 线包在左右内。""" - if klc.pre is None or klc.next is None: - return Chan_FX_TYPE.UNKNOWN - # 顶分型:中间 K 线高点最高 - if klc.high > klc.pre.high and klc.high > klc.next.high: - klc.set_fx(Chan_FX_TYPE.TOP) - return Chan_FX_TYPE.TOP - # 底分型:中间 K 线低点最低 - if klc.low < klc.pre.low and klc.low < klc.next.low: - klc.set_fx(Chan_FX_TYPE.BOTTOM) - return Chan_FX_TYPE.BOTTOM - return Chan_FX_TYPE.UNKNOWN - - def cal_bi_list_chanlun(self, klc_list): - """ - 与 cal_bi_list 逻辑完全一致,仅分型用 check_fx_chanlun(标准缠论分型,不看 MACD)。 - """ - bi_list = [] - last_top = None - last_bottom = None - for klc in klc_list: - fx = self.check_fx_chanlun(klc) - if fx == Chan_FX_TYPE.TOP: - if last_bottom: - if self.check_top_fx(last_bottom, klc) == False: - fx = Chan_FX_TYPE.UNKNOWN - if fx == Chan_FX_TYPE.BOTTOM: - if last_top: - if self.check_bottom_fx(last_top, klc) == False: - fx = Chan_FX_TYPE.UNKNOWN - if fx == Chan_FX_TYPE.UNKNOWN: - if len(bi_list) > 0: - bi_list[-1].add_klc(klc) - continue - if len(bi_list) > 0 and klc.end_klu: - last_bi = bi_list[-1] - if last_top and last_bi.dir == Chan_BI_DIR.DOWN: - if last_bottom and klc.high > last_bi.high: - last_bi.set_end_klc(last_bottom, klc) - bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP) - last_bi.set_next(bi) - bi.set_pre(last_bi) - for klc_index in range(last_bi.end_klc.index, len(klc_list)): - bi.add_klc(klc_list[klc_index]) - bi_list.append(bi) - last_top = klc - klc.set_bi(bi) - else: - if last_bottom and last_bi.dir == Chan_BI_DIR.UP: - if last_top and klc.low < last_bi.low: - last_bi.set_end_klc(last_top, klc) - bi = ChanBI(last_top, len(bi_list), Chan_BI_DIR.DOWN) - last_bi.set_next(bi) - bi.set_pre(last_bi) - for klc_index in range(last_bi.end_klc.index, len(klc_list)): - bi.add_klc(klc_list[klc_index]) - bi_list.append(bi) - last_bottom = klc - klc.set_bi(bi) - else: - if fx == Chan_FX_TYPE.TOP: - if last_top: - if last_bottom: - if last_bottom.index < last_top.index: - if last_top.high > klc.high: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - last_top = klc - klc.set_klc_fx_type(Chan_KLC_FX.TOP1) - self.check_fx_pattern(klc) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - if last_bottom.index + 4 > klc.index: - if last_top.high > klc.high: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - if last_top.index + 4 < klc.index and len(bi_list) > 1: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - klc.set_fx(Chan_FX_TYPE.PTOP) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - last_bi = bi_list[-1] - if not last_bi.is_sure: - last_bi.set_end_klc(last_bottom, klc) - bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP) - last_bi.set_next(bi) - bi.set_pre(last_bi) - bi.add_klc(klc) - bi_list.append(bi) - last_top = klc - klc.set_klc_fx_type(Chan_KLC_FX.TOP2) - self.check_fx_pattern(klc) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - if last_top.high < klc.high: - last_bi = bi_list[-1] - last_bi.set_start_klc(klc, Chan_BI_DIR.DOWN) - last_top = klc - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - klc.set_fx(Chan_FX_TYPE.TT) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - if last_bottom: - if last_bottom.index + 4 > klc.index: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - last_top = klc - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - last_top = klc - bi = ChanBI(klc, len(bi_list), Chan_BI_DIR.DOWN) - bi_list.append(bi) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - if last_bottom: - if last_top: - if last_top.index < last_bottom.index: - if last_bottom.low < klc.low: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - last_bottom = klc - klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM1) - self.check_fx_pattern(klc) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - if last_top.index + 4 > klc.index: - if last_bottom.low < klc.low: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - if last_bottom.index + 4 < klc.index and len(bi_list) > 1: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - last_bi = bi_list[-1] - if not last_bi.is_sure: - last_bi.set_end_klc(last_top, klc) - bi = ChanBI(last_top, len(bi_list), Chan_BI_DIR.DOWN) - last_bi.set_next(bi) - bi.set_pre(last_bi) - bi.add_klc(klc) - bi_list.append(bi) - last_bottom = klc - klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) - self.check_fx_pattern(klc) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - if last_bottom.low > klc.low: - last_bi = bi_list[-1] - last_bi.set_start_klc(klc, Chan_BI_DIR.UP) - last_bottom = klc - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - klc.set_fx(Chan_FX_TYPE.BB) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - if last_top: - if last_top.index + 4 > klc.index: - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - last_bottom = klc - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - last_bottom = klc - bi = ChanBI(klc, len(bi_list), Chan_BI_DIR.UP) - bi_list.append(bi) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - return bi_list - def cal_bi_zs(self, seg_list): bi_zs_list = [] for seg in seg_list: @@ -1453,7 +1266,7 @@ class TF_DF(): Chan_BSP_DIR.SELL, leave_bi.sure_time, zs.index+1, zs, None - ) + ) leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S1) bsp_list.append(bsp) # 回拉笔 @@ -1496,7 +1309,7 @@ class TF_DF(): Chan_BSP_DIR.BUY, leave_bi.sure_time, zs.index+1, zs, None - ) + ) leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B1) bsp_list.append(bsp) # 反弹笔 @@ -1713,7 +1526,8 @@ class TF_DF(): bsp_list.append(bsp) return bsp_list - + def calculate_zs(self, bi_list, seg_list): + return self.get_zs_list(bi_list, seg_list) def get_zs_list(self, bi_list, seg_list): zs_list = [] bsp_list = [] diff --git a/data_provider/main.py b/data_provider/main.py index c632e03..9e5f680 100644 --- a/data_provider/main.py +++ b/data_provider/main.py @@ -30,7 +30,7 @@ TIMEFRAME_TO_MS: Dict[str, int] = { } DERIVED_TIMEFRAME_PLAN: Dict[str, List[str]] = { "1m": ["2m", "3m", "4m", "5m", "10m", "15m", "20m", "25m", "30m", "45m"], - "1h": ["2h", "3h", "4h", "6h", "8h", "12h", "16h", "20h"], + "1h": ["2h", "3h", "4h", "5h","6h", "7h", "8h", "9h", "10", "11h", "12h", "16h", "20h"], "1d": ["2d", "3d", "4d", "5d", "6d"], "1w": ["2w", "3w"], } diff --git a/strategies/ChanLun_BTC_30.py b/strategies/ChanLun_BTC_30.py index f4ba615..1b777b8 100644 --- a/strategies/ChanLun_BTC_30.py +++ b/strategies/ChanLun_BTC_30.py @@ -136,7 +136,7 @@ class ChanLun_BTC_30(IStrategy): #state_list = self.chan.get_klu_state(dataframe_1d) #dataframe_1d['state'] = state_list if self.last_time + timedelta(minutes=1) < datetime.now(): - print("-------------------------------------------------------------------------------") + #print("-------------------------------------------------------------------------------") self.last_time = datetime.now() #dataframe = resampled_merge(dataframe, dataframe_3) dataframe = resampled_merge(dataframe, dataframe_5) diff --git a/web/app.py b/web/app.py index 3b506ca..40896f2 100644 --- a/web/app.py +++ b/web/app.py @@ -18,25 +18,25 @@ import numpy as np # 添加父目录到系统路径 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from ChanLun import ChanLun +from ChanLun import ChanLun, TF_DF from ChanEnum import Chan_BI_DIR, Chan_SEG_DIR, Chan_KLC_FX, Chan_FX_TYPE, Chan_MACDSEG_DIR, Chan_MACDHISTSET_DIR from cn_stock_data import ChinaStockData from ChanMACD import ChanMACD # 添加买卖点枚举类型 class TRADE_POINT_TYPE: - BUY1 = 1 # 一类买点 - BUY2 = 2 # 二类买点 - BUY3 = 3 # 三类买点 - SELL1 = -1 # 一类卖点 - SELL2 = -2 # 二类卖点 - SELL3 = -3 # 三类卖点 + BUY1 = 1 # 一类买点 + BUY2 = 2 # 二类买点 + BUY3 = 3 # 三类买点 + SELL1 = -1 # 一类卖点 + SELL2 = -2 # 二类卖点 + SELL3 = -3 # 三类卖点 app = Flask(__name__) # 初始化交易所 exchange = ccxt.binance({ - 'enableRateLimit': True, + 'enableRateLimit': True, }) # 初始化A股数据获取器 @@ -47,26 +47,26 @@ logger = logging.getLogger(__name__) DATA_SERVICE_URL = os.environ.get("DATA_SERVICE_URL", os.environ.get("DATASVC_URL", "http://127.0.0.1:9009")) #DATA_SERVICE_URL = os.environ.get("DATA_SERVICE_URL", os.environ.get("DATASVC_URL", "http://192.168.1.9:9009")) DEFAULT_TIMEFRAME_LABELS = OrderedDict([ - ("1m", "1分钟"), - ("3m", "3分钟"), - ("5m", "5分钟"), - ("15m", "15分钟"), - ("30m", "30分钟"), - ("1h", "1小时"), - ("2h", "2小时"), - ("4h", "4小时"), - ("6h", "6小时"), - ("8h", "8小时"), - ("12h", "12小时"), - ("1d", "日线"), - ("3d", "3日线"), - ("1w", "周线"), - ("1M", "月线"), + ("1m", "1分钟"), + ("3m", "3分钟"), + ("5m", "5分钟"), + ("15m", "15分钟"), + ("30m", "30分钟"), + ("1h", "1小时"), + ("2h", "2小时"), + ("4h", "4小时"), + ("6h", "6小时"), + ("8h", "8小时"), + ("12h", "12小时"), + ("1d", "日线"), + ("3d", "3日线"), + ("1w", "周线"), + ("1M", "月线"), ]) DEFAULT_SYMBOLS = [ - 'SOL/USDT:USDT', 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'XRP/USDT:USDT', 'WIF/USDT:USDT', - 'ADA/USDT:USDT', 'DOGE/USDT:USDT', 'AVAX/USDT:USDT', 'DOT/USDT:USDT', 'MATIC/USDT:USDT' + 'SOL/USDT:USDT', 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'XRP/USDT:USDT', 'WIF/USDT:USDT', + 'ADA/USDT:USDT', 'DOGE/USDT:USDT', 'AVAX/USDT:USDT', 'DOT/USDT:USDT', 'MATIC/USDT:USDT' ] TIMEFRAMES = DEFAULT_TIMEFRAME_LABELS.copy() @@ -76,131 +76,131 @@ SERVICE_METADATA_LAST_REFRESH = 0 def timeframe_to_minutes(tf: str): - """将时间周期转换为分钟数,用于排序。""" - if not tf: - return None - unit = tf[-1] - try: - value = int(tf[:-1]) - except (ValueError, TypeError): - return None - multiplier = { - 'm': 1, - 'h': 60, - 'd': 1440, - 'w': 10080, - 'M': 43200, # 30天近似 - }.get(unit) - if multiplier is None: - return None - return value * multiplier + """将时间周期转换为分钟数,用于排序。""" + if not tf: + return None + unit = tf[-1] + try: + value = int(tf[:-1]) + except (ValueError, TypeError): + return None + multiplier = { + 'm': 1, + 'h': 60, + 'd': 1440, + 'w': 10080, + 'M': 43200, # 30天近似 + }.get(unit) + if multiplier is None: + return None + return value * multiplier def format_timeframe_label(tf: str) -> str: - """将时间周期转换为可读标签。""" - if not tf: - return tf - unit = tf[-1] - try: - value = int(tf[:-1]) - except (ValueError, TypeError): - return tf - if unit == 'm': - return f"{value}分钟" - if unit == 'h': - return f"{value}小时" - if unit == 'd': - return "日线" if value == 1 else f"{value}日线" - if unit == 'w': - return "周线" if value == 1 else f"{value}周线" - if unit == 'M': - return "月线" if value == 1 else f"{value}月线" - return tf + """将时间周期转换为可读标签。""" + if not tf: + return tf + unit = tf[-1] + try: + value = int(tf[:-1]) + except (ValueError, TypeError): + return tf + if unit == 'm': + return f"{value}分钟" + if unit == 'h': + return f"{value}小时" + if unit == 'd': + return "日线" if value == 1 else f"{value}日线" + if unit == 'w': + return "周线" if value == 1 else f"{value}周线" + if unit == 'M': + return "月线" if value == 1 else f"{value}月线" + return tf def build_timeframe_labels(timeframes): - ordered = sorted( - timeframes, - key=lambda tf: timeframe_to_minutes(tf) if timeframe_to_minutes(tf) is not None else float('inf'), - ) - labels = OrderedDict() - for tf in ordered: - labels[tf] = format_timeframe_label(tf) - return labels + ordered = sorted( + timeframes, + key=lambda tf: timeframe_to_minutes(tf) if timeframe_to_minutes(tf) is not None else float('inf'), + ) + labels = OrderedDict() + for tf in ordered: + labels[tf] = format_timeframe_label(tf) + return labels def _parse_time_input(value): - if value in (None, '', 0): - return None - try: - return int(float(value)) - except (ValueError, TypeError): - return None + if value in (None, '', 0): + return None + try: + return int(float(value)) + except (ValueError, TypeError): + return None def refresh_data_service_metadata(force=False): - """刷新数据服务提供的交易对与周期元信息。""" - global DATA_SERVICE_AVAILABLE, TIMEFRAMES, SYMBOLS, SERVICE_METADATA_LAST_REFRESH - now = time.time() - if not force and DATA_SERVICE_AVAILABLE and now - SERVICE_METADATA_LAST_REFRESH < 60: - return True - try: - resp = requests.get(f"{DATA_SERVICE_URL}/health", timeout=5) - resp.raise_for_status() - payload = resp.json() - service_symbols = payload.get("symbols") or payload.get("symbol_list") or [] - base_timeframes = payload.get("timeframes") or payload.get("base_timeframes") or [] - derived = payload.get("derived_timeframes") or [] - service_timeframes = list(base_timeframes) - for tf in derived: - if tf not in service_timeframes: - service_timeframes.append(tf) - if service_symbols: - SYMBOLS[:] = service_symbols - if service_timeframes: - TIMEFRAMES.clear() - TIMEFRAMES.update(build_timeframe_labels(service_timeframes)) - DATA_SERVICE_AVAILABLE = True - SERVICE_METADATA_LAST_REFRESH = now - return True - except Exception as exc: - logger.warning("无法加载数据服务元信息: %s", exc) - if not DATA_SERVICE_AVAILABLE: - TIMEFRAMES.clear() - TIMEFRAMES.update(DEFAULT_TIMEFRAME_LABELS) - SYMBOLS[:] = DEFAULT_SYMBOLS - DATA_SERVICE_AVAILABLE = False - return False + """刷新数据服务提供的交易对与周期元信息。""" + global DATA_SERVICE_AVAILABLE, TIMEFRAMES, SYMBOLS, SERVICE_METADATA_LAST_REFRESH + now = time.time() + if not force and DATA_SERVICE_AVAILABLE and now - SERVICE_METADATA_LAST_REFRESH < 60: + return True + try: + resp = requests.get(f"{DATA_SERVICE_URL}/health", timeout=5) + resp.raise_for_status() + payload = resp.json() + service_symbols = payload.get("symbols") or payload.get("symbol_list") or [] + base_timeframes = payload.get("timeframes") or payload.get("base_timeframes") or [] + derived = payload.get("derived_timeframes") or [] + service_timeframes = list(base_timeframes) + for tf in derived: + if tf not in service_timeframes: + service_timeframes.append(tf) + if service_symbols: + SYMBOLS[:] = service_symbols + if service_timeframes: + TIMEFRAMES.clear() + TIMEFRAMES.update(build_timeframe_labels(service_timeframes)) + DATA_SERVICE_AVAILABLE = True + SERVICE_METADATA_LAST_REFRESH = now + return True + except Exception as exc: + logger.warning("无法加载数据服务元信息: %s", exc) + if not DATA_SERVICE_AVAILABLE: + TIMEFRAMES.clear() + TIMEFRAMES.update(DEFAULT_TIMEFRAME_LABELS) + SYMBOLS[:] = DEFAULT_SYMBOLS + DATA_SERVICE_AVAILABLE = False + return False def _fetch_kl_from_datasvc(symbol, timeframe, start_ms=None, end_ms=None, limit=None): - params = {"symbol": symbol, "tf": timeframe} - if start_ms is not None: - params["start"] = int(start_ms) - if end_ms is not None: - params["end"] = int(end_ms) - resp = requests.get(f"{DATA_SERVICE_URL}/api/candles", params=params, timeout=10) - resp.raise_for_status() - data = resp.json() - if not data: - return None - df = pd.DataFrame(data) - if df.empty or "timestamp" not in df.columns: - return None - numeric_cols = ["open", "high", "low", "close", "volume"] - df["timestamp"] = pd.to_numeric(df["timestamp"], errors="coerce") - df = df.dropna(subset=["timestamp"]) - df["timestamp"] = df["timestamp"].astype("int64") - for col in numeric_cols: - if col in df.columns: - df[col] = pd.to_numeric(df[col], errors="coerce") - df = df.dropna(subset=numeric_cols) - df = df.sort_values("timestamp") - if limit and len(df) > limit: - df = df.tail(limit) - df = df.reset_index(drop=True) - df["date"] = pd.to_datetime(df["timestamp"], unit='ms', utc=True).dt.tz_convert('Asia/Shanghai') - return df + params = {"symbol": symbol, "tf": timeframe} + if start_ms is not None: + params["start"] = int(start_ms) + if end_ms is not None: + params["end"] = int(end_ms) + resp = requests.get(f"{DATA_SERVICE_URL}/api/candles", params=params, timeout=10) + resp.raise_for_status() + data = resp.json() + if not data: + return None + df = pd.DataFrame(data) + if df.empty or "timestamp" not in df.columns: + return None + numeric_cols = ["open", "high", "low", "close", "volume"] + df["timestamp"] = pd.to_numeric(df["timestamp"], errors="coerce") + df = df.dropna(subset=["timestamp"]) + df["timestamp"] = df["timestamp"].astype("int64") + for col in numeric_cols: + if col in df.columns: + df[col] = pd.to_numeric(df[col], errors="coerce") + df = df.dropna(subset=numeric_cols) + df = df.sort_values("timestamp") + if limit and len(df) > limit: + df = df.tail(limit) + df = df.reset_index(drop=True) + df["date"] = pd.to_datetime(df["timestamp"], unit='ms', utc=True).dt.tz_convert('Asia/Shanghai') + return df # 模块加载时尝试预取一次元信息,但失败不阻塞后续流程 @@ -210,1496 +210,1501 @@ refresh_data_service_metadata(force=True) A_STOCK_SYMBOLS = china_stock.get_popular_stocks() def detect_symbol_type(symbol): - """检测交易对类型:crypto 或 a_stock""" - if '/' in symbol and 'USDT' in symbol: - return 'crypto' - elif len(symbol) == 6 and symbol.isdigit(): - return 'a_stock' - else: - return 'unknown' + """检测交易对类型:crypto 或 a_stock""" + if '/' in symbol and 'USDT' in symbol: + return 'crypto' + elif len(symbol) == 6 and symbol.isdigit(): + return 'a_stock' + else: + return 'unknown' def get_kl_data(symbol, timeframe, limit=100000, start_time=None, end_time=None): - """获取K线数据,支持加密货币和A股""" - symbol_type = detect_symbol_type(symbol) - - if symbol_type == 'crypto': - return get_crypto_kl_data(symbol, timeframe, limit, start_time, end_time) - elif symbol_type == 'a_stock': - return get_a_stock_kl_data(symbol, timeframe, limit, start_time, end_time) - else: - return None + """获取K线数据,支持加密货币和A股""" + symbol_type = detect_symbol_type(symbol) + + if symbol_type == 'crypto': + return get_crypto_kl_data(symbol, timeframe, limit, start_time, end_time) + elif symbol_type == 'a_stock': + return get_a_stock_kl_data(symbol, timeframe, limit, start_time, end_time) + else: + return None def _get_crypto_kl_data_via_ccxt(symbol, timeframe, limit=100000, start_time=None, end_time=None): - """获取加密货币K线数据,支持分页加载确保获取指定时间范围内的所有数据""" - try: - # 初始化参数 - since = None - if start_time: - try: - since = int(start_time) - except ValueError: - pass - - # 结束时间处理 - until = None - if end_time: - try: - until = int(end_time) - except ValueError: - pass - - # 根据时间周期调整每次请求的数据量 - batch_size = 1000 # 默认批次大小 - if timeframe in ['1m', '3m', '5m']: - batch_size = 1000 # 分钟级数据减少批次大小 - elif timeframe in ['15m', '30m', '1h']: - batch_size = 1000 - else: - batch_size = 1500 # 日线及以上可以获取更多 - batch_size = 1500 # 默认批次大小 - # 初始化存储所有K线数据的列表 - all_ohlcv = [] - - # 初始化当前查询的开始时间 - current_since = since - - # 添加请求计数和最大限制 - request_count = 0 - max_requests = 300 # 最大请求次数,防止无限循环 - - # 分页加载数据 - while request_count < max_requests: - request_count += 1 - - try: - # 获取当前页的数据 - ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=batch_size) - - # 如果没有获取到数据,结束循环 - if not ohlcv or len(ohlcv) == 0: - break - - # 将获取到的数据添加到总列表中 - all_ohlcv.extend(ohlcv) - - # 获取最后一条数据的时间戳 - last_timestamp = ohlcv[-1][0] - - # 如果已达到结束时间,结束循环 - if until and last_timestamp >= until: - break - - # 如果获取的数据条数小于限制数,说明已经获取完所有数据 - if len(ohlcv) < batch_size: - break - - # 更新下一页的开始时间(加1毫秒避免重复) - current_since = last_timestamp + 1 - - except Exception as e: - # 如果单个批次失败,继续尝试下一个批次 - if current_since: - # 尝试增加时间跳过可能的问题时间点 - current_since += 60000 # 跳过1分钟 - else: - break - - # 防止API请求过于频繁 - time.sleep(0.3) # 减少到0.3秒提高效率 - - # 数据为空的情况 - if not all_ohlcv or len(all_ohlcv) == 0: - return None - - # 转换为DataFrame - df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) - df['date'] = pd.to_datetime(df['timestamp'], unit='ms').dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai') - - # 在客户端进行结束时间过滤 - if until: - df = df[df['timestamp'] <= until] - - # 去除重复数据 - df = df.drop_duplicates(subset=['timestamp']) - - # 按时间排序 - df = df.sort_values('timestamp') - - # 限制数据条数的逻辑 - 优先考虑时间范围 - if start_time and end_time: - # 如果指定了明确的时间范围,返回该时间范围内的所有数据 - 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) - - # 如果过滤后没有数据,返回None - if len(df) == 0: - return None - return df - - except Exception as e: - return None + """获取加密货币K线数据,支持分页加载确保获取指定时间范围内的所有数据""" + try: + # 初始化参数 + since = None + if start_time: + try: + since = int(start_time) + except ValueError: + pass + + # 结束时间处理 + until = None + if end_time: + try: + until = int(end_time) + except ValueError: + pass + + # 根据时间周期调整每次请求的数据量 + batch_size = 1000 # 默认批次大小 + if timeframe in ['1m', '3m', '5m']: + batch_size = 1000 # 分钟级数据减少批次大小 + elif timeframe in ['15m', '30m', '1h']: + batch_size = 1000 + else: + batch_size = 1500 # 日线及以上可以获取更多 + batch_size = 1500 # 默认批次大小 + # 初始化存储所有K线数据的列表 + all_ohlcv = [] + + # 初始化当前查询的开始时间 + current_since = since + + # 添加请求计数和最大限制 + request_count = 0 + max_requests = 300 # 最大请求次数,防止无限循环 + + # 分页加载数据 + while request_count < max_requests: + request_count += 1 + + try: + # 获取当前页的数据 + ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=batch_size) + + # 如果没有获取到数据,结束循环 + if not ohlcv or len(ohlcv) == 0: + break + + # 将获取到的数据添加到总列表中 + all_ohlcv.extend(ohlcv) + + # 获取最后一条数据的时间戳 + last_timestamp = ohlcv[-1][0] + + # 如果已达到结束时间,结束循环 + if until and last_timestamp >= until: + break + + # 如果获取的数据条数小于限制数,说明已经获取完所有数据 + if len(ohlcv) < batch_size: + break + + # 更新下一页的开始时间(加1毫秒避免重复) + current_since = last_timestamp + 1 + + except Exception as e: + # 如果单个批次失败,继续尝试下一个批次 + if current_since: + # 尝试增加时间跳过可能的问题时间点 + current_since += 60000 # 跳过1分钟 + else: + break + + # 防止API请求过于频繁 + time.sleep(0.3) # 减少到0.3秒提高效率 + + # 数据为空的情况 + if not all_ohlcv or len(all_ohlcv) == 0: + return None + + # 转换为DataFrame + df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) + df['date'] = pd.to_datetime(df['timestamp'], unit='ms').dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai') + + # 在客户端进行结束时间过滤 + if until: + df = df[df['timestamp'] <= until] + + # 去除重复数据 + df = df.drop_duplicates(subset=['timestamp']) + + # 按时间排序 + df = df.sort_values('timestamp') + + # 限制数据条数的逻辑 - 优先考虑时间范围 + if start_time and end_time: + # 如果指定了明确的时间范围,返回该时间范围内的所有数据 + 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) + + # 如果过滤后没有数据,返回None + if len(df) == 0: + return None + return df + + except Exception as e: + return None def get_crypto_kl_data(symbol, timeframe, limit=100000, start_time=None, end_time=None): - """优先通过本地数据服务获取加密货币K线,失败时回退至交易所API。""" - start_ms = _parse_time_input(start_time) - end_ms = _parse_time_input(end_time) + """优先通过本地数据服务获取加密货币K线,失败时回退至交易所API。""" + start_ms = _parse_time_input(start_time) + end_ms = _parse_time_input(end_time) - refresh_data_service_metadata() - if DATA_SERVICE_AVAILABLE: - try: - df = _fetch_kl_from_datasvc( - symbol=symbol, - timeframe=timeframe, - start_ms=start_ms, - end_ms=end_ms, - limit=limit, - ) - if df is not None and not df.empty: - return df - except Exception as exc: - logger.warning("数据服务请求失败,准备回退至交易所 API:%s", exc) + refresh_data_service_metadata() + if DATA_SERVICE_AVAILABLE: + try: + df = _fetch_kl_from_datasvc( + symbol=symbol, + timeframe=timeframe, + start_ms=start_ms, + end_ms=end_ms, + limit=limit, + ) + if df is not None and not df.empty: + return df + except Exception as exc: + logger.warning("数据服务请求失败,准备回退至交易所 API:%s", exc) - return _get_crypto_kl_data_via_ccxt(symbol, timeframe, limit, start_time, end_time) + return _get_crypto_kl_data_via_ccxt(symbol, timeframe, limit, start_time, end_time) def get_a_stock_kl_data(symbol, timeframe, limit=100000, start_time=None, end_time=None): - """获取A股K线数据""" - try: - # 处理时间戳参数转换为日期字符串 - start_date = None - end_date = None - - if start_time: - try: - # 尝试解析时间戳(毫秒) - start_timestamp = int(start_time) - start_date = datetime.fromtimestamp(start_timestamp / 1000).strftime('%Y-%m-%d') - except (ValueError, TypeError): - # 如果不是时间戳,尝试解析datetime-local格式 (YYYY-MM-DDTHH:MM) - try: - if 'T' in str(start_time): - # datetime-local格式:2025-05-19T06:07 - start_date = str(start_time).split('T')[0] # 只取日期部分 - else: - start_date = str(start_time) - except: - start_date = start_time - - if end_time: - try: - # 尝试解析时间戳(毫秒) - end_timestamp = int(end_time) - end_date = datetime.fromtimestamp(end_timestamp / 1000).strftime('%Y-%m-%d') - except (ValueError, TypeError): - # 如果不是时间戳,尝试解析datetime-local格式 - try: - if 'T' in str(end_time): - # datetime-local格式:2025-05-26T06:07 - end_date = str(end_time).split('T')[0] # 只取日期部分 - else: - end_date = str(end_time) - except: - end_date = end_time - - # 如果用户指定了时间范围,优先获取该范围内的所有数据 - actual_limit = limit - if start_date and end_date: - actual_limit = None # 不限制数据条数,获取完整时间范围数据 - - # 调用A股数据获取器 - df = china_stock.get_kl_data(symbol, timeframe, start_date, end_date, actual_limit) - - if df is None: - return None - return df - - except Exception as e: - return None + """获取A股K线数据""" + try: + # 处理时间戳参数转换为日期字符串 + start_date = None + end_date = None + + if start_time: + try: + # 尝试解析时间戳(毫秒) + start_timestamp = int(start_time) + start_date = datetime.fromtimestamp(start_timestamp / 1000).strftime('%Y-%m-%d') + except (ValueError, TypeError): + # 如果不是时间戳,尝试解析datetime-local格式 (YYYY-MM-DDTHH:MM) + try: + if 'T' in str(start_time): + # datetime-local格式:2025-05-19T06:07 + start_date = str(start_time).split('T')[0] # 只取日期部分 + else: + start_date = str(start_time) + except: + start_date = start_time + + if end_time: + try: + # 尝试解析时间戳(毫秒) + end_timestamp = int(end_time) + end_date = datetime.fromtimestamp(end_timestamp / 1000).strftime('%Y-%m-%d') + except (ValueError, TypeError): + # 如果不是时间戳,尝试解析datetime-local格式 + try: + if 'T' in str(end_time): + # datetime-local格式:2025-05-26T06:07 + end_date = str(end_time).split('T')[0] # 只取日期部分 + else: + end_date = str(end_time) + except: + end_date = end_time + + # 如果用户指定了时间范围,优先获取该范围内的所有数据 + actual_limit = limit + if start_date and end_date: + actual_limit = None # 不限制数据条数,获取完整时间范围数据 + + # 调用A股数据获取器 + df = china_stock.get_kl_data(symbol, timeframe, start_date, end_date, actual_limit) + + if df is None: + return None + return df + + except Exception as e: + return None def add_indicators(df): - fast = 12*1 - slow = 26*1 - period = 9*1 - macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) + fast = 12*1 + slow = 26*1 + period = 9*1 + macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) - df['macd'] = macd['macd'] - df['macdsignal'] = macd['macdsignal'] - df['macdhist'] = macd['macdhist'] - df['ma5'] = (ta.MA(df, timeperiod=5)).fillna(0) - df['ma10'] = (ta.MA(df, timeperiod=10)).fillna(0) - df['ma30'] = (ta.EMA(df, timeperiod=30)).fillna(0) - df['ma250'] = (ta.MA(df, timeperiod=250)).fillna(0) - # 新增 EMA 指标 - df['ema5'] = (ta.EMA(df, timeperiod=5)).fillna(0) - df['ema10'] = (ta.EMA(df, timeperiod=10)).fillna(0) - df['ema24'] = (ta.EMA(df, timeperiod=24)).fillna(0) - df['ema52'] = (ta.EMA(df, timeperiod=52)).fillna(0) - # 常用SMA 24/52 - try: - df['sma24'] = (ta.SMA(df, timeperiod=24)).fillna(0) - df['sma52'] = (ta.SMA(df, timeperiod=52)).fillna(0) - except Exception: - df['sma24'] = 0 - df['sma52'] = 0 - df['rsi'] = ta.RSI(df, timeperiod=14) - - # 计算布林带 (当前周期 - 20周期,2标准差) - bb = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0) - df['bb_upper'] = bb['upperband'].fillna(0) - df['bb_middle'] = bb['middleband'].fillna(0) - df['bb_lower'] = bb['lowerband'].fillna(0) - bb30 = ta.BBANDS(df, timeperiod=41, nbdevup=2.3, nbdevdn=2.3, matype=0) - #bb30 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) - df['bbup30'] = bb30['upperband'].fillna(0) - df['bblow30'] = bb30['lowerband'].fillna(0) - bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0) - #bb302 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) - df['bbup302'] = bb302['upperband'].fillna(0) - df['bblow302'] = bb302['lowerband'].fillna(0) - # 计算次周期布林带 (14周期,2标准差) - bb_element = ta.BBANDS(df, timeperiod=14, nbdevup=2.0, nbdevdn=2.0, matype=0) - df['element_bb_upper'] = bb_element['upperband'].fillna(0) - df['element_bb_middle'] = bb_element['middleband'].fillna(0) - df['element_bb_lower'] = bb_element['lowerband'].fillna(0) - - df['macd'] = df['macd'].fillna(0) - df['macdsignal'] = df['macdsignal'].fillna(0) - df['macdhist'] = df['macdhist'].fillna(0) - df['ma5'] = df['ma5'].fillna(0) - df['ma10'] = df['ma10'].fillna(0) - df['ma30'] = df['ma30'].fillna(0) - df['ma250'] = df['ma250'].fillna(0) - df['ema5'] = df['ema5'].fillna(0) - df['ema10'] = df['ema10'].fillna(0) - df['ema24'] = df['ema24'].fillna(0) - df['ema52'] = df['ema52'].fillna(0) - df['sma24'] = df['sma24'].fillna(0) - df['sma52'] = df['sma52'].fillna(0) - df['rsi'] = df['rsi'].fillna(0) - df['avg_volume'] = df['volume'].rolling(10).mean() - # 计算量比,避免产生Infinity值 - df['volume_ratio'] = df['volume'] / df['avg_volume'] - # 填充缺失值(前N根K线) - df['volume_ratio'] = df['volume_ratio'].fillna(1.0) - df['avg_volume'] = df['avg_volume'].fillna(0) - - # 处理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 + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + df['ma5'] = (ta.MA(df, timeperiod=5)).fillna(0) + df['ma10'] = (ta.MA(df, timeperiod=10)).fillna(0) + df['ma30'] = (ta.EMA(df, timeperiod=30)).fillna(0) + df['ma250'] = (ta.MA(df, timeperiod=250)).fillna(0) + # 新增 EMA 指标 + df['ema5'] = (ta.EMA(df, timeperiod=5)).fillna(0) + df['ema10'] = (ta.EMA(df, timeperiod=10)).fillna(0) + df['ema24'] = (ta.EMA(df, timeperiod=24)).fillna(0) + df['ema52'] = (ta.EMA(df, timeperiod=52)).fillna(0) + # 常用SMA 24/52 + try: + df['sma24'] = (ta.SMA(df, timeperiod=24)).fillna(0) + df['sma52'] = (ta.SMA(df, timeperiod=52)).fillna(0) + except Exception: + df['sma24'] = 0 + df['sma52'] = 0 + df['rsi'] = ta.RSI(df, timeperiod=14) + + # 计算布林带 (当前周期 - 20周期,2标准差) + bb = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0) + df['bb_upper'] = bb['upperband'].fillna(0) + df['bb_middle'] = bb['middleband'].fillna(0) + df['bb_lower'] = bb['lowerband'].fillna(0) + bb30 = ta.BBANDS(df, timeperiod=41, nbdevup=2.3, nbdevdn=2.3, matype=0) + #bb30 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) + df['bbup30'] = bb30['upperband'].fillna(0) + df['bblow30'] = bb30['lowerband'].fillna(0) + bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0) + #bb302 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) + df['bbup302'] = bb302['upperband'].fillna(0) + df['bblow302'] = bb302['lowerband'].fillna(0) + # 计算次周期布林带 (14周期,2标准差) + bb_element = ta.BBANDS(df, timeperiod=14, nbdevup=2.0, nbdevdn=2.0, matype=0) + df['element_bb_upper'] = bb_element['upperband'].fillna(0) + df['element_bb_middle'] = bb_element['middleband'].fillna(0) + df['element_bb_lower'] = bb_element['lowerband'].fillna(0) + + df['macd'] = df['macd'].fillna(0) + df['macdsignal'] = df['macdsignal'].fillna(0) + df['macdhist'] = df['macdhist'].fillna(0) + df['ma5'] = df['ma5'].fillna(0) + df['ma10'] = df['ma10'].fillna(0) + df['ma30'] = df['ma30'].fillna(0) + df['ma250'] = df['ma250'].fillna(0) + df['ema5'] = df['ema5'].fillna(0) + df['ema10'] = df['ema10'].fillna(0) + df['ema24'] = df['ema24'].fillna(0) + df['ema52'] = df['ema52'].fillna(0) + df['sma24'] = df['sma24'].fillna(0) + df['sma52'] = df['sma52'].fillna(0) + df['rsi'] = df['rsi'].fillna(0) + df['avg_volume'] = df['volume'].rolling(10).mean() + # 计算量比,避免产生Infinity值 + df['volume_ratio'] = df['volume'] / df['avg_volume'] + # 填充缺失值(前N根K线) + df['volume_ratio'] = df['volume_ratio'].fillna(1.0) + df['avg_volume'] = df['avg_volume'].fillna(0) + + # 处理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) + bb2633 = ta.BBANDS(df, timeperiod=26, nbdevup=3.0, nbdevdn=3.0, matype=0) + bbp2633 = (df['close'] - bb2633['lowerband']) / (bb2633['upperband'] - bb2633['lowerband']) + df['bb2633upper'] = bb2633['upperband'].fillna(0) + df['bb2633lower'] = bb2633['lowerband'].fillna(0) + df['bbp2633'] = bbp2633.fillna(0) + df['bb2633middle'] = bb2633['middleband'].fillna(0) + return df def calculate_macd(df): - """计算MACD指标""" - exp1 = df['close'].ewm(span=12, adjust=False).mean() - exp2 = df['close'].ewm(span=26, adjust=False).mean() - macd = exp1 - exp2 - signal = macd.ewm(span=9, adjust=False).mean() - histogram = macd - signal - - return { - 'macd': macd.tolist(), - 'signal': signal.tolist(), - 'histogram': histogram.tolist() - } + """计算MACD指标""" + exp1 = df['close'].ewm(span=12, adjust=False).mean() + exp2 = df['close'].ewm(span=26, adjust=False).mean() + macd = exp1 - exp2 + signal = macd.ewm(span=9, adjust=False).mean() + histogram = macd - signal + + return { + 'macd': macd.tolist(), + 'signal': signal.tolist(), + 'histogram': histogram.tolist() + } def analyze_chan(df, symbol=None, timeframe=None): - """进行缠论分析""" - chan = ChanLun() - - # 初始化多时间周期数据以获取EMA52 - ema52_dict = None - # 获取分析结果 - klu_list = chan.get_kl_data(df) - klc_list = chan.get_klc_list(klu_list) - bi_list = chan.cal_bi_list(klc_list) - #for index in range(0, 10): - #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 = [] - bsp_list = [] - if len(bi_zs_list) > 0: - bsp_list = chan.find_all_bsp(bi_list, bi_zs_list) - for bsp in bsp_list: - print(bsp.end_time, bsp.type, bsp.dir) - # 添加买卖点识别 - for bi in bi_list: - bi.cal_macdhist() - for bi in bi_list: - bi.cal_macd_div() - #print(bi.start_time, bi.macd_hist, bi.macd_div) - - # 添加ChanMACD分析 - chan_macd = None - chan_macd_data = {} - try: - - if klu_list and len(klu_list) > 0: - print(f"获取到KLU列表,长度: {len(klu_list)}") - chan_macd = ChanMACD(klu_list) - chan_macd_data = { - 'seg_list': chan_macd.seg_list, - 'unittf_list': chan_macd.unittf_list, - 'histset_list': chan_macd.histset_list, - 'klu_list': chan_macd.klu_list, - 'high_position_list': chan_macd.high_position_list, - 'high_empty_list': chan_macd.high_empty_list, - 'low_position_list': getattr(chan_macd, 'low_position_list', []), - 'low_empty_list': getattr(chan_macd, 'low_empty_list', []), - 'return_zero_list': chan_macd.return_zero_list, - 'cross0_up_list': chan_macd.cross0_up_list, - 'cross0_down_list': chan_macd.cross0_down_list - } - print(f"ChanMACD分析完成: seg={len(chan_macd.seg_list)}, unittf={len(chan_macd.unittf_list)}, histset={len(chan_macd.histset_list)}") - else: - print("未能获取KLU列表或列表为空") - chan_macd_data = { - 'seg_list': [], - 'unittf_list': [], - 'histset_list': [], - 'high_position_list': [], - 'high_empty_list': [], - 'return_zero_list': [], - 'cross0_up_list': [], - 'cross0_down_list': [] - } - except Exception as e: - print(f"ChanMACD分析出错: {e}") - import traceback - traceback.print_exc() - chan_macd_data = { - 'seg_list': [], - 'unittf_list': [], - 'histset_list': [], - 'high_position_list': [], - 'high_empty_list': [], - 'low_position_list': [], - 'low_empty_list': [], - 'return_zero_list': [], - 'cross0_up_list': [], - 'cross0_down_list': [] - } - - # 提取K线分型信息 - klc_fx_info = [] - for klc in klc_list: - if hasattr(klc, 'klc_fx_type') and klc.klc_fx_type != Chan_KLC_FX.UNKNOWN: - try: - # 计算分型强度 - fx_strength = 0 - fx_strength_level = "" - is_strong_fx = False - - # 统一使用cal_fx_strength函数 - if hasattr(klc, 'cal_fx_strength'): - fx_strength = klc.cal_fx_strength(5) - - # 尝试获取分型强度等级 - if hasattr(klc, 'get_fx_strength_level'): - fx_strength_level = klc.get_fx_strength_level() - - # 尝试判断是否为强分型 - if hasattr(klc, 'is_strong_fx'): - is_strong_fx = klc.is_strong_fx() - - # 如果分型强度小于1,设为0 - if fx_strength < 1: - fx_strength = 0 - if klc.bb_out: - klc_fx_info.append({ - 'time': klc.end_time, - 'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high, - 'fx_type': str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""), - 'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM, - 'fx_strength': fx_strength, # 分型强度分数 (0-100) - 'fx_strength_level': fx_strength_level, # 分型强度等级 (极强/强/中等/弱/极弱) - 'is_strong_fx': is_strong_fx # 是否为强分型 - }) - except Exception as e: - # 如果出错,仍然添加基本信息,但分型强度为0 - klc_fx_info.append({ - 'time': klc.end_time, - 'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high, - 'fx_type': str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""), - 'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM, - 'fx_strength': 0, - 'fx_strength_level': "", - 'is_strong_fx': False - }) - - - return { - 'klc_list': klc_list, - 'klu_list': klu_list, # 添加KLU列表 - 'bi_list': bi_list, - 'seg_list': seg_list, - 'zs_list': zs_list, - 'bi_zs_list': bi_zs_list, # 添加BI中枢列表 - 'bsp_list': bsp_list, # 添加买卖点列表 - 'klc_fx_info': klc_fx_info, # KLC分型信息 - 'chan_macd': chan_macd_data, # 添加ChanMACD分析数据 - 'ema52_dict': ema52_dict # 添加多时间周期EMA52数据 - } + """进行缠论分析""" + chan = TF_DF() + + # 初始化多时间周期数据以获取EMA52 + ema52_dict = None + # 获取分析结果 + klu_list = chan.get_kl_data(df) + klc_list = chan.get_klc_list(klu_list) + bi_list = chan.cal_bi_list(klc_list) + #for index in range(0, 10): + #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 = [] + bsp_list = [] + if len(bi_zs_list) > 0: + bsp_list = chan.find_all_bsp(bi_list, bi_zs_list) + #for bsp in bsp_list: + #print(bsp.end_time, bsp.type, bsp.dir) + # 添加买卖点识别 + for bi in bi_list: + bi.cal_macdhist() + for bi in bi_list: + bi.cal_macd_div() + #print(bi.start_time, bi.macd_hist, bi.macd_div) + + # 添加ChanMACD分析 + chan_macd = None + chan_macd_data = {} + try: + + if klu_list and len(klu_list) > 0: + print(f"获取到KLU列表,长度: {len(klu_list)}") + chan_macd = ChanMACD(klu_list) + chan_macd_data = { + 'seg_list': chan_macd.seg_list, + 'unittf_list': chan_macd.unittf_list, + 'histset_list': chan_macd.histset_list, + 'klu_list': chan_macd.klu_list, + 'high_position_list': chan_macd.high_position_list, + 'high_empty_list': chan_macd.high_empty_list, + 'low_position_list': getattr(chan_macd, 'low_position_list', []), + 'low_empty_list': getattr(chan_macd, 'low_empty_list', []), + 'return_zero_list': chan_macd.return_zero_list, + 'cross0_up_list': chan_macd.cross0_up_list, + 'cross0_down_list': chan_macd.cross0_down_list + } + print(f"ChanMACD分析完成: seg={len(chan_macd.seg_list)}, unittf={len(chan_macd.unittf_list)}, histset={len(chan_macd.histset_list)}") + else: + print("未能获取KLU列表或列表为空") + chan_macd_data = { + 'seg_list': [], + 'unittf_list': [], + 'histset_list': [], + 'high_position_list': [], + 'high_empty_list': [], + 'return_zero_list': [], + 'cross0_up_list': [], + 'cross0_down_list': [] + } + except Exception as e: + print(f"ChanMACD分析出错: {e}") + import traceback + traceback.print_exc() + chan_macd_data = { + 'seg_list': [], + 'unittf_list': [], + 'histset_list': [], + 'high_position_list': [], + 'high_empty_list': [], + 'low_position_list': [], + 'low_empty_list': [], + 'return_zero_list': [], + 'cross0_up_list': [], + 'cross0_down_list': [] + } + + # 提取K线分型信息 + klc_fx_info = [] + for klc in klc_list: + if hasattr(klc, 'klc_fx_type') and klc.klc_fx_type != Chan_KLC_FX.UNKNOWN: + try: + # 计算分型强度 + fx_strength = 0 + fx_strength_level = "" + is_strong_fx = False + + # 统一使用cal_fx_strength函数 + if hasattr(klc, 'cal_fx_strength'): + fx_strength = klc.cal_fx_strength(5) + + # 尝试获取分型强度等级 + if hasattr(klc, 'get_fx_strength_level'): + fx_strength_level = klc.get_fx_strength_level() + + # 尝试判断是否为强分型 + if hasattr(klc, 'is_strong_fx'): + is_strong_fx = klc.is_strong_fx() + + # 如果分型强度小于1,设为0 + if fx_strength < 1: + fx_strength = 0 + if klc.bb_out: + klc_fx_info.append({ + 'time': klc.end_time, + 'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high, + 'fx_type': str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""), + 'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM, + 'fx_strength': fx_strength, # 分型强度分数 (0-100) + 'fx_strength_level': fx_strength_level, # 分型强度等级 (极强/强/中等/弱/极弱) + 'is_strong_fx': is_strong_fx # 是否为强分型 + }) + except Exception as e: + # 如果出错,仍然添加基本信息,但分型强度为0 + klc_fx_info.append({ + 'time': klc.end_time, + 'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high, + 'fx_type': str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""), + 'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM, + 'fx_strength': 0, + 'fx_strength_level': "", + 'is_strong_fx': False + }) + + + return { + 'klc_list': klc_list, + 'klu_list': klu_list, # 添加KLU列表 + 'bi_list': bi_list, + 'seg_list': seg_list, + 'zs_list': zs_list, + 'bi_zs_list': bi_zs_list, # 添加BI中枢列表 + 'bsp_list': bsp_list, # 添加买卖点列表 + 'klc_fx_info': klc_fx_info, # KLC分型信息 + 'chan_macd': chan_macd_data, # 添加ChanMACD分析数据 + 'ema52_dict': ema52_dict # 添加多时间周期EMA52数据 + } # 辅助函数,转换缠论方向枚举为整数 def convert_direction(direction): - """转换方向枚举为数字""" - if direction == Chan_BI_DIR.UP or direction == Chan_SEG_DIR.UP: - return 1 - elif direction == Chan_BI_DIR.DOWN or direction == Chan_SEG_DIR.DOWN: - return -1 - else: - return 0 + """转换方向枚举为数字""" + if direction == Chan_BI_DIR.UP or direction == Chan_SEG_DIR.UP: + return 1 + elif direction == Chan_BI_DIR.DOWN or direction == Chan_SEG_DIR.DOWN: + return -1 + else: + return 0 def format_time_safely(time_obj, client_tz): - """安全地格式化时间对象,处理字符串和datetime两种情况""" - if time_obj is None: - return None - - if isinstance(time_obj, str): - # 尝试将字符串解析为datetime - try: - from dateutil import parser - time_obj = parser.parse(time_obj) - return time_obj.astimezone(client_tz).isoformat() - except: - return time_obj - else: - # 已经是datetime对象 - return time_obj.astimezone(client_tz).isoformat() + """安全地格式化时间对象,处理字符串和datetime两种情况""" + if time_obj is None: + return None + + if isinstance(time_obj, str): + # 尝试将字符串解析为datetime + try: + from dateutil import parser + time_obj = parser.parse(time_obj) + return time_obj.astimezone(client_tz).isoformat() + except: + return time_obj + else: + # 已经是datetime对象 + return time_obj.astimezone(client_tz).isoformat() def serialize_chan_macd_data(chan_macd_data, client_tz): - """序列化ChanMACD数据为JSON可序列化格式""" - serialized_data = { - 'seg_list': [], - 'unittf_list': [], - 'histset_list': [], - # 状态标记数据 - 'high_position_list': [], - 'high_empty_list': [], - 'low_position_list': [], - 'low_empty_list': [], - 'return_zero_list': [], - 'cross0_up_list': [], - 'cross0_down_list': [], - # 新增:输出KLU的继续背驰/分离背驰标志 - 'klu_list': [] - } - - # 序列化seg_list - for seg in chan_macd_data.get('seg_list', []): - try: - seg_data = { - 'start_time': format_time_safely(seg.start_time, client_tz), - 'end_time': format_time_safely(seg.end_time, client_tz) if seg.end_time else None, - 'seg_dir': 'ABOVE' if seg.seg_dir == Chan_MACDSEG_DIR.ABOVE else 'UNDER', - 'klu_count': len(seg.klu_list) if hasattr(seg, 'klu_list') else 0, - 'unittf_count': len(seg.unittf_list) if hasattr(seg, 'unittf_list') else 0, - 'histset_count': len(seg.hist_set) if hasattr(seg, 'hist_set') else 0 - } - serialized_data['seg_list'].append(seg_data) - except Exception as e: - print(f"序列化seg出错: {e}") - continue - - # 序列化unittf_list(兼容新结构与枚举类型) - for unittf in chan_macd_data.get('unittf_list', []): - try: - dir_value = getattr(unittf, 'uinttf_dir', None) - dir_name = getattr(dir_value, 'name', dir_value if isinstance(dir_value, str) else None) - start_t = getattr(unittf, 'start_type', None) - start_type = getattr(start_t, 'name', start_t) - end_t = getattr(unittf, 'end_type', None) - end_type = getattr(end_t, 'name', end_t) - peak_abs = getattr(unittf, 'peak_abs', None) - if peak_abs is None: - peak_abs = getattr(unittf, 'peak_hist', None) - length = getattr(unittf, 'length', None) - if length is None: - length = len(unittf.klu_list) if hasattr(unittf, 'klu_list') else None + """序列化ChanMACD数据为JSON可序列化格式""" + serialized_data = { + 'seg_list': [], + 'unittf_list': [], + 'histset_list': [], + # 状态标记数据 + 'high_position_list': [], + 'high_empty_list': [], + 'low_position_list': [], + 'low_empty_list': [], + 'return_zero_list': [], + 'cross0_up_list': [], + 'cross0_down_list': [], + # 新增:输出KLU的继续背驰/分离背驰标志 + 'klu_list': [] + } + + # 序列化seg_list + for seg in chan_macd_data.get('seg_list', []): + try: + seg_data = { + 'start_time': format_time_safely(seg.start_time, client_tz), + 'end_time': format_time_safely(seg.end_time, client_tz) if seg.end_time else None, + 'seg_dir': 'ABOVE' if seg.seg_dir == Chan_MACDSEG_DIR.ABOVE else 'UNDER', + 'klu_count': len(seg.klu_list) if hasattr(seg, 'klu_list') else 0, + 'unittf_count': len(seg.unittf_list) if hasattr(seg, 'unittf_list') else 0, + 'histset_count': len(seg.hist_set) if hasattr(seg, 'hist_set') else 0 + } + serialized_data['seg_list'].append(seg_data) + except Exception as e: + print(f"序列化seg出错: {e}") + continue + + # 序列化unittf_list(兼容新结构与枚举类型) + for unittf in chan_macd_data.get('unittf_list', []): + try: + dir_value = getattr(unittf, 'uinttf_dir', None) + dir_name = getattr(dir_value, 'name', dir_value if isinstance(dir_value, str) else None) + start_t = getattr(unittf, 'start_type', None) + start_type = getattr(start_t, 'name', start_t) + end_t = getattr(unittf, 'end_type', None) + end_type = getattr(end_t, 'name', end_t) + peak_abs = getattr(unittf, 'peak_abs', None) + if peak_abs is None: + peak_abs = getattr(unittf, 'peak_hist', None) + length = getattr(unittf, 'length', None) + if length is None: + length = len(unittf.klu_list) if hasattr(unittf, 'klu_list') else None - unittf_data = { - 'start_time': format_time_safely(getattr(unittf, 'start_time', None), client_tz), - 'end_time': format_time_safely(getattr(unittf, 'end_time', None), client_tz) if getattr(unittf, 'end_time', None) else None, - 'dir': dir_name, # 'ABOVE' | 'UNDER' | None - 'start_type': start_type, # e.g. 'START' | 'CROSS0' | 'NEAR0_UP' | 'NEAR0_DOWN' - 'end_type': end_type, - 'invalid': getattr(unittf, 'invalid', False), - 'peak_abs': peak_abs, - 'length': length, - 'klu_count': len(unittf.klu_list) if hasattr(unittf, 'klu_list') else 0, - 'histset_count': len(unittf.histset_list) if hasattr(unittf, 'histset_list') else 0 - } - serialized_data['unittf_list'].append(unittf_data) - except Exception as e: - print(f"序列化unittf出错: {e}") - continue - - # 序列化histset_list - for histset in chan_macd_data.get('histset_list', []): - try: - histset_data = { - 'start_time': format_time_safely(getattr(histset, 'start_time', None), client_tz), - 'end_time': format_time_safely(getattr(histset, 'end_time', None), client_tz), - 'histset_dir': 'ABOVE' if histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE else 'UNDER', - 'klu_count': len(histset.klu_list) if hasattr(histset, 'klu_list') else 0 - } - serialized_data['histset_list'].append(histset_data) - except Exception as e: - print(f"序列化histset出错: {e}") - continue - - # 序列化状态标记数据 - # 序列化高位列表 - for high_pos in chan_macd_data.get('high_position_list', []): - try: - high_pos_data = { - 'time': format_time_safely(high_pos['time'], client_tz), - 'end_time': format_time_safely(high_pos.get('end_time'), client_tz) if high_pos.get('end_time') else None, - 'type': high_pos.get('type', 'start'), - 'macd': high_pos.get('macd'), - 'signal': high_pos.get('signal'), - 'macdhist': high_pos.get('macdhist'), - 'end_macd': high_pos.get('end_macd'), - 'end_signal': high_pos.get('end_signal'), - 'end_macdhist': high_pos.get('end_macdhist') - } - serialized_data['high_position_list'].append(high_pos_data) - except Exception as e: - print(f"序列化high_position出错: {e}") - continue - - # 序列化高位空列表 - for high_empty in chan_macd_data.get('high_empty_list', []): - try: - high_empty_data = { - 'time': format_time_safely(high_empty['time'], client_tz), - 'end_time': format_time_safely(high_empty.get('end_time'), client_tz) if high_empty.get('end_time') else None, - 'type': high_empty.get('type', 'start'), - 'macd': high_empty.get('macd'), - 'signal': high_empty.get('signal'), - 'macdhist': high_empty.get('macdhist'), - 'end_macd': high_empty.get('end_macd'), - 'end_signal': high_empty.get('end_signal'), - 'end_macdhist': high_empty.get('end_macdhist') - } - serialized_data['high_empty_list'].append(high_empty_data) - except Exception as e: - print(f"序列化high_empty出错: {e}") - continue - - # 序列化低位与低位空 - for low_pos in chan_macd_data.get('low_position_list', []): - try: - low_pos_data = { - 'time': format_time_safely(low_pos['time'], client_tz), - 'end_time': format_time_safely(low_pos.get('end_time'), client_tz) if low_pos.get('end_time') else None, - 'type': low_pos.get('type', 'start'), - 'macd': low_pos.get('macd'), - 'signal': low_pos.get('signal'), - 'macdhist': low_pos.get('macdhist'), - 'end_macd': low_pos.get('end_macd'), - 'end_signal': low_pos.get('end_signal'), - 'end_macdhist': low_pos.get('end_macdhist') - } - serialized_data['low_position_list'].append(low_pos_data) - except Exception as e: - print(f"序列化low_position出错: {e}") - continue + unittf_data = { + 'start_time': format_time_safely(getattr(unittf, 'start_time', None), client_tz), + 'end_time': format_time_safely(getattr(unittf, 'end_time', None), client_tz) if getattr(unittf, 'end_time', None) else None, + 'dir': dir_name, # 'ABOVE' | 'UNDER' | None + 'start_type': start_type, # e.g. 'START' | 'CROSS0' | 'NEAR0_UP' | 'NEAR0_DOWN' + 'end_type': end_type, + 'invalid': getattr(unittf, 'invalid', False), + 'peak_abs': peak_abs, + 'length': length, + 'klu_count': len(unittf.klu_list) if hasattr(unittf, 'klu_list') else 0, + 'histset_count': len(unittf.histset_list) if hasattr(unittf, 'histset_list') else 0 + } + serialized_data['unittf_list'].append(unittf_data) + except Exception as e: + print(f"序列化unittf出错: {e}") + continue + + # 序列化histset_list + for histset in chan_macd_data.get('histset_list', []): + try: + histset_data = { + 'start_time': format_time_safely(getattr(histset, 'start_time', None), client_tz), + 'end_time': format_time_safely(getattr(histset, 'end_time', None), client_tz), + 'histset_dir': 'ABOVE' if histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE else 'UNDER', + 'klu_count': len(histset.klu_list) if hasattr(histset, 'klu_list') else 0 + } + serialized_data['histset_list'].append(histset_data) + except Exception as e: + print(f"序列化histset出错: {e}") + continue + + # 序列化状态标记数据 + # 序列化高位列表 + for high_pos in chan_macd_data.get('high_position_list', []): + try: + high_pos_data = { + 'time': format_time_safely(high_pos['time'], client_tz), + 'end_time': format_time_safely(high_pos.get('end_time'), client_tz) if high_pos.get('end_time') else None, + 'type': high_pos.get('type', 'start'), + 'macd': high_pos.get('macd'), + 'signal': high_pos.get('signal'), + 'macdhist': high_pos.get('macdhist'), + 'end_macd': high_pos.get('end_macd'), + 'end_signal': high_pos.get('end_signal'), + 'end_macdhist': high_pos.get('end_macdhist') + } + serialized_data['high_position_list'].append(high_pos_data) + except Exception as e: + print(f"序列化high_position出错: {e}") + continue + + # 序列化高位空列表 + for high_empty in chan_macd_data.get('high_empty_list', []): + try: + high_empty_data = { + 'time': format_time_safely(high_empty['time'], client_tz), + 'end_time': format_time_safely(high_empty.get('end_time'), client_tz) if high_empty.get('end_time') else None, + 'type': high_empty.get('type', 'start'), + 'macd': high_empty.get('macd'), + 'signal': high_empty.get('signal'), + 'macdhist': high_empty.get('macdhist'), + 'end_macd': high_empty.get('end_macd'), + 'end_signal': high_empty.get('end_signal'), + 'end_macdhist': high_empty.get('end_macdhist') + } + serialized_data['high_empty_list'].append(high_empty_data) + except Exception as e: + print(f"序列化high_empty出错: {e}") + continue + + # 序列化低位与低位空 + for low_pos in chan_macd_data.get('low_position_list', []): + try: + low_pos_data = { + 'time': format_time_safely(low_pos['time'], client_tz), + 'end_time': format_time_safely(low_pos.get('end_time'), client_tz) if low_pos.get('end_time') else None, + 'type': low_pos.get('type', 'start'), + 'macd': low_pos.get('macd'), + 'signal': low_pos.get('signal'), + 'macdhist': low_pos.get('macdhist'), + 'end_macd': low_pos.get('end_macd'), + 'end_signal': low_pos.get('end_signal'), + 'end_macdhist': low_pos.get('end_macdhist') + } + serialized_data['low_position_list'].append(low_pos_data) + except Exception as e: + print(f"序列化low_position出错: {e}") + continue - for low_empty in chan_macd_data.get('low_empty_list', []): - try: - low_empty_data = { - 'time': format_time_safely(low_empty['time'], client_tz), - 'end_time': format_time_safely(low_empty.get('end_time'), client_tz) if low_empty.get('end_time') else None, - 'type': low_empty.get('type', 'start'), - 'macd': low_empty.get('macd'), - 'signal': low_empty.get('signal'), - 'macdhist': low_empty.get('macdhist'), - 'end_macd': low_empty.get('end_macd'), - 'end_signal': low_empty.get('end_signal'), - 'end_macdhist': low_empty.get('end_macdhist') - } - serialized_data['low_empty_list'].append(low_empty_data) - except Exception as e: - print(f"序列化low_empty出错: {e}") - continue + for low_empty in chan_macd_data.get('low_empty_list', []): + try: + low_empty_data = { + 'time': format_time_safely(low_empty['time'], client_tz), + 'end_time': format_time_safely(low_empty.get('end_time'), client_tz) if low_empty.get('end_time') else None, + 'type': low_empty.get('type', 'start'), + 'macd': low_empty.get('macd'), + 'signal': low_empty.get('signal'), + 'macdhist': low_empty.get('macdhist'), + 'end_macd': low_empty.get('end_macd'), + 'end_signal': low_empty.get('end_signal'), + 'end_macdhist': low_empty.get('end_macdhist') + } + serialized_data['low_empty_list'].append(low_empty_data) + except Exception as e: + print(f"序列化low_empty出错: {e}") + continue - # 序列化归零轴列表 - for return_zero in chan_macd_data.get('return_zero_list', []): - try: - return_zero_data = { - 'time': format_time_safely(return_zero['time'], client_tz), - 'end_time': format_time_safely(return_zero.get('end_time'), client_tz) if return_zero.get('end_time') else None, - 'type': return_zero.get('type', 'start'), - 'macd': return_zero.get('macd'), - 'signal': return_zero.get('signal'), - 'macdhist': return_zero.get('macdhist'), - 'end_macd': return_zero.get('end_macd'), - 'end_signal': return_zero.get('end_signal'), - 'end_macdhist': return_zero.get('end_macdhist') - } - serialized_data['return_zero_list'].append(return_zero_data) - except Exception as e: - print(f"序列化return_zero出错: {e}") - continue - - # 序列化穿越零轴列表 - for cross0_up in chan_macd_data.get('cross0_up_list', []): - try: - cross0_up_data = { - 'time': format_time_safely(cross0_up['time'], client_tz), - 'type': cross0_up.get('type', 'start'), - 'macd': cross0_up.get('macd'), - 'signal': cross0_up.get('signal'), - 'macdhist': cross0_up.get('macdhist') - } - serialized_data['cross0_up_list'].append(cross0_up_data) - except Exception as e: - print(f"序列化cross0_up出错: {e}") - continue - - for cross0_down in chan_macd_data.get('cross0_down_list', []): - try: - cross0_down_data = { - 'time': format_time_safely(cross0_down['time'], client_tz), - 'type': cross0_down.get('type', 'start'), - 'macd': cross0_down.get('macd'), - 'signal': cross0_down.get('signal'), - 'macdhist': cross0_down.get('macdhist') - } - serialized_data['cross0_down_list'].append(cross0_down_data) - except Exception as e: - print(f"序列化cross0_down出错: {e}") - continue + # 序列化归零轴列表 + for return_zero in chan_macd_data.get('return_zero_list', []): + try: + return_zero_data = { + 'time': format_time_safely(return_zero['time'], client_tz), + 'end_time': format_time_safely(return_zero.get('end_time'), client_tz) if return_zero.get('end_time') else None, + 'type': return_zero.get('type', 'start'), + 'macd': return_zero.get('macd'), + 'signal': return_zero.get('signal'), + 'macdhist': return_zero.get('macdhist'), + 'end_macd': return_zero.get('end_macd'), + 'end_signal': return_zero.get('end_signal'), + 'end_macdhist': return_zero.get('end_macdhist') + } + serialized_data['return_zero_list'].append(return_zero_data) + except Exception as e: + print(f"序列化return_zero出错: {e}") + continue + + # 序列化穿越零轴列表 + for cross0_up in chan_macd_data.get('cross0_up_list', []): + try: + cross0_up_data = { + 'time': format_time_safely(cross0_up['time'], client_tz), + 'type': cross0_up.get('type', 'start'), + 'macd': cross0_up.get('macd'), + 'signal': cross0_up.get('signal'), + 'macdhist': cross0_up.get('macdhist') + } + serialized_data['cross0_up_list'].append(cross0_up_data) + except Exception as e: + print(f"序列化cross0_up出错: {e}") + continue + + for cross0_down in chan_macd_data.get('cross0_down_list', []): + try: + cross0_down_data = { + 'time': format_time_safely(cross0_down['time'], client_tz), + 'type': cross0_down.get('type', 'start'), + 'macd': cross0_down.get('macd'), + 'signal': cross0_down.get('signal'), + 'macdhist': cross0_down.get('macdhist') + } + serialized_data['cross0_down_list'].append(cross0_down_data) + except Exception as e: + print(f"序列化cross0_down出错: {e}") + continue - # 序列化 KLU 列表(仅导出需要的时间与背驰标志) - for klu in chan_macd_data.get('klu_list', []): - try: - serialized_data['klu_list'].append({ - 'time': format_time_safely(getattr(klu, 'time', None), client_tz), - 'continue_div': bool(getattr(klu, 'continue_div', False)), - 'separate_div': int(getattr(klu, 'separate_div', 0)) if getattr(klu, 'separate_div', 0) is not None else 0, - 'near0_return': int(getattr(klu, 'near0_return', 0)) if getattr(klu, 'near0_return', 0) is not None else 0 - }) - except Exception as e: - print(f"序列化klu出错: {e}") - continue - - return serialized_data + # 序列化 KLU 列表(仅导出需要的时间与背驰标志) + for klu in chan_macd_data.get('klu_list', []): + try: + serialized_data['klu_list'].append({ + 'time': format_time_safely(getattr(klu, 'time', None), client_tz), + 'continue_div': bool(getattr(klu, 'continue_div', False)), + 'separate_div': int(getattr(klu, 'separate_div', 0)) if getattr(klu, 'separate_div', 0) is not None else 0, + 'near0_return': int(getattr(klu, 'near0_return', 0)) if getattr(klu, 'near0_return', 0) is not None else 0 + }) + except Exception as e: + print(f"序列化klu出错: {e}") + continue + + return serialized_data def is_smaller_timeframe(tf1, tf2): - """判断时间周期tf1是否小于tf2""" - tf1_value = timeframe_to_minutes(tf1) - tf2_value = timeframe_to_minutes(tf2) - if tf1_value is None or tf2_value is None: - return False - return tf1_value < tf2_value + """判断时间周期tf1是否小于tf2""" + tf1_value = timeframe_to_minutes(tf1) + tf2_value = timeframe_to_minutes(tf2) + if tf1_value is None or tf2_value is None: + return False + return tf1_value < tf2_value def is_smaller_or_equal_timeframe(tf1, tf2): - """判断时间周期tf1是否小于等于tf2""" - tf1_value = timeframe_to_minutes(tf1) - tf2_value = timeframe_to_minutes(tf2) - if tf1_value is None or tf2_value is None: - return False - return tf1_value <= tf2_value + """判断时间周期tf1是否小于等于tf2""" + tf1_value = timeframe_to_minutes(tf1) + tf2_value = timeframe_to_minutes(tf2) + if tf1_value is None or tf2_value is None: + return False + return tf1_value <= tf2_value def clean_dataframe_for_json(df): - """清理DataFrame数据用于JSON序列化""" - # 创建副本避免修改原始数据 - clean_df = df.copy() - - # 替换NaN值为None - clean_df = clean_df.where(pd.notnull(clean_df), None) - - return clean_df + """清理DataFrame数据用于JSON序列化""" + # 创建副本避免修改原始数据 + clean_df = df.copy() + + # 替换NaN值为None + clean_df = clean_df.where(pd.notnull(clean_df), None) + + return clean_df # ====== 趋势判定与趋势筛选(币对) ====== def classify_trend_stage(df): - """根据 EMA 斜率与多空排列判断趋势方向与阶段 - 返回: direction in {"bull","bear","sideways"}, stage in {"early","mid","late"}, strength_score (0-100) - """ - if df is None or len(df) < 60: - return "sideways", "early", 0 + """根据 EMA 斜率与多空排列判断趋势方向与阶段 + 返回: direction in {"bull","bear","sideways"}, stage in {"early","mid","late"}, strength_score (0-100) + """ + if df is None or len(df) < 60: + return "sideways", "early", 0 - # 使用 EMA5/10/24/52 - closes = df['close'].values - ema5 = df['ema5'].values if 'ema5' in df else ta.EMA(df, timeperiod=5) - ema10 = df['ema10'].values if 'ema10' in df else ta.EMA(df, timeperiod=10) - ema24 = df['ema24'].values if 'ema24' in df else ta.EMA(df, timeperiod=24) - ema52 = df['ema52'].values if 'ema52' in df else ta.EMA(df, timeperiod=52) + # 使用 EMA5/10/24/52 + closes = df['close'].values + ema5 = df['ema5'].values if 'ema5' in df else ta.EMA(df, timeperiod=5) + ema10 = df['ema10'].values if 'ema10' in df else ta.EMA(df, timeperiod=10) + ema24 = df['ema24'].values if 'ema24' in df else ta.EMA(df, timeperiod=24) + ema52 = df['ema52'].values if 'ema52' in df else ta.EMA(df, timeperiod=52) - # 最近N根用于斜率与排列判定 - lookback = min(30, len(df) - 1) - if lookback <= 5: - return "sideways", "early", 0 + # 最近N根用于斜率与排列判定 + lookback = min(30, len(df) - 1) + if lookback <= 5: + return "sideways", "early", 0 - # 简单斜率: 最近k根的线性变化率近似 - def slope(arr, k=10): - k = min(k, len(arr) - 1) - if k < 2: - return 0.0 - y = arr[-k:] - x = np.arange(k) - # 最小二乘拟合斜率 - denom = np.dot(x - x.mean(), x - x.mean()) - if denom == 0: - return 0.0 - m = np.dot(y - y.mean(), x - x.mean()) / denom - return float(m) + # 简单斜率: 最近k根的线性变化率近似 + def slope(arr, k=10): + k = min(k, len(arr) - 1) + if k < 2: + return 0.0 + y = arr[-k:] + x = np.arange(k) + # 最小二乘拟合斜率 + denom = np.dot(x - x.mean(), x - x.mean()) + if denom == 0: + return 0.0 + m = np.dot(y - y.mean(), x - x.mean()) / denom + return float(m) - k_slope = 12 # 斜率窗口 - s5 = slope(ema5, k_slope) - s10 = slope(ema10, k_slope) - s24 = slope(ema24, k_slope) - s52 = slope(ema52, k_slope) + k_slope = 12 # 斜率窗口 + s5 = slope(ema5, k_slope) + s10 = slope(ema10, k_slope) + s24 = slope(ema24, k_slope) + s52 = slope(ema52, k_slope) - # 多空排列 - last5, last10, last24, last52 = ema5[-1], ema10[-1], ema24[-1], ema52[-1] - bull_stack = last5 > last10 > last24 > last52 - bear_stack = last5 < last10 < last24 < last52 + # 多空排列 + last5, last10, last24, last52 = ema5[-1], ema10[-1], ema24[-1], ema52[-1] + bull_stack = last5 > last10 > last24 > last52 + bear_stack = last5 < last10 < last24 < last52 - # 波动性与动量增强: MACD 柱体最近均值 - macdhist = df['macdhist'].values if 'macdhist' in df else calculate_macd(df)['histogram'] - hist_recent = macdhist[-lookback:] - hist_power = float(np.mean(np.abs(hist_recent))) if len(hist_recent) else 0.0 + # 波动性与动量增强: MACD 柱体最近均值 + macdhist = df['macdhist'].values if 'macdhist' in df else calculate_macd(df)['histogram'] + hist_recent = macdhist[-lookback:] + hist_power = float(np.mean(np.abs(hist_recent))) if len(hist_recent) else 0.0 - # 方向 - if bull_stack and s24 > 0 and s52 > 0: - direction = "bull" - elif bear_stack and s24 < 0 and s52 < 0: - direction = "bear" - else: - # 用价格相对 EMA52 辅助 - if closes[-1] > last52 and (s24 + s52) > 0: - direction = "bull" - elif closes[-1] < last52 and (s24 + s52) < 0: - direction = "bear" - else: - direction = "sideways" + # 方向 + if bull_stack and s24 > 0 and s52 > 0: + direction = "bull" + elif bear_stack and s24 < 0 and s52 < 0: + direction = "bear" + else: + # 用价格相对 EMA52 辅助 + if closes[-1] > last52 and (s24 + s52) > 0: + direction = "bull" + elif closes[-1] < last52 and (s24 + s52) < 0: + direction = "bear" + else: + direction = "sideways" - # 阶段: 依据(斜率大小、与EMA52距离、MACD柱体扩张/收敛) - dist52 = float((closes[-1] - last52) / last52) if last52 else 0.0 - slope_score = max(0.0, (abs(s24) + abs(s52)) * 1000.0) # 归一化 - dist_score = min(50.0, abs(dist52) * 200.0) - hist_score = min(30.0, hist_power * 10.0) - strength = float(min(100.0, slope_score + dist_score + hist_score)) + # 阶段: 依据(斜率大小、与EMA52距离、MACD柱体扩张/收敛) + dist52 = float((closes[-1] - last52) / last52) if last52 else 0.0 + slope_score = max(0.0, (abs(s24) + abs(s52)) * 1000.0) # 归一化 + dist_score = min(50.0, abs(dist52) * 200.0) + hist_score = min(30.0, hist_power * 10.0) + strength = float(min(100.0, slope_score + dist_score + hist_score)) - # 简单阶段判定 - if direction == "sideways": - stage = "early" - strength = min(strength, 30.0) - else: - # 查看最近 hist 是否在扩大或收敛 - if len(hist_recent) >= 6: - recent_growth = np.mean(np.abs(hist_recent[-3:])) - np.mean(np.abs(hist_recent[-6:-3])) - else: - recent_growth = 0.0 + # 简单阶段判定 + if direction == "sideways": + stage = "early" + strength = min(strength, 30.0) + else: + # 查看最近 hist 是否在扩大或收敛 + if len(hist_recent) >= 6: + recent_growth = np.mean(np.abs(hist_recent[-3:])) - np.mean(np.abs(hist_recent[-6:-3])) + else: + recent_growth = 0.0 - if recent_growth > 0 and abs(dist52) < 0.05: - stage = "early" - elif recent_growth > 0 and abs(dist52) >= 0.05: - stage = "mid" - else: - stage = "late" + if recent_growth > 0 and abs(dist52) < 0.05: + stage = "early" + elif recent_growth > 0 and abs(dist52) >= 0.05: + stage = "mid" + else: + stage = "late" - return direction, stage, strength + return direction, stage, strength def load_crypto_symbols(limit=200): - """加载常见USDT永续合约交易对,返回列表""" - refresh_data_service_metadata() - if SYMBOLS: - return SYMBOLS[:limit] - try: - markets = exchange.load_markets() - symbols = [s for s in markets.keys() if '/USDT' in s and ':USDT' in s] - return symbols[:limit] - except Exception: - return DEFAULT_SYMBOLS[:limit] + """加载常见USDT永续合约交易对,返回列表""" + refresh_data_service_metadata() + if SYMBOLS: + return SYMBOLS[:limit] + try: + markets = exchange.load_markets() + symbols = [s for s in markets.keys() if '/USDT' in s and ':USDT' in s] + return symbols[:limit] + except Exception: + return DEFAULT_SYMBOLS[:limit] @app.route('/api/trend_filter', methods=['GET']) def trend_filter(): - """趋势筛选接口(币对) - 参数: - timeframe: K线周期 - start_time, end_time: 毫秒时间戳,可选 - direction: bull/bear/sideways 可选 - stage: early/mid/late 可选 - min_strength: 0-100 可选 - symbols: 逗号分隔列表,可选;不传则自动加载部分USDT币对 - 返回符合条件的币对与简要统计 - """ - timeframe = request.args.get('timeframe', '1h') - start_time = request.args.get('start_time') - end_time = request.args.get('end_time') - want_direction = request.args.get('direction') # 可为 None - want_stage = request.args.get('stage') # 可为 None - try: - min_strength = float(request.args.get('min_strength', '0')) - except ValueError: - min_strength = 0.0 + """趋势筛选接口(币对) + 参数: + timeframe: K线周期 + start_time, end_time: 毫秒时间戳,可选 + direction: bull/bear/sideways 可选 + stage: early/mid/late 可选 + min_strength: 0-100 可选 + symbols: 逗号分隔列表,可选;不传则自动加载部分USDT币对 + 返回符合条件的币对与简要统计 + """ + timeframe = request.args.get('timeframe', '1h') + start_time = request.args.get('start_time') + end_time = request.args.get('end_time') + want_direction = request.args.get('direction') # 可为 None + want_stage = request.args.get('stage') # 可为 None + try: + min_strength = float(request.args.get('min_strength', '0')) + except ValueError: + min_strength = 0.0 - symbols_param = request.args.get('symbols') - if symbols_param: - symbols_list = [s.strip() for s in symbols_param.split(',') if s.strip()] - else: - symbols_list = load_crypto_symbols(limit=150) + symbols_param = request.args.get('symbols') + if symbols_param: + symbols_list = [s.strip() for s in symbols_param.split(',') if s.strip()] + else: + symbols_list = load_crypto_symbols(limit=150) - results = [] - for sym in symbols_list: - try: - df = get_crypto_kl_data(sym, timeframe, start_time=start_time, end_time=end_time) - if df is None or len(df) < 60: - continue - df = add_indicators(df) - direction, stage, strength = classify_trend_stage(df) + results = [] + for sym in symbols_list: + try: + df = get_crypto_kl_data(sym, timeframe, start_time=start_time, end_time=end_time) + if df is None or len(df) < 60: + continue + df = add_indicators(df) + direction, stage, strength = classify_trend_stage(df) - if want_direction and direction != want_direction: - continue - if want_stage and stage != want_stage: - continue - if strength < min_strength: - continue + if want_direction and direction != want_direction: + continue + if want_stage and stage != want_stage: + continue + if strength < min_strength: + continue - last_row = df.iloc[-1] - results.append({ - 'symbol': sym, - 'time': int(last_row['timestamp']), - 'close': float(last_row['close']), - 'direction': direction, - 'stage': stage, - 'strength': float(round(strength, 2)), - 'ema5': float(last_row['ema5']), - 'ema10': float(last_row['ema10']), - 'ema24': float(last_row['ema24']), - 'ema52': float(last_row['ema52']) - }) - except Exception: - continue + last_row = df.iloc[-1] + results.append({ + 'symbol': sym, + 'time': int(last_row['timestamp']), + 'close': float(last_row['close']), + 'direction': direction, + 'stage': stage, + 'strength': float(round(strength, 2)), + 'ema5': float(last_row['ema5']), + 'ema10': float(last_row['ema10']), + 'ema24': float(last_row['ema24']), + 'ema52': float(last_row['ema52']) + }) + except Exception: + continue - # 按强度降序 - results.sort(key=lambda x: x['strength'], reverse=True) - return jsonify({ - 'count': len(results), - 'results': results - }) + # 按强度降序 + results.sort(key=lambda x: x['strength'], reverse=True) + return jsonify({ + 'count': len(results), + 'results': results + }) @app.route('/api/trend_detail', methods=['GET']) def trend_detail(): - """返回单个币对的K线与EMA、用于前端绘制趋势线 - 参数: symbol, timeframe, start_time, end_time - """ - symbol = request.args.get('symbol') - timeframe = request.args.get('timeframe', '1h') - start_time = request.args.get('start_time') - end_time = request.args.get('end_time') - timezone_name = request.args.get('timezone', 'Asia/Shanghai') + """返回单个币对的K线与EMA、用于前端绘制趋势线 + 参数: symbol, timeframe, start_time, end_time + """ + symbol = request.args.get('symbol') + timeframe = request.args.get('timeframe', '1h') + start_time = request.args.get('start_time') + end_time = request.args.get('end_time') + timezone_name = request.args.get('timezone', 'Asia/Shanghai') - if not symbol: - return jsonify({'error': 'symbol不能为空'}) + if not symbol: + return jsonify({'error': 'symbol不能为空'}) - df = get_crypto_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time) - if df is None or len(df) == 0: - return jsonify({'error': '获取数据失败'}) + df = get_crypto_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time) + if df is None or len(df) == 0: + return jsonify({'error': '获取数据失败'}) - df = add_indicators(df) - direction, stage, strength = classify_trend_stage(df) + df = add_indicators(df) + direction, stage, strength = classify_trend_stage(df) - # 简单趋势线: 用最近N根收盘价做线性拟合 - N = min(80, len(df)) - sub = df.tail(N) - y = sub['close'].values - x = np.arange(len(y)) - denom = np.dot(x - x.mean(), x - x.mean()) - if denom != 0: - m = float(np.dot(y - y.mean(), x - x.mean()) / denom) - b = float(y.mean() - m * x.mean()) - else: - m, b = 0.0, float(y[-1]) + # 简单趋势线: 用最近N根收盘价做线性拟合 + N = min(80, len(df)) + sub = df.tail(N) + y = sub['close'].values + x = np.arange(len(y)) + denom = np.dot(x - x.mean(), x - x.mean()) + if denom != 0: + m = float(np.dot(y - y.mean(), x - x.mean()) / denom) + b = float(y.mean() - m * x.mean()) + else: + m, b = 0.0, float(y[-1]) - client_tz = timezone(timezone_name) + client_tz = timezone(timezone_name) - return jsonify({ - 'symbol': symbol, - 'timeframe': timeframe, - 'timezone': timezone_name, - 'direction': direction, - 'stage': stage, - 'strength': float(round(strength, 2)), - 'kline_data': clean_dataframe_for_json(df)[['timestamp','open','high','low','close','volume','ema5','ema10','ema24','ema52']].to_dict('records'), - 'trend_line': { - 'offset': int(df.index[-N]), - 'slope': m, - 'intercept': b, - 'length': int(N) - } - }) + return jsonify({ + 'symbol': symbol, + 'timeframe': timeframe, + 'timezone': timezone_name, + 'direction': direction, + 'stage': stage, + 'strength': float(round(strength, 2)), + 'kline_data': clean_dataframe_for_json(df)[['timestamp','open','high','low','close','volume','ema5','ema10','ema24','ema52']].to_dict('records'), + 'trend_line': { + 'offset': int(df.index[-N]), + 'slope': m, + 'intercept': b, + 'length': int(N) + } + }) @app.route('/') def index(): - """主页""" - refresh_data_service_metadata() - timeframe_items = list(TIMEFRAMES.items()) - timeframe_keys = [item[0] for item in timeframe_items] - symbols = SYMBOLS if SYMBOLS else DEFAULT_SYMBOLS + """主页""" + refresh_data_service_metadata() + timeframe_items = list(TIMEFRAMES.items()) + timeframe_keys = [item[0] for item in timeframe_items] + symbols = SYMBOLS if SYMBOLS else DEFAULT_SYMBOLS - preferred_main = next((tf for tf in ['5m', '15m', '1h'] if tf in TIMEFRAMES), None) - default_main = preferred_main or (timeframe_keys[0] if timeframe_keys else '1m') - if default_main not in TIMEFRAMES and timeframe_keys: - default_main = timeframe_keys[0] + preferred_main = next((tf for tf in ['5m', '15m', '1h'] if tf in TIMEFRAMES), None) + default_main = preferred_main or (timeframe_keys[0] if timeframe_keys else '1m') + if default_main not in TIMEFRAMES and timeframe_keys: + default_main = timeframe_keys[0] - if timeframe_keys: - try: - idx = timeframe_keys.index(default_main) - default_element = timeframe_keys[idx - 1] if idx > 0 else timeframe_keys[0] - except ValueError: - default_element = timeframe_keys[0] - else: - default_element = default_main + if timeframe_keys: + try: + idx = timeframe_keys.index(default_main) + default_element = timeframe_keys[idx - 1] if idx > 0 else timeframe_keys[0] + except ValueError: + default_element = timeframe_keys[0] + else: + default_element = default_main - default_symbol = 'BTC/USDT:USDT' if 'BTC/USDT:USDT' in symbols else (symbols[0] if symbols else '') + default_symbol = 'BTC/USDT:USDT' if 'BTC/USDT:USDT' in symbols else (symbols[0] if symbols else '') - return render_template( - 'index.html', - timeframes=TIMEFRAMES, - symbols=symbols, - a_stock_symbols=A_STOCK_SYMBOLS, - default_main_timeframe=default_main, - default_element_timeframe=default_element, - default_symbol=default_symbol, - timeframe_keys_json=json.dumps(timeframe_keys), - data_service_available=DATA_SERVICE_AVAILABLE, - ) + return render_template( + 'index.html', + timeframes=TIMEFRAMES, + symbols=symbols, + a_stock_symbols=A_STOCK_SYMBOLS, + default_main_timeframe=default_main, + default_element_timeframe=default_element, + default_symbol=default_symbol, + timeframe_keys_json=json.dumps(timeframe_keys), + data_service_available=DATA_SERVICE_AVAILABLE, + ) @app.route('/api/analyze') def analyze(): - """分析接口""" - symbol = request.args.get('symbol', 'SOL/USDT:USDT') - timeframe = request.args.get('timeframe', '5m') - - # 验证交易对不为空 - if not symbol or symbol.strip() == '': - return jsonify({'error': '交易对不能为空'}) - - # 获取时间范围参数 - start_time = request.args.get('start_time') - end_time = request.args.get('end_time') - - # 获取客户端请求的时区 - client_timezone = request.args.get('timezone', 'Asia/Shanghai') - - # 获取分形元素时间周期 - element_timeframe = request.args.get('element_timeframe') - - # 获取是否只需要分形元素数据的参数 - elements_only_param = request.args.get('elements_only') - elements_only = elements_only_param == 'true' - - # 验证小周期是否小于主周期 - if element_timeframe and not is_smaller_or_equal_timeframe(element_timeframe, timeframe): - return jsonify({'error': '分形元素时间周期必须小于或等于主图表时间周期'}) - - # 获取数据 - df = get_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time) - if df is None: - return jsonify({'error': '获取数据失败'}) - - if len(df) == 0: - return jsonify({'error': '所选时间范围内没有数据'}) - - # 使用客户端指定的时区 - client_tz = timezone(client_timezone) - - # 如果只需要分形元素数据而不需要主周期数据,则初始化一个空结果 - result = { - 'timezone': client_timezone - } - - # 如果不是只需要分形元素数据,则添加主周期数据 - if not elements_only: - # 添加技术指标(包括布林带) - df = add_indicators(df) - - # 进行缠论分析 - analysis_result = analyze_chan(df, symbol, timeframe) - - # 计算MACD - macd_data = calculate_macd(df) - - # 基于已有 KLC 列表生成趋势标记(不做额外计算) - klc_trend = [] - try: - for klc in analysis_result.get('klc_list', []): - trend_val = getattr(klc, 'trend', None) - t_obj = getattr(klc, 'end_time', None) or getattr(klc, 'start_time', None) - if trend_val is None or t_obj is None: - continue - # 统一成字符串:UP/DOWN/FLAT/UNKNOWN - trend_name = str(trend_val) - if '.' in trend_name: - trend_name = trend_name.split('.')[-1] - time_str = format_time_safely(t_obj, client_tz) - if time_str: - klc_trend.append({'time': time_str, 'trend': trend_name}) - except Exception: - klc_trend = [] + """分析接口""" + symbol = request.args.get('symbol', 'SOL/USDT:USDT') + timeframe = request.args.get('timeframe', '5m') + + # 验证交易对不为空 + if not symbol or symbol.strip() == '': + return jsonify({'error': '交易对不能为空'}) + + # 获取时间范围参数 + start_time = request.args.get('start_time') + end_time = request.args.get('end_time') + + # 获取客户端请求的时区 + client_timezone = request.args.get('timezone', 'Asia/Shanghai') + + # 获取分形元素时间周期 + element_timeframe = request.args.get('element_timeframe') + + # 获取是否只需要分形元素数据的参数 + elements_only_param = request.args.get('elements_only') + elements_only = elements_only_param == 'true' + + # 验证小周期是否小于主周期 + if element_timeframe and not is_smaller_or_equal_timeframe(element_timeframe, timeframe): + return jsonify({'error': '分形元素时间周期必须小于或等于主图表时间周期'}) + + # 获取数据 + df = get_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time) + if df is None: + return jsonify({'error': '获取数据失败'}) + + if len(df) == 0: + return jsonify({'error': '所选时间范围内没有数据'}) + + # 使用客户端指定的时区 + client_tz = timezone(client_timezone) + + # 如果只需要分形元素数据而不需要主周期数据,则初始化一个空结果 + result = { + 'timezone': client_timezone + } + + # 如果不是只需要分形元素数据,则添加主周期数据 + if not elements_only: + # 添加技术指标(包括布林带) + df = add_indicators(df) + + # 进行缠论分析 + analysis_result = analyze_chan(df, symbol, timeframe) + + # 计算MACD + macd_data = calculate_macd(df) + + # 基于已有 KLC 列表生成趋势标记(不做额外计算) + klc_trend = [] + try: + for klc in analysis_result.get('klc_list', []): + trend_val = getattr(klc, 'trend', None) + t_obj = getattr(klc, 'end_time', None) or getattr(klc, 'start_time', None) + if trend_val is None or t_obj is None: + continue + # 统一成字符串:UP/DOWN/FLAT/UNKNOWN + trend_name = str(trend_val) + if '.' in trend_name: + trend_name = trend_name.split('.')[-1] + time_str = format_time_safely(t_obj, client_tz) + if time_str: + klc_trend.append({'time': time_str, 'trend': trend_name}) + except Exception: + klc_trend = [] - # 添加主周期分析结果到返回数据 - result.update({ - 'kline_data': clean_dataframe_for_json(df).to_dict('records'), - 'klc_list': [{ - 'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(), - 'open': float(klc.open), - 'high': float(klc.high), - 'low': float(klc.low), - 'close': float(klc.close), - 'volume': float(klc.volume) if hasattr(klc, 'volume') else 0, - 'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''), - 'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''), - 'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''), - 'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '') - } for klc in analysis_result['klc_list'] if hasattr(klc, 'end_time') and klc.end_time], - '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], - # 添加主周期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(), - 'end_time': None, # 未完成中枢没有结束时间 - 'zg': zs.zg, - 'zd': zs.zd, - 'gg': zs.gg, - 'dd': zs.dd, - 'is_sure': zs.is_sure # 未完成中枢的is_sure为False - } for zs in analysis_result['zs_list'] if not zs.is_sure], - # 添加未完成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)], + # 添加主周期分析结果到返回数据 + result.update({ + 'kline_data': clean_dataframe_for_json(df).to_dict('records'), + 'klc_list': [{ + 'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(), + 'open': float(klc.open), + 'high': float(klc.high), + 'low': float(klc.low), + 'close': float(klc.close), + 'volume': float(klc.volume) if hasattr(klc, 'volume') else 0, + 'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''), + 'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''), + 'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''), + 'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '') + } for klc in analysis_result['klc_list'] if hasattr(klc, 'end_time') and klc.end_time], + '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], + # 添加主周期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(), + 'end_time': None, # 未完成中枢没有结束时间 + 'zg': zs.zg, + 'zd': zs.zd, + 'gg': zs.gg, + 'dd': zs.dd, + 'is_sure': zs.is_sure # 未完成中枢的is_sure为False + } for zs in analysis_result['zs_list'] if not zs.is_sure], + # 添加未完成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': { - 'upper': df['bb_upper'].tolist(), - 'middle': df['bb_middle'].tolist(), - 'lower': df['bb_lower'].tolist() - }, - 'element_bollinger': { - 'upper': df['element_bb_upper'].tolist(), - '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), - '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']], - # 添加ChanMACD分析数据 - 'chan_macd': serialize_chan_macd_data(analysis_result.get('chan_macd', {}), client_tz), - # 添加多时间周期EMA52数据 - 'ema52_dict': analysis_result.get('ema52_dict', {}), - # 直接输出KLC趋势标记(使用已有trend字段) - 'klc_trend': klc_trend, - # 添加主周期买卖点列表 - # 注意:部分枚举在转为字符串时可能形如 "Chan_BSP_TYPE.BSP1(1)", - # 这里进行健壮的解析,确保前端拿到的始终是 "BSP1" / "BUY" 这种简洁形式, - # 以便与前端的 BSP_STYLE 键(如 "BSP1_BUY")正确匹配。 - 'bsp_list': [{ - 'time': format_time_safely(bsp.end_time, client_tz), - 'price': float(bsp.klc.low if 'BUY' in str(bsp.dir) else bsp.klc.high), - # -- 规范化 type 名称,例如: - # "Chan_BSP_TYPE.BSP1" -> "BSP1" - # "Chan_BSP_TYPE.BSP1(1)" -> "BSP1" - # "BSP1" -> "BSP1" - 'type': ( - lambda raw: ( - (raw.split('.')[-1] if '.' in raw else raw).split('(')[0] - ) - )(str(bsp.type)), - # -- 规范化 dir 名称,例如: - # "Chan_BSP_DIR.BUY" -> "BUY" - # "Chan_BSP_DIR.BUY(1)" -> "BUY" - # "BUY" -> "BUY" - 'dir': ( - lambda raw: ( - (raw.split('.')[-1] if '.' in raw else raw).split('(')[0] - ) - )(str(bsp.dir)), - 'is_sure': bool(bsp.is_sure), - 'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None, - 'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0 - } for bsp in analysis_result.get('bsp_list', [])] - }) - - - # 如果有指定分形元素时间周期,获取小周期数据 - if element_timeframe: - # 获取小周期数据,使用与主周期相同的时间范围 - element_df = get_kl_data(symbol, element_timeframe, start_time=start_time, end_time=end_time) - - if element_df is not None and len(element_df) > 0: - # 添加小周期技术指标(包括布林带) - element_df = add_indicators(element_df) - - # 对小周期数据进行缠论分析 - element_analysis = analyze_chan(element_df, symbol, element_timeframe) - - # 计算小周期MACD数据 - element_macd_data = calculate_macd(element_df) - - # 组装小周期 KLC 趋势(仅提取已有 trend,不做重算) - try: - element_klc_trend = [] - for klc in element_analysis.get('klc_list', []): - trend_val = getattr(klc, 'trend', None) - t_obj = getattr(klc, 'end_time', None) or getattr(klc, 'start_time', None) - if trend_val is None or t_obj is None: - continue - trend_name = str(trend_val) - if '.' in trend_name: - trend_name = trend_name.split('.')[-1] - time_str = format_time_safely(t_obj, client_tz) - if time_str: - element_klc_trend.append({'time': time_str, 'trend': trend_name}) - except Exception: - element_klc_trend = [] + 'macd': macd_data, + # 添加布林带数据 + 'bollinger': { + 'upper': df['bb_upper'].tolist(), + 'middle': df['bb_middle'].tolist(), + 'lower': df['bb_lower'].tolist() + }, + 'element_bollinger': { + 'upper': df['element_bb_upper'].tolist(), + '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), + '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']], + # 添加ChanMACD分析数据 + 'chan_macd': serialize_chan_macd_data(analysis_result.get('chan_macd', {}), client_tz), + # 添加多时间周期EMA52数据 + 'ema52_dict': analysis_result.get('ema52_dict', {}), + # 直接输出KLC趋势标记(使用已有trend字段) + 'klc_trend': klc_trend, + # 添加主周期买卖点列表 + # 注意:部分枚举在转为字符串时可能形如 "Chan_BSP_TYPE.BSP1(1)", + # 这里进行健壮的解析,确保前端拿到的始终是 "BSP1" / "BUY" 这种简洁形式, + # 以便与前端的 BSP_STYLE 键(如 "BSP1_BUY")正确匹配。 + 'bsp_list': [{ + 'time': format_time_safely(bsp.end_time, client_tz), + 'price': float(bsp.klc.low if 'BUY' in str(bsp.dir) else bsp.klc.high), + # -- 规范化 type 名称,例如: + # "Chan_BSP_TYPE.BSP1" -> "BSP1" + # "Chan_BSP_TYPE.BSP1(1)" -> "BSP1" + # "BSP1" -> "BSP1" + 'type': ( + lambda raw: ( + (raw.split('.')[-1] if '.' in raw else raw).split('(')[0] + ) + )(str(bsp.type)), + # -- 规范化 dir 名称,例如: + # "Chan_BSP_DIR.BUY" -> "BUY" + # "Chan_BSP_DIR.BUY(1)" -> "BUY" + # "BUY" -> "BUY" + 'dir': ( + lambda raw: ( + (raw.split('.')[-1] if '.' in raw else raw).split('(')[0] + ) + )(str(bsp.dir)), + 'is_sure': bool(bsp.is_sure), + 'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None, + 'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0 + } for bsp in analysis_result.get('bsp_list', [])] + }) + + + # 如果有指定分形元素时间周期,获取小周期数据 + if element_timeframe: + # 获取小周期数据,使用与主周期相同的时间范围 + element_df = get_kl_data(symbol, element_timeframe, start_time=start_time, end_time=end_time) + + if element_df is not None and len(element_df) > 0: + # 添加小周期技术指标(包括布林带) + element_df = add_indicators(element_df) + + # 对小周期数据进行缠论分析 + element_analysis = analyze_chan(element_df, symbol, element_timeframe) + + # 计算小周期MACD数据 + element_macd_data = calculate_macd(element_df) + + # 组装小周期 KLC 趋势(仅提取已有 trend,不做重算) + try: + element_klc_trend = [] + for klc in element_analysis.get('klc_list', []): + trend_val = getattr(klc, 'trend', None) + t_obj = getattr(klc, 'end_time', None) or getattr(klc, 'start_time', None) + if trend_val is None or t_obj is None: + continue + trend_name = str(trend_val) + if '.' in trend_name: + trend_name = trend_name.split('.')[-1] + time_str = format_time_safely(t_obj, client_tz) + if time_str: + element_klc_trend.append({'time': time_str, 'trend': trend_name}) + except Exception: + element_klc_trend = [] - # 添加小周期分析结果到返回数据 - result['element_timeframe'] = element_timeframe - result['element_macd'] = element_macd_data # 添加小周期MACD数据 - - # 添加小周期布林带数据 - result['element_bollinger'] = { - 'upper': element_df['bb_upper'].tolist(), - 'middle': element_df['bb_middle'].tolist(), - 'lower': element_df['bb_lower'].tolist() - } - result['element_element_bollinger'] = { - 'upper': element_df['element_bb_upper'].tolist(), - 'middle': element_df['element_bb_middle'].tolist(), - '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, - '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 element_analysis['bi_list'] if bi.end_klc] - # 添加次周期未完成笔列表 - result['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 element_analysis['bi_list'] if not bi.end_klc] - - # 添加小周期K线数据 - result['element_kline_data'] = clean_dataframe_for_json(element_df).to_dict('records') - - # 添加小周期KLC列表 - result['element_klc_list'] = [{ - 'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(), - 'open': float(klc.open), - 'high': float(klc.high), - 'low': float(klc.low), - 'close': float(klc.close), - 'volume': float(klc.volume) if hasattr(klc, 'volume') else 0, - 'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''), - 'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''), - 'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''), - 'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '') - } for klc in element_analysis['klc_list'] if hasattr(klc, 'end_time') and klc.end_time] - - result['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 element_analysis['seg_list'] if seg.is_sure] - # 添加次周期未完成线段列表 - result['element_uncompleted_seg_list'] = get_uncompleted_seg_list(element_analysis['seg_list'], client_tz) - - result['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 element_analysis['zs_list'] if zs.end_klc] - - result['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 # 未完成中枢的is_sure为False - } for zs in element_analysis['zs_list'] if not zs.is_sure] + # 添加小周期分析结果到返回数据 + result['element_timeframe'] = element_timeframe + result['element_macd'] = element_macd_data # 添加小周期MACD数据 + + # 添加小周期布林带数据 + result['element_bollinger'] = { + 'upper': element_df['bb_upper'].tolist(), + 'middle': element_df['bb_middle'].tolist(), + 'lower': element_df['bb_lower'].tolist() + } + result['element_element_bollinger'] = { + 'upper': element_df['element_bb_upper'].tolist(), + 'middle': element_df['element_bb_middle'].tolist(), + '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, + '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 element_analysis['bi_list'] if bi.end_klc] + # 添加次周期未完成笔列表 + result['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 element_analysis['bi_list'] if not bi.end_klc] + + # 添加小周期K线数据 + result['element_kline_data'] = clean_dataframe_for_json(element_df).to_dict('records') + + # 添加小周期KLC列表 + result['element_klc_list'] = [{ + 'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(), + 'open': float(klc.open), + 'high': float(klc.high), + 'low': float(klc.low), + 'close': float(klc.close), + 'volume': float(klc.volume) if hasattr(klc, 'volume') else 0, + 'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''), + 'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''), + 'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''), + 'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '') + } for klc in element_analysis['klc_list'] if hasattr(klc, 'end_time') and klc.end_time] + + result['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 element_analysis['seg_list'] if seg.is_sure] + # 添加次周期未完成线段列表 + result['element_uncompleted_seg_list'] = get_uncompleted_seg_list(element_analysis['seg_list'], client_tz) + + result['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 element_analysis['zs_list'] if zs.end_klc] + + result['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 # 未完成中枢的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_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 element_analysis['klc_fx_info']] + # 添加次周期 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_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 element_analysis['klc_fx_info']] - # 添加次周期ChanMACD分析数据 - result['element_chan_macd'] = serialize_chan_macd_data(element_analysis.get('chan_macd', {}), client_tz) - - # 添加小周期 KLC 趋势标记 - result['element_klc_trend'] = element_klc_trend - - # 添加次周期买卖点列表 - result['element_bsp_list'] = [{ - 'time': format_time_safely(bsp.end_time, client_tz), - 'price': float(bsp.klc.low if str(bsp.dir) == 'Chan_BSP_DIR.BUY' else bsp.klc.high), - 'type': str(bsp.type).replace('Chan_BSP_TYPE.', ''), - 'dir': str(bsp.dir).replace('Chan_BSP_DIR.', ''), - 'is_sure': bool(bsp.is_sure), - 'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None, - 'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0 - } for bsp in element_analysis.get('bsp_list', [])] + # 添加次周期ChanMACD分析数据 + result['element_chan_macd'] = serialize_chan_macd_data(element_analysis.get('chan_macd', {}), client_tz) + + # 添加小周期 KLC 趋势标记 + result['element_klc_trend'] = element_klc_trend + + # 添加次周期买卖点列表 + result['element_bsp_list'] = [{ + 'time': format_time_safely(bsp.end_time, client_tz), + 'price': float(bsp.klc.low if str(bsp.dir) == 'Chan_BSP_DIR.BUY' else bsp.klc.high), + 'type': str(bsp.type).replace('Chan_BSP_TYPE.', ''), + 'dir': str(bsp.dir).replace('Chan_BSP_DIR.', ''), + 'is_sure': bool(bsp.is_sure), + 'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None, + 'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0 + } for bsp in element_analysis.get('bsp_list', [])] - pass - - return jsonify(result) + pass + + return jsonify(result) @app.route('/api/symbols') def get_symbols(): - """获取可用交易对""" - refresh_data_service_metadata() - if SYMBOLS: - return jsonify(SYMBOLS) - try: - markets = exchange.load_markets() - # 合约交易对通常是以USDT结尾的永续合约 - symbols = [symbol for symbol in markets.keys() if '/USDT' in symbol and ':USDT' in symbol] - return jsonify(symbols) - except Exception as e: - return jsonify(DEFAULT_SYMBOLS) + """获取可用交易对""" + refresh_data_service_metadata() + if SYMBOLS: + return jsonify(SYMBOLS) + try: + markets = exchange.load_markets() + # 合约交易对通常是以USDT结尾的永续合约 + symbols = [symbol for symbol in markets.keys() if '/USDT' in symbol and ':USDT' in symbol] + return jsonify(symbols) + except Exception as e: + return jsonify(DEFAULT_SYMBOLS) @app.route('/api/a_stocks') def get_a_stocks(): - """获取A股股票列表""" - try: - stock_list = china_stock.get_stock_list() - return jsonify(stock_list) - except Exception as e: - return jsonify({'error': str(e)}) + """获取A股股票列表""" + try: + stock_list = china_stock.get_stock_list() + return jsonify(stock_list) + except Exception as e: + return jsonify({'error': str(e)}) @app.route('/api/popular_a_stocks') def get_popular_a_stocks(): - """获取热门A股股票""" - try: - return jsonify(china_stock.get_popular_stocks()) - except Exception as e: - return jsonify({'error': str(e)}) + """获取热门A股股票""" + try: + return jsonify(china_stock.get_popular_stocks()) + except Exception as e: + return jsonify({'error': str(e)}) @app.route('/api/sectors') def get_sectors(): - """获取所有行业分类""" - try: - sectors = china_stock.get_all_sectors() - return jsonify(sectors) - except Exception as e: - return jsonify({'error': str(e)}) + """获取所有行业分类""" + try: + sectors = china_stock.get_all_sectors() + return jsonify(sectors) + except Exception as e: + return jsonify({'error': str(e)}) @app.route('/api/stocks_by_sector') def get_stocks_by_sector(): - """根据行业获取股票""" - try: - sector = request.args.get('sector') - if sector: - stocks = china_stock.get_stock_by_sector(sector) - return jsonify(stocks) - else: - # 返回所有行业的股票分组 - all_sectors = china_stock.get_stock_by_sector() - return jsonify(all_sectors) - except Exception as e: - return jsonify({'error': str(e)}) + """根据行业获取股票""" + try: + sector = request.args.get('sector') + if sector: + stocks = china_stock.get_stock_by_sector(sector) + return jsonify(stocks) + else: + # 返回所有行业的股票分组 + all_sectors = china_stock.get_stock_by_sector() + return jsonify(all_sectors) + except Exception as e: + return jsonify({'error': str(e)}) @app.route('/api/search_stock') def search_stock(): - """搜索股票 - 增强版""" - try: - keyword = request.args.get('keyword', '') - if not keyword: - return jsonify({'error': '搜索关键词不能为空'}) - - results = china_stock.search_stock(keyword) - return jsonify(results) - except Exception as e: - return jsonify({'error': str(e)}) + """搜索股票 - 增强版""" + try: + keyword = request.args.get('keyword', '') + if not keyword: + return jsonify({'error': '搜索关键词不能为空'}) + + results = china_stock.search_stock(keyword) + return jsonify(results) + except Exception as e: + return jsonify({'error': str(e)}) def get_uncompleted_seg_list(seg_list, client_tz): - """获取未完成线段列表,正确处理倒数第二个和最后一个未完成线段""" - uncompleted_segs = [seg for seg in seg_list if not seg.is_sure] - - if len(uncompleted_segs) == 0: - return [] - - result = [] - - for i, seg in enumerate(uncompleted_segs): - is_last = (i == len(uncompleted_segs) - 1) # 是否为最后一个未完成线段 - - seg_data = { - '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(), - '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, - 'direction': convert_direction(seg.dir) - } - - if is_last: - # 最后一个未完成线段:没有结束时间和价格 - seg_data['end_time'] = None - seg_data['end_price'] = None - else: - # 倒数第二个及之前的未完成线段:使用实际的结束时间和价格 - if seg.end_bi and seg.end_bi.end_klc: - seg_data['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() - seg_data['end_price'] = seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low - else: - # 如果没有结束笔,设为None - seg_data['end_time'] = None - seg_data['end_price'] = None - - result.append(seg_data) - - return result + """获取未完成线段列表,正确处理倒数第二个和最后一个未完成线段""" + uncompleted_segs = [seg for seg in seg_list if not seg.is_sure] + + if len(uncompleted_segs) == 0: + return [] + + result = [] + + for i, seg in enumerate(uncompleted_segs): + is_last = (i == len(uncompleted_segs) - 1) # 是否为最后一个未完成线段 + + seg_data = { + '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(), + '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, + 'direction': convert_direction(seg.dir) + } + + if is_last: + # 最后一个未完成线段:没有结束时间和价格 + seg_data['end_time'] = None + seg_data['end_price'] = None + else: + # 倒数第二个及之前的未完成线段:使用实际的结束时间和价格 + if seg.end_bi and seg.end_bi.end_klc: + seg_data['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() + seg_data['end_price'] = seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low + else: + # 如果没有结束笔,设为None + seg_data['end_time'] = None + seg_data['end_price'] = None + + result.append(seg_data) + + return result if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=8128) \ No newline at end of file + app.run(debug=True, host='0.0.0.0', port=8128) \ No newline at end of file