tdxformula 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.
- tdxformula-0.1.0/PKG-INFO +114 -0
- tdxformula-0.1.0/README.md +97 -0
- tdxformula-0.1.0/pyproject.toml +25 -0
- tdxformula-0.1.0/setup.cfg +4 -0
- tdxformula-0.1.0/src/tdxformula/__init__.py +18 -0
- tdxformula-0.1.0/src/tdxformula/formula.py +330 -0
- tdxformula-0.1.0/src/tdxformula.egg-info/PKG-INFO +114 -0
- tdxformula-0.1.0/src/tdxformula.egg-info/SOURCES.txt +10 -0
- tdxformula-0.1.0/src/tdxformula.egg-info/dependency_links.txt +1 -0
- tdxformula-0.1.0/src/tdxformula.egg-info/requires.txt +1 -0
- tdxformula-0.1.0/src/tdxformula.egg-info/top_level.txt +1 -0
- tdxformula-0.1.0/tests/test_formula.py +104 -0
|
@@ -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,97 @@
|
|
|
1
|
+
<p align="center"><img src="assets/cover.png" alt="tdxformula" width="720"/></p>
|
|
2
|
+
|
|
3
|
+
# tdxformula — 通达信公式 Python 转译器
|
|
4
|
+
|
|
5
|
+
把**通达信技术指标公式**(DSL)解析并转译为 numpy 序列运算,语义经通达信
|
|
6
|
+
真实数据**逐根核对**(MACD 与 pandas 独立实现误差为 0;KDJ 由作者在通达信
|
|
7
|
+
客户端逐值核对一致)。
|
|
8
|
+
|
|
9
|
+
- 📖 **解析指标公式 DSL**:赋值线 / 中间变量(`:=`)/ 四则 / 比较 / `AND OR` / 注释
|
|
10
|
+
- 🧮 **序列语义对齐通达信**:`HHV/LLV` 前端增长窗口、`SMA(X,N,M)` 加权递推种子等
|
|
11
|
+
特殊行为全部按通达信真实行为实现(市面开源实现的重灾区)
|
|
12
|
+
- 🐍 **双模式**:直接求值返回 numpy 数组,或生成 Python 源码
|
|
13
|
+
- ✅ **可信度**:KDJ 在上证指数 2026-06-08 实测 K=17.98 / D=21.68 / J=10.59,
|
|
14
|
+
与通达信客户端显示完全一致(测试内置该断言)
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install tdxformula numpy
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 快速开始
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import numpy as np
|
|
26
|
+
from tdxdata import read_day
|
|
27
|
+
from tdxformula import eval_formula, gen_python
|
|
28
|
+
|
|
29
|
+
bars = read_day(r"D:\new_tdx64\vipdoc\sh\lday\sh000001.day")
|
|
30
|
+
env = {k.upper(): np.array([getattr(b, k) for b in bars], float)
|
|
31
|
+
for k in ("open", "high", "low", "close", "vol")}
|
|
32
|
+
|
|
33
|
+
macd = """
|
|
34
|
+
DIF:EMA(CLOSE,12)-EMA(CLOSE,26);
|
|
35
|
+
DEA:EMA(DIF,9);
|
|
36
|
+
MACD:(DIF-DEA)*2;
|
|
37
|
+
"""
|
|
38
|
+
r = eval_formula(macd, env)
|
|
39
|
+
print(r["DIF"][-1], r["DEA"][-1]) # numpy 序列,直接进你的回测框架
|
|
40
|
+
print(r["__outputs__"]) # ['DIF', 'DEA', 'MACD']
|
|
41
|
+
|
|
42
|
+
kdj = """
|
|
43
|
+
RSV:=(CLOSE-LLV(LOW,9))/(HHV(HIGH,9)-LLV(LOW,9))*100;
|
|
44
|
+
K:SMA(RSV,3,1);
|
|
45
|
+
D:SMA(K,3,1);
|
|
46
|
+
J:3*K-2*D;
|
|
47
|
+
"""
|
|
48
|
+
r2 = eval_formula(kdj, env)
|
|
49
|
+
|
|
50
|
+
print(gen_python(macd))
|
|
51
|
+
# DIF = (EMA(CLOSE, 12.0) - EMA(CLOSE, 26.0))
|
|
52
|
+
# DEA = EMA(DIF, 9.0)
|
|
53
|
+
# MACD = ((DIF - DEA) * 2.0)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 已支持的函数
|
|
57
|
+
|
|
58
|
+
| 类别 | 函数 |
|
|
59
|
+
|---|---|
|
|
60
|
+
| 均线 | `MA` `EMA` `SMA`(通达信加权版) |
|
|
61
|
+
| 引用 | `REF` `HHV` `LLV` `BARSLAST` |
|
|
62
|
+
| 统计 | `COUNT` `EVERY` `EXIST` `SUM` |
|
|
63
|
+
| 逻辑 | `CROSS` `AND/OR/&&/\|\|`、比较运算 |
|
|
64
|
+
| 算术 | `+ - * /` `ABS` `MAX` `MIN` |
|
|
65
|
+
|
|
66
|
+
已在路线图:`DRAW*` 系列转 [tdxdraw](https://gitee.com/mengyiheng/tdxdraw) 画线、
|
|
67
|
+
公式参数、跨公式引用(`"MACD.DEA"#DAY`)、更多函数。
|
|
68
|
+
|
|
69
|
+
## 通达信语义对齐说明(本项目存在的理由)
|
|
70
|
+
|
|
71
|
+
| 行为 | 通达信真实语义 | 本库实现 |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `HHV/LLV` 前 N-1 根 | 用现有数据增长窗口,照常出值 | ✅ |
|
|
74
|
+
| `SMA(X,N,M)` 种子 | 首个有效值,第一根即出值 | ✅ |
|
|
75
|
+
| `MA` 前 N-1 根 | 无值 | ✅ NaN |
|
|
76
|
+
| `EMA` 递推 | `(2X+(N-1)EMA')/(N+1)` | ✅ 与 pandas ewm(span) 误差 0 |
|
|
77
|
+
|
|
78
|
+
## 免责声明
|
|
79
|
+
|
|
80
|
+
本项目仅为公式文本的格式转换工具,不提供任何行情数据,不构成任何投资建议。
|
|
81
|
+
使用前请[阅读完整免责声明](DISCLAIMER.md)。
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
|
86
|
+
|
|
87
|
+
## 量化相关定制需求
|
|
88
|
+
|
|
89
|
+
作者承接公式迁移与转译器扩展的定制开发:
|
|
90
|
+
|
|
91
|
+
| 方向 | 示例 |
|
|
92
|
+
|---|---|
|
|
93
|
+
| 📦 公式批量迁移 | 你的私藏公式库整批转译 + 与通达信逐根比对验收 |
|
|
94
|
+
| 🧮 函数扩充 | 按需实现未覆盖的序列函数(对齐通达信语义) |
|
|
95
|
+
| 🔗 深度集成 | 选股公式转盘中扫描(接 tdxalert)、绘图指令转 tdxdraw |
|
|
96
|
+
|
|
97
|
+
**联系合作**:[Gitee Issues](https://gitee.com/mengyiheng/tdxformula/issues) 或 [Gitee 私信](https://gitee.com/mengyiheng)。迁移类需求请附:公式数量、是否含绘图指令。
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tdxformula"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "通达信指标公式 Python 转译器:逐根对齐通达信真实计算结果"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "mengyiheng" }]
|
|
13
|
+
dependencies = ["numpy"]
|
|
14
|
+
keywords = ["tdx", "tongdaxin", "formula", "indicator", "quant", "dsl"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["src"]
|
|
@@ -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__"]
|
|
@@ -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,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/tdxformula/__init__.py
|
|
4
|
+
src/tdxformula/formula.py
|
|
5
|
+
src/tdxformula.egg-info/PKG-INFO
|
|
6
|
+
src/tdxformula.egg-info/SOURCES.txt
|
|
7
|
+
src/tdxformula.egg-info/dependency_links.txt
|
|
8
|
+
src/tdxformula.egg-info/requires.txt
|
|
9
|
+
src/tdxformula.egg-info/top_level.txt
|
|
10
|
+
tests/test_formula.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tdxformula
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from datetime import date
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
10
|
+
from tdxformula import eval_formula, gen_python # noqa: E402
|
|
11
|
+
|
|
12
|
+
# ---------- 合成数据 ----------
|
|
13
|
+
|
|
14
|
+
N = 120
|
|
15
|
+
rng = np.random.default_rng(7)
|
|
16
|
+
CLOSE = 100 + np.cumsum(rng.normal(0, 1, N))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _env():
|
|
20
|
+
return {
|
|
21
|
+
"CLOSE": CLOSE.copy(),
|
|
22
|
+
"OPEN": CLOSE - 1, "HIGH": CLOSE + 2, "LOW": CLOSE - 2,
|
|
23
|
+
"VOL": np.abs(rng.normal(1000, 100, N)), "__len__": N,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------- MACD:与 pandas 独立实现交叉验证 ----------
|
|
28
|
+
|
|
29
|
+
def test_macd_matches_pandas_ewm():
|
|
30
|
+
r = eval_formula("DIF:EMA(CLOSE,12)-EMA(CLOSE,26);DEA:EMA(DIF,9);", _env())
|
|
31
|
+
close = pd.Series(CLOSE)
|
|
32
|
+
dif_ref = (close.ewm(span=12, adjust=False).mean()
|
|
33
|
+
- close.ewm(span=26, adjust=False).mean()).values
|
|
34
|
+
assert np.nanmax(np.abs(r["DIF"] - dif_ref)) < 1e-8
|
|
35
|
+
assert r["__outputs__"] == ["DIF", "DEA"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_macd_real_data_zero_error():
|
|
39
|
+
"""用户在通达信核对过的真实场景:DIF 与 pandas ewm 误差为 0。"""
|
|
40
|
+
p = Path(r"D:\new_tdx64\vipdoc\sh\lday\sh000001.day")
|
|
41
|
+
if not p.exists():
|
|
42
|
+
pytest.skip("本机无通达信日线数据")
|
|
43
|
+
from tdxdata import read_day
|
|
44
|
+
bars = read_day(p)
|
|
45
|
+
close = np.array([b.close for b in bars], float)
|
|
46
|
+
r = eval_formula("DIF:EMA(CLOSE,12)-EMA(CLOSE,26);", {"CLOSE": close, "__len__": len(close)})
|
|
47
|
+
s = pd.Series(close)
|
|
48
|
+
ref = (s.ewm(span=12, adjust=False).mean() - s.ewm(span=26, adjust=False).mean()).values
|
|
49
|
+
assert np.nanmax(np.abs(r["DIF"] - ref)) == 0.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------- KDJ:用户已在通达信逐根核对 ----------
|
|
53
|
+
|
|
54
|
+
def test_kdj_values_verified_against_tdx():
|
|
55
|
+
"""2026-06-08 上证指数:K=17.98 D=21.68 J=10.59,用户在通达信核对一致。"""
|
|
56
|
+
p = Path(r"D:\new_tdx64\vipdoc\sh\lday\sh000001.day")
|
|
57
|
+
if not p.exists():
|
|
58
|
+
pytest.skip("本机无通达信日线数据")
|
|
59
|
+
from tdxdata import read_day
|
|
60
|
+
bars = read_day(p)
|
|
61
|
+
env = {k.upper(): np.array([getattr(b, k) for b in bars], float)
|
|
62
|
+
for k in ("open", "high", "low", "close", "vol")}
|
|
63
|
+
r = eval_formula("""
|
|
64
|
+
RSV:=(CLOSE-LLV(LOW,9))/(HHV(HIGH,9)-LLV(LOW,9))*100;
|
|
65
|
+
K:SMA(RSV,3,1);
|
|
66
|
+
D:SMA(K,3,1);
|
|
67
|
+
J:3*K-2*D;
|
|
68
|
+
""", env)
|
|
69
|
+
assert round(r["K"][-1], 2) == 17.98
|
|
70
|
+
assert round(r["D"][-1], 2) == 21.68
|
|
71
|
+
assert round(r["J"][-1], 2) == 10.59
|
|
72
|
+
assert bars[-1].date == 20260608
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------- 语义细节 ----------
|
|
76
|
+
|
|
77
|
+
def test_hhv_llv_growing_window_at_head():
|
|
78
|
+
"""通达信 HHV/LLV 前几根用增长窗口(不满 N 根也出值)。"""
|
|
79
|
+
r = eval_formula("H:HHV(CLOSE,5);", {"CLOSE": CLOSE, "__len__": N})
|
|
80
|
+
assert not np.isnan(r["H"][0])
|
|
81
|
+
assert r["H"][0] == CLOSE[0]
|
|
82
|
+
assert r["H"][4] == pytest.approx(max(CLOSE[:5]))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_hidden_variable_not_in_outputs():
|
|
86
|
+
r = eval_formula("A:=CLOSE+1;B:CLOSE*2;", _env())
|
|
87
|
+
assert r["__outputs__"] == ["B"]
|
|
88
|
+
assert np.allclose(r["A"], CLOSE + 1)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_comparison_and_logic():
|
|
92
|
+
r = eval_formula("SIG:CROSS(CLOSE,REF(CLOSE,1)) AND CLOSE>0;", _env())
|
|
93
|
+
sig = r["SIG"]
|
|
94
|
+
assert sig.dtype == bool
|
|
95
|
+
assert not sig[0] # REF(1) 首位为 NaN,不触发
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_gen_python_and_error_cases():
|
|
99
|
+
src = "DIF:EMA(CLOSE,12)-EMA(CLOSE,26);"
|
|
100
|
+
assert "EMA(CLOSE, 12.0)" in gen_python(src)
|
|
101
|
+
with pytest.raises(ValueError):
|
|
102
|
+
eval_formula("X:NOTEXIST(CLOSE,5);", _env())
|
|
103
|
+
with pytest.raises(ValueError):
|
|
104
|
+
eval_formula("X:FOO(CLOSE);", _env())
|