diff --git a/chanlun-py/Cargo.toml b/chanlun-py/Cargo.toml index 9d9e328..4b8944d 100644 --- a/chanlun-py/Cargo.toml +++ b/chanlun-py/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chanlun-py" -version = "26.6.34" +version = "26.6.42" edition = "2024" description = "缠论技术分析库 — Rust 高性能 Python 绑定" authors = ["YuYuKunKun"] diff --git a/chanlun-py/chanlun/chan.py b/chanlun-py/chanlun/chan.py index 6cf1be2..c5c8f92 100644 --- a/chanlun-py/chanlun/chan.py +++ b/chanlun-py/chanlun/chan.py @@ -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) diff --git a/chanlun-py/pyproject.toml b/chanlun-py/pyproject.toml index 354cfe4..243c313 100644 --- a/chanlun-py/pyproject.toml +++ b/chanlun-py/pyproject.toml @@ -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" } diff --git a/chanlun-py/src/business_py.rs b/chanlun-py/src/business_py.rs index 0224ddd..2121a78 100644 --- a/chanlun-py/src/business_py.rs +++ b/chanlun-py/src/business_py.rs @@ -693,6 +693,7 @@ impl 买卖点Py { #[pyclass(name = "观察者", module = "chanlun._chanlun", subclass)] pub struct 观察者Py { pub(crate) inner: Option>>, + 配置缓存: std::sync::Mutex>>, } 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> { + 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> { 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), + }) } /// 拆分各序列数据,单独存文件,文件名为对应变量名 diff --git a/chanlun-py/src/config_py.rs b/chanlun-py/src/config_py.rs index a6975f4..c7930b2 100644 --- a/chanlun-py/src/config_py.rs +++ b/chanlun-py/src/config_py.rs @@ -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})")) } diff --git a/chanlun-py/src/indicators_py.rs b/chanlun-py/src/indicators_py.rs index 9ac7d52..9a755e8 100644 --- a/chanlun-py/src/indicators_py.rs +++ b/chanlun-py/src/indicators_py.rs @@ -985,26 +985,17 @@ impl 指标计算器Py { /// 增量计算所有开启的指标,将结果写入 当前K线.指标 #[staticmethod] fn 计算并挂载( - 当前K线: &Bound<'_, crate::kline_py::K线Py>, + _当前K线: &Bound<'_, crate::kline_py::K线Py>, 全序列: Vec>, 配置: &Bound<'_, crate::config_py::缠论配置Py>, py: Python<'_>, ) -> PyResult<()> { let config = 配置.borrow().to_rust_config(py)?; - // 全序列包含 当前K线 在末尾;Rust 计算并挂载 的 现有序列 不含当前K线 - let 现有序列: Vec> = 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> = 全序列 + .iter() + .map(|k| k.bind(py).borrow().inner.clone()) + .collect(); + chanlun::indicators::指标计算器::计算并挂载(&全序列_rust, &config); Ok(()) } } diff --git a/chanlun-py/tests/btcusd-300-1777649100-1778398800.nb b/chanlun-py/tests/btcusd-300-1777649100-1778398800.nb new file mode 100644 index 0000000..1a4d494 Binary files /dev/null and b/chanlun-py/tests/btcusd-300-1777649100-1778398800.nb differ diff --git a/chanlun-py/tests/test_all.py b/chanlun-py/tests/test_all.py index 0430915..6ca7623 100644 --- a/chanlun-py/tests/test_all.py +++ b/chanlun-py/tests/test_all.py @@ -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) + + # ============================================================ # 集成对比测试 # ============================================================ diff --git a/chanlun/README.md b/chanlun/README.md index b3e09c3..a081c41 100644 --- a/chanlun/README.md +++ b/chanlun/README.md @@ -1,11 +1,14 @@ # chanlun — 缠论技术分析 Rust 核心库 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![Rust](https://img.shields.io/badge/rust-1.70%2B-orange.svg)](https://www.rust-lang.org) +[![Rust](https://img.shields.io/badge/rust-1.85%2B-orange.svg)](https://www.rust-lang.org) +[![edition](https://img.shields.io/badge/edition-2024-9cf.svg)](https://doc.rust-lang.org/edition-guide/rust-2024/) -基于[缠中说禅](https://zh.wikipedia.org/wiki/%E7%BC%A0%E4%B8%AD%E8%AF%B4%E7%A6%85)理论的量化技术分析核心库,用 Rust 实现。支持流式数据处理,可作为独立 Rust crate 使用,也可通过 PyO3 绑定在 Python 中调用(见 [`chanlun-py`](../chanlun-py/))。 +基于[缠中说禅](https://zh.wikipedia.org/wiki/%E7%BC%A0%E4%B8%AD%E8%AF%B4%E7%A6%85)理论的量化技术分析核心库,将 Python 参考实现 (`chan.py`, ~4200行) 完整移植为 Rust,同时保持了 API 的完全兼容。 -约 9,400 行 Rust 代码,覆盖缠论的完整算法体系:K线包含处理、分型识别、笔划分、线段划分、中枢识别、背驰检测、买卖点识别、多周期联立分析。 +**约 13,700 行 Rust 代码**,39 个源文件,覆盖缠论的完整算法体系:K线包含处理、分型识别、笔划分、线段划分(含特征序列与缺口处理)、中枢识别(含延伸/扩展/多级中枢)、背驰检测(MACD/斜率/测度三种方式)、买卖点识别(18种类型)、多周期联立分析。 + +核心 crate 位于 `chanlun/` 目录。Python 绑定通过独立的 [`chanlun-py`](../chanlun-py/) crate 实现,使用 PyO3 将 Rust 核心封装为 Python 模块,API 与 `chan.py` 完全一致。所有类型名、方法名、字段名均使用中文标识符,与 Python 版本保持 1:1 对应。 --- @@ -15,12 +18,14 @@ - [项目结构](#项目结构) - [快速开始](#快速开始) - [数据管线](#数据管线) -- [核心类型](#核心类型) +- [核心类型详解](#核心类型详解) - [配置体系](#配置体系) - [算法模块](#算法模块) - [技术指标](#技术指标) +- [线程安全与并发](#线程安全与并发) - [Python 绑定](#python-绑定) - [数据序列化](#数据序列化) +- [测试](#测试) - [许可](#许可) --- @@ -35,8 +40,8 @@ 核心理念: - **走势终完美**:任何级别的任何走势类型终要完成 -- **自同构性**:不同级别的走势呈现相同的形态结构 -- **完全分类**:将走势分为上涨、下跌、盘整三种 +- **自同构性**:不同级别的走势呈现相同的形态结构,可通过递归分析进行多级别联立 +- **完全分类**:将走势分为上涨、下跌、盘整三种,每个节点都可通过买卖点找到操作依据 --- @@ -44,47 +49,59 @@ ``` chanlun/ -├── Cargo.toml # 依赖: serde, byteorder, chrono +├── Cargo.toml # 依赖: serde, byteorder, chrono, cached, tracing └── src/ - ├── lib.rs # 模块注册 - ├── config.rs # 缠论配置 (60+ 参数, serde) - ├── types/ # 基础类型 - │ ├── mod.rs - │ ├── bsp_type.rs # 买卖点类型 (18 种) - │ ├── direction.rs # 相对方向 (9 种) - │ ├── fractal.rs # 分型结构 (5 种) - │ └── gap.rs # 缺口 - ├── kline/ # K线层 - │ ├── mod.rs - │ ├── bar.rs # 原始K线 (OHLCV + 指标) - │ └── chan_kline.rs # 缠论K线 (包含处理 + 合并) - ├── indicators/ # 技术指标 - │ ├── mod.rs - │ ├── macd.rs # MACD (平滑异同移动平均线) - │ ├── rsi.rs # RSI (相对强弱指数) - │ └── kdj.rs # KDJ (随机指标) - ├── algorithm/ # 核心算法 - │ ├── mod.rs - │ ├── bi.rs # 笔划分算法 - │ ├── segment.rs # 线段划分算法 - │ ├── hub.rs # 中枢识别算法 - │ └── divergence.rs # 背驰检测算法 - ├── structure/ # 结构体 - │ ├── mod.rs - │ ├── dash_line.rs # 虚线 (笔/线段的通用抽象) - │ ├── fractal_obj.rs # 分型对象 - │ ├── segment_feat.rs # 线段特征 - │ └── feat_fractal.rs # 特征分型 - ├── business/ # 业务层 - │ ├── mod.rs - │ ├── bsp.rs # 基础买卖点 + 买卖点 - │ ├── observer.rs # 观察者 (单周期分析器) - │ ├── synthesizer.rs # K线合成器 (周期合成) - │ └── multi_frame.rs # 立体分析器 (多周期联立) - └── utils/ # 工具 - ├── mod.rs - ├── datetime.rs # 时间戳转换 - └── format.rs # 格式化输出 + ├── lib.rs # 模块注册 (#![allow(non_snake_case)]) + ├── main.rs # 入口(CLI 工具) + ├── config.rs # 缠论配置 (62 字段, serde, 622 行) + │ + ├── types/ # 基础类型 (5 文件, 514 行) + │ ├── mod.rs # 重导出: 买卖点类型, 相对方向, 分型结构, 缺口, SyncF64 + │ ├── direction.rs # 相对方向 (9 变体): 向上/向下/向上缺口/向下缺口/衔接向上/衔接向下/顺/逆/同 + │ ├── bsp_type.rs # 买卖点类型 (18 变体): 一二三买/卖 + T1/T1P/T2/T2S/T3A/T3B + │ ├── fractal.rs # 分型结构 (5 变体) + 有高低 trait + │ ├── gap.rs # 缺口: 高/低价格区间 + 居中截取 + │ └── sync_f64.rs # SyncF64: 基于 AtomicU64 的线程安全 f64 容器 + │ + ├── kline/ # K线层 (2 文件, 1113 行) + │ ├── mod.rs # + │ ├── bar.rs # 原始K线 (388 行): OHLCV + RwLock<指标容器>, Clone/Display, 大端字节序序列化 + │ └── chan_kline.rs # 缠论K线 (725 行): 经包含处理后的K线, 分型标记, AtomicI64/SyncF64 内部可变性 + │ + ├── indicators/ # 技术指标 (6 文件, 1589 行) + │ ├── mod.rs # K线取值: 开/高/低/收/高低均值/高低收均值/开高低收均值 + │ ├── macd.rs # 平滑异同移动平均线 (215 行): EMA 快慢线 + DIF/DEA/MACD柱 + │ ├── rsi.rs # 相对强弱指数 (240 行): Wilder SMA 平滑 + 超买超卖线 + │ ├── kdj.rs # 随机指标 (289 行): RSV → K → D → J + 超买超卖阈值 + │ ├── boll.rs # 布林带 (169 行): 中轨/上轨/下轨 + 带宽 + │ ├── calculator.rs # 指标计算器 (476 行): 增量计算 + 回填新指标 + 均线 + │ └── container.rs # 指标容器 (202 行): 注册表模式, 动态指标存取 + │ + ├── algorithm/ # 核心算法 (4 文件, 4987 行) + │ ├── mod.rs # + │ ├── bi.rs # 笔划分算法 (1009 行): 递归分析, 笔弱化, 次级成笔, 实际高/低点 + │ ├── segment.rs # 线段划分算法 (2078 行): 特征序列, 缺口处理, 四种修正机制, 扩展线段 + │ ├── hub.rs # 中枢识别算法 (1041 行): 重叠检测, 延伸/扩展, 第三买卖点, 多级中枢 + │ └── divergence.rs # 背驰检测 (227 行): MACD背驰/斜率背驰/测度背驰 + 四种组合模式 + │ + ├── structure/ # 结构体 (4 文件, 2089 行) + │ ├── mod.rs # + │ ├── dash_line.rs # 虚线 (1712 行): 笔/线段的通用抽象, 买卖意义, MACD行为统计, 分段, 背驰检测 + │ ├── fractal_obj.rs # 分型 (344 行): 左中右构型, 分型模式开关, 强度判定, MACD分型匹配 + │ ├── segment_feat.rs # 线段特征 (668 行): 特征序列元素, 文/武取极值, 静态分析, 分型序列 + │ └── feat_fractal.rs # 特征分型 (62 行): 三线段特征元素构成的分型 + │ + ├── business/ # 业务层 (4 文件, 1648 行) + │ ├── mod.rs # + │ ├── observer.rs # 观察者 (1138 行): 单周期分析器, 流式管线, 3级线段/扩展/混合扩展 + │ ├── bsp.rs # 基础买卖点 + 买卖点工厂 (595 行): 18种类型, 偏移/失效/终结 + │ ├── synthesizer.rs # K线合成器 (161 行): 小周期→大周期合成, 时间戳对齐 + │ └── multi_frame.rs # 立体分析器 (192 行): 多周期联立, 合成器+观察者协调 + │ + └── utils/ # 工具 (2 文件, 145 行) + ├── mod.rs # + ├── datetime.rs # 时间戳转换: 字符串↔Unix时间戳 + └── format.rs # 格式化: f64→最小字符串 (format_f64_g) ``` --- @@ -98,390 +115,855 @@ chanlun/ chanlun = { path = "path/to/chanlun" } ``` -### 基本用法 +### 基本用法 — 单周期分析 ```rust use chanlun::config::缠论配置; use chanlun::kline::bar::K线; use chanlun::business::observer::观察者; -// 1. 创建配置 +// 1. 创建配置(所有字段带默认值) let 配置 = 缠论配置::default(); -// 2. 创建观察者(单周期分析器) -let mut 观察员 = 观察者::new("BTCUSD".into(), 3600, 配置); +// 2. 创建观察者(返回 Arc>) +let 观察员 = 观察者::new("BTCUSD".into(), 3600, 配置); -// 3. 逐根投喂K线(流式增量分析) +// 3. 逐根投喂K线(流式增量分析,每根K线触发全管线运算) for k线 in k线数据流 { - 观察员.增加原始K线(k线); + 观察员.write().unwrap().增加原始K线(k线); } // 4. 读取分析结果 -for 笔 in 观察员.笔序列() { - println!("笔: {:?}", 笔.方向()); -} -for 中枢 in 观察员.中枢序列() { - println!("中枢: 高={:?}, 低={:?}", 中枢.高(), 中枢.低()); +let obs = 观察员.read().unwrap(); +println!("笔数量: {}", obs.笔序列.len()); +println!("线段数量: {}", obs.线段序列().len()); +println!("中枢数量: {}", obs.中枢序列().len()); + +// 遍历笔 +for 笔 in &obs.笔序列 { + println!("笔方向: {}, 高: {}, 低: {}", 笔.方向(), 笔.高(), 笔.低()); } ``` +### 便捷投喂 — 直接从 OHLCV + +```rust +let 观察员 = 观察者::new("ETHUSD".into(), 300, Default::default()); +let mut obs = 观察员.write().unwrap(); +obs.投喂原始数据(1736640000, 3500.0, 3550.0, 3480.0, 3520.0, 1200.0); +// ... 继续投喂 ... +``` + +### 加载 .nb 数据文件 + +```rust +let 观察员 = 观察者::new("btcusd".into(), 300, Default::default()); +观察员.write().unwrap().读取数据文件("btcusd-300-1000000-1100000.nb", Default::default())?; +``` + ### 多周期分析 ```rust use chanlun::business::multi_frame::立体分析器; // 周期组: [最小输入周期, 更多显示周期...] +// 只投喂最小周期K线,大周期自动合成 let mut 分析器 = 立体分析器::new( "BTCUSD".into(), vec![60, 300, 1800, 7200], // 1分钟 → 5分钟 → 30分钟 → 2小时 - 缠论配置::default(), - std::collections::HashMap::new(), + None, // 默认配置 + None, // 无周期特定配置 ); -// 只投喂最小周期K线,大周期自动合成 +// 投喂最小周期K线 for k线 in 小周期K线流 { 分析器.投喂K线(k线); } // 获取各周期观察者 -let 观察员_1min = 分析器.获取观察员(60); -let 观察员_5min = 分析器.获取观察员(300); +if let Some(日线观察员) = 分析器.获取观察者(86400) { + let obs = 日线观察员.read().unwrap(); + println!("日线笔数: {}", obs.笔序列.len()); +} ``` --- ## 数据管线 -缠论分析按严格的层级顺序进行,每收到一根新K线,增量更新所有层级: +缠论分析按严格的层级顺序进行,每收到一根新K线,**增量更新**所有层级(而非批量重算)。这是该库适合实时交易系统的关键设计。 + +### 管线流程 ``` 原始K线 (K线) │ - ├── 指标计算: MACD / RSI / KDJ (增量更新) + ├── 指标计算: MACD / RSI / KDJ / BOLL (增量更新, 含回填新指标) + │ ├── K线取值: 开/高/低/收/高低均值/高低收均值/开高低收均值 + │ ├── 多参数组: MACD_参数列表 / RSI_周期列表 / KDJ_参数列表 / BOLL_参数列表 + │ └── 均线组: SMA/EMA (均线_类型列表 × 均线_周期列表) │ ├── 包含处理 → 缠论K线 (缠论K线) - │ │ - │ ├── 顺序包含合并 - │ ├── 逆序包含合并 - │ └── 方向判定 (向上/向下) + │ ├── 顺序包含合并 (顺方向取极值) + │ ├── 逆序包含合并 (更新时间/标的K线) + │ ├── 方向判定 (相对方向: 向上/向下/向上缺口/向下缺口/衔接/包含) + │ └── 合并替换模式: 缠K合并替换=false(原地修改) / true(产出新缠K) │ ├── 分型识别 → 分型 (分型) - │ │ │ ├── 由左中右三根缠论K线构成 - │ ├── 类型: 顶分型 / 底分型 / 上 / 下 / 散 - │ └── 强度: 强 / 中 / 弱 + │ ├── 类型: 顶分型 / 底分型 / 三连向上 / 三连向下 / 向右扩散 + │ ├── 强度: 强 / 中 / 弱 (基于左中右收盘价关系) + │ ├── 分型模式: 全局开关控制缓存/实时读取 + │ └── MACD柱子分型匹配 (左中右MACD柱形态判定) │ - ├── 笔划分 → 笔 (虚线, 标识="笔") - │ │ - │ ├── 递归分析算法 - │ ├── 支持笔弱化、次级成笔等策略 - │ └── 实际高/低点选择 (first/last) + ├── 笔划分 → 笔 (虚线, 标识="笔", 级别=1) + │ ├── 递归分析算法: 顶底分型交替验证 + │ ├── 成笔条件: 笔内元素数量≥5 (弱化模式≥3) + │ ├── 笔弱化策略: 放宽条件时尝试次高/次低成笔 + │ ├── 实际高/低点选择: first/last (笔内相同终点取舍) + │ ├── 笔内起始分型包含整笔验证 + │ └── 笔内原始K线包含整笔验证 │ - ├── 线段划分 → 线段 (虚线, 标识="线段") - │ │ - │ ├── 特征序列提取 - │ ├── 缺口检测 (老阴/老阳) - │ ├── 笔破坏判断 - │ ├── 缺口突破修正 - │ └── 非缺口下穿刺修正 + ├── 线段划分 → 线段 (虚线, 标识="线段", 级别=2) + │ ├── 特征序列提取: 从笔序列中提取同向特征序列元素 (线段特征.静态分析) + │ ├── 缺口处理: + │ │ ├── 有缺口 (老阴/老阳): 严格包含处理, 等待特征分型完成 + │ │ └── 无缺口 (小阳/少阴): 允许逆序包含 + │ ├── 四种修正机制 (依次尝试): + │ │ ├── _缺口突破: 老阳/老阴时价格突破前线段极值 + │ │ ├── _非缺口下穿刺: 贯穿伤后出现方向相同的3笔 + │ │ ├── _缺口后紧急修正: 前一线段有缺口时自动修正 + │ │ └── _修正: 配置启用的短路修正 (≥9笔) + │ ├── 线段破坏判定: 新线段形成后前一线段被破坏 + │ ├── 3级递归: 线段 → 线段<线段> → 线段<线段<线段>> + │ └── 扩展线段分析: 扩展线段 → 扩展线段<扩展线段> → ... │ ├── 中枢识别 → 中枢 (中枢) - │ │ - │ ├── 连续三笔/段重叠构成 - │ ├── 中枢延伸 (添加虚线) - │ ├── 中枢扩展 (≥9段时生成扩展中枢) - │ ├── 第三类买卖点检测 - │ └── 多级中枢: 笔中枢 / 线段中枢 / 扩展线段中枢 + │ ├── 基础检查: 三条连续虚线重叠且方向关系正确 + │ ├── 中枢延伸: 后继虚线未离开中枢则加入基础序列 + │ ├── 第三买卖点: 虚线离开中枢后不回中枢, 记录第三买卖线 + │ ├── 中枢完成: 第三买卖线确立后开始新中枢 + │ ├── 中枢扩展: ≥9段时调用扩展分析生成扩展中枢 + │ └── 多级中枢: + │ ├── 笔中枢 (级别=1, 基于笔序列) + │ ├── 线段中枢 (级别=2, 基于线段序列) + │ ├── 扩展线段中枢 (基于扩展线段序列) + │ ├── 混合扩展中枢 (基于扩展线段<线段>序列) + │ └── 线段<线段>中枢 / 扩展线段<扩展线段>中枢 等 │ - └── 买卖点识别 → 买卖点 (买卖点) - │ - ├── 6 类买卖点: T1 / T1P / T2 / T2S / T3A / T3B - ├── 指标匹配: MACD / KDJ / RSI - ├── 背驰确认 - └── 中枢来源: 实 / 虚 / 合 + └── 买卖点识别 → 基础买卖点 (18种类型) + ├── 6 类经典买卖点: 一买/一卖/二买/二卖/三买/三卖 + ├── 12 类扩展买卖点: T1/T1P/T2/T2S/T3A/T3B 各含买卖 + ├── 指标匹配: MACD柱子匹配 / KDJ金叉死叉 / RSI超买超卖 + ├── 背驰确认: MACD背驰 / 斜率背驰 / 测度背驰 + ├── 中枢来源策略: 实 / 虚 / 合 + ├── 买卖意义缓存: LRU 128条目, 指针身份key + └── 偏移/失效偏移/终结K线管理 ``` -**流式增量处理**:`观察者.__处理数据` 每收到一根新K线就逐层触发所有后续分析,而非批量重算。这使得该库适合实时交易系统。 +### 流式增量处理 + +`观察者.__处理数据` 每收到一根新K线: +1. 计算指标并挂载到新K线 +2. 执行缠K包含处理,产出缠K +3. 识别分型,添加到分型序列 +4. 递归分析笔(可能生成新笔或修改最后一笔) +5. 递归分析中枢(笔中枢) +6. 递归分析线段(3级递归:线段→线段<线段>→线段<线段<线段>>) +7. 递归分析扩展线段(3级递归) +8. 递归分析混合扩展线段(3级递归,源序列为线段序列组[i]) + +每步都只处理最后几个元素,时间复杂度 O(1) 均摊。 --- -## 核心类型 +## 核心类型详解 ### 枚举类型 -| 类型 | 变体 | 说明 | -|------|------|------| -| `相对方向` | 向上 / 向下 / 向上缺口 / 向下缺口 / 衔接向上 / 衔接向下 / 顺 / 逆 / 同 | 两个价格区间的相对位置关系 | -| `分型结构` | 上 / 下 / 顶 / 底 / 散 | 三根K线构成的形态 | -| `买卖点类型` | 一买/一卖/二买/二卖/三买/三卖 + T1~T3B | 18 种买卖点分类 | +#### 相对方向 (`types::direction`) +用于描述两个价格区间(两根K线或两个分型)之间的位置关系,共 9 个变体: -### 数据结构 +| 变体 | 说明 | 示例场景 | +|------|------|---------| +| `向上` | 后K线高低点均高于前K线,且不构成缺口 | 正常上涨 | +| `向下` | 后K线高低点均低于前K线,且不构成缺口 | 正常下跌 | +| `向上缺口` | 后K线低点 > 前K线高点 | 大幅高开 | +| `向下缺口` | 后K线高点 < 前K线低点 | 大幅低开 | +| `衔接向上` | 后K线低点 == 前K线高点 | 精准衔接上涨 | +| `衔接向下` | 后K线高点 == 前K线低点 | 精准衔接下跌 | +| `顺` | 前K线包含后K线(前高≥后高 且 前低≤后低) | 顺序包含 | +| `逆` | 后K线包含前K线(前高≤后高 且 前低≥后低) | 逆序包含 | +| `同` | 两K线高低点完全相同 | 重复数据 | -| 类型 | 关键字段 | 所有权模型 | -|------|---------|-----------| -| `K线` | 标识, 序号, 周期, 时间戳, 高, 低, 开盘价, 收盘价, 成交量, macd?, rsi?, kdj? | 值类型 (Clone) | -| `缠论K线` | 序号, 时间戳, 高, 低, 方向, 分型?, 分型特征值, 原始起始序号, 原始结束序号, 标的K线 | Rc 引用 | -| `分型` | 左?, 中, 右?, 结构, 时间戳, 分型特征值 | Rc 引用 | -| `虚线` | 标识, 序号, 级别, 文, 武, 有效性, 基础序列, 特征序列, 实/虚/合_中枢序列, 确认K线, 模式, 前一缺口 | Rc 引用 | -| `线段特征` | 标识, 线段方向, (list子类) | Rc 引用 | -| `特征分型` | 左, 中, 右, 结构 | Rc 引用 | -| `中枢` | 序号, 标识, 级别, 基础序列, 第三买卖线?, 本级_第三买卖线? | Rc 引用 | -| `基础买卖点` | 类型, 买卖点K线, 买卖点分型, 备注, 偏移, 失效偏移, 有效性, 破位值 | 值类型 | -| `买卖点` | (继承基础买卖点) | 值类型 | -| `缺口` | 高, 低 | 值类型 | +提供方法:`翻转()`, `是否向上()`, `是否向下()`, `是否包含()`, `是否缺口()`, `是否衔接()`, `分析(前高,前低,后高,后低)` + +#### 分型结构 (`types::fractal`) +三根K线构成的结构形态,共 5 个变体: + +| 变体 | 左中右关系 | +|------|-----------| +| `上` | 向上类 + 向上类 | +| `下` | 向下类 + 向下类 | +| `顶` | 向上类 + 向下类(Λ 形) | +| `底` | 向下类 + 向上类(V 形) | +| `散` | 逆序包含 + 逆序包含 | + +提供方法:`分析(左,中,右, 可以逆序包含, 忽视顺序包含)` — 通用分型识别 + +#### 买卖点类型 (`types::bsp_type`) +18 种买卖点分类: + +| 类别 | 买点 | 卖点 | 说明 | +|------|------|------|------| +| 一类 | `一买` | `一卖` | 中枢背驰后的第一类买卖点 | +| 二类 | `二买` | `二卖` | 回调到中枢内的第二类买卖点 | +| 三类 | `三买` | `三卖` | 离开中枢后不回中枢的第三类买卖点 | +| 扩展T1 | `T1买` | `T1卖` | 事后确认型 | +| 扩展T1P | `T1P买` | `T1P卖` | 事后确认型+ | +| 扩展T2 | `T2买` | `T2卖` | 中枢回调型 | +| 扩展T2S | `T2S买` | `T2S卖` | 中枢回调次级 | +| 扩展T3A | `T3A买` | `T3A卖` | 第三类扩展A | +| 扩展T3B | `T3B买` | `T3B卖` | 第三类扩展B | + +提供方法:`是买点()`, `是卖点()` + +### 数据结构详解 + +#### K线 (`kline::bar`) — 原始OHLCV + 指标 +``` +K线 { + 标识, 序号, 周期, 时间戳, + 高, 低, 开盘价, 收盘价, 成交量, + 指标: RwLock<指标容器> // MACD/RSI/KDJ/BOLL/均线 +} +``` +- 实现 `Clone` (深拷贝, 包括指标容器) +- 支持大端字节序序列化 (`to_bytes()` → 48字节, `from_bytes()`) +- 提供 `创建普K()` 工厂方法, `相等()` 结构化校验 +- `获取MACD()` 计算两K线间的 MACD 柱面积 (阳/阴/合/总) +- `截取rc()` 按 Arc 指针截取序列片段 + +#### 缠论K线 (`kline::chan_kline`) — 包含处理后的K线 +``` +缠论K线 { + 序号: AtomicI64, + 时间戳: AtomicI64, + 高: SyncF64, // 经包含处理可能被拉高 + 低: SyncF64, // 经包含处理可能被压低 + 方向: RwLock<相对方向>, + 分型: RwLock>, + 分型特征值: SyncF64, // 历史高低点极值 + 周期, 标识, + 原始起始序号, // 包含处理前 + 原始结束序号: AtomicI64, // 包含处理后更新 + 标的K线: RwLock>, + 买卖点信息: RwLock>, +} +``` +- 核心算法:`_兼并()` — 顺序/逆序包含合并, 重复提交检测, 原地修改 +- `分析()` — 完整的普K→缠K+分型管线 +- 指标匹配:`与MACD柱子匹配()` / `与RSI匹配()` / `与KDJ匹配()` +- 时间戳对齐:跨周期对齐至基线 +- `相等()` — 20+ 字段逐项递归校验 + +#### 分型 (`structure::fractal_obj`) — 顶/底分型 +``` +分型 { + 左: Option>, + 中: Arc<缠论K线>, // 核心K线 + 右: Option>, + 结构: 分型结构, // 构造时缓存 + 时间戳: i64, // 构造时缓存 + 分型特征值: f64, // 构造时缓存 +} +``` +- **分型模式** (`分型模式: AtomicBool`): 全局开关 + - `true` (默认) — `时间戳()/结构()/分型特征值()` 返回构造时缓存值 + - `false` — 从 `中` 缠K 实时读取(逆序包含后使用) +- 提供方法:`强度()` (强/中/弱), `关系组()` (左中右三组关系), `与MACD柱子分型匹配()` +- `相等()` — 递归校验左/中/右缠K + 自有缓存字段 +- 实现 `有高低` trait: 高/低 → 中.高/中.低 + +#### 虚线 (`structure::dash_line`) — 笔和线段的通用数据结构 +``` +虚线 { + 标识: RwLock, // "笔"/"线段"/"扩展线段"/"线段<线段>" 等 + 序号: AtomicI64, + 级别: AtomicI64, // 笔=1, 线段=2, 线段<线段>=3, ... + 文: Arc<分型>, // 起点分型(不可变) + 武: RwLock>, // 终点分型(可变,用于动态更新) + 有效性: AtomicBool, + 基础序列: RwLock>>, // 构成虚线的子级虚线序列 + 特征序列: RwLock>>>, // 线段特征序列 + 实_中枢序列: RwLock>>, + 虚_中枢序列: RwLock>>, + 合_中枢序列: RwLock>>, + 确认K线: RwLock>>, + 模式: RwLock, // 买卖点匹配模式 + _特征序列_显示: AtomicBool, + 前一缺口: RwLock>, + 前一结束位置: RwLock>>, + 短路修正: AtomicBool, +} +``` +- 静态工厂:`创建笔()` (级别=1), `创建线段()` (级别=笔级别+1, 基础序列=笔序列) +- 属性:`方向()`, `高()`, `低()`, `之前是()`, `之后是()` +- 数据访问:`获取普K序列()`, `获取缠K序列()`, `获取_武()` (递归到底层笔) +- 买卖点模式匹配:`全量/任意/配置/相对` 四种指标组合策略 +- MACD相关:`计算MACD柱子均值()`, `武之MACD均值()`, `武之MACD极值()` +- 趋势分析:`计算K线序列MACD趋向背驰()` (MACD柱/DIF/DEA三项背驰) +- 分段:`计算MACD柱子分段()` (按正负号分段) +- 行为统计:`统计MACD行为()` → `MACD行为统计 {DIF上下穿0, DEA上下穿0, 金叉/死叉次数, 密集交叉区域}` +- 核心判断:`买卖意义()` — LRU 缓存 128 条目,指针身份作为 key +- 序列化:`获取数据文本()` — 笔输出简洁格式,线段输出完整格式 +- `相等()` — 递归校验文/武/基础序列/特征序列/中枢序列/确认K线/模式/缺口/结束位置等全部字段 + +#### 线段特征 (`structure::segment_feat`) — 特征序列元素 +``` +线段特征 { + 序号: AtomicI64, + 标识: RwLock, + 线段方向: 相对方向, + 基础序列: Vec>, // 构成该元素的虚线序列 +} +``` +- 虚拟端点:`文()` — 取特征值最大/最小的起点分型 +- 虚拟端点:`武()` — 取特征值最大/最小的终点分型 +- tiebreaker: 同特征值时取后时间戳 +- `高()/低()` — 文/武中分型特征值的较大/较小者 +- `方向()` — 线段方向的翻转 +- 静态分析:从虚线序列生成特征序列元素列表,含分型替换逻辑 +- 分型序列:从连续特征序列元素中提取特征分型 + +#### 特征分型 (`structure::feat_fractal`) +``` +特征分型 { + 左: Arc<线段特征>, + 中: Arc<线段特征>, + 右: Arc<线段特征>, + 结构: 分型结构, // 由左中右三元素分析得出 +} +``` + +#### 中枢 (`algorithm::hub`) +``` +中枢 { + 序号: AtomicI64, + 标识: RwLock, // "笔中枢<0>"/"线段中枢<1>" 等 + 级别: AtomicI64, + 基础序列: RwLock>>, // 至少3根, 延伸后可达9+ + 第三买卖线: RwLock>>, + 本级_第三买卖线: RwLock>>, +} +``` +- 属性:`高()` (前三根虚线的最高重叠), `低()` (前三根虚线的最低重叠) +- `文()` → `武()` → `方向()` +- `延伸序列()` → 首尾之外的虚线 +- `获取数据文本()` — 完整中枢结构化描述 +- `相等()` — 逐项递归校验基础序列和第三买卖线 +- 向序列中添加/更新中枢(含显示名称映射) + +#### 基础买卖点 (`business::bsp`) +``` +基础买卖点 { + 备注: String, + 类型: 买卖点类型, + 买卖点分型: Arc<分型>, + 买卖点K线: Arc<缠论K线>, // = 买卖点分型.中 + 当前K线: Arc, + 失效K线: Option>, + 终结K线: Option>, + 破位值: f64, + 结构: Option<分型结构>, + 当前缠K序号: Option, +} +``` +- `偏移()` — 当前缠K序号与买卖点K线序号的差 +- `有效性()` — 失效K线是否存在 ### 服务类型 -| 类型 | 职责 | -|------|------| -| `观察者` | 单周期分析器,接收K线流,维护所有层级序列 | -| `K线合成器` | 将小周期K线合成为大周期K线 | -| `立体分析器` | 多周期联立分析,内含合成器+每周期一个观察者 | +| 类型 | 职责 | 位置 | +|------|------|------| +| `观察者` | 单周期分析器, 接收K线流, 维护所有层级序列, 3级递归线段分析 | `business::observer` | +| `K线合成器` | 小周期→大周期合成, 高取max/低取min/量求和, 时间戳对齐 | `business::synthesizer` | +| `立体分析器` | 多周期联立, 内含合成器 + 每周期一个观察者, 对齐缠K序列 | `business::multi_frame` | ### 静态算法类 -这些类型只包含 `#[staticmethod]` / `classmethod`,不创建实例: - -| 类型 | 职责 | -|------|------| -| `笔` | 笔划分算法:分型→笔 (递归分析) | -| `线段` | 线段划分算法:笔→线段 (特征序列+缺口处理) | -| `中枢` | 中枢识别/管理:虚线→中枢 (重叠检测+扩展) | -| `背驰分析` | 背驰检测:MACD/斜率/测度三种方式 | -| `指标` | K线取值工具:按计算方式取价格 | +| 类型 | 职责 | 位置 | +|------|------|------| +| `笔` | 笔划分: 分型→笔 (递归分析, 笔弱化, 次级成笔, 实际高/低点, 是否背驰过) | `algorithm::bi` | +| `线段` | 线段划分: 笔→线段 (特征序列+缺口处理+四种修正+扩展分析+内部背驰判断) | `algorithm::segment` | +| `中枢` | 中枢识别: 虚线→中枢 (重叠检测+延伸/扩展+第三买卖点+向序列添加) | `algorithm::hub` | +| `背驰分析` | 背驰检测: MACD/斜率/测度 + 全量/任意/配置/相对四种组合 | `algorithm::divergence` | +| `指标计算器` | 指标增量计算: MACD/RSI/KDJ/BOLL + 回填新指标 + 均线 | `indicators::calculator` | --- ## 配置体系 -`缠论配置` 是一个 serde 驱动的结构体,62 个字段全部带默认值,支持 JSON 往返。 +`缠论配置` 是一个 serde 驱动的结构体,**62 个字段**全部带默认值,支持 JSON 往返和部分反序列化容错。 -### 配置分组 +### 完整配置表 -```rust -let config = 缠论配置 { - // ---- 缠K ---- - 缠K合并替换: false, // false: 在原地合并, true: 产出新缠K +#### 基础设置 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `标识` | `"bar"` | 品种标识 | - // ---- 笔 ---- - 笔内元素数量: 5, // 成笔最低K线数 - 笔弱化: false, // 放宽成笔条件(弱化后最低3根) - 笔次级成笔: false, // 允许次高/次低成笔 - 笔内相同终点取舍: false, // false: 取first, true: 取last +#### 缠K设置 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `缠K合并替换` | `false` | false=原地修改, true=产出新缠K | - // ---- 线段 ---- - 线段_特征序列忽视老阴老阳: false, // 缺口时不严格处理包含 - 线段_缺口后紧急修正: true, // 缺口后自动修正 - 线段_修正: false, // 短路修正(不推荐) +#### 笔设置 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `笔内元素数量` | `5` | 成笔最低缠K数 | +| `笔内相同终点取舍` | `false` | false=取first, true=取last | +| `笔内起始分型包含整笔` | `false` | 起点分型区间必须包含整笔 | +| `笔内起始分型包含整笔_包括右` | `false` | 同上+包含右端点 | +| `笔内原始K线包含整笔` | `false` | 原始K线区间必须包含整笔 | +| `笔次级成笔` | `false` | 允许在非分型处成笔 | +| `笔弱化` | `false` | 放宽成笔条件 | +| `笔弱化_原始数量` | `3` | 弱化模式最小原始K线数 | - // ---- 分析开关 ---- - 分析笔: true, - 分析线段: true, - 分析扩展线段: true, - 分析笔中枢: true, - 分析线段中枢: true, +#### 线段设置 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `线段_非缺口下穿刺` | `false` | 非缺口下的穿刺处理 | +| `线段_特征序列忽视老阴老阳` | `false` | 缺口时不严格处理包含 | +| `线段_缺口后紧急修正` | `true` | 缺口后自动修正 | +| `线段_修正` | `false` | 短路修正(不推荐) | +| `线段内部中枢图显` | `true` | 线段内部中枢图表显示 | +| `扩展线段_当下分析` | `false` | 扩展线段实时分析模式 | - // ---- 指标参数 ---- - 平滑异同移动平均线_快线周期: 13, // MACD 快线 EMA 周期 - 平滑异同移动平均线_慢线周期: 31, // MACD 慢线 EMA 周期 - 平滑异同移动平均线_信号周期: 11, // MACD 信号线 EMA 周期 - 相对强弱指数_周期: 13, - 相对强弱指数_超买阈值: 75.0, - 相对强弱指数_超卖阈值: 25.0, - 随机指标_RSV周期: 13, - 随机指标_K值平滑周期: 5, - 随机指标_D值平滑周期: 5, - 随机指标_超买阈值: 80.0, - 随机指标_超卖阈值: 20.0, +#### 分析开关 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `分析笔` | `true` | | +| `分析线段` | `true` | | +| `分析扩展线段` | `true` | | +| `分析笔中枢` | `true` | | +| `分析线段中枢` | `true` | | - // ---- 买卖点 ---- - 买卖点偏移: 1, // 最大偏移量 - 买卖点_指标模式: "配置", // 配置 / 任意 / 全量 / 相对 - 买卖点_计算方式: "峰", // 峰 / 面 - 买卖点_背离率: f64::INFINITY, - 买卖点_中枢来源: "合", // 实 / 虚 / 合 +#### 终止 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `手动终止` | `""` | 手动终止时间字符串 | - // ---- 背驰 ---- - 线段内部背驰_MACD: true, - 线段内部背驰_斜率: true, - 线段内部背驰_测度: true, - 线段内部背驰_模式: "相对", // 全量 / 任意 / 配置 / 相对 +#### 指标设置 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `计算指标` | `true` | 是否计算技术指标 | +| `计算BOLL` | `false` | 是否计算布林带 | +| `指标计算方式` | `"收"` | 开/高/低/收/高低均值/高低收均值/开高低收均值 | - // ---- 图表推送 ---- - 图表展示: true, - 推送K线: true, - 推送笔: true, - 推送线段: true, - 推送中枢: true, +#### MACD 参数 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `平滑异同移动平均线_快线周期` | `13` | | +| `平滑异同移动平均线_慢线周期` | `31` | | +| `平滑异同移动平均线_信号周期` | `11` | | +| `MACD_参数列表` | `[]` | 多参数变体: `Vec<(key, 快, 慢, 信号)>` | - // ... 还有 20+ 个图表细分开关 - ..Default::default() -}; -``` +#### RSI 参数 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `相对强弱指数_周期` | `13` | RSI计算周期 | +| `相对强弱指数_移动平均线周期` | `13` | RSI SMA平滑周期 | +| `相对强弱指数_超买阈值` | `75.0` | | +| `相对强弱指数_超卖阈值` | `25.0` | | +| `RSI_周期列表` | `[]` | 多周期变体: `Vec<(key, 周期)>` | + +#### KDJ 参数 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `随机指标_RSV周期` | `13` | | +| `随机指标_K值平滑周期` | `5` | | +| `随机指标_D值平滑周期` | `5` | | +| `随机指标_超买阈值` | `80.0` | | +| `随机指标_超卖阈值` | `20.0` | | +| `KDJ_参数列表` | `[]` | 多参数变体 | + +#### BOLL 参数 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `布林带_周期` | `20` | | +| `布林带_标准差倍数` | `2.0` | | +| `BOLL_参数列表` | `[]` | 多参数变体 | + +#### 均线参数 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `均线_类型列表` | `[]` | `["SMA", "EMA", ...]` | +| `均线_周期列表` | `[]` | `[5, 10, 20, ...]` | + +#### 推送/图表显示 +| 字段 | 默认值 | +|------|--------| +| `图表展示` | `true` | +| `推送K线` | `true` | +| `推送笔` | `true` | +| `推送线段` | `true` | +| `推送中枢` | `true` | +| `图表展示_笔` | `true` | +| `图表展示_线段` | `true` | +| `图表展示_扩展线段` | `true` | +| `图表展示_扩展线段_线段` | `true` | +| `图表展示_线段_线段` | `true` | +| `图表展示_中枢_笔` | `true` | +| `图表展示_中枢_线段` | `true` | +| `图表展示_中枢_扩展线段` | `true` | +| `图表展示_中枢_扩展线段_线段` | `true` | +| `图表展示_中枢_线段_线段` | `true` | +| `图表展示_中枢_线段内部` | `true` | + +#### 买卖点设置 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `买卖点偏移` | `1` | 最大偏移量 | +| `买卖点激进识别` | `false` | | +| `买卖点与MACD柱强相关` | `false` | | +| `买卖点错过误差值` | `0.01` | | +| `买卖点_指标模式` | `"配置"` | 任意/配置/全量/相对 | +| `买卖点_指标匹配_MACD` | `true` | | +| `买卖点_指标匹配_KDJ` | `true` | | +| `买卖点_指标匹配_RSI` | `true` | | +| `买卖点_背离率` | `INFINITY` | 不使用 | +| `买卖点_T2_回调阈值` | `1.0` | | +| `买卖点_T2S_最大层级` | `3` | | +| `买卖点_峰值条件` | `false` | | +| `买卖点_计算方式` | `"峰"` | | +| `买卖点_计算线段BSP1` | `true` | | +| `买卖点_处理BSP2` | `true` | | +| `买卖点_计算线段BSP3` | `true` | | +| `买卖点_依赖T1` | `true` | | +| `买卖点_中枢来源` | `"合"` | 实/虚/合 | +| `买卖点_调试输出` | `false` | | + +#### 背驰设置 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `线段内部背驰_MACD` | `true` | 使用 MACD 背驰 | +| `线段内部背驰_斜率` | `true` | 使用斜率背驰 | +| `线段内部背驰_测度` | `true` | 使用测度背驰 | +| `线段内部背驰_模式` | `"相对"` | 任意/配置/全量/相对 | + +#### 文件 +| 字段 | 默认值 | 说明 | +|------|--------|------| +| `加载文件路径` | `""` | 数据文件路径 | ### 配置方法 ```rust // JSON 往返 -let json = config.to_json(); -let config2 = 缠论配置::from_json(&json)?; +let json = config.to_json(); // 序列化 +let config2 = 缠论配置::from_json(&json)?; // 反序列化 -// 文件持久化 -config.保存配置("/path/to/config.json")?; -let loaded = 缠论配置::加载配置("/path/to/config.json")?; +// 文件 I/O +config.保存配置("/path/to/config.json")?; // 保存 +let loaded = 缠论配置::加载配置("/path/to/config.json")?; // 加载 -// 静默模式(关闭所有推送) -let silent = config.不推送(); +// 部分反序列化(缺失字段→默认值) +let config: 缠论配置 = serde_json::from_str(r#"{"笔内元素数量": 7}"#)?; + +// 静默模式 +let silent = config.不推送(); // 所有推送/展示开关设为 false // 差异对比 -let diffs = config_a.对比(&config_b); -// → ["笔内元素数量", "买卖点偏移", ...] +let diffs = config_a.对比(&config_b); // → ["笔内元素数量", ...] + +// 多参数解析(列表非空则使用列表,否则使用默认单组) +let macd参数 = config._解析MACD参数列表(); // Vec<(key, 快, 慢, 信号)> +let rsi周期 = config._解析RSI周期列表(); // Vec<(key, 周期)> +let kdj参数 = config._解析KDJ参数列表(); // Vec<(key, RSV, K, D)> +let boll参数 = config._解析BOLL参数列表(); // Vec<(key, 周期, 倍数)> + +// 复合key重组(兼容旧版 "1_open" 格式) +let configs = 缠论配置::按序号重组字典(&default_config, &raw_json); ``` --- ## 算法模块 -### 笔划分 (`algorithm::bi`) +### 笔划分 (`algorithm::bi`, 1009 行) -核心递归算法 `笔.分析`,每次收到新分型时: -1. 检查顶/底分型交替 -2. 验证笔内K线数量 ≥ 配置阈值 -3. 确定实际高/低点(考虑笔弱化、次高点/次低点) -4. 检查笔内起始分型包含整笔条件 -5. 支持笔弱化策略:当严格条件不满足时,尝试放宽到3根 +`笔` 是一个纯静态方法命名空间,核心方法: -### 线段划分 (`algorithm::segment`) +| 方法 | 说明 | +|------|------| +| `分析()` | 主算法:接收分型,递归分析生成笔 | +| `_获取缠K数量()` | 计算可成笔的缠K数量(含弱化模式) | +| `_实际高点() / _实际低点()` | 潜在成笔区域的最高/最低缠K | +| `_获取成笔核心分型()` | 笔弱化模式下获取次高/次低成笔的候选分型 | +| `根据缠K找笔()` | 在笔序列中查找包含指定缠K的笔 | +| `是否背驰过()` | 检查笔范围内是否发生过MACD背驰 | -最复杂的递归算法,核心方法 `线段.分析`: -1. **特征序列提取**:从笔序列中提取同向特征序列(`线段特征.静态分析`) -2. **缺口处理**: - - 有缺口(老阴/老阳):严格包含处理,等待特征分型完成 - - 无缺口(小阳/少阴):允许逆序包含 -3. **四种修正机制**(依次尝试): - - `_缺口突破`:老阳/老阴时价格突破前线段极值 - - `_非缺口下穿刺`:贯穿伤后出现方向相同的3笔 - - `_缺口后紧急修正`:前一线段有缺口时自动修正 - - `_修正`:配置启用的短路修正(≥9笔) -4. **线段破坏判定**:新线段形成后,前一线段被破坏 +关键设计: +- 笔弱化:当严格条件不满足时,允许 `笔弱化_原始数量` (≥3) 根原始K线成笔 +- 次级成笔:允许次高/次低成笔的候选分型 +- 相同终点取舍:false=取first, true=取last (Python `list.index` 行为) +- 笔内验证:起始分型包含整笔 / 原始K线包含整笔 -### 中枢识别 (`algorithm::hub`) +### 线段划分 (`algorithm::segment`, 2078 行) -核心递归算法 `中枢.分析`: -1. **基础检查**:三条连续虚线,重叠且方向关系正确 -2. **中枢延伸**:在后继虚线中找离开中枢的虚线,未离开则加入基础序列 -3. **第三买卖点**:虚线离开中枢(向上缺口/向下缺口),记录第三买卖线 -4. **中枢完成**:第三买卖线确立后,在中枢之上的虚线开始新中枢 -5. **中枢扩展**:≥9段时调用线段扩展分析生成扩展中枢 -6. **多级中枢**:笔中枢、线段中枢、扩展线段中枢、线段线段中枢等 +`线段` 是**最复杂的算法模块**,核心方法: -### 背驰检测 (`algorithm::divergence`) +| 方法 | 说明 | +|------|------| +| `分析()` | 主算法:从笔序列计算线段 | +| `扩展分析()` | 扩展线段分析(不同的缺口处理策略) | +| `分割序列()` | 将特征序列分割为前/后/三/贯穿伤四个部分 | +| `特征序列状态()` | 返回 (特征_a, 特征_b, 特征_c) 布尔标志 | +| `四象()` | 判断特征序列的类型:老阳/老阴/小阳/少阴 | +| `获取缺口()` | 获取特征序列第一二元素间的缺口 | +| `判断线段内部是否背驰()` | 线段内部进入段vs离开段背驰判断 | +| `是否背驰过()` | 线段范围内是否发生过背驰 | +| `获取所有停顿位置()` | 获取线段内部所有可能的停顿位置 | +| `获取内部中枢序列()` | 获取线段内部的中枢序列 | -三种检测方式,可自由组合: +四种修正机制(按优先级): +1. `_缺口突破` — 老阳/老阴状态 + 价格突破前线段极值 → 线段完成 +2. `_非缺口下穿刺` — 贯穿伤存在 + 方向相同的3笔 → 线段完成 +3. `_缺口后紧急修正` — 前一线段有缺口 + 价格反向突破 → 修正前一结束位置 +4. `_修正` — 短线路径 (≥9笔) → 快速完成 + +### 中枢识别 (`algorithm::hub`, 1041 行) + +`中枢` 既是数据结构也是算法命名空间: + +| 方法 | 说明 | +|------|------| +| `分析()` | 主算法:虚线→中枢,同时识别第三买卖点 | +| `线段扩展分析()` | 中枢扩展:≥9段时生成扩展中枢 | +| `高()` | 前三根虚线重叠区域的最高值 | +| `低()` | 前三根虚线重叠区域的最低值 | +| `文()` | 第一根虚线的起点分型 | +| `武()` | 第三根虚线的终点分型 | +| `方向()` | 当中枢高 > 中枢低时为向上,否则向下 | +| `延伸序列()` | 首尾之外的虚线 | +| `获取离开虚线()` | 找到离开中枢的虚线(第三买卖点) | +| `获取剩余虚线()` | 第三买卖点之后的虚线 | +| `获取第N买卖线()` | 获取第N个买卖点相关的虚线 | + +中枢识别核心逻辑: +1. 基础检查:三条连续虚线必须重叠(`max(低) < min(高)`)且方向关系正确 +2. 中枢延伸:在后继虚线中找离开中枢的虚线,未离开则加入基础序列 +3. 第三买卖点:虚线离开中枢且不回中枢,记录 `第三买卖线` +4. 中枢完成:`第三买卖线` 确立后,在上方虚线开始新中枢 +5. 中枢扩展:≥9段时调用 `线段扩展分析` 生成扩展中枢 + +### 背驰检测 (`algorithm::divergence`, 227 行) + +`背驰分析` 提供三种检测方式: | 方式 | 原理 | 方法 | |------|------|------| -| MACD背驰 | 进入段vs离开段的MACD柱面积对比 | `MACD背驰` | -| 斜率背驰 | 价格变化速率减弱 | `斜率背驰` | -| 测度背驰 | 价格-时间向量长度缩减 | `测度背驰` | +| MACD背驰 | 进入段vs离开段的MACD柱面积对比 (`阳 + |阴|` 绝对值和) | `MACD背驰()` | +| 斜率背驰 | 价格变化速率 (dy/dx) 减弱 | `斜率背驰()` | +| 测度背驰 | 价格-时间向量长度 (√(dx²+dy²)) 缩减 | `测度背驰()` | -组合模式: -- **全量**:三种全满足 -- **任意**:任一种满足 -- **配置**:根据配置开关组合 -- **相对**:至少两种满足 +四种组合模式: + +| 模式 | 条件 | +|------|------| +| `全量` | MACD 且 斜率 且 测度 全满足 | +| `任意` | MACD 或 斜率 或 测度 任意满足 | +| `配置` | 根据 `线段内部背驰_MACD/斜率/测度` 配置组合 | +| `相对` | 至少两种方式满足(多数投票) | --- ## 技术指标 -三个指标均位于 `indicators/` 模块,支持增量计算(适合流式数据)。 +所有指标位于 `indicators/` 模块,均支持**首次计算**和**增量计算**两种模式。 -### MACD (`indicators::macd`) +### 统一指标容器 (`indicators::container`) + +基于注册表模式,通过 `指标值` 枚举统一管理五种指标类型: ```rust -pub struct 平滑异同移动平均线 { - // 输入 - pub 时间戳: i64, pub 收盘价: f64, - pub 快线周期: i64, pub 慢线周期: i64, pub 信号周期: i64, - - // 输出 - pub DIF: Option, // 快线EMA - 慢线EMA - pub DEA: Option, // DIF的信号线EMA - pub MACD柱: Option, // DIF - DEA - - // 计算中间值 (增量用) - pub 快线EMA: Option, - pub 慢线EMA: Option, - pub DEA_EMA: Option, +pub enum 指标值 { + MACD(平滑异同移动平均线), + RSI(相对强弱指数), + KDJ(随机指标), + BOLL(布林带), + 均线(HashMap), + 单值(HashMap), } ``` -### RSI (`indicators::rsi`) +- 预注册默认槽位:`macd`, `rsi`, `kdj`, `boll`, `均线`, `单值` +- 支持多参数变体:`MACD_13_31_11`, `RSI_14`, `KDJ_9_3_3` 等 +- 便捷方法:`macd()`, `rsi()`, `kdj()`, `boll()`, `均线()` +- 动态注册:`注册(name, default_value)`, `设置(name, value)` -```rust -pub struct 相对强弱指数 { - pub 时间戳: i64, pub 收盘价: f64, - pub 周期: i64, - pub 超买阈值: f64, pub 超卖阈值: f64, +### 指标计算器 (`indicators::calculator`, 476 行) - pub RSI: Option, - pub RSI_SMA: Option, // RSI的SMA信号线 +`指标计算器::计算并挂载(全序列, 配置)` 是一次完整的指标计算周期: - // Wilder平滑中间值 - pub 平均上涨: Option, - pub 平均下跌: Option, +1. **MACD组** — 遍历 `_解析MACD参数列表()`,对每个 (key, 快, 慢, 信号) 计算 +2. **RSI组** — 遍历 `_解析RSI周期列表()` +3. **KDJ组** — 遍历 `_解析KDJ参数列表()` +4. **BOLL组** — 遍历 `_解析BOLL参数列表()` +5. **均线** — 遍历 `均线_类型列表 × 均线_周期列表`,MA/EMA 计算 +6. **回填新指标** — 如果当前K线有前序K线遗漏的指标,回溯填充 + +K线取值 (`K线取值()`) 支持 7 种计算方式: +`开` / `高` / `低` / `收` / `高低均值` / `高低收均值` / `开高低收均值` + +### MACD (`indicators::macd`, 215 行) + +``` +平滑异同移动平均线 { + 时间戳, 收盘价, 快线周期, 慢线周期, 信号周期, + DIF: Option, // 快线EMA - 慢线EMA + DEA: Option, // DIF的信号线EMA + MACD柱: f64, // 2 * (DIF - DEA) + 快线EMA: Option, // 计算中间值(增量用) + 慢线EMA: Option, + DEA_EMA: Option, } ``` -### KDJ (`indicators::kdj`) +- `首次计算(收盘价, 时间戳, 快线周期, 慢线周期, 信号周期)` — 初始EMA=SMA近似 +- `增量计算(prev, 收盘价, 时间戳)` — EMA递推公式 +- `计算K线MACD(普K序列, 快, 慢, 信号)` — 遍历序列批量计算 -```rust -pub struct 随机指标 { - pub 时间戳: i64, pub 最高价: f64, pub 最低价: f64, pub 收盘价: f64, - pub RSV周期: i64, pub K值平滑周期: i64, pub D值平滑周期: i64, - pub 超买阈值: f64, pub 超卖阈值: f64, +### RSI (`indicators::rsi`, 240 行) - pub K: Option, // RSV的移动平均 - pub D: Option, // K的移动平均 - pub J: Option, // 3*K - 2*D +``` +相对强弱指数 { + 时间戳, 收盘价, 周期, + 超买阈值, 超卖阈值, + RSI: Option, + RSI_SMA: Option, // RSI的SMA信号线 + 平均上涨: Option, // Wilder平滑中间值(增量用) + 平均下跌: Option, } ``` -所有三个指标都提供 `首次计算` → `增量计算` 的流式计算接口,以及对应的 `_K线` 便捷版本。 +- Wilder SMA 平滑算法 +- `首次计算()` — 初始均值=SMA +- `增量计算()` — Wilder递推 + +### KDJ (`indicators::kdj`, 289 行) + +``` +随机指标 { + 时间戳, 最高价, 最低价, 收盘价, + RSV周期, K值平滑周期, D值平滑周期, + 超买阈值, 超卖阈值, + K: Option, // RSV的移动平均 + D: Option, // K的移动平均 + J: Option, // 3*K - 2*D +} +``` + +- `首次计算(全体)` — 从序列计算初始KDJ值 +- `增量计算(全体, 当前, 周期, K平滑, D平滑)` — 增量更新 + +### BOLL (`indicators::boll`, 169 行) + +``` +布林带 { + 时间戳, 收盘价, 周期, + 标准差倍数, + 中轨: Option, // SMA + 上轨: Option, // 中轨 + 倍数 * 标准差 + 下轨: Option, // 中轨 - 倍数 * 标准差 +} +``` + +--- + +## 线程安全与并发 + +### 内部可变性策略 + +缠K/虚线/中枢等结构体需要在多 `Arc` 共享时支持原地修改(如笔动态替换武、中枢延伸添加虚线)。Rust 中 `Arc` 的 `&self` 是不可变的,因此大量字段使用以下策略实现内部可变性: + +| 机制 | 类型 | 适用场景 | +|------|------|---------| +| `SyncF64` | `AtomicU64` 位转换 | 价格字段 (高/低/分型特征值),需跨线程原子读写 | +| `AtomicI64` | 原子整数 | 序号/时间戳/原始结束序号 | +| `AtomicBool` | 原子布尔 | 有效性/短路修正/特征序列显示 | +| `RwLock` | 读写锁 | 复杂字段 (Arc<分型>/Vec/标识/模式/中枢序列) | + +### 全局缓存 + +- **买卖意义缓存**: `LazyLock>>` — 128条目,key为 (虚线指针, 观察者指针) 地址对 +- **与 Python 对齐**:Python 使用 `@lru_cache(maxsize=128)` 装饰器,Rust 使用全局 `LazyLock>` 替代 PyO3 的 `thread_local!`(解决跨线程不可见问题) + +### Send + Sync 保证 + +所有核心数据结构(`缠论K线`, `虚线`, `中枢`, `观察者`)均实现 `Send + Sync`,可以安全地跨线程传递和共享: + +```rust +// 编译期断言 +fn _需要_Send_Sync() {} +_需要_Send_Sync::<缠论K线>(); +_需要_Send_Sync::<虚线>(); +_需要_Send_Sync::<中枢>(); +``` + +`观察者` 使用 `Arc>` 模式,多线程可以并发读取同一个观察者的分析结果。 + +### RwLock 死锁预防 + +Rust 的 `RwLock` 是**非递归锁**。在已有读锁的线程中尝试获取写锁会死锁。核心规则: +- 将读锁作用域化:在所有调用带写锁的方法之前释放 `prev_guard` +- 典型模式:`{ let prev_guard = ... ; /* 使用 prev_guard */ } // 作用域结束释放 → 安全调用回填` --- ## Python 绑定 -Python 调用通过独立的 [`chanlun-py`](../chanlun-py/) crate 提供,使用 PyO3 包装: +Python 调用通过独立的 [`chanlun-py`](../chanlun-py/) crate 提供,使用 PyO3 包装。所有类型、方法、字段名与 `chan.py` **完全一致**,API 兼容。 ```bash cd chanlun-py -cargo build --release -cp target/release/libchanlun.so ~/.local/lib/python3.14/site-packages/chanlun/_chanlun.cpython-314-x86_64-linux-gnu.so +maturin develop --release ``` ```python import chanlun -# 创建配置 +# 创建/修改配置 config = chanlun.缠论配置() config.笔内元素数量 = 7 +config.MACD_参数列表 = [("my_macd", 12, 26, 9)] # 创建观察者 obs = chanlun.观察者("BTCUSD", 3600, config) +# 通过 Python 属性直接修改配置(自动同步到核心层) +obs.配置.笔弱化 = True + # 投喂K线 k = chanlun.K线.创建普K("bar", 1736640000, 50000, 51000, 49000, 50500, 100, 0, 3600) obs.增加原始K线(k) # 获取分析结果 print(f"笔数量: {len(obs.笔序列)}") +print(f"线段数量: {len(obs.线段序列)}") print(f"中枢数量: {len(obs.中枢序列)}") + +# 全局开关 +chanlun.分型模式 = False # 从缠K实时读取分型值(逆序包含后使用) ``` -所有类型、方法、字段名与 `chan.py` 完全一致,API 兼容。 +### 绑定层特性 + +- **配置缓存**: `obs.配置` 返回同一个 Python 对象(而非每次新建),确保 Python 端修改后 Rust 端可见 +- **分型模式**: 模块级 `chanlun.分型模式` getter/setter,控制分型值的读取策略 +- **数据文件兼容**: `chanlun.读取数据文件("file.nb", config, 观察员=obs)` 与 Python 版签名一致 +- **跨线程身份**: 全局 `LazyLock` 保证跨线程 `is` 判断一致 --- @@ -489,23 +971,29 @@ print(f"中枢数量: {len(obs.中枢序列)}") ### K线二进制格式 -K 线以大端字节序 `struct.pack(">6d")` 格式序列化,每根 48 字节: +与 Python `struct.pack(">6d")` 完全兼容的大端字节序格式,每根K线 48 字节: | 偏移 | 大小 | 字段 | |------|------|------| -| 0 | 8 | 时间戳 (Unix秒 as f64) | -| 8 | 8 | 开盘价 | -| 16 | 8 | 最高价 | -| 24 | 8 | 最低价 | -| 32 | 8 | 收盘价 | -| 40 | 8 | 成交量 | +| 0 | 8 字节 | 时间戳 (Unix秒 as f64) | +| 8 | 8 字节 | 开盘价 | +| 16 | 8 字节 | 最高价 | +| 24 | 8 字节 | 最低价 | +| 32 | 8 字节 | 收盘价 | +| 40 | 8 字节 | 成交量 | ```rust // 序列化 -let bytes = k线.to_bytes(); // Vec, 48 bytes +let bytes: [u8; 48] = k线.to_bytes(); // 反序列化 -let k线 = K线::读取大端字节数组(&bytes, 3600, "bar"); +let k线 = K线::读取大端字节数组(&bytes, 周期, 标识); + +// 解析原始数据(不构造K线) +let (时间戳, 开, 高, 低, 收, 量) = K线::解析原始数据(&bytes)?; + +// 批量保存 +K线::保存到DAT文件("output.dat", &[&k1, &k2])?; ``` ### 配置 JSON @@ -515,11 +1003,55 @@ let k线 = K线::读取大端字节数组(&bytes, 3600, "bar"); "标识": "BTCUSD", "笔内元素数量": 5, "平滑异同移动平均线_快线周期": 13, - "平滑异同移动平均线_慢线周期": 31 + "平滑异同移动平均线_慢线周期": 31, + "买卖点_指标模式": "配置", + "线段内部背驰_模式": "相对" } ``` -使用 `#[serde(default)]` 实现前向/后向兼容,缺失字段自动使用默认值。 +- 使用 `#[serde(default)]` 实现前向/后向兼容,缺失字段自动使用默认值 +- 字符串枚举字段(`指标计算方式`, `买卖点_指标模式`, `线段内部背驰_模式`)带有自定义反序列化器,非法值自动回退默认值并发出 `warn!` 日志 + +### 结构化相等校验 + +所有核心结构体都实现了 `相等(&self, other, 浮点容差) → (bool, String)` 方法,递归校验包括: +- 浮点字段使用容差比较(而非直接 `==`) +- `Arc` 指针共享的结构体深度校验值语义 +- 嵌套容器(Vec, Option, HashMap)逐一比对 +- 返回详细的差异描述,便于调试 + +--- + +## 测试 + +### 核心层测试 (`chanlun/`) + +```bash +cargo test # 运行所有测试 +cargo test -- test_重复计算 # 运行匹配模式的测试 +``` + +测试覆盖: +- **配置**: 默认值、JSON 往返、部分反序列化、非法值回退、不推送模式 +- **K线**: 方向判定、大端字节序列化往返 +- **缠K**: 创建/镜像/包含合并/分型识别、三K线生成分型 +- **分型**: 左中右构造、字段读写、关联指针 +- **虚线**: Cell/RefCell 读写、Clone 后 Rc 指针一致、多 Arc 共享可见性、获取武递归、武修改不影响文 +- **线段特征**: 文取max/min, 武取max/min, tiebreaker, 添加/删除 +- **中枢**: 字段读写、Clone 后指针一致、多 Arc 共享 +- **买卖点**: 偏移/失效偏移/有效性、18种类型生成 +- **观察者**: 指针一致性 (普K/分型/笔/线段/中枢)、重复计算确定性、重置后数据一致、RefCell 借用安全、跨线程 Send/Sync + +### Python 绑定层测试 (`chanlun-py/tests/test_all.py`) + +```bash +cd chanlun-py && maturin develop +python3 -m pytest tests/test_all.py -v +# 107+ tests, 涵盖: +# - 指标挂载与回填 +# - 跨线程身份一致性 (is 判断) +# - 买卖意义/趋向背驰/行为统计/停顿位置/线段内部背驰等 双端(Rust绑定 vs chan.py)数据对比 +``` --- @@ -532,5 +1064,5 @@ MIT License. 详见 [LICENSE](LICENSE). ## 相关项目 - [`chan.py`](../chan.py) — Python 参考实现(~4200 行) -- [`chanlun-py`](../chanlun-py/) — PyO3 绑定,将本 crate 导出为 Python 模块 -- [`main.py`](../main.py) — Python 图表程序入口 +- [`chanlun-py`](../chanlun-py/) — PyO3 绑定,将本 crate 导出为 Python `chanlun` 模块 +- [`main.py`](../main.py) — Python 图表可视化程序 diff --git a/chanlun/src/algorithm/divergence.rs b/chanlun/src/algorithm/divergence.rs index 455d3cb..cecc45f 100644 --- a/chanlun/src/algorithm/divergence.rs +++ b/chanlun/src/algorithm/divergence.rs @@ -222,6 +222,6 @@ struct MACD面积 { impl MACD面积 { fn 总(&self) -> f64 { - self.阳 + self.阴 + self.阳 + self.阴.abs() } } diff --git a/chanlun/src/algorithm/segment.rs b/chanlun/src/algorithm/segment.rs index e9b2745..8689f95 100644 --- a/chanlun/src/algorithm/segment.rs +++ b/chanlun/src/algorithm/segment.rs @@ -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>, Vec>, Vec>); -/// 线段内部背驰判断缓存 — 仿照 dash_line.rs 的 买卖意义缓存 模式 -static 线段背驰缓存: LazyLock>> = - 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 { diff --git a/chanlun/src/indicators/calculator.rs b/chanlun/src/indicators/calculator.rs index 6ea33af..1515809 100644 --- a/chanlun/src/indicators/calculator.rs +++ b/chanlun/src/indicators/calculator.rs @@ -34,18 +34,35 @@ pub struct 指标计算器; impl 指标计算器 { /// 增量计算所有开启的指标,将结果写入 当前K线.指标 /// - /// `现有序列` 不包含当前K线;prev 取自 现有序列.last() + /// `全序列` 包含当前K线(在末尾);prev 取自 全序列[..-1].last() /// 通过 RwLock 内部可变性,以 `&K线` 共享引用写入指标值 - pub fn 计算并挂载(当前K线: &K线, 现有序列: &[Arc], 配置: &缠论配置) { - 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], 配置: &缠论配置) { + 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], 配置: &缠论配置) { + // 作用域化首尾读锁:在回填写循环之前释放,避免读锁与写锁冲突 + 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); + } + } + } } diff --git a/chanlun/src/kline/chan_kline.rs b/chanlun/src/kline/chan_kline.rs index b33a900..d93f2c1 100644 --- a/chanlun/src/kline/chan_kline.rs +++ b/chanlun/src/kline/chan_kline.rs @@ -408,8 +408,7 @@ impl 缠论K线 { } // 计算指标: 对齐 Python,仅当 计算指标 开启时执行 if 配置.计算指标 { - let n = 普K序列.len(); - 指标计算器::计算并挂载(&普K序列[n - 1], &普K序列[..n - 1], 配置); + 指标计算器::计算并挂载(普K序列, 配置); } // ---- 阶段2: 缠K合并 ---- diff --git a/main.py b/main.py index d916601..6f11477 100644 --- a/main.py +++ b/main.py @@ -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: