信DLL公式實戰(zhàn):用C語言實現(xiàn)MA均線指標(biāo))
簡介通達(dá)信插件開發(fā)資料包面向具有C語言與Windows編程基礎(chǔ)、希望擴(kuò)展通達(dá)信分析功能的開發(fā)者。資源圍繞通達(dá)信C接口、MA移動平均線算法與DLL公式展開展示了如何把均線計算邏輯封裝為動態(tài)庫函數(shù)并通過通達(dá)信外部接口集成到行情軟件中適合學(xué)習(xí)插件調(diào)用機制或作為自定義指標(biāo)二次開發(fā)模板。壓縮包共12個文件以cpp源碼與h頭文件為核心配合dsp、dsw工程文件組織編譯另含plg、opt、ncb等輔助配置整體僅10KB結(jié)構(gòu)緊湊便于快速定位MA算法實現(xiàn)與接口聲明。已有780人學(xué)習(xí)下載開發(fā)者可直接對照源碼理解通達(dá)信DLL公式調(diào)用方式和MA周期參數(shù)傳遞流程也可在此基礎(chǔ)上擴(kuò)展其他技術(shù)指標(biāo)。1. 通達(dá)信DLL公式把 C 算法掛進(jìn)行情軟件的一條近路純公式寫 MA 均線只需一行可一旦你想復(fù)用現(xiàn)成的 C 算法或者要在通達(dá)信客戶端里跑自定義的指標(biāo)邏輯公式腳本就會變成一個說不清的黑匣子數(shù)據(jù)對齊、參數(shù)傳遞、輸出緩沖區(qū)的生命周期全靠感覺。C 接口 DLL 公式就是為這條路準(zhǔn)備的——把算法編譯成標(biāo)準(zhǔn) DLL注冊給通達(dá)信公式引擎之后像調(diào)用內(nèi)置函數(shù)一樣在公式里使用。TestPluginTCale.zip 提供的正是這樣一條最小可用的 C 接口實現(xiàn)路線核心是 MA 算法包內(nèi)工程把接口注冊、數(shù)據(jù)交換、均線計算拆得很干凈。適合已經(jīng)寫過幾個通達(dá)信公式、對 C 有基礎(chǔ)但第一次接觸 DLL 擴(kuò)展的開發(fā)者。它解決的不是“畫出均線”而是“把自己的算法安全掛進(jìn) K 線計算流程”。2. 通達(dá)信DLL公式接口拆解RegisterTdxFunc 與 CalcFunc 的職責(zé)分工2.1 你拿到的包里哪些文件才是關(guān)鍵TestPluginTCale.zip 解壓后內(nèi)容按“接口聲明—算法實現(xiàn)—注冊入口”三個層次組織。我通常先翻 readme 而不是直接開工程因為 readme 會寫明編譯器的版本要求和 DLL 放置目錄這兩點在通達(dá)信 DLL 開發(fā)里最容易翻車。文件作用通常需要改的位置tdx_interface.h定義與公式引擎交換數(shù)據(jù)的結(jié)構(gòu)體和導(dǎo)出宏只在換接口版本時動plugin_register.c實現(xiàn) RegisterTdxFunc 和 CalcFunc 兩個導(dǎo)出入口增加新函數(shù)時在這里登記ma_func.c核心 MA 算法與接口層無關(guān)替換成你自己的算法時主要改這里工程文件 / 構(gòu)建腳本控制編譯參數(shù)與 CRT 鏈接方式切換 32 位 / 64 位時必改readme編譯、注冊、驗證步驟說明按你的實測經(jīng)驗補充這種拆分的好處是接口層和算法層解耦。后續(xù)想把 DLL 里的 MA 換成 EMA只需要改 ma_func.c不用動導(dǎo)出入口反過來如果客戶端升級導(dǎo)致接口結(jié)構(gòu)體字段發(fā)生變化只動 tdx_interface.h 和 plugin_register.c 就能兜住。踩過一次 DLL 加載失敗的坑之后我對文件職責(zé)劃分就特別敏感——把注冊邏輯和算法邏輯揉在一個文件里出了問題你根本分不清是協(xié)議寫錯還是算法算錯。2.2 數(shù)據(jù)從公式系統(tǒng)傳進(jìn) DLL 的路徑通達(dá)信客戶端在計算公式時會先從行情里取到 K 線時間序列把 CLOSE 這樣的序列整理成 float 數(shù)組然后調(diào)用 DLL 的導(dǎo)出函數(shù)。整個過程可以簡化為三步先把 CLOSE 序列、參數(shù)值、輸出緩沖區(qū)的地址打包進(jìn)一個結(jié)構(gòu)體再進(jìn)入 CalcFunc最后把輸出數(shù)組寫回給公式引擎。這里的關(guān)鍵在于輸出緩沖區(qū)。公式引擎為了減少內(nèi)存分配開銷經(jīng)常復(fù)用同一塊緩沖里面可能殘留上一次計算的結(jié)果。你的函數(shù)不能依賴“初始值是 0”必須在循環(huán)里對所有索引位逐根賦值否則就會出現(xiàn)畫面上殘留舊均線、數(shù)據(jù)錯位這類看起來像玄學(xué)的問題。// tdx_interface.h節(jié)選 typedef struct tagTDX_CALC_INFO { int nDataCount; // 參與計算的K線根數(shù) int nParamCount; // 公式里傳入的參數(shù)個數(shù) float* pfParam; // 參數(shù)數(shù)組pfParam[0]是第1個參數(shù) const float* pfInput; // 輸入序列比如CLOSE的收盤價 float* pfOutput; // 輸出序列長度與nDataCount一致 } TDX_CALC_INFO;注意pfInput聲明為 const float*這是工程里比較規(guī)范的做法算法層只讀行情序列避免誤改輸入數(shù)據(jù)。不同客戶端版本對結(jié)構(gòu)體字段順序略有差異以你拿到的包內(nèi)頭文件為準(zhǔn)但主要字段就是這幾個。提示先確認(rèn) nDataCount 的實際含義它代表本次調(diào)用參與計算的 K 線根數(shù)不等同于當(dāng)前 K 線的絕對位置。2.3 參數(shù)表和輸出緩沖區(qū)的三個約定實戰(zhàn)中容易在參數(shù)表上出問題我總結(jié)了三點第一參數(shù)從pfParam[0]開始不是pfParam[1]。公式里寫MA(CLOSE,5)那么pfParam[0]就是 5.0f第一次寫 DLL 的人常在這里錯讀一位。第二nParamCount是公式里傳參的總個數(shù)。如果公式里寫了MA(CLOSE,5,1)第三個參數(shù)也會傳進(jìn)來即使你的算法只用前一個。DLL 內(nèi)部要做參數(shù)個數(shù)校驗別假定pfParam一定夠長。第三對無結(jié)果的 K 線位置必須顯式寫值。MA5 的前 4 根本來就沒有有效值你需要在 pOut 里寫 0 或者 NAN不能跳過不寫。至于 0 和 NAN 哪個會被公式引擎當(dāng)作“空值”不同版本處理不一樣包內(nèi)默認(rèn)寫 0如果你的客戶端不識別需要改成 NAN 分支。注冊入口的代碼也在這個包里邏輯非常直接// plugin_register.c // 告訴公式引擎這個DLL里有哪些函數(shù)每個函數(shù)幾個參數(shù) BOOL WINAPI RegisterTdxFunc(TDX_REG_INFO* pRegInfo) { if (pRegInfo NULL) return FALSE; pRegInfo-nFuncCount 1; pRegInfo-pFuncs[0].nParamCount 1; pRegInfo-pFuncs[0].strFuncName MA; return TRUE; }這段代碼里最需要注意的是WINAPI調(diào)用約定也就是__stdcall。如果編譯器默認(rèn)用__cdecl導(dǎo)出表里的函數(shù)名和參數(shù)棧處理方式都會變客戶端加載時可能直接報“不是有效的 DLL”。這個坑我踩過一次后來凡是寫通達(dá)信 DLL 導(dǎo)出函數(shù)我都會先確認(rèn)編譯宏里是不是WINAPI。3. 用 C 實現(xiàn) MA 算法的核心循環(huán)滑動窗口與邊界處理3.1 雙重循環(huán)求均值為什么不能直接搬進(jìn)來最順手的 MA 寫法是兩層循環(huán)外層遍歷每個 K 線索引內(nèi)層從 i-N1 累加到 i。這種寫法在數(shù)據(jù)量小的時候沒有明顯問題但放進(jìn)通達(dá)信 DLL 環(huán)境就不合適了。公式引擎刷新一次可能調(diào)用 CalcFunc 多次每次傳入的 K 線數(shù)量可能是幾千根。如果你在公式里又嵌套調(diào)用了這個 DLL 函數(shù)時間復(fù)雜度會逼近 O(N2)快速翻票時能感覺到明顯的卡頓。更隱蔽的是浮點誤差問題——每次重新累加窗口內(nèi)的浮點數(shù)末尾幾根均線可能出現(xiàn)可觀察的漂移導(dǎo)致后面兩線重合測試時差出零點幾個點看起來像接口沒寫對其實是算法層精度不夠。這兩層問題合在一起決定了 MA 算法不能直接照搬教科書寫法要用滑動窗口。3.2 滑動窗口求均值避免累計誤差的寫法// ma_func.c // 計算簡單移動平均 MA(N) // pIn: 輸入序列長度 nCount // pOut: 輸出序列長度 nCount // nMA: 移動平均周期調(diào)用前已經(jīng)做合法性檢查 int CalcSMA(const float* pIn, float* pOut, int nCount, int nMA) { if (pIn NULL || pOut NULL || nCount 0 || nMA 0) return 0; double sum 0.0; for (int i 0; i nCount; i) { sum pIn[i]; // 窗口滑出舊值inMA 時pIn[i-nMA] 已經(jīng)不在窗口內(nèi) if (i nMA) sum - pIn[i - nMA]; if (i nMA - 1) pOut[i] (float)(sum / nMA); // 窗口已滿輸出有效均值 else pOut[i] 0.0f; // 前 nMA-1 根沒有有效值 } return 1; }這個寫法維護(hù)一個增量累加變量 sum新值進(jìn)窗口就加舊值出窗口就減避免了每次從零開始重新累加時間復(fù)雜度降到 O(N)。累加用 double 而不是 float是為了避免連續(xù)幾千次 float 累加造成的精度損失尤其當(dāng) nMA 比較大的時候float 的舍入誤差會累積到結(jié)果尾部。對前導(dǎo)無效區(qū)的處理我用的是pOut[i] 0.0f這是包內(nèi)默認(rèn)行為。實際項目中如果客戶端支持 NAN 作為空值標(biāo)記我會在編譯時加一個宏分支#ifdef TDX_NAN_EMPTY pOut[i] (float)NAN; #else pOut[i] 0.0f; #endif具體用哪個以你在目標(biāo)客戶端上做的空值實驗為準(zhǔn)。3.3 把 MA 算法掛進(jìn) CalcFunc 的完整主干代碼算法本身寫好了還需要一個殼把它接到通達(dá)信公式引擎上這就是 CalcFunc 的職責(zé)// plugin_register.c #include tdx_interface.h #include windows.h // 通達(dá)信公式引擎每次計算都會調(diào)用這個函數(shù) BOOL WINAPI CalcFunc(TDX_CALC_INFO* pInfo) { int nMA; if (pInfo NULL || pInfo-pfInput NULL || pInfo-pfOutput NULL) return FALSE; // pfParam[0] 是公式里傳入的周期參數(shù)四舍五入取整 nMA (int)(pInfo-pfParam[0] 0.5f); if (nMA 1 || nMA 500) return FALSE; // 調(diào)用算法層真正的MA計算在ma_func.c里 if (CalcSMA(pInfo-pfInput, pInfo-pfOutput, pInfo-nDataCount, nMA) 0) return FALSE; return TRUE; }參數(shù)說明(int)(pInfo-pfParam[0] 0.5f)是把浮點參數(shù)四舍五入成整數(shù)。用戶在公式里可能寫MA(CLOSE, 5)也可能寫MA(CLOSE, 5.0)不加處理直接強轉(zhuǎn) int 會把 5.0 之外的邊緣值截斷出錯。周期上限限制在 500 是我的習(xí)慣防止公式里誤寫一個超大周期導(dǎo)致計算量失控。返回 FALSE 時公式引擎會認(rèn)為這次調(diào)用無效不會使用輸出緩沖區(qū)里的結(jié)果這是比較安全的失敗模式。如果你在 CalcFunc 里需要調(diào)試我一般會在開頭加一個計數(shù)器自增然后在公式里調(diào)用它看數(shù)值是否變化以此確認(rèn) DLL 確實被調(diào)入執(zhí)行而不是公式引擎只用了緩存。4. 編譯、安裝與調(diào)用讓 DLL 在通達(dá)信客戶端里跑起來4.1 編譯前必須確認(rèn)的配置打開工程文件之前先確認(rèn)三件事否則后面全是白忙。第一目標(biāo)平臺。絕大多數(shù)通達(dá)信客戶端仍是 32 位進(jìn)程即使運行在 64 位 Windows 上DLL 也必須按 x86 編譯。編出 x64 版本會導(dǎo)致導(dǎo)入時直接報“模塊無法加載”。第二調(diào)用約定。導(dǎo)出函數(shù)必須使用WINAPI也就是__stdcall。C 源文件里可以直接寫B(tài)OOL WINAPI CalcFunc(...)如果是 C 文件還需要加上extern C防止函數(shù)名被修飾。第三CRT 鏈接方式。建議使用靜態(tài)運行時庫/MT而不是動態(tài)的/MD??蛻舳藱C器上可能沒有對應(yīng)版本的 VC 運行庫動態(tài)鏈接部署到別的電腦時容易找不到msvcr*.dll。在開發(fā)命令行里編譯的命令大致是cl /c /O2 /MT /D WIN32 ma_func.c plugin_register.c link /DLL /OUT:TDXDLL1.dll ma_func.obj plugin_register.obj參數(shù)說明/O2是速度優(yōu)化/MT是靜態(tài)鏈接運行庫/DLL告訴鏈接器生成動態(tài)庫而不是 EXE。鏈接完成后用dumpbin /exports TDXDLL1.dll檢查導(dǎo)出表確認(rèn)能看到 RegisterTdxFunc 和 CalcFunc 兩個符號名稱沒有被編譯器修飾過。4.2 復(fù)制 DLL 與導(dǎo)入注冊編譯完成后把 TDXDLL1.dll 復(fù)制到通達(dá)信安裝目錄下與 DLL 公式相關(guān)的子目錄。不同版本的客戶端放置路徑不完全一樣常見做法是放在安裝目錄的公式子目錄或者用戶目錄下的公式目錄里。然后打開通達(dá)信客戶端進(jìn)入公式管理器切換到 DLL 函數(shù)一欄點導(dǎo)入選中剛才的 DLL。導(dǎo)入成功后會列出函數(shù)名 MA參數(shù)個數(shù) 1。導(dǎo)入完成建議重啟一次客戶端讓公式引擎徹底重新加載 DLL避免舊實例占用文件句柄。注意如果你改了 DLL 代碼重新編譯需要先在公式管理器里移除舊的 DLL 引用再覆蓋文件。否則 DLL 文件被客戶端占用復(fù)制會提示失敗。4.3 在公式里寫第一行調(diào)用注冊完成以后在 K 線圖主圖新建一個公式寫入A:TDXDLL1.MA(CLOSE,5),COLORWHITE; B:MA(CLOSE,5),COLORYELLOW;A 是 DLL 算出來的 MA5B 是通達(dá)信內(nèi)置的 MA5。把兩條線疊加在同一個主圖上如果接口和算法都正常白色線和黃色線會完全重合。第一次跑這個公式時建議先把行情切換到日線級別、選擇一根已經(jīng)收盤的股票或指數(shù)因為盤中最后一根 K 線的 CLOSE 還在變化對比內(nèi)置 MA 時容易出現(xiàn)“差一點對不上”的干擾。確認(rèn)日線下重合之后再切到分時周期看盤中表現(xiàn)。4.4 判斷接口有沒有被真正執(zhí)行兩線重合只能說明最終結(jié)果對但有時候你改完代碼重新編譯發(fā)現(xiàn)數(shù)值一點沒變這時候要先懷疑 DLL 根本沒被重新加載。我一般會在 CalcFunc 開頭加一個全局計數(shù)// 調(diào)試用全局變量 volatile int g_nCallCount 0; BOOL WINAPI CalcFunc(TDX_CALC_INFO* pInfo) { g_nCallCount; // ... 原有邏輯 }然后在公式里用一個自定義輸出函數(shù)把這個計數(shù)顯示出來或者寫到一個日志文件里。如果切換公式周期后計數(shù)不增長說明 DLL 沒有被重新調(diào)用問題出在注冊或緩存上而不是算法。5. 避坑與排查裝載失敗、數(shù)值錯位、白屏和緩存四類典型問題5.1 現(xiàn)象DLL 導(dǎo)入時報錯或函數(shù)列表為空導(dǎo)入 DLL 時提示不是有效的程序或者導(dǎo)入成功但函數(shù)列表空白。原因通常是兩種一是位數(shù)不匹配編出來的 DLL 是 x64 而客戶端是 32 位二是導(dǎo)出符號被編譯器修飾了函數(shù)名變成_RegisterTdxFunc4這樣的形式客戶端找不到原始符號。還有一種比較隱蔽的是編譯時用了/clr生成托管入口通達(dá)信完全認(rèn)不了。解決方法是先把工程改成 32 位純 C 編譯再確認(rèn)導(dǎo)出函數(shù)聲明了WINAPI。然后用 dumpbin 檢查dumpbin /exports TDXDLL1.dll輸出里應(yīng)該出現(xiàn)未修飾的RegisterTdxFunc和CalcFunc。如果看到后綴或下劃線前綴去檢查是不是沒有加__stdcall或者沒有用.def文件指定導(dǎo)出名。5.2 現(xiàn)象兩線不重合且所有點整體偏移一根K線白色線和黃色線形狀一樣但整體錯開一根 K 線的位置日線上看特別明顯。原因多半不是 DLL 算法算錯而是輸入序列引用方式不同。內(nèi)置 MA 直接引用當(dāng)前 K 線 CLOSE而公式里如果你寫成TDXDLL1.MA(REF(CLOSE,1),5)那么輸入序列整體滯后了一根。另一層干擾來自盤中最后一根未收盤 K 線CLOSE 隨時間變化和內(nèi)置 MA 對比時最后一根會出現(xiàn)動態(tài)偏差。解決方法是先切到已收盤的日線再對比把盤中變量排除掉公式里只寫CLOSE不要加REF。確認(rèn)這層沒問題后再檢查 DLL 內(nèi)部是不是對 pIn 的起始位置做了偏移比如把pIn[i]誤寫成pIn[i1]。5.3 現(xiàn)象切換周期后數(shù)值一直沿用上一周期的結(jié)果先看日線數(shù)據(jù)正常切到五分鐘線后 DLL 輸出的 MA 還是日線數(shù)值完全不更新。原因有兩個層面。第一公式引擎對計算結(jié)果做了緩存周期性切換時沒有觸發(fā) DLL 重新調(diào)用第二你自己的 DLL 代碼里用了 static 變量保存中間結(jié)果比如把上一周期的 sum 緩存下來新周期數(shù)據(jù)進(jìn)來時沒有正確判斷數(shù)據(jù)長度變化。解決方法是先確認(rèn) DLL 是否被重新調(diào)用可以用上一章的計數(shù)法。如果計數(shù)在增長但數(shù)值沒變那就是 DLL 內(nèi)部緩存的問題。不要在 CalcFunc 里用 static 數(shù)組或 static 累加變量存放中間結(jié)果每次調(diào)用都按pInfo-nDataCount重新計算。如果確實需要緩存把數(shù)據(jù)長度和參數(shù)值拼接成緩存鍵數(shù)據(jù)長度一變就整體失效。5.4 現(xiàn)象連續(xù)切換股票后軟件白屏或閃退快速翻票時軟件偶爾白屏嚴(yán)重時整個客戶端退出。這類問題大概率是內(nèi)存越界。常見越界點有兩個一是 nMA 設(shè)置得比 nDataCount 還大循環(huán)內(nèi)讀 pIn[i] 時已經(jīng)越過有效區(qū)間二是對 pfOutput 的寫入超過了 nDataCount 范圍。還有一種不太容易察覺的是在 CalcFunc 里修改 pfParam 數(shù)組內(nèi)容導(dǎo)致后續(xù)調(diào)用參數(shù)錯亂連鎖引發(fā)崩潰。解決方法是進(jìn)入 CalcFunc 后先做防御性校驗if (pInfo NULL || pInfo-pfInput NULL || pInfo-pfOutput NULL) return FALSE; if (nMA 0 || nMA pInfo-nDataCount) return FALSE;這里把nMA nDataCount也攔截掉因為當(dāng) K 線根數(shù)少于均線周期時滑動窗口根本不成立直接返回 FALSE 比強行計算安全得多。6. 驗證與擴(kuò)展從 MA 到 EMA 的接口改造習(xí)慣6.1 每次改完都跑一遍“兩線重合”我建議把驗證公式存成模板文件內(nèi)容就是 DLL 輸出和內(nèi)置 MA 輸出并排疊加。每次改完 DLL 代碼重新編譯、導(dǎo)入然后拉到已收盤的日線上跑一遍這個模板。凡是寄存器協(xié)議問題、參數(shù)位數(shù)問題、邊界處理問題都會在兩線不重合時暴露出來。再抽查最后 10 根 K 線的具體數(shù)值在圖表上用光標(biāo)逐根對比能發(fā)現(xiàn)肉眼看不清的細(xì)微偏差。6.2 把 MA 換成 EMA 只需動一個文件MA 驗證通過以后算法層就可以做擴(kuò)展了。EMA 和 MA 的差異只在遞推公式上接口層完全不用改// 在 ma_func.c 中追加 EMA 實現(xiàn) int CalcEMA(const float* pIn, float* pOut, int nCount, int nParam) { if (nCount 0 || nParam 0) return 0; float k 2.0f / (nParam 1); double ema pIn[0]; pOut[0] (float)ema; for (int i 1; i nCount; i) { ema pIn[i] * k ema * (1.0f - k); pOut[i] (float)ema; } return 1; }這段代碼里ema用 double 保持遞推精度輸出轉(zhuǎn) float 時只在最后一刻截斷避免每一步都引入舍入誤差。如果你想在通達(dá)信公式里同時調(diào)用 MA 和 EMA需要在 RegisterTdxFunc 里把函數(shù)個數(shù)改成 2并登記第二個函數(shù)名。這個擴(kuò)展點正好驗證了第 2 章接口層和算法層拆分的設(shè)計價值。之前做 DLL 擴(kuò)展時有一次我在切換周期后盯著屏幕反復(fù)懷疑寄存器協(xié)議寫錯了折騰很久才發(fā)現(xiàn)只是 DLL 內(nèi)部一個 static 變量沒清理。從那以后我每次修改完 DLL都會強制走一遍模板驗證并且重新編譯后順手看一眼 dumpbin 導(dǎo)出表確認(rèn)函數(shù)名沒被編譯器加料。這兩個習(xí)慣讓我少花了很多在客戶端里反復(fù)試錯的功夫希望幫到你。本文還有配套的精品資源點擊獲取