赫茲股票量化軟件:從頭開始開發(fā)一款智能交易系統(tǒng)
計劃
創(chuàng)造事物最困難的部分是弄清楚事物應(yīng)該如何運作。 這個思路應(yīng)該表述得非常清楚,如此我們就能按需創(chuàng)建最低代碼,因為若是創(chuàng)建的代碼越復(fù)雜,出現(xiàn)運行時錯誤的可能性就越大。 考慮到這一點,我嘗試讓代碼變得非常簡單,但依舊最大可能地利用 赫茲股票量化軟件提供的功能。 該平臺非??煽?,它在不斷進行測試,故此錯誤不會出現(xiàn)在平臺一端。
代碼將采用 OOP(面向?qū)ο缶幊蹋?這種方法能夠隔離代碼,并促進其維護和未來的開發(fā),預(yù)防我們想要添加新功能,并進行改進。赫茲股票量化軟件
盡管本文討論的 EA 是出于在 B3(巴西交易所)上進行交易而設(shè)計的,特別是為期貨(迷你指數(shù)和迷你美元)交易而設(shè)計的,但只需略微修改即可擴展到所有市場。 為了另事情變得更簡單,且不必列舉或檢查交易資產(chǎn),我們將使用以下枚舉:
enum eTypeSymbolFast {WIN, WDO, OTHER};
如果您想交易其它資產(chǎn),需用到某些特殊功能,請將其添加到枚舉之中。 這也需要在代碼中做一些微小的修改,但用枚舉會更容易一些,因為它還降低了出錯的可能性。 代碼中一個有趣的部分是 AdjustPrice 函數(shù):赫茲股票量化軟件
? double AdjustPrice(const double arg)
? ? {
? ? ?double v0, v1;
? ? ?if(m_Infos.TypeSymbol == OTHER)
? ? ? ? return arg;
? ? ?v0 = (m_Infos.TypeSymbol == WDO ? round(arg * 10.0) : round(arg));
? ? ?v1 = fmod(round(v0), 5.0);
? ? ?v0 -= ((v1 != 0) || (v1 != 5) ? v1 : 0);
? ? ?return (m_Infos.TypeSymbol == WDO ? v0 / 10.0 : v0);
? ? };
此函數(shù)將調(diào)整價格中用到的數(shù)值,從而在圖表準確定位價格線。 為什么我們不能簡單地在圖表上放一條線呢? 這是因為一些資產(chǎn)在價格之間存在一定的階梯。 對于 WDO (迷你美元) 這個階梯是 0.5 個點。 對于 WIN (迷你指數(shù)) 個階梯是 5 個點,而對于股票,它是 0.01 個點。 換言之,不同資產(chǎn)的點數(shù)值不同。 它會把價格調(diào)整為正確的即時報價數(shù)值,從而該數(shù)值能在訂單中正確使用,否則填寫有錯的訂單會被服務(wù)器拒絕。赫茲股票量化軟件
若無此函數(shù),可能很難知道訂單中所采用的數(shù)值是否正確。 故而,服務(wù)器就會通知訂單填寫錯誤,并阻止其執(zhí)行。 現(xiàn)在,我們繼續(xù)討論智能交易系統(tǒng)的核心函數(shù):CreateOrderPendent。 函數(shù)如下:
? ulong CreateOrderPendent(const bool IsBuy, const double Volume, const double Price, const double Take, const double Stop, const bool DayTrade = true)
? ? {
? ? ?double last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST);
? ? ?ZeroMemory(TradeRequest);
? ? ?ZeroMemory(TradeResult);
? ? ?TradeRequest.action ? ? ? ?= TRADE_ACTION_PENDING;
? ? ?TradeRequest.symbol ? ? ? ?= m_szSymbol;
? ? ?TradeRequest.volume ? ? ? ?= Volume;
? ? ?TradeRequest.type ? ? ? ? ?= (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
? ? ?TradeRequest.price ? ? ? ? = NormalizeDouble(Price, m_Infos.nDigits);
? ? ?TradeRequest.sl ? ? ? ? ? ?= NormalizeDouble(Stop, m_Infos.nDigits);
? ? ?TradeRequest.tp ? ? ? ? ? ?= NormalizeDouble(Take, m_Infos.nDigits);
? ? ?TradeRequest.type_time ? ? = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
? ? ?TradeRequest.stoplimit ? ? = 0;
? ? ?TradeRequest.expiration ? ?= 0;
? ? ?TradeRequest.type_filling ?= ORDER_FILLING_RETURN;
? ? ?TradeRequest.deviation ? ? = 1000;
? ? ?TradeRequest.comment ? ? ? = "Order Generated by Experts Advisor.";
? ? ?if(!OrderSend(TradeRequest, TradeResult))
? ? ? ?{
? ? ? ? MessageBox(StringFormat("Error Number: %d", TradeResult.retcode), "Nano EA");
? ? ? ? return 0;
? ? ? ?};
? ? ?return TradeResult.order;
? ? };
該函數(shù)非常簡單,就是為了安全而設(shè)計的。 我們將在這里創(chuàng)建一個 OCO(一筆取消其它)訂單,該訂單將被發(fā)送到交易服務(wù)器。 請注意,我們使用的是?LIMIT(限價)?或?STOP(破位)?訂單。 這是因為這類訂單更簡單,即使在價格突然波動的情況下也能保證執(zhí)行。
所采用用的訂單類型取決于交易工具的執(zhí)行價格和當(dāng)前價格,以及您入場操作是買入還是賣出。 這是通過以下方式實現(xiàn)的:
TradeRequest.type = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
通過在以下代碼行中指定交易工具,也可以創(chuàng)建 CROSS(交叉)訂單:
TradeRequest.symbol = m_szSymbol;
但在這樣做時,您還需要添加一些代碼,以便通過交叉訂單系統(tǒng)處理持倉或掛單,因為您會有一個“錯誤”的圖表。 我們來看一個示例。 您可以在完整指數(shù)圖表(IND)上交易迷你指數(shù)(WIN),但若您在 IND 圖表上使用 MetaTrader 5 時,它不會顯示持倉或 WIN 掛單。 因此,有必要添加代碼,從而令訂單可見。 這可以通過讀取持倉數(shù)值,并在圖表上用線條示意來實現(xiàn)。 這在交易和跟蹤品種交易歷史時非常有用。 例如,當(dāng)您使用 CROSS(交叉)訂單時,您可以依據(jù)?WIN$?圖表(迷你指數(shù)歷史圖表)交易?WIN(迷你指數(shù))。赫茲股票量化軟件
接下來,請注意以下代碼行:
? ? ?TradeRequest.price ? ? ? ? = NormalizeDouble(Price, m_Infos.nDigits);
? ? ?TradeRequest.sl ? ? ? ? ? ?= NormalizeDouble(Stop, m_Infos.nDigits);
? ? ?TradeRequest.tp ? ? ? ? ? ?= NormalizeDouble(Take, m_Infos.nDigits);
這三行將創(chuàng)建OCO訂單止損水平和持倉未平倉價格。 如果您交易的是短線訂單(可能只持續(xù)幾秒鐘),不使用 OCO 訂單是不可取的,因為波動會令價格在點位間跳轉(zhuǎn)時,沒有明確的方向。 當(dāng)您采用 OCO 時,交易服務(wù)器自身會關(guān)注我們的倉位。 OCO 訂單如下所示。

