
作者:码上工坊版本:0.1.0日期:2026年1月许可证:MIT License项目地址1:https://gitee.com/icodewr/tdxpy_formulas项目地址2:https://github.com/ICodeWR/tdxpy_formulas
做开源项目,以开源的方式学习编程,是最高效、最具成长性的编程学习方法;为自己写软件、为自己的真实需求写软件,既是一种扎根务实的编程理念,更是一种向内求索、自我赋能的技术格局。
学习编程的目标是解决实际问题——tdxpy_formulas项目的设计目标,就是让我们在掌握编程技能的同时,直接获得一个能投入使用的实用工具。让代码为我们赋能,这也是本号的一个宗旨。
tdxpy_formulas项目完美践行"用中学、边学边用"的理念——以"用Python编写通达信指标公式"这一具体需求为牵引,将C++、Python、跨语言通信、插件开发等知识点融入实际场景。对于编程学习者而言,它既是可直接落地的实用工具,更是一套完整的"问题解决式学习"案例集。
项目的学习特性:
知识点不孤立:学习C++不是死记语法,而是为了实现插件注册与高效数据传输;掌握Python不是单纯写脚本,而是为了开发灵活的量化指标;理解JSON配置不是背格式,而是为了实现公式的动态映射。
反馈闭环快:每完成一个模块开发,就能在通达信中看到实际效果(比如自己写的MA指标在K线图上显示),这种"即时反馈"能极大提升学习动力。
难度梯度合理:从简单的公式编写(Python),到核心的跨语言通信(C++),再到复杂的性能优化与功能扩展,难度逐步提升,符合循序渐进的学习规律。
学开源、做开源:通过做开源软件,学习编程,边做边学、边学边做,学以致用,用中学。
从"学会"到"能用" :从"看懂代码"到"会改代码";从"跑通示例"到"落地场景";从"被动使用"到"主动定制"。
编程学习终极目标:不为学而学,为用而学;不为他人写代码,为自己解问题,为己赋能。
开源的本质是「开放、共建、复盘」,自用的本质是「刚需、务实、深耕」,二者结合,让我们的编程成长,既有方向,又有深度,更有格局。
通达信作为国内主流的证券行情与交易软件,凭借稳定的行情接收、丰富的技术指标体系和便捷的公式编辑功能,成为千万投资者和量化分析师的必备工具。
Python 作为数据科学领域的"瑞士军刀",在数据处理、统计分析、人工智能等方面拥有无可比拟的生态优势。
tdxpy_formulas 项目是一个基于MIT开源协议的通达信Python插件框架,它通过构建C++与Python的混合编程桥梁,让开发者能够直接在通达信中调用Python脚本,实现自定义技术指标。项目采用模块化设计,兼顾性能与易用性,为量化分析提供了更灵活的实现方案。
要让通达信运行Python代码,核心是解决三个关键问题:通达信插件接口适配、跨语言数据传输、Python解释器嵌入。以下从原理层面拆解实现逻辑。
通达信提供了DLL插件接口,允许第三方开发者通过特定规范的函数注册,将自定义功能集成到软件中。其核心工作流程如下:
关键约束:插件必须导出RegisterTdxFunc函数,函数参数为TdxPluginFunctionInfo**类型,用于向通达信注册功能函数表。每个功能函数需遵循固定签名:
typedefvoid(*TdxPluginFunction)(int dataLength, float* output, float* inputA, float* inputB, float* inputC);C++与Python的通信是项目核心,采用"嵌入式Python解释器"方案,而非进程间通信,以保证数据传输效率:
核心流程示意图:
通达信软件 → 调用DLL函数 → C++插件框架 → 数据格式转换 → Python函数执行 ↓通达信展示结果 ← 接收计算结果 ← C++插件框架 ← 结果格式转换 ← Python返回结果热重载功能允许在不重启通达信的情况下更新Python代码,核心通过以下机制实现:
PyImport_ReloadModule重新加载指定模块注:本功能下一版本解决,当前版本暂不支持。
Visual Studio 2022
CMake
Git
Python 3.14.2
third_party/Python3142-32目录# 克隆项目仓库git clone https://gitee.com/icodewr/tdxpy_formulascd tdxpy_formulas# 初始化子模块(如需第三方依赖源码)git submodule update --init --recursive项目核心目录结构说明:
tdxpy_formulas/├── src/ # C++源代码目录(核心框架实现)├── include/ # C++头文件目录├── pythonenv/ # Python公式脚本与虚拟环境├── config/ # 配置文件目录├── third_party/ # 第三方依赖(Python、jsoncpp)├── tests/ # 单元测试代码├── docs/ # 项目文档└── CMakeLists.txt # 构建配置主文件# 创建构建目录mkdir buildcd build# 配置CMake(32位Release版本,通达信默认兼容)cmake .. -G "Visual Studio 17 2022" -A Win32 -DCMAKE_BUILD_TYPE=Release# 编译项目(并行编译加速)cmake --build . --config Release -- /m# 安装输出文件到指定目录cmake --install . --config Releasetdxpy_formulasx86-Release(32位Release版本)tdxpy_formulas项目,选择"设为启动项目"Ctrl+Shift+B编译项目,生成的DLL文件位于build/bin/Release目录负责向通达信注册功能函数,是通达信与插件交互的入口。核心实现如下:
// 生成插件函数包装器#define GENERATE_PLUGIN_FUNCTION(n) \void pluginFunction##n(int dataLength, float *output, float *inputA, float *inputB, float *inputC) \{ \ pluginFunctionDispatcher(n, dataLength, output, inputA, inputB, inputC); \}// 生成0-100号函数GENERATE_PLUGIN_FUNCTION(0)GENERATE_PLUGIN_FUNCTION(1)// ... 省略中间函数 ...GENERATE_PLUGIN_FUNCTION(100)voidpluginFunctionDispatcher(int functionId, int dataLength, float *output,float *inputA, float *inputB, float *inputC){ TDXPY_LOG_DEBUG("函数调用调度: ID=" + std::to_string(functionId) + ", 数据长度=" + std::to_string(dataLength));if (!tdxpyRunPythonPlugin(functionId, dataLength, output, inputA, inputB, inputC)) {// 调用失败时输出清零if (functionId != 0) {std::fill(output, output + dataLength, 0.0f); } }}RegisterTdxFunc函数extern"C" __declspec(dllexport) BOOL RegisterTdxFunc(TdxPluginFunctionInfo **pluginFuncTable){if (*pluginFuncTable == nullptr) { (*pluginFuncTable) = g_pluginFunctionTable; // 函数表指针赋值return TRUE; }return FALSE;}负责Python解释器的初始化、Python函数调用、数据格式转换等核心功能。
inttdxpyPythonInitialize(){// 双检锁确保线程安全初始化if (g_pythonInitialized.load(std::memory_order_acquire)) {return0; }std::lock_guard<std::mutex> lock(g_initMutex);if (g_pythonInitialized.load(std::memory_order_relaxed)) {return0; }// 初始化Python配置 PyConfig pythonConfig; PyConfig_InitPythonConfig(&pythonConfig);// 设置Python路径与搜索目录 pythonConfig.executable = Py_DecodeLocale(pythonExe.c_str(), nullptr); pythonConfig.home = Py_DecodeLocale(pythonHome.c_str(), nullptr); pythonConfig.module_search_paths_set = 1;for (constauto& path : pySearchPaths) { PyWideStringList_Append(&pythonConfig.module_search_paths, Py_DecodeLocale(path.c_str(), nullptr)); }// 初始化Python解释器 PyStatus status = Py_InitializeFromConfig(&pythonConfig);if (PyStatus_Exception(status)) { TDXPY_LOG_ERROR("Python解释器初始化失败");return1; } g_pythonInitialized.store(true, std::memory_order_release);return0;}inttdxpyRunPythonPlugin(int functionId, int dataLength,float* output, float* inputA, float* inputB, float* inputC){// 参数校验if (dataLength <= 0 || !output) return0;// 获取公式配置(从JSON配置中查找函数ID对应的Python模块和函数)const tdxpy::config::FormulaConfig* pFormula = g_tdxpyConfig.getFormulaById(functionId);if (!pFormula) return0;// GIL管理(RAII自动加锁解锁) PyGILLocker gil;if (!gil.isAcquired()) return0;// 导入Python模块 PyObject* pModule = PyImport_ImportModule(pFormula->moduleName.c_str());if (!pModule) { SafePyErrPrint();return0; }// 获取Python函数 PyObject* pFunction = PyObject_GetAttrString(pModule, pFormula->function.c_str());if (!pFunction || !PyCallable_Check(pFunction)) { SafePyErrPrint(); Py_DECREF(pModule);return0; }// 构建函数参数(转换C++数组为Python列表) PyObject* pArgs = PyTuple_New(6); PyTuple_SetItem(pArgs, 0, PyLong_FromLong(functionId)); PyTuple_SetItem(pArgs, 1, PyLong_FromLong(dataLength)); PyTuple_SetItem(pArgs, 2, convertFloatArrayToPythonList(inputA, dataLength)); PyTuple_SetItem(pArgs, 3, convertFloatArrayToPythonList(inputB, dataLength)); PyTuple_SetItem(pArgs, 4, convertFloatArrayToPythonList(inputC, dataLength)); PyTuple_SetItem(pArgs, 5, PyUnicode_FromString(pFormula->userParams.c_str()));// 调用Python函数 PyObject* pResult = PyObject_CallObject(pFunction, pArgs);// 处理返回结果(转换Python列表为C++数组)if (pResult && PyList_Check(pResult)) { convertPythonListToFloatArray(pResult, output, dataLength); Py_DECREF(pResult); Py_DECREF(pArgs); Py_DECREF(pFunction); Py_DECREF(pModule);return1; }// 资源清理 Py_XDECREF(pResult); Py_DECREF(pArgs); Py_DECREF(pFunction); Py_DECREF(pModule);return0;}// C++浮点数组转Python列表PyObject* convertFloatArrayToPythonList(constfloat* data, int length){if (!data || length <= 0) return PyList_New(0); PyObject* pList = PyList_New(length);for (int i = 0; i < length; ++i) { PyList_SetItem(pList, i, PyFloat_FromDouble(data[i])); }return pList;}// Python列表转C++浮点数组voidconvertPythonListToFloatArray(PyObject* pList, float* output, int length){if (!pList || !output || length <= 0) return; Py_ssize_t listSize = PyList_Size(pList); Py_ssize_t safeSize = std::min(listSize, static_cast<Py_ssize_t>(length));for (Py_ssize_t i = 0; i < safeSize; ++i) { PyObject* pItem = PyList_GetItem(pList, i);if (pItem && PyFloat_Check(pItem)) { output[i] = static_cast<float>(PyFloat_AsDouble(pItem)); } else { output[i] = 0.0f; } }// 填充剩余元素for (Py_ssize_t i = safeSize; i < length; ++i) { output[i] = 0.0f; }}负责解析JSON配置文件,管理Python路径、公式映射、日志级别等配置信息。
核心功能:
配置文件示例(tdxpy_config.json):
{"tdxpy_formulas": {"version": "0.1.0","description": "通达信Python公式插件配置文件" },"python_config": {"python_home": "./third_party/Python3142-32/","search_paths": ["./third_party/Python3142-32/Lib","./pythonenv/tdxpy_formulas" ] },"formula_mappings": [ {"name": "TDXPY_MA","description": "移动平均线","module_name": "tdxpy_ma","function": "tdxpy_ma","id": 1,"user_params": "5,10,20,60" } ]}提供多级别日志记录功能,支持控制台输出与文件记录,便于开发调试与运行监控。
核心特性:
使用示例:
// 不同级别日志调用TDXPY_LOG_DEBUG("Python模块加载成功: " + moduleName);TDXPY_LOG_INFO("插件初始化完成,版本: " + version);TDXPY_LOG_WARNING("配置文件缺少可选字段: " + fieldName);TDXPY_LOG_ERROR("Python函数调用失败: " + functionName);Python公式需遵循特定的函数签名与目录规范,才能被插件正确识别和调用:
pythonenv/tdxpy_formulas目录下tdxpy_ma.py的模块名为tdxpy_ma)def 函数名(function_id, data_length, input_a, input_b, input_c, user_params):""" 通达信Python公式入口函数 参数说明: function_id: 函数ID(对应配置文件中的id字段) data_length: 输入数据长度 input_a: 输入数据A(通常为收盘价) input_b: 输入数据B(通常为最高价) input_c: 输入数据C(通常为最低价) user_params: 用户参数字符串(从配置文件读取) 返回值: list: 计算结果列表,长度必须等于data_length """# 业务逻辑实现return 结果列表# tdxpy_ma.py - 移动平均线公式实现deftdxpy_ma(function_id, data_length, input_a, input_b, input_c, user_params):""" 计算简单移动平均线(SMA) user_params: 周期参数,逗号分隔(如"5,10,20,60") """# 解析用户参数,默认周期为5ifnot user_params: periods = [5]else: periods = [int(p.strip()) for p in user_params.split(',') if p.strip()]# 取第一个周期计算(如需多周期输出可扩展) period = periods[0] result = []for i in range(data_length):if i < period - 1:# 数据不足周期时返回0 result.append(0.0)else:# 计算指定周期的移动平均 start_idx = i - period + 1 end_idx = i + 1 avg_value = sum(input_a[start_idx:end_idx]) / period result.append(avg_value)return result利用NumPy的向量化计算提升性能,适用于大数据量场景:
# tdxpy_macd.py - NumPy加速的MACD指标import numpy as npdeftdxpy_macd(function_id, data_length, input_a, input_b, input_c, user_params):""" 计算MACD指标(指数平滑异同平均线) user_params: 快速周期,慢速周期,信号周期(默认"12,26,9") """# 解析参数ifnot user_params: fast_period, slow_period, signal_period = 12, 26, 9else: params = [int(p.strip()) for p in user_params.split(',') if p.strip()] fast_period = params[0] if len(params) > 0else12 slow_period = params[1] if len(params) > 1else26 signal_period = params[2] if len(params) > 2else9# 转换为NumPy数组 close_prices = np.array(input_a[:data_length], dtype=np.float32)# 计算EMAdefema(prices, period): alpha = 2 / (period + 1)return np.convolve(prices, [alpha * (1 - alpha)**i for i in range(period)], mode='same')# 计算MACD线 ema_fast = ema(close_prices, fast_period) ema_slow = ema(close_prices, slow_period) macd_line = ema_fast - ema_slow# 计算信号线 signal_line = ema(macd_line, signal_period)# 计算柱状线 histogram = (macd_line - signal_line) * 2# 返回柱状线结果(可根据需求返回MACD线或信号线)return histogram.tolist()在config/tdxpy_config.json的formula_mappings中添加公式映射:
{"formula_mappings": [ {"name": "TDXPY_MA","description": "简单移动平均线","module_name": "tdxpy_ma","function": "tdxpy_ma","id": 1,"user_params": "5,10,20,60" }, {"name": "TDXPY_MACD","description": "NumPy加速MACD指标","module_name": "tdxpy_macd","function": "tdxpy_macd","id": 2,"user_params": "12,26,9" } ]}编译完成后,在build/bin/Release目录获取以下文件:
tdxpy_formulas.dll(核心插件文件)python314.dll(Python运行时)jsoncpp.dll(JSON解析库)创建部署目录结构,复制相关文件:
T0002/dlls/ # 通达信插件目录├── tdxpy_formulas.dll # 核心插件├── python314.dll # Python运行时├── jsoncpp.dll # JSON库├── config/ # 配置文件目录│ └── tdxpy_config.json # 配置文件└── pythonenv/ # Python公式目录 └── tdxpy_formulas/ # 公式脚本 ├── tdxpy_ma.py └── tdxpy_macd.pyT0002/dlls目录(通达信安装目录可通过软件"系统"→"数据维护工具"查看)Ctrl+F打开公式管理器T0002/dlls/tdxpy_formulas.dll,完成绑定// 通达信公式编辑器中调用Python公式MA5:TDXDLL1(1, CLOSE, 0, 0), COLORBLUE; // 调用ID=1的MA公式(收盘价)MACD:TDXDLL1(2, CLOSE, 0, 0), COLORRED; // 调用ID=2的MACD公式当修改Python公式后,无需重启通达信,只需在公式中调用ID=0的函数即可实现热重载:
// 热重载Python模块(修改公式后刷新K线图即可生效)热重载:TDXDLL1(0, 0, 0, 0), NODRAW;注:该功能待完善
T0002/dlls/logs/tdxpy_formula.log)获取具体错误信息pythonenv/tdxpy_formulas目录module_name与function字段是否正确data_lengthtdxw.exe(通达信进程)TDXPY_LOG_DEBUG记录关键变量值按照项目规范开发新的Python公式,只需:
pythonenv/tdxpy_formulas目录本项目采用MIT开源协议,您可以:
限制条件:
欢迎通过以下方式参与项目贡献:
项目命名规则.md)贡献流程:
git checkout -b feature/xxx)git commit -m "Add xxx feature")git push origin feature/xxx)tdxpy_formulas项目通过C++与Python的协同编程,成功打通了通达信与Python的调用执行,项目的核心价值在于:
本项目不仅是一个实用的技术工具,更是一个学习跨语言编程、插件开发、量化分析的优秀实践案例。希望通过开源协作,让更多开发者参与进来,共同开发完善更多的学习项目和实用工具。
版权声明:本文档与项目代码均遵循MIT开源协议,© 2026 码上工坊 保留所有权利。
作者简介:码上工坊,探索用编程为己赋能,定期分享编程知识和项目实战经验。持续学习、适应变化、记录点滴、复盘反思、成长进步。
重要提示:本文主要是记录自己的学习与实践过程,所提内容或者观点仅代表个人意见,只是我以为的,不代表完全正确,欢迎交流讨论。