ChanPivotMonitor: 实时中枢特征跟踪 + Telegram推送

- ChanPivotClassifier: 提取 calc_duration/contraction/shift 为 @staticmethod,新增 compute_features()
- ChanPivotMonitor: 实时追踪当前中枢,bi_count 增长时重新计算 shift/contraction/duration
- bsp_monitor/fetcher: 改用 data_provider HTTP API 替代直连 CCXT
- bsp_monitor/notify: 新增 send_telegram_message() 通用推送
- bsp_monitor/main: 集成 ChanPivotMonitor,有新笔或 BSP 时推送到 Telegram

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jackyu66git
2026-05-26 11:35:09 +08:00
co-authored by Claude Opus 4.7
parent 5ad761fad4
commit b1cbdca707
5 changed files with 300 additions and 40 deletions
+46 -13
View File
@@ -31,14 +31,16 @@ class ChanPivotClassifier:
# Feature extraction
# ------------------------------------------------------------------
def _calc_duration(self, zs) -> int:
@staticmethod
def calc_duration(zs) -> int:
"""持续时间: 第一笔首K → 最后一笔末K 的 index 差"""
bi_list = zs.bi_list
start_idx = bi_list[0].start_klc.index
end_idx = bi_list[-1].end_klc.index
return end_idx - start_idx
def _calc_contraction(self, zs) -> float:
@staticmethod
def calc_contraction(zs) -> float:
"""收敛率: 后窗口振幅均值 / 前窗口振幅均值"""
bi_list = zs.bi_list
if len(bi_list) < 4:
@@ -55,7 +57,8 @@ class ChanPivotClassifier:
return 1.0
return last_mean / first_mean
def _calc_shift(self, zs) -> tuple[float, float]:
@staticmethod
def calc_shift(zs) -> tuple[float, float]:
"""重心漂移: 前后半段重心均值差 (原始值, 归一化值)"""
bi_list = zs.bi_list
mid = len(bi_list) // 2
@@ -76,6 +79,39 @@ class ChanPivotClassifier:
return shift_raw, shift_norm
@staticmethod
def compute_duration_norm(duration_raw: int, historical_durations: list) -> float:
"""用历史窗口均值归一化 duration"""
if not historical_durations:
return 1.0
avg = sum(historical_durations) / len(historical_durations)
if avg == 0:
return 1.0
return duration_raw / avg
@staticmethod
def compute_features(zs, historical_durations: list | None = None):
"""计算单个中枢的全部结构特征(实时友好)"""
duration_raw = ChanPivotClassifier.calc_duration(zs)
contraction = ChanPivotClassifier.calc_contraction(zs)
shift_raw, shift_norm = ChanPivotClassifier.calc_shift(zs)
if historical_durations is not None and len(historical_durations) > 0:
duration_norm = ChanPivotClassifier.compute_duration_norm(
duration_raw, historical_durations
)
else:
duration_norm = 1.0
return {
"duration_raw": duration_raw,
"duration_norm": round(duration_norm, 4),
"contraction": round(contraction, 4),
"shift_raw": round(shift_raw, 6),
"shift_norm": round(shift_norm, 4),
"zs_height": round(zs.zg - zs.zd, 6),
}
# ------------------------------------------------------------------
# Label computation
# ------------------------------------------------------------------
@@ -189,9 +225,9 @@ class ChanPivotClassifier:
if not zs.is_sure or len(zs.bi_list) < 3:
continue
duration_raw = self._calc_duration(zs)
contraction = self._calc_contraction(zs)
shift_raw, shift_norm = self._calc_shift(zs)
duration_raw = ChanPivotClassifier.calc_duration(zs)
contraction = ChanPivotClassifier.calc_contraction(zs)
shift_raw, shift_norm = ChanPivotClassifier.calc_shift(zs)
raw.append({
"zs": zs,
@@ -203,17 +239,14 @@ class ChanPivotClassifier:
"zs_height": zs.zg - zs.zd,
})
# 归一化 duration: 除以均值
if raw:
avg_duration = sum(r["duration_raw"] for r in raw) / len(raw)
else:
avg_duration = 1
# 第二遍:组装输出 + 计算 label
result = []
for r in raw:
zs = r["zs"]
duration_norm = r["duration_raw"] / avg_duration if avg_duration > 0 else 1.0
historical = [x["duration_raw"] for x in raw]
duration_norm = ChanPivotClassifier.compute_duration_norm(
r["duration_raw"], historical
)
label_info = self._compute_label(zs, r["contraction"], r["shift_norm"])
# 时间处理