指標(biāo)間的數(shù)據(jù)交換:易如反掌!
簡(jiǎn)介
赫茲量化之所以欣賞初學(xué)者,是因?yàn)樗麄児虉?zhí)地不愿意使用搜索,而諸如“常見問題解答”、“初學(xué)者指南”或“婦孺皆知的問題解答”的話題隨處可見。他們真正的任務(wù)是提出問題,比如“如何...”、“有沒有可能...”,但他們常常獲得“沒門”、“不可能”等諸如此類的答案。 恒古流傳的警句“永不說不”一直激勵(lì)著科學(xué)家、工程師和程序員思考和推陳出新。
1. 問題定義 例如,下文引用自 MQL4 社區(qū)論壇的一個(gè)主題(譯自俄語): ...有兩個(gè)指標(biāo)(我們稱之為 A 和 B)。像往常一樣,指標(biāo) A 使用直接來自價(jià)格圖表的數(shù)據(jù),B 使用指標(biāo) A 的數(shù)據(jù)。問題如下:如何使用指標(biāo) A 的數(shù)據(jù)(已經(jīng)附加)而不是 iCustom(“指標(biāo) A”,...)動(dòng)態(tài)執(zhí)行 B 的計(jì)算,即,如果我更改了指標(biāo) A 的某些設(shè)置,更改應(yīng)該在指標(biāo) B 的參數(shù)更改上體現(xiàn)出來。
或換言之: ...假設(shè)我們已將“移動(dòng)平均線”附加至圖表。現(xiàn)在要如何才能直接訪問其數(shù)據(jù)緩沖區(qū)? 又如:
... 如果我使用 iCustom 調(diào)用某項(xiàng)指標(biāo),即使該指標(biāo)在調(diào)用前已經(jīng)加載也仍將重新加載。是否有什么方法防止指標(biāo)重復(fù)加載?
諸如此類的問題還有很多,并且被反復(fù)問及 - 提問者不僅僅是初學(xué)者。概括而言,問題在于赫茲量化沒有辦法訪問自定義指標(biāo)數(shù)據(jù)而不使用 iCustom (MQL4) 或 iCustom + CopyBuffer 綁定 (MQL5)。但是用 MQL 代碼編寫一些函數(shù)以使用來自指定圖表的數(shù)據(jù)獲取數(shù)據(jù)或進(jìn)行計(jì)算,對(duì)于開發(fā)下一部大作而言是十分誘人的。 使用上文中提到的標(biāo)準(zhǔn)函數(shù)并不方便:例如,在 MQL4 中,當(dāng)調(diào)用 iCustom 時(shí),將為每個(gè)調(diào)用方創(chuàng)建指標(biāo)的副本;而對(duì)于 MQL5,通過使用句柄使問題得到了部分解決,現(xiàn)在僅為原本執(zhí)行一次計(jì)算。但句柄也不是萬靈藥:如果涉及的指標(biāo)占用大量的計(jì)算資源,則終端互斥等待幾乎是通過每次重新初始化的十幾秒鐘保證。 我記得有幾個(gè)提供的方法可訪問原本,如下所示:

