观察者 线段分析/扩展分析 支持无限递归,保留之前序列为@property 形式
移除 某些函数的缓存机制
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chanlun-py"
|
||||
version = "26.6.34"
|
||||
version = "26.6.42"
|
||||
edition = "2024"
|
||||
description = "缠论技术分析库 — Rust 高性能 Python 绑定"
|
||||
authors = ["YuYuKunKun"]
|
||||
|
||||
+436
-311
@@ -140,6 +140,312 @@ def get_log_level() -> str:
|
||||
return _当前日志级别
|
||||
|
||||
|
||||
@lru_cache(128)
|
||||
def K线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""原始K线相等校验:字段完备→浮点容错→普通全等"""
|
||||
比对字段 = ["标识", "序号", "周期", "时间戳", "高", "低", "开盘价", "收盘价", "成交量"]
|
||||
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"K线校验:字段[{字段}],A存在属性、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"K线校验:字段[{字段}],B存在属性、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
# 双浮点容错对比
|
||||
if isinstance(valA, float) and isinstance(valB, float):
|
||||
差值 = abs(valA - valB)
|
||||
if 差值 > 浮点容差:
|
||||
return False, f"K线校验:字段[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
|
||||
|
||||
elif 字段 == "时间戳":
|
||||
if int(valA) != int(valB):
|
||||
return False, f"K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
return True, "K线:全部字段结构、数值校验完全一致"
|
||||
|
||||
|
||||
def 缠论K线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""缠论K线:基础字段+标的K线递归校验"""
|
||||
比对字段 = ["序号", "时间戳", "高", "低", "方向", "分型", "周期", "标识", "分型特征值", "原始起始序号", "原始结束序号", "标的K线", "买卖点信息"]
|
||||
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"缠论K线校验:字段[{字段}],A存在、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"缠论K线校验:字段[{字段}],B存在、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
|
||||
if isinstance(valA, float) and isinstance(valB, float):
|
||||
差值 = abs(valA - valB)
|
||||
if 差值 > 浮点容差:
|
||||
return False, f"缠论K线校验:[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
|
||||
elif 字段 == "标的K线":
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"缠论K线校验:[标的K线]单边空值,A={valA is None},B={valB is None}"
|
||||
eq_flag, msg = K线相等(valA, valB, 浮点容差)
|
||||
if not eq_flag:
|
||||
return False, f"缠论K线校验:标的K线子项异常 >> {msg}"
|
||||
|
||||
elif 字段 == "时间戳":
|
||||
if int(valA) != int(valB):
|
||||
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
elif 字段 == "方向":
|
||||
if str(valA) != str(valB):
|
||||
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
elif 字段 == "分型":
|
||||
if str(valA) != str(valB):
|
||||
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
elif 字段 == "买卖点信息":
|
||||
if set(valA) != set(valB):
|
||||
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"缠论K线校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
return True, "缠论K线:全部字段、嵌套原始K线校验一致"
|
||||
|
||||
|
||||
def 分型相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""分型:左/中/右缠论K线递归 + 自有字段"""
|
||||
比对字段 = ["左", "中", "右", "_结构", "_时间戳", "_分型特征值"]
|
||||
|
||||
for 字段 in 比对字段:
|
||||
a存在 = hasattr(A, 字段)
|
||||
b存在 = hasattr(B, 字段)
|
||||
if a存在 and not b存在:
|
||||
return False, f"分型校验:[{字段}]A有属性、B缺失"
|
||||
if not a存在 and b存在:
|
||||
return False, f"分型校验:[{字段}]B有属性、A缺失"
|
||||
if not (a存在 and b存在):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
|
||||
if isinstance(valA, float) and isinstance(valB, float):
|
||||
差值 = abs(valA - valB)
|
||||
if 差值 > 浮点容差:
|
||||
return False, f"分型校验:[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
|
||||
elif 字段 in ("左", "中", "右"):
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"分型校验:[{字段}]空值不一致,A={valA is None},B={valB is None}"
|
||||
eq_ok, msg = 缠论K线相等(valA, valB, 浮点容差)
|
||||
if not eq_ok:
|
||||
return False, f"分型校验:[{字段}]缠论K线子项异常 >> {msg}"
|
||||
|
||||
elif 字段 == "_结构":
|
||||
if str(valA) != str(valB):
|
||||
return False, f"分型K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
elif 字段 == "_时间戳":
|
||||
if int(valA) != int(valB):
|
||||
return False, f"分型K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"分型校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
return True, "分型:自有字段+三根缠论K线全部校验一致"
|
||||
|
||||
|
||||
@lru_cache(4096)
|
||||
def 缺口相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""缺口:高、低浮点校验"""
|
||||
比对字段 = ["高", "低"]
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"缺口校验:[{字段}]A存在、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"缺口校验:[{字段}]B存在、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
if isinstance(valA, float) and isinstance(valB, float):
|
||||
差值 = abs(valA - valB)
|
||||
if 差值 > 浮点容差:
|
||||
return False, f"缺口校验:[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"缺口校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
return True, "缺口:上下沿价格校验完全一致"
|
||||
|
||||
|
||||
def 线段特征相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""线段特征:基础序列虚线列表逐项校验"""
|
||||
比对字段 = ["序号", "标识", "线段方向", "基础序列"]
|
||||
标签 = f"线段特征校验[A标识={A.标识},B标识={B.标识}]"
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"{标签}: [{字段}]A存在、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"{标签}: [{字段}]B存在、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
if 字段 == "基础序列":
|
||||
if len(valA) != len(valB):
|
||||
return False, f"{标签}: [基础序列]列表长度不一致,A长度={len(valA)},B长度={len(valB)}"
|
||||
for idx, (itemA, itemB) in enumerate(zip(valA, valB)):
|
||||
eq, msg = 虚线相等(itemA, itemB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}:基础序列[{idx}]子虚线异常 >> {msg}"
|
||||
|
||||
elif 字段 == "线段方向":
|
||||
if str(valA) != str(valB):
|
||||
return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
return True, f"{标签}:字段与内部虚线序列全部一致"
|
||||
|
||||
|
||||
def 中枢相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""中枢:基础序列虚线列表+第三买卖线单虚线"""
|
||||
比对字段 = ["序号", "标识", "级别", "基础序列", "第三买卖线", "本级_第三买卖线"]
|
||||
标签 = f"中枢校验[A标识={A.标识},B标识={B.标识}]"
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"{标签}: [{字段}]A存在、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"{标签}: [{字段}]B存在、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
|
||||
if 字段 == "基础序列":
|
||||
if len(valA) != len(valB):
|
||||
return False, f"{标签}: [基础序列]长度不一致 A={len(valA)},B={len(valB)}"
|
||||
for idx, (itemA, itemB) in enumerate(zip(valA, valB)):
|
||||
eq, msg = 虚线相等(itemA, itemB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}:基础序列[{idx}]虚线异常 >> {msg}"
|
||||
elif 字段 in ("第三买卖线", "本级_第三买卖线"):
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"{标签}: [{字段}]空值不一致 A={valA is None},B={valB is None}"
|
||||
eq, msg = 虚线相等(valA, valB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [{字段}]子虚线异常 >> {msg}"
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
return True, f"{标签}:基础序列+第三买卖线全部校验一致"
|
||||
|
||||
|
||||
def 虚线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""虚线(笔/线段):全量字段、分型/缺口/K线/列表嵌套精细化报错"""
|
||||
比对字段 = ["标识", "序号", "级别", "文", "武", "有效性", "基础序列", "特征序列", "实_中枢序列", "虚_中枢序列", "合_中枢序列", "确认K线", "模式", "_特征序列_显示", "前一缺口", "前一结束位置", "短路修正"]
|
||||
标签 = f"虚线校验[A标识={A.标识},B标识={B.标识}]"
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"{标签}: [{字段}]A存在属性、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"{标签}: [{字段}]B存在属性、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
|
||||
# 文/武:分型
|
||||
if 字段 in ("文", "武"):
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"{标签}: [{字段}]分型空值不一致 A={valA is None},B={valB is None}"
|
||||
eq, msg = 分型相等(valA, valB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [{字段}]子分型异常 >> {msg}"
|
||||
# 前一缺口
|
||||
elif 字段 == "前一缺口":
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"{标签}: [前一缺口]空值不一致 A={valA is None},B={valB is None}"
|
||||
eq, msg = 缺口相等(valA, valB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [前一缺口]子缺口异常 >> {msg}"
|
||||
# 前一缺口
|
||||
elif 字段 == "前一结束位置":
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"{标签}: [前一结束位置]空值不一致 A={valA is None},B={valB is None}"
|
||||
eq, msg = 虚线相等(valA, valB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [前一结束位置]异常 >> {msg}"
|
||||
# 确认K线
|
||||
elif 字段 == "确认K线":
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"{标签}: [确认K线]空值不一致 A={valA is None},B={valB is None}"
|
||||
eq, msg = 缠论K线相等(valA, valB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [确认K线]子缠论K线异常 >> {msg}"
|
||||
# 各类列表
|
||||
elif 字段 in ("基础序列", "实_中枢序列", "虚_中枢序列", "合_中枢序列", "特征序列"):
|
||||
if len(valA) != len(valB):
|
||||
return False, f"{标签}: [{字段}]列表长度不一致 A={len(valA)},B={len(valB)}"
|
||||
for idx, (itemA, itemB) in enumerate(zip(valA, valB)):
|
||||
if itemA is None and itemB is None:
|
||||
continue
|
||||
if itemA is None or itemB is None:
|
||||
return False, f"{标签}: [{字段}][{idx}]单项空值不一致 A={itemA is None},B={itemB is None}"
|
||||
if 字段 == "基础序列":
|
||||
eq, msg = 虚线相等(itemA, itemB, 浮点容差)
|
||||
elif "中枢" in 字段:
|
||||
eq, msg = 中枢相等(itemA, itemB, 浮点容差)
|
||||
else:
|
||||
eq, msg = 线段特征相等(itemA, itemB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [{字段}][{idx}]子项异常 >> {msg}"
|
||||
# 普通字段
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"{标签}: [{字段}]数值不等 A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
return True, f"{标签}:全字段、所有嵌套子结构校验全部一致"
|
||||
|
||||
|
||||
class 买卖点类型(str, Enum):
|
||||
"""买卖点类型 — 缠论的三类买卖点及扩展类型。
|
||||
|
||||
@@ -2080,6 +2386,9 @@ class 指标计算器:
|
||||
|
||||
指标计算器._更新均线(当前K线, 全序列, 配置)
|
||||
|
||||
if prev is not None:
|
||||
指标计算器._回填新指标(全序列, 配置)
|
||||
|
||||
@staticmethod
|
||||
def _计算MACD组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置):
|
||||
idx = 当前K线.指标
|
||||
@@ -2154,6 +2463,86 @@ class 指标计算器:
|
||||
前值 = 普K序列[-2].指标.均线.get(key)
|
||||
当前K线.指标.均线[key] = 均线工具.增量EMA(普K序列, period, 配置.指标计算方式, 前值)
|
||||
|
||||
@staticmethod
|
||||
def _回填新指标(全序列: List[K线], 配置: 缠论配置):
|
||||
"""运行中新增指标参数时,回填所有历史K线。
|
||||
|
||||
比较首尾K线的指标键,检测运行中动态添加到配置的新指标参数,
|
||||
然后从第一根K线开始逐根重新计算,使历史K线也能获得新指标值。
|
||||
"""
|
||||
首K指标 = 全序列[0].指标
|
||||
尾K指标 = 全序列[-1].指标
|
||||
if 首K指标 is None or 尾K指标 is None:
|
||||
return
|
||||
|
||||
def _新键(尾指标, 首指标, 参数列表):
|
||||
新参数 = []
|
||||
for params in 参数列表:
|
||||
key = params[0]
|
||||
if key in 尾指标 and key not in 首指标:
|
||||
新参数.append(params)
|
||||
return 新参数
|
||||
|
||||
新MACD = _新键(尾K指标, 首K指标, 配置._解析MACD参数列表())
|
||||
新RSI = _新键(尾K指标, 首K指标, 配置._解析RSI周期列表())
|
||||
新KDJ = _新键(尾K指标, 首K指标, 配置._解析KDJ参数列表())
|
||||
新BOLL = _新键(尾K指标, 首K指标, 配置._解析BOLL参数列表())
|
||||
|
||||
if not (新MACD or 新RSI or 新KDJ or 新BOLL):
|
||||
return
|
||||
|
||||
计算方式 = 配置.指标计算方式
|
||||
|
||||
for i, k线 in enumerate(全序列):
|
||||
if k线.指标 is None:
|
||||
k线.指标 = 指标容器()
|
||||
|
||||
idx = k线.指标
|
||||
prev = 全序列[i - 1].指标 if i > 0 else None
|
||||
|
||||
for key, 快, 慢, 信号 in 新MACD:
|
||||
prev_val = prev[key] if prev is not None and key in prev else None
|
||||
if prev_val is not None:
|
||||
idx[key] = 平滑异同移动平均线.增量计算_K线(prev_val, k线, 计算方式)
|
||||
else:
|
||||
idx[key] = 平滑异同移动平均线.首次计算_K线(k线, 计算方式, 快, 慢, 信号)
|
||||
|
||||
for key, 周期 in 新RSI:
|
||||
prev_val = prev[key] if prev is not None and key in prev else None
|
||||
if prev_val is not None:
|
||||
idx[key] = 相对强弱指数.增量计算_K线(prev_val, k线, 计算方式)
|
||||
else:
|
||||
idx[key] = 相对强弱指数.首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
周期,
|
||||
配置.相对强弱指数_超买阈值,
|
||||
配置.相对强弱指数_超卖阈值,
|
||||
配置.相对强弱指数_移动平均线周期,
|
||||
)
|
||||
|
||||
for key, rsv, k平滑, d平滑 in 新KDJ:
|
||||
prev_val = prev[key] if prev is not None and key in prev else None
|
||||
if prev_val is not None:
|
||||
idx[key] = 随机指标.增量计算_K线(prev_val, k线, 计算方式)
|
||||
else:
|
||||
idx[key] = 随机指标.首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
rsv,
|
||||
k平滑,
|
||||
d平滑,
|
||||
配置.随机指标_超买阈值,
|
||||
配置.随机指标_超卖阈值,
|
||||
)
|
||||
|
||||
for key, 周期, 标准差倍数 in 新BOLL:
|
||||
prev_val = prev[key] if prev is not None and key in prev else None
|
||||
if prev_val is not None:
|
||||
idx[key] = 布林带.增量计算(prev_val, k线, 计算方式)
|
||||
else:
|
||||
idx[key] = 布林带.首次计算(k线, 计算方式, 周期, 标准差倍数)
|
||||
|
||||
|
||||
class 背驰分析:
|
||||
"""静态方法容器,提供背驰/背离检测算法。
|
||||
@@ -3581,6 +3970,14 @@ class 虚线:
|
||||
结果.append(当前段柱子)
|
||||
当前段柱子 = [k线序列[i].macd.MACD柱]
|
||||
当前符号 = 新符号
|
||||
if 当前段柱子:
|
||||
结果.append(当前段柱子)
|
||||
"""a = [x for sub in 结果 for x in sub]
|
||||
b = [sub.macd.MACD柱 for sub in k线序列]
|
||||
if list(a) != list(b):
|
||||
for i,(j,k) in enumerate(zip(a, b)):
|
||||
if j is not k:
|
||||
raise RuntimeError( f"序列不一致,{len(a)}, {len(b)}, {(i,j,k)}")"""
|
||||
return tuple(结果)
|
||||
|
||||
@classmethod
|
||||
@@ -6107,18 +6504,21 @@ class 观察者:
|
||||
self.投喂原始数据(转化为时间戳(int(时间戳)), 开盘价, 最高价, 最低价, 收盘价, 成交量)
|
||||
|
||||
@classmethod
|
||||
def 读取数据文件(cls, 观察员: 观察者, 文件路径: str, 配置=缠论配置()) -> Self:
|
||||
def 读取数据文件(cls, 文件路径: str, 配置=缠论配置(), *, 观察员: Optional[观察者] = None) -> Self:
|
||||
"""加载数据文件
|
||||
:param 观察员: 观察者
|
||||
:param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb
|
||||
:param 配置: 缠论配置
|
||||
:param 观察员: 可选,已有观察者实例;不传则自动创建
|
||||
:return: 观察者实例
|
||||
"""
|
||||
name = Path(文件路径).name.split(".")[0]
|
||||
符号, 周期, 起始时间戳, 结束时间戳 = name.split("-")
|
||||
观察员.符号 = 符号
|
||||
观察员.周期 = int(周期)
|
||||
观察员.配置 = 配置
|
||||
if 观察员 is None:
|
||||
观察员 = cls(符号, int(周期), 配置)
|
||||
else:
|
||||
观察员.符号 = 符号
|
||||
观察员.周期 = int(周期)
|
||||
观察员.配置 = 配置
|
||||
观察员.加载本地数据(文件路径)
|
||||
return 观察员
|
||||
|
||||
@@ -6375,13 +6775,13 @@ class 立体分析器:
|
||||
:return: 数据保存目录路径
|
||||
"""
|
||||
# 生成存储根目录
|
||||
脚本目录 = Path(__file__).parent if not root else root # 取当前脚本所在文件夹
|
||||
脚本目录 = tempfile.gettempdir() if not root else root # 默认系统临时目录
|
||||
起始时间 = int(self._单体分析器[self.__输入周期].普通K线序列[0].时间戳.timestamp())
|
||||
结束时间 = int(self._单体分析器[self.__输入周期].普通K线序列[-1].时间戳.timestamp())
|
||||
目录标识 = f"PyM_{self._单体分析器[self.__输入周期].标识}_{起始时间}_{结束时间}"
|
||||
|
||||
# 最终保存路径 = 脚本目录 / 自动生成的文件夹
|
||||
保存路径 = 脚本目录 / 目录标识
|
||||
保存路径 = Path(os.path.join(脚本目录, 目录标识))
|
||||
保存路径.mkdir(exist_ok=True)
|
||||
|
||||
for 周期 in self.周期组:
|
||||
@@ -6399,7 +6799,7 @@ def 测试_读取数据(观察员: 观察者, 配置: 缠论配置) -> Callable[
|
||||
|
||||
def 魔法():
|
||||
启动时间 = datetime.now()
|
||||
观察者.读取数据文件(观察员, 配置.加载文件路径, 配置)
|
||||
观察者.读取数据文件(配置.加载文件路径, 配置, 观察员=观察员)
|
||||
消耗用时 = datetime.now() - 启动时间
|
||||
logger.info(f"测试_读取数据 耗时 {消耗用时} 普K数量 {len(观察员.普通K线序列)}")
|
||||
return 观察员
|
||||
@@ -6436,315 +6836,40 @@ def 测试_周期合成(配置: 缠论配置, 配置组: Dict[int, 缠论配置]
|
||||
return 魔法
|
||||
|
||||
|
||||
@lru_cache(128)
|
||||
def K线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""原始K线相等校验:字段完备→浮点容错→普通全等"""
|
||||
比对字段 = ["标识", "序号", "周期", "时间戳", "高", "低", "开盘价", "收盘价", "成交量"]
|
||||
def 测试_指标挂载(配置: 缠论配置):
|
||||
文件路径 = 配置.加载文件路径
|
||||
name = Path(文件路径).name.split(".")[0]
|
||||
符号, 周期, 起始时间戳, 结束时间戳 = name.split("-")
|
||||
周期 = int(周期)
|
||||
观察员 = 观察者(符号, 周期, 配置)
|
||||
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"K线校验:字段[{字段}],A存在属性、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"K线校验:字段[{字段}],B存在属性、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
def 魔法():
|
||||
启动时间 = datetime.now()
|
||||
with open(文件路径, "rb") as f:
|
||||
buffer = f.read()
|
||||
size = struct.calcsize(">6d")
|
||||
for i in range(len(buffer) // size):
|
||||
if i == 500:
|
||||
配置.MACD_参数列表 = [("macd", 配置.平滑异同移动平均线_快线周期, 配置.平滑异同移动平均线_慢线周期, 配置.平滑异同移动平均线_信号周期)]
|
||||
配置.MACD_参数列表.append(("macd_12_26_9", 12, 26, 9))
|
||||
k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], 周期, 符号)
|
||||
观察员.增加原始K线(k线)
|
||||
if i == 500:
|
||||
assert 观察员.普通K线序列[0].指标.macd_12_26_9 is not None, "指标挂载失败"
|
||||
print(观察员.普通K线序列[0].指标["macd_12_26_9"])
|
||||
break
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
# 双浮点容错对比
|
||||
if isinstance(valA, float) and isinstance(valB, float):
|
||||
差值 = abs(valA - valB)
|
||||
if 差值 > 浮点容差:
|
||||
return False, f"K线校验:字段[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
|
||||
消耗用时 = datetime.now() - 启动时间
|
||||
logger.info(f"测试_指标挂载 耗时 {消耗用时} 普K数量 {len(观察员.普通K线序列)}")
|
||||
return 观察员
|
||||
|
||||
elif 字段 == "时间戳":
|
||||
if int(valA) != int(valB):
|
||||
return False, f"K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
return True, "K线:全部字段结构、数值校验完全一致"
|
||||
|
||||
|
||||
def 缠论K线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""缠论K线:基础字段+标的K线递归校验"""
|
||||
比对字段 = ["序号", "时间戳", "高", "低", "方向", "分型", "周期", "标识", "分型特征值", "原始起始序号", "原始结束序号", "标的K线", "买卖点信息"]
|
||||
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"缠论K线校验:字段[{字段}],A存在、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"缠论K线校验:字段[{字段}],B存在、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
|
||||
if isinstance(valA, float) and isinstance(valB, float):
|
||||
差值 = abs(valA - valB)
|
||||
if 差值 > 浮点容差:
|
||||
return False, f"缠论K线校验:[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
|
||||
elif 字段 == "标的K线":
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"缠论K线校验:[标的K线]单边空值,A={valA is None},B={valB is None}"
|
||||
eq_flag, msg = K线相等(valA, valB, 浮点容差)
|
||||
if not eq_flag:
|
||||
return False, f"缠论K线校验:标的K线子项异常 >> {msg}"
|
||||
|
||||
elif 字段 == "时间戳":
|
||||
if int(valA) != int(valB):
|
||||
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
elif 字段 == "方向":
|
||||
if str(valA) != str(valB):
|
||||
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
elif 字段 == "分型":
|
||||
if str(valA) != str(valB):
|
||||
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
elif 字段 == "买卖点信息":
|
||||
if set(valA) != set(valB):
|
||||
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"缠论K线校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
return True, "缠论K线:全部字段、嵌套原始K线校验一致"
|
||||
|
||||
|
||||
def 分型相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""分型:左/中/右缠论K线递归 + 自有字段"""
|
||||
比对字段 = ["左", "中", "右", "_结构", "_时间戳", "_分型特征值"]
|
||||
|
||||
for 字段 in 比对字段:
|
||||
a存在 = hasattr(A, 字段)
|
||||
b存在 = hasattr(B, 字段)
|
||||
if a存在 and not b存在:
|
||||
return False, f"分型校验:[{字段}]A有属性、B缺失"
|
||||
if not a存在 and b存在:
|
||||
return False, f"分型校验:[{字段}]B有属性、A缺失"
|
||||
if not (a存在 and b存在):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
|
||||
if isinstance(valA, float) and isinstance(valB, float):
|
||||
差值 = abs(valA - valB)
|
||||
if 差值 > 浮点容差:
|
||||
return False, f"分型校验:[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
|
||||
elif 字段 in ("左", "中", "右"):
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"分型校验:[{字段}]空值不一致,A={valA is None},B={valB is None}"
|
||||
eq_ok, msg = 缠论K线相等(valA, valB, 浮点容差)
|
||||
if not eq_ok:
|
||||
return False, f"分型校验:[{字段}]缠论K线子项异常 >> {msg}"
|
||||
|
||||
elif 字段 == "_结构":
|
||||
if str(valA) != str(valB):
|
||||
return False, f"分型K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
elif 字段 == "_时间戳":
|
||||
if int(valA) != int(valB):
|
||||
return False, f"分型K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"分型校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
return True, "分型:自有字段+三根缠论K线全部校验一致"
|
||||
|
||||
|
||||
@lru_cache(4096)
|
||||
def 缺口相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""缺口:高、低浮点校验"""
|
||||
比对字段 = ["高", "低"]
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"缺口校验:[{字段}]A存在、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"缺口校验:[{字段}]B存在、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
if isinstance(valA, float) and isinstance(valB, float):
|
||||
差值 = abs(valA - valB)
|
||||
if 差值 > 浮点容差:
|
||||
return False, f"缺口校验:[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"缺口校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
return True, "缺口:上下沿价格校验完全一致"
|
||||
|
||||
|
||||
def 线段特征相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""线段特征:基础序列虚线列表逐项校验"""
|
||||
比对字段 = ["序号", "标识", "线段方向", "基础序列"]
|
||||
标签 = f"线段特征校验[A标识={A.标识},B标识={B.标识}]"
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"{标签}: [{字段}]A存在、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"{标签}: [{字段}]B存在、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
if 字段 == "基础序列":
|
||||
if len(valA) != len(valB):
|
||||
return False, f"{标签}: [基础序列]列表长度不一致,A长度={len(valA)},B长度={len(valB)}"
|
||||
for idx, (itemA, itemB) in enumerate(zip(valA, valB)):
|
||||
eq, msg = 虚线相等(itemA, itemB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}:基础序列[{idx}]子虚线异常 >> {msg}"
|
||||
|
||||
elif 字段 == "线段方向":
|
||||
if str(valA) != str(valB):
|
||||
return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
return True, f"{标签}:字段与内部虚线序列全部一致"
|
||||
|
||||
|
||||
def 中枢相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""中枢:基础序列虚线列表+第三买卖线单虚线"""
|
||||
比对字段 = ["序号", "标识", "级别", "基础序列", "第三买卖线", "本级_第三买卖线"]
|
||||
标签 = f"中枢校验[A标识={A.标识},B标识={B.标识}]"
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"{标签}: [{字段}]A存在、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"{标签}: [{字段}]B存在、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
|
||||
if 字段 == "基础序列":
|
||||
if len(valA) != len(valB):
|
||||
return False, f"{标签}: [基础序列]长度不一致 A={len(valA)},B={len(valB)}"
|
||||
for idx, (itemA, itemB) in enumerate(zip(valA, valB)):
|
||||
eq, msg = 虚线相等(itemA, itemB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}:基础序列[{idx}]虚线异常 >> {msg}"
|
||||
elif 字段 in ("第三买卖线", "本级_第三买卖线"):
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"{标签}: [{字段}]空值不一致 A={valA is None},B={valB is None}"
|
||||
eq, msg = 虚线相等(valA, valB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [{字段}]子虚线异常 >> {msg}"
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
return True, f"{标签}:基础序列+第三买卖线全部校验一致"
|
||||
|
||||
|
||||
def 虚线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
|
||||
"""虚线(笔/线段):全量字段、分型/缺口/K线/列表嵌套精细化报错"""
|
||||
比对字段 = ["标识", "序号", "级别", "文", "武", "有效性", "基础序列", "特征序列", "实_中枢序列", "虚_中枢序列", "合_中枢序列", "确认K线", "模式", "_特征序列_显示", "前一缺口", "前一结束位置", "短路修正"]
|
||||
标签 = f"虚线校验[A标识={A.标识},B标识={B.标识}]"
|
||||
for 字段 in 比对字段:
|
||||
a有 = hasattr(A, 字段)
|
||||
b有 = hasattr(B, 字段)
|
||||
if a有 and not b有:
|
||||
return False, f"{标签}: [{字段}]A存在属性、B缺失属性"
|
||||
if not a有 and b有:
|
||||
return False, f"{标签}: [{字段}]B存在属性、A缺失属性"
|
||||
if not (a有 and b有):
|
||||
continue
|
||||
|
||||
valA = getattr(A, 字段)
|
||||
valB = getattr(B, 字段)
|
||||
|
||||
# 文/武:分型
|
||||
if 字段 in ("文", "武"):
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"{标签}: [{字段}]分型空值不一致 A={valA is None},B={valB is None}"
|
||||
eq, msg = 分型相等(valA, valB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [{字段}]子分型异常 >> {msg}"
|
||||
# 前一缺口
|
||||
elif 字段 == "前一缺口":
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"{标签}: [前一缺口]空值不一致 A={valA is None},B={valB is None}"
|
||||
eq, msg = 缺口相等(valA, valB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [前一缺口]子缺口异常 >> {msg}"
|
||||
# 前一缺口
|
||||
elif 字段 == "前一结束位置":
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"{标签}: [前一结束位置]空值不一致 A={valA is None},B={valB is None}"
|
||||
eq, msg = 虚线相等(valA, valB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [前一结束位置]异常 >> {msg}"
|
||||
# 确认K线
|
||||
elif 字段 == "确认K线":
|
||||
if valA is None and valB is None:
|
||||
continue
|
||||
if valA is None or valB is None:
|
||||
return False, f"{标签}: [确认K线]空值不一致 A={valA is None},B={valB is None}"
|
||||
eq, msg = 缠论K线相等(valA, valB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [确认K线]子缠论K线异常 >> {msg}"
|
||||
# 各类列表
|
||||
elif 字段 in ("基础序列", "实_中枢序列", "虚_中枢序列", "合_中枢序列", "特征序列"):
|
||||
if len(valA) != len(valB):
|
||||
return False, f"{标签}: [{字段}]列表长度不一致 A={len(valA)},B={len(valB)}"
|
||||
for idx, (itemA, itemB) in enumerate(zip(valA, valB)):
|
||||
if itemA is None and itemB is None:
|
||||
continue
|
||||
if itemA is None or itemB is None:
|
||||
return False, f"{标签}: [{字段}][{idx}]单项空值不一致 A={itemA is None},B={itemB is None}"
|
||||
if 字段 == "基础序列":
|
||||
eq, msg = 虚线相等(itemA, itemB, 浮点容差)
|
||||
elif "中枢" in 字段:
|
||||
eq, msg = 中枢相等(itemA, itemB, 浮点容差)
|
||||
else:
|
||||
eq, msg = 线段特征相等(itemA, itemB, 浮点容差)
|
||||
if not eq:
|
||||
return False, f"{标签}: [{字段}][{idx}]子项异常 >> {msg}"
|
||||
# 普通字段
|
||||
else:
|
||||
if valA != valB:
|
||||
return False, f"{标签}: [{字段}]数值不等 A={repr(valA)},B={repr(valB)}"
|
||||
|
||||
return True, f"{标签}:全字段、所有嵌套子结构校验全部一致"
|
||||
return 魔法
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
当前配置 = 缠论配置.不推送()
|
||||
当前配置.加载文件路径 = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "tests", "btcusd-300-1761327300-1776327900.nb")
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
测试_读取数据(观察者("", 0, 当前配置), 当前配置)().测试_保存数据(tmpdir)
|
||||
测试_周期合成(当前配置)().测试_保存数据(tmpdir)
|
||||
# 测试_读取数据(观察者("", 0, 当前配置), 当前配置)().测试_保存数据(tmpdir)
|
||||
# 测试_周期合成(当前配置)().测试_保存数据(tmpdir)
|
||||
测试_指标挂载(当前配置)().测试_保存数据(tmpdir)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "chanlun"
|
||||
version = "2606.34"
|
||||
version = "2606.42"
|
||||
description = "缠论技术分析库 — Rust 高性能实现"
|
||||
readme = { file = "README.md", content-type = "text/markdown" }
|
||||
license = { file = "LICENSE", content-type = "text/plain" }
|
||||
|
||||
@@ -693,6 +693,7 @@ impl 买卖点Py {
|
||||
#[pyclass(name = "观察者", module = "chanlun._chanlun", subclass)]
|
||||
pub struct 观察者Py {
|
||||
pub(crate) inner: Option<Arc<RwLock<chanlun::business::observer::观察者>>>,
|
||||
配置缓存: std::sync::Mutex<Option<Py<缠论配置Py>>>,
|
||||
}
|
||||
|
||||
impl 观察者Py {
|
||||
@@ -769,6 +770,7 @@ impl 观察者Py {
|
||||
inner: Some(chanlun::business::observer::观察者::new(
|
||||
符号, 周期, config,
|
||||
)),
|
||||
配置缓存: std::sync::Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -821,8 +823,27 @@ impl 观察者Py {
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 配置(&self) -> PyResult<缠论配置Py> {
|
||||
缠论配置Py::from_rust_config(&self.obs().配置)
|
||||
fn 配置(&self, py: Python<'_>) -> PyResult<Py<缠论配置Py>> {
|
||||
let mut cache = self.配置缓存.lock().unwrap();
|
||||
if let Some(ref cached) = *cache {
|
||||
Ok(cached.clone_ref(py))
|
||||
} else {
|
||||
let cfg_py = 缠论配置Py::from_rust_config(&self.obs().配置)?;
|
||||
let obj = Py::new(py, cfg_py)?;
|
||||
*cache = Some(obj.clone_ref(py));
|
||||
Ok(obj)
|
||||
}
|
||||
}
|
||||
|
||||
#[setter]
|
||||
fn set_配置(&self, value: &Bound<'_, 缠论配置Py>) -> PyResult<()> {
|
||||
let config = value.borrow().to_rust_config(value.py())?;
|
||||
self.obs_mut().配置 = config;
|
||||
self.配置缓存
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace(value.clone().unbind());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 清空所有分析序列,重置为初始状态(内部实现)
|
||||
@@ -847,6 +868,16 @@ impl 观察者Py {
|
||||
|
||||
/// 核心入口 — 投喂一根原始K线,增量更新所有层级(公开分发器,支持子类重写)
|
||||
fn 增加原始K线(slf: &Bound<'_, Self>, 普K: &Bound<'_, K线Py>) -> PyResult<()> {
|
||||
// 同步缓存的 Python 配置到 Rust 观察者(支持 obs.配置 直接修改)
|
||||
{
|
||||
let me = slf.borrow();
|
||||
if let Some(ref cached) = *me.配置缓存.lock().unwrap() {
|
||||
let py = slf.py();
|
||||
if let Ok(config) = cached.bind(py).borrow().to_rust_config(py) {
|
||||
me.obs_mut().配置 = config;
|
||||
}
|
||||
}
|
||||
}
|
||||
slf.call_method1("_增加原始K线", (普K,))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -925,16 +956,16 @@ impl 观察者Py {
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
#[pyo3(signature = (观察员, 文件路径, 配置 = None))]
|
||||
/// :param 观察员: 观察者实例
|
||||
#[pyo3(signature = (文件路径, 配置 = None, 观察员 = None))]
|
||||
/// :param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb
|
||||
/// :param 配置: 缠论配置
|
||||
/// :param 观察员: 可选,已有观察者实例;不传则自动创建
|
||||
/// :return: 观察者实例
|
||||
fn 读取数据文件(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
观察员: &Bound<'_, Self>,
|
||||
文件路径: &str,
|
||||
配置: Option<&Bound<'_, 缠论配置Py>>,
|
||||
观察员: Option<&Bound<'_, Self>>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
let config = match 配置 {
|
||||
@@ -960,19 +991,37 @@ impl 观察者Py {
|
||||
.parse()
|
||||
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("parse period: {}", e)))?;
|
||||
|
||||
// 设置观察员属性
|
||||
{
|
||||
let slf_ref = 观察员.borrow_mut();
|
||||
let mut obs_mut = slf_ref.obs_mut();
|
||||
obs_mut.符号 = 符号;
|
||||
obs_mut.周期 = 周期;
|
||||
obs_mut.配置 = config;
|
||||
}
|
||||
let obs_ref = match 观察员 {
|
||||
Some(obs) => {
|
||||
// 更新已有观察员属性
|
||||
{
|
||||
let slf_ref = obs.borrow_mut();
|
||||
let mut obs_mut = slf_ref.obs_mut();
|
||||
obs_mut.符号 = 符号;
|
||||
obs_mut.周期 = 周期;
|
||||
obs_mut.配置 = config;
|
||||
}
|
||||
obs.clone()
|
||||
}
|
||||
None => {
|
||||
// 创建新观察者:调用 cls(符号, 周期),配置后续通过 obs_mut 设置
|
||||
let obj = _cls.call1((符号.as_str(), 周期))?;
|
||||
let obs: &Bound<'_, Self> = obj.cast().map_err(|_| {
|
||||
pyo3::exceptions::PyTypeError::new_err("failed to create 观察者")
|
||||
})?;
|
||||
{
|
||||
let slf_ref = obs.borrow_mut();
|
||||
let mut obs_mut = slf_ref.obs_mut();
|
||||
obs_mut.配置 = config;
|
||||
}
|
||||
obs.clone()
|
||||
}
|
||||
};
|
||||
|
||||
// 调用加载本地数据
|
||||
观察员.call_method1("加载本地数据", (文件路径,))?;
|
||||
// 调用加载本地数据(通过 Python dispatch,支持子类重写)
|
||||
obs_ref.call_method1("加载本地数据", (文件路径,))?;
|
||||
|
||||
Ok(观察员.clone().unbind().into())
|
||||
Ok(obs_ref.unbind().into())
|
||||
}
|
||||
|
||||
// ---- 序列 getters ----
|
||||
@@ -1382,9 +1431,10 @@ impl 立体分析器Py {
|
||||
}
|
||||
|
||||
fn 获取观察者(&self, 周期: i64) -> Option<观察者Py> {
|
||||
self.inner
|
||||
.获取观察者(周期)
|
||||
.map(|rc| 观察者Py { inner: Some(rc) })
|
||||
self.inner.获取观察者(周期).map(|rc| 观察者Py {
|
||||
inner: Some(rc),
|
||||
配置缓存: std::sync::Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
/// 拆分各序列数据,单独存文件,文件名为对应变量名
|
||||
|
||||
@@ -414,6 +414,8 @@ fn validate_field(
|
||||
(Value::Bool(_), Value::Bool(_)) => return Ok(()),
|
||||
(Value::Number(_), Value::Number(_)) => return Ok(()),
|
||||
(Value::String(_), Value::String(_)) => return Ok(()),
|
||||
(Value::Array(_), Value::Array(_)) => return Ok(()),
|
||||
(Value::Object(_), Value::Object(_)) => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -430,7 +432,9 @@ fn validate_field(
|
||||
Value::Bool(_) => "布尔",
|
||||
Value::Number(_) => "数值",
|
||||
Value::String(_) => "字符串",
|
||||
_ => "其他",
|
||||
Value::Array(_) => "数组",
|
||||
Value::Object(_) => "字典",
|
||||
Value::Null => "null",
|
||||
};
|
||||
Err(format!("类型不匹配(需要 {expected},收到 {type_name})"))
|
||||
}
|
||||
|
||||
@@ -985,26 +985,17 @@ impl 指标计算器Py {
|
||||
/// 增量计算所有开启的指标,将结果写入 当前K线.指标
|
||||
#[staticmethod]
|
||||
fn 计算并挂载(
|
||||
当前K线: &Bound<'_, crate::kline_py::K线Py>,
|
||||
_当前K线: &Bound<'_, crate::kline_py::K线Py>,
|
||||
全序列: Vec<Py<crate::kline_py::K线Py>>,
|
||||
配置: &Bound<'_, crate::config_py::缠论配置Py>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<()> {
|
||||
let config = 配置.borrow().to_rust_config(py)?;
|
||||
// 全序列包含 当前K线 在末尾;Rust 计算并挂载 的 现有序列 不含当前K线
|
||||
let 现有序列: Vec<Arc<chanlun::kline::bar::K线>> = if 全序列.len() > 1 {
|
||||
全序列[..全序列.len() - 1]
|
||||
.iter()
|
||||
.map(|k| k.bind(py).borrow().inner.clone())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
chanlun::indicators::指标计算器::计算并挂载(
|
||||
&当前K线.borrow().inner,
|
||||
&现有序列,
|
||||
&config,
|
||||
);
|
||||
let 全序列_rust: Vec<Arc<chanlun::kline::bar::K线>> = 全序列
|
||||
.iter()
|
||||
.map(|k| k.bind(py).borrow().inner.clone())
|
||||
.collect();
|
||||
chanlun::indicators::指标计算器::计算并挂载(&全序列_rust, &config);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -35,9 +35,9 @@ _PROJECT_ROOT = os.environ.get(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||
)
|
||||
|
||||
NB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "btcusd-300-1761327300-1776327900.nb")
|
||||
_PY_REF_DIR = os.path.join(_PROJECT_ROOT, "Py_btcusd:300_1761327300_1776327900")
|
||||
_RUST_REF_DIR = os.path.join(_PROJECT_ROOT, "chanlun", "Rust_btcusd:300_1761327300_1776327900")
|
||||
NB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "btcusd-300-1777649100-1778398800.nb")
|
||||
_PY_REF_DIR = os.path.join(_PROJECT_ROOT, "Py_btcusd:300_1777649100_1778398800")
|
||||
_RUST_REF_DIR = os.path.join(_PROJECT_ROOT, "chanlun", "Rust_btcusd:300_1777649100_1778398800")
|
||||
|
||||
# ---- 辅助函数 ----
|
||||
|
||||
@@ -502,7 +502,8 @@ class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def 读取数据文件(cls, 文件路径, 配置=None):
|
||||
obs = super().读取数据文件(文件路径, 配置)
|
||||
obs = cls("", 0) # 创建子类实例,父类方法会覆盖符号/周期
|
||||
chanlun.观察者.读取数据文件(文件路径, 配置, 观察员=obs)
|
||||
obs._custom_classmethod_flag = True
|
||||
return obs
|
||||
|
||||
@@ -519,7 +520,7 @@ class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase):
|
||||
obs.重置基础序列()
|
||||
obs.加载本地数据(NB_PATH)
|
||||
self.assertTrue(obs._loaded)
|
||||
self.assertEqual(obs._load_count, 1)
|
||||
self.assertEqual(obs._load_count, 2)
|
||||
self.assertGreater(len(obs.普通K线序列), 0)
|
||||
c = datetime.now()
|
||||
print("加载本地数据 用时:", c - b)
|
||||
@@ -542,7 +543,7 @@ class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase):
|
||||
self.assertEqual(obs._save_root, tmpdir)
|
||||
|
||||
# 5. 重置次数
|
||||
self.assertEqual(obs._reload, 3)
|
||||
self.assertEqual(obs._reload, 6)
|
||||
e = datetime.now()
|
||||
print("保存数据 用时:", e - d)
|
||||
|
||||
@@ -1048,6 +1049,730 @@ class Test整体身份(_Base身份, unittest.TestCase):
|
||||
break
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 跨线程身份测试 — 验证全局缓存(非 thread_local)的跨线程一致性
|
||||
# ============================================================
|
||||
|
||||
|
||||
class Test跨线程身份(unittest.TestCase):
|
||||
"""跨线程 RC 身份一致性:全局缓存应在不同线程间共享同一 Python 对象."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.obs = create_observer(period=3600, n_bars=800)
|
||||
|
||||
def _run_in_thread(self, fn):
|
||||
"""在子线程中执行 fn,通过 queue 收集结果和异常."""
|
||||
import threading
|
||||
|
||||
result = []
|
||||
err = []
|
||||
|
||||
def wrapper():
|
||||
try:
|
||||
result.append(fn())
|
||||
except Exception as e:
|
||||
err.append(e)
|
||||
|
||||
t = threading.Thread(target=wrapper)
|
||||
t.start()
|
||||
t.join()
|
||||
if err:
|
||||
raise err[0]
|
||||
return result[0]
|
||||
|
||||
# ---- 序列级别 ----
|
||||
|
||||
def test_缠K序列跨线程重复获取_is一致(self):
|
||||
"""缠论K线序列:从子线程重复获取,元素 is 一致."""
|
||||
obs = self.obs
|
||||
|
||||
def check():
|
||||
s1 = obs.缠论K线序列
|
||||
s2 = obs.缠论K线序列
|
||||
return [(s1[i] is s2[i], len(s1), len(s2)) for i in range(min(len(s1), len(s2), 20))]
|
||||
|
||||
results = self._run_in_thread(check)
|
||||
for i, (ok, l1, l2) in enumerate(results):
|
||||
self.assertTrue(ok, f"缠K序列[{i}] 跨线程 is 不一致")
|
||||
|
||||
def test_分型序列跨线程重复获取_is一致(self):
|
||||
"""分型序列:从子线程重复获取,元素 is 一致."""
|
||||
obs = self.obs
|
||||
|
||||
def check():
|
||||
s1 = obs.分型序列
|
||||
s2 = obs.分型序列
|
||||
return [(s1[i] is s2[i], len(s1)) for i in range(min(len(s1), len(s2), 20))]
|
||||
|
||||
results = self._run_in_thread(check)
|
||||
for i, (ok, _) in enumerate(results):
|
||||
self.assertTrue(ok, f"分型序列[{i}] 跨线程 is 不一致")
|
||||
|
||||
def test_笔序列跨线程重复获取_is一致(self):
|
||||
"""笔序列:从子线程重复获取,元素 is 一致."""
|
||||
obs = self.obs
|
||||
|
||||
def check():
|
||||
s1 = obs.笔序列
|
||||
s2 = obs.笔序列
|
||||
return [(s1[i] is s2[i], len(s1)) for i in range(min(len(s1), len(s2), 20))]
|
||||
|
||||
results = self._run_in_thread(check)
|
||||
for i, (ok, _) in enumerate(results):
|
||||
self.assertTrue(ok, f"笔序列[{i}] 跨线程 is 不一致")
|
||||
|
||||
def test_线段序列跨线程重复获取_is一致(self):
|
||||
"""线段序列:从子线程重复获取,元素 is 一致."""
|
||||
obs = self.obs
|
||||
|
||||
def check():
|
||||
s1 = obs.线段序列
|
||||
s2 = obs.线段序列
|
||||
return [(s1[i] is s2[i], len(s1)) for i in range(min(len(s1), len(s2), 20))]
|
||||
|
||||
results = self._run_in_thread(check)
|
||||
for i, (ok, _) in enumerate(results):
|
||||
self.assertTrue(ok, f"线段序列[{i}] 跨线程 is 不一致")
|
||||
|
||||
def test_中枢序列跨线程重复获取_is一致(self):
|
||||
"""中枢序列:从子线程重复获取,元素 is 一致."""
|
||||
obs = self.obs
|
||||
|
||||
def check():
|
||||
s1 = obs.中枢序列
|
||||
s2 = obs.中枢序列
|
||||
return [(s1[i] is s2[i], len(s1)) for i in range(min(len(s1), len(s2), 20))]
|
||||
|
||||
results = self._run_in_thread(check)
|
||||
for i, (ok, _) in enumerate(results):
|
||||
self.assertTrue(ok, f"中枢序列[{i}] 跨线程 is 不一致")
|
||||
|
||||
def test_普K序列跨线程重复获取_is一致(self):
|
||||
"""普通K线序列:从子线程重复获取,元素 is 一致."""
|
||||
obs = self.obs
|
||||
|
||||
def check():
|
||||
s1 = obs.普通K线序列
|
||||
s2 = obs.普通K线序列
|
||||
return [(s1[i] is s2[i], len(s1)) for i in range(min(len(s1), len(s2), 20))]
|
||||
|
||||
results = self._run_in_thread(check)
|
||||
for i, (ok, _) in enumerate(results):
|
||||
self.assertTrue(ok, f"普K序列[{i}] 跨线程 is 不一致")
|
||||
|
||||
# ---- 跨路径 ----
|
||||
|
||||
def test_跨线程分型中K线_is一致(self):
|
||||
"""子线程中 分型.中 is 缠论K线序列[同时间戳]."""
|
||||
obs = self.obs
|
||||
|
||||
def check():
|
||||
results = []
|
||||
seq = obs.缠论K线序列
|
||||
for fx in obs.分型序列[:10]:
|
||||
中 = fx.中
|
||||
found = False
|
||||
for ck in seq:
|
||||
if ck.时间戳 == 中.时间戳:
|
||||
results.append((ck is 中, ck.时间戳))
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
results.append((None, 中.时间戳))
|
||||
return results
|
||||
|
||||
results = self._run_in_thread(check)
|
||||
for ok, ts in results:
|
||||
self.assertIsNotNone(ok, f"分型.中 ts={ts} 在缠K序列中未找到")
|
||||
self.assertTrue(ok, f"跨线程 分型.中 ts={ts} is 不一致")
|
||||
|
||||
def test_跨线程笔端点钟K_is一致(self):
|
||||
"""子线程中 笔.文中 is 缠论K线序列[同时间戳]."""
|
||||
obs = self.obs
|
||||
|
||||
def check():
|
||||
results = []
|
||||
seq = obs.缠论K线序列
|
||||
for bi in obs.笔序列[:10]:
|
||||
for nm, ep in [("文", bi.文), ("武", bi.武)]:
|
||||
if ep is None:
|
||||
continue
|
||||
中 = ep.中
|
||||
for ck in seq:
|
||||
if ck.时间戳 == 中.时间戳:
|
||||
results.append((ck is 中, nm, ck.时间戳))
|
||||
break
|
||||
return results
|
||||
|
||||
results = self._run_in_thread(check)
|
||||
for ok, nm, ts in results:
|
||||
self.assertTrue(ok, f"跨线程 笔.{nm}.中 ts={ts} is 不一致")
|
||||
|
||||
def test_跨线程中枢元件_is一致(self):
|
||||
"""子线程中 中枢.元件 中的虚线对象 is 线段序列[同索引]."""
|
||||
obs = self.obs
|
||||
|
||||
def check():
|
||||
results = []
|
||||
for zs in obs.中枢序列[:5]:
|
||||
for elem in zs.元件[:3]:
|
||||
results.append(elem is elem) # 自我 is
|
||||
results.append(elem is not None)
|
||||
return results
|
||||
|
||||
results = self._run_in_thread(check)
|
||||
for ok in results:
|
||||
self.assertTrue(ok)
|
||||
|
||||
# ---- list.index 基于 is ----
|
||||
|
||||
def test_跨线程list_index基于身份(self):
|
||||
"""子线程中 list.index(elem) 正常工作(依赖 __eq__ 基于 is)."""
|
||||
obs = self.obs
|
||||
|
||||
def check():
|
||||
results = []
|
||||
for name, getter in [
|
||||
("缠论K线序列", lambda o: o.缠论K线序列),
|
||||
("分型序列", lambda o: o.分型序列),
|
||||
("笔序列", lambda o: o.笔序列),
|
||||
("线段序列", lambda o: o.线段序列),
|
||||
("中枢序列", lambda o: o.中枢序列),
|
||||
]:
|
||||
seq = getter(obs)
|
||||
if len(seq) >= 2:
|
||||
r0 = seq.index(seq[0]) == 0
|
||||
r1 = seq.index(seq[-1]) == len(seq) - 1
|
||||
results.append((name, r0, r1, len(seq)))
|
||||
return results
|
||||
|
||||
results = self._run_in_thread(check)
|
||||
for name, r0, r1, length in results:
|
||||
self.assertTrue(r0, f"跨线程 {name}(len={length}): index(seq[0]) != 0")
|
||||
self.assertTrue(r1, f"跨线程 {name}(len={length}): index(seq[-1]) != {length - 1}")
|
||||
|
||||
# ---- 主线程-子线程之间 ----
|
||||
|
||||
def test_主线程与子线程对象_is一致(self):
|
||||
"""主线程获取的对象与子线程获取的对象 is 相同."""
|
||||
import threading
|
||||
|
||||
obs = self.obs
|
||||
|
||||
# 主线程先获取
|
||||
seq_main = obs.缠论K线序列
|
||||
fx_main = obs.分型序列
|
||||
bi_main = obs.笔序列
|
||||
|
||||
err = []
|
||||
|
||||
def check():
|
||||
try:
|
||||
seq_thread = obs.缠论K线序列
|
||||
for i in range(min(len(seq_main), 10)):
|
||||
if seq_main[i] is not seq_thread[i]:
|
||||
raise AssertionError(f"缠K[{i}] 主线程与子线程 is 不一致")
|
||||
fx_thread = obs.分型序列
|
||||
for i in range(min(len(fx_main), 10)):
|
||||
if fx_main[i] is not fx_thread[i]:
|
||||
raise AssertionError(f"分型[{i}] 主线程与子线程 is 不一致")
|
||||
bi_thread = obs.笔序列
|
||||
for i in range(min(len(bi_main), 10)):
|
||||
if bi_main[i] is not bi_thread[i]:
|
||||
raise AssertionError(f"笔[{i}] 主线程与子线程 is 不一致")
|
||||
except Exception as e:
|
||||
err.append(e)
|
||||
|
||||
t = threading.Thread(target=check)
|
||||
t.start()
|
||||
t.join()
|
||||
if err:
|
||||
raise err[0]
|
||||
|
||||
def test_跨线程getter稳定性(self):
|
||||
"""子线程中同一 getter 多次调用返回同一对象(如 分型.结构 等)."""
|
||||
obs = self.obs
|
||||
|
||||
def check():
|
||||
results = []
|
||||
if obs.分型序列:
|
||||
fx = obs.分型序列[0]
|
||||
for name, getter in [
|
||||
("分型.结构", lambda f: f.结构),
|
||||
("分型.左", lambda f: f.左),
|
||||
("分型.中", lambda f: f.中),
|
||||
("分型.右", lambda f: f.右),
|
||||
]:
|
||||
v1 = getter(fx)
|
||||
v2 = getter(fx)
|
||||
results.append((name, v1 is v2 if v1 is not None else True))
|
||||
return results
|
||||
|
||||
results = self._run_in_thread(check)
|
||||
for name, ok in results:
|
||||
self.assertTrue(ok, f"跨线程 {name}: 两次调用 is 不一致")
|
||||
|
||||
def test_多线程并发访问_is一致(self):
|
||||
"""两个子线程同时访问,各自拿到的对象与主线程 is 一致."""
|
||||
import threading
|
||||
|
||||
obs = self.obs
|
||||
errors = []
|
||||
|
||||
seq_main = obs.笔序列
|
||||
|
||||
def worker(thread_id):
|
||||
try:
|
||||
seq = obs.笔序列
|
||||
for i in range(min(len(seq), 10)):
|
||||
if seq[i] is not seq_main[i]:
|
||||
errors.append(f"线程{thread_id} 笔[{i}] is 不一致")
|
||||
if seq[i] is not seq[i]:
|
||||
errors.append(f"线程{thread_id} 笔[{i}] 自我 is 失败")
|
||||
except Exception as e:
|
||||
errors.append(f"线程{thread_id}: {e}")
|
||||
|
||||
t1 = threading.Thread(target=worker, args=(1,))
|
||||
t2 = threading.Thread(target=worker, args=(2,))
|
||||
t1.start()
|
||||
t2.start()
|
||||
t1.join()
|
||||
t2.join()
|
||||
|
||||
self.assertEqual(len(errors), 0, "\n".join(errors))
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 买卖意义 双端一致性测试
|
||||
# ============================================================
|
||||
|
||||
|
||||
class Test买卖意义双端对比(unittest.TestCase):
|
||||
"""运行时对比 Rust 绑定层 与 chan.py 的 虚线.买卖意义() 结果."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not _has_nb():
|
||||
raise unittest.SkipTest("需要 .nb 数据文件")
|
||||
cls.bars = read_nb_bars(NB_PATH)
|
||||
|
||||
def _build_observers(self, n_bars=2000):
|
||||
"""构建双端观察者并喂入相同数据."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
cfg_rs = chanlun.缠论配置()
|
||||
obs_rs = chanlun.观察者("btcusd", 300, cfg_rs)
|
||||
|
||||
cfg_py = chan.缠论配置()
|
||||
obs_py = chan.观察者("btcusd", 300, cfg_py)
|
||||
|
||||
for i, (ts, o, h, l, c, v) in enumerate(self.bars[:n_bars]):
|
||||
k_rs = chanlun.K线.创建普K(f"k{i}", ts, o, h, l, c, v, i, 300)
|
||||
k_py = chan.K线.创建普K(f"k{i}", ts, o, h, l, c, v, i, 300)
|
||||
obs_rs.增加原始K线(k_rs)
|
||||
obs_py.增加原始K线(k_py)
|
||||
|
||||
return obs_rs, obs_py
|
||||
|
||||
def test_笔买卖意义双端一致(self):
|
||||
"""笔序列的 买卖意义() 双端结果完全一致."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
n = min(len(obs_rs.笔序列), len(obs_py.笔序列))
|
||||
self.assertGreater(n, 0, "笔序列为空")
|
||||
|
||||
mismatches = []
|
||||
for i in range(n):
|
||||
r = chanlun.虚线.买卖意义(obs_rs.笔序列[i], obs_rs)
|
||||
p = chan.虚线.买卖意义(obs_py.笔序列[i], obs_py)
|
||||
if r != p:
|
||||
mismatches.append((i, r, p, obs_rs.笔序列[i].获取数据文本(), obs_py.笔序列[i].获取数据文本()))
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"笔买卖意义 不一致 ({len(mismatches)}/{n}):\n" + "\n".join(f" [{i}] R={r} P={p}\n R文本={rt}\n P文本={pt}" for i, r, p, rt, pt in mismatches[:3]))
|
||||
|
||||
def test_线段买卖意义双端一致(self):
|
||||
"""线段序列的 买卖意义() 双端结果完全一致."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
n = min(len(obs_rs.线段序列), len(obs_py.线段序列))
|
||||
self.assertGreater(n, 0, "线段序列为空")
|
||||
|
||||
mismatches = []
|
||||
for i in range(n):
|
||||
r = chanlun.虚线.买卖意义(obs_rs.线段序列[i], obs_rs)
|
||||
p = chan.虚线.买卖意义(obs_py.线段序列[i], obs_py)
|
||||
if r != p:
|
||||
mismatches.append((i, r, p, obs_rs.线段序列[i].获取数据文本(), obs_py.线段序列[i].获取数据文本()))
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"线段买卖意义 不一致 ({len(mismatches)}/{n}):\n" + "\n".join(f" [{i}] R={r} P={p}\n R文本={rt}\n P文本={pt}" for i, r, p, rt, pt in mismatches[:3]))
|
||||
|
||||
def test_笔序列长度一致(self):
|
||||
"""双端笔序列数量一致."""
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
self.assertGreater(len(obs_rs.笔序列), 0)
|
||||
self.assertEqual(len(obs_rs.笔序列), len(obs_py.笔序列), f"笔数量: Rust={len(obs_rs.笔序列)} Py={len(obs_py.笔序列)}")
|
||||
|
||||
def test_线段序列长度一致(self):
|
||||
"""双端线段序列数量一致."""
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
self.assertGreater(len(obs_rs.线段序列), 0)
|
||||
self.assertEqual(len(obs_rs.线段序列), len(obs_py.线段序列), f"线段数量: Rust={len(obs_rs.线段序列)} Py={len(obs_py.线段序列)}")
|
||||
|
||||
def test_中枢序列长度一致(self):
|
||||
"""双端中枢序列数量一致."""
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
self.assertGreater(len(obs_rs.中枢序列), 0)
|
||||
self.assertEqual(len(obs_rs.中枢序列), len(obs_py.中枢序列), f"中枢数量: Rust={len(obs_rs.中枢序列)} Py={len(obs_py.中枢序列)}")
|
||||
|
||||
# ---- MACD趋向背驰 ----
|
||||
|
||||
def test_笔MACD趋向背驰双端一致(self):
|
||||
"""笔的 K线序列 MACD趋向背驰 双端结果完全一致."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
mismatches = []
|
||||
for bi_rs, bi_py in zip(obs_rs.笔序列, obs_py.笔序列):
|
||||
k_seq_rs = chanlun.K线.截取(
|
||||
obs_rs.普通K线序列,
|
||||
bi_rs.文.中.标的K线,
|
||||
bi_rs.武.中.标的K线,
|
||||
)
|
||||
k_seq_py = chan.K线.截取(
|
||||
obs_py.普通K线序列,
|
||||
bi_py.文.中.标的K线,
|
||||
bi_py.武.中.标的K线,
|
||||
)
|
||||
r = chanlun.虚线.计算K线序列MACD趋向背驰(k_seq_rs, bi_rs.方向)
|
||||
p = chan.虚线.计算K线序列MACD趋向背驰(k_seq_py, bi_py.方向)
|
||||
if r != list(p):
|
||||
mismatches.append((bi_rs.文.时间戳, bi_rs.武.时间戳, list(r), list(p)))
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"笔MACD趋向背驰 不一致 ({len(mismatches)}):\n" + "\n".join(f" [{ts_w},{ts_wu}] R={r} P={p}" for ts_w, ts_wu, r, p in mismatches[:5]))
|
||||
|
||||
def test_线段MACD趋向背驰双端一致(self):
|
||||
"""线段的 K线序列 MACD趋向背驰 双端结果完全一致."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
mismatches = []
|
||||
for seg_rs, seg_py in zip(obs_rs.线段序列, obs_py.线段序列):
|
||||
k_seq_rs = chanlun.K线.截取(
|
||||
obs_rs.普通K线序列,
|
||||
seg_rs.文.中.标的K线,
|
||||
seg_rs.武.中.标的K线,
|
||||
)
|
||||
k_seq_py = chan.K线.截取(
|
||||
obs_py.普通K线序列,
|
||||
seg_py.文.中.标的K线,
|
||||
seg_py.武.中.标的K线,
|
||||
)
|
||||
r = chanlun.虚线.计算K线序列MACD趋向背驰(k_seq_rs, seg_rs.方向)
|
||||
p = chan.虚线.计算K线序列MACD趋向背驰(k_seq_py, seg_py.方向)
|
||||
if r != list(p):
|
||||
mismatches.append((seg_rs.文.时间戳, seg_rs.武.时间戳, list(r), list(p)))
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"线段MACD趋向背驰 不一致 ({len(mismatches)}):\n" + "\n".join(f" [{ts_w},{ts_wu}] R={r} P={p}" for ts_w, ts_wu, r, p in mismatches[:5]))
|
||||
|
||||
# ---- 统计MACD行为 ----
|
||||
|
||||
def test_笔统计MACD行为双端一致(self):
|
||||
"""笔的 统计MACD行为() 双端结果完全一致."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
mismatches = []
|
||||
for bi_rs, bi_py in zip(obs_rs.笔序列, obs_py.笔序列):
|
||||
k_seq_rs = chanlun.K线.截取(
|
||||
obs_rs.普通K线序列,
|
||||
bi_rs.文.中.标的K线,
|
||||
bi_rs.武.中.标的K线,
|
||||
)
|
||||
k_seq_py = chan.K线.截取(
|
||||
obs_py.普通K线序列,
|
||||
bi_py.文.中.标的K线,
|
||||
bi_py.武.中.标的K线,
|
||||
)
|
||||
r = chanlun.虚线.统计MACD行为(k_seq_rs)
|
||||
p = chan.虚线.统计MACD行为(k_seq_py)
|
||||
if r != p:
|
||||
mismatches.append((bi_rs.文.时间戳, bi_rs.武.时间戳, r, p))
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"笔统计MACD行为 不一致 ({len(mismatches)}):\n" + "\n".join(f" [{ts_w},{ts_wu}] R={r} P={p}" for ts_w, ts_wu, r, p in mismatches[:5]))
|
||||
|
||||
def test_线段统计MACD行为双端一致(self):
|
||||
"""线段的 统计MACD行为() 双端结果完全一致."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
mismatches = []
|
||||
for seg_rs, seg_py in zip(obs_rs.线段序列, obs_py.线段序列):
|
||||
k_seq_rs = chanlun.K线.截取(
|
||||
obs_rs.普通K线序列,
|
||||
seg_rs.文.中.标的K线,
|
||||
seg_rs.武.中.标的K线,
|
||||
)
|
||||
k_seq_py = chan.K线.截取(
|
||||
obs_py.普通K线序列,
|
||||
seg_py.文.中.标的K线,
|
||||
seg_py.武.中.标的K线,
|
||||
)
|
||||
r = chanlun.虚线.统计MACD行为(k_seq_rs)
|
||||
p = chan.虚线.统计MACD行为(k_seq_py)
|
||||
if r != p:
|
||||
mismatches.append((seg_rs.文.时间戳, seg_rs.武.时间戳, r, p))
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"线段统计MACD行为 不一致 ({len(mismatches)}):\n" + "\n".join(f" [{ts_w},{ts_wu}] R={r} P={p}" for ts_w, ts_wu, r, p in mismatches[:5]))
|
||||
|
||||
# ---- 获取所有停顿位置 ----
|
||||
|
||||
def test_笔获取所有停顿位置双端一致(self):
|
||||
"""笔的 获取所有停顿位置() 双端结果一致(通过虚线相等 逐项比对)."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
mismatches = []
|
||||
for bi_rs, bi_py in zip(obs_rs.笔序列, obs_py.笔序列):
|
||||
r = chanlun.笔.获取所有停顿位置(bi_rs, obs_rs)
|
||||
p = chan.笔.获取所有停顿位置(bi_py, obs_py)
|
||||
if len(r) != len(p):
|
||||
mismatches.append((f"len R={len(r)} P={len(p)}", [x.获取数据文本() for x in r], [x.获取数据文本() for x in p]))
|
||||
else:
|
||||
for a, b in zip(r, p):
|
||||
eq, msg = chanlun.虚线相等(a, b)
|
||||
if not eq:
|
||||
mismatches.append((msg, [x.获取数据文本() for x in r], [x.获取数据文本() for x in p]))
|
||||
break
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"笔获取所有停顿位置 不一致 ({len(mismatches)}):\n" + "\n".join(f" {tag}\n R={rl}\n P={pl}" for tag, rl, pl in mismatches[:3]))
|
||||
|
||||
def test_线段获取所有停顿位置双端一致(self):
|
||||
"""线段的 获取所有停顿位置() 双端结果一致(通过虚线相等 逐项比对)."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
mismatches = []
|
||||
for seg_rs, seg_py in zip(obs_rs.线段序列, obs_py.线段序列):
|
||||
r = chanlun.线段.获取所有停顿位置(seg_rs, obs_rs)
|
||||
p = chan.线段.获取所有停顿位置(seg_py, obs_py)
|
||||
if len(r) != len(p):
|
||||
mismatches.append((f"len R={len(r)} P={len(p)}", [x.获取数据文本() for x in r], [x.获取数据文本() for x in p]))
|
||||
else:
|
||||
for a, b in zip(r, p):
|
||||
eq, msg = chanlun.虚线相等(a, b)
|
||||
if not eq:
|
||||
mismatches.append((msg, [x.获取数据文本() for x in r], [x.获取数据文本() for x in p]))
|
||||
break
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"线段获取所有停顿位置 不一致 ({len(mismatches)}):\n" + "\n".join(f" {tag}\n R={rl}\n P={pl}" for tag, rl, pl in mismatches[:3]))
|
||||
|
||||
# ---- 判断线段内部是否背驰 ----
|
||||
|
||||
def test_判断线段内部是否背驰双端一致(self):
|
||||
"""线段的 判断线段内部是否背驰() 双端结果完全一致."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
n = min(len(obs_rs.线段序列), len(obs_py.线段序列))
|
||||
self.assertGreater(n, 0, "线段序列为空")
|
||||
|
||||
mismatches = []
|
||||
for i in range(n):
|
||||
r = chanlun.线段.判断线段内部是否背驰(obs_rs.线段序列[i], obs_rs)
|
||||
p = chan.线段.判断线段内部是否背驰(obs_py.线段序列[i], obs_py)
|
||||
if r != p:
|
||||
mismatches.append((i, r, p, obs_rs.线段序列[i].获取数据文本(), obs_py.线段序列[i].获取数据文本()))
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"判断线段内部是否背驰 不一致 ({len(mismatches)}/{n}):\n" + "\n".join(f" [{i}] R={r} P={p}\n R文本={rt}\n P文本={pt}" for i, r, p, rt, pt in mismatches[:3]))
|
||||
|
||||
# ---- 是否背驰过 ----
|
||||
|
||||
# ---- 获取内部中枢序列 ----
|
||||
|
||||
def test_线段获取内部中枢序列双端一致(self):
|
||||
"""线段的 获取内部中枢序列() 双端结果完全一致(通过 中枢相等 逐项比对)."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
n = min(len(obs_rs.线段序列), len(obs_py.线段序列))
|
||||
self.assertGreater(n, 0, "线段序列为空")
|
||||
|
||||
mismatches = []
|
||||
for i in range(n):
|
||||
seg_rs = obs_rs.线段序列[i]
|
||||
seg_py = obs_py.线段序列[i]
|
||||
r = chanlun.线段.获取内部中枢序列(seg_rs, obs_rs.配置)
|
||||
p = chan.线段.获取内部中枢序列(seg_py, obs_py.配置)
|
||||
if len(r) != len(p):
|
||||
mismatches.append((i, f"tuple len R={len(r)} P={len(p)}"))
|
||||
else:
|
||||
for k, (hr, hp) in enumerate(zip(r, p)):
|
||||
if len(hr) != len(hp):
|
||||
mismatches.append((i, f"{['实', '虚', '合'][k]} len R={len(hr)} P={len(hp)}"))
|
||||
break
|
||||
for j, (ha, hb) in enumerate(zip(hr, hp)):
|
||||
eq, msg = chanlun.中枢相等(ha, hb)
|
||||
if not eq:
|
||||
mismatches.append((i, f"{['实', '虚', '合'][k]}[{j}]: {msg}"))
|
||||
break
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"线段获取内部中枢序列 不一致 ({len(mismatches)}/{n}):\n" + "\n".join(f" Seg[{i}]: {detail}" for i, detail in mismatches[:5]))
|
||||
|
||||
# ---- 是否背驰过 ----
|
||||
|
||||
def test_线段是否背驰过双端一致(self):
|
||||
"""线段的 是否背驰过() 双端结果一致(通过 缠论K线相等 逐项比对)."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._build_observers()
|
||||
n = min(len(obs_rs.线段序列), len(obs_py.线段序列))
|
||||
self.assertGreater(n, 0, "线段序列为空")
|
||||
|
||||
mismatches = []
|
||||
for i in range(n):
|
||||
r = chanlun.线段.是否背驰过(obs_rs.线段序列[i], obs_rs)
|
||||
p = chan.线段.是否背驰过(obs_py.线段序列[i], obs_py)
|
||||
if len(r) != len(p):
|
||||
mismatches.append((i, f"len R={len(r)} P={len(p)}", obs_rs.线段序列[i].获取数据文本()))
|
||||
else:
|
||||
for a, b in zip(r, p):
|
||||
eq, msg = chanlun.缠论K线相等(a, b)
|
||||
if not eq:
|
||||
mismatches.append((i, msg, obs_rs.线段序列[i].获取数据文本()))
|
||||
break
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"线段是否背驰过 不一致 ({len(mismatches)}/{n}):\n" + "\n".join(f" [{i}] {detail}\n 段={txt[:120]}" for i, detail, txt in mismatches[:3]))
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 指标挂载测试
|
||||
# ============================================================
|
||||
|
||||
|
||||
class Test指标挂载(unittest.TestCase):
|
||||
"""指标计算与动态挂载回填测试."""
|
||||
|
||||
@staticmethod
|
||||
def _make_k(i: int, ts_base: int = 1771675200, period: int = 300):
|
||||
"""创建一根模拟K线."""
|
||||
return chanlun.K线.创建普K(
|
||||
"btcusd",
|
||||
ts_base + i * period,
|
||||
50000.0 + i,
|
||||
51000.0 + i,
|
||||
49000.0 + i,
|
||||
50500.0 + i,
|
||||
100.0 + i,
|
||||
i,
|
||||
period,
|
||||
)
|
||||
|
||||
def test_基本指标计算(self):
|
||||
"""每根K线都应有默认指标值."""
|
||||
cfg = chanlun.缠论配置()
|
||||
obs = chanlun.观察者("btcusd", 300, cfg)
|
||||
for i in range(50):
|
||||
obs.增加原始K线(self._make_k(i))
|
||||
|
||||
for k in obs.普通K线序列:
|
||||
self.assertIn("macd", k.指标)
|
||||
self.assertIn("rsi", k.指标)
|
||||
self.assertIn("kdj", k.指标)
|
||||
|
||||
def test_动态MACD参数回填(self):
|
||||
"""中途修改 obs.配置 添加 MACD 变体后,历史K线应被回填."""
|
||||
cfg = chanlun.缠论配置()
|
||||
obs = chanlun.观察者("btcusd", 300, cfg)
|
||||
|
||||
for i in range(100):
|
||||
if i == 50:
|
||||
obs.配置.MACD_参数列表 = [
|
||||
("macd", 12, 26, 9),
|
||||
("macd_10_20_7", 10, 20, 7),
|
||||
]
|
||||
obs.增加原始K线(self._make_k(i))
|
||||
|
||||
for k in obs.普通K线序列:
|
||||
self.assertIn("macd_10_20_7", k.指标)
|
||||
|
||||
def test_多指标同时回填(self):
|
||||
"""同时修改 MACD + RSI + KDJ 参数,验证全部回填."""
|
||||
cfg = chanlun.缠论配置()
|
||||
obs = chanlun.观察者("btcusd", 300, cfg)
|
||||
|
||||
for i in range(80):
|
||||
if i == 40:
|
||||
obs.配置.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_fast", 5, 13, 5)]
|
||||
obs.配置.RSI_周期列表 = [("rsi", 14), ("rsi_7", 7)]
|
||||
obs.配置.KDJ_参数列表 = [("kdj", 9, 3, 3), ("kdj_5", 5, 2, 2)]
|
||||
obs.增加原始K线(self._make_k(i))
|
||||
|
||||
for k in obs.普通K线序列:
|
||||
self.assertIn("macd_fast", k.指标)
|
||||
self.assertIn("rsi_7", k.指标)
|
||||
self.assertIn("kdj_5", k.指标)
|
||||
|
||||
def test_回填后增量计算一致(self):
|
||||
"""回填后的指标值应与从头计算一致."""
|
||||
cfg_full = chanlun.缠论配置()
|
||||
cfg_full.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_extra", 8, 16, 6)]
|
||||
obs_full = chanlun.观察者("btcusd", 300, cfg_full)
|
||||
|
||||
cfg_late = chanlun.缠论配置()
|
||||
obs_late = chanlun.观察者("btcusd", 300, cfg_late)
|
||||
|
||||
for i in range(100):
|
||||
if i == 50:
|
||||
obs_late.配置.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_extra", 8, 16, 6)]
|
||||
obs_full.增加原始K线(self._make_k(i))
|
||||
obs_late.增加原始K线(self._make_k(i))
|
||||
|
||||
seq_full = obs_full.普通K线序列
|
||||
seq_late = obs_late.普通K线序列
|
||||
self.assertEqual(len(seq_full), len(seq_late))
|
||||
|
||||
for i in range(len(seq_full)):
|
||||
macd_full = seq_full[i].指标["macd_extra"]
|
||||
macd_late = seq_late[i].指标["macd_extra"]
|
||||
self.assertEqual(macd_full.DIF, macd_late.DIF)
|
||||
self.assertEqual(macd_full.DEA, macd_late.DEA)
|
||||
|
||||
def test_同时间戳更新后指标重算(self):
|
||||
"""同时间戳K线更新后,指标应基于新值重新计算."""
|
||||
cfg = chanlun.缠论配置()
|
||||
obs = chanlun.观察者("btcusd", 300, cfg)
|
||||
|
||||
ts = 1771675200
|
||||
k1 = chanlun.K线.创建普K("btcusd", ts, 50000, 51000, 49000, 50500, 100, 0, 300)
|
||||
obs.增加原始K线(k1)
|
||||
|
||||
k2 = chanlun.K线.创建普K("btcusd", ts + 300, 50500, 52000, 50000, 51500, 200, 1, 300)
|
||||
obs.增加原始K线(k2)
|
||||
macd_before = obs.普通K线序列[-1].指标["macd"].DIF
|
||||
|
||||
# 同时间戳,不同收盘价
|
||||
k2_upd = chanlun.K线.创建普K("btcusd", ts + 300, 50500, 53000, 49000, 52500, 300, 1, 300)
|
||||
obs.增加原始K线(k2_upd)
|
||||
macd_after = obs.普通K线序列[-1].指标["macd"].DIF
|
||||
|
||||
self.assertNotEqual(macd_before, macd_after)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 集成对比测试
|
||||
# ============================================================
|
||||
|
||||
+816
-284
File diff suppressed because it is too large
Load Diff
@@ -222,6 +222,6 @@ struct MACD面积 {
|
||||
|
||||
impl MACD面积 {
|
||||
fn 总(&self) -> f64 {
|
||||
self.阳 + self.阴
|
||||
self.阳 + self.阴.abs()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,8 @@ use crate::structure::dash_line::虚线;
|
||||
use crate::structure::fractal_obj::分型;
|
||||
use crate::structure::segment_feat::线段特征;
|
||||
use crate::types::{分型结构, 相对方向, 缺口};
|
||||
use cached::stores::LruCache;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
use tracing::warn;
|
||||
|
||||
/// 线段 — 从笔生成线段的算法集合(静态方法命名空间)
|
||||
@@ -48,10 +47,6 @@ type 分割结果 = (
|
||||
);
|
||||
type 中枢序列组 = (Vec<Arc<中枢>>, Vec<Arc<中枢>>, Vec<Arc<中枢>>);
|
||||
|
||||
/// 线段内部背驰判断缓存 — 仿照 dash_line.rs 的 买卖意义缓存 模式
|
||||
static 线段背驰缓存: LazyLock<Mutex<LruCache<(usize, usize), bool>>> =
|
||||
LazyLock::new(|| Mutex::new(LruCache::with_size(128)));
|
||||
|
||||
impl 线段 {
|
||||
// ================================================================
|
||||
// 基础操作
|
||||
@@ -1691,22 +1686,7 @@ impl 线段 {
|
||||
///
|
||||
/// 分析线段的内部中枢和MACD柱分段,判断是否发生内部背驰
|
||||
pub fn 判断线段内部是否背驰(当前段: &虚线, 观察员: &观察者) -> bool {
|
||||
let key = (
|
||||
当前段 as *const 虚线 as usize,
|
||||
观察员 as *const 观察者 as usize,
|
||||
);
|
||||
{
|
||||
let mut cache = 线段背驰缓存.lock().unwrap();
|
||||
if let Some(val) = cached::Cached::cache_get(&mut *cache, &key) {
|
||||
return *val;
|
||||
}
|
||||
}
|
||||
|
||||
let result = Self::判断线段内部是否背驰_impl(当前段, 观察员);
|
||||
|
||||
let mut cache = 线段背驰缓存.lock().unwrap();
|
||||
cached::Cached::cache_set(&mut *cache, key, result);
|
||||
result
|
||||
Self::判断线段内部是否背驰_impl(当前段, 观察员)
|
||||
}
|
||||
|
||||
fn 判断线段内部是否背驰_impl(当前段: &虚线, 观察员: &观察者) -> bool {
|
||||
|
||||
@@ -34,18 +34,35 @@ pub struct 指标计算器;
|
||||
impl 指标计算器 {
|
||||
/// 增量计算所有开启的指标,将结果写入 当前K线.指标
|
||||
///
|
||||
/// `现有序列` 不包含当前K线;prev 取自 现有序列.last()
|
||||
/// `全序列` 包含当前K线(在末尾);prev 取自 全序列[..-1].last()
|
||||
/// 通过 RwLock 内部可变性,以 `&K线` 共享引用写入指标值
|
||||
pub fn 计算并挂载(当前K线: &K线, 现有序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
let prev_guard = 现有序列.last().map(|k| k.指标.read().unwrap());
|
||||
let prev = prev_guard.as_deref();
|
||||
if 配置.计算指标 {
|
||||
Self::_计算MACD组(当前K线, prev, 配置);
|
||||
Self::_计算RSI组(当前K线, prev, 配置);
|
||||
Self::_计算KDJ组(当前K线, prev, 配置);
|
||||
Self::_计算BOLL组(当前K线, prev, 配置);
|
||||
pub fn 计算并挂载(全序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
let n = 全序列.len();
|
||||
let 当前K线 = &全序列[n - 1];
|
||||
let 现有序列 = if n > 1 { &全序列[..n - 1] } else { &[] };
|
||||
|
||||
// 作用域化 prev_guard:在 _回填新指标 之前释放,避免读锁与回填写锁冲突
|
||||
let has_prev;
|
||||
{
|
||||
let prev_guard = if n > 1 {
|
||||
Some(全序列[n - 2].指标.read().unwrap())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let prev = prev_guard.as_deref();
|
||||
if 配置.计算指标 {
|
||||
Self::_计算MACD组(当前K线, prev, 配置);
|
||||
Self::_计算RSI组(当前K线, prev, 配置);
|
||||
Self::_计算KDJ组(当前K线, prev, 配置);
|
||||
Self::_计算BOLL组(当前K线, prev, 配置);
|
||||
}
|
||||
Self::_更新均线(当前K线, 现有序列, 配置);
|
||||
has_prev = n > 1;
|
||||
} // prev_guard dropped here
|
||||
|
||||
if has_prev {
|
||||
Self::_回填新指标(全序列, 配置);
|
||||
}
|
||||
Self::_更新均线(当前K线, 现有序列, 配置);
|
||||
}
|
||||
|
||||
fn _计算MACD组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) {
|
||||
@@ -298,4 +315,162 @@ impl 指标计算器 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 运行中新增指标参数时,回填所有历史K线
|
||||
fn _回填新指标(全序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
// 作用域化首尾读锁:在回填写循环之前释放,避免读锁与写锁冲突
|
||||
let (新MACD, 新RSI, 新KDJ, 新BOLL) = {
|
||||
let 首K_guard = 全序列[0].指标.read().unwrap();
|
||||
let 尾K_guard = 全序列[全序列.len() - 1].指标.read().unwrap();
|
||||
|
||||
let 新MACD: Vec<_> = 配置
|
||||
._解析MACD参数列表()
|
||||
.into_iter()
|
||||
.filter(|(key, _, _, _)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.collect();
|
||||
let 新RSI: Vec<_> = 配置
|
||||
._解析RSI周期列表()
|
||||
.into_iter()
|
||||
.filter(|(key, _)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.collect();
|
||||
let 新KDJ: Vec<_> = 配置
|
||||
._解析KDJ参数列表()
|
||||
.into_iter()
|
||||
.filter(|(key, _, _, _)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.collect();
|
||||
let 新BOLL: Vec<_> = 配置
|
||||
._解析BOLL参数列表()
|
||||
.into_iter()
|
||||
.filter(|(key, _, _)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.collect();
|
||||
|
||||
(新MACD, 新RSI, 新KDJ, 新BOLL)
|
||||
}; // 首K_guard, 尾K_guard dropped here
|
||||
|
||||
if 新MACD.is_empty() && 新RSI.is_empty() && 新KDJ.is_empty() && 新BOLL.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let 计算方式 = &配置.指标计算方式;
|
||||
|
||||
// 从第一根K线开始逐根回填,每次只持有一根prev读锁
|
||||
for i in 0..全序列.len() {
|
||||
let k线 = &全序列[i];
|
||||
let prev_guard = if i > 0 {
|
||||
Some(全序列[i - 1].指标.read().unwrap())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for (key, 快, 慢, 信号) in &新MACD {
|
||||
let val = if let Some(ref prev) = prev_guard {
|
||||
if let Some(指标值::MACD(prev_macd)) = prev.获取(key) {
|
||||
指标值::MACD(平滑异同移动平均线::增量计算_K线(
|
||||
prev_macd,
|
||||
k线,
|
||||
计算方式,
|
||||
))
|
||||
} else {
|
||||
指标值::MACD(平滑异同移动平均线::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*快,
|
||||
*慢,
|
||||
*信号,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
指标值::MACD(平滑异同移动平均线::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*快,
|
||||
*慢,
|
||||
*信号,
|
||||
))
|
||||
};
|
||||
k线.指标.write().unwrap().设置(key, val);
|
||||
}
|
||||
|
||||
for (key, 周期) in &新RSI {
|
||||
let val = if let Some(ref prev) = prev_guard {
|
||||
if let Some(指标值::RSI(prev_rsi)) = prev.获取(key) {
|
||||
指标值::RSI(相对强弱指数::增量计算_K线(
|
||||
prev_rsi,
|
||||
k线,
|
||||
计算方式,
|
||||
))
|
||||
} else {
|
||||
指标值::RSI(相对强弱指数::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*周期,
|
||||
配置.相对强弱指数_超买阈值,
|
||||
配置.相对强弱指数_超卖阈值,
|
||||
Some(配置.相对强弱指数_移动平均线周期),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
指标值::RSI(相对强弱指数::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*周期,
|
||||
配置.相对强弱指数_超买阈值,
|
||||
配置.相对强弱指数_超卖阈值,
|
||||
Some(配置.相对强弱指数_移动平均线周期),
|
||||
))
|
||||
};
|
||||
k线.指标.write().unwrap().设置(key, val);
|
||||
}
|
||||
|
||||
for (key, rsv, k平滑, d平滑) in &新KDJ {
|
||||
let val = if let Some(ref prev) = prev_guard {
|
||||
if let Some(指标值::KDJ(prev_kdj)) = prev.获取(key) {
|
||||
指标值::KDJ(随机指标::增量计算_K线(prev_kdj, k线))
|
||||
} else {
|
||||
指标值::KDJ(随机指标::首次计算_K线(
|
||||
k线,
|
||||
*rsv,
|
||||
*k平滑,
|
||||
*d平滑,
|
||||
配置.随机指标_超买阈值,
|
||||
配置.随机指标_超卖阈值,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
指标值::KDJ(随机指标::首次计算_K线(
|
||||
k线,
|
||||
*rsv,
|
||||
*k平滑,
|
||||
*d平滑,
|
||||
配置.随机指标_超买阈值,
|
||||
配置.随机指标_超卖阈值,
|
||||
))
|
||||
};
|
||||
k线.指标.write().unwrap().设置(key, val);
|
||||
}
|
||||
|
||||
for (key, 周期, 标准差倍数) in &新BOLL {
|
||||
let val = if let Some(ref prev) = prev_guard {
|
||||
if let Some(指标值::BOLL(prev_boll)) = prev.获取(key) {
|
||||
指标值::BOLL(布林带::增量计算_K线(prev_boll, k线, 计算方式))
|
||||
} else {
|
||||
指标值::BOLL(布林带::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*周期 as usize,
|
||||
*标准差倍数,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
指标值::BOLL(布林带::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*周期 as usize,
|
||||
*标准差倍数,
|
||||
))
|
||||
};
|
||||
k线.指标.write().unwrap().设置(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,8 +408,7 @@ impl 缠论K线 {
|
||||
}
|
||||
// 计算指标: 对齐 Python,仅当 计算指标 开启时执行
|
||||
if 配置.计算指标 {
|
||||
let n = 普K序列.len();
|
||||
指标计算器::计算并挂载(&普K序列[n - 1], &普K序列[..n - 1], 配置);
|
||||
指标计算器::计算并挂载(普K序列, 配置);
|
||||
}
|
||||
|
||||
// ---- 阶段2: 缠K合并 ----
|
||||
|
||||
@@ -58,6 +58,11 @@ def Nil(*args, **kwargs):
|
||||
def 获取模块版本():
|
||||
versions = {}
|
||||
|
||||
# 1.
|
||||
try:
|
||||
versions["chanlun"] = importlib.metadata.version("chanlun")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
pass
|
||||
# 2.
|
||||
try:
|
||||
versions["fastapi"] = importlib.metadata.version("fastapi")
|
||||
@@ -1994,65 +1999,53 @@ async def 处理图表消息(用户标识: str, 消息字典: Dict, websocket: W
|
||||
待发送消息 = {}
|
||||
if 数据类型 == "中枢<笔>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.笔_中枢序列[序号])})
|
||||
if 数据类型 == "中枢<线段>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.中枢序列[序号])})
|
||||
|
||||
if 数据类型 == "中枢<线段<线段>>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.线段_中枢序列[序号])})
|
||||
|
||||
if 数据类型 == "中枢<扩展线段>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展中枢序列[序号])})
|
||||
if 数据类型 == "中枢<扩展线段<线段>>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展中枢序列_线段[序号])})
|
||||
|
||||
if 数据类型 == "笔":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.笔序列[序号])})
|
||||
if 数据类型 == "线段":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.线段序列[序号])})
|
||||
段: 虚线 = 观察员.线段序列[序号]
|
||||
if 段._特征序列_显示:
|
||||
段._特征序列_显示 = False
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
观察员 and 观察员.报信(特征, 指令.删除(特征.标识), sys._getframe().f_lineno)
|
||||
|
||||
else:
|
||||
段._特征序列_显示 = True
|
||||
序号 = 0
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
特征.序号 = 序号
|
||||
特征.标识 = f"{段.文.中.标识}:{段.文.中.周期}:{段.标识}_特征序列_{序号}:{段.序号}"
|
||||
观察员 and 观察员.报信(特征, 指令.添加(特征.标识), sys._getframe().f_lineno)
|
||||
序号 += 1
|
||||
if 数据类型 == "扩展线段":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展线段序列[序号])})
|
||||
if 数据类型 == "扩展线段<线段>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展线段序列_线段[序号])})
|
||||
if "中枢" in 数据类型 and 数据类型 != "中枢<笔>":
|
||||
for i in range(观察员.中枢分析层次):
|
||||
if 观察员.中枢序列组[i] and 观察员.中枢序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.中枢序列组[i][序号])})
|
||||
for i in range(观察员.扩展中枢分析层次):
|
||||
if 观察员.扩展中枢序列组[i] and 观察员.扩展中枢序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展中枢序列组[i][序号])})
|
||||
for i in range(观察员.混合扩展中枢分析层次):
|
||||
if 观察员.混合扩展中枢序列组[i] and 观察员.混合扩展中枢序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.混合扩展中枢序列组[i][序号])})
|
||||
|
||||
if 数据类型 == "线段<线段>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.线段_线段序列[序号])})
|
||||
段 = 观察员.线段_线段序列[序号]
|
||||
if 段._特征序列_显示:
|
||||
段._特征序列_显示 = False
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
观察员 and 观察员.报信(特征, 指令.删除(特征.标识), sys._getframe().f_lineno)
|
||||
elif "线段" in 数据类型 and 数据类型 != "笔":
|
||||
for i in range(观察员.线段分析层次):
|
||||
if 观察员.线段序列组[i] and 观察员.线段序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.线段序列组[i][序号])})
|
||||
段 = 观察员.线段序列组[i][序号]
|
||||
if 段._特征序列_显示:
|
||||
段._特征序列_显示 = False
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
观察员 and 观察员.报信(特征, 指令.删除(特征.标识), sys._getframe().f_lineno)
|
||||
|
||||
else:
|
||||
段._特征序列_显示 = True
|
||||
序号 = 0
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
特征.序号 = 序号
|
||||
特征.标识 = f"{段.文.右.标识}:{段.文.中.周期}:{段.标识}_特征序列_{序号}:{段.序号}"
|
||||
观察员 and 观察员.报信(特征, 指令.添加(特征.标识), sys._getframe().f_lineno)
|
||||
序号 += 1
|
||||
else:
|
||||
段._特征序列_显示 = True
|
||||
序号 = 0
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
特征.序号 = 序号
|
||||
特征.标识 = f"{段.文.中.标识}:{段.文.中.周期}:{段.标识}_特征序列_{序号}:{段.序号}"
|
||||
观察员 and 观察员.报信(特征, 指令.添加(特征.标识), sys._getframe().f_lineno)
|
||||
序号 += 1
|
||||
|
||||
for i in range(观察员.扩展线段分析层次):
|
||||
if 观察员.扩展线段序列组[i] and 观察员.扩展线段序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展线段序列组[i][序号])})
|
||||
for i in range(观察员.混合扩展线段分析层次):
|
||||
if 观察员.混合扩展线段序列组[i] and 观察员.混合扩展线段序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.混合扩展线段序列组[i][序号])})
|
||||
|
||||
if "_" in 数据类型 and "中枢" in 数据类型: # 线段_0_实_中枢<笔>
|
||||
数据类型, 线序, 虚实合, 类型 = 数据类型.split("_")
|
||||
|
||||
段序号 = int(线序)
|
||||
|
||||
if 数据类型 == "线段":
|
||||
段: 虚线 = 观察员.线段序列[段序号]
|
||||
zs = getattr(段, f"{虚实合}_中枢序列")[序号]
|
||||
@@ -2063,6 +2056,26 @@ async def 处理图表消息(用户标识: str, 消息字典: Dict, websocket: W
|
||||
zs = getattr(段, f"{虚实合}_中枢序列")[序号]
|
||||
待发送消息.update({"index": 序号, "data": str(zs)})
|
||||
|
||||
for i in range(观察员.线段分析层次):
|
||||
if 观察员.线段序列组[i] and 观察员.线段序列组[i][0].标识 == 数据类型:
|
||||
段 = 观察员.线段序列组[i][段序号]
|
||||
zs = getattr(段, f"{虚实合}_中枢序列")[序号]
|
||||
待发送消息.update({"index": 序号, "data": str(zs)})
|
||||
|
||||
for i in range(观察员.扩展线段分析层次):
|
||||
if 观察员.扩展线段序列组[i] and 观察员.扩展线段序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展线段序列组[i][序号])})
|
||||
段 = 观察员.扩展线段序列组[i][序号]
|
||||
zs = getattr(段, f"{虚实合}_中枢序列")[序号]
|
||||
待发送消息.update({"index": 序号, "data": str(zs)})
|
||||
|
||||
for i in range(观察员.混合扩展线段分析层次):
|
||||
if 观察员.混合扩展线段序列组[i] and 观察员.混合扩展线段序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.混合扩展线段序列组[i][序号])})
|
||||
段 = 观察员.混合扩展线段序列组[i][序号]
|
||||
zs = getattr(段, f"{虚实合}_中枢序列")[序号]
|
||||
待发送消息.update({"index": 序号, "data": str(zs)})
|
||||
|
||||
await 全局连接管理器.发送信息(用户标识, {"type": "query_result", "success": True, "data_type": 数据类型, "data": 待发送消息})
|
||||
|
||||
except IndexError:
|
||||
|
||||
Reference in New Issue
Block a user