Metadata-Version: 2.4
Name: chan-lun-core
Version: 0.10.0
Summary: 缠论(缠中说禅理论)量化核心库: 分型/笔/线段/中枢/背驰/买卖点 + 30 分钟跨级别共振确认, 纯 Python 实现, 仅依赖 numpy/pandas
Author: lushi78778
License-Expression: MIT
Project-URL: Homepage, https://github.com/lushi78778/chan-lun
Project-URL: Repository, https://github.com/lushi78778/chan-lun
Keywords: chan,chanlun,缠论,缠中说禅,technical-analysis,quant,stock,kline,trading
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
Classifier: Natural Language :: Chinese (Simplified)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.6
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.14
Requires-Dist: pandas>=0.23
Dynamic: license-file

# chan-lun-core

缠论结构识别与理论辅助计算库。输入单标的、单周期的行情或结构序列，
输出分型、笔、线段、中枢、背驰、买卖点，以及多级分解和辅助状态。
算法用 Python 实现，第三方依赖只有 numpy 与 pandas；MACD 内置计算。

- 发行名：`chan-lun-core`；Python 导入名：`chan`。
- 当前源码版本：`0.10.0`（本地阶段版；公开发布仍为 `0.9.0`；正式
  1.0.0 目标尚未全部验收）。安装环境的实际版本以 `chan.__version__` 为准。
- 版本更迭的完整记录见 [CHANGELOG.md](CHANGELOG.md)（每版定位、主要
  变化、行为变化与发布状态）。
- 仓库：[lushi78778/chan-lun](https://github.com/lushi78778/chan-lun)。
- 许可证：[MIT](LICENSE)。

本库负责结构与指标计算。行情下载、交易日历、复权、跨周期取数、组合管理、
调度和下单由调用方负责。批量入口每次重新计算所提供的完整前缀。

## 阅读路线

| 你是谁 / 要做什么 | 阅读位置 |
|---|---|
| 投资者/使用者：第一次用这个库 | [第一部分 §1—§3](#第一部分投资者视角)：能做什么、安装、快速开始 |
| 投资者/使用者：读懂结果、避免未来函数 | [§4—§7](#4-读懂计算结果)：结果字段、时间语义、买卖点边界、使用守则 |
| 投资者/使用者：评估这个库能不能用 | [§8 可用范围与正式1.0.0门槛](#8-可用范围与正式1000门槛)、[免责声明](#免责声明) |
| 开发者：接入行情、调用全部 API | [第二部分](#第二部分开发者视角)：输入契约、模块与公开入口 |
| 开发者：参与开发、测试、发版 | [§10 从源码开发](#10-从源码开发)、[§17 测试与开发](#17-测试与开发)、[CONTRIBUTING.md](CONTRIBUTING.md) |
| 查某个版本改了什么 | [CHANGELOG.md](CHANGELOG.md) |

---

# 第一部分：投资者视角

## 1. 这个库是什么、能做什么

这是一个**缠论结构计算库**：你把行情给它，它把缠论概念算成明确的
数据对象。它负责"结构是什么"，不负责"该不该买"；信号解释、仓位、
风控和下单由你决定。

### 能力一览

| 能力 | 入口 | 输出 |
|---|---|---|
| K 线包含处理与分型 | `chan_fx_bi` | 无包含 K 线、三 K 分型 |
| 笔（81 课 / 106 课两种标准） | `find_bis`、`chan_fx_bi(bars, standard="106")` | 笔对象列表 |
| 线段 | `find_xds` | 线段对象，区分两种破坏方式 |
| 中枢与走势分类 | `find_zs`、`classify_trend` | 中枢对象、走势类型、事件流 |
| 背驰（MACD 辅助） | `find_trend_bc`、`find_pan_bc` | 趋势/盘整背驰候选 |
| 三类买卖点（笔级简化） | `find_buy_points`、`find_sell_points` | 一/二/三类事件 |
| 二/三类点条件判定 | `find_second_points`、`find_third_points` | 消费已确认走势的精确条件接口 |
| MACD 辅助一类候选 | `find_first_points`、`scan_first_points` | 一类候选与否决诊断 |
| 点位链与底/顶构造 | `analyze_point_chain`、`formation_state`、`fractal_range_state` | 完整点位链、构造状态 |
| 按观察时点计算（防未来） | `analyze_at`、`analyze_available`、`replay_bars`、`replay_available` | 历史可见状态与修订账本 |
| 辅助系统 | 中阴、BOLL、震荡监视、吻、均线九类、板块强弱等 | 辅助状态与提示 |

### 它不是什么

- **不是策略引擎**：不会自动产生"完整精确的交易信号"，多数点位输出
  是候选或条件判定，需要你提供已确认的走势上下文。
- **不下载数据、不下单**：行情来源、复权、跨周期对齐、调度全部由
  调用方负责。
- **没有收益承诺**：结构与时间验收不代替策略研究；详见[免责声明](#免责声明)。
- **理论覆盖有边界**：当前 `bs` 是笔级简化识别，不等同于第 101 课的
  完整精确定义；一类点、实际走势完成和因果级别的自动识别仍在补强。

## 2. 安装与环境检查

### 普通 Python 环境

使用将要运行程序的解释器安装，固定版本便于复现：

```bash
python -m pip install 'chan-lun-core==0.9.0'
python -c "import chan; print(chan.__version__); print(chan.__file__)"
```

`pip` 发行名与 `import` 名不同，不要安装名为 `chan` 的其他发行包来替代。
依赖下限为 Python 3.6、numpy 1.14、pandas 0.23。现代本地开发环境与
聚宽旧环境分开准备，兼容性验证范围见[§17 测试与开发](#17-测试与开发)。

### 聚宽研究 Notebook

一个安装单元格，保留换行：

```python
import sys
!{sys.executable} -m pip install --user chan-lun-core==0.9.0 --no-cache-dir
```

如果当前镜像尚未提供 `0.9.0`，可继续使用已经安装并可导入的版本。
镜像同步时间不由本库控制；版本不可获取时不要把安装失败当成算法错误。
如能访问 GitHub Release，也可安装指定 wheel：

```python
import sys
!{sys.executable} -m pip install --user https://github.com/lushi78778/chan-lun/releases/download/v0.9.0/chan_lun_core-0.9.0-py3-none-any.whl
```

离线时，先下载该 wheel 并上传到 Notebook 能读取的目录，再用当前解释器
对实际文件绝对路径执行 `-m pip install --user /绝对路径/文件.whl`。
保持已有 numpy/pandas，不为安装本库升级聚宽内置环境。

安装后若此前已导入过 `chan`，重启内核，再在新的单元格确认：

```python
import sys
import chan

print("python:", sys.version.split()[0])
print("chan version:", chan.__version__)
print("chan file:", chan.__file__)
print("has find_bis:", hasattr(chan, "find_bis"))
```

各版本能力速查：`0.4.0` 可以使用其已有基础链与理论模块；`analyze_bars`、
行情校验和分型力度从 `0.4.2` 起可用，106 课笔标准从 `0.4.3` 起可用，
MACD 防狼从 `0.4.4` 起可用，底/顶构造从 `0.4.5` 起可用，板块强弱从
`0.5.0` 起可用，观察时点接口从 `0.5.1` 起可用，二类点条件接口从
`0.5.2` 起可用，实际可用时间门控从 `0.7.0` 起可用，自动一类候选与
点位链从 `0.7.4` 起可用。`0.9.1` 及之后的本地小版本未发布到 PyPI。
本文示例按当前源码版本 `0.10.0` 编写，并随契约测试自动执行。
冻结研究脚本的原有版本要求另行保留。

### `chan.__version__` 不存在时

先确认实际导入对象，不要给错误模块手工补版本号：

```python
import sys
import chan

print("python:", sys.version.split()[0])
print("file:", getattr(chan, "__file__", None))
print("path:", list(getattr(chan, "__path__", [])))
print("spec:", getattr(chan, "__spec__", None))
print("version:", getattr(chan, "__version__", None))
print("has find_bis:", hasattr(chan, "find_bis"))
```

| 现象 | 含义与处理 |
|---|---|
| `file=None`、`origin='namespace'`，路径为工作目录 `/home/jquser/chan` | 导入了同名目录形成的命名空间包。用当前内核解释器安装 `chan-lun-core`，重启内核后再查 |
| 文件来自工作区的 `chan.py` 或其他 `chan/__init__.py` | 有同名模块遮蔽。检查导入路径和该文件用途，消除遮蔽后重新启动 |
| 文件来自 `site-packages/chan/__init__.py`，但版本或入口不符合预期 | 检查当前解释器的安装元数据、安装版本及内核缓存 |
| `ModuleNotFoundError` | 当前解释器找不到包；用同一解释器安装，而不是其他终端中的 pip |

聚宽 Python 3.6 可额外检查安装元数据：

```python
import pkg_resources

try:
    dist = pkg_resources.get_distribution("chan-lun-core")
    print("installed:", dist.version, dist.location)
except pkg_resources.DistributionNotFound:
    print("chan-lun-core: 未安装")
```

元数据说明发行包已安装，不单独证明 `import chan` 加载了它。
完整 Chan 工作区提供 `tools/diagnose_chan.py`，可同时核对运行时路径、
版本、安装元数据和关键公开入口。

## 3. 快速开始

下面使用合成行情，完整复制后即可运行，无需行情账号：

```python
import math
import json
from datetime import datetime, timedelta
import pandas as pd
from chan import normalize_bars, analyze_bars

closes = [20.0 + 3.0 * math.sin(i / 6.0) + i / 80.0 for i in range(160)]
df = pd.DataFrame({
    "open": closes,
    "close": closes,
    "high": [price + 0.3 for price in closes],
    "low": [price - 0.3 for price in closes],
    "volume": [1000.0] * len(closes),
}, index=[datetime(2025, 1, 1) + timedelta(days=i) for i in range(160)])

bars = normalize_bars(df)
result = analyze_bars(bars)
print("as_of:", result.as_of)
print("分型/笔/线段:", len(result.fxs), len(result.bis), len(result.xds))
print("笔级中枢:", len(result.bi_zss))
print("买点:", result.buy_points)
print("卖点:", result.sell_points)
snapshot = result.to_dict()
json_text = json.dumps(snapshot, ensure_ascii=False, allow_nan=False)
```

合成数据只演示调用方法，不保证每类结构或信号都会出现。
`analyze_bars` 提供基础链默认参数；需要自定义 MACD、背驰参数、三买力度
门槛或辅助模块时，使用各模块函数组合（见[§15 模块与公开入口](#15-模块与公开入口)）。

接入自己的行情前，先读[§11 行情输入契约](#11-行情输入契约)；用结果做
历史研究前，先读[§5 时间语义](#5-时间语义结构时间不是交易时间)。

## 4. 读懂计算结果

### `AnalysisResult`

| 字段 | 内容 |
|---|---|
| `as_of` | 最后一根输入 bar 的时间；空输入为 None |
| `new_bars`、`fxs`、`bis`、`xds` | 无包含 K 线、分型、笔、线段的对象列表 |
| `bi_zss`、`xd_zss` | 分别由笔和线段得到的中枢，明确分开 |
| `bi_trends` | 笔级中枢的走势分类 |
| `trend_bc`、`pan_bc` | 使用笔级中枢和笔序列的背驰候选 |
| `buy_points`、`sell_points` | 同一笔级口径的买卖点事件列表 |
| `dif`、`dea`、`hist` | 与原始 bars 等长的 numpy 数组，默认参数 12/26/9 |

结果字段不可重新绑定，但内部列表、数组和结构对象可以修改。
每次批量计算独立分配结果；`.to_dict()` 生成深拷贝，将时间转为字符串、
数组转为列表，适合 JSON 存档。不要把任意底层对象都当成统一 JSON schema。

### 结构对象速览

| 对象 | 关键字段 | 含义 |
|---|---|---|
| `NewBar` | `elements` | 包含处理后的 K 线；`elements` 记录原始 bar 下标 |
| `FX` | `bar_index` | 分型；`bar_index` 是无包含序列下标 |
| `BI` | `direction`、起止时间/值、高低点 | 笔；`direction` 为 `up/down` |
| `XD` | `mode`、`gg/dd`、`complete`、`evidence_dt` | 线段；`mode` 为情况 1/2，`gg/dd` 表达内部极值 |
| `ZS` | `zd/zg`、`gg/dd`、`start_dt/end_dt/direction`、`extended_9` | 中枢；`zd/zg` 为边界，`gg/dd` 为构成波动极值 |

## 5. 时间语义：结构时间不是交易时间

这是使用本库最重要的一节。**分型的中心 K 线时间、笔端点时间、买卖点
`dt` 表示结构所在位置，不自动等于当时可识别或可成交的时刻。**

- 分型需要右邻确认，包含合并还可能延长确认等待。
- 未完成的笔、线段和上推结构尾部会随新数据修订。
- `as_of` 只表示输入截止，不证明最后一根 bar 已经收盘。
- 候选的"首次出现"不是"永久确认"：固定输入版本的账本记录过末笔修订
  和买卖候选撤销，首次出现可能被后续数据撤回。

历史回放应逐前缀重算，或读取当时存下的可见快照：

```python
from chan import analyze_bars

observed = []
for stop in range(1, len(bars) + 1):
    current = analyze_bars(bars[:stop])
    observed.append((current.as_of, current.to_dict()))
```

上例说明正确的可见性边界，长样本需要自行控制存储和计算量。
对完整样本最终结果仅按结构端点 `dt` 过滤，不能还原历史可见状态。

更正式的按观察时点入口（`analyze_at`/`replay_bars`）与实际可用时间
门控（`analyze_available`）见[§12](#12-按观察时点计算与追踪修订)和
[§14](#14-按收盘与到达时间观察)。

### 复权与取数截止

取数截止与前复权基准均锚定观察日；聚宽可能填充未来日期行情，调用方需
钳制 `end_date` 并再次过滤返回时间。跨周期数据也按对应观察时刻截断。
本库不复权：若复权因子随观察时点变化，应分别构造各时点的行情前缀。

## 6. 买卖点：候选、条件判定与理论边界

本库的点位能力分两层，不要混淆：

### 第一层：笔级简化识别（`bs` 模块）

`find_buy_points`/`find_sell_points` 在笔级结构上运行，输出候选事件：

| 点类型 | 当前识别条件 |
|---|---|
| 一买/一卖 | 下跌/上涨趋势背驰；当前取首个方向匹配的趋势背驰锚点 |
| 二买/二卖 | 一类点后首次回调/反弹，不破对应一类点极值 |
| 三买/三卖 | 离开中枢后首次回抽/反抽守边界，并比较离开与回拉力度 |

三类点回抽恰好触边界可接受。默认力度比较使用保留两位小数的比值
`> 1`；传入 `min_power_ratio=n` 改为 `>= n`。阈值属于工程参数。
信号公共字段为 `type/dt/price/basis/zs_idx`；事件按判定顺序返回，
需要时间排序时由调用方显式排序。

**理论覆盖边界**：这一层是笔级简化，不等同于第 101 课的完整精确定义。
第 101 课允许二买低于一买的弱情形，现有"不破一类点"门槛尚未覆盖；
一类点、实际走势完成和因果级别的自动识别仍需补强，不能只删除旧
价格门槛扩大输出。

### 第二层：精确条件接口（消费已确认走势）

这些入口要求你先证明输入（已确认结束的次级走势、当时已知的中枢），
它们只做条件判定，不会把笔级候选自动升级为理论事件：

| 入口 | 判定内容 |
|---|---|
| `find_second_points` | 课53/101 二类点：弱二类点、小转大无本级一类点、因果中枢二三合一 |
| `find_third_points` | 三类点：整个首回试区间守 ZG/ZD（触沿允许），同一中枢只输出第一个成立点 |
| `find_first_points` | 课24 MACD 辅助一类候选（不是严格背驰证明） |
| `scan_first_points` | 无候选时的第一项否决诊断，可复核 |
| `confirm_level_up` | 已确认走势类型逐证据到达递归 |
| `analyze_point_chain` | 完整点位链：引发中枢三类点驱动第108课底/顶构造 |
| `formation_state` | 同级同因果中枢首次三类点结束的底/顶构造状态 |
| `fractal_range_state` | 第108课分型区间辅助构造状态（与 `formation_state` 分别命名，不能互换） |

调用示例与完整契约见[§15](#15-模块与公开入口)。

## 7. 使用守则

- **端点时间 ≠ 确认时间**：任何历史研究先做逐前缀回放或读取当时快照；
  严禁用全样本 `complete=True` 给历史端点倒填确认时间。
- **输入只放当时可见的数据**：盘中不要把未收盘 bar 当已收盘；存在
  延迟时用 `analyze_available` 明确 `closed_dt`/`available_dt`。
- **聚宽取数钳制**：`end_date` 钳制到观察日并再次过滤返回日期；
  申万行业指数可能 18:00 后更新，30m 时效需实测。
- **固定版本**：安装固定版本号；升级前先读 [CHANGELOG.md](CHANGELOG.md)
  的"行为变化"条目，保存旧结果并逐前缀比较。
- **候选不是确认**：不把候选首次出现解释为可交易信号；没有确认依据
  的候选保持候选。
- **区分两套口径**：笔级简化候选与精确条件接口的结果分别命名、分别
  存档，不互相冒充。

## 8. 可用范围与正式1.0.0门槛

当前版本可用于：离线结构研究、实际可用前缀分析、候选/确认结构账本，
及调用方提供已确认次级走势后的二/三类点条件判定。它是 **beta 阶段库**，
不是自动产生完整精确交易信号的策略引擎。

正式 1.0.0 前仍需补全：原图形态及真实理论分支标注的完整覆盖、自动
完成走势与级别/因果上下文、均线版点位组合及分型区间底顶定义的收口，
并在聚宽旧环境实跑。没有确认依据的候选保持候选；没有实际发布历史的
数据不宣称完成盘中可见性验证。本库没有收益承诺，结构与时间验收不
代替策略研究。

---

# 第二部分：开发者视角

## 9. 组织方式与架构

采用**数据对象＋纯函数＋组合入口**，按职责组织：

| 层 | 代表入口 | 职责 |
|---|---|---|
| 行情适配与校验 | `normalize_bars`、`validate_bars` | 转换数据格式、检查原始行情契约 |
| 结构与结果对象 | `NewBar`、`FX`、`BI`、`XD`、`ZS`、`AnalysisResult` | 表达结构、持有结果、提供具名字段 |
| 计算函数 | `find_bis`、`find_zs`、`level_up` 等 | 接收明确输入并计算结果，不持有跨调用分析状态 |
| 组合入口 | `analyze_bars`、`analyze_at`、`analyze_available` | 连接基础识别链，区分笔级和线段级结果 |

结构对象已有必要的面向对象表达。一个算法对应一个带 `calculate()` 的类，
不会自动改善其职责和可测试性，因此现阶段保持函数组合。
`track_zs`、`formation_state` 等函数可以计算历史状态，但不会在多次调用间
自动保存上一轮状态。

当前没有 `update/reset` 实时分析器、共享缓存、行情源基类或策略框架。
以后如需实时增量处理，须另行定义未收盘 bar 更新、同时间替换、历史修订、
确认事件和缓存失效，再逐前缀与批量实现比较。
详见 [ARCHITECTURE.md](ARCHITECTURE.md)。

## 10. 从源码开发

本仓库采用 src-layout。包文件在仓库根的 `src/chan/`，不是仓库根本身。

```bash
# 在独立 chan-lun 仓库根运行，使用现代开发解释器
python -m pip install -e .
python -m unittest discover -s tests -p 'test_*.py'
```

完整 Chan 工作区中，本仓库在 `src/chan/`，安装命令为
`python -m pip install -e ./src/chan`。如果显式设置源码导入路径，应该是
工作区的 `src/chan/src`，不是外层 `src` 或 `src/chan`。
构建后端要求现代 setuptools；旧 Python 环境优先安装已构建的 wheel。

开发分层、原文核验、概念版本和交付流程见 [CONTRIBUTING.md](CONTRIBUTING.md)。

## 11. 行情输入契约

### 标准原始 bar

输入为按时间严格递增的 dict 列表或元组，每条至少含六个字段：

```python
bars = [
    {"dt": "2026-01-05", "open": 10.0, "high": 10.8,
     "low": 9.8, "close": 10.5, "volume": 1000.0},
    {"dt": "2026-01-06", "open": 10.5, "high": 11.0,
     "low": 10.2, "close": 10.7, "volume": 1200.0},
]
```

| 字段/条件 | 契约 |
|---|---|
| `dt` | 严格递增、唯一，整段序列使用一致的时间类型与表示 |
| `open/high/low/close` | 有限、严格为正的数值；布尔值不作为价格 |
| 价格包络 | `low <= open/close <= high` |
| `volume` | 有限且非负；单位由调用方统一 |
| 额外字段 | 可存在；批量入口只复制标准 OHLCV 和时间 |
| 空序列 | 合法；批量入口返回空结构，`as_of=None` |

支持 `date/datetime/pandas.Timestamp`；datetime 的 UTC 偏移需保持一致。
字符串可用 `YYYY-MM-DD`，或带秒的 `YYYY-MM-DD HH:MM:SS[.微秒]` /
`YYYY-MM-DDTHH:MM:SS[.微秒]`。同一序列不要混用日期与时间、空格与 T、
不同小数精度或不同类型；字符串形式不支持时区后缀。

```python
from chan import validate_bars

validate_bars(bars)  # 合法时无返回值；坏数据抛出异常
```

`validate_bars` 不排序、不填充、不去重，不静默删除坏行。
错误容器使用 `TypeError`，不符合行情契约的行使用 `ValueError` 并指出位置。
底层算法未统一执行完整校验，直接调用时由调用方保证契约。
正价格限制与当前相对价格力度口径一致；零价、负价资产需要先定义适用口径。

### DataFrame 适配

```python
from chan import normalize_bars, bars_to_df

bars = normalize_bars(
    df, dt_col="date", open_col="open", high_col="high",
    low_col="low", close_col="close", volume_col="volume",
    drop_paused=True,
)
restored_df = bars_to_df(bars)
```

有 `dt_col` 列时取该列，否则取 index；OHLCV 转为 float，并按时间排序。
默认停牌判据是 `volume == 0` 且 `high == low`，满足时剔除。
`normalize_bars` 是格式适配，不代替完整校验；重复时间、NaN 和价格包络
仍须由 `validate_bars` 或 `analyze_bars` 检查。

标的、周期、时区、前复权基准、成交量单位及 bar 是否收盘均由调用方管理。
多标的表须先分组；日线和分钟线分别形成各自的序列。
包含处理后的 `NewBar` 是形态对象，其首开末收可能落在合并形态区间之外，
不要把原始 OHLC 包络校验直接用于它。

## 12. 按观察时点计算与追踪修订

`analyze_at`（0.5.1 起）先筛选原始 `dt<=cutoff` 行情，再计算包含、MACD
和结构。截止值与行情时间使用相同类型、字符串格式和 UTC 偏移。未来
OHLCV 不参与计算；完整时间索引仍须有效且严格递增。返回的 `as_of` 是
最后可见行情时间，可能早于请求截止。行情是否收盘由调用方保证。

```python
from chan import analyze_at, replay_bars

# bars 已按本观察时点可用的复权因子处理, 并且只包含已收盘bar。
cutoff = bars[-1]["dt"]
current = analyze_at(bars, cutoff, bi_standard="106")
for snapshot in replay_bars(bars, observations=[row["dt"] for row in bars[-20:]]):
    for change in snapshot.changes:
        print(change.kind, change.field, change.first_seen, change.observed_dt)
```

`replay_bars` 逐个观察时间独立计算并返回 `ReplaySnapshot`，记录分型、
笔和旧买卖候选的 `appeared/revised/withdrawn`。`first_seen` 仅是当前
观察网格首次出现时间，不能当作理论确认时间。未完成末笔可能被同向
新极值替换；候选撤销保留在账本中。输入历史版本改变时重新建账本。
账本身份适用于固定输入序列，不是跨重采样或数据修订的全局中枢身份。

本入口不复权。若复权因子随观察时点变化，应分别构造各时点的行情前缀
后调用 `analyze_at`，不能拿最终复权行情假定其当时可用。跨周期数据也
须按同一实际观察时间截断。完整样本计算后按结构端点筛选不满足此契约。

## 13. 线段边界与时间确认

`find_xds` 不足三笔返回空列表；首三笔须重叠，不能把一笔当未完成线段。
`XD.complete` 表示给定笔序列下破坏定义成立；`mode` 只区分两种破坏方式。
`evidence_dt/evidence_end_index` 为最后证据笔的端点，不等于实际行情确认时间。
完成段的 `gg/dd` 仅覆盖自身构成笔，后续确认笔另记证据；缺口的第二序列
从假设转折点完整收集，未成立时新极值使旧假设撤销。分界前后不跨界包含。

历史行为修正（升级时注意，详见 [CHANGELOG.md](CHANGELOG.md)）：

- 0.6.1/0.7.0：按原图修正线段分界、缺口确认与区间范围，会改变线段
  及线段级中枢结果；已发布版本和冻结脚本不自动升级。
- 0.7.5：修正特殊包含和内部极值线段。
- 0.7.7：`find_xds` 修正假设转折后首次包含方向，按第 71 课检验局部转折。
- 0.7.8：第 65/78 课反向三笔穿越旧段起价的破坏证据允许旧段的极值在
  内部；完成区间仍使用该段全部真实极值，后继确认笔不计入旧段区间。
- 0.7.9：修正首反向笔内部包含造成的提前完成——一笔反弹仍待确认，
  反向方向已选择或后继线段已破坏后才完成。逆时间传递的两段使用实际
  后继证据时间；旧版部分线段的完成时间和分界会变化，回放须从原始
  行情前缀重新计算。

平端点的完成类型仍能参与递归和中枢形成。自动点位组合通过
`unresolved_move_ids` 显式报告它，当前轮次的有向二/三类判定在首个
平端点前停止，保留此前结果；不会猜方向或删段后拼接。

包测试 `fixtures/xd_originals.json` 用相对价位保留课67/71/81原图不等式，
不声称恢复原图的真实行情。原图完整覆盖仍在正式计划登记。

## 14. 按收盘与到达时间观察

`analyze_at` 的 dt 过滤适合调用方已保证全部 bar 收盘的离线前缀。用于
盘中或存在数据延迟时，使用 `analyze_available`，每根原始 bar 增加两个
字段：`closed_dt` 是本周期结束时间，`available_dt` 是本版本行情可用时间。
日期标签、收盘时间和到达时间须满足 `dt <= closed_dt <= available_dt`。
缺字段报错，不会退回日期标签；本库不推断交易日历或供应历史版本。

```python
from datetime import datetime
from chan import analyze_available, replay_available

# dt是交易日标签; 当日日线在15:05才成为这个输入版本的可用数据。
available_bars = [dict(dt=datetime(2026, 7, 31),
                       closed_dt=datetime(2026, 7, 31, 15),
                       available_dt=datetime(2026, 7, 31, 15, 5),
                       open=4.0, high=4.2, low=3.9, close=4.1, volume=1000)]
assert analyze_available(available_bars, datetime(2026, 7, 31, 14)).as_of is None
visible = analyze_available(available_bars, datetime(2026, 7, 31, 15, 5))
for observed in replay_available(available_bars):
    print(observed.observed_dt, observed.result.as_of)
    print([item.to_dict() for item in observed.structures])
```

`replay_available` 独立重算每个实际可用前缀，保留原分型/笔/旧 bs 候选变更，
并记录保守结构链：后继笔出现后固定前笔；只用这些笔确认线段；三个已确认
线段形成线段构造层中枢。末笔和未完成线段不进入保守确认结构。
`StructureObservation.first_seen` 是该观察网格首次确认可见时间，不是端点
或下单时刻；中枢延伸另有修订记录。`structures` 不包含自动一类点上下文，
也不能直接把其中一笔或一条线段转换为 `ConfirmedMove`。
候选 `buy_points/sell_points` 仍采用旧笔级算法，保留其独立近似范围。

调用方必须提前截断传入分钟行情，确认函数不会替调用方统一裁剪所有输入。
取到信号之后的行情可能改变分型确认与末端结构。

## 15. 模块与公开入口

公开函数和结果类可直接 `from chan import ...`，也可从所属模块导入。
完整导出清单是 `chan.__all__`；以下按用途列出主要入口，阈值和边界细节
见函数 docstring 与模块测试。

### 15.1 基础识别链：分型、笔、线段、中枢

对快速开始的同一个 `df`，可分步运行：

```python
from chan import (
    normalize_bars, validate_bars, chan_fx_bi, find_xds, find_zs,
    classify_trend, macd_series, find_trend_bc, find_pan_bc,
    find_buy_points, find_sell_points,
)

# 1. 行情 DataFrame -> 标准 bar 列表
bars = normalize_bars(df)
validate_bars(bars)
new_bars, fxs, bis = chan_fx_bi(bars)
xds = find_xds(bis)
zss = find_zs(bis)
segment_zss = find_zs(xds)
trends = classify_trend(zss)
dif, dea, hist = macd_series(bars)
trend_bc = find_trend_bc(bis, zss, bars, hist=hist)
pan_bc = find_pan_bc(bis, zss, bars, hist=hist)
buys = find_buy_points(bis, zss, trend_bc)
sells = find_sell_points(bis, zss, trend_bc)
```

| 模块 | 入口/对象 | 用法与边界 |
|---|---|---|
| `bars` | `BAR_KEYS`、`normalize_bars`、`bars_to_df`、`validate_bars` | 原始行情适配与校验 |
| `analysis` | `analyze_bars`、`AnalysisResult` | 基础链批量组合 |
| `fx` | `remove_includes`、`find_fxs`、`NewBar`、`FX` | 先按顺序处理包含，再识别三 K 分型 |
| `bi` | `find_bis`、`chan_fx_bi`、`BI` | 分步或一次返回 `(new_bars, fxs, bis)` |
| `xd` | `find_xds`、`chan_bis_xds`、`XD` | 特征序列识别线段，支持第一种情况及缺口确认 |
| `zs` | `find_zs`、`track_zs`、`ZS`、`ZsEvent` | 返回中枢，或 `(zss, events)` 事件流 |
| `zs` | `zs_relation`、`build_expanded_zs`、`classify_trend` | 中枢关系、扩展构造与走势粗分类 |

默认成笔标准是 81 课：`MIN_K_GAP=3`，两个分型中心在无包含序列的
下标差至少为 4。显式选择 106 课标准：

```python
from chan import analyze_bars, chan_fx_bi

new_bars_106, fxs_106, bis_106 = chan_fx_bi(bars, standard="106")
result_106 = analyze_bars(bars, bi_standard="106")
```

106 课要求两个完整三 K 分型至少六个无包含 K 线单位，中心端点区间触及
方向相应的原始 MA5；默认间隔为 2。直接调用 `find_bis(..., standard="106",
ma5=...)` 时，MA5 必须按每个包含组最后原始下标对齐。
`min_k_gap` 可另设间隔，但不能取消 106 课六 K 单位等条件。

`ZS.zd/zg` 为中枢边界，`gg/dd` 为构成波动的最高/最低点；另有
`start_dt/end_dt/direction/xd_count/extended_9`。
`classify_trend` 分组保留 `zs_list`，顶层 `zd/zg` 是组内首个中枢。
`track_zs` 事件反映形成、延伸、九段升级、完成与新生，其事件 `dt`
仍取源单元端点，不自动提供实际确认时间。

边界约定因概念而异：`zs` 的初始三段要求 `ZD < ZG`；同级分解和
递归使用闭区间交集，`ZD == ZG` 可计为交集。不要跨模块抹平这些差异。

### 15.2 背驰与买卖点

| 模块 | 入口 | 内容 |
|---|---|---|
| `bc` | `ema`、`macd_series`、`segment_area` | EMA、MACD 与段内柱面积 |
| `bc` | `find_trend_bc`、`find_pan_bc` | 趋势/盘整背驰候选，可复用 `hist` |
| `bs` | `find_buy_points`、`find_sell_points` | 第一、二、三类事件 |

MACD 返回 `(dif, dea, hist)`，`hist = 2 * (dif - dea)`。
趋势背驰使用相应趋势与创新极值条件下的柱面积比较；盘整背驰另有位置
过滤参数 `max_dev=0.15`。这些是当前量化识别口径。

不传 `trend_bc` 时只识别第三类。三类点另有
`power_ratio/zs_zd/zs_zg/pull_start_dt/pull_end_dt/pull_start/pull_end`；
二买有 `b1_price`，二卖有 `s1_price`。`zs_idx` 是本次列表的局部索引，
不能用作长期中枢身份。

理论覆盖边界与两层口径的区别见[§6](#6-买卖点候选条件判定与理论边界)；
`second` 提供已确认次级走势及上下文的独立条件入口（下节）。

### 15.3 已确认次级走势的二类点（0.5.2 起）

`find_second_points` 是课53/101 的条件判定入口，独立于旧 `find_buy_points`
和 `find_sell_points`。它接收已经确认结束的次级走势，允许二买低于一买，
支持小转大没有本级一类点的上下文，并可识别给定因果中枢的二三合一。
每个反转独立使用稳定 `context_id`，只取第一次反弹和紧接的第一次回试。

```python
from chan import ConfirmedMove, SecondPointContext, find_second_points

# 条件输入示例：调用方已确认这两个次级走势，以及本级一买上下文。
# 这些对象不是由任意两笔自动转换而来。
moves = [
    ConfirmedMove("rise", "sub", "up", "2026-01-01", "2026-01-03",
                  10, 15, 10, 15, "2026-01-04"),
    ConfirmedMove("pull", "sub", "down", "2026-01-03", "2026-01-05",
                  15, 9, 9, 15, "2026-01-06"),
]
context = SecondPointContext("cycle-A", "buy", "main", "sub",
                             "2026-01-01", 10, "2026-01-02")
assert find_second_points(moves, [context], "2026-01-05") == []
point = find_second_points(moves, [context], "2026-01-06")[0]
assert point.strength == "weak"
assert point.dt == "2026-01-05"          # 回试端点
assert point.confirmed_dt == "2026-01-06"  # 可用时间
```

| 输入 | 契约 |
|---|---|
| `ConfirmedMove` | 次级走势的稳定身份、级别、方向、端点价、全程极值及实际确认时间；未完成尾部不输入 |
| `SecondPointContext` | 一类点或小转大反转锚点、目标/次级级别及上下文确认时间；`ended_dt` 是本次中阴结束的确认时间 |
| `centers` | 可选 `context_id -> ConfirmedCenter`，须为反转前已形成的同级因果中枢；缺省不判最强合一 |
| `ConfirmedDivergence` | 小转大回试创新极值分支所需的盘整背驰证据，按走势身份、级别、方向关联 |

输出 `SecondPoint` 保留端点及最晚必要证据的确认时间。原中枢向上首次
离开后回试全程低点不破 ZG 可标 `also_third=True`，卖点镜像；无额外
力度比门槛。后续中阴结束不会删除此前成立的二类点，但结束后才确认
的点被排除。小转大回试创新低/高时须有当时已确认的盘整背驰证据。

本条件入口不自动识别一类点、走势完成、级别邻接或因果中枢；自动 MACD
辅助接线见[§15.13](#1513-自动一类候选与因果点位链074)。它也不将旧
笔级候选直接升级为精确点。调用方需用当时前缀/存档确定这些输入；
严禁用全样本 `complete=True` 给历史端点倒填确认时间。

### 15.4 已确认次级走势的三类点

`find_third_points` 与 `find_second_points` 共用 `ConfirmedMove` 输入契约，
但独立返回三类点。只消费真实完成的次级走势与当时已知中枢；不会将
笔级候选直接升级为理论事件。整个首回试区间必须守 ZG/ZD，触沿允许，
不加旧笔级算法的力度比门槛。同一中枢只输出第一个成立的三类点。

```python
from chan import ConfirmedMove, ConfirmedCenter, ThirdPointContext, find_third_points
third_moves = [
    ConfirmedMove("leave", "sub", "up", "2026-01-02", "2026-01-04",
                  11, 14, 11, 14, "2026-01-05"),
    ConfirmedMove("return", "sub", "down", "2026-01-04", "2026-01-06",
                  14, 12, 12, 14, "2026-01-07"),
]
third_context = ThirdPointContext("cycle", "main", "sub",
    ConfirmedCenter("center", "main", 10, 12, "2026-01-01"))
assert find_third_points(third_moves, [third_context], "2026-01-06") == []
third_points = find_third_points(third_moves, [third_context], "2026-01-07")
assert third_points[0].kind == "buy3"
assert third_points[0].confirmed_dt == "2026-01-07"
```

输入须包含从该中枢形成起的完整后续走势，不能截掉首回试后把第二次
回试冒充首次。保守口径要求中枢在离开起点前已确认可见；更晚才确认的
中枢不会追认历史离开。次级走势的完成、级别邻接、因果中枢仍由调用方
证明；当前尚不是从 K 线自动识别全部三类点的引擎。

### 15.5 完成走势类型的递归确认（0.7.3）

`confirm_level_up` 只接受已确认的 `ConfirmedMove` 或完整的
`ConfirmedMoveType`，按实际确认时间逐批重算可见前缀。调用方按明确 f1
规则冻结的最低层完整类型也可用后者表示，保留类型、中枢和进入方向。
它沿用允许中枢延伸的 `level_up` 规则，输出已完成走势，未完成尾部和
无中枢残段不输出。输入须同级、同一冻结价格版本，时间及端点价格连续；
乱序确认或缺段报错。原始笔、线段、裸 `MoveType.complete` 不带这些
证明，不能直接输入。

下面只演示调用方已经确认的最低层类型序列；合成端点不构成最低层识别器。

```python
from datetime import datetime, timedelta
from chan import ConfirmedMove, confirm_level_up
completion_prices = [10, 14, 11, 15, 13, 18, 16, 20, 14]
t0 = datetime(2026, 1, 1)
completion_units = [
    ConfirmedMove("u%d" % i, "base", "up" if b > a else "down",
                  t0 + timedelta(days=i), t0 + timedelta(days=i+1),
                  a, b, min(a, b), max(a, b), t0 + timedelta(days=i+2))
    for i, (a, b) in enumerate(zip(completion_prices, completion_prices[1:]))
]
assert confirm_level_up(completion_units, t0 + timedelta(days=8), "L1") == []
completion_result = confirm_level_up(completion_units, t0 + timedelta(days=9), "L1")
assert completion_result[0].end_dt == t0 + timedelta(days=7)
assert completion_result[0].confirmed_dt == t0 + timedelta(days=9)
assert len(completion_result[0].unit_ids) == 7
assert len(completion_result[0].evidence_ids) == 8
next_result = confirm_level_up(completion_result, t0 + timedelta(days=9), "L2")
assert next_result == []  # 单个已完成类型不足以形成上一级中枢。
point_move = completion_result[0].to_confirmed_move()
assert point_move.direction == "up"
```

`unit_ids` 是组成单元，`evidence_ids` 是首次完成时的全部可见次级前缀，
包含可能位于端点之后的完成证据。`confirmed_dt` 取该批证据到达时间，
不能改成端点日或只取组成单元的最晚确认；较晚才调用也保留较早证据时间。
输出无可变嵌套，既有完成结果不随输入增长改变。各次调用须使用同一身份、
坐标和输入版本；供应修订要另存版本，本入口不存历史行情。

`direction` 是原 f2 进入方向，可为 `None`；`to_confirmed_move()` 以端点
净涨跌表示二/三类点所需的整体回抽方向，保留确认时间和全程极值。
平端点盘整显式报错，不能跳过后拼接两侧走势；它仍可参与结构递归。
转换不生成一类点、背驰、反转上下文或因果中枢。级别名由递归相邻关系
声明；继续递归时保留完整 `ConfirmedMoveType`，不要转换后丢失类型及
进入方向。级别不对应固定分钟时长。

### 15.6 同级分解、级别递归与状态

```python
from chan import same_level_decompose, level_up, decompose_levels, level_reading

moves = same_level_decompose(bis)
higher_moves = level_up(moves)
levels = decompose_levels(bis, max_levels=8)
reading = level_reading(levels, at_dt=bars[-1]["dt"]) if bars else []
```

| 模块 | 主要入口 | 内容与解释 |
|---|---|---|
| `decompose` | `MoveType`、`same_level_decompose` | 固定三段中枢的同级分解；不把中枢延伸混入切分 |
| `recurse` | `level_up` | 次级单元递归聚合，处理延伸与更高级别结构 |
| `completion` | `ConfirmedMoveType`、`confirm_level_up` | 已确认类型逐到达前缀递归，冻结组成、中枢、完成证据与确认时刻 |
| `levels` | `LevelDecomposition`、`decompose_levels`、`level_reading`、`change_starts_low` | 多级分解、指定端点时间读数、上下级方向改变自洽性 |
| `zhongyin` | `ZhongYinResult`、`track_zhongyin` | 中阴阶段：进入、中枢形成及结束状态 |
| `zhongyin` | `boll_bands`、`boll_state`、`boll_events`、`boll_bs1_hints` | BOLL 辅助事件与一类点提示 |
| `osc` | `OscReport`、`oscillation_monitor`、`osc_strength`、`zn_next_estimate` | 中枢震荡统计、力度及数值估计 |
| `biaoli` | `bi_state`、`bi_transition_valid`、`disease_stage`、`followup_quality`、`zhongyin_health` | 笔四状态、合法迁移、双级状态与震荡强弱 |

同级分解与递归是不同规则，不能互相替换。`MoveType.complete=False`
表示尾部尚未完成，`kind='incomplete'` 表示没有形成完整走势类别，
两种标记不能混为一谈。

**类型口径**：`same_level_decompose` 的 `kind='up'/'down'` 表示明示
同级切分规则中的中枢核心 ZD/ZG 同向移位；`complete=True` 表示当前
输入的后继结束证据已具备。这两个字段不独立证明课 20 严格趋势或
背驰。`zs_relation` 另比较完整波动 DD/GG，两种口径不可混用。

`decompose_levels` 第 1 级保留输入单元，以后逐级上推；序号不自动对应
1 分钟/5 分钟/日线等时间周期。`level_reading` 按起止端点的闭区间读数，
历史可见性仍要先做前缀计算。

震荡监视器的 `center` 是 `(zd, zg)`，单元需有价格区间与方向属性。
BOLL 阈值和震荡估计都是辅助口径，BOLL 提示不等同于 `bs` 的确认事件。
双级"病情"和"健康"标签相对原走势方向解释，不自动映射为开平仓动作。

### 15.7 力度、转折和均线吻

| 模块 | 主要入口 | 输入/输出职责 |
|---|---|---|
| `strength` | `sma_series`、`ma_areas`、`MaArea`、`find_ma_bc`、`avg_strength_state`、`MaStrengthState` | 两条均线差值面积、同向力度比较、形成中的平均力度 |
| `turn` | `classify_bc_turn`、`guaranteed_rebound_gap` | 背驰后转折三分类与回拉幅度条件 |
| `minor_turn` | `classify_minor_turn` | 小级别背驰引发大级别转折的阶段分类 |
| `kiss` | `alignment_series`、`classify_kiss`、`find_kisses`、`KissEvent` | 两均线体位与飞吻/唇吻/湿吻，保留吻次序和量能原料 |
| `gap` | `Gap`、`find_gaps`、`classify_gap` | 原始 K 线缺口与基于可见后续行情的力度分类 |

`strength` 的短长均线等长，`sma_series` 暖机段为 None；输入价格需完整。
`find_ma_bc` 比较已闭合的同向均线面积，`use_avg=True` 使用平均力度。
`avg_strength_state` 可描述形成中的面积，不应作为已完成背驰替代品。

`find_kisses(short, long, volumes=None, proximity=0.5, min_depth=0.2,
flip_confirm=0.5)` 返回吻事件。`alignment_series` 的 `up/down` 分别表示
短线在长线上方/下方；触零也算湿吻。三个默认比例是对定性描述的量化，
吻本身不自动生成买卖点，需要配合价格结构和背驰。

`classify_bc_turn` 接收原中枢、后续回拉、原趋势方向；`classify_minor_turn`
还可接收次级中枢和三类点信息。分类保留必要条件与充分证据的区别。
缺口最终如何回补依赖后续输入；回放时只能使用当时已知行情。

### 15.8 固定两级与任意多级区间套

```python
from chan import confirm_buy3_event_30m

# 以下变量由调用方提供：evt 为三买事件，bars30 为截止目标观察时刻的30m行情
# result30 = confirm_buy3_event_30m(evt, bars30, stale_days=3,
#                                 min_prev_swing_ratio=0.002)
```

| 入口 | 作用 |
|---|---|
| `find_run_exhaustion` | 指定方向运行段的内部力度衰减 |
| `confirm_buy3_30m`、`confirm_sell3_30m` | 三买/三卖回拉内部背驰与中枢边界确认 |
| `confirm_buy3_event_30m`、`confirm_sell3_event_30m` | 接收 `bs` 事件，自动取边界与 `pull_end_dt`，缺失时取 `dt` |
| `confirm_buy2_30m` | 30m 底分型及一买价格守位的轻量确认 |
| `nested_bc`、`BcNestInput`、`BcNestLevel`、`NestBcState` | 任意多级候选段嵌套与力度判定 |

固定 30m 确认使用段首末 DIF 摆动，端点各顺延两根 K 线吸收 EMA 惯性；
MACD 柱面积只作参考。常见返回状态为 `confirmed/weak/no_exhaustion/broke/
stale/no_data`；找不到相应运行段时也可能出现 `none`。
`note` 给出解释，其他字段按判定路径提供，不能假定每次都有完整力度数据。

库默认 `stale_days=1`（自然日）；历史研究脚本可能明确采用 3。
`min_prev_swing_ratio` 默认为 None；设值后要求对照力度达到最低规模。
三买 `zg_tol`、三卖 `zd_tol` 是保留参数，目前不影响判定；实际弱确认
容忍由 `weak_tol` 控制。二买采用 `pierce_tol`，不套用三买的离开段逻辑。

任意多级 `nested_bc(inputs, strength_fn=None)` 从大级别到小级别接收
候选段链；默认力度为 MACD 柱面积，也可注入 `strength_fn(bars, move)`。
区间不嵌套、未创新极值或力度未衰减等都会使链断开；结果 `complete`
表示给定证据链成立。`BcNestInput.center` 仅用于记录，中枢因果关系和
趋势前提需要调用方核对。

### 15.9 分型力度、均线突破与 MACD 防狼

| 模块 | 入口/对象 | 结果 |
|---|---|---|
| `fxpower` | `classify_fx_power`、`classify_fx`、`FxPowerResult` | 三K形态 `weak/neutral/strong/severe` 及包含标签 |
| `fxpower` | `ma_break_state`、`MaBreakState` | 收盘相对均线的 `none/testing/effective` 历史确认 |
| `macd_guard` | `macd_below_zero`、`macd_guard_series` | DIF 与 DEA 都严格小于零为 True，有效其他值为 False，缺测为 None |

`classify_fx_power(b1, b2, b3, direction='top', long_body=0.35,
small_body=0.15, shadow=0.25)` 假定三 K 分型已由调用方确定；底分型镜像。
三个比例是形态量化阈值，不是原文给出的通用数值。
`classify_fx(fx, new_bars)` 是已有分型对象的便捷入口；NewBar 实体是
包含组首开末收的近似，精细原始形态分析优先使用原始 K 线。

`ma_break_state(closes, mas, direction, confirm=2)` 的观察序列从分型
右邻之后开始；均线先在完整历史上计算再对齐截取。有效突破需连续指定
根数的收盘严格越线，缺测中断连续计数。一旦有效，后续回归不会取消
历史有效记录；有效前的恢复可表现为假突破。

```python
from chan import macd_below_zero, macd_guard_series

assert macd_below_zero(-0.2, -0.1) is True
assert macd_below_zero(0.0, -0.1) is False
assert macd_guard_series([-0.2, None], [-0.1, -0.1]) == [True, None]
```

触零或跨轴只说明负区判据不成立，不等于重新站稳，也不自动产生买入许可。

### 15.10 底部与顶部构造

```python
from chan import FormationEvent, formation_state

# confirmed_dt 来自逐前缀识别或当时存档，不是全样本结构端点
first = FormationEvent("buy1", "2026-01-05", "day")
events = [FormationEvent("buy3", "2026-01-12", "day", "center-A")]
state = formation_state(first, "center-A", events, as_of="2026-01-10")
assert state.in_formation
assert formation_state(first, "center-A", events, "2026-01-12").phase == "completed"
```

`FormationEvent(kind, confirmed_dt, level, center_id=None)` 支持
`buy1/sell1/buy3/sell3`，`level` 为非空级别名；三类点需要稳定的中枢身份。
一类点尚不可见时等待，可见后进入构造。同级、同因果中枢的首次三类点
结束该轮构造：底部由三买完成、三卖失败，顶部镜像。

函数只消费 `as_of` 之前实际确认的事件；未来事件不参与当前判定。
终态保持，同一确认时刻的相冲突三类事件需先去重或消歧，否则拒绝判定。
一类点与因果中枢的关联由调用方提供；分型箱体的粗略底部定义不混入
这个精确走势构造状态。结果类为 `FormationState`。

### 15.11 分型区间的底部／顶部构造（第108课）

`fractal_range_state` 消费已确认并冻结的 `ConfirmedFractalRange`，返回
`waiting / constructing / testing / completed / failed`。底部盘中严格跌破
区间最低点即失败；连续 `confirm` 根收盘严格高于上沿则成功，顶部镜像。
默认 `confirm=2` 是"有效站住"的工程约定，原文没有规定次数。触沿不算破位，
也不计成功次数；未确认的收盘越沿被收回后重新计数。首次终态冻结，后续反向
价格不改写旧构造；新分型另建区间。这是辅助状态，不返回买卖信号或成笔证明。

```python
from datetime import datetime, timedelta
from chan import ConfirmedFractalRange, fractal_range_state
fractal_box = ConfirmedFractalRange("bottom", datetime(2026, 7, 1), 10, 12,
                                    datetime(2026, 7, 2, 15, 5), "day")
range_bars = []
for day, close in [(3, 13), (4, 14)]:
    dt = datetime(2026, 7, day)
    range_bars.append(dict(dt=dt, open=close, high=close+1, low=11,
                           close=close, volume=100,
                           closed_dt=dt+timedelta(hours=15),
                           available_dt=dt+timedelta(hours=15, minutes=5)))
assert fractal_range_state(fractal_box, range_bars,
                           datetime(2026, 7, 3, 15, 5)).phase == "testing"
range_state = fractal_range_state(fractal_box, range_bars,
                                  datetime(2026, 7, 4, 15, 5))
assert range_state.phase == "completed"
assert range_state.end_dt == datetime(2026, 7, 4, 15, 5)
```

区间可取当时 `FX.low / FX.high` 的三根包含处理后 K 线总范围，调用方应明确
选取方式并冻结价格版本，不能从完整历史挑选最终分型回填过去。分型中间
K 线的 `dt` 不是确认时间。确认时间由当时观察或存档提供；保守做法为右邻
合并组被后续无包含 K 线固定后再登记。该入口不自动判断月线分型已形成。

后续行情必须同标的、同周期、同价格基准，携带收盘/到达时间；在分型确认
之前已闭合的行不计入其后构造。状态入口要求可见行的到达时间非降序，
乱序交付或历史修订须由调用方先形成对应版本快照。价格依据所选观察日
可得因子，不使用最新复权箱体配旧行情。这里的分型区间与 `formation_state`
使用一类点及因果中枢的精确走势定义分别命名，不能互换。

### 15.12 均线版辅助候选（第11、12、15课）

`find_ma_points` 从实际已到达的原始行情计算 SMA、吻和封闭面积，返回
`ma_buy1_candidate / ma_sell1_candidate / ma_buy2_candidate / ma_sell2_candidate`。
这些名称与中枢版点位分开；结果不保证反转，也不直接用于下单。

- `post_kiss_divergence`：第二次及以后已完成湿吻后的同向面积衰减，
  同时本段价格高点和低点均顺趋势移动。封闭面积取下一根相交行情到达时确认，
  不能按面积末端提前使用。`last_kiss_proven=False`，不回看未来挑最后一次。
- `first_continuation`：已经观察到体位转化之后的首次已完成同体位中继；
  接受飞吻、唇吻和湿吻，不要求先出现本模块的一类候选。窗口直接始于某体位
  而没有转化证据时不声称"第一次"。默认前腿涨跌强度至少 2%，前腿均量不超过
  此前长均线周期均量的 2 倍；可配置，`max_volume_ratio=None` 明确关闭量能过滤。

原文未规定吻的数值边界、前腿强度和放量阈值；本接口沿用 `find_kisses`
的量化参数并保留调整入口。第一类为保守封闭面积法，不覆盖飞吻／唇吻后的
全部背驰或尚未结束的即时预警，后者见 `avg_strength_state`。价格趋势采用
同向面积腿的高低区间近似，不宣称已经完成精确走势类型划分。

```python
from datetime import datetime, timedelta
from chan import find_ma_points
# 1/2周期仅为易核对的演示，实际参数按所研究周期选择。
ma_point_bars = []
closes = [500.0]
for diff in [-10, -8, 1, -11, -10, 1, -12, -1, 0]:
    closes.append(closes[-1] + 2*diff)
for i, close in enumerate(closes):
    dt = datetime(2026, 1, 1) + timedelta(days=i)
    ma_point_bars.append(dict(dt=dt, open=close, close=close,
                              high=close+1, low=close-1, volume=1,
                              closed_dt=dt+timedelta(hours=15),
                              available_dt=dt+timedelta(hours=15, minutes=5)))
ma_candidates = find_ma_points(ma_point_bars, datetime(2026, 1, 10, 15, 5),
                                short_period=1, long_period=2)
assert ma_candidates[0].kind == "ma_buy1_candidate"
assert ma_candidates[0].dt == datetime(2026, 1, 9)
assert ma_candidates[0].confirmed_dt == datetime(2026, 1, 10, 15, 5)
assert ma_candidates[0].to_dict()["last_kiss_proven"] is False
```

`dt / price` 是证据区间内最早的最低／最高点，`confirmed_dt` 是所需证据的
实际到达时刻，两者不同。只截断当时已关闭并已到达的行情，再计算均线；
调用方负责同标的、同周期、同价格基准及供应历史版本，到达时间非降序。
当时已产生的候选是历史判定事实，后续行情不会把它改为"最终反转成功"。

### 15.13 自动一类候选与因果点位链（0.7.4）

`find_first_points(units, bars, as_of, level)` 消费与
`confirm_level_up` 同契约的已确认次级走势，自动寻找已经完成的上涨/
下跌，追溯最后两个中枢、连接段与离开段，输出 `FirstPoint`。
`bars` 必须提供 `closed_dt`、`available_dt`，所有走势端点须在行情中
精确对应。行情和结构均先按实际到达时间截断。确认晚于离开段端点时，
返回的 `confirmed_dt` 保留这段等待时间。

这是第 24 课的 **MACD 辅助一类候选**，`method='macd_area'`，不是严格
背驰证明。它要求中枢周围波动区间严格分离、最后段创新极值、同向柱
面积减弱、B 双线回拉零轴。面积采用 `(start,end]`，共用端点不重复计数；
"零轴附近"默认量化为双线幅值/收盘价不超过 0.5%，可通过
`zero_axis_ratio` 调整，原文没有该数值。单中枢盘整背驰、未完成走势、
内部极值发生时间未知、零宽中枢等不输出。基础类型真实性、供应版本和
复权价格坐标仍由输入负责；EMA 暖机从所给历史第一根开始。

```python
# units: 连续的ConfirmedMove或ConfirmedMoveType，不是单笔/单线段
# bars: 同坐标行情，带closed_dt和available_dt
points = chan.find_first_points(units, bars, as_of, level='F2')
chain = chan.analyze_point_chain(units, bars, as_of, level='F2')
print(chain.first_points, chain.second_points, chain.third_points)
print(chain.formations)
```

`first_point_contexts` 自动生成每轮 `SecondPointContext`，并区分两个归属：

| 字段 | 用途与可见证据 |
|---|---|
| `previous_center` | 原趋势最后中枢；第 101 课二三合一只在它反转前已知时判定 |
| `causal_center` | 一类候选锚点之后连续次级走势的首次三段重叠中枢；确认取三段实际证据和点位最晚到达时间 |

形成时的中枢边界和身份冻结，尚未形成时为 `None`；零宽交集不能用
现有三类接口表达，不跳过去改选后面的中枢。`analyze_point_chain` 是薄组合
入口，用引发中枢的首次三类点驱动第 108 课底/顶构造，用原趋势中枢判
二三合一；一类候选性质继续沿整个链传递。

`scan_first_points` 返回 `FirstPointScan(points, exclusions, completed_types)`，
在没有候选时可查看完成类型的首个否决条件。它区分盘整、比较段不足、
中枢波动区间重叠、未创新极值、内部极值时间未知、价格证据待到达、
面积没有减弱、未回零轴和零宽中枢。尚未完成的类型不在否决列表中。
这使"没有候选"可以复核，而不把它解释成全部理论分支验证通过。

更大级别重组的中枢身份继承、严格区间套一类证明及真实基础类型人工
验收仍在正式计划跟进。

### 15.14 均线九类与板块强弱

```python
from chan import ma_strength_class, sector_strength

# 原始历史保留 SMA 暖机；start_index 标记当时确定的本轮反弹起点
a = ma_strength_class([10.0] * 233 + [12.0, 9.0], start_index=233)
b = ma_strength_class([10.0] * 235, start_index=233)
report = sector_strength({"asset-A": a, "asset-B": b})
assert (a.class_no, b.class_no, report.mean_class) == (9, 1, 5.0)
```

`ma_strength_class(closes, start_index, periods=MA_PERIODS, confirm=1)`
默认采用 `(5, 13, 21, 34, 55, 89, 144, 233)` 八条 SMA，得到九个类别。
默认攻克标准为一次收盘严格高于相应均线，可通过 `confirm` 调整。
保留本轮已攻克历史，后续回落不自动降类；下一轮由调用方明确起点。

已有均线可用 `classify_ma_strength(closes, ma_by_period, start_index,
confirm=1)`。`MaClassResult` 记录类别、各均线状态和攻克位置。
缺测不当作"从未攻克"；无法判定类别时 `class_no=None`。

`sector_strength` 接收唯一成员标识到 `MaClassResult` 的映射，返回
`SectorStrength`。成员必须使用相同周期与确认标准；调用方还须保证
观察日期、复权、时间周期和反弹起点定义一致。
均值只统计已知类别，需要同时查看 `coverage/valid_count/total_count`；
空板块或无有效成员时均值为 None，不填成第 1 类或 0。

## 16. 原图与真实算术回归

包内测试夹具（fixtures）分两族：**原图几何标注**与**真实行情独立标注**。
通用原则：每组记录来源（原 PDF 页/图名或数据集与摘要）、完整输入、
人工预期、理由及必要确认前缀；预期先冻结后与库输出对比，不是从算法
输出生成预期；上/下镜像和全部逐笔前缀进入回归；编译器脚本不导入
chan、拒绝覆盖输出、fixture 两遍编译逐字节一致；逐前缀回放与未来
毒值（前缀中加入未来数据不得改变既有结论）必须通过。

### 原图几何标注

`fixtures/fx_bi_originals.json` 和 `fixtures/xd_originals.json` 共 92 组
独立相对价格标注。第 62 课定义图 1—9、第 67 课编者八种分析补图及
第 81 课点 5/7 关系的三分支更正均覆盖；还包括第 65 课十二题/两图、
第 71 课作者条件与编者组合、第 78 课古怪/回复区/分析补图和第 77 课
条件结构复刻。第 67/71/78 课的作者原文与编者分析分开引用；课 77
结构复刻不声称恢复原始行情。课 106 历史均线图面核对留在外层来源
登记，缺 OHLC 时不反造报价。

### 真实行情独立标注

真实数据使用 ETF 历史数据集 `v2026-07-31-r2` 的代表 ETF（512880、
159915、518880、588000 等），按观察日锚定复权、核验文件 SHA-256；
整数价格按 0.001 格点核验；人工推导与库输出 100% 一致后冻结。

| 版本 | 夹具/资产 | 覆盖 |
|---|---|---|
| 0.7.10 | `real_foundation.json` | 512880 25 根真实日线：人工合并/分型/81/106 笔及未完成 XD；全部 25 个原始日前缀 |
| 0.8.0 | `real_xd_originals.json` | 四窗口 428 行真实线段：81/106 分叉、第一/第二种完成 XD、真实古怪线段 |
| 0.9.0 | `real_f1_originals.json` | 六窗口 928 行两条 F1 路径与相邻 F2：固定笔 F1 38（盘整 35/上涨 2/下跌 1）、完成 XD 盘整 F1 3 |
| 0.9.1 | `real_xd_f1.json` | 两个自然起点完成 XD 构造 F1 窗口（700 根日线、14 条完成 XD、工程 F1 up/down 各 1） |
| 0.9.2—0.9.6 | `real_strict_trend*.json` | 严格趋势 Zn 与 a+A+b+B+c 完整角色、完成首例 C1/D1/X1/Y1（方向×路径四格闭合） |
| 0.9.5/0.9.7/0.9.8 | 三类点夹具 | 三类买卖点成立/否决/pending 与触沿允许（margin=0）成立、xd 级触沿样本 |
| 0.9.9 | 一类点夹具 | 一类买卖点成立（B1/C1）与不成立（no_divergence/no_new_extreme）正反例 |
| 0.9.10 | 二类点夹具 | 二类买卖点六窗：最弱/一般/最强（二三合一）三档正反例 |
| 0.9.11 | 因果底顶夹具 | 底/顶部完成/失败四格闭合，引发中枢与结局映射 |

F1 的两条最低操作构造规则：

- **F1-bi**：逐原始日前缀重新识别笔，排除仍可延伸的末笔，以固定笔进行
  `same_level_decompose` 的无延伸切分。仅含中枢且后继已确认结束的类型
  才冻结为 F1，单笔不直接包装为完成走势。
- **F1-xd**：从上述固定笔重新计算线段，仅使用 `complete=True` 的 XD，
  再做同级切分；活动 XD、仅有分型确认但后继笔尚未固定的证明均等待。

这些标注不证明严格自动一类、因果继承或正式 1.0 验收已完成；不直接
充当已完成走势类型；`same_level_decompose` 的 `kind/complete` 字段
口径见[§15.6](#156-同级分解级别递归与状态)。详细裁定算术、窗口台账
与测试清单一律登记在完整工作区正式计划 §9 的对应版本锚点。

## 17. 测试与开发

### 源码测试

在独立包仓库根运行：

```bash
python -m pip install -e .
python -m unittest discover -s tests -p 'test_*.py'
# 定向检查公开契约和 README 示例
python -m unittest discover -s tests -p 'test_contracts.py'
```

测试覆盖理论定义分支、退化输入、时间状态、组合入口等价性、输入与
快照隔离、公开导出、版本一致和 Python 3.6 语法。README 的基础链、
批量入口、构造状态和板块强弱示例随契约测试执行——修改 README 示例
必须保持可执行且与输出断言一致。

### 安装产物验证

源码测试主动使用本仓库源码，不能证明 wheel 包含了全部文件。
构建并安装 wheel 后另行运行隔离检查：

```bash
python -m pip install build
python -m build
python -m pip install --force-reinstall --no-deps dist/chan_lun_core-0.9.0-py3-none-any.whl
python -I tools/verify_installed.py
```

检查导入路径来自安装位置、运行时与元数据版本一致、公开入口完整及
代表性计算可运行。`-I` 避免源码路径使漏打包问题被掩盖。
不要在源码安装环境中把常规 `import chan` 的成功当成产物验收。

### 验证范围与交付

CI 在 Python 3.10/3.13 构建及验收，并增加 Python 3.6/旧 numpy 与
pandas 容器运行检查；容器通过不代替聚宽实际取数。tag 发布另有 Release
和 PyPI 步骤。Python 3.6 语法检查、现代依赖本地运行、旧解释器旧依赖
实跑、聚宽取数、真实行情、报告显示和研究有效性是不同层次的验证，
任一项通过不代替其他项。

聚宽已确认 `0.4.0` 可安装并导入，但这不代替全部新增算法在该环境的实跑。
新的辅助结果不会自动接入冻结研究脚本或改变策略参数。

真实行情验证使用 ETF 历史数据集 `v2026-07-31-r2` 的十只代表 ETF
（26,727 根日线与 240,559 根 30m）；核验读取文件 SHA-256，按观察日
锚定复权。真实数据检验的是计算及时间契约；该样本未观察到一类候选
用于底/顶构造、仅有一个可接线的普通二买条件样本，弱二类点与因果
中枢的全部真实分支尚未验证。它不证明理论自动识别完备、交易收益、
旧依赖或聚宽可运行。数据源未提供历史发布版本，无法据此证明历史
因子未被供应方事后修订。详细报告在完整工作区 `doc/validation/`。

开发分层、原文核验、概念版本和交付流程见 [CONTRIBUTING.md](CONTRIBUTING.md)。
完整 Chan 工作区的理论进度唯一维护在 `doc/缠论理论逐一实现计划.md` §6；
本 README 维护用法，版本历史维护在 [CHANGELOG.md](CHANGELOG.md)，
不另设理论完成状态表。

## 18. 版本与发布

- 版本号在 `pyproject.toml` 与 `src/chan/__init__.py` 双处一致，由
  `test_contracts.py` 检查；1.0 前允许行为重构，公开行为变化须在
  [CHANGELOG.md](CHANGELOG.md) 标注"行为变化"并保留旧版本。
- 阶段 tag 始终指向该版已验收的源码提交；本地 tag、远端推送、CI、
  Release/PyPI 分别核验，不把本地完成写成已发布。
- 升级注意：线段相关修正在 0.6.1/0.7.0/0.7.5/0.7.7/0.7.8/0.7.9 多次
  改变旧版线段划分与完成时间（详见[§13](#13-线段边界与时间确认)与
  [CHANGELOG.md](CHANGELOG.md)）；升级前保存旧结果并逐前缀比较。

## 免责声明

本库仅用于研究与学习。缠论信号是对价格结构的客观描述，**不构成任何
投资建议**；交易有风险，使用本库产生的一切后果由使用者自行承担。

## 许可证

MIT，见 [LICENSE](LICENSE)。