編輯搜圖
在編輯窗口中,相同的訂單如下所示:

編輯搜圖
一旦填完所有必填字段后,服務(wù)器將接管訂單。 一旦達到最大盈利或最大虧損,系統(tǒng)將平倉。 但若您沒有指定最大盈利或最大虧損,訂單可能會一直保持,直到另一個事件發(fā)生。 如果訂單類型設(shè)置為日內(nèi)交易,系統(tǒng)將在交易日結(jié)束時關(guān)閉。 否則,該筆持倉將繼續(xù)持有,直到您手動平倉,或者直到?jīng)]有更多資金來保有持倉。
一些智能交易系統(tǒng)使用訂單來平倉:一旦開倉,就會發(fā)送一筆逆反的訂單,在指定的點位平倉,且交易量相同。 但在某些情況下,這可能不起作用,因為如果資產(chǎn)在交易期間出于某種原因進入拍賣,則掛單可能會被取消,并應(yīng)予以替換。 這將另 EA 操作復(fù)雜化,因為您需要加入檢查哪些訂單處于有效狀態(tài),哪些訂單處于無效狀態(tài);如果出現(xiàn)任何錯誤,若無任何標準則 EA 將會一筆接一筆地發(fā)送訂單。赫茲股票量化軟件
? void Initilize(int nContracts, int FinanceTake, int FinanceStop, color cp, color ct, color cs, bool b1)
? ? {
? ? ?string sz0 = StringSubstr(m_szSymbol = _Symbol, 0, 3);
? ? ?double v1 = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) / SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
? ? ?m_Infos.Id = ChartID();
? ? ?m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER));
? ? ?m_Infos.nDigits = (int) SymbolInfoInteger(m_szSymbol, SYMBOL_DIGITS);
? ? ?m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN));
? ? ?m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume);
? ? ?m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume);
? ? ?m_Infos.IsDayTrade = b1;
? ? ?CreateHLine(m_Infos.szHLinePrice, m_Infos.cPrice = cp);
? ? ?CreateHLine(m_Infos.szHLineTake, m_Infos.cTake = ct);
? ? ?CreateHLine(m_Infos.szHLineStop, m_Infos.cStop = cs);
? ? ?ChartSetInteger(m_Infos.Id, CHART_COLOR_VOLUME, m_Infos.cPrice);
? ? ?ChartSetInteger(m_Infos.Id, CHART_COLOR_STOP_LEVEL, m_Infos.cStop);
? ? };
上面的例程負責(zé)初始化用戶指示的 EA 數(shù)據(jù) — 它創(chuàng)建一筆 OCO 訂單。 我們只需要在這個程序中做以下修改。
m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER));
在此,如果您需要一些特定的信息,我們將在當(dāng)前品種的基礎(chǔ)上添加交易品種類型。
? ? ?m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN));
? ? ?m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume);
? ? ?m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume);
以上三行是為了正確創(chuàng)建訂單而進行的必要調(diào)整。?nContracts?是一個杠桿系數(shù),選取 1、2、3 等值。 換句話說,您不需要知道要交易品種的最小交易量。 您真正需要的就是指出這個最小交易量的杠桿系數(shù)。 例如,如果所需的最小交易量為 5 份合同,并且您指定的杠桿系數(shù)為 3,則系統(tǒng)將開立 15 份合約的訂單。 基于用戶指定的參數(shù),另外兩行相應(yīng)地設(shè)置了止盈和止損。 級別隨訂單交易量調(diào)整:如果訂單增加,級別降低,反之亦然。 有了這段代碼,您在開倉時就不必進行計算 — EA 會自行計算所有東西:您指示 EA 交易的金融工具,杠桿系數(shù),您想賺多少錢,準備虧損多少錢,而 EA 將為您創(chuàng)建一筆相應(yīng)的訂單。
? inline void MoveTo(int X, int Y, uint Key)
? ? {
? ? ?int w = 0;
? ? ?datetime dt;
? ? ?bool bEClick, bKeyBuy, bKeySell;
? ? ?double take = 0, stop = 0, price;
? ? ?bEClick ?= (Key & 0x01) == 0x01; ? ?//Left mouse button click ? ? ?bKeyBuy ?= (Key & 0x04) == 0x04; ? ?//Pressed SHIFT ? ? ?bKeySell = (Key & 0x08) == 0x08; ? ?//Pressed CTRL ? ? ?ChartXYToTimePrice(m_Infos.Id, X, Y, w, dt, price);
? ? ?ObjectMove(m_Infos.Id, m_Infos.szHLinePrice, 0, 0, price = (bKeyBuy != bKeySell ? AdjustPrice(price) : 0));
? ? ?ObjectMove(m_Infos.Id, m_Infos.szHLineTake, 0, 0, take = price + (m_Infos.TakeProfit * (bKeyBuy ? 1 : -1)));
? ? ?ObjectMove(m_Infos.Id, m_Infos.szHLineStop, 0, 0, stop = price + (m_Infos.StopLoss * (bKeyBuy ? -1 : 1)));
? ? ?if((bEClick) && (bKeyBuy != bKeySell))
? ? ? ? CreateOrderPendent(bKeyBuy, m_Infos.Volume, price, take, stop, m_Infos.IsDayTrade);
? ? ?ObjectSetInteger(m_Infos.Id, m_Infos.szHLinePrice, OBJPROP_COLOR, (bKeyBuy != bKeySell ? m_Infos.cPrice : clrNONE));
? ? ?ObjectSetInteger(m_Infos.Id, m_Infos.szHLineTake, OBJPROP_COLOR, (take > 0 ? m_Infos.cTake : clrNONE));
? ? ?ObjectSetInteger(m_Infos.Id, m_Infos.szHLineStop, OBJPROP_COLOR, (stop > 0 ? m_Infos.cStop : clrNONE));
? ? };
上述代碼將顯示要創(chuàng)建的訂單。 它使用鼠標來顯示訂單將要放置的價位。 您還要通知 EA 是想買入(按住 SHIFT 鍵),還是想賣出(按住 CTRL 鍵)。 一旦單擊鼠標左鍵后,此時將創(chuàng)建一筆掛單。
如果您需要顯示更多數(shù)據(jù),例如盈虧平衡點,請將相關(guān)對象添加到代碼之中。
現(xiàn)在我們擁有了一個完整的 EA,它可以工作,并創(chuàng)建 OCO 訂單。 但這里的一切并非都是完美的...
問題出在 OCO 訂單
OCO 訂單存在一個問題,這并非?赫茲股票量化軟件系統(tǒng)或交易服務(wù)器的故障。 它與市場中不斷出現(xiàn)的波動性本身有關(guān)。 從理論上講,價格應(yīng)該是線性波動的,沒有回滾;但有時我們會遇到高波動性,這會在燭條內(nèi)部造成跳空缺口。 當(dāng)這些跳空缺口出現(xiàn)在止損或止盈訂單的價位時,這些點位將不會被觸發(fā),因此,將不會平倉。 當(dāng)用戶移動這些點位時,價格也可能超出止損和止盈形成的走廊。 在這種情況下,訂單也不會平倉。 這是一種非常危險的狀況,無法預(yù)測。 作為一名程序員,您必須提供一個相應(yīng)的機制,以盡量減少可能的危害。
為了刷新價格,并試圖將其維持在走廊內(nèi),我們將使用兩個子例程。 第一個如下:
? void UpdatePosition(void)
? ? {
? ? ?for(int i0 = PositionsTotal() - 1; i0 >= 0; i0--)
? ? ? ? if(PositionGetSymbol(i0) == m_szSymbol)
? ? ? ? ? {
? ? ? ? ? ?m_Take ? ? ?= PositionGetDouble(POSITION_TP);
? ? ? ? ? ?m_Stop ? ? ?= PositionGetDouble(POSITION_SL);
? ? ? ? ? ?m_IsBuy ? ? = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
? ? ? ? ? ?m_Volume ? ?= PositionGetDouble(POSITION_VOLUME);
? ? ? ? ? ?m_Ticket ? ?= PositionGetInteger(POSITION_TICKET);
? ? ? ? ? }
? ? };
它將在?OnTrade?中被調(diào)用,即 MetaTrader 5 在每次持倉變化時調(diào)用的函數(shù)。 下一個要用到的子例程則由?OnTick?調(diào)用。 它檢查并確保價格在走廊范圍內(nèi),或在 OCO 訂單的范圍內(nèi)。 其如下所示:
? inline bool CheckPosition(const double price = 0, const int factor = 0)
? ? {
? ? ?double last;
? ? ?if(m_Ticket == 0)
? ? ? ? return false;
? ? ?last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST);
? ? ?if(m_IsBuy)
? ? ? ?{
? ? ? ? if((last > m_Take) || (last < m_Stop))
? ? ? ? ? ?return ClosePosition();
? ? ? ? if((price > 0) && (price >= last))
? ? ? ? ? ?return ClosePosition(factor);
? ? ? ?}
? ? ?else ? ? ? ?{
? ? ? ? if((last < m_Take) || (last > m_Stop))
? ? ? ? ? ?return ClosePosition();
? ? ? ? if((price > 0) && (price <= last))
? ? ? ? ? ?return ClosePosition(factor);
? ? ? ?}
? ? ?return false;
? ? };
這個代碼片段非常關(guān)鍵,因為它將在每次即時報價變化時執(zhí)行,因此它必須盡可能簡單,以便盡可能高效地執(zhí)行計算和測試。 請注意,雖然我們將價格維持在走廊內(nèi),但我們也會檢查一些有趣的東西;如果需要,可以刪除這些東西。 我將在下一章節(jié)中解釋這個附加測試。 在這個子程序中,我們有以下函數(shù)調(diào)用: