观察者 线段分析/扩展分析 支持无限递归,保留之前序列为@property 形式

移除 某些函数的缓存机制
This commit is contained in:
YuWuKunCheng
2026-06-07 13:08:19 +08:00
parent 0a7e9dc896
commit c83cddbcc4
14 changed files with 2316 additions and 722 deletions
+1 -1
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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" }
+69 -19
View File
@@ -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),
})
}
/// 拆分各序列数据,单独存文件,文件名为对应变量名
+5 -1
View File
@@ -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}"))
}
+6 -15
View File
@@ -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.
+731 -6
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -222,6 +222,6 @@ struct MACD面积 {
impl MACD面积 {
fn (&self) -> f64 {
self. + self.
self. + self..abs()
}
}
+2 -22
View File
@@ -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 {
+185 -10
View File
@@ -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);
}
}
}
}
+1 -2
View File
@@ -408,8 +408,7 @@ impl 缠论K线 {
}
// 计算指标: 对齐 Python,仅当 计算指标 开启时执行
if . {
let n = K序列.len();
::(&K序列[n - 1], &K序列[..n - 1], );
::(K序列, );
}
// ---- 阶段2: 缠K合并 ----
+62 -49
View File
@@ -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: