From 0b4d7693b80a9ba5094511caf361c42a32d71ded Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Fri, 28 Aug 2026 04:04:44 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BC=A0=E8=AE=BA=E5=BC=95=E6=93=8E=E6=8F=90?= =?UTF-8?q?=E9=80=9F=202.6x=EF=BC=8C=E7=93=B6=E9=A2=88=E6=98=AF=E9=80=90?= =?UTF-8?q?=E8=A1=8C=20Series=20=E6=9F=A5=E6=89=BE=E8=80=8C=E9=9D=9E?= =?UTF-8?q?=E6=8C=87=E6=A0=87=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原以为浪费在 add_indicators 算了太多用不到的指标,实测它只占全量构建的 1.3%——talib 是向量化 C 代码,便宜。真正的两处: cal_kl_data 占 96%:每根 K 线 df.iloc[i] 新建一个 40 列 Series,再在其上做 几十次逐键查找。改为预取 ndarray 后 2 万根 1946ms → 824ms。 ChanKLC.cal_all_ema_status 占 25%:每次合并 KLU 都立即重算,而它产出的 ema_status / ema52_pos / ema52_status 全仓无任何读取方(含前端)。改为惰性 求值,保留属性形式以防将来有人读。顺带删掉 get_klc_list 里累加一整轮后直接 丢弃的 ema_up_list / ema_down_list。 另加 TF_DF(lean=True):只构建到中枢,跳过线段/走势中枢/MACD 状态机——这些 只服务 bsp_list 与 web 展示,笔和中枢不依赖。研究与实盘走这条快 3.6x。 结果 2 万根 5m:full 1946 → 754ms,lean → 543ms。 step46_engine_parity.py 是配套的安全网,改引擎前先跑一次 --save。它对 KLC 端点与分型、笔起止价与 is_sure、中枢 zg/zd/available_ts/阶梯、信号全部输出列, 以及 26 个被下游消费的 dataframe 列取哈希。本次三处改动逐步验证,另用 git stash 切回改动前代码在 20 万根 × 5 用例上做了跨版本逐位对拍,全部一致; 增量路径与 web API 也各验一遍。 Co-authored-by: Cursor --- chanlun/core/ChanKLC.py | 40 ++- chanlun/core/ChanKLU.py | 53 +-- chanlun/pipeline/builders/kline.py | 95 +++-- chanlun/pipeline/timeframe.py | 18 +- research/out/step46_baseline.json | 487 ++++++++++++++++++++++++++ research/out/step46_baseline_big.json | 487 ++++++++++++++++++++++++++ research/step46_engine_parity.py | 200 +++++++++++ 7 files changed, 1300 insertions(+), 80 deletions(-) create mode 100644 research/out/step46_baseline.json create mode 100644 research/out/step46_baseline_big.json create mode 100644 research/step46_engine_parity.py diff --git a/chanlun/core/ChanKLC.py b/chanlun/core/ChanKLC.py index 03cc4e8..4e6ac32 100644 --- a/chanlun/core/ChanKLC.py +++ b/chanlun/core/ChanKLC.py @@ -63,10 +63,11 @@ class ChanKLC(): self.bsp = False self.bsp_type = Chan_BSP_TYPE.NONE # EMA状态字典:key为EMA名称,value为 {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC} - self.ema_status = {} + self._ema_status = {} + self._ema_status_dirty = False # 向后兼容:保留 ema52_status 和 ema52_pos - self.ema52_status = 0 - self.ema52_pos = Chan_EMA_POS.UNKNOWN + self._ema52_status = 0 + self._ema52_pos = Chan_EMA_POS.UNKNOWN self.bb2633upper = klu.bb2633upper self.bb2633lower = klu.bb2633lower self.bb2633middle = klu.bb2633middle @@ -283,21 +284,44 @@ class ChanKLC(): 'ema156': self.ema156, 'ema208': self.ema208, } - self.ema_status = {} + self._ema_status_dirty = False + self._ema_status = {} for name, value in ema_configs.items(): # 按 EMA 值的百分比自动计算阈值 threshold = abs(value) * self.threshold_pct if value and self.threshold_pct > 0 else 0 pos = ChanKLC.cal_ema_pos(self.high, self.low, self.close, value, threshold) semantic = ChanKLC.cal_ema_semantic(pos, self.dir, self.ema_dir) - self.ema_status[name] = { + self._ema_status[name] = { 'pos': pos, 'semantic': semantic, 'value': value, 'threshold': threshold, } # 向后兼容 - self.ema52_pos = self.ema_status['ema52']['pos'] - self.ema52_status = ChanKLC.semantic_to_int(self.ema_status['ema52']['semantic']) + self._ema52_pos = self._ema_status['ema52']['pos'] + self._ema52_status = ChanKLC.semantic_to_int(self._ema_status['ema52']['semantic']) + + # 以下三个改成惰性求值。原来 set_end_klu 每次合并 KLU 都会立刻重算一遍, + # 实测占 TF_DF 构建的约 25%,而全仓(含前端)没有任何地方读取它的产出。 + # 保留属性形式是为了任何外部读取仍拿到正确值,只是推迟到真被读时才算。 + @property + def ema_status(self): + if self._ema_status_dirty: + self.cal_all_ema_status() + return self._ema_status + + @property + def ema52_pos(self): + if self._ema_status_dirty: + self.cal_all_ema_status() + return self._ema52_pos + + @property + def ema52_status(self): + if self._ema_status_dirty: + self.cal_all_ema_status() + return self._ema52_status + def get_ema_pos(self, ema_name): """获取指定EMA的客观位置,如 klc.get_ema_pos('ema24')""" if ema_name in self.ema_status: @@ -448,7 +472,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() + self._ema_status_dirty = True if self.open > self.high: self.open = self.high if self.close > self.high: diff --git a/chanlun/core/ChanKLU.py b/chanlun/core/ChanKLU.py index 36ae434..435ec62 100644 --- a/chanlun/core/ChanKLU.py +++ b/chanlun/core/ChanKLU.py @@ -160,27 +160,40 @@ class ChanKLU: return False else: return True + # 属性名 ← dataframe 列名。两条赋值路径(逐行 Series / 预取 ndarray)共用这张表, + # 免得将来加指标时只改一处、另一处静默漏掉。 + INDICATOR_FIELDS = ( + ('macd', 'macd'), ('signal', 'macdsignal'), ('macdhist', 'macdhist'), + ('ema26', 'ema26'), ('ema52', 'ema52'), ('ema24', 'ema24'), + ('ema104', 'ema104'), ('ema156', 'ema156'), ('ema208', 'ema208'), + ('ema13', 'ema13'), ('ema7', 'ema7'), ('ema5', 'ema5'), + ('rsi', 'rsi'), ('volume_ratio', 'volume_ratio'), + ('bb52upper', 'bb52upper'), ('bb52lower', 'bb52lower'), + ('bb2633upper', 'bb2633upper'), ('bb2633lower', 'bb2633lower'), + ('bb2633middle', 'bb2633middle'), ('ma5', 'ma5'), + ) + def set_indicators(self, item): - self.macd = float(item['macd']) if 'macd' in item and item['macd'] else 0 - self.signal = float(item['macdsignal']) if 'macdsignal' in item and item['macdsignal'] else 0 - self.macdhist = float(item['macdhist']) if 'macdhist' in item and item['macdhist'] else 0 - self.ema26 = float(item['ema26']) if 'ema26' in item and item['ema26'] else 0 - self.ema52 = float(item['ema52']) if 'ema52' in item and item['ema52'] else 0 - self.ema24 = float(item['ema24']) if 'ema24' in item and item['ema24'] else 0 - self.ema104 = float(item['ema104']) if 'ema104' in item and item['ema104'] else 0 - self.ema156 = float(item['ema156']) if 'ema156' in item and item['ema156'] else 0 - self.ema208 = float(item['ema208']) if 'ema208' in item and item['ema208'] else 0 - self.ema13 = float(item['ema13']) if 'ema13' in item and item['ema13'] else 0 - self.ema7 = float(item['ema7']) if 'ema7' in item and item['ema7'] else 0 - self.rsi = float(item['rsi']) if 'rsi' in item and item['rsi'] else 0 - 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 - self.ma5 = float(item['ma5']) if 'ma5' in item and item['ma5'] else 0 - self.ema5 = float(item['ema5']) if 'ema5' in item and item['ema5'] else 0 + """单根赋值。增量追加时每次只有一根,走这条即可。 + + 原写法是 `float(item[c]) if c in item and item[c] else 0`。其中的真值判断 + 是空转:值为 0.0 时 float(0.0) 仍是 0,值为 NaN 时 NaN 为真值、照样透传。 + 唯一起作用的是「列不存在则填 0」,所以这里只保留那一层。 + """ + for attr, col in self.INDICATOR_FIELDS: + v = item[col] if col in item else 0 + setattr(self, attr, float(v) if v else 0) + + def set_indicators_from(self, cols, i): + """从预取的 {列名: ndarray} 按下标赋值,语义与 set_indicators 相同。 + + 全量构建时用这条:避免每根 `df.iloc[i]` 构造一个 Series,再在其上做 + 几十次逐键查找——那是 TF_DF 构建 96% 的耗时所在。 + """ + for attr, col in self.INDICATOR_FIELDS: + arr = cols.get(col) + v = arr[i] if arr is not None else 0 + setattr(self, attr, float(v) if v else 0) def cal_macd_state(self): # 按定义精简实现:优先级 CROSS0 > 位置(HIGH/HE/RETURN_ZERO) > NEAR0 > UNKNOWN # 首条或缺前一根 diff --git a/chanlun/pipeline/builders/kline.py b/chanlun/pipeline/builders/kline.py index 807ff2f..9289d60 100644 --- a/chanlun/pipeline/builders/kline.py +++ b/chanlun/pipeline/builders/kline.py @@ -147,41 +147,53 @@ class KlineBuilderMixin: return df['volume_ratio'] def cal_kl_data(self, dataframe:DataFrame): - fields = "time,open,high,low,close,volume" + """按行构造 KLU 链。 + + 这里刻意不用 `dataframe.iloc[i]`:那会为每一根新建一个几十列的 Series, + 随后 set_indicators 再在其上做几十次逐键查找。实测这两件事合计占 TF_DF + 构建耗时的 96%。改为先把用到的列取成 ndarray,循环里只做整数下标访问。 + """ + n = len(dataframe) + if n == 0: + return [] + + times = self._format_times(dataframe['date']) + o_a = dataframe['open'].to_numpy(dtype=float) + h_a = dataframe['high'].to_numpy(dtype=float) + l_a = dataframe['low'].to_numpy(dtype=float) + c_a = dataframe['close'].to_numpy(dtype=float) + v_a = dataframe['volume'].to_numpy(dtype=float) + + has_ind = 'macd' in dataframe.columns + ind_cols = {} + if has_ind: + for _attr, col in ChanKLU.INDICATOR_FIELDS: + if col in dataframe.columns: + ind_cols[col] = dataframe[col].to_numpy(dtype=float) + klu_list = [] last_klu = None - for i in range(0, len(dataframe)): - item = dataframe.iloc[i] - date = item['date'] - o = item['open'] - h = item['high'] - l = item['low'] - c = item['close'] - v = item['volume'] - # time_obj = date.fromtimestamp(date) - # date = date + timedelta(hours=8) - time_str = date.strftime('%Y-%m-%d %H:%M:%S') - item_data = [ - time_str, - o, - h, - l, - c, - v - ] - # klu = KLU(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields))) - klu = ChanKLU(time_str, o, h, l, c, v) - # print(klu.time, klu.open, klu.high, klu.low, klu.close, klu.volume) + for i in range(n): + klu = ChanKLU(times[i], o_a[i], h_a[i], l_a[i], c_a[i], v_a[i]) klu.set_idx(i) klu_list.append(klu) if last_klu: last_klu.set_next(klu) klu.set_pre(last_klu) last_klu = klu - if 'macd' in item: - klu.set_indicators(item) + if has_ind: + klu.set_indicators_from(ind_cols, i) return klu_list + @staticmethod + def _format_times(col): + """向量化 strftime。非 datetime 列(少见)退回逐个格式化。""" + fmt = '%Y-%m-%d %H:%M:%S' + try: + return col.dt.strftime(fmt).to_numpy() + except AttributeError: + return np.array([d.strftime(fmt) for d in col], dtype=object) + def get_kl_data(self, dataframe:DataFrame): return self.cal_kl_data(dataframe) @@ -224,35 +236,20 @@ class KlineBuilderMixin: def get_klc_list(self, klu_list): klc_list = [] - last_klu = None - # ChanMACD.__init__ 已调用 cal_macd_state,切勿再调一次(会重复堆积 seg/unittf) - macd = ChanMACD(klu_list) - klu_list = macd.klu_list - self._last_chan_macd = macd - ema_up_list = [] - ema_down_list = [] - ema_up_count = 0 - ema_down_count = 0 + # ChanMACD.__init__ 已调用 cal_macd_state,切勿再调一次(会重复堆积 seg/unittf)。 + # lean 模式跳过整套 MACD 状态机:它只服务于 bsp/背驰/web 展示,笔与中枢不依赖它。 + if getattr(self, 'lean', False): + self._last_chan_macd = None + else: + macd = ChanMACD(klu_list) + klu_list = macd.klu_list + self._last_chan_macd = macd + last_klu = None for klu in klu_list: - ema = klu.ema52 - last_ema = last_klu.ema52 if last_klu else 0 - if klu.close >= ema: - ema_up_count += 1 - elif klu.close < ema: - ema_down_count += 1 - if last_klu and last_klu.close >= last_ema and klu.close < ema: - ema_up_list.append(ema_up_count) - #print(last_klu.time, ema_up_count, "UP END") - ema_up_count = 0 - elif last_klu and last_klu.close < last_ema and klu.close >= ema: - ema_down_list.append(ema_down_count) - #print(last_klu.time, ema_down_count, "DOWN END") - ema_down_count = 0 self._push_klu_into_klc_list(klc_list, klu, last_klu) last_klu = klu klc_list = self.cal_trend(klc_list) - #print(ema52_up_list, ema52_down_list) return klc_list diff --git a/chanlun/pipeline/timeframe.py b/chanlun/pipeline/timeframe.py index 992d347..5572c42 100644 --- a/chanlun/pipeline/timeframe.py +++ b/chanlun/pipeline/timeframe.py @@ -38,10 +38,18 @@ from chanlun.pipeline.builders.seg import SegBuilderMixin from chanlun.pipeline.builders.zs import ZsBuilderMixin class TF_DF(IndicatorsBuilderMixin, KlineBuilderMixin, BiBuilderMixin, SegBuilderMixin, ZsBuilderMixin, BspBuilderMixin, FastBspBuilderMixin, IncrementalBuilderMixin): - def __init__(self, df=None, interval=0, timeframe=None): + def __init__(self, df=None, interval=0, timeframe=None, lean=False): + """lean=True 只构建到中枢,跳过线段/走势中枢/MACD 状态机。 + + 研究与实盘只吃 bi_list → 中枢 → fast_bsp 这条链;线段、zs、big_zs 和整套 + MACD 背驰状态机是 web 展示与 bsp_list 才用的。实测这些占全量构建的约四成。 + 注意 lean 下 bsp_list/seg_list/chanmacd 均为空,**不要给 web 用**。 + """ + self.lean = lean if df is not None: - self.init_TF_DF(df, interval, timeframe) - def init_TF_DF(self, df, interval, timeframe): + self.init_TF_DF(df, interval, timeframe, lean=lean) + def init_TF_DF(self, df, interval, timeframe, lean=False): + self.lean = lean self.timeframe = timeframe self.interval = interval # 检查 DataFrame 是否为空或没有 date 列 @@ -69,6 +77,10 @@ class TF_DF(IndicatorsBuilderMixin, KlineBuilderMixin, BiBuilderMixin, SegBuilde self.klc_list = self.get_klc_list(self.klu_list) self.bi_list = self.cal_bi_list(self.klc_list) self.bi_zs_list = self.cal_bi_zs_list_pure(self.bi_list) + if self.lean: + self.big_zs_list = [] + self.chanmacd = None + return self.seg_list = self.get_seg_list(self.bi_list) self.zs_list = self.get_zs_list(self.bi_list, self.seg_list) self.big_zs_list = self.get_big_zs_list(self.zs_list) diff --git a/research/out/step46_baseline.json b/research/out/step46_baseline.json new file mode 100644 index 0000000..478bd06 --- /dev/null +++ b/research/out/step46_baseline.json @@ -0,0 +1,487 @@ +{ + "BTC_1m": { + "n_rows": 20000, + "n_klu": 20000, + "n_klc": 12496, + "n_bi": 913, + "n_seg": 131, + "n_bi_zs": 114, + "n_zs": 16, + "n_bsp": 0, + "klc_high": "986038d3e673336e", + "klc_low": "ee349f689ebfe387", + "klc_fx": "b323365a81e5e666", + "bi_start": "f829c2ce6845f569", + "bi_end": "f829c2ce6845f569", + "bi_sure": "64458b0f3100544e", + "n_zones": 114, + "zone_zg": "00c54276171344e0", + "zone_zd": "e0dcb978a1105305", + "zone_avail": "06d947c90bbab2f7", + "zone_ladder": "c6312a30d149c5b8", + "n_sig": 31, + "sig_entry_idx": "117daf6e8c55678c", + "sig_direction": "82ee301abc35d11b", + "sig_bo_idx": "37ffa946fad579ce", + "sig_pb_idx": "8552ade1f84eb3d3", + "sig_lag": "da1bcee041ab9685", + "sig_depth": "e0e60c3d319358e3", + "sig_zone_i": "434c25ef783c3d1d", + "col_open": "3197bbad7b435c9f", + "col_high": "41e4ad11f6f6d786", + "col_low": "c309926c23dfa574", + "col_close": "c48f76a626b629c6", + "col_volume": "07f9f254f17399a0", + "col_atr": "074464dfb5648113", + "col_macd": "135a69f5aa1df7dc", + "col_macdsignal": "62ce36e9da9f37ec", + "col_macdhist": "61ddcf3bb139bb81", + "col_ema5": "e0ea01077a59b84b", + "col_ema13": "ab9a31bbf5de8b57", + "col_ema24": "44e8e295560b85cf", + "col_ema26": "a710c87eeec607ea", + "col_ema52": "578454b2d9147d96", + "col_ema104": "36649911c3df0136", + "col_ema156": "bba5874bd409fa90", + "col_ema208": "a0bc2fa3e690681c", + "col_ema7": "e1e76d337b9139e4", + "col_rsi": "cc09f5884225b37f", + "col_volume_ratio": "2382df013004571f", + "col_bb2633upper": "85839e5a7bb36dc7", + "col_bb2633lower": "13861ca562994024", + "col_bb2633middle": "bf3c9bd564c2e8d7", + "col_bbp30": "d00623b2d18c6653", + "col_bbp120": "36dd361b5c618dcc", + "col_bbp365": "5cd3e7d640c7d5dd", + "_all_columns": [ + "atr", + "bb2633lower", + "bb2633middle", + "bb2633upper", + "bblow120", + "bblow30", + "bblow302", + "bblow365", + "bbmiddle30", + "bbp120", + "bbp2633", + "bbp30", + "bbp302", + "bbp365", + "bbup120", + "bbup30", + "bbup302", + "bbup365", + "close", + "date", + "ema10", + "ema104", + "ema13", + "ema156", + "ema208", + "ema24", + "ema26", + "ema5", + "ema52", + "ema7", + "high", + "low", + "macd", + "macdhist", + "macdsignal", + "open", + "rsi", + "timestamp", + "volume", + "volume_ratio" + ] + }, + "BTC_5m": { + "n_rows": 20000, + "n_klu": 20000, + "n_klc": 14323, + "n_bi": 974, + "n_seg": 149, + "n_bi_zs": 115, + "n_zs": 15, + "n_bsp": 0, + "klc_high": "0d3d5dcb6be9d8cf", + "klc_low": "2ae0ce7158f99ee5", + "klc_fx": "db8c606c09803b39", + "bi_start": "eb36252ef42df30d", + "bi_end": "eb36252ef42df30d", + "bi_sure": "a61c15678945f9e1", + "n_zones": 115, + "zone_zg": "5eaa2f8f738c8566", + "zone_zd": "d48ee940dac1fdba", + "zone_avail": "bfc273ac2c80a856", + "zone_ladder": "822ab80c13164fcf", + "n_sig": 48, + "sig_entry_idx": "4ca6e0b3ad9650db", + "sig_direction": "74de66c775f3159f", + "sig_bo_idx": "020d83285f111211", + "sig_pb_idx": "c9d29e0e1cf23857", + "sig_lag": "630718fda8c18f8b", + "sig_depth": "cbe22a059a935417", + "sig_zone_i": "499d9a4ffc4e4fac", + "col_open": "da5240f8ff94b7d3", + "col_high": "c06a481db2faee01", + "col_low": "07108f56862e97f8", + "col_close": "878c50482e134764", + "col_volume": "e57b8dbfb3589a2a", + "col_atr": "2e57310f4bb4f259", + "col_macd": "807220fb465a46f1", + "col_macdsignal": "079fd0757f9a6040", + "col_macdhist": "2372736ddaa73786", + "col_ema5": "5f7cc824e469e468", + "col_ema13": "be17db658716e40f", + "col_ema24": "dae2098cf4b4a029", + "col_ema26": "1d6e667de928a8b4", + "col_ema52": "a9521436966fff79", + "col_ema104": "1d1373a8c3e48cd9", + "col_ema156": "679d2e502207f085", + "col_ema208": "8790e0e5c50bfbc3", + "col_ema7": "b631eeb99b06b174", + "col_rsi": "f1b3f6541bcefdc9", + "col_volume_ratio": "3db5f52fe7051017", + "col_bb2633upper": "cbd0d954047104f5", + "col_bb2633lower": "e1a83624de00b90c", + "col_bb2633middle": "a2a24306ccdc04c4", + "col_bbp30": "453235723fa5bc76", + "col_bbp120": "f67bad6c333f35b7", + "col_bbp365": "d68b613efed41cd6", + "_all_columns": [ + "atr", + "bb2633lower", + "bb2633middle", + "bb2633upper", + "bblow120", + "bblow30", + "bblow302", + "bblow365", + "bbmiddle30", + "bbp120", + "bbp2633", + "bbp30", + "bbp302", + "bbp365", + "bbup120", + "bbup30", + "bbup302", + "bbup365", + "close", + "date", + "ema10", + "ema104", + "ema13", + "ema156", + "ema208", + "ema24", + "ema26", + "ema5", + "ema52", + "ema7", + "high", + "low", + "macd", + "macdhist", + "macdsignal", + "open", + "rsi", + "timestamp", + "volume", + "volume_ratio" + ] + }, + "ETH_5m": { + "n_rows": 20000, + "n_klu": 20000, + "n_klc": 14342, + "n_bi": 927, + "n_seg": 142, + "n_bi_zs": 100, + "n_zs": 15, + "n_bsp": 0, + "klc_high": "131ed2d2cc245008", + "klc_low": "d53c72dc569bbd4f", + "klc_fx": "ec3177b225143a7a", + "bi_start": "fb708274964722c2", + "bi_end": "fb708274964722c2", + "bi_sure": "b7b4cb502f2a0d70", + "n_zones": 100, + "zone_zg": "dca84f6a299fd1aa", + "zone_zd": "d462f78cc4b7538b", + "zone_avail": "b92e97fc130e310d", + "zone_ladder": "cfc5c84c6a6d90f2", + "n_sig": 42, + "sig_entry_idx": "82e50dda58170b06", + "sig_direction": "c7e63a0158b14ef2", + "sig_bo_idx": "d9c826c620ff6bf2", + "sig_pb_idx": "25340fc752a8143c", + "sig_lag": "a6fa56f9f5b5f367", + "sig_depth": "71d0776582fad395", + "sig_zone_i": "f87a3a29b6b26adb", + "col_open": "4875825277eb22d2", + "col_high": "efc10b5252481a18", + "col_low": "c2978c8eb3cdf78c", + "col_close": "e0bc2b374fa5b33d", + "col_volume": "f97c30ec4bb0ecf8", + "col_atr": "8d6f2061d285ad98", + "col_macd": "c8fc7f49467985ee", + "col_macdsignal": "b6af44d80ea485c1", + "col_macdhist": "fa854fe5a2055930", + "col_ema5": "c6f2736ca8f67a11", + "col_ema13": "2103d16e3db9dff7", + "col_ema24": "e72b6c1ac9118ebd", + "col_ema26": "a83023c8930a807d", + "col_ema52": "ce2971fa029ee7b7", + "col_ema104": "3cbe3efc7b6d8457", + "col_ema156": "84e140907ef54b4c", + "col_ema208": "e28316d21c6d29e7", + "col_ema7": "5bbf9ce6610e1574", + "col_rsi": "c4c6ff1145c88981", + "col_volume_ratio": "bfb7636b8d92fca1", + "col_bb2633upper": "f0068bec633e28d4", + "col_bb2633lower": "78936103ef228327", + "col_bb2633middle": "3b4a8fd306318760", + "col_bbp30": "225773d67ca05c7e", + "col_bbp120": "1084331d91660254", + "col_bbp365": "ea71e0285f2801ba", + "_all_columns": [ + "atr", + "bb2633lower", + "bb2633middle", + "bb2633upper", + "bblow120", + "bblow30", + "bblow302", + "bblow365", + "bbmiddle30", + "bbp120", + "bbp2633", + "bbp30", + "bbp302", + "bbp365", + "bbup120", + "bbup30", + "bbup302", + "bbup365", + "close", + "date", + "ema10", + "ema104", + "ema13", + "ema156", + "ema208", + "ema24", + "ema26", + "ema5", + "ema52", + "ema7", + "high", + "low", + "macd", + "macdhist", + "macdsignal", + "open", + "rsi", + "timestamp", + "volume", + "volume_ratio" + ] + }, + "SOL_15m": { + "n_rows": 20000, + "n_klu": 20000, + "n_klc": 14051, + "n_bi": 894, + "n_seg": 131, + "n_bi_zs": 115, + "n_zs": 7, + "n_bsp": 0, + "klc_high": "6514126fb15a2495", + "klc_low": "1a95961cd8510b61", + "klc_fx": "39a6f1e8cbe15296", + "bi_start": "93d17db2c0726ad2", + "bi_end": "93d17db2c0726ad2", + "bi_sure": "72a7eeed9f203249", + "n_zones": 115, + "zone_zg": "bdddd8960357e2c2", + "zone_zd": "a72a80f2ec747fa9", + "zone_avail": "d6bec0bead4ca986", + "zone_ladder": "4bbd1a9e4080a3d4", + "n_sig": 33, + "sig_entry_idx": "57154349f7256bd5", + "sig_direction": "3456806b7f3f915a", + "sig_bo_idx": "d3a1e9433c85a1d0", + "sig_pb_idx": "7584cbbe94a65ea5", + "sig_lag": "0d9429413d5a72ba", + "sig_depth": "5fefacd49949cd9f", + "sig_zone_i": "99844be679dbc613", + "col_open": "0c38967f4d88ca2f", + "col_high": "9d506e4c74c1d73c", + "col_low": "416ac69aa8a7a5c4", + "col_close": "27579fe4a877d720", + "col_volume": "a8c30f4f5a732e91", + "col_atr": "366c4ec27cd52632", + "col_macd": "449587b2c37e8df2", + "col_macdsignal": "83de7bfbe9224c61", + "col_macdhist": "e94458f1558bd910", + "col_ema5": "cd13382ef7069033", + "col_ema13": "6a9429bffb033b9a", + "col_ema24": "41a037d614e701ba", + "col_ema26": "c46e3a74f471bbb0", + "col_ema52": "bf8d0b1208647291", + "col_ema104": "4f229c578d7998e4", + "col_ema156": "1fd8a8c38d11f081", + "col_ema208": "3e2704d5c53ee9f8", + "col_ema7": "21281407046b4322", + "col_rsi": "a800c451254f433e", + "col_volume_ratio": "ec05ddc04f54b31b", + "col_bb2633upper": "9c0bb9a91102bcd9", + "col_bb2633lower": "b55705f39cdb0c63", + "col_bb2633middle": "365c7763ddad555c", + "col_bbp30": "8ab505323d0cb0a9", + "col_bbp120": "8a16100ed6fe37b6", + "col_bbp365": "437b91a2cdde91eb", + "_all_columns": [ + "atr", + "bb2633lower", + "bb2633middle", + "bb2633upper", + "bblow120", + "bblow30", + "bblow302", + "bblow365", + "bbmiddle30", + "bbp120", + "bbp2633", + "bbp30", + "bbp302", + "bbp365", + "bbup120", + "bbup30", + "bbup302", + "bbup365", + "close", + "date", + "ema10", + "ema104", + "ema13", + "ema156", + "ema208", + "ema24", + "ema26", + "ema5", + "ema52", + "ema7", + "high", + "low", + "macd", + "macdhist", + "macdsignal", + "open", + "rsi", + "timestamp", + "volume", + "volume_ratio" + ] + }, + "XRP_30m": { + "n_rows": 20000, + "n_klu": 20000, + "n_klc": 14763, + "n_bi": 849, + "n_seg": 125, + "n_bi_zs": 108, + "n_zs": 11, + "n_bsp": 0, + "klc_high": "567e7651c968c00c", + "klc_low": "c1b8189a9d447452", + "klc_fx": "67b72deec2a5593b", + "bi_start": "04f30102940f974d", + "bi_end": "04f30102940f974d", + "bi_sure": "da571f5278b770e1", + "n_zones": 108, + "zone_zg": "1eefb80dd6c76a53", + "zone_zd": "8fa0d96149ddc204", + "zone_avail": "7e652fb397da32da", + "zone_ladder": "4177c4b5a40b6247", + "n_sig": 40, + "sig_entry_idx": "d550aee46e46c917", + "sig_direction": "81f88af0167a498c", + "sig_bo_idx": "f704a273ed48013f", + "sig_pb_idx": "2e030812f5243076", + "sig_lag": "1c1eebf161a0ff83", + "sig_depth": "c671af2668c0cc78", + "sig_zone_i": "49971affbaf93434", + "col_open": "e064238e9f5e7baa", + "col_high": "a28e55a860c236ce", + "col_low": "7585fe470cd2ecfd", + "col_close": "3470d49402aa5a5e", + "col_volume": "0b473a543b78cd89", + "col_atr": "967be1c16f118183", + "col_macd": "cf3b1cc2d9f3ca98", + "col_macdsignal": "993858809b2f3a2e", + "col_macdhist": "469650fc16672829", + "col_ema5": "0c7e735c6abfc0b5", + "col_ema13": "bc9799632b2232f5", + "col_ema24": "9397472a7982e2e4", + "col_ema26": "59ec2e87acb64341", + "col_ema52": "f61f6de2169678b8", + "col_ema104": "fe786b2dc9436f25", + "col_ema156": "1291ef40a77682d5", + "col_ema208": "7e63bf51454a5dfb", + "col_ema7": "25983717dea0f463", + "col_rsi": "b992c428c2643962", + "col_volume_ratio": "4d70f107c08380cc", + "col_bb2633upper": "cbe0b203046d7377", + "col_bb2633lower": "166c968182153faf", + "col_bb2633middle": "7ba495bfe685d0eb", + "col_bbp30": "598150e822aac882", + "col_bbp120": "36778d36238896c4", + "col_bbp365": "a1f88436a790dc2a", + "_all_columns": [ + "atr", + "bb2633lower", + "bb2633middle", + "bb2633upper", + "bblow120", + "bblow30", + "bblow302", + "bblow365", + "bbmiddle30", + "bbp120", + "bbp2633", + "bbp30", + "bbp302", + "bbp365", + "bbup120", + "bbup30", + "bbup302", + "bbup365", + "close", + "date", + "ema10", + "ema104", + "ema13", + "ema156", + "ema208", + "ema24", + "ema26", + "ema5", + "ema52", + "ema7", + "high", + "low", + "macd", + "macdhist", + "macdsignal", + "open", + "rsi", + "timestamp", + "volume", + "volume_ratio" + ] + } +} \ No newline at end of file diff --git a/research/out/step46_baseline_big.json b/research/out/step46_baseline_big.json new file mode 100644 index 0000000..0e221dd --- /dev/null +++ b/research/out/step46_baseline_big.json @@ -0,0 +1,487 @@ +{ + "BTC_1m": { + "n_rows": 200000, + "n_klu": 200000, + "n_klc": 131701, + "n_bi": 9504, + "n_seg": 1406, + "n_bi_zs": 1073, + "n_zs": 155, + "n_bsp": 0, + "klc_high": "992af879db2e8e43", + "klc_low": "8ba4f68b829b552c", + "klc_fx": "03870041e7b334e7", + "bi_start": "3d8fbede022d751d", + "bi_end": "3d8fbede022d751d", + "bi_sure": "bd74890c6552b345", + "n_zones": 1073, + "zone_zg": "0436269233f63984", + "zone_zd": "5b3cc3b0b0eafd20", + "zone_avail": "b45174f22f908d5b", + "zone_ladder": "1342f5e104d399fa", + "n_sig": 396, + "sig_entry_idx": "c9728ba1d4a739c5", + "sig_direction": "8bc66cd2f6c0ffa8", + "sig_bo_idx": "2981b0d3134b99cc", + "sig_pb_idx": "21ec7c5b1f85cca4", + "sig_lag": "2f5640376a99c4ae", + "sig_depth": "cf3f0bf3d239c2f9", + "sig_zone_i": "8654e213e0faaa5f", + "col_open": "32e16cc827550cce", + "col_high": "9c9cb56b72814b2b", + "col_low": "4db855614f0f3254", + "col_close": "3707a170acb64306", + "col_volume": "4e3eb344a25eeec2", + "col_atr": "c6e1a3b27bfaec47", + "col_macd": "94a485845cf30bea", + "col_macdsignal": "87ec0074d9bc8cbc", + "col_macdhist": "5127835e6b590aff", + "col_ema5": "5f4851ec918152d5", + "col_ema13": "5ede020d68cde473", + "col_ema24": "dc45445e03565a7d", + "col_ema26": "7ffc3a0799531172", + "col_ema52": "dafc5dbfdac7ec7d", + "col_ema104": "6a5911c3a9daead3", + "col_ema156": "f7aa34a1c04ecd66", + "col_ema208": "773341a5170f61ee", + "col_ema7": "25b47f9884bc7eff", + "col_rsi": "d07bc07917c51b98", + "col_volume_ratio": "2b3cbd8c2fd4a0c7", + "col_bb2633upper": "8abeb464d97ed502", + "col_bb2633lower": "54f96c202ecd17c2", + "col_bb2633middle": "bf7a068c7592c379", + "col_bbp30": "e6b3f087308509bd", + "col_bbp120": "eb53451f0dadd779", + "col_bbp365": "c32489b1e8216c96", + "_all_columns": [ + "atr", + "bb2633lower", + "bb2633middle", + "bb2633upper", + "bblow120", + "bblow30", + "bblow302", + "bblow365", + "bbmiddle30", + "bbp120", + "bbp2633", + "bbp30", + "bbp302", + "bbp365", + "bbup120", + "bbup30", + "bbup302", + "bbup365", + "close", + "date", + "ema10", + "ema104", + "ema13", + "ema156", + "ema208", + "ema24", + "ema26", + "ema5", + "ema52", + "ema7", + "high", + "low", + "macd", + "macdhist", + "macdsignal", + "open", + "rsi", + "timestamp", + "volume", + "volume_ratio" + ] + }, + "BTC_5m": { + "n_rows": 200000, + "n_klu": 200000, + "n_klc": 146735, + "n_bi": 9670, + "n_seg": 1485, + "n_bi_zs": 1093, + "n_zs": 160, + "n_bsp": 0, + "klc_high": "dc921e6668a807e9", + "klc_low": "47a5508f3736f23b", + "klc_fx": "4a459666e121fee2", + "bi_start": "12144ccb870d12a1", + "bi_end": "12144ccb870d12a1", + "bi_sure": "0ee81c335c3c858e", + "n_zones": 1093, + "zone_zg": "54918583a2371b0f", + "zone_zd": "cb698e8570e2b3fc", + "zone_avail": "70a78276ce8d81e8", + "zone_ladder": "4eaf9f4810737d9b", + "n_sig": 396, + "sig_entry_idx": "c166fe6bbec08d80", + "sig_direction": "d376c8684119da87", + "sig_bo_idx": "1963ec7608066cf6", + "sig_pb_idx": "c7db54ba1103fe3b", + "sig_lag": "8cfbb30f7e26bcff", + "sig_depth": "fbc0589ca7908cee", + "sig_zone_i": "aa0a7f54921bfdb2", + "col_open": "b0ef7c7ed1122a9b", + "col_high": "c036264627035105", + "col_low": "aad6caaa73079e29", + "col_close": "00ccedeb197fd137", + "col_volume": "11686a1bed642a1f", + "col_atr": "1fa43f86914c4a8e", + "col_macd": "ad66e34357a586c7", + "col_macdsignal": "3b9a127b992897b3", + "col_macdhist": "c564a280ef8eb0f1", + "col_ema5": "74e476c0269798a9", + "col_ema13": "f6968ae7f127ebe2", + "col_ema24": "db3ae3b801d846f5", + "col_ema26": "adda6c2af103a6ed", + "col_ema52": "4d663556702cd4df", + "col_ema104": "c14c8d69fa4be179", + "col_ema156": "9e5df963de200c54", + "col_ema208": "30e0eabc1ef6e4fb", + "col_ema7": "a78d7798f10638fc", + "col_rsi": "c4dc1ca7d74b64f9", + "col_volume_ratio": "62694ace333f324a", + "col_bb2633upper": "35149f0ee32941d5", + "col_bb2633lower": "ce61849352496a88", + "col_bb2633middle": "62cf973be0c1d775", + "col_bbp30": "d8dc8265680ed17c", + "col_bbp120": "d951e014ae714540", + "col_bbp365": "a4f5700a169eed20", + "_all_columns": [ + "atr", + "bb2633lower", + "bb2633middle", + "bb2633upper", + "bblow120", + "bblow30", + "bblow302", + "bblow365", + "bbmiddle30", + "bbp120", + "bbp2633", + "bbp30", + "bbp302", + "bbp365", + "bbup120", + "bbup30", + "bbup302", + "bbup365", + "close", + "date", + "ema10", + "ema104", + "ema13", + "ema156", + "ema208", + "ema24", + "ema26", + "ema5", + "ema52", + "ema7", + "high", + "low", + "macd", + "macdhist", + "macdsignal", + "open", + "rsi", + "timestamp", + "volume", + "volume_ratio" + ] + }, + "ETH_5m": { + "n_rows": 200000, + "n_klu": 200000, + "n_klc": 147071, + "n_bi": 9668, + "n_seg": 1447, + "n_bi_zs": 1064, + "n_zs": 151, + "n_bsp": 0, + "klc_high": "b474c10434b7d1be", + "klc_low": "16c8b368522eff67", + "klc_fx": "454f30149c5437e9", + "bi_start": "e962ff5f90072953", + "bi_end": "e962ff5f90072953", + "bi_sure": "6702ee0ec3ba05b7", + "n_zones": 1064, + "zone_zg": "4f8fdc5ce3699a83", + "zone_zd": "55f8826e3850bbbb", + "zone_avail": "b4af5d59909c20de", + "zone_ladder": "7718718df232fbb6", + "n_sig": 357, + "sig_entry_idx": "6de39296c88fcbb5", + "sig_direction": "62e7be93232d7c6d", + "sig_bo_idx": "6ec163434edaf6e9", + "sig_pb_idx": "ca9b4000a3d8b167", + "sig_lag": "16e524f721f0a3aa", + "sig_depth": "bd78d76d5ac56d18", + "sig_zone_i": "ec18317accf8f34f", + "col_open": "6f693fe747fe72ea", + "col_high": "e5cf5f170c1b74c5", + "col_low": "9a21e97b26b281ee", + "col_close": "c65c480ea3acb4cf", + "col_volume": "deca090b35e34611", + "col_atr": "5c598706876091c4", + "col_macd": "3f38452148fbb5db", + "col_macdsignal": "848e9ea874b125cb", + "col_macdhist": "3e2301728b8c73ae", + "col_ema5": "e66614435995ca8c", + "col_ema13": "5b90f189ecb9dbde", + "col_ema24": "dc98f535eb5b9a14", + "col_ema26": "98ce247c33012910", + "col_ema52": "ce2d49aa246f7d1b", + "col_ema104": "37a07762a9d5bfe1", + "col_ema156": "1040f54fb1f1a28c", + "col_ema208": "2430fa3290e62444", + "col_ema7": "e85e8582b2dd352a", + "col_rsi": "f3d09ac8721406bd", + "col_volume_ratio": "5842f57c75a5c5a8", + "col_bb2633upper": "1e1622aad5a3dc46", + "col_bb2633lower": "58392bfb6e652321", + "col_bb2633middle": "72376e41d5a46393", + "col_bbp30": "1b99ed1cb7132665", + "col_bbp120": "b11c01bd8702106e", + "col_bbp365": "9613ed3ed332a0e2", + "_all_columns": [ + "atr", + "bb2633lower", + "bb2633middle", + "bb2633upper", + "bblow120", + "bblow30", + "bblow302", + "bblow365", + "bbmiddle30", + "bbp120", + "bbp2633", + "bbp30", + "bbp302", + "bbp365", + "bbup120", + "bbup30", + "bbup302", + "bbup365", + "close", + "date", + "ema10", + "ema104", + "ema13", + "ema156", + "ema208", + "ema24", + "ema26", + "ema5", + "ema52", + "ema7", + "high", + "low", + "macd", + "macdhist", + "macdsignal", + "open", + "rsi", + "timestamp", + "volume", + "volume_ratio" + ] + }, + "SOL_15m": { + "n_rows": 200000, + "n_klu": 200000, + "n_klc": 147387, + "n_bi": 9221, + "n_seg": 1443, + "n_bi_zs": 1097, + "n_zs": 153, + "n_bsp": 0, + "klc_high": "9873e17761a5f439", + "klc_low": "c26a95692a503700", + "klc_fx": "e9da3d4ecae9b7d6", + "bi_start": "7ae8f7ca13826214", + "bi_end": "7ae8f7ca13826214", + "bi_sure": "4dc74bb88bebe27b", + "n_zones": 1097, + "zone_zg": "4ac63d81a0c7f518", + "zone_zd": "38d5b7afd33e2e15", + "zone_avail": "532fd21eed5584f7", + "zone_ladder": "6139c6b7503d0c44", + "n_sig": 371, + "sig_entry_idx": "d74faae30a9c9d49", + "sig_direction": "038ed1f04dbe7141", + "sig_bo_idx": "9c2251f45e7b31f6", + "sig_pb_idx": "ac324f507086aec8", + "sig_lag": "b429021aa0a4d4da", + "sig_depth": "654d5249825e9903", + "sig_zone_i": "f070e3f31075cb19", + "col_open": "1c9998e987e16a5f", + "col_high": "80ed4bf62274de03", + "col_low": "09b8b6c12e4e3f06", + "col_close": "223ae3e7e1b7d05a", + "col_volume": "f311c91d123a55b5", + "col_atr": "ed2d754bd3b15f77", + "col_macd": "290ea71faef859e4", + "col_macdsignal": "70008a039281deb8", + "col_macdhist": "16535492810b17e8", + "col_ema5": "c2b75b157fee520a", + "col_ema13": "a38553a7296f8c14", + "col_ema24": "aa12c0c0fc8b839f", + "col_ema26": "2e04f45e2f2067fb", + "col_ema52": "361bd4886eaf510d", + "col_ema104": "beca8cd07bb88835", + "col_ema156": "941a4949be08fcbd", + "col_ema208": "82f3b64ae8e6c97c", + "col_ema7": "466b0319cd174bb2", + "col_rsi": "cd3eb3fa8896a0e6", + "col_volume_ratio": "c4d3a5bf0a821829", + "col_bb2633upper": "3599736cc0c8ed31", + "col_bb2633lower": "4afcdb806d790258", + "col_bb2633middle": "fea10757ba7fdeb6", + "col_bbp30": "64222d075acd186e", + "col_bbp120": "d17f37169b2a5966", + "col_bbp365": "943089e90d9da3ba", + "_all_columns": [ + "atr", + "bb2633lower", + "bb2633middle", + "bb2633upper", + "bblow120", + "bblow30", + "bblow302", + "bblow365", + "bbmiddle30", + "bbp120", + "bbp2633", + "bbp30", + "bbp302", + "bbp365", + "bbup120", + "bbup30", + "bbup302", + "bbup365", + "close", + "date", + "ema10", + "ema104", + "ema13", + "ema156", + "ema208", + "ema24", + "ema26", + "ema5", + "ema52", + "ema7", + "high", + "low", + "macd", + "macdhist", + "macdsignal", + "open", + "rsi", + "timestamp", + "volume", + "volume_ratio" + ] + }, + "XRP_30m": { + "n_rows": 116374, + "n_klu": 116374, + "n_klc": 82461, + "n_bi": 5109, + "n_seg": 784, + "n_bi_zs": 572, + "n_zs": 74, + "n_bsp": 0, + "klc_high": "2f214dccb66c2acb", + "klc_low": "11763813065965dc", + "klc_fx": "16a8abb38cc08276", + "bi_start": "ddb3716c5603fc9f", + "bi_end": "ddb3716c5603fc9f", + "bi_sure": "45d38e2edf89a78e", + "n_zones": 572, + "zone_zg": "6301d2ba0529348a", + "zone_zd": "02798be778a12b3c", + "zone_avail": "f7627b9e187434f8", + "zone_ladder": "79b4eba53b7f16b4", + "n_sig": 185, + "sig_entry_idx": "3fd118bcca9c0318", + "sig_direction": "4e5c8a66c981612e", + "sig_bo_idx": "359749625e049f17", + "sig_pb_idx": "30d938988a47152e", + "sig_lag": "6ecaeb56a6377c02", + "sig_depth": "b472e1cc5387f001", + "sig_zone_i": "beb04af56f37bf08", + "col_open": "d8eb9bfb0cb6375a", + "col_high": "c8305b04a6736acf", + "col_low": "b00124fab8245af3", + "col_close": "69e3cdd2e55f4809", + "col_volume": "839836338744c27c", + "col_atr": "5900706833e9edba", + "col_macd": "88f65443d79c489f", + "col_macdsignal": "f1fd2dcea6c73575", + "col_macdhist": "fce95fc9ccb6b57e", + "col_ema5": "5c6b418babd70b93", + "col_ema13": "4775865a25779b16", + "col_ema24": "f24136710438fa4e", + "col_ema26": "c8a14eab58bc38dc", + "col_ema52": "602f9cd13a369f59", + "col_ema104": "aefabaf6c26d4977", + "col_ema156": "a09b1eb8b3435f96", + "col_ema208": "861be94bc86d83b8", + "col_ema7": "7e605fb6cd8fa03f", + "col_rsi": "ab9617b826fb1cf1", + "col_volume_ratio": "adeab54de59ff72c", + "col_bb2633upper": "a4e7709df8bebb48", + "col_bb2633lower": "e62b8f228e730261", + "col_bb2633middle": "5799755b2c60e9fe", + "col_bbp30": "a404273b177e495e", + "col_bbp120": "46c63a30348ad94e", + "col_bbp365": "f974d0696160d78d", + "_all_columns": [ + "atr", + "bb2633lower", + "bb2633middle", + "bb2633upper", + "bblow120", + "bblow30", + "bblow302", + "bblow365", + "bbmiddle30", + "bbp120", + "bbp2633", + "bbp30", + "bbp302", + "bbp365", + "bbup120", + "bbup30", + "bbup302", + "bbup365", + "close", + "date", + "ema10", + "ema104", + "ema13", + "ema156", + "ema208", + "ema24", + "ema26", + "ema5", + "ema52", + "ema7", + "high", + "low", + "macd", + "macdhist", + "macdsignal", + "open", + "rsi", + "timestamp", + "volume", + "volume_ratio" + ] + } +} \ No newline at end of file diff --git a/research/step46_engine_parity.py b/research/step46_engine_parity.py new file mode 100644 index 0000000..049bb70 --- /dev/null +++ b/research/step46_engine_parity.py @@ -0,0 +1,200 @@ +"""Step 46:缠论引擎的逐位对拍基线。 + +重构引擎前先固化一份指纹,改完再对一次。没有它,任何"精简"都无法证明 +没有改变行为——而 HANDOFF 里所有回测数字都绑定当前实现,**行为变化是静默的**: +不报错、不崩溃,只是信号悄悄变了一批。 + +指纹覆盖三条链路各自依赖的东西: + + 结构 klu / klc / bi / bi_zs / seg 的数量与关键端点 + 信号 中枢表(zg/zd/available_ts) 与 fast_bsp3 的全部输出列 + 数值 dataframe 上被下游真正消费的列(逐位比较) + +用法: + python step46_engine_parity.py --save # 改动前,存基线 + python step46_engine_parity.py --check # 改动后,对比 +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import sys +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd + +warnings.filterwarnings("ignore") +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) +sys.path.insert(0, str(HERE.parent)) +pd.set_option("display.width", 300) + +BASELINE = HERE / "out" / "step46_baseline.json" + +# 覆盖面:多币多级别,1m 用较短窗口以免跑太久 +CASES = [ + ("BTC/USDT:USDT", "1m", 20_000), + ("BTC/USDT:USDT", "5m", 20_000), + ("ETH/USDT:USDT", "5m", 20_000), + ("SOL/USDT:USDT", "15m", 20_000), + ("XRP/USDT:USDT", "30m", 20_000), +] + +# 下游真正消费的列(见 §5.5 的列使用扫描)。精简若动到这些,必须体现在指纹里。 +CONSUMED = ["open", "high", "low", "close", "volume", "atr", + "macd", "macdsignal", "macdhist", + "ema5", "ema13", "ema24", "ema26", "ema52", "ema104", "ema156", "ema208", "ema7", + "rsi", "volume_ratio", + "bb2633upper", "bb2633lower", "bb2633middle", + "bbp30", "bbp120", "bbp365"] + + +def _h(arr) -> str: + a = np.asarray(arr, dtype=np.float64) + a = np.nan_to_num(a, nan=-9.87654321e30, posinf=1e300, neginf=-1e300) + return hashlib.sha256(a.tobytes()).hexdigest()[:16] + + +def fingerprint(pair: str, tf: str, rows: int) -> dict: + from chanlun import TF_DF + from chanlun.analysis.fast_bsp import ( + add_zone_ladder, build_htf_zones, find_fast_bsp3, + ) + from lib.data import fetch_ohlcv + + df = fetch_ohlcv(pair, tf, rows) + chan = TF_DF(df, 1, tf) + cdf = chan.dataframe + + fp: dict = {"n_rows": int(len(cdf))} + + # --- 结构 --- + fp["n_klu"] = len(getattr(chan, "klu_list", []) or []) + fp["n_klc"] = len(getattr(chan, "klc_list", []) or []) + fp["n_bi"] = len(getattr(chan, "bi_list", []) or []) + fp["n_seg"] = len(getattr(chan, "seg_list", []) or []) + fp["n_bi_zs"] = len(getattr(chan, "bi_zs_list", []) or []) + fp["n_zs"] = len(getattr(chan, "zs_list", []) or []) + fp["n_bsp"] = len(getattr(chan, "bsp_list", []) or []) + + # KLC 端点(包含关系的结果,最容易被指标改动影响) + klc = getattr(chan, "klc_list", []) or [] + fp["klc_high"] = _h([k.high for k in klc]) + fp["klc_low"] = _h([k.low for k in klc]) + fp["klc_fx"] = _h([float(getattr(k.fx, "value", 0) or 0) for k in klc]) + + # 笔端点 + bi = getattr(chan, "bi_list", []) or [] + fp["bi_start"] = _h([float(getattr(b, "start_price", 0) or 0) for b in bi]) + fp["bi_end"] = _h([float(getattr(b, "end_price", 0) or 0) for b in bi]) + fp["bi_sure"] = _h([1.0 if getattr(b, "is_sure", False) else 0.0 for b in bi]) + + # --- 信号 --- + zones = build_htf_zones(cdf, tf, chan=chan) + fp["n_zones"] = int(len(zones)) + if len(zones): + zl = add_zone_ladder(zones.reset_index(drop=True)) + fp["zone_zg"] = _h(zl["zg"]) + fp["zone_zd"] = _h(zl["zd"]) + fp["zone_avail"] = _h(zl["available_ts"]) + fp["zone_ladder"] = _h(zl["z_above"].astype(float) * 2 + zl["z_below"].astype(float)) + sig = find_fast_bsp3(cdf, zl) + fp["n_sig"] = int(len(sig)) + for c in ("entry_idx", "direction", "bo_idx", "pb_idx", "lag", "depth", "zone_i"): + if c in sig.columns: + fp[f"sig_{c}"] = _h(sig[c]) + else: + fp["n_sig"] = 0 + + # --- 数值列(只对下游消费的列逐位比较)--- + for c in CONSUMED: + fp[f"col_{c}"] = _h(cdf[c]) if c in cdf.columns else "MISSING" + fp["_all_columns"] = sorted(map(str, cdf.columns)) + return fp + + +def collect() -> dict: + out = {} + for pair, tf, rows in CASES: + key = f"{pair.split('/')[0]}_{tf}" + print(f" 计算 {key} ...", flush=True) + try: + out[key] = fingerprint(pair, tf, rows) + except Exception as e: + out[key] = {"error": repr(e)[:200]} + print(f" 失败: {e!r}", flush=True) + return out + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--save", action="store_true", help="存为基线") + ap.add_argument("--check", action="store_true", help="与基线对比") + ap.add_argument("--rows", type=int, default=0, help="覆盖各用例根数(验证大样本时用)") + ap.add_argument("--out", default="", help="基线文件名,便于大小样本各存一份") + args = ap.parse_args() + + global BASELINE, CASES + if args.out: + BASELINE = HERE / "out" / args.out + if args.rows: + CASES = [(p, tf, args.rows) for p, tf, _ in CASES] + if not (args.save or args.check): + ap.error("需要 --save 或 --check") + + BASELINE.parent.mkdir(exist_ok=True) + print(f"[引擎对拍] {len(CASES)} 个用例\n") + cur = collect() + + if args.save: + BASELINE.write_text(json.dumps(cur, ensure_ascii=False, indent=1)) + print(f"\n基线已存:{BASELINE}") + for k, v in cur.items(): + if "error" in v: + continue + print(f" {k}: klu {v['n_klu']} klc {v['n_klc']} bi {v['n_bi']} " + f"中枢 {v['n_zones']} 信号 {v['n_sig']} 列数 {len(v['_all_columns'])}") + return + + if not BASELINE.exists(): + print(f"基线不存在:{BASELINE},先跑 --save") + return + old = json.loads(BASELINE.read_text()) + + print("\n" + "=" * 90) + bad = 0 + for key in sorted(set(old) | set(cur)): + o, n = old.get(key), cur.get(key) + if o is None or n is None: + print(f"❌ {key}: 用例缺失") + bad += 1 + continue + # 列集合单独看:删列是预期内的,不算行为变化 + o_cols, n_cols = set(o.get("_all_columns", [])), set(n.get("_all_columns", [])) + diffs = [k for k in o if k != "_all_columns" and o.get(k) != n.get(k)] + dropped, added = sorted(o_cols - n_cols), sorted(n_cols - o_cols) + # 被删列在指纹里会变成 MISSING,若该列本就不被消费则无害 + harmful = [d for d in diffs if not (d.startswith("col_") and n.get(d) == "MISSING" + and d[4:] not in CONSUMED)] + if not harmful: + print(f"✅ {key}: 行为一致" + + (f"(删列 {len(dropped)} 个)" if dropped else "")) + else: + bad += 1 + print(f"❌ {key}: {len(harmful)} 项不一致") + for d in harmful[:12]: + print(f" {d}: {o.get(d)} → {n.get(d)}") + if dropped: + print(f" 删掉的列: {dropped}") + if added: + print(f" 新增的列: {added}") + + print("\n" + ("✅ 全部用例行为一致,可以放心继续" if not bad + else f"❌ {bad} 个用例有行为变化——**回测数字已失效,不要继续**")) + + +if __name__ == "__main__": + main()