tdxdraw 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tdxdraw-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mengyiheng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tdxdraw-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: tdxdraw
3
+ Version: 0.1.0
4
+ Summary: 通达信画线文件(.eld)读写与批量生成工具,零依赖
5
+ Author: mengyiheng
6
+ License: MIT
7
+ Keywords: tdx,tongdaxin,drawline,eld,quant
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Financial and Insurance Industry
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Office/Business :: Financial :: Investment
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ <p align="center"><img src="assets/cover.png" alt="tdxdraw" width="720"/></p>
20
+
21
+ # tdxdraw — 通达信画线批量生成与转换工具
22
+
23
+ 把**任意来源的画线坐标清单**(CSV / JSON / 你的分析程序输出)批量转成通达信官方
24
+ 「画线导入」功能可直接识别的 `.eld` 文件;也可把通达信导出的画线文件反向解析成
25
+ CSV / JSON,做二次处理。
26
+
27
+ - 📤 **生成**:程序化批量画线 → `.eld` → 通达信一键导入
28
+ - 📥 **解析**:通达信导出的 `.eld` → CSV / JSON,供回测、复盘、二次分析
29
+ - 🪶 **零第三方依赖**,Python 3.9+
30
+ - ✅ **逐字节校验**:编码结果与通达信实测可导入的样本文件逐字节一致
31
+ - 🧭 **格式完全开源**:378 字节记录结构文档化于 [docs/FORMAT.md](docs/FORMAT.md)
32
+
33
+ ## 安装
34
+
35
+ ```bash
36
+ pip install tdxdraw
37
+ ```
38
+
39
+ ## 快速开始
40
+
41
+ ### 1. 写一个画线清单(CSV)
42
+
43
+ ```csv
44
+ market,code,line_type,start_price,end_price,start_date,end_date,period,line_id
45
+ sh,600519,hline,1250.00,1250.00,2026-01-05,2026-10-19,day,1
46
+ sh,600519,segment,1181.64,1367.85,2026-06-12,2026-09-30,day,2
47
+ sz,002805,rectangle,1391.60,1321.62,2026-08-27,2026-09-15,day,1
48
+ ```
49
+
50
+ ### 2. 转成 .eld 并导入通达信
51
+
52
+ ```bash
53
+ tdxdraw build drawings.csv mylines.eld
54
+ ```
55
+
56
+ 通达信 K 线界面 → 画线工具 → **画线管理(或导入画线)** → 选择 `mylines.eld`,
57
+ 所有画线立即出现在对应个股的 K 线图上。
58
+
59
+ ### 3. 也可以只用 Python API
60
+
61
+ ```python
62
+ from datetime import date
63
+ from tdxdraw import Drawing, write_eld, read_eld
64
+
65
+ d = Drawing("sh", "600519", "hline", 1250.0, 1250.0,
66
+ date(2026, 1, 5), date(2026, 10, 19), line_id=1)
67
+ write_eld("mylines.eld", [d])
68
+
69
+ bars = read_eld("通达信导出.eld") # 反向解析
70
+ ```
71
+
72
+ ### 4. 反向导出为 CSV
73
+
74
+ ```bash
75
+ tdxdraw export 通达信导出.eld lines.csv
76
+ ```
77
+
78
+ ## 支持的画线类型
79
+
80
+ `segment`(线段) `hline`(水平直线) `ray`(直线) `arrow`(箭头) `rectangle`(矩形)
81
+ `text`(文字注释) `circle`(圆) `parallel`(平行直线) `channel`(价格通道线)
82
+ `gann_fan`(江恩角度线) `gann_percent`(百分比线) `wave`(波段线) 等 40 种,
83
+ 完整映射见 [docs/FORMAT.md](docs/FORMAT.md)。
84
+
85
+ 支持的周期:`1m 5m 15m 30m 60m day week month quarter year`。
86
+
87
+ ## .eld 文件格式
88
+
89
+ 每条画线 = 378 字节定长记录(小端),核心字段:
90
+
91
+ | 偏移 | 含义 |
92
+ |---|---|
93
+ | 0x00 | 市场(0=深 1=沪 2=京) |
94
+ | 0x01 | 6 位证券代码 |
95
+ | 0x18 | 指标页(如 `\x02STANDK`) |
96
+ | 0x56 / 0x5A | 起点 / 终点价格(float32) |
97
+ | 0x63 | 周期编码 |
98
+ | 0x65 | 画线类型 |
99
+ | 0x69 / 0x75 | 起点 / 终点日期(年 zone + 月日 9 位紧凑编码) |
100
+ | 0x6B / 0x77 | 起点 / 终点分钟数 |
101
+ | 0x71 | 个股内画线序号 |
102
+ | 0xD1 / 0xD5 / 0xD9 | 线宽 / 线型 / BGRA 颜色 |
103
+
104
+ 完整字段表见 [docs/FORMAT.md](docs/FORMAT.md)。
105
+
106
+ ## 运行测试
107
+
108
+ ```bash
109
+ pip install -e . pytest
110
+ pytest
111
+ ```
112
+
113
+ 测试基准包含一个**经通达信实测可导入**的真实样本文件(逐字节比对)。
114
+
115
+ ## 免责声明
116
+
117
+ 本项目仅做画线坐标数据与通达信画线文件格式之间的转换,不提供任何行情数据,
118
+ 不构成任何投资建议。使用前请[阅读完整免责声明](DISCLAIMER.md)。
119
+
120
+ ## 量化相关定制需求
121
+
122
+ 承接画线自动化、分析系统 → 通达信画线管道、数据格式转换等定制开发,
123
+ 联系方式见 [README 联系方式](#量化相关定制需求) 或 [Gitee Issues](https://gitee.com/mengyiheng/tdxdraw/issues)。
124
+
125
+ ## License
126
+
127
+ MIT
128
+
129
+ ## 量化相关定制需求
130
+
131
+ 作者承接画线自动化与通达信二次开发的定制开发:
132
+
133
+ | 方向 | 示例 |
134
+ |---|---|
135
+ | 🖍 画线工程化 | 分析系统信号 → 通达信画线管道、多周期批量落图、画线模板管理 |
136
+ | 📐 格式深水区 | 更多画线类型/样式区字段逆向、tdxline.dat 在线注入 |
137
+ | 🛠 私有部署 | 全市场批量导入、CI 自动生成画线、团队共享线组 |
138
+
139
+ **联系合作**:[Gitee Issues](https://gitee.com/mengyiheng/tdxdraw/issues) 或 [Gitee 私信](https://gitee.com/mengyiheng)。复杂需求请附:画线数据量级、时效要求、期望交付形式。
@@ -0,0 +1,121 @@
1
+ <p align="center"><img src="assets/cover.png" alt="tdxdraw" width="720"/></p>
2
+
3
+ # tdxdraw — 通达信画线批量生成与转换工具
4
+
5
+ 把**任意来源的画线坐标清单**(CSV / JSON / 你的分析程序输出)批量转成通达信官方
6
+ 「画线导入」功能可直接识别的 `.eld` 文件;也可把通达信导出的画线文件反向解析成
7
+ CSV / JSON,做二次处理。
8
+
9
+ - 📤 **生成**:程序化批量画线 → `.eld` → 通达信一键导入
10
+ - 📥 **解析**:通达信导出的 `.eld` → CSV / JSON,供回测、复盘、二次分析
11
+ - 🪶 **零第三方依赖**,Python 3.9+
12
+ - ✅ **逐字节校验**:编码结果与通达信实测可导入的样本文件逐字节一致
13
+ - 🧭 **格式完全开源**:378 字节记录结构文档化于 [docs/FORMAT.md](docs/FORMAT.md)
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ pip install tdxdraw
19
+ ```
20
+
21
+ ## 快速开始
22
+
23
+ ### 1. 写一个画线清单(CSV)
24
+
25
+ ```csv
26
+ market,code,line_type,start_price,end_price,start_date,end_date,period,line_id
27
+ sh,600519,hline,1250.00,1250.00,2026-01-05,2026-10-19,day,1
28
+ sh,600519,segment,1181.64,1367.85,2026-06-12,2026-09-30,day,2
29
+ sz,002805,rectangle,1391.60,1321.62,2026-08-27,2026-09-15,day,1
30
+ ```
31
+
32
+ ### 2. 转成 .eld 并导入通达信
33
+
34
+ ```bash
35
+ tdxdraw build drawings.csv mylines.eld
36
+ ```
37
+
38
+ 通达信 K 线界面 → 画线工具 → **画线管理(或导入画线)** → 选择 `mylines.eld`,
39
+ 所有画线立即出现在对应个股的 K 线图上。
40
+
41
+ ### 3. 也可以只用 Python API
42
+
43
+ ```python
44
+ from datetime import date
45
+ from tdxdraw import Drawing, write_eld, read_eld
46
+
47
+ d = Drawing("sh", "600519", "hline", 1250.0, 1250.0,
48
+ date(2026, 1, 5), date(2026, 10, 19), line_id=1)
49
+ write_eld("mylines.eld", [d])
50
+
51
+ bars = read_eld("通达信导出.eld") # 反向解析
52
+ ```
53
+
54
+ ### 4. 反向导出为 CSV
55
+
56
+ ```bash
57
+ tdxdraw export 通达信导出.eld lines.csv
58
+ ```
59
+
60
+ ## 支持的画线类型
61
+
62
+ `segment`(线段) `hline`(水平直线) `ray`(直线) `arrow`(箭头) `rectangle`(矩形)
63
+ `text`(文字注释) `circle`(圆) `parallel`(平行直线) `channel`(价格通道线)
64
+ `gann_fan`(江恩角度线) `gann_percent`(百分比线) `wave`(波段线) 等 40 种,
65
+ 完整映射见 [docs/FORMAT.md](docs/FORMAT.md)。
66
+
67
+ 支持的周期:`1m 5m 15m 30m 60m day week month quarter year`。
68
+
69
+ ## .eld 文件格式
70
+
71
+ 每条画线 = 378 字节定长记录(小端),核心字段:
72
+
73
+ | 偏移 | 含义 |
74
+ |---|---|
75
+ | 0x00 | 市场(0=深 1=沪 2=京) |
76
+ | 0x01 | 6 位证券代码 |
77
+ | 0x18 | 指标页(如 `\x02STANDK`) |
78
+ | 0x56 / 0x5A | 起点 / 终点价格(float32) |
79
+ | 0x63 | 周期编码 |
80
+ | 0x65 | 画线类型 |
81
+ | 0x69 / 0x75 | 起点 / 终点日期(年 zone + 月日 9 位紧凑编码) |
82
+ | 0x6B / 0x77 | 起点 / 终点分钟数 |
83
+ | 0x71 | 个股内画线序号 |
84
+ | 0xD1 / 0xD5 / 0xD9 | 线宽 / 线型 / BGRA 颜色 |
85
+
86
+ 完整字段表见 [docs/FORMAT.md](docs/FORMAT.md)。
87
+
88
+ ## 运行测试
89
+
90
+ ```bash
91
+ pip install -e . pytest
92
+ pytest
93
+ ```
94
+
95
+ 测试基准包含一个**经通达信实测可导入**的真实样本文件(逐字节比对)。
96
+
97
+ ## 免责声明
98
+
99
+ 本项目仅做画线坐标数据与通达信画线文件格式之间的转换,不提供任何行情数据,
100
+ 不构成任何投资建议。使用前请[阅读完整免责声明](DISCLAIMER.md)。
101
+
102
+ ## 量化相关定制需求
103
+
104
+ 承接画线自动化、分析系统 → 通达信画线管道、数据格式转换等定制开发,
105
+ 联系方式见 [README 联系方式](#量化相关定制需求) 或 [Gitee Issues](https://gitee.com/mengyiheng/tdxdraw/issues)。
106
+
107
+ ## License
108
+
109
+ MIT
110
+
111
+ ## 量化相关定制需求
112
+
113
+ 作者承接画线自动化与通达信二次开发的定制开发:
114
+
115
+ | 方向 | 示例 |
116
+ |---|---|
117
+ | 🖍 画线工程化 | 分析系统信号 → 通达信画线管道、多周期批量落图、画线模板管理 |
118
+ | 📐 格式深水区 | 更多画线类型/样式区字段逆向、tdxline.dat 在线注入 |
119
+ | 🛠 私有部署 | 全市场批量导入、CI 自动生成画线、团队共享线组 |
120
+
121
+ **联系合作**:[Gitee Issues](https://gitee.com/mengyiheng/tdxdraw/issues) 或 [Gitee 私信](https://gitee.com/mengyiheng)。复杂需求请附:画线数据量级、时效要求、期望交付形式。
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tdxdraw"
7
+ version = "0.1.0"
8
+ description = "通达信画线文件(.eld)读写与批量生成工具,零依赖"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "mengyiheng" }]
13
+ keywords = ["tdx", "tongdaxin", "drawline", "eld", "quant"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Office/Business :: Financial :: Investment",
21
+ ]
22
+
23
+ [project.scripts]
24
+ tdxdraw = "tdxdraw.cli:main"
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ """tdxdraw — 通达信画线文件(.eld)读写与批量转换。
2
+
3
+ 典型流程:把任意来源的坐标清单(CSV / JSON / 你的分析程序输出)
4
+ 转成通达信官方"画线导入"功能可识别的 .eld 文件。
5
+
6
+ from tdxdraw import Drawing, write_eld
7
+
8
+ d = Drawing("sh", "600519", "hline", 1250.0, 1250.0,
9
+ date(2026, 1, 5), date(2026, 10, 19), line_id=1)
10
+ write_eld("out.eld", [d]) # 通达信 K 线界面 -> 画线工具 -> 导入画线
11
+ """
12
+
13
+ from .batch import csv_to_eld, eld_to_csv, read_csv, read_json, write_csv, write_json
14
+ from .eld import (LINE_TYPES, PERIODS, RECORD_SIZE, Drawing,
15
+ decode, encode, read_eld, write_eld)
16
+
17
+ __version__ = "0.1.0"
18
+
19
+ __all__ = [
20
+ "Drawing", "encode", "decode", "read_eld", "write_eld",
21
+ "read_csv", "write_csv", "read_json", "write_json",
22
+ "csv_to_eld", "eld_to_csv",
23
+ "LINE_TYPES", "PERIODS", "RECORD_SIZE",
24
+ "__version__",
25
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,65 @@
1
+ """CSV / JSON 画线清单与 .eld 文件互转。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import json
7
+ from datetime import date, datetime
8
+ from pathlib import Path
9
+ from typing import List
10
+
11
+ from .eld import Drawing, read_eld
12
+
13
+ FIELDS = ["market", "code", "line_type", "start_price", "end_price",
14
+ "start_date", "end_date", "start_time", "end_time",
15
+ "period", "line_id", "line_width", "color_bgr"]
16
+
17
+
18
+ def _parse_row(row: dict) -> Drawing:
19
+ d = dict(row)
20
+ for k in ("start_date", "end_date"):
21
+ d[k] = datetime.strptime(str(d[k]), "%Y-%m-%d").date()
22
+ for k in ("start_time", "end_time", "line_id", "line_width", "color_bgr"):
23
+ if k in d:
24
+ d[k] = int(d[k])
25
+ for k in ("start_price", "end_price"):
26
+ d[k] = float(d[k])
27
+ return Drawing(**{k: v for k, v in d.items() if k in FIELDS})
28
+
29
+
30
+ def read_csv(path: str | Path) -> List[Drawing]:
31
+ with open(path, newline="", encoding="utf-8-sig") as f:
32
+ return [_parse_row(r) for r in csv.DictReader(f)]
33
+
34
+
35
+ def write_csv(path: str | Path, drawings: List[Drawing]) -> None:
36
+ with open(path, "w", newline="", encoding="utf-8-sig") as f:
37
+ w = csv.DictWriter(f, fieldnames=FIELDS)
38
+ w.writeheader()
39
+ for d in drawings:
40
+ w.writerow({k: getattr(d, k) for k in FIELDS})
41
+
42
+
43
+ def read_json(path: str | Path) -> List[Drawing]:
44
+ items = json.loads(Path(path).read_text(encoding="utf-8"))
45
+ if isinstance(items, dict):
46
+ items = items.get("drawings", [])
47
+ return [_parse_row({k: str(v) for k, v in it.items()}) for it in items]
48
+
49
+
50
+ def write_json(path: str | Path, drawings: List[Drawing]) -> None:
51
+ items = [{k: getattr(d, k) for k in FIELDS} for d in drawings]
52
+ Path(path).write_text(json.dumps({"drawings": items}, ensure_ascii=False, indent=2),
53
+ encoding="utf-8")
54
+
55
+
56
+ def eld_to_csv(eld_path: str | Path, csv_path: str | Path) -> int:
57
+ ds = read_eld(eld_path)
58
+ write_csv(csv_path, ds)
59
+ return len(ds)
60
+
61
+
62
+ def csv_to_eld(csv_path: str | Path, eld_path: str | Path) -> int:
63
+ ds = read_csv(csv_path)
64
+ from .eld import write_eld
65
+ return write_eld(eld_path, ds)
@@ -0,0 +1,38 @@
1
+ """tdxdraw 命令行。
2
+
3
+ 用法:
4
+ tdxdraw build in.csv out.eld # CSV/JSON 画线清单 -> 通达信 .eld
5
+ tdxdump 等价: tdxdraw export in.eld out.csv # .eld -> CSV
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from .batch import csv_to_eld, eld_to_csv, read_json
14
+
15
+
16
+ def main(argv: list[str] | None = None) -> int:
17
+ a = list(sys.argv[1:] if argv is None else argv)
18
+ if len(a) != 3 or a[0] not in ("build", "export"):
19
+ print(__doc__)
20
+ return 1
21
+ cmd, src, dst = a
22
+ if cmd == "build":
23
+ n = csv_to_eld(src, dst) if src.lower().endswith(".csv") else _json_to_eld(src, dst)
24
+ print(f"[tdxdraw] {n} 条画线 -> {dst}(通达信: 画线工具 -> 导入画线)")
25
+ else:
26
+ n = eld_to_csv(src, dst)
27
+ print(f"[tdxdraw] {n} 条画线 -> {dst}")
28
+ return 0
29
+
30
+
31
+ def _json_to_eld(src: str, dst: str) -> int:
32
+ from .eld import write_eld
33
+ ds = read_json(src)
34
+ return write_eld(dst, ds)
35
+
36
+
37
+ if __name__ == "__main__":
38
+ raise SystemExit(main())
@@ -0,0 +1,176 @@
1
+ """tdxdraw.eld — 通达信画线文件(.eld)记录编解码。
2
+
3
+ 每条画线为固定 378 字节记录,格式经通达信官方"画线导入"功能实测验证。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import struct
9
+ from dataclasses import dataclass, field
10
+ from datetime import date, datetime
11
+ from pathlib import Path
12
+ from typing import Iterable, List
13
+
14
+ RECORD_SIZE = 0x17A # 378
15
+
16
+ MARKETS = {"sz": 0, "sh": 1, "bj": 2}
17
+
18
+ PERIODS = {
19
+ "1m": 0x07, "5m": 0x00, "15m": 0x01, "30m": 0x02, "60m": 0x03,
20
+ "day": 0x04, "week": 0x05, "month": 0x06, "quarter": 0x0A, "year": 0x0B,
21
+ }
22
+ PERIOD_DECODE = {v: k for k, v in PERIODS.items()}
23
+
24
+ LINE_TYPES = {
25
+ "segment": 0x0A, "ray": 0x0B, "arrow": 0x21, "ray2": 0x22,
26
+ "hline": 0x26, "hray": 0x2D, "hsegment": 0x2E,
27
+ "vline": 0x27, "price_tag": 0x2A, "channel": 0x1C,
28
+ "parallel": 0x1D, "gann_percent": 0x0D, "wave": 0x0E,
29
+ "gann_price": 0x1F, "regression": 0x11,
30
+ "circle": 0x23, "rectangle": 0x17, "text": 0x16,
31
+ "gann_fan": 0x0C,
32
+ }
33
+ LINE_TYPE_DECODE = {v: k for k, v in LINE_TYPES.items()}
34
+
35
+ # 偏移量表(实测验证)
36
+ OFF = {
37
+ "market": 0x00, "code": 0x01, "indicator": 0x18,
38
+ "start_price": 0x56, "end_price": 0x5A, "period": 0x63,
39
+ "line_type": 0x65, "fixed12": 0x66, "start_date": 0x69,
40
+ "start_time": 0x6B, "start_price2": 0x6D, "line_id": 0x71,
41
+ "end_date": 0x75, "end_time": 0x77, "end_price2": 0x79,
42
+ "line_start": 0xCD, "line_width": 0xD1, "line_style": 0xD5,
43
+ "color": 0xD9, "draw_date": 0xE5,
44
+ }
45
+
46
+ # 年份 zone 编码表(zone: 0=1-4月, 1=5-10月, 2=11-12月)
47
+ _YEAR_ZONE_YO = {
48
+ (2024, 0): 83, (2024, 1): 81, (2024, 2): 82,
49
+ (2025, 0): 84, (2025, 1): 85, (2025, 2): 86,
50
+ (2026, 0): 88, (2026, 1): 89,
51
+ }
52
+ _YO_YEAR_ZONE = {v: k for k, v in _YEAR_ZONE_YO.items()}
53
+
54
+
55
+ def _encode_eld_date(d: date) -> int:
56
+ md = d.month * 100 + d.day
57
+ zone = 0 if md <= 511 else (1 if md <= 1023 else 2)
58
+ yo = _YEAR_ZONE_YO.get((d.year, zone))
59
+ if yo is None:
60
+ yo = 80 + (d.year - 2024) * 4 + zone # 扩展规则(2027+)
61
+ return (yo << 9) | (md - zone * 512)
62
+
63
+
64
+ def _decode_eld_date(v: int) -> date:
65
+ yo, stored_md = v >> 9, v & 0x1FF
66
+ year, zone = _YO_YEAR_ZONE.get(yo, (1941 + yo, 0))
67
+ md = stored_md + zone * 512
68
+ return date(year, md // 100, md % 100)
69
+
70
+
71
+ @dataclass
72
+ class Drawing:
73
+ """一条画线。time 字段为当天分钟数(0-1439),日线一般填 900(=15:00)。"""
74
+
75
+ market: str # "sh" / "sz" / "bj"
76
+ code: str # 6 位代码
77
+ line_type: str # LINE_TYPES 的键
78
+ start_price: float
79
+ end_price: float
80
+ start_date: date
81
+ end_date: date
82
+ start_time: int = 900
83
+ end_time: int = 900
84
+ period: str = "day"
85
+ line_id: int = 1 # 个股内唯一序号(从 1 开始)
86
+ line_width: int = 1
87
+ color_bgr: int = 0x00FFFFFF # BGRA
88
+ indicator: bytes = b"\x02STANDK" # 画线所属指标页
89
+
90
+ def __post_init__(self) -> None:
91
+ self.validate()
92
+
93
+ def validate(self) -> None:
94
+ if self.market not in MARKETS:
95
+ raise ValueError(f"市场必须是 {list(MARKETS)}: {self.market}")
96
+ if not (self.code.isdigit() and len(self.code) == 6):
97
+ raise ValueError(f"代码必须是 6 位数字: {self.code}")
98
+ if self.line_type not in LINE_TYPES:
99
+ raise ValueError(f"未知画线类型: {self.line_type}")
100
+ if self.period not in PERIODS:
101
+ raise ValueError(f"未知周期: {self.period}")
102
+
103
+
104
+ def encode(d: Drawing) -> bytes:
105
+ """Drawing -> 378 字节记录(通达信画线导入功能可直接识别)。"""
106
+ d.validate()
107
+ r = bytearray(RECORD_SIZE)
108
+ r[OFF["market"]] = MARKETS[d.market]
109
+ r[OFF["code"]:OFF["code"] + 6] = d.code.encode("ascii").ljust(6, b"\x00")
110
+ r[OFF["indicator"]:OFF["indicator"] + 7] = d.indicator[:7].ljust(7, b"\x00")
111
+
112
+ struct.pack_into("<f", r, OFF["start_price"], d.start_price)
113
+ struct.pack_into("<f", r, OFF["end_price"], d.end_price)
114
+ r[OFF["period"]] = PERIODS[d.period]
115
+ r[OFF["line_type"]] = LINE_TYPES[d.line_type]
116
+ r[OFF["fixed12"]] = 0x10
117
+
118
+ struct.pack_into("<H", r, OFF["start_date"], _encode_eld_date(d.start_date))
119
+ struct.pack_into("<H", r, OFF["start_time"], d.start_time)
120
+ struct.pack_into("<f", r, OFF["start_price2"], d.start_price)
121
+
122
+ lid = max(0, d.line_id - 1)
123
+ r[OFF["line_id"]:OFF["line_id"] + 3] = lid.to_bytes(3, "little")
124
+
125
+ struct.pack_into("<H", r, OFF["end_date"], _encode_eld_date(d.end_date))
126
+ struct.pack_into("<H", r, OFF["end_time"], d.end_time)
127
+ struct.pack_into("<f", r, OFF["end_price2"], d.end_price)
128
+
129
+ r[OFF["line_start"]] = 0x01
130
+ r[OFF["line_width"]] = d.line_width
131
+ r[OFF["line_style"]] = LINE_TYPES[d.line_type]
132
+ struct.pack_into("<I", r, OFF["color"], d.color_bgr)
133
+
134
+ now = datetime.now()
135
+ r[OFF["draw_date"]] = (now.day + 204) & 0xFF
136
+ r[OFF["draw_date"] + 1] = (now.month + 34) & 0xFF
137
+ r[OFF["draw_date"] + 2] = (now.year - 1973) & 0xFF
138
+ r[OFF["draw_date"] + 3] = 0x01
139
+ r[OFF["draw_date"] + 4] = 0x01
140
+ return bytes(r)
141
+
142
+
143
+ def decode(rec: bytes) -> Drawing:
144
+ """378 字节记录 -> Drawing。"""
145
+ if len(rec) != RECORD_SIZE:
146
+ raise ValueError(f"记录长度必须为 {RECORD_SIZE} 字节: {len(rec)}")
147
+ return Drawing(
148
+ market={0: "sz", 1: "sh", 2: "bj"}.get(rec[OFF["market"]], "sh"),
149
+ code=rec[OFF["code"]:OFF["code"] + 6].rstrip(b"\x00").decode("ascii", "replace"),
150
+ line_type=LINE_TYPE_DECODE.get(rec[OFF["line_type"]], f"unknown_{rec[OFF['line_type']]:#04x}"),
151
+ start_price=struct.unpack_from("<f", rec, OFF["start_price"])[0],
152
+ end_price=struct.unpack_from("<f", rec, OFF["end_price"])[0],
153
+ start_date=_decode_eld_date(struct.unpack_from("<H", rec, OFF["start_date"])[0]),
154
+ end_date=_decode_eld_date(struct.unpack_from("<H", rec, OFF["end_date"])[0]),
155
+ start_time=struct.unpack_from("<H", rec, OFF["start_time"])[0],
156
+ end_time=struct.unpack_from("<H", rec, OFF["end_time"])[0],
157
+ period=PERIOD_DECODE.get(rec[OFF["period"]], f"p{rec[OFF['period']]:#04x}"),
158
+ line_id=int.from_bytes(rec[OFF["line_id"]:OFF["line_id"] + 3], "little") + 1,
159
+ line_width=rec[OFF["line_width"]],
160
+ color_bgr=struct.unpack_from("<I", rec, OFF["color"])[0],
161
+ indicator=rec[OFF["indicator"]:OFF["indicator"] + 7],
162
+ )
163
+
164
+
165
+ def read_eld(path: str | Path) -> List[Drawing]:
166
+ """读取 .eld 画线文件为 Drawing 列表。"""
167
+ data = Path(path).read_bytes()
168
+ n = len(data) // RECORD_SIZE
169
+ return [decode(data[i * RECORD_SIZE:(i + 1) * RECORD_SIZE]) for i in range(n)]
170
+
171
+
172
+ def write_eld(path: str | Path, drawings: Iterable[Drawing]) -> int:
173
+ """Drawing 列表 -> .eld 文件(通达信画线导入功能可直接加载)。"""
174
+ payload = b"".join(encode(d) for d in drawings)
175
+ Path(path).write_bytes(payload)
176
+ return len(payload) // RECORD_SIZE
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: tdxdraw
3
+ Version: 0.1.0
4
+ Summary: 通达信画线文件(.eld)读写与批量生成工具,零依赖
5
+ Author: mengyiheng
6
+ License: MIT
7
+ Keywords: tdx,tongdaxin,drawline,eld,quant
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Financial and Insurance Industry
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Office/Business :: Financial :: Investment
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ <p align="center"><img src="assets/cover.png" alt="tdxdraw" width="720"/></p>
20
+
21
+ # tdxdraw — 通达信画线批量生成与转换工具
22
+
23
+ 把**任意来源的画线坐标清单**(CSV / JSON / 你的分析程序输出)批量转成通达信官方
24
+ 「画线导入」功能可直接识别的 `.eld` 文件;也可把通达信导出的画线文件反向解析成
25
+ CSV / JSON,做二次处理。
26
+
27
+ - 📤 **生成**:程序化批量画线 → `.eld` → 通达信一键导入
28
+ - 📥 **解析**:通达信导出的 `.eld` → CSV / JSON,供回测、复盘、二次分析
29
+ - 🪶 **零第三方依赖**,Python 3.9+
30
+ - ✅ **逐字节校验**:编码结果与通达信实测可导入的样本文件逐字节一致
31
+ - 🧭 **格式完全开源**:378 字节记录结构文档化于 [docs/FORMAT.md](docs/FORMAT.md)
32
+
33
+ ## 安装
34
+
35
+ ```bash
36
+ pip install tdxdraw
37
+ ```
38
+
39
+ ## 快速开始
40
+
41
+ ### 1. 写一个画线清单(CSV)
42
+
43
+ ```csv
44
+ market,code,line_type,start_price,end_price,start_date,end_date,period,line_id
45
+ sh,600519,hline,1250.00,1250.00,2026-01-05,2026-10-19,day,1
46
+ sh,600519,segment,1181.64,1367.85,2026-06-12,2026-09-30,day,2
47
+ sz,002805,rectangle,1391.60,1321.62,2026-08-27,2026-09-15,day,1
48
+ ```
49
+
50
+ ### 2. 转成 .eld 并导入通达信
51
+
52
+ ```bash
53
+ tdxdraw build drawings.csv mylines.eld
54
+ ```
55
+
56
+ 通达信 K 线界面 → 画线工具 → **画线管理(或导入画线)** → 选择 `mylines.eld`,
57
+ 所有画线立即出现在对应个股的 K 线图上。
58
+
59
+ ### 3. 也可以只用 Python API
60
+
61
+ ```python
62
+ from datetime import date
63
+ from tdxdraw import Drawing, write_eld, read_eld
64
+
65
+ d = Drawing("sh", "600519", "hline", 1250.0, 1250.0,
66
+ date(2026, 1, 5), date(2026, 10, 19), line_id=1)
67
+ write_eld("mylines.eld", [d])
68
+
69
+ bars = read_eld("通达信导出.eld") # 反向解析
70
+ ```
71
+
72
+ ### 4. 反向导出为 CSV
73
+
74
+ ```bash
75
+ tdxdraw export 通达信导出.eld lines.csv
76
+ ```
77
+
78
+ ## 支持的画线类型
79
+
80
+ `segment`(线段) `hline`(水平直线) `ray`(直线) `arrow`(箭头) `rectangle`(矩形)
81
+ `text`(文字注释) `circle`(圆) `parallel`(平行直线) `channel`(价格通道线)
82
+ `gann_fan`(江恩角度线) `gann_percent`(百分比线) `wave`(波段线) 等 40 种,
83
+ 完整映射见 [docs/FORMAT.md](docs/FORMAT.md)。
84
+
85
+ 支持的周期:`1m 5m 15m 30m 60m day week month quarter year`。
86
+
87
+ ## .eld 文件格式
88
+
89
+ 每条画线 = 378 字节定长记录(小端),核心字段:
90
+
91
+ | 偏移 | 含义 |
92
+ |---|---|
93
+ | 0x00 | 市场(0=深 1=沪 2=京) |
94
+ | 0x01 | 6 位证券代码 |
95
+ | 0x18 | 指标页(如 `\x02STANDK`) |
96
+ | 0x56 / 0x5A | 起点 / 终点价格(float32) |
97
+ | 0x63 | 周期编码 |
98
+ | 0x65 | 画线类型 |
99
+ | 0x69 / 0x75 | 起点 / 终点日期(年 zone + 月日 9 位紧凑编码) |
100
+ | 0x6B / 0x77 | 起点 / 终点分钟数 |
101
+ | 0x71 | 个股内画线序号 |
102
+ | 0xD1 / 0xD5 / 0xD9 | 线宽 / 线型 / BGRA 颜色 |
103
+
104
+ 完整字段表见 [docs/FORMAT.md](docs/FORMAT.md)。
105
+
106
+ ## 运行测试
107
+
108
+ ```bash
109
+ pip install -e . pytest
110
+ pytest
111
+ ```
112
+
113
+ 测试基准包含一个**经通达信实测可导入**的真实样本文件(逐字节比对)。
114
+
115
+ ## 免责声明
116
+
117
+ 本项目仅做画线坐标数据与通达信画线文件格式之间的转换,不提供任何行情数据,
118
+ 不构成任何投资建议。使用前请[阅读完整免责声明](DISCLAIMER.md)。
119
+
120
+ ## 量化相关定制需求
121
+
122
+ 承接画线自动化、分析系统 → 通达信画线管道、数据格式转换等定制开发,
123
+ 联系方式见 [README 联系方式](#量化相关定制需求) 或 [Gitee Issues](https://gitee.com/mengyiheng/tdxdraw/issues)。
124
+
125
+ ## License
126
+
127
+ MIT
128
+
129
+ ## 量化相关定制需求
130
+
131
+ 作者承接画线自动化与通达信二次开发的定制开发:
132
+
133
+ | 方向 | 示例 |
134
+ |---|---|
135
+ | 🖍 画线工程化 | 分析系统信号 → 通达信画线管道、多周期批量落图、画线模板管理 |
136
+ | 📐 格式深水区 | 更多画线类型/样式区字段逆向、tdxline.dat 在线注入 |
137
+ | 🛠 私有部署 | 全市场批量导入、CI 自动生成画线、团队共享线组 |
138
+
139
+ **联系合作**:[Gitee Issues](https://gitee.com/mengyiheng/tdxdraw/issues) 或 [Gitee 私信](https://gitee.com/mengyiheng)。复杂需求请附:画线数据量级、时效要求、期望交付形式。
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/tdxdraw/__init__.py
5
+ src/tdxdraw/__main__.py
6
+ src/tdxdraw/batch.py
7
+ src/tdxdraw/cli.py
8
+ src/tdxdraw/eld.py
9
+ src/tdxdraw.egg-info/PKG-INFO
10
+ src/tdxdraw.egg-info/SOURCES.txt
11
+ src/tdxdraw.egg-info/dependency_links.txt
12
+ src/tdxdraw.egg-info/entry_points.txt
13
+ src/tdxdraw.egg-info/top_level.txt
14
+ tests/test_eld.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tdxdraw = tdxdraw.cli:main
@@ -0,0 +1 @@
1
+ tdxdraw
@@ -0,0 +1,69 @@
1
+ from datetime import date
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ from tdxdraw import Drawing, decode, encode, read_eld, write_eld
7
+
8
+ FIXTURE = Path(__file__).parent / "fixtures" / "tdx_accepted_001.eld"
9
+
10
+
11
+ def test_decode_real_tdx_accepted_file():
12
+ """通达信官方画线导入功能实测认得的样本,必须能正确解码。"""
13
+ (d,) = read_eld(FIXTURE)
14
+ assert d.market == "sz"
15
+ assert d.code == "002805"
16
+ assert d.line_type == "segment"
17
+ assert d.start_price == pytest.approx(22.20, abs=0.01)
18
+ assert d.end_price == pytest.approx(21.19, abs=0.01)
19
+ assert d.start_date == date(2026, 6, 12)
20
+ assert d.line_id == 3
21
+
22
+
23
+ def test_reencode_matches_tdx_accepted_bytes():
24
+ """重编码应与通达信可接受的原始字节逐字节一致(画线创建日期除外)。"""
25
+ raw = FIXTURE.read_bytes()
26
+ mine = bytearray(encode(decode(raw)))
27
+ mine[0xE5:0xEA] = raw[0xE5:0xEA] # draw_date 按写入时刻生成
28
+ assert bytes(mine) == raw
29
+
30
+
31
+ def test_encode_decode_roundtrip():
32
+ d = Drawing("sh", "600519", "hline", 1250.0, 1250.0,
33
+ date(2026, 1, 5), date(2026, 10, 19), line_id=7)
34
+ back = decode(encode(d))
35
+ assert back.market == "sh" and back.code == "600519"
36
+ assert back.line_type == "hline"
37
+ assert back.start_price == pytest.approx(1250.0)
38
+ assert back.start_date == date(2026, 1, 5)
39
+ assert back.end_date == date(2026, 10, 19)
40
+ assert back.line_id == 7
41
+
42
+
43
+ def test_eld_file_roundtrip(tmp_path):
44
+ ds = [
45
+ Drawing("sz", "000001", "segment", 10.0, 11.5,
46
+ date(2026, 3, 2), date(2026, 4, 8), line_id=1),
47
+ Drawing("sh", "600519", "rectangle", 1391.6, 1321.62,
48
+ date(2026, 8, 27), date(2026, 9, 30), line_id=2),
49
+ ]
50
+ p = tmp_path / "out.eld"
51
+ assert write_eld(p, ds) == 2
52
+ assert p.stat().st_size == 2 * 378
53
+ back = read_eld(p)
54
+ assert len(back) == 2
55
+ for a, b in zip(back, ds):
56
+ assert a.market == b.market and a.code == b.code
57
+ assert a.line_type == b.line_type
58
+ assert a.start_price == pytest.approx(b.start_price, abs=0.01)
59
+ assert a.end_price == pytest.approx(b.end_price, abs=0.01)
60
+ assert (a.start_date, a.end_date, a.line_id) == (b.start_date, b.end_date, b.line_id)
61
+
62
+
63
+ def test_validation():
64
+ with pytest.raises(ValueError):
65
+ Drawing("xx", "600519", "hline", 1, 1, date(2026, 1, 1), date(2026, 1, 1))
66
+ with pytest.raises(ValueError):
67
+ Drawing("sh", "60051", "hline", 1, 1, date(2026, 1, 1), date(2026, 1, 1))
68
+ with pytest.raises(ValueError):
69
+ Drawing("sh", "600519", "noline", 1, 1, date(2026, 1, 1), date(2026, 1, 1))