feat(chanlun): 分钟级别日期自适应输出时分 YYYY-MM-DD HH:MM

This commit is contained in:
Justin Gu
2026-06-17 18:17:32 +08:00
parent 87f11e409c
commit 85e0f8a65f
4 changed files with 81 additions and 10 deletions
+6
View File
@@ -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 格式检查失败(纯格式调整,无功能变化)。
+1 -1
View File
@@ -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"
+21 -9
View File
@@ -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
+53
View File
@@ -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 的日期字段。