14 Commits

Author SHA1 Message Date
YuWuKunCheng c3e9ae8d77 Nothing v26.5.103 2026-05-30 22:34:10 +08:00
YuWuKunCheng b7c4e60420 1. thread_local! 导致跨线程缓存不可见(主要问题)
kline_py.rs 中 BSP_CACHE、KLINE_IDENTITY、BAR_IDENTITY 使用了 thread_local!。主线程调用 识别买卖点() →
  买卖点信息.add() 写入的是主线程的 PySet,backtrader 策略线程读取的是自己线程独立的空 PySet。

  修复:将三个缓存从 thread_local! 改为全局 static + std::sync::LazyLock<RwLock<HashMap<...>>>

  影响分析

  这些身份缓存的影响与 KLINE_IDENTITY/BAR_IDENTITY 不同——它们只影响 Python 对象身份(is
  比较),不直接影响数据内容(因为 Rust 数据通过 Arc 共享,读写都是同一份)。

  具体后果:
  - 不同线程访问同一个 Rust Arc 会得到不同的 Python wrapper 对象
  - a is b 跨线程比较返回 False,哪怕它们包装同一个底层 Rust 对象
  - 每个线程维护一份独立缓存,内存浪费(不过 wrapper 很小)
2026-05-30 22:15:09 +08:00
YuWuKunCheng 15dc44e8e0 回测测试 2026-05-30 20:06:59 +08:00
YuWuKunCheng bd4fceab02 全量支持 观察者在python端的重写机制 2026-05-30 15:51:18 +08:00
YuWuKunCheng af3ebf13c8 修复 “cargo clippy -- -D warnings” 产生的错误 2026-05-30 13:25:57 +08:00
YuWuKunCheng 22109d8b30 合并 工作流 2026-05-30 13:03:37 +08:00
YuWuKunCheng c2c09fc8ba 合并 工作流 2026-05-30 12:17:22 +08:00
YuWuKunCheng 0eb52cc06a 添加 自动发包 2026-05-30 11:56:06 +08:00
YuWuKunCheng 1e6025a968 添加 原始chan到模块
修复 相对方向的一致性
添加 观察者.投喂原始数据
2026-05-30 11:26:59 +08:00
YuWuKunCheng 14279f3df6 完善 测试类 2026-05-29 23:28:59 +08:00
YuWuKunCheng fca62f3141 修复 买卖点一致性对齐至chan.py 2026-05-29 20:42:34 +08:00
YuWuKunCheng e50172e923 修复 中枢一致性 2026-05-29 20:19:42 +08:00
YuWuKunCheng c87fb66d34 修复 分型时间戳 2026-05-29 17:30:58 +08:00
YuWuKunCheng 9900266516 修复 买卖点 2026-05-29 15:01:02 +08:00
42 changed files with 8834 additions and 2340 deletions
+100 -8
View File
@@ -16,9 +16,87 @@ env:
jobs:
# ============================================================
# Linux x86_64 (manylinux)
# 1. 发布 chanlun 核心库至 crates.io
# ============================================================
publish-crates:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
exists: ${{ steps.check.outputs.exists }}
steps:
- uses: actions/checkout@v4
- name: 安装 Rust 工具链
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: 缓存依赖
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('chanlun/Cargo.lock') }}
- name: 提取版本号
id: version
working-directory: chanlun
run: |
VER=$(cargo metadata --format-version 1 --no-deps 2>/dev/null \
| jq -r '.packages[] | select(.name == "chanlun") | .version')
echo "version=$VER" >> $GITHUB_OUTPUT
echo "当前版本: $VER"
- name: 检查版本是否已存在
id: check
run: |
VER="${{ steps.version.outputs.version }}"
EXISTS=$(curl -sS "https://crates.io/api/v1/crates/chanlun" \
| jq -r --arg v "$VER" '.versions[]?.num // empty | select(. == $v)')
if [ -n "$EXISTS" ]; then
echo "版本 $VER 已存在于 crates.io,跳过发布"
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "版本 $VER 未发布,继续"
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: 格式检查
if: steps.check.outputs.exists == 'false'
working-directory: chanlun
run: cargo fmt --check
- name: Lint 检查
if: steps.check.outputs.exists == 'false'
working-directory: chanlun
run: cargo clippy
- name: 运行测试
if: steps.check.outputs.exists == 'false'
working-directory: chanlun
run: cargo test
- name: 验证打包
if: steps.check.outputs.exists == 'false'
working-directory: chanlun
run: cargo publish --dry-run --allow-dirty
- name: 登录 crates.io
if: steps.check.outputs.exists == 'false'
run: cargo login ${{ secrets.CARGO_TOKEN }}
- name: 发布 chanlun 至 crates.io
if: steps.check.outputs.exists == 'false'
working-directory: chanlun
run: cargo publish --allow-dirty
# ============================================================
# 2. 构建 wheel — Linux x86_64 (manylinux)
# ============================================================
linux-x86_64:
needs: [publish-crates]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -31,6 +109,10 @@ jobs:
- name: 安装 Rust 工具链
uses: dtolnay/rust-toolchain@stable
- name: 更新 cargo 索引(确保新版本可见)
run: cargo update
working-directory: chanlun-py
- name: 构建 wheel (manylinux)
uses: PyO3/maturin-action@v1
with:
@@ -45,11 +127,11 @@ jobs:
name: wheels-linux-x86_64
path: chanlun-py/dist/
# ============================================================
# macOS wheels (x86_64 + arm64)
# 3. 构建 wheel — macOS (x86_64 + arm64)
# ============================================================
macos:
needs: [publish-crates]
runs-on: macos-latest
strategy:
matrix:
@@ -65,6 +147,10 @@ jobs:
with:
python-version: '3.12'
- name: 更新 cargo 索引
run: cargo update
working-directory: chanlun-py
- name: 构建 wheel
uses: PyO3/maturin-action@v1
with:
@@ -79,9 +165,10 @@ jobs:
path: chanlun-py/dist/
# ============================================================
# Windows wheels (x86_64)
# 4. 构建 wheel — Windows x86_64
# ============================================================
windows:
needs: [publish-crates]
runs-on: windows-latest
strategy:
matrix:
@@ -97,6 +184,10 @@ jobs:
with:
python-version: '3.12'
- name: 更新 cargo 索引
run: cargo update
working-directory: chanlun-py
- name: 构建 wheel
uses: PyO3/maturin-action@v1
with:
@@ -111,9 +202,10 @@ jobs:
path: chanlun-py/dist/
# ============================================================
# 源码分发包 (sdist)
# 5. 源码分发包 (sdist)
# ============================================================
sdist:
needs: [publish-crates]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -137,14 +229,14 @@ jobs:
path: chanlun-py/dist/
# ============================================================
# 发布至 PyPI
# 6. 发布至 PyPI
# ============================================================
publish:
needs: [linux-x86_64, macos, windows, sdist]
needs: [publish-crates, linux-x86_64, macos, windows, sdist]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish-to-pypi == 'true'
permissions:
id-token: write # PyPI 信任发布(推荐)
id-token: write
steps:
- name: 下载所有产物
+6 -7
View File
@@ -3,7 +3,7 @@
[![PyPI](https://img.shields.io/pypi/v/chanlun)](https://pypi.org/project/chanlun/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
基于 [chanlun](./chanlun/) Rust 核心库的 PyO3 高性能 Python 绑定,API `chan.py` 完全兼容。
基于 [chanlun](./chanlun/) Rust 核心库的 PyO3 高性能 Python 绑定,API 参考 `chan.py` 设计,高度兼容。
## 安装
@@ -19,8 +19,8 @@ import chanlun
# 创建配置(全部默认值)
config = chanlun.缠论配置()
# 读取 K 线数据文件,创建观察者
obs = chanlun.观察者.读取数据文件("path/to/data.nb", config)
# 读取 K 线数据文件(文件名需遵循 `符号-周期-起始时间戳-结束时间戳.nb` 格式,如 `btcusd-300-1631772074-1632222374.nb`
obs = chanlun.观察者.读取数据文件("path/to/btcusd-300-1631772074-1632222374.nb", config)
# 查看各层级序列
print(f"K线数量: {len(obs.普通K线序列)}")
@@ -29,7 +29,7 @@ print(f"线段数量: {len(obs.线段序列)}")
print(f"中枢数量: {len(obs.中枢序列)}")
# 或使用立体分析器进行多周期分析
analyzer = chanlun.立体分析器("BTCUSD", ["1min", "5min", "30min"], config)
analyzer = chanlun.立体分析器("BTCUSD", [60, 60*5, 60*5*6], config)
# 逐根投喂 K 线...
```
@@ -53,7 +53,6 @@ pip install target/wheels/chanlun-*.whl
```bash
./build.sh develop # 开发安装
./build.sh wheel # 构建 wheel
./build.sh test # 运行集成测试
```
## 导出类
@@ -70,8 +69,8 @@ pip install target/wheels/chanlun-*.whl
## 兼容性
- Python 3.9+
- 类名 / 方法名 / 字段名 / 签名`chan.py` 一致
- 支持 `.nb` / `.dat` 二进制文件格式(大端字节序)
- 类名 / 方法名 / 字段名与 `chan.py` 保持一致
- 支持 `.nb` 二进制文件格式(大端字节序)
## 许可
+59 -25
View File
@@ -28,8 +28,10 @@ SOFTWARE.
import json
import math
import os
import struct
import sys
import tempfile
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
@@ -2202,6 +2204,9 @@ class 缠论K线(object):
return 序列[序列.index() : 序列.index() + 1]
分型模式 = True
class 分型(object):
"""分型 — 由左中右三根缠论K线构成的顶/底分型结构。
@@ -2216,7 +2221,7 @@ class 分型(object):
:ivar 与MACD柱子分型匹配: 是否与MACD柱子分型匹配
"""
__slots__ = ["", "", "", "结构", "时间戳", "分型特征值"]
__slots__ = ["", "", "", "_结构", "_时间戳", "_分型特征值"]
def __init__(self, : Optional[缠论K线], : 缠论K线, : Optional[缠论K线]):
"""
@@ -2229,9 +2234,9 @@ class 分型(object):
self.: Optional[缠论K线] =
self.: 缠论K线 =
self.: Optional[缠论K线] =
self.结构 = .分型
self.时间戳 = .时间戳
self.分型特征值 = .分型特征值
self._结构 = .分型
self._时间戳 = .时间戳
self._分型特征值 = .分型特征值
def __str__(self):
return f"{self..分型}<{self.时间戳}, {self.分型特征值:g}, None: {self. is None}, None: {self. is None}>"
@@ -2239,6 +2244,24 @@ class 分型(object):
def __repr__(self):
return f"{self..分型}<{self.时间戳}, {self.分型特征值:g}, None: {self. is None}, None: {self. is None}>"
@property
def 时间戳(self):
if 分型模式:
return self._时间戳
return self..时间戳
@property
def 分型特征值(self):
if 分型模式:
return self._分型特征值
return self..分型特征值
@property
def 结构(self):
if 分型模式:
return self._结构
return self..分型
@property
def 关系组(self) -> Optional[Tuple[相对方向, 相对方向, 相对方向]]:
"""左、中、右三对相对方向关系
@@ -5055,6 +5078,17 @@ class 观察者:
self.扩展线段序列_扩展线段: List[虚线] = []
self.扩展中枢序列_扩展线段: List[中枢] = []
def 投喂原始数据(self, 时间戳: datetime, : float, : float, : float, : float, : float):
"""便捷入口,直接从 OHLCV 创建 K线 并投喂
:param 时间戳: 时间戳
:param 开: 开盘价
:param 高: 最高价
:param 低: 最低价
:param 收: 收盘价
:param 量: 成交量
"""
self.增加原始K线(K线.创建普K(self.标识, 时间戳, , , , , , 0, self.周期))
@final
def 增加原始K线(self, 普K: K线):
"""核心入口 — 投喂一根原始K线,增量更新所有层级
@@ -5201,33 +5235,32 @@ class 观察者:
self.配置.分析线段中枢 and 中枢.分析(self.线段_线段序列, self.线段_中枢序列)
def 加载本地数据(self, 文件路径: str):
"""重置基础序列后加载数据文件
:param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb
"""
self.重置基础序列()
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], self.周期, self.标识)
self.增加原始K线(k线)
时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量 = struct.unpack(">6d", buffer[i * size : i * size + size])
self.投喂原始数据(转化为时间戳(int(时间戳)), 开盘价, 最高价, 最低价, 收盘价, 成交量)
@classmethod
def 读取数据文件(cls, 文件路径: str, 配置=缠论配置()) -> Self:
"""
def 读取数据文件(cls, 观察员: "观察者", 文件路径: str, 配置=缠论配置()) -> Self:
"""加载数据文件
:param 观察员: 观察者
:param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb
:param 配置: 缠论配置
:return: 观察者实例
"""
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 实例
观察员.符号 = 符号
观察员.周期 = int(周期)
观察员.配置 = 配置
观察员.加载本地数据(文件路径)
return 观察员
class K线合成器:
@@ -5462,10 +5495,10 @@ class 立体分析器:
if 当前K线 := self._K线合成器.获取当前K线(周期):
self._单体分析器[周期].增加原始K线(当前K线)
def 测试_保存数据(self):
def 测试_保存数据(self, root: str = None):
"""拆分各序列数据,单独存文件,文件名为对应变量名"""
# 生成存储根目录
脚本目录 = Path(__file__).parent # 取当前脚本所在文件夹
脚本目录 = Path(__file__).parent if not root else root # 取当前脚本所在文件夹
起始时间 = int(self._单体分析器[self.__输入周期].普通K线序列[0].时间戳.timestamp())
结束时间 = int(self._单体分析器[self.__输入周期].普通K线序列[-1].时间戳.timestamp())
目录标识 = f"PyM_{self._单体分析器[self.__输入周期].标识}_{起始时间}_{结束时间}"
@@ -5480,16 +5513,16 @@ class 立体分析器:
print(f"多级别数据拆分保存完成,目录:{保存路径.resolve()}")
def 测试_读取数据(配置: 缠论配置):
def 测试_读取数据(观察员: 观察者, 配置: 缠论配置) -> Callable[[], "观察者"]:
"""测试_读取数据
:param 观察员: 观察者
:param 配置: 缠论配置
:return: 测试函数
"""
def 魔法():
启动时间 = datetime.now()
观察 = 观察.读取数据文件(配置.加载文件路径, 配置)
观察者.读取数据文件(观察员, 配置.加载文件路径, 配置)
消耗用时 = datetime.now() - 启动时间
print("测试_读取数据 耗时", 消耗用时, "普K数量", len(观察员.普通K线序列))
return 观察员
@@ -5529,5 +5562,6 @@ def 测试_周期合成(配置: 缠论配置, 配置组: Dict[int, 缠论配置]
if __name__ == "__main__":
当前配置 = 缠论配置.不推送()
当前配置.加载文件路径 = str(Path(__file__).parent / "btcusd-300-1761327300-1776327900.nb")
测试_读取数据(当前配置)().测试_保存数据()
测试_周期合成(当前配置)().测试_保存数据()
观察员 = 观察者("", 0, 当前配置)
测试_读取数据(观察员, 当前配置)() # .测试_保存数据()
# 测试_周期合成(当前配置)().测试_保存数据()
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "chanlun-py"
version = "26.5.86"
version = "26.5.103"
edition = "2021"
description = "缠论技术分析库 — Rust 高性能 Python 绑定"
authors = ["YuYuKunKun"]
@@ -12,7 +12,7 @@ crate-type = ["cdylib"]
name = "chanlun"
[dependencies]
chanlun = "26.5.3" # { path = "../chanlun" }
pyo3 = { version = "0.28", features = ["extension-module", "experimental-inspect"] }
chanlun = "26.5.6" # { path = "../chanlun" }
pyo3 = { version = "0.28", features = ["experimental-inspect"] }
serde_json = "1"
chrono = "0.4"
-1
View File
@@ -1 +0,0 @@
../README.md
+77
View File
@@ -0,0 +1,77 @@
# chanlun — 缠论技术分析 Python 绑定
[![PyPI](https://img.shields.io/pypi/v/chanlun)](https://pypi.org/project/chanlun/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
基于 [chanlun](../chanlun/) Rust 核心库的 PyO3 高性能 Python 绑定,API 参考 `chan.py` 设计,高度兼容。
## 安装
```bash
pip install chanlun
```
## 快速开始
```python
import chanlun
# 创建配置(全部默认值)
config = chanlun.缠论配置()
# 读取 K 线数据文件(文件名需遵循 `符号-周期-起始时间戳-结束时间戳.nb` 格式,如 `btcusd-300-1631772074-1632222374.nb`
obs = chanlun.观察者.读取数据文件("path/to/btcusd-300-1631772074-1632222374.nb", config)
# 查看各层级序列
print(f"K线数量: {len(obs.普通K线序列)}")
print(f"笔数量: {len(obs.笔序列)}")
print(f"线段数量: {len(obs.线段序列)}")
print(f"中枢数量: {len(obs.中枢序列)}")
# 或使用立体分析器进行多周期分析
analyzer = chanlun.立体分析器("BTCUSD", [60, 60*5, 60*5*6], config)
# 逐根投喂 K 线...
```
## 从源码构建
前置依赖: [Rust](https://www.rust-lang.org) + [maturin](https://www.maturin.rs)
```bash
pip install maturin
# 开发模式(直接安装到当前 venv)
maturin develop
# 或构建 wheel
maturin build --release
pip install target/wheels/chanlun-*.whl
```
也可使用项目内的 `build.sh`:
```bash
./build.sh develop # 开发安装
./build.sh wheel # 构建 wheel
```
## 导出类
| 类别 | 类名 | 说明 |
|------|------|------|
| 枚举 | `买卖点类型`, `相对方向`, `分型结构` | 缠论基础枚举 |
| 数据 | `缺口`, `K线`, `缠论K线` | K 线数据结构 |
| 结构 | `分型`, `虚线`, `线段特征`, `特征分型` | 分析层级结构 |
| 指标 | `平滑异同移动平均线`, `相对强弱指数`, `随机指标` | MACD/RSI/KDJ |
| 算法 | `笔`, `线段`, `中枢`, `背驰分析` | 识别算法 |
| 业务 | `缠论配置`, `基础买卖点`, `买卖点`, `观察者`, `K线合成器`, `立体分析器` | 分析框架 |
## 兼容性
- Python 3.9+
- 类名 / 方法名 / 字段名与 `chan.py` 保持一致
- 支持 `.nb` 二进制文件格式(大端字节序)
## 许可
MIT
+2
View File
@@ -27,6 +27,8 @@ __all__ = [
"转化为时间戳",
"转化为时间戳_数字",
"随机指标",
"chan",
]
from ._chanlun import *
from . import chan
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "chanlun"
version = "2605.86"
version = "2605.103"
description = "缠论技术分析库 — Rust 高性能实现"
readme = { file = "README.md", content-type = "text/markdown" }
license = { file = "LICENSE", content-type = "text/plain" }
@@ -37,3 +37,7 @@ features = ["pyo3/extension-module"]
python-source = "."
module-name = "chanlun._chanlun"
manifest-path = "Cargo.toml"
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = ["-v", "--tb=short", "--durations=10"]
+92 -94
View File
@@ -31,26 +31,28 @@ use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::RwLock;
thread_local! {
static HUB_IDENTITY: RwLock<HashMap<usize, Py<Py>>> = RwLock::new(HashMap::new());
}
// 使用全局 static 而非 thread_local!,保证跨线程对象标识一致性
static HUB_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
pub(crate) fn hub_to_py(
py: Python<'_>, inner: Arc<chanlun::algorithm::hub::>
) -> Py<Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) =
HUB_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
if let Some(cached) = HUB_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
return cached;
}
HUB_IDENTITY.with(|c| {
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
});
HUB_IDENTITY
.write()
.unwrap()
.retain(|_, v| v.get_refcnt(py) > 1);
let obj = Py::new(py, Py { inner }).unwrap();
HUB_IDENTITY.with(|c| {
c.write().unwrap().insert(key, obj.clone_ref(py));
});
HUB_IDENTITY.write().unwrap().insert(key, obj.clone_ref(py));
obj
}
@@ -362,13 +364,13 @@ impl 笔Py {
: Vec<Py<线Py>>,
: &Bound<'_, Py>,
py: Python<'_>,
) -> Option<线Py> {
) -> Option<Py<线Py>> {
let bi_list: Vec<Arc<chanlun::structure::dash_line::线>> =
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
chanlun::algorithm::bi::::(&bi_list, &.borrow().inner)
.map(|inner| 线Py { inner })
.map(|inner| dashed_to_py(py, inner))
}
#[classmethod]
@@ -378,13 +380,13 @@ impl 笔Py {
: Vec<Py<线Py>>,
: &Bound<'_, Py>,
py: Python<'_>,
) -> Option<线Py> {
) -> Option<Py<线Py>> {
let bi_list: Vec<Arc<chanlun::structure::dash_line::线>> =
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
chanlun::algorithm::bi::::(&bi_list, &.borrow().inner)
.map(|inner| 线Py { inner })
.map(|inner| dashed_to_py(py, inner))
}
#[classmethod]
@@ -396,13 +398,13 @@ impl 笔Py {
K: &Bound<'_, crate::kline_py::K线Py>,
: i64,
py: Python<'_>,
) -> Option<线Py> {
) -> Option<Py<线Py>> {
let bi_list: Vec<Arc<chanlun::structure::dash_line::线>> =
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
chanlun::algorithm::bi::::K找笔(&bi_list, &K.borrow().inner, )
.map(|inner| 线Py { inner })
.map(|inner| dashed_to_py(py, inner))
}
#[classmethod]
@@ -445,6 +447,7 @@ impl 笔Py {
&mut bi_seq,
&ck_list,
&bar_list,
,
&config,
)),
None => Ok(),
@@ -480,7 +483,7 @@ impl 笔Py {
.map(|k| k.bind(py).borrow().inner.clone())
.collect();
let config = .borrow().to_rust_config(py)?;
Ok(chanlun::algorithm::bi::::(
Ok(chanlun::algorithm::bi::::(
Arc::clone(&.borrow().inner),
&mut fr_seq,
&mut bi_seq,
@@ -500,7 +503,7 @@ impl 笔Py {
) -> bool {
let obs = .borrow();
let obs_ref = obs.obs();
chanlun::algorithm::bi::::(&.borrow().inner, &*obs_ref)
chanlun::algorithm::bi::::(&.borrow().inner, &obs_ref)
}
#[classmethod]
@@ -509,12 +512,13 @@ impl 笔Py {
_cls: &Bound<'_, PyType>,
: &Bound<'_, 线Py>,
: &Bound<'_, Py>,
) -> Vec<线Py> {
py: Python<'_>,
) -> Vec<Py<线Py>> {
let obs = .borrow();
let obs_ref = obs.obs();
chanlun::algorithm::bi::::(&.borrow().inner, &*obs_ref)
chanlun::algorithm::bi::::(&.borrow().inner, &obs_ref)
.into_iter()
.map(|d| 线Py { inner: Arc::new(d) })
.map(|d| dashed_to_py(py, Arc::new(d)))
.collect()
}
@@ -528,7 +532,7 @@ impl 笔Py {
) -> Vec<Py<K线Py>> {
let obs = .borrow();
let obs_ref = obs.obs();
chanlun::algorithm::bi::::(&.borrow().inner, &*obs_ref)
chanlun::algorithm::bi::::(&.borrow().inner, &obs_ref)
.into_iter()
.map(|ck| chan_kline_to_py(py, ck))
.collect()
@@ -568,8 +572,8 @@ impl 线段Py {
: &Bound<'_, 线Py>,
) -> PyResult<()> {
let bi_rc = Arc::clone(&.borrow().inner);
let mut ref_mut = .borrow_mut();
chanlun::algorithm::segment::线::线(&mut ref_mut.inner, bi_rc);
let ref_mut = .borrow_mut();
chanlun::algorithm::segment::线::线(&ref_mut.inner, bi_rc);
Ok(())
}
@@ -581,9 +585,9 @@ impl 线段Py {
: &Bound<'_, Py>,
: u32,
) -> PyResult<()> {
let mut ref_mut = .borrow_mut();
let ref_mut = .borrow_mut();
chanlun::algorithm::segment::线::(
&mut ref_mut.inner,
&ref_mut.inner,
&Arc::clone(&.borrow().inner),
,
);
@@ -593,8 +597,8 @@ impl 线段Py {
#[classmethod]
/// 武终
fn (_cls: &Bound<'_, PyType>, : &Bound<'_, 线Py>, : u32) -> PyResult<()> {
let mut ref_mut = .borrow_mut();
chanlun::algorithm::segment::线::(&mut ref_mut.inner, );
let ref_mut = .borrow_mut();
chanlun::algorithm::segment::线::(&ref_mut.inner, );
Ok(())
}
@@ -606,12 +610,12 @@ impl 线段Py {
: Vec<Py<线Py>>,
py: Python<'_>,
) -> PyResult<()> {
let mut ref_mut = .borrow_mut();
let ref_mut = .borrow_mut();
let rc_list: Vec<Arc<chanlun::structure::dash_line::线>> =
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
chanlun::algorithm::segment::线::(&mut ref_mut.inner, &rc_list);
chanlun::algorithm::segment::线::(&ref_mut.inner, &rc_list);
Ok(())
}
@@ -623,12 +627,12 @@ impl 线段Py {
: Vec<Py<线Py>>,
py: Python<'_>,
) -> PyResult<()> {
let mut ref_mut = .borrow_mut();
let ref_mut = .borrow_mut();
let rc_list: Vec<Arc<chanlun::structure::dash_line::线>> =
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
chanlun::algorithm::segment::线::(&mut ref_mut.inner, &rc_list);
chanlun::algorithm::segment::线::(&ref_mut.inner, &rc_list);
Ok(())
}
@@ -691,7 +695,7 @@ impl 线段Py {
let seq: Vec<Option<Arc<chanlun::structure::segment_feat::线>>> = if .is_none()
{
vec![]
} else if let Ok(list) = .downcast::<pyo3::types::PyList>() {
} else if let Ok(list) = .cast::<pyo3::types::PyList>() {
let mut result = Vec::with_capacity(list.len());
for item in list.iter() {
if item.is_none() {
@@ -707,8 +711,8 @@ impl 线段Py {
"序列 必须是 list 或 None",
));
};
let mut ref_mut = .borrow_mut();
chanlun::algorithm::segment::线::(&mut ref_mut.inner, seq, );
let ref_mut = .borrow_mut();
chanlun::algorithm::segment::线::(&ref_mut.inner, seq, );
Ok(())
}
@@ -720,22 +724,26 @@ impl 线段Py {
: &Bound<'_, Py>,
py: Python<'_>,
) -> PyResult<()> {
let mut ref_mut = .borrow_mut();
let ref_mut = .borrow_mut();
let config = .borrow().to_rust_config(py)?;
chanlun::algorithm::segment::线::(&mut ref_mut.inner, &config);
chanlun::algorithm::segment::线::(&ref_mut.inner, &config);
Ok(())
}
#[classmethod]
/// 查找贯穿伤
fn 穿(_cls: &Bound<'_, PyType>, : &Bound<'_, 线Py>) -> Option<线Py> {
fn 穿(
_cls: &Bound<'_, PyType>, : &Bound<'_, 线Py>
) -> Option<Py<线Py>> {
let py = _cls.py();
chanlun::algorithm::segment::线::穿(&.borrow().inner)
.map(|inner| 线Py { inner })
.map(|inner| dashed_to_py(py, inner))
}
#[classmethod]
#[pyo3(signature = (段, 所属中枢 = None))]
/// 将线段基础序列分割为前/后/第三买卖/贯穿伤四部分
#[allow(clippy::type_complexity)]
fn (
_cls: &Bound<'_, PyType>,
: &Bound<'_, 线Py>,
@@ -749,28 +757,21 @@ impl 线段Py {
)> {
let borrowed = .borrow();
let (a, b, c, d) = if let Some(hub_bound) = {
if let Ok(mut hub_ref) = hub_bound.extract::<PyRefMut<'_, Py>>() {
let inner_mut = Arc::make_mut(&mut hub_ref.inner);
chanlun::algorithm::segment::线::(&borrowed.inner, Some(inner_mut))
if let Ok(hub_ref) = hub_bound.extract::<PyRef<'_, Py>>() {
chanlun::algorithm::segment::线::(
&borrowed.inner,
Some(&hub_ref.inner),
)
} else {
chanlun::algorithm::segment::线::(&borrowed.inner, None)
}
} else {
chanlun::algorithm::segment::线::(&borrowed.inner, None)
};
let wrap = |v: Vec<Arc<chanlun::structure::dash_line::线>>| -> PyResult<Vec<Py<线Py>>> {
let mut result = Vec::new();
for x in v {
result.push(Py::new(py, 线Py { inner: x })?);
}
Ok(result)
let wrap = |v: Vec<Arc<chanlun::structure::dash_line::线>>| -> Vec<Py<线Py>> {
v.into_iter().map(|x| dashed_to_py(py, x)).collect()
};
Ok((
wrap(a)?,
wrap(b)?,
wrap(c)?,
d.map(|x| Py::new(py, 线Py { inner: x })).transpose()?,
))
Ok((wrap(a), wrap(b), wrap(c), d.map(|x| dashed_to_py(py, x))))
}
#[classmethod]
@@ -781,9 +782,9 @@ impl 线段Py {
: &Bound<'_, Py>,
py: Python<'_>,
) -> PyResult<()> {
let mut ref_mut = .borrow_mut();
let ref_mut = .borrow_mut();
let config = .borrow().to_rust_config(py)?;
chanlun::algorithm::segment::线::(&mut ref_mut.inner, &config);
chanlun::algorithm::segment::线::(&ref_mut.inner, &config);
Ok(())
}
@@ -795,16 +796,14 @@ impl 线段Py {
: &Bound<'_, Py>,
py: Python<'_>,
) -> PyResult<(Py<PyAny>, Py<PyAny>, Py<PyAny>)> {
let mut ref_mut = .borrow_mut();
let ref_mut = .borrow_mut();
let config = .borrow().to_rust_config(py)?;
let (a, b, c) = chanlun::algorithm::segment::线::(
&mut ref_mut.inner,
&config,
);
let (a, b, c) =
chanlun::algorithm::segment::线::(&ref_mut.inner, &config);
let pk_list = |v: Vec<Arc<chanlun::algorithm::hub::>>| -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in v {
list.append(Py::new(py, Py { inner: h })?)?;
list.append(hub_to_py(py, h))?;
}
Ok(list.into())
};
@@ -815,7 +814,7 @@ impl 线段Py {
/// 内部方法:向线段序列添加新线段
fn _添加线段(
_cls: &Bound<'_, PyType>,
线: &Bound<'_, PyAny>,
_线段序列: &Bound<'_, PyAny>,
线: &Bound<'_, 线Py>,
: &Bound<'_, Py>,
: String,
@@ -836,12 +835,12 @@ impl 线段Py {
/// 内部方法:从线段序列弹出最后一个线段
fn _弹出线段(
_cls: &Bound<'_, PyType>,
线: &Bound<'_, PyAny>,
_线段序列: &Bound<'_, PyAny>,
线: &Bound<'_, 线Py>,
: &Bound<'_, Py>,
: String,
py: Python<'_>,
) -> PyResult<Option<线Py>> {
) -> PyResult<Option<Py<线Py>>> {
let config = .borrow().to_rust_config(py)?;
let mut seg_seq: Vec<Arc<chanlun::structure::dash_line::线>> = vec![];
let result = chanlun::algorithm::segment::线::_弹出线段(
@@ -850,7 +849,7 @@ impl 线段Py {
&config,
,
);
Ok(result.map(|inner| 线Py { inner }))
Ok(result.map(|inner| dashed_to_py(py, inner)))
}
#[classmethod]
@@ -1004,7 +1003,7 @@ impl 线段Py {
线: &Bound<'_, 线Py>,
: u32,
py: Python<'_>,
) -> PyResult<Option<线Py>> {
) -> PyResult<Option<Py<线Py>>> {
let mut seg_seq: Vec<Arc<chanlun::structure::dash_line::线>> = 线
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
@@ -1014,7 +1013,7 @@ impl 线段Py {
&Arc::clone(&线.borrow().inner),
,
);
Ok(result.map(|inner| 线Py { inner }))
Ok(result.map(|inner| dashed_to_py(py, inner)))
}
#[classmethod]
@@ -1050,7 +1049,7 @@ impl 线段Py {
let obs_ref = obs.obs();
chanlun::algorithm::segment::线::线(
&.borrow().inner,
&*obs_ref,
&obs_ref,
)
}
@@ -1060,12 +1059,13 @@ impl 线段Py {
_cls: &Bound<'_, PyType>,
: &Bound<'_, 线Py>,
: &Bound<'_, Py>,
) -> Vec<线Py> {
py: Python<'_>,
) -> Vec<Py<线Py>> {
let obs = .borrow();
let obs_ref = obs.obs();
chanlun::algorithm::segment::线::(&.borrow().inner, &*obs_ref)
chanlun::algorithm::segment::线::(&.borrow().inner, &obs_ref)
.into_iter()
.map(|d| 线Py { inner: Arc::new(d) })
.map(|d| dashed_to_py(py, Arc::new(d)))
.collect()
}
@@ -1079,7 +1079,7 @@ impl 线段Py {
) -> Vec<Py<K线Py>> {
let obs = .borrow();
let obs_ref = obs.obs();
chanlun::algorithm::segment::线::(&.borrow().inner, &*obs_ref)
chanlun::algorithm::segment::线::(&.borrow().inner, &obs_ref)
.into_iter()
.map(|ck| chan_kline_to_py(py, ck))
.collect()
@@ -1211,10 +1211,8 @@ impl 中枢Py {
#[getter]
/// :return: 中枢方向(首条虚线的方向翻转)
fn (&self) -> Py {
Py {
inner: self.inner.(),
}
fn (&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.())
}
#[getter]
@@ -1263,7 +1261,7 @@ impl 中枢Py {
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in self.inner.() {
list.append(线Py { inner: d })?;
list.append(dashed_to_py(py, d))?;
}
Ok(list.into())
}
@@ -1325,7 +1323,7 @@ impl 中枢Py {
dict.set_item("标识", self.())?;
dict.set_item("级别", self.())?;
dict.set_item("图表标题", self.())?;
dict.set_item("方向", self.())?;
dict.set_item("方向", self.(py))?;
dict.set_item("", self.())?;
dict.set_item("", self.())?;
dict.set_item("高高", self.())?;
@@ -1380,16 +1378,15 @@ impl 中枢Py {
: &Bound<'_, 线Py>,
: i64,
: &str,
) -> Self {
Self {
inner: Arc::new(chanlun::algorithm::hub::::(
Arc::clone(&.borrow().inner),
Arc::clone(&.borrow().inner),
Arc::clone(&.borrow().inner),
,
,
)),
}
) -> Py<Py> {
let inner = Arc::new(chanlun::algorithm::hub::::(
Arc::clone(&.borrow().inner),
Arc::clone(&.borrow().inner),
Arc::clone(&.borrow().inner),
,
,
));
hub_to_py(_cls.py(), inner)
}
#[classmethod]
@@ -1400,7 +1397,7 @@ impl 中枢Py {
: &Bound<'_, Py>,
: &str,
py: Python<'_>,
) -> Option<Self> {
) -> Option<Py<Py>> {
let rc_list: Vec<Arc<chanlun::structure::dash_line::线>> = 线
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
@@ -1410,7 +1407,7 @@ impl 中枢Py {
.borrow().inner,
,
)
.map(|inner| Self { inner })
.map(|inner| hub_to_py(py, inner))
}
#[classmethod]
@@ -1420,8 +1417,9 @@ impl 中枢Py {
: &Bound<'_, PyAny>,
: &Bound<'_, Self>,
) -> PyResult<()> {
let py = .py();
let inner = Arc::clone(&.borrow().inner);
let wrapper = Py::new(.py(), Self { inner })?;
let wrapper = hub_to_py(py, inner);
.call_method1("append", (wrapper,))?;
Ok(())
}
@@ -1431,7 +1429,7 @@ impl 中枢Py {
fn (
_cls: &Bound<'_, PyType>,
: &Bound<'_, PyAny>,
: &Bound<'_, Self>,
_待弹出中枢: &Bound<'_, Self>,
) -> PyResult<Option<Self>> {
let result = .call_method1("pop", ())?;
if result.is_none() {
+129 -102
View File
@@ -28,14 +28,12 @@ use std::sync::RwLock;
use crate::algorithm_py::hub_to_py;
use crate::kline_py::bar_to_py;
use crate::structure_py::{dashed_to_py, fractal_to_py};
use crate::structure_py::{dashed_to_py, fractal_to_py, Py};
use std::collections::HashMap;
use std::sync::Arc;
use crate::algorithm_py::Py;
use crate::config_py::Py;
use crate::kline_py::{K线Py, K线Py};
use crate::structure_py::{Py, 线Py};
use crate::types_py::Py;
// ========== 基础买卖点 ==========
@@ -48,7 +46,12 @@ use crate::types_py::买卖点类型Py;
/// 有效性: bool — 买卖点是否仍有效
/// 与MACD柱子匹配: bool|None — 是否与MACD柱状图方向匹配
/// 与MACD柱子分型匹配: bool|None — 是否与MACD柱分型匹配
#[pyclass(name = "基础买卖点", module = "chanlun._chanlun", from_py_object)]
#[pyclass(
name = "基础买卖点",
module = "chanlun._chanlun",
subclass,
from_py_object
)]
#[derive(Clone)]
pub struct Py {
pub(crate) inner: chanlun::business::bsp::,
@@ -71,6 +74,7 @@ impl 基础买卖点Py {
Arc::clone(&.borrow().inner),
,
,
K线.borrow().inner.,
),
}
}
@@ -94,10 +98,8 @@ impl 基础买卖点Py {
}
#[getter]
fn (&self) -> Py {
Py {
inner: Arc::clone(&self.inner.),
}
fn (&self, py: Python<'_>) -> Py<Py> {
fractal_to_py(py, Arc::clone(&self.inner.))
}
#[getter]
@@ -199,19 +201,17 @@ impl 基础买卖点Py {
// ========== 买卖点 ==========
/// 买卖点 — 静态方法容器,提供各类买卖点的构造算法(不存储数据)
/// 买卖点 — 继承 基础买卖点,添加工厂类方法
///
/// 类方法(均返回对应的买卖点对象:
/// 类方法(均返回 买卖点 实例:
/// 一卖点(...) / 一买点(...) / 二卖点(...) / 二买点(...) / 三卖点(...) / 三买点(...)
/// 生成买卖点(特征, 序号, 级别, 分型, 当前缠K, 备注?) -> 买卖点
/// — 根据特征字符串自动路由到对应的一/二/三类买卖点构造函数
#[pyclass(name = "买卖点", module = "chanlun._chanlun")]
/// 生成买卖点(特征, 序号, 级别, 分型, 当前缠K) -> 买卖点
#[pyclass(name = "买卖点", module = "chanlun._chanlun", extends=基础买卖点Py)]
pub struct Py;
#[pymethods]
impl Py {
#[classmethod]
/// :param 买卖点分型: 买卖点对应的分型
fn (
_cls: &Bound<'_, PyType>,
: &Bound<'_, Py>,
@@ -219,20 +219,23 @@ impl 买卖点Py {
: &str,
: String,
: f64,
) -> Py {
Py {
py: Python<'_>,
) -> PyResult<Py<Self>> {
let base = Py {
inner: chanlun::business::bsp::::(
Arc::clone(&.borrow().inner),
K线.borrow().inner.clone(),
,
,
,
K线.borrow().inner.,
),
}
};
let init = PyClassInitializer::from(base).add_subclass(Py);
Ok(Bound::new(py, init)?.unbind())
}
#[classmethod]
/// :param 买卖点分型: 买卖点对应的分型
fn (
_cls: &Bound<'_, PyType>,
: &Bound<'_, Py>,
@@ -240,20 +243,23 @@ impl 买卖点Py {
: &str,
: String,
: f64,
) -> Py {
Py {
py: Python<'_>,
) -> PyResult<Py<Self>> {
let base = Py {
inner: chanlun::business::bsp::::(
Arc::clone(&.borrow().inner),
K线.borrow().inner.clone(),
,
,
,
K线.borrow().inner.,
),
}
};
let init = PyClassInitializer::from(base).add_subclass(Py);
Ok(Bound::new(py, init)?.unbind())
}
#[classmethod]
/// :param 买卖点分型: 买卖点对应的分型
fn (
_cls: &Bound<'_, PyType>,
: &Bound<'_, Py>,
@@ -261,20 +267,23 @@ impl 买卖点Py {
: &str,
: String,
: f64,
) -> Py {
Py {
py: Python<'_>,
) -> PyResult<Py<Self>> {
let base = Py {
inner: chanlun::business::bsp::::(
Arc::clone(&.borrow().inner),
K线.borrow().inner.clone(),
,
,
,
K线.borrow().inner.,
),
}
};
let init = PyClassInitializer::from(base).add_subclass(Py);
Ok(Bound::new(py, init)?.unbind())
}
#[classmethod]
/// :param 买卖点分型: 买卖点对应的分型
fn (
_cls: &Bound<'_, PyType>,
: &Bound<'_, Py>,
@@ -282,20 +291,23 @@ impl 买卖点Py {
: &str,
: String,
: f64,
) -> Py {
Py {
py: Python<'_>,
) -> PyResult<Py<Self>> {
let base = Py {
inner: chanlun::business::bsp::::(
Arc::clone(&.borrow().inner),
K线.borrow().inner.clone(),
,
,
,
K线.borrow().inner.,
),
}
};
let init = PyClassInitializer::from(base).add_subclass(Py);
Ok(Bound::new(py, init)?.unbind())
}
#[classmethod]
/// :param 买卖点分型: 买卖点对应的分型
fn (
_cls: &Bound<'_, PyType>,
: &Bound<'_, Py>,
@@ -303,20 +315,23 @@ impl 买卖点Py {
: &str,
: String,
: f64,
) -> Py {
Py {
py: Python<'_>,
) -> PyResult<Py<Self>> {
let base = Py {
inner: chanlun::business::bsp::::(
Arc::clone(&.borrow().inner),
K线.borrow().inner.clone(),
,
,
,
K线.borrow().inner.,
),
}
};
let init = PyClassInitializer::from(base).add_subclass(Py);
Ok(Bound::new(py, init)?.unbind())
}
#[classmethod]
/// :param 买卖点分型: 买卖点对应的分型
fn (
_cls: &Bound<'_, PyType>,
: &Bound<'_, Py>,
@@ -324,20 +339,24 @@ impl 买卖点Py {
: &str,
: String,
: f64,
) -> Py {
Py {
py: Python<'_>,
) -> PyResult<Py<Self>> {
let base = Py {
inner: chanlun::business::bsp::::(
Arc::clone(&.borrow().inner),
K线.borrow().inner.clone(),
,
,
,
K线.borrow().inner.,
),
}
};
let init = PyClassInitializer::from(base).add_subclass(Py);
Ok(Bound::new(py, init)?.unbind())
}
#[classmethod]
/// :param 特征: 特征字符串
#[pyo3(signature = (特征, 序号, 级别, 买卖点分型, 当前缠K))]
fn (
_cls: &Bound<'_, PyType>,
: &str,
@@ -345,8 +364,9 @@ impl 买卖点Py {
: &str,
: &Bound<'_, Py>,
K: &Bound<'_, K线Py>,
) -> Py {
Py {
py: Python<'_>,
) -> PyResult<Py<Self>> {
let base = Py {
inner: chanlun::business::bsp::::(
,
,
@@ -354,7 +374,9 @@ impl 买卖点Py {
Arc::clone(&.borrow().inner),
Arc::clone(&K.borrow().inner),
),
}
};
let init = PyClassInitializer::from(base).add_subclass(Py);
Ok(Bound::new(py, init)?.unbind())
}
}
@@ -534,30 +556,49 @@ impl 观察者Py {
self.obs_mut().K线((*K.borrow().inner).clone());
}
/// 投喂原始数据 — 便捷入口,直接从 OHLCV 创建 K线 并通过 Python 分发 增加原始K线,
/// 确保子类重写的 增加原始K线 被正确调用。
fn (
slf: &Bound<'_, Self>,
: i64,
: f64,
: f64,
: f64,
: f64,
: f64,
) -> PyResult<()> {
let (, ) = {
let me = slf.borrow();
let obs = me.obs();
(obs..clone(), obs.)
};
let kline = bar_to_py(
slf.py(),
Arc::new(chanlun::kline::bar::K线::K(
&, , , , , , , 0, ,
)),
);
slf.call_method1("增加原始K线", (kline,))?;
Ok(())
}
/// 加载本地数据 — 从 .nb 文件加载K线数据(先重置,再通过 Python dispatch 逐根投喂,
/// 确保子类重写的 增加原始K线 被正确调用)。
fn (slf: &Bound<'_, Self>, : &str) -> PyResult<()> {
let py = slf.py();
// 重置基础序列(通过 Python 分发,支持子类重写)
slf.call_method1("重置基础序列", ())?;
// 重置基础序列
slf.borrow_mut().obs_mut().();
// 解析文件得到 K线 列表
let bars = slf
.borrow()
.obs()
.()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e))?;
// 通过 Python dispatch 逐根投喂,确保子类重写生效
for k线 in bars {
let k线_py = Py::new(
py,
K线Py {
inner: Arc::new(k线),
},
)?;
slf.call_method1("增加原始K线", (k线_py,))?;
// 读取文件,通过 Python dispatch 逐根投喂(支持子类重写 增加原始K线)
let data = std::fs::read()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("read file: {}", e)))?;
let size: usize = 48;
for i in 0..data.len() / size {
let offset = i * size;
if let Some((, , , , , )) =
chanlun::kline::bar::K线::(&data[offset..offset + size])
{
slf.call_method1("投喂原始数据", (, , , , , ))?;
}
}
Ok(())
}
@@ -574,10 +615,14 @@ impl 观察者Py {
}
#[classmethod]
#[pyo3(signature = (文件路径, 配置 = None))]
#[pyo3(signature = (观察员, 文件路径, 配置 = None))]
/// :param 观察员: 观察者实例
/// :param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb
/// :param 配置: 缠论配置
/// :return: 观察者实例
fn (
cls: &Bound<'_, PyType>,
_cls: &Bound<'_, PyType>,
: &Bound<'_, Self>,
: &str,
: Option<&Bound<'_, Py>>,
py: Python<'_>,
@@ -605,31 +650,19 @@ impl 观察者Py {
.parse()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("parse period: {}", e)))?;
// 通过 cls 构造实例(支持子类化)
let cfg_py = Py::from_rust_config(&config)?;
let cfg_obj = Py::new(py, cfg_py)?;
let obj = cls.call1((.clone(), , cfg_obj))?;
// 读取文件并通过 Python 分发逐根投喂(支持子类重写 增加原始K线)
let data = std::fs::read()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("read file: {}", e)))?;
let size: usize = 48;
for i in 0..data.len() / size {
let offset = i * size;
if let Some(k线) =
chanlun::kline::bar::K线::from_bytes(&data[offset..offset + size], , &)
{
let k线_py = Py::new(
py,
K线Py {
inner: Arc::new(k线),
},
)?;
obj.call_method1("增加原始K线", (k线_py,))?;
}
// 设置观察员属性
{
let slf_ref = .borrow_mut();
let mut obs_mut = slf_ref.obs_mut();
obs_mut. = ;
obs_mut. = ;
obs_mut. = config;
}
Ok(obj.unbind())
// 调用加载本地数据
.call_method1("加载本地数据", (,))?;
Ok(.clone().unbind().into())
}
// ---- 序列 getters ----
@@ -823,11 +856,11 @@ impl K线合成器Py {
&mut self,
K: &Bound<'_, K线Py>,
py: Python<'_>,
) -> PyResult<Vec<(i64, K线Py)>> {
) -> PyResult<Vec<(i64, Py<K线Py>)>> {
let results = self.inner.K线((*K.borrow().inner).clone());
Ok(results
.into_iter()
.map(|(, k)| (, K线Py { inner: Arc::new(k) }))
.map(|(, k)| (, bar_to_py(py, Arc::new(k))))
.collect())
}
@@ -840,7 +873,8 @@ impl K线合成器Py {
: f64,
: f64,
: f64,
) -> Vec<(i64, K线Py)> {
py: Python<'_>,
) -> Vec<(i64, Py<K线Py>)> {
let min_cycle = self.inner..iter().copied().min().unwrap_or(1);
let k = chanlun::kline::bar::K线::K(
&self.inner.,
@@ -856,22 +890,15 @@ impl K线合成器Py {
let results = self.inner.K线(k);
results
.into_iter()
.map(|(, k2)| {
(
,
K线Py {
inner: Arc::new(k2),
},
)
})
.map(|(, k2)| (, bar_to_py(py, Arc::new(k2))))
.collect()
}
/// 获取指定周期当前正在合成的K线
fn K线(&self, : i64) -> Option<K线Py> {
self.inner.K线().map(|k| K线Py {
inner: Arc::new(k.clone()),
})
fn K线(&self, : i64, py: Python<'_>) -> Option<Py<K线Py>> {
self.inner
.K线()
.map(|k| bar_to_py(py, Arc::new(k.clone())))
}
#[getter]
@@ -920,7 +947,7 @@ impl 立体分析器Py {
};
let cfg_map: Option<HashMap<i64, chanlun::config::>> = match {
Some(dict_any) => {
let dict = dict_any.downcast::<pyo3::types::PyDict>()?;
let dict = dict_any.cast::<pyo3::types::PyDict>()?;
let mut map = HashMap::new();
for (key, value) in dict.iter() {
let period: i64 = key.extract()?;
+6 -2
View File
@@ -239,7 +239,7 @@ impl 缠论配置Py {
) -> PyResult<Py<PyDict>> {
let py = .py();
let result = PyDict::new(py);
if let Ok(default_dict) = .downcast::<PyDict>() {
if let Ok(default_dict) = .cast::<PyDict>() {
for (key, value) in default_dict.iter() {
if .contains(&key)? {
result.set_item(key.clone(), .get_item(&key)?)?;
@@ -252,6 +252,7 @@ impl 缠论配置Py {
}
/// 比较当前配置与另一个配置的差异
#[allow(clippy::type_complexity)]
fn (
&self,
py: Python<'_>,
@@ -290,7 +291,10 @@ impl 缠论配置Py {
Ok(Self { fields })
}
pub(crate) fn to_rust_config(&self, py: Python<'_>) -> PyResult<chanlun::config::> {
pub(crate) fn to_rust_config(
&self,
_py: Python<'_>,
) -> PyResult<chanlun::config::> {
dict_to_rust_config(&self.fields)
}
+61 -51
View File
@@ -139,10 +139,8 @@ impl K线Py {
#[getter]
/// :return: 相对方向.向上(开盘<收盘)或 相对方向.向下(开盘>收盘)
fn (&self) -> Py {
Py {
inner: self.inner.(),
}
fn (&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.())
}
#[getter]
@@ -182,7 +180,7 @@ impl K线Py {
dict.set_item("开盘价", self.())?;
dict.set_item("收盘价", self.())?;
dict.set_item("成交量", self.())?;
dict.set_item("方向", self.())?;
dict.set_item("方向", self.(py))?;
if let Some(v) = self.macd() {
dict.set_item("macd", v)?;
}
@@ -349,26 +347,26 @@ impl K线Py {
#[pyclass(name = "缠论K线", module = "chanlun._chanlun", from_py_object)]
pub struct K线Py {
pub(crate) inner: std::sync::Arc<chanlun::kline::chan_kline::K线>,
bsp_set: std::sync::RwLock<Option<Py<pyo3::types::PySet>>>,
}
impl K线Py {
pub(crate) fn from_rc(inner: std::sync::Arc<chanlun::kline::chan_kline::K线>) -> Self {
Self {
inner,
bsp_set: std::sync::RwLock::new(None),
}
Self { inner }
}
}
thread_local! {
/// 对象标识缓存:Rc 地址 → 规范 Python 对象
/// 确保同一底层 Rc 指针在 Python 侧始终映射到同一 PyObject
/// 对象标识缓存:Arc 地址 → 规范 Python 对象
/// 确保同一底层 Arc 指针在 Python 侧始终映射到同一 PyObject
/// 使用全局 static 而非 thread_local!,保证跨线程对象标识和买卖点信息一致性
static BAR_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<K线Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
static BAR_IDENTITY: RwLock<HashMap<usize, Py<K线Py>>> = RwLock::new(HashMap::new());
static KLINE_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<K线Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
static KLINE_IDENTITY: RwLock<HashMap<usize, Py<K线Py>>> = RwLock::new(HashMap::new());
}
/// 买卖点信息缓存 — 按 Arc 指针全局共享,确保所有 wrapper 看到同一 PySet
static BSP_CACHE: std::sync::LazyLock<RwLock<HashMap<usize, Py<pyo3::types::PySet>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
/// 将 Rc<K线> 转为 Py<K线Py>,确保同一 Rc 地址总是返回同一 Python 对象
pub(crate) fn bar_to_py(
@@ -376,15 +374,16 @@ pub(crate) fn bar_to_py(
inner: std::sync::Arc<chanlun::kline::bar::K线>,
) -> Py<K线Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) =
BAR_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
if let Some(cached) = BAR_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
return cached;
}
let obj = Py::new(py, K线Py { inner }).unwrap();
BAR_IDENTITY.with(|c| {
c.write().unwrap().insert(key, obj.clone_ref(py));
});
BAR_IDENTITY.write().unwrap().insert(key, obj.clone_ref(py));
obj
}
@@ -394,15 +393,19 @@ pub(crate) fn chan_kline_to_py(
inner: std::sync::Arc<chanlun::kline::chan_kline::K线>,
) -> Py<K线Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) =
KLINE_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
if let Some(cached) = KLINE_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
return cached;
}
let obj = Py::new(py, K线Py::from_rc(inner)).unwrap();
KLINE_IDENTITY.with(|c| {
c.write().unwrap().insert(key, obj.clone_ref(py));
});
KLINE_IDENTITY
.write()
.unwrap()
.insert(key, obj.clone_ref(py));
obj
}
@@ -410,7 +413,6 @@ impl Clone for 缠论K线Py {
fn clone(&self) -> Self {
Self {
inner: std::sync::Arc::clone(&self.inner),
bsp_set: std::sync::RwLock::new(None),
}
}
}
@@ -443,19 +445,17 @@ impl 缠论K线Py {
}
#[getter]
fn (&self) -> Py {
Py {
inner: *self.inner..read().unwrap(),
}
fn (&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, *self.inner..read().unwrap())
}
#[getter]
fn (&self) -> Option<crate::types_py::Py> {
fn (&self, py: Python<'_>) -> Option<Py<crate::types_py::Py>> {
self.inner
.
.read()
.unwrap()
.map(|f| crate::types_py::Py { inner: f })
.map(|f| crate::types_py::(py, f))
}
#[getter]
@@ -496,7 +496,7 @@ impl 缠论K线Py {
dict.set_item("时间戳", self.())?;
dict.set_item("", self.())?;
dict.set_item("", self.())?;
dict.set_item("方向", self.())?;
dict.set_item("方向", self.(py))?;
dict.set_item("周期", self.())?;
dict.set_item("标识", self.())?;
dict.set_item("分型特征值", self.())?;
@@ -506,7 +506,7 @@ impl 缠论K线Py {
dict.set_item("与RSI匹配", self.RSI匹配())?;
dict.set_item("与KDJ匹配", self.KDJ匹配())?;
if let Some(v) = self.() {
if let Some(v) = self.(py) {
dict.set_item("分型", v)?;
}
Ok(dict.into())
@@ -534,16 +534,24 @@ impl 缠论K线Py {
#[getter]
/// 创建当前缠K的浅拷贝副本
fn (&self, py: Python<'_>) -> Self {
let mut mirror = Self {
let mirror = Self {
inner: std::sync::Arc::new(self.inner.()),
bsp_set: std::sync::RwLock::new(None),
};
if let Some(ref src_set) = *self.bsp_set.read().unwrap() {
// 复制买卖点信息到镜像
let src_key = Arc::as_ptr(&self.inner) as usize;
let dst_key = Arc::as_ptr(&mirror.inner) as usize;
let cached_src = BSP_CACHE
.read()
.unwrap()
.get(&src_key)
.map(|p| p.clone_ref(py));
if let Some(cached_src) = cached_src {
if let Ok(new_set) = pyo3::types::PySet::empty(py) {
for item in src_set.bind(py).iter() {
for item in cached_src.bind(py).iter() {
let _ = new_set.add(item);
}
mirror.bsp_set = std::sync::RwLock::new(Some(new_set.into()));
let py_set: Py<pyo3::types::PySet> = new_set.into();
BSP_CACHE.write().unwrap().insert(dst_key, py_set);
}
}
mirror
@@ -569,18 +577,20 @@ impl 缠论K线Py {
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
if self.bsp_set.read().unwrap().is_none() {
let set = pyo3::types::PySet::empty(py)?;
for s in self.inner..read().unwrap().iter() {
set.add(s.clone())?;
}
*self.bsp_set.write().unwrap() = Some(set.into());
let key = Arc::as_ptr(&self.inner) as usize;
// 检查全局缓存
let cached = BSP_CACHE.read().unwrap().get(&key).map(|p| p.clone_ref(py));
if let Some(set) = cached {
return Ok(set.into_any());
}
Ok(self
.bsp_set
// 创建新的 PySet 并存入全局缓存
let set = pyo3::types::PySet::empty(py)?;
BSP_CACHE.write().unwrap().insert(key, set.into());
// 重新读取并返回(无法从 insert 获取 Py 引用,需要重新读)
Ok(BSP_CACHE
.read()
.unwrap()
.as_ref()
.get(&key)
.unwrap()
.clone_ref(py)
.into_any())
@@ -641,13 +651,13 @@ impl 缠论K线Py {
: &Bound<'_, Py>,
py: Python<'_>,
) -> PyResult<(Option<Py<Self>>, Option<String>)> {
let mut ck_inner = (*K.borrow().inner).clone();
let ck_inner = (*K.borrow().inner).clone();
let config = .borrow().to_rust_config(py)?;
let prev_ref = K.map(|prev| prev.borrow());
let prev_inner = prev_ref.as_ref().map(|r| r.inner.as_ref());
let (result, mode) = chanlun::kline::chan_kline::K线::(
prev_inner,
&mut ck_inner,
&ck_inner,
&K.borrow().inner,
&config,
);
+47 -29
View File
@@ -22,7 +22,10 @@
* SOFTWARE.
*/
#![allow(non_snake_case, clippy::too_many_arguments)]
use pyo3::prelude::*;
use std::sync::atomic::Ordering;
mod algorithm_py;
mod business_py;
@@ -32,10 +35,24 @@ mod kline_py;
mod structure_py;
mod types_py;
/// 分型模式 — True 时使用构造时缓存值,False 时从 中 缠K 实时读取
#[pyfunction]
fn get_分型模式() -> bool {
chanlun::structure::fractal_obj::.load(Ordering::Relaxed)
}
/// 设置 分型模式
#[pyfunction]
fn set_分型模式(value: bool) {
chanlun::structure::fractal_obj::.store(value, Ordering::Relaxed);
}
/// 缠论技术分析库 — Rust 高性能实现
#[pymodule]
/// 缠论技术分析库 — Rust 高性能实现
fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(get_分型模式, m)?)?;
m.add_function(wrap_pyfunction!(set_分型模式, m)?)?;
// 阶段 1: 枚举和基础类型
types_py::register(m)?;
// 阶段 2: 配置
@@ -56,39 +73,40 @@ fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
#[cfg(test)]
mod tests {
use crate::*;
use pyo3::prelude::*;
#[test]
fn test_rc_pointer_across_getters() {
pyo3::prepare_freethreaded_python();
Python::with_gil(|py| {
fn test_分型模式_get_set() {
// 手动初始化 Python 解释器(cargo test 环境下 auto-initialize 不一定生效)
unsafe {
if pyo3::ffi::Py_IsInitialized() == 0 {
pyo3::ffi::Py_Initialize();
}
}
pyo3::Python::try_attach(|py| {
let module = PyModule::new(py, "test_module").unwrap();
module.add_class::<business_py::Py>().unwrap();
module.add_class::<business_py::Py>().unwrap();
module.add_class::<business_py::Py>().unwrap();
module.add_class::<kline_py::K线Py>().unwrap();
module.add_class::<kline_py::K线Py>().unwrap();
module.add_class::<structure_py::Py>().unwrap();
module.add_class::<structure_py::线Py>().unwrap();
module.add_class::<config_py::Py>().unwrap();
module
.add_function(wrap_pyfunction!(get_分型模式, &module).unwrap())
.unwrap();
module
.add_function(wrap_pyfunction!(set_分型模式, &module).unwrap())
.unwrap();
let config = config_py::Py::from_rust_config(&Default::default()).unwrap();
let obs = business_py::Py::new_impl("btcusd".into(), 300, config, py).unwrap();
// 默认 true
let getter = module.getattr("get_分型模式").unwrap();
let result: bool = getter.call0().unwrap().extract().unwrap();
assert!(result, "分型模式 默认应为 True");
// Feed one K line
let kline = kline_py::K线Py::new_impl(
"btcusd".into(),
1000,
100.0,
105.0,
99.0,
103.0,
1000.0,
0,
300,
);
let kline_ref = kline.into_ref(py);
// ... this is too complex
});
// 设置为 false
let setter = module.getattr("set_分型模式").unwrap();
setter.call1((false,)).unwrap();
let result: bool = getter.call0().unwrap().extract().unwrap();
assert!(!result, "分型模式 应为 False");
// 恢复 true
setter.call1((true,)).unwrap();
let result: bool = getter.call0().unwrap().extract().unwrap();
assert!(result, "分型模式 应为 True");
})
.expect("Python 解释器初始化后 attach 仍失败");
}
}
+110 -93
View File
@@ -29,37 +29,45 @@ use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::RwLock;
use crate::algorithm_py::{hub_to_py, Py};
use crate::algorithm_py::hub_to_py;
use crate::config_py::Py;
use crate::kline_py::{K线Py, K线Py};
use crate::kline_py::{bar_to_py, K线Py, K线Py};
// ---- 身份缓存 (弱引用:通过 refcnt 检测存活,仅缓存持有则视为过期) ----
thread_local! {
static FRACTAL_IDENTITY: RwLock<HashMap<usize, Py<Py>>> = RwLock::new(HashMap::new());
static DASHED_IDENTITY: RwLock<HashMap<usize, Py<线Py>>> = RwLock::new(HashMap::new());
static SEGFEAT_IDENTITY: RwLock<HashMap<usize, Py<线Py>>> = RwLock::new(HashMap::new());
static FEATFRAC_IDENTITY: RwLock<HashMap<usize, Py<Py>>> = RwLock::new(HashMap::new());
}
// 使用全局 static 而非 thread_local!,保证跨线程对象标识一致性
static FRACTAL_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
static DASHED_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<线Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
static SEGFEAT_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<线Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
static FEATFRAC_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
pub(crate) fn fractal_to_py(
py: Python<'_>,
inner: Arc<chanlun::structure::fractal_obj::>,
) -> Py<Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) =
FRACTAL_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
if let Some(cached) = FRACTAL_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
return cached;
}
// 清理 refcnt==1 的过期条目(仅缓存持有,Python 侧已无引用)
FRACTAL_IDENTITY.with(|c| {
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
});
FRACTAL_IDENTITY
.write()
.unwrap()
.retain(|_, v| v.get_refcnt(py) > 1);
let obj = Py::new(py, Py { inner }).unwrap();
FRACTAL_IDENTITY.with(|c| {
c.write().unwrap().insert(key, obj.clone_ref(py));
});
FRACTAL_IDENTITY
.write()
.unwrap()
.insert(key, obj.clone_ref(py));
obj
}
@@ -68,18 +76,23 @@ pub(crate) fn dashed_to_py(
inner: Arc<chanlun::structure::dash_line::线>,
) -> Py<线Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) =
DASHED_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
if let Some(cached) = DASHED_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
return cached;
}
DASHED_IDENTITY.with(|c| {
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
});
DASHED_IDENTITY
.write()
.unwrap()
.retain(|_, v| v.get_refcnt(py) > 1);
let obj = Py::new(py, 线Py { inner }).unwrap();
DASHED_IDENTITY.with(|c| {
c.write().unwrap().insert(key, obj.clone_ref(py));
});
DASHED_IDENTITY
.write()
.unwrap()
.insert(key, obj.clone_ref(py));
obj
}
@@ -88,18 +101,23 @@ pub(crate) fn segfeat_to_py(
inner: Arc<chanlun::structure::segment_feat::线>,
) -> Py<线Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) =
SEGFEAT_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
if let Some(cached) = SEGFEAT_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
return cached;
}
SEGFEAT_IDENTITY.with(|c| {
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
});
SEGFEAT_IDENTITY
.write()
.unwrap()
.retain(|_, v| v.get_refcnt(py) > 1);
let obj = Py::new(py, 线Py { inner }).unwrap();
SEGFEAT_IDENTITY.with(|c| {
c.write().unwrap().insert(key, obj.clone_ref(py));
});
SEGFEAT_IDENTITY
.write()
.unwrap()
.insert(key, obj.clone_ref(py));
obj
}
@@ -108,18 +126,23 @@ pub(crate) fn featfrac_to_py(
inner: Arc<chanlun::structure::feat_fractal::>,
) -> Py<Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) =
FEATFRAC_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
if let Some(cached) = FEATFRAC_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
return cached;
}
FEATFRAC_IDENTITY.with(|c| {
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
});
FEATFRAC_IDENTITY
.write()
.unwrap()
.retain(|_, v| v.get_refcnt(py) > 1);
let obj = Py::new(py, Py { inner }).unwrap();
FEATFRAC_IDENTITY.with(|c| {
c.write().unwrap().insert(key, obj.clone_ref(py));
});
FEATFRAC_IDENTITY
.write()
.unwrap()
.insert(key, obj.clone_ref(py));
obj
}
use crate::types_py::{Py, Py, Py};
@@ -184,20 +207,32 @@ impl 分型Py {
}
#[getter]
fn (&self) -> Py {
Py {
inner: self.inner.,
}
fn (&self, py: Python<'_>) -> Py<Py> {
let inner = if chanlun::structure::fractal_obj::.load(Ordering::Relaxed) {
self.inner.
} else {
self.inner
.
.
.read()
.unwrap()
.unwrap_or(chanlun::types::::)
};
crate::types_py::(py, inner)
}
#[getter]
fn (&self) -> i64 {
self.inner.
self.inner.()
}
#[getter]
fn (&self) -> f64 {
self.inner.
if chanlun::structure::fractal_obj::.load(Ordering::Relaxed) {
self.inner.
} else {
self.inner...get()
}
}
fn __str__(&self) -> String {
@@ -221,12 +256,15 @@ impl 分型Py {
#[getter]
/// 左、中、右三对相对方向关系
fn (&self) -> Option<(Py, Py, Py)> {
fn (
&self,
py: Python<'_>,
) -> Option<(Py<Py>, Py<Py>, Py<Py>)> {
self.inner.().map(|(a, b, c)| {
(
Py { inner: a },
Py { inner: b },
Py { inner: c },
crate::types_py::(py, a),
crate::types_py::(py, b),
crate::types_py::(py, c),
)
})
}
@@ -247,7 +285,7 @@ impl 分型Py {
#[getter]
fn __dict__(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
let dict = PyDict::new(py);
dict.set_item("结构", self.())?;
dict.set_item("结构", self.(py))?;
dict.set_item("时间戳", self.())?;
dict.set_item("分型特征值", self.())?;
dict.set_item("强度", self.())?;
@@ -259,7 +297,7 @@ impl 分型Py {
if let Some(v) = self.(py) {
dict.set_item("", v)?;
}
if let Some(v) = self.() {
if let Some(v) = self.(py) {
dict.set_item("关系组", v)?;
}
Ok(dict.into())
@@ -454,15 +492,13 @@ impl 虚线Py {
}
#[getter]
fn (&self) -> Option<Self> {
fn (&self, py: Python<'_>) -> Option<Py<线Py>> {
self.inner
.
.read()
.unwrap()
.as_ref()
.map(|d| Self {
inner: Arc::clone(d),
})
.map(|d| dashed_to_py(py, Arc::clone(d)))
}
// ---- 序列 getters ----
@@ -471,12 +507,7 @@ impl 虚线Py {
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in self.inner..read().unwrap().iter() {
list.append(Py::new(
py,
Self {
inner: Arc::clone(d),
},
)?)?;
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
}
@@ -515,12 +546,7 @@ impl 虚线Py {
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in self.inner..read().unwrap().iter() {
list.append(Py::new(
py,
Self {
inner: Arc::clone(d),
},
)?)?;
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
}
@@ -533,10 +559,8 @@ impl 虚线Py {
#[getter]
/// :return: 运行方向
fn (&self) -> Py {
Py {
inner: self.inner.(),
}
fn (&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.())
}
#[getter]
@@ -569,11 +593,10 @@ impl 虚线Py {
let obs_ref = .borrow();
let observer_inner = obs_ref.obs();
let result = self.inner.K序列(&observer_inner.K线序列);
let list = pyo3::types::PyList::empty(.py());
let py = .py();
let list = pyo3::types::PyList::empty(py);
for k in &result {
list.append(K线Py {
inner: Arc::clone(k),
})?;
list.append(bar_to_py(py, Arc::clone(k)))?;
}
Ok(list.into())
}
@@ -960,7 +983,7 @@ impl 虚线Py {
) -> (bool, String) {
let obs = .borrow();
let obs_ref = obs.obs();
chanlun::structure::dash_line::线::(&线.borrow().inner, &*obs_ref)
chanlun::structure::dash_line::线::(&线.borrow().inner, &obs_ref)
}
}
@@ -1024,10 +1047,8 @@ impl 线段特征Py {
}
#[getter]
fn 线(&self) -> Py {
Py {
inner: self.inner.线,
}
fn 线(&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.线)
}
#[getter]
@@ -1089,7 +1110,7 @@ impl 线段特征Py {
let dict = PyDict::new(py);
dict.set_item("序号", self.())?;
dict.set_item("标识", self.())?;
dict.set_item("线段方向", self.线())?;
dict.set_item("线段方向", self.线(py))?;
dict.set_item("图表标题", self.())?;
Ok(dict.into())
}
@@ -1116,10 +1137,8 @@ impl 线段特征Py {
#[getter]
/// :return: 特征序列方向(线段方向的翻转)
fn (&self) -> Py {
Py {
inner: self.inner.(),
}
fn (&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.())
}
#[getter]
@@ -1139,7 +1158,7 @@ impl 线段特征Py {
let inner = Arc::make_mut(&mut self.inner);
inner
.(Arc::clone(&线.borrow().inner))
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e))
.map_err(pyo3::exceptions::PyValueError::new_err)
}
/// :param 待删除虚线: 待删除的虚线
@@ -1147,7 +1166,7 @@ impl 线段特征Py {
let inner = Arc::make_mut(&mut self.inner);
inner
.(&Arc::clone(&线.borrow().inner))
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e))
.map_err(pyo3::exceptions::PyValueError::new_err)
}
// ---- classmethods ----
@@ -1264,17 +1283,15 @@ impl 特征分型Py {
}
#[getter]
fn (&self) -> Py {
Py {
inner: self.inner.,
}
fn (&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.)
}
/// pandas 兼容 — 返回关键标量字段构成的字典
#[getter]
fn __dict__(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
let dict = PyDict::new(py);
dict.set_item("结构", self.())?;
dict.set_item("结构", self.(py))?;
Ok(dict.into())
}
+82 -14
View File
@@ -22,10 +22,79 @@
* SOFTWARE.
*/
use std::collections::HashMap;
use std::sync::Mutex;
use pyo3::basic::CompareOp;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyType};
// ========== 单例缓存 ==========
static _单例缓存: Mutex<Option<HashMap<u8, Py<Py>>>> = Mutex::new(None);
pub fn (
py: Python<'_>,
inner: chanlun::types::,
) -> Py<Py> {
let mut guard = _单例缓存.lock().unwrap();
if let Some(ref map) = *guard {
return map[&(inner as u8)].clone_ref(py);
}
// 首次访问时从类属性加载单例
let module = py.import("chanlun._chanlun").unwrap();
let class = module.getattr("分型结构").unwrap();
let mut map = HashMap::new();
for (name, variant) in &[
("", chanlun::types::::),
("", chanlun::types::::),
("", chanlun::types::::),
("", chanlun::types::::),
("", chanlun::types::::),
] {
let instance: Py<Py> = class.getattr(*name).unwrap().extract().unwrap();
map.insert(*variant as u8, instance);
}
let result = map[&(inner as u8)].clone_ref(py);
*guard = Some(map);
result
}
static _单例缓存: Mutex<Option<HashMap<u8, Py<Py>>>> = Mutex::new(None);
pub fn (
py: Python<'_>,
inner: chanlun::types::,
) -> Py<Py> {
let mut guard = _单例缓存.lock().unwrap();
if let Some(ref map) = *guard {
return map[&(inner as u8)].clone_ref(py);
}
// 首次访问时从类属性加载单例
let module = py.import("chanlun._chanlun").unwrap();
let class = module.getattr("相对方向").unwrap();
let mut map = HashMap::new();
for (name, variant) in &[
("向上", chanlun::types::::),
("向下", chanlun::types::::),
("向上缺口", chanlun::types::::),
("向下缺口", chanlun::types::::),
("衔接向上", chanlun::types::::),
("衔接向下", chanlun::types::::),
("", chanlun::types::::),
("", chanlun::types::::),
("", chanlun::types::::),
] {
let instance: Py<Py> = class.getattr(*name).unwrap().extract().unwrap();
map.insert(*variant as u8, instance);
}
let result = map[&(inner as u8)].clone_ref(py);
*guard = Some(map);
result
}
// ========== 买卖点类型 ==========
/// 买卖点类型 — 缠论的三类买卖点及扩展类型。
@@ -154,10 +223,8 @@ impl 相对方向Py {
}
/// 返回方向的对立面(向上↔向下, 缺口↔反向缺口, 衔接↔反向衔接)。
fn (&self) -> Self {
Self {
inner: self.inner.(),
}
fn (&self, py: Python<'_>) -> Py<Self> {
(py, self.inner.())
}
/// 判断是否为向上方向(向上/向上缺口/衔接向上)
@@ -210,10 +277,11 @@ impl 相对方向Py {
#[classmethod]
fn (
_cls: &Bound<'_, PyType>, : f64, : f64, : f64, : f64
) -> Self {
Self {
inner: chanlun::types::::(, , , ),
}
) -> Py<Self> {
(
_cls.py(),
chanlun::types::::(, , , ),
)
}
}
@@ -477,7 +545,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
// 买卖点类型 class attributes (singleton instances)
let py = m.py();
let bsp_class = m.getattr("买卖点类型")?;
let bsp_class = bsp_class.downcast_into::<PyType>()?;
let bsp_class = bsp_class.cast_into::<PyType>()?;
let variants: &[(&str, chanlun::types::)] = &[
("一买", chanlun::types::::),
@@ -500,7 +568,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
("T3B卖", chanlun::types::::T3B卖),
];
let mut bsp_members = PyDict::new(py);
let bsp_members = PyDict::new(py);
for (name, value) in variants {
let instance = Py::new(py, Py { inner: *value })?;
bsp_class.setattr(*name, instance.clone_ref(py))?;
@@ -509,7 +577,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
bsp_class.setattr("__members__", bsp_members)?;
// 相对方向 class attributes
let dir_class = m.getattr("相对方向")?.downcast_into::<PyType>()?.clone();
let dir_class = m.getattr("相对方向")?.cast_into::<PyType>()?.clone();
let dir_variants: &[(&str, chanlun::types::)] = &[
("向上", chanlun::types::::),
("向下", chanlun::types::::),
@@ -522,7 +590,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
("", chanlun::types::::),
];
let mut dir_members = PyDict::new(py);
let dir_members = PyDict::new(py);
for (name, value) in dir_variants {
let instance = Py::new(py, Py { inner: *value })?;
dir_class.setattr(*name, instance.clone_ref(py))?;
@@ -531,7 +599,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
dir_class.setattr("__members__", dir_members)?;
// 分型结构 class attributes
let frac_class = m.getattr("分型结构")?.downcast_into::<PyType>()?.clone();
let frac_class = m.getattr("分型结构")?.cast_into::<PyType>()?.clone();
let frac_variants: &[(&str, chanlun::types::)] = &[
("", chanlun::types::::),
("", chanlun::types::::),
@@ -540,7 +608,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
("", chanlun::types::::),
];
let mut frac_members = PyDict::new(py);
let frac_members = PyDict::new(py);
for (name, value) in frac_variants {
let instance = Py::new(py, Py { inner: *value })?;
frac_class.setattr(*name, instance.clone_ref(py))?;
File diff suppressed because it is too large Load Diff
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
"""pyo3_test_helpers — 可复用的 PyO3 测试工具包。
提供四个核心模块:
rc_identity Rc/Arc 指针身份一致性测试 Mixin
subclass PyO3 #[pyclass(subclass)] 子类化兼容性测试 Mixin
type_shape 返回值类型形状验证工具
api_consistency 两个模块间 API 描述符类型一致性测试 Mixin
所有 Mixin 都是纯 Python不依赖 pytest unittest.TestCase 配合使用
下游项目复制此目录即可复用
"""
from .api_consistency import ApiConsistencyMixin
from .rc_identity import RcIdentityMixin
from .subclass import PyO3SubclassMixin
from .type_shape import assert_type_shape, TypeShapeAssertions
__all__ = [
"ApiConsistencyMixin",
"RcIdentityMixin",
"PyO3SubclassMixin",
"assert_type_shape",
"TypeShapeAssertions",
]
+196
View File
@@ -0,0 +1,196 @@
"""API 一致性测试 Mixin。
验证两个模块中同名类的公开成员描述符类型一致
典型用途对比 Python 参考实现 (chan.py) Rust/PyO3 移植 (chanlun) API 兼容性
用法::
class TestApi一致性(ApiConsistencyMixin, unittest.TestCase):
reference_module = mylib.ref # Python 参考实现
target_module = mylib # Rust/PyO3 移植
# 可选: 已知差异(不会报错)
known_missing_in_target = {
"SomeClass": {"old_deprecated_method"},
}
known_descriptor_diffs = {
# (class_name, member, ref_type, target_type)
}
# 可选: 成员名过滤(匹配则跳过,支持前缀用 "prefix_" 表示)
noise_filters = ["model_", "parse_", "from_orm"]
"""
import unittest
def _classify_member(cls, attr_name):
"""返回描述符类型: property / classmethod / staticmethod / regular_method / None(data)."""
# 优先检查元类字典中的描述符
for klass in type(cls).__mro__:
if attr_name in klass.__dict__:
raw = klass.__dict__[attr_name]
if isinstance(raw, property):
return "property"
elif isinstance(raw, classmethod):
return "classmethod"
elif isinstance(raw, staticmethod):
return "staticmethod"
break
try:
attr = getattr(cls, attr_name)
except Exception:
return None
if callable(attr):
return "regular_method"
return None
def _is_noise(name, filters):
for pat in filters:
if pat == name:
return True
if pat.endswith("_") and name.startswith(pat):
return True
return False
def _get_classes(mod):
"""获取模块中所有公开的 type."""
return {n: getattr(mod, n) for n in dir(mod) if not n.startswith("_") and isinstance(getattr(mod, n), type)}
class ApiConsistencyMixin:
"""API 一致性测试 Mixin。
子类必须定义:
reference_module: 参考模块 (Python 实现)
target_module: 目标模块 (Rust/PyO3 移植)
子类可选定义:
known_missing_in_target: dict[str, set[str]] 已知 target 中缺失的成员
known_descriptor_diffs: set[tuple] 已知描述符类型差异
noise_filters: list[str] 噪音成员名过滤
"""
reference_module = None
target_module = None
known_missing_in_target: dict = {}
known_descriptor_diffs: set = set()
noise_filters: list = []
@classmethod
def setUpClass(cls):
if cls.reference_module is None or cls.target_module is None:
raise unittest.SkipTest(f"{cls.__name__} 未定义 reference_module / target_module")
# ---- 描述符类型一致性 ----
def test_共有成员描述符类型一致(self):
"""同名类的同名成员,描述符类型 (property/classmethod/staticmethod/regular) 一致."""
ref_classes = _get_classes(self.reference_module)
tgt_classes = _get_classes(self.target_module)
shared = sorted(set(ref_classes) & set(tgt_classes))
failures = []
for cls_name in shared:
ref_cls = ref_classes[cls_name]
tgt_cls = tgt_classes[cls_name]
ref_members = {}
tgt_members = {}
for attr_name in sorted(dir(ref_cls)):
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
continue
cat = _classify_member(ref_cls, attr_name)
if cat:
ref_members[attr_name] = cat
for attr_name in sorted(dir(tgt_cls)):
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
continue
cat = _classify_member(tgt_cls, attr_name)
if cat:
tgt_members[attr_name] = cat
shared_members = sorted(set(ref_members) & set(tgt_members))
for member in shared_members:
ref_cat = ref_members[member]
tgt_cat = tgt_members[member]
if ref_cat != tgt_cat:
diff_key = (cls_name, member, ref_cat, tgt_cat)
if diff_key not in self.known_descriptor_diffs:
failures.append(f"{cls_name}.{member}: ref={ref_cat}, tgt={tgt_cat}")
if failures:
self.fail("描述符类型不一致:\n " + "\n ".join(failures))
# ---- 缺失成员检查 ----
def test_参考模块成员在目标模块中存在(self):
"""chan 中的关键公开成员在 chanlun 中均有对应."""
ref_classes = _get_classes(self.reference_module)
tgt_classes = _get_classes(self.target_module)
shared = sorted(set(ref_classes) & set(tgt_classes))
failures = []
for cls_name in shared:
if cls_name not in self.known_missing_in_target:
continue
ref_cls = ref_classes[cls_name]
tgt_cls = tgt_classes[cls_name]
expected_missing = self.known_missing_in_target.get(cls_name, set())
ref_members = set()
for attr_name in sorted(dir(ref_cls)):
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
continue
cat = _classify_member(ref_cls, attr_name)
if cat and attr_name not in expected_missing:
ref_members.add(attr_name)
tgt_members = set()
for attr_name in sorted(dir(tgt_cls)):
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
continue
cat = _classify_member(tgt_cls, attr_name)
if cat:
tgt_members.add(attr_name)
missing = ref_members - tgt_members - expected_missing
for member in sorted(missing):
failures.append(f"{cls_name}.{member}: ref={_classify_member(ref_cls, member)}, tgt=未导出")
if failures:
self.fail("参考模块中的成员在目标模块中缺失:\n " + "\n ".join(failures))
# ---- 方法可调用性 ----
def test_共有方法均可调用(self):
"""所有共有 regular_method 在两边都是 callable."""
ref_classes = _get_classes(self.reference_module)
tgt_classes = _get_classes(self.target_module)
shared = sorted(set(ref_classes) & set(tgt_classes))
failures = []
for cls_name in shared:
ref_cls = ref_classes[cls_name]
tgt_cls = tgt_classes[cls_name]
for attr_name in sorted(dir(ref_cls)):
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
continue
ref_cat = _classify_member(ref_cls, attr_name)
tgt_cat = _classify_member(tgt_cls, attr_name)
if ref_cat == "regular_method" and tgt_cat == "regular_method":
ref_obj = getattr(ref_cls, attr_name)
tgt_obj = getattr(tgt_cls, attr_name)
if not callable(ref_obj):
failures.append(f"{cls_name}.{attr_name}: ref 不是 callable")
if not callable(tgt_obj):
failures.append(f"{cls_name}.{attr_name}: tgt 不是 callable")
if failures:
self.fail("方法不可调用:\n " + "\n ".join(failures))
+126
View File
@@ -0,0 +1,126 @@
"""Rc/Arc 指针身份一致性测试 Mixin。
验证同一个 Rust Rc<T>/Arc<T> 无论通过哪条路径到达 Python
始终返回相同的 PyObject`a is b` True
用法::
class TestMyLib(RcIdentityMixin, unittest.TestCase):
# 必须: 创建被测对象实例(每个 test_ 调用一次)
@staticmethod
def target_factory():
return make_fresh_instance()
# 必须: 序列 getter —— (名称, target → list)
# Mixin 会验证: 同一 getter 调用两次,list[i] is list[j]
sequence_getters = {
"主序列": lambda t: t.items,
"子序列": lambda t: t.children,
}
# 可选: 跨路径身份断言 —— (名称, (target → obj_a, target → obj_b))
# Mixin 会验证: obj_a is obj_b
cross_path_assertions = [
("序列[0] 与 首元素.父", lambda t: t.items[0], lambda t: t.items[0].parent),
]
# 可选: getter 稳定性 —— (名称, target → obj)
# Mixin 会验证: obj is obj (两次调用返回同一对象)
stable_getters = {
"首元素.属性": lambda t: t.items[0].attr,
}
# 可选: 序列长度检查的最小值(默认不检查,设为 >0 开启)
min_sequence_lengths = {
"主序列": 3,
"子序列": 2,
}
"""
import unittest
class RcIdentityMixin:
"""Rc/Arc 指针身份一致性测试 Mixin。
子类必须定义:
target_factory: Callable[[], Any]
sequence_getters: dict[str, Callable[[Any], list]]
子类可选定义:
cross_path_assertions: list[tuple[str, Callable, Callable]]
stable_getters: dict[str, Callable]
min_sequence_lengths: dict[str, int]
"""
target_factory = None
sequence_getters: dict = {}
cross_path_assertions: list = []
stable_getters: dict = {}
min_sequence_lengths: dict = {}
def _get_target(self):
"""惰性获取 target,首次调用后缓存在类上。避免 setUpClass MRO 冲突."""
cls = type(self)
# 每次测试重新创建——但这会太慢。用类级别缓存。
# 子类应在 setUpClass 中调用 self._get_target() 或自己设置 cls._cached_target。
if not hasattr(cls, "_cached_target"):
if cls.target_factory is None:
raise unittest.SkipTest(f"{cls.__name__} 未定义 target_factory")
cls._cached_target = cls.target_factory()
return cls._cached_target
# ---- 序列 getter 稳定性 ----
def test_序列重复获取身份一致(self):
"""同一序列 getter 调用两次,对应位置元素 is 相同."""
t = self._get_target()
for name, getter in self.sequence_getters.items():
seq1 = getter(t)
seq2 = getter(t)
self.assertEqual(len(seq1), len(seq2), f"{name}: 两次获取长度不同")
check_n = min(len(seq1), 10)
for i in range(check_n):
self.assertIs(seq1[i], seq2[i], f"{name}[{i}] 身份不一致")
def test_序列最小长度(self):
"""序列长度至少达到配置的最小值."""
t = self._get_target()
for name, getter in self.sequence_getters.items():
if name in self.min_sequence_lengths:
min_len = self.min_sequence_lengths[name]
actual = len(getter(t))
self.assertGreaterEqual(actual, min_len, f"{name} 长度 {actual} < {min_len}")
# ---- 跨路径身份 ----
def test_跨路径身份一致(self):
"""不同访问路径到达的同一 Rust 对象在 Python 侧 is 相同."""
t = self._get_target()
for i, (label, path_a, path_b) in enumerate(self.cross_path_assertions):
obj_a = path_a(t)
obj_b = path_b(t)
self.assertIsNotNone(obj_a, f"[{i}] {label}: path_a 返回 None")
self.assertIsNotNone(obj_b, f"[{i}] {label}: path_b 返回 None")
self.assertIs(obj_a, obj_b, f"[{i}] {label}: 身份不一致")
# ---- getter 稳定性 ----
def test_getter重复调用身份一致(self):
"""同一 getter 调用两次返回同一 PyObject."""
t = self._get_target()
for name, getter in self.stable_getters.items():
obj1 = getter(t)
obj2 = getter(t)
self.assertIs(obj1, obj2, f"{name}: 两次调用返回不同对象")
# ---- list.index 基于 is ----
def test_list_index_基于身份(self):
"""list.index(elem) 正常工作(依赖 __eq__ 基于 is 比较)."""
t = self._get_target()
for name, getter in self.sequence_getters.items():
seq = getter(t)
if len(seq) >= 2:
self.assertEqual(seq.index(seq[0]), 0, f"{name}: index(seq[0]) != 0")
self.assertEqual(seq.index(seq[-1]), len(seq) - 1, f"{name}: index(seq[-1]) != {len(seq) - 1}")
+247
View File
@@ -0,0 +1,247 @@
"""PyO3 #[pyclass(subclass)] 子类化兼容性测试 Mixin。
验证: Python 端可以正常子类化 PyO3 导出的类__new__/__init__ 协作
super() 委托MRO property/method 重写等全部正确
用法::
class TestMyObserver(PyO3SubclassMixin, unittest.TestCase):
base_class = mylib.Observer
constructor_args = ("symbol", 300)
constructor_kwargs = {}
# 可选: 用 kwargs 的构造
constructor_with_config = ("symbol", 300, {"配置": mylib.Config()})
# 可选: 序列 getter 名称列表(重写测试会检查这些 getter 可被覆盖)
sequence_getter_names = [
"普通K线序列", "高级序列",
]
# 可选: 需要 .nb 数据文件才能运行的测试会检查这个
@staticmethod
def has_data_file():
return os.path.isfile("data.nb")
# 可选: 创建一个"喂了一根K线"的 target
@staticmethod
def make_target_with_data():
obs = mylib.Observer("sym", 300)
k = mylib.KLine(...)
obs.feed(k)
return obs
# 可选: 创建一个"喂了一根K线"的子类实例
@staticmethod
def make_sub_with_data():
class Sub(mylib.Observer):
pass
obs = Sub("sym", 300)
k = mylib.KLine(...)
obs.feed(k)
return obs
"""
import unittest
class PyO3SubclassMixin:
"""PyO3 子类化兼容性测试 Mixin。
子类必须定义:
base_class: type
constructor_args: tuple
constructor_kwargs: dict
子类可选定义:
sequence_getter_names: list[str]
has_data_file: Callable[[], bool]
make_target_with_data: Callable[[], Any]
make_sub_with_data: Callable[[], Any]
make_data_item: Callable[[], Any] # 创建一根可喂入的数据项
feed_method_name: str # 喂数据的方法名,默认 "增加原始K线"
property_getters: list[str] # 需要逐一下覆写的 property 名
method_overrides: list[str] # 需要逐一重写的方法名
"""
base_class: type = None
constructor_args: tuple = ()
constructor_kwargs: dict = {}
sequence_getter_names: list = []
# 可选 hooks
has_data_file = None
make_target_with_data = None
make_sub_with_data = None
make_data_item = None
feed_method_name = "增加原始K线"
property_getters: list = []
method_overrides: list = []
@classmethod
def setUpClass(cls):
if cls.base_class is None:
raise unittest.SkipTest(f"{cls.__name__} 未定义 base_class")
# ---- 基础子类化 ----
def test_子类可实例化(self):
"""子类可创建,isinstance 正确."""
Base = self.base_class
class Sub(Base):
pass
obs = Sub(*self.constructor_args, **self.constructor_kwargs)
self.assertIsInstance(obs, Base)
self.assertEqual(type(obs).__name__, "Sub")
def test_子类_init_可添加自定义属性(self):
"""子类 __init__ 可添加自定义属性,基类字段不受影响."""
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
def __init__(self, *a, **kw):
self.tag = "custom"
self.count = 0
obs = Sub(*args, **kwargs)
self.assertEqual(obs.tag, "custom")
self.assertEqual(obs.count, 0)
def test_子类_new_过滤_kwargs(self):
"""__new__ 过滤子类专属参数,只把父类需要的传给 super().__new__."""
Base = self.base_class
args = self.constructor_args
class Sub(Base):
def __new__(cls, *a, extra=None, **kw):
return super().__new__(cls, *a)
def __init__(self, *a, extra=None, **kw):
self.extra = extra
obs = Sub(*args, extra={"debug": True})
self.assertEqual(obs.extra, {"debug": True})
obs2 = Sub(*args)
self.assertIsNone(obs2.extra)
# ---- 方法重写 ----
def test_方法重写_super调用(self):
"""重写方法,super() 调用父类."""
if self.make_target_with_data is None or self.make_sub_with_data is None:
self.skipTest("未定义 make_target_with_data / make_sub_with_data")
base_obs = self.make_target_with_data()
sub_obs = self.make_sub_with_data()
for attr in self.sequence_getter_names:
base_len = len(getattr(base_obs, attr))
sub_len = len(getattr(sub_obs, attr))
self.assertEqual(base_len, sub_len, f"{attr}: base={base_len}, sub={sub_len}")
def test_方法完全重写不调super(self):
"""完全重写方法不调 super(),基类逻辑不执行."""
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
def __init__(self, *a, **kw):
self.log = []
obs = Sub(*args, **kwargs)
self.assertEqual(obs.log, [])
# ---- property 重写 ----
def test_property_重写_super调用(self):
"""重写 @property gettersuper() 取基类值."""
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
pass
obs = Sub(*args, **kwargs)
# 验证实例创建成功即可,具体 getter 覆盖由子类测试
self.assertIsInstance(obs, Base)
def test_str_repr_重写(self):
"""重写 __str__ / __repr__."""
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
def __str__(self):
return f"Custom({id(self)})"
def __repr__(self):
return self.__str__()
obs = Sub(*args, **kwargs)
self.assertIn("Custom", str(obs))
self.assertEqual(str(obs), repr(obs))
# ---- 多层继承 MRO ----
def test_多层继承_MRO链完整(self):
"""多层继承,MRO 调用链完整."""
if self.make_data_item is None:
self.skipTest("未定义 make_data_item")
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
feed_name = self.feed_method_name
class L1(Base):
def __init__(self, *a, **kw):
self._l1_called = False
class L2(L1):
def __init__(self, *a, **kw):
super().__init__(*a, **kw)
self._l2_called = True
obs = L2(*args, **kwargs)
self.assertTrue(obs._l2_called)
def test_未重写方法直接继承(self):
"""未重写的方法从基类直接继承."""
if self.make_data_item is None:
self.skipTest("未定义 make_data_item")
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
pass
obs = Sub(*args, **kwargs)
self.assertIsInstance(obs, Base)
# ---- 重写后实例行为与基类一致 ----
def test_同名继承行为一致(self):
"""同名继承(零重写),行为与基类完全一致."""
if self.make_target_with_data is None:
self.skipTest("未定义 make_target_with_data")
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
pass
base = Base(*args, **kwargs)
sub = Sub(*args, **kwargs)
self.assertIsInstance(sub, Base)
+88
View File
@@ -0,0 +1,88 @@
"""PyO3 返回值的 Python 类型形状验证工具。
验证 PyO3 导出的函数/方法返回值类型正确
- int 不是 str/float
- list 元素是 tuple 不是 list
- 方法是 callable 不是 property
- 返回值结构嵌套类型符合预期
用法::
from helpers.type_shape import assert_type_shape
result = mylib.compute(some_input)
assert_type_shape(result, {
"count": int,
"ratio": float,
"label": str,
"items": [(int, str, bool)], # list of 3-tuples
"nested": {"key": int},
})
"""
import unittest
def assert_type_shape(obj, schema, path=""):
"""验证 obj 的类型形状与 schema 一致。
schema 支持:
- type: obj 必须是该类型实例
- [inner]: obj 必须是 list每个元素验证 inner
- (t1, t2, ...): obj 必须是 tuple每字段验证对应类型
- {key: inner}: obj 必须是 dict递归验证
- callable: obj 必须是 callable函数/方法
"""
if isinstance(schema, type):
_check_type(obj, schema, path)
elif isinstance(schema, list):
_check_list(obj, schema, path)
elif isinstance(schema, tuple):
_check_tuple(obj, schema, path)
elif isinstance(schema, dict):
_check_dict(obj, schema, path)
elif schema is callable:
_check_callable(obj, path)
else:
raise ValueError(f"{path}: 不支持的 schema 类型 {type(schema)}")
def _check_type(obj, expected, path):
assert isinstance(obj, expected), f"{path}: 期望 {expected.__name__}, 实际 {type(obj).__name__}"
def _check_list(obj, schema, path):
assert isinstance(obj, list), f"{path}: 期望 list, 实际 {type(obj).__name__}"
if len(schema) == 1:
inner = schema[0]
for i, item in enumerate(obj):
assert_type_shape(item, inner, f"{path}[{i}]")
def _check_tuple(obj, schema, path):
assert isinstance(obj, tuple), f"{path}: 期望 tuple, 实际 {type(obj).__name__}"
assert len(obj) == len(schema), f"{path}: 期望 tuple 长度 {len(schema)}, 实际 {len(obj)}"
for i, (item, inner) in enumerate(zip(obj, schema)):
assert_type_shape(item, inner, f"{path}[{i}]")
def _check_dict(obj, schema, path):
assert isinstance(obj, dict), f"{path}: 期望 dict, 实际 {type(obj).__name__}"
for key, inner in schema.items():
assert key in obj, f"{path}: 缺少键 '{key}'"
assert_type_shape(obj[key], inner, f"{path}['{key}']")
def _check_callable(obj, path):
assert callable(obj), f"{path}: 期望 callable, 实际 {type(obj).__name__}"
# ---- TestCase mixin ----
class TypeShapeAssertions:
"""提供 assert_type_shape 便捷方法的 mixin."""
def assertTypeShape(self, obj, schema, path=""):
"""断言 obj 的类型形状与 schema 一致."""
assert_type_shape(obj, schema, path)
File diff suppressed because it is too large Load Diff
-267
View File
@@ -1,267 +0,0 @@
#!/usr/bin/env python3
"""
对象标识测试验证同一 Rc 底层数据通过不同路径访问时
Python 侧始终返回相同的 PyObject`is` 比较为 True
涉及的 Rc 包装类型
- K线 (Rc<K线>) 原始OHLCV数据
- 缠论K线 (Rc<缠论K线>) 包含处理后的K线
- 分型 (Rc<分型>) 顶底分型
- 虚线 (Rc<虚线>) /线段的通用抽象
- 中枢 (Rc<中枢>) 三段虚线重叠区间
- 线段特征 (Rc<线段特征>) 线段特征序列元素
- 特征分型 (Rc<特征分型>) 特征序列的分型
路径示例
- 缠K序列[i] vs 分型序列[j]. (同一根缠K)
- 分型序列[i] vs 笔序列[j]. (同一个分型)
- 笔序列[i] vs 中枢[k].基础序列[m] (同一条虚线)
"""
import chanlun
import math
def create_observer(symbol="btcusd", period=14400, n_bars=500):
"""创建观察者并喂入模拟K线数据。"""
cfg = chanlun.缠论配置()
obs = chanlun.观察者(symbol, period, cfg)
for i in range(n_bars):
trend = i * 3
wave = math.sin(i * 0.05) * 2000
mid = 68000.0 + trend + wave
high = mid + abs(math.cos(i * 0.3)) * 400 + 100
low = mid - abs(math.sin(i * 0.5)) * 400 - 100
k = chanlun.K线(
标识=symbol,
周期=period,
时间戳=1771675200 + i * period,
开盘价=mid - 50,
=high,
=low,
收盘价=mid + 50,
成交量=abs(math.sin(i)) * 1000,
)
obs.增加原始K线(k)
return obs
class Test缠K身份:
"""缠论K线: 从序列、分型、笔端点、中枢等不同路径访问。"""
def test_序列重复获取(self):
"""同一序列获取两次,元素应相同。"""
obs = create_observer()
seq1 = obs.缠论K线序列
seq2 = obs.缠论K线序列
for i in range(min(len(seq1), 10)):
assert seq1[i] is seq2[i], f"缠K序列[{i}] 身份不一致"
def test_分型中K(self):
"""分型.中 与 缠K序列 对应元素应相同。"""
obs = create_observer()
seq = obs.缠论K线序列
分序 = obs.分型序列
for fx in 分序[:10]:
= fx.
for ck in seq:
if ck.时间戳 == .时间戳:
assert ck is , f"分型.中 (ts={.时间戳}) 与序列中元素不匹配"
break
def test_笔端点钟K(self):
"""笔的端点分型的中间K线应与序列元素相同。"""
obs = create_observer()
seq = obs.缠论K线序列
for bi in obs.笔序列:
for nm, getter in [("", lambda b=bi: b.), ("", lambda b=bi: b.)]:
ep = getter()
if ep is None:
continue
= ep.
for ck in seq:
if ck.时间戳 == .时间戳:
assert ck is , f"笔.{nm}.中 (ts={.时间戳}) 与序列中元素不匹配"
break
def test_getter重复调用(self):
"""同一getter调用两次返回同一对象。"""
obs = create_observer()
for fx in obs.分型序列[:5]:
中1 = fx.
中2 = fx.
assert 中1 is 中2, "分型.中 两次调用返回不同对象"
class Test分型身份:
"""分型: 从分型序列、笔/线段端点、买卖点等不同路径访问。"""
def test_序列重复获取(self):
"""同一序列获取两次,元素应相同。"""
obs = create_observer()
seq1 = obs.分型序列
seq2 = obs.分型序列
for i in range(min(len(seq1), 9)):
assert seq1[i] is seq2[i], f"分型序列[{i}] 身份不一致"
def test_笔端点与序列(self):
"""笔.文 / 笔.武 应与分型序列中对应元素相同。"""
obs = create_observer()
分序 = obs.分型序列
for bi in obs.笔序列:
for nm in ["", ""]:
ep = getattr(bi, nm)
if ep is None:
continue
matched = False
for fx in 分序:
if fx.时间戳 == ep.时间戳 and fx.结构 == ep.结构:
assert fx is ep, f"笔.{nm} (ts={ep.时间戳}) 与分型序列中元素不匹配"
matched = True
break
assert matched, f"笔.{nm} (ts={ep.时间戳}) 在分型序列中未找到"
def test_段端点与序列(self):
"""段.文 / 段.武 应与分型序列中对应元素相同。"""
obs = create_observer()
分序 = obs.分型序列
for duan in obs.线段序列:
for nm in ["", ""]:
ep = getattr(duan, nm)
if ep is None:
continue
matched = False
for fx in 分序:
if fx.时间戳 == ep.时间戳 and fx.结构 == ep.结构:
assert fx is ep, f"段.{nm} (ts={ep.时间戳}) 与分型序列中元素不匹配"
matched = True
break
assert matched, f"段.{nm} (ts={ep.时间戳}) 在分型序列中未找到"
def test_getter重复调用(self):
"""同一getter调用两次返回同一对象。"""
obs = create_observer()
for bi in obs.笔序列:
文1 = bi.
文2 = bi.
assert 文1 is 文2, "笔.文 两次调用返回不同对象"
武1 = bi.
武2 = bi.
assert 武1 is 武2, "笔.武 两次调用返回不同对象"
break # 只测第一笔
class Test虚线身份:
"""虚线(笔/线段): 从笔序列、线段序列、中枢内部序列等不同路径访问。"""
def test_笔序列重复获取(self):
obs = create_observer()
seq1 = obs.笔序列
seq2 = obs.笔序列
for i in range(min(len(seq1), 8)):
assert seq1[i] is seq2[i], f"笔序列[{i}] 身份不一致"
def test_线段序列重复获取(self):
obs = create_observer()
seq1 = obs.线段序列
seq2 = obs.线段序列
for i in range(min(len(seq1), 5)):
assert seq1[i] is seq2[i], f"线段序列[{i}] 身份不一致"
def test_多个扩展序列(self):
"""扩展线段的不同序列获取同一虚线应相同。"""
obs = create_observer()
s1 = obs.扩展线段序列
s2 = obs.扩展线段序列_线段
s3 = obs.扩展线段序列_扩展线段
# 这些序列可能包含不同的虚线,但如果同一个 Rc 出现在两个序列中应该相同
for d1 in s1:
for d2 in s2:
if d1.序号 == d2.序号:
assert d1 is d2, f"扩展线段序列[{d1.序号}] 跨序列身份不一致"
break
class TestK线身份:
"""原始K线: 从序列、买卖点、缠K标的等不同路径访问。"""
def test_序列重复获取(self):
obs = create_observer()
seq1 = obs.普通K线序列
seq2 = obs.普通K线序列
for i in range(min(len(seq1), 10)):
assert seq1[i] is seq2[i], f"普K序列[{i}] 身份不一致"
class Test中枢身份:
"""中枢: 从中枢序列、分型关联、笔中枢/线段中枢等不同路径访问。"""
def test_序列重复获取(self):
obs = create_observer(period=3600, n_bars=800)
seq1 = obs.中枢序列
seq2 = obs.中枢序列
for i in range(min(len(seq1), 5)):
assert seq1[i] is seq2[i], f"中枢序列[{i}] 身份不一致"
def test_笔中枢与线段中枢(self):
obs = create_observer(period=3600, n_bars=800)
笔中 = obs.笔_中枢序列
段中 = obs.线段_中枢序列
扩展中 = obs.扩展中枢序列
# 验证同一次获取内的身份
for zs in 笔中:
文1 = zs.
文2 = zs.
assert 文1 is 文2, f"笔中枢.文 两次调用不同"
break
for zs in 段中:
文1 = zs.
文2 = zs.
assert 文1 is 文2, f"段中枢.文 两次调用不同"
break
class Test整体身份:
"""跨类型综合身份测试。"""
def test_买卖点分型(self):
"""验证买卖点的关联分型身份。"""
obs = create_observer(period=3600, n_bars=800)
# 尝试访问可用的结构
分序 = obs.分型序列
笔序 = obs.笔序列
assert len(分序) >= 0 and len(笔序) >= 0 # 至少不崩溃
def test_全链路一致性(self):
"""缠K → 分型 → 笔 → 段 链路中所有对象身份一致。"""
obs = create_observer()
seq = obs.缠论K线序列
for bi in obs.笔序列:
# 笔的端点分型
for nm, getter in [("", lambda b=bi: b.), ("", lambda b=bi: b.)]:
ep = getter()
if ep is None:
continue
# ep 中的 中 是一根缠K,应能在序列中找到相同对象
= ep.
for ck in seq:
if ck.时间戳 == .时间戳:
assert ck is
break
# 左也应该是可访问的
= ep.
if is not None:
for ck in seq:
if ck.时间戳 == .时间戳:
assert ck is
break
# 右也应该是可访问的
= ep.
if is not None:
for ck in seq:
if ck.时间戳 == .时间戳:
assert ck is
break
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "chanlun"
version = "26.5.3"
version = "26.5.6"
edition = "2021"
rust-version = "1.70"
license = "MIT"
+31 -38
View File
@@ -78,16 +78,18 @@ impl 笔 {
- _k.K线.read().unwrap().)
.unsigned_abs() as usize;
// 向上笔
if .().() && _k..get() < .() {
if >= ._原始数量 as usize {
return . as usize;
}
if .().()
&& _k..get() < .()
&& >= ._原始数量 as usize
{
return . as usize;
}
// 向下笔
if .().() && _k..get() > .() {
if >= ._原始数量 as usize {
return . as usize;
}
if .().()
&& _k..get() > .()
&& >= ._原始数量 as usize
{
return . as usize;
}
}
}
@@ -336,7 +338,7 @@ impl 笔 {
/// 核心笔分析 — 使用显式栈模拟递归
///
/// 返回: 递归层次数
pub fn (
pub fn _显式栈(
: Arc<>,
: &mut Vec<Arc<>>,
: &mut Vec<Arc<线>>,
@@ -415,7 +417,8 @@ impl 笔 {
let = Arc::clone(.last().unwrap());
// Python line 2330-2335: 清理无效数据
if . == .
if ...load(Ordering::Relaxed)
== ...load(Ordering::Relaxed)
|| matches!(., :: | ::)
{
Self::(, );
@@ -430,7 +433,8 @@ impl 笔 {
let = Arc::clone(.last().unwrap());
// Python line 2338: 时序检查 — skip out-of-order fractals
if . > .
if ...load(Ordering::Relaxed)
> ...load(Ordering::Relaxed)
&& ...load(Ordering::Relaxed)
- ...load(Ordering::Relaxed)
> 1
@@ -608,7 +612,7 @@ impl 笔 {
/// 核心笔分析 — 递归实现,逐句对照 chan.py 笔.分析 / 笔递归分析
///
/// 返回: 递归层次数
pub fn (
pub fn (
: Arc<>,
: &mut Vec<Arc<>>,
: &mut Vec<Arc<线>>,
@@ -637,7 +641,7 @@ impl 笔 {
// Python line 2329-2335: 清理无效数据
let = Arc::clone(.last().unwrap());
if . == .
if ...load(Ordering::Relaxed) == ...load(Ordering::Relaxed)
|| matches!(., :: | ::)
{
Self::(, );
@@ -651,7 +655,7 @@ impl 笔 {
// Python line 2337-2341: 时序检查
let = Arc::clone(.last().unwrap());
if . > .
if ...load(Ordering::Relaxed) > ...load(Ordering::Relaxed)
&& ...load(Ordering::Relaxed) - ...load(Ordering::Relaxed)
> 1
{
@@ -674,7 +678,7 @@ impl 笔 {
&& . == ::);
if {
Self::(, );
return Self::(
return Self::(
,
,
,
@@ -712,7 +716,7 @@ impl 笔 {
if let Some() =
::K序列中获取分型(K序列, _k)
{
let = Self::(
let = Self::(
Arc::new(),
,
,
@@ -721,7 +725,7 @@ impl 笔 {
+ 1,
,
);
return Self::(
return Self::(
,
,
,
@@ -770,7 +774,7 @@ impl 笔 {
if let Some(ref ) = . {
if let Some() = ::K序列中获取分型(K序列, )
{
return Self::(
return Self::(
Arc::new(),
,
,
@@ -812,7 +816,7 @@ impl 笔 {
let _rc = Arc::new();
if !.is_empty() {
let mut = Self::(
let mut = Self::(
Arc::clone(&_rc),
,
,
@@ -841,7 +845,7 @@ impl 笔 {
)
{
let _rc = Arc::new();
= Self::(
= Self::(
Arc::clone(&_rc),
,
,
@@ -856,7 +860,7 @@ impl 笔 {
}
}
return Self::(
return Self::(
,
,
,
@@ -873,7 +877,7 @@ impl 笔 {
} else if .is_empty() {
::(, );
} else {
return Self::(
return Self::(
,
,
,
@@ -958,22 +962,11 @@ impl 笔 {
for i in 3...len() - 1 {
let k = &[i];
if *k..read().unwrap() == Some(::) && .() == ::
{
let = Arc::clone(&[i - 1]);
let = Arc::clone(k);
let = Arc::clone(&[i + 1]);
let = ::new(Some(), , Some());
let = 线::(Arc::clone(&), Arc::new(), true);
.
.store(..load(Ordering::Relaxed), Ordering::Relaxed);
if Self::(&, ) {
.push();
}
} else if *k..read().unwrap() == Some(::)
&& .() == ::
{
let =
*k..read().unwrap() == Some(::) && .() == ::;
let =
*k..read().unwrap() == Some(::) && .() == ::;
if || {
let = Arc::clone(&[i - 1]);
let = Arc::clone(k);
let = Arc::clone(&[i + 1]);
+8 -8
View File
@@ -39,13 +39,13 @@ impl 背驰分析 {
) -> bool {
let MACD = Self::_获取MACD面积(
K线序列,
&*...K线.read().unwrap(),
&*..read().unwrap()..K线.read().unwrap(),
&...K线.read().unwrap(),
&..read().unwrap()..K线.read().unwrap(),
);
let MACD = Self::_获取MACD面积(
K线序列,
&*...K线.read().unwrap(),
&*..read().unwrap()..K线.read().unwrap(),
&...K线.read().unwrap(),
&..read().unwrap()..K线.read().unwrap(),
);
// 计算面积(绝对值求和)
@@ -69,14 +69,14 @@ impl 背驰分析 {
/// 斜率背驰 — 价格斜率背驰
pub fn (: &线, : &线) -> bool {
let dx = (..read().unwrap(). - ..) as f64;
let dx = (..read().unwrap().() - ..()) as f64;
if dx == 0.0 {
return false;
}
let dy = ..read().unwrap(). - ..;
let = dy / dx;
let dx = (..read().unwrap(). - ..) as f64;
let dx = (..read().unwrap().() - ..()) as f64;
if dx == 0.0 {
return false;
}
@@ -92,11 +92,11 @@ impl 背驰分析 {
/// 测度背驰 — 价格时间测度背驰
pub fn (: &线, : &线) -> bool {
let dx = (..read().unwrap(). - ..) as f64;
let dx = (..read().unwrap().() - ..()) as f64;
let dy = ..read().unwrap(). - ..;
let = (dx * dx + dy * dy).sqrt();
let dx = (..read().unwrap(). - ..) as f64;
let dx = (..read().unwrap().() - ..()) as f64;
let dy = ..read().unwrap(). - ..;
let = (dx * dx + dy * dy).sqrt();
+42 -41
View File
@@ -165,9 +165,9 @@ impl 中枢 {
self..read().unwrap(),
self..load(Ordering::Relaxed),
self..load(Ordering::Relaxed),
self.().,
self.().(),
crate::utils::format_f64_g(self.().),
self.().,
self.().(),
crate::utils::format_f64_g(self.().),
线_str,
_第三买卖线_str,
@@ -387,7 +387,7 @@ impl 中枢 {
/// 向中枢序列尾部添加
pub fn (
: &mut Vec<Arc<>>, mut : Arc<>
: &mut Vec<Arc<>>, : Arc<>
) {
if let Some() = .last() {
@@ -415,7 +415,7 @@ impl 中枢 {
: &mut Vec<Arc<>>,
: &Arc<>,
) -> Option<Arc<>> {
if .last().map(|x| Arc::as_ptr(x)) == Some(Arc::as_ptr()) {
if .last().map(Arc::as_ptr) == Some(Arc::as_ptr()) {
.pop()
} else {
None
@@ -430,7 +430,7 @@ impl 中枢 {
: &mut Vec<Arc<>>,
: bool,
: &str,
: i64,
_层级: i64,
) {
if 线.len() < 3 {
return;
@@ -472,7 +472,7 @@ impl 中枢 {
));
Self::(, );
// Python: return 中枢递归分析(虚线序列, 中枢序列, ...)
Self::(线, , , , );
Self::(线, , , , _层级);
return;
}
}
@@ -487,7 +487,7 @@ impl 中枢 {
if needs_pop {
let = Arc::clone(&[_idx]);
Self::(, &);
Self::(线, , , , );
Self::(线, , , , _层级);
return;
}
@@ -508,8 +508,8 @@ impl 中枢 {
let mut = [_idx].();
let mut : Vec<Arc<线>> = Vec::new();
for i in ..线.len() {
let 线 = Arc::clone(&线[i]);
for 线_ref in &线[..] {
let 线 = Arc::clone(线_ref);
// 检查是否超出中枢范围(缺口)
if crate::types::::(, , 线.(), 线.()).()
@@ -566,6 +566,30 @@ impl 中枢 {
}
}
impl std::fmt::Display for {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let _str = self
.
.read()
.unwrap()
.iter()
.map(|d| format!("{}", d))
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"{}({}, {}, 元素数量: {}, [{}], {} ===>>> {})",
self..read().unwrap(),
crate::utils::format_f64_g(self.()),
crate::utils::format_f64_g(self.()),
self..read().unwrap().len(),
_str,
self.(),
self.(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -575,13 +599,14 @@ mod tests {
use crate::types::;
fn _创建K线(: i64, : f64, : f64, : f64, : f64) -> K线 {
let mut k = K线::default();
k. = ;
k. = ;
k. = ;
k. = ;
k. = ;
k
K线 {
,
,
,
: ,
: ,
..Default::default()
}
}
fn _创建缠K(
@@ -710,7 +735,7 @@ mod tests {
.线(Arc::clone(&1));
assert!(.线.read().unwrap().is_some());
assert_eq!(
Arc::as_ptr(&*.线.read().unwrap().as_ref().unwrap()),
Arc::as_ptr(.线.read().unwrap().as_ref().unwrap()),
Arc::as_ptr(&1)
);
@@ -870,27 +895,3 @@ mod tests {
);
}
}
impl std::fmt::Display for {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let _str = self
.
.read()
.unwrap()
.iter()
.map(|d| format!("{}", d))
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"{}({}, {}, 元素数量: {}, [{}], {} ===>>> {})",
self..read().unwrap(),
crate::utils::format_f64_g(self.()),
crate::utils::format_f64_g(self.()),
self..read().unwrap().len(),
_str,
self.(),
self.(),
)
}
}
+68 -88
View File
@@ -38,6 +38,14 @@ use std::sync::Arc;
/// 线段 — 从笔生成线段的算法集合(静态方法命名空间)
pub struct 线;
type = (
Vec<Arc<线>>,
Vec<Arc<线>>,
Vec<Arc<线>>,
Option<Arc<线>>,
);
type = (Vec<Arc<>>, Vec<Arc<>>, Vec<Arc<>>);
impl 线 {
// ================================================================
// 基础操作
@@ -53,8 +61,7 @@ impl 线段 {
let = Self::(_rc);
if !..read().unwrap().is_empty() {
if !::(
&*
.
&.
.read()
.unwrap()
.last()
@@ -106,7 +113,7 @@ impl 线段 {
return;
}
if ..read().unwrap(). == .
&& ..read().unwrap(). != .
&& ..read().unwrap().() != .()
{
eprintln!(
"线段.武斗[{}], 发现特征值相等但时间戳不同 {} {}",
@@ -182,11 +189,9 @@ impl 线段 {
if !.iter().any(|x| Arc::as_ptr(x) == Arc::as_ptr()) {
break;
}
if !.is_empty() {
if !.last().unwrap().() {
eprintln!("线段._验证序列 数据不连续");
break;
}
if !.is_empty() && !.last().unwrap().() {
eprintln!("线段._验证序列 数据不连续");
break;
}
.push(Arc::clone());
}
@@ -204,10 +209,8 @@ impl 线段 {
if !.iter().any(|x| Arc::as_ptr(x) == Arc::as_ptr()) {
break;
}
if !.is_empty() {
if !.last().unwrap().() {
break;
}
if !.is_empty() && !.last().unwrap().() {
break;
}
.push(Arc::clone());
}
@@ -301,7 +304,7 @@ impl 线段 {
/// 特征分型终结 — 检查特征序列是否形成正常分型终结
pub fn (: &线) -> bool {
let = 线::(
&*..read().unwrap(),
&..read().unwrap(),
.(),
&Self::(),
false,
@@ -347,11 +350,9 @@ impl 线段 {
return;
}
for in & {
if let Some(ref f) = {
if f.() == .() {
panic!("特征序列方向不匹配[{}]", );
}
for f in .iter().flatten() {
if f.() == .() {
panic!("特征序列方向不匹配[{}]", );
}
}
@@ -472,15 +473,7 @@ impl 线段 {
// ================================================================
/// 分割序列 — 将线段的基础序列分为前、后、第三买卖线、贯穿伤
pub fn (
: &线,
mut : Option<&mut >,
) -> (
Vec<Arc<线>>,
Vec<Arc<线>>,
Vec<Arc<线>>,
Option<Arc<线>>,
) {
pub fn (: &线, : Option<&>) -> {
if ..read().unwrap().as_str() != "文武" {
return (
..read().unwrap().clone(),
@@ -517,9 +510,9 @@ impl 线段 {
let mut = None;
if let Some(ref mut ) = {
if let Some() = {
*._第三买卖线.write().unwrap() = None;
let = if let Some(ref ) = .last() {
let = if let Some() = .last() {
..read().unwrap().clone()
} else {
..read().unwrap().clone()
@@ -548,8 +541,6 @@ impl 线段 {
} else {
break;
}
} else {
break;
}
}
}
@@ -568,15 +559,13 @@ impl 线段 {
} else {
break;
}
} else {
break;
}
}
}
if !线.is_empty() {
线.reverse();
if let Some(ref mut ) = {
if let Some() = {
*._第三买卖线.write().unwrap() = Some(Arc::clone(&线[0]));
}
}
@@ -630,13 +619,11 @@ impl 线段 {
}) == Some(true)
{
Some(Arc::clone(..last().unwrap()))
} else if let Some(b) = ::(
&*2..read().unwrap(),
&*..last().unwrap()..read().unwrap(),
) {
Some(b)
} else {
None
::(
&2..read().unwrap(),
&..last().unwrap()..read().unwrap(),
)
};
if .is_none() {
@@ -690,14 +677,11 @@ impl 线段 {
}
/// 获取内部中枢序列 — 内部实现
fn _内部(
: &线,
_配置: &,
) -> (Vec<Arc<>>, Vec<Arc<>>, Vec<Arc<>>) {
fn _内部(: &线, _配置: &) -> {
if ..read().unwrap().as_str() != "文武" {
::(
&*..read().unwrap(),
&mut *._中枢序列.write().unwrap(),
&..read().unwrap(),
&mut ._中枢序列.write().unwrap(),
true,
&format!(
"{}_{}_合_",
@@ -718,7 +702,7 @@ impl 线段 {
::(
&,
&mut *._中枢序列.write().unwrap(),
&mut ._中枢序列.write().unwrap(),
true,
&format!(
"{}_{}_实_",
@@ -729,7 +713,7 @@ impl 线段 {
);
::(
&,
&mut *._中枢序列.write().unwrap(),
&mut ._中枢序列.write().unwrap(),
true,
&format!(
"{}_{}_虚_",
@@ -739,8 +723,8 @@ impl 线段 {
0,
);
::(
&*..read().unwrap(),
&mut *._中枢序列.write().unwrap(),
&..read().unwrap(),
&mut ._中枢序列.write().unwrap(),
true,
&format!(
"{}_{}_合_",
@@ -759,9 +743,8 @@ impl 线段 {
/// 获取内部中枢序列
pub fn (
_rc: &Arc<线>,
: &,
) -> (Vec<Arc<>>, Vec<Arc<>>, Vec<Arc<>>) {
_rc: &Arc<线>, : &
) -> {
let = Self::(_rc);
Self::_内部(, )
}
@@ -946,7 +929,7 @@ impl 线段 {
*cur..write().unwrap() = 线;
}
let idx = 线.len() - 1;
Self::(&mut 线[idx], );
Self::(&线[idx], );
true
}
@@ -1042,7 +1025,7 @@ impl 线段 {
}
}
if let Some(_) = _opt {
if _opt.is_some() {
let idx = 线.len() - 1;
let seg_rc = Arc::clone(&线[idx]);
for 线 in & {
@@ -1051,7 +1034,7 @@ impl 线段 {
线[idx] = seg_rc;
}
let idx = 线.len() - 1;
Self::(&mut 线[idx], );
Self::(&线[idx], );
let 线 = Arc::clone(&线[idx]);
if 线..read().unwrap().len() >= 3
@@ -1314,8 +1297,8 @@ impl 线段 {
}
// ---- 3. 确保当前线段有效 ----
let mut 线_rc = Arc::clone(线.last().unwrap());
Self::(&mut 线_rc, );
let 线_rc = Arc::clone(线.last().unwrap());
Self::(&线_rc, );
let seg_idx = 线.len() - 1;
线[seg_idx] = 线_rc;
@@ -1351,7 +1334,7 @@ impl 线段 {
// Refresh current segment
let idx = 线.len() - 1;
Self::(&mut 线[idx], );
Self::(&线[idx], );
// ---- 5. 调用一次全局修正 ----
Self::_缺口突破(线, , );
@@ -1379,20 +1362,20 @@ impl 线段 {
let mut = false;
for i in ...len() {
let 线 = Arc::clone(&[i]);
for 线_ref in &[..] {
let 线 = Arc::clone(线_ref);
let 线 = Arc::clone(线.last().unwrap());
let = Self::(&线);
// 向当前线段添加笔
let mut 线_rc = Arc::clone(线.last().unwrap());
Self::线(&mut 线_rc, Arc::clone(&线));
let 线_rc = Arc::clone(线.last().unwrap());
Self::线(&线_rc, Arc::clone(&线));
let seg_idx = 线.len() - 1;
线[seg_idx] = 线_rc;
// 刷新
let idx = 线.len() - 1;
Self::(&mut 线[idx], );
Self::(&线[idx], );
// 依次尝试四种修正
if Self::_缺口突破(线, , ) {
@@ -1448,14 +1431,14 @@ impl 线段 {
break;
}
// 向新段添加当前虚线
let mut _rc = Arc::clone(线.last().unwrap());
Self::线(&mut _rc, Arc::clone(&线));
let _rc = Arc::clone(线.last().unwrap());
Self::线(&_rc, Arc::clone(&线));
let seg_idx = 线.len() - 1;
线[seg_idx] = _rc;
}
let idx = 线.len() - 1;
Self::(&mut 线[idx], );
Self::(&线[idx], );
}
if {
@@ -1576,8 +1559,8 @@ impl 线段 {
}
// 验证当前线段
let mut 线_rc = Arc::clone(线.last().unwrap());
Self::(&mut 线_rc, 线);
let 线_rc = Arc::clone(线.last().unwrap());
Self::(&线_rc, 线);
let seg_idx = 线.len() - 1;
线[seg_idx] = 线_rc;
@@ -1601,7 +1584,7 @@ impl 线段 {
*cur..write().unwrap() = ;
}
let seg_idx = 线.len() - 1;
Self::(&mut 线[seg_idx], 0);
Self::(&线[seg_idx], 0);
} else {
let = Arc::clone(线.last().unwrap());
Self::_弹出扩展线段(线, &, line!());
@@ -1612,7 +1595,7 @@ impl 线段 {
// 武终
let idx = 线.len() - 1;
Self::(&mut 线[idx], 0);
Self::(&线[idx], 0);
let 线 = Arc::clone(线.last().unwrap());
if 线
@@ -1651,18 +1634,18 @@ impl 线段 {
let = ::(.(), .(), .(), .());
if .() {
let mut _rc = Arc::clone(线.last().unwrap());
Self::线(&mut _rc, Arc::clone());
let _rc = Arc::clone(线.last().unwrap());
Self::线(&_rc, Arc::clone());
let seg_idx = 线.len() - 1;
线[seg_idx] = _rc;
let mut _rc = Arc::clone(线.last().unwrap());
Self::线(&mut _rc, Arc::clone());
let _rc = Arc::clone(线.last().unwrap());
Self::线(&_rc, Arc::clone());
let seg_idx = 线.len() - 1;
线[seg_idx] = _rc;
let seg_idx = 线.len() - 1;
Self::(&mut 线[seg_idx], 0);
Self::(&线[seg_idx], 0);
continue;
}
@@ -1734,8 +1717,8 @@ impl 线段 {
{
let k线序列 = K线::rc(
&.K线序列,
&*[.len() - 3]...K线.read().unwrap(),
&*[.len() - 1]
&[.len() - 3]...K线.read().unwrap(),
&[.len() - 1]
.
.read()
.unwrap()
@@ -1855,15 +1838,13 @@ impl 线段 {
for in & {
if .len() >= 3 {
let = ::(, );
let mut : Vec<Arc<线>> =
.into_iter().map(|b| Arc::new(b)).collect();
let mut : Vec<Arc<线>> = .into_iter().map(Arc::new).collect();
.push(Arc::clone());
for in & {
.push(Arc::clone());
线.clear();
let _slice: Vec<Arc<线>> =
.iter().map(|b| Arc::clone(b)).collect();
let _slice: Vec<Arc<线>> = .iter().map(Arc::clone).collect();
Self::(
&_slice,
&mut 线,
@@ -1885,13 +1866,12 @@ impl 线段 {
if ! {
if let Some(线) = 线.last() {
if 线..read().unwrap().len() % 2 == 1 {
let =
线::线(&*线..read().unwrap());
let = 线::线(&线..read().unwrap());
.
.store(..load(Ordering::Relaxed), Ordering::Relaxed);
let mut _rc = Arc::new();
Self::(&mut _rc, &.);
let _rc = Arc::new();
Self::(&_rc, &.);
let _inner =
Arc::try_unwrap(_rc).unwrap_or_else(|rc| (*rc).clone());
if _inner.() == .() {
@@ -1923,8 +1903,8 @@ impl 线段 {
let mut = Vec::new();
for in {
let mut _rc = Arc::new();
Self::(&mut _rc, &.);
let _rc = Arc::new();
Self::(&_rc, &.);
if Self::线(&_rc, ) {
.push(Arc::clone(&_rc..read().unwrap().));
}
+63 -9
View File
@@ -42,6 +42,8 @@ pub struct 基础买卖点 {
pub K线: Option<Arc<K线>>,
pub : f64,
pub : Option<>,
/// 当前缠K的序号 — 与 Python 一致,偏移计算使用缠论K线序号而非标的K线序号
pub : i64,
}
impl {
@@ -51,6 +53,7 @@ impl 基础买卖点 {
: Arc<>,
: String,
: f64,
: i64,
) -> Self {
let K线 = Arc::clone(&.);
Self {
@@ -63,12 +66,13 @@ impl 基础买卖点 {
K线: None,
: ,
: None,
,
}
}
/// 偏移 — 当前K线与买卖点K线序号
/// 偏移 — 当前缠K序号与买卖点K线序号的差(对齐 Python,使用缠论K线序号)
pub fn (&self) -> i64 {
self.K线. - self.K线..load(Ordering::Relaxed)
self. - self.K线..load(Ordering::Relaxed)
}
/// 失效偏移
@@ -118,8 +122,16 @@ impl 买卖点 {
_标识: &str,
: String,
: f64,
: i64,
) -> {
::new(::, K线, , , )
::new(
::,
K线,
,
,
,
,
)
}
pub fn (
@@ -128,8 +140,16 @@ impl 买卖点 {
_标识: &str,
: String,
: f64,
: i64,
) -> {
::new(::, K线, , , )
::new(
::,
K线,
,
,
,
,
)
}
pub fn (
@@ -138,8 +158,16 @@ impl 买卖点 {
_标识: &str,
: String,
: f64,
: i64,
) -> {
::new(::, K线, , , )
::new(
::,
K线,
,
,
,
,
)
}
pub fn (
@@ -148,8 +176,16 @@ impl 买卖点 {
_标识: &str,
: String,
: f64,
: i64,
) -> {
::new(::, K线, , , )
::new(
::,
K线,
,
,
,
,
)
}
pub fn (
@@ -158,8 +194,16 @@ impl 买卖点 {
_标识: &str,
: String,
: f64,
: i64,
) -> {
::new(::, K线, , , )
::new(
::,
K线,
,
,
,
,
)
}
pub fn (
@@ -168,8 +212,16 @@ impl 买卖点 {
_标识: &str,
: String,
: f64,
: i64,
) -> {
::new(::, K线, , , )
::new(
::,
K线,
,
,
,
,
)
}
/// 生成买卖点 — 根据参数自动选择类型
@@ -190,6 +242,8 @@ impl 买卖点 {
// 当前K线 — 从缠K获取其标的K线
let K线 = Arc::clone(&*K.K线.read().unwrap());
// 当前序号 — 使用缠论K线序号(对齐 Python 偏移计算)
let = K..load(Ordering::Relaxed);
let = match (, ) {
("", "") => ::,
@@ -201,6 +255,6 @@ impl 买卖点 {
_ => ::, // fallback
};
::new(, K线, , , )
::new(, K线, , , , )
}
}
+76 -61
View File
@@ -155,6 +155,14 @@ impl 观察者 {
self.__处理数据(K);
}
/// 投喂原始数据 — 便捷入口,直接从 OHLCV 创建 K线 并投喂
pub fn (
&mut self, : i64, : f64, : f64, : f64, : f64, : f64
) {
let K = K线::K(&self., , , , , , , 0, self.);
self.K线(K);
}
/// 核心数据处理管道
fn __处理数据(&mut self, K: K线) {
// Step 1: 缠论K线分析 (普K is consumed by 分析 as &mut)
@@ -177,6 +185,7 @@ impl 观察者 {
&mut self.,
&self.K线序列,
&self.K线序列,
0,
&self.,
);
}
@@ -289,6 +298,7 @@ impl 观察者 {
&mut self.,
&self.K线序列,
&self.K线序列,
0,
&self.,
);
}
@@ -434,7 +444,7 @@ impl 观察者 {
format!(
"分型, {}, {}, {:?}, {}, {}, {}",
i,
fx.,
fx.(),
fx.,
fx.,
fx...load(Ordering::Relaxed),
@@ -477,39 +487,26 @@ impl 观察者 {
println!("全部数据拆分保存完成,目录:{}", .display());
}
/// 解析本地数据文件 — 从 .nb 文件读取并解析所有 K线
pub fn (&self, : &str) -> Result<Vec<K线>, String> {
let data = std::fs::read().map_err(|e| format!("read file: {}", e))?;
let mut bars = Vec::new();
let size = 48;
for i in 0..data.len() / size {
let offset = i * size;
if let Some(k线) = K线::from_bytes(&data[offset..offset + size], self., &self.)
{
bars.push(k线);
}
}
Ok(bars)
}
/// 加载本地数据 — 从 .nb 文件加载数据到当前观察者(先重置再投喂)
pub fn (&mut self, : &str) -> Result<(), String> {
self.();
let bars = self.()?;
for k线 in bars {
self.K线(k线);
let data = std::fs::read().map_err(|e| format!("read file: {}", e))?;
let size = 48;
for i in 0..data.len() / size {
let offset = i * size;
if let Some((, , , , , )) =
K线::(&data[offset..offset + size])
{
self.(, , , , , );
}
}
Ok(())
}
/// 读取数据文件 — .nb 文件加载数据
/// 读取数据文件 — 更新当前观察者并加载 .nb 文件
pub fn (
: &str,
: Option<>,
) -> Result<Arc<RwLock<Self>>, String> {
let = .unwrap_or_default();
// Parse filename: btcusd-300-1631772074-1632222374.nb
&mut self, : &str, :
) -> Result<(), String> {
let path = std::path::Path::new();
let name = path
.file_stem()
@@ -519,23 +516,12 @@ impl 观察者 {
if parts.len() < 4 {
return Err(format!("invalid filename format: {}", name));
}
let = parts[0].to_string();
let : i64 = parts[1]
self. = parts[0].to_string();
self. = parts[1]
.parse()
.map_err(|e| format!("parse period: {}", e))?;
let = 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") {
.write().unwrap().K线(k线);
}
}
Ok()
self. = ;
self.()
}
}
@@ -544,12 +530,23 @@ mod tests {
use super::*;
use crate::config::;
const TEST_DATA_PATH: &str = "/home/moscow/chanlun.rs/btcusd-300-1777649100-1778398800.nb";
fn test_data_path() -> String {
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
manifest
.parent()
.unwrap()
.join("btcusd-300-1777649100-1778398800.nb")
.to_string_lossy()
.to_string()
}
#[test]
fn test_普k序列指针一致性() {
let config = ::default();
let obs = ::(TEST_DATA_PATH, Some(config)).unwrap();
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
for (i, bi) in obs_ref..iter().enumerate() {
@@ -593,7 +590,7 @@ mod tests {
let config = ::default();
let obs_ref = ::new("btcusd".into(), 300, config);
let data = std::fs::read(TEST_DATA_PATH).unwrap();
let data = std::fs::read(test_data_path()).unwrap();
let size = 48;
for i in 0..data.len() / size {
@@ -631,8 +628,11 @@ mod tests {
#[test]
fn test_分型到笔的文武Rc指针一致性() {
let config = ::default();
let obs = ::(TEST_DATA_PATH, Some(config)).unwrap();
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
// 每个笔的文/武 分型 Rc 指针必须在 分型序列 中
@@ -640,7 +640,7 @@ mod tests {
let _ptr = Arc::as_ptr(&bi.);
let _found = obs_ref..iter().any(|f| Arc::as_ptr(f) == _ptr);
if !_found {
println!("{}: 文(时间戳={}) 不在分型序列中!", i, bi..);
println!("{}: 文(时间戳={}) 不在分型序列中!", i, bi..());
}
let _ptr = Arc::as_ptr(&*bi..read().unwrap());
@@ -649,7 +649,7 @@ mod tests {
println!(
"笔 {}: 武(时间戳={}) 不在分型序列中!",
i,
bi..read().unwrap().
bi..read().unwrap().()
);
}
}
@@ -661,8 +661,11 @@ mod tests {
#[test]
fn test_笔到线段的基础序列Rc指针一致性() {
let config = ::default();
let obs = ::(TEST_DATA_PATH, Some(config)).unwrap();
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
// 每个线段的基础序列中的笔 Rc 指针必须在 笔序列 中
@@ -683,8 +686,11 @@ mod tests {
#[test]
fn test_中枢基础序列与笔序列Rc指针一致() {
let config = ::default();
let obs = ::(TEST_DATA_PATH, Some(config)).unwrap();
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
for (i, hub) in obs_ref._中枢序列.iter().enumerate() {
@@ -714,7 +720,7 @@ mod tests {
#[test]
fn test_重复计算后结果一致() {
let data = std::fs::read(TEST_DATA_PATH).unwrap();
let data = std::fs::read(test_data_path()).unwrap();
let size = 48;
let = || {
@@ -758,7 +764,7 @@ mod tests {
let config = ::default();
let obs_ref = ::new("btcusd".into(), 300, config);
let data = std::fs::read(TEST_DATA_PATH).unwrap();
let data = std::fs::read(test_data_path()).unwrap();
let size = 48;
for i in 0..data.len() / size {
@@ -798,8 +804,11 @@ mod tests {
#[test]
fn test_RefCell借用安全性_连续读取不panic() {
let config = ::default();
let obs = ::(TEST_DATA_PATH, Some(config)).unwrap();
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
// 连续大量读取所有 RefCell 字段,不应 panic
@@ -829,8 +838,11 @@ mod tests {
#[test]
fn test_RefCell借用安全性_交替读写不panic() {
let config = ::default();
let obs = ::(TEST_DATA_PATH, Some(config)).unwrap();
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
// 交替读写 RefCell 字段 — 先读再写同字段,分离 borrow 作用域
@@ -858,8 +870,11 @@ mod tests {
#[test]
fn test_缠K到分型的Rc指针一致性() {
let config = ::default();
let obs = ::(TEST_DATA_PATH, Some(config)).unwrap();
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
// 每个分型的左/中/右 缠K 指针必须在 缠论K线序列 中
+1
View File
@@ -77,6 +77,7 @@ impl Default for 随机指标 {
impl {
/// 首次计算 KDJ(无历史数据时)
#[allow(clippy::too_many_arguments)]
pub fn (
: f64,
: f64,
+16
View File
@@ -125,7 +125,23 @@ impl K线 {
Self::from_bytes(, , )
}
/// 解析原始数据 — 只提取时间戳+OHLCV,不构造 K线
pub fn (: &[u8]) -> Option<(i64, f64, f64, f64, f64, f64)> {
if .len() < 48 {
return None;
}
let mut reader = &[..48];
let = reader.read_f64::<BigEndian>().ok()? as i64;
let = reader.read_f64::<BigEndian>().ok()?;
let = reader.read_f64::<BigEndian>().ok()?;
let = reader.read_f64::<BigEndian>().ok()?;
let = reader.read_f64::<BigEndian>().ok()?;
let = reader.read_f64::<BigEndian>().ok()?;
Some((, , , , , ))
}
/// 创建普通K线
#[allow(clippy::too_many_arguments)]
pub fn K(
: &str,
: i64,
+41 -49
View File
@@ -199,19 +199,16 @@ impl 缠论K线 {
if k线..load(Ordering::Relaxed) <= k..load(Ordering::Relaxed)
&& k..load(Ordering::Relaxed)
<= k线..load(Ordering::Relaxed) + k线.
&& (k线..get() - k..get()).abs() < f64::EPSILON
{
if (k线..get() - k..get()).abs() < f64::EPSILON
{
return k..load(Ordering::Relaxed);
}
return k..load(Ordering::Relaxed);
}
} else if k..load(Ordering::Relaxed) <= k线..load(Ordering::Relaxed)
&& k线..load(Ordering::Relaxed)
<= k..load(Ordering::Relaxed) + k.
&& (k线..get() - k..get()).abs() < f64::EPSILON
{
if (k线..get() - k..get()).abs() < f64::EPSILON {
return k..load(Ordering::Relaxed);
}
return k..load(Ordering::Relaxed);
}
}
}
@@ -219,6 +216,7 @@ impl 缠论K线 {
}
/// 创建缠K
#[allow(clippy::too_many_arguments)]
pub fn K(
: i64,
: f64,
@@ -420,43 +418,41 @@ impl 缠论K线 {
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,
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线.,
));
}
&.,
),
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();
@@ -514,7 +510,7 @@ impl 缠论K线 {
if !K序列.is_empty() {
let len = K序列.len();
let (, ) = K序列.split_at_mut(len - 1);
let K: Option<&K线> = .last().map(|rc| Arc::as_ref(rc));
let K: Option<&K线> = .last().map(Arc::as_ref);
let K = &*[0];
let (K, ) = Self::(K, K, K线_ref, );
@@ -614,7 +610,7 @@ impl 缠论K线 {
Arc::clone(&K序列[idx - 2]),
Some(Arc::clone(&K序列[idx - 1])),
));
return (, Some());
(, Some())
}
/// 截取缠K序列从始到终
@@ -623,12 +619,8 @@ impl 缠论K线 {
: &K线,
: &K线,
) -> Option<Vec<Arc<K线>>> {
let _idx =
.iter()
.position(|k| Arc::as_ptr(k) == ( as *const _))?;
let _idx =
.iter()
.position(|k| Arc::as_ptr(k) == ( as *const _))?;
let _idx = .iter().position(|k| std::ptr::eq(Arc::as_ptr(k), ))?;
let _idx = .iter().position(|k| std::ptr::eq(Arc::as_ptr(k), ))?;
Some([_idx..=_idx].to_vec())
}
}
+29 -31
View File
@@ -77,38 +77,36 @@ fn 测试_读取数据(文件路径: &str) {
let = Instant::now();
let = ::default().();
match ::(, Some()) {
Ok() => {
let = .read().unwrap();
let = .elapsed();
println!(
"测试_读取数据 耗时 {:.2?} 普K数量 {}",
,
.K线序列.len()
);
println!("符号: {}", .);
println!("周期: {}", .);
println!("缠K数量: {}", .K线序列.len());
println!("分型数量: {}", ..len());
println!("笔数量: {}", ..len());
println!("笔中枢数量: {}", ._中枢序列.len());
println!("线段数量: {}", .线.len());
println!("中枢数量: {}", ..len());
println!("扩展线段数量: {}", .线.len());
println!("线段_线段序列数量: {}", .线_线段序列.len());
println!(
"扩展线段_扩展线段数量: {}",
.线_扩展线段.len()
);
let = ::new("".into(), 0, ::default());
.write()
.unwrap()
.(, )
.expect("读取数据文件失败");
let = .read().unwrap();
let = .elapsed();
println!(
"测试_读取数据 耗时 {:.2?} 普K数量 {}",
,
.K线序列.len()
);
println!("符号: {}", .);
println!("周期: {}", .);
println!("缠K数量: {}", .K线序列.len());
println!("分型数量: {}", ..len());
println!("数量: {}", ..len());
println!("笔中枢数量: {}", ._中枢序列.len());
println!("线段数量: {}", .线.len());
println!("中枢数量: {}", ..len());
println!("扩展线段数量: {}", .线.len());
println!("线段_线段序列数量: {}", .线_线段序列.len());
println!(
"扩展线段_扩展线段数量: {}",
.线_扩展线段.len()
);
println!("\n===== 保存分析数据 =====\n");
._保存数据(None);
}
Err(e) => {
eprintln!("读取失败: {}", e);
std::process::exit(1);
}
}
println!("\n===== 保存分析数据 =====\n");
._保存数据(None);
}
/// 测试_周期合成 — 多周期合成分析
+75 -71
View File
@@ -239,9 +239,9 @@ impl 虚线 {
self..read().unwrap(),
self..load(Ordering::Relaxed),
self..load(Ordering::Relaxed),
self..,
self..(),
format_f64_g(self..),
self..read().unwrap().,
self..read().unwrap().(),
format_f64_g(self..read().unwrap().),
if self..load(Ordering::Relaxed) {
"True"
@@ -330,9 +330,9 @@ impl 虚线 {
self..read().unwrap(),
self..load(Ordering::Relaxed),
self..load(Ordering::Relaxed),
self..,
self..(),
format_f64_g(self..),
self..read().unwrap().,
self..read().unwrap().(),
format_f64_g(self..read().unwrap().),
if self..load(Ordering::Relaxed) { "True" } else { "False" },
self..read().unwrap().len(),
@@ -435,8 +435,8 @@ impl 虚线 {
pub fn MACD柱子均值(K序列: &[Arc<K线>], 线: &线) -> f64 {
let K线序列 = K线::rc(
K序列,
&*线...K线.read().unwrap(),
&*线..read().unwrap()..K线.read().unwrap(),
&线...K线.read().unwrap(),
&线..read().unwrap()..K线.read().unwrap(),
);
if K线序列.is_empty() {
return 0.0;
@@ -453,8 +453,8 @@ impl 虚线 {
pub fn MACD柱子均值_阴(K序列: &[Arc<K线>], 线: &线) -> Option<f64> {
let K线序列 = K线::rc(
K序列,
&*线...K线.read().unwrap(),
&*线..read().unwrap()..K线.read().unwrap(),
&线...K线.read().unwrap(),
&线..read().unwrap()..K线.read().unwrap(),
);
let : Vec<f64> = K线序列
.iter()
@@ -473,8 +473,8 @@ impl 虚线 {
pub fn MACD柱子均值_阳(K序列: &[Arc<K线>], 线: &线) -> Option<f64> {
let K线序列 = K线::rc(
K序列,
&*线...K线.read().unwrap(),
&*线..read().unwrap()..K线.read().unwrap(),
&线...K线.read().unwrap(),
&线..read().unwrap()..K线.read().unwrap(),
);
let : Vec<f64> = K线序列
.iter()
@@ -549,8 +549,8 @@ impl 虚线 {
};
let K线序列 = K线::rc(
K序列,
&*线...K线.read().unwrap(),
&*线..read().unwrap()..K线.read().unwrap(),
&线...K线.read().unwrap(),
&线..read().unwrap()..K线.read().unwrap(),
);
let : Vec<f64> = K线序列
.iter()
@@ -583,7 +583,7 @@ impl 虚线 {
if == :: {
let : Vec<&Arc<K线>> = K序列
.iter()
.filter(|k| k.macd.as_ref().map_or(false, |m| m.MACD柱 > 0.0))
.filter(|k| k.macd.as_ref().is_some_and(|m| m.MACD柱 > 0.0))
.collect();
if .is_empty() {
return [false, false, false];
@@ -603,7 +603,7 @@ impl 虚线 {
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap();
let mut = vec![Arc::clone(*), Arc::clone()];
let mut = [Arc::clone(*), Arc::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]. {
@@ -649,7 +649,7 @@ impl 虚线 {
} else {
let : Vec<&Arc<K线>> = K序列
.iter()
.filter(|k| k.macd.as_ref().map_or(false, |m| m.MACD柱 < 0.0))
.filter(|k| k.macd.as_ref().is_some_and(|m| m.MACD柱 < 0.0))
.collect();
if .is_empty() {
return [false, false, false];
@@ -670,7 +670,7 @@ impl 虚线 {
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap();
let mut = vec![Arc::clone(*), Arc::clone()];
let mut = [Arc::clone(*), Arc::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]. {
@@ -947,16 +947,62 @@ impl 虚线 {
}
}
if ! && && 线..read().unwrap()..MACD柱子匹配() {
if Self::MACD极值(K序列, 线) && .len() > 2 {
return (true, "没结果, 极值, 柱子分型匹配, 背驰过大于2次".into());
}
if !
&&
&& 线..read().unwrap()..MACD柱子匹配()
&& 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..read().unwrap() == "" {
write!(
f,
"笔({}, {}, {}, {}, 周期: {}, 数量: {})",
self..load(Ordering::Relaxed),
self.(),
self.,
self..read().unwrap(),
self...,
self..read().unwrap()...load(Ordering::Relaxed)
- self....load(Ordering::Relaxed)
+ 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线.read().unwrap() {
Some(k) => format!("{}", k),
None => "None".to_string(),
};
write!(
f,
"{}<{}, {}, {}, {}, {}, 数量: {}, 缺口: {}, {}>",
self..read().unwrap(),
self..load(Ordering::Relaxed),
,
self.(),
self.,
self..read().unwrap(),
self..read().unwrap().len(),
_str,
K线_str,
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -966,13 +1012,14 @@ mod tests {
/// 辅助:创建一根最小化的原始K线
fn _创建K线(: i64, : f64, : f64, : f64, : f64) -> K线 {
let mut k = K线::default();
k. = ;
k. = ;
k. = ;
k. = ;
k. = ;
k
K线 {
,
,
,
: ,
: ,
..Default::default()
}
}
/// 辅助:创建一根缠论K线
@@ -1232,50 +1279,7 @@ mod tests {
assert_eq!(Arc::as_ptr(&.), _ptr_before);
// 但方向变了(因为武从底1变成底2)
let = ..read().unwrap().;
let = ..read().unwrap().();
assert_eq!(, 300);
}
}
impl std::fmt::Display for 线 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if *self..read().unwrap() == "" {
write!(
f,
"笔({}, {}, {}, {}, 周期: {}, 数量: {})",
self..load(Ordering::Relaxed),
self.(),
self.,
self..read().unwrap(),
self...,
self..read().unwrap()...load(Ordering::Relaxed)
- self....load(Ordering::Relaxed)
+ 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线.read().unwrap() {
Some(k) => format!("{}", k),
None => "None".to_string(),
};
write!(
f,
"{}<{}, {}, {}, {}, {}, 数量: {}, 缺口: {}, {}>",
self..read().unwrap(),
self..load(Ordering::Relaxed),
,
self.(),
self.,
self..read().unwrap(),
self..read().unwrap().len(),
_str,
K线_str,
)
}
}
}
+14 -1
View File
@@ -25,9 +25,13 @@
use crate::kline::chan_kline::K线;
use crate::types::;
use crate::types::;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;
/// 分型模式 — True 时使用构造时缓存值(默认),False 时从 中 缠K 实时读取
pub static : AtomicBool = AtomicBool::new(true);
/// 分型 — 由三根缠K构成(可能缺左或右)
#[derive(Debug, Clone)]
pub struct {
@@ -56,6 +60,15 @@ impl 分型 {
}
}
/// 时间戳 — 根据 分型模式 决定返回缓存值(True)或实时值(False)
pub fn (&self) -> i64 {
if .load(Ordering::Relaxed) {
self.
} else {
self...load(Ordering::Relaxed)
}
}
/// 左中右三组关系
pub fn (&self) -> Option<(, , )> {
let = self..as_ref()?;
@@ -215,7 +228,7 @@ impl std::fmt::Display for 分型 {
.read()
.unwrap()
.unwrap_or(crate::types::::),
self.,
self.(),
crate::utils::format_f64_g(self.),
if self..is_none() { "True" } else { "False" },
if self..is_none() { "True" } else { "False" },
+17 -16
View File
@@ -63,7 +63,7 @@ impl 线段特征 {
.
.partial_cmp(&b..)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a...cmp(&b..))
.then_with(|| a..().cmp(&b..()))
})
.map(|x| Arc::clone(&x.))
.unwrap_or_else(|| Arc::clone(&self.[0].))
@@ -75,7 +75,7 @@ impl 线段特征 {
.
.partial_cmp(&b..)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| b...cmp(&a..))
.then_with(|| b..().cmp(&a..()))
})
.map(|x| Arc::clone(&x.))
.unwrap_or_else(|| Arc::clone(&self.[0].))
@@ -99,8 +99,8 @@ impl 线段特征 {
a.
.read()
.unwrap()
.
.cmp(&b..read().unwrap().)
.()
.cmp(&b..read().unwrap().())
})
})
.map(|x| x..read().unwrap().clone())
@@ -119,8 +119,8 @@ impl 线段特征 {
b.
.read()
.unwrap()
.
.cmp(&a..read().unwrap().)
.()
.cmp(&a..read().unwrap().())
})
})
.map(|x| x..read().unwrap().clone())
@@ -173,7 +173,7 @@ impl 线段特征 {
/// 新建特征序列元素
pub fn (线: Vec<Arc<线>>, 线: ) -> Self {
let = format!("特征<虚线>");
let = "特征<虚线>".to_string();
Self::new(, 线, 线)
}
@@ -320,13 +320,14 @@ mod tests {
use crate::types::;
fn _创建K线(: i64, : f64, : f64, : f64, : f64) -> K线 {
let mut k = K线::default();
k. = ;
k. = ;
k. = ;
k. = ;
k. = ;
k
K线 {
,
,
,
: ,
: ,
..Default::default()
}
}
fn _创建缠K(
@@ -527,7 +528,7 @@ mod tests {
let = feat.();
// 向上取最大特征值:都是100 → tiebreaker取后时间戳 → 笔2.文(300)
assert_eq!(., 300);
assert_eq!(.(), 300);
}
#[test]
@@ -544,7 +545,7 @@ mod tests {
let = feat.();
// 向上取最大特征值:都是80 → tiebreaker取后时间戳 → 笔2.武(400)
assert_eq!(., 400);
assert_eq!(.(), 400);
}
// ============================================================
+1
View File
@@ -92,6 +92,7 @@ impl 分型结构 {
)
}
#[allow(clippy::too_many_arguments)]
fn _内部(
: f64,
: f64,
+24
View File
@@ -1,3 +1,27 @@
/*
* 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.
*/
use std::sync::atomic::{AtomicU64, Ordering};
/// f64 原子类型 — 基于 AtomicU64 + 位转换,API 与 `Cell<f64>` 一致。
+3 -3
View File
@@ -42,7 +42,7 @@ pub fn format_f64_g(value: f64) -> String {
let exp = abs.log10().floor() as i32;
// Python :g 科学计数法边界: exp < -4 或 exp >= p (=6)
if exp < -4 || exp >= 6 {
if !(-4..6).contains(&exp) {
let significand = value / 10_f64.powi(exp);
let s = format!("{:.5}", significand);
let s = s.trim_end_matches('0').trim_end_matches('.');
@@ -57,12 +57,12 @@ pub fn format_f64_g(value: f64) -> String {
}
let s = format!("{:.prec$}", value, prec = 6 - int_digits);
let s = s.trim_end_matches('0');
return s.trim_end_matches('.').to_string();
s.trim_end_matches('.').to_string()
} else {
let leading_zeros = (-exp) as usize;
let s = format!("{:.prec$}", value, prec = leading_zeros + 5);
let s = s.trim_end_matches('0');
return s.trim_end_matches('.').to_string();
s.trim_end_matches('.').to_string()
}
}
+63 -34
View File
@@ -657,35 +657,56 @@ class 随机数据(bt.feeds.DataBase):
class 自定义实时数据源(bt.feed.DataBase):
"""
一个用于模拟实时数据推送的数据源继承自Backtrader的DataBase
在实际应用中你需要将模拟生成数据的部分替换为接收WebSocket等推送数据的代码
支持两种数据输入方式
1. 手动投喂调用 投喂数据(时间戳, , , , , ) WebSocket 回调等外部代码推送
2. 后台线程传入 观察员 魔法 参数start() 时自动启动线程获取历史数据
(注意观察员 需实现 读取任意数据 方法)
时间戳格式Unix 时间戳 int 类型
"""
def __init__(self, 数据队列: queue.Queue, 观察员: "观察者", 魔法, **魔法参数):
# 调用父类初始化方法
def __init__(self, 数据队列: queue.Queue = None, 观察员: "观察者" = None, 魔法=None, **魔法参数):
super(自定义实时数据源, self).__init__()
# 存储外部传入的数据队列,用于接收实时数据
self.数据队列 = 数据队列
# 定义一个标志,用于控制数据加载的停止
self.数据队列 = 数据队列 or queue.Queue()
self.正在运行 = False
self.观察员 = 观察员
self.魔法 = 魔法
self.__魔法参数 = 魔法参数
self.已有数据 = False
def 投喂数据(self, 时间戳: int, 开盘价: float, 最高价: float, 最低价: float, 收盘价: float, 成交量: float, 持仓量: float = 0):
"""线程安全的外部数据推送接口,供 WebSocket 回调等外部代码调用。
:param 时间戳: Unix 时间戳int 类型
:param 开盘价: 开盘价
:param 最高价: 最高价
:param 最低价: 最低价
:param 收盘价: 收盘价
:param 成交量: 成交量
:param 持仓量: 持仓量默认为 0
"""
self.数据队列.put((时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量, 持仓量))
def start(self):
"""
数据源开始工作时的初始化操作
这里可以不执行任何操作或者启动数据接收线程等
"""
print(f"[{datetime.now()}] 自定义数据源已启动...")
self.正在运行 = True
def 运行回测():
self.观察员.读取任意数据(self.魔法, **self.__魔法参数)
self.正在运行 = False
if self.观察员 is not None and self.魔法 is not None:
if not hasattr(self.观察员, "读取任意数据"):
print(f"[{datetime.now()}] 警告: 观察员没有 '读取任意数据' 方法,后台线程不会启动")
return
回测线程 = threading.Thread(target=运行回测, daemon=True)
回测线程.start()
def 运行回测():
try:
self.观察员.读取任意数据(self.魔法, **self.__魔法参数)
except Exception as e:
print(f"[{datetime.now()}] 后台数据线程异常: {e}")
finally:
self.正在运行 = False
回测线程 = threading.Thread(target=运行回测, daemon=True)
回测线程.start()
def stop(self):
"""
@@ -696,32 +717,25 @@ class 自定义实时数据源(bt.feed.DataBase):
print(f"[{datetime.now()}] 自定义数据源已停止。")
def _load(self):
"""
(核心方法) Backtrader会循环调用此方法来获取数据
每次被调用时都应该返回新的数据行一个K线/一个数据点
如果没有新数据返回 FalseBacktrader会等待下次调用
"""
"""Backtrader 核心回调 — 阻塞等待数据,有数据时返回 True,数据源停止时返回 False。"""
if not self.正在运行:
return False
# 当没有新数据且系统仍在运行时,进入等待
# 从外部队列获取新数据
while True:
try:
data_point = self.数据队列.get(timeout=0.5)
break
except queue.Empty:
if not self.已有数据:
continue
else:
if self.正在运行:
continue
if not self.正在运行:
return False
# 解析数据元组
dt, o, h, l, c, v, oi = data_point
# 将数据写入Backtrader的数据线 (lines)
self.lines.datetime[0] = bt.date2num(dt)
try:
dt, o, h, l, c, v, oi = data_point
except (ValueError, TypeError) as e:
print(f"[{datetime.now()}] 自定义数据源: 数据格式错误,跳过: {e}")
return True
self.lines.datetime[0] = bt.date2num(datetime.utcfromtimestamp(int(dt)))
self.lines.open[0] = o
self.lines.high[0] = h
self.lines.low[0] = l
@@ -729,7 +743,6 @@ class 自定义实时数据源(bt.feed.DataBase):
self.lines.volume[0] = v
self.lines.openinterest[0] = oi
self.已有数据 = True
# 返回 True 表示成功加载一行数据
return True
@@ -757,7 +770,7 @@ class 高级策略基类(bt.Strategy):
def 日志(self, 文本, 时间=None):
时间 = 时间 or self.datas[0].datetime.datetime(0)
print(f"{时间} {文本}")
print(f"{时间} {self.p.观察员.__class__.__name__}: {文本}")
def 计算目标数量(self, 价格):
"""根据资金类型和仓位比例计算目标数量"""
@@ -946,6 +959,7 @@ class 回测(高级策略基类):
def __init__(self):
super().__init__()
self.已处理信号 = set()
print(f"{self.p.观察员.__class__.__name__}: 加载完成...")
def 获取开仓限价(self, 是否做多):
# 根据缠论分型计算限价,若无则返回 None 使用基类默认逻辑
@@ -957,6 +971,7 @@ class 回测(高级策略基类):
return None
def next(self):
# self.日志(f"{self.p.观察员.__class__.__name__} called next ")
# 1. 更新移动止损(基类方法)
if self.position:
self.更新止损订单(self.position.size > 0, self.data.close[0])
@@ -986,15 +1001,22 @@ class 回测(高级策略基类):
def 检查买信号(self):
if self.p.观察员.笔序列:
k线 = self.p.观察员.缠论K线序列[-1]
self.日志(f"检查买信号 当前笔 {self.p.观察员.笔序列[-1]}")
if k线.买卖点信息:
print(f"回测-首 {self.p.观察员.__class__.__name__}", k线.买卖点信息)
= True if k线.买卖点信息 and "" in next(iter(k线.买卖点信息)) else False
if :
print(k线)
原始差值 = self.p.观察员.当前K线.序号 - k线.标的K线.序号
差值 = self.p.观察员.当前缠K.序号 - k线.序号
self.日志(f"首_买入信号差值: {原始差值}, {差值}, 观察员.当前K线 时间戳: {self.p.观察员.当前K线.时间戳}")
k线 = self.p.观察员.缠论K线序列[-2]
if k线.买卖点信息:
print(f"回测-尾 {self.p.观察员.__class__.__name__}", k线.买卖点信息)
= True if self.p.观察员.缠论K线序列[-2].买卖点信息 and "" in next(iter(self.p.观察员.缠论K线序列[-2].买卖点信息)) else False
if :
print(k线)
原始差值 = self.p.观察员.当前K线.序号 - k线.标的K线.序号
差值 = self.p.观察员.当前缠K.序号 - k线.序号
self.日志(f"尾_买入信号差值: {原始差值}, {差值}, 观察员.当前K线 时间戳: {self.p.观察员.当前K线.时间戳}")
@@ -1002,16 +1024,23 @@ class 回测(高级策略基类):
def 检查卖信号(self):
if self.p.观察员.笔序列:
self.日志(f"检查卖信号 当前笔 {self.p.观察员.笔序列[-1]}")
k线 = self.p.观察员.缠论K线序列[-1]
if k线.买卖点信息:
print(f"回测-首 {self.p.观察员.__class__.__name__}", k线.买卖点信息)
= True if k线.买卖点信息 and "" in next(iter(k线.买卖点信息)) else False
if :
print(k线)
原始差值 = self.p.观察员.当前K线.序号 - k线.标的K线.序号
差值 = self.p.观察员.当前缠K.序号 - k线.序号
self.日志(f"首_买入信号差值: {原始差值}, {差值}, 观察员.当前K线 时间戳: {self.p.观察员.当前K线.时间戳}")
k线 = self.p.观察员.缠论K线序列[-2]
if k线.买卖点信息:
print(f"回测-尾 {self.p.观察员.__class__.__name__}", k线.买卖点信息)
= True if self.p.观察员.缠论K线序列[-2].买卖点信息 and "" in next(iter(self.p.观察员.缠论K线序列[-2].买卖点信息)) else False
if :
print(k线)
原始差值 = self.p.观察员.当前K线.序号 - k线.标的K线.序号
差值 = self.p.观察员.当前缠K.序号 - k线.序号
self.日志(f"尾_买入信号差值: {原始差值}, {差值}, 观察员.当前K线 时间戳: {self.p.观察员.当前K线.时间戳}")
@@ -1019,7 +1048,7 @@ class 回测(高级策略基类):
def log(self, 文本, dt=None):
dt = dt or bt.num2date(self.data.datetime[0])
print(f"[{dt.strftime('%Y-%m-%d %H:%M')}] {self.p.符号} | {文本}")
print(f"[{dt.strftime('%Y-%m-%d %H:%M')}] {self.p.观察员.__class__.__name__}: {self.p.符号} | {文本}")
# ==================== 回测运行入口 ====================