diff --git a/btcusd-300-1761327300-1776327900.nb b/btcusd-300-1761327300-1776327900.nb new file mode 100644 index 0000000..5fb16cb Binary files /dev/null and b/btcusd-300-1761327300-1776327900.nb differ diff --git a/btcusd-300-1777649100-1778398800.nb b/btcusd-300-1777649100-1778398800.nb new file mode 100644 index 0000000..1a4d494 Binary files /dev/null and b/btcusd-300-1777649100-1778398800.nb differ diff --git a/chan.py b/chan.py new file mode 100644 index 0000000..9030f78 --- /dev/null +++ b/chan.py @@ -0,0 +1,4184 @@ +""" +MIT License + +Copyright (c) 2026 YuYuKunKun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +# -*- coding: utf-8 -*- +# @Time : 2024/10/15 16:45 +# @Author : YuYuKunKun +# @File : chan.py + +import json +import math +import struct +import sys +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from functools import lru_cache +from pathlib import Path +from typing import ( + List, + Self, + Optional, + Tuple, + final, + Dict, + Any, + Union, + Sequence, + Callable, +) + +from pydantic import BaseModel, Field, model_validator, ValidationError, field_validator +from termcolor import colored + + +__all__ = [ + "K线", + "K线合成器", + "中枢", + "买卖点", + "买卖点类型", + "分型", + "分型结构", + "基础买卖点", + "平滑异同移动平均线", + "指标", + "测试_读取数据", + "特征分型", + "相对强弱指数", + "相对方向", + "立体分析器", + "笔", + "线段", + "线段特征", + "缠论K线", + "缠论配置", + "缺口", + "背驰分析", + "虚线", + "观察者", + "转化为时间戳", + "转化为时间戳_数字", + "随机指标", +] + + +class 买卖点类型(str, Enum): + # 传统分类 + 一买 = "一买" + 一卖 = "一卖" + 二买 = "二买" + 二卖 = "二卖" + 三买 = "三买" + 三卖 = "三卖" + # 缠论六类买卖点 + T1买 = "T1买" + T1卖 = "T1卖" + T1P买 = "T1P买" + T1P卖 = "T1P卖" + T2买 = "T2买" + T2卖 = "T2卖" + T2S买 = "T2S买" + T2S卖 = "T2S卖" + T3A买 = "T3A买" + T3A卖 = "T3A卖" + T3B买 = "T3B买" + T3B卖 = "T3B卖" + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return self.name + + @property + def 是买点(self) -> bool: + return "买" in self.value + + @property + def 是卖点(self) -> bool: + return "卖" in self.value + + +class 基础买卖点: + def __init__(self, 类型: 买卖点类型, 当前K线: "K线", 买卖点分型: "分型", 备注: str, 中枢破位值: float): + self.备注: str = 备注 + self.类型 = 类型 + self.买卖点分型 = 买卖点分型 + self.买卖点K线 = 买卖点分型.中 # .镜像 + self.__当前K线: "K线" = 当前K线 + self.失效K线: Optional["K线"] = None + self.终结K线: Optional["K线"] = None # 卖出 or 买入 + self.__破位值 = 中枢破位值 + self.结构 = None + + def __str__(self): + return f"{self.类型.value}<{self.买卖点K线}, {self.偏移}, {self.失效偏移}>" + + def __repr__(self): + return f"{self.类型.value}<{self.买卖点K线}, {self.偏移}, {self.失效偏移}>" + + @property + def 当前K线(self): + return self.__当前K线 + + @property + def 破位值(self) -> float: + return self.__破位值 + + @property + def 偏移(self) -> int: + return self.__当前K线.序号 - self.买卖点K线.序号 + + @property + def 失效偏移(self) -> int: + if self.失效K线 is None: + return -1 + return self.失效K线.序号 - self.买卖点K线.序号 + + @property + def 有效性(self) -> bool: + return self.失效K线 is not None + + @property + def 与MACD柱子匹配(self) -> bool: + return self.买卖点K线.与MACD柱子匹配 + + @property + def 与MACD柱子分型匹配(self) -> bool: + return self.买卖点分型.与MACD柱子分型匹配 + + +@final +class 买卖点(基础买卖点): + @classmethod + def 一卖点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + return 买卖点(备注=备注, 类型=买卖点类型.一卖, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) + + @classmethod + def 一买点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + return 买卖点(备注=备注, 类型=买卖点类型.一买, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) + + @classmethod + def 二卖点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + return 买卖点(备注=备注, 类型=买卖点类型.二卖, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) + + @classmethod + def 二买点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + return 买卖点(备注=备注, 类型=买卖点类型.二买, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) + + @classmethod + def 三卖点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + return 买卖点(备注=备注, 类型=买卖点类型.三卖, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) + + @classmethod + def 三买点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + return 买卖点(备注=备注, 类型=买卖点类型.三买, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) + + @classmethod + def 生成买卖点(cls, 特征: str, 序号: str, 级别: str, 买卖点分型: "分型", 当前缠K: "缠论K线"): + 买卖 = "买" if 买卖点分型.结构 in (分型结构.底, 分型结构.下) else "卖" + 第几 = 序号 + 备注 = f"{特征}_{级别}{第几}{买卖}" + 买卖点函数 = getattr(买卖点, f"{第几}{买卖}点") + 破位值 = 买卖点分型.分型特征值 + return 买卖点函数(买卖点分型, 当前缠K, 特征, 备注, 破位值) + + +class datetime(datetime): # 用于对齐C输出 + def __str__(self): + return f"{int(self.timestamp())}" + + def __repr__(self): + return f"{int(self.timestamp())}" + + +def 转化为时间戳(ts: Union[str, datetime, int, float]) -> datetime: + """ + 将不同类型的时间戳转换为datetime对象(统一比较标准) + 支持:datetime对象、字符串("%Y-%m-%d %H:%M:%S")、数值型时间戳(秒级) + 可根据实际需求扩展时间格式(如毫秒级、仅日期等) + """ + if isinstance(ts, datetime): + return ts + elif isinstance(ts, str): + # 自定义时间字符串格式,按需修改(如"%Y-%m-%d") + return datetime.strptime(ts, "%Y-%m-%d %H:%M:%S") + elif isinstance(ts, (int, float)): + # 若为毫秒级时间戳,需除以1000:return datetime.fromtimestamp(ts / 1000) + return datetime.fromtimestamp(ts) + else: + raise TypeError(f"不支持的时间戳类型: {type(ts)}") + + +def 转化为时间戳_数字(ts: Union[str, datetime, int, float]) -> int: + """ + 将不同类型的时间戳转换为datetime对象(统一比较标准) + 支持:datetime对象、字符串("%Y-%m-%d %H:%M:%S")、数值型时间戳(秒级) + 可根据实际需求扩展时间格式(如毫秒级、仅日期等) + """ + if isinstance(ts, datetime): + return int(ts.timestamp()) + elif isinstance(ts, str): + # 自定义时间字符串格式,按需修改(如"%Y-%m-%d") + return int(datetime.strptime(ts, "%Y-%m-%d %H:%M:%S").timestamp()) + elif isinstance(ts, (int, float)): + # 若为毫秒级时间戳,需除以1000:return datetime.fromtimestamp(ts / 1000) + return int(ts) + else: + raise TypeError(f"不支持的时间戳类型: {type(ts)}") + + +@final +class 缠论配置(BaseModel): + 标识: str = "bar" + 缠K合并替换: bool = False # False: 在原缠K上合并, True: 产出新缠K + + 笔内元素数量: int = 5 # 成BI最低长度 + + 笔内相同终点取舍: bool = False # 一笔终点存在多个终点时 True: last, False: first + 笔内起始分型包含整笔: bool = False # True: 一笔起始分型高低包含整支笔对象则不成笔, False: 只判断分型中间数据是否包含 + 笔内起始分型包含整笔_包括右: bool = False # True: 将笔之武.右纳入 + 笔内原始K线包含整笔: bool = False # 在非 [笔内起始分型包含整笔] 时判断原始K线包含整笔的情况 + + 笔次级成笔: bool = False + 笔弱化: bool = False + 笔弱化_原始数量: int = 3 + # 笔_必须对齐:bool = False # 强迫症设为True, 将获得无与伦比满足。。。 + + 线段_非缺口下穿刺: bool = False # True: 非缺口状态下[小阳, 少阴]时,存在贯穿伤与之后紧邻的三个元素有方向相同的线段时回退, 此举在当下是否有任何意义呢? + 线段_特征序列忽视老阴老阳: bool = False # True 不用严格的特征序列包含,也就是忽视缺口全以无缺口对待 + 线段_缺口后紧急修正: bool = True # True: 当 线段_特征序列忽视老阴老阳=False 时生效,同样 线段_特征序列忽视老阴老阳=True时等同于修正武斗不异常,但只产出一个线段 + 线段_修正: bool = False # 短路修正,不建议使用,但此修正将走势显示的更加清晰 + + 线段内部中枢图显: bool = True + 扩展线段_当下分析: bool = False # 以当下来看的分析规则,否则以事后来看 + + 分析笔: bool = True # 是否计算BI + 分析线段: bool = True # 是否计算XD + 分析扩展线段: bool = True # 是否计算XD + 分析笔中枢: bool = True # 是否计算BI中枢 + 分析线段中枢: bool = True # 是否计算XD中枢 + + 手动终止: str = "" # 2099-12-31 00:00:00 + 计算指标: bool = True + 指标计算方式: str = "收" # (开, 高, 低, 收, 高低均值, 高低收均值, 开高低收均值), 默认 收盘价 + 平滑异同移动平均线_快线周期: int = 13 + 平滑异同移动平均线_慢线周期: int = 31 + 平滑异同移动平均线_信号周期: int = 11 + + 相对强弱指数_周期: int = 13 + 相对强弱指数_移动平均线周期: int = 13 + 相对强弱指数_超买阈值: float = 75.0 + 相对强弱指数_超卖阈值: float = 25.0 + + 随机指标_RSV周期: int = 13 + 随机指标_K值平滑周期: int = 5 + 随机指标_D值平滑周期: int = 5 + 随机指标_超买阈值: float = 80.0 + 随机指标_超卖阈值: float = 20.0 + + 图表展示: bool = True + 推送K线: bool = True + 推送笔: bool = True + 推送线段: bool = True + 推送中枢: bool = True + + 图表展示_笔: bool = True + 图表展示_线段: bool = True + 图表展示_扩展线段: bool = True + 图表展示_扩展线段_线段: bool = True + 图表展示_线段_线段: bool = True + + 图表展示_中枢_笔: bool = True + 图表展示_中枢_线段: bool = True + 图表展示_中枢_扩展线段: bool = True + 图表展示_中枢_扩展线段_线段: bool = True + 图表展示_中枢_线段_线段: bool = True + 图表展示_中枢_线段内部: bool = True + + 买卖点偏移: int = 1 # 最大偏移 + 买卖点激进识别: bool = False # 激进模式下将不考虑分型的完整性 + 买卖点与MACD柱强相关: bool = False # True: 卖点需正值 买点需负值 + 买卖点错过误差值: float = 0.01 # 距离买卖点值上下之内 + + 买卖点_指标模式: str = "配置" # 【任意,配置,全量, 相对】 对应K线 + 买卖点_指标匹配_MACD: bool = True # 买在负,卖在正! + 买卖点_指标匹配_KDJ: bool = True # 买在死叉之后,卖在金叉之后 + 买卖点_指标匹配_RSI: bool = True # 买在均线之下,卖在均线之上 + + # --- 缠论六类买卖点识别参数 --- + 买卖点_背离率: float = float("inf") # T1/T1P背离率,默认无限大(不筛选) + 买卖点_T2_回调阈值: float = 1.0 # T2回调幅度/突破振幅上限 + 买卖点_T2S_最大层级: int = 3 # T2S最大搜索深度 + 买卖点_峰值条件: bool = False # T1是否要求突破中枢内所有元素的极值 + 买卖点_计算方式: str = "峰" # "峰"=最大MACD柱绝对值, "面"=MACD柱面积 + 买卖点_计算线段BSP1: bool = True # 是否启用T1/T1P识别 + 买卖点_处理BSP2: bool = True # 是否启用T2/T2S识别 + 买卖点_计算线段BSP3: bool = True # 是否启用T3A/T3B识别 + 买卖点_依赖T1: bool = True # T2是否必须依赖T1 + 买卖点_中枢来源: str = "合" # "实"/"虚"/"合" - 取线段的哪个内部中枢序列 + 买卖点_调试输出: bool = False # 是否打印调试信息 + + 线段内部背驰_MACD: bool = True + 线段内部背驰_斜率: bool = True + 线段内部背驰_测度: bool = True + 线段内部背驰_模式: str = "相对" # 【任意,配置,全量,相对】 + + 加载文件路径: str = "./templates/last.nb" + + @model_validator(mode="before") + def 兼容旧版本配置(cls, values: Dict[str, Any]) -> Dict[str, Any]: + """ + 自动兼容: + 1. 旧版本少字段 → 使用默认值 + 2. 新版本多字段 → 自动忽略多余字段 + 3. 字段改名/删除 → 不报错 + """ + return values + + @field_validator("*", mode="wrap") + def bool_parse_fallback_default(cls, value, handler, info): + field = cls.model_fields.get(info.field_name) + if not field: + return handler(value) + 允许值 = { + "指标计算方式": ["开", "高", "低", "收", "高低均值", "高低收均值", "开高低收均值"], + } + # 字段类型 & 默认值 + type_ = field.annotation + default = field.default + fname = info.field_name + + try: + # --- 1. 处理 bool:使用内置 bool_parsing --- + if type_ is bool: + return handler(value) + + # --- 2. 处理 int:使用内置 int_parsing --- + elif type_ is int: + return handler(value) + + # --- 3. 处理指定 str:必须在允许列表内 --- + elif fname in 允许值: + result = handler(value) + if result not in 允许值[fname]: + raise ValueError(f"值不在允许范围内: {result}") + return result + + # 其他类型不处理 + else: + return handler(value) + + # 验证失败 → 统一用默认值 + except ValidationError as e: + if "bool_parsing" in str(e) or "int_parsing" in str(e): + print(f"[{fname}] = {value} 解析失败,使用默认值:{default}") + return default + raise + + # 字符串不在允许列表 → 用默认值 + except ValueError as e: + print(f"[{fname}] = {value} {str(e)},使用默认值:{default}") + return default + + def to_dict(self) -> dict: + """对象 → 字典""" + return self.model_dump() + + def to_json(self) -> str: + """对象 → JSON字符串""" + return self.model_dump_json(ensure_ascii=False, indent=2) + + def 保存配置(self, path="缠论配置.json"): + with open(path, "w", encoding="utf-8") as f: + f.write(self.to_json()) + + @staticmethod + def 加载配置(path="缠论配置.json") -> "缠论配置": + with open(path, "r", encoding="utf-8") as f: + return 缠论配置.from_json(f.read()) + + @classmethod + def from_dict(cls, data: dict) -> "缠论配置": + """字典 → 对象(自动兼容缺失/多余字段)""" + return cls(**data) + + @classmethod + def from_json(cls, json_str: str) -> "缠论配置": + """JSON字符串 → 对象""" + data = json.loads(json_str) + return cls.from_dict(data) + + @classmethod + def 不推送(cls): + return cls( + 线段内部中枢图显=False, + 图表展示=False, + 推送K线=False, + 推送笔=False, + 推送线段=False, + 推送中枢=False, + 图表展示_笔=False, + 图表展示_线段=False, + 图表展示_扩展线段=False, + 图表展示_扩展线段_线段=False, + 图表展示_线段_线段=False, + 图表展示_中枢_笔=False, + 图表展示_中枢_线段=False, + 图表展示_中枢_扩展线段=False, + 图表展示_中枢_扩展线段_线段=False, + 图表展示_中枢_线段_线段=False, + 图表展示_中枢_线段内部=False, + ) + + @classmethod + def 按序号重组字典(cls, 默认配置, 原始字典: dict) -> dict: + """ + { + "1_open": 10, + "1_close": 11, + "2_open": 20, + "name": "BTC", # 无法拆分 + "time": 123456 # 无法拆分 + } + 转化为 + { + 1: {"open": 10, "close": 11}, + 2: {"open": 20}, + "无法拆分": { + "name": "BTC", + "time": 123456 + } + } + + """ + + 结果 = {} + 无法拆分项 = {} + + for 复合键, 值 in 原始字典.items(): + # 尝试拆分 + if "_" in 复合键: + 序号部分, 键部分 = 复合键.split("_", 1) + try: + 序号 = int(序号部分) + # 能正常拆分 → 分组 + if 序号 not in 结果: + 结果[序号] = {} + 结果[序号][键部分] = 值 + except: + # 格式异常 → 单独处理 + 无法拆分项[复合键] = 值 + else: + # 无下划线 → 无法拆分 → 单独存放 + 无法拆分项[复合键] = 值 + + # 把无法拆分的也放进结果顶层(你要的结构) + """if 无法拆分项: + 结果["无法拆分"] = 无法拆分项""" + 配置组 = dict() + for k, v in 结果.items(): + 配置组[k] = 默认配置.model_copy( + update=v, + deep=True, + ) + + return 配置组 + + def 对比(self, other: "缠论配置") -> dict: + """ + 比较当前配置与另一个配置的差异 + 返回: { + "字段名": { + "旧值": 当前配置的值, + "新值": 另一个配置的值 + }, + ... + } + 仅当值不同时才包含该字段 + """ + diff_dict = {} + # 获取所有字段名 + for field_name in self.model_fields.keys(): + old_value = getattr(self, field_name) + new_value = getattr(other, field_name) + # 直接比较值(支持 None) + if old_value != new_value: + diff_dict[field_name] = new_value + return diff_dict + + +class 相对方向(Enum): + 向上 = "交叠向上" + 向下 = "交叠向下" + 向上缺口 = "向上缺口" + 向下缺口 = "向下缺口" + 衔接向上 = "衔接向上" # 前终点为后起点 + 衔接向下 = "衔接向下" # 前终点为后起点 + 顺 = "顺序包含" # 左边包含右边 + 逆 = "逆序包含" # 右边包含左边 + 同 = "相同包含" # 左右两边数值相同 + + def __str__(self): + return f"相对方向.{self.name}" + + def __repr__(self): + return f"相对方向.{self.name}" + + def 翻转(self) -> "相对方向": # 反转 + match self: + case 相对方向.向上: + return 相对方向.向下 + case 相对方向.向下: + return 相对方向.向上 + case 相对方向.向下缺口: + return 相对方向.向上缺口 + case 相对方向.向上缺口: + return 相对方向.向下缺口 + case 相对方向.衔接向上: + return 相对方向.衔接向下 + case 相对方向.衔接向下: + return 相对方向.衔接向上 + case 相对方向.顺: + return 相对方向.逆 + case 相对方向.逆: + return 相对方向.顺 + case _: + return self + + def 是否向上(self) -> bool: + return self in (相对方向.向上, 相对方向.向上缺口, 相对方向.衔接向上) + + def 是否向下(self) -> bool: + return self in (相对方向.向下, 相对方向.向下缺口, 相对方向.衔接向下) + + def 是否包含(self) -> bool: + return self in (相对方向.顺, 相对方向.逆, 相对方向.同) + + def 是否缺口(self) -> bool: + return self in (相对方向.向下缺口, 相对方向.向上缺口) + + def 是否衔接(self) -> bool: + return self in (相对方向.衔接向下, 相对方向.衔接向上) + + @classmethod + def 分析(cls, 前高: float, 前低: float, 后高: float, 后低: float) -> "相对方向": + if 前高 == 后高 and 前低 == 后低: + return 相对方向.同 + + if 前高 > 后高 and 前低 > 后低: + if 前低 == 后高: + return 相对方向.衔接向下 + if 前低 > 后高: + return 相对方向.向下缺口 + return 相对方向.向下 + + if 前高 < 后高 and 前低 < 后低: + if 前高 == 后低: + return 相对方向.衔接向上 + if 前高 < 后低: + return 相对方向.向上缺口 + return 相对方向.向上 + + if 前高 >= 后高 and 前低 <= 后低: + return 相对方向.顺 + + if 前高 <= 后高 and 前低 >= 后低: + return 相对方向.逆 + raise RuntimeError("无法识别的方向") + + +class 分型结构(Enum): + 上 = "三连向上" + 下 = "三连向下" + 顶 = "顶分型" + 底 = "底分型" + 散 = "向右扩散" + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + @classmethod + def 分析(cls, 左, 中, 右, 可以逆序包含: bool = False, 忽视顺序包含: bool = False) -> Optional["分型结构"]: + 左中关系 = 相对方向.分析(左.高, 左.低, 中.高, 中.低) + 中右关系 = 相对方向.分析(中.高, 中.低, 右.高, 右.低) + # 左右关系 = 相对方向.分析(左.高, 左.低, 右.高, 右.低) + match (左中关系, 中右关系): + case (相对方向.顺, _): + if 忽视顺序包含: + ... # print("顺序包含 左中相对方向", 左, 中) + else: + raise ValueError("顺序包含 左中相对方向", 左, 中) + case (_, 相对方向.顺): + if 忽视顺序包含: + ... # print("顺序包含 中右相对方向", 中, 右) + else: + raise ValueError("顺序包含 中右相对方向", 中, 右) + + case (相对方向.向上 | 相对方向.向上缺口 | 相对方向.衔接向上, 相对方向.向上 | 相对方向.向上缺口 | 相对方向.衔接向上): + return 分型结构.上 + case (相对方向.向上 | 相对方向.向上缺口 | 相对方向.衔接向上, 相对方向.向下 | 相对方向.向下缺口 | 相对方向.衔接向下): + return 分型结构.顶 + case (相对方向.向上 | 相对方向.向上缺口 | 相对方向.衔接向上, 相对方向.逆) if 可以逆序包含: + return 分型结构.上 + + case (相对方向.向下 | 相对方向.向下缺口 | 相对方向.衔接向下, 相对方向.向上 | 相对方向.向上缺口 | 相对方向.衔接向上): + return 分型结构.底 + case (相对方向.向下 | 相对方向.向下缺口 | 相对方向.衔接向下, 相对方向.向下 | 相对方向.向下缺口 | 相对方向.衔接向下): + return 分型结构.下 + case (相对方向.向下 | 相对方向.向下缺口 | 相对方向.衔接向下, 相对方向.逆) if 可以逆序包含: + return 分型结构.下 + + case (相对方向.逆, 相对方向.向上 | 相对方向.向上缺口 | 相对方向.衔接向上) if 可以逆序包含: + return 分型结构.底 + case (相对方向.逆, 相对方向.向下 | 相对方向.向下缺口 | 相对方向.衔接向下) if 可以逆序包含: + return 分型结构.顶 + case (相对方向.逆, 相对方向.逆) if 可以逆序包含: + return 分型结构.散 + case _: + print("未匹配的关系", 可以逆序包含, 左中关系, 中右关系) + return None + + +@final +class 缺口: + def __init__(self, 高: float, 低: float) -> None: + assert 高 > 低 + self.高 = 高 + self.低 = 低 + + def __str__(self) -> str: + return f"缺口区间<{self.低:g} <=> {self.高:g}>" + + def __repr__(self) -> str: + return f"缺口区间<{self.低:g} <=> {self.高:g}>" + + @classmethod + def 居中截取区间(cls, 起点: float, 终点: float, 比例: float = 0.15) -> Optional[缺口]: + """ + 以原区间中心为基准,向两侧各取总长度的 `比例` 作为新区间。 + 如果新区间超出原边界则裁剪到边界;若完全越界则返回 None。 + + 参数: + 起点: 区间起始值 + 终点: 区间结束值 + 比例: 向两侧扩展的长度占总长度的比例(0~1 之间) + + 返回: + Optional[缺口] + """ + if 起点 > 终点: + 起点, 终点 = 终点, 起点 + 总长 = 终点 - 起点 + 偏移 = 总长 * 比例 + 中心 = (起点 + 终点) / 2 + 下界 = 中心 - 偏移 + 上界 = 中心 + 偏移 + + # 完全越界:新区间与原区间无重叠 + if 下界 > 终点 or 上界 < 起点: + return None + + # 裁剪到原边界 + if 下界 < 起点: + 下界 = 起点 + if 上界 > 终点: + 上界 = 终点 + + 高 = max(下界, 上界) + 低 = min(下界, 上界) + return 缺口(高, 低) + + +class 指标: + @classmethod + def K线取值(cls, k线: "K线", 指标计算方式): + match 指标计算方式: + case "开": + return k线.开盘价 + case "高": + return k线.高 + case "低": + return k线.低 + case "收": + return k线.收盘价 + case "高低均值": + return (k线.高 + k线.低) / 2 + case "高低收均值": + return (k线.高 + k线.低 + k线.收盘价) / 3 + case "开高低收均值": + return (k线.高 + k线.低 + k线.开盘价 + k线.收盘价) / 4 + case _: + return k线.收盘价 + + +class 平滑异同移动平均线(BaseModel): + # 原始数据 + 时间戳: datetime = Field(..., description="数据点对应的时间") + 收盘价: float = Field(..., description="当前K线的收盘价格") + + # 计算参数 + 快线周期: int = Field(12, gt=0, description="短期EMA周期,默认为12") + 慢线周期: int = Field(26, gt=0, description="长期EMA周期,默认为26") + 信号周期: int = Field(9, gt=0, description="信号线EMA周期,默认为9") + + # 核心指标值 + DIF: Optional[float] = Field(None, description="差离值 = EMA(快线) - EMA(慢线)") + DEA: Optional[float] = Field(None, description="信号线 = EMA(DIF, 信号周期)") + MACD柱: Optional[float] = Field(None, description="MACD柱状图 = (DIF - DEA) * 2") + + # EMA中间值(用于增量计算) + 快线EMA: Optional[float] = Field(None, description="快线EMA的当前值") + 慢线EMA: Optional[float] = Field(None, description="慢线EMA的当前值") + DEA_EMA: Optional[float] = Field(None, description="DEA的EMA当前值") + + # 模型配置 + model_config = { + "arbitrary_types_allowed": True, # 允许特殊类型 + "json_encoders": { + datetime: lambda v: v.isoformat(), # 日期时间序列化 + Enum: lambda v: v.value, # 枚举值序列化 + }, + } + + @classmethod + def 首次计算(cls, 初始收盘价: float, 初始时间: datetime, 快线周期: int = 12, 慢线周期: int = 26, 信号周期: int = 9) -> "平滑异同移动平均线": + """ + 首次计算MACD指标(没有历史数据时使用) + + :param 初始收盘价: 第一个数据点的收盘价 + :param 初始时间: 第一个数据点的时间戳 + :param 快线周期: + :param 慢线周期: + :param 信号周期: + :return: MACD指标对象 + """ + # 初始化EMA值(使用第一个收盘价) + 快线EMA = 初始收盘价 + 慢线EMA = 初始收盘价 + + # 计算DIF(差离值) + DIF = 快线EMA - 慢线EMA # 首次计算为0 + + # 初始化DEA(信号线) + DEA_EMA = DIF # 首次计算等于DIF + + # 计算MACD柱 + MACD柱 = 2 * (DIF - DEA_EMA) # 首次计算为0 + + return cls( + 时间戳=初始时间, + 收盘价=初始收盘价, + 快线周期=快线周期, + 慢线周期=慢线周期, + 信号周期=信号周期, + DIF=DIF, + DEA=DEA_EMA, + MACD柱=MACD柱, + 快线EMA=快线EMA, + 慢线EMA=慢线EMA, + DEA_EMA=DEA_EMA, + ) + + @classmethod + def 首次计算_K线(cls, k线: "K线", 计算方式: str, 快线周期: int = 12, 慢线周期: int = 26, 信号周期: int = 9) -> "平滑异同移动平均线": + 初始收盘价: float = 指标.K线取值(k线, 计算方式) + 初始时间: datetime = k线.时间戳 + return cls.首次计算(初始收盘价, 初始时间, 快线周期, 慢线周期, 信号周期) + + @classmethod + def 增量计算(cls, 前一个MACD: "平滑异同移动平均线", 当前收盘价: float, 当前时间: datetime) -> "平滑异同移动平均线": + """ + 基于前一个MACD指标增量计算当前MACD指标 + 适用于实时交易系统或流式数据处理 + + :param 前一个MACD: 前一个周期的MACD指标对象 + :param 当前收盘价: 当前K线的收盘价 + :param 当前时间: 当前K线的时间戳 + :return: 当前MACD指标对象 + """ + + # 计算EMA的平滑系数 + def 平滑系数(周期): + return 2 / (周期 + 1) + + # 计算快线EMA + if 前一个MACD.快线EMA is None: + 快线EMA = 当前收盘价 + raise RuntimeError + else: + # 快线EMA = 当前收盘价 * 平滑系数(前一个MACD.快线周期) + 前一个MACD.快线EMA * (1 - 平滑系数(前一个MACD.快线周期)) + # 快线EMA = (当前收盘价 - 前一个MACD.快线EMA) * 平滑系数(前一个MACD.快线周期) + 前一个MACD.快线EMA + 快线EMA = 当前收盘价 * 平滑系数(前一个MACD.快线周期) + 前一个MACD.快线EMA * ((前一个MACD.快线周期 - 1) / (前一个MACD.快线周期 + 1)) + + # 计算慢线EMA + if 前一个MACD.慢线EMA is None: + 慢线EMA = 当前收盘价 + raise RuntimeError + else: + # 慢线EMA = 当前收盘价 * 平滑系数(前一个MACD.慢线周期) + 前一个MACD.慢线EMA * (1 - 平滑系数(前一个MACD.慢线周期)) + # 慢线EMA = (当前收盘价 - 前一个MACD.慢线EMA) * 平滑系数(前一个MACD.慢线周期) + 前一个MACD.慢线EMA + 慢线EMA = 当前收盘价 * 平滑系数(前一个MACD.慢线周期) + 前一个MACD.慢线EMA * ((前一个MACD.慢线周期 - 1) / (前一个MACD.慢线周期 + 1)) + + # 计算DIF + DIF = 快线EMA - 慢线EMA + + # 计算DEA的EMA + if 前一个MACD.DEA_EMA is None: + DEA_EMA = DIF + else: + # DEA_EMA = 当前收盘价 * 平滑系数(前一个MACD.信号周期) + 前一个MACD.DEA_EMA * (1 - 平滑系数(前一个MACD.信号周期)) + # DEA_EMA = (DIF - 前一个MACD.DEA_EMA) * 平滑系数(前一个MACD.信号周期) + 前一个MACD.DEA_EMA + DEA_EMA = DIF * 平滑系数(前一个MACD.信号周期) + 前一个MACD.DEA_EMA * ((前一个MACD.信号周期 - 1) / (前一个MACD.信号周期 + 1)) + + # 计算MACD柱 + MACD柱 = DIF - DEA_EMA # * 2 + + return cls( + 时间戳=当前时间, + 收盘价=当前收盘价, + 快线周期=前一个MACD.快线周期, + 慢线周期=前一个MACD.慢线周期, + 信号周期=前一个MACD.信号周期, + DIF=DIF, + DEA=DEA_EMA, + MACD柱=MACD柱, + 快线EMA=快线EMA, + 慢线EMA=慢线EMA, + DEA_EMA=DEA_EMA, + ) + + @classmethod + def 增量计算_K线(cls, 前一个MACD: "平滑异同移动平均线", 当前K线: "K线", 计算方式: "str") -> "平滑异同移动平均线": + 当前收盘价: float = 指标.K线取值(当前K线, 计算方式) + 当前时间: datetime = 当前K线.时间戳 + return cls.增量计算(前一个MACD, 当前收盘价, 当前时间) + + +class 相对强弱指数(BaseModel): + """ + 相对强弱指数 (RSI) 指标 + 使用 Wilder 平滑(RMA)进行增量计算,提供完整的中间平滑值, + 并支持对RSI值计算SMA(简单移动平均) + """ + + # 原始数据 + 时间戳: datetime = Field(..., description="数据点对应的时间") + 收盘价: float = Field(..., description="当前K线的收盘价格") + + # 参数 + 周期: int = Field(14, gt=0, description="RSI周期,默认为14") + 超买阈值: float = Field(70.0, description="超买阈值") + 超卖阈值: float = Field(30.0, description="超卖阈值") + RSI_SMA周期: Optional[int] = Field(None, description="RSI的SMA周期(可选),用于生成信号线") + + # 核心指标值 + RSI: Optional[float] = Field(None, description="当前RSI值") + + # 中间平滑值(Wilder平滑) + 平均上涨: Optional[float] = Field(None, description="平均上涨幅度的平滑值") + 平均下跌: Optional[float] = Field(None, description="平均下跌幅度的平滑值") + + # 原始变化值(用于调试) + 上涨幅度: float = Field(0.0, description="当前价格变化中的上涨部分") + 下跌幅度: float = Field(0.0, description="当前价格变化中的下跌部分") + + # 平滑系数(α = 1/周期) + 平滑系数: float = Field(0.0, description="Wilder平滑系数") + + # RSI的SMA(信号线)相关字段 + RSI_SMA: Optional[float] = Field(None, description="RSI的简单移动平均值") + RSI历史队列: List[float] = Field(default_factory=list, description="用于计算SMA的RSI历史队列") + + model_config = { + "arbitrary_types_allowed": True, + "json_encoders": { + datetime: lambda v: v.isoformat(), + Enum: lambda v: v.value, + }, + } + + @classmethod + def 首次计算(cls, 初始收盘价: float, 初始时间: datetime, 周期: int = 14, 超买阈值: float = 70.0, 超卖阈值: float = 30.0, RSI_SMA周期: Optional[int] = None) -> "相对强弱指数": + """ + 首次计算RSI(没有足够历史数据时使用) + 此时无法计算真实RSI,设为 None,但记录初始收盘价作为起点 + """ + return cls( + 时间戳=初始时间, + 收盘价=初始收盘价, + 周期=周期, + RSI=None, + 平均上涨=0.0, + 平均下跌=0.0, + 上涨幅度=0.0, + 下跌幅度=0.0, + 平滑系数=1.0 / 周期, + 超买阈值=超买阈值, + 超卖阈值=超卖阈值, + RSI_SMA周期=RSI_SMA周期, + RSI_SMA=None, + RSI历史队列=[], + ) + + @classmethod + def 首次计算_K线(cls, k线: "K线", 计算方式: str, 周期: int = 14, 超买阈值: float = 70.0, 超卖阈值: float = 30.0, RSI_SMA周期: Optional[int] = None) -> "相对强弱指数": + 初始收盘价: float = 指标.K线取值(k线, 计算方式) + 初始时间: datetime = k线.时间戳 + return cls.首次计算(初始收盘价, 初始时间, 周期, 超买阈值, 超卖阈值, RSI_SMA周期) + + @classmethod + def 增量计算(cls, 前一个RSI: "相对强弱指数", 当前收盘价: float, 当前时间: datetime) -> "相对强弱指数": + """ + 基于前一个RSI指标增量计算当前RSI + 支持可选的RSI_SMA(简单移动平均) + """ + # 复制参数 + 周期 = 前一个RSI.周期 + 超买阈值 = 前一个RSI.超买阈值 + 超卖阈值 = 前一个RSI.超卖阈值 + RSI_SMA周期 = 前一个RSI.RSI_SMA周期 + 平滑系数 = 1.0 / 周期 + + # 计算价格变化 + 变化 = 当前收盘价 - 前一个RSI.收盘价 + 上涨 = max(变化, 0) + 下跌 = max(-变化, 0) + + # 更新平均上涨和平均下跌(Wilder平滑) + if 前一个RSI.平均上涨 is None or 前一个RSI.平均下跌 is None: + 平均上涨 = 上涨 + 平均下跌 = 下跌 + else: + 平均上涨 = 前一个RSI.平均上涨 * (1 - 平滑系数) + 上涨 * 平滑系数 + 平均下跌 = 前一个RSI.平均下跌 * (1 - 平滑系数) + 下跌 * 平滑系数 + + # 计算RSI + if 平均下跌 == 0: + RSI = 100.0 if 平均上涨 > 0 else 50.0 + else: + RS = 平均上涨 / 平均下跌 + RSI = 100 - (100 / (1 + RS)) + + # ----- 计算RSI的SMA(简单移动平均) ----- + RSI_SMA = None + 历史队列 = 前一个RSI.RSI历史队列.copy() if 前一个RSI.RSI历史队列 else [] + if RSI_SMA周期 is not None and RSI_SMA周期 > 0 and RSI is not None: + # 将当前RSI加入队列 + 历史队列.append(RSI) + # 保持队列长度不超过周期 + if len(历史队列) > RSI_SMA周期: + 历史队列.pop(0) + # 计算SMA(即使队列未满也计算当前平均值) + if 历史队列: + RSI_SMA = sum(历史队列) / len(历史队列) + else: + # 未启用SMA,清空队列 + 历史队列 = [] + + return cls( + 时间戳=当前时间, + 收盘价=当前收盘价, + 周期=周期, + RSI=RSI, + 平均上涨=平均上涨, + 平均下跌=平均下跌, + 上涨幅度=上涨, + 下跌幅度=下跌, + 平滑系数=平滑系数, + 超买阈值=超买阈值, + 超卖阈值=超卖阈值, + RSI_SMA周期=RSI_SMA周期, + RSI_SMA=RSI_SMA, + RSI历史队列=历史队列, + ) + + @classmethod + def 增量计算_K线(cls, 前一个RSI: "相对强弱指数", 当前K线: "K线", 计算方式: "str") -> "相对强弱指数": + 当前收盘价: float = 指标.K线取值(当前K线, 计算方式) + 当前时间: datetime = 当前K线.时间戳 + return cls.增量计算(前一个RSI, 当前收盘价, 当前时间) + + +class 随机指标(BaseModel): + """ + KDJ 随机指标 (Stochastic Oscillator) + 使用标准参数:N=9, M1=3, M2=3 + 支持增量计算,需提供当前K线的最高价、最低价、收盘价 + """ + + # 原始数据 + 时间戳: datetime = Field(..., description="数据点对应的时间") + 最高价: float = Field(..., description="当前K线的最高价") + 最低价: float = Field(..., description="当前K线的最低价") + 收盘价: float = Field(..., description="当前K线的收盘价") + + # 参数 + N: int = Field(9, ge=1, description="RSV的周期(取最近N根K线)") + M1: int = Field(3, ge=1, description="K值的平滑周期") + M2: int = Field(3, ge=1, description="D值的平滑周期") + 超买阈值: float = Field(80.0, description="超买阈值") + 超卖阈值: float = Field(20.0, description="超卖阈值") + + # 核心指标值 + RSV: Optional[float] = Field(None, description="未成熟随机值") + K: Optional[float] = Field(None, description="K值(快速随机指标)") + D: Optional[float] = Field(None, description="D值(慢速随机指标)") + J: Optional[float] = Field(None, description="J值 = 3K - 2D") + + # 中间状态(用于增量计算) + 历史最高价队列: list[float] = Field(default_factory=list, description="最近N根K线的最高价队列") + 历史最低价队列: list[float] = Field(default_factory=list, description="最近N根K线的最低价队列") + 前一个RSV: Optional[float] = Field(None, description="上一个RSV值(用于平滑K)") + 前一个K: Optional[float] = Field(None, description="上一个K值") + 前一个D: Optional[float] = Field(None, description="上一个D值") + + model_config = { + "arbitrary_types_allowed": True, + "json_encoders": { + datetime: lambda v: v.isoformat(), + Enum: lambda v: v.value, + }, + } + + @classmethod + def 首次计算(cls, 初始最高价: float, 初始最低价: float, 初始收盘价: float, 初始时间: datetime, N: int = 9, M1: int = 3, M2: int = 3, 超买阈值: float = 80.0, 超卖阈值: float = 20.0) -> "随机指标": + """ + 首次计算KDJ(无历史数据时) + 此时无法计算RSV和K/D/J,仅记录初始三价,初始化队列 + """ + # 初始化历史队列,放入当前三价 + return cls( + 时间戳=初始时间, + 最高价=初始最高价, + 最低价=初始最低价, + 收盘价=初始收盘价, + N=N, + M1=M1, + M2=M2, + 超买阈值=超买阈值, + 超卖阈值=超卖阈值, + RSV=None, + K=None, + D=None, + J=None, + 历史最高价队列=[初始最高价], + 历史最低价队列=[初始最低价], + 前一个RSV=None, + 前一个K=None, + 前一个D=None, + ) + + @classmethod + def 首次计算_K线(cls, k线: "K线", 计算方式: str, RSV周期: int = 9, K值平滑周期: int = 3, D值平滑周期: int = 3, 超买阈值: float = 80.0, 超卖阈值: float = 20.0) -> "随机指标": + 初始最高价: float = k线.高 + 初始最低价: float = k线.低 + 初始收盘价: float = k线.收盘价 + 初始时间: datetime = k线.时间戳 + return cls.首次计算(初始最高价, 初始最低价, 初始收盘价, 初始时间, RSV周期, K值平滑周期, D值平滑周期, 超买阈值, 超卖阈值) + + @classmethod + def 增量计算(cls, 前一个KDJ: "随机指标", 当前最高价: float, 当前最低价: float, 当前收盘价: float, 当前时间: datetime) -> "随机指标": + """ + 基于前一个KDJ对象和当前三价,增量计算当前KDJ值 + """ + # 复制参数 + N = 前一个KDJ.N + M1 = 前一个KDJ.M1 + M2 = 前一个KDJ.M2 + 超买阈值 = 前一个KDJ.超买阈值 + 超卖阈值 = 前一个KDJ.超卖阈值 + + # 更新历史最高价队列 + 历史最高价 = 前一个KDJ.历史最高价队列.copy() + 历史最高价.append(当前最高价) + if len(历史最高价) > N: + 历史最高价.pop(0) + + # 更新历史最低价队列 + 历史最低价 = 前一个KDJ.历史最低价队列.copy() + 历史最低价.append(当前最低价) + if len(历史最低价) > N: + 历史最低价.pop(0) + + # 计算RSV(需要队列长度达到N才能计算) + RSV = None + if len(历史最高价) == N and len(历史最低价) == N: + highest = max(历史最高价) + lowest = min(历史最低价) + if highest != lowest: + RSV = (当前收盘价 - lowest) / (highest - lowest) * 100 + else: + RSV = 50.0 # 如果最高=最低,RSV取50 + + # 计算K值(使用前一天的K值平滑,首次使用RSV) + K = None + if RSV is not None: + if 前一个KDJ.K is None: + K = RSV # 首次K值等于RSV + else: + # 标准公式:K = 2/3 * 前一日K + 1/3 * 当日RSV + K = (前一个KDJ.K * (M1 - 1) + RSV) / M1 + else: + # 数据不足时,K值沿用前一个(若没有则为None) + K = 前一个KDJ.K + + # 计算D值(使用前一天的D值平滑,首次使用K值) + D = None + if K is not None: + if 前一个KDJ.D is None: + D = K # 首次D值等于K + else: + # 标准公式:D = 2/3 * 前一日D + 1/3 * 当日K + D = (前一个KDJ.D * (M2 - 1) + K) / M2 + else: + D = 前一个KDJ.D + + # 计算J值 + J = None + if K is not None and D is not None: + J = 3 * K - 2 * D + + return cls( + 时间戳=当前时间, + 最高价=当前最高价, + 最低价=当前最低价, + 收盘价=当前收盘价, + N=N, + M1=M1, + M2=M2, + 超买阈值=超买阈值, + 超卖阈值=超卖阈值, + RSV=RSV, + K=K, + D=D, + J=J, + 历史最高价队列=历史最高价, + 历史最低价队列=历史最低价, + 前一个RSV=RSV, + 前一个K=K, + 前一个D=D, + ) + + @classmethod + def 增量计算_K线(cls, 前一个KDJ: "随机指标", 当前K线: "K线", 计算方式: "str") -> "随机指标": + 当前最高价: float = 当前K线.高 + 当前最低价: float = 当前K线.低 + 当前收盘价: float = 当前K线.收盘价 + 当前时间: datetime = 当前K线.时间戳 + return cls.增量计算(前一个KDJ, 当前最高价, 当前最低价, 当前收盘价, 当前时间) + + +class 背驰分析: + @staticmethod + def MACD背驰(进入段: "虚线", 离开段: "虚线", K线序列: List["K线"], 方式: str = "总") -> bool: + """MACD柱状线面积背驰""" + 进入MACD = K线.获取MACD(K线序列, 进入段.文.中.标的K线, 进入段.武.中.标的K线) + 离开MACD = K线.获取MACD(K线序列, 离开段.文.中.标的K线, 离开段.武.中.标的K线) + + # 计算面积(绝对值求和) + 进入面积 = abs(进入MACD["总"] if 方式 == "总" else (进入MACD["阳"] if 进入段.方向 is 相对方向.向上 else 进入MACD["阴"])) + 离开面积 = abs(离开MACD["总"] if 方式 == "总" else (离开MACD["阳"] if 进入段.方向 is 相对方向.向上 else 离开MACD["阴"])) + + return 离开面积 < 进入面积 + + @staticmethod + def 斜率背驰(进入段: "虚线", 离开段: "虚线") -> bool: + """价格斜率背驰""" + # 计算线段的速度 + dx = 进入段.武.时间戳.timestamp() - 进入段.文.时间戳.timestamp() # self.武.时间戳 - self.文.时间戳 # 时间差 + dy = 进入段.武.分型特征值 - 进入段.文.分型特征值 # 价格差 + 进入斜率 = dy / dx + # 计算线段的速度 + dx = 离开段.武.时间戳.timestamp() - 离开段.文.时间戳.timestamp() # self.武.时间戳 - self.文.时间戳 # 时间差 + dy = 离开段.武.分型特征值 - 离开段.文.分型特征值 # 价格差 + 离开斜率 = dy / dx + + if 进入段.方向 == 相对方向.向上: + if 离开段.高 > 进入段.高 and abs(离开斜率) < abs(进入斜率): + return True + else: + if 离开段.低 < 进入段.低 and abs(离开斜率) < abs(进入斜率): + return True + return False + + @staticmethod + def 测度背驰(进入段: "虚线", 离开段: "虚线") -> bool: + """价格斜率背驰""" + dx = 进入段.武.时间戳.timestamp() - 进入段.文.时间戳.timestamp() # 时间差 self.武.中.标的K线.序号 - self.文.中.标的K线.序号 # + dy = 进入段.武.分型特征值 - 进入段.文.分型特征值 # 价格差的绝对值 + 进入测度 = math.sqrt(dx * dx + dy * dy) + + dx = 离开段.武.时间戳.timestamp() - 离开段.文.时间戳.timestamp() # 时间差 self.武.中.标的K线.序号 - self.文.中.标的K线.序号 # + dy = 离开段.武.分型特征值 - 离开段.文.分型特征值 # 价格差的绝对值 + 离开测度 = math.sqrt(dx * dx + dy * dy) + + if 进入段.方向 == 相对方向.向上: + if 离开段.高 > 进入段.高 and abs(离开测度) < abs(进入测度): + return True + else: + if 离开段.低 < 进入段.低 and abs(离开测度) < abs(进入测度): + return True + return False + + @staticmethod + def 全量背驰(进入段: "虚线", 离开段: "虚线", 普K序列: List["K线"]) -> bool: + return all([背驰分析.MACD背驰(进入段, 离开段, 普K序列), 背驰分析.测度背驰(进入段, 离开段), 背驰分析.斜率背驰(进入段, 离开段)]) + + @staticmethod + def 任意背驰(进入段: "虚线", 离开段: "虚线", 普K序列: List["K线"]) -> bool: + return any([背驰分析.MACD背驰(进入段, 离开段, 普K序列), 背驰分析.测度背驰(进入段, 离开段), 背驰分析.斜率背驰(进入段, 离开段)]) + + @staticmethod + def 配置背驰(进入段: "虚线", 离开段: "虚线", 普K序列: List["K线"], 配置: 缠论配置) -> bool: + match 配置.线段内部背驰_MACD, 配置.线段内部背驰_测度, 配置.线段内部背驰_斜率: + case True, True, True: + return 背驰分析.MACD背驰(进入段, 离开段, 普K序列) and 背驰分析.测度背驰(进入段, 离开段) and 背驰分析.斜率背驰(进入段, 离开段) + case False, False, False: + ... + + case True, False, True: + return 背驰分析.MACD背驰(进入段, 离开段, 普K序列) and 背驰分析.斜率背驰(进入段, 离开段) + case False, True, False: + return 背驰分析.测度背驰(进入段, 离开段) + + case True, False, False: + return 背驰分析.MACD背驰(进入段, 离开段, 普K序列) + case False, True, True: + return 背驰分析.测度背驰(进入段, 离开段) and 背驰分析.斜率背驰(进入段, 离开段) + + case False, False, True: + return 背驰分析.斜率背驰(进入段, 离开段) + case True, True, False: + return 背驰分析.MACD背驰(进入段, 离开段, 普K序列) and 背驰分析.测度背驰(进入段, 离开段) + + return False + + @staticmethod + def 任选背驰(进入段: "虚线", 离开段: "虚线", 普K序列: List["K线"]) -> bool: + 混沌槽 = [背驰分析.MACD背驰(进入段, 离开段, 普K序列), 背驰分析.测度背驰(进入段, 离开段), 背驰分析.斜率背驰(进入段, 离开段)] + return len([背驰 for 背驰 in 混沌槽 if 背驰]) >= 2 + + @staticmethod + def 背驰模式(进入段: "虚线", 离开段: "虚线", 普K序列: List["K线"], 配置: 缠论配置, 模式: str) -> bool: + match 模式: + case "全量": + return 背驰分析.全量背驰(进入段, 离开段, 普K序列) + case "任意": + return 背驰分析.任意背驰(进入段, 离开段, 普K序列) + case "配置": + return 背驰分析.配置背驰(进入段, 离开段, 普K序列, 配置) + case "相对": + return 背驰分析.任选背驰(进入段, 离开段, 普K序列) + return False + + +class K线(object): + __slots__ = ["标识", "序号", "周期", "时间戳", "高", "低", "开盘价", "收盘价", "成交量", "macd", "rsi", "kdj"] + + def __init__( + self, + 标识: str, + 序号: int, + 周期: int, + 时间戳: datetime, + 开盘价: float, + 最高价: float, + 最低价: float, + 收盘价: float, + 成交量: float, + macd: 平滑异同移动平均线 = None, + rsi: 相对强弱指数 = None, + kdj: 随机指标 = None, + ): + self.序号: int = 序号 + self.标识: str = 标识 + self.时间戳: datetime = 时间戳 + self.开盘价: float = 开盘价 + self.高: float = 最高价 + self.低: float = 最低价 + self.收盘价: float = 收盘价 + self.成交量: float = 成交量 + self.周期: int = 周期 + self.macd: 平滑异同移动平均线 = macd + self.rsi: 相对强弱指数 = rsi + self.kdj: 随机指标 = kdj + + def __str__(self): + return f"{self.标识}<{self.序号}, {self.周期}, {self.方向}, {self.时间戳}, {self.开盘价:g}, {self.高:g}, {self.低:g}, {self.收盘价:g}>" + + def __repr__(self): + return f"{self.标识}<{self.序号}, {self.周期}, {self.方向}, {self.时间戳}, {self.开盘价:g}, {self.高:g}, {self.低:g}, {self.收盘价:g}>" + + @property + def 方向(self) -> 相对方向: + return 相对方向.向上 if self.开盘价 < self.收盘价 else 相对方向.向下 + + def __bytes__(self): + return struct.pack( + ">6d", + int(self.时间戳.timestamp()), + round(self.开盘价, 8), + round(self.高, 8), + round(self.低, 8), + round(self.收盘价, 8), + round(self.成交量, 8), + ) + + @classmethod + def 创建普K(cls, 标识: str, 时间戳: datetime, 开盘价: float, 最高价: float, 最低价: float, 收盘价: float, 成交量: float, 序号: int, 周期: int) -> "K线": + k线 = K线( + 标识=标识, + 序号=序号, + 周期=周期, + 时间戳=时间戳, + 开盘价=开盘价, + 最高价=最高价, + 最低价=最低价, + 收盘价=收盘价, + 成交量=成交量, + ) + return k线 + + @classmethod + def 保存到DAT文件(cls, 路径: str, K线序列: List["K线"]): + with open(路径, "wb") as f: + for K in K线序列: + f.write(bytes(K)) + print(f"保存到DAT文件: {路径}") + + @classmethod + def 读取大端字节数组(cls, 字节组: bytes, 周期: int = 60, 标识: str = "Bar") -> "K线": + 时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量 = struct.unpack(">6d", 字节组[: struct.calcsize(">6d")]) + + return cls( + 标识=标识, + 序号=0, + 周期=周期, + 时间戳=datetime.fromtimestamp(时间戳), + 开盘价=开盘价, + 最高价=最高价, + 最低价=最低价, + 收盘价=收盘价, + 成交量=成交量, + ) + + @classmethod + def 获取MACD(cls, K线序列: List["K线"], 始: "K线", 终: "K线") -> Dict[str, float]: + 基序 = K线序列[K线序列.index(始) : K线序列.index(终) + 1] + + 阳 = 0.0 + 阴 = 0.0 + for k in 基序: + histogram = k.macd.MACD柱 + if histogram >= 0: + 阳 += histogram + else: + 阴 += histogram + + 合 = 阳 + 阴 + return {"阳": 阳, "阴": 阴, "合": 合, "总": 阳 + abs(阴)} + + @staticmethod + def 截取(序列: List["K线"], 始: "K线", 终: "K线") -> List["K线"]: + return 序列[序列.index(始) : 序列.index(终) + 1] + + +class 缠论K线(object): + __slots__ = ["序号", "时间戳", "高", "低", "方向", "分型", "周期", "标识", "分型特征值", "原始起始序号", "原始结束序号", "标的K线", "买卖点信息"] + + def __init__( + self, + 序号: int, + 时间戳: datetime, + 最高价: float, + 最低价: float, + 最终方向: 相对方向, + 普K: "K线", + 原始起始序号: int, + 原始结束序号: int, + 分型: Optional[分型结构] = None, + ): + self.序号: int = 序号 + self.时间戳: datetime = 时间戳 + self.高: float = 最高价 + self.低: float = 最低价 + self.方向: 相对方向 = 最终方向 + self.分型: Optional[分型结构] = 分型 + self.周期: int = 普K.周期 + self.标识: str = 普K.标识 + self.分型特征值: float = 最高价 + + self.原始起始序号: int = 原始起始序号 + self.原始结束序号: int = 原始结束序号 + self.标的K线: "K线" = 普K + + def __str__(self): + return f"{self.标识}<{self.序号}, {self.分型}, {self.周期}, {self.方向}, {self.时间戳}, {self.高:g}, {self.低:g}>" + + def __repr__(self): + return f"{self.标识}<{self.序号}, {self.分型}, {self.周期}, {self.方向}, {self.时间戳}, {self.高:g}, {self.低:g}>" + + @property + def 镜像(self): + K = 缠论K线(self.序号, self.时间戳, self.高, self.低, self.方向, self.标的K线, self.原始起始序号, self.原始结束序号, self.分型) + return K + + @property + def 与MACD柱子匹配(self) -> bool: + if self.分型 in (分型结构.底, 分型结构.下): + return self.标的K线.macd.MACD柱 < 0 + + if self.分型 in (分型结构.顶, 分型结构.上): + return self.标的K线.macd.MACD柱 > 0 + return False + + @property + def 与RSI匹配(self) -> bool: + if self.分型 in (分型结构.底, 分型结构.下): + return self.标的K线.rsi.RSI < self.标的K线.rsi.RSI_SMA + + if self.分型 in (分型结构.顶, 分型结构.上): + return self.标的K线.rsi.RSI > self.标的K线.rsi.RSI_SMA + return False + + @property + def 与KDJ匹配(self) -> bool: + if self.标的K线.kdj.K is None or self.标的K线.kdj.D is None: + return False + if self.分型 in (分型结构.底, 分型结构.下): + return self.标的K线.kdj.K < self.标的K线.kdj.D + + if self.分型 in (分型结构.顶, 分型结构.上): + return self.标的K线.kdj.K > self.标的K线.kdj.D + return False + + @classmethod + def 时间戳对齐(cls, 基线: List["缠论K线"], k线: "缠论K线"): + if 基线: + for k in 基线[::-1]: + if 基线[0].周期 < k线.周期: + if k线.时间戳.timestamp() <= k.时间戳.timestamp() <= k线.时间戳.timestamp() + k线.周期: + if k线.分型特征值 == k.分型特征值: + return k.时间戳 + else: + if k.时间戳.timestamp() <= k线.时间戳.timestamp() <= k.时间戳.timestamp() + k.周期: + if k线.分型特征值 == k.分型特征值: + return k.时间戳 + return k线.时间戳 + + @classmethod + def 创建缠K(cls, 时间戳: datetime, 高: float, 低: float, 方向: 相对方向, 结构: 分型结构, 原始序号: int, 普k: "K线", 之前: Optional["缠论K线"] = None) -> "缠论K线": + assert 高 >= 低 + 序号 = 0 + 当前 = 缠论K线( + 序号=序号, + 时间戳=时间戳, + 最高价=高, + 最低价=低, + 最终方向=方向, + 分型=结构, + 原始起始序号=原始序号, + 原始结束序号=原始序号, + 普K=普k, + ) + + if 之前 is not None: + 当前.序号 = 之前.序号 + 1 + + if 相对方向.分析(之前.高, 之前.低, 当前.高, 当前.低).是否包含(): + raise ValueError(f"\n {相对方向.分析(之前.高, 之前.低, 当前.高, 当前.低)}\n {之前},\n {当前}") + return 当前 + + @classmethod + def 兼并(cls, 之前缠K: Optional["缠论K线"], 当前缠K: "缠论K线", 当前普K: "K线", 配置: 缠论配置) -> Tuple[Optional["缠论K线"], Optional[str]]: + 关系 = 相对方向.分析(当前缠K.高, 当前缠K.低, 当前普K.高, 当前普K.低) + if not 关系.是否包含(): + 新缠K = 缠论K线.创建缠K( + 时间戳=当前普K.时间戳, + 高=当前普K.高, + 低=当前普K.低, + 方向=当前普K.方向, + 原始序号=当前普K.序号, + 之前=当前缠K, + 普k=当前普K, + 结构=分型结构.下 if 关系.是否向下() else 分型结构.上, + ) + 新缠K.序号 = 当前缠K.序号 + 1 + return 新缠K, "添加" + + if 当前普K.序号 == 当前缠K.原始结束序号: + # 当序号相同时认为是重复提交K线 + ... + + if 当前普K.序号 - 1 != 当前缠K.原始结束序号 and 当前普K.序号 != 当前缠K.原始结束序号: + raise ValueError(f"NewBar.merger: 不可追加不连续元素 缠K.原始结束序号: {当前缠K.原始结束序号}, 当前普K.序号: {当前普K.序号}.") + + # 方向 = 相对方向.向上 + 取值函数 = max + if 之前缠K is not None: + if 相对方向.分析(之前缠K.高, 之前缠K.低, 当前缠K.高, 当前缠K.低).是否向下(): + 取值函数 = min + + if 关系 is not 相对方向.顺: + 当前缠K.时间戳 = 当前普K.时间戳 + 当前缠K.标的K线 = 当前普K + 当前缠K.高 = 取值函数(当前缠K.高, 当前普K.高) + 当前缠K.低 = 取值函数(当前缠K.低, 当前普K.低) + 当前缠K.原始结束序号 = 当前普K.序号 + 当前缠K.方向 = 当前普K.方向 # FIXME 涉及 买卖点,MACD, 均线 + + if 之前缠K is not None: + 当前缠K.序号 = 之前缠K.序号 + 1 + + if 配置.缠K合并替换: + return 当前缠K.镜像, "替换" + return None, None + + @classmethod + def 分析(cls, 当前K线: "K线", 缠K序列: List["缠论K线"], 普K序列: List["K线"], 配置: 缠论配置) -> tuple[str, Optional["分型"]]: + 当前K线.标识 = 配置.标识 + if not 普K序列: + if 配置.计算指标: + 当前K线.macd = 平滑异同移动平均线.首次计算_K线(当前K线, 配置.指标计算方式, 配置.平滑异同移动平均线_快线周期, 配置.平滑异同移动平均线_慢线周期, 配置.平滑异同移动平均线_信号周期) + 当前K线.rsi = 相对强弱指数.首次计算_K线(当前K线, 配置.指标计算方式, 配置.相对强弱指数_周期, 配置.相对强弱指数_超买阈值, 配置.相对强弱指数_超卖阈值, 配置.相对强弱指数_移动平均线周期) + 当前K线.kdj = 随机指标.首次计算_K线(当前K线, 配置.指标计算方式, 配置.随机指标_RSV周期, 配置.随机指标_K值平滑周期, 配置.随机指标_D值平滑周期, 配置.随机指标_超买阈值, 配置.随机指标_超卖阈值) + 普K序列.append(当前K线) + else: + 之前普K = 普K序列[-1] + if 之前普K.时间戳 == 当前K线.时间戳: + 当前K线.序号 = 普K序列[-1].序号 + 普K序列[-1] = 当前K线 + if 配置.计算指标: + try: + 当前K线.macd = 平滑异同移动平均线.增量计算_K线(普K序列[-2].macd, 当前K线, 配置.指标计算方式) + 当前K线.rsi = 相对强弱指数.增量计算_K线(普K序列[-2].rsi, 当前K线, 配置.指标计算方式) + 当前K线.kdj = 随机指标.增量计算_K线(普K序列[-2].kdj, 当前K线, 配置.指标计算方式) + except IndexError: + pass # traceback.print_exc() + else: + if 之前普K.时间戳 > 当前K线.时间戳: + raise RuntimeError("时序错误") + 当前K线.序号 = 之前普K.序号 + 1 + if 配置.计算指标: + 当前K线.macd = 平滑异同移动平均线.增量计算_K线(之前普K.macd, 当前K线, 配置.指标计算方式) + 当前K线.rsi = 相对强弱指数.增量计算_K线(之前普K.rsi, 当前K线, 配置.指标计算方式) + 当前K线.kdj = 随机指标.增量计算_K线(之前普K.kdj, 当前K线, 配置.指标计算方式) + 普K序列.append(当前K线) + + 之前缠K: Optional[缠论K线] = None + 状态, 形态 = None, None + if 缠K序列: + try: + 之前缠K = 缠K序列[-2] + except IndexError: + pass + 新缠K, 模式 = 缠论K线.兼并(之前缠K, 缠K序列[-1], 当前K线, 配置) + if 新缠K is not None: + if 模式 == "添加": + 缠K序列.append(新缠K) + 状态 = "创建" + elif 模式 == "替换": + 缠K序列[-1] = 新缠K + 状态 = "替换" + else: + raise RuntimeError() + else: + 状态 = "兼并" + else: + 新缠K = 缠论K线.创建缠K(时间戳=当前K线.时间戳, 高=当前K线.高, 低=当前K线.低, 方向=当前K线.方向, 原始序号=当前K线.序号, 之前=None, 普k=当前K线, 结构=None) + 缠K序列.append(新缠K) + 状态 = "新建" + + try: + 左, 中, 右 = 缠K序列[-3:] + except ValueError: + return 状态, 形态 + + 结构 = 分型结构.分析(左, 中, 右) + 中.分型 = 结构 + + if 结构 is 分型结构.底: + 中.分型特征值 = 中.低 + 右.分型特征值 = 右.高 + 右.分型 = 分型结构.顶 + + if 结构 is 分型结构.顶: + 中.分型特征值 = 中.高 + 右.分型特征值 = 右.低 + 右.分型 = 分型结构.底 + + if 结构 is 分型结构.上: + 中.分型特征值 = 中.高 + 右.分型特征值 = 右.高 + 右.分型 = 分型结构.顶 + + if 结构 is 分型结构.下: + 中.分型特征值 = 中.低 + 右.分型特征值 = 右.低 + 右.分型 = 分型结构.底 + + 形态 = 分型(左=左, 中=中, 右=右) + if 结构 in (分型结构.上, 分型结构.下): + 形态 = 分型(中, 右, None) + return 状态, 形态 + + @staticmethod + def 截取(序列: List["缠论K线"], 始: "缠论K线", 终: "缠论K线") -> List["缠论K线"]: + return 序列[序列.index(始) : 序列.index(终) + 1] + + +class 分型(object): + __slots__ = ["左", "中", "右", "结构", "时间戳", "分型特征值"] + + def __init__(self, 左: Optional[缠论K线], 中: 缠论K线, 右: Optional[缠论K线]): + if 左 and 右: + assert 左.时间戳 < 中.时间戳 < 右.时间戳 + self.左: Optional[缠论K线] = 左 + self.中: 缠论K线 = 中 + self.右: Optional[缠论K线] = 右 + self.结构 = 中.分型 + self.时间戳 = 中.时间戳 + self.分型特征值 = 中.分型特征值 + + def __str__(self): + return f"{self.中.分型}<{self.时间戳}, {self.分型特征值:g}, None: {self.左 is None}, None: {self.右 is None}>" + + def __repr__(self): + return f"{self.中.分型}<{self.时间戳}, {self.分型特征值:g}, None: {self.左 is None}, None: {self.右 is None}>" + + @property + def 关系组(self) -> Optional[Tuple[相对方向, 相对方向, 相对方向]]: + if self.左 and self.右: + return 相对方向.分析(self.左.高, self.左.低, self.中.高, self.中.低), 相对方向.分析(self.中.高, self.中.低, self.右.高, self.右.低), 相对方向.分析(self.左.高, self.左.低, self.右.高, self.右.低) + return None + + @property + def 强度(self): + if self.结构 not in (分型结构.底, 分型结构.顶): + return "未知" + if not self.右 or not self.左: + return "未知" + + if 关系组 := self.关系组: + if self.结构 is 分型结构.底: + if 关系组[-1].是否向下(): + return "弱" + elif 关系组[-1].是否向上(): + return "强" + else: + return "中" + + elif self.结构 is 分型结构.顶: + if 关系组[-1].是否向上(): + return "弱" + elif 关系组[-1].是否向下(): + return "强" + else: + return "中" + + if self.右 and self.左: + if self.结构 is 分型结构.底: + if self.右.标的K线.收盘价 > self.左.标的K线.高: + return "强" + elif self.右.标的K线.收盘价 > self.中.标的K线.高: + return "中" + else: + return "弱" + elif self.结构 is 分型结构.顶: + if self.右.标的K线.收盘价 < self.左.标的K线.低: + return "强" + elif self.右.标的K线.收盘价 < self.中.标的K线.低: + return "中" + else: + return "弱" + return "未知" + + @property + def 与MACD柱子分型匹配(self) -> bool: + if self.右 and self.左: + if self.结构 is 分型结构.底: + return self.左.标的K线.macd.MACD柱 > self.中.标的K线.macd.MACD柱 < self.右.标的K线.macd.MACD柱 + if self.结构 is 分型结构.顶: + return self.左.标的K线.macd.MACD柱 < self.中.标的K线.macd.MACD柱 > self.右.标的K线.macd.MACD柱 + return False + + @classmethod + def 判断分型(cls, 左: "分型", 右: "分型", 模式: str = "中") -> bool: + return 左 is 右 + + @staticmethod + def 从缠K序列中获取分型(K线序列: List[缠论K线], 中: 缠论K线) -> "分型": + 索引 = K线序列.index(中) + try: + return 分型(左=K线序列[索引 - 1], 中=中, 右=K线序列[索引 + 1]) + except IndexError: + return 分型(左=K线序列[索引 - 1], 中=中, 右=None) + + @staticmethod + def 向序列中添加(分型序列: List["分型"], 当前分型: "分型"): + if not 分型序列 and 当前分型.结构 not in (分型结构.顶, 分型结构.底): + raise ValueError("首次添加分型不为 顶底", 当前分型) + if 分型序列: + if 分型序列[-1].结构 is 当前分型.结构: + raise ValueError("分型相同无法添加", 分型序列[-1], 当前分型) + if 分型序列[-1].右 is None: + print("分型.向序列中添加, 分型异常", 分型序列[-1]) + + 分型序列.append(当前分型) + + +class 虚线(object): + __slots__ = ["标识", "序号", "级别", "文", "武", "有效性", "基础序列", "特征序列", "实_中枢序列", "虚_中枢序列", "合_中枢序列", "确认K线", "模式", "_特征序列_显示", "前一缺口", "前一结束位置", "短路修正"] + + def __init__(self, 序号: int, 标识: str, 文: 分型, 武: 分型, 级别: int, 有效性: bool = True): + self.序号 = 序号 + self.标识 = 标识 + self.级别 = 级别 + + self.文 = 文 + self.武 = 武 + + self.有效性 = 有效性 + + self.基础序列: List["虚线"] = [] + self.特征序列: List[Optional[线段特征]] = [] + + self.实_中枢序列: List["中枢"] = [] + self.虚_中枢序列: List["中枢"] = [] + self.合_中枢序列: List["中枢"] = [] + self.确认K线: Optional[缠论K线] = None + self.模式: str = "文武" + self._特征序列_显示 = False + self.前一缺口: Optional[缺口] = None + self.前一结束位置 = None + self.短路修正 = False + + def __str__(self): + if self.标识 == "笔": + return f"笔({self.序号}, {self.方向}, {self.文}, {self.武}, 周期: {self.文.中.周期}, 数量: {self.武.中.序号 - self.文.中.序号 + 1})" + else: + return f"{self.标识}<{self.序号}, {线段.四象(self)}, {self.方向}, {self.文}, {self.武}, 数量: {len(self.基础序列)}, 缺口: {线段.获取缺口(self)}, {self.确认K线}>" + + def __repr__(self): + return self.__str__() + + @property + def 笔序列(self): + return self.基础序列 + + @property + def 图表标题(self) -> str: + return f"{self.文.中.标识}:{self.文.中.周期}:{self.标识}:{self.序号}" + + @property + def 方向(self) -> "相对方向": + match (self.文.结构, self.武.结构): + case (分型结构.顶, 分型结构.底): + return 相对方向.向下 + case (分型结构.顶, 分型结构.下): + return 相对方向.向下 + + case (分型结构.底, 分型结构.顶): + return 相对方向.向上 + case (分型结构.底, 分型结构.上): + return 相对方向.向上 + + case _: + raise RuntimeError("无法识别的方向", self.文.结构, self.武.结构) + + @property + def 高(self) -> float: + """if self.模式 != "文武": + if type(self[0] is 笔): + return max(self.__基础序列__, key=lambda x: x.高).高""" + + if self.方向 is 相对方向.向上: + return self.武.中.高 + return self.文.中.高 + + @property + def 低(self) -> float: + """if self.模式 != "文武": + if type(self[0] is 笔): + return min(self.__基础序列__, key=lambda x: x.低).低""" + if self.方向 is 相对方向.向下: + return self.武.中.低 + return self.文.中.低 + + def 之前是(self, 之前: "虚线") -> bool: + if self.标识 == 之前.标识: + return 分型.判断分型(之前.武, self.文) + return False + + def 之后是(self, 之后: "虚线") -> bool: + if self.标识 == 之后.标识: + return 分型.判断分型(self.武, 之后.文) + return False + + def 获取普K序列(self, 观察员: "观察者") -> List[K线]: + return K线.截取(观察员.普通K线序列, self.文.中.标的K线, self.武.中.标的K线) + + def 获取缠K序列(self, 观察员: "观察者") -> List[缠论K线]: + return 缠论K线.截取(观察员.缠论K线序列, self.文.中, self.武.中) + + def 获取数据文本(self): + if self.标识 == "笔": + return f"{self.标识}, {self.序号}, {self.级别}, 文:({int(self.文.时间戳.timestamp())},{self.文.分型特征值:g}), 武:({int(self.武.时间戳.timestamp())},{self.武.分型特征值:g}), {self.有效性}" + 前, 后, 三, 贯穿伤 = 线段.分割序列(self) + return f"{self.标识}, {self.序号}, {self.级别}, 文:({int(self.文.时间戳.timestamp())},{self.文.分型特征值:g}), 武:({int(self.武.时间戳.timestamp())},{self.武.分型特征值:g}), {self.有效性}, {len(self.基础序列)}, {线段.特征序列状态(self)}, (前: {str(前)}, 后: {str(后)}, 三: {str(三)}, 伤: {str(贯穿伤)}), 实: {str(self.实_中枢序列)}, 虚: {str(self.虚_中枢序列)}, 合: {str(self.合_中枢序列)}, {self.模式}, {str(self.前一缺口)}, {str(self.前一结束位置)}, {self.短路修正}" + + @classmethod + def 创建笔(cls, 文: 分型, 武: 分型, 有效性: bool = True) -> "虚线": + return 虚线(0, "笔", 文, 武, 1, 有效性) + + @classmethod + def 创建线段(cls, 虚线序列: List["虚线"]) -> "虚线": + 文 = 虚线序列[0].文 + 武 = 虚线序列[-1].武 + 标识 = "线段" if 虚线序列[0].标识 == "笔" else f"线段<{虚线序列[0].标识}>" + 段 = 虚线(0, 标识, 文, 武, 虚线序列[0].级别 + 1) + 段.特征序列 = [None] * 3 + 段.实_中枢序列 = [] + 段.虚_中枢序列 = [] + 段.合_中枢序列 = [] + 段.基础序列 = 虚线序列 + return 段 + + @classmethod + def 缠K买卖点模式(cls, 模式: str, 缠K: "缠论K线", 配置: 缠论配置): + match 模式: + case "全量": + return cls.买卖点全量匹配(缠K) + case "任意": + return cls.买卖点任意匹配(缠K) + case "配置": + return cls.买卖点配置匹配(缠K, 配置) + case "相对": + return cls.买卖点相对匹配(缠K) + return False + + @classmethod + def 买卖点配置匹配(cls, 缠K: "缠论K线", 配置: 缠论配置): + match 配置.买卖点_指标匹配_MACD, 配置.买卖点_指标匹配_KDJ, 配置.买卖点_指标匹配_RSI: + case True, True, True: + return 缠K.与MACD柱子匹配 and 缠K.与KDJ匹配 and 缠K.与RSI匹配 + case False, False, False: + ... + + case True, False, True: + return 缠K.与MACD柱子匹配 and 缠K.与RSI匹配 + case False, True, False: + return 缠K.与KDJ匹配 + + case True, False, False: + return 缠K.与MACD柱子匹配 + case False, True, True: + return 缠K.与KDJ匹配 and 缠K.与RSI匹配 + + case False, False, True: + return 缠K.与RSI匹配 + case True, True, False: + return 缠K.与MACD柱子匹配 and 缠K.与KDJ匹配 + return False + + @classmethod + def 买卖点任意匹配(cls, 缠K: "缠论K线"): + return any([缠K.与MACD柱子匹配, 缠K.与KDJ匹配, 缠K.与RSI匹配]) + + @classmethod + def 买卖点全量匹配(csl, 缠K: "缠论K线"): + return all([缠K.与MACD柱子匹配, 缠K.与KDJ匹配, 缠K.与RSI匹配]) + + @classmethod + def 买卖点相对匹配(cls, 缠K: "缠论K线"): + 混沌槽 = [缠K.与MACD柱子匹配, 缠K.与KDJ匹配, 缠K.与RSI匹配] + return len([匹配 for 匹配 in 混沌槽 if 匹配]) >= 2 + + @classmethod + def 计算MACD柱子均值(cls, 普K序列: List[K线], 实线: "虚线") -> float: + K线序列: List[K线] = K线.截取(普K序列, 实线.文.中.标的K线, 实线.武.中.标的K线) + return sum([abs(K.macd.MACD柱) for K in K线序列]) / len(K线序列) + + @classmethod + def 计算MACD柱子均值_阴(cls, 普K序列: List[K线], 实线: "虚线") -> float: + K线序列: List[K线] = K线.截取(普K序列, 实线.文.中.标的K线, 实线.武.中.标的K线) + 总 = [abs(K.macd.MACD柱) for K in K线序列 if K.macd.MACD柱 < 0] + if 总: + return sum(总) / len(总) + return False + + @classmethod + def 计算MACD柱子均值_阳(cls, 普K序列: List[K线], 实线: "虚线") -> float: + K线序列: List[K线] = K线.截取(普K序列, 实线.文.中.标的K线, 实线.武.中.标的K线) + 总 = [abs(K.macd.MACD柱) for K in K线序列 if K.macd.MACD柱 > 0] + if 总: + return sum(总) / len(总) + return False + + @classmethod + def 武之全量MACD均值(cls, 普K序列: List[K线], 实线: "虚线") -> bool: + """ + 小于均值则背驰 + """ + return abs(实线.武.中.标的K线.macd.MACD柱) < cls.计算MACD柱子均值(普K序列, 实线) + + @classmethod + def 武之MACD均值(cls, 普K序列: List[K线], 实线: "虚线") -> bool: + """ + 小于均值则背驰 + """ + if 实线.方向 is 相对方向.向上: + return cls.武之MACD均值_阳(普K序列, 实线) + else: + return cls.武之MACD均值_阴(普K序列, 实线) + + @classmethod + def 武之MACD均值_阴(cls, 普K序列: List[K线], 实线: "虚线") -> bool: + """ + 小于均值则背驰 + """ + return abs(实线.武.中.标的K线.macd.MACD柱) < abs(cls.计算MACD柱子均值_阴(普K序列, 实线)) + + @classmethod + def 武之MACD均值_阳(cls, 普K序列: List[K线], 实线: "虚线") -> bool: + """ + 小于均值则背驰 + """ + return abs(实线.武.中.标的K线.macd.MACD柱) < abs(cls.计算MACD柱子均值_阳(普K序列, 实线)) + + @classmethod + def 武之MACD极值(cls, 普K序列: List[K线], 实线: "虚线") -> bool: + """ + 最高最低 + """ + K线序列: List[K线] = K线.截取(普K序列, 实线.文.中.标的K线, 实线.武.中.标的K线) + 所有柱子 = [K.macd.MACD柱 for K in K线序列] + if 实线.武.中.标的K线.macd.MACD柱 > 0: + 取值函数 = max + else: + 取值函数 = min + if 取值函数(所有柱子) == 实线.武.中.标的K线.macd.MACD柱: + return True + return False + + @classmethod + def 计算K线序列MACD趋向背驰(cls, 普K序列: Sequence["K线"], 方向: 相对方向): + if 方向 is 相对方向.向上: + 柱子序列 = [k线 for k线 in 普K序列 if k线.macd.MACD柱 > 0] + if not 柱子序列: + return [False, False, False] + 最高柱子 = max(柱子序列, key=lambda k线: k线.macd.MACD柱) + 最低柱子 = min(柱子序列, key=lambda k线: k线.macd.MACD柱) + 排序柱子 = sorted(柱子序列, key=lambda k线: k线.macd.MACD柱) + 离差值序列 = [k线 for k线 in 普K序列 if k线.macd.DIF > 0] + 最高离差值 = max(柱子序列, key=lambda k线: k线.macd.DIF) + 最低离差值 = min(柱子序列, key=lambda k线: k线.macd.DIF) + 信号线序列 = [k线 for k线 in 普K序列 if k线.macd.DEA > 0] + 最高信号线 = max(柱子序列, key=lambda k线: k线.macd.DEA) + 最低信号线 = min(柱子序列, key=lambda k线: k线.macd.DEA) + + 结果 = [] + 柱子 = [最高柱子, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.MACD柱 > 柱子[1].macd.MACD柱 and 柱子[0].高 < 柱子[1].高: + 结果.append(True) + else: + 结果.append(False) + + 柱子 = [最高离差值, 普K序列[-1]] + if 柱子[0].macd.DIF > 柱子[1].macd.DIF and 柱子[0].高 < 柱子[1].高: + 结果.append(True) + else: + 结果.append(False) + + 柱子 = [最高信号线, 普K序列[-1]] + if 柱子[0].macd.DEA > 柱子[1].macd.DEA and 柱子[0].高 < 柱子[1].高: + 结果.append(True) + else: + 结果.append(False) + + return 结果 + else: + 柱子序列 = [k线 for k线 in 普K序列 if k线.macd.MACD柱 < 0] + if not 柱子序列: + return [False, False, False] + 最高柱子 = max(柱子序列, key=lambda k线: abs(k线.macd.MACD柱)) + 最低柱子 = min(柱子序列, key=lambda k线: abs(k线.macd.MACD柱)) + 排序柱子 = sorted(柱子序列, key=lambda k线: abs(k线.macd.MACD柱)) + 离差值序列 = [k线 for k线 in 普K序列 if k线.macd.DIF < 0] + 最高离差值 = max(柱子序列, key=lambda k线: abs(k线.macd.DIF)) + 最低离差值 = min(柱子序列, key=lambda k线: abs(k线.macd.DIF)) + 信号线序列 = [k线 for k线 in 普K序列 if k线.macd.DEA < 0] + 最高信号线 = max(柱子序列, key=lambda k线: abs(k线.macd.DEA)) + 最低信号线 = min(柱子序列, key=lambda k线: abs(k线.macd.DEA)) + + 结果 = [] + 柱子 = [最高柱子, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.MACD柱 < 柱子[1].macd.MACD柱 and 柱子[0].低 > 柱子[1].低: + 结果.append(True) + else: + 结果.append(False) + + 柱子 = [最高离差值, 普K序列[-1]] + if 柱子[0].macd.DIF < 柱子[1].macd.DIF and 柱子[0].低 > 柱子[1].低: + 结果.append(True) + else: + 结果.append(False) + + 柱子 = [最高信号线, 普K序列[-1]] + if 柱子[0].macd.DEA < 柱子[1].macd.DEA and 柱子[0].低 > 柱子[1].低: + 结果.append(True) + else: + 结果.append(False) + + return 结果 + + @classmethod + @lru_cache(maxsize=128) + def 买卖意义(cls, 实线: "虚线", 观察员: "观察者") -> Tuple[bool, str]: + """ + 静止是相对的,而运动是绝对的 + + """ + 普K序列: List[K线] = 观察员.普通K线序列 + 配置: 缠论配置 = 观察员.配置 + if 实线.标识 not in ("笔", "线段", "线段<线段>"): + return False, "标识不在范围内" + if 实线.武.中.标的K线.kdj.K is None or 实线.武.中.标的K线.kdj.D is None or 实线.武.中.标的K线.kdj.J is None: + return False, "KDJ指标不完整" + 意义 = cls.缠K买卖点模式(配置.买卖点_指标模式, 实线.武.中, 配置) + 结果 = False + 背驰过 = 笔.是否背驰过(实线, 观察员) if 实线.标识 == "笔" else 线段.是否背驰过(实线, 观察员) + if 意义: # and self.武.强度 in ("强", ""): + if 实线.标识 == "笔": + if cls.武之MACD均值(普K序列, 实线): + return True, "武之MACD均值" + if cls.武之MACD极值(普K序列, 实线) and 背驰过: + return True, "背驰过且极值" + else: + if 实线.武.与MACD柱子分型匹配: + return True, f"背驰过:{len(背驰过)},极值:{cls.武之MACD极值(普K序列, 实线)},柱子分型匹配" + if 实线.标识 != "笔" and 线段.判断线段内部是否背驰(实线, 观察员): + return True, "线段内部背驰" + + if not 结果 and 意义 and 实线.武.中.与MACD柱子匹配: + if cls.武之MACD极值(普K序列, 实线) and len(背驰过) > 2: + return True, "没结果, 极值, 柱子分型匹配, 背驰过大于2次" + return 结果, "" + + @classmethod + def 计算MACD柱子分段(cls, k线序列: List["K线"]) -> Tuple[List[List["K线"]], ...]: + if not k线序列: + return () + + def 符号(x: float) -> str: + if x > 0: + return "正" + else: + return "负" + + 当前符号 = 符号(k线序列[0].macd.MACD柱) + 当前段柱子 = [k线序列[0].macd.MACD柱] + 结果 = [] + + for i in range(1, len(k线序列)): + 新符号 = 符号(k线序列[i].macd.MACD柱) + if 新符号 == 当前符号: + 当前段柱子.append(k线序列[i].macd.MACD柱) + else: + 结果.append(当前段柱子) + 当前段柱子 = [k线序列[i].macd.MACD柱] + 当前符号 = 新符号 + return tuple(结果) + + @classmethod + def 密集区域按间隔(cls, 交叉标记: List[int], 最大间隔: int = 5, 最少交叉数: int = 3) -> List[Tuple[int, int, int]]: + """ + 交叉标记: 长度为len(macd_list)的列表,0=无交叉, 1=金叉, -1=死叉 + 最大间隔: 相邻交叉索引差 ≤ 此值则归入同一密集区 + 最少交叉数: 一个密集区内至少包含的交叉次数 + 返回: [(起始交叉索引, 结束交叉索引, 区内交叉次数), ...] + """ + # 提取所有交叉点的索引 + 交叉索引 = [i for i, v in enumerate(交叉标记) if v != 0] + if not 交叉索引: + return [] + + 密集区 = [] + 当前块起始 = 交叉索引[0] + 当前块交叉数 = 1 + + for i in range(1, len(交叉索引)): + prev_idx = 交叉索引[i - 1] + curr_idx = 交叉索引[i] + if curr_idx - prev_idx <= 最大间隔: + 当前块交叉数 += 1 + else: + # 当前块结束 + if 当前块交叉数 >= 最少交叉数: + 密集区.append((当前块起始, prev_idx, 当前块交叉数)) + # 开始新块 + 当前块起始 = curr_idx + 当前块交叉数 = 1 + + # 处理最后一个块 + if 当前块交叉数 >= 最少交叉数: + 密集区.append((当前块起始, 交叉索引[-1], 当前块交叉数)) + + return 密集区 + + @classmethod + def 统计MACD行为(cls, 普K序列: List[K线], 最大间隔: int = 8, 最少交叉数: int = 3) -> dict: + # 1. 穿越零轴计数 + dif_up = dif_down = dea_up = dea_down = 0 + for i in range(1, len(普K序列)): + pre, cur = 普K序列[i - 1].macd, 普K序列[i].macd + if pre.DIF is None or cur.DIF is None: + continue + if pre.DIF < 0 <= cur.DIF: + dif_up += 1 + elif pre.DIF > 0 >= cur.DIF: + dif_down += 1 + if pre.DEA < 0 <= cur.DEA: + dea_up += 1 + elif pre.DEA > 0 >= cur.DEA: + dea_down += 1 + + # 2. DIF与DEA交叉(带标记) + golden = death = 0 + 交叉标记 = [0] # 第0个位置无前值,先填0 + for i in range(1, len(普K序列)): + pre, cur = 普K序列[i - 1].macd, 普K序列[i].macd + if pre.DIF is None or cur.DIF is None or pre.DEA is None or cur.DEA is None: + 交叉标记.append(0) + continue + if pre.DIF <= pre.DEA and cur.DIF > cur.DEA: + golden += 1 + 交叉标记.append(1) + elif pre.DIF >= pre.DEA and cur.DIF < cur.DEA: + death += 1 + 交叉标记.append(-1) + else: + 交叉标记.append(0) + + # 3. 按间隔密集区域 + 密集区 = cls.密集区域按间隔(交叉标记, 最大间隔=最大间隔, 最少交叉数=最少交叉数) + + return { + "DIF上穿0": dif_up, + "DIF下穿0": dif_down, + "DEA上穿0": dea_up, + "DEA下穿0": dea_down, + "金叉次数": golden, + "死叉次数": death, + "密集交叉区域": 密集区, # (起始交叉索引, 结束交叉索引, 交叉次数) + } + + +class 笔(object): + __slots__ = [] + + @staticmethod + def 获取缠K数量(缠K序列: List[缠论K线], 笔序列: List[虚线], 配置: 缠论配置) -> int: + 实际数量 = len(缠K序列) + if 实际数量 >= 配置.笔内元素数量: + return 实际数量 + + if 配置.笔弱化 and 实际数量 >= 3: + 实际高点 = 笔.实际高点(缠K序列, 配置.笔内相同终点取舍) + 实际低点 = 笔.实际低点(缠K序列, 配置.笔内相同终点取舍) + 原始数量 = 1 + abs(实际低点.标的K线.序号 - 实际高点.标的K线.序号) + if 原始数量 >= 配置.笔内元素数量: + return 配置.笔内元素数量 + + if 笔序列: + 筆 = 笔.根据缠K找笔(笔序列, 实际高点) or 笔.根据缠K找笔(笔序列, 实际低点) + if 筆: + if 筆.方向 is 相对方向.向上 and 实际低点.低 < 筆.低: + if 原始数量 >= 配置.笔弱化_原始数量: + return 配置.笔内元素数量 + if 筆.方向 is 相对方向.向下 and 实际低点.低 > 筆.高: + if 原始数量 >= 配置.笔弱化_原始数量: + return 配置.笔内元素数量 + + return 实际数量 + + @staticmethod + def 次高(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: + 序列 = sorted(缠K序列, key=lambda k: k.高) + highs: List[缠论K线] = [k for k in 序列 if k.高 != 序列[-1].高] # 排除 + highs: List[缠论K线] = [k for k in highs if k.高 == highs[-1].高] # 筛选 + highs.sort(key=lambda k: k.时间戳) # 排序 + return highs[-1] if 笔内相同终点取舍 else highs[0] + + @staticmethod + def 次低(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: + 序列 = sorted(缠K序列, key=lambda k: k.低) + lows: List[缠论K线] = [k for k in 序列 if k.低 != 序列[0].低] + lows: List[缠论K线] = [k for k in lows if k.低 == lows[0].低] + lows.sort(key=lambda k: k.时间戳) + return lows[-1] if 笔内相同终点取舍 else lows[0] + + @staticmethod + def 实际高点(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: + 序列 = sorted(缠K序列, key=lambda k: k.高) + highs: List[缠论K线] = [k for k in 序列 if k.高 == 序列[-1].高] + highs.sort(key=lambda k: k.时间戳) + return highs[-1] if 笔内相同终点取舍 else highs[0] + + @staticmethod + def 实际低点(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: + 序列 = sorted(缠K序列, key=lambda k: k.低) + lows: List[缠论K线] = [k for k in 序列 if k.低 == 序列[0].低] + lows.sort(key=lambda k: k.时间戳) + return lows[-1] if 笔内相同终点取舍 else lows[0] + + @staticmethod + def 相对关系(筆: 虚线, 配置: 缠论配置) -> bool: + if 配置.笔内起始分型包含整笔: + 有效序列 = [k线 for k线 in (筆.文.左, 筆.文.中, 筆.文.右) if k线 is not None] + 文 = 缺口(max(有效序列, key=lambda k: k.高).高, min(有效序列, key=lambda k: k.低).低) + 有效序列 = [k线 for k线 in (筆.武.左, 筆.武.中, 筆.武.右 if 配置.笔内起始分型包含整笔_包括右 else None) if k线 is not None] # 排除 右 + 武 = 缺口(max(有效序列, key=lambda k: k.高).高, min(有效序列, key=lambda k: k.低).低) + 相对关系 = 相对方向.分析(文.高, 文.低, 武.高, 武.低) + else: + 相对关系 = 相对方向.分析(筆.文.中.高, 筆.文.中.低, 筆.武.中.高, 筆.武.中.低) + if 配置.笔内原始K线包含整笔 and 相对方向.分析(筆.文.中.标的K线.高, 筆.文.中.标的K线.低, 筆.武.中.标的K线.高, 筆.武.中.标的K线.低).是否包含(): # TODO 建议增加相关配置 + return False + + if 筆.方向 is 相对方向.向下: + return 相对关系.是否向下() + return 相对关系.是否向上() + + @classmethod + def _弹出旧笔(cls, 分型序列: List[分型], 笔序列: List[虚线], 行号): + 旧分型 = 分型序列.pop() + if 笔序列: + 旧笔 = 笔序列.pop() + assert 旧笔.武 is 旧分型, f"最后一笔终点错误{行号}" + 旧笔.有效性 = False + + @classmethod + def _添加新笔(cls, 分型序列: List[分型], 笔序列: List[虚线], 待添加分型: "分型", 待添加新笔: 虚线, 行号): + if not 分型序列 and 待添加分型.结构 not in (分型结构.顶, 分型结构.底): + raise ValueError("首次添加分型不为 顶底", 待添加分型) + if 分型序列: + if 分型序列[-1].结构 is 待添加分型.结构: + raise ValueError("分型相同无法添加", 分型序列[-1], 待添加分型) + if 分型序列[-1].右 is None: + print("分型.向序列中添加, 分型异常", 分型序列[-1]) + + 分型序列.append(待添加分型) + if 笔序列 and not 笔序列[-1].之后是(待添加新笔): + raise ValueError("笔.向序列中添加 不连续", 笔序列[-1], 待添加新笔) + + if 笔序列: + 待添加新笔.序号 = 笔序列[-1].序号 + 1 + if 待添加新笔.武.左 is None and 待添加新笔.武.右 is None: + 待添加新笔.有效性 = False + if 笔序列[-1].武.结构 in (分型结构.上, 分型结构.下): + print(f"_添加新笔[{行号}] 出现无效分型", 笔序列[-1]) + + 笔序列.append(待添加新笔) + + @classmethod + def 分析(cls, 当前分型: Optional[分型], 分型序列: List[分型], 笔序列: List[虚线], 缠K序列: List[缠论K线], 普K序列: List[K线], 递归层次: int, 配置: 缠论配置): + if 当前分型 is None: + return 递归层次 + + if 递归层次 > 64: + print(f"笔.分析 递归深度超出 64 < {递归层次}") + # return 递归层次 + + if 当前分型.结构 not in (分型结构.顶, 分型结构.底): + return 递归层次 + + if not 分型序列: + if 当前分型.结构 in (分型结构.顶, 分型结构.底): + 分型序列.append(当前分型) + return 递归层次 + + 笔递归分析 = 笔.分析 + + 之前分型 = 分型序列[-1] + if (之前分型.中.时间戳 == 当前分型.中.时间戳) or (之前分型.结构 in (分型结构.上, 分型结构.下)): + 笔._弹出旧笔(分型序列, 笔序列, sys._getframe().f_lineno) + if not 分型序列: + if 当前分型.右 is not None: + 分型.向序列中添加(分型序列, 当前分型) + return 递归层次 + + 之前分型 = 分型序列[-1] + if 之前分型.中.时间戳 > 当前分型.中.时间戳 and 之前分型.中.序号 - 当前分型.中.序号 > 1: + # raise RuntimeError(f"时序错误-{递归层次}, {之前分型}, {当前分型}") + print(colored(f"时序错误-{递归层次}, {之前分型}, {当前分型}", "red")) + return 递归层次 + + if 配置.笔弱化 and 笔序列: + 前一笔 = 笔序列[-1] + if 前一笔.武.中.序号 - 前一笔.文.中.序号 + 1 == 3: + if (前一笔.方向.是否向上() and 前一笔.低 > 当前分型.分型特征值 and 当前分型.结构 is 分型结构.底) or (前一笔.方向.是否向下() and 前一笔.高 < 当前分型.分型特征值 and 当前分型.结构 is 分型结构.顶): + 笔._弹出旧笔(分型序列, 笔序列, sys._getframe().f_lineno) + return 笔递归分析(当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) + + if 之前分型.结构 is not 当前分型.结构: + 基础序列 = 缠论K线.截取(缠K序列, 之前分型.中, 当前分型.中) + 当前笔 = 虚线.创建笔(文=之前分型, 武=当前分型, 有效性=True) + if 笔.获取缠K数量(基础序列, 笔序列, 配置) >= 配置.笔内元素数量: + if 之前分型.结构 is 分型结构.顶 and 当前分型.结构 is 分型结构.底: + 文官 = 笔.实际高点(基础序列, False) + else: + 文官 = 笔.实际低点(基础序列, False) + + if 文官 is not 之前分型.中: + 临时分型 = 分型.从缠K序列中获取分型(缠K序列, 文官) + if 之前分型.结构 is 分型结构.顶 and 当前分型.结构 is 分型结构.底: + assert 临时分型.结构 is 分型结构.顶, 临时分型 + else: + assert 临时分型.结构 is 分型结构.底, 临时分型 + 递归层次 = 笔递归分析(临时分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) + 递归层次 = 笔递归分析(当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) + return 递归层次 + + if 之前分型.结构 is 分型结构.顶 and 当前分型.结构 is 分型结构.底: + 武将 = 笔.实际低点(基础序列, 配置.笔内相同终点取舍) + else: + 武将 = 笔.实际高点(基础序列, 配置.笔内相同终点取舍) + + if 笔.相对关系(当前笔, 配置) and 当前分型.中 is 武将: + 笔._添加新笔(分型序列, 笔序列, 当前分型, 当前笔, sys._getframe().f_lineno) + return 递归层次 + + if 配置.笔次级成笔: + if 之前分型.结构 is 分型结构.顶 and 当前分型.结构 is 分型结构.底: + 武将 = 笔.次低(基础序列, 配置.笔内相同终点取舍) + else: + 武将 = 笔.次高(基础序列, 配置.笔内相同终点取舍) + if 笔.相对关系(当前笔, 配置) and 当前分型.中 is 武将: + 笔._添加新笔(分型序列, 笔序列, 当前分型, 当前笔, sys._getframe().f_lineno) + return 递归层次 + + else: + if 当前分型.右: + 临时分型 = 分型.从缠K序列中获取分型(缠K序列, 当前分型.右) + 递归层次 = 笔递归分析(临时分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) + + else: + 分型特征值 = 当前分型.分型特征值 + + if (之前分型.结构 is 分型结构.顶 and 之前分型.分型特征值 < 分型特征值) or (之前分型.结构 is 分型结构.底 and 之前分型.分型特征值 > 分型特征值): + 笔._弹出旧笔(分型序列, 笔序列, sys._getframe().f_lineno) + k线序列 = 缠论K线.截取(缠K序列, 之前分型.中, 当前分型.中) + if 之前分型.结构 is 分型结构.顶: + 武将 = 笔.实际低点(k线序列, 配置.笔内相同终点取舍) + else: + 武将 = 笔.实际高点(k线序列, 配置.笔内相同终点取舍) + 临时分型 = 分型.从缠K序列中获取分型(缠K序列, 武将) + + if 分型序列: + 递归层次 = 笔递归分析(临时分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) + if 分型序列 and 分型序列[-1] is 临时分型: + # 进行修复错过的笔 + for ck in 缠K序列[缠K序列.index(武将) :]: + if ck.分型 in (分型结构.底, 分型结构.顶): + 临时分型 = 分型.从缠K序列中获取分型(缠K序列, ck) + 递归层次 = 笔递归分析(临时分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) + if 分型序列 and 分型序列[-1] is 临时分型: + """""" + # print("笔.分析 事后修复错过的笔", 临时分型, "当前分型", 当前分型) + + 递归层次 = 笔递归分析(当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) + return 递归层次 + else: + 分型.向序列中添加(分型序列, 当前分型) + + return 递归层次 + + @staticmethod + def 以文会友(笔序列: List[虚线], 文: 分型) -> Optional[虚线]: + for 筆 in 笔序列: + if 筆.文 is 文: + return 筆 + return None + + @staticmethod + def 以武会友(笔序列: List[虚线], 武: 分型) -> Optional[虚线]: + for 筆 in 笔序列[::-1]: + if 筆.武 is 武: + return 筆 + return None + + @staticmethod + def 根据缠K找笔(笔序列: List[虚线], 缠K: "缠论K线", 偏移: int = 1): + for 筆 in 笔序列[::-1]: + if 筆.文.中.序号 - 偏移 <= 缠K.序号 <= 筆.武.中.序号: + # if 缠K in 筆.缠K序列[偏移:]: + return 筆 + + return None + + @classmethod + def 自检(cls, 筆: 虚线, 观察员: "观察者") -> bool: + 笔序列: List[虚线] = 观察员.笔序列 + 配置: 缠论配置 = 观察员.配置 + 基础序列 = 筆.获取缠K序列(观察员) + if 笔.获取缠K数量(基础序列, 笔序列, 配置) >= 配置.笔内元素数量: + if 筆.方向 is 相对方向.向下 and 筆.文.中 is 笔.实际高点(基础序列, False) and 筆.武.中 is 笔.实际低点(基础序列, 配置.笔内相同终点取舍): + return True + if 筆.方向 is 相对方向.向上 and 筆.文.中 is 笔.实际低点(基础序列, False) and 筆.武.中 is 笔.实际高点(基础序列, 配置.笔内相同终点取舍): + return True + return False + + @classmethod + def 获取所有停顿位置(cls, 筆: 虚线, 观察员: "观察者"): + 笔序列 = [] + 文 = 筆.文 + 基础序列 = 筆.获取缠K序列(观察员.观察员) + for i in range(3, len(基础序列) - 1): + K = 基础序列[i] + 武 = 分型(基础序列[i - 1], K, 基础序列[i + 1]) + if K.分型 is 分型结构.顶 and 筆.方向 is 相对方向.向上: + 当前笔 = 虚线.创建笔(文=文, 武=武) + 当前笔.序号 = 筆.序号 + 笔.自检(当前笔, 观察员) and 笔序列.append(当前笔) + + if K.分型 is 分型结构.底 and 筆.方向 is 相对方向.向下: + 当前笔 = 虚线.创建笔(文=文, 武=武) + 当前笔.序号 = 筆.序号 + 笔.自检(当前笔, 观察员) and 笔序列.append(当前笔) + + return 笔序列 + + @classmethod + def 是否背驰过(cls, 当前筆: 虚线, 观察员: "观察者"): + 停顿位置 = 笔.获取所有停顿位置(当前筆, 观察员) + 结果 = [] + + for 筆 in 停顿位置: + if all(虚线.计算K线序列MACD趋向背驰(K线.截取(观察员.普通K线序列, 当前筆.文.中.标的K线, 当前筆.武.中.标的K线), 筆.方向)): # if 筆.武之MACD均值: + 结果.append(筆.武.中) + return 结果 + + +class 线段特征(list): + __slots__ = ["序号", "标识", "线段方向"] + + def __init__(self, 标识: str, 基础序列: List[虚线], 线段方向: 相对方向): + super().__init__(基础序列) + self.序号 = 0 + self.标识: str = 标识 + self.线段方向: 相对方向 = 线段方向 + + @property + def 图表标题(self) -> str: + return self.标识 # f"{self.标识}:{self.序号}" + + def __str__(self): + if not len(self): + return colored(f"{self.标识}<{self.线段方向}, 空>", "green") + return f"{self.标识}<{self.线段方向}, {self.文}, {self.武}, {len(self)}>" + + def __repr__(self): + if not len(self): + return colored(f"{self.标识}<{self.线段方向}, 空>", "green") + return f"{self.标识}<{self.线段方向}, {self.文}, {self.武}, {len(self)}>" + + @property + def 文(self) -> 分型: + if self.线段方向 is 相对方向.向上: # 取高高 + return max( + [线.文 for 线 in self], + key=lambda o: (o.中.分型特征值, o.中.时间戳), + ) + else: + return min( + [线.文 for 线 in self], + key=lambda o: (o.中.分型特征值, -o.中.时间戳.timestamp()), + ) + + @property + def 武(self) -> 分型: + if self.线段方向 is 相对方向.向上: + return max( + [线.武 for 线 in self], + key=lambda o: (o.中.分型特征值, o.中.时间戳), + ) + else: + return min( + [线.武 for 线 in self], + key=lambda o: (o.中.分型特征值, -o.中.时间戳.timestamp()), + ) + + @property + def 高(self) -> float: + return max([self.文, self.武], key=lambda fx: fx.中.分型特征值).中.分型特征值 + + @property + def 低(self) -> float: + return min([self.文, self.武], key=lambda fx: fx.中.分型特征值).中.分型特征值 + + @property + def 方向(self) -> 相对方向: + return self.线段方向.翻转() + + def 添加(self, 待添加虚线: Union[虚线]): + if 待添加虚线.方向 == self.线段方向: + raise ValueError("方向不匹配", self.线段方向, 待添加虚线, self) + self.append(待添加虚线) + + def 删除(self, 待删除虚线: Union[虚线]): + if 待删除虚线.方向 == self.方向: + raise ValueError("方向不匹配", self.线段方向, 待删除虚线, self) + self.remove(待删除虚线) + + @classmethod + def 新建(cls, 虚线序列: List[虚线], 线段方向: 相对方向) -> "线段特征": + return 线段特征(标识=f"特征<{虚线序列[0].__class__.__name__}>", 基础序列=虚线序列, 线段方向=线段方向) + + @classmethod + def 静态分析(cls, 虚线序列: List[虚线], 线段方向: 相对方向, 四象: str, 是否忽视: bool = False) -> List["线段特征"]: + """ + :param 虚线序列: + :param 线段方向: + :param 四象: 老阴,老阳,少阴,小阳 + 老阴 老阳 分别代表 缺口顶分型后的向下线段 与 缺口底分型后的向上线段 + :return: 特征序列元组 + """ + # 确定需要合并的方向序列 + if 四象 in ("老阳", "老阴") and not 是否忽视: + # 特征序列带有缺口时 要严格处理包含关系 + 需要被合并方向序列 = (相对方向.顺, 相对方向.逆, 相对方向.同) + # 需要被合并方向序列 = (相对方向.顺, 相对方向.同) + else: + 需要被合并方向序列 = (相对方向.顺, 相对方向.同) + + 特征序列: List[线段特征] = [] + + for 当前虚线 in 虚线序列: + # ----- 情况1:方向相同(可能触发分型替换)----- + if 当前虚线.方向 is 线段方向: + # 守卫:特征序列不足3个时,直接跳过本虚线(不执行任何合并) + if len(特征序列) < 3: + continue + + 左, 中, 右 = 特征序列[-3], 特征序列[-2], 特征序列[-1] + 结构 = 分型结构.分析(左, 中, 右, 可以逆序包含=True, 忽视顺序包含=True) + + # 条件:向上线段形成顶分型且当前虚线高点高于中,或向下线段形成底分型且当前虚线低点低于中 + if (线段方向 is 相对方向.向上 and 结构 is 分型结构.顶 and 当前虚线.高 > 中.高) or (线段方向 is 相对方向.向下 and 结构 is 分型结构.底 and 当前虚线.低 < 中.低): + 小号虚线 = min(中, key=lambda o: o.序号) + 大号虚线 = max(右, key=lambda o: o.序号) + fake = 虚线.创建笔(文=小号虚线.文, 武=大号虚线.武, 有效性=False) + 特征序列.pop() + 特征序列[-1] = 线段特征.新建([fake], 线段方向) + # 无论是否替换,本虚线不进入合并逻辑 + continue + + # ----- 情况2:方向不同(执行特征序列的合并/添加)----- + # 若特征序列为空,直接添加新特征序列 + if not 特征序列: + 特征序列.append(线段特征.新建([当前虚线], 线段方向)) + continue + + # 特征序列非空:检查与最后一个特征序列的方向关系 + 之前线段特征 = 特征序列[-1] + if 相对方向.分析(之前线段特征.高, 之前线段特征.低, 当前虚线.高, 当前虚线.低) in 需要被合并方向序列: + 之前线段特征.添加(当前虚线) + else: + 特征序列.append(线段特征.新建([当前虚线], 线段方向)) + + return 特征序列 + + @classmethod + def 获取分型序列(cls, 特征序列: List): + 结构序列 = [] + for i in range(1, len(特征序列) - 1): + 结构 = 分型结构.分析(特征序列[i - 1], 特征序列[i], 特征序列[i + 1], True, True) + 结构序列.append(特征分型(特征序列[i - 1], 特征序列[i], 特征序列[i + 1], 结构)) + if 结构序列: + assert 特征序列[-1] is 结构序列[-1].右 + return 结构序列 + + +class 特征分型: + __slots__ = ["左", "中", "右", "结构"] + + def __init__(self, 左: 线段特征, 中: 线段特征, 右: 线段特征, 结构: 分型结构): + self.左: 线段特征 = 左 + self.中: 线段特征 = 中 + self.右: 线段特征 = 右 + self.结构 = 结构 + + def __str__(self): + return f"特征分型<{self.结构}, {self.中}>" + + def __repr__(self): + return f"特征分型<{self.结构}, {self.中}>" + + +class 线段(object): + __slots__ = [] + + @classmethod + def 添加虚线(cls, 段: 虚线, 筆: 虚线): + if len(段.基础序列) and not 分型.判断分型(段.基础序列[-1].武, 筆.文): + raise ValueError(f"{段.标识}.添加虚线 不连续", 段.基础序列[-1], 筆) + + if len(段.基础序列) and 段.基础序列[-1].标识 != 筆.标识: + raise ValueError(f"{段.标识}.添加虚线 标识不符", 段.基础序列[-1].标识, 筆.标识) + 段.基础序列.append(筆) + + @classmethod + def 武斗(cls, 段: 虚线, 武: 分型, 行号: int): + # print(f"{段.标识}.武斗[{行号}], ", 武) + if 段.武 is 武: + # print(f"{段.标识}.武斗[{行号}], 相同") + return + if 段.武.分型特征值 == 武.分型特征值 and 段.武.时间戳 != 武.时间戳: + print(f"{段.标识}.武斗[{行号}], 发现特征值相等但时间戳不同", 段.武, 武) + assert 段.文.结构 is not 武.结构, (f"文武结构相同 {行号}", 段.文, 武) + if 武.右 is not None and 分型结构.分析(武.左, 武.中, 武.右) is not 武.结构: + raise RuntimeError(分型结构.分析(武.左, 武.中, 武.右), 武.结构) + if 段.方向 is 相对方向.向上: + if 武.分型特征值 < 段.文.分型特征值: + raise RuntimeError(f"向上{段.标识}, 结束点 小于 起点", 段.标识, 段.文, 武) + # if max([段.武, 武], key=lambda k: k.分型特征值) is not 武 and 段.模式 == "文武": + # pass # print(colored(f"{段.标识}.武斗[{行号}] 出现回退 从 {段.武} ==>>> {武}", "red", "on_green")) # raise RuntimeError(段.武, 武) + else: + if 武.分型特征值 > 段.文.分型特征值: + raise RuntimeError(f"向下{段.标识}, 结束点 大于 起点", 段.标识, 段.文, 武) + # if min([段.武, 武], key=lambda k: k.分型特征值) is not 武 and 段.模式 == "文武": + # pass # print(colored(f"{段.标识}.武斗[{行号}] 出现回退 从 {段.武} ==>>> {武}", "red", "on_green")) # raise RuntimeError(段.武, 武) + 段.武 = 武 + + @classmethod + def 特征分型终结(cls, 段: 虚线) -> bool: + """ + 是否符合特征序列 正常分型 终结 + """ + 特征序列 = 线段特征.静态分析(段.基础序列, 段.方向, 线段.四象(段)) + if len(特征序列) >= 3: + 结构 = 分型结构.分析(特征序列[-3], 特征序列[-2], 特征序列[-1], True, True) + if 段.方向 is 相对方向.向上: + if 结构 is 分型结构.顶: + return True + else: + if 结构 is 分型结构.底: + return True + + return False + + @classmethod + def 特征序列状态(cls, 段: 虚线) -> Tuple[bool, bool, bool]: + return tuple(特征 is not None for 特征 in 段.特征序列) + + @classmethod + def 获取缺口(cls, 段: 虚线) -> Optional[缺口]: + if 段.模式 != "文武": + return None + 左, 中, 右 = 段.特征序列 + if 左 is None: + return None + if 中 is None: + return None + 相对关系 = 相对方向.分析(左.高, 左.低, 中.高, 中.低) + if 相对关系.是否缺口(): + 高低 = [左.文.分型特征值, 中.文.分型特征值] + return 缺口(max(*高低), min(*高低)) + return None + + @classmethod + def 四象(cls, 段: 虚线) -> str: + """ + 老阳: 向下线段第一二特征序列有缺口时,后一向上线段 + 老阴: 向上线段第一二特征序列有缺口时,后一向下线段 + 小阳: 向上线段 + 少阴: 向下线段 + """ + if 段.前一缺口 is not None: + return "老阳" if 段.方向 is 相对方向.向上 else "老阴" + return "小阳" if 段.方向 is 相对方向.向上 else "少阴" + + @classmethod + def 设置特征序列(cls, 段: 虚线, 序列, 行号): + # print(f"线段.设置特征序列[{行号}]", self) + if 段.模式 != "文武": + return + + for 特征 in 序列: + if 特征 and 特征.方向 == 段.方向: + raise ValueError(f"特征序列方向不匹配[{行号}]") + 左, 中, 右 = 序列 + 段.特征序列 = [左, 中, 右] + if 右 is not None: + 基础序列 = [] + if 右[-1] not in 段.基础序列: + raise ValueError() + for 元素 in 段.基础序列: + 基础序列.append(元素) + if 元素 is 右[-1]: + break + + if (len(基础序列) >= 6) and (len(基础序列) % 2 == 0): + 段.基础序列[:] = 基础序列[:] + else: + raise RuntimeError() + else: + pass + + @classmethod + def 刷新特征序列(cls, 段: 虚线, 配置: 缠论配置): + if 段.模式 != "文武": + return + 基础序列 = 段.基础序列 + if 段.前一结束位置 and 段.前一结束位置 in 基础序列: + 基础序列 = 段.基础序列[段.基础序列.index(段.前一结束位置) - 1 :] + + 特征序列 = 线段特征.静态分析(基础序列, 段.方向, 线段.四象(段), 配置.线段_特征序列忽视老阴老阳) + if len(特征序列) >= 3: + 分型序列 = 线段特征.获取分型序列(特征序列) + if (段.方向 is 相对方向.向上 and 分型序列[-1].结构 is 分型结构.顶) or (段.方向 is 相对方向.向下 and 分型序列[-1].结构 is 分型结构.底): + 线段.设置特征序列(段, [分型序列[-1].左, 分型序列[-1].中, 分型序列[-1].右], sys._getframe().f_lineno) + + else: + 线段.设置特征序列(段, [特征序列[-2], 特征序列[-1], None], sys._getframe().f_lineno) + else: + 特征序列.extend([None] * (3 - len(特征序列))) + 线段.设置特征序列(段, 特征序列, sys._getframe().f_lineno) + + @classmethod + def 分割序列(cls, 段: 虚线, 所属中枢: Optional["中枢"] = None) -> Tuple[List[虚线], List[虚线], List[虚线], Optional[虚线]]: + if 段.模式 != "文武": + return 段.基础序列[:], [], [], None + if len(段.基础序列) == 0: + print(段.标识, 段.序号) + assert 段.基础序列[0].文 is 段.文, (段.基础序列[0].文, 段.文) + 前: List[虚线] = [] + 后: List[虚线] = [] + 第三买卖线 = [] + 贯穿伤 = None + + for 筆 in 段.基础序列: + if not 前: + 前.append(筆) + continue + if 前[-1].武 is not 段.武 and not 后: + 前.append(筆) + + if 后: + 后.append(筆) + if 筆.文 is 段.武: + 后.append(筆) + + 状态 = None + + if 所属中枢: + 所属中枢.本级_第三买卖线 = None + 尾部 = 段.武 + if 后: + 尾部 = 后[-1].武 + if 所属中枢.高 >= 尾部.分型特征值 >= 所属中枢.低: + 状态 = "中枢之中" + elif 所属中枢.高 < 尾部.分型特征值: + 状态 = "中枢之上" + elif 所属中枢.低 > 尾部.分型特征值: + 状态 = "中枢之下" + assert "中枢" in 状态 + + if 状态 == "中枢之上": + for 筆 in 段.基础序列[::-1]: + if 筆.方向 is 相对方向.向下: + 关系 = 相对方向.分析(所属中枢.高, 所属中枢.低, 筆.高, 筆.低) + if 关系 is 相对方向.向上缺口: + 第三买卖线.append(筆) + else: + break + + if 状态 == "中枢之下": + for 筆 in 段.基础序列[::-1]: + if 筆.方向 is 相对方向.向上: + 关系 = 相对方向.分析(所属中枢.高, 所属中枢.低, 筆.高, 筆.低) + if 关系 is 相对方向.向下缺口: + 第三买卖线.append(筆) + else: + break + + if 第三买卖线 and 所属中枢: + 第三买卖线.reverse() + 所属中枢.本级_第三买卖线 = 第三买卖线[0] + # 所属中枢.本级_第三买卖线.备注 = 所属中枢.标识 + + if 后: + if 段.方向.是否向上(): + if 后[0].武.分型特征值 < 段.文.分型特征值: + 贯穿伤 = 后[0] + else: + if 后[0].武.分型特征值 > 段.文.分型特征值: + 贯穿伤 = 后[0] + + return 前, 后, 第三买卖线, 贯穿伤 + + @classmethod + def 刷新(cls, 段: 虚线, 配置: 缠论配置): + if 段.模式 != "文武": + return + if not len(段.基础序列): + print(" 线段.刷新 基础序列为空") + return + + 线段.刷新特征序列(段, 配置) + 有效特征序列 = [特征 for 特征 in 段.特征序列 if 特征 is not None] + if len(有效特征序列) == 3: + 线段.武斗(段, 段.特征序列[1].文, sys._getframe().f_lineno) + + elif len(有效特征序列) >= 1: + 最近特征 = 有效特征序列[-1] + + if 最近特征[-1] not in 段.基础序列: + 特征后一笔 = 笔.以武会友(段.基础序列, 最近特征[-1].武) + else: + 特征后一笔 = 最近特征[-1] + + if 特征后一笔 is not None: + 序号 = 段.基础序列.index(特征后一笔) + if 序号 < len(段.基础序列) - 1: + 下一笔 = 段.基础序列[序号 + 1] + if (段.方向 is 相对方向.向上 and 段.高 <= 下一笔.高) or (段.方向 is 相对方向.向下 and 段.低 >= 下一笔.低): + 线段.武斗(段, 下一笔.武, sys._getframe().f_lineno) + else: + print(" 线段.刷新 特征后一笔 = None, ", 段, 有效特征序列) + else: + raise RuntimeError(len(有效特征序列)) + 线段.获取内部中枢序列(段, 配置) + + @classmethod + def 序列重置(cls, 段: 虚线, 序列: Sequence): + 基础序列 = [] + for 元素 in 段.基础序列: + if 元素 not in 序列: + break + if 基础序列: + if not 基础序列[-1].之后是(元素): + break + 基础序列.append(元素) + + 段.基础序列[:] = 基础序列[:] + 段.特征序列[2] = None + + @classmethod + def 查找贯穿伤(cls, 段: 虚线) -> Optional[虚线]: + for 贯穿伤 in 段.基础序列[3:]: + if 段.方向.是否向上(): + if 贯穿伤.武.分型特征值 < 段.文.分型特征值: + return 贯穿伤 + else: + if 贯穿伤.武.分型特征值 > 段.文.分型特征值: + return 贯穿伤 + return None + + @classmethod + def 获取内部中枢序列(cls, 段: 虚线, 配置: 缠论配置) -> Tuple[List["中枢"], List["中枢"], List["中枢"]]: + # 线段内部如存在中枢则级别比无中枢要大 + if 段.模式 != "文武": + 中枢.分析(段.基础序列, 段.合_中枢序列, 标识=f"{段.标识}_{段.序号}_合_") + return [], [], 段.合_中枢序列 + 实, 虚, _, _ = 线段.分割序列(段) + + 中枢.分析(实, 段.实_中枢序列, 标识=f"{段.标识}_{段.序号}_实_") + 中枢.分析(虚, 段.虚_中枢序列, 标识=f"{段.标识}_{段.序号}_虚_") + 中枢.分析(段.基础序列, 段.合_中枢序列, 标识=f"{段.标识}_{段.序号}_合_") + return 段.虚_中枢序列, 段.实_中枢序列, 段.合_中枢序列 # 阴 阳 合 + + @classmethod + def 基础判断(cls, 左: 虚线, 中: 虚线, 右: 虚线, 关系序列: List[相对方向]) -> bool: + """ + 连续三笔且重叠 + """ + + if not 左.之后是(中): + return False + if not 中.之后是(右): + return False + + if not 相对方向.分析(左.高, 左.低, 中.高, 中.低).是否包含(): + return False + if not 相对方向.分析(中.高, 中.低, 右.高, 右.低).是否包含(): + return False + + 关系 = 相对方向.分析(左.高, 左.低, 右.高, 右.低) + if 关系 not in 关系序列: + return False + + if 左.方向 is 相对方向.向下 and not 关系.是否向下(): + return False + if 左.方向 is 相对方向.向上 and not 关系.是否向上(): + return False + return True + + @classmethod + def _添加线段(cls, 线段序列: List[虚线], 待添加线段: 虚线, 配置: 缠论配置, 行号: str): + if 线段序列 and not 线段序列[-1].之后是(待添加线段): + raise ValueError(f"线段.向序列中添加 不连续[{行号}]", 线段序列[-1].武, 待添加线段.文) + 待添加线段.模式 = "文武" + + if not 线段序列: + 线段序列.append(待添加线段) + return + + 之前线段 = 线段序列[-1] + + if not 之前线段.特征序列[2] and not 之前线段.短路修正: + assert not 待添加线段.短路修正 and 之前线段.特征序列[2][-1] in 待添加线段.基础序列 + raise RuntimeError(f"线段._向序列中添加[{行号}], 之前线段.右 = None", 之前线段) + + if 之前线段.基础序列[-1] not in 待添加线段.基础序列 and not 之前线段.短路修正: + raise RuntimeError(f"线段._向序列中添加[{行号}], 之前线段[-1] not in 待添加虚线!", 之前线段) + + 待添加线段.序号 = 之前线段.序号 + 1 + 待添加线段.前一缺口 = 线段.获取缺口(之前线段) + 待添加线段.前一结束位置 = 之前线段.基础序列[-1] + + if 线段.四象(之前线段) in ("老阴", "老阳"): + 待添加线段.前一缺口 = None + + 线段序列.append(待添加线段) + # print(f"线段._向序列中添加[{行号}]", 待添加虚线) + + @classmethod + def _弹出线段(cls, 线段序列: List[虚线], 待弹出线段: 虚线, 配置: 缠论配置, 行号: str): + if not 线段序列: + return None + + if 线段序列[-1] is not 待弹出线段: + raise ValueError("线段._从序列中删除 弹出数据不在列表中", 待弹出线段) + + 左, 中, 右 = 待弹出线段.特征序列 + if 右 is not None: + 结构 = 分型结构.分析(左, 中, 右, True, True) + if 结构 in (分型结构.顶, 分型结构.底) and not 相对方向.分析(左.高, 左.低, 中.高, 中.低).是否缺口(): + print(colored(f"[警告<{行号}>]:", "yellow"), colored("线段._从序列中删除 发现分型完毕, 且特征序列无缺口", "red"), 待弹出线段) + + 线段序列.pop() + 待弹出线段.前一结束位置 = None + 待弹出线段.有效性 = False + + return 待弹出线段 + + @classmethod + def _缺口突破(cls, 线段序列: List[虚线], 配置: 缠论配置, 层级: int) -> bool: + 当前线段 = 线段序列[-1] + 当前虚线: 虚线 = 当前线段.基础序列[-1] + 四象 = 线段.四象(当前线段) + 同向 = 当前虚线.方向 is 当前线段.方向 + + # 条件1:不能同向 + if 同向: + return False + + # 条件2:四象必须是老阳或老阴 + if 四象 not in ("老阳", "老阴"): + return False + + # 条件3:当前线段特征序列[2]必须为None + if 当前线段.特征序列[2] is not None: + return False + + # 条件4:具体突破方向判断 + if not ((四象 == "老阳" and 当前虚线.低 < 当前线段.低) or (四象 == "老阴" and 当前虚线.高 > 当前线段.高)): + return False + + # 执行修正 + 序列 = 当前线段.基础序列[:] + 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 当前线段 = 线段序列[-1] + assert 当前线段.特征序列[2] is not None + 当前线段基础序列 = 线段.分割序列(当前线段)[0] + 当前线段基础序列.extend(序列) + + 当前线段.基础序列[:] = 当前线段基础序列[:] + 线段.刷新(当前线段, 配置) + return True + + @classmethod + def _非缺口下穿刺(cls, 线段序列: List[虚线], 配置: 缠论配置, 层级: int) -> bool: + 当前线段 = 线段序列[-1] + 四象 = 线段.四象(当前线段) + + # 外层条件 + if not (配置.线段_非缺口下穿刺 and 四象 in ("小阳", "少阴") and 当前线段.特征序列[2] is None): + return False + + # 查找贯穿伤 + 贯穿伤 = 线段.查找贯穿伤(当前线段) + if not 贯穿伤: + return False + + # 切割基础序列 + 基础序列 = 当前线段.基础序列[当前线段.基础序列.index(贯穿伤) :] + + # 长度条件 + if not (len(基础序列) == 4 and len(线段序列) >= 2): + return False + + 左, 中, 右 = 基础序列[-3], 基础序列[-2], 基础序列[-1] + + # 方向条件 + if 相对方向.分析(左.高, 左.低, 右.高, 右.低) is not 当前线段.方向: + return False + + # 执行修正 + print(colored(f"[警告<{sys._getframe().f_lineno}, {层级}>]:", "yellow"), colored("线段.修复贯穿伤", "red"), 贯穿伤, 基础序列) # 异常弹出 + + 基础序列 = 当前线段.基础序列[:] + 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 当前线段 = 线段序列[-1] + 当前线段.特征序列[2] = None + for 临时虚线 in 基础序列[基础序列.index(当前线段.基础序列[-1]) + 1 :]: + 线段.添加虚线(当前线段, 临时虚线) + 线段.刷新(当前线段, 配置) + + if 当前线段.特征序列[2]: + 段 = 虚线.创建线段([左, 中, 右]) + 线段._添加线段(线段序列, 段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 段.特征序列[0] = 线段特征.新建([中], 段.方向) + + return True + + @classmethod + def _缺口后紧急修正(cls, 线段序列: List[虚线], 配置: 缠论配置, 层级: int) -> bool: + 当前线段 = 线段序列[-1] + 四象 = 线段.四象(当前线段) + + # 外层条件检查 + if not (配置.线段_缺口后紧急修正 and not 配置.线段_特征序列忽视老阴老阳 and 四象 in ("小阳", "少阴") and 当前线段.特征序列[2] is None): + return False + + # 内层条件:长度和前一线的四象 + if not (len(线段序列) >= 2 and 线段.四象(线段序列[-2]) in ("老阴", "老阳")): + return False + + 基础序列 = 线段.分割序列(当前线段)[1] + if len(基础序列) < 3: + return False + + 需要修正 = False + if 当前线段.方向 is 相对方向.向上: + if 相对方向.分析(基础序列[0].高, 基础序列[0].低, 基础序列[2].高, 基础序列[2].低) is 相对方向.向下: + 需要修正 = True + else: # 向下方向 + if 相对方向.分析(基础序列[0].高, 基础序列[0].低, 基础序列[2].高, 基础序列[2].低) is 相对方向.向上: + 需要修正 = True + + if not 需要修正: + return False + + # 执行修正 + 当前线段.短路修正 = True + 新段 = 虚线.创建线段(基础序列) + 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + return True + + @classmethod + def _修正(cls, 线段序列: List[虚线], 配置: 缠论配置, 层级: int) -> bool: + 当前线段 = 线段序列[-1] + + # 条件1:配置允许修正且当前线段基础序列长度足够 + if not (配置.线段_修正 and len(当前线段.基础序列) >= 9): + return False + + # 分割序列 + 当前基础序列, 之后基础序列, _, _ = 线段.分割序列(当前线段) + + # 条件2:之后基础序列长度至少为6 + if len(之后基础序列) < 6: + return False + + # 取倒数第3和第1个元素 + 前, 后 = 之后基础序列[-3], 之后基础序列[-1] + + # 条件3:当前线段方向与后两个元素形成的方向一致 + if 当前线段.方向 is not 相对方向.分析(前.高, 前.低, 后.高, 后.低): + return False + + # 所有条件满足,执行修正 + 当前线段.短路修正 = True + + # 创建第一个新段(之后基础序列去掉最后3个) + 新段 = 虚线.创建线段(之后基础序列[:-3]) + 新段.短路修正 = True + 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + + # 根据当前线段的四象决定是否清空前一个缺口 + if 线段.四象(当前线段) in ("老阴", "老阳"): + 新段.前一缺口 = None + + # 创建第二个新段(最后3个元素) + 新段 = 虚线.创建线段(之后基础序列[-3:]) + 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + + return True + + @classmethod + def 分析(cls, 笔序列: List[虚线], 线段序列: List[虚线], 配置: 缠论配置, 层级: int = 0, 关系序列=[相对方向.向上, 相对方向.向下]) -> None: + """ + 注意笔序列前三个元素必须符合线段基本要求 + 四象: 老阴,老阳,少阴,小阳 + 老阴 老阳 分别代表 缺口顶分型后的向下线段 与 缺口底分型后的向上线段 + 当其分型完成时需要对 线段.前一缺口 设置为None,新线段不在考虑之前是否有缺口的问题 + 无缺口: 即笔破坏 + 笔破坏不去处理特征序列的逆序包含 + """ + # 递归深度守卫 + if 层级 > 256: + print("线段.分析 递归深度超出 256") + return None + # raise RuntimeError("线段分析 层级过深") + + 线段递归分析 = 线段.分析 + + # -------------------- 1. 初始化第一个线段 -------------------- + if not 线段序列: + for i in range(1, len(笔序列) - 1): + 左, 中, 右 = 笔序列[i - 1], 笔序列[i], 笔序列[i + 1] + if not 线段.基础判断(左, 中, 右, 关系序列): # FIXME 首个线段必须有明确方向 + continue + 段 = 虚线.创建线段([左, 中, 右]) + 线段._添加线段(线段序列, 段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 段.特征序列[0] = 线段特征.新建([中], 段.方向) + break + if not 线段序列: + return None + + # -------------------- 2. 清理无效的尾部引用 -------------------- + while 线段序列 and 线段序列[-1].前一结束位置: + if 线段序列[-1].前一结束位置 not in 笔序列: + 线段._弹出线段(线段序列, 线段序列[-1], 配置, f"{sys._getframe().f_lineno}, {层级}") + else: + break + + if not 线段序列: + return 线段递归分析(笔序列, 线段序列, 配置, 层级 + 1, 关系序列) + + # -------------------- 3. 确保当前线段有效 -------------------- + 当前线段 = 线段序列[-1] + 线段.序列重置(当前线段, 笔序列) + + if len(当前线段.基础序列) < 3: + 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") + if not 线段序列: + return 线段递归分析(笔序列, 线段序列, 配置, 层级 + 1, 关系序列) + + 当前线段 = 线段序列[-1] + + # -------------------- 4. 特征序列已完整时的处理 -------------------- + if 当前线段.特征序列[2] is not None: + 基础序列 = 线段.分割序列(当前线段)[1] + 新段 = 虚线.创建线段(基础序列) + 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + if 线段.四象(当前线段) in ("老阴", "老阳"): + 新段.前一缺口 = None + + 当前线段 = 线段序列[-1] + 线段.刷新(当前线段, 配置) + + # -------------------- 5. 调用一次全局修正(不循环) -------------------- + 线段._缺口突破(线段序列, 配置, 层级) + 线段._非缺口下穿刺(线段序列, 配置, 层级) + 线段._缺口后紧急修正(线段序列, 配置, 层级) + 线段._修正(线段序列, 配置, 层级) + + # -------------------- 6. 循环处理后续的笔 -------------------- + 当前线段 = 线段序列[-1] + if not 当前线段.基础序列: + raise RuntimeError + 起始索引 = 笔序列.index(当前线段.基础序列[-1]) + 1 + + for 当前虚线 in 笔序列[起始索引:]: + 当前线段 = 线段序列[-1] + 四象 = 线段.四象(当前线段) + + 线段.添加虚线(当前线段, 当前虚线) + 线段.刷新(当前线段, 配置) + + # 依次尝试四种修正,任意一个成功则跳过后续处理 + if 线段._缺口突破(线段序列, 配置, 层级): + continue + if 线段._非缺口下穿刺(线段序列, 配置, 层级): + continue + if 线段._缺口后紧急修正(线段序列, 配置, 层级): + continue + if 线段._修正(线段序列, 配置, 层级): + continue + + # 无修正触发,且特征序列[2]已存在 → 创建新段 + if 当前线段.特征序列[2] is None: + continue + + 基础序列 = 线段.分割序列(当前线段)[1] + 新段 = 虚线.创建线段(基础序列) + 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + if 四象 in ("老阴", "老阳"): + 新段.前一缺口 = None + + # 检查新段与当前虚线的连续性 + if 新段.基础序列[-1] is not 当前虚线: + if not 新段.基础序列[-1].之后是(当前虚线): + return 线段递归分析(笔序列, 线段序列, 配置, 层级 + 1, 关系序列) + 线段.添加虚线(新段, 当前虚线) + + 线段.刷新(新段, 配置) + + return None + + @classmethod + def 武终(cls, 段: 虚线, 行号: int): + if 段.模式 != "文武": + 线段.武斗(段, 段.基础序列[-1].武, 行号) + + @classmethod + def 验证序列(cls, 段: 虚线, 序列: Sequence): + 基础序列 = [] + for 元素 in 段.基础序列: + if 元素 not in 序列: + break + if 基础序列: + if not 基础序列[-1].之后是(元素): + print(" 线段._验证序列 数据不连续") + break + 基础序列.append(元素) + 段.基础序列[:] = 基础序列[:] + if len(段.基础序列) % 2 == 0: + 段.基础序列 and 段.基础序列.pop() + + @classmethod + def _添加扩展线段(cls, 线段序列: List[虚线], 待添加线段: 虚线, 行号: int): + 待添加线段.模式 = "高低" + 待添加线段.标识 = f"扩展{待添加线段.标识}" if 待添加线段.基础序列[0].标识 != "笔" else "扩展线段" + if 线段序列 and not 线段序列[-1].之后是(待添加线段): + raise ValueError(f"{线段序列[-1].标识}.向序列中添加 不连续[{行号}]", 线段序列[-1].武, 待添加线段.文) + if 线段序列: + 之前线段 = 线段序列[-1] + 待添加线段.序号 = 之前线段.序号 + 1 + + 线段序列.append(待添加线段) + # print(f"线段._向序列中添加[{行号}]", 待添加线段) + + @classmethod + def _弹出扩展线段(cls, 线段序列: List[虚线], 待弹出线段: 虚线, 行号: int): + if not 线段序列: + return None + + if 线段序列[-1] is 待弹出线段: + drop = 线段序列.pop() + 待弹出线段.有效性 = False + # print(f"线段._从序列中删除[{行号}]", 待弹出线段) + return drop + raise ValueError("线段._从序列中删除 弹出数据不在列表中", 待弹出线段) + + @classmethod + def 扩展分析(cls, 虚线序列: List[虚线], 线段序列: List[虚线], 配置: 缠论配置) -> None: + """ + 即同级别分析 + 将笔看成线段 + """ + if not 虚线序列: + return None + try: + 虚线序列[2] + except IndexError: + return None + 线段递归扩展分析 = 线段.扩展分析 + + if not 线段序列: + for i in range(1, len(虚线序列) - 1): + 左, 中, 右 = 虚线序列[i - 1], 虚线序列[i], 虚线序列[i + 1] + 关系 = 相对方向.分析(左.高, 左.低, 右.高, 右.低) + if 关系 not in (相对方向.向下, 相对方向.向上, 相对方向.顺, 相对方向.逆, 相对方向.同): # FIXME 此处为首个线段 + continue + + 段 = 虚线.创建线段([左, 中, 右]) + 线段._添加扩展线段(线段序列, 段, sys._getframe().f_lineno) + break + + # 检查线段元素 + if not 线段序列: + return None + + 当前线段 = 线段序列[-1] + 线段.验证序列(当前线段, 虚线序列) + if len(当前线段.基础序列) < 3: + 线段._弹出扩展线段(线段序列, 当前线段, sys._getframe().f_lineno) + return 线段递归扩展分析(虚线序列, 线段序列, 配置) + + if not 配置.扩展线段_当下分析: + 左, 中, 右 = 当前线段.基础序列[:3] + if not 相对方向.分析(左.高, 左.低, 右.高, 右.低).是否缺口(): + 当前线段.基础序列[:] = 当前线段.笔序列[:3] + 线段.武终(当前线段, sys._getframe().f_lineno) + else: + 线段._弹出扩展线段(线段序列, 当前线段, sys._getframe().f_lineno) + return 线段递归扩展分析(虚线序列, 线段序列, 配置) + + 线段.武终(当前线段, sys._getframe().f_lineno) + if 当前线段.基础序列[-1].序号 + 3 > 虚线序列[-1].序号: + return None + + 序号 = 虚线序列.index(当前线段.基础序列[-1]) + 1 + if 序号 >= len(虚线序列): + return None + + for i in range(序号 + 1, len(虚线序列) - 1): + 左, 中, 右 = 虚线序列[i - 1], 虚线序列[i], 虚线序列[i + 1] + 相对关系 = 相对方向.分析(左.高, 左.低, 右.高, 右.低) + if 相对关系.是否缺口(): + 线段.添加虚线(当前线段, 左) + 线段.添加虚线(当前线段, 中) + 线段.武终(当前线段, sys._getframe().f_lineno) + continue + + if 左 in 当前线段.基础序列: + continue + + 段 = 虚线.创建线段([左, 中, 右]) + 线段._添加扩展线段(线段序列, 段, sys._getframe().f_lineno) + return 线段递归扩展分析(虚线序列, 线段序列, 配置) + + @classmethod + @lru_cache(maxsize=128) + def 判断线段内部是否背驰(cls, 当前段: 虚线, 观察员: 观察者): + 虚, 实, 合 = 线段.获取内部中枢序列(当前段, 观察员.配置) + 阳, 阴, _, _ = 线段.分割序列(当前段) + if len(阴) > 0: + """ + 1.大于2且未完成,第二特征序列正在合并中, + 0.继续合并 + 1.之后突破第二序列线段延续 + 2.反之 出现第三特征序列线段完成 出现相反线段 + 1.有缺口 + 2.无缺口 + + 总之就是有 阴 则不判断 + """ + 笔之实数 = len(阳) + if 笔之实数 < 3: + return False + + 进入段 = 阳[-3] + 离开段 = 阳[-1] + assert 进入段.序号 < 离开段.序号 + 关系 = 相对方向.分析(进入段.高, 进入段.低, 离开段.高, 离开段.低) + 背驰 = False + 盘整背驰 = False + if ((进入段.方向.是否向上() and 关系.是否向上()) or (进入段.方向.是否向下() and 关系.是否向下())) and 背驰分析.背驰模式(进入段, 离开段, 观察员.普通K线序列, 观察员.配置, 观察员.配置.线段内部背驰_模式): + k线序列 = K线.截取(观察员.普通K线序列, 阳[-3].文.中.标的K线, 阳[-1].武.中.标的K线) + if len(虚线.计算MACD柱子分段(k线序列)) >= 3: + 盘整背驰 = True # and MACD均值背驰 + + """进入段 = 阳[0] + 离开段 = 阳[-1] + if ((进入段.方向.是否向上() and 关系.是否向上()) or (进入段.方向.是否向下() and 关系.是否向下())) and 背驰分析.背驰模式(进入段, 离开段, 观察员.普通K线序列, 观察员.配置, 观察员.配置.线段内部背驰_模式): + 盘整背驰 = 阳[-1].武之MACD均值 if not 盘整背驰 else 盘整背驰""" + + if 当前段.实_中枢序列: + if 阳[-1] in 当前段.实_中枢序列[-1].基础序列: + # 当前最后一笔在最后一中枢里 + 序号 = 当前段.基础序列.index(当前段.实_中枢序列[-1].基础序列[0]) + 进入段 = 当前段.基础序列[序号 - 1] + 离开段 = 阳[-1] + assert 进入段.序号 < 离开段.序号, (进入段.序号, 离开段.序号) + if 进入段.方向 is not 离开段.方向: + return 背驰分析.测度背驰(进入段, 离开段) and 虚线.买卖意义(离开段, 观察员) + 关系 = 相对方向.分析(进入段.高, 进入段.低, 离开段.高, 离开段.低) + if ((进入段.方向.是否向上() and 关系.是否向上()) or (进入段.方向.是否向下() and 关系.是否向下())) and 背驰分析.背驰模式(进入段, 离开段, 观察员.普通K线序列, 观察员.配置, 观察员.配置.线段内部背驰_模式): + return True + + elif 当前段.实_中枢序列[-1].第三买卖线: + # 第三买卖点后后 盘整背驰 + 进入段 = 阳[-3] + 离开段 = 阳[-1] + assert 进入段.序号 < 离开段.序号 + if 进入段.方向 is not 离开段.方向: + return 背驰分析.测度背驰(进入段, 离开段) and 虚线.买卖意义(离开段, 观察员) + 关系 = 相对方向.分析(进入段.高, 进入段.低, 离开段.高, 离开段.低) + if ((进入段.方向.是否向上() and 关系.是否向上()) or (进入段.方向.是否向下() and 关系.是否向下())) and 背驰分析.背驰模式(进入段, 离开段, 观察员.普通K线序列, 观察员.配置, 观察员.配置.线段内部背驰_模式): + return True + else: + # 没有中枢 + if 笔之实数 == 3: + 背驰 = 盘整背驰 + + return 背驰 or 盘整背驰 + + @classmethod + def 段获取所有停顿位置(cls, 段: 虚线, 观察员: "观察者"): + self = 段 + 结果 = [] + if self.模式 != "文武": + return 结果 + if self.标识 != "线段": # 不考虑段的段,不如直接更换K线周期 + return 结果 + 阳, 阴, _, _ = 线段.分割序列(段, None) + 线段序列 = [] + 笔序列 = [] + 当前停顿 = None + + for 筆 in 阳: + if len(笔序列) >= 2: + 筆停顿 = 笔.获取所有停顿位置(筆, 观察员) + 筆停顿.append(筆) + for 停顿 in 筆停顿: + 笔序列.append(停顿) + 线段.分析(笔序列, 线段序列, 观察员.配置, 关系序列=[相对方向.向下, 相对方向.向上, 相对方向.顺, 相对方向.逆, 相对方向.同]) + if 线段序列 and 线段序列[-1].武 is not 当前停顿: + 新段 = 虚线.创建线段(线段序列[-1][:]) + 新段.序号 = self.序号 + 线段.刷新(新段, 观察员.配置) + if 新段.方向 is self.方向: + 结果.append(新段) + 当前停顿 = 线段序列[-1].武 + + if 停顿 is not 筆: + 笔序列.pop().有效性 = False + else: + 笔序列.append(筆) + return 结果 + + @classmethod + def 是否背驰过(cls, 当前段: 虚线, 观察员: "观察者"): + 停顿位置 = cls.段获取所有停顿位置(当前段, 观察员) + 结果 = [] + + for 段 in 停顿位置: + 段.获取内部中枢序列(观察员.配置) + if cls.判断线段内部是否背驰(段, 观察员): + 结果.append(段.武.中) + return 结果 + + +class 中枢(object): + __slots__ = ["序号", "标识", "级别", "基础序列", "第三买卖线", "本级_第三买卖线"] + + def __init__(self, 序号: int, 标识: str, 级别: int, 基础序列: List[虚线]): + self.基础序列 = 基础序列[:3] + self.序号: int = 序号 + self.标识: str = 标识 + self.级别: int = 级别 + self.第三买卖线: Optional[虚线] = None + self.本级_第三买卖线: Optional[虚线] = None + + def 添加虚线(self, 实线: 虚线): + self.基础序列.append(实线) + self.本级_第三买卖线: Optional[虚线] = None + self.第三买卖线: Optional[虚线] = None + + def __str__(self): + return f"{self.标识}({self.高:g}, {self.低:g}, 元素数量: {len(self.基础序列)}, {str(self.基础序列)}, {self.基础序列[0].文} ===>>> {self.基础序列[-1].武})" + + def __repr__(self): + return str(self) + + @property + def 图表标题(self) -> str: + return f"{self.文.中.标识}:{self.文.中.周期}:{self.标识}:{self.序号}" + + @property + def 离开段(self) -> 虚线: + return self.基础序列[-1] + + @property + def 方向(self) -> 相对方向: + return self.基础序列[0].方向.翻转() + + @property + def 高(self) -> float: + return min(self.基础序列[:3], key=lambda o: o.高).高 + + @property + def 低(self) -> float: + return max(self.基础序列[:3], key=lambda o: o.低).低 + + @property + def 高高(self) -> float: + if len(self.基础序列) > 3: + return max(self.基础序列, key=lambda o: o.高).高 + return max(self.基础序列, key=lambda o: o.高).高 + + @property + def 低低(self) -> float: + if len(self.基础序列) > 3: + return min(self.基础序列, key=lambda o: o.低).低 + return min(self.基础序列, key=lambda o: o.低).低 + + @property + def 文(self) -> 分型: + return self.基础序列[0].文 + + @property + def 武(self) -> 分型: + return self.基础序列[-1].武 + + def 获取数据文本(self): + return f"{self.标识}, {self.序号}, {self.级别}, 文:({int(self.文.时间戳.timestamp())},{self.文.分型特征值:g}), 武:({int(self.武.时间戳.timestamp())},{self.武.分型特征值:g}), {self.第三买卖线}, {self.本级_第三买卖线}" + + def 完整性(self, 虚实: str = "合"): + """ + + 详情见 教你炒股票 43:有关背驰的补习课(2007-04-06 15:31:28) + 不完整时 下一个中枢大概率会与当前中枢发生扩展! + + """ + if self.基础序列[0].标识 == "笔": + # 笔中枢 + return self.第三买卖线 is not None + + else: + # if self.本级_第三买卖线: + # return True + 线段内部中枢 = self.基础序列[-1].合_中枢序列 if 虚实 == "合" else self.基础序列[-1].实_中枢序列 + for 内部中枢 in 线段内部中枢: + if 相对方向.分析(self.高, self.低, 内部中枢.高, 内部中枢.低).是否缺口(): + return True + return False + + def 获取序列(self) -> List[虚线]: + 序列: List = self.基础序列[:] + if self.第三买卖线 is not None: + 序列.append(self.第三买卖线) + return 序列 + + def 获取扩展中枢(self, 扩展中枢: List, 配置: 缠论配置): + if len(self.基础序列) >= 9: + 扩展线段 = [] + 线段.扩展分析(self.基础序列, 扩展线段, 配置) + 中枢.分析(扩展线段, 扩展中枢, False, f"{self.标识}_扩展中枢_") + + def 校验合法性(self, 序列: Sequence[虚线], 中枢序列) -> bool: + 有效序列 = self.基础序列[:] + 无效序列 = [] + for 元素 in self.基础序列: + if 元素 not in 序列: + 无效序列.append(元素) + + if 无效序列: + 无效 = 无效序列[0] + 序号 = self.基础序列.index(无效) + 有效序列 = self.基础序列[:序号] + + if len(有效序列) < 3: + self.第三买卖线 = None + self.本级_第三买卖线 = None + return False + + self.基础序列[:] = 有效序列 + + 有效序列 = [] + for 元素 in self.基础序列: + if 相对方向.分析(self.高, self.低, 元素.高, 元素.低).是否缺口(): + break + 有效序列.append(元素) + self.基础序列[:] = 有效序列 + + if len(self.基础序列) < 3: + return False + + for i in range(1, len(self.基础序列)): + 前 = self.基础序列[i - 1] + 后 = self.基础序列[i] + if not 前.之后是(后): + return False + + if not 相对方向.分析(self.基础序列[0].高, self.基础序列[0].低, self.基础序列[2].高, self.基础序列[2].低).是否缺口(): + 重叠高 = min(self.基础序列[:3], key=lambda o: o.高).高 + 重叠低 = max(self.基础序列[:3], key=lambda o: o.低).低 + if 重叠低 > 重叠高: + return False + + if self.第三买卖线 is not None: + if self.第三买卖线 in 序列: + if not self.基础序列[-1].之后是(self.第三买卖线): + self.设置第三买卖线(None) + else: + if not 相对方向.分析(self.高, self.低, self.第三买卖线.高, self.第三买卖线.低).是否缺口(): + self.添加虚线(self.第三买卖线) + self.设置第三买卖线(None) + + else: + self.设置第三买卖线(None) + return True + + def 设置第三买卖线(self, 线: Union[虚线, None]): + self.第三买卖线 = 线 + + def 当前状态(self): + """ + 详情见 教你炒股票 49:利润率最大的操作模式(2007-04-26 08:16:56) + 当前中枢最后一段所处的位置关系 + + 一、当下在该中枢之中。 + 因为在中枢里,由于这时候怎么演化都是对的,不操作是最好的操作,等待其演化第二、三类, + 当然,如果你技术好点,可以判断出次级别的第二类买点,这些买点很多情况下都是在中枢中出现的,那当然也是可以参与的。 + 但如果没有这种技术,那就有了再说了。只把握你自己当下技术水平能把握的机会,这才是最重要的。 + 二、当下在该中枢之下。 + 1.当下之前未出现该中枢第三类卖点。 + 2.当下之前已出现该中枢第三类卖点(正出现也包括在这种情况下,按最严格的定义,这最精确的卖点,是瞬间完成的,而具有操作意义的第三类卖点,其实是一个包含该最精确卖点的足够小区间) + 三、当下在该中枢之上。 + 1.当下之前未出现该中枢第三类买点。 + 2.当下之前已出现该中枢第三类买点。 + """ + 状态 = "中枢之中" + 尾部 = self.基础序列[-1].武 if self.基础序列[-1].标识 == "笔" else self.基础序列[-1].基础序列[-1].武 + 关系 = 相对方向.分析(self.高, self.低, 尾部.中.高, 尾部.中.低) + if 关系 is 相对方向.向上缺口: + 状态 = "中枢之上" + elif 关系 is 相对方向.向下缺口: + 状态 = "中枢之下" + return 状态 + + @classmethod + def 基础检查(cls, 左: 虚线, 中: 虚线, 右: 虚线) -> bool: + if not 左.之后是(中): + return False + if not 中.之后是(右): + return False + """ + if not 相对方向.分析(左.高, 左.低, 中.高, 中.低).是否包含(): + return False + if not 相对方向.分析(中.高, 中.低, 右.高, 右.低).是否包含(): + return False + """ + + return 相对方向.分析(左.高, 左.低, 右.高, 右.低) in (相对方向.向下, 相对方向.向上, 相对方向.顺, 相对方向.逆, 相对方向.同) + + @classmethod + def 创建(cls, 左: 虚线, 中: 虚线, 右: 虚线, 级别: int, 标识: str = "") -> "中枢": + assert 中枢.基础检查(左, 中, 右) + return 中枢( + 序号=0, + 标识=f"{标识}中枢<{中.标识}>", + 基础序列=[左, 中, 右], + 级别=级别, + ) + + @classmethod + def 从序列中获取中枢(cls, 虚线序列: Sequence[虚线], 起始方向: 相对方向, 标识: str) -> Optional["中枢"]: + if len(虚线序列) < 3: + return None + + for i in range(1, len(虚线序列) - 1): + 左, 中, 右 = 虚线序列[i - 1], 虚线序列[i], 虚线序列[i + 1] + if 中枢.基础检查(左, 中, 右): + if 左.方向 is 起始方向: + return 中枢.创建(左, 中, 右, 级别=0, 标识=标识) + + return None + + @classmethod + def 向中枢序列尾部添加(cls, 中枢序列: List["中枢"], 待添加中枢: "中枢"): + if 中枢序列: + 待添加中枢.序号 = 中枢序列[-1].序号 + 1 + if 中枢序列[-1].获取序列()[-1].序号 > 待添加中枢.获取序列()[-1].序号: + raise ValueError() + 中枢序列.append(待添加中枢) + + @classmethod + def 从中枢序列尾部弹出(cls, 中枢序列: List["中枢"], 待弹出中枢: "中枢") -> Optional["中枢"]: + if not 中枢序列: + return None + if 中枢序列[-1] is 待弹出中枢: + return 中枢序列.pop() + return None + + @classmethod + def 分析(cls, 虚线序列: Sequence[虚线], 中枢序列: List["中枢"], 跳过首部: bool = True, 标识: str = "", 层级: int = 0) -> None: + if len(虚线序列) < 3: + return None + + 中枢递归分析 = 中枢.分析 + + if not 中枢序列: + for i in range(1, len(虚线序列) - 1): + 左, 中, 右 = 虚线序列[i - 1], 虚线序列[i], 虚线序列[i + 1] + if 中枢.基础检查(左, 中, 右): + 新中枢 = 中枢.创建(左, 中, 右, 中.级别, 标识) + 序号 = 虚线序列.index(左) + if 跳过首部 and (左.序号 == 0 or 序号 == 0): + continue # 方便计算走势 + if 序号 >= 2: + 同向相对关系 = 相对方向.分析(虚线序列[序号 - 2].高, 虚线序列[序号 - 2].低, 左.高, 左.低) + if 同向相对关系.是否向上() and 左.方向.是否向上(): + continue + if 同向相对关系.是否向下() and 左.方向.是否向下(): + continue + + 中枢.向中枢序列尾部添加(中枢序列, 新中枢) + return 中枢递归分析(虚线序列, 中枢序列, 跳过首部, 标识, 层级 + 1) + + return None + + 当前中枢 = 中枢序列[-1] + + if not 当前中枢.校验合法性(虚线序列, 中枢序列): + 中枢.从中枢序列尾部弹出(中枢序列, 当前中枢) + return 中枢递归分析(虚线序列, 中枢序列, 跳过首部, 标识, 层级 + 1) + + 序号 = 虚线序列.index(当前中枢.基础序列[-1]) + 1 + + 基础序列 = [] + for 当前虚线 in 虚线序列[序号:]: + if 相对方向.分析(当前中枢.高, 当前中枢.低, 当前虚线.高, 当前虚线.低).是否缺口(): + 基础序列.append(当前虚线) + if 当前中枢.基础序列[-1].之后是(当前虚线): + 当前中枢.设置第三买卖线(当前虚线) + else: + ... + else: + if not 基础序列: + assert 当前中枢.基础序列[-1].之后是(当前虚线), (当前中枢.基础序列[-1], 当前虚线) + 当前中枢.添加虚线(当前虚线) + else: + 基础序列.append(当前虚线) + + while len(基础序列) >= 3: + 新中枢 = 中枢.从序列中获取中枢(基础序列, 当前中枢.基础序列[-1].方向.翻转(), 标识) + if 新中枢 is None: + 基础序列.pop(0) + else: + 中枢.向中枢序列尾部添加(中枢序列, 新中枢) + 当前中枢 = 新中枢 + 基础序列 = [] + return None + + +class 观察者: + def __init__(self, 符号: str, 周期: int, 配置: 缠论配置): + 配置.标识 = 符号 + self.符号: str = 符号 + self.周期: int = int(周期) + self.配置: 缠论配置 = 配置 + self.__终止时间戳: Optional[datetime] = 转化为时间戳(self.配置.手动终止) if self.配置.手动终止 else None + + self.重置基础序列() + + @property + def 观察员(self): + return self # 用于兼容 chanlun.c99 + + @property + def 标识(self) -> str: + return f"{self.符号}:{self.周期}" + + @property + def 当前K线(self) -> Optional["K线"]: + return self.普通K线序列[-1] if self.普通K线序列 else None + + @property + def 当前缠K(self) -> Optional["缠论K线"]: + return self.缠论K线序列[-1] if self.缠论K线序列 else None + + def 重置基础序列(self): + self.基础缠K序列: List[缠论K线] = [] + + self.普通K线序列: List[K线] = [] + self.缠论K线序列: List[缠论K线] = [] + + self.分型序列: List[分型] = [] + + self.笔序列: List[虚线] = [] + self.笔_中枢序列: List[中枢] = [] + + self.线段序列: List[虚线] = [] + self.中枢序列: List[中枢] = [] + + self.扩展线段序列: List[虚线] = [] + self.扩展中枢序列: List[中枢] = [] + + self.扩展线段序列_线段: List[虚线] = [] + self.扩展中枢序列_线段: List[中枢] = [] + + self.线段_线段序列: List[虚线] = [] + self.线段_中枢序列: List[中枢] = [] + + self.扩展线段序列_扩展线段: List[虚线] = [] + self.扩展中枢序列_扩展线段: List[中枢] = [] + + @final + def 增加原始K线(self, 普K: K线): + if self.__终止时间戳 and 普K.时间戳 > self.__终止时间戳: + return + self.__处理数据(普K) + + def __处理数据(self, 普K: K线): + 状态, 当前分型 = 缠论K线.分析(普K, self.缠论K线序列, self.普通K线序列, self.配置) + if 当前分型 is None: + return + + self.配置.分析笔 and 笔.分析(当前分型, self.分型序列, self.笔序列, self.缠论K线序列, self.普通K线序列, 0, self.配置) + if not self.分型序列: + return + + self.配置.分析笔中枢 and 中枢.分析(self.笔序列, self.笔_中枢序列) + if not self.笔序列: + return + + self.配置.分析线段 and 线段.分析(self.笔序列, self.线段序列, self.配置) + self.配置.分析线段中枢 and 中枢.分析(self.线段序列, self.中枢序列) + + self.配置.分析扩展线段 and 线段.扩展分析(self.笔序列, self.扩展线段序列, self.配置) + self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列, self.扩展中枢序列) + + self.配置.分析扩展线段 and 线段.扩展分析(self.线段序列, self.扩展线段序列_线段, self.配置) + self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列_线段, self.扩展中枢序列_线段) + + self.配置.分析线段 and 线段.分析(self.线段序列, self.线段_线段序列, self.配置) + self.配置.分析线段中枢 and 中枢.分析(self.线段_线段序列, self.线段_中枢序列) + + self.配置.分析扩展线段 and 线段.扩展分析(self.扩展线段序列, self.扩展线段序列_扩展线段, self.配置) + self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列_扩展线段, self.扩展中枢序列_扩展线段) + + def 测试_保存数据(self, root: str = None): + """拆分各序列数据,单独存文件,文件名为对应变量名""" + # 提取各类文本数据 + 笔序列_文本数据 = [筆.获取数据文本() for 筆 in self.笔序列] + 线段序列_文本数据 = [实线.获取数据文本() for 实线 in self.线段序列] + 扩展线段序列_数据文本 = [实线.获取数据文本() for 实线 in self.扩展线段序列] + 扩展线段序列_线段_数据文本 = [实线.获取数据文本() for 实线 in self.扩展线段序列_线段] + 线段_线段序列_数据文本 = [实线.获取数据文本() for 实线 in self.线段_线段序列] + 扩展线段序列_扩展线段_数据文本 = [实线.获取数据文本() for 实线 in self.扩展线段序列_扩展线段] + + 笔_中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.笔_中枢序列] + 中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.中枢序列] + 扩展中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.扩展中枢序列] + 扩展中枢序列_线段_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.扩展中枢序列_线段] + 线段_中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.线段_中枢序列] + 扩展中枢序列_扩展线段_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.扩展中枢序列_扩展线段] + + # ===================== 优化点:优先使用传入的 root 目录 ===================== + if root is not None: + # 使用用户指定的根目录 + 根目录 = Path(root) + else: + # 默认:当前脚本所在目录 + 根目录 = Path(__file__).parent + + # 生成子目录名称(不变) + 起始时间 = int(self.普通K线序列[0].时间戳.timestamp()) + 结束时间 = int(self.普通K线序列[-1].时间戳.timestamp()) + 目录标识 = f"Py_{self.标识}_{起始时间}_{结束时间}" + + # 最终保存路径 = 根目录 / 自动生成的子文件夹 + 保存路径 = 根目录 / 目录标识 + 保存路径.mkdir(exist_ok=True, parents=True) # parents=True 支持多级目录自动创建 + + # 映射:变量名 -> 数据列表 + 数据映射 = [ + ("笔序列_文本数据", 笔序列_文本数据), + ("线段序列_文本数据", 线段序列_文本数据), + ("扩展线段序列_数据文本", 扩展线段序列_数据文本), + ("扩展线段序列_线段_数据文本", 扩展线段序列_线段_数据文本), + ("线段_线段序列_数据文本", 线段_线段序列_数据文本), + ("扩展线段序列_扩展线段_数据文本", 扩展线段序列_扩展线段_数据文本), + ("笔_中枢序列_数据文本", 笔_中枢序列_数据文本), + ("中枢序列_数据文本", 中枢序列_数据文本), + ("扩展中枢序列_数据文本", 扩展中枢序列_数据文本), + ("扩展中枢序列_线段_数据文本", 扩展中枢序列_线段_数据文本), + ("线段_中枢序列_数据文本", 线段_中枢序列_数据文本), + ("扩展中枢序列_扩展线段_数据文本", 扩展中枢序列_扩展线段_数据文本), + ] + + # 逐个写入独立文件 + for 文件名, 数据列表 in 数据映射: + 文件全路径 = 保存路径 / f"{文件名}.txt" + with open(文件全路径, "w", encoding="utf-8") as f: + f.write("\n".join(数据列表)) + f.write("\n") # 向 C99 对齐 + print(f"全部数据拆分保存完成,目录:{保存路径.resolve()}") + + def 识别买卖点(self): + pass + + def 静态重新分析(self): + self.分型序列: List[分型] = [] + + self.笔序列: List[虚线] = [] + self.笔_中枢序列: List[中枢] = [] + + self.线段序列: List[虚线] = [] + self.中枢序列: List[中枢] = [] + + self.扩展线段序列: List[虚线] = [] + self.扩展中枢序列: List[中枢] = [] + + self.扩展线段序列_线段: List[虚线] = [] + self.扩展中枢序列_线段: List[中枢] = [] + + self.线段_线段序列: List[虚线] = [] + self.线段_中枢序列: List[中枢] = [] + + self.扩展线段序列_扩展线段: List[虚线] = [] + self.扩展中枢序列_扩展线段: List[中枢] = [] + + for i in range(1, len(self.缠论K线序列) - 1): + 当前分型 = 分型(self.缠论K线序列[i - 1], self.缠论K线序列[i], self.缠论K线序列[i + 1]) + 笔.分析(当前分型, self.分型序列, self.笔序列, self.缠论K线序列, self.普通K线序列, 0, self.配置) + + self.配置.分析笔中枢 and 中枢.分析(self.笔序列, self.笔_中枢序列) + + self.配置.分析线段 and 线段.分析(self.笔序列, self.线段序列, self.配置) + self.配置.分析线段中枢 and 中枢.分析(self.线段序列, self.中枢序列) + + self.配置.分析扩展线段 and 线段.扩展分析(self.笔序列, self.扩展线段序列, self.配置) + self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列, self.扩展中枢序列) + + self.配置.分析扩展线段 and 线段.扩展分析(self.线段序列, self.扩展线段序列_线段, self.配置) + self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列_线段, self.扩展中枢序列_线段) + + self.配置.分析线段 and 线段.分析(self.线段序列, self.线段_线段序列, self.配置) + self.配置.分析线段中枢 and 中枢.分析(self.线段_线段序列, self.线段_中枢序列) + + @classmethod + def 读取数据文件(cls, 文件路径: str, 配置=缠论配置()) -> Self: + # btcusd-300-1631772074-1632222374.nb + name = Path(文件路径).name.split(".")[0] + 符号, 周期, 起始时间戳, 结束时间戳 = name.split("-") + 实例 = cls(符号=符号, 周期=int(周期), 配置=配置) + + with open(文件路径, "rb") as f: + buffer = f.read() + size = struct.calcsize(">6d") + for i in range(len(buffer) // size): + k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], int(周期)) + 实例.增加原始K线(k线) + + return 实例 + + +class K线合成器: + def __init__(self, 标识: str, 周期组: List[int], 事件回调: Optional[Callable] = None): + self.标识 = 标识 + self.周期组 = sorted(周期组) # 按周期从小到大排序 + self.当前K线: Dict[int, Optional[K线]] = {周期: None for 周期 in 周期组} + self.合成K线列表: Dict[int, List[K线]] = {周期: [] for 周期 in 周期组} + self.事件回调 = 事件回调 # 新增:事件回调函数 + + def 设置事件回调(self, 回调函数: Callable): + """设置事件回调函数""" + self.事件回调 = 回调函数 + + def 投喂(self, 时间戳: datetime, 开: float, 高: float, 低: float, 收: float, 量: float): + """投喂原始tick数据""" + 普K = K线.创建普K( + 标识=self.标识, + 序号=0, # 原始数据序号不重要 + 时间戳=时间戳, + 开盘价=开, + 最高价=高, + 最低价=低, + 收盘价=收, + 成交量=量, + 周期=0, # 原始数据是0秒周期 + ) + self.投喂K线(普K) + + def 投喂K线(self, 普K: K线): + """投喂K线对象""" + for 周期 in self.周期组: + self._处理单个周期(周期, 普K) + + def _处理单个周期(self, 周期: int, 普K: K线): + """处理单个周期的K线合成""" + 目标时间戳 = self._对齐时间戳(普K.时间戳, 周期) + 当前K线 = self.当前K线[周期] + if 当前K线 is None: + # 创建新的K线 + self.当前K线[周期] = self._创建新K线(周期, 目标时间戳, 普K) + elif 当前K线.时间戳 == 目标时间戳: + # 更新当前K线 + self._更新K线(当前K线, 普K) + else: + # 完成当前K线,创建新K线 + self._完成K线(周期) + self.当前K线[周期] = self._创建新K线(周期, 目标时间戳, 普K) + + def _对齐时间戳(self, 时间戳: datetime, 周期: int) -> datetime: + """将时间戳对齐到周期边界""" + total_seconds = int(时间戳.timestamp()) + aligned_seconds = (total_seconds // 周期) * 周期 + return datetime.fromtimestamp(aligned_seconds) + + def _创建新K线(self, 周期: int, 时间戳: datetime, 普K: K线) -> K线: + """创建新的合成K线""" + return K线.创建普K( + 标识=self.标识, + 序号=0 if not self.合成K线列表[周期] else self.合成K线列表[周期][-1].序号 + 1, + 时间戳=时间戳, + 开盘价=普K.开盘价, + 最高价=普K.高, + 最低价=普K.低, + 收盘价=普K.收盘价, + 成交量=普K.成交量, + 周期=周期, + ) + + def _更新K线(self, 当前K线: K线, 新数据: K线): + """更新当前K线数据""" + 当前K线.高 = max(当前K线.高, 新数据.高) + 当前K线.低 = min(当前K线.低, 新数据.低) + 当前K线.收盘价 = 新数据.收盘价 + 当前K线.成交量 += 新数据.成交量 + # 当前K线.原始结束序号 = 新数据.序号 + + def _完成K线(self, 周期: int): + """完成当前K线并添加到列表""" + 当前K线 = self.当前K线[周期] + if 当前K线 is None: + return + + # 这里可以添加最终的处理逻辑,比如计算MACD等 + if self.合成K线列表[周期]: + 当前K线.序号 = self.合成K线列表[周期][-1].序号 + 1 + + self.合成K线列表[周期].append(当前K线) + + self.当前K线[周期] = None + # 新增:产生完成K线信号 + self._产生完成K线信号(周期, 当前K线) + + def _产生完成K线信号(self, 周期: int, 完成K线: K线): + """产生K线完成信号""" + if self.事件回调: + try: + self.事件回调(信号类型="K线完成", 标识=self.标识, 周期=周期, 完成K线=完成K线) + except Exception as e: + print(f"K线合成器信号回调错误: {e}") + + def 获取当前K线(self, 周期: int) -> Optional[K线]: + """获取指定周期当前正在合成的K线""" + return self.当前K线[周期] + + +class 立体分析器: + def __init__(self, 符号: str, 周期组: List[int], 配置: 缠论配置 = 缠论配置(), 配置组: Dict[int, 缠论配置] = dict()): + self.周期组 = 周期组 + + self.__输入周期 = self.周期组[0] # 最小输入K线周期 + self.__显示周期 = self.周期组[1] + self._K线合成器 = K线合成器(符号, self.周期组, self.__K线回调) + + self._单体分析器 = dict() + for 周期 in self.周期组: + 临时配置 = 配置组.get(周期, 配置) + 当前配置 = 临时配置.model_copy( + update={ + "推送K线": False, + # "推送笔": False, + "推送线段": False, + # "图表展示": False, + }, + deep=True, + ) + self._单体分析器[周期] = 观察者(符号=符号, 周期=周期, 配置=当前配置) + + self._单体分析器[self.__显示周期].配置.推送K线 = True + self._单体分析器[self.__显示周期].配置.推送笔 = True + self._单体分析器[self.__显示周期].配置.推送线段 = True + self._单体分析器[self.__显示周期].配置.图表展示 = True + self._单体分析器[self.__显示周期].重置基础序列() + + for 周期 in self.周期组: # 将不同周期对其至显示周期 + if 周期 != self.__显示周期: + self._单体分析器[周期].基础缠K序列 = self._单体分析器[self.__显示周期].缠论K线序列 + + def 投喂K线(self, 普K: K线): + if 普K.周期 != self.__输入周期: + raise RuntimeError("立体分析器.投喂K线", 普K.周期, self.__输入周期) + self._K线合成器.投喂K线(普K) + + def __K线回调(self, 信号类型: str, 标识: str, 周期: int, 完成K线: K线): + self._单体分析器[周期].增加原始K线(完成K线) + if 当前K线 := self._K线合成器.获取当前K线(周期): + self._单体分析器[周期].增加原始K线(当前K线) + + def 测试_保存数据(self): + """拆分各序列数据,单独存文件,文件名为对应变量名""" + + # 生成存储根目录 + 脚本目录 = Path(__file__).parent # 取当前脚本所在文件夹 + 起始时间 = int(self._单体分析器[self.__输入周期].普通K线序列[0].时间戳.timestamp()) + 结束时间 = int(self._单体分析器[self.__输入周期].普通K线序列[-1].时间戳.timestamp()) + 目录标识 = f"PyM_{self._单体分析器[self.__输入周期].标识}_{起始时间}_{结束时间}" + + # 最终保存路径 = 脚本目录 / 自动生成的文件夹 + 保存路径 = 脚本目录 / 目录标识 + 保存路径.mkdir(exist_ok=True) + + for 周期 in self.周期组: + self._单体分析器[周期].测试_保存数据(保存路径) + + print(f"多级别数据拆分保存完成,目录:{保存路径.resolve()}") + + +def 测试_读取数据(配置: 缠论配置): + def 魔法(): + 启动时间 = datetime.now() + 观察员 = 观察者.读取数据文件(配置.加载文件路径, 配置) + 消耗用时 = datetime.now() - 启动时间 + print("测试_读取数据 耗时", 消耗用时, "普K数量", len(观察员.普通K线序列)) + return 观察员 + + return 魔法 + + +def 测试_周期合成(配置: 缠论配置, 配置组: Dict[int, 缠论配置] = dict()): + 文件路径 = 配置.加载文件路径 + name = Path(文件路径).name.split(".")[0] + 符号, 周期, 起始时间戳, 结束时间戳 = name.split("-") + 周期 = int(周期) + 周期组 = [周期, 周期 * 5, 周期 * 5 * 6] + + def 魔法(): + 启动时间 = datetime.now() + 多级别分析 = 立体分析器(符号, 周期组, 配置, 配置组) + with open(文件路径, "rb") as f: + buffer = f.read() + size = struct.calcsize(">6d") + for i in range(len(buffer) // size): + k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], 周期) + 多级别分析.投喂K线(k线) + 消耗用时 = datetime.now() - 启动时间 + print("测试_周期合成", 消耗用时, "普K数量", len(多级别分析._单体分析器[周期].普通K线序列)) + return 多级别分析 + + return 魔法 + + +if __name__ == "__main__": + 当前配置 = 缠论配置.不推送() + 当前配置.加载文件路径 = "./btcusd-300-1761327300-1776327900.nb" + 测试_读取数据(当前配置)().测试_保存数据() + 测试_周期合成(当前配置)().测试_保存数据() diff --git a/chanlun/Cargo.toml b/chanlun/Cargo.toml new file mode 100644 index 0000000..abb7daa --- /dev/null +++ b/chanlun/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "chanlun" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +byteorder = "1" +chrono = { version = "0.4", features = ["serde"] } diff --git a/chanlun/src/algorithm/bi.rs b/chanlun/src/algorithm/bi.rs new file mode 100644 index 0000000..aa11e75 --- /dev/null +++ b/chanlun/src/algorithm/bi.rs @@ -0,0 +1,821 @@ +use crate::business::observer::观察者; +use crate::config::缠论配置; +use crate::kline::bar::K线; +use crate::kline::chan_kline::缠论K线; +use crate::structure::dash_line::虚线; +use crate::structure::fractal_obj::分型; +use crate::types::{分型结构, 相对方向}; +use std::rc::Rc; + +/// 笔 — 从分型生成笔的算法集合(静态方法命名空间) +pub struct 笔; + +impl 笔 { + /// 获取可成笔的缠K数量(考虑弱化模式) + pub fn 获取缠K数量(缠K序列: &[Rc<缠论K线>], 笔序列: &[Rc<虚线>], 配置: &缠论配置) -> usize { + let 实际数量 = 缠K序列.len(); + if 实际数量 >= 配置.笔内元素数量 as usize { + return 实际数量; + } + + if 配置.笔弱化 && 实际数量 >= 3 { + let 实际高点 = Self::实际高点(缠K序列, 配置.笔内相同终点取舍); + let 实际低点 = Self::实际低点(缠K序列, 配置.笔内相同终点取舍); + + if let (Some(ref 高点), Some(ref 低点)) = (&实际高点, &实际低点) { + let 原始数量 = 1 + (低点.标的K线.序号 - 高点.标的K线.序号).unsigned_abs() as usize; + if 原始数量 >= 配置.笔内元素数量 as usize { + return 配置.笔内元素数量 as usize; + } + } + + if !笔序列.is_empty() { + // Try both high and low points (Python: 根据缠K找笔(笔序列, 实际高点) or 根据缠K找笔(笔序列, 实际低点)) + let 筆 = 实际高点.as_ref() + .and_then(|h| Self::根据缠K找笔(笔序列, h, 1)) + .or_else(|| 实际低点.as_ref().and_then(|l| Self::根据缠K找笔(笔序列, l, 1))); + + if let Some(ref 筆) = 筆 { + if let (Some(ref 高_k), Some(ref 低_k)) = (&实际高点, &实际低点) { + let 原始数量 = 1 + (低_k.标的K线.序号 - 高_k.标的K线.序号).unsigned_abs() as usize; + // 向上笔 + if 筆.方向().是否向上() && 低_k.低 < 筆.低() { + if 原始数量 >= 配置.笔弱化_原始数量 as usize { + return 配置.笔内元素数量 as usize; + } + } + // 向下笔 + if 筆.方向().是否向下() && 低_k.低 > 筆.高() { + if 原始数量 >= 配置.笔弱化_原始数量 as usize { + return 配置.笔内元素数量 as usize; + } + } + } + } + } + } + 实际数量 + } + + /// 次高 — 排除最高值后的次高点 + pub fn 次高(缠K序列: &[Rc<缠论K线>], 取舍: bool) -> Option> { + if 缠K序列.len() < 2 { + return 缠K序列.first().cloned(); + } + let max_高 = 缠K序列.iter().map(|k| k.高).fold(f64::NEG_INFINITY, f64::max); + // 排除最高值 + let filtered: Vec<&Rc<缠论K线>> = 缠K序列.iter().filter(|k| k.高 != max_高).collect(); + if filtered.is_empty() { + return 缠K序列.first().cloned(); + } + // 筛选次高值 + let second_高 = filtered.iter().map(|k| k.高).fold(f64::NEG_INFINITY, f64::max); + let mut candidates: Vec<&Rc<缠论K线>> = filtered.iter().filter(|k| k.高 == second_高).copied().collect(); + // 按时间戳排序 + candidates.sort_by(|a, b| a.时间戳.cmp(&b.时间戳)); + if 取舍 { + Some(Rc::clone(candidates[candidates.len() - 1])) + } else { + Some(Rc::clone(candidates[0])) + } + } + + /// 次低 — 排除最低值后的次低点 + pub fn 次低(缠K序列: &[Rc<缠论K线>], 取舍: bool) -> Option> { + if 缠K序列.len() < 2 { + return 缠K序列.first().cloned(); + } + let min_低 = 缠K序列.iter().map(|k| k.低).fold(f64::INFINITY, f64::min); + // 排除最低值 + let filtered: Vec<&Rc<缠论K线>> = 缠K序列.iter().filter(|k| k.低 != min_低).collect(); + if filtered.is_empty() { + return 缠K序列.first().cloned(); + } + // 筛选次低值 + let second_低 = filtered.iter().map(|k| k.低).fold(f64::INFINITY, f64::min); + let mut candidates: Vec<&Rc<缠论K线>> = filtered.iter().filter(|k| k.低 == second_低).copied().collect(); + // 按时间戳排序 + candidates.sort_by(|a, b| a.时间戳.cmp(&b.时间戳)); + if 取舍 { + Some(Rc::clone(candidates[candidates.len() - 1])) + } else { + Some(Rc::clone(candidates[0])) + } + } + + /// 实际高点 + pub fn 实际高点(缠K序列: &[Rc<缠论K线>], 取舍: bool) -> Option> { + if 缠K序列.is_empty() { + return None; + } + let max_高 = 缠K序列.iter().map(|k| k.高).fold(f64::NEG_INFINITY, f64::max); + let mut candidates: Vec<&Rc<缠论K线>> = 缠K序列.iter().filter(|k| k.高 == max_高).collect(); + if candidates.is_empty() { + return Some(Rc::clone(&缠K序列[0])); + } + // 按时间戳排序 + candidates.sort_by(|a, b| a.时间戳.cmp(&b.时间戳)); + if 取舍 { + Some(Rc::clone(candidates[candidates.len() - 1])) + } else { + Some(Rc::clone(candidates[0])) + } + } + + /// 实际低点 + pub fn 实际低点(缠K序列: &[Rc<缠论K线>], 取舍: bool) -> Option> { + if 缠K序列.is_empty() { + return None; + } + let min_低 = 缠K序列.iter().map(|k| k.低).fold(f64::INFINITY, f64::min); + let mut candidates: Vec<&Rc<缠论K线>> = 缠K序列.iter().filter(|k| k.低 == min_低).collect(); + if candidates.is_empty() { + return Some(Rc::clone(&缠K序列[0])); + } + // 按时间戳排序 + candidates.sort_by(|a, b| a.时间戳.cmp(&b.时间戳)); + if 取舍 { + Some(Rc::clone(candidates[candidates.len() - 1])) + } else { + Some(Rc::clone(candidates[0])) + } + } + + /// 判断笔的相对关系是否合理 + pub fn 相对关系(筆: &虚线, _配置: &缠论配置) -> bool { + let 文 = &筆.文; + let 武 = &筆.武; + + // 向上笔:文(底)低 → 武(顶)高 + if 筆.方向().是否向上() { + return 武.分型特征值 > 文.分型特征值; + } + // 向下笔:文(顶)高 → 武(底)低 + 武.分型特征值 < 文.分型特征值 + } + + /// 以文会友 — 根据起点分型找笔 + pub fn 以文会友(笔序列: &[Rc<虚线>], 文: &Rc<分型>) -> Option> { + 笔序列.iter().find(|b| Rc::as_ptr(&b.文) == Rc::as_ptr(文)).cloned() + } + + /// 以武会友 — 根据终点分型找笔 + pub fn 以武会友(笔序列: &[Rc<虚线>], 武: &Rc<分型>) -> Option> { + 笔序列.iter().find(|b| Rc::as_ptr(&b.武) == Rc::as_ptr(武)).cloned() + } + + /// 根据缠K找对应的笔 + pub fn 根据缠K找笔(笔序列: &[Rc<虚线>], 缠K: &Rc<缠论K线>, 偏移: i64) -> Option> { + // Python iterates in reverse: for 筆 in 笔序列[::-1] + for b in 笔序列.iter().rev() { + // Python: 筆.文.中.序号 - 偏移 <= 缠K.序号 <= 筆.武.中.序号 + if b.文.中.序号 - 偏移 <= 缠K.序号 && 缠K.序号 <= b.武.中.序号 { + return Some(Rc::clone(b)); + } + } + None + } + + /// 从分型序列中弹出最后一个分型和对应的笔 + fn 弹出旧笔(分型序列: &mut Vec>, 笔序列: &mut Vec>) { + 分型序列.pop(); + if !笔序列.is_empty() { + // Python sets旧笔.有效性 = False; with Rc we just drop the笔 + 笔序列.pop(); + } + } + + /// 核心笔分析 — 使用显式栈模拟递归 + /// + /// 返回: 递归层次数 + pub fn 分析( + 初始分型: Rc<分型>, + 分型序列: &mut Vec>, + 笔序列: &mut Vec>, + 缠K序列: &[Rc<缠论K线>], + _普K序列: &[Rc], + 配置: &缠论配置, + ) -> i64 { + enum 栈项 { + 分型(Rc<分型>, i64), + /// 修复错过笔哨兵: 若临时分型被接受为最后一个元素,则扫描武将之后的所有分型 + 修复错过笔 { + 临时分型: Rc<分型>, + 武将缠K: Rc<缠论K线>, + 层次: i64, + }, + } + + let mut 栈: Vec<栈项> = Vec::new(); + 栈.push(栈项::分型(初始分型, 0)); + + while let Some(项) = 栈.pop() { + let (当前分型, 递归层次) = match 项 { + 栈项::分型(fx, lvl) => (fx, lvl), + 栈项::修复错过笔 { + 临时分型, + 武将缠K, + 层次, + } => { + // Python line 2406: only scan if临时分型 was accepted as last element + if !分型序列.is_empty() { + if let Some(last_fx) = 分型序列.last() { + if Rc::as_ptr(last_fx) == Rc::as_ptr(&临时分型) { + if let Some(武_idx) = 缠K序列 + .iter() + .position(|k| Rc::as_ptr(k) == Rc::as_ptr(&武将缠K)) + { + let mut 错过: Vec> = Vec::new(); + for ck in &缠K序列[武_idx..] { + if ck.分型 == Some(分型结构::底) + || ck.分型 == Some(分型结构::顶) + { + if let Some(fx) = + 分型::从缠K序列中获取分型(缠K序列, ck) + { + 错过.push(Rc::new(fx)); + } + } + } + // Push in reverse so first in slice is processed first + for m in 错过.into_iter().rev() { + 栈.push(栈项::分型(m, 层次 + 1)); + } + } + } + } + } + // 当前分型 is already next on the stack (pushed before this哨兵) + continue; + } + }; + + if 递归层次 > 256 { + continue; + } + + if !matches!(当前分型.结构, 分型结构::顶 | 分型结构::底) { + continue; + } + + // Python line 2322-2325: 第一个分型直接追加 + if 分型序列.is_empty() { + 分型序列.push(当前分型); + continue; + } + + let 之前分型 = Rc::clone(分型序列.last().unwrap()); + + // Python line 2330-2335: 清理无效数据 + if 之前分型.时间戳 == 当前分型.时间戳 + || matches!(之前分型.结构, 分型结构::上 | 分型结构::下) + { + Self::弹出旧笔(分型序列, 笔序列); + if 分型序列.is_empty() { + if 当前分型.右.is_some() { + 分型序列.push(当前分型); + } + continue; + } + } + + let 之前分型 = Rc::clone(分型序列.last().unwrap()); + + // Python line 2338: 时序检查 — skip out-of-order fractals + if 之前分型.时间戳 > 当前分型.时间戳 && 之前分型.中.序号 - 当前分型.中.序号 > 1 { + continue; + } + + // Python line 2343-2348: 笔弱化模式 + if 配置.笔弱化 && !笔序列.is_empty() { + let 前一笔 = 笔序列.last().unwrap(); + let 前一笔缠K数 = 前一笔.武.中.序号 - 前一笔.文.中.序号 + 1; + if 前一笔缠K数 == 3 { + let 破位 = (前一笔.方向().是否向上() + && 前一笔.低() > 当前分型.分型特征值 + && 当前分型.结构 == 分型结构::底) + || (前一笔.方向().是否向下() + && 前一笔.高() < 当前分型.分型特征值 + && 当前分型.结构 == 分型结构::顶); + if 破位 { + Self::弹出旧笔(分型序列, 笔序列); + 栈.push(栈项::分型(当前分型, 递归层次 + 1)); + continue; + } + } + } + + // Re-read之前分型 again after笔弱化 pop + let 之前分型 = Rc::clone(分型序列.last().unwrap()); + + // Python line 2350: 分型结构相反 → 可能成笔 + if 之前分型.结构 != 当前分型.结构 { + let 文_idx = 缠K序列.iter().position(|k| Rc::as_ptr(k) == Rc::as_ptr(&之前分型.中)); + let 武_idx = 缠K序列.iter().position(|k| Rc::as_ptr(k) == Rc::as_ptr(&当前分型.中)); + + if let (Some(文_idx), Some(武_idx)) = (文_idx, 武_idx) { + let 基础序列 = &缠K序列[文_idx..=武_idx]; + let 缠K数量 = Self::获取缠K数量(基础序列, 笔序列, 配置); + + if 缠K数量 >= 配置.笔内元素数量 as usize { + // Python line 2354-2357: 文官 always uses false (not 笔内相同终点取舍) + let 文官 = match 之前分型.结构 { + 分型结构::顶 => Self::实际高点(基础序列, false), + _ => Self::实际低点(基础序列, false), + }; + + // Python line 2359-2367: 文官 != 之前分型.中 → adjust + if let Some(ref 文官_k) = 文官 { + if Rc::as_ptr(文官_k) != Rc::as_ptr(&之前分型.中) { + if let Some(临时分型) = + 分型::从缠K序列中获取分型(缠K序列, 文官_k) + { + 栈.push(栈项::分型(当前分型, 递归层次)); + 栈.push(栈项::分型(Rc::new(临时分型), 递归层次 + 1)); + continue; + } + } + } + + // Python line 2369-2372: 武将 + let 武将 = match 当前分型.结构 { + 分型结构::底 => Self::实际低点(基础序列, 配置.笔内相同终点取舍), + _ => Self::实际高点(基础序列, 配置.笔内相同终点取舍), + }; + + let 新笔 = Rc::new(虚线::创建笔( + Rc::clone(&之前分型), + Rc::clone(&当前分型), + true, + )); + + // Python line 2374-2376: 相对关系 and武将 matches + if Self::相对关系(&新笔, 配置) { + if let Some(ref 武将_k) = 武将 { + if Rc::as_ptr(武将_k) == Rc::as_ptr(&当前分型.中) { + Self::_添加新笔递归(分型序列, 笔序列, 当前分型, 新笔); + continue; + } + } + } + + // Python line 2378-2385: 笔次级成笔 + if 配置.笔次级成笔 { + let 武将 = match 当前分型.结构 { + 分型结构::底 => Self::次低(基础序列, 配置.笔内相同终点取舍), + _ => Self::次高(基础序列, 配置.笔内相同终点取舍), + }; + if let Some(ref 武将_k) = 武将 { + if Rc::as_ptr(武将_k) == Rc::as_ptr(&当前分型.中) + && Self::相对关系(&新笔, 配置) + { + Self::_添加新笔递归(分型序列, 笔序列, 当前分型, 新笔); + continue; + } + } + } + } else { + // Python line 2388-2390: 元素不足 → 右元素扩展 + if let Some(ref 右) = 当前分型.右 { + if let Some(临时分型) = 分型::从缠K序列中获取分型(缠K序列, 右) { + 栈.push(栈项::分型(Rc::new(临时分型), 递归层次 + 1)); + continue; + } + } + } + } + } else { + // Python line 2392-2419: 分型结构相同 → 更强则替换 + 修复错过笔 + let 分型特征值 = 当前分型.分型特征值; + + let 更强 = match 之前分型.结构 { + 分型结构::顶 => 之前分型.分型特征值 < 分型特征值, + 分型结构::底 => 之前分型.分型特征值 > 分型特征值, + _ => false, + }; + + if 更强 { + let 被替换分型 = Rc::clone(&之前分型); + Self::弹出旧笔(分型序列, 笔序列); + + if let Some(k线序列) = + 缠论K线::截取(缠K序列, &被替换分型.中, &当前分型.中) + { + let 武将 = match 被替换分型.结构 { + 分型结构::顶 => { + Self::实际低点(&k线序列, 配置.笔内相同终点取舍) + } + _ => Self::实际高点(&k线序列, 配置.笔内相同终点取舍), + }; + + if let Some(ref 武将_k) = 武将 { + if let Some(临时分型) = + 分型::从缠K序列中获取分型(缠K序列, 武将_k) + { + let 临时分型_rc = Rc::new(临时分型); + + if !分型序列.is_empty() { + // Push in reverse processing order (LIFO): + 栈.push(栈项::分型(Rc::clone(&当前分型), 递归层次 + 2)); + 栈.push(栈项::修复错过笔 { + 临时分型: Rc::clone(&临时分型_rc), + 武将缠K: Rc::clone(武将_k), + 层次: 递归层次 + 1, + }); + 栈.push(栈项::分型(临时分型_rc, 递归层次 + 1)); + continue; + } else { + 分型序列.push(当前分型); + continue; + } + } + } + } + + if 分型序列.is_empty() { + 分型序列.push(当前分型); + } else { + 栈.push(栈项::分型(当前分型, 递归层次 + 1)); + } + } + } + } + + 栈.len() as i64 + } + + /// 核心笔分析 — 递归实现,逐句对照 chan.py 笔.分析 / 笔递归分析 + /// + /// 返回: 递归层次数 + pub fn 分析递归( + 当前分型: Rc<分型>, + 分型序列: &mut Vec>, + 笔序列: &mut Vec>, + 缠K序列: &[Rc<缠论K线>], + _普K序列: &[Rc], + 递归层次: i64, + 配置: &缠论配置, + ) -> i64 { + // Python line 2315-2317: 递归深度限制 + if 递归层次 > 64 { + println!("笔.分析 递归深度超出 64 < {}", 递归层次); + } + + // Python line 2319-2320: 非顶底分型跳过 + if !matches!(当前分型.结构, 分型结构::顶 | 分型结构::底) { + return 递归层次; + } + + // Python line 2322-2325: 第一个分型直接追加 + if 分型序列.is_empty() { + if matches!(当前分型.结构, 分型结构::顶 | 分型结构::底) { + 分型序列.push(当前分型); + } + return 递归层次; + } + + // Python line 2329-2335: 清理无效数据 + let 之前分型 = Rc::clone(分型序列.last().unwrap()); + if 之前分型.时间戳 == 当前分型.时间戳 + || matches!(之前分型.结构, 分型结构::上 | 分型结构::下) + { + Self::弹出旧笔(分型序列, 笔序列); + if 分型序列.is_empty() { + if 当前分型.右.is_some() { + 分型::向序列中添加(分型序列, 当前分型); + } + return 递归层次; + } + } + + // Python line 2337-2341: 时序检查 + let 之前分型 = Rc::clone(分型序列.last().unwrap()); + if 之前分型.时间戳 > 当前分型.时间戳 && 之前分型.中.序号 - 当前分型.中.序号 > 1 { + println!("时序错误-{}, {}, {}", 递归层次, 之前分型, 当前分型); + return 递归层次; + } + + // Python line 2343-2348: 笔弱化模式 + if 配置.笔弱化 && !笔序列.is_empty() { + let 前一笔 = 笔序列.last().unwrap(); + let 前一笔缠K数 = 前一笔.武.中.序号 - 前一笔.文.中.序号 + 1; + if 前一笔缠K数 == 3 { + let 破位 = (前一笔.方向().是否向上() + && 前一笔.低() > 当前分型.分型特征值 + && 当前分型.结构 == 分型结构::底) + || (前一笔.方向().是否向下() + && 前一笔.高() < 当前分型.分型特征值 + && 当前分型.结构 == 分型结构::顶); + if 破位 { + Self::弹出旧笔(分型序列, 笔序列); + return Self::分析递归( + 当前分型, 分型序列, 笔序列, 缠K序列, _普K序列, 递归层次 + 1, 配置, + ); + } + } + } + + // Python line 2350: 分型结构相反 → 可能成笔 + let 之前分型 = Rc::clone(分型序列.last().unwrap()); + if 之前分型.结构 != 当前分型.结构 { + if let Some(基础序列) = 缠论K线::截取(缠K序列, &之前分型.中, &当前分型.中) { + let 当前笔 = Rc::new(虚线::创建笔( + Rc::clone(&之前分型), + Rc::clone(&当前分型), + true, + )); + + if Self::获取缠K数量(&基础序列, 笔序列, 配置) >= 配置.笔内元素数量 as usize + { + // Python line 2354-2357: 文官 + let 文官 = match 之前分型.结构 { + 分型结构::顶 => Self::实际高点(&基础序列, false), + _ => Self::实际低点(&基础序列, false), + }; + + // Python line 2359-2367: 文官调整 + if let Some(ref 文官_k) = 文官 { + if Rc::as_ptr(文官_k) != Rc::as_ptr(&之前分型.中) { + if let Some(临时分型) = + 分型::从缠K序列中获取分型(缠K序列, 文官_k) + { + let 递归层次 = Self::分析递归( + Rc::new(临时分型), + 分型序列, + 笔序列, + 缠K序列, + _普K序列, + 递归层次 + 1, + 配置, + ); + return Self::分析递归( + 当前分型, 分型序列, 笔序列, 缠K序列, _普K序列, + 递归层次 + 1, + 配置, + ); + } + } + } + + // Python line 2369-2375: 武将 and笔形成 + let 武将 = match 当前分型.结构 { + 分型结构::底 => { + Self::实际低点(&基础序列, 配置.笔内相同终点取舍) + } + _ => Self::实际高点(&基础序列, 配置.笔内相同终点取舍), + }; + + if Self::相对关系(&当前笔, 配置) { + if let Some(ref 武将_k) = 武将 { + if Rc::as_ptr(武将_k) == Rc::as_ptr(&当前分型.中) { + // 直接添加(对照 Python _添加新笔:直接 append) + Self::_添加新笔递归(分型序列, 笔序列, 当前分型, 当前笔); + return 递归层次; + } + } + } + + // Python line 2378-2385: 笔次级成笔 + if 配置.笔次级成笔 { + let 武将 = match 当前分型.结构 { + 分型结构::底 => { + Self::次低(&基础序列, 配置.笔内相同终点取舍) + } + _ => Self::次高(&基础序列, 配置.笔内相同终点取舍), + }; + if let Some(ref 武将_k) = 武将 { + if Rc::as_ptr(武将_k) == Rc::as_ptr(&当前分型.中) + && Self::相对关系(&当前笔, 配置) + { + Self::_添加新笔递归(分型序列, 笔序列, 当前分型, 当前笔); + return 递归层次; + } + } + } + } else { + // Python line 2388-2390: 元素不足 → 右元素扩展 + if let Some(ref 右) = 当前分型.右 { + if let Some(临时分型) = + 分型::从缠K序列中获取分型(缠K序列, 右) + { + return Self::分析递归( + Rc::new(临时分型), + 分型序列, + 笔序列, + 缠K序列, + _普K序列, + 递归层次 + 1, + 配置, + ); + } + } + } + } + } else { + // Python line 2392-2419: 分型结构相同 → 更强则替换 + 修复错过笔 + let 分型特征值 = 当前分型.分型特征值; + + let 更强 = match 之前分型.结构 { + 分型结构::顶 => 之前分型.分型特征值 < 分型特征值, + 分型结构::底 => 之前分型.分型特征值 > 分型特征值, + _ => false, + }; + + if 更强 { + // 保存被弹出的之前分型(用于修复错过笔的范围计算) + let 被替换分型 = Rc::clone(&之前分型); + Self::弹出旧笔(分型序列, 笔序列); + + if let Some(k线序列) = + 缠论K线::截取(缠K序列, &被替换分型.中, &当前分型.中) + { + let 武将 = match 被替换分型.结构 { + 分型结构::顶 => { + Self::实际低点(&k线序列, 配置.笔内相同终点取舍) + } + _ => Self::实际高点(&k线序列, 配置.笔内相同终点取舍), + }; + + if let Some(ref 武将_k) = 武将 { + if let Some(临时分型) = + 分型::从缠K序列中获取分型(缠K序列, 武将_k) + { + let 临时分型_rc = Rc::new(临时分型); + + if !分型序列.is_empty() { + let mut 递归层次 = Self::分析递归( + Rc::clone(&临时分型_rc), + 分型序列, + 笔序列, + 缠K序列, + _普K序列, + 递归层次 + 1, + 配置, + ); + + // 修复错过的笔: 扫描武将之后的所有分型 + if !分型序列.is_empty() + && Rc::as_ptr(分型序列.last().unwrap()) + == Rc::as_ptr(&临时分型_rc) + { + if let Some(武_idx) = 缠K序列 + .iter() + .position(|k| Rc::as_ptr(k) == Rc::as_ptr(武将_k)) + { + for ck in &缠K序列[武_idx..] { + if ck.分型 == Some(分型结构::底) + || ck.分型 == Some(分型结构::顶) + { + if let Some(错过分型) = + 分型::从缠K序列中获取分型(缠K序列, ck) + { + let 错过分型_rc = + Rc::new(错过分型); + 递归层次 = Self::分析递归( + Rc::clone(&错过分型_rc), + 分型序列, + 笔序列, + 缠K序列, + _普K序列, + 递归层次 + 1, + 配置, + ); + } + } + } + } + } + + return Self::分析递归( + 当前分型, 分型序列, 笔序列, 缠K序列, _普K序列, + 递归层次 + 1, + 配置, + ); + } else { + 分型::向序列中添加(分型序列, 当前分型); + } + } + } + } else if 分型序列.is_empty() { + 分型::向序列中添加(分型序列, 当前分型); + } else { + return Self::分析递归( + 当前分型, 分型序列, 笔序列, 缠K序列, _普K序列, 递归层次 + 1, 配置, + ); + } + } + } + + 递归层次 + } + + /// 添加新笔到序列(递归版本 — 直接追加,对应 Python _添加新笔) + fn _添加新笔递归( + 分型序列: &mut Vec>, + 笔序列: &mut Vec>, + 新分型: Rc<分型>, + 新笔: Rc<虚线>, + ) { + 分型序列.push(新分型); + let 序号 = if 笔序列.is_empty() { + 0 + } else { + 笔序列.last().unwrap().序号 + 1 + }; + let mut 虚线笔 = (*新笔).clone(); + 虚线笔.序号 = 序号; + if 虚线笔.武.左.is_none() && 虚线笔.武.右.is_none() { + 虚线笔.有效性 = false; + } + 笔序列.push(Rc::new(虚线笔)); + } + + /// 自检 — 验证笔的有效性(文为实际高/低点,武为实际低/高点) + pub fn 自检(筆: &虚线, 观察员: &观察者) -> bool { + let 笔序列 = &观察员.笔序列; + let 配置 = &观察员.配置; + let 基础序列 = 筆.获取缠K序列(&观察员.缠论K线序列); + if Self::获取缠K数量(&基础序列, 笔序列, 配置) >= 配置.笔内元素数量 as usize { + if 筆.方向() == 相对方向::向下 { + if let (Some(实际高), Some(实际低)) = ( + Self::实际高点(&基础序列, false), + Self::实际低点(&基础序列, 配置.笔内相同终点取舍), + ) { + if Rc::ptr_eq(&筆.文.中, &实际高) && Rc::ptr_eq(&筆.武.中, &实际低) { + return true; + } + } + } + if 筆.方向() == 相对方向::向上 { + if let (Some(实际低), Some(实际高)) = ( + Self::实际低点(&基础序列, false), + Self::实际高点(&基础序列, 配置.笔内相同终点取舍), + ) { + if Rc::ptr_eq(&筆.文.中, &实际低) && Rc::ptr_eq(&筆.武.中, &实际高) { + return true; + } + } + } + } + false + } + + /// 获取所有停顿位置 — 在笔范围内找出所有能成笔的分型组合 + pub fn 获取所有停顿位置(筆: &虚线, 观察员: &观察者) -> Vec<虚线> { + let mut 笔序列 = Vec::new(); + let 文 = Rc::clone(&筆.文); + let 基础序列 = 筆.获取缠K序列(&观察员.缠论K线序列); + + if 基础序列.len() < 5 { + return 笔序列; + } + + for i in 3..基础序列.len() - 1 { + let k = &基础序列[i]; + + if k.分型 == Some(分型结构::顶) && 筆.方向() == 相对方向::向上 { + let 左 = Rc::clone(&基础序列[i - 1]); + let 中 = Rc::clone(k); + let 右 = Rc::clone(&基础序列[i + 1]); + let 武 = 分型::new(Some(左), 中, Some(右)); + let mut 当前笔 = 虚线::创建笔(Rc::clone(&文), Rc::new(武), true); + 当前笔.序号 = 筆.序号; + if Self::自检(&当前笔, 观察员) { + 笔序列.push(当前笔); + } + } else if k.分型 == Some(分型结构::底) && 筆.方向() == 相对方向::向下 { + let 左 = Rc::clone(&基础序列[i - 1]); + let 中 = Rc::clone(k); + let 右 = Rc::clone(&基础序列[i + 1]); + let 武 = 分型::new(Some(左), 中, Some(右)); + let mut 当前笔 = 虚线::创建笔(Rc::clone(&文), Rc::new(武), true); + 当前笔.序号 = 筆.序号; + if Self::自检(&当前笔, 观察员) { + 笔序列.push(当前笔); + } + } + } + + 笔序列 + } + + /// 是否背驰过 — 判断笔是否在停顿位置出现过MACD趋向背驰 + pub fn 是否背驰过(当前筆: &虚线, 观察员: &观察者) -> Vec> { + let 停顿位置 = Self::获取所有停顿位置(当前筆, 观察员); + let mut 结果 = Vec::new(); + + for 筆 in &停顿位置 { + let k线范围 = K线::截取rc( + &观察员.普通K线序列, + &当前筆.文.中.标的K线, + &当前筆.武.中.标的K线, + ); + let 背驰信号 = 虚线::计算K线序列MACD趋向背驰(&k线范围, 筆.方向()); + if 背驰信号.iter().all(|&x| x) { + 结果.push(Rc::clone(&筆.武)); + } + } + + 结果 + } +} diff --git a/chanlun/src/algorithm/divergence.rs b/chanlun/src/algorithm/divergence.rs new file mode 100644 index 0000000..ad93480 --- /dev/null +++ b/chanlun/src/algorithm/divergence.rs @@ -0,0 +1,139 @@ +use crate::config::缠论配置; +use crate::kline::bar::K线; +use crate::structure::dash_line::虚线; +use std::rc::Rc; + +/// 背驰分析 — 判断进入段和离开段之间是否存在背驰 +pub struct 背驰分析; + +impl 背驰分析 { + /// MACD背驰 — 离开段MACD柱子面积小于进入段 + pub fn MACD背驰(进入段: &虚线, 离开段: &虚线, K线序列: &[Rc], 方式: &str) -> bool { + // 获取进入段和离开段对应的K线 + let 进入段_始 = 进入段.文.中.原始起始序号 as usize; + let 进入段_终 = 进入段.武.中.原始结束序号 as usize; + let 离开段_始 = 离开段.文.中.原始起始序号 as usize; + let 离开段_终 = 离开段.武.中.原始结束序号 as usize; + + if 进入段_终 >= K线序列.len() || 离开段_终 >= K线序列.len() { + return false; + } + + let 进入MACD = Self::柱子面积(&K线序列[进入段_始..=进入段_终], 方式); + let 离开MACD = Self::柱子面积(&K线序列[离开段_始..=离开段_终], 方式); + + // 背驰条件: 离开段面积绝对值小于进入段 + let 进入总面积 = 进入MACD.阳 + 进入MACD.阴.abs(); + let 离开总面积 = 离开MACD.阳 + 离开MACD.阴.abs(); + + if 进入总面积 < f64::EPSILON { + return false; + } + 离开总面积 < 进入总面积 + } + + /// 斜率背驰 — 离开段斜率小于进入段 + pub fn 斜率背驰(进入段: &虚线, 离开段: &虚线) -> bool { + let 进入斜率 = (进入段.武.分型特征值 - 进入段.文.分型特征值).abs() + / (进入段.武.时间戳 - 进入段.文.时间戳).abs() as f64; + let 离开斜率 = (离开段.武.分型特征值 - 离开段.文.分型特征值).abs() + / (离开段.武.时间戳 - 离开段.文.时间戳).abs() as f64; + + 离开斜率 < 进入斜率 + } + + /// 测度背驰 — 价格测度的背驰判断 + pub fn 测度背驰(进入段: &虚线, 离开段: &虚线) -> bool { + let 进入幅度 = (进入段.武.分型特征值 - 进入段.文.分型特征值).abs(); + let 离开幅度 = (离开段.武.分型特征值 - 离开段.文.分型特征值).abs(); + 离开幅度 < 进入幅度 + } + + /// 全量背驰 — MACD + 斜率 + 测度 三者全满足 + pub fn 全量背驰(进入段: &虚线, 离开段: &虚线, 普K序列: &[Rc]) -> bool { + Self::MACD背驰(进入段, 离开段, 普K序列, "总") + && Self::斜率背驰(进入段, 离开段) + && Self::测度背驰(进入段, 离开段) + } + + /// 任意背驰 — 任一条件满足即可 + pub fn 任意背驰(进入段: &虚线, 离开段: &虚线, 普K序列: &[Rc]) -> bool { + Self::MACD背驰(进入段, 离开段, 普K序列, "总") + || Self::斜率背驰(进入段, 离开段) + || Self::测度背驰(进入段, 离开段) + } + + /// 配置背驰 — 根据配置选择判断方式 + pub fn 配置背驰(进入段: &虚线, 离开段: &虚线, 普K序列: &[Rc], 配置: &缠论配置) -> bool { + let mut result = true; + if 配置.线段内部背驰_MACD { + result = result && Self::MACD背驰(进入段, 离开段, 普K序列, "总"); + } + if 配置.线段内部背驰_斜率 { + result = result && Self::斜率背驰(进入段, 离开段); + } + if 配置.线段内部背驰_测度 { + result = result && Self::测度背驰(进入段, 离开段); + } + result + } + + /// 任选背驰 + pub fn 任选背驰(进入段: &虚线, 离开段: &虚线, 普K序列: &[Rc]) -> bool { + Self::任意背驰(进入段, 离开段, 普K序列) + } + + /// 背驰模式 — 根据模式字符串选择判断方式 + pub fn 背驰模式( + 进入段: &虚线, + 离开段: &虚线, + 普K序列: &[Rc], + 配置: &缠论配置, + 模式: &str, + ) -> bool { + match 模式 { + "全量" => Self::全量背驰(进入段, 离开段, 普K序列), + "任意" => Self::任意背驰(进入段, 离开段, 普K序列), + "配置" => Self::配置背驰(进入段, 离开段, 普K序列, 配置), + "相对" => Self::测度背驰(进入段, 离开段), + _ => Self::配置背驰(进入段, 离开段, 普K序列, 配置), + } + } + + // ---- 内部辅助 ---- + + fn 柱子面积(K线序列: &[Rc], 方式: &str) -> MACD面积 { + let mut 阳 = 0.0f64; + let mut 阴 = 0.0f64; + for k in K线序列 { + if let Some(ref macd) = k.macd { + let hist = macd.MACD柱; + match 方式 { + "阳" => { + if hist > 0.0 { + 阳 += hist + } + } + "阴" => { + if hist < 0.0 { + 阴 += hist + } + } + "总" | _ => { + if hist >= 0.0 { + 阳 += hist + } else { + 阴 += hist + } + } + } + } + } + MACD面积 { 阳, 阴 } + } +} + +struct MACD面积 { + 阳: f64, + 阴: f64, +} diff --git a/chanlun/src/algorithm/hub.rs b/chanlun/src/algorithm/hub.rs new file mode 100644 index 0000000..258c6ad --- /dev/null +++ b/chanlun/src/algorithm/hub.rs @@ -0,0 +1,348 @@ +use crate::structure::dash_line::虚线; +use crate::structure::fractal_obj::分型; +use crate::types::相对方向; +use std::rc::Rc; + +/// 中枢 — 三段虚线重叠区间构成的价格中枢 +#[derive(Debug, Clone)] +pub struct 中枢 { + pub 序号: i64, + pub 标识: String, + pub 级别: i64, + pub 基础序列: Vec>, + pub 第三买卖线: Option>, + pub 本级_第三买卖线: Option>, +} + +impl 中枢 { + pub fn new(序号: i64, 标识: String, 级别: i64, 基础序列: Vec>) -> Self { + Self { + 序号, + 标识, + 级别, + 基础序列: 基础序列.into_iter().take(3).collect(), + 第三买卖线: None, + 本级_第三买卖线: None, + } + } + + pub fn 添加虚线(&mut self, 实线: Rc<虚线>) { + self.基础序列.push(实线); + self.本级_第三买卖线 = None; + self.第三买卖线 = None; + } + + pub fn 图表标题(&self) -> String { + format!( + "{}:{}:{}:{}", + self.文().中.标识, self.文().中.周期, self.标识, self.序号 + ) + } + + pub fn 离开段(&self) -> Rc<虚线> { + Rc::clone(&self.基础序列[self.基础序列.len() - 1]) + } + + pub fn 方向(&self) -> 相对方向 { + self.基础序列[0].方向().翻转() + } + + pub fn 高(&self) -> f64 { + self.基础序列[..3] + .iter() + .map(|x| x.高()) + .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or(0.0) + } + + pub fn 低(&self) -> f64 { + self.基础序列[..3] + .iter() + .map(|x| x.低()) + .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or(0.0) + } + + pub fn 高高(&self) -> f64 { + self.基础序列 + .iter() + .map(|x| x.高()) + .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or(0.0) + } + + pub fn 低低(&self) -> f64 { + self.基础序列 + .iter() + .map(|x| x.低()) + .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or(0.0) + } + + pub fn 文(&self) -> Rc<分型> { + Rc::clone(&self.基础序列[0].文) + } + + pub fn 武(&self) -> Rc<分型> { + Rc::clone(&self.基础序列[self.基础序列.len() - 1].武) + } + + pub fn 设置第三买卖线(&mut self, 线: Rc<虚线>) { + self.第三买卖线 = Some(线); + } + + pub fn 获取数据文本(&self) -> String { + let 第三买卖线_str = match &self.第三买卖线 { + Some(x) => format!("{}", x), + None => "None".to_string(), + }; + let 本级_第三买卖线_str = match &self.本级_第三买卖线 { + Some(x) => format!("{}", x), + None => "None".to_string(), + }; + format!( + "{}, {}, {}, 文:({},{}), 武:({},{}), {}, {}", + self.标识, + self.序号, + self.级别, + self.文().时间戳, + crate::utils::format_f64_g(self.文().分型特征值), + self.武().时间戳, + crate::utils::format_f64_g(self.武().分型特征值), + 第三买卖线_str, + 本级_第三买卖线_str, + ) + } + + /// 校验中枢合法性 + pub fn 校验合法性(&self, 虚线序列: &[Rc<虚线>], _中枢序列: &[Rc<中枢>]) -> bool { + // 1. 检查基础序列中的元素是否仍在虚线序列中 + for 元素 in &self.基础序列 { + if !虚线序列.iter().any(|x| Rc::as_ptr(x) == Rc::as_ptr(元素)) { + return false; + } + } + // 2. 检查前三根重叠是否有效 + if self.基础序列.len() >= 3 { + let 高 = self.高(); + let 低 = self.低(); + if 高 <= 低 { + return false; + } + } + true + } + + // ---- 关联函数 ---- + + /// 基础检查 — 三根虚线是否能形成中枢 + pub fn 基础检查(左: &虚线, 中: &虚线, 右: &虚线) -> bool { + if !左.之后是(中) || !中.之后是(右) { + return false; + } + let 高 = [左.高(), 中.高(), 右.高()] + .iter() + .copied() + .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or(0.0); + let 低 = [左.低(), 中.低(), 右.低()] + .iter() + .copied() + .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or(0.0); + 高 > 低 + } + + /// 创建中枢 + pub fn 创建(左: Rc<虚线>, 中: Rc<虚线>, 右: Rc<虚线>, 级别: i64, 标识: &str) -> Self { + Self::new(0, format!("{}中枢<{}>", 标识, 中.标识), 级别, vec![左, 中, 右]) + } + + /// 从序列中获取中枢 + pub fn 从序列中获取中枢( + 虚线序列: &[Rc<虚线>], + 起始方向: 相对方向, + 标识: &str, + ) -> Option> { + for i in 2..虚线序列.len() { + let 左 = &虚线序列[i - 2]; + let 中 = &虚线序列[i - 1]; + let 右 = &虚线序列[i]; + if Self::基础检查(左, 中, 右) && 左.方向() == 起始方向 { + let 中枢 = Self::创建(Rc::clone(左), Rc::clone(中), Rc::clone(右), 0, 标识); + return Some(Rc::new(中枢)); + } + } + None + } + + /// 向中枢序列尾部添加 + pub fn 向中枢序列尾部添加(中枢序列: &mut Vec>, 待添加中枢: Rc<中枢>) { + // Dedup + if let Some(前一个) = 中枢序列.last() { + if 前一个.高() == 待添加中枢.高() && 前一个.低() == 待添加中枢.低() { + return; + } + } + // Set序号: Python — if中枢序列: 待添加中枢.序号 = 中枢序列[-1].序号 + 1 else: stays 0 + if let Some(前一个) = 中枢序列.last() { + let 新序号 = 前一个.序号 + 1; + // Clone to mutate since Rc is immutable + let mut cloned = (*待添加中枢).clone(); + cloned.序号 = 新序号; + 中枢序列.push(Rc::new(cloned)); + } else { + 中枢序列.push(待添加中枢); + } + } + + /// 从中枢序列尾部弹出 + pub fn 从中枢序列尾部弹出(中枢序列: &mut Vec>, 待弹出: &Rc<中枢>) -> Option> { + if 中枢序列.last().map(|x| Rc::as_ptr(x)) == Some(Rc::as_ptr(待弹出)) { + 中枢序列.pop() + } else { + None + } + } + + /// 中枢分析 — 从虚线序列生成中枢序列(增量算法) + /// + /// 每收到新的虚线序列数据后调用,更新中枢序列 + pub fn 分析( + 虚线序列: &[Rc<虚线>], + 中枢序列: &mut Vec>, + 跳过首部: bool, + 标识: &str, + 层级: i64, + ) { + if 虚线序列.len() < 3 { + return; + } + + // 初始化第一个中枢 + if 中枢序列.is_empty() { + for i in 1..虚线序列.len() - 1 { + let 左 = &虚线序列[i - 1]; + let 中 = &虚线序列[i]; + let 右 = &虚线序列[i + 1]; + + if Self::基础检查(左, 中, 右) { + // Python: 序号 = 虚线序列.index(左) + let 序号 = 虚线序列.iter().position(|x| Rc::as_ptr(x) == Rc::as_ptr(左)).unwrap_or(i - 1); + if 跳过首部 && (左.序号 == 0 || 序号 == 0) { + continue; + } + if 序号 >= 2 { + let 前 = &虚线序列[序号 - 2]; + let 同向相对关系 = crate::types::相对方向::分析(前.高(), 前.低(), 左.高(), 左.低()); + if 同向相对关系.是否向上() && 左.方向().是否向上() { + continue; + } + if 同向相对关系.是否向下() && 左.方向().是否向下() { + continue; + } + } + let 新中枢 = Rc::new(Self::创建( + Rc::clone(左), + Rc::clone(中), + Rc::clone(右), + 中.级别, + 标识, + )); + Self::向中枢序列尾部添加(中枢序列, 新中枢); + // Python: return 中枢递归分析(虚线序列, 中枢序列, ...) + Self::分析(虚线序列, 中枢序列, 跳过首部, 标识, 层级); + return; + } + } + return; + } + + // 增量更新 + let 当前中枢_idx = 中枢序列.len() - 1; + let 当前中枢 = Rc::clone(&中枢序列[当前中枢_idx]); + + if !当前中枢.校验合法性(虚线序列, 中枢序列) { + Self::从中枢序列尾部弹出(中枢序列, &当前中枢); + Self::分析(虚线序列, 中枢序列, 跳过首部, 标识, 层级); + return; + } + + // 找到当前中枢最后一个元素在虚线序列中的位置 + let 最后元素 = &当前中枢.基础序列[当前中枢.基础序列.len() - 1]; + let 起始索引 = match 虚线序列 + .iter() + .position(|x| Rc::as_ptr(x) == Rc::as_ptr(最后元素)) + { + Some(idx) => idx + 1, + None => return, + }; + + let 中枢高 = 当前中枢.高(); + let 中枢低 = 当前中枢.低(); + let mut 候选序列: Vec> = Vec::new(); + + let mut 新当前中枢 = (*当前中枢).clone(); + + for i in 起始索引..虚线序列.len() { + let 当前虚线 = Rc::clone(&虚线序列[i]); + + // 检查是否超出中枢范围(缺口) + if crate::types::相对方向::分析(中枢高, 中枢低, 当前虚线.高(), 当前虚线.低()).是否缺口() { + 候选序列.push(当前虚线.clone()); + + // Python: if 当前中枢.基础序列[-1].之后是(当前虚线): + if 新当前中枢.基础序列.last().unwrap().之后是(&当前虚线) { + 新当前中枢.设置第三买卖线(当前虚线.clone()); + } + } else { + if 候选序列.is_empty() { + // 仍在范围内:延伸中枢 + 新当前中枢.添加虚线(当前虚线); + } else { + 候选序列.push(当前虚线); + } + } + + // 候选序列积满3个:尝试创建新中枢 + while 候选序列.len() >= 3 { + let 起始方向 = 新当前中枢.基础序列.last().unwrap().方向().翻转(); + match Self::从序列中获取中枢(&候选序列, 起始方向, 标识) { + Some(新中枢) => { + 中枢序列[当前中枢_idx] = Rc::new(新当前中枢.clone()); + Self::向中枢序列尾部添加(中枢序列, 新中枢); + return; + } + None => { + 候选序列.remove(0); // 滑动窗口 + } + } + } + } + + // 更新当前中枢 + 中枢序列[当前中枢_idx] = Rc::new(新当前中枢); + } +} + +impl std::fmt::Display for 中枢 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let 序列_str = self + .基础序列 + .iter() + .map(|d| format!("{}", d)) + .collect::>() + .join(", "); + write!( + f, + "{}({}, {}, 元素数量: {}, [{}], {} ===>>> {})", + self.标识, + crate::utils::format_f64_g(self.高()), + crate::utils::format_f64_g(self.低()), + self.基础序列.len(), + 序列_str, + self.文(), + self.武(), + ) + } +} diff --git a/chanlun/src/algorithm/mod.rs b/chanlun/src/algorithm/mod.rs new file mode 100644 index 0000000..ce0b3de --- /dev/null +++ b/chanlun/src/algorithm/mod.rs @@ -0,0 +1,4 @@ +pub mod bi; +pub mod divergence; +pub mod hub; +pub mod segment; diff --git a/chanlun/src/algorithm/segment.rs b/chanlun/src/algorithm/segment.rs new file mode 100644 index 0000000..97d8a56 --- /dev/null +++ b/chanlun/src/algorithm/segment.rs @@ -0,0 +1,1726 @@ +use crate::algorithm::bi::笔; +use crate::algorithm::hub::中枢; +use crate::business::observer::观察者; +use crate::config::缠论配置; +use crate::kline::bar::K线; +use crate::structure::dash_line::虚线; +use crate::structure::fractal_obj::分型; +use crate::structure::segment_feat::线段特征; +use crate::types::{分型结构, 相对方向, 缺口}; +use std::rc::Rc; + +/// 线段 — 从笔生成线段的算法集合(静态方法命名空间) +pub struct 线段; + +impl 线段 { + // ================================================================ + // 基础操作 + // ================================================================ + + /// 获取Rc<虚线>的可变引用(clone-on-write) + fn 取段可变(段_rc: &mut Rc<虚线>) -> &mut 虚线 { + if Rc::strong_count(段_rc) > 1 { + let 克隆 = (**段_rc).clone(); + *段_rc = Rc::new(克隆); + } + Rc::get_mut(段_rc).unwrap() + } + + /// 向线段的基础序列中添加一笔 + pub fn 添加虚线(段_rc: &mut Rc<虚线>, 筆: Rc<虚线>) { + let 段 = Self::取段可变(段_rc); + if !段.基础序列.is_empty() { + if !分型::判断分型(&段.基础序列.last().unwrap().武, &筆.文, "中") { + eprintln!( + "线段.添加虚线 不连续 {:?} {:?}", + 段.基础序列.last().unwrap(), 筆 + ); + return; + } + if 段.基础序列.last().unwrap().标识 != 筆.标识 { + eprintln!( + "线段.添加虚线 标识不符 {} {}", + 段.基础序列.last().unwrap().标识, 筆.标识 + ); + return; + } + } + 段.基础序列.push(筆); + } + + /// 更新线段的终点分型 + pub fn 武斗(段_rc: &mut Rc<虚线>, 武: &Rc<分型>, 行号: u32) { + let 段 = Self::取段可变(段_rc); + if Rc::as_ptr(&段.武) == Rc::as_ptr(武) { + return; + } + if 段.武.分型特征值 == 武.分型特征值 && 段.武.时间戳 != 武.时间戳 { + eprintln!( + "线段.武斗[{}], 发现特征值相等但时间戳不同 {:?} {:?}", + 行号, 段.武, 武 + ); + } + if 段.文.结构 == 武.结构 { + panic!("文武结构相同 {} {:?} {:?}", 行号, 段.文, 武); + } + if 段.方向() == 相对方向::向上 { + if 武.分型特征值 < 段.文.分型特征值 { + panic!( + "向上线段, 结束点小于起点 {} {:?} {:?}", + 段.标识, 段.文, 武 + ); + } + } else { + if 武.分型特征值 > 段.文.分型特征值 { + panic!( + "向下线段, 结束点大于起点 {} {:?} {:?}", + 段.标识, 段.文, 武 + ); + } + } + 段.武 = Rc::clone(武); + } + + /// 武终 — 线段终结时设置终点 + pub fn 武终(段_rc: &mut Rc<虚线>, 行号: u32) { + let 武 = { + let 段 = Self::取段可变(段_rc); + if 段.模式 != "文武" { + Some(Rc::clone(&段.基础序列.last().unwrap().武)) + } else { + None + } + }; + if let Some(武) = 武 { + Self::武斗(段_rc, &武, 行号); + } + } + + /// 验证序列 — 截断无效尾部 + pub fn 验证序列(段_rc: &mut Rc<虚线>, 序列: &[Rc<虚线>]) { + let 段 = Self::取段可变(段_rc); + let mut 基础序列: Vec> = Vec::new(); + for 元素 in &段.基础序列 { + if !序列.iter().any(|x| Rc::as_ptr(x) == Rc::as_ptr(元素)) { + break; + } + if !基础序列.is_empty() { + if !基础序列.last().unwrap().之后是(元素) { + eprintln!("线段._验证序列 数据不连续"); + break; + } + } + 基础序列.push(Rc::clone(元素)); + } + if 基础序列.len() % 2 == 0 { + 基础序列.pop(); + } + 段.基础序列 = 基础序列; + } + + /// 序列重置 — 截取到序列中的有效部分 + pub fn 序列重置(段_rc: &mut Rc<虚线>, 序列: &[Rc<虚线>]) { + let 段 = Self::取段可变(段_rc); + let mut 基础序列: Vec> = Vec::new(); + for 元素 in &段.基础序列 { + if !序列.iter().any(|x| Rc::as_ptr(x) == Rc::as_ptr(元素)) { + break; + } + if !基础序列.is_empty() { + if !基础序列.last().unwrap().之后是(元素) { + break; + } + } + 基础序列.push(Rc::clone(元素)); + } + 段.基础序列 = 基础序列; + if 段.特征序列.len() > 2 { + 段.特征序列[2] = None; + } + } + + // ================================================================ + // 基础判断 + // ================================================================ + + /// 基础判断 — 连续三笔且重叠才能构成线段 + pub fn 基础判断(左: &虚线, 中: &虚线, 右: &虚线, 关系序列: &[相对方向]) -> bool { + if !左.之后是(中) { + return false; + } + if !中.之后是(右) { + return false; + } + + if !相对方向::分析(左.高(), 左.低(), 中.高(), 中.低()).是否包含() { + return false; + } + if !相对方向::分析(中.高(), 中.低(), 右.高(), 右.低()).是否包含() { + return false; + } + + let 关系 = 相对方向::分析(左.高(), 左.低(), 右.高(), 右.低()); + if !关系序列.contains(&关系) { + return false; + } + + if 左.方向() == 相对方向::向下 && !关系.是否向下() { + return false; + } + if 左.方向() == 相对方向::向上 && !关系.是否向上() { + return false; + } + true + } + + // ================================================================ + // 四象 / 缺口 / 特征序列 + // ================================================================ + + /// 四象 — 线段的四种状态分类 + /// + /// 老阳: 向下线段第一二特征序列有缺口, 后一向上线段 + /// 老阴: 向上线段第一二特征序列有缺口, 后一向下线段 + /// 小阳: 向上线段 + /// 少阴: 向下线段 + pub fn 四象(段: &虚线) -> String { + if 段.前一缺口.is_some() { + if 段.方向() == 相对方向::向上 { + "老阳".into() + } else { + "老阴".into() + } + } else if 段.方向() == 相对方向::向上 { + "小阳".into() + } else { + "少阴".into() + } + } + + /// 获取缺口 — 从特征序列第一二元素之间检测缺口 + pub fn 获取缺口(段: &虚线) -> Option<缺口> { + if 段.模式 != "文武" { + return None; + } + if 段.特征序列.len() < 2 { + return None; + } + let 左 = 段.特征序列[0].as_ref()?; + let 中 = 段.特征序列[1].as_ref()?; + let 相对关系 = 相对方向::分析(左.高(), 左.低(), 中.高(), 中.低()); + if 相对关系.是否缺口() { + let 高 = 左.文().分型特征值.max(中.文().分型特征值); + let 低 = 左.文().分型特征值.min(中.文().分型特征值); + Some(缺口 { 高, 低 }) + } else { + None + } + } + + /// 特征分型终结 — 检查特征序列是否形成正常分型终结 + pub fn 特征分型终结(段: &虚线) -> bool { + let 特征序列 = 线段特征::静态分析(&段.基础序列, 段.方向(), &Self::四象(段), false); + if 特征序列.len() >= 3 { + let idx = 特征序列.len(); + if let Some(结构) = 分型结构::分析( + &*特征序列[idx - 3], + &*特征序列[idx - 2], + &*特征序列[idx - 1], + true, + true, + ) { + if 段.方向() == 相对方向::向上 { + return 结构 == 分型结构::顶; + } else { + return 结构 == 分型结构::底; + } + } + } + false + } + + /// 特征序列状态 — 返回三个特征序列元素是否为 Some + pub fn 特征序列状态(段: &虚线) -> (bool, bool, bool) { + let get = |i: usize| { + 段.特征序列 + .get(i) + .map(|x| x.is_some()) + .unwrap_or(false) + }; + (get(0), get(1), get(2)) + } + + /// 设置特征序列 + pub fn 设置特征序列(段_rc: &mut Rc<虚线>, 序列: Vec>>, 行号: u32) { + let 段 = Self::取段可变(段_rc); + if 段.模式 != "文武" { + return; + } + + for 特征 in &序列 { + if let Some(ref f) = 特征 { + if f.方向() == 段.方向() { + panic!("特征序列方向不匹配[{}]", 行号); + } + } + } + + let 左 = 序列[0].clone(); + let 中 = 序列[1].clone(); + let 右 = 序列[2].clone(); + 段.特征序列 = vec![左, 中, 右]; + + if let Some(ref 右特征) = 段.特征序列[2] { + let mut 基础序列: Vec> = Vec::new(); + let 右尾 = 右特征.元素.last().expect("特征序列元素不应为空"); + if !段.基础序列.iter().any(|x| Rc::as_ptr(x) == Rc::as_ptr(右尾)) { + panic!("右特征最后一个元素不在基础序列中"); + } + for 元素 in &段.基础序列 { + 基础序列.push(Rc::clone(元素)); + if Rc::as_ptr(元素) == Rc::as_ptr(右尾) { + break; + } + } + + if 基础序列.len() >= 6 && 基础序列.len() % 2 == 0 { + 段.基础序列 = 基础序列; + } else { + panic!("设置特征序列: 基础序列长度不足或非偶数"); + } + } + } + + /// 刷新特征序列 + pub fn 刷新特征序列(段_rc: &mut Rc<虚线>, 配置: &缠论配置) { + // Work on a clone since we need to read and write + let mut 新段 = (**段_rc).clone(); + if 新段.模式 != "文武" { + return; + } + let mut 基础序列 = 新段.基础序列.clone(); + if let Some(ref 前结束) = 新段.前一结束位置 { + if let Some(idx) = 基础序列 + .iter() + .position(|x| Rc::as_ptr(x) == Rc::as_ptr(前结束)) + { + if idx > 0 { + 基础序列 = 基础序列[idx - 1..].to_vec(); + } + } + } + + let 四象 = Self::四象(&新段); + let 特征序列 = 线段特征::静态分析( + &基础序列, + 新段.方向(), + &四象, + 配置.线段_特征序列忽视老阴老阳, + ); + + if 特征序列.len() >= 3 { + let 分型序列 = 线段特征::获取分型序列(&特征序列); + if !分型序列.is_empty() { + let 最后分型 = &分型序列[分型序列.len() - 1]; + if (新段.方向() == 相对方向::向上 + && 最后分型.结构 == 分型结构::顶) + || (新段.方向() == 相对方向::向下 + && 最后分型.结构 == 分型结构::底) + { + 新段.特征序列 = vec![ + Some(Rc::clone(&最后分型.左)), + Some(Rc::clone(&最后分型.中)), + Some(Rc::clone(&最后分型.右)), + ]; + } else { + 新段.特征序列 = vec![ + Some(Rc::clone(&特征序列[特征序列.len() - 2])), + Some(Rc::clone(&特征序列[特征序列.len() - 1])), + None, + ]; + } + } else { + 新段.特征序列 = vec![ + Some(Rc::clone(&特征序列[特征序列.len() - 2])), + Some(Rc::clone(&特征序列[特征序列.len() - 1])), + None, + ]; + } + } else { + let mut 填充: Vec>> = + 特征序列.into_iter().map(Some).collect(); + 填充.resize(3, None); + 新段.特征序列 = 填充; + } + + *段_rc = Rc::new(新段); + } + + /// 查找贯穿伤 — 基础序列中穿透文分型特征值的笔 + pub fn 查找贯穿伤(段: &虚线) -> Option> { + for 贯穿伤 in 段.基础序列.iter().skip(3) { + if 段.方向().是否向上() { + if 贯穿伤.武.分型特征值 < 段.文.分型特征值 { + return Some(Rc::clone(贯穿伤)); + } + } else { + if 贯穿伤.武.分型特征值 > 段.文.分型特征值 { + return Some(Rc::clone(贯穿伤)); + } + } + } + None + } + + // ================================================================ + // 分割序列 + // ================================================================ + + /// 分割序列 — 将线段的基础序列分为前、后、第三买卖线、贯穿伤 + pub fn 分割序列( + 段: &虚线, + 所属中枢: Option<&Rc<中枢>>, + ) -> (Vec>, Vec>, Vec>, Option>) { + if 段.模式 != "文武" { + return (段.基础序列.clone(), Vec::new(), Vec::new(), None); + } + + let mut 前: Vec> = Vec::new(); + let mut 后: Vec> = Vec::new(); + let mut 第三买卖线: Vec> = Vec::new(); + let mut 贯穿伤: Option> = None; + + for 筆 in &段.基础序列 { + if 前.is_empty() { + 前.push(Rc::clone(筆)); + continue; + } + if Rc::as_ptr(&前.last().unwrap().武) != Rc::as_ptr(&段.武) && 后.is_empty() { + 前.push(Rc::clone(筆)); + } + + if !后.is_empty() { + 后.push(Rc::clone(筆)); + } + if Rc::as_ptr(&筆.文) == Rc::as_ptr(&段.武) { + 后.push(Rc::clone(筆)); + } + } + + let mut 状态 = None; + + if let Some(中枢) = 所属中枢 { + let 尾部 = if let Some(ref 后笔) = 后.last() { + Rc::clone(&后笔.武) + } else { + Rc::clone(&段.武) + }; + + if 中枢.高() >= 尾部.分型特征值 && 尾部.分型特征值 >= 中枢.低() { + 状态 = Some("中枢之中"); + } else if 中枢.高() < 尾部.分型特征值 { + 状态 = Some("中枢之上"); + } else if 中枢.低() > 尾部.分型特征值 { + 状态 = Some("中枢之下"); + } + } + + if 状态 == Some("中枢之上") { + for 筆 in 段.基础序列.iter().rev() { + if 筆.方向() == 相对方向::向下 { + let 关系 = 相对方向::分析( + 所属中枢.unwrap().高(), + 所属中枢.unwrap().低(), + 筆.高(), + 筆.低(), + ); + if 关系 == 相对方向::向上缺口 { + 第三买卖线.push(Rc::clone(筆)); + } else { + break; + } + } else { + break; + } + } + } + + if 状态 == Some("中枢之下") { + for 筆 in 段.基础序列.iter().rev() { + if 筆.方向() == 相对方向::向上 { + let 关系 = 相对方向::分析( + 所属中枢.unwrap().高(), + 所属中枢.unwrap().低(), + 筆.高(), + 筆.低(), + ); + if 关系 == 相对方向::向下缺口 { + 第三买卖线.push(Rc::clone(筆)); + } else { + break; + } + } else { + break; + } + } + } + + if !第三买卖线.is_empty() { + 第三买卖线.reverse(); + } + + if !后.is_empty() { + if 段.方向().是否向上() { + if 后[0].武.分型特征值 < 段.文.分型特征值 { + 贯穿伤 = Some(Rc::clone(&后[0])); + } + } else { + if 后[0].武.分型特征值 > 段.文.分型特征值 { + 贯穿伤 = Some(Rc::clone(&后[0])); + } + } + } + + (前, 后, 第三买卖线, 贯穿伤) + } + + /// 刷新 — 完整刷新线段的特征序列和内部中枢 + pub fn 刷新(段_rc: &mut Rc<虚线>, 配置: &缠论配置) { + let 段 = Self::取段可变(段_rc); + if 段.模式 != "文武" { + return; + } + if 段.基础序列.is_empty() { + eprintln!(" 线段.刷新 基础序列为空"); + return; + } + + Self::刷新特征序列(段_rc, 配置); + + // After 刷新特征序列, work with the updated segment + let (武斗_武文, 特征后一笔_opt) = { + let 段2 = Self::取段可变(段_rc); + let 有效特征序列: Vec<&Rc<线段特征>> = 段2.特征序列.iter().filter_map(|x| x.as_ref()).collect(); + + if 有效特征序列.len() == 3 { + (Some(Rc::clone(&有效特征序列[1].文())), None) + } else if !有效特征序列.is_empty() { + let 最近特征 = 有效特征序列[有效特征序列.len() - 1]; + + let 特征后一笔 = if 最近特征.元素.last().map(|x| { + 段2.基础序列 + .iter() + .any(|b| Rc::as_ptr(b) == Rc::as_ptr(x)) + }) == Some(true) + { + Some(Rc::clone(最近特征.元素.last().unwrap())) + } else if let Some(b) = + 笔::以武会友(&段2.基础序列, &最近特征.元素.last().unwrap().武) + { + Some(b) + } else { + None + }; + + if 特征后一笔.is_none() { + eprintln!( + " 线段.刷新 特征后一笔 = None, {:?}, 有效特征: {}", + 段2, + 有效特征序列.len() + ); + } + (None, 特征后一笔) + } else { + (None, None) + } + }; + + if let Some(武文) = 武斗_武文 { + Self::武斗(段_rc, &武文, 0); + } else if let Some(特征后一笔) = 特征后一笔_opt { + + let 武斗候选 = { + let 段2 = Self::取段可变(段_rc); + if let Some(序号) = 段2 + .基础序列 + .iter() + .position(|x| Rc::as_ptr(x) == Rc::as_ptr(&特征后一笔)) + { + if 序号 < 段2.基础序列.len() - 1 { + let 下一笔 = Rc::clone(&段2.基础序列[序号 + 1]); + if (段2.方向() == 相对方向::向上 && 段2.高() <= 下一笔.高()) + || (段2.方向() == 相对方向::向下 && 段2.低() >= 下一笔.低()) + { + Some(Rc::clone(&下一笔.武)) + } else { + None + } + } else { + None + } + } else { + None + } + }; + if let Some(武) = 武斗候选 { + Self::武斗(段_rc, &武, 0); + } + } + + let 段3 = Self::取段可变(段_rc); + let _ = Self::获取内部中枢序列_内部(段3, 配置); + } + + /// 获取内部中枢序列 — 内部实现 + fn 获取内部中枢序列_内部(段: &mut 虚线, _配置: &缠论配置) -> (Vec>, Vec>, Vec>) { + if 段.模式 != "文武" { + 中枢::分析(&段.基础序列, &mut 段.合_中枢序列, true, &format!("{}_{}_合_", 段.标识, 段.序号), 0); + return (Vec::new(), Vec::new(), 段.合_中枢序列.clone()); + } + + // Clone base sequence to avoid borrow issues + let 基础序列 = 段.基础序列.clone(); + let _文 = Rc::clone(&段.文); + let 武 = Rc::clone(&段.武); + + // Reconstruct 前/后 from base sequence + let mut 前: Vec> = Vec::new(); + let mut 后: Vec> = Vec::new(); + + for 筆 in &基础序列 { + if 前.is_empty() { + 前.push(Rc::clone(筆)); + continue; + } + if Rc::as_ptr(&前.last().unwrap().武) != Rc::as_ptr(&武) && 后.is_empty() { + 前.push(Rc::clone(筆)); + } + if !后.is_empty() { + 后.push(Rc::clone(筆)); + } + if Rc::as_ptr(&筆.文) == Rc::as_ptr(&武) { + 后.push(Rc::clone(筆)); + } + } + + 中枢::分析(&前, &mut 段.实_中枢序列, true, &format!("{}_{}_实_", 段.标识, 段.序号), 0); + 中枢::分析(&后, &mut 段.虚_中枢序列, true, &format!("{}_{}_虚_", 段.标识, 段.序号), 0); + 中枢::分析(&基础序列, &mut 段.合_中枢序列, true, &format!("{}_{}_合_", 段.标识, 段.序号), 0); + + (段.虚_中枢序列.clone(), 段.实_中枢序列.clone(), 段.合_中枢序列.clone()) + } + + /// 获取内部中枢序列 + pub fn 获取内部中枢序列(段_rc: &mut Rc<虚线>, 配置: &缠论配置) -> (Vec>, Vec>, Vec>) { + let 段 = Self::取段可变(段_rc); + Self::获取内部中枢序列_内部(段, 配置) + } + + // ================================================================ + // 线段序列管理 + // ================================================================ + + /// _添加线段 — 向线段序列尾部添加线段(内部方法) + pub fn _添加线段( + 线段序列: &mut Vec>, + 待添加线段_rc: &mut Rc<虚线>, + _配置: &缠论配置, + 行号: String, + ) { + let 待添加线段 = Self::取段可变(待添加线段_rc); + + if let Some(前一个) = 线段序列.last() { + if !前一个.之后是(待添加线段) { + panic!( + "线段.向序列中添加 不连续[{}] {:?} {:?}", + 行号, 前一个.武, 待添加线段.文 + ); + } + } + + 待添加线段.模式 = "文武".into(); + + if 线段序列.is_empty() { + let 新段 = Rc::new(待添加线段.clone()); + 线段序列.push(新段); + return; + } + + let 之前线段 = 线段序列.last().unwrap(); + + // Check 之前线段.特征序列[2] is valid + if 之前线段.特征序列.len() >= 3 && 之前线段.特征序列[2].is_none() && !之前线段.短路修正 { + // The Python code asserts here; we warn + eprintln!( + "线段._向序列中添加[{}], 之前线段.右 = None {:?}", + 行号, 之前线段 + ); + } + + if !待添加线段 + .基础序列 + .iter() + .any(|x| Rc::as_ptr(x) == Rc::as_ptr(之前线段.基础序列.last().unwrap())) + && !之前线段.短路修正 + { + panic!( + "线段._向序列中添加[{}], 之前线段[-1] not in 待添加虚线! {:?}", + 行号, 之前线段 + ); + } + + 待添加线段.序号 = 之前线段.序号 + 1; + 待添加线段.前一缺口 = Self::获取缺口(之前线段); + 待添加线段.前一结束位置 = Some(Rc::clone(之前线段.基础序列.last().unwrap())); + + if ["老阴", "老阳"].contains(&Self::四象(之前线段).as_str()) { + 待添加线段.前一缺口 = None; + } + + let 新段 = Rc::new(待添加线段.clone()); + 线段序列.push(新段); + } + + /// _弹出线段 — 从线段序列尾部弹出线段(内部方法) + pub fn _弹出线段( + 线段序列: &mut Vec>, + 待弹出线段: &Rc<虚线>, + _配置: &缠论配置, + 行号: String, + ) -> Option> { + if 线段序列.is_empty() { + return None; + } + + if Rc::as_ptr(线段序列.last().unwrap()) != Rc::as_ptr(待弹出线段) { + panic!( + "线段._从序列中删除 弹出数据不在列表中 {:?}", + 待弹出线段 + ); + } + + if 待弹出线段.特征序列.len() >= 3 { + let 左 = &待弹出线段.特征序列[0]; + let 中 = &待弹出线段.特征序列[1]; + let 右 = &待弹出线段.特征序列[2]; + if let (Some(l), Some(m), Some(r)) = (左, 中, 右) { + if let Some(结构) = 分型结构::分析(&**l, &**m, &**r, true, true) { + if matches!(结构, 分型结构::顶 | 分型结构::底) + && !相对方向::分析(l.高(), l.低(), m.高(), m.低()).是否缺口() + { + eprintln!( + "[警告<{}>]: 线段._从序列中删除 发现分型完毕, 且特征序列无缺口 {:?}", + 行号, 待弹出线段 + ); + } + } + } + } + + let 弹出 = 线段序列.pop().unwrap(); + + // Mark popped segment as invalid + // Since it's behind Rc, we mark it by replacing its clone in caller context + // For now, just pop it from the sequence + + Some(弹出) + } + + // ================================================================ + // 四种修正机制 + // ================================================================ + + /// _缺口突破 — 老阳/老阴缺口突破修正 + pub fn _缺口突破( + 线段序列: &mut Vec>, + 配置: &缠论配置, + 层级: i64, + ) -> bool { + if 线段序列.is_empty() { + return false; + } + + let 当前线段 = Rc::clone(线段序列.last().unwrap()); + if 当前线段.基础序列.is_empty() { + return false; + } + let 当前虚线 = Rc::clone(&当前线段.基础序列[当前线段.基础序列.len() - 1]); + let 四象 = Self::四象(&当前线段); + let 同向 = 当前虚线.方向() == 当前线段.方向(); + + // 条件1:不能同向 + if 同向 { + return false; + } + + // 条件2:四象必须是老阳或老阴 + if 四象 != "老阳" && 四象 != "老阴" { + return false; + } + + // 条件3:当前线段特征序列[2]必须为None + if 当前线段.特征序列.len() >= 3 && 当前线段.特征序列[2].is_some() { + return false; + } + + // 条件4:具体突破方向判断 + let 突破 = (四象 == "老阳" && 当前虚线.低() < 当前线段.低()) + || (四象 == "老阴" && 当前虚线.高() > 当前线段.高()); + if !突破 { + return false; + } + + // 执行修正 + let 序列 = 当前线段.基础序列.clone(); + Self::_弹出线段( + 线段序列, + &Rc::clone(线段序列.last().unwrap()), + 配置, + format!("{}, {}", 0, 层级), + ); + + if 线段序列.is_empty() { + return true; + } + + let mut 当前线段_克隆 = (**线段序列.last().unwrap()).clone(); + if 当前线段_克隆.特征序列.len() < 3 || 当前线段_克隆.特征序列[2].is_none() { + // Shouldn't happen after correction + return true; + } + + let (前, _, _, _) = Self::分割序列(&当前线段_克隆, None); + let mut 当前线段基础序列 = 前; + 当前线段基础序列.extend(序列); + + 当前线段_克隆.基础序列 = 当前线段基础序列; + let idx = 线段序列.len() - 1; + 线段序列[idx] = Rc::new(当前线段_克隆); + + Self::刷新(&mut 线段序列[idx], 配置); + true + } + + /// _非缺口下穿刺 — 贯穿伤修复 + pub fn _非缺口下穿刺( + 线段序列: &mut Vec>, + 配置: &缠论配置, + 层级: i64, + ) -> bool { + if 线段序列.is_empty() { + return false; + } + + let 当前线段 = Rc::clone(线段序列.last().unwrap()); + let 四象 = Self::四象(&当前线段); + + // 外层条件 + if !(配置.线段_非缺口下穿刺 + && (四象 == "小阳" || 四象 == "少阴") + && 当前线段.特征序列.len() >= 3 + && 当前线段.特征序列[2].is_none()) + { + return false; + } + + // 查找贯穿伤 + let 贯穿伤 = Self::查找贯穿伤(&当前线段); + if 贯穿伤.is_none() { + return false; + } + let 贯穿伤 = 贯穿伤.unwrap(); + + // 切割基础序列 + let 贯穿伤_idx = 当前线段 + .基础序列 + .iter() + .position(|x| Rc::as_ptr(x) == Rc::as_ptr(&贯穿伤)); + + if 贯穿伤_idx.is_none() { + return false; + } + let 基础序列: Vec> = 当前线段.基础序列[贯穿伤_idx.unwrap()..].to_vec(); + + // 长度条件 + if !(基础序列.len() == 4 && 线段序列.len() >= 2) { + return false; + } + + let 左 = Rc::clone(&基础序列[基础序列.len() - 3]); + let 右 = Rc::clone(&基础序列[基础序列.len() - 1]); + + // 方向条件 + if 相对方向::分析(左.高(), 左.低(), 右.高(), 右.低()) != 当前线段.方向() { + return false; + } + + // 执行修正 + eprintln!( + "[警告<{}, {}>]: 线段.修复贯穿伤 {:?}", + 层级, 层级, 贯穿伤 + ); + + let 原始基础序列 = 当前线段.基础序列.clone(); + Self::_弹出线段( + 线段序列, + &Rc::clone(线段序列.last().unwrap()), + 配置, + format!("{}, {}", 0, 层级), + ); + + if 线段序列.is_empty() { + return true; + } + + let mut 当前线段_克隆 = (**线段序列.last().unwrap()).clone(); + if 当前线段_克隆.特征序列.len() >= 3 { + 当前线段_克隆.特征序列[2] = None; + } + + let 开始笔 = Rc::clone(当前线段_克隆.基础序列.last().unwrap()); + let 开始序号 = 原始基础序列 + .iter() + .position(|x| Rc::as_ptr(x) == Rc::as_ptr(&开始笔)); + + if let Some(开始序号) = 开始序号 { + for 临时虚线 in &原始基础序列[开始序号 + 1..] { + 当前线段_克隆.基础序列.push(Rc::clone(临时虚线)); + } + } + + let idx = 线段序列.len() - 1; + 线段序列[idx] = Rc::new(当前线段_克隆); + + Self::刷新(&mut 线段序列[idx], 配置); + + let 当前线段 = Rc::clone(&线段序列[idx]); + if 当前线段.特征序列.len() >= 3 && 当前线段.特征序列[2].is_some() { + let 段 = 虚线::创建线段(&[ + Rc::clone(&基础序列[基础序列.len() - 3]), + Rc::clone(&基础序列[基础序列.len() - 2]), + Rc::clone(&基础序列[基础序列.len() - 1]), + ]); + let mut 段_rc = Rc::new(段); + Self::_添加线段(线段序列, &mut 段_rc, 配置, format!("{}, {}", 0, 层级)); + + // Set feature sequence [0] + let 新段 = Self::取段可变(线段序列.last_mut().unwrap()); + if 新段.特征序列.is_empty() { + 新段.特征序列.push(None); + } + let 中笔 = Rc::clone(&基础序列[基础序列.len() - 2]); + 新段.特征序列[0] = Some(Rc::new(线段特征::新建( + vec![中笔], + 新段.方向(), + ))); + } + + true + } + + /// _缺口后紧急修正 — 老阴/老阳后的紧急修正 + pub fn _缺口后紧急修正( + 线段序列: &mut Vec>, + 配置: &缠论配置, + 层级: i64, + ) -> bool { + if 线段序列.is_empty() { + return false; + } + + let 当前线段 = Rc::clone(线段序列.last().unwrap()); + let 四象 = Self::四象(&当前线段); + + // 外层条件 + if !(配置.线段_缺口后紧急修正 + && !配置.线段_特征序列忽视老阴老阳 + && (四象 == "小阳" || 四象 == "少阴") + && 当前线段.特征序列.len() >= 3 + && 当前线段.特征序列[2].is_none()) + { + return false; + } + + // 内层条件 + if 线段序列.len() < 2 { + return false; + } + let 前一线段_idx = 线段序列.len() - 2; + if !["老阴", "老阳"].contains(&Self::四象(&线段序列[前一线段_idx]).as_str()) + { + return false; + } + + let (_, 基础序列, _, _) = Self::分割序列(&当前线段, None); + if 基础序列.len() < 3 { + return false; + } + + let mut 需要修正 = false; + if 当前线段.方向() == 相对方向::向上 { + if 相对方向::分析( + 基础序列[0].高(), + 基础序列[0].低(), + 基础序列[2].高(), + 基础序列[2].低(), + ) == 相对方向::向下 + { + 需要修正 = true; + } + } else { + if 相对方向::分析( + 基础序列[0].高(), + 基础序列[0].低(), + 基础序列[2].高(), + 基础序列[2].低(), + ) == 相对方向::向上 + { + 需要修正 = true; + } + } + + if !需要修正 { + return false; + } + + // 执行修正 — set 短路修正 and create new segment + let idx = 线段序列.len() - 1; + let mut 当前线段_克隆 = (*线段序列[idx]).clone(); + 当前线段_克隆.短路修正 = true; + 线段序列[idx] = Rc::new(当前线段_克隆); + + let 新段 = 虚线::创建线段(&基础序列); + let mut 新段_rc = Rc::new(新段); + Self::_添加线段(线段序列, &mut 新段_rc, 配置, format!("{}, {}", 0, 层级)); + true + } + + /// _修正 — 通用线段修正(后段足够长时拆分) + pub fn _修正( + 线段序列: &mut Vec>, + 配置: &缠论配置, + 层级: i64, + ) -> bool { + if 线段序列.is_empty() { + return false; + } + + let 当前线段 = Rc::clone(线段序列.last().unwrap()); + + // 条件1 + if !(配置.线段_修正 && 当前线段.基础序列.len() >= 9) { + return false; + } + + let (_, 之后基础序列, _, _) = Self::分割序列(&当前线段, None); + + // 条件2 + if 之后基础序列.len() < 6 { + return false; + } + + let 前 = Rc::clone(&之后基础序列[之后基础序列.len() - 3]); + let 后 = Rc::clone(&之后基础序列[之后基础序列.len() - 1]); + + // 条件3 + if 当前线段.方向() != 相对方向::分析(前.高(), 前.低(), 后.高(), 后.低()) { + return false; + } + + // 执行修正 + let idx = 线段序列.len() - 1; + let mut 当前线段_克隆 = (*线段序列[idx]).clone(); + 当前线段_克隆.短路修正 = true; + 线段序列[idx] = Rc::new(当前线段_克隆); + + // 第一个新段 + let 新段1 = 虚线::创建线段(&之后基础序列[..之后基础序列.len() - 3]); + let mut 新段1_rc = Rc::new(新段1); + // Set 短路修正 + Self::取段可变(&mut 新段1_rc).短路修正 = true; + Self::_添加线段(线段序列, &mut 新段1_rc, 配置, format!("{}, {}", 0, 层级)); + + if ["老阴", "老阳"].contains(&Self::四象(&当前线段).as_str()) { + Self::取段可变(线段序列.last_mut().unwrap()).前一缺口 = None; + } + + // 第二个新段 + let start = 之后基础序列.len() - 3; + let 新段2 = 虚线::创建线段(&之后基础序列[start..]); + let mut 新段2_rc = Rc::new(新段2); + Self::_添加线段(线段序列, &mut 新段2_rc, 配置, format!("{}, {}", 0, 层级)); + + true + } + + // ================================================================ + // 核心分析 — 使用显式栈(loop)模拟递归 + // ================================================================ + + /// 分析 — 从笔序列生成线段序列 + /// + /// 使用显式栈(loop + continue)模拟 Python 的递归调用,避免栈溢出。 + pub fn 分析( + 笔序列: &[Rc<虚线>], + 线段序列: &mut Vec>, + 配置: &缠论配置, + 层级: i64, + 关系序列: &[相对方向], + ) { + let mut 当前层级 = 层级; + + loop { + if 当前层级 > 256 { + eprintln!("线段.分析 递归深度超出 256"); + return; + } + + if 笔序列.len() < 3 { + return; + } + + // ---- 1. 初始化第一个线段 ---- + if 线段序列.is_empty() { + for i in 1..笔序列.len() - 1 { + let 左 = &笔序列[i - 1]; + let 中 = &笔序列[i]; + let 右 = &笔序列[i + 1]; + + if !Self::基础判断(左, 中, 右, 关系序列) { + continue; + } + let 段 = 虚线::创建线段(&[ + Rc::clone(左), + Rc::clone(中), + Rc::clone(右), + ]); + let mut 段_rc = Rc::new(段); + Self::_添加线段( + 线段序列, + &mut 段_rc, + 配置, + format!("{}, {}", 0, 当前层级), + ); + + // 段.特征序列[0] = 线段特征.新建([中], 段.方向) + let 段 = Self::取段可变(线段序列.last_mut().unwrap()); + if 段.特征序列.is_empty() { + 段.特征序列.push(None); + } + 段.特征序列[0] = Some(Rc::new(线段特征::新建( + vec![Rc::clone(中)], + 段.方向(), + ))); + break; + } + if 线段序列.is_empty() { + return; + } + } + + // ---- 2. 清理无效的尾部引用 ---- + while !线段序列.is_empty() + && 线段序列.last().unwrap().前一结束位置.is_some() + { + let 前一结束 = Rc::clone( + 线段序列.last().unwrap().前一结束位置.as_ref().unwrap(), + ); + if !笔序列 + .iter() + .any(|x| Rc::as_ptr(x) == Rc::as_ptr(&前一结束)) + { + let 当前 = Rc::clone(线段序列.last().unwrap()); + Self::_弹出线段( + 线段序列, + &当前, + 配置, + format!("{}, {}", 0, 当前层级), + ); + } else { + break; + } + } + + if 线段序列.is_empty() { + 当前层级 += 1; + continue; + } + + // ---- 3. 确保当前线段有效 ---- + let mut 当前线段_rc = Rc::clone(线段序列.last().unwrap()); + Self::序列重置(&mut 当前线段_rc, 笔序列); + let seg_idx = 线段序列.len() - 1; + 线段序列[seg_idx] = 当前线段_rc; + + if 线段序列.last().unwrap().基础序列.len() < 3 { + let 当前 = Rc::clone(线段序列.last().unwrap()); + Self::_弹出线段(线段序列, &当前, 配置, format!("{}, {}", 0, 当前层级)); + if 线段序列.is_empty() { + 当前层级 += 1; + continue; + } + } + + // ---- 4. 特征序列已完整时的处理 ---- + { + let 当前线段 = Rc::clone(线段序列.last().unwrap()); + if 当前线段.特征序列.len() >= 3 && 当前线段.特征序列[2].is_some() { + let (_, 基础序列, _, _) = Self::分割序列(&当前线段, None); + let 新段 = 虚线::创建线段(&基础序列); + let mut 新段_rc = Rc::new(新段); + Self::_添加线段( + 线段序列, + &mut 新段_rc, + 配置, + format!("{}, {}", 0, 当前层级), + ); + if ["老阴", "老阳"].contains(&Self::四象(&当前线段).as_str()) { + let last_idx = 线段序列.len() - 1; + let mut 克隆 = (**线段序列.last().unwrap()).clone(); + 克隆.前一缺口 = None; + 线段序列[last_idx] = Rc::new(克隆); + } + } + } + + // Refresh current segment + let idx = 线段序列.len() - 1; + Self::刷新(&mut 线段序列[idx], 配置); + + // ---- 5. 调用一次全局修正 ---- + Self::_缺口突破(线段序列, 配置, 当前层级); + Self::_非缺口下穿刺(线段序列, 配置, 当前层级); + Self::_缺口后紧急修正(线段序列, 配置, 当前层级); + Self::_修正(线段序列, 配置, 当前层级); + + // ---- 6. 循环处理后续的笔 ---- + let 当前线段 = Rc::clone(线段序列.last().unwrap()); + if 当前线段.基础序列.is_empty() { + panic!("线段.分析: 基础序列为空"); + } + let 最后笔 = Rc::clone(当前线段.基础序列.last().unwrap()); + let 起始索引 = match 笔序列 + .iter() + .position(|x| Rc::as_ptr(x) == Rc::as_ptr(&最后笔)) + { + Some(idx) => idx + 1, + None => { + // Last笔 not in 笔序列 — restart + 当前层级 += 1; + continue; + } + }; + + let mut 需要递归 = false; + + for i in 起始索引..笔序列.len() { + let 当前虚线 = Rc::clone(&笔序列[i]); + let 当前线段 = Rc::clone(线段序列.last().unwrap()); + let 四象 = Self::四象(&当前线段); + + // 向当前线段添加笔 + let mut 线段_rc = Rc::clone(线段序列.last().unwrap()); + Self::添加虚线(&mut 线段_rc, Rc::clone(&当前虚线)); + let seg_idx = 线段序列.len() - 1; + 线段序列[seg_idx] = 线段_rc; + + // 刷新 + let idx = 线段序列.len() - 1; + Self::刷新(&mut 线段序列[idx], 配置); + + // 依次尝试四种修正 + if Self::_缺口突破(线段序列, 配置, 当前层级) { + continue; + } + if Self::_非缺口下穿刺(线段序列, 配置, 当前层级) { + continue; + } + if Self::_缺口后紧急修正(线段序列, 配置, 当前层级) { + continue; + } + if Self::_修正(线段序列, 配置, 当前层级) { + continue; + } + + // 无修正触发,检查特征序列 + let 当前线段 = Rc::clone(线段序列.last().unwrap()); + if 当前线段.特征序列.len() < 3 || 当前线段.特征序列[2].is_none() { + continue; + } + + // 特征序列[2]存在 → 创建新段 + let (_, 基础序列, _, _) = Self::分割序列(&当前线段, None); + let 新段 = 虚线::创建线段(&基础序列); + let mut 新段_rc = Rc::new(新段); + Self::_添加线段( + 线段序列, + &mut 新段_rc, + 配置, + format!("{}, {}", 0, 当前层级), + ); + + if ["老阴", "老阳"].contains(&四象.as_str()) { + let last_idx = 线段序列.len() - 1; + let mut 克隆 = (**线段序列.last().unwrap()).clone(); + 克隆.前一缺口 = None; + 线段序列[last_idx] = Rc::new(克隆); + } + + // 检查新段与当前虚线的连续性 + let 新段 = Rc::clone(线段序列.last().unwrap()); + if Rc::as_ptr(新段.基础序列.last().unwrap()) != Rc::as_ptr(&当前虚线) { + if !新段.基础序列.last().unwrap().之后是(&当前虚线) { + 需要递归 = true; + break; + } + // 向新段添加当前虚线 + let mut 新段_rc = Rc::clone(线段序列.last().unwrap()); + Self::添加虚线(&mut 新段_rc, Rc::clone(&当前虚线)); + let seg_idx = 线段序列.len() - 1; + 线段序列[seg_idx] = 新段_rc; + } + + let idx = 线段序列.len() - 1; + Self::刷新(&mut 线段序列[idx], 配置); + } + + if 需要递归 { + 当前层级 += 1; + continue; + } + + break; + } + } + + // ================================================================ + // 扩展线段 + // ================================================================ + + /// _添加扩展线段 + pub fn _添加扩展线段( + 线段序列: &mut Vec>, + 待添加线段_rc: &mut Rc<虚线>, + 行号: u32, + ) { + let 待添加线段 = Self::取段可变(待添加线段_rc); + 待添加线段.模式 = "高低".into(); + 待添加线段.标识 = if 待添加线段.基础序列[0].标识 != "笔" { + format!("扩展{}", 待添加线段.标识) + } else { + "扩展线段".into() + }; + + if let Some(前一个) = 线段序列.last() { + if !前一个.之后是(待添加线段) { + panic!( + "线段.向序列中添加 不连续[{}] {:?} {:?}", + 行号, 前一个.武, 待添加线段.文 + ); + } + let 之前线段 = 线段序列.last().unwrap(); + 待添加线段.序号 = 之前线段.序号 + 1; + } + + let 新段 = Rc::new(待添加线段.clone()); + 线段序列.push(新段); + } + + /// _弹出扩展线段 + pub fn _弹出扩展线段( + 线段序列: &mut Vec>, + 待弹出线段: &Rc<虚线>, + _行号: u32, + ) -> Option> { + if 线段序列.is_empty() { + return None; + } + + if Rc::as_ptr(线段序列.last().unwrap()) == Rc::as_ptr(待弹出线段) { + Some(线段序列.pop().unwrap()) + } else { + panic!( + "线段._从序列中删除 弹出数据不在列表中 {:?}", + 待弹出线段 + ); + } + } + + /// 扩展分析 — 将笔视为线段进行同级别分析 + pub fn 扩展分析( + 虚线序列: &[Rc<虚线>], + 线段序列: &mut Vec>, + 配置: &缠论配置, + ) { + if 虚线序列.len() < 3 { + return; + } + + let mut 当前层级 = 0i64; + + loop { + if 当前层级 > 256 { + eprintln!("线段.扩展分析 递归深度超出 256"); + return; + } + + // 初始化第一个扩展线段 + if 线段序列.is_empty() { + for i in 1..虚线序列.len() - 1 { + let 左 = &虚线序列[i - 1]; + let 中 = &虚线序列[i]; + let 右 = &虚线序列[i + 1]; + let 关系 = 相对方向::分析(左.高(), 左.低(), 右.高(), 右.低()); + if !matches!( + 关系, + 相对方向::向下 + | 相对方向::向上 + | 相对方向::顺 + | 相对方向::逆 + | 相对方向::同 + ) { + continue; + } + + let 段 = 虚线::创建线段(&[ + Rc::clone(左), + Rc::clone(中), + Rc::clone(右), + ]); + let mut 段_rc = Rc::new(段); + Self::_添加扩展线段(线段序列, &mut 段_rc, 0); + break; + } + + if 线段序列.is_empty() { + return; + } + } + + // 验证当前线段 + let mut 当前线段_rc = Rc::clone(线段序列.last().unwrap()); + Self::验证序列(&mut 当前线段_rc, 虚线序列); + let seg_idx = 线段序列.len() - 1; + 线段序列[seg_idx] = 当前线段_rc; + + if 线段序列.last().unwrap().基础序列.len() < 3 { + let 当前 = Rc::clone(线段序列.last().unwrap()); + Self::_弹出扩展线段(线段序列, &当前, 0); + 当前层级 += 1; + continue; + } + + if !配置.扩展线段_当下分析 { + let 当前线段 = Rc::clone(线段序列.last().unwrap()); + let 左 = Rc::clone(&当前线段.基础序列[0]); + let 右 = Rc::clone(&当前线段.基础序列[2]); + + if !相对方向::分析(左.高(), 左.低(), 右.高(), 右.低()).是否缺口() { + let mut 克隆 = (**线段序列.last().unwrap()).clone(); + 克隆.基础序列 = 克隆.基础序列[..3].to_vec(); + let mut 段_rc = Rc::new(克隆); + Self::武终(&mut 段_rc, 0); + let seg_idx = 线段序列.len() - 1; + 线段序列[seg_idx] = 段_rc; + } else { + let 当前 = Rc::clone(线段序列.last().unwrap()); + Self::_弹出扩展线段(线段序列, &当前, 0); + 当前层级 += 1; + continue; + } + } + + // 武终 + let idx = 线段序列.len() - 1; + Self::武终(&mut 线段序列[idx], 0); + + let 当前线段 = Rc::clone(线段序列.last().unwrap()); + if 当前线段.基础序列.last().unwrap().序号 + 3 > 虚线序列.last().unwrap().序号 { + return; + } + + let 最后笔 = Rc::clone(当前线段.基础序列.last().unwrap()); + let 序号 = match 虚线序列 + .iter() + .position(|x| Rc::as_ptr(x) == Rc::as_ptr(&最后笔)) + { + Some(idx) => idx + 1, + None => return, + }; + + if 序号 >= 虚线序列.len() { + return; + } + + let mut 需要递归 = false; + + for i in 序号 + 1..虚线序列.len() - 1 { + let 左 = &虚线序列[i - 1]; + let 中 = &虚线序列[i]; + let 右 = &虚线序列[i + 1]; + let 相对关系 = 相对方向::分析(左.高(), 左.低(), 右.高(), 右.低()); + + if 相对关系.是否缺口() { + let mut 段_rc = Rc::clone(线段序列.last().unwrap()); + Self::添加虚线(&mut 段_rc, Rc::clone(左)); + let seg_idx = 线段序列.len() - 1; + 线段序列[seg_idx] = 段_rc; + + let mut 段_rc = Rc::clone(线段序列.last().unwrap()); + Self::添加虚线(&mut 段_rc, Rc::clone(中)); + let seg_idx = 线段序列.len() - 1; + 线段序列[seg_idx] = 段_rc; + + let seg_idx = 线段序列.len() - 1; + Self::武终(&mut 线段序列[seg_idx], 0); + continue; + } + + if 线段序列 + .last() + .unwrap() + .基础序列 + .iter() + .any(|x| Rc::as_ptr(x) == Rc::as_ptr(左)) + { + continue; + } + + let 段 = 虚线::创建线段(&[ + Rc::clone(左), + Rc::clone(中), + Rc::clone(右), + ]); + let mut 段_rc = Rc::new(段); + Self::_添加扩展线段(线段序列, &mut 段_rc, 0); + 需要递归 = true; + break; + } + + if 需要递归 { + 当前层级 += 1; + continue; + } + + break; + } + } + + // ================================================================ + // 背驰相关 + // ================================================================ + + /// 判断线段内部是否背驰 + /// + /// 分析线段的内部中枢和MACD柱分段,判断是否发生内部背驰 + pub fn 判断线段内部是否背驰(当前段: &虚线, 观察员: &观察者) -> bool { + let _虚 = &当前段.虚_中枢序列; + let 实 = &当前段.实_中枢序列; + let 阳: Vec<&Rc<虚线>> = 当前段 + .基础序列 + .iter() + .filter(|b| b.方向() == 相对方向::向上) + .collect(); + let 阴: Vec<&Rc<虚线>> = 当前段 + .基础序列 + .iter() + .filter(|b| b.方向() == 相对方向::向下) + .collect(); + + if !阴.is_empty() { + // 阴不为空表示特征序列仍在合并中,不判断 + } + let 笔之实数 = 阳.len(); + if 笔之实数 < 3 { + return false; + } + + let 进入段 = 阳[阳.len() - 3]; + let 离开段 = 阳[阳.len() - 1]; + if 进入段.序号 >= 离开段.序号 { + return false; + } + let 关系 = 相对方向::分析(进入段.高(), 进入段.低(), 离开段.高(), 离开段.低()); + let mut 背驰 = false; + let mut 盘整背驰 = false; + + if ((进入段.方向().是否向上() && 关系.是否向上()) + || (进入段.方向().是否向下() && 关系.是否向下())) + && crate::algorithm::divergence::背驰分析::背驰模式( + 进入段, + 离开段, + &观察员.普通K线序列, + &观察员.配置, + &观察员.配置.线段内部背驰_模式, + ) + { + let k线序列 = K线::截取rc( + &观察员.普通K线序列, + &阳[阳.len() - 3].文.中.标的K线, + &阳[阳.len() - 1].武.中.标的K线, + ); + if 虚线::计算MACD柱子分段(&k线序列).len() >= 3 { + 盘整背驰 = true; + } + } + + if !实.is_empty() { + let 最后中枢 = &实[实.len() - 1]; + if 最后中枢.基础序列.iter().any(|b| Rc::ptr_eq(b, 阳[阳.len() - 1])) { + // 最后一笔在最后一个中枢内 + if let Some(序号) = 当前段.基础序列.iter().position(|b| { + Rc::ptr_eq(b, &最后中枢.基础序列[0]) + }) { + if 序号 > 0 { + let 进入段 = &当前段.基础序列[序号 - 1]; + let 离开段 = 阳[阳.len() - 1]; + if 进入段.序号 < 离开段.序号 { + if 进入段.方向() != 离开段.方向() { + return crate::algorithm::divergence::背驰分析::测度背驰(进入段, 离开段) + && 虚线::买卖意义(离开段, 观察员).0; + } + let 关系 = 相对方向::分析( + 进入段.高(), + 进入段.低(), + 离开段.高(), + 离开段.低(), + ); + if ((进入段.方向().是否向上() && 关系.是否向上()) + || (进入段.方向().是否向下() && 关系.是否向下())) + && crate::algorithm::divergence::背驰分析::背驰模式( + 进入段, + 离开段, + &观察员.普通K线序列, + &观察员.配置, + &观察员.配置.线段内部背驰_模式, + ) + { + return true; + } + } + } + } + } else if 最后中枢.第三买卖线.is_some() { + // 第三买卖点后盘整背驰 + let 进入段 = 阳[阳.len() - 3]; + let 离开段 = 阳[阳.len() - 1]; + if 进入段.序号 < 离开段.序号 { + if 进入段.方向() != 离开段.方向() { + return crate::algorithm::divergence::背驰分析::测度背驰(进入段, 离开段) + && 虚线::买卖意义(离开段, 观察员).0; + } + let 关系 = + 相对方向::分析(进入段.高(), 进入段.低(), 离开段.高(), 离开段.低()); + if ((进入段.方向().是否向上() && 关系.是否向上()) + || (进入段.方向().是否向下() && 关系.是否向下())) + && crate::algorithm::divergence::背驰分析::背驰模式( + 进入段, + 离开段, + &观察员.普通K线序列, + &观察员.配置, + &观察员.配置.线段内部背驰_模式, + ) + { + return true; + } + } + } + } else { + // 没有中枢 + if 笔之实数 == 3 { + 背驰 = 盘整背驰; + } + } + + 背驰 || 盘整背驰 + } + + /// 段获取所有停顿位置 — 在线段范围内找出所有停顿位置 + pub fn 段获取所有停顿位置(段: &虚线, 观察员: &观察者) -> Vec<虚线> { + let mut 结果 = Vec::new(); + if 段.模式 != "文武" || 段.标识 != "线段" { + return 结果; + } + + let 阳: Vec> = 段 + .基础序列 + .iter() + .filter(|b| b.方向() == 相对方向::向上) + .cloned() + .collect(); + if 阳.len() < 3 { + return 结果; + } + + // Simplified: only handle the basic case (笔停顿 + 线段停顿) + // Full implementation requires complex sequence management + let mut 笔序列: Vec> = Vec::new(); + + for 筆 in &阳 { + if 笔序列.len() >= 2 { + let 筆停顿 = 笔::获取所有停顿位置(筆, 观察员); + let mut 停顿列表: Vec<虚线> = 筆停顿; + 停顿列表.push((**筆).clone()); + + for 停顿 in 停顿列表 { + 笔序列.push(Rc::new(停顿.clone())); + // 线段.分析 with the accumulated 笔序列 + let mut 线段序列: Vec> = Vec::new(); + let 笔序列_slice: Vec> = 笔序列.iter().map(|b| Rc::clone(b)).collect(); + Self::分析( + &笔序列_slice, + &mut 线段序列, + &观察员.配置, + 0, + &[ + 相对方向::向下, + 相对方向::向上, + 相对方向::顺, + 相对方向::逆, + 相对方向::同, + ], + ); + + let 重复 = match (线段序列.last(), 结果.last()) { + (Some(a), Some(b)) => Rc::ptr_eq(&a.武, &b.武), + _ => false, + }; + if !重复 { + if let Some(最后线段) = 线段序列.last() { + let 新段 = 虚线::创建线段(&最后线段.基础序列); + if 新段.方向() == 段.方向() { + 结果.push(新段); + } + } + } + + // If this 停顿 is not the original 筆, pop it + let 停顿_ptr: *const 虚线 = &停顿; + let 筆_ptr: *const 虚线 = 筆.as_ref(); + if 停顿_ptr != 筆_ptr { + 笔序列.pop(); + } + } + } else { + 笔序列.push(Rc::clone(筆)); + } + } + 结果 + } + + /// 是否背驰过 — 判断线段是否在停顿位置出现过背驰 + pub fn 是否背驰过(当前段: &虚线, 观察员: &观察者) -> Vec> { + let 停顿位置 = Self::段获取所有停顿位置(当前段, 观察员); + let mut 结果 = Vec::new(); + + for 段 in 停顿位置 { + let mut 段_rc = Rc::new(段); + Self::获取内部中枢序列(&mut 段_rc, &观察员.配置); + if Self::判断线段内部是否背驰(&段_rc, 观察员) { + 结果.push(Rc::clone(&段_rc.武)); + } + } + + 结果 + } +} diff --git a/chanlun/src/business/bsp.rs b/chanlun/src/business/bsp.rs new file mode 100644 index 0000000..ca11643 --- /dev/null +++ b/chanlun/src/business/bsp.rs @@ -0,0 +1,181 @@ +use crate::kline::bar::K线; +use crate::kline::chan_kline::缠论K线; +use crate::structure::fractal_obj::分型; +use crate::types::bsp_type::买卖点类型; +use crate::types::分型结构; +use std::rc::Rc; + +/// 基础买卖点 — 买卖点的基础数据结构 +#[derive(Debug, Clone)] +pub struct 基础买卖点 { + pub 备注: String, + pub 类型: 买卖点类型, + pub 买卖点分型: Rc<分型>, + pub 买卖点K线: Rc<缠论K线>, + pub 当前K线: Rc, + pub 失效K线: Option>, + pub 终结K线: Option>, + pub 破位值: f64, + pub 结构: Option<分型结构>, +} + +impl 基础买卖点 { + pub fn new( + 类型: 买卖点类型, + 当前K线: Rc, + 买卖点分型: Rc<分型>, + 备注: String, + 中枢破位值: f64, + ) -> Self { + let 买卖点K线 = Rc::clone(&买卖点分型.中); + Self { + 备注, + 类型, + 买卖点分型, + 买卖点K线, + 当前K线, + 失效K线: None, + 终结K线: None, + 破位值: 中枢破位值, + 结构: None, + } + } + + /// 偏移 — 当前K线与买卖点K线的序号差 + pub fn 偏移(&self) -> i64 { + self.当前K线.序号 - self.买卖点K线.序号 + } + + /// 失效偏移 + pub fn 失效偏移(&self) -> i64 { + match &self.失效K线 { + Some(k) => k.序号 - self.买卖点K线.序号, + None => -1, + } + } + + /// 有效性 — 失效K线是否存在 + pub fn 有效性(&self) -> bool { + self.失效K线.is_some() + } + + /// 与MACD柱子匹配 + pub fn 与MACD柱子匹配(&self) -> bool { + self.买卖点K线.与MACD柱子匹配() + } + + /// 与MACD柱子分型匹配 + pub fn 与MACD柱子分型匹配(&self) -> bool { + self.买卖点分型.与MACD柱子分型匹配() + } +} + +impl std::fmt::Display for 基础买卖点 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}<{}, {}, {}>", + self.类型, + self.买卖点K线.as_ref(), + self.偏移(), + self.失效偏移(), + ) + } +} + +/// 买卖点 — 包含一二三类买卖点的工厂方法 +pub struct 买卖点; + +impl 买卖点 { + pub fn 一卖点( + 买卖点分型: Rc<分型>, + 当前K线: Rc, + _标识: &str, + 备注: String, + 中枢破位值: f64, + ) -> 基础买卖点 { + 基础买卖点::new(买卖点类型::一卖, 当前K线, 买卖点分型, 备注, 中枢破位值) + } + + pub fn 一买点( + 买卖点分型: Rc<分型>, + 当前K线: Rc, + _标识: &str, + 备注: String, + 中枢破位值: f64, + ) -> 基础买卖点 { + 基础买卖点::new(买卖点类型::一买, 当前K线, 买卖点分型, 备注, 中枢破位值) + } + + pub fn 二卖点( + 买卖点分型: Rc<分型>, + 当前K线: Rc, + _标识: &str, + 备注: String, + 中枢破位值: f64, + ) -> 基础买卖点 { + 基础买卖点::new(买卖点类型::二卖, 当前K线, 买卖点分型, 备注, 中枢破位值) + } + + pub fn 二买点( + 买卖点分型: Rc<分型>, + 当前K线: Rc, + _标识: &str, + 备注: String, + 中枢破位值: f64, + ) -> 基础买卖点 { + 基础买卖点::new(买卖点类型::二买, 当前K线, 买卖点分型, 备注, 中枢破位值) + } + + pub fn 三卖点( + 买卖点分型: Rc<分型>, + 当前K线: Rc, + _标识: &str, + 备注: String, + 中枢破位值: f64, + ) -> 基础买卖点 { + 基础买卖点::new(买卖点类型::三卖, 当前K线, 买卖点分型, 备注, 中枢破位值) + } + + pub fn 三买点( + 买卖点分型: Rc<分型>, + 当前K线: Rc, + _标识: &str, + 备注: String, + 中枢破位值: f64, + ) -> 基础买卖点 { + 基础买卖点::new(买卖点类型::三买, 当前K线, 买卖点分型, 备注, 中枢破位值) + } + + /// 生成买卖点 — 根据参数自动选择类型 + pub fn 生成买卖点( + 特征: &str, + 序号: &str, + 级别: &str, + 买卖点分型: Rc<分型>, + 当前缠K: Rc<缠论K线>, + ) -> 基础买卖点 { + let 买卖 = if matches!(买卖点分型.结构, 分型结构::底 | 分型结构::下) { + "买" + } else { + "卖" + }; + let 备注 = format!("{}_{}{}{}", 特征, 级别, 序号, 买卖); + let 破位值 = 买卖点分型.分型特征值; + + // 当前K线 — 从缠K获取其标的K线 + let 当前K线 = Rc::clone(&当前缠K.标的K线); + + let 类型 = match (序号, 买卖) { + ("一", "买") => 买卖点类型::一买, + ("一", "卖") => 买卖点类型::一卖, + ("二", "买") => 买卖点类型::二买, + ("二", "卖") => 买卖点类型::二卖, + ("三", "买") => 买卖点类型::三买, + ("三", "卖") => 买卖点类型::三卖, + _ => 买卖点类型::一买, // fallback + }; + + 基础买卖点::new(类型, 当前K线, 买卖点分型, 备注, 破位值) + } +} diff --git a/chanlun/src/business/mod.rs b/chanlun/src/business/mod.rs new file mode 100644 index 0000000..e27f29c --- /dev/null +++ b/chanlun/src/business/mod.rs @@ -0,0 +1,4 @@ +pub mod bsp; +pub mod multi_frame; +pub mod observer; +pub mod synthesizer; diff --git a/chanlun/src/business/multi_frame.rs b/chanlun/src/business/multi_frame.rs new file mode 100644 index 0000000..a2a0e75 --- /dev/null +++ b/chanlun/src/business/multi_frame.rs @@ -0,0 +1,148 @@ +use crate::business::observer::观察者; +use crate::business::synthesizer::K线合成器; +use crate::config::缠论配置; +use crate::kline::bar::K线; +use std::collections::HashMap; + +/// 立体分析器 — 多周期协调器 +/// +/// 包含一个K线合成器和每周期一个观察者。 +/// 输入最小周期K线,合成大周期后分发到对应观察者。 +pub struct 立体分析器 { + pub 周期组: Vec, + 输入周期: i64, + 显示周期: i64, + K线合成器: K线合成器, + 单体分析器: HashMap, + 合成K线计数: HashMap, +} + +impl 立体分析器 { + pub fn new( + 符号: String, + 周期组: Vec, + 配置: Option<缠论配置>, + 配置组: Option>, + ) -> Self { + let mut 周期组 = 周期组; + 周期组.sort(); + let 输入周期 = 周期组[0]; + let 显示周期 = if 周期组.len() > 1 { + 周期组[1] + } else { + 周期组[0] + }; + + let 默认配置 = 配置.unwrap_or_default(); + let 配置组 = 配置组.unwrap_or_default(); + + let K线合成器 = K线合成器::new(符号.clone(), 周期组.clone()); + + let mut 单体分析器 = HashMap::new(); + for &周期 in &周期组 { + let mut 当前配置 = 配置组.get(&周期).cloned().unwrap_or_else(|| 默认配置.clone()); + 当前配置.推送K线 = false; + 当前配置.推送线段 = false; + // Set 标识 to match 符号 + 当前配置.标识 = 符号.clone(); + + let 观察员 = 观察者::new(符号.clone(), 周期, 当前配置); + 单体分析器.insert(周期, 观察员); + } + + // Configure display period + if let Some(显示观察员) = 单体分析器.get_mut(&显示周期) { + 显示观察员.配置.推送K线 = true; + 显示观察员.配置.推送笔 = true; + 显示观察员.配置.推送线段 = true; + 显示观察员.配置.图表展示 = true; + 显示观察员.重置基础序列(); + } + + // Align other periods to display period's 缠K序列 + // (in practice, this is done during data loading) + for &周期 in &周期组 { + if 周期 != 显示周期 { + // Other periods will reference display period's 缠论K线序列 + // This is done during the K-line callback flow + } + } + + let mut 合成K线计数 = HashMap::new(); + for &周期 in &周期组 { + 合成K线计数.insert(周期, 0); + } + + Self { + 周期组, + 输入周期, + 显示周期, + K线合成器, + 单体分析器, + 合成K线计数, + } + } + + /// 投喂K线 — 统一入口,接收最小周期K线 + pub fn 投喂K线(&mut self, 普K: K线) { + if 普K.周期 != self.输入周期 { + eprintln!( + "立体分析器.投喂K线 周期不匹配 {} != {}", + 普K.周期, self.输入周期 + ); + return; + } + + // Record current K-line counts before feeding + let mut 之前计数 = HashMap::new(); + for &周期 in &self.周期组 { + 之前计数.insert(周期, self.K线合成器.合成K线列表.get(&周期).map(|v| v.len()).unwrap_or(0)); + } + + // Feed to synthesizer + self.K线合成器.投喂K线(普K); + + // Dispatch new K-lines to observers + for &周期 in &self.周期组 { + let 新K线列表 = self.K线合成器.合成K线列表.get(&周期); + let 之前计数 = 之前计数[&周期]; + if let Some(列表) = 新K线列表 { + for k线 in 列表.iter().skip(之前计数) { + // Clone the K-line for feeding to observer + let 完成K线 = k线.clone(); + if let Some(观察员) = self.单体分析器.get_mut(&周期) { + 观察员.增加原始K线(完成K线); + } + } + } + + // Also feed the current in-progress K-line (if any) + if let Some(Some(当前K线)) = self.K线合成器.当前K线.get(&周期).cloned() { + if let Some(观察员) = self.单体分析器.get_mut(&周期) { + // Feed a clone of current in-progress K-line for real-time updates + // Note: this may cause duplicate data, matching Python behavior + 观察员.增加原始K线(当前K线); + } + } + } + } + + /// 获取指定周期的观察者 + pub fn 获取观察者(&self, 周期: i64) -> Option<&观察者> { + self.单体分析器.get(&周期) + } + + /// 获取指定周期的观察者(可变) + pub fn 获取观察者_mut(&mut self, 周期: i64) -> Option<&mut 观察者> { + self.单体分析器.get_mut(&周期) + } + + /// 测试_保存数据 + pub fn 测试_保存数据(&self) { + for 周期 in &self.周期组 { + if let Some(观察员) = self.单体分析器.get(周期) { + 观察员.测试_保存数据(None); + } + } + } +} diff --git a/chanlun/src/business/observer.rs b/chanlun/src/business/observer.rs new file mode 100644 index 0000000..c21d95c --- /dev/null +++ b/chanlun/src/business/observer.rs @@ -0,0 +1,503 @@ +use crate::algorithm::bi::笔; +use crate::algorithm::hub::中枢; +use crate::algorithm::segment::线段; +use crate::config::缠论配置; +use crate::kline::bar::K线; +use crate::kline::chan_kline::缠论K线; +use crate::structure::dash_line::虚线; +use crate::structure::fractal_obj::分型; +use crate::types::相对方向; +use crate::utils::datetime; +use std::rc::Rc; + +/// 观察者 — 单周期分析器,持有所有层级序列,接收K线流式输入后逐层计算 +pub struct 观察者 { + pub 符号: String, + pub 周期: i64, + pub 配置: 缠论配置, + + // K线序列 + pub 普通K线序列: Vec>, + pub 缠论K线序列: Vec>, + + // 分型与笔 + pub 分型序列: Vec>, + pub 笔序列: Vec>, + pub 笔_中枢序列: Vec>, + + // 线段 + pub 线段序列: Vec>, + pub 中枢序列: Vec>, + + // 扩展线段(笔级) + pub 扩展线段序列: Vec>, + pub 扩展中枢序列: Vec>, + + // 扩展线段(线段级) + pub 扩展线段序列_线段: Vec>, + pub 扩展中枢序列_线段: Vec>, + + // 线段之线段 + pub 线段_线段序列: Vec>, + pub 线段_中枢序列: Vec>, + + // 扩展线段之扩展线段 + pub 扩展线段序列_扩展线段: Vec>, + pub 扩展中枢序列_扩展线段: Vec>, + + // 终止时间戳 + 终止时间戳: Option, +} + +impl 观察者 { + pub fn new(符号: String, 周期: i64, 配置: 缠论配置) -> Self { + let 终止时间戳 = if 配置.手动终止 != "1970-01-01 00:00:00" + && !配置.手动终止.is_empty() + { + datetime::转化为时间戳(&配置.手动终止) + } else { + None + }; + + let mut instance = Self { + 符号: 符号.clone(), + 周期, + 配置, + 普通K线序列: Vec::new(), + 缠论K线序列: Vec::new(), + 分型序列: Vec::new(), + 笔序列: Vec::new(), + 笔_中枢序列: Vec::new(), + 线段序列: Vec::new(), + 中枢序列: Vec::new(), + 扩展线段序列: Vec::new(), + 扩展中枢序列: Vec::new(), + 扩展线段序列_线段: Vec::new(), + 扩展中枢序列_线段: Vec::new(), + 线段_线段序列: Vec::new(), + 线段_中枢序列: Vec::new(), + 扩展线段序列_扩展线段: Vec::new(), + 扩展中枢序列_扩展线段: Vec::new(), + 终止时间戳, + }; + instance.配置.标识 = 符号; + instance + } + + /// 标识 + pub fn 标识(&self) -> String { + format!("{}:{}", self.符号, self.周期) + } + + /// 当前K线 + pub fn 当前K线(&self) -> Option<&Rc> { + self.普通K线序列.last() + } + + /// 当前缠K + pub fn 当前缠K(&self) -> Option<&Rc<缠论K线>> { + self.缠论K线序列.last() + } + + /// 重置基础序列 + pub fn 重置基础序列(&mut self) { + self.普通K线序列.clear(); + self.缠论K线序列.clear(); + self.分型序列.clear(); + self.笔序列.clear(); + self.笔_中枢序列.clear(); + self.线段序列.clear(); + self.中枢序列.clear(); + self.扩展线段序列.clear(); + self.扩展中枢序列.clear(); + self.扩展线段序列_线段.clear(); + self.扩展中枢序列_线段.clear(); + self.线段_线段序列.clear(); + self.线段_中枢序列.clear(); + self.扩展线段序列_扩展线段.clear(); + self.扩展中枢序列_扩展线段.clear(); + } + + /// 增加原始K线 — 单根K线投喂入口 + pub fn 增加原始K线(&mut self, 普K: K线) { + if let Some(终止) = self.终止时间戳 { + if 普K.时间戳 > 终止 { + return; + } + } + self.__处理数据(普K); + } + + /// 核心数据处理管道 + fn __处理数据(&mut self, mut 普K: K线) { + // Step 1: 缠论K线分析 (普K is consumed by 分析 as &mut) + let (_, 当前分型) = 缠论K线::分析( + &mut 普K, + &mut self.缠论K线序列, + &mut self.普通K线序列, + &self.配置, + ); + let 当前分型 = match 当前分型 { + Some(fx) => fx, + None => return, + }; + + // Step 2: 笔分析 + if self.配置.分析笔 { + 笔::分析( + 当前分型, + &mut self.分型序列, + &mut self.笔序列, + &self.缠论K线序列, + &self.普通K线序列, + &self.配置, + ); + } + if self.分型序列.is_empty() { + return; + } + + // Step 3: 笔中枢分析 + if self.配置.分析笔中枢 { + 中枢::分析(&self.笔序列, &mut self.笔_中枢序列, true, "", 0); + } + if self.笔序列.is_empty() { + return; + } + + // Step 4: 线段分析 + if self.配置.分析线段 { + 线段::分析( + &self.笔序列, + &mut self.线段序列, + &self.配置, + 0, + &[相对方向::向上, 相对方向::向下], + ); + } + // Clone to avoid borrow issues + let 线段序列_克隆 = self.线段序列.clone(); + if self.配置.分析线段中枢 { + 中枢::分析(&线段序列_克隆, &mut self.中枢序列, true, "", 0); + } + + // Step 5: 扩展线段(笔级) + let 笔序列_克隆 = self.笔序列.clone(); + if self.配置.分析扩展线段 { + 线段::扩展分析(&笔序列_克隆, &mut self.扩展线段序列, &self.配置); + } + let 扩展线段序列_克隆 = self.扩展线段序列.clone(); + if self.配置.分析线段中枢 { + 中枢::分析(&扩展线段序列_克隆, &mut self.扩展中枢序列, true, "", 0); + } + + // Step 6: 扩展线段(线段级) + let 线段序列_克隆2 = self.线段序列.clone(); + if self.配置.分析扩展线段 { + 线段::扩展分析(&线段序列_克隆2, &mut self.扩展线段序列_线段, &self.配置); + } + let 扩展线段_线段_克隆 = self.扩展线段序列_线段.clone(); + if self.配置.分析线段中枢 { + 中枢::分析( + &扩展线段_线段_克隆, + &mut self.扩展中枢序列_线段, + true, + "", + 0, + ); + } + + // Step 7: 线段之线段 + let 线段序列_克隆3 = self.线段序列.clone(); + if self.配置.分析线段 { + 线段::分析( + &线段序列_克隆3, + &mut self.线段_线段序列, + &self.配置, + 0, + &[ + 相对方向::向下, + 相对方向::向上, + 相对方向::顺, + 相对方向::逆, + 相对方向::同, + ], + ); + } + let 线段_线段_克隆 = self.线段_线段序列.clone(); + if self.配置.分析线段中枢 { + 中枢::分析( + &线段_线段_克隆, + &mut self.线段_中枢序列, + true, + "", + 0, + ); + } + + // Step 8: 扩展线段之扩展线段 + let 扩展线段序列_克隆2 = self.扩展线段序列.clone(); + if self.配置.分析扩展线段 { + 线段::扩展分析( + &扩展线段序列_克隆2, + &mut self.扩展线段序列_扩展线段, + &self.配置, + ); + } + let 扩展线段_扩展线段_克隆 = self.扩展线段序列_扩展线段.clone(); + if self.配置.分析线段中枢 { + 中枢::分析( + &扩展线段_扩展线段_克隆, + &mut self.扩展中枢序列_扩展线段, + true, + "", + 0, + ); + } + } + + /// 静态重新分析 — 遍历所有缠K重新生成分型/笔/线段 + pub fn 静态重新分析(&mut self) { + self.分型序列.clear(); + self.笔序列.clear(); + self.笔_中枢序列.clear(); + self.线段序列.clear(); + self.中枢序列.clear(); + self.扩展线段序列.clear(); + self.扩展中枢序列.clear(); + self.扩展线段序列_线段.clear(); + self.扩展中枢序列_线段.clear(); + self.线段_线段序列.clear(); + self.线段_中枢序列.clear(); + self.扩展线段序列_扩展线段.clear(); + self.扩展中枢序列_扩展线段.clear(); + + let 缠K克隆 = self.缠论K线序列.clone(); + let 普K克隆 = self.普通K线序列.clone(); + + for i in 1..缠K克隆.len() - 1 { + let 当前分型 = 分型::new( + Some(Rc::clone(&缠K克隆[i - 1])), + Rc::clone(&缠K克隆[i]), + Some(Rc::clone(&缠K克隆[i + 1])), + ); + 笔::分析( + Rc::new(当前分型), + &mut self.分型序列, + &mut self.笔序列, + &缠K克隆, + &普K克隆, + &self.配置, + ); + } + + if self.配置.分析笔中枢 { + 中枢::分析(&self.笔序列, &mut self.笔_中枢序列, true, "", 0); + } + + let 笔克隆 = self.笔序列.clone(); + if self.配置.分析线段 { + 线段::分析( + &笔克隆, + &mut self.线段序列, + &self.配置, + 0, + &[相对方向::向上, 相对方向::向下], + ); + } + let 线段克隆 = self.线段序列.clone(); + if self.配置.分析线段中枢 { + 中枢::分析(&线段克隆, &mut self.中枢序列, true, "", 0); + } + + let 笔克隆2 = self.笔序列.clone(); + if self.配置.分析扩展线段 { + 线段::扩展分析(&笔克隆2, &mut self.扩展线段序列, &self.配置); + } + let 扩展线段克隆 = self.扩展线段序列.clone(); + if self.配置.分析线段中枢 { + 中枢::分析(&扩展线段克隆, &mut self.扩展中枢序列, true, "", 0); + } + + let 线段克隆2 = self.线段序列.clone(); + if self.配置.分析扩展线段 { + 线段::扩展分析(&线段克隆2, &mut self.扩展线段序列_线段, &self.配置); + } + let 扩展线段_线段_克隆 = self.扩展线段序列_线段.clone(); + if self.配置.分析线段中枢 { + 中枢::分析( + &扩展线段_线段_克隆, + &mut self.扩展中枢序列_线段, + true, + "", + 0, + ); + } + + let 线段克隆3 = self.线段序列.clone(); + if self.配置.分析线段 { + 线段::分析( + &线段克隆3, + &mut self.线段_线段序列, + &self.配置, + 0, + &[ + 相对方向::向下, + 相对方向::向上, + 相对方向::顺, + 相对方向::逆, + 相对方向::同, + ], + ); + } + let 线段_线段_克隆 = self.线段_线段序列.clone(); + if self.配置.分析线段中枢 { + 中枢::分析( + &线段_线段_克隆, + &mut self.线段_中枢序列, + true, + "", + 0, + ); + } + } + + /// 测试_保存数据 — 输出各序列数据文本到文件 + pub fn 测试_保存数据(&self, root: Option<&str>) { + + let 笔序列_文本数据: Vec = self + .笔序列 + .iter() + .map(|b| b.获取数据文本()) + .collect(); + let 线段序列_文本数据: Vec = self + .线段序列 + .iter() + .map(|s| s.获取数据文本()) + .collect(); + let 扩展线段序列_数据文本: Vec = self + .扩展线段序列 + .iter() + .map(|s| s.获取数据文本()) + .collect(); + let 扩展线段序列_线段_数据文本: Vec = self + .扩展线段序列_线段 + .iter() + .map(|s| s.获取数据文本()) + .collect(); + let 线段_线段序列_数据文本: Vec = self + .线段_线段序列 + .iter() + .map(|s| s.获取数据文本()) + .collect(); + let 扩展线段序列_扩展线段_数据文本: Vec = self + .扩展线段序列_扩展线段 + .iter() + .map(|s| s.获取数据文本()) + .collect(); + + let 笔_中枢序列_数据文本: Vec = self + .笔_中枢序列 + .iter() + .map(|h| h.获取数据文本()) + .collect(); + let 中枢序列_数据文本: Vec = self + .中枢序列 + .iter() + .map(|h| h.获取数据文本()) + .collect(); + let 扩展中枢序列_数据文本: Vec = self + .扩展中枢序列 + .iter() + .map(|h| h.获取数据文本()) + .collect(); + let 扩展中枢序列_线段_数据文本: Vec = self + .扩展中枢序列_线段 + .iter() + .map(|h| h.获取数据文本()) + .collect(); + let 线段_中枢序列_数据文本: Vec = self + .线段_中枢序列 + .iter() + .map(|h| h.获取数据文本()) + .collect(); + let 扩展中枢序列_扩展线段_数据文本: Vec = self + .扩展中枢序列_扩展线段 + .iter() + .map(|h| h.获取数据文本()) + .collect(); + + // 确定根目录 + let 根目录 = match root { + Some(r) => std::path::PathBuf::from(r), + None => std::env::current_dir().unwrap_or_default(), + }; + + // 生成子目录名称 + let 起始时间 = self.普通K线序列.first().map(|k| k.时间戳).unwrap_or(0); + let 结束时间 = self.普通K线序列.last().map(|k| k.时间戳).unwrap_or(0); + let 目录标识 = format!("Rust_{}_{}_{}", self.符号, 起始时间, 结束时间); + + let 保存路径 = 根目录.join(&目录标识); + if let Err(e) = std::fs::create_dir_all(&保存路径) { + eprintln!("创建目录失败: {} -> {}", 保存路径.display(), e); + return; + } + + let 数据映射: Vec<(&str, &[String])> = vec![ + ("笔序列_文本数据", &笔序列_文本数据), + ("线段序列_文本数据", &线段序列_文本数据), + ("扩展线段序列_数据文本", &扩展线段序列_数据文本), + ("扩展线段序列_线段_数据文本", &扩展线段序列_线段_数据文本), + ("线段_线段序列_数据文本", &线段_线段序列_数据文本), + ("扩展线段序列_扩展线段_数据文本", &扩展线段序列_扩展线段_数据文本), + ("笔_中枢序列_数据文本", &笔_中枢序列_数据文本), + ("中枢序列_数据文本", &中枢序列_数据文本), + ("扩展中枢序列_数据文本", &扩展中枢序列_数据文本), + ("扩展中枢序列_线段_数据文本", &扩展中枢序列_线段_数据文本), + ("线段_中枢序列_数据文本", &线段_中枢序列_数据文本), + ("扩展中枢序列_扩展线段_数据文本", &扩展中枢序列_扩展线段_数据文本), + ]; + + for (文件名, 数据列表) in &数据映射 { + let 文件路径 = 保存路径.join(format!("{}.txt", 文件名)); + let 内容 = 数据列表.join("\n") + "\n"; + if let Err(e) = std::fs::write(&文件路径, &内容) { + eprintln!("写入文件失败: {} -> {}", 文件路径.display(), e); + } + } + + println!("全部数据拆分保存完成,目录:{}", 保存路径.display()); + } + + /// 读取数据文件 — 从 .nb 文件加载数据 + pub fn 读取数据文件(文件路径: &str, 配置: Option<缠论配置>) -> Result { + let 配置 = 配置.unwrap_or_default(); + + // Parse filename: btcusd-300-1631772074-1632222374.nb + let path = std::path::Path::new(文件路径); + let name = path + .file_stem() + .and_then(|n| n.to_str()) + .ok_or("invalid filename")?; + let parts: Vec<&str> = name.split('-').collect(); + if parts.len() < 4 { + return Err(format!("invalid filename format: {}", name)); + } + let 符号 = parts[0].to_string(); + let 周期: i64 = parts[1].parse().map_err(|e| format!("parse period: {}", e))?; + + let mut 实例 = Self::new(符号, 周期, 配置); + + let data = std::fs::read(文件路径).map_err(|e| format!("read file: {}", e))?; + let size = 48; // 6 × 8 bytes (big-endian double) + for i in 0..data.len() / size { + let offset = i * size; + if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 周期, "nb") { + 实例.增加原始K线(k线); + } + } + + Ok(实例) + } +} diff --git a/chanlun/src/business/synthesizer.rs b/chanlun/src/business/synthesizer.rs new file mode 100644 index 0000000..31a7780 --- /dev/null +++ b/chanlun/src/business/synthesizer.rs @@ -0,0 +1,115 @@ +use crate::kline::bar::K线; +use std::collections::HashMap; + +/// K线合成器 — 将小周期K线合成为大周期K线 +pub struct K线合成器 { + pub 标识: String, + pub 周期组: Vec, + pub 当前K线: HashMap>, + pub 合成K线列表: HashMap>, +} + +impl K线合成器 { + pub fn new(标识: String, 周期组: Vec) -> Self { + let mut 周期组 = 周期组; + 周期组.sort(); + + let mut 当前K线 = HashMap::new(); + let mut 合成K线列表 = HashMap::new(); + for &周期 in &周期组 { + 当前K线.insert(周期, None); + 合成K线列表.insert(周期, Vec::new()); + } + + Self { + 标识, + 周期组, + 当前K线, + 合成K线列表, + } + } + + /// 投喂K线 — 输入最小周期K线,合成为所有目标周期 + pub fn 投喂K线(&mut self, 普K: K线) { + let 周期组 = self.周期组.clone(); + for 周期 in 周期组 { + self._处理单个周期(周期, &普K); + } + } + + fn _处理单个周期(&mut self, 周期: i64, 普K: &K线) { + let 目标时间戳 = self._对齐时间戳(普K.时间戳, 周期); + let 当前K线 = self.当前K线.get(&周期).unwrap().clone(); + + if 当前K线.is_none() { + // 创建新K线 + let 新K线 = self._创建新K线(周期, 目标时间戳, 普K); + *self.当前K线.get_mut(&周期).unwrap() = Some(新K线); + } else if 当前K线.as_ref().unwrap().时间戳 == 目标时间戳 { + // 更新当前K线 + let mut k线 = 当前K线.unwrap(); + self._更新K线(&mut k线, 普K); + *self.当前K线.get_mut(&周期).unwrap() = Some(k线); + } else { + // 完成当前K线,创建新K线 + self._完成K线(周期); + let 新K线 = self._创建新K线(周期, 目标时间戳, 普K); + *self.当前K线.get_mut(&周期).unwrap() = Some(新K线); + } + } + + fn _对齐时间戳(&self, 时间戳: i64, 周期: i64) -> i64 { + if 周期 == 0 { + return 时间戳; + } + (时间戳 / 周期) * 周期 + } + + fn _创建新K线(&self, 周期: i64, 时间戳: i64, 普K: &K线) -> K线 { + let 序号 = self + .合成K线列表 + .get(&周期) + .and_then(|list| list.last()) + .map(|k| k.序号 + 1) + .unwrap_or(0); + + K线::创建普K( + &self.标识, + 时间戳, + 普K.开盘价, + 普K.高, + 普K.低, + 普K.收盘价, + 普K.成交量, + 序号, + 周期, + ) + } + + fn _更新K线(&self, 当前K线: &mut K线, 新数据: &K线) { + 当前K线.高 = 当前K线.高.max(新数据.高); + 当前K线.低 = 当前K线.低.min(新数据.低); + 当前K线.收盘价 = 新数据.收盘价; + 当前K线.成交量 += 新数据.成交量; + } + + fn _完成K线(&mut self, 周期: i64) { + let 当前K线 = self.当前K线.get(&周期).and_then(|k| k.clone()); + if let Some(mut k线) = 当前K线 { + k线.序号 = self + .合成K线列表 + .get(&周期) + .and_then(|list| list.last()) + .map(|k| k.序号 + 1) + .unwrap_or(0); + + self.合成K线列表.get_mut(&周期).unwrap().push(k线); + *self.当前K线.get_mut(&周期).unwrap() = None; + } + } + + /// 获取指定周期当前正在合成的K线 + pub fn 获取当前K线(&self, 周期: i64) -> Option<&K线> { + self.当前K线.get(&周期).and_then(|k| k.as_ref()) + } +} diff --git a/chanlun/src/config.rs b/chanlun/src/config.rs new file mode 100644 index 0000000..5911544 --- /dev/null +++ b/chanlun/src/config.rs @@ -0,0 +1,338 @@ +use serde::{Deserialize, Serialize}; + +fn is_infinite_f64(v: &f64) -> bool { + v.is_infinite() +} + +/// 缠论配置 —— 控制所有分析阶段的行为 +/// +/// 所有字段带默认值,使用 `#[serde(default)]` 实现缺失字段容错 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct 缠论配置 { + // ---- 基础 ---- + pub 标识: String, + + // ---- 缠K ---- + pub 缠K合并替换: bool, + + // ---- 笔 ---- + pub 笔内元素数量: i64, + pub 笔内相同终点取舍: bool, + pub 笔内起始分型包含整笔: bool, + pub 笔内起始分型包含整笔_包括右: bool, + pub 笔内原始K线包含整笔: bool, + pub 笔次级成笔: bool, + pub 笔弱化: bool, + pub 笔弱化_原始数量: i64, + + // ---- 线段 ---- + pub 线段_非缺口下穿刺: bool, + pub 线段_特征序列忽视老阴老阳: bool, + pub 线段_缺口后紧急修正: bool, + pub 线段_修正: bool, + pub 线段内部中枢图显: bool, + pub 扩展线段_当下分析: bool, + + // ---- 分析开关 ---- + pub 分析笔: bool, + pub 分析线段: bool, + pub 分析扩展线段: bool, + pub 分析笔中枢: bool, + pub 分析线段中枢: bool, + + // ---- 终止 ---- + pub 手动终止: String, + + // ---- 指标 ---- + pub 计算指标: bool, + pub 指标计算方式: String, + + // ---- MACD ---- + pub 平滑异同移动平均线_快线周期: i64, + pub 平滑异同移动平均线_慢线周期: i64, + pub 平滑异同移动平均线_信号周期: i64, + + // ---- RSI ---- + pub 相对强弱指数_周期: i64, + pub 相对强弱指数_移动平均线周期: i64, + pub 相对强弱指数_超买阈值: f64, + pub 相对强弱指数_超卖阈值: f64, + + // ---- KDJ ---- + pub 随机指标_RSV周期: i64, + pub 随机指标_K值平滑周期: i64, + pub 随机指标_D值平滑周期: i64, + pub 随机指标_超买阈值: f64, + pub 随机指标_超卖阈值: f64, + + // ---- 推送/显示 ---- + pub 图表展示: bool, + pub 推送K线: bool, + pub 推送笔: bool, + pub 推送线段: bool, + pub 推送中枢: bool, + + // ---- 图表展示细分 ---- + pub 图表展示_笔: bool, + pub 图表展示_线段: bool, + pub 图表展示_扩展线段: bool, + pub 图表展示_扩展线段_线段: bool, + pub 图表展示_线段_线段: bool, + pub 图表展示_中枢_笔: bool, + pub 图表展示_中枢_线段: bool, + pub 图表展示_中枢_扩展线段: bool, + pub 图表展示_中枢_扩展线段_线段: bool, + pub 图表展示_中枢_线段_线段: bool, + pub 图表展示_中枢_线段内部: bool, + + // ---- 买卖点 ---- + pub 买卖点偏移: i64, + pub 买卖点激进识别: bool, + pub 买卖点与MACD柱强相关: bool, + pub 买卖点错过误差值: f64, + pub 买卖点_指标模式: String, + pub 买卖点_指标匹配_MACD: bool, + pub 买卖点_指标匹配_KDJ: bool, + pub 买卖点_指标匹配_RSI: bool, + #[serde(skip_serializing_if = "is_infinite_f64")] + pub 买卖点_背离率: f64, + pub 买卖点_T2_回调阈值: f64, + pub 买卖点_T2S_最大层级: i64, + pub 买卖点_峰值条件: bool, + pub 买卖点_计算方式: String, + pub 买卖点_计算线段BSP1: bool, + pub 买卖点_处理BSP2: bool, + pub 买卖点_计算线段BSP3: bool, + pub 买卖点_依赖T1: bool, + pub 买卖点_中枢来源: String, + pub 买卖点_调试输出: bool, + + // ---- 背驰 ---- + pub 线段内部背驰_MACD: bool, + pub 线段内部背驰_斜率: bool, + pub 线段内部背驰_测度: bool, + pub 线段内部背驰_模式: String, + + // ---- 文件 ---- + pub 加载文件路径: String, +} + +impl Default for 缠论配置 { + fn default() -> Self { + Self { + 标识: "bar".into(), + 缠K合并替换: false, + 笔内元素数量: 5, + 笔内相同终点取舍: false, + 笔内起始分型包含整笔: false, + 笔内起始分型包含整笔_包括右: false, + 笔内原始K线包含整笔: false, + 笔次级成笔: false, + 笔弱化: false, + 笔弱化_原始数量: 3, + 线段_非缺口下穿刺: false, + 线段_特征序列忽视老阴老阳: false, + 线段_缺口后紧急修正: true, + 线段_修正: false, + 线段内部中枢图显: true, + 扩展线段_当下分析: false, + 分析笔: true, + 分析线段: true, + 分析扩展线段: true, + 分析笔中枢: true, + 分析线段中枢: true, + 手动终止: String::new(), + 计算指标: true, + 指标计算方式: "收".into(), + 平滑异同移动平均线_快线周期: 13, + 平滑异同移动平均线_慢线周期: 31, + 平滑异同移动平均线_信号周期: 11, + 相对强弱指数_周期: 13, + 相对强弱指数_移动平均线周期: 13, + 相对强弱指数_超买阈值: 75.0, + 相对强弱指数_超卖阈值: 25.0, + 随机指标_RSV周期: 13, + 随机指标_K值平滑周期: 5, + 随机指标_D值平滑周期: 5, + 随机指标_超买阈值: 80.0, + 随机指标_超卖阈值: 20.0, + 图表展示: true, + 推送K线: true, + 推送笔: true, + 推送线段: true, + 推送中枢: true, + 图表展示_笔: true, + 图表展示_线段: true, + 图表展示_扩展线段: true, + 图表展示_扩展线段_线段: true, + 图表展示_线段_线段: true, + 图表展示_中枢_笔: true, + 图表展示_中枢_线段: true, + 图表展示_中枢_扩展线段: true, + 图表展示_中枢_扩展线段_线段: true, + 图表展示_中枢_线段_线段: true, + 图表展示_中枢_线段内部: true, + 买卖点偏移: 1, + 买卖点激进识别: false, + 买卖点与MACD柱强相关: false, + 买卖点错过误差值: 0.01, + 买卖点_指标模式: "配置".into(), + 买卖点_指标匹配_MACD: true, + 买卖点_指标匹配_KDJ: true, + 买卖点_指标匹配_RSI: true, + 买卖点_背离率: f64::INFINITY, + 买卖点_T2_回调阈值: 1.0, + 买卖点_T2S_最大层级: 3, + 买卖点_峰值条件: false, + 买卖点_计算方式: "峰".into(), + 买卖点_计算线段BSP1: true, + 买卖点_处理BSP2: true, + 买卖点_计算线段BSP3: true, + 买卖点_依赖T1: true, + 买卖点_中枢来源: "合".into(), + 买卖点_调试输出: false, + 线段内部背驰_MACD: true, + 线段内部背驰_斜率: true, + 线段内部背驰_测度: true, + 线段内部背驰_模式: "相对".into(), + 加载文件路径: "./templates/last.nb".into(), + } + } +} + +impl 缠论配置 { + pub fn to_json(&self) -> String { + serde_json::to_string_pretty(self).unwrap_or_default() + } + + pub fn from_json(json_str: &str) -> Result { + serde_json::from_str(json_str) + } + + pub fn 保存配置(&self, path: &str) -> std::io::Result<()> { + std::fs::write(path, self.to_json()) + } + + pub fn 加载配置(path: &str) -> Result> { + let content = std::fs::read_to_string(path)?; + let config = Self::from_json(&content)?; + Ok(config) + } + + /// 返回一个关闭所有推送/显示的新配置 + pub fn 不推送(&self) -> Self { + Self { + 图表展示: false, + 推送K线: false, + 推送笔: false, + 推送线段: false, + 推送中枢: false, + 图表展示_笔: false, + 图表展示_线段: false, + 图表展示_扩展线段: false, + 图表展示_扩展线段_线段: false, + 图表展示_线段_线段: false, + 图表展示_中枢_笔: false, + 图表展示_中枢_线段: false, + 图表展示_中枢_扩展线段: false, + 图表展示_中枢_扩展线段_线段: false, + 图表展示_中枢_线段_线段: false, + 图表展示_中枢_线段内部: false, + ..self.clone() + } + } + + /// 按序号重组字典 — 兼容旧版配置的复合key格式 + pub fn 按序号重组字典(默认配置: &Self, 原始字典: &serde_json::Value) -> Vec<(i64, Self)> { + let mut result = Vec::new(); + if let serde_json::Value::Object(map) = 原始字典 { + // 按数字前缀分组: "1_open" → group 1 key "open" + let mut groups: std::collections::BTreeMap> = + std::collections::BTreeMap::new(); + for (key, value) in map { + if let Some(pos) = key.find('_') { + if let Ok(num) = key[..pos].parse::() { + let field = key[pos + 1..].to_string(); + groups + .entry(num) + .or_default() + .insert(field, value.clone()); + } + } + } + for (num, fields) in groups { + let mut config = 默认配置.clone(); + if let Ok(partial) = serde_json::from_value::<缠论配置>(serde_json::Value::Object(fields)) { + // merge partial into config (override matching fields) + config = partial; + } + result.push((num, config)); + } + } + result + } + + /// 对比两个配置,返回差异字段 + pub fn 对比(&self, other: &Self) -> Vec { + let mut diffs = Vec::new(); + let self_json = serde_json::to_value(self).unwrap(); + let other_json = serde_json::to_value(other).unwrap(); + if let (serde_json::Value::Object(self_map), serde_json::Value::Object(other_map)) = + (&self_json, &other_json) + { + for (key, self_val) in self_map { + if let Some(other_val) = other_map.get(key) { + if self_val != other_val { + diffs.push(key.clone()); + } + } + } + } + diffs + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_config_roundtrip() { + let config = 缠论配置::default(); + let json = config.to_json(); + let parsed: 缠论配置 = 缠论配置::from_json(&json).unwrap(); + let json2 = parsed.to_json(); + assert_eq!(json, json2); + } + + #[test] + fn test_default_values() { + let config = 缠论配置::default(); + assert_eq!(config.标识, "bar"); + assert_eq!(config.笔内元素数量, 5); + assert!(config.买卖点_背离率.is_infinite()); + assert_eq!(config.指标计算方式, "收"); + } + + #[test] + fn test_partial_deserialize() { + let json = r#"{"标识": "custom", "笔内元素数量": 7}"#; + let config: 缠论配置 = serde_json::from_str(json).unwrap(); + assert_eq!(config.标识, "custom"); + assert_eq!(config.笔内元素数量, 7); + // 未指定字段使用默认值 + assert_eq!(config.买卖点偏移, 1); + } + + #[test] + fn test_不推送() { + let config = 缠论配置::default(); + let muted = config.不推送(); + assert!(!muted.推送K线); + assert!(!muted.推送笔); + assert!(!muted.图表展示); + // 其他字段不变 + assert_eq!(muted.笔内元素数量, 5); + } +} diff --git a/chanlun/src/indicators/kdj.rs b/chanlun/src/indicators/kdj.rs new file mode 100644 index 0000000..470a560 --- /dev/null +++ b/chanlun/src/indicators/kdj.rs @@ -0,0 +1,204 @@ +use serde::{Deserialize, Serialize}; + +/// 随机指标 (KDJ) +/// +/// 使用滑动窗口 + 逐值平滑进行增量计算 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct 随机指标 { + pub 时间戳: i64, + pub 最高价: f64, + pub 最低价: f64, + pub 收盘价: f64, + pub N: i64, + pub M1: i64, + pub M2: i64, + pub 超买阈值: f64, + pub 超卖阈值: f64, + pub RSV: Option, + pub K: Option, + pub D: Option, + pub J: Option, + pub 历史最高价队列: Vec, + pub 历史最低价队列: Vec, + pub 前一个RSV: Option, + pub 前一个K: Option, + pub 前一个D: Option, +} + +impl Default for 随机指标 { + fn default() -> Self { + Self { + 时间戳: 0, + 最高价: 0.0, + 最低价: 0.0, + 收盘价: 0.0, + N: 9, + M1: 3, + M2: 3, + 超买阈值: 80.0, + 超卖阈值: 20.0, + RSV: None, + K: None, + D: None, + J: None, + 历史最高价队列: Vec::new(), + 历史最低价队列: Vec::new(), + 前一个RSV: None, + 前一个K: None, + 前一个D: None, + } + } +} + +impl 随机指标 { + /// 首次计算 KDJ(无历史数据时) + pub fn 首次计算( + 初始最高价: f64, + 初始最低价: f64, + 初始收盘价: f64, + 初始时间: i64, + N: i64, + M1: i64, + M2: i64, + 超买阈值: f64, + 超卖阈值: f64, + ) -> Self { + Self { + 时间戳: 初始时间, + 最高价: 初始最高价, + 最低价: 初始最低价, + 收盘价: 初始收盘价, + N, + M1, + M2, + 超买阈值, + 超卖阈值, + RSV: None, + K: None, + D: None, + J: None, + 历史最高价队列: vec![初始最高价], + 历史最低价队列: vec![初始最低价], + 前一个RSV: None, + 前一个K: None, + 前一个D: None, + } + } + + /// 基于前一个 KDJ 增量计算当前 KDJ + pub fn 增量计算(前一个KDJ: &Self, 当前最高价: f64, 当前最低价: f64, 当前收盘价: f64, 当前时间: i64) -> Self { + let N = 前一个KDJ.N; + let M1 = 前一个KDJ.M1; + let M2 = 前一个KDJ.M2; + let 超买阈值 = 前一个KDJ.超买阈值; + let 超卖阈值 = 前一个KDJ.超卖阈值; + + // 更新历史最高价队列 + let mut 历史最高价 = 前一个KDJ.历史最高价队列.clone(); + 历史最高价.push(当前最高价); + if 历史最高价.len() > N as usize { + 历史最高价.remove(0); + } + + // 更新历史最低价队列 + let mut 历史最低价 = 前一个KDJ.历史最低价队列.clone(); + 历史最低价.push(当前最低价); + if 历史最低价.len() > N as usize { + 历史最低价.remove(0); + } + + // RSV + let RSV = if 历史最高价.len() == N as usize && 历史最低价.len() == N as usize { + let highest = 历史最高价.iter().cloned().fold(f64::NEG_INFINITY, f64::max); + let lowest = 历史最低价.iter().cloned().fold(f64::INFINITY, f64::min); + if (highest - lowest).abs() > f64::EPSILON { + Some((当前收盘价 - lowest) / (highest - lowest) * 100.0) + } else { + Some(50.0) + } + } else { + None + }; + + // K值 + let K = match RSV { + Some(rsv) => match 前一个KDJ.K { + None => Some(rsv), + Some(prev_k) => Some((prev_k * (M1 - 1) as f64 + rsv) / M1 as f64), + }, + None => 前一个KDJ.K, + }; + + // D值 + let D = match K { + Some(k) => match 前一个KDJ.D { + None => Some(k), + Some(prev_d) => Some((prev_d * (M2 - 1) as f64 + k) / M2 as f64), + }, + None => 前一个KDJ.D, + }; + + // J值 + let J = match (K, D) { + (Some(k), Some(d)) => Some(3.0 * k - 2.0 * d), + _ => None, + }; + + Self { + 时间戳: 当前时间, + 最高价: 当前最高价, + 最低价: 当前最低价, + 收盘价: 当前收盘价, + N, + M1, + M2, + 超买阈值, + 超卖阈值, + RSV, + K, + D, + J, + 历史最高价队列: 历史最高价, + 历史最低价队列: 历史最低价, + 前一个RSV: RSV, + 前一个K: K, + 前一个D: D, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_first_calc() { + let kdj = 随机指标::首次计算(110.0, 90.0, 100.0, 1000, 9, 3, 3, 80.0, 20.0); + assert_eq!(kdj.历史最高价队列, vec![110.0]); + assert_eq!(kdj.历史最低价队列, vec![90.0]); + assert_eq!(kdj.K, None); + } + + #[test] + fn test_incremental_after_n_bars() { + let mut kdj = 随机指标::首次计算(110.0, 90.0, 100.0, 1000, 5, 3, 3, 80.0, 20.0); + + // 喂入足够数据填充窗口 + let data = [ + (112.0, 91.0, 105.0), + (115.0, 93.0, 110.0), + (113.0, 95.0, 108.0), + (116.0, 98.0, 112.0), + (118.0, 100.0, 115.0), + ]; + for (i, (高, 低, 收)) in data.iter().enumerate() { + kdj = 随机指标::增量计算(&kdj, *高, *低, *收, 1001 + i as i64); + } + + // 窗口填满后 KDJ 应有值 + assert!(kdj.K.is_some()); + assert!(kdj.D.is_some()); + assert!(kdj.J.is_some()); + } +} diff --git a/chanlun/src/indicators/macd.rs b/chanlun/src/indicators/macd.rs new file mode 100644 index 0000000..0883b8c --- /dev/null +++ b/chanlun/src/indicators/macd.rs @@ -0,0 +1,149 @@ +use serde::{Deserialize, Serialize}; + +/// 平滑异同移动平均线 (MACD) +/// +/// 使用 EMA 递推算法进行增量计算 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct 平滑异同移动平均线 { + pub 时间戳: i64, + pub 收盘价: f64, + pub 快线周期: i64, + pub 慢线周期: i64, + pub 信号周期: i64, + pub DIF: Option, + pub DEA: Option, + #[serde(rename = "MACD柱")] + #[serde(default)] + pub MACD柱: f64, + pub 快线EMA: Option, + pub 慢线EMA: Option, + pub DEA_EMA: Option, +} + +impl Default for 平滑异同移动平均线 { + fn default() -> Self { + Self { + 时间戳: 0, + 收盘价: 0.0, + 快线周期: 12, + 慢线周期: 26, + 信号周期: 9, + DIF: None, + DEA: None, + MACD柱: 0.0, + 快线EMA: None, + 慢线EMA: None, + DEA_EMA: None, + } + } +} + +fn 平滑系数(周期: i64) -> f64 { + 2.0 / (周期 as f64 + 1.0) +} + +impl 平滑异同移动平均线 { + /// 首次计算 MACD 指标(无历史数据时使用) + pub fn 首次计算(初始收盘价: f64, 初始时间: i64, 快线周期: i64, 慢线周期: i64, 信号周期: i64) -> Self { + let 快线EMA = 初始收盘价; + let 慢线EMA = 初始收盘价; + let DIF = 快线EMA - 慢线EMA; + let DEA_EMA = DIF; + let MACD柱 = DIF - DEA_EMA; + + Self { + 时间戳: 初始时间, + 收盘价: 初始收盘价, + 快线周期, + 慢线周期, + 信号周期, + DIF: Some(DIF), + DEA: Some(DEA_EMA), + MACD柱, + 快线EMA: Some(快线EMA), + 慢线EMA: Some(慢线EMA), + DEA_EMA: Some(DEA_EMA), + } + } + + /// 基于前一个 MACD 指标增量计算当前 MACD + pub fn 增量计算(前一个MACD: &Self, 当前收盘价: f64, 当前时间: i64) -> Self { + // 快线 EMA + let 快线EMA = match 前一个MACD.快线EMA { + Some(prev) => { + 当前收盘价 * 平滑系数(前一个MACD.快线周期) + + prev * ((前一个MACD.快线周期 - 1) as f64 / (前一个MACD.快线周期 + 1) as f64) + } + None => 当前收盘价, + }; + + // 慢线 EMA + let 慢线EMA = match 前一个MACD.慢线EMA { + Some(prev) => { + 当前收盘价 * 平滑系数(前一个MACD.慢线周期) + + prev * ((前一个MACD.慢线周期 - 1) as f64 / (前一个MACD.慢线周期 + 1) as f64) + } + None => 当前收盘价, + }; + + // DIF + let DIF = 快线EMA - 慢线EMA; + + // DEA_EMA + let DEA_EMA = match 前一个MACD.DEA_EMA { + Some(prev) => { + DIF * 平滑系数(前一个MACD.信号周期) + + prev * ((前一个MACD.信号周期 - 1) as f64 / (前一个MACD.信号周期 + 1) as f64) + } + None => DIF, + }; + + // MACD 柱 (注意: Python 版没有 ×2) + let MACD柱 = DIF - DEA_EMA; + + Self { + 时间戳: 当前时间, + 收盘价: 当前收盘价, + 快线周期: 前一个MACD.快线周期, + 慢线周期: 前一个MACD.慢线周期, + 信号周期: 前一个MACD.信号周期, + DIF: Some(DIF), + DEA: Some(DEA_EMA), + MACD柱, + 快线EMA: Some(快线EMA), + 慢线EMA: Some(慢线EMA), + DEA_EMA: Some(DEA_EMA), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_first_calc() { + let macd = 平滑异同移动平均线::首次计算(100.0, 1000, 12, 26, 9); + assert_eq!(macd.DIF, Some(0.0)); + assert_eq!(macd.MACD柱, 0.0); + assert_eq!(macd.快线EMA, Some(100.0)); + assert_eq!(macd.慢线EMA, Some(100.0)); + } + + #[test] + fn test_incremental_calc() { + let first = 平滑异同移动平均线::首次计算(100.0, 1000, 12, 26, 9); + + // 价格上升 + let second = 平滑异同移动平均线::增量计算(&first, 102.0, 1001); + assert!(second.DIF.unwrap() > 0.0); + // 快线EMA 应该比慢线EMA 变化更快 + assert!(second.快线EMA.unwrap() > second.慢线EMA.unwrap()); + + // 价格下降 + let third = 平滑异同移动平均线::增量计算(&second, 98.0, 1002); + // DIF 应该变小 + assert!(third.DIF.unwrap() < second.DIF.unwrap()); + } +} diff --git a/chanlun/src/indicators/mod.rs b/chanlun/src/indicators/mod.rs new file mode 100644 index 0000000..f1258ab --- /dev/null +++ b/chanlun/src/indicators/mod.rs @@ -0,0 +1,21 @@ +pub mod macd; +pub mod kdj; +pub mod rsi; + +pub use macd::平滑异同移动平均线; +pub use kdj::随机指标; +pub use rsi::相对强弱指数; + +/// K线取值 —— 根据计算方式从K线提取对应的价格 +pub fn K线取值(开盘价: f64, 高: f64, 低: f64, 收盘价: f64, 计算方式: &str) -> f64 { + match 计算方式 { + "开" => 开盘价, + "高" => 高, + "低" => 低, + "收" => 收盘价, + "高低均值" => (高 + 低) / 2.0, + "高低收均值" => (高 + 低 + 收盘价) / 3.0, + "开高低收均值" => (高 + 低 + 开盘价 + 收盘价) / 4.0, + _ => 收盘价, + } +} diff --git a/chanlun/src/indicators/rsi.rs b/chanlun/src/indicators/rsi.rs new file mode 100644 index 0000000..a65210c --- /dev/null +++ b/chanlun/src/indicators/rsi.rs @@ -0,0 +1,180 @@ +use serde::{Deserialize, Serialize}; + +/// 相对强弱指数 (RSI) +/// +/// 使用 Wilder 平滑(RMA)进行增量计算 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct 相对强弱指数 { + pub 时间戳: i64, + pub 收盘价: f64, + pub 周期: i64, + pub 超买阈值: f64, + pub 超卖阈值: f64, + pub RSI_SMA周期: Option, + pub RSI: Option, + pub 平均上涨: Option, + pub 平均下跌: Option, + pub 上涨幅度: f64, + pub 下跌幅度: f64, + pub 平滑系数: f64, + pub RSI_SMA: Option, + pub RSI历史队列: Vec, +} + +impl Default for 相对强弱指数 { + fn default() -> Self { + Self { + 时间戳: 0, + 收盘价: 0.0, + 周期: 14, + 超买阈值: 70.0, + 超卖阈值: 30.0, + RSI_SMA周期: None, + RSI: None, + 平均上涨: None, + 平均下跌: None, + 上涨幅度: 0.0, + 下跌幅度: 0.0, + 平滑系数: 0.0, + RSI_SMA: None, + RSI历史队列: Vec::new(), + } + } +} + +impl 相对强弱指数 { + /// 首次计算 RSI(历史数据不足时) + pub fn 首次计算( + 初始收盘价: f64, + 初始时间: i64, + 周期: i64, + 超买阈值: f64, + 超卖阈值: f64, + RSI_SMA周期: Option, + ) -> Self { + Self { + 时间戳: 初始时间, + 收盘价: 初始收盘价, + 周期, + 超买阈值, + 超卖阈值, + RSI_SMA周期, + RSI: None, + 平均上涨: Some(0.0), + 平均下跌: Some(0.0), + 上涨幅度: 0.0, + 下跌幅度: 0.0, + 平滑系数: 1.0 / 周期 as f64, + RSI_SMA: None, + RSI历史队列: Vec::new(), + } + } + + /// 基于前一个 RSI 增量计算当前 RSI + pub fn 增量计算(前一个RSI: &Self, 当前收盘价: f64, 当前时间: i64) -> Self { + let 周期 = 前一个RSI.周期; + let 超买阈值 = 前一个RSI.超买阈值; + let 超卖阈值 = 前一个RSI.超卖阈值; + let RSI_SMA周期 = 前一个RSI.RSI_SMA周期; + let 平滑系数 = 1.0 / 周期 as f64; + + // 价格变化 + let 变化 = 当前收盘价 - 前一个RSI.收盘价; + let 上涨 = 变化.max(0.0); + let 下跌 = (-变化).max(0.0); + + // Wilder 平滑 + let (平均上涨, 平均下跌) = match (前一个RSI.平均上涨, 前一个RSI.平均下跌) { + (Some(prev_up), Some(prev_down)) => { + let avg_up = prev_up * (1.0 - 平滑系数) + 上涨 * 平滑系数; + let avg_down = prev_down * (1.0 - 平滑系数) + 下跌 * 平滑系数; + (avg_up, avg_down) + } + _ => (上涨, 下跌), + }; + + // RSI + let RSI = if 平均下跌 == 0.0 { + if 平均上涨 > 0.0 { + 100.0 + } else { + 50.0 + } + } else { + let RS = 平均上涨 / 平均下跌; + 100.0 - (100.0 / (1.0 + RS)) + }; + + // RSI_SMA + let (RSI_SMA, RSI历史队列) = match RSI_SMA周期 { + Some(sma周期) if sma周期 > 0 => { + let mut 队列 = 前一个RSI.RSI历史队列.clone(); + 队列.push(RSI); + if 队列.len() > sma周期 as usize { + 队列.remove(0); + } + let sma = if 队列.is_empty() { + None + } else { + Some(队列.iter().sum::() / 队列.len() as f64) + }; + (sma, 队列) + } + _ => (None, Vec::new()), + }; + + Self { + 时间戳: 当前时间, + 收盘价: 当前收盘价, + 周期, + 超买阈值, + 超卖阈值, + RSI_SMA周期, + RSI: Some(RSI), + 平均上涨: Some(平均上涨), + 平均下跌: Some(平均下跌), + 上涨幅度: 上涨, + 下跌幅度: 下跌, + 平滑系数, + RSI_SMA, + RSI历史队列, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_first_calc() { + let rsi = 相对强弱指数::首次计算(100.0, 1000, 14, 70.0, 30.0, None); + assert_eq!(rsi.RSI, None); + assert_eq!(rsi.平滑系数, 1.0 / 14.0); + } + + #[test] + fn test_incremental_calc() { + let first = 相对强弱指数::首次计算(100.0, 1000, 14, 70.0, 30.0, None); + let second = 相对强弱指数::增量计算(&first, 102.0, 1001); + // 价格上涨 → RSI > 50 + assert!(second.RSI.unwrap() > 50.0); + + let third = 相对强弱指数::增量计算(&second, 98.0, 1002); + // 价格低于之前 → RSI 下降 + assert!(third.RSI.unwrap() < second.RSI.unwrap()); + } + + #[test] + fn test_rsi_sma() { + let mut rsi = 相对强弱指数::首次计算(100.0, 1000, 14, 70.0, 30.0, Some(5)); + // 喂入多根K线来积累RSI历史队列 + let prices = [102.0, 103.0, 101.0, 104.0, 105.0, 103.0, 106.0]; + for (i, price) in prices.iter().enumerate() { + rsi = 相对强弱指数::增量计算(&rsi, *price, 1001 + i as i64); + } + // SMA 应该已被计算(队列够长) + assert!(rsi.RSI_SMA.is_some()); + } +} diff --git a/chanlun/src/kline/bar.rs b/chanlun/src/kline/bar.rs new file mode 100644 index 0000000..542383b --- /dev/null +++ b/chanlun/src/kline/bar.rs @@ -0,0 +1,244 @@ +use crate::indicators::{平滑异同移动平均线, 随机指标, 相对强弱指数}; +use crate::types::相对方向; +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::io::Write; +use std::rc::Rc; + +/// 原始K线 (OHLCV + 指标) +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct K线 { + pub 标识: String, + pub 序号: i64, + pub 周期: i64, + pub 时间戳: i64, + pub 高: f64, + pub 低: f64, + pub 开盘价: f64, + pub 收盘价: f64, + pub 成交量: f64, + pub macd: Option<平滑异同移动平均线>, + pub rsi: Option<相对强弱指数>, + pub kdj: Option<随机指标>, +} + +impl Default for K线 { + fn default() -> Self { + Self { + 标识: "bar".into(), + 序号: 0, + 周期: 60, + 时间戳: 0, + 高: 0.0, + 低: 0.0, + 开盘价: 0.0, + 收盘价: 0.0, + 成交量: 0.0, + macd: None, + rsi: None, + kdj: None, + } + } +} + +impl K线 { + /// 方向:阳(收盘 > 开盘)为向上,否则向下 + pub fn 方向(&self) -> 相对方向 { + if self.开盘价 < self.收盘价 { + 相对方向::向上 + } else { + 相对方向::向下 + } + } + + /// 序列化为大端字节序 48 字节 + /// 格式: >6d (时间戳, 开盘价, 高, 低, 收盘价, 成交量) + pub fn to_bytes(&self) -> [u8; 48] { + let mut buf = [0u8; 48]; + { + let mut writer = &mut buf[..]; + writer.write_f64::(self.时间戳 as f64).unwrap(); + writer.write_f64::(self.开盘价).unwrap(); + writer.write_f64::(self.高).unwrap(); + writer.write_f64::(self.低).unwrap(); + writer.write_f64::(self.收盘价).unwrap(); + writer.write_f64::(self.成交量).unwrap(); + } + buf + } + + /// 从大端字节序反序列化 + pub fn from_bytes(字节组: &[u8], 周期: i64, 标识: &str) -> Option { + if 字节组.len() < 48 { + return None; + } + let mut reader = &字节组[..48]; + let 时间戳 = reader.read_f64::().ok()? as i64; + let 开盘价 = reader.read_f64::().ok()?; + let 高 = reader.read_f64::().ok()?; + let 低 = reader.read_f64::().ok()?; + let 收盘价 = reader.read_f64::().ok()?; + let 成交量 = reader.read_f64::().ok()?; + + Some(Self { + 时间戳, + 开盘价, + 高, + 低, + 收盘价, + 成交量, + 周期, + 标识: 标识.to_string(), + 序号: 0, + ..Default::default() + }) + } + + /// 读取 .nb 文件中的所有 K线 + pub fn 读取大端字节数组(字节组: &[u8], 周期: i64, 标识: &str) -> Option { + Self::from_bytes(字节组, 周期, 标识) + } + + /// 创建普通K线 + pub fn 创建普K( + 标识: &str, + 时间戳: i64, + 开盘价: f64, + 最高价: f64, + 最低价: f64, + 收盘价: f64, + 成交量: f64, + 序号: i64, + 周期: i64, + ) -> Self { + Self { + 标识: 标识.to_string(), + 序号, + 周期, + 时间戳, + 高: 最高价, + 低: 最低价, + 开盘价, + 收盘价, + 成交量, + macd: None, + rsi: None, + kdj: None, + } + } + + /// 保存K线序列到 DAT 文件 + pub fn 保存到DAT文件(路径: &str, K线序列: &[&Self]) -> std::io::Result<()> { + let mut f = std::fs::File::create(路径)?; + for k in K线序列 { + f.write_all(&k.to_bytes())?; + } + Ok(()) + } + + /// 获取两K线之间的 MACD 柱面积 + pub fn 获取MACD(K线序列: &[&Self], 始: &Self, 终: &Self) -> HashMap { + let 始_idx = K线序列.iter().position(|k| std::ptr::eq(*k, 始)).unwrap_or(0); + let 终_idx = K线序列.iter().position(|k| std::ptr::eq(*k, 终)).unwrap_or(0); + let 基序 = &K线序列[始_idx..=终_idx]; + + let mut 阳 = 0.0f64; + let mut 阴 = 0.0f64; + for k in 基序 { + if let Some(ref macd) = k.macd { + let hist = macd.MACD柱; + if hist >= 0.0 { + 阳 += hist; + } else { + 阴 += hist; + } + } + } + let 合 = 阳 + 阴; + let mut map = HashMap::new(); + map.insert("阳".into(), 阳); + map.insert("阴".into(), 阴); + map.insert("合".into(), 合); + map.insert("总".into(), 阳 + 阴.abs()); + map + } + + /// 截取K线序列中从始到终的片段 + pub fn 截取<'a>(序列: &'a [Self], 始: &'a Self, 终: &'a Self) -> Option<&'a [Self]> { + let 始_idx = 序列.iter().position(|k| std::ptr::eq(k, 始))?; + let 终_idx = 序列.iter().position(|k| std::ptr::eq(k, 终))?; + Some(&序列[始_idx..=终_idx]) + } + + /// 截取Rc序列中从始到终的片段 + pub fn 截取rc(序列: &[Rc], 始: &Rc, 终: &Rc) -> Vec> { + let 始_ptr = Rc::as_ptr(始); + let 终_ptr = Rc::as_ptr(终); + let 始_idx = 序列.iter().position(|k| Rc::as_ptr(k) == 始_ptr); + let 终_idx = 序列.iter().position(|k| Rc::as_ptr(k) == 终_ptr); + match (始_idx, 终_idx) { + (Some(s), Some(e)) => 序列[s..=e].to_vec(), + _ => Vec::new(), + } + } +} + +impl std::fmt::Display for K线 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use crate::utils::format_f64_g; + write!( + f, + "{}<{}, {}, {}, {}, {}, {}, {}, {}>", + self.标识, + self.序号, + self.周期, + self.方向(), + self.时间戳, + format_f64_g(self.开盘价), + format_f64_g(self.高), + format_f64_g(self.低), + format_f64_g(self.收盘价) + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_方向() { + let 阳 = K线::创建普K("test", 1000, 100.0, 110.0, 95.0, 105.0, 1000.0, 0, 60); + assert_eq!(阳.方向(), 相对方向::向上); + + let 阴 = K线::创建普K("test", 1000, 105.0, 110.0, 95.0, 100.0, 1000.0, 0, 60); + assert_eq!(阴.方向(), 相对方向::向下); + } + + #[test] + fn test_serialization_roundtrip() { + let k = K线::创建普K("test", 1600000000, 100.5, 110.2, 95.3, 105.7, 5000.0, 42, 60); + let bytes = k.to_bytes(); + let restored = K线::from_bytes(&bytes, 60, "test").unwrap(); + + assert_eq!(restored.时间戳, 1600000000); + assert!((restored.开盘价 - 100.5).abs() < 0.01); + assert!((restored.高 - 110.2).abs() < 0.01); + assert!((restored.低 - 95.3).abs() < 0.01); + assert!((restored.收盘价 - 105.7).abs() < 0.01); + assert!((restored.成交量 - 5000.0).abs() < 0.01); + } + + #[test] + fn test_获取MACD_empty() { + let k1 = K线::default(); + let k2 = K线::default(); + let seq = vec![&k1, &k2]; + let result = K线::获取MACD(&seq, &k1, &k2); + assert_eq!(result.get("阳"), Some(&0.0)); + assert_eq!(result.get("阴"), Some(&0.0)); + assert_eq!(result.get("总"), Some(&0.0)); + } +} diff --git a/chanlun/src/kline/chan_kline.rs b/chanlun/src/kline/chan_kline.rs new file mode 100644 index 0000000..03159fd --- /dev/null +++ b/chanlun/src/kline/chan_kline.rs @@ -0,0 +1,601 @@ +use crate::config::缠论配置; +use crate::indicators::{K线取值, 平滑异同移动平均线, 随机指标, 相对强弱指数}; +use crate::kline::bar::K线; +use crate::structure::fractal_obj::分型; +use crate::types::分型结构; +use crate::types::相对方向; +use std::rc::Rc; + +/// 缠论K线 — 经包含处理过后的K线 +#[derive(Debug, Clone)] +pub struct 缠论K线 { + pub 序号: i64, + pub 时间戳: i64, + pub 高: f64, + pub 低: f64, + pub 方向: 相对方向, + pub 分型: Option<分型结构>, + pub 周期: i64, + pub 标识: String, + pub 分型特征值: f64, + pub 原始起始序号: i64, + pub 原始结束序号: i64, + pub 标的K线: Rc, + pub 买卖点信息: Option<()>, // 占位,后续替换为实际类型 +} + +impl std::fmt::Display for 缠论K线 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use crate::utils::format_f64_g; + write!( + f, + "{}<{}, {}, {}, {}, {}, {}, {}>", + self.标识, + self.序号, + self.分型.map_or("None".to_string(), |fx| fx.to_string()), + self.周期, + self.方向, + self.时间戳, + format_f64_g(self.高), + format_f64_g(self.低) + ) + } +} + +impl 缠论K线 { + /// 创建镜像(浅拷贝 Rc 引用) + pub fn 镜像(&self) -> Self { + Self { + 序号: self.序号, + 时间戳: self.时间戳, + 高: self.高, + 低: self.低, + 方向: self.方向, + 分型: self.分型, + 周期: self.周期, + 标识: self.标识.clone(), + 分型特征值: self.分型特征值, + 原始起始序号: self.原始起始序号, + 原始结束序号: self.原始结束序号, + 标的K线: Rc::clone(&self.标的K线), + 买卖点信息: None, + } + } + + /// 与MACD柱子匹配 — 底分型时MACD柱应<0, 顶分型时>0 + pub fn 与MACD柱子匹配(&self) -> bool { + match self.分型 { + Some(分型结构::底) | Some(分型结构::下) => { + if let Some(ref macd) = self.标的K线.macd { + macd.MACD柱 < 0.0 + } else { + false + } + } + Some(分型结构::顶) | Some(分型结构::上) => { + if let Some(ref macd) = self.标的K线.macd { + macd.MACD柱 > 0.0 + } else { + false + } + } + _ => false, + } + } + + /// 与RSI匹配 — 底分型时RSI应低于SMA, 顶分型时高于SMA + pub fn 与RSI匹配(&self) -> bool { + match self.分型 { + Some(分型结构::底) | Some(分型结构::下) => { + if let Some(ref rsi) = self.标的K线.rsi { + match (rsi.RSI, rsi.RSI_SMA) { + (Some(r), Some(sma)) => r < sma, + _ => false, + } + } else { + false + } + } + Some(分型结构::顶) | Some(分型结构::上) => { + if let Some(ref rsi) = self.标的K线.rsi { + match (rsi.RSI, rsi.RSI_SMA) { + (Some(r), Some(sma)) => r > sma, + _ => false, + } + } else { + false + } + } + _ => false, + } + } + + /// 与KDJ匹配 — 底分型时K应低于D(死叉后), 顶分型时K应高于D(金叉后) + pub fn 与KDJ匹配(&self) -> bool { + match self.分型 { + Some(分型结构::底) | Some(分型结构::下) => { + if let Some(ref kdj) = self.标的K线.kdj { + match (kdj.K, kdj.D) { + (Some(k), Some(d)) => k < d, + _ => false, + } + } else { + false + } + } + Some(分型结构::顶) | Some(分型结构::上) => { + if let Some(ref kdj) = self.标的K线.kdj { + match (kdj.K, kdj.D) { + (Some(k), Some(d)) => k > d, + _ => false, + } + } else { + false + } + } + _ => false, + } + } + + /// 时间戳对齐 — 从基线序列中找匹配的时间戳 + pub fn 时间戳对齐(基线: &[Rc<缠论K线>], k线: &缠论K线) -> i64 { + if let Some(基) = 基线.first() { + for k in 基线.iter().rev() { + if 基.周期 < k线.周期 { + if k线.时间戳 <= k.时间戳 && k.时间戳 <= k线.时间戳 + k线.周期 { + if (k线.分型特征值 - k.分型特征值).abs() < f64::EPSILON { + return k.时间戳; + } + } + } else if k.时间戳 <= k线.时间戳 && k线.时间戳 <= k.时间戳 + k.周期 { + if (k线.分型特征值 - k.分型特征值).abs() < f64::EPSILON { + return k.时间戳; + } + } + } + } + k线.时间戳 + } + + /// 创建缠K + pub fn 创建缠K( + 时间戳: i64, + 高: f64, + 低: f64, + 方向: 相对方向, + 结构: Option<分型结构>, + 原始序号: i64, + 普k: Rc, + 之前: Option<&缠论K线>, + ) -> Self { + assert!(高 >= 低, "缠K高必须>=低: 高={高}, 低={低}"); + + let 周期 = 普k.周期; + let 标识 = 普k.标识.clone(); + + let mut 当前 = Self { + 序号: 0, + 时间戳, + 高, + 低, + 方向, + 分型: 结构, + 周期, + 标识, + 分型特征值: 高, + 原始起始序号: 原始序号, + 原始结束序号: 原始序号, + 标的K线: 普k, + 买卖点信息: None, + }; + + if let Some(之前) = 之前 { + 当前.序号 = 之前.序号 + 1; + let 关系 = 相对方向::分析(之前.高, 之前.低, 当前.高, 当前.低); + if 关系.是否包含() { + panic!( + "创建缠K 包含关系: {:?}\n 之前: {}\n 当前: {}", + 关系, 之前, 当前 + ); + } + } + 当前 + } + + /// 兼并(合并)处理 — 缠论包含处理的核心算法 + /// + /// 返回 (新缠K, 模式) — 模式: "添加"/"替换"/None + pub fn 兼并( + 之前缠K: Option<&缠论K线>, + 当前缠K: &缠论K线, + 当前普K: &K线, + 配置: &缠论配置, + ) -> (Option>, Option) { + let 关系 = 相对方向::分析(当前缠K.高, 当前缠K.低, 当前普K.高, 当前普K.低); + + // 无包含关系 — 创建新元素追加 + if !关系.是否包含() { + let 结构 = if 关系.是否向下() { + Some(分型结构::下) + } else { + Some(分型结构::上) + }; + let mut 新缠K = Self::创建缠K( + 当前普K.时间戳, + 当前普K.高, + 当前普K.低, + 当前普K.方向(), + 结构, + 当前普K.序号, + Rc::new(当前普K.clone()), + Some(当前缠K), + ); + 新缠K.序号 = 当前缠K.序号 + 1; + return (Some(Rc::new(新缠K)), Some("添加".into())); + } + + // 包含关系 — 合并到当前缠K + // 取值方向:向下 → 取低低、高高(即全部取最低);向上 → 取高高、低低(即全部取最高) + // 方向由之前缠K与当前缠K的关系决定 + let 取值函数: fn(f64, f64) -> f64 = if let Some(之前) = 之前缠K { + if 相对方向::分析(之前.高, 之前.低, 当前缠K.高, 当前缠K.低).是否向下() { + f64::min + } else { + f64::max + } + } else { + f64::max // 默认向上取max + }; + + let mut 合并后 = 当前缠K.镜像(); + + // 逆序包含时更新时间和标的K线 + if 关系 != 相对方向::顺 { + 合并后.时间戳 = 当前普K.时间戳; + 合并后.标的K线 = Rc::new(当前普K.clone()); + } + 合并后.高 = 取值函数(合并后.高, 当前普K.高); + 合并后.低 = 取值函数(合并后.低, 当前普K.低); + 合并后.原始结束序号 = 当前普K.序号; + 合并后.方向 = 当前普K.方向(); + + if let Some(之前) = 之前缠K { + 合并后.序号 = 之前.序号 + 1; + } + + if 配置.缠K合并替换 { + (Some(Rc::new(合并后)), Some("替换".into())) + } else { + // 兼并:返回新值让调用方决定是原地修改还是替换 + (Some(Rc::new(合并后)), Some("兼并".into())) + } + } + + /// 完整的缠K分析 — 普K → 缠K + 分型 + /// + /// 返回 (状态, 形态) + pub fn 分析( + 当前K线: &mut K线, + 缠K序列: &mut Vec>, + 普K序列: &mut Vec>, + 配置: &缠论配置, + ) -> (String, Option>) { + 当前K线.标识 = 配置.标识.clone(); + + // ---- 阶段1: 普K序列管理 + 指标增量计算 ---- + if 普K序列.is_empty() { + if 配置.计算指标 { + 当前K线.macd = Some(平滑异同移动平均线::首次计算( + K线取值(当前K线.开盘价, 当前K线.高, 当前K线.低, 当前K线.收盘价, &配置.指标计算方式), + 当前K线.时间戳, + 配置.平滑异同移动平均线_快线周期, + 配置.平滑异同移动平均线_慢线周期, + 配置.平滑异同移动平均线_信号周期, + )); + 当前K线.rsi = Some(相对强弱指数::首次计算( + K线取值(当前K线.开盘价, 当前K线.高, 当前K线.低, 当前K线.收盘价, &配置.指标计算方式), + 当前K线.时间戳, + 配置.相对强弱指数_周期, + 配置.相对强弱指数_超买阈值, + 配置.相对强弱指数_超卖阈值, + Some(配置.相对强弱指数_移动平均线周期), + )); + 当前K线.kdj = Some(随机指标::首次计算( + 当前K线.高, + 当前K线.低, + 当前K线.收盘价, + 当前K线.时间戳, + 配置.随机指标_RSV周期, + 配置.随机指标_K值平滑周期, + 配置.随机指标_D值平滑周期, + 配置.随机指标_超买阈值, + 配置.随机指标_超卖阈值, + )); + } + let 当前K线_rc = Rc::new(当前K线.clone()); + 普K序列.push(当前K线_rc); + } else { + let 之前普K = 普K序列.last().unwrap(); + if 之前普K.时间戳 == 当前K线.时间戳 { + // 同时间戳更新 + 当前K线.序号 = 之前普K.序号; + if 配置.计算指标 { + if 普K序列.len() >= 2 { + if let Some(ref prev_macd) = 普K序列[普K序列.len() - 2].macd { + 当前K线.macd = Some(平滑异同移动平均线::增量计算( + prev_macd, + K线取值(当前K线.开盘价, 当前K线.高, 当前K线.低, 当前K线.收盘价, &配置.指标计算方式), + 当前K线.时间戳, + )); + } + if let Some(ref prev_rsi) = 普K序列[普K序列.len() - 2].rsi { + 当前K线.rsi = Some(相对强弱指数::增量计算( + prev_rsi, + K线取值(当前K线.开盘价, 当前K线.高, 当前K线.低, 当前K线.收盘价, &配置.指标计算方式), + 当前K线.时间戳, + )); + } + if let Some(ref prev_kdj) = 普K序列[普K序列.len() - 2].kdj { + 当前K线.kdj = Some(随机指标::增量计算( + prev_kdj, + 当前K线.高, + 当前K线.低, + 当前K线.收盘价, + 当前K线.时间戳, + )); + } + } + } + 普K序列.pop(); + 普K序列.push(Rc::new(当前K线.clone())); + } else { + if 之前普K.时间戳 > 当前K线.时间戳 { + panic!("时序错误: 之前={}, 当前={}", 之前普K.时间戳, 当前K线.时间戳); + } + 当前K线.序号 = 之前普K.序号 + 1; + if 配置.计算指标 { + if let Some(ref prev_macd) = 之前普K.macd { + 当前K线.macd = Some(平滑异同移动平均线::增量计算( + prev_macd, + K线取值(当前K线.开盘价, 当前K线.高, 当前K线.低, 当前K线.收盘价, &配置.指标计算方式), + 当前K线.时间戳, + )); + } + if let Some(ref prev_rsi) = 之前普K.rsi { + 当前K线.rsi = Some(相对强弱指数::增量计算( + prev_rsi, + K线取值(当前K线.开盘价, 当前K线.高, 当前K线.低, 当前K线.收盘价, &配置.指标计算方式), + 当前K线.时间戳, + )); + } + if let Some(ref prev_kdj) = 之前普K.kdj { + 当前K线.kdj = Some(随机指标::增量计算( + prev_kdj, + 当前K线.高, + 当前K线.低, + 当前K线.收盘价, + 当前K线.时间戳, + )); + } + } + 普K序列.push(Rc::new(当前K线.clone())); + } + } + + // ---- 阶段2: 缠K合并 ---- + let 之前缠K: Option<&缠论K线> = if 缠K序列.len() >= 2 { + Some(&缠K序列[缠K序列.len() - 2]) + } else { + None + }; + + let 状态: String; + if !缠K序列.is_empty() { + let 最后一个缠K = &缠K序列[缠K序列.len() - 1]; + // 需要从 Rc 中取出引用 + let 最后一个缠K_ref = Rc::as_ref(最后一个缠K); + let (新缠K, 模式) = Self::兼并(之前缠K, 最后一个缠K_ref, 当前K线, 配置); + + if let Some(k) = 新缠K { + if let Some(ref m) = 模式 { + if m == "添加" { + 缠K序列.push(k); + 状态 = "创建".into(); + } else if m == "替换" { + 缠K序列.pop(); + 缠K序列.push(k); + 状态 = "替换".into(); + } else { + // 兼并:尝试原地修改以保持Rc指针不变 + let 最后_idx = 缠K序列.len() - 1; + if let Some(原地) = Rc::get_mut(&mut 缠K序列[最后_idx]) { + 原地.高 = k.高; + 原地.低 = k.低; + 原地.时间戳 = k.时间戳; + 原地.方向 = k.方向; + 原地.原始结束序号 = k.原始结束序号; + 原地.标的K线 = k.标的K线.clone(); + 原地.序号 = k.序号; + 状态 = "兼并".into(); + } else { + // 有其他引用时回退到替换 + 缠K序列.pop(); + 缠K序列.push(k); + 状态 = "兼并".into(); + } + } + } else { + 状态 = "兼并".into(); + } + } else { + 状态 = "兼并".into(); + } + } else { + let 新缠K = Self::创建缠K( + 当前K线.时间戳, + 当前K线.高, + 当前K线.低, + 当前K线.方向(), + None, + 当前K线.序号, + Rc::new(当前K线.clone()), + None, + ); + 缠K序列.push(Rc::new(新缠K)); + 状态 = "新建".into(); + } + + // ---- 阶段3: 分型识别 ---- + if 缠K序列.len() < 3 { + return (状态, None); + } + + let idx = 缠K序列.len(); + let 左 = Rc::clone(&缠K序列[idx - 3]); + let 中 = Rc::clone(&缠K序列[idx - 2]); + let 右 = Rc::clone(&缠K序列[idx - 1]); + + let 结构 = 分型结构::分析(&*左, &*中, &*右, false, false); + + // 需要通过 Rc::get_mut 或 RefCell 修改 中.分型 + // 由于使用 Rc,中是不可变的。这里采用创建新 Rc 替换的方式。 + // 但这是在 Vec 内部修改,需要使用 Rc::make_mut 或重新构建 + if let Some(结构) = 结构 { + // 只在分型未设置或需要更新时才修改缠K,以保持 Rc 指针不变 + let 当前分型标记 = 缠K序列[idx - 2].分型; + let 中需要更新 = 当前分型标记.is_none() || 当前分型标记 != Some(结构); + + if 中需要更新 { + let 中_mut = Rc::make_mut(&mut 缠K序列[idx - 2]); + 中_mut.分型 = Some(结构); + + match 结构 { + 分型结构::底 => { + 中_mut.分型特征值 = 中_mut.低; + let 右标记 = 缠K序列[idx - 1].分型; + if 右标记.is_none() { + let 右_mut = Rc::make_mut(&mut 缠K序列[idx - 1]); + 右_mut.分型特征值 = 右_mut.高; + 右_mut.分型 = Some(分型结构::顶); + } + } + 分型结构::顶 => { + 中_mut.分型特征值 = 中_mut.高; + let 右标记 = 缠K序列[idx - 1].分型; + if 右标记.is_none() { + let 右_mut = Rc::make_mut(&mut 缠K序列[idx - 1]); + 右_mut.分型特征值 = 右_mut.低; + 右_mut.分型 = Some(分型结构::底); + } + } + 分型结构::上 => { + 中_mut.分型特征值 = 中_mut.高; + let 右标记 = 缠K序列[idx - 1].分型; + if 右标记.is_none() { + let 右_mut = Rc::make_mut(&mut 缠K序列[idx - 1]); + 右_mut.分型特征值 = 右_mut.高; + 右_mut.分型 = Some(分型结构::顶); + } + } + 分型结构::下 => { + 中_mut.分型特征值 = 中_mut.低; + let 右标记 = 缠K序列[idx - 1].分型; + if 右标记.is_none() { + let 右_mut = Rc::make_mut(&mut 缠K序列[idx - 1]); + 右_mut.分型特征值 = 右_mut.低; + 右_mut.分型 = Some(分型结构::底); + } + } + 分型结构::散 => {} + } + } + + let 形态 = if matches!(结构, 分型结构::上 | 分型结构::下) { + // 三连方向不算完整分型 — 只有中+右 + Rc::new(分型::new( + None, + Rc::clone(&缠K序列[idx - 2]), + None, + )) + } else { + Rc::new(分型::new( + Some(Rc::clone(&缠K序列[idx - 3])), + Rc::clone(&缠K序列[idx - 2]), + Some(Rc::clone(&缠K序列[idx - 1])), + )) + }; + + return (状态, Some(形态)); + } + + (状态, None) + } + + /// 截取缠K序列从始到终 + pub fn 截取(序列: &[Rc<缠论K线>], 始: &缠论K线, 终: &缠论K线) -> Option>> { + let 始_idx = 序列.iter().position(|k| Rc::as_ptr(k) == (始 as *const _))?; + let 终_idx = 序列.iter().position(|k| Rc::as_ptr(k) == (终 as *const _))?; + Some(序列[始_idx..=终_idx].to_vec()) + } +} + +impl crate::types::fractal::有高低 for 缠论K线 { + fn 高(&self) -> f64 { + self.高 + } + fn 低(&self) -> f64 { + self.低 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::缠论配置; + + fn make_普K(时间戳: i64, 开: f64, 高: f64, 低: f64, 收: f64, 序号: i64) -> K线 { + K线::创建普K("test", 时间戳, 开, 高, 低, 收, 1000.0, 序号, 60) + } + + #[test] + fn test_创建缠K_basic() { + let pk = Rc::new(make_普K(1000, 100.0, 110.0, 95.0, 105.0, 0)); + let ck = 缠论K线::创建缠K(1000, 110.0, 95.0, 相对方向::向上, None, 0, pk, None); + assert_eq!(ck.高, 110.0); + assert_eq!(ck.低, 95.0); + assert_eq!(ck.序号, 0); + } + + #[test] + fn test_分析_empty_sequence() { + let config = 缠论配置::default(); + let mut pk = make_普K(1000, 100.0, 110.0, 95.0, 105.0, 0); + let mut 缠K序列 = Vec::new(); + let mut 普K序列 = Vec::new(); + + let (状态, 形态) = 缠论K线::分析(&mut pk, &mut 缠K序列, &mut 普K序列, &config); + assert_eq!(状态, "新建"); + assert_eq!(缠K序列.len(), 1); + assert!(形态.is_none()); // 不够3根 + } + + #[test] + fn test_分析_three_bars_fractal() { + let config = 缠论配置::default(); + let mut 缠K序列 = Vec::new(); + let mut 普K序列 = Vec::new(); + + // 三根形成顶分型: 低高 → 更高高 → 低高 + let mut pk1 = make_普K(1000, 100.0, 110.0, 95.0, 105.0, 0); + let 状态1 = 缠论K线::分析(&mut pk1, &mut 缠K序列, &mut 普K序列, &config); + assert_eq!(状态1.0, "新建"); + + let mut pk2 = make_普K(1001, 105.0, 115.0, 102.0, 112.0, 1); + let 状态2 = 缠论K线::分析(&mut pk2, &mut 缠K序列, &mut 普K序列, &config); + assert!(状态2.1.is_none()); // 仍不够 + + let mut pk3 = make_普K(1002, 112.0, 113.0, 100.0, 103.0, 2); + let (_状态3, 形态) = 缠论K线::分析(&mut pk3, &mut 缠K序列, &mut 普K序列, &config); + assert!(形态.is_some()); // 分型产生了 + } +} diff --git a/chanlun/src/kline/mod.rs b/chanlun/src/kline/mod.rs new file mode 100644 index 0000000..9b68892 --- /dev/null +++ b/chanlun/src/kline/mod.rs @@ -0,0 +1,2 @@ +pub mod bar; +pub mod chan_kline; diff --git a/chanlun/src/lib.rs b/chanlun/src/lib.rs new file mode 100644 index 0000000..731e928 --- /dev/null +++ b/chanlun/src/lib.rs @@ -0,0 +1,11 @@ +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] + +pub mod algorithm; +pub mod business; +pub mod config; +pub mod indicators; +pub mod kline; +pub mod structure; +pub mod types; +pub mod utils; diff --git a/chanlun/src/main.rs b/chanlun/src/main.rs new file mode 100644 index 0000000..6ddc285 --- /dev/null +++ b/chanlun/src/main.rs @@ -0,0 +1,34 @@ +use chanlun::business::observer::观察者; +use std::env; + +fn main() { + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("用法: {} <.nb文件路径>", args[0]); + std::process::exit(1); + } + + let 文件路径 = &args[1]; + println!("读取文件: {}", 文件路径); + + match 观察者::读取数据文件(文件路径, None) { + Ok(观察员) => { + println!("符号: {}", 观察员.符号); + println!("周期: {}", 观察员.周期); + println!("普K数量: {}", 观察员.普通K线序列.len()); + println!("缠K数量: {}", 观察员.缠论K线序列.len()); + println!("分型数量: {}", 观察员.分型序列.len()); + println!("笔数量: {}", 观察员.笔序列.len()); + println!("笔中枢数量: {}", 观察员.笔_中枢序列.len()); + println!("线段数量: {}", 观察员.线段序列.len()); + println!("中枢数量: {}", 观察员.中枢序列.len()); + + println!("\n===== 数据分析 =====\n"); + 观察员.测试_保存数据(None); + } + Err(e) => { + eprintln!("读取失败: {}", e); + std::process::exit(1); + } + } +} diff --git a/chanlun/src/structure/dash_line.rs b/chanlun/src/structure/dash_line.rs new file mode 100644 index 0000000..8114ce1 --- /dev/null +++ b/chanlun/src/structure/dash_line.rs @@ -0,0 +1,771 @@ +use crate::algorithm::hub::中枢; +use crate::config::缠论配置; +use crate::kline::chan_kline::缠论K线; +use crate::kline::bar::K线; +use crate::structure::fractal_obj::分型; +use crate::structure::segment_feat::线段特征; +use crate::types::{分型结构, 相对方向, 缺口}; +use std::rc::Rc; + +/// 虚线 — 笔和线段的通用数据结构 +/// +/// 笔和线段共享此 struct,通过 `标识` 字段区分 ("笔"/"线段"/"扩展线段"等) +#[derive(Debug, Clone)] +pub struct 虚线 { + pub 标识: String, + pub 序号: i64, + pub 级别: i64, + pub 文: Rc<分型>, + pub 武: Rc<分型>, + pub 有效性: bool, + pub 基础序列: Vec>, + pub 特征序列: Vec>>, + pub 实_中枢序列: Vec>, + pub 虚_中枢序列: Vec>, + pub 合_中枢序列: Vec>, + pub 确认K线: Option>, + pub 模式: String, + pub _特征序列_显示: bool, + pub 前一缺口: Option<缺口>, + pub 前一结束位置: Option>, + pub 短路修正: bool, +} + +impl 虚线 { + pub fn new(序号: i64, 标识: String, 文: Rc<分型>, 武: Rc<分型>, 级别: i64, 有效性: bool) -> Self { + Self { + 序号, + 标识, + 级别, + 文, + 武, + 有效性, + 基础序列: Vec::new(), + 特征序列: Vec::new(), + 实_中枢序列: Vec::new(), + 虚_中枢序列: Vec::new(), + 合_中枢序列: Vec::new(), + 确认K线: None, + 模式: "文武".into(), + _特征序列_显示: false, + 前一缺口: None, + 前一结束位置: None, + 短路修正: false, + } + } + + /// 笔序列(基础序列的别名) + pub fn 笔序列(&self) -> &Vec> { + &self.基础序列 + } + + pub fn 图表标题(&self) -> String { + format!("{}:{}:{}:{}", self.文.中.标识, self.文.中.周期, self.标识, self.序号) + } + + /// 方向 — 文到武的方向 + pub fn 方向(&self) -> 相对方向 { + match (self.文.结构, self.武.结构) { + (分型结构::顶, 分型结构::底) => 相对方向::向下, + (分型结构::顶, 分型结构::下) => 相对方向::向下, + (分型结构::上, 分型结构::底) => 相对方向::向下, + (分型结构::上, 分型结构::下) => 相对方向::向下, + _ => 相对方向::向上, + } + } + + /// 虚线高 — 文和武中高的那个 + pub fn 高(&self) -> f64 { + self.武.分型特征值.max(self.文.分型特征值) + } + + /// 虚线低 — 文和武中低的那个 + pub fn 低(&self) -> f64 { + self.武.分型特征值.min(self.文.分型特征值) + } + + /// 判断两个虚线是否首尾相连 + pub fn 之前是(&self, 之前: &虚线) -> bool { + self.文.中.序号 == 之前.武.中.序号 + } + + /// 判断两个虚线是否首尾相连 + pub fn 之后是(&self, 之后: &虚线) -> bool { + self.武.中.序号 == 之后.文.中.序号 + } + + /// 获取该虚线范围内的普K序列 + pub fn 获取普K序列(&self, 普K序列: &[Rc]) -> Vec> { + let 始 = self.文.中.原始起始序号 as usize; + let 终 = self.武.中.原始结束序号 as usize; + if 始 < 普K序列.len() && 终 < 普K序列.len() && 始 <= 终 { + 普K序列[始..=终].to_vec() + } else { + Vec::new() + } + } + + /// 获取该虚线范围内的缠K序列 + pub fn 获取缠K序列(&self, 缠K序列: &[Rc<缠论K线>]) -> Vec> { + 缠论K线::截取(缠K序列, &self.文.中, &self.武.中).unwrap_or_default() + } + + /// 获取数据文本(用于保存/调试) + pub fn 获取数据文本(&self) -> String { + use crate::utils::format_f64_g; + if self.标识 == "笔" { + return format!( + "{}, {}, {}, 文:({},{}), 武:({},{}), {}", + self.标识, + self.序号, + self.级别, + self.文.时间戳, + format_f64_g(self.文.分型特征值), + self.武.时间戳, + format_f64_g(self.武.分型特征值), + if self.有效性 { "True" } else { "False" }, + ); + } + + // 非笔:线段/扩展线段等,完整输出 + let (前, 后, 三, 贯穿伤) = + crate::algorithm::segment::线段::分割序列(self, None); + let (特征_a, 特征_b, 特征_c) = crate::algorithm::segment::线段::特征序列状态(self); + let 特征_bool = |b: bool| -> &str { if b { "True" } else { "False" } }; + + let 前一缺口_str = match &self.前一缺口 { + Some(g) => format!("{}", g), + None => "None".to_string(), + }; + let 前一结束位置_str = match &self.前一结束位置 { + Some(d) => format!("{}", d), + None => "None".to_string(), + }; + + // Format中枢序列 as Python-style list representations + let 实_str = format!("[{}]", self.实_中枢序列.iter().map(|h| format!("{}", h)).collect::>().join(", ")); + let 虚_str = format!("[{}]", self.虚_中枢序列.iter().map(|h| format!("{}", h)).collect::>().join(", ")); + let 合_str = format!("[{}]", self.合_中枢序列.iter().map(|h| format!("{}", h)).collect::>().join(", ")); + + let 前_str = format!("[{}]", 前.iter().map(|d| format!("{}", d)).collect::>().join(", ")); + let 后_str = format!("[{}]", 后.iter().map(|d| format!("{}", d)).collect::>().join(", ")); + let 三_str = format!("[{}]", 三.iter().map(|d| format!("{}", d)).collect::>().join(", ")); + + format!( + "{}, {}, {}, 文:({},{}), 武:({},{}), {}, {}, ({}, {}, {}), (前: {}, 后: {}, 三: {}, 伤: {}), 实: {}, 虚: {}, 合: {}, {}, {}, {}, {}", + self.标识, + self.序号, + self.级别, + self.文.时间戳, + format_f64_g(self.文.分型特征值), + self.武.时间戳, + format_f64_g(self.武.分型特征值), + if self.有效性 { "True" } else { "False" }, + self.基础序列.len(), + 特征_bool(特征_a), + 特征_bool(特征_b), + 特征_bool(特征_c), + 前_str, + 后_str, + 三_str, + match &贯穿伤 { Some(d) => format!("{}", d), None => "None".to_string() }, + 实_str, + 虚_str, + 合_str, + self.模式, + 前一缺口_str, + 前一结束位置_str, + if self.短路修正 { "True" } else { "False" }, + ) + } + + // ---- 关联函数(静态工厂方法) ---- + + /// 创建笔 + pub fn 创建笔(文: Rc<分型>, 武: Rc<分型>, 有效性: bool) -> Self { + Self::new(0, "笔".into(), 文, 武, 1, 有效性) + } + + /// 创建线段 + pub fn 创建线段(虚线序列: &[Rc<虚线>]) -> Self { + let 文 = Rc::clone(&虚线序列[0].文); + let 武 = Rc::clone(&虚线序列[虚线序列.len() - 1].武); + let 标识 = if 虚线序列[0].标识 == "笔" { + "线段".into() + } else { + format!("线段<{}>", 虚线序列[0].标识) + }; + let 级别 = 虚线序列[0].级别 + 1; + let mut 段 = Self::new(0, 标识, 文, 武, 级别, true); + 段.基础序列 = 虚线序列.to_vec(); + 段.模式 = "文武".into(); + 段 + } + + // ---- 买卖点模式匹配 ---- + + /// 缠K买卖点模式 — 根据模式字符串选择匹配方法 + pub fn 缠K买卖点模式(模式: &str, 缠K: &缠论K线, 配置: &缠论配置) -> bool { + match 模式 { + "全量" => Self::买卖点全量匹配(缠K), + "任意" => Self::买卖点任意匹配(缠K), + "配置" => Self::买卖点配置匹配(缠K, 配置), + "相对" => Self::买卖点相对匹配(缠K), + _ => false, + } + } + + /// 买卖点配置匹配 — 根据配置中的指标开关组合判断 + pub fn 买卖点配置匹配(缠K: &缠论K线, 配置: &缠论配置) -> bool { + match (配置.买卖点_指标匹配_MACD, 配置.买卖点_指标匹配_KDJ, 配置.买卖点_指标匹配_RSI) { + (true, true, true) => 缠K.与MACD柱子匹配() && 缠K.与KDJ匹配() && 缠K.与RSI匹配(), + (false, false, false) => false, + (true, false, true) => 缠K.与MACD柱子匹配() && 缠K.与RSI匹配(), + (false, true, false) => 缠K.与KDJ匹配(), + (true, false, false) => 缠K.与MACD柱子匹配(), + (false, true, true) => 缠K.与KDJ匹配() && 缠K.与RSI匹配(), + (false, false, true) => 缠K.与RSI匹配(), + (true, true, false) => 缠K.与MACD柱子匹配() && 缠K.与KDJ匹配(), + } + } + + /// 买卖点任意匹配 — 任一指标匹配 + pub fn 买卖点任意匹配(缠K: &缠论K线) -> bool { + 缠K.与MACD柱子匹配() || 缠K.与KDJ匹配() || 缠K.与RSI匹配() + } + + /// 买卖点全量匹配 — 全部指标匹配 + pub fn 买卖点全量匹配(缠K: &缠论K线) -> bool { + 缠K.与MACD柱子匹配() && 缠K.与KDJ匹配() && 缠K.与RSI匹配() + } + + /// 买卖点相对匹配 — 至少两个指标匹配 + pub fn 买卖点相对匹配(缠K: &缠论K线) -> bool { + let 混沌槽 = [ + 缠K.与MACD柱子匹配(), + 缠K.与KDJ匹配(), + 缠K.与RSI匹配(), + ]; + 混沌槽.iter().filter(|&&x| x).count() >= 2 + } + + // ---- MACD柱子均值计算 ---- + + /// 计算MACD柱子均值 — 虚线范围内所有MACD柱的绝对值均值 + pub fn 计算MACD柱子均值(普K序列: &[Rc], 实线: &虚线) -> f64 { + let K线序列 = K线::截取rc(普K序列, &实线.文.中.标的K线, &实线.武.中.标的K线); + if K线序列.is_empty() { + return 0.0; + } + let 总: f64 = K线序列 + .iter() + .filter_map(|k| k.macd.as_ref()) + .map(|m| m.MACD柱.abs()) + .sum(); + 总 / K线序列.len() as f64 + } + + /// 计算MACD柱子均值_阴 — 负柱的绝对值均值 + pub fn 计算MACD柱子均值_阴(普K序列: &[Rc], 实线: &虚线) -> Option { + let K线序列 = K线::截取rc(普K序列, &实线.文.中.标的K线, &实线.武.中.标的K线); + let 总: Vec = K线序列 + .iter() + .filter_map(|k| k.macd.as_ref()) + .filter(|m| m.MACD柱 < 0.0) + .map(|m| m.MACD柱.abs()) + .collect(); + if 总.is_empty() { + None + } else { + Some(总.iter().sum::() / 总.len() as f64) + } + } + + /// 计算MACD柱子均值_阳 — 正柱的绝对值均值 + pub fn 计算MACD柱子均值_阳(普K序列: &[Rc], 实线: &虚线) -> Option { + let K线序列 = K线::截取rc(普K序列, &实线.文.中.标的K线, &实线.武.中.标的K线); + let 总: Vec = K线序列 + .iter() + .filter_map(|k| k.macd.as_ref()) + .filter(|m| m.MACD柱 > 0.0) + .map(|m| m.MACD柱.abs()) + .collect(); + if 总.is_empty() { + None + } else { + Some(总.iter().sum::() / 总.len() as f64) + } + } + + // ---- 武之MACD比较 ---- + + /// 武之全量MACD均值 — 武端MACD柱是否小于均值(背驰) + pub fn 武之全量MACD均值(普K序列: &[Rc], 实线: &虚线) -> bool { + let 武_MACD = match &实线.武.中.标的K线.macd { + Some(m) => m.MACD柱.abs(), + None => return false, + }; + 武_MACD < Self::计算MACD柱子均值(普K序列, 实线) + } + + /// 武之MACD均值 — 按方向选择阴/阳均值比对 + pub fn 武之MACD均值(普K序列: &[Rc], 实线: &虚线) -> bool { + if 实线.方向() == 相对方向::向上 { + Self::武之MACD均值_阳(普K序列, 实线) + } else { + Self::武之MACD均值_阴(普K序列, 实线) + } + } + + /// 武之MACD均值_阴 — 武端负柱是否小于阴均值 + pub fn 武之MACD均值_阴(普K序列: &[Rc], 实线: &虚线) -> bool { + let 武_MACD = match &实线.武.中.标的K线.macd { + Some(m) => m.MACD柱.abs(), + None => return false, + }; + match Self::计算MACD柱子均值_阴(普K序列, 实线) { + Some(均值) => 武_MACD < 均值.abs(), + None => false, + } + } + + /// 武之MACD均值_阳 — 武端正柱是否小于阳均值 + pub fn 武之MACD均值_阳(普K序列: &[Rc], 实线: &虚线) -> bool { + let 武_MACD = match &实线.武.中.标的K线.macd { + Some(m) => m.MACD柱.abs(), + None => return false, + }; + match Self::计算MACD柱子均值_阳(普K序列, 实线) { + Some(均值) => 武_MACD < 均值, + None => false, + } + } + + /// 武之MACD极值 — 武端MACD柱是否为区间极值 + pub fn 武之MACD极值(普K序列: &[Rc], 实线: &虚线) -> bool { + let 武_MACD = match &实线.武.中.标的K线.macd { + Some(m) => m.MACD柱, + None => return false, + }; + let K线序列 = K线::截取rc(普K序列, &实线.文.中.标的K线, &实线.武.中.标的K线); + let 所有柱子: Vec = K线序列 + .iter() + .filter_map(|k| k.macd.as_ref()) + .map(|m| m.MACD柱) + .collect(); + if 所有柱子.is_empty() { + return false; + } + if 武_MACD > 0.0 { + let 极值 = 所有柱子.iter().cloned().fold(f64::NEG_INFINITY, f64::max); + 极值 == 武_MACD + } else { + let 极值 = 所有柱子.iter().cloned().fold(f64::INFINITY, f64::min); + 极值 == 武_MACD + } + } + + // ---- MACD趋向背驰 ---- + + /// 计算K线序列MACD趋向背驰 — 分析 MACD柱/DIF/DEA 三项背驰信号 + pub fn 计算K线序列MACD趋向背驰(普K序列: &[Rc], 方向: 相对方向) -> [bool; 3] { + if 普K序列.is_empty() { + return [false, false, false]; + } + let 最后 = &普K序列[普K序列.len() - 1]; + + if 方向 == 相对方向::向上 { + let 柱子序列: Vec<&Rc> = 普K序列 + .iter() + .filter(|k| k.macd.as_ref().map_or(false, |m| m.MACD柱 > 0.0)) + .collect(); + if 柱子序列.is_empty() { + return [false, false, false]; + } + + let mut 结果 = [false; 3]; + + // MACD柱背驰 + let 最高柱子 = 柱子序列 + .iter() + .max_by(|a, b| { + a.macd.as_ref().unwrap().MACD柱 + .partial_cmp(&b.macd.as_ref().unwrap().MACD柱) + .unwrap() + }) + .unwrap(); + let mut 柱对 = vec![Rc::clone(*最高柱子), Rc::clone(最后)]; + 柱对.sort_by_key(|k| k.时间戳); + if let (Some(m0), Some(m1)) = (柱对[0].macd.as_ref(), 柱对[1].macd.as_ref()) { + if m0.MACD柱 > m1.MACD柱 && 柱对[0].高 < 柱对[1].高 { + 结果[0] = true; + } + } + + // DIF背驰 + let 最高离差值 = 柱子序列 + .iter() + .max_by(|a, b| { + let da = a.macd.as_ref().and_then(|m| m.DIF).unwrap_or(0.0); + let db = b.macd.as_ref().and_then(|m| m.DIF).unwrap_or(0.0); + da.partial_cmp(&db).unwrap() + }) + .unwrap(); + let mut 柱对2 = vec![Rc::clone(*最高离差值), Rc::clone(最后)]; + 柱对2.sort_by_key(|k| k.时间戳); + if let (Some(m0), Some(m1)) = (柱对2[0].macd.as_ref(), 柱对2[1].macd.as_ref()) { + let dif0 = m0.DIF.unwrap_or(0.0); + let dif1 = m1.DIF.unwrap_or(0.0); + if dif0 > dif1 && 柱对2[0].高 < 柱对2[1].高 { + 结果[1] = true; + } + } + + // DEA背驰 + let 最高信号线 = 柱子序列 + .iter() + .max_by(|a, b| { + let da = a.macd.as_ref().and_then(|m| m.DEA).unwrap_or(0.0); + let db = b.macd.as_ref().and_then(|m| m.DEA).unwrap_or(0.0); + da.partial_cmp(&db).unwrap() + }) + .unwrap(); + let mut 柱对3 = vec![Rc::clone(*最高信号线), Rc::clone(最后)]; + 柱对3.sort_by_key(|k| k.时间戳); + if let (Some(m0), Some(m1)) = (柱对3[0].macd.as_ref(), 柱对3[1].macd.as_ref()) { + let dea0 = m0.DEA.unwrap_or(0.0); + let dea1 = m1.DEA.unwrap_or(0.0); + if dea0 > dea1 && 柱对3[0].高 < 柱对3[1].高 { + 结果[2] = true; + } + } + + 结果 + } else { + let 柱子序列: Vec<&Rc> = 普K序列 + .iter() + .filter(|k| k.macd.as_ref().map_or(false, |m| m.MACD柱 < 0.0)) + .collect(); + if 柱子序列.is_empty() { + return [false, false, false]; + } + + let mut 结果 = [false; 3]; + + // MACD柱背驰 (负向: absolute value comparison) + let 最高柱子 = 柱子序列 + .iter() + .max_by(|a, b| { + a.macd.as_ref().unwrap().MACD柱.abs() + .partial_cmp(&b.macd.as_ref().unwrap().MACD柱.abs()) + .unwrap() + }) + .unwrap(); + let mut 柱对 = vec![Rc::clone(*最高柱子), Rc::clone(最后)]; + 柱对.sort_by_key(|k| k.时间戳); + if let (Some(m0), Some(m1)) = (柱对[0].macd.as_ref(), 柱对[1].macd.as_ref()) { + if m0.MACD柱 < m1.MACD柱 && 柱对[0].低 > 柱对[1].低 { + 结果[0] = true; + } + } + + // DIF背驰 + let 最高离差值 = 柱子序列 + .iter() + .max_by(|a, b| { + let da = a.macd.as_ref().and_then(|m| m.DIF).unwrap_or(0.0).abs(); + let db = b.macd.as_ref().and_then(|m| m.DIF).unwrap_or(0.0).abs(); + da.partial_cmp(&db).unwrap() + }) + .unwrap(); + let mut 柱对2 = vec![Rc::clone(*最高离差值), Rc::clone(最后)]; + 柱对2.sort_by_key(|k| k.时间戳); + if let (Some(m0), Some(m1)) = (柱对2[0].macd.as_ref(), 柱对2[1].macd.as_ref()) { + let dif0 = m0.DIF.unwrap_or(0.0); + let dif1 = m1.DIF.unwrap_or(0.0); + if dif0 < dif1 && 柱对2[0].低 > 柱对2[1].低 { + 结果[1] = true; + } + } + + // DEA背驰 + let 最高信号线 = 柱子序列 + .iter() + .max_by(|a, b| { + let da = a.macd.as_ref().and_then(|m| m.DEA).unwrap_or(0.0).abs(); + let db = b.macd.as_ref().and_then(|m| m.DEA).unwrap_or(0.0).abs(); + da.partial_cmp(&db).unwrap() + }) + .unwrap(); + let mut 柱对3 = vec![Rc::clone(*最高信号线), Rc::clone(最后)]; + 柱对3.sort_by_key(|k| k.时间戳); + if let (Some(m0), Some(m1)) = (柱对3[0].macd.as_ref(), 柱对3[1].macd.as_ref()) { + let dea0 = m0.DEA.unwrap_or(0.0); + let dea1 = m1.DEA.unwrap_or(0.0); + if dea0 < dea1 && 柱对3[0].低 > 柱对3[1].低 { + 结果[2] = true; + } + } + + 结果 + } + } + + // ---- MACD柱子分段 ---- + + /// 计算MACD柱子分段 — 按正负号将MACD柱子分段 + pub fn 计算MACD柱子分段(k线序列: &[Rc]) -> Vec> { + if k线序列.is_empty() { + return Vec::new(); + } + + let 符号 = |x: f64| -> &str { + if x > 0.0 { "正" } else { "负" } + }; + + let 首_MACD = match &k线序列[0].macd { + Some(m) => m.MACD柱, + None => return Vec::new(), + }; + let mut 当前符号 = 符号(首_MACD); + let mut 当前段 = vec![首_MACD]; + let mut 结果 = Vec::new(); + + for k线 in &k线序列[1..] { + let macd = match &k线.macd { + Some(m) => m.MACD柱, + None => continue, + }; + let 新符号 = 符号(macd); + if 新符号 == 当前符号 { + 当前段.push(macd); + } else { + 结果.push(std::mem::take(&mut 当前段)); + 当前段.push(macd); + 当前符号 = 新符号; + } + } + if !当前段.is_empty() { + 结果.push(当前段); + } + 结果 + } + + // ---- 密集区域按间隔 ---- + + /// 密集区域按间隔 — 找出交叉标记中的密集区域 + pub fn 密集区域按间隔( + 交叉标记: &[i32], + 最大间隔: usize, + 最少交叉数: usize, + ) -> Vec<(usize, usize, usize)> { + let 交叉索引: Vec = (0..交叉标记.len()).filter(|&i| 交叉标记[i] != 0).collect(); + if 交叉索引.is_empty() { + return Vec::new(); + } + + let mut 密集区 = Vec::new(); + let mut 当前块起始 = 交叉索引[0]; + let mut 当前块交叉数 = 1; + + for i in 1..交叉索引.len() { + let prev = 交叉索引[i - 1]; + let curr = 交叉索引[i]; + if curr - prev <= 最大间隔 { + 当前块交叉数 += 1; + } else { + if 当前块交叉数 >= 最少交叉数 { + 密集区.push((当前块起始, prev, 当前块交叉数)); + } + 当前块起始 = curr; + 当前块交叉数 = 1; + } + } + + if 当前块交叉数 >= 最少交叉数 { + 密集区.push((当前块起始, 交叉索引[交叉索引.len() - 1], 当前块交叉数)); + } + + 密集区 + } + + // ---- 统计MACD行为 ---- + + /// 统计MACD行为 — 分析DIF/DEA穿零轴和金叉死叉 + pub fn 统计MACD行为(普K序列: &[Rc], 最大间隔: usize, 最少交叉数: usize) -> std::collections::HashMap { + use std::collections::HashMap; + + let mut dif_up = 0; + let mut dif_down = 0; + let mut dea_up = 0; + let mut dea_down = 0; + + for i in 1..普K序列.len() { + let pre = &普K序列[i - 1].macd; + let cur = &普K序列[i].macd; + if pre.is_none() || cur.is_none() { + continue; + } + let (pre_dif, cur_dif) = (pre.as_ref().unwrap().DIF, cur.as_ref().unwrap().DIF); + let (pre_dea, cur_dea) = (pre.as_ref().unwrap().DEA, cur.as_ref().unwrap().DEA); + + if let (Some(pd), Some(cd)) = (pre_dif, cur_dif) { + if pd < 0.0 && cd >= 0.0 { dif_up += 1; } + if pd > 0.0 && cd <= 0.0 { dif_down += 1; } + } + if let (Some(pd), Some(cd)) = (pre_dea, cur_dea) { + if pd < 0.0 && cd >= 0.0 { dea_up += 1; } + if pd > 0.0 && cd <= 0.0 { dea_down += 1; } + } + } + + let mut golden = 0; + let mut death = 0; + let mut 交叉标记 = vec![0i32]; + + for i in 1..普K序列.len() { + let pre = &普K序列[i - 1].macd; + let cur = &普K序列[i].macd; + if pre.is_none() || cur.is_none() { + 交叉标记.push(0); + continue; + } + let pre_dif = pre.as_ref().unwrap().DIF; + let pre_dea = pre.as_ref().unwrap().DEA; + let cur_dif = cur.as_ref().unwrap().DIF; + let cur_dea = cur.as_ref().unwrap().DEA; + + if let (Some(pd), Some(cd), Some(pe), Some(ce)) = (pre_dif, cur_dif, pre_dea, cur_dea) { + if pd <= pe && cd > ce { + golden += 1; + 交叉标记.push(1); + } else if pd >= pe && cd < ce { + death += 1; + 交叉标记.push(-1); + } else { + 交叉标记.push(0); + } + } else { + 交叉标记.push(0); + } + } + + let 密集区 = Self::密集区域按间隔(&交叉标记, 最大间隔, 最少交叉数); + + let mut map = HashMap::new(); + map.insert("DIF上穿0".into(), dif_up.to_string()); + map.insert("DIF下穿0".into(), dif_down.to_string()); + map.insert("DEA上穿0".into(), dea_up.to_string()); + map.insert("DEA下穿0".into(), dea_down.to_string()); + map.insert("金叉次数".into(), golden.to_string()); + map.insert("死叉次数".into(), death.to_string()); + map.insert("密集交叉区域".into(), format!("{:?}", 密集区)); + map + } + + // ---- 买卖意义 ---- + + /// 买卖意义 — 核心买卖点判断逻辑 + /// + /// 返回 (是否有意义, 原因字符串) + pub fn 买卖意义(实线: &虚线, 观察员: &crate::business::observer::观察者) -> (bool, String) { + let 普K序列 = &观察员.普通K线序列; + let 配置 = &观察员.配置; + + if 实线.标识 != "笔" && 实线.标识 != "线段" && !实线.标识.starts_with("线段<") { + return (false, "标识不在范围内".into()); + } + + // KDJ指标完整性检查 + match &实线.武.中.标的K线.kdj { + Some(kdj) if kdj.K.is_some() && kdj.D.is_some() && kdj.J.is_some() => {} + _ => return (false, "KDJ指标不完整".into()), + } + + let 意义 = Self::缠K买卖点模式(&配置.买卖点_指标模式, &实线.武.中, 配置); + let 结果 = false; + + let 背驰过: Vec> = if 实线.标识 == "笔" { + crate::algorithm::bi::笔::是否背驰过(实线, 观察员) + } else { + crate::algorithm::segment::线段::是否背驰过(实线, 观察员) + }; + + if 意义 { + if 实线.标识 == "笔" { + if Self::武之MACD均值(普K序列, 实线) { + return (true, "武之MACD均值".into()); + } + if Self::武之MACD极值(普K序列, 实线) && !背驰过.is_empty() { + return (true, "背驰过且极值".into()); + } else if 实线.武.与MACD柱子分型匹配() { + return ( + true, + format!( + "背驰过:{},极值:{},柱子分型匹配", + 背驰过.len(), + Self::武之MACD极值(普K序列, 实线) + ), + ); + } + } + if 实线.标识 != "笔" + && crate::algorithm::segment::线段::判断线段内部是否背驰(实线, 观察员) + { + return (true, "线段内部背驰".into()); + } + } + + if !结果 && 意义 && 实线.武.中.与MACD柱子匹配() { + if Self::武之MACD极值(普K序列, 实线) && 背驰过.len() > 2 { + return ( + true, + "没结果, 极值, 柱子分型匹配, 背驰过大于2次".into(), + ); + } + } + + (结果, "".into()) + } +} + +impl std::fmt::Display for 虚线 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.标识 == "笔" { + write!( + f, + "笔({}, {}, {}, {}, 周期: {}, 数量: {})", + self.序号, + self.方向(), + self.文, + self.武, + self.文.中.周期, + self.武.中.序号 - self.文.中.序号 + 1 + ) + } else { + let 四象 = crate::algorithm::segment::线段::四象(self); + let 缺口 = crate::algorithm::segment::线段::获取缺口(self); + let 缺口_str = match 缺口 { + Some(g) => format!("{}", g), + None => "None".to_string(), + }; + let 确认K线_str = match &self.确认K线 { + Some(k) => format!("{}", k), + None => "None".to_string(), + }; + write!( + f, + "{}<{}, {}, {}, {}, {}, 数量: {}, 缺口: {}, {}>", + self.标识, + self.序号, + 四象, + self.方向(), + self.文, + self.武, + self.基础序列.len(), + 缺口_str, + 确认K线_str, + ) + } + } +} diff --git a/chanlun/src/structure/feat_fractal.rs b/chanlun/src/structure/feat_fractal.rs new file mode 100644 index 0000000..64b461e --- /dev/null +++ b/chanlun/src/structure/feat_fractal.rs @@ -0,0 +1,24 @@ +use crate::structure::segment_feat::线段特征; +use crate::types::分型结构; +use std::rc::Rc; + +/// 特征分型 — 由三个线段特征元素构成的分型 +#[derive(Debug, Clone)] +pub struct 特征分型 { + pub 左: Rc<线段特征>, + pub 中: Rc<线段特征>, + pub 右: Rc<线段特征>, + pub 结构: 分型结构, +} + +impl 特征分型 { + pub fn new(左: Rc<线段特征>, 中: Rc<线段特征>, 右: Rc<线段特征>, 结构: 分型结构) -> Self { + Self { 左, 中, 右, 结构 } + } +} + +impl std::fmt::Display for 特征分型 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "特征分型<{}, {}>", self.结构, self.中) + } +} diff --git a/chanlun/src/structure/fractal_obj.rs b/chanlun/src/structure/fractal_obj.rs new file mode 100644 index 0000000..7d0f8db --- /dev/null +++ b/chanlun/src/structure/fractal_obj.rs @@ -0,0 +1,173 @@ +use crate::kline::chan_kline::缠论K线; +use crate::types::分型结构; +use crate::types::相对方向; +use std::rc::Rc; + +/// 分型 — 由三根缠K构成(可能缺左或右) +#[derive(Debug, Clone)] +pub struct 分型 { + pub 左: Option>, + pub 中: Rc<缠论K线>, + pub 右: Option>, + pub 结构: 分型结构, + pub 时间戳: i64, + pub 分型特征值: f64, +} + +impl 分型 { + pub fn new(左: Option>, 中: Rc<缠论K线>, 右: Option>) -> Self { + let 结构 = 中.分型.unwrap_or(分型结构::散); + let 时间戳 = 中.时间戳; + let 分型特征值 = 中.分型特征值; + Self { + 左, + 中, + 右, + 结构, + 时间戳, + 分型特征值, + } + } + + /// 左中右三组关系 + pub fn 关系组(&self) -> Option<(相对方向, 相对方向, 相对方向)> { + let 左 = self.左.as_ref()?; + let 右 = self.右.as_ref()?; + Some(( + 相对方向::分析(左.高, 左.低, self.中.高, self.中.低), + 相对方向::分析(self.中.高, self.中.低, 右.高, 右.低), + 相对方向::分析(左.高, 左.低, 右.高, 右.低), + )) + } + + /// 分型强度 + pub fn 强度(&self) -> f64 { + match self.结构 { + 分型结构::顶 | 分型结构::上 => { + if let Some(ref 左) = self.左 { + self.中.高 - 左.高 + } else { + 0.0 + } + } + 分型结构::底 | 分型结构::下 => { + if let Some(ref 左) = self.左 { + 左.低 - self.中.低 + } else { + 0.0 + } + } + 分型结构::散 => 0.0, + } + } + + /// MACD柱子分型匹配 + pub fn 与MACD柱子分型匹配(&self) -> bool { + match self.结构 { + 分型结构::顶 | 分型结构::上 => { + if let Some(ref 左) = self.左 { + 左.与MACD柱子匹配() + } else { + false + } + } + 分型结构::底 | 分型结构::下 => { + if let Some(ref 右) = self.右 { + 右.与MACD柱子匹配() + } else { + false + } + } + 分型结构::散 => false, + } + } + + /// 判断两个分型是否匹配 + pub fn 判断分型(左: &分型, 右: &分型, 模式: &str) -> bool { + match 模式 { + "中" => 左.中.序号 == 右.中.序号, + _ => false, + } + } + + /// 从缠K序列中获取以指定缠K为中元素的分型 + pub fn 从缠K序列中获取分型(K线序列: &[Rc<缠论K线>], 中: &Rc<缠论K线>) -> Option { + let idx = K线序列.iter().position(|k| Rc::as_ptr(k) == Rc::as_ptr(中))?; + let 左 = if idx > 0 { + Some(Rc::clone(&K线序列[idx - 1])) + } else { + None + }; + let 右 = if idx + 1 < K线序列.len() { + Some(Rc::clone(&K线序列[idx + 1])) + } else { + None + }; + Some(Self::new(左, Rc::clone(中), 右)) + } + + /// 向分型序列中添加新分型 + pub fn 向序列中添加(分型序列: &mut Vec>, 当前分型: Rc<分型>) { + if let Some(前一个) = 分型序列.last() { + if 前一个.时间戳 == 当前分型.时间戳 { + // 同一时间戳: 比较强度,保留更强的 + match 当前分型.结构 { + 分型结构::顶 | 分型结构::上 => { + if 当前分型.分型特征值 >= 前一个.分型特征值 { + 分型序列.pop(); + 分型序列.push(当前分型); + } + return; + } + 分型结构::底 | 分型结构::下 => { + if 当前分型.分型特征值 <= 前一个.分型特征值 { + 分型序列.pop(); + 分型序列.push(当前分型); + } + return; + } + 分型结构::散 => {} + } + } + // 相同结构只保留更强的 + if 前一个.结构 == 当前分型.结构 { + if (当前分型.结构 == 分型结构::顶 || 当前分型.结构 == 分型结构::上) + && 当前分型.分型特征值 >= 前一个.分型特征值 + { + 分型序列.pop(); + 分型序列.push(当前分型); + } else if (当前分型.结构 == 分型结构::底 || 当前分型.结构 == 分型结构::下) + && 当前分型.分型特征值 <= 前一个.分型特征值 + { + 分型序列.pop(); + 分型序列.push(当前分型); + } + return; + } + } + 分型序列.push(当前分型); + } +} + +impl crate::types::fractal::有高低 for 分型 { + fn 高(&self) -> f64 { + self.中.高 + } + fn 低(&self) -> f64 { + self.中.低 + } +} + +impl std::fmt::Display for 分型 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}<{}, {}, None: {}, None: {}>", + self.中.分型.unwrap_or(crate::types::分型结构::散), + self.时间戳, + crate::utils::format_f64_g(self.分型特征值), + if self.左.is_none() { "True" } else { "False" }, + if self.右.is_none() { "True" } else { "False" }, + ) + } +} diff --git a/chanlun/src/structure/mod.rs b/chanlun/src/structure/mod.rs new file mode 100644 index 0000000..8ccac6b --- /dev/null +++ b/chanlun/src/structure/mod.rs @@ -0,0 +1,4 @@ +pub mod dash_line; +pub mod feat_fractal; +pub mod fractal_obj; +pub mod segment_feat; diff --git a/chanlun/src/structure/segment_feat.rs b/chanlun/src/structure/segment_feat.rs new file mode 100644 index 0000000..4cc5e4c --- /dev/null +++ b/chanlun/src/structure/segment_feat.rs @@ -0,0 +1,254 @@ +use crate::structure::dash_line::虚线; +use crate::structure::feat_fractal::特征分型; +use crate::structure::fractal_obj::分型; +use crate::types::{分型结构, 相对方向}; +use std::rc::Rc; + +/// 线段特征 — 特征序列元素(内部是虚线的集合) +#[derive(Debug, Clone)] +pub struct 线段特征 { + pub 序号: i64, + pub 标识: String, + pub 线段方向: 相对方向, + pub 元素: Vec>, +} + +impl 线段特征 { + pub fn new(标识: String, 基础序列: Vec>, 线段方向: 相对方向) -> Self { + Self { + 序号: 0, + 标识, + 线段方向, + 元素: 基础序列, + } + } + + pub fn 图表标题(&self) -> String { + self.标识.clone() + } + + /// 文 — 取特征序列元素中分型特征值最大/最小的文分型 + pub fn 文(&self) -> Rc<分型> { + if self.线段方向.是否向上() { + // 向上 → 取最大的文 + self.元素 + .iter() + .max_by(|a, b| { + a.文 + .分型特征值 + .partial_cmp(&b.文.分型特征值) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|x| Rc::clone(&x.文)) + .unwrap_or_else(|| Rc::clone(&self.元素[0].文)) + } else { + // 向下 → 取最小的文 + self.元素 + .iter() + .min_by(|a, b| { + a.文 + .分型特征值 + .partial_cmp(&b.文.分型特征值) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|x| Rc::clone(&x.文)) + .unwrap_or_else(|| Rc::clone(&self.元素[0].文)) + } + } + + /// 武 — 取特征序列元素中分型特征值最大/最小的武分型 + pub fn 武(&self) -> Rc<分型> { + if self.线段方向.是否向上() { + self.元素 + .iter() + .max_by(|a, b| { + a.武 + .分型特征值 + .partial_cmp(&b.武.分型特征值) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|x| Rc::clone(&x.武)) + .unwrap_or_else(|| Rc::clone(&self.元素[0].武)) + } else { + self.元素 + .iter() + .min_by(|a, b| { + a.武 + .分型特征值 + .partial_cmp(&b.武.分型特征值) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|x| Rc::clone(&x.武)) + .unwrap_or_else(|| Rc::clone(&self.元素[0].武)) + } + } + + pub fn 高(&self) -> f64 { + let 文 = self.文(); + let 武 = self.武(); + 文.分型特征值.max(武.分型特征值) + } + + pub fn 低(&self) -> f64 { + let 文 = self.文(); + let 武 = self.武(); + 文.分型特征值.min(武.分型特征值) + } + + /// 方向 — 线段方向的翻转 + pub fn 方向(&self) -> 相对方向 { + self.线段方向.翻转() + } + + /// 向特征序列元素中添加虚线 + pub fn 添加(&mut self, 待添加虚线: Rc<虚线>) -> Result<(), String> { + if 待添加虚线.方向() == self.线段方向 { + return Err("添加方向与线段方向相同".into()); + } + self.元素.push(待添加虚线); + Ok(()) + } + + /// 新建特征序列元素 + pub fn 新建(虚线序列: Vec>, 线段方向: 相对方向) -> Self { + let 标识 = format!("特征<虚线>"); + Self::new(标识, 虚线序列, 线段方向) + } + + /// 静态分析 — 从虚线序列生成特征序列元素列表 + pub fn 静态分析( + 虚线序列: &[Rc<虚线>], + 线段方向: 相对方向, + 四象: &str, + 是否忽视: bool, + ) -> Vec> { + let mut 结果: Vec> = Vec::new(); + + // 需要被合并的方向集合 + let 需要合并: Vec<相对方向> = match 四象 { + "老阳" | "老阴" if !是否忽视 => vec![相对方向::顺, 相对方向::逆, 相对方向::同], + _ => vec![相对方向::顺, 相对方向::同], + }; + + for 虚线 in 虚线序列 { + // 情况1:方向相同(可能触发分型替换) + if 虚线.方向() == 线段方向 { + if 结果.len() >= 3 { + let 左 = Rc::clone(&结果[结果.len() - 3]); + let 中 = Rc::clone(&结果[结果.len() - 2]); + let 右 = Rc::clone(&结果[结果.len() - 1]); + + if let Some(结构) = 分型结构::分析(&*左, &*中, &*右, true, true) { + let 应替换 = (线段方向 == 相对方向::向上 + && 结构 == 分型结构::顶 + && 虚线.高() > 中.高()) + || (线段方向 == 相对方向::向下 + && 结构 == 分型结构::底 + && 虚线.低() < 中.低()); + + if 应替换 { + let 小号虚线 = if 中.元素[0].序号 < 右.元素[0].序号 { + &中.元素[0] + } else { + &右.元素[0] + }; + let 大号虚线 = if 中.元素.last().unwrap().序号 + > 右.元素.last().unwrap().序号 + { + &中.元素[中.元素.len() - 1] + } else { + &右.元素[右.元素.len() - 1] + }; + let fake = 虚线::创建笔( + Rc::clone(&小号虚线.文), + Rc::clone(&大号虚线.武), + false, + ); + 结果.pop(); + let idx = 结果.len() - 1; + 结果[idx] = + Rc::new(Self::新建(vec![Rc::new(fake)], 线段方向)); + } + } + } + continue; + } + + // 情况2:方向不同(执行特征序列的合并/添加) + if 结果.is_empty() { + 结果.push(Rc::new(Self::新建(vec![Rc::clone(虚线)], 线段方向))); + continue; + } + + // 检查与最后一个特征序列的方向关系 + let 最后_idx = 结果.len() - 1; + let 之前线段特征 = &*结果[最后_idx]; + if 需要合并.contains(&相对方向::分析( + 之前线段特征.高(), + 之前线段特征.低(), + 虚线.高(), + 虚线.低(), + )) { + // Clone-modify-replace + let mut 新特征 = (*结果[最后_idx]).clone(); + let _ = 新特征.添加(Rc::clone(虚线)); + 结果[最后_idx] = Rc::new(新特征); + } else { + 结果.push(Rc::new(Self::新建(vec![Rc::clone(虚线)], 线段方向))); + } + } + + 结果 + } + + /// 获取分型序列 + pub fn 获取分型序列(特征序列: &[Rc<线段特征>]) -> Vec<特征分型> { + let mut 结果 = Vec::new(); + if 特征序列.len() < 3 { + return 结果; + } + for i in 2..特征序列.len() { + let 左 = Rc::clone(&特征序列[i - 2]); + let 中 = Rc::clone(&特征序列[i - 1]); + let 右 = Rc::clone(&特征序列[i]); + + if let Some(结构) = 分型结构::分析_对象( + &*左 as &dyn crate::types::fractal::有高低, + &*中 as &dyn crate::types::fractal::有高低, + &*右 as &dyn crate::types::fractal::有高低, + true, + true, + ) { + 结果.push(特征分型::new(左, 中, 右, 结构)); + } + } + 结果 + } +} + +impl crate::types::fractal::有高低 for 线段特征 { + fn 高(&self) -> f64 { + self.高() + } + fn 低(&self) -> f64 { + self.低() + } +} + +impl std::fmt::Display for 线段特征 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.元素.is_empty() { + write!(f, "{}<{}, 空>", self.标识, self.线段方向) + } else { + write!( + f, + "{}<{}, {}, {}, {}>", + self.标识, + self.线段方向, + self.文(), + self.武(), + self.元素.len() + ) + } + } +} diff --git a/chanlun/src/types/bsp_type.rs b/chanlun/src/types/bsp_type.rs new file mode 100644 index 0000000..f873f81 --- /dev/null +++ b/chanlun/src/types/bsp_type.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; + +/// 买卖点类型 —— 六类买卖点(一二三 + T1/T1P/T2/T2S/T3A/T3B) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum 买卖点类型 { + #[serde(rename = "一买")] + 一买, + #[serde(rename = "一卖")] + 一卖, + #[serde(rename = "二买")] + 二买, + #[serde(rename = "二卖")] + 二卖, + #[serde(rename = "三买")] + 三买, + #[serde(rename = "三卖")] + 三卖, + #[serde(rename = "T1买")] + T1买, + #[serde(rename = "T1卖")] + T1卖, + #[serde(rename = "T1P买")] + T1P买, + #[serde(rename = "T1P卖")] + T1P卖, + #[serde(rename = "T2买")] + T2买, + #[serde(rename = "T2卖")] + T2卖, + #[serde(rename = "T2S买")] + T2S买, + #[serde(rename = "T2S卖")] + T2S卖, + #[serde(rename = "T3A买")] + T3A买, + #[serde(rename = "T3A卖")] + T3A卖, + #[serde(rename = "T3B买")] + T3B买, + #[serde(rename = "T3B卖")] + T3B卖, +} + +impl std::fmt::Display for 买卖点类型 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::一买 => write!(f, "一买"), + Self::一卖 => write!(f, "一卖"), + Self::二买 => write!(f, "二买"), + Self::二卖 => write!(f, "二卖"), + Self::三买 => write!(f, "三买"), + Self::三卖 => write!(f, "三卖"), + Self::T1买 => write!(f, "T1买"), + Self::T1卖 => write!(f, "T1卖"), + Self::T1P买 => write!(f, "T1P买"), + Self::T1P卖 => write!(f, "T1P卖"), + Self::T2买 => write!(f, "T2买"), + Self::T2卖 => write!(f, "T2卖"), + Self::T2S买 => write!(f, "T2S买"), + Self::T2S卖 => write!(f, "T2S卖"), + Self::T3A买 => write!(f, "T3A买"), + Self::T3A卖 => write!(f, "T3A卖"), + Self::T3B买 => write!(f, "T3B买"), + Self::T3B卖 => write!(f, "T3B卖"), + } + } +} + +impl 买卖点类型 { + pub fn 是买点(&self) -> bool { + matches!( + self, + Self::一买 + | Self::二买 + | Self::三买 + | Self::T1买 + | Self::T1P买 + | Self::T2买 + | Self::T2S买 + | Self::T3A买 + | Self::T3B买 + ) + } + + pub fn 是卖点(&self) -> bool { + matches!( + self, + Self::一卖 + | Self::二卖 + | Self::三卖 + | Self::T1卖 + | Self::T1P卖 + | Self::T2卖 + | Self::T2S卖 + | Self::T3A卖 + | Self::T3B卖 + ) + } +} diff --git a/chanlun/src/types/direction.rs b/chanlun/src/types/direction.rs new file mode 100644 index 0000000..e47bfc8 --- /dev/null +++ b/chanlun/src/types/direction.rs @@ -0,0 +1,108 @@ +use serde::{Deserialize, Serialize}; + +/// 相对方向 —— K线之间的相对位置关系 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum 相对方向 { + #[serde(rename = "交叠向上")] + 向上, + #[serde(rename = "交叠向下")] + 向下, + #[serde(rename = "向上缺口")] + 向上缺口, + #[serde(rename = "向下缺口")] + 向下缺口, + #[serde(rename = "衔接向上")] + 衔接向上, + #[serde(rename = "衔接向下")] + 衔接向下, + #[serde(rename = "顺序包含")] + 顺, + #[serde(rename = "逆序包含")] + 逆, + #[serde(rename = "相同包含")] + 同, +} + +impl 相对方向 { + pub fn 翻转(&self) -> Self { + match self { + Self::向上 => Self::向下, + Self::向下 => Self::向上, + Self::向上缺口 => Self::向下缺口, + Self::向下缺口 => Self::向上缺口, + Self::衔接向上 => Self::衔接向下, + Self::衔接向下 => Self::衔接向上, + Self::顺 => Self::逆, + Self::逆 => Self::顺, + other => *other, + } + } + + pub fn 是否向上(&self) -> bool { + matches!(self, Self::向上 | Self::向上缺口 | Self::衔接向上) + } + + pub fn 是否向下(&self) -> bool { + matches!(self, Self::向下 | Self::向下缺口 | Self::衔接向下) + } + + pub fn 是否包含(&self) -> bool { + matches!(self, Self::顺 | Self::逆 | Self::同) + } + + pub fn 是否缺口(&self) -> bool { + matches!(self, Self::向下缺口 | Self::向上缺口) + } + + pub fn 是否衔接(&self) -> bool { + matches!(self, Self::衔接向下 | Self::衔接向上) + } + + /// 分析两个K线之间的相对方向 + pub fn 分析(前高: f64, 前低: f64, 后高: f64, 后低: f64) -> Self { + if (前高 - 后高).abs() < f64::EPSILON && (前低 - 后低).abs() < f64::EPSILON { + return Self::同; + } + if 前高 > 后高 && 前低 > 后低 { + if (前低 - 后高).abs() < f64::EPSILON { + return Self::衔接向下; + } + if 前低 > 后高 { + return Self::向下缺口; + } + return Self::向下; + } + if 前高 < 后高 && 前低 < 后低 { + if (前高 - 后低).abs() < f64::EPSILON { + return Self::衔接向上; + } + if 前高 < 后低 { + return Self::向上缺口; + } + return Self::向上; + } + if 前高 >= 后高 && 前低 <= 后低 { + return Self::顺; + } + if 前高 <= 后高 && 前低 >= 后低 { + return Self::逆; + } + panic!("无法识别的方向: 前({},{}), 后({},{})", 前高, 前低, 后高, 后低); + } +} + +impl std::fmt::Display for 相对方向 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::向上 => write!(f, "相对方向.向上"), + Self::向下 => write!(f, "相对方向.向下"), + Self::向上缺口 => write!(f, "相对方向.向上缺口"), + Self::向下缺口 => write!(f, "相对方向.向下缺口"), + Self::衔接向上 => write!(f, "相对方向.衔接向上"), + Self::衔接向下 => write!(f, "相对方向.衔接向下"), + Self::顺 => write!(f, "相对方向.顺"), + Self::逆 => write!(f, "相对方向.逆"), + Self::同 => write!(f, "相对方向.同"), + } + } +} diff --git a/chanlun/src/types/fractal.rs b/chanlun/src/types/fractal.rs new file mode 100644 index 0000000..a8b873a --- /dev/null +++ b/chanlun/src/types/fractal.rs @@ -0,0 +1,109 @@ +use serde::{Deserialize, Serialize}; + +/// 分型结构 —— 三根K线构成的结构形态 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum 分型结构 { + #[serde(rename = "三连向上")] + 上, + #[serde(rename = "三连向下")] + 下, + #[serde(rename = "顶分型")] + 顶, + #[serde(rename = "底分型")] + 底, + #[serde(rename = "向右扩散")] + 散, +} + +impl std::fmt::Display for 分型结构 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::上 => write!(f, "上"), + Self::下 => write!(f, "下"), + Self::顶 => write!(f, "顶"), + Self::底 => write!(f, "底"), + Self::散 => write!(f, "散"), + } + } +} + +impl 分型结构 { + /// 分析三根K线构成的分型结构(泛型版本) + pub fn 分析( + 左: &impl 有高低, + 中: &impl 有高低, + 右: &impl 有高低, + 可以逆序包含: bool, + 忽视顺序包含: bool, + ) -> Option { + Self::分析_内部(左.高(), 左.低(), 中.高(), 中.低(), 右.高(), 右.低(), 可以逆序包含, 忽视顺序包含) + } + + /// 分析三元素构成的分型结构(trait object 版本) + pub fn 分析_对象( + 左: &dyn 有高低, + 中: &dyn 有高低, + 右: &dyn 有高低, + 可以逆序包含: bool, + 忽视顺序包含: bool, + ) -> Option { + Self::分析_内部(左.高(), 左.低(), 中.高(), 中.低(), 右.高(), 右.低(), 可以逆序包含, 忽视顺序包含) + } + + fn 分析_内部( + 左高: f64, 左低: f64, + 中高: f64, 中低: f64, + 右高: f64, 右低: f64, + 可以逆序包含: bool, + 忽视顺序包含: bool, + ) -> Option { + use crate::types::direction::相对方向; + + let 左中关系 = 相对方向::分析(左高, 左低, 中高, 中低); + let 中右关系 = 相对方向::分析(中高, 中低, 右高, 右低); + + let 向上类 = |d: 相对方向| d.是否向上(); + let 向下类 = |d: 相对方向| d.是否向下(); + + match (左中关系, 中右关系) { + // 顺序包含 — 忽视时可以绕过 + (相对方向::顺, _) if !忽视顺序包含 => { + panic!("顺序包含: {:?} {:?}", 左中关系, 中右关系); + } + (_, 相对方向::顺) if !忽视顺序包含 => { + panic!("顺序包含: {:?} {:?}", 左中关系, 中右关系); + } + // 向上 + 向上 = 三连上 + (a, b) if 向上类(a) && 向上类(b) => Some(Self::上), + // 向上 + 向下 = 顶分型 + (a, b) if 向上类(a) && 向下类(b) => Some(Self::顶), + // 向上 + 逆序包含 = 上 + (a, 相对方向::逆) if 向上类(a) && 可以逆序包含 => Some(Self::上), + // 向下 + 向上 = 底分型 + (a, b) if 向下类(a) && 向上类(b) => Some(Self::底), + // 向下 + 向下 = 三连下 + (a, b) if 向下类(a) && 向下类(b) => Some(Self::下), + // 向下 + 逆序包含 = 下 + (a, 相对方向::逆) if 向下类(a) && 可以逆序包含 => Some(Self::下), + // 逆序包含 + 向上 = 底 + (相对方向::逆, a) if 向上类(a) && 可以逆序包含 => Some(Self::底), + // 逆序包含 + 向下 = 顶 + (相对方向::逆, a) if 向下类(a) && 可以逆序包含 => Some(Self::顶), + // 逆序包含 + 逆序包含 = 散 + (相对方向::逆, 相对方向::逆) if 可以逆序包含 => Some(Self::散), + _ => { + eprintln!( + "无法识别的分型结构: 左中={:?}, 中右={:?}", + 左中关系, 中右关系 + ); + None + } + } + } +} + +/// Trait for types that have 高 and 低 (used by 分型结构::分析) +pub trait 有高低 { + fn 高(&self) -> f64; + fn 低(&self) -> f64; +} diff --git a/chanlun/src/types/gap.rs b/chanlun/src/types/gap.rs new file mode 100644 index 0000000..cef1fd0 --- /dev/null +++ b/chanlun/src/types/gap.rs @@ -0,0 +1,51 @@ +use serde::{Deserialize, Serialize}; + +/// 缺口 —— 两个价格区间之间的空隙 +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct 缺口 { + pub 高: f64, + pub 低: f64, +} + +impl 缺口 { + pub fn new(高: f64, 低: f64) -> Self { + assert!(高 > 低, "缺口高必须大于低: 高={高}, 低={低}"); + Self { 高, 低 } + } + + /// 在 [起点, 终点] 区间内居中截取一个子区间 + pub fn 居中截取区间(起点: f64, 终点: f64, 比例: f64) -> Option { + let (起点, 终点) = if 起点 <= 终点 { + (起点, 终点) + } else { + (终点, 起点) + }; + + let 总长 = 终点 - 起点; + let 偏移 = 总长 * 比例; + let 中心 = (起点 + 终点) / 2.0; + + let 下界 = 中心 - 偏移; + let 上界 = 中心 + 偏移; + + if 下界 > 终点 || 上界 < 起点 { + return None; + } + + let 下界 = 下界.max(起点); + let 上界 = 上界.min(终点); + + Some(Self::new(上界.max(下界), 上界.min(下界))) + } +} + +impl std::fmt::Display for 缺口 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "缺口区间<{} <=> {}>", + crate::utils::format_f64_g(self.低), + crate::utils::format_f64_g(self.高) + ) + } +} diff --git a/chanlun/src/types/mod.rs b/chanlun/src/types/mod.rs new file mode 100644 index 0000000..a398ef1 --- /dev/null +++ b/chanlun/src/types/mod.rs @@ -0,0 +1,9 @@ +pub mod bsp_type; +pub mod direction; +pub mod fractal; +pub mod gap; + +pub use bsp_type::买卖点类型; +pub use direction::相对方向; +pub use fractal::分型结构; +pub use gap::缺口; diff --git a/chanlun/src/utils/datetime.rs b/chanlun/src/utils/datetime.rs new file mode 100644 index 0000000..7e0be6d --- /dev/null +++ b/chanlun/src/utils/datetime.rs @@ -0,0 +1,27 @@ +use chrono::DateTime; + +/// 将多种类型统一转为时间戳 (Unix epoch 秒) +pub fn 转化为时间戳(ts: &str) -> Option { + // 尝试解析为整数时间戳 + if let Ok(v) = ts.parse::() { + return Some(v); + } + // 尝试解析为浮点时间戳 + if let Ok(v) = ts.parse::() { + return Some(v as i64); + } + // 尝试 ISO 格式日期字符串 + if let Ok(dt) = DateTime::parse_from_rfc3339(ts) { + return Some(dt.timestamp()); + } + // 尝试 "YYYY-MM-DD HH:MM:SS" 格式 + if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(ts, "%Y-%m-%d %H:%M:%S") { + return Some(dt.and_utc().timestamp()); + } + None +} + +/// 将多种类型转为时间戳数字 (i64) +pub fn 转化为时间戳_数字(ts: &str) -> Option { + 转化为时间戳(ts) +} diff --git a/chanlun/src/utils/format.rs b/chanlun/src/utils/format.rs new file mode 100644 index 0000000..a688b39 --- /dev/null +++ b/chanlun/src/utils/format.rs @@ -0,0 +1,14 @@ +/// Format f64 with Python :g semantics — strip trailing zeros, no scientific notation for common values +pub fn format_f64_g(value: f64) -> String { + if value.is_nan() { + return "nan".to_string(); + } + if value.is_infinite() { + return if value > 0.0 { "inf".to_string() } else { "-inf".to_string() }; + } + + // Use high precision then trim trailing zeros + let s = format!("{:.15}", value); + let s = s.trim_end_matches('0'); + s.trim_end_matches('.').to_string() +} diff --git a/chanlun/src/utils/mod.rs b/chanlun/src/utils/mod.rs new file mode 100644 index 0000000..6070737 --- /dev/null +++ b/chanlun/src/utils/mod.rs @@ -0,0 +1,5 @@ +pub mod datetime; +pub mod format; + +pub use datetime::{转化为时间戳, 转化为时间戳_数字}; +pub use format::format_f64_g;