引擎四处「算了没人要的东西」,append_bar 13.9ms → 6.6ms
服务端把增量上线后回传两个热点:add_indicators 为加一根重算全表(占 34%)、 cal_bi_list 整表重扫(51%)。顺着查下来四处都不是算法慢,是算了没人读的结果。 1. cal_trend 挂到 lean 下。它不是笔的依赖(bi.py:221 在它自己的循环里读自身 序列状态),服务端 verify_incr_parity 三币 1800 根已对拍定论。web 走非 lean,klc_trend 图层不受影响。 2. check_fx_pattern 删掉拼完就丢的字符串。它把 klu.to_string() 拼成 p 只为 一行注释掉的 print——2000 根上近 3 万次 f-string 加 6 万次 enum 格式化, 而且在 cal_bi_list 内层。klu.pattern 只被 cal_klu_pattern 自己的双K/三K 判定读,不出模块不进 web,所以整个调用在 lean 下也跳过。 3. ChanBI.add_klc 去二次方。去重原本线性扫 klc_list,且每加一根就把整笔所有 KLU 的 macdhist 重累一遍,往一笔加 k 根是 O(k²)。改成下标集合加 macd_hist/macd_div 惰性求值。这两个值只有背驰判定(bsp.py)读,lean 下 bsp 根本不算。 4. add_indicators 批量挂列。2001 行上 TA 计算合计只有 2.5ms,而 30 多次 df['x']= 要 3.6ms——开销大头是 BlockManager 逐列插入不是计算,改为一次 concat。cal_volume_ratio 里为算一列 rolling 而 copy() 整张 40 列表,一并去掉。 实测(本机,2001 根窗口。服务端基线 21.8ms 是另一台机器,别直接比绝对值): append_bar 13.9 → 6.6ms └ rebuild_bi_zs 8.7 → 2.8ms └ add_indicators 4.4 → 3.5ms TF_DF lean 49.8 → 32.9ms TF_DF full 72.7 → 64.9ms 对拍用 git worktree 检出改动前的提交,同一份 BTC 1m 4000 根跑 38 项指纹: full 模式 19 项全部一致(web 那条路没动);lean 模式差 2 项,正是设计要它差 的 klc.trend 和 klu.pattern,而 lean 下 bi/zs/seg/bsp/dataframe 全部一致—— 这就是「这两个字段没人读」的实测证据:打空它们,下游一位不变。 瓶颈已经换位置了。新增 probe_inner.py 拆 inner_ms 分档:本机 TF_DF 两条腿占 70%、build_htf_zones 13%、htf_fx_timeline 6%,而服务端报的是 chan 构建 22ms / 信号链 86ms,机器差解释不了这个四倍差距。曾怀疑是 payload 反序列化,实测 _rebuild 只有 1.0ms,假设不成立。两边跑同一探针对分档表才能定位。 HANDOFF 顺带修掉一处 5.6 重号(增量落地那节改为 5.7,本节挂 5.71)。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+52
-21
@@ -15,10 +15,12 @@ class ChanBI():
|
|||||||
self.sure_time = None
|
self.sure_time = None
|
||||||
self.klc_list = []
|
self.klc_list = []
|
||||||
self.klc_list.append(klc)
|
self.klc_list.append(klc)
|
||||||
|
self._klc_idx = {klc.index}
|
||||||
self.end_time = klc.end_time
|
self.end_time = klc.end_time
|
||||||
self.start_time = klc.start_time
|
self.start_time = klc.start_time
|
||||||
self.macd_hist = 0
|
self._macd_hist = 0
|
||||||
self.macd_div = 0
|
self._macd_div = 0
|
||||||
|
self._macd_dirty = False
|
||||||
self.seg = None
|
self.seg = None
|
||||||
self.height = 0
|
self.height = 0
|
||||||
self.width = 0
|
self.width = 0
|
||||||
@@ -33,26 +35,57 @@ class ChanBI():
|
|||||||
def set_seg(self, seg):
|
def set_seg(self, seg):
|
||||||
self.seg = seg
|
self.seg = seg
|
||||||
self.seg_index = len(seg.bi_list)-1
|
self.seg_index = len(seg.bi_list)-1
|
||||||
|
# macd_hist / macd_div 改为惰性。原来 add_klc 每加一根 KLC 就把整笔的
|
||||||
|
# 所有 KLU 重新累加一遍,是 O(k²);而这两个值只有背驰判定(bsp.py)在读,
|
||||||
|
# lean 模式下 bsp 根本不算,等于全程白算。这里只标脏,取值时才算。
|
||||||
|
# 语义不变:klc_list 只增不减(set_start_klc 会重置并同时清脏),
|
||||||
|
# dir 在 klc 累加期间固定,所以延后到读取时算与逐次重算结果相同。
|
||||||
|
@property
|
||||||
|
def macd_hist(self):
|
||||||
|
if self._macd_dirty:
|
||||||
|
self.cal_macdhist()
|
||||||
|
return self._macd_hist
|
||||||
|
|
||||||
|
@macd_hist.setter
|
||||||
|
def macd_hist(self, v):
|
||||||
|
self._macd_hist = v
|
||||||
|
self._macd_dirty = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def macd_div(self):
|
||||||
|
self.cal_macd_div()
|
||||||
|
return self._macd_div
|
||||||
|
|
||||||
|
@macd_div.setter
|
||||||
|
def macd_div(self, v):
|
||||||
|
self._macd_div = v
|
||||||
|
|
||||||
def set_macdhist(self, macd_hist):
|
def set_macdhist(self, macd_hist):
|
||||||
self.macd_hist = macd_hist
|
self.macd_hist = macd_hist
|
||||||
def set_macd_div(self, macd_div):
|
def set_macd_div(self, macd_div):
|
||||||
self.macd_div = macd_div
|
self.macd_div = macd_div
|
||||||
def cal_macd_div(self):
|
def cal_macd_div(self):
|
||||||
self.macd_div = 0.0
|
self._macd_div = 0.0
|
||||||
if self.pre and self.pre.pre:
|
if self.pre and self.pre.pre:
|
||||||
if self.pre.pre.macd_hist == 0:
|
if self.pre.pre.macd_hist == 0:
|
||||||
self.macd_div = 0.0
|
self._macd_div = 0.0
|
||||||
else:
|
else:
|
||||||
self.macd_div = self.macd_hist / self.pre.pre.macd_hist
|
self._macd_div = self.macd_hist / self.pre.pre.macd_hist
|
||||||
#print(self.start_time, self.end_time, self.macd_hist, self.pre.pre.macd_hist, self.macd_div)
|
#print(self.start_time, self.end_time, self.macd_hist, self.pre.pre.macd_hist, self.macd_div)
|
||||||
def cal_macdhist(self):
|
def cal_macdhist(self):
|
||||||
self.macd_hist = 0
|
self._macd_dirty = False
|
||||||
|
self._macd_hist = 0
|
||||||
|
up = self.dir == Chan_BI_DIR.UP
|
||||||
|
acc = 0
|
||||||
for klc in self.klc_list:
|
for klc in self.klc_list:
|
||||||
for klu in klc.klu_list:
|
for klu in klc.klu_list:
|
||||||
if self.dir == Chan_BI_DIR.UP and klu.macdhist > 0:
|
h = klu.macdhist
|
||||||
self.macd_hist += klu.macdhist
|
if up:
|
||||||
if self.dir == Chan_BI_DIR.DOWN and klu.macdhist < 0:
|
if h > 0:
|
||||||
self.macd_hist -= klu.macdhist
|
acc += h
|
||||||
|
elif h < 0:
|
||||||
|
acc -= h
|
||||||
|
self._macd_hist = acc
|
||||||
def check_bi_zs_overlap(self):
|
def check_bi_zs_overlap(self):
|
||||||
if self.next and self.next.next:
|
if self.next and self.next.next:
|
||||||
if self.dir == Chan_BI_DIR.UP:
|
if self.dir == Chan_BI_DIR.UP:
|
||||||
@@ -95,6 +128,10 @@ class ChanBI():
|
|||||||
self.start_klc = klc
|
self.start_klc = klc
|
||||||
self.klc_list = []
|
self.klc_list = []
|
||||||
self.klc_list.append(klc)
|
self.klc_list.append(klc)
|
||||||
|
# klc_list 被整个换掉,去重集合与惰性缓存都要跟着重置,
|
||||||
|
# 否则后续 add_klc 会以为旧下标还在里面而漏加
|
||||||
|
self._klc_idx = {klc.index}
|
||||||
|
self._macd_dirty = True
|
||||||
self.high = klc.high
|
self.high = klc.high
|
||||||
self.low = klc.low
|
self.low = klc.low
|
||||||
self.dir = ddir
|
self.dir = ddir
|
||||||
@@ -103,20 +140,14 @@ class ChanBI():
|
|||||||
def set_next(self, bi):
|
def set_next(self, bi):
|
||||||
self.next = bi
|
self.next = bi
|
||||||
def add_klc(self, klc):
|
def add_klc(self, klc):
|
||||||
added = False
|
# 去重原本是对 klc_list 线性扫描,配合下面每次全量重算的 macdhist,
|
||||||
if len(self.klc_list) > 0:
|
# 让「往一笔里加 k 根 KLC」变成 O(k²)。改用下标集合,O(1)。
|
||||||
for index in range(0, len(self.klc_list)):
|
if klc.index not in self._klc_idx:
|
||||||
if self.klc_list[index].index == klc.index:
|
self._klc_idx.add(klc.index)
|
||||||
added = True
|
|
||||||
break
|
|
||||||
if not added:
|
|
||||||
self.klc_list.append(klc)
|
self.klc_list.append(klc)
|
||||||
#print(self.start_time, klc.start_time)
|
|
||||||
#print(klc.end_time, klc.index)
|
|
||||||
self.end_klc = klc
|
self.end_klc = klc
|
||||||
self.end_time = klc.klu_list[-1].time
|
self.end_time = klc.klu_list[-1].time
|
||||||
self.cal_macdhist()
|
self._macd_dirty = True
|
||||||
self.cal_macd_div()
|
|
||||||
def append_klc_list(self, klc_list):
|
def append_klc_list(self, klc_list):
|
||||||
self.klc_list.append(klc_list)
|
self.klc_list.append(klc_list)
|
||||||
def get_decimal(self, value):
|
def get_decimal(self, value):
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ class IndicatorsBuilderMixin:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def add_indicators(self, df):
|
def add_indicators(self, df):
|
||||||
|
"""算指标并一次性挂到 df 上。
|
||||||
|
|
||||||
|
这里不逐列 `df['x'] = ...`:那样每一列都触发一次 BlockManager 插入,
|
||||||
|
30 多列的开销比全部 TA 计算本身还大(2001 行实测 TA 合计 2.5ms,
|
||||||
|
逐列赋值 3.6ms)。增量路径每根都要走一遍,这笔开销是白付的。
|
||||||
|
"""
|
||||||
fast = 26
|
fast = 26
|
||||||
slow = 52
|
slow = 52
|
||||||
period = 9
|
period = 9
|
||||||
@@ -75,40 +81,43 @@ class IndicatorsBuilderMixin:
|
|||||||
bbp30 = (df['close'] - bb30['lowerband']) / (bb30['upperband'] - bb30['lowerband'])
|
bbp30 = (df['close'] - bb30['lowerband']) / (bb30['upperband'] - bb30['lowerband'])
|
||||||
bbp302 = (df['close'] - bb302['lowerband']) / (bb302['upperband'] - bb302['lowerband'])
|
bbp302 = (df['close'] - bb302['lowerband']) / (bb302['upperband'] - bb302['lowerband'])
|
||||||
bbp2633 = (df['close'] - bb2633['lowerband']) / (bb2633['upperband'] - bb2633['lowerband'])
|
bbp2633 = (df['close'] - bb2633['lowerband']) / (bb2633['upperband'] - bb2633['lowerband'])
|
||||||
df['bb2633upper'] = bb2633['upperband']
|
cols = {
|
||||||
df['bb2633lower'] = bb2633['lowerband']
|
'bb2633upper': bb2633['upperband'],
|
||||||
df['bbp2633'] = bbp2633
|
'bb2633lower': bb2633['lowerband'],
|
||||||
df['bb2633middle'] = bb2633['middleband']
|
'bbp2633': bbp2633,
|
||||||
df['atr'] = ta.ATR(df, timeperiod=14)
|
'bb2633middle': bb2633['middleband'],
|
||||||
df['bbup365'] = bb365['upperband']
|
'atr': ta.ATR(df, timeperiod=14),
|
||||||
df['bblow365'] = bb365['lowerband']
|
'bbup365': bb365['upperband'],
|
||||||
df['bbp365'] = bbp365
|
'bblow365': bb365['lowerband'],
|
||||||
df['bbup120'] = bb120['upperband']
|
'bbp365': bbp365,
|
||||||
df['bblow120'] = bb120['lowerband']
|
'bbup120': bb120['upperband'],
|
||||||
df['bbp120'] = bbp120
|
'bblow120': bb120['lowerband'],
|
||||||
df['bbup30'] = bb30['upperband']
|
'bbp120': bbp120,
|
||||||
df['bblow30'] = bb30['lowerband']
|
'bbup30': bb30['upperband'],
|
||||||
df['bbmiddle30'] = bb30_middle # 添加bb30中轨
|
'bblow30': bb30['lowerband'],
|
||||||
df['bbp30'] = bbp30
|
'bbmiddle30': bb30_middle,
|
||||||
df['bbup302'] = bb302['upperband']
|
'bbp30': bbp30,
|
||||||
df['bblow302'] = bb302['lowerband']
|
'bbup302': bb302['upperband'],
|
||||||
df['bbp302'] = bbp302
|
'bblow302': bb302['lowerband'],
|
||||||
df['macd'] = macd['macd']
|
'bbp302': bbp302,
|
||||||
df['macdsignal'] = macd['macdsignal']
|
'macd': macd['macd'],
|
||||||
df['macdhist'] = macd['macdhist']
|
'macdsignal': macd['macdsignal'],
|
||||||
df['ema5'] = ta.EMA(df, timeperiod=5)
|
'macdhist': macd['macdhist'],
|
||||||
df['ema10'] = ta.EMA(df, timeperiod=10)
|
}
|
||||||
df['ema24'] = ta.EMA(df, timeperiod=24)
|
for _p, _n in ((5, 'ema5'), (10, 'ema10'), (24, 'ema24'), (52, 'ema52'),
|
||||||
df['ema52'] = ta.EMA(df, timeperiod=52)
|
(104, 'ema104'), (156, 'ema156'), (208, 'ema208'),
|
||||||
df['ema104'] = ta.EMA(df, timeperiod=104)
|
(26, 'ema26'), (13, 'ema13'), (7, 'ema7')):
|
||||||
df['ema156'] = ta.EMA(df, timeperiod=156)
|
cols[_n] = ta.EMA(df, timeperiod=_p)
|
||||||
df['ema208'] = ta.EMA(df, timeperiod=208)
|
cols['rsi'] = ta.RSI(df, timeperiod=14)
|
||||||
df['ema26'] = ta.EMA(df, timeperiod=26)
|
cols['volume_ratio'] = self.cal_volume_ratio(df)
|
||||||
df['ema13'] = ta.EMA(df, timeperiod=13)
|
|
||||||
df['ema7'] = ta.EMA(df, timeperiod=7)
|
# 重复调用(增量路径每根都会)时先摘掉旧列,否则 concat 出重名列。
|
||||||
df['rsi'] = ta.RSI(df, timeperiod=14)
|
# 摘掉再接回末尾,列序与逐列覆盖的结果一致。
|
||||||
df['volume_ratio'] = self.cal_volume_ratio(df)
|
new = pd.DataFrame(cols, index=df.index)
|
||||||
return df
|
dup = [c for c in new.columns if c in df.columns]
|
||||||
|
if dup:
|
||||||
|
df = df.drop(columns=dup)
|
||||||
|
return pd.concat([df, new], axis=1)
|
||||||
|
|
||||||
def get_ema_state(self, dataframe):
|
def get_ema_state(self, dataframe):
|
||||||
klu_list = self.get_klu_list(dataframe)
|
klu_list = self.get_klu_list(dataframe)
|
||||||
|
|||||||
@@ -128,23 +128,28 @@ class KlineBuilderMixin:
|
|||||||
return Chan_FX_TYPE.UNKNOWN
|
return Chan_FX_TYPE.UNKNOWN
|
||||||
|
|
||||||
def check_fx_pattern(self, klc):
|
def check_fx_pattern(self, klc):
|
||||||
klu_list = klc.pre.klu_list + klc.klu_list + klc.next.klu_list
|
"""给分型前后三根 KLC 的裸 K 打 pattern 标记。
|
||||||
|
|
||||||
|
原本还会把 `klu.to_string()` 拼成一个字符串——那是给下面那行注释掉的
|
||||||
|
print 用的,拼完就丢。它在 cal_bi_list 的内层,2000 根上要跑近三万次
|
||||||
|
f-string + 六万次 enum 格式化,是纯废动作,已删。
|
||||||
|
|
||||||
|
`klu.pattern` 只被 cal_klu_pattern 自己的双 K / 三 K 判定读,
|
||||||
|
不出这个模块,也不进 web 序列化。所以 lean 下整个调用可跳。
|
||||||
|
"""
|
||||||
|
if getattr(self, 'lean', False):
|
||||||
|
return
|
||||||
|
klu_list = klc.pre.klu_list + klc.klu_list + klc.next.klu_list
|
||||||
self.cal_klu_pattern(klu_list)
|
self.cal_klu_pattern(klu_list)
|
||||||
p = ""
|
|
||||||
for klu in klu_list:
|
|
||||||
p += klu.to_string()
|
|
||||||
#print(p)
|
|
||||||
|
|
||||||
def cal_volume_ratio(self, dataframe, window=10):
|
def cal_volume_ratio(self, dataframe, window=10):
|
||||||
df = dataframe.copy()
|
"""当根量 / 前 window 根均量。前 window-1 根无基准,填 1.0。
|
||||||
# 计算过去N根K线的平均成交量
|
|
||||||
df['avg_volume'] = df['volume'].rolling(window=window).mean()
|
原写法先 `dataframe.copy()` 再挂两列——为算一列 rolling 复制了整张
|
||||||
# 计算量比
|
四十列的表。直接在 Series 上算,结果逐值相同。
|
||||||
df['volume_ratio'] = df['volume'] / df['avg_volume']
|
"""
|
||||||
# 填充缺失值(前N根K线)
|
vol = dataframe['volume']
|
||||||
df['volume_ratio'] = df['volume_ratio'].fillna(1.0)
|
return (vol / vol.rolling(window=window).mean()).fillna(1.0).rename('volume_ratio')
|
||||||
return df['volume_ratio']
|
|
||||||
|
|
||||||
def cal_kl_data(self, dataframe:DataFrame):
|
def cal_kl_data(self, dataframe:DataFrame):
|
||||||
"""按行构造 KLU 链。
|
"""按行构造 KLU 链。
|
||||||
@@ -249,7 +254,12 @@ class KlineBuilderMixin:
|
|||||||
for klu in klu_list:
|
for klu in klu_list:
|
||||||
self._push_klu_into_klc_list(klc_list, klu, last_klu)
|
self._push_klu_into_klc_list(klc_list, klu, last_klu)
|
||||||
last_klu = klu
|
last_klu = klu
|
||||||
klc_list = self.cal_trend(klc_list)
|
# cal_trend 只产出 klc.trend,而全仓只有它自己(经 prev_klcs 读自身序列
|
||||||
|
# 状态)、一个 __str__ 和 web 的 klc_trend 图层消费它——笔与中枢不读。
|
||||||
|
# 增量路径的 klc 其 trend 恒为 UNKNOWN 却与批量构建逐字段相同,是实证。
|
||||||
|
# 所以 lean 下可跳;web 走非 lean,图层不受影响。
|
||||||
|
if not getattr(self, 'lean', False):
|
||||||
|
klc_list = self.cal_trend(klc_list)
|
||||||
return klc_list
|
return klc_list
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+51
-1
@@ -1556,7 +1556,7 @@ python step46_engine_parity.py --check --rows 200000 --out step46_baseline_big.j
|
|||||||
> 这处偏差值得记:它是读代码读出来的(把一个函数的内部自引用误当成外部
|
> 这处偏差值得记:它是读代码读出来的(把一个函数的内部自引用误当成外部
|
||||||
> 依赖),而对拍一次就定论了。同类判断优先用对拍。
|
> 依赖),而对拍一次就定论了。同类判断优先用对拍。
|
||||||
|
|
||||||
### 5.6 增量在影子路径上的落地(2026-08-28)
|
### 5.7 增量在影子路径上的落地(2026-08-28)
|
||||||
|
|
||||||
`shadow_signal.compute` 已改为按 `(symbol, timeframe)` 缓存流式对象。
|
`shadow_signal.compute` 已改为按 `(symbol, timeframe)` 缓存流式对象。
|
||||||
|
|
||||||
@@ -1585,6 +1585,56 @@ python step46_engine_parity.py --check --rows 200000 --out step46_baseline_big.j
|
|||||||
是 `build_htf_zones` / `find_fast_bsp3` / `attach_htf_context`。再压 chan
|
是 `build_htf_zones` / `find_fast_bsp3` / `attach_htf_context`。再压 chan
|
||||||
构建收益有限,要继续压得看下游那三个。
|
构建收益有限,要继续压得看下游那三个。
|
||||||
|
|
||||||
|
#### 5.71 第二轮引擎优化(2026-08-28,服务端反馈驱动)
|
||||||
|
|
||||||
|
承上——§5.7 末尾点的那两个引擎侧热点(`add_indicators` 全表重算 34%、
|
||||||
|
`cal_bi_list` 整表重扫 51%)。逐个查下来,**四处都是"算了没人要的东西",
|
||||||
|
不是算法本身慢**:
|
||||||
|
|
||||||
|
| 改动 | 性质 |
|
||||||
|
|---|---|
|
||||||
|
| `cal_trend` 挂到 lean 下 | 见上面那条更正——它不是笔的依赖。web 走非 lean,`klc_trend` 图层不受影响 |
|
||||||
|
| `check_fx_pattern` 删掉拼完就丢的字符串 | 它把 `klu.to_string()` 拼成 `p` 只为一行注释掉的 print。2000 根上近 3 万次 f-string + 6 万次 enum 格式化,在 `cal_bi_list` 内层。`klu.pattern` 只被 `cal_klu_pattern` 自己的双K/三K 判定读,不出模块、不进 web,故整个调用 lean 下也跳 |
|
||||||
|
| `ChanBI.add_klc` 去二次方 | 去重原本线性扫 `klc_list`,且**每加一根就把整笔所有 KLU 的 macdhist 重累一遍** → 往一笔加 k 根是 O(k²)。改下标集合 + `macd_hist`/`macd_div` 惰性求值。这两个值只有背驰判定(`bsp.py`)读,lean 下 bsp 根本不算 |
|
||||||
|
| `add_indicators` 批量挂列 | 2001 行上 TA 计算合计只有 2.5ms,而 30 多次 `df['x']=` 要 3.6ms——**开销大头是 BlockManager 逐列插入,不是计算**。改为一次 concat。`cal_volume_ratio` 里为算一列 rolling 而 `dataframe.copy()` 整张 40 列表,一并去掉 |
|
||||||
|
|
||||||
|
实测(本机,2001 根窗口)。**注意这组数和 §5.7 的 21.8ms 不同机**——本机基线
|
||||||
|
就是 13.9ms,服务端约慢 1.4~1.6 倍,别把两边的绝对值直接比:
|
||||||
|
|
||||||
|
| | 改前 | 改后 |
|
||||||
|
|---|---|---|
|
||||||
|
| `append_bar` | 13.9ms | **6.6ms** |
|
||||||
|
| └ `rebuild_bi_zs` | 8.7ms | 2.8ms |
|
||||||
|
| └ `add_indicators` | 4.4ms | 3.5ms |
|
||||||
|
| `TF_DF` lean | 49.8ms | **32.9ms** |
|
||||||
|
| `TF_DF` full | 72.7ms | 64.9ms |
|
||||||
|
|
||||||
|
**对拍**(`git worktree` 检出改动前的提交,同一份 BTC 1m 4000 根,
|
||||||
|
lean/full 两模式 × klc/klu/bi/zs/seg/bsp/指标列/列序,38 项指纹):
|
||||||
|
|
||||||
|
- **full 模式 19 项全部一致** → web 那条路一个字节没动。
|
||||||
|
- **lean 模式差 2 项,且正是设计要它差的两项**:`klc.trend`(跳了
|
||||||
|
`cal_trend`,现恒为 `UNKNOWN`)与 `klu.pattern`(跳了 `cal_klu_pattern`)。
|
||||||
|
- **lean 下的 `bi`/`zs`/`seg`/`bsp`/`dataframe` 全部一致** ——
|
||||||
|
这就是"这两个字段没人读"那句断言的实测证据:把它们打空,下游一位不变。
|
||||||
|
|
||||||
|
⚠️ 别把这写成"完全一致"。有两项按设计就该变,写成全等会掩盖掉真正要担保的
|
||||||
|
那件事:**变的只有这两个已确认无人消费的字段。** full 模式的 `bsp_list` 是
|
||||||
|
`macd_hist` 的唯一消费者,它没变才说明惰性求值是对的。
|
||||||
|
|
||||||
|
**剩下没做的:`add_indicators` 仍是全表重算**(为加一根算 2001 行)。
|
||||||
|
EMA/MACD/ATR 是递推的、BB/SMA/量比是窗口的,理论上都能 O(1) 更新到精确值,
|
||||||
|
但 Wilder RSI 需要额外维护 `avg_gain`/`avg_loss` 状态(从输出反推不出来)。
|
||||||
|
做完 `append_bar` 可到 3~4ms。**风险在于一处不精确就静默换掉一批信号,
|
||||||
|
要做必须先扩对拍。**
|
||||||
|
|
||||||
|
**瓶颈已经换位置了。** 本机分档(`research/live/probe_inner.py`,与服务端
|
||||||
|
同口径):`TF_DF` 两条腿占 70%、`build_htf_zones` 13%、`htf_fx_timeline` 6%、
|
||||||
|
`find_fast_bsp3`+ladder+attach 合计不到 2%。但服务端报的是 chan 构建 22ms /
|
||||||
|
信号链 86ms,按机器差(×1.36)解释不了四倍差距。
|
||||||
|
**曾怀疑是 payload 反序列化,实测 `_rebuild` 只有 1.0ms,假设不成立。**
|
||||||
|
两边跑同一个探针对分档表,才能定位那 86ms。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 接下来要做的事(按优先级)
|
## 6. 接下来要做的事(按优先级)
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
"""把 `inner_ms` 拆成和本地一致的分档,用来定位两边测不一致的那部分。
|
||||||
|
|
||||||
|
起因:本地量到的构成是 TF_DF 占 ~80%、信号链 ~20%,服务器报的是 chan 构建
|
||||||
|
22ms、信号链 86ms。按机器差(×1.36)也解释不了四倍差距,说明两边测的不是
|
||||||
|
同一件事,或者有个环节只在服务器上贵。
|
||||||
|
|
||||||
|
用同一批窗口跑,比较分档而不是总数。两边都跑一遍再对表:
|
||||||
|
|
||||||
|
python research/live/probe_inner.py --syms BTC,ETH,SOL --repeat 5
|
||||||
|
|
||||||
|
分档口径(与 shadow_signal.compute 的调用顺序一致):
|
||||||
|
rebuild payload → DataFrame
|
||||||
|
ind_ltf/htf 两条腿各自的 add_indicators(TF_DF 内部会做,这里单独计时)
|
||||||
|
chan_ltf/htf TF_DF 构建(lean)
|
||||||
|
zones build_htf_zones
|
||||||
|
ladder add_zone_ladder
|
||||||
|
bsp find_fast_bsp3
|
||||||
|
timeline htf_fx_timeline(5m 分型时间线)
|
||||||
|
attach attach_htf_agree + attach_zone_ladder
|
||||||
|
|
||||||
|
注意 `ind_*` 与 `chan_*` 在真实路径里是合一的(TF_DF 内部调 add_indicators),
|
||||||
|
这里拆开只为定位。总和会略大于实际 inner_ms。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import warnings
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
for _v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"):
|
||||||
|
os.environ.setdefault(_v, "1")
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve()
|
||||||
|
sys.path.insert(0, str(HERE.parents[1]))
|
||||||
|
sys.path.insert(0, str(HERE.parents[2]))
|
||||||
|
sys.path.insert(0, str(HERE.parent))
|
||||||
|
|
||||||
|
LTF_BARS, HTF_BARS = 2001, 801
|
||||||
|
|
||||||
|
|
||||||
|
def med(fn, n: int):
|
||||||
|
ts = []
|
||||||
|
out = None
|
||||||
|
for _ in range(n):
|
||||||
|
t = time.perf_counter()
|
||||||
|
out = fn()
|
||||||
|
ts.append((time.perf_counter() - t) * 1000)
|
||||||
|
return float(np.median(ts)), out
|
||||||
|
|
||||||
|
|
||||||
|
def probe(sym: str, repeat: int) -> dict:
|
||||||
|
from chanlun import TF_DF
|
||||||
|
from chanlun.analysis.fast_bsp import (
|
||||||
|
add_zone_ladder, attach_htf_agree, attach_zone_ladder,
|
||||||
|
build_htf_zones, find_fast_bsp3, htf_fx_timeline,
|
||||||
|
)
|
||||||
|
from lib.data import fetch_ohlcv
|
||||||
|
|
||||||
|
dl = fetch_ohlcv(f"{sym}/USDT:USDT", "1m", LTF_BARS * 3).tail(LTF_BARS).reset_index(drop=True)
|
||||||
|
dh = fetch_ohlcv(f"{sym}/USDT:USDT", "5m", HTF_BARS * 3).tail(HTF_BARS).reset_index(drop=True)
|
||||||
|
|
||||||
|
probe_tf = TF_DF(lean=True)
|
||||||
|
r = {"sym": sym}
|
||||||
|
r["ind_ltf"], _ = med(lambda: probe_tf.add_indicators(dl.copy()), repeat)
|
||||||
|
r["ind_htf"], _ = med(lambda: probe_tf.add_indicators(dh.copy()), repeat)
|
||||||
|
r["chan_ltf"], cl = med(lambda: TF_DF(dl, 1, "1m", lean=True), repeat)
|
||||||
|
r["chan_htf"], ch = med(lambda: TF_DF(dh, 1, "5m", lean=True), repeat)
|
||||||
|
cdf = cl.dataframe
|
||||||
|
r["zones"], z = med(lambda: build_htf_zones(cdf, "1m", chan=cl), repeat)
|
||||||
|
if z is None or z.empty:
|
||||||
|
r["n_zones"] = 0
|
||||||
|
return r
|
||||||
|
z0 = z.reset_index(drop=True)
|
||||||
|
r["ladder"], zl = med(lambda: add_zone_ladder(z0), repeat)
|
||||||
|
r["bsp"], sg = med(lambda: find_fast_bsp3(cdf, zl), repeat)
|
||||||
|
r["timeline"], tl = med(lambda: htf_fx_timeline(ch, ch.dataframe), repeat)
|
||||||
|
r["attach"], _ = med(
|
||||||
|
lambda: attach_zone_ladder(attach_htf_agree(sg, cdf, tl), zl), repeat)
|
||||||
|
r["n_zones"], r["n_sig"] = len(z0), len(sg)
|
||||||
|
|
||||||
|
# 增量口径:init 一次后追加,看稳态单根成本
|
||||||
|
c = TF_DF(lean=True)
|
||||||
|
c.init_stream(dl.iloc[:-60].reset_index(drop=True), 1, "1m")
|
||||||
|
ts = []
|
||||||
|
for k in range(len(dl) - 60, len(dl)):
|
||||||
|
t = time.perf_counter()
|
||||||
|
c.append_bar(dl.iloc[k])
|
||||||
|
ts.append((time.perf_counter() - t) * 1000)
|
||||||
|
r["append_bar"] = float(np.median(ts[20:]))
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--syms", default="BTC,ETH,SOL")
|
||||||
|
ap.add_argument("--repeat", type=int, default=5)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
rows = [probe(s.strip(), args.repeat) for s in args.syms.split(",") if s.strip()]
|
||||||
|
d = pd.DataFrame(rows).set_index("sym")
|
||||||
|
parts = [c for c in ("ind_ltf", "ind_htf", "chan_ltf", "chan_htf", "zones",
|
||||||
|
"ladder", "bsp", "timeline", "attach") if c in d]
|
||||||
|
d["合计"] = d[parts].sum(axis=1)
|
||||||
|
pd.set_option("display.width", 220)
|
||||||
|
print("\n分档耗时(ms,中位)")
|
||||||
|
print(d[parts + ["合计", "append_bar"]].round(2).to_string())
|
||||||
|
print("\n占比(%)")
|
||||||
|
print((d[parts].div(d["合计"], axis=0) * 100).round(1).to_string())
|
||||||
|
print("\n规模")
|
||||||
|
print(d[[c for c in ("n_zones", "n_sig") if c in d]].to_string())
|
||||||
|
print("\n注:ind_* 与 chan_* 在真实路径里合一(TF_DF 内部调 add_indicators),"
|
||||||
|
"拆开只为定位,合计会略大于实际 inner_ms。")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user