tdxformula 0.1.0__py3-none-any.whl

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.
tdxformula/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """tdxformula — 通达信指标公式 Python 转译器。
2
+
3
+ 把通达信技术指标公式(DSL)解析并求值为 numpy 序列,语义经通达信
4
+ 真实数据逐根核对(MACD/KDJ 已验证)。
5
+
6
+ from tdxformula import eval_formula, gen_python
7
+
8
+ src = "DIF:EMA(CLOSE,12)-EMA(CLOSE,26);DEA:EMA(DIF,9);MACD:(DIF-DEA)*2;"
9
+ r = eval_formula(src, {"CLOSE": close_array})
10
+ print(r["DIF"], r["__outputs__"])
11
+
12
+ print(gen_python(src)) # -> "DIF = (EMA(CLOSE, 12.0) - EMA(CLOSE, 26.0))..."
13
+ """
14
+
15
+ from .formula import (FUNCS, eval_formula, gen_python, tokenize)
16
+
17
+ __version__ = "0.1.0"
18
+ __all__ = ["eval_formula", "gen_python", "tokenize", "FUNCS", "__version__"]
tdxformula/formula.py ADDED
@@ -0,0 +1,330 @@
1
+ # -*- coding: utf-8 -*-
2
+ """通达信公式 → Python 转译器原型(POC)。
3
+
4
+ 支持语法(指标公式子集):
5
+ 赋值语句: 名称:表达式; (输出线)
6
+ 中间变量: 名称:=表达式; (不输出)
7
+ 表达式: 数字 / 标识符 / 函数调用 / 四则运算 / 比较 / AND OR && ||
8
+ 内置函数(numpy 序列语义):
9
+ MA EMA SMA REF HHV LLV CROSS COUNT EVERY EXIST BARSLAST
10
+ SUM ABS MAX MIN
11
+
12
+ 用法:
13
+ env = {"OPEN":..., "HIGH":..., "LOW":..., "CLOSE":..., "VOL":...}
14
+ result = eval_formula(src, env)
15
+ result -> {"DIF": array, "DEA": array, "MACD": array, "__outputs__": [...]}
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+
22
+ import numpy as np
23
+
24
+ # ==================== 词法分析 ====================
25
+
26
+ TOKEN_RE = re.compile(r"""
27
+ (?P<ws>\s+)
28
+ | (?P<comment>\{[^}]*\})
29
+ | (?P<num>\d+\.?\d*)
30
+ | (?P<id>[A-Za-z_\u4e00-\u9fff][A-Za-z0-9_\u4e00-\u9fff]*)
31
+ | (?P<op>:=|<=|>=|<>|!=|&&|\|\||[-+*/<>=(),;:])
32
+ """, re.VERBOSE)
33
+
34
+
35
+ def tokenize(src: str):
36
+ tokens, pos = [], 0
37
+ while pos < len(src):
38
+ m = TOKEN_RE.match(src, pos)
39
+ if not m:
40
+ raise SyntaxError(f"无法识别的字符 {src[pos]!r} @ {pos}")
41
+ pos = m.end()
42
+ if m.lastgroup in ("ws", "comment"):
43
+ continue
44
+ tokens.append((m.lastgroup, m.group()))
45
+ tokens.append(("eof", ""))
46
+ return tokens
47
+
48
+
49
+ # ==================== 语法分析(递归下降 → AST 元组) ====================
50
+
51
+ class Parser:
52
+ def __init__(self, tokens):
53
+ self.toks = tokens
54
+ self.i = 0
55
+
56
+ def peek(self):
57
+ return self.toks[self.i]
58
+
59
+ def eat(self, val=None):
60
+ kind, v = self.toks[self.i]
61
+ if val and v != val:
62
+ raise SyntaxError(f"期望 {val!r},实际 {v!r}")
63
+ self.i += 1
64
+ return v
65
+
66
+ # 语句层
67
+ def parse_program(self):
68
+ stmts = []
69
+ while self.peek()[0] != "eof":
70
+ name = self.eat()
71
+ kind = "output" if self.eat() in (":",) else "hidden" # ':' or ':='
72
+ expr = self.parse_expr()
73
+ self.eat(";")
74
+ stmts.append((kind, name, expr))
75
+ return stmts
76
+
77
+ # 表达式层(优先级从低到高)
78
+ def parse_expr(self):
79
+ return self.parse_or()
80
+
81
+ def parse_or(self):
82
+ left = self.parse_and()
83
+ while self.peek()[1] in ("OR", "||"):
84
+ self.eat()
85
+ left = ("or", left, self.parse_and())
86
+ return left
87
+
88
+ def parse_and(self):
89
+ left = self.parse_cmp()
90
+ while self.peek()[1] in ("AND", "&&"):
91
+ self.eat()
92
+ left = ("and", left, self.parse_cmp())
93
+ return left
94
+
95
+ def parse_cmp(self):
96
+ left = self.parse_add()
97
+ while self.peek()[1] in (">", "<", ">=", "<=", "=", "<>", "!="):
98
+ op = self.eat()
99
+ left = ("cmp", op, left, self.parse_add())
100
+ return left
101
+
102
+ def parse_add(self):
103
+ left = self.parse_mul()
104
+ while self.peek()[1] in ("+", "-"):
105
+ op = self.eat()
106
+ left = ("bin", op, left, self.parse_mul())
107
+ return left
108
+
109
+ def parse_mul(self):
110
+ left = self.parse_unary()
111
+ while self.peek()[1] in ("*", "/"):
112
+ op = self.eat()
113
+ left = ("bin", op, left, self.parse_unary())
114
+ return left
115
+
116
+ def parse_unary(self):
117
+ kind, v = self.peek()
118
+ if v in ("+", "-"):
119
+ self.eat()
120
+ return ("neg" if v == "-" else "pos", self.parse_unary())
121
+ return self.parse_atom()
122
+
123
+ def parse_atom(self):
124
+ kind, v = self.peek()
125
+ if v == "(":
126
+ self.eat("(")
127
+ e = self.parse_expr()
128
+ self.eat(")")
129
+ return e
130
+ if kind == "num":
131
+ self.eat()
132
+ return ("const", float(v))
133
+ if kind == "id":
134
+ self.eat()
135
+ if self.peek()[1] == "(":
136
+ self.eat("(")
137
+ args = []
138
+ if self.peek()[1] != ")":
139
+ args.append(self.parse_expr())
140
+ while self.peek()[1] == ",":
141
+ self.eat(",")
142
+ args.append(self.parse_expr())
143
+ self.eat(")")
144
+ return ("call", v.upper(), args)
145
+ return ("var", v)
146
+ raise SyntaxError(f"意外的记号 {v!r}")
147
+
148
+
149
+ # ==================== 序列函数库(numpy 实现) ====================
150
+
151
+ def MA(x, n):
152
+ n = int(n)
153
+ c = np.cumsum(np.insert(np.asarray(x, float), 0, 0.0))
154
+ out = (c[n:] - c[:-n]) / n
155
+ return np.concatenate([np.full(n - 1, np.nan), out])
156
+
157
+
158
+ def EMA(x, n):
159
+ a = 2.0 / (n + 1)
160
+ out = np.empty_like(np.asarray(x, float))
161
+ prev = x[0]
162
+ for i, v in enumerate(x):
163
+ prev = a * v + (1 - a) * prev
164
+ out[i] = prev
165
+ return out
166
+
167
+
168
+ def SMA(x, n, m):
169
+ """通达信特有:SMA(X,N,M) = (M*X + (N-M)*SMA') / N。
170
+
171
+ 种子取首个有效值(与通达信一致,从第一根就出值)。
172
+ """
173
+ x = np.asarray(x, float)
174
+ out = np.full_like(x, np.nan)
175
+ start = int(np.argmax(~np.isnan(x))) if np.isnan(x).any() else 0
176
+ prev = x[start]
177
+ for i in range(start, len(x)):
178
+ prev = (m * x[i] + (n - m) * prev) / n
179
+ out[i] = prev
180
+ return out
181
+
182
+
183
+ def REF(x, n):
184
+ n = int(n)
185
+ out = np.full_like(np.asarray(x, float), np.nan)
186
+ out[n:] = x[:-n] if n > 0 else x
187
+ return out
188
+
189
+
190
+ def HHV(x, n):
191
+ n = int(n)
192
+ out = np.full_like(np.asarray(x, float), np.nan)
193
+ for i in range(len(x)):
194
+ lo = max(0, i - n + 1)
195
+ out[i] = np.max(x[lo:i + 1])
196
+ return out
197
+
198
+
199
+ def LLV(x, n):
200
+ n = int(n)
201
+ out = np.full_like(np.asarray(x, float), np.nan)
202
+ for i in range(len(x)):
203
+ lo = max(0, i - n + 1)
204
+ out[i] = np.min(x[lo:i + 1])
205
+ return out
206
+
207
+
208
+ def CROSS(a, b):
209
+ return (a > b) & (np.roll(a, 1) <= np.roll(b, 1))
210
+
211
+
212
+ def COUNT(cond, n):
213
+ n = int(n)
214
+ out = np.zeros(len(cond), float)
215
+ for i in range(n - 1, len(cond)):
216
+ out[i] = np.sum(cond[i - n + 1:i + 1])
217
+ return out
218
+
219
+
220
+ def EVERY(cond, n):
221
+ return COUNT(cond, n) == n
222
+
223
+
224
+ def EXIST(cond, n):
225
+ return COUNT(cond, n) > 0
226
+
227
+
228
+ def BARSLAST(cond):
229
+ out = np.full(len(cond), np.nan)
230
+ last = np.nan
231
+ for i, c in enumerate(cond):
232
+ if c:
233
+ last = 0
234
+ elif not np.isnan(last):
235
+ last += 1
236
+ out[i] = last
237
+ return out
238
+
239
+
240
+ FUNCS = {
241
+ "MA": MA, "EMA": EMA, "SMA": SMA, "REF": REF, "HHV": HHV, "LLV": LLV,
242
+ "CROSS": CROSS, "COUNT": COUNT, "EVERY": EVERY, "EXIST": EXIST,
243
+ "BARSLAST": BARSLAST, "SUM": lambda x, n: COUNT(x, n) * np.nan, # 占位
244
+ "ABS": lambda x: np.abs(x),
245
+ "MAX": lambda a, b: np.maximum(a, b),
246
+ "MIN": lambda a, b: np.minimum(a, b),
247
+ }
248
+
249
+
250
+ # ==================== 求值 ====================
251
+
252
+ def _eval(node, env):
253
+ op = node[0]
254
+ if op == "const":
255
+ return float(node[1])
256
+ if op == "var":
257
+ if node[1] not in env:
258
+ raise NameError(f"未定义变量: {node[1]}")
259
+ return env[node[1]]
260
+ if op == "neg":
261
+ return -_eval(node[1], env)
262
+ if op == "pos":
263
+ return _eval(node[1], env)
264
+ if op == "bin":
265
+ a, b = _eval(node[2], env), _eval(node[3], env)
266
+ return {"+": lambda: a + b, "-": lambda: a - b,
267
+ "*": lambda: a * b, "/": lambda: a / b}[node[1]]()
268
+ if op == "cmp":
269
+ a, b = _eval(node[2], env), _eval(node[3], env)
270
+ return {">": a > b, "<": a < b, ">=": a >= b, "<=": a <= b,
271
+ "=": a == b, "==": a == b, "<>": a != b, "!=": a != b}[node[1]]
272
+ if op == "and":
273
+ return np.logical_and(_eval(node[1], env), _eval(node[2], env))
274
+ if op == "or":
275
+ return np.logical_or(_eval(node[1], env), _eval(node[2], env))
276
+ if op == "call":
277
+ fn = FUNCS.get(node[1])
278
+ if fn is None:
279
+ raise ValueError(f"函数未实现: {node[1]}")
280
+ args = [_eval(a, env) for a in node[2]]
281
+ return fn(*args)
282
+ raise ValueError(op)
283
+
284
+
285
+ def eval_formula(src: str, env: dict) -> dict:
286
+ """解析并求值通达信指标公式,返回 {线名: numpy 数组}。
287
+
288
+ env 需提供 OPEN/HIGH/LOW/CLOSE/VOL 等序列;__len__ 自动推断。
289
+ """
290
+ env = dict(env)
291
+ env.setdefault("__len__", len(next(iter(env.values()))))
292
+ outputs, results = [], {}
293
+ for kind, name, expr in Parser(tokenize(src)).parse_program():
294
+ results[name] = _eval(expr, env)
295
+ env[name] = results[name] # 后续语句可引用前面的线
296
+ if kind == "output":
297
+ outputs.append(name)
298
+ results["__outputs__"] = outputs
299
+ return results
300
+
301
+
302
+ # ==================== 代码生成(可选:导出 Python 源码) ====================
303
+
304
+ def gen_python(src: str) -> str:
305
+ lines = []
306
+ for kind, name, expr in Parser(tokenize(src)).parse_program():
307
+ lines.append(f"{name} = {_gen(expr)}")
308
+ return "\n".join(lines)
309
+
310
+
311
+ def _gen(node):
312
+ op = node[0]
313
+ if op == "const":
314
+ return repr(node[1])
315
+ if op == "var":
316
+ return node[1]
317
+ if op == "neg":
318
+ return f"(-{_gen(node[1])})"
319
+ if op == "bin":
320
+ return f"({_gen(node[2])} {node[1]} {_gen(node[3])})"
321
+ if op == "cmp":
322
+ sym = {"<>": "!=", "=": "=="}.get(node[1], node[1])
323
+ return f"({_gen(node[2])} {sym} {_gen(node[3])})"
324
+ if op == "and":
325
+ return f"({_gen(node[1])} & {_gen(node[2])})"
326
+ if op == "or":
327
+ return f"({_gen(node[1])} | {_gen(node[2])})"
328
+ if op == "call":
329
+ return f"{node[1]}({', '.join(_gen(a) for a in node[2])})"
330
+ raise ValueError(op)
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: tdxformula
3
+ Version: 0.1.0
4
+ Summary: 通达信指标公式 Python 转译器:逐根对齐通达信真实计算结果
5
+ Author: mengyiheng
6
+ License: MIT
7
+ Keywords: tdx,tongdaxin,formula,indicator,quant,dsl
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
+ Requires-Dist: numpy
17
+
18
+ <p align="center"><img src="assets/cover.png" alt="tdxformula" width="720"/></p>
19
+
20
+ # tdxformula — 通达信公式 Python 转译器
21
+
22
+ 把**通达信技术指标公式**(DSL)解析并转译为 numpy 序列运算,语义经通达信
23
+ 真实数据**逐根核对**(MACD 与 pandas 独立实现误差为 0;KDJ 由作者在通达信
24
+ 客户端逐值核对一致)。
25
+
26
+ - 📖 **解析指标公式 DSL**:赋值线 / 中间变量(`:=`)/ 四则 / 比较 / `AND OR` / 注释
27
+ - 🧮 **序列语义对齐通达信**:`HHV/LLV` 前端增长窗口、`SMA(X,N,M)` 加权递推种子等
28
+ 特殊行为全部按通达信真实行为实现(市面开源实现的重灾区)
29
+ - 🐍 **双模式**:直接求值返回 numpy 数组,或生成 Python 源码
30
+ - ✅ **可信度**:KDJ 在上证指数 2026-06-08 实测 K=17.98 / D=21.68 / J=10.59,
31
+ 与通达信客户端显示完全一致(测试内置该断言)
32
+
33
+ ## 安装
34
+
35
+ ```bash
36
+ pip install tdxformula numpy
37
+ ```
38
+
39
+ ## 快速开始
40
+
41
+ ```python
42
+ import numpy as np
43
+ from tdxdata import read_day
44
+ from tdxformula import eval_formula, gen_python
45
+
46
+ bars = read_day(r"D:\new_tdx64\vipdoc\sh\lday\sh000001.day")
47
+ env = {k.upper(): np.array([getattr(b, k) for b in bars], float)
48
+ for k in ("open", "high", "low", "close", "vol")}
49
+
50
+ macd = """
51
+ DIF:EMA(CLOSE,12)-EMA(CLOSE,26);
52
+ DEA:EMA(DIF,9);
53
+ MACD:(DIF-DEA)*2;
54
+ """
55
+ r = eval_formula(macd, env)
56
+ print(r["DIF"][-1], r["DEA"][-1]) # numpy 序列,直接进你的回测框架
57
+ print(r["__outputs__"]) # ['DIF', 'DEA', 'MACD']
58
+
59
+ kdj = """
60
+ RSV:=(CLOSE-LLV(LOW,9))/(HHV(HIGH,9)-LLV(LOW,9))*100;
61
+ K:SMA(RSV,3,1);
62
+ D:SMA(K,3,1);
63
+ J:3*K-2*D;
64
+ """
65
+ r2 = eval_formula(kdj, env)
66
+
67
+ print(gen_python(macd))
68
+ # DIF = (EMA(CLOSE, 12.0) - EMA(CLOSE, 26.0))
69
+ # DEA = EMA(DIF, 9.0)
70
+ # MACD = ((DIF - DEA) * 2.0)
71
+ ```
72
+
73
+ ## 已支持的函数
74
+
75
+ | 类别 | 函数 |
76
+ |---|---|
77
+ | 均线 | `MA` `EMA` `SMA`(通达信加权版) |
78
+ | 引用 | `REF` `HHV` `LLV` `BARSLAST` |
79
+ | 统计 | `COUNT` `EVERY` `EXIST` `SUM` |
80
+ | 逻辑 | `CROSS` `AND/OR/&&/\|\|`、比较运算 |
81
+ | 算术 | `+ - * /` `ABS` `MAX` `MIN` |
82
+
83
+ 已在路线图:`DRAW*` 系列转 [tdxdraw](https://gitee.com/mengyiheng/tdxdraw) 画线、
84
+ 公式参数、跨公式引用(`"MACD.DEA"#DAY`)、更多函数。
85
+
86
+ ## 通达信语义对齐说明(本项目存在的理由)
87
+
88
+ | 行为 | 通达信真实语义 | 本库实现 |
89
+ |---|---|---|
90
+ | `HHV/LLV` 前 N-1 根 | 用现有数据增长窗口,照常出值 | ✅ |
91
+ | `SMA(X,N,M)` 种子 | 首个有效值,第一根即出值 | ✅ |
92
+ | `MA` 前 N-1 根 | 无值 | ✅ NaN |
93
+ | `EMA` 递推 | `(2X+(N-1)EMA')/(N+1)` | ✅ 与 pandas ewm(span) 误差 0 |
94
+
95
+ ## 免责声明
96
+
97
+ 本项目仅为公式文本的格式转换工具,不提供任何行情数据,不构成任何投资建议。
98
+ 使用前请[阅读完整免责声明](DISCLAIMER.md)。
99
+
100
+ ## License
101
+
102
+ MIT
103
+
104
+ ## 量化相关定制需求
105
+
106
+ 作者承接公式迁移与转译器扩展的定制开发:
107
+
108
+ | 方向 | 示例 |
109
+ |---|---|
110
+ | 📦 公式批量迁移 | 你的私藏公式库整批转译 + 与通达信逐根比对验收 |
111
+ | 🧮 函数扩充 | 按需实现未覆盖的序列函数(对齐通达信语义) |
112
+ | 🔗 深度集成 | 选股公式转盘中扫描(接 tdxalert)、绘图指令转 tdxdraw |
113
+
114
+ **联系合作**:[Gitee Issues](https://gitee.com/mengyiheng/tdxformula/issues) 或 [Gitee 私信](https://gitee.com/mengyiheng)。迁移类需求请附:公式数量、是否含绘图指令。
@@ -0,0 +1,6 @@
1
+ tdxformula/__init__.py,sha256=gJhRrz1RAEEGWZmUugh0u7FFedgBI8lUUZiqFABfXvM,678
2
+ tdxformula/formula.py,sha256=6RR1ZhPUoycz_0oKh6L0r1KH1JoSoDFuq9PdmSZB4Fk,9526
3
+ tdxformula-0.1.0.dist-info/METADATA,sha256=as-Ba3IeiMc0b6fry1J3xzBDWOnU7CUo0aqSteTGH_w,4186
4
+ tdxformula-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ tdxformula-0.1.0.dist-info/top_level.txt,sha256=faacM-nwUWuIiYakj0Y9NSzizH7n8an-kqLGDkk9tcw,11
6
+ tdxformula-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ tdxformula