tags: 通达信 教程 量化 [TOC] # 通达信外部信号显示教程 > 环境:通达信 v7.72,Python 3.x,安装路径 `C:/new_tdx64/` --- ## 一、方法概览 将外部 Python 生成的 B/S 信号显示在通达信 K 线图上,主要有以下几种方式: | 方法 | 函数 | 原理 | 状态 | |------|------|------|------| | **① TQ 策略接口** | `SIGNALS_TQ(ID, TYPE)` | Python 通过 tqcenter 推送数据到通达信,公式读取 | ✅ 已验证可用 | | **② 自定义数据(序列)** | `SIGNALS_USER(ID, IDX)` | Python 写二进制 .dat 文件,通达信读取 | ⏳ 待验证(日线级可用,分钟级待确认) | | **③ 扩展数据** | `EXTDATA_USER(编号)` | 扩展数据管理器,每股一个快照值,无时间轴 | ⏳ 适合截面数据,不适合分时信号 | | **④ DLL 外部指标** | `TDXDLL1(nFuncMark,...)` | 编写 C DLL,通达信加载,DLL 读 Python 写的 CSV 文件 | ✅ 已验证可用,实时生效无需手动刷新 | --- ## 二、方法一:TQ 策略接口(已验证) ### 2.1 原理 ``` Python 脚本(外部) ↓ tqcenter.tq.send_bt_data() 通达信 TQ 策略管理器(内部运行) ↓ SIGNALS_TQ(ID, TYPE) K 线图公式显示 B/S 信号 ``` **关键点:** Python 脚本必须通过通达信的 **TQ策略管理器** 运行,不能直接在外部终端执行。 --- ### 2.2 前置条件 - 通达信支持 TQ 插件,`C:/new_tdx64/PYPlugins/user/tqcenter.py` (安装之后这个文件存在就符合要求) - 通达信处于**登录状态**(进入行情界面) - 顶部菜单栏有 **TQ策略** 菜单 --- ### 2.3 第一步:编写 Python 策略脚本 将脚本放到 `C:/new_tdx64/PYPlugins/user/` 目录下。 **示例:`send_001896_s_1007_tq.py`**(支持全周期显示) ```python from datetime import datetime, time, timedelta from tqcenter import tq STOCK_CODE = "001896.SZ" TRADE_DATE = "20260401" SIGNAL_TIME = datetime.strptime("20260401 10:07", "%Y%m%d %H:%M") def _build_bars(trade_date, step_minutes, sessions): d = datetime.strptime(trade_date, "%Y%m%d").date() out = [] for st, et in sessions: cur = datetime.combine(d, st) end_dt = datetime.combine(d, et) while cur <= end_dt: out.append(cur) cur += timedelta(minutes=step_minutes) return out def build_1m(td): return _build_bars(td, 1, [(time(9,31), time(11,30)), (time(13,1), time(15,0))]) def build_5m(td): return _build_bars(td, 5, [(time(9,35), time(11,30)), (time(13,5), time(15,0))]) def build_15m(td): return _build_bars(td, 15, [(time(9,45), time(11,30)), (time(13,15), time(15,0))]) def build_30m(td): return _build_bars(td, 30, [(time(10,0), time(11,30)), (time(14,0), time(15,0))]) def build_60m(td): d = datetime.strptime(td, "%Y%m%d").date() return [datetime.combine(d, t) for t in [time(10,30), time(11,30), time(14,0), time(15,0)]] def send_period(bars, signal_dt, label): """找到包含信号时间的 bar,发送该周期数据。""" signal_bar = next((b for b in bars if b >= signal_dt), None) if signal_bar is None: print(f"[{label}] no matching bar") return time_list = [b.strftime("%Y%m%d%H%M%S") for b in bars] data_list = [["0", "1" if b == signal_bar else "0"] for b in bars] res = tq.send_bt_data(stock_code=STOCK_CODE, time_list=time_list, data_list=data_list, count=len(time_list)) print(f"[{label}] signal_bar={signal_bar.strftime('%H:%M')} res={res}") def main(): tq.initialize(__file__) try: send_period(build_1m(TRADE_DATE), SIGNAL_TIME, "1m") send_period(build_5m(TRADE_DATE), SIGNAL_TIME, "5m") send_period(build_15m(TRADE_DATE), SIGNAL_TIME, "15m") send_period(build_30m(TRADE_DATE), SIGNAL_TIME, "30m") send_period(build_60m(TRADE_DATE), SIGNAL_TIME, "60m") # 日线 res = tq.send_bt_data(stock_code=STOCK_CODE, time_list=[TRADE_DATE], data_list=[["0","1"]], count=1) print(f"[daily] res={res}") finally: tq.close() if __name__ == "__main__": main() ``` **data_list 格式说明:** ``` time_list[i] = "YYYYMMDDHHMMSS" 每根 bar 的时间 data_list[i] = ["v1", "v2", ...] 该 bar 对应的多列数据(字符串) SIGNALS_TQ(1, 0) → 读取 data_list[i][0](第1列) SIGNALS_TQ(2, 0) → 读取 data_list[i][1](第2列) SIGNALS_TQ(N, 0) → 读取 data_list[i][N-1](第N列) ``` --- ### 2.4 第二步:在 TQ 策略管理器中注册并运行 1. 通达信菜单栏 → **TQ策略** → **TQ策略管理** 2. 点 **新建**,选择脚本文件,保存 3. 选中该策略,点 **运行** 4. 等待运行完成,输出窗口出现类似: ``` send_bt_data: {'ErrorId': '0', 'Msg': '发送TQ数据成功.', 'run_id': '1'} ``` --- ### 2.5 第三步:创建通达信公式 在通达信 **公式管理器** 中新建指标: - **名称:** `PY_BS_TQ` - **类型:** 主图叠加 ``` {只显示卖出信号} SELL_SIGNAL:=SIGNALS_TQ(2,0)>0; DRAWICON(SELL_SIGNAL,HIGH,2); DRAWTEXT(SELL_SIGNAL,HIGH,'S'),COLORRED; ``` 如果同时需要买入信号: ``` BUY_SIGNAL:=SIGNALS_TQ(1,0)>0; SELL_SIGNAL:=SIGNALS_TQ(2,0)>0; DRAWICON(BUY_SIGNAL,LOW,1); DRAWICON(SELL_SIGNAL,HIGH,2); DRAWTEXT(BUY_SIGNAL,LOW,'B'),COLORGREEN; DRAWTEXT(SELL_SIGNAL,HIGH,'S'),COLORRED; ``` **SIGNALS_TQ 参数说明:** | 参数 | 说明 | |------|------| | ID | 数据列序号,从 1 开始,对应 data_list 每行的第 ID 个元素 | | TYPE=0 | 不做平滑,无数据的 bar 返回 0 | | TYPE=1 | 平滑处理,无数据时返回上一根的值 | | TYPE=2 | 无数据时返回 0(与 0 类似) | --- ### 2.6 第四步:在 K 线图上显示,并触发刷新 1. 打开目标股票的 K 线图(任意周期) 2. 加载 `PY_BS_TQ` 指标(主图叠加) 3. 信号时间点对应的 K 线上会显示 B/S 文字和箭头图标 4. **1m / 5m / 15m / 30m / 60m / 日线均可显示**(脚本已为每个周期发送对应 bar 数据) > **分时走势图(F5)不支持 `SIGNALS_TQ`**,请在 **1 分钟 K 线图(F6)** 查看,1 分钟图的信号时刻与分时走势图完全对应。 --- ### ⚠️ 2.6.1 关键步骤:触发图表刷新(必做,否则信号不显示) **公式加载后,图表不会自动刷新,必须手动触发一次数据加载:** ``` TQ策略管理 → 策略管理器 → 策略数据 → 双击想要查看的股票 ``` 操作步骤: 1. 在 TQ 策略管理窗口,切换到 **策略管理器** 标签 2. 切换到 **策略数据** 子标签 3. 在股票列表中找到目标股票(如 `001896.SZ`),**双击**它 4. 此时通达信 K 线图才会刷新并显示信号 > **注意:** 每次重新运行脚本推送新信号后,都需要重复以上双击操作,图表才能更新。 --- ### 2.7 更新信号的流程 每次需要推送新信号时: 1. 修改脚本中的 `STOCK_CODE`、`TRADE_DATE`、`BUY_TIME`、`SELL_TIME` 等参数 2. 在 TQ 策略管理器中重新**运行**一次 3. **TQ策略管理 → 策略管理器 → 策略数据 → 双击目标股票**,触发图表刷新 --- ### 2.8 清空旧信号的方法 tqcenter 没有专用的 clear API。清空旧信号的标准做法是:**先发一遍全零数据覆盖,再发实际信号**。 ```python # 第一步:全零覆盖(清空) zero_data_list = [["0", "0"] for _ in bars] tq.send_bt_data(stock_code=STOCK_CODE, time_list=time_list, data_list=zero_data_list, count=len(time_list)) # 第二步:发实际 B/S 信号 tq.send_bt_data(stock_code=STOCK_CODE, time_list=time_list, data_list=actual_data_list, count=len(time_list)) ``` 发送完成后同样需要在 **策略数据 → 双击股票** 触发刷新。 --- ### 2.9 本次修复摘要(为什么分时和1分钟变正常) 这次联调里真正起作用的是以下 4 点: 1. **按周期分别发送,不混在一个时间轴里** - 正确做法:`1m / 5m / 15m` 分别构造各自 `bars`,分别调用 `send_bt_data`。 - 不要把多周期信号塞到同一个 `time_list` 里再靠公式 `PERIOD` 分流,否则容易出现“信号挤到开盘附近/错位”。 2. **每个周期先做“信号时刻 -> 该周期bar”映射** - 用规则:`signal_bar = first(bar_time >= signal_time)`。 - 例如 `13:06`: - 1m -> `13:06` - 5m -> `13:10` - 15m -> `13:15` 3. **公式保持简单:固定读取 ID1/ID2** - `BUY_SIGNAL:=SIGNALS_TQ(1,0)>0;` - `SELL_SIGNAL:=SIGNALS_TQ(2,0)>0;` - 由“每周期独立发送”保证各周期都取到正确 B/S,不再在公式里做复杂分支。 4. **脚本尾部增加短暂 sleep,避免误判异常退出** - 发送完成后很快退出是正常行为; - 增加 `sleep(3~5)` 让策略管理器显示更稳定; - 同时保留 `finally: tq.close()`,避免残留运行状态影响下次启动。 --- ## 三、待验证方法 ### 方法二:SIGNALS_USER(文件方式) **思路:** Python 写二进制 `.dat` 文件 → 通达信读取,无需 TQ 插件。 - 文件路径:`C:/new_tdx64/T0002/signals/signals_user_/#.dat` - 记录格式:`uint32 date_yyyymmdd + uint32 seconds + 22×float32 = 96 字节/条` - 需在 **自定义数据管理器** 中注册(属性选"序列(日期,数值)") - 已有脚本:`write_signals_user_bs.py` - **结论:分钟级数据无法正常读取**,日线级可用,不推荐用于分时信号 --- ## 三、方法三:DLL 外部指标(已验证)✅ ### 3.1 原理 ``` Python 写信号文件(CSV) ↓ DLL 实时读取文件,按 DATE/TIME 匹配每根 bar ↓ TDXDLL1(nFuncMark, DATE, TIME, 0) K 线图公式显示 B/S 信号(实时生效,无需手动刷新) ``` **优势:** 公式每次重算时 DLL 自动检测文件变动并重新加载,更新信号只需覆盖 CSV 文件,无需重启通达信。 --- ### 3.2 编译环境 本节所有内容在以下环境验证通过: | 组件 | 版本 | |------|------| | OS | Windows 10 (x64) | | 通达信 | v7.72,64位(安装目录含 `new_tdx64`) | | 编译器 | Visual Studio 2017 Community,MSVC 14.16.27023(v15.9.50) | | Windows SDK | 10.0.17763.0 | | Shell | Cygwin bash 3.4.8 | | Python | 3.9(仅用于写信号 CSV,不参与编译) | > **注意:** 编译器使用的是 **Visual Studio 2017 的 MSVC**,通过 `vcvars64.bat` 初始化 x64 编译环境。**不需要 MinGW**,也不需要 Cygwin 参与编译。 参考代码来源:通达信官方示例 https://help.tdx.com.cn/book.html(DLL函数编程规范,含 `TestPluginTCale` 示例工程) --- ### 3.3 官方 DLL 接口规范 来源:`TestPluginTCale/TCalcFuncSets.cpp`(通达信官方示例) ```c // 函数签名(注意:pfOUT 是第2个参数,不是最后一个) void MyFunc(int DataLen, float* pfOUT, float* pfINa, float* pfINb, float* pfINc); // 注册函数数组(以 {0, NULL} 结尾) PluginTCalcFuncInfo g_CalcFuncSets[] = { {1, (pPluginFUNC)&MyFunc1}, {2, (pPluginFUNC)&MyFunc2}, {0, NULL}, // 结尾标记,必须有 }; // 导出函数 BOOL RegisterTdxFunc(PluginTCalcFuncInfo** pFun) { if (*pFun == NULL) { *pFun = g_CalcFuncSets; return TRUE; } return FALSE; } ``` **⚠️ 常见错误:** 函数签名里 `pfOUT` 必须是第2个参数。写成最后一个参数会导致读到垃圾数据(`funcNo = -2147483648`)。 **TDXDLL1 参数映射:** ``` 公式:TDXDLL1(nFuncMark, Param1, Param2, Param3) ↓ DLL:MyFunc(DataLen, pfOUT, pfINa=Param1, pfINb=Param2, pfINc=Param3) ``` - `nFuncMark`:选择调用哪个注册函数(对应 `g_CalcFuncSets` 中的序号) - **最少 4 个参数**,否则通达信报"参数太少"错误,第4个可传 `0` 占位 **TDX 内置变量格式:** | 变量 | 格式 | 示例 | |------|------|------| | `DATE` | `yyyymmdd - 19000000` | 20260403 → 1260403 | | `TIME` | `HHMM`(4位整数) | 09:50 → 950,10:10 → 1010 | --- ### 3.4 信号文件格式 文件放在通达信安装目录下:`\T0002\signals\tdx_signal.csv` ``` # type,yyyymmdd,HHMM B,20260403,950 S,20260403,1010 ``` - `type`:`B`=买入,`S`=卖出 - `yyyymmdd`:完整日期(DLL 内部自动转换为 TDX DATE 格式) - `HHMM`:4位时间,与 TDX `TIME` 变量一致 **Python 写信号脚本:** `tdx_signal_dll/write_tdx_signal_csv.py` --- ### 3.5 DLL 源码 文件:`tdx_signal_dll/TdxSignal.cpp` 核心逻辑: - `RegisterTdxFunc`:注册两个函数,nFuncMark=1(买入),nFuncMark=2(卖出) - `CalcCore`:检测信号文件变动 → 重新加载 → 遍历每根 bar 匹配 DATE+TIME - 匹配命中则 `pfOUT[i] = 1.0f`,否则 `0.0f` - 调试日志输出到 `\T0002\signals\tdx_signal_debug.log` --- ### 3.6 编译脚本(已验证) 文件:`tdx_signal_dll/build.bat`,双击运行,自动寻找 VS2017 编译环境: ```bat @echo off setlocal REM 自动寻找 VS2017 vcvars64.bat set VCVARS= for /f "delims=" %%i in ('dir /b /s "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\*\VC\Auxiliary\Build\vcvars64.bat" 2^>nul') do ( set VCVARS=%%i goto :found_vc ) echo [ERROR] 未找到 VS2017,请确认已安装 Visual Studio 2017 pause & exit /b 1 :found_vc echo [INFO] 使用: %VCVARS% call "%VCVARS%" cd /d "%~dp0" cl.exe /nologo /O2 /EHsc /LD ^ /DWIN32 /D_WINDOWS /D_USRDLL /DPLUGIN_EXPORTS ^ TdxSignal.cpp ^ /link /DLL /OUT:TdxSignal.dll /MACHINE:X64 ^ kernel32.lib if %ERRORLEVEL% neq 0 ( echo [ERROR] 编译失败 pause & exit /b 1 ) echo [OK] 编译成功: TdxSignal.dll pause ``` 编译产物 `TdxSignal.dll` 手动复制到 `\T0002\dlls\` 目录下。 > **注意:** build.bat 必须在 **Windows 命令提示符(cmd)** 或 **文件管理器双击** 运行,不能直接在 Cygwin bash 里执行。从 Cygwin/Python 调用的方式见下方。 **从 Python 调用编译(适合在 Cygwin 环境下):** ```python import subprocess vcvars = r'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat' src_dir = r'<项目目录>\tdx_signal_dll' cmd = f'cmd /c "call "{vcvars}" && cd /d "{src_dir}" && cl.exe /nologo /O2 /EHsc /LD /DWIN32 /D_WINDOWS /D_USRDLL /DPLUGIN_EXPORTS TdxSignal.cpp /link /DLL /OUT:TdxSignal.dll /MACHINE:X64 kernel32.lib"' subprocess.run(cmd, shell=True) ``` --- ### 3.7 在通达信中注册 DLL 1. 公式管理器 → **DLL函数** → 新建 2. 选择文件 `\T0002\dlls\TdxSignal.dll` 3. 注册为**第1号函数**(对应公式里的 `TDXDLL1`) > **更新 DLL**:需先关闭通达信,替换文件,再重启。DLL 被通达信进程锁住时无法覆盖。 --- ### 3.8 通达信公式 公式名:`PY_BS_DLL`,类型:**主图叠加** ``` B:=TDXDLL1(1,DATE,TIME,0); S:=TDXDLL1(2,DATE,TIME,0); DIST:=ATR(14); {箭头:紧贴K线} DRAWICON(B,LOW-DIST*0.3,1); DRAWICON(S,HIGH+DIST*0.3,2); {文字:离箭头更远,用ATR动态偏移} DRAWTEXT(B,LOW-DIST*1.8,'▲ BUY'),COLORRED; DRAWTEXT(S,HIGH+DIST*1.8,'▼ SELL'),COLORGREEN; ``` **说明:** - `TDXDLL1(1,...)` → 调用 nFuncMark=1(买入信号) - `TDXDLL1(2,...)` → 调用 nFuncMark=2(卖出信号) - 第4个参数 `0` 为占位符(必须,否则报"参数太少") - `ATR(14)` 动态控制偏移距离,适应不同价格量级的股票 - `FONTSIZE` 通达信不支持,用全角字符/更长文字代替 - **1分钟和5分钟通用**:只要信号时刻恰好是5分钟bar整点(如09:50、10:10),同一公式两个周期均可显示 --- ### 3.9 更新信号流程 1. 修改 `write_tdx_signal_csv.py` 中的日期和时间 2. 运行脚本(外部终端即可,无需通达信内部运行) 3. 切换一下 K 线周期(触发公式重算),信号立即更新 **无需重启通达信,无需手动双击刷新。** --- ### 3.10 调试方法 DLL 会将每次加载和命中写入日志:`\T0002\signals\tdx_signal_debug.log` 日志内容示例: ``` [reload] loading tdx_signal.csv [reload] type=1 date=1260403 time=950 [reload] type=2 date=1260403 time=1010 [calc] sigType=1 DataLen=420 [calc] pfINa[0]=1260402.000000(date?) pfINb[0]=931.000000(time?) [calc] HIT type=1 date=1260403 time=950 bar=259 ``` 如果日志为空:DLL 未被加载(检查注册和文件路径)。 如果有 `[reload]` 但无 `[HIT]`:DATE 或 TIME 格式不匹配(对照日志中实际值)。 --- ### 3.11 编译常见问题 **Q: `build.bat` 在 Cygwin bash 里执行没有反应** - bat 文件必须在 Windows cmd 环境下运行,Cygwin 的 bash 不识别 `.bat` - 解决:在文件管理器双击 `build.bat`,或用 Python subprocess 调用 **Q: 编译报 `fatal error C1083: 无法打开包含文件 "windows.h"`** - `vcvars64.bat` 没有被正确调用,或 Windows SDK 未安装 - 确认 `vcvars64.bat` 路径正确,且该脚本 `call` 执行后 `INCLUDE` 环境变量已包含 SDK 路径 **Q: 编译成功但通达信加载 DLL 报错** - 确认编译为 **x64**(`/MACHINE:X64`),通达信 64 位版不能加载 32 位 DLL **Q: 替换 DLL 时提示"拒绝访问"** - 通达信运行时锁住 DLL,必须先关闭通达信再覆盖 **Q: 公式调用时 `pfOUT` 里读到 `-2147483648`(INT_MIN)** - 函数签名参数顺序写错了,`pfOUT` 必须是**第2个**参数: `void Func(int DataLen, float* pfOUT, float* pfINa, float* pfINb, float* pfINc)` **Q: 信号文件更新了但 DLL 没有重新加载** - DLL 通过 `GetFileAttributesEx` 检测文件 `LastWriteTime` 来判断是否重新加载 - 确认文件确实被写入(覆盖),而不仅仅是内容相同 --- ## 五、常见问题 **Q: 信号只在 1 分钟图显示,其他周期没有(TQ方式)** - 脚本只发送了 1 分钟数据,需要为每个周期分别发送对应 bar 的时间戳 - 使用上面的完整版脚本(含 `send_period` 函数),会自动处理 1m/5m/15m/30m/60m/日线 - 不要把 `1m/5m/15m` 同时塞到同一个 `time_list + 多ID` 里再用 `PERIOD` 分流,实测容易错位 **Q: TQ 公式加载后没有任何信号显示** - **首先检查:是否完成了"策略数据双击"步骤**(TQ策略管理 → 策略管理器 → 策略数据 → 双击目标股票),这是触发图表刷新的必要操作,极易遗漏 - 确认通达信处于登录状态 - 确认 TQ 策略已从策略管理器内**运行**(外部终端运行无效) - 确认公式类型为**主图叠加**,不是副图 - 确认在 **1 分钟 K 线图**(F6),不是分时走势图(F5) **Q: SIGNALS_TQ 返回值全是 0** - 重新从 TQ 策略管理器运行一次脚本 - 检查 data_list 的列序号是否与 SIGNALS_TQ 的 ID 参数对应 **Q: DLL 公式报"参数太少"错误** - `TDXDLL1` 至少需要 4 个参数,第4个传 `0` 占位:`TDXDLL1(1,DATE,TIME,0)` **Q: DLL 公式无报错但信号不显示** - 查看调试日志 `C:\new_tdx64\T0002\signals\tdx_signal_debug.log` - 日志为空 → DLL 未被加载,检查注册和文件路径 - 有 `[reload]` 无 `[HIT]` → DATE 或 TIME 格式不匹配,对照日志中实际值 - 常见原因:CSV 里 TIME 写成了 6 位 HHMMSS(如 95000),但 TDX 传入的是 4 位 HHMM(如 950) **Q: 替换 DLL 时提示权限拒绝** - 通达信运行时会锁住 DLL 文件,必须先关闭通达信再覆盖文件 **Q: TDXDLL1 公式里能否用 FONTSIZE 设置字号** - 通达信不支持 `FONTSIZE` 参数,用更长文字或全角字符(如 `▲ BUY`)代替 **Q: 如何同时为多只股票推送信号(TQ 方式)** - 多次调用 `send_bt_data`,每次传不同的 `stock_code` - 或在脚本中循环处理股票列表