3 Commits

Author SHA1 Message Date
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
17 changed files with 434 additions and 252 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 -- -D warnings
- 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 package --dry-run
- 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
# ============================================================
# 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: 下载所有产物
+30 -19
View File
@@ -5078,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线,增量更新所有层级
@@ -5224,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线合成器:
@@ -5503,16 +5513,16 @@ class 立体分析器:
print(f"多级别数据拆分保存完成,目录:{保存路径.resolve()}")
def 测试_读取数据(配置: 缠论配置):
def 测试_读取数据(观察员: 观察者, 配置: 缠论配置)-> Callable[[], "观察者"]:
"""测试_读取数据
:param 观察员: 观察者
:param 配置: 缠论配置
:return: 测试函数
"""
def 魔法():
启动时间 = datetime.now()
观察 = 观察.读取数据文件(配置.加载文件路径, 配置)
观察者.读取数据文件(观察员, 配置.加载文件路径, 配置)
消耗用时 = datetime.now() - 启动时间
print("测试_读取数据 耗时", 消耗用时, "普K数量", len(观察员.普通K线序列))
return 观察员
@@ -5552,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.94"
version = "26.5.101"
edition = "2021"
description = "缠论技术分析库 — Rust 高性能 Python 绑定"
authors = ["YuYuKunKun"]
@@ -12,7 +12,7 @@ crate-type = ["cdylib"]
name = "chanlun"
[dependencies]
chanlun = "26.5.4" # { path = "../chanlun" }
pyo3 = { version = "0.28", features = ["extension-module", "experimental-inspect"] }
chanlun = "26.5.5" # { path = "../chanlun" }
pyo3 = { version = "0.28", features = ["experimental-inspect"] }
serde_json = "1"
chrono = "0.4"
+2
View File
@@ -27,6 +27,8 @@ __all__ = [
"转化为时间戳",
"转化为时间戳_数字",
"随机指标",
"chan",
]
from ._chanlun import *
from . import chan
+28 -18
View File
@@ -5078,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线,增量更新所有层级
@@ -5224,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线合成器:
@@ -5503,16 +5513,16 @@ class 立体分析器:
print(f"多级别数据拆分保存完成,目录:{保存路径.resolve()}")
def 测试_读取数据(配置: 缠论配置):
def 测试_读取数据(观察员: 观察者, 配置: 缠论配置)-> Callable[()]:
"""测试_读取数据
:param 观察员: 观察者
:param 配置: 缠论配置
:return: 测试函数
"""
def 魔法():
启动时间 = datetime.now()
观察 = 观察.读取数据文件(配置.加载文件路径, 配置)
观察者.读取数据文件(观察员, 配置.加载文件路径, 配置)
消耗用时 = datetime.now() - 启动时间
print("测试_读取数据 耗时", 消耗用时, "普K数量", len(观察员.普通K线序列))
return 观察员
@@ -5553,5 +5563,5 @@ if __name__ == "__main__":
当前配置 = 缠论配置.不推送()
当前配置.加载文件路径 = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "tests", "btcusd-300-1761327300-1776327900.nb")
with tempfile.TemporaryDirectory() as tmpdir:
测试_读取数据(当前配置)().测试_保存数据(tmpdir)
测试_读取数据(观察者("", 0, 当前配置), 当前配置)().测试_保存数据(tmpdir)
测试_周期合成(当前配置)().测试_保存数据(tmpdir)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "chanlun"
version = "2605.94"
version = "2605.101"
description = "缠论技术分析库 — Rust 高性能实现"
readme = { file = "README.md", content-type = "text/markdown" }
license = { file = "LICENSE", content-type = "text/plain" }
+3 -5
View File
@@ -1214,10 +1214,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]
@@ -1328,7 +1326,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.())?;
+35 -43
View File
@@ -560,30 +560,30 @@ impl 观察者Py {
self.obs_mut().K线((*K.borrow().inner).clone());
}
/// 投喂原始数据 — 便捷入口,直接从 OHLCV 创建 K线 并投喂
fn (
&mut self, : i64, : f64, : f64, : f64, : f64, : f64
) {
self.obs_mut().(, , , , , );
}
/// 加载本地数据 — 从 .nb 文件加载K线数据(先重置,再通过 Python dispatch 逐根投喂,
/// 确保子类重写的 增加原始K线 被正确调用)。
fn (slf: &Bound<'_, Self>, : &str) -> PyResult<()> {
let py = slf.py();
// 重置基础序列
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(())
}
@@ -600,10 +600,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<'_>,
@@ -631,31 +635,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 ----
+6 -10
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)?;
}
@@ -443,10 +441,8 @@ 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]
@@ -496,7 +492,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.())?;
+30 -29
View File
@@ -71,39 +71,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 仍失败");
}
}
+15 -18
View File
@@ -233,12 +233,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),
)
})
}
@@ -271,7 +274,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())
@@ -545,10 +548,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]
@@ -1036,10 +1037,8 @@ impl 线段特征Py {
}
#[getter]
fn 线(&self) -> Py {
Py {
inner: self.inner.线,
}
fn 线(&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.线)
}
#[getter]
@@ -1101,7 +1100,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())
}
@@ -1128,10 +1127,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]
+41 -8
View File
@@ -61,6 +61,40 @@ pub fn 获取分型结构单例(
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
}
// ========== 买卖点类型 ==========
/// 买卖点类型 — 缠论的三类买卖点及扩展类型。
@@ -189,10 +223,8 @@ impl 相对方向Py {
}
/// 返回方向的对立面(向上↔向下, 缺口↔反向缺口, 衔接↔反向衔接)。
fn (&self) -> Self {
Self {
inner: self.inner.(),
}
fn (&self, py: Python<'_>) -> Py<Self> {
(py, self.inner.())
}
/// 判断是否为向上方向(向上/向上缺口/衔接向上)
@@ -245,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::::(, , , ),
)
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "chanlun"
version = "26.5.4"
version = "26.5.5"
edition = "2021"
rust-version = "1.70"
license = "MIT"
+71 -58
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)
@@ -479,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()
@@ -521,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.()
}
}
@@ -546,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() {
@@ -595,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 {
@@ -633,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 指针必须在 分型序列 中
@@ -663,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 指针必须在 笔序列 中
@@ -685,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() {
@@ -716,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 = || {
@@ -760,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 {
@@ -800,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
@@ -831,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 作用域
@@ -860,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线序列 中
+15
View File
@@ -125,6 +125,21 @@ 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线
pub fn K(
: &str,
+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);
}
/// 测试_周期合成 — 多周期合成分析
+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>` 一致。