refactor: 缠论引擎包化与 Web 分层(ECR-001)

将根目录引擎迁入 chanlun/ 并保留兼容 shim;拆分 TF_DF 与 web 服务;
前端模块化;strategies 改用 chanlun 导入;补充 ESS 文档与 golden 回归。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-05 18:48:20 +08:00
co-authored by Cursor
parent e2e45bc1bc
commit 74dec4e50b
160 changed files with 25576 additions and 23104 deletions
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""将 strategies / web / tests 的根 shim 导入改为 chanlun 包导入。"""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
REPLACEMENTS = [
(r"from ChanLun import ", "from chanlun import "),
(r"from TF_DF import ", "from chanlun import "), # TF_DF also exported from chanlun
(r"from ChanEnum import ", "from chanlun.core.ChanEnum import "),
(r"from ChanLun_Classifier import ", "from chanlun.analysis.ChanLun_Classifier import "),
(r"from ChanPY import ", "from chanlun.analysis.ChanPY import "),
(r"from ChanKLU import ", "from chanlun.core.ChanKLU import "),
(r"from ChanKLC import ", "from chanlun.core.ChanKLC import "),
(r"from ChanBI import ", "from chanlun.core.ChanBI import "),
(r"from ChanCTime import ", "from chanlun.core.ChanCTime import "),
(r"from ChanMACD import ", "from chanlun.indicators.ChanMACD import "),
(r"from ChanZone import ", "from chanlun.analysis.ChanZone import "),
(r"from ChanBSP import ", "from chanlun.core.ChanBSP import "),
(r"from ChanSEG import ", "from chanlun.core.ChanSEG import "),
(r"from ChanZS import ", "from chanlun.core.ChanZS import "),
(r"from ChanBIZS import ", "from chanlun.core.ChanBIZS import "),
(r"from ChanSBI import ", "from chanlun.core.ChanSBI import "),
(r"from ChanPivotClassifier import ", "from chanlun.analysis.ChanPivotClassifier import "),
(r"from ChanPivotMonitor import ", "from chanlun.analysis.ChanPivotMonitor import "),
(r"from fx_strength_config import ", "from chanlun.analysis.fx_strength_config import "),
]
def rewrite_file(path: Path) -> bool:
text = path.read_text(encoding="utf-8")
orig = text
for pat, repl in REPLACEMENTS:
text = re.sub(pat, repl, text)
# Fix: from chanlun import TF_DF only cases that were `from TF_DF import TF_DF`
# and `from ChanLun import ChanLun, TF_DF` already becomes from chanlun import ChanLun, TF_DF — good
# Special: from chanlun import TF_DF when file only imported TF_DF — need TF_DF in chanlun.__init__
if text == orig:
return False
path.write_text(text, encoding="utf-8")
return True
def main():
changed = []
targets = []
targets += list((ROOT / "strategies").glob("*.py"))
targets += list((ROOT / "web").rglob("*.py"))
targets += list((ROOT / "tests").rglob("*.py"))
for p in targets:
if "charting_library" in str(p) or "__pycache__" in str(p):
continue
if rewrite_file(p):
changed.append(str(p.relative_to(ROOT)))
print(f"updated {len(changed)} files")
for c in changed:
print(" -", c)
if __name__ == "__main__":
main()
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""一次性迁移:根目录引擎模块 → chanlun/ 包 + 根 shim。"""
from __future__ import annotations
import re
import shutil
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CORE = [
"ChanEnum.py",
"ChanCTime.py",
"ChanKLU.py",
"ChanKLC.py",
"ChanBI.py",
"ChanSBI.py",
"ChanSEG.py",
"ChanZS.py",
"ChanBIZS.py",
"ChanBSP.py",
"Chan_FX_Box.py",
]
INDICATORS = [
"ChanMACD.py",
"ChanMACDHistSet.py",
"ChanMACDSeg.py",
"ChanMACDUnitTF.py",
]
ANALYSIS = [
"ChanZone.py",
"ChanLun_Classifier.py",
"ChanPivotClassifier.py",
"ChanPivotMonitor.py",
"ChanHeng.py",
"ChanPY.py",
"Find_Trend.py",
"fx_strength_config.py",
]
PIPELINE = {
"ChanLun.py": "orchestrator.py",
"TF_DF.py": "timeframe.py",
}
MODULE_PKG: dict[str, str] = {}
for name in CORE:
MODULE_PKG[name[:-3]] = "chanlun.core"
for name in INDICATORS:
MODULE_PKG[name[:-3]] = "chanlun.indicators"
for name in ANALYSIS:
MODULE_PKG[name[:-3]] = "chanlun.analysis"
IMPORT_TARGET = {
**{k: f"{v}.{k}" for k, v in MODULE_PKG.items()},
"ChanLun": "chanlun.pipeline.orchestrator",
"TF_DF": "chanlun.pipeline.timeframe",
}
KNOWN = set(IMPORT_TARGET)
def rewrite_imports(text: str) -> str:
"""只改写已知缠论模块;绝不拆分 typing/talib 等标准 from-import。"""
out_lines = []
for line in text.splitlines(keepends=True):
nl = "\n" if line.endswith("\n") else ""
raw = line[:-1] if nl else line
indent_m = re.match(r"^(\s*)", raw)
indent = indent_m.group(1) if indent_m else ""
stripped = raw[len(indent) :]
if stripped.startswith("from ") and " import " in stripped:
m = re.match(r"^from (\S+) import (.*)$", stripped)
if m and m.group(1) in KNOWN:
raw = f"{indent}from {IMPORT_TARGET[m.group(1)]} import {m.group(2)}"
out_lines.append(raw + nl)
continue
if stripped.startswith("import "):
rest = stripped[len("import ") :]
# 跳过 import x as y 复合以外的非已知模块整行
parts = [p.strip() for p in rest.split(",")]
if not any(p.split(" as ")[0].strip() in KNOWN for p in parts):
out_lines.append(line)
continue
new_parts = []
for p in parts:
base = p.split(" as ")[0].strip()
if base not in KNOWN:
new_parts.append(f"import {p}")
continue
target = IMPORT_TARGET[base]
if " as " in p:
new_parts.append(
f"import {target} as {p.split(' as ', 1)[1].strip()}"
)
elif base in ("ChanLun", "TF_DF"):
new_parts.append(f"from {target} import {base}")
else:
new_parts.append(f"import {target} as {base}")
# 多模块拆成多行,保持可读
out_lines.append(nl.join(indent + x for x in new_parts) + nl)
continue
out_lines.append(line)
return "".join(out_lines)
def write_shim(mod_name: str):
path = ROOT / f"{mod_name}.py"
if mod_name == "ChanLun":
body = (
'"""兼容 shim — 请优先 from chanlun import ..."""\n'
"from chanlun.pipeline.orchestrator import ChanLun # noqa: F401\n"
"from chanlun.pipeline.timeframe import TF_DF # noqa: F401\n"
)
elif mod_name == "TF_DF":
body = (
'"""兼容 shim — 请优先 from chanlun import ..."""\n'
"from chanlun.pipeline.timeframe import TF_DF # noqa: F401\n"
)
else:
target = IMPORT_TARGET[mod_name]
body = (
f'"""兼容 shim — 请优先 from chanlun import ..."""\n'
f"from {target} import * # noqa: F403\n"
)
path.write_text(body, encoding="utf-8")
def main():
for sub in ("core", "indicators", "analysis", "pipeline", "pipeline/builders"):
d = ROOT / "chanlun" / sub
d.mkdir(parents=True, exist_ok=True)
(d / "__init__.py").write_text("", encoding="utf-8")
moved = []
def move_list(names, dest_pkg: Path):
for name in names:
src = ROOT / name
if not src.exists():
print("skip missing", name)
continue
dst = dest_pkg / name
text = rewrite_imports(src.read_text(encoding="utf-8"))
dst.write_text(text, encoding="utf-8")
src.unlink()
moved.append(name)
move_list(CORE, ROOT / "chanlun" / "core")
move_list(INDICATORS, ROOT / "chanlun" / "indicators")
move_list(ANALYSIS, ROOT / "chanlun" / "analysis")
for src_name, dst_name in PIPELINE.items():
src = ROOT / src_name
if not src.exists():
continue
dst = ROOT / "chanlun" / "pipeline" / dst_name
text = rewrite_imports(src.read_text(encoding="utf-8"))
dst.write_text(text, encoding="utf-8")
src.unlink()
moved.append(src_name)
# 恢复 get_zs_list(受控 L1
tf_path = ROOT / "chanlun" / "pipeline" / "timeframe.py"
tf_text = tf_path.read_text(encoding="utf-8")
if "def get_zs_list" not in tf_text:
needle = "\tdef calculate_seg_zs(self, seg_list):\n\t\treturn self.get_seg_zs_list(seg_list)\n"
insert = (
"\tdef get_zs_list(self, bi_list, seg_list):\n"
"\t\t\"\"\"兼容历史 API:线段中枢列表。\"\"\"\n"
"\t\treturn self.get_seg_zs_list(seg_list)\n"
+ needle
)
if needle in tf_text:
tf_path.write_text(tf_text.replace(needle, insert), encoding="utf-8")
else:
tf_path.write_text(
tf_text
+ "\n\tdef get_zs_list(self, bi_list, seg_list):\n"
+ "\t\treturn self.get_seg_zs_list(seg_list)\n",
encoding="utf-8",
)
print("patched get_zs_list")
for name in CORE + INDICATORS + ANALYSIS:
write_shim(name[:-3])
write_shim("ChanLun")
write_shim("TF_DF")
(ROOT / "chanlun" / "__init__.py").write_text(
'"""缠论引擎正式包。"""\n'
"from chanlun.pipeline.orchestrator import ChanLun\n"
"from chanlun.pipeline.timeframe import TF_DF\n"
'__all__ = ["ChanLun", "TF_DF"]\n',
encoding="utf-8",
)
examples = ROOT / "examples"
examples.mkdir(exist_ok=True)
notes = ROOT / "docs" / "notes"
notes.mkdir(parents=True, exist_ok=True)
for f in ("fx_strength_example.py", "realtime_fx_example.py"):
p = ROOT / f
if p.exists():
dest = examples / f
if dest.exists():
dest.unlink()
shutil.move(str(p), str(dest))
for f in ("缠论.txt", "操作策略.txt", "chanlun.txt", "K线动能理论.txt"):
p = ROOT / f
if p.exists():
dest = notes / f
if dest.exists():
dest.unlink()
shutil.move(str(p), str(dest))
tc = ROOT / "test_classifier.py"
if tc.exists():
dest = ROOT / "tests" / "test_classifier.py"
if dest.exists():
dest.unlink()
shutil.move(str(tc), str(dest))
print("moved", len(moved), "modules")
if __name__ == "__main__":
main()
+221
View File
@@ -0,0 +1,221 @@
#!/usr/bin/env python3
"""将 timeframe.TF_DF 拆为 mixin builders(方法体原样搬移)。"""
from __future__ import annotations
import ast
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "chanlun" / "pipeline" / "timeframe.py"
BUILDERS = ROOT / "chanlun" / "pipeline" / "builders"
# method name -> builder module
ASSIGN = {
"get_ema52": "indicators",
"get_ema24": "indicators",
"add_indicators": "indicators",
"get_klu_state": "kline",
"get_ema_state": "indicators",
"check_fx1": "kline",
"check_fx": "kline",
"check_fx2": "kline",
"check_fx_pattern": "kline",
"cal_volume_ratio": "kline",
"cal_kl_data": "kline",
"get_kl_data": "kline",
"get_klc_list": "kline",
"get_klu_list": "kline",
"cal_klu_pattern": "kline",
"_detect_single_reversal_pattern": "kline",
"_detect_double_pattern": "kline",
"_detect_triple_pattern": "kline",
"cal_trend": "bi",
"get_bi_list": "bi",
"cal_bi_list": "bi",
"check_top_fx": "bi",
"check_bottom_fx": "bi",
"get_bsp_state": "bsp",
"get_above_zero_bsp": "bsp",
"find_all_bsp": "bsp",
"check_bi_div": "bsp",
"find_first_bsp": "bsp",
"find_second_bsp": "bsp",
"get_zs_state": "zs",
"cal_bi_zs": "zs",
"cal_bi_zs_list": "zs",
"get_bi_zs_list": "zs",
"cal_bi_zs_list_pure": "zs",
"calculate_seg_zs": "zs",
"get_seg_zs_list": "zs",
"get_big_zs_list": "zs",
"get_zs_list": "zs",
"get_seg_list": "seg",
"get_decimal": "indicators",
}
HEADER = '''\
"""TF_DF builder mixin — 由 split_tfdf_builders 自动生成,逻辑与原 TF_DF 一致。"""
from __future__ import annotations
from datetime import timedelta
from decimal import Decimal
import numpy as np
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from technical.util import resample_to_interval
from chanlun.core.ChanBI import ChanBI
from chanlun.core.ChanBIZS import ChanBIZS
from chanlun.core.ChanBSP import ChanBSP
from chanlun.core.ChanEnum import (
Chan_BI_DIR,
Chan_BSP_DIR,
Chan_BSP_TYPE,
Chan_FX_TYPE,
Chan_K_DIR,
Chan_KLC_FX,
Chan_KLC_STATE,
Chan_KLINE_DIR,
Chan_KLU_PATTERN,
Chan_PRICE_TREND,
Chan_SEG_DIR,
Chan_ZS_DIR,
)
from chanlun.core.ChanKLC import ChanKLC
from chanlun.core.ChanKLU import ChanKLU
from chanlun.core.ChanSBI import ChanSBI
from chanlun.core.ChanSEG import ChanSEG
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
from chanlun.indicators.ChanMACD import ChanMACD
'''
def main():
source = SRC.read_text(encoding="utf-8")
# Use tabs; extract method bodies by line scanning
lines = source.splitlines(keepends=True)
# find class TF_DF
class_start = None
for i, line in enumerate(lines):
if line.startswith("class TF_DF"):
class_start = i
break
if class_start is None:
raise SystemExit("TF_DF not found")
# collect methods: name -> (start, end exclusive)
methods = []
i = class_start + 1
while i < len(lines):
line = lines[i]
m = re.match(r"^\tdef ([A-Za-z_][\w]*)\(", line)
if m:
name = m.group(1)
start = i
i += 1
while i < len(lines):
if re.match(r"^\tdef [A-Za-z_]", lines[i]) or (
lines[i] and not lines[i].startswith("\t") and not lines[i].startswith(" ") and lines[i].strip()
):
break
# nested def inside method starts with \t\tdef
i += 1
methods.append((name, start, i))
continue
i += 1
by_mod: dict[str, list[str]] = {k: [] for k in ("indicators", "kline", "bi", "seg", "zs", "bsp")}
keep_in_facade = [] # __init__, init_TF_DF, get_current_klc
for name, start, end in methods:
body = "".join(lines[start:end])
mod = ASSIGN.get(name)
if mod is None:
keep_in_facade.append((name, body))
else:
by_mod[mod].append(body)
BUILDERS.mkdir(parents=True, exist_ok=True)
(BUILDERS / "__init__.py").write_text("", encoding="utf-8")
mixin_classes = []
for mod, bodies in by_mod.items():
class_name = "".join(p.title() for p in mod.split("_")) + "BuilderMixin"
mixin_classes.append(class_name)
content = HEADER + f"class {class_name}:\n"
if not bodies:
content += "\tpass\n"
else:
content += "\n".join(bodies)
if not content.endswith("\n"):
content += "\n"
(BUILDERS / f"{mod}.py").write_text(content, encoding="utf-8")
print(f"wrote {mod}.py methods={len(bodies)} class={class_name}")
# rewrite timeframe.py facade
facade_imports = '''\
from datetime import timedelta
import numpy as np
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from technical.util import resample_to_interval
from chanlun.core.ChanBI import ChanBI
from chanlun.core.ChanBIZS import ChanBIZS
from chanlun.core.ChanBSP import ChanBSP
from chanlun.core.ChanEnum import (
Chan_BI_DIR,
Chan_BSP_DIR,
Chan_BSP_TYPE,
Chan_FX_TYPE,
Chan_K_DIR,
Chan_KLC_FX,
Chan_KLC_STATE,
Chan_KLINE_DIR,
Chan_KLU_PATTERN,
Chan_PRICE_TREND,
Chan_SEG_DIR,
Chan_ZS_DIR,
)
from chanlun.core.ChanKLC import ChanKLC
from chanlun.core.ChanKLU import ChanKLU
from chanlun.core.ChanSBI import ChanSBI
from chanlun.core.ChanSEG import ChanSEG
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
from chanlun.indicators.ChanMACD import ChanMACD
from chanlun.pipeline.builders.bi import BiBuilderMixin
from chanlun.pipeline.builders.bsp import BspBuilderMixin
from chanlun.pipeline.builders.indicators import IndicatorsBuilderMixin
from chanlun.pipeline.builders.kline import KlineBuilderMixin
from chanlun.pipeline.builders.seg import SegBuilderMixin
from chanlun.pipeline.builders.zs import ZsBuilderMixin
'''
bases = ", ".join(
[
"IndicatorsBuilderMixin",
"KlineBuilderMixin",
"BiBuilderMixin",
"SegBuilderMixin",
"ZsBuilderMixin",
"BspBuilderMixin",
]
)
facade = facade_imports + f"class TF_DF({bases}):\n"
for name, body in keep_in_facade:
facade += body
if not body.endswith("\n"):
facade += "\n"
SRC.write_text(facade, encoding="utf-8")
print("rewrote timeframe.py facade, kept", [n for n, _ in keep_in_facade])
if __name__ == "__main__":
main()