From 85e0f8a65f371e9327804327eb7bf462dd597715 Mon Sep 17 00:00:00 2001 From: Justin Gu <97915@qq.com> Date: Wed, 17 Jun 2026 18:17:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(chanlun):=20=E5=88=86=E9=92=9F=E7=BA=A7?= =?UTF-8?q?=E5=88=AB=E6=97=A5=E6=9C=9F=E8=87=AA=E9=80=82=E5=BA=94=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E6=97=B6=E5=88=86=20YYYY-MM-DD=20HH:MM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++ pyproject.toml | 2 +- src/easy_tdx/chanlun/analyser.py | 30 ++++++++++++------ tests/unit/test_chanlun.py | 53 ++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e328322..e009a25 100644 --- a/README.md +++ b/README.md @@ -1618,6 +1618,12 @@ ruff format --check src/ tests/ # format check ## Changelog +### 1.14.5 (2026-06-17) + +**缠论可视化日期自适应时分** — 响应网友反馈,分钟级别(1/5/15/30/60min)的缠论结果日期字段现在输出完整时分 `YYYY-MM-DD HH:MM`,日/周/月/年级别仍只输出日期 `YYYY-MM-DD`(无多余 `00:00`)。 + +新增 `ChanlunResult._fmt_dt()` 按 `frequency` 自适应格式化,统一作用于 `bis` / `zss` / `mmds` / `bcs` / `xds` 所有日期字段。兼容 CLI 原始值(`5MIN`/`30MIN`)与 Web 映射值(`5min`/`30min`)的大小写。三层接入同步生效。 + ### 1.14.4 (2026-06-16) **CI 修复** — 修复 v1.14.3 中 `cmd_chanlun.py` 两处 `click.echo(...)` 未按 `ruff format` 行宽规则合并导致的 CI 格式检查失败(纯格式调整,无功能变化)。 diff --git a/pyproject.toml b/pyproject.toml index 63e62a3..2df395d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "easy-tdx" -version = "1.14.4" +version = "1.14.5" description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步" readme = "README.md" requires-python = ">=3.10" diff --git a/src/easy_tdx/chanlun/analyser.py b/src/easy_tdx/chanlun/analyser.py index 17234be..54e8ed0 100644 --- a/src/easy_tdx/chanlun/analyser.py +++ b/src/easy_tdx/chanlun/analyser.py @@ -8,6 +8,7 @@ K线合并 → 分型识别 → 笔计算 → 中枢计算 → 线段 → 买卖 from __future__ import annotations from dataclasses import dataclass, field +from datetime import datetime from typing import Any import pandas as pd @@ -66,6 +67,17 @@ class ChanlunResult: bcs: list[BC] = field(default_factory=list) macd: dict[str, list[float]] = field(default_factory=dict) + def _fmt_dt(self, dt: datetime) -> str: + """按 frequency 自适应格式化日期。 + + 分钟级别(1/5/15/30/60min)输出完整时分 YYYY-MM-DD HH:MM, + 日/周/月/年级别只输出日期 YYYY-MM-DD(无多余 00:00)。 + frequency 来自 CLI 原始值(如 5MIN/30MIN)或 Web 映射值(5min/30min), + 统一转小写后判断是否含 'min'。 + """ + fmt = "%Y-%m-%d %H:%M" if "min" in self.frequency.lower() else "%Y-%m-%d" + return dt.strftime(fmt) + def to_dict(self) -> dict[str, Any]: """将结果转为可序列化的字典(用于 JSON 输出)。""" return { @@ -83,8 +95,8 @@ class ChanlunResult: { "index": bi.index, "direction": bi.direction.value, - "start_date": bi.start.k.date.strftime("%Y-%m-%d"), - "end_date": bi.end.k.date.strftime("%Y-%m-%d"), + "start_date": self._fmt_dt(bi.start.k.date), + "end_date": self._fmt_dt(bi.end.k.date), "high": round(bi.high, 2), "low": round(bi.low, 2), "done": bi.is_done(), @@ -99,8 +111,8 @@ class ChanlunResult: "gg": round(zs.gg, 2), "dd": round(zs.dd, 2), "line_count": zs.line_count, - "start_date": zs.start.k.date.strftime("%Y-%m-%d") if zs.start else None, - "end_date": zs.end.k.date.strftime("%Y-%m-%d") if zs.end else None, + "start_date": self._fmt_dt(zs.start.k.date) if zs.start else None, + "end_date": self._fmt_dt(zs.end.k.date) if zs.end else None, "done": zs.done, } for zs in self.zss @@ -109,8 +121,8 @@ class ChanlunResult: { "index": xd.index, "direction": xd.direction.value, - "start_date": xd.start.k.date.strftime("%Y-%m-%d"), - "end_date": xd.end.k.date.strftime("%Y-%m-%d"), + "start_date": self._fmt_dt(xd.start.k.date), + "end_date": self._fmt_dt(xd.end.k.date), "high": round(xd.high, 2), "low": round(xd.low, 2), } @@ -119,7 +131,7 @@ class ChanlunResult: "mmds": [ { "type": mmd.mmd_type.value, - "date": mmd.bi.end.k.date.strftime("%Y-%m-%d") if mmd.bi else None, + "date": self._fmt_dt(mmd.bi.end.k.date) if mmd.bi else None, "msg": mmd.msg, } for mmd in self.mmds @@ -128,8 +140,8 @@ class ChanlunResult: { "type": bc.bc_type.value, "bc": bc.bc, - "curr_date": bc.curr.end.k.date.strftime("%Y-%m-%d") if bc.curr else None, - "prev_date": bc.prev.end.k.date.strftime("%Y-%m-%d") if bc.prev else None, + "curr_date": self._fmt_dt(bc.curr.end.k.date) if bc.curr else None, + "prev_date": self._fmt_dt(bc.prev.end.k.date) if bc.prev else None, "msg": bc.msg, } for bc in self.bcs diff --git a/tests/unit/test_chanlun.py b/tests/unit/test_chanlun.py index a1c6914..b732132 100644 --- a/tests/unit/test_chanlun.py +++ b/tests/unit/test_chanlun.py @@ -629,6 +629,59 @@ class TestChanlunAnalyser: assert date_re.match(bc["curr_date"]) assert date_re.match(bc["prev_date"]) + def test_result_to_dict_minute_frequency_includes_time(self) -> None: + """分钟级别 frequency 下,日期字段应输出完整时分 YYYY-MM-DD HH:MM。 + + 对应网友反馈:分钟/低级别也需要时分用于分时可视化。 + 覆盖 CLI 原始值(5MIN/30MIN)与 Web 映射值(5min/30min)两种大小写。 + """ + import math + import re + + import pandas as pd + + from easy_tdx.chanlun.analyser import ChanlunAnalyser + + dates = pd.date_range("2025-01-02 09:30", periods=60, freq="5min") + highs = [15 + 5 * math.sin(i / 2) + i * 0.01 for i in range(60)] + lows = [highs[i] - 1.5 for i in range(60)] + df = pd.DataFrame( + { + "datetime": dates, + "open": [h - 0.5 for h in highs], + "close": [h - 0.2 for h in highs], + "high": highs, + "low": lows, + "vol": [1000] * 60, + } + ) + datetime_re = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$") + + # CLI 原始值(大写 5MIN)与 Web 映射值(小写 5min)应行为一致 + for freq in ("5MIN", "5min"): + d = ChanlunAnalyser(code="SZ000001", frequency=freq).process_klines(df).to_dict() + + # bis 日期应带时分 + assert len(d["bis"]) > 0 + for bi in d["bis"]: + assert datetime_re.match(bi["start_date"]) + assert datetime_re.match(bi["end_date"]) + + # zss/mmds/bcs 若产出,同样应带时分(有就检查) + for zs in d["zss"]: + if zs["start_date"] is not None: + assert datetime_re.match(zs["start_date"]) + if zs["end_date"] is not None: + assert datetime_re.match(zs["end_date"]) + for mmd in d["mmds"]: + if mmd["date"] is not None: + assert datetime_re.match(mmd["date"]) + for bc in d["bcs"]: + if bc["curr_date"] is not None: + assert datetime_re.match(bc["curr_date"]) + if bc["prev_date"] is not None: + assert datetime_re.match(bc["prev_date"]) + def test_print_table_with_dates(self) -> None: """CLI table 模式应正确消费 zss/mmds/bcs 的日期字段。