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:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user