編輯切換為居中
在物理磁盤或內(nèi)存上映射文件;
經(jīng)由 DLL 共享內(nèi)存使用以用于數(shù)據(jù)交換;
使用客戶端的全局變量以用于數(shù)據(jù)交換及其存儲(chǔ)。
此外還有屬于上述方法的變體的其他一些方法,以及一些不常用的方法,如套接字、郵件槽等(有一種基本方法在“EA 交易”中經(jīng)常使用,即直接將指標(biāo)計(jì)算轉(zhuǎn)移至“EA 交易”代碼,但這遠(yuǎn)遠(yuǎn)超出了本文論述的范圍)。 在筆者看來,所有這些方法都有可取之處,但它們有一個(gè)共同的劣勢(shì):數(shù)據(jù)首先是復(fù)制到某個(gè)位置,然后才分發(fā)給其他方。首先,這需要占用部分 CPU 資源;其次,產(chǎn)生了傳輸數(shù)據(jù)相關(guān)性的新問題(在此我們不做論述)。
所以,讓我們嘗試定義問題:我們希望創(chuàng)建這樣一個(gè)環(huán)境,即能夠提供對(duì)附加于圖表的指標(biāo)的數(shù)據(jù)訪問,并具有以下屬性:
沒有數(shù)據(jù)復(fù)制(也沒有數(shù)據(jù)相關(guān)性的問題);
只需稍加修改我們需要使用的可用方法的代碼;
MQL 代碼優(yōu)先(當(dāng)然,我們必須使用 DLL,但我們將只使用一些 C++ 代碼字符串)。
筆者將 C++ Builder 用于 DLL 創(chuàng)建和赫茲量化客戶端。下面的源代碼以 MQL5 編寫,MQL4 代碼已附于本文;赫茲量化將在下文中討論兩種代碼的主要區(qū)別。
2. 數(shù)組 首先,赫茲量化需要一些理論,因?yàn)槲覀儗⒁褂弥笜?biāo)緩沖區(qū),并且我們必須知道緩沖區(qū)的數(shù)據(jù)在內(nèi)存中是如何分布的。這一信息沒有適當(dāng)?shù)匚臋n化。 MQL 中的動(dòng)態(tài)數(shù)組是具有可變大小的結(jié)構(gòu),如果數(shù)組大小增加而在數(shù)組后沒有空閑內(nèi)存,則 MQL 如何解決數(shù)據(jù)重新分配的問題將變得十分有趣。我們有兩種方法解決問題:
在可用內(nèi)存的其他部分重新分配新數(shù)據(jù)(并儲(chǔ)存該數(shù)組所有部分的地址,例如使用引用列表),或
將整個(gè)數(shù)組作為整體移動(dòng)至內(nèi)存新的部分,該部分有足夠的空間用于分配數(shù)組。
第一種方法帶來了其他一些問題,因?yàn)樵谶@種情況下我們必須調(diào)查 MQL 編譯程序創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)。以下考慮仍然證明了第二個(gè)變體(仍然較慢):當(dāng)動(dòng)態(tài)數(shù)組傳遞至外部函數(shù)時(shí)(傳遞至 DLL),后者獲得數(shù)據(jù)第一個(gè)元素的指針,其他數(shù)組元素按照順序排列在第一個(gè)數(shù)組元素后。 當(dāng)通過引用傳遞時(shí),數(shù)組數(shù)據(jù)可以更改,因此這意味著對(duì)于第一種方法,復(fù)制整個(gè)數(shù)組至單獨(dú)的內(nèi)存區(qū)域以傳遞給外部函數(shù),然后將結(jié)果添加至源數(shù)組存在一個(gè)問題,即,執(zhí)行第二種方法隱含的相同的操作。
雖然這一結(jié)論在邏輯上不是 100% 精確,赫茲量化仍可將它視為是相當(dāng)可靠的(這也被我們基于此理念的產(chǎn)品的正確運(yùn)行所證明)。
因此,我們可以假設(shè)下面的說法是正確的:
在每一時(shí)刻,動(dòng)態(tài)數(shù)組的數(shù)據(jù)在內(nèi)存中一個(gè)接一個(gè)地連續(xù)排列;并且數(shù)組可重新分配至計(jì)算機(jī)內(nèi)存的其他區(qū)域,但只能作為一個(gè)整體操作。
當(dāng)動(dòng)態(tài)數(shù)組作為參數(shù)通過引用傳遞至外部函數(shù)時(shí),第一個(gè)數(shù)組元素的地址被傳遞至 DLL 庫(kù)。
然而我們還需要其他一些假設(shè):所謂時(shí)刻,赫茲量化指的是調(diào)用對(duì)應(yīng)指標(biāo)的 OnCalculate()(MQL4 則為 start () 函數(shù))函數(shù)的時(shí)刻;此外,為了簡(jiǎn)便,我們假設(shè)我們的數(shù)據(jù)緩沖區(qū)具有相同的維度,且類型均為 double []。 3. 必要條件 MQL 不支持指針(除了所謂的對(duì)象指針,對(duì)象指針并不是普遍意義上的指針),因?yàn)檫@已被 MetaQuotes Software 公司的代表反復(fù)聲明和確認(rèn)。因此,我們來看看事實(shí)是否如此。
什么是指針?指針并不只是帶星號(hào)的的標(biāo)識(shí)符,它是計(jì)算機(jī)內(nèi)存單元的地址。那么什么是單元地址?單元地址是起始于某個(gè)起點(diǎn)的序列號(hào)。最后,什么是序列號(hào)?序列號(hào)是一個(gè)整數(shù),對(duì)應(yīng)于計(jì)算機(jī)內(nèi)存的某個(gè)單元。為什么我們不能像使用整數(shù)那樣使用指針?不,我們可以,因?yàn)?MQL 程序可完美處理整數(shù)! 那么如何將指針轉(zhuǎn)換為整數(shù)?動(dòng)態(tài)鏈接庫(kù)可幫助我們達(dá)成目的,我們將利用 C++ 類型轉(zhuǎn)換的機(jī)會(huì)。由于 C++ 指針為四字節(jié)數(shù)據(jù)類型,對(duì)我們而言,可以很方便地將 int 用作此類四字節(jié)數(shù)據(jù)類型。 符號(hào)位并不重要,我們對(duì)其不作考慮(如果它等于 1,意味著整數(shù)為負(fù)),重要的是我們可以保持所有指針位不變。當(dāng)然,赫茲量化可以使用無符號(hào)整數(shù),但 MQL5 和 MQL4 的代碼相似則更為理想,因?yàn)?MQL4 未提供無符號(hào)整數(shù)。
因此, extern "C" __declspec(dllexport) int __stdcall GetPtr(double *a) { ? ? ? ? return((int)a); ?} 這樣,赫茲量化就有了值為數(shù)組起始地址的長(zhǎng)型變量!現(xiàn)在,我們需要了解如何讀取 i-th 數(shù)組元素的值:
extern "C" __declspec(dllexport) double __stdcall GetValue(int pointer,int i) { ? ? ? ? return(((double*) pointer)[i]); ?}
... 并寫入值(在我們的示例中它仍然不是必須的...)
extern "C" __declspec(dllexport) void __stdcall SetValue(int pointer,int i,double value) { ? ? ? ? ((double*) pointer)[i]=value; ?} 以上便是全部?jī)?nèi)容。現(xiàn)在我們可以在 MQL 中使用指針。
4. 包裝
我們已創(chuàng)建系統(tǒng)的內(nèi)核,現(xiàn)在必須為其在 MQL 程序中使用方便做準(zhǔn)備。然而,美學(xué)粉也不要感到不安,我們會(huì)向大家展現(xiàn)一些不同之處。
有很多種方法可用,我們將選擇以下方式。讓我們回想一下,客戶端具有一個(gè)特別功能用于獨(dú)立 MQL 程序間的數(shù)據(jù)交換 - 全局變量。最自然的方式是用它們存儲(chǔ)指向我們將要訪問的指標(biāo)緩沖區(qū)的指針。我們會(huì)將這些變量作為描述符表考慮。每個(gè)描述符將具有如下所示的以字符串表示的名稱:
string_identifier#buffer_number#symbol#period#buffer_length#indexing_direction#random_number, 并且其值將等于計(jì)算機(jī)內(nèi)存中相應(yīng)緩沖區(qū)指針的整數(shù)顯示。 有關(guān)描述符字段的一些細(xì)節(jié)。
string_identifier – 任意字符串(例如,指標(biāo)的名稱 - 可使用 short_name 變量等);它將被用于搜索必要的指針,我們指的是一些指標(biāo)將使用相同的標(biāo)識(shí)符注冊(cè)描述符,并在相互間使用字段進(jìn)行區(qū)分:
buffer_number – 將用于區(qū)分緩沖區(qū);
buffer_length – 我們需要用它來控制界限,否則可能導(dǎo)致客戶端和 Windows 崩潰Blue Screen of Death :);
symbol, period – 交易品種和周期用于指定圖表窗口;
ordering_direction – 它指定了數(shù)組元素的排序方向:0 – 正常排序,1 – 逆向排序(AS_SERIES 標(biāo)志為 true);
random_number – 如果指標(biāo)有多個(gè)副本通過不同的窗口或不同的參數(shù)集附加至客戶端時(shí)使用(它們的第一個(gè)和第二個(gè)字段可設(shè)置相同的值,這也是我們需要通過某些方式區(qū)分它們的原因)- 也許這不是最佳的解決方案,但它確實(shí)管用。
首先,赫茲量化需要用到一些函數(shù)來注冊(cè)和刪除描述符??匆豢春瘮?shù)的第一個(gè)字符串 - 調(diào)用 UnregisterBuffer() 函數(shù)對(duì)于從全局變量列表中刪除舊的描述符來說是必要的。 對(duì)于每個(gè)新柱,緩沖區(qū)大小將增加 1,因此我們必須調(diào)用 RegisterBuffer()。如果緩沖區(qū)大小改變,新的描述符將在表中創(chuàng)建(其大小信息包含在其名稱內(nèi)),舊的描述符將留在表中。這就是我們使用它的原因。 void RegisterBuffer(double &Buffer[], string name, int mode) export { ? ?UnregisterBuffer(Buffer); ? ? ? ? ? ? ? ? ? ?//首先刪除變量以防萬一 int direction=0; ? ?if(ArrayGetAsSeries(Buffer)) direction=1; ? ?//設(shè)置正確的ordering_direction ? ? name=name+"#"+mode+"#"+Symbol()+"#"+Period()+"#"+ArraySize(Buffer)+"#"+direction; ? ?int ptr=GetPtr(Buffer); ? ? ? ? ? ? ? ? ? ? ?// 獲取緩存指針 if(ptr==0) return; ? ? ? ?MathSrand(ptr); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?//使用指針值代替當(dāng)前時(shí)間,來生成隨機(jī)數(shù)比較方便 ? while(true) ? ?{ ? ? ? int rnd=MathRand(); ? ? ? if(!GlobalVariableCheck(name+"#"+rnd)) ? ?//檢查獨(dú)特的名稱 - 我們假設(shè) ? ? ? { ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //沒有人會(huì)使用更多的RAND_MAX緩存 :) ? ? ? ? ?name=name+"#"+rnd; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? GlobalVariableSet(name,ptr); ? ? ? ? ? //寫入全局變量 break; ? ? ? } ? ?} ? ?}} void UnregisterBuffer(double &Buffer[]) export { ? ?int ptr=GetPtr(Buffer); ? ? ? ? ? ? ? ? ? ? ?//我們將在緩存的真實(shí)地址中注冊(cè) if(ptr==0) return; ? ? ? ?int gt=GlobalVariablesTotal(); ? ? ? ? ? ? ? ? ? int i; ? ?for(i=gt-1;i>=0;i--) ? ? ? ? ? ? ? ? ? ? ? ? //遍歷所有全局變量 ? ?{ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?//并且從所有地方刪除緩存 string name=GlobalVariableName(i); ? ? ? ? ? ? ? if(GlobalVariableGet(name)==ptr) ? ? ? ? ?GlobalVariableDel(name); ? ? } ? ? ? ?} 詳細(xì)的注釋在此處是不必要的,我們只是指出第一個(gè)函數(shù)在全局變量列表中創(chuàng)建上述格式的新的描述符的事實(shí);第二個(gè)函數(shù)搜索所有全局變量并刪除變量和其值等于指針的描述符。
現(xiàn)在考慮第二個(gè)任務(wù) - 從指標(biāo)獲取數(shù)據(jù)。在我們實(shí)施對(duì)數(shù)據(jù)的直接訪問前,我們需要找出相應(yīng)的描述符??梢允褂靡韵潞瘮?shù)實(shí)現(xiàn)這一點(diǎn)。它的算法如下所述:我們遍歷所有全局變量,并檢查是否存在描述符中指定的字段值。 如果找到,赫茲量化將其名稱添加至數(shù)組,作為最后一個(gè)參數(shù)傳遞。因此,結(jié)果是函數(shù)返回所有匹配搜索條件的內(nèi)存地址。返回值是找到的描述符的數(shù)量。 int FindBuffers(string name, int mode, string symbol, int period, string &buffers[]) export { ? ?int count=0; ? ?int i; ? ?bool found; ? ?string name_i; ? ?string descriptor[]; ? ?int gt=GlobalVariablesTotal(); ?StringTrimLeft(name); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?//整理字符串的不必要空間 StringTrimRight(name); ? ? ? ?ArrayResize(buffers,count); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?//將大小重置為0 for(i=gt-1;i>=0;i--) ? ?{ ? ? ? found=true; ? ? ? name_i=GlobalVariableName(i); ? ? ? ? ? ? ?StringExplode(name_i,"#",descriptor); ? ? ? ? ? ? ? ? //分割字符串 if(StringFind(descriptor[0],name)<0&&name!=NULL) found=false; //根據(jù)匹配條件檢查每一個(gè)字段 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? if(descriptor[1]!=mode&&mode>=0) found=false; ? ? ? if(descriptor[2]!=symbol&&symbol!=NULL) found=false; ? ? ? if(descriptor[3]!=period&&period>0) found=false; ? ? ? ? ? ? ?if(found) ? ? ? { ? ? ? ? ?count++; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //條件滿足,將其添加到列表中 ArrayResize(buffers,count); ? ? ? ? ?buffers[count-1]=name_i; ? ? ? ?} ? ? } ? ? ? ?return(count); ?} 正如我們?cè)诤瘮?shù)代碼中所看到的,某些搜索條件可以省略。例如,如果我們傳遞 NULL 作為名稱,系統(tǒng)將省略對(duì) string_identifier 的檢查。其他字段也是一樣:mode<0,symbol:=NULL 或 period<=0。它允許在描述符表中擴(kuò)展搜索選項(xiàng)。 例如,您可以找出所有圖表窗口中的“移動(dòng)平均線”指標(biāo),或使用周期 M15 將范圍限制在 EURUSD 圖表中。補(bǔ)充說明:對(duì) string_identifier 的檢查通過函數(shù) StringFind() 執(zhí)行而不是嚴(yán)格的相等檢查。這樣做是為了能夠有機(jī)會(huì)通過描述符的一部分執(zhí)行搜索(也就是說,當(dāng)有多個(gè)指標(biāo)設(shè)置為 "MA(xxx)" 類型的字符串時(shí),搜索可通過子字符串 "MA" 進(jìn)行 - 結(jié)果是我們將找出所有注冊(cè)的“移動(dòng)平均線”)。
赫茲量化還使用了函數(shù) StringExplode(string s, string separator, string &result[])。該函數(shù)使用分隔符將指定的字符串分割為子字符串,并將結(jié)果寫入結(jié)果數(shù)組。