通达信外部信号显示教程

通达信外部信号显示教程

环境:通达信 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(支持全周期显示)

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_CODETRADE_DATEBUY_TIMESELL_TIME 等参数
  2. 在 TQ 策略管理器中重新运行一次
  3. TQ策略管理 → 策略管理器 → 策略数据 → 双击目标股票,触发图表刷新

2.8 清空旧信号的方法

tqcenter 没有专用的 clear API。清空旧信号的标准做法是:先发一遍全零数据覆盖,再发实际信号

# 第一步:全零覆盖(清空)
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. 按周期分别发送,不混在一个时间轴里
  2. 正确做法:1m / 5m / 15m 分别构造各自 bars,分别调用 send_bt_data
  3. 不要把多周期信号塞到同一个 time_list 里再靠公式 PERIOD 分流,否则容易出现“信号挤到开盘附近/错位”。

  4. 每个周期先做“信号时刻 -> 该周期bar”映射

  5. 用规则:signal_bar = first(bar_time >= signal_time)
  6. 例如 13:06

    • 1m -> 13:06
    • 5m -> 13:10
    • 15m -> 13:15
  7. 公式保持简单:固定读取 ID1/ID2

  8. BUY_SIGNAL:=SIGNALS_TQ(1,0)>0;
  9. SELL_SIGNAL:=SIGNALS_TQ(2,0)>0;
  10. 由“每周期独立发送”保证各周期都取到正确 B/S,不再在公式里做复杂分支。

  11. 脚本尾部增加短暂 sleep,避免误判异常退出

  12. 发送完成后很快退出是正常行为;
  13. 增加 sleep(3~5) 让策略管理器显示更稳定;
  14. 同时保留 finally: tq.close(),避免残留运行状态影响下次启动。

三、待验证方法

方法二:SIGNALS_USER(文件方式)

思路: Python 写二进制 .dat 文件 → 通达信读取,无需 TQ 插件。

  • 文件路径:C:/new_tdx64/T0002/signals/signals_user_<ID>/<market>#<code>.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(通达信官方示例)

// 函数签名(注意: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 信号文件格式

文件放在通达信安装目录下:<TDX安装目录>\T0002\signals\tdx_signal.csv

# type,yyyymmdd,HHMM
B,20260403,950
S,20260403,1010
  • typeB=买入,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 - 调试日志输出到 <TDX安装目录>\T0002\signals\tdx_signal_debug.log


3.6 编译脚本(已验证)

文件:tdx_signal_dll/build.bat,双击运行,自动寻找 VS2017 编译环境:

@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 手动复制到 <TDX安装目录>\T0002\dlls\ 目录下。

注意: build.bat 必须在 Windows 命令提示符(cmd)文件管理器双击 运行,不能直接在 Cygwin bash 里执行。从 Cygwin/Python 调用的方式见下方。

从 Python 调用编译(适合在 Cygwin 环境下):

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. 选择文件 <TDX安装目录>\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 会将每次加载和命中写入日志:<TDX安装目录>\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 - 或在脚本中循环处理股票列表

build:   20260403