第十版
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chanlun-py"
|
||||
version = "26.6.42"
|
||||
version = "26.6.44"
|
||||
edition = "2024"
|
||||
description = "缠论技术分析库 — Rust 高性能 Python 绑定"
|
||||
authors = ["YuYuKunKun"]
|
||||
@@ -12,7 +12,7 @@ crate-type = ["cdylib"]
|
||||
name = "chanlun"
|
||||
|
||||
[dependencies]
|
||||
chanlun = { path = "../chanlun" }
|
||||
chanlun = "26.6.2" #{ path = "../chanlun" }
|
||||
lru = "0.18"
|
||||
pyo3 = { version = "0.28", features = ["experimental-inspect"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "chanlun"
|
||||
version = "2606.42"
|
||||
version = "2606.44"
|
||||
description = "缠论技术分析库 — Rust 高性能实现"
|
||||
readme = { file = "README.md", content-type = "text/markdown" }
|
||||
license = { file = "LICENSE", content-type = "text/plain" }
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
use crate::kline_py::chan_kline_to_py;
|
||||
use crate::structure_py::{dashed_to_py, fractal_to_py};
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyType};
|
||||
use pyo3::types::{PyDict, PyList, PyType};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
@@ -308,11 +308,13 @@ impl 笔Py {
|
||||
#[classmethod]
|
||||
#[pyo3(signature = (当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次, 配置))]
|
||||
/// 笔划分核心递归算法
|
||||
/// 分型序列/笔序列 原地修改(与 chan.py 行为一致)
|
||||
/// :return: 递归层次
|
||||
fn 分析(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
当前分型: Option<&Bound<'_, 分型Py>>,
|
||||
分型序列: Vec<Py<分型Py>>,
|
||||
笔序列: Vec<Py<虚线Py>>,
|
||||
分型序列: &Bound<'_, PyList>,
|
||||
笔序列: &Bound<'_, PyList>,
|
||||
缠K序列: Vec<Py<crate::kline_py::缠论K线Py>>,
|
||||
普K序列: Vec<Py<K线Py>>,
|
||||
递归层次: i64,
|
||||
@@ -321,14 +323,19 @@ impl 笔Py {
|
||||
) -> PyResult<i64> {
|
||||
let _ = 递归层次; // Python API 兼容参数,核心从0开始计数
|
||||
let 当前分型_rc = 当前分型.map(|f| Arc::clone(&f.borrow().inner));
|
||||
let mut fr_seq: Vec<Arc<chanlun::structure::fractal_obj::分型>> = 分型序列
|
||||
.iter()
|
||||
.map(|f| Arc::clone(&f.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut bi_seq: Vec<Arc<chanlun::structure::dash_line::虚线>> = 笔序列
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
|
||||
// 从 Python 列表提取
|
||||
let mut fr_seq = Vec::with_capacity(分型序列.len());
|
||||
for item in 分型序列.iter() {
|
||||
let f: PyRef<'_, 分型Py> = item.extract()?;
|
||||
fr_seq.push(Arc::clone(&f.inner));
|
||||
}
|
||||
let mut bi_seq = Vec::with_capacity(笔序列.len());
|
||||
for item in 笔序列.iter() {
|
||||
let d: PyRef<'_, 虚线Py> = item.extract()?;
|
||||
bi_seq.push(Arc::clone(&d.inner));
|
||||
}
|
||||
|
||||
let ck_list: Vec<Arc<chanlun::kline::chan_kline::缠论K线>> = 缠K序列
|
||||
.iter()
|
||||
.map(|k| Arc::clone(&k.bind(py).borrow().inner))
|
||||
@@ -338,8 +345,8 @@ impl 笔Py {
|
||||
.map(|k| k.bind(py).borrow().inner.clone())
|
||||
.collect();
|
||||
let config = 配置.borrow().to_rust_config(py)?;
|
||||
match 当前分型_rc {
|
||||
Some(fr) => Ok(chanlun::algorithm::bi::笔::分析(
|
||||
let depth = match 当前分型_rc {
|
||||
Some(fr) => chanlun::algorithm::bi::笔::分析(
|
||||
fr,
|
||||
&mut fr_seq,
|
||||
&mut bi_seq,
|
||||
@@ -347,9 +354,21 @@ impl 笔Py {
|
||||
&bar_list,
|
||||
递归层次,
|
||||
&config,
|
||||
)),
|
||||
None => Ok(递归层次),
|
||||
),
|
||||
None => 递归层次,
|
||||
};
|
||||
|
||||
// 写回 Python 列表
|
||||
分型序列.call_method0("clear")?;
|
||||
for f in fr_seq {
|
||||
分型序列.call_method1("append", (fractal_to_py(py, f),))?;
|
||||
}
|
||||
笔序列.call_method0("clear")?;
|
||||
for d in bi_seq {
|
||||
笔序列.call_method1("append", (dashed_to_py(py, d),))?;
|
||||
}
|
||||
|
||||
Ok(depth)
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
@@ -502,11 +521,13 @@ impl 线段Py {
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
#[pyo3(signature = (笔序列, 线段序列, 配置, 层级 = 0, 关系序列 = None))]
|
||||
/// 线段划分核心递归算法
|
||||
/// 线段序列 原地修改(与 chan.py 行为一致)
|
||||
fn 分析(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
笔序列: Vec<Py<虚线Py>>,
|
||||
线段序列: Vec<Py<虚线Py>>,
|
||||
线段序列: &Bound<'_, PyList>,
|
||||
配置: &Bound<'_, 缠论配置Py>,
|
||||
层级: i64,
|
||||
关系序列: Option<Vec<相对方向Py>>,
|
||||
@@ -516,10 +537,13 @@ impl 线段Py {
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut seg_seq: Vec<Arc<chanlun::structure::dash_line::虚线>> = 线段序列
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
|
||||
let mut seg_seq = Vec::with_capacity(线段序列.len());
|
||||
for item in 线段序列.iter() {
|
||||
let d: PyRef<'_, 虚线Py> = item.extract()?;
|
||||
seg_seq.push(Arc::clone(&d.inner));
|
||||
}
|
||||
|
||||
let config = 配置.borrow().to_rust_config(py)?;
|
||||
let default_rel = vec![
|
||||
chanlun::types::相对方向::向上,
|
||||
@@ -535,15 +559,22 @@ impl 线段Py {
|
||||
层级,
|
||||
&rel_list,
|
||||
);
|
||||
|
||||
// 写回 Python 列表
|
||||
线段序列.call_method0("clear")?;
|
||||
for d in seg_seq {
|
||||
线段序列.call_method1("append", (dashed_to_py(py, d),))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
/// 即同级别分析
|
||||
/// 线段序列 原地修改(与 chan.py 行为一致)
|
||||
fn 扩展分析(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
虚线序列: Vec<Py<虚线Py>>,
|
||||
线段序列: Vec<Py<虚线Py>>,
|
||||
线段序列: &Bound<'_, PyList>,
|
||||
配置: &Bound<'_, 缠论配置Py>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<()> {
|
||||
@@ -551,12 +582,21 @@ impl 线段Py {
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut seg_seq: Vec<Arc<chanlun::structure::dash_line::虚线>> = 线段序列
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
|
||||
let mut seg_seq = Vec::with_capacity(线段序列.len());
|
||||
for item in 线段序列.iter() {
|
||||
let d: PyRef<'_, 虚线Py> = item.extract()?;
|
||||
seg_seq.push(Arc::clone(&d.inner));
|
||||
}
|
||||
|
||||
let config = 配置.borrow().to_rust_config(py)?;
|
||||
chanlun::algorithm::segment::线段::扩展分析(&dash_list, &mut seg_seq, &config);
|
||||
|
||||
// 写回 Python 列表
|
||||
线段序列.call_method0("clear")?;
|
||||
for d in seg_seq {
|
||||
线段序列.call_method1("append", (dashed_to_py(py, d),))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -792,18 +832,26 @@ impl 中枢Py {
|
||||
}
|
||||
|
||||
/// 当基础序列>=9时,从中枢中提取扩展线段中枢
|
||||
/// 扩展中枢 原地修改(与 chan.py 行为一致)
|
||||
fn 获取扩展中枢(
|
||||
&self,
|
||||
扩展中枢: Vec<Py<Self>>,
|
||||
扩展中枢: &Bound<'_, PyList>,
|
||||
配置: &Bound<'_, crate::config_py::缠论配置Py>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<()> {
|
||||
let mut hub_seq: Vec<Arc<chanlun::algorithm::hub::中枢>> = 扩展中枢
|
||||
.iter()
|
||||
.map(|h| Arc::clone(&h.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut hub_seq = Vec::with_capacity(扩展中枢.len());
|
||||
for item in 扩展中枢.iter() {
|
||||
let h: PyRef<'_, 中枢Py> = item.extract()?;
|
||||
hub_seq.push(Arc::clone(&h.inner));
|
||||
}
|
||||
let config = 配置.borrow().to_rust_config(配置.py())?;
|
||||
self.inner.获取扩展中枢(&mut hub_seq, &config);
|
||||
|
||||
// 写回 Python 列表
|
||||
扩展中枢.call_method0("clear")?;
|
||||
for h in hub_seq {
|
||||
扩展中枢.call_method1("append", (hub_to_py(py, h),))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -889,10 +937,11 @@ impl 中枢Py {
|
||||
#[classmethod]
|
||||
#[pyo3(signature = (虚线序列, 中枢序列, 跳过首部 = true, 标识 = "", 层级 = 0))]
|
||||
/// 中枢识别核心递归算法
|
||||
/// 中枢序列 原地修改(与 chan.py 行为一致)
|
||||
fn 分析(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
虚线序列: Vec<Py<虚线Py>>,
|
||||
中枢序列: Vec<Py<Self>>,
|
||||
中枢序列: &Bound<'_, PyList>,
|
||||
跳过首部: bool,
|
||||
标识: &str,
|
||||
层级: i64,
|
||||
@@ -902,11 +951,20 @@ impl 中枢Py {
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut hub_seq: Vec<Arc<chanlun::algorithm::hub::中枢>> = 中枢序列
|
||||
.iter()
|
||||
.map(|h| Arc::clone(&h.bind(py).borrow().inner))
|
||||
.collect();
|
||||
|
||||
let mut hub_seq = Vec::with_capacity(中枢序列.len());
|
||||
for item in 中枢序列.iter() {
|
||||
let h: PyRef<'_, 中枢Py> = item.extract()?;
|
||||
hub_seq.push(Arc::clone(&h.inner));
|
||||
}
|
||||
|
||||
chanlun::algorithm::hub::中枢::分析(&rc_list, &mut hub_seq, 跳过首部, 标识, 层级);
|
||||
|
||||
// 写回 Python 列表
|
||||
中枢序列.call_method0("clear")?;
|
||||
for h in hub_seq {
|
||||
中枢序列.call_method1("append", (hub_to_py(py, h),))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+26
-11
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyBytes, PyDict, PyType};
|
||||
use pyo3::types::{PyBytes, PyDict, PyList, PyType};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
@@ -663,25 +663,30 @@ impl 缠论K线Py {
|
||||
|
||||
#[classmethod]
|
||||
/// 分析K线,执行指标计算+包含处理+分型判定
|
||||
/// 缠K序列/普K序列 原地修改(与 chan.py 行为一致)
|
||||
/// :return: (状态, 分型|None)
|
||||
fn 分析(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
当前K线: &Bound<'_, K线Py>,
|
||||
缠K序列: Vec<Py<Self>>,
|
||||
普K序列: Vec<Py<K线Py>>,
|
||||
缠K序列: &Bound<'_, PyList>,
|
||||
普K序列: &Bound<'_, PyList>,
|
||||
配置: &Bound<'_, 缠论配置Py>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<(String, Option<Py<PyAny>>)> {
|
||||
let ck_inner = (*当前K线.borrow().inner).clone();
|
||||
let config = 配置.borrow().to_rust_config(py)?;
|
||||
|
||||
let mut ck_seq: Vec<_> = 缠K序列
|
||||
.iter()
|
||||
.map(|k| std::sync::Arc::clone(&k.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut bar_seq: Vec<_> = 普K序列
|
||||
.iter()
|
||||
.map(|k| k.bind(py).borrow().inner.clone())
|
||||
.collect();
|
||||
// 从 Python 列表提取
|
||||
let mut ck_seq = Vec::with_capacity(缠K序列.len());
|
||||
for item in 缠K序列.iter() {
|
||||
let ck: PyRef<'_, Self> = item.extract()?;
|
||||
ck_seq.push(std::sync::Arc::clone(&ck.inner));
|
||||
}
|
||||
let mut bar_seq = Vec::with_capacity(普K序列.len());
|
||||
for item in 普K序列.iter() {
|
||||
let bar: PyRef<'_, K线Py> = item.extract()?;
|
||||
bar_seq.push(bar.inner.clone());
|
||||
}
|
||||
|
||||
let (status, fractal) = chanlun::kline::chan_kline::缠论K线::分析(
|
||||
ck_inner,
|
||||
@@ -690,6 +695,16 @@ impl 缠论K线Py {
|
||||
&config,
|
||||
);
|
||||
|
||||
// 写回 Python 列表(clear + extend)
|
||||
缠K序列.call_method0("clear")?;
|
||||
for k in ck_seq {
|
||||
缠K序列.call_method1("append", (chan_kline_to_py(py, k),))?;
|
||||
}
|
||||
普K序列.call_method0("clear")?;
|
||||
for k in bar_seq {
|
||||
普K序列.call_method1("append", (bar_to_py(py, k),))?;
|
||||
}
|
||||
|
||||
Ok((status, fractal.map(|f| fractal_to_py(py, f).into_any())))
|
||||
}
|
||||
|
||||
|
||||
@@ -508,13 +508,10 @@ class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase):
|
||||
return obs
|
||||
|
||||
# 4. 读取数据文件(classmethod 重写
|
||||
a = datetime.now()
|
||||
obs = Sub.读取数据文件(NB_PATH)
|
||||
self.assertIsInstance(obs, Sub)
|
||||
self.assertTrue(obs._custom_classmethod_flag)
|
||||
self.assertGreater(len(obs.普通K线序列), 0)
|
||||
b = datetime.now()
|
||||
print("读取数据文件 用时:", b - a)
|
||||
|
||||
# 1. 加载本地数据
|
||||
obs.重置基础序列()
|
||||
@@ -522,8 +519,6 @@ class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase):
|
||||
self.assertTrue(obs._loaded)
|
||||
self.assertEqual(obs._load_count, 2)
|
||||
self.assertGreater(len(obs.普通K线序列), 0)
|
||||
c = datetime.now()
|
||||
print("加载本地数据 用时:", c - b)
|
||||
|
||||
# 2. 静态重新分析(复用已加载数据的 obs)
|
||||
obs.重置基础序列()
|
||||
@@ -531,8 +526,6 @@ class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase):
|
||||
obs.静态重新分析()
|
||||
self.assertEqual(obs._reanalyzed, 1)
|
||||
self.assertGreaterEqual(len(obs.笔序列), 0)
|
||||
d = datetime.now()
|
||||
print("静态重新分析 用时:", d - c)
|
||||
|
||||
# 3. 保存数据
|
||||
obs.重置基础序列()
|
||||
@@ -544,8 +537,6 @@ class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase):
|
||||
|
||||
# 5. 重置次数
|
||||
self.assertEqual(obs._reload, 6)
|
||||
e = datetime.now()
|
||||
print("保存数据 用时:", e - d)
|
||||
|
||||
def test_override_completely_no_super(self):
|
||||
k = self.make_data_item()
|
||||
@@ -1954,5 +1945,211 @@ class TestApi一致性(ApiConsistencyMixin, unittest.TestCase):
|
||||
# ============================================================
|
||||
|
||||
|
||||
class Test导出函数双端等效(unittest.TestCase):
|
||||
"""验证: 序列修改类导出函数与 chan.py 行为一致 (就地修改 + 返回值等效)"""
|
||||
|
||||
_TEST_COUNT = 300 # 投喂 K 线数
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not _has_nb():
|
||||
raise unittest.SkipTest("需要 .nb 数据文件")
|
||||
cls.bars = read_nb_bars(NB_PATH)
|
||||
|
||||
# ---- 辅助 ----
|
||||
|
||||
def _make_observers(self):
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
cfg_rs = chanlun.缠论配置()
|
||||
cfg_rs.计算指标 = True
|
||||
cfg_py = chan.缠论配置()
|
||||
cfg_py.计算指标 = True
|
||||
|
||||
obs_rs = chanlun.观察者("btcusd", 300, cfg_rs)
|
||||
obs_py = chan.观察者("btcusd", 300, cfg_py)
|
||||
|
||||
for i, (ts, o, h, l, c, v) in enumerate(self.bars[: self._TEST_COUNT]):
|
||||
obs_rs.投喂原始数据(ts, o, h, l, c, v)
|
||||
obs_py.投喂原始数据(ts, o, h, l, c, v)
|
||||
|
||||
return obs_rs, obs_py
|
||||
|
||||
# ================================================================
|
||||
# 缠论K线.分析
|
||||
# ================================================================
|
||||
|
||||
def test_缠论K线分析_等效(self):
|
||||
"""缠论K线.分析 双端行为一致 (返回值 + 序列长度)."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._make_observers()
|
||||
|
||||
ck_rs: list = []
|
||||
bar_rs: list = []
|
||||
ck_py: list = []
|
||||
bar_py: list = []
|
||||
mismatches = []
|
||||
|
||||
for k_rs, k_py in zip(obs_rs.普通K线序列[100:200], obs_py.普通K线序列[100:200]):
|
||||
st_rs, fx_rs = chanlun.缠论K线.分析(k_rs, ck_rs, bar_rs, obs_rs.配置)
|
||||
st_py, fx_py = chan.缠论K线.分析(k_py, ck_py, bar_py, obs_py.配置)
|
||||
|
||||
if st_rs != st_py:
|
||||
mismatches.append(f"状态: R={st_rs} P={st_py}")
|
||||
if (fx_rs is None) != (fx_py is None):
|
||||
mismatches.append(f"分型None: R={fx_rs is None} P={fx_py is None}")
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"缠论K线.分析 不一致 ({len(mismatches)}):\n" + "\n".join(mismatches[:5]))
|
||||
self.assertEqual(len(ck_rs), len(ck_py), f"缠K序列长度: R={len(ck_rs)} P={len(ck_py)}")
|
||||
self.assertEqual(len(bar_rs), len(bar_py), f"普K序列长度: R={len(bar_rs)} P={len(bar_py)}")
|
||||
|
||||
# ================================================================
|
||||
# 笔.分析
|
||||
# ================================================================
|
||||
|
||||
def test_笔分析_等效(self):
|
||||
"""笔.分析 双端行为一致 (返回值 + 序列修改)."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._make_observers()
|
||||
|
||||
ck_rs = list(obs_rs.缠论K线序列)
|
||||
ck_py = list(obs_py.缠论K线序列)
|
||||
bar_rs = list(obs_rs.普通K线序列)
|
||||
bar_py = list(obs_py.普通K线序列)
|
||||
|
||||
fr_rs: list = []
|
||||
bi_rs: list = []
|
||||
fr_py: list = []
|
||||
bi_py: list = []
|
||||
mismatches = []
|
||||
|
||||
for fx_rs, fx_py in zip(obs_rs.分型序列[2:], obs_py.分型序列[2:]):
|
||||
d_rs = chanlun.笔.分析(fx_rs, fr_rs, bi_rs, ck_rs, bar_rs, 0, obs_rs.配置)
|
||||
d_py = chan.笔.分析(fx_py, fr_py, bi_py, ck_py, bar_py, 0, obs_py.配置)
|
||||
|
||||
if d_rs != d_py:
|
||||
mismatches.append(f"递归层次: R={d_rs} P={d_py}")
|
||||
|
||||
self.assertEqual(len(mismatches), 0, f"笔.分析 不一致 ({len(mismatches)}):\n" + "\n".join(mismatches[:3]))
|
||||
self.assertEqual(len(fr_rs), len(fr_py), f"分型序列长度: R={len(fr_rs)} P={len(fr_py)}")
|
||||
self.assertGreater(len(bi_rs), 0, "笔序列为空 (Rust)")
|
||||
self.assertEqual(len(bi_rs), len(bi_py), f"笔序列长度: R={len(bi_rs)} P={len(bi_py)}")
|
||||
|
||||
# ================================================================
|
||||
# 线段.分析
|
||||
# ================================================================
|
||||
|
||||
def test_线段分析_等效(self):
|
||||
"""线段.分析 双端行为一致 (序列修改)."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._make_observers()
|
||||
|
||||
seg_rs = list(obs_rs.线段序列)
|
||||
seg_py = list(obs_py.线段序列)
|
||||
|
||||
# 先用 chanlun.线段.分析 重新分析
|
||||
bi_list_rs = list(obs_rs.笔序列)
|
||||
bi_list_py = list(obs_py.笔序列)
|
||||
|
||||
# 重置线段序列
|
||||
seg_rs.clear()
|
||||
seg_py.clear()
|
||||
|
||||
chanlun.线段.分析(bi_list_rs, seg_rs, obs_rs.配置)
|
||||
chan.线段.分析(bi_list_py, seg_py, obs_py.配置)
|
||||
|
||||
self.assertGreater(len(seg_rs), 0, "线段序列为空 (Rust)")
|
||||
self.assertEqual(len(seg_rs), len(seg_py), f"线段序列长度: R={len(seg_rs)} P={len(seg_py)}")
|
||||
|
||||
# ================================================================
|
||||
# 线段.扩展分析
|
||||
# ================================================================
|
||||
|
||||
def test_线段扩展分析_等效(self):
|
||||
"""线段.扩展分析 双端行为一致."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._make_observers()
|
||||
|
||||
ext_rs = list(obs_rs.扩展线段序列)
|
||||
ext_py = list(obs_py.扩展线段序列)
|
||||
|
||||
ext_rs.clear()
|
||||
ext_py.clear()
|
||||
|
||||
bi_list_rs = list(obs_rs.笔序列)
|
||||
bi_list_py = list(obs_py.笔序列)
|
||||
|
||||
chanlun.线段.扩展分析(bi_list_rs, ext_rs, obs_rs.配置)
|
||||
chan.线段.扩展分析(bi_list_py, ext_py, obs_py.配置)
|
||||
|
||||
self.assertGreater(len(ext_rs), 0, "扩展线段序列为空 (Rust)")
|
||||
self.assertEqual(len(ext_rs), len(ext_py), f"扩展线段: R={len(ext_rs)} P={len(ext_py)}")
|
||||
|
||||
# ================================================================
|
||||
# 中枢.分析
|
||||
# ================================================================
|
||||
|
||||
def test_中枢分析_等效(self):
|
||||
"""中枢.分析 双端行为一致."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._make_observers()
|
||||
|
||||
hub_rs: list = []
|
||||
hub_py: list = []
|
||||
bi_list_rs = list(obs_rs.笔序列)
|
||||
bi_list_py = list(obs_py.笔序列)
|
||||
|
||||
chanlun.中枢.分析(bi_list_rs, hub_rs, True, "", 0)
|
||||
chan.中枢.分析(bi_list_py, hub_py, True, "", 0)
|
||||
|
||||
self.assertGreater(len(hub_rs), 0, "笔中枢序列为空 (Rust)")
|
||||
self.assertEqual(len(hub_rs), len(hub_py), f"笔中枢: R={len(hub_rs)} P={len(hub_py)}")
|
||||
|
||||
# ================================================================
|
||||
# 中枢.获取扩展中枢
|
||||
# ================================================================
|
||||
|
||||
def test_获取扩展中枢_等效(self):
|
||||
"""中枢.获取扩展中枢 双端行为一致."""
|
||||
import chanlun
|
||||
from chanlun import chan
|
||||
|
||||
obs_rs, obs_py = self._make_observers()
|
||||
|
||||
ext_hub_rs = list(obs_rs.扩展中枢序列)
|
||||
ext_hub_py = list(obs_py.扩展中枢序列)
|
||||
|
||||
ext_hub_rs.clear()
|
||||
ext_hub_py.clear()
|
||||
|
||||
# init with 笔中枢
|
||||
bi_list_rs = list(obs_rs.笔序列)
|
||||
bi_list_py = list(obs_py.笔序列)
|
||||
chanlun.中枢.分析(bi_list_rs, ext_hub_rs, True, "", 0)
|
||||
chan.中枢.分析(bi_list_py, ext_hub_py, True, "", 0)
|
||||
|
||||
# 若基础序列≥9,调用获取扩展中枢
|
||||
for hub_rs, hub_py in zip(ext_hub_rs, ext_hub_py):
|
||||
sub_rs: list = []
|
||||
sub_py: list = []
|
||||
hub_rs.获取扩展中枢(sub_rs, obs_rs.配置)
|
||||
hub_py.获取扩展中枢(sub_py, obs_py.配置)
|
||||
if len(sub_rs) != len(sub_py):
|
||||
self.fail(f"获取扩展中枢 长度不一致: R={len(sub_rs)} P={len(sub_py)}")
|
||||
|
||||
# all passed (or vacuously true if no hubs with >=9 segments)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chanlun"
|
||||
version = "26.6.1"
|
||||
version = "26.6.2"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "基于缠论(缠中说禅)理论的量化技术分析核心库,支持流式数据处理和多周期联立分析。"
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""检测 Rust 源码文件头部是否有 MIT 协议,若无则自动注入。
|
||||
|
||||
用法:
|
||||
python3 check_license.py # 检测 chanlun/ 和 chanlun-py/ 下所有 .rs
|
||||
python3 check_license.py --check-only # 仅检测,不修改
|
||||
python3 check_license.py --fix # 检测并修复
|
||||
python3 check_license.py path/to/dir # 指定目录
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_repo_root() -> Path:
|
||||
"""从脚本位置向上查找仓库根目录(含 LICENSE 文件的目录)。"""
|
||||
current = Path(__file__).resolve().parent
|
||||
while current != current.parent:
|
||||
if (current / "LICENSE").exists():
|
||||
return current
|
||||
current = current.parent
|
||||
# Fallback: 脚本所在目录的父目录
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def build_license_header(license_path: Path) -> str:
|
||||
"""读取 LICENSE 文件并格式化为 Rust 块注释头。"""
|
||||
lines = license_path.read_text(encoding="utf-8").rstrip("\n").split("\n")
|
||||
header_lines = ["/*"]
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
header_lines.append(f" * {line}")
|
||||
else:
|
||||
header_lines.append(" *")
|
||||
header_lines.append(" */")
|
||||
header_lines.append("") # 末尾空行分隔
|
||||
return "\n".join(header_lines) + "\n"
|
||||
|
||||
|
||||
def has_license_header(file_path: Path) -> bool:
|
||||
"""检测文件头部是否已包含块注释风格的 MIT License。"""
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
head = f.read(512)
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return True
|
||||
return head.lstrip().startswith("/*") and "MIT License" in head
|
||||
|
||||
|
||||
def strip_old_license_header(text: str) -> str:
|
||||
"""去除文件中已有的 // 风格 license header(重新注入前调用)。"""
|
||||
stripped = text.lstrip("\n")
|
||||
if stripped.startswith("// MIT License"):
|
||||
# 找到 // 注释块结束位置(第一个非 // 非空行)
|
||||
lines = stripped.split("\n")
|
||||
end_idx = 0
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("//") or line.strip() == "":
|
||||
end_idx = i + 1
|
||||
else:
|
||||
break
|
||||
return "\n".join(lines[end_idx:])
|
||||
return text
|
||||
|
||||
|
||||
def inject_license(file_path: Path, header: str) -> bool:
|
||||
"""将 license header 注入文件头部。返回 True 表示已修改。"""
|
||||
original = file_path.read_text(encoding="utf-8")
|
||||
# 已有块注释风格则跳过
|
||||
if original.lstrip().startswith("/*") and "MIT License" in original[:512]:
|
||||
return False
|
||||
# 去除旧的 // 风格 header(如果存在)
|
||||
cleaned = strip_old_license_header(original)
|
||||
file_path.write_text(header + cleaned, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def collect_rs_files(roots: list[Path]) -> list[Path]:
|
||||
"""递归收集所有 .rs 文件,排除 target/ 等构建产物目录。"""
|
||||
exclude_dirs = {"target", ".git", "__pycache__", "dist", "build", ".venv", "venv"}
|
||||
files = []
|
||||
for root in roots:
|
||||
if not root.is_dir():
|
||||
continue
|
||||
for path in root.rglob("*.rs"):
|
||||
if any(excl in path.parts for excl in exclude_dirs):
|
||||
continue
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="检测 Rust 源码 MIT 协议头")
|
||||
parser.add_argument(
|
||||
"paths",
|
||||
nargs="*",
|
||||
help="要检测的目录(默认: chanlun 和 chanlun-py 源码目录)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-only",
|
||||
action="store_true",
|
||||
help="仅检测,不修改文件",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fix",
|
||||
action="store_true",
|
||||
help="检测并自动注入缺失的协议头(默认行为)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = find_repo_root()
|
||||
license_path = repo_root / "LICENSE"
|
||||
|
||||
if not license_path.exists():
|
||||
print(f"错误: 未找到 LICENSE 文件 ({license_path})", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
header = build_license_header(license_path)
|
||||
|
||||
# 确定扫描目录
|
||||
if args.paths:
|
||||
roots = [Path(p).resolve() for p in args.paths]
|
||||
else:
|
||||
roots = [
|
||||
repo_root / "chanlun" / "src",
|
||||
repo_root / "chanlun-py" / "src",
|
||||
repo_root / "chanlun" / "tests",
|
||||
repo_root / "chanlun-py" / "tests",
|
||||
]
|
||||
roots = [r for r in roots if r.is_dir()]
|
||||
|
||||
if not roots:
|
||||
print("错误: 未找到任何源码目录", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
rs_files = collect_rs_files(roots)
|
||||
|
||||
if not rs_files:
|
||||
print("未找到 .rs 文件")
|
||||
return 0
|
||||
|
||||
missing = []
|
||||
injected = []
|
||||
|
||||
for f in rs_files:
|
||||
if has_license_header(f):
|
||||
continue
|
||||
missing.append(f)
|
||||
if not args.check_only:
|
||||
if inject_license(f, header):
|
||||
injected.append(f)
|
||||
|
||||
if args.check_only:
|
||||
if missing:
|
||||
print(f"缺失 MIT 协议头: {len(missing)} 个文件")
|
||||
for f in missing:
|
||||
print(f" {f}")
|
||||
return 1
|
||||
else:
|
||||
print(f"全部 {len(rs_files)} 个 .rs 文件均已包含 MIT 协议头")
|
||||
return 0
|
||||
else:
|
||||
if injected:
|
||||
print(f"已注入 MIT 协议头: {len(injected)} 个文件")
|
||||
for f in injected:
|
||||
print(f" {f}")
|
||||
if missing:
|
||||
already = len(missing) - len(injected)
|
||||
if already > 0:
|
||||
print(f"已有协议头: {already} 个文件(无需修改)")
|
||||
total = len(rs_files) - len(missing)
|
||||
print(f"总计: {len(rs_files)} 个 .rs 文件, {total} 个已含协议头")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+52
-19
@@ -848,6 +848,7 @@ print("=== 趋势分析 ===")
|
||||
this.websocketManager = websocketManager;
|
||||
this.widget = null;
|
||||
this.shapeIds = new Map();
|
||||
this._pendingShapeCreate = new Map(); // id → Promise, 追踪正在创建的图形
|
||||
this.onRealtimeCallback = null;
|
||||
this.initDataFeed();
|
||||
this.websocketManager.on('chart', (message) => {
|
||||
@@ -1052,7 +1053,7 @@ print("=== 趋势分析 ===")
|
||||
}
|
||||
}
|
||||
|
||||
handleChartMessage(message) {
|
||||
async handleChartMessage(message) {
|
||||
Utils.log("处理图表消息:", message.type);
|
||||
switch (message.type) {
|
||||
case 'realtime':
|
||||
@@ -1070,11 +1071,11 @@ print("=== 趋势分析 ===")
|
||||
break;
|
||||
case 'shape':
|
||||
if (message.cmd === "APPEND") {
|
||||
this.addShape(message.id, message.points, message.name, message.overrides);
|
||||
await this.addShape(message.id, message.points, message.name, message.overrides);
|
||||
} else if (message.cmd === "REMOVE") {
|
||||
this.removeShape(message.id);
|
||||
await this.removeShape(message.id);
|
||||
} else if (message.cmd === "MODIFY") {
|
||||
this.updateShape(message.id, message.points, message.overrides);
|
||||
await this.updateShape(message.id, message.points, message.overrides);
|
||||
}
|
||||
break;
|
||||
case 'query_result':
|
||||
@@ -1104,6 +1105,12 @@ print("=== 趋势分析 ===")
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待之前相同 ID 的创建完成(避免竞态)
|
||||
const prevPending = this._pendingShapeCreate.get(id);
|
||||
if (prevPending) {
|
||||
await prevPending;
|
||||
}
|
||||
|
||||
// 重复判断
|
||||
const existShapeId = this.shapeIds.get(id);
|
||||
if (existShapeId) {
|
||||
@@ -1112,26 +1119,40 @@ print("=== 趋势分析 ===")
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建图形
|
||||
const shapeId = await this.widget.chart().createMultipointShape(points, options);
|
||||
const shape = this.widget.chart().getShapeById(shapeId);
|
||||
// 记录 pending 状态,供 removeShape/updateShape 等待
|
||||
const createPromise = (async () => {
|
||||
// 创建图形
|
||||
const shapeId = await this.widget.chart().createMultipointShape(points, options);
|
||||
const shape = this.widget.chart().getShapeById(shapeId);
|
||||
|
||||
if (!shape) {
|
||||
Utils.error("添加形状失败:shape 为 null", {id, shapeId});
|
||||
return;
|
||||
if (!shape) {
|
||||
Utils.error("添加形状失败:shape 为 null", {id, shapeId});
|
||||
return null;
|
||||
}
|
||||
|
||||
// 设置属性
|
||||
//Utils.log("shapeId:", shape.getProperties());
|
||||
//shape.setProperties(properties);
|
||||
shape.bringToFront();
|
||||
|
||||
// 保存映射
|
||||
this.shapeIds.set(id, shapeId);
|
||||
Utils.log("添加形状", {id, shapeId});
|
||||
|
||||
return shapeId;
|
||||
})();
|
||||
|
||||
this._pendingShapeCreate.set(id, createPromise);
|
||||
|
||||
const shapeId = await createPromise;
|
||||
if (shapeId === null) {
|
||||
// shape creation failed
|
||||
}
|
||||
|
||||
// 设置属性
|
||||
//Utils.log("shapeId:", shape.getProperties());
|
||||
//shape.setProperties(properties);
|
||||
shape.bringToFront();
|
||||
|
||||
// 保存映射
|
||||
this.shapeIds.set(id, shapeId);
|
||||
Utils.log("添加形状", {id, shapeId});
|
||||
|
||||
} catch (error) {
|
||||
Utils.error("添加形状失败", error);
|
||||
} finally {
|
||||
this._pendingShapeCreate.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1139,6 +1160,12 @@ print("=== 趋势分析 ===")
|
||||
try {
|
||||
if (!this.widget || !this.widget.chart) return;
|
||||
|
||||
// 等待该 ID 的 addShape 完成(避免竞态)
|
||||
const pending = this._pendingShapeCreate.get(id);
|
||||
if (pending) {
|
||||
await pending;
|
||||
}
|
||||
|
||||
const shapeId = this.shapeIds.get(id);
|
||||
if (!shapeId) {
|
||||
Utils.log("图形不存在,无需删除", {id});
|
||||
@@ -1162,6 +1189,12 @@ print("=== 趋势分析 ===")
|
||||
try {
|
||||
if (!this.widget || !this.widget.chart) return;
|
||||
|
||||
// 等待该 ID 的 addShape 完成(避免竞态)
|
||||
const pending = this._pendingShapeCreate.get(id);
|
||||
if (pending) {
|
||||
await pending;
|
||||
}
|
||||
|
||||
const shapeId = this.shapeIds.get(id);
|
||||
if (!shapeId) {
|
||||
Utils.error("更新失败:图形不存在", {id});
|
||||
|
||||
Reference in New Issue
Block a user