帶你學習硬核策略 之 「C++ 版 OKEX合約對衝策略」

帶你學習硬核策略 之 「C++ 版 OKEX合約對衝策略」web

提及對衝策略,在各個市場中有多種多樣的策略、多種多樣的組合、多種多樣的思路。咱們從最經典的跨期對衝來初探對衝策略的設計思路、理念。現在,數字貨幣市場活躍度遠比市場造成之初高了不少,也出現了不少合約交易所,提供了大量可套利對衝的機會。現貨跨市場套利、期現對衝套利、期貨跨期套利、期貨跨市場套利等等,策略層出不窮。下面咱們就一塊兒 領略一個 由C++語言編寫,交易市場 爲OKEX合約交易所的「硬核」跨期對衝策略,策略基於「發明者量化交易平臺」編寫。json

策略原理websocket

爲何說策略有些硬核,緣由在於 策略使用C++ 語言編寫,策略閱讀難度略高。可是並不妨礙讀者學習到這個策略設計、思路方面的精華。策略通篇較爲簡潔,代碼長度適中,只有500多行。行情數據獲取方面,不一樣於以往策略使用 rest 接口,該策略使用了websocket接口,接受交易所行情推送。
設計方面,策略結構合理,代碼耦合度很低,很方便擴展或者優化。邏輯思路清晰,這樣的設計不只使用、擴展很方便。做爲教學策略,學習策略設計也是一個很好的範例。策略原理比較簡單,即 遠期合約 和近期合約 作正、反套對衝,基本原理上和商品期貨的跨期對衝一致。
正套,作空遠期合約,作多近期合約。
反套,作多遠期合約,作空近期合約。
清楚了基本原理,剩下的就是策略如何觸發對衝開倉,如何平倉,如何加倉。倉位控制方式,策略細節處理。
對衝策略主要關注的是標的物差價的波動,對差價作迴歸交易。然而,差價是有可能小幅震盪,或者大幅震盪,或者單邊一個方向。
這就帶來了對衝盈虧的不肯定性,可是風險仍是遠小於單邊趨勢的。對於跨期策略的各類優化不少都是選擇從倉位控制層面入手,從開倉平倉觸發上入手。例如經典的用布林指標作爲差價波動時,正套、反套的開倉、平倉點。本策略因爲設計合理、耦合度很低,也能夠很容易的修改爲布林指標跨期對衝策略。

策略代碼剖析數據結構

通篇大概看一下代碼,能夠總結出,代碼大概主要分爲四個部分。

- 1.枚舉值定義,定義一些狀態值,用於標記狀態。一些和策略思路無關的功能函數,例如 url 編碼函數,時間轉換函數等,這些函數和策略思路沒有關係,僅僅是對於數據作處理使用。
- 2.K線數據生成器類:策略由該生成器類對象生成的K線數據驅動。
- 3.對衝類:該類的對象能夠執行具體的交易邏輯,對衝操做、策略細節的處理機制等。
- 4.策略主函數,也就是 main 函數。main 函數是策略的入口函數,主要循環在此函數內執行,此外這個函數內還執行一個重要的操做,即訪問交易所的websocket 接口,獲取推送來的tick行情數據,做爲K線數據生成器的原料數據。socket

經過對策略代碼總體上的瞭解,咱們下面就能夠經過逐步剖析各個環節,從而完整的學習該策略的設計、思路、技巧。

枚舉值定義,其它功能函數函數

一、枚舉類型 State 聲明
工具

    enum State {                    // 枚舉類型  定義一些 狀態
        STATE_NA,                   // 非正常狀態
        STATE_IDLE,                 // 空閒
        STATE_HOLD_LONG,            // 持多倉
        STATE_HOLD_SHORT,           // 持空倉
    };

 




由於代碼中有些函數返回某個狀態,因此這些狀態都定義在了枚舉類型 State中。
看到代碼中出現 STATE_NA 即爲 非正常狀態, STATE_IDLE 爲空閒狀態,便可以對衝操做的狀態。 STATE_HOLD_LONG 爲持有正套對衝倉位的狀態。 STATE_HOLD_SHORT 爲持有反套對衝倉位的狀態。

二、字符串替換,本策略中沒有調用,算是一個備用的工具函數,主要處理字符串。

string replace(string s, const string from, const string& to)


三、功能爲轉換爲十六進制字符的函數 toHex

inline unsigned char toHex(unsigned char x)


四、處理url編碼的函數

std::string urlencode(const std::string& str)


五、時間轉換函數,把字符串格式的時間轉換爲時間戳。

uint64_t _Time(string &s)


K線數據生成器類
oop

    class BarFeeder {                                                                       // K線 數據生成器類
        public:
            BarFeeder(int period) : _period(period) {                                       // 構造函數,參數爲 period 週期, 初始化列表中初始化
                _rs.Valid = true;                                                           // 構造函數體中初始化 K線數據的 Valid屬性。
            }    

            void feed(double price, Chart *c=nullptr, int chartIdx=0) {                     // 輸入數據,nullptr 空指針類型,chartIdx 索引默認參數爲 0
                uint64_t epoch = uint64_t(Unix() / _period) * _period * 1000;               // 秒級時間戳祛除不完整時間週期(不完整的_period 秒數),轉爲 毫秒級時間戳。
                bool newBar = false;                                                        // 標記 新K線Bar 的標記變量
                if (_rs.size() == 0 || _rs[_rs.size()-1].Time < epoch) {                    // 若是 K線數據 長度爲 0 。 或者 最後一bar 的時間戳小於 epoch(K線最後一bar 比當前最近的週期時間戳還要靠前)
                    Record r;                                                               // 聲明一個 K線bar 結構
                    r.Time = epoch;                                                         // 構造當前週期的K線bar 
                    r.Open = r.High = r.Low = r.Close = price;                              // 初始化 屬性
                    _rs.push_back(r);                                                       // K線bar 壓入 K線數據結構
                    if (_rs.size() > 2000) {                                                // 若是K線數據結構長度超過 2000 , 就剔除最先的數據。
                        _rs.erase(_rs.begin());
                    }
                    newBar = true;                                                          // 標記
                } else {                                                                    // 其它狀況,不是出現新bar 的狀況下的處理。
                    Record &r = _rs[_rs.size() - 1];                                        // 引用 數據中最後一bar 的數據。
                    r.High = max(r.High, price);                                            // 對引用數據的最高價更新操做。
                    r.Low = min(r.Low, price);                                              // 對引用數據的最低價更新操做。
                    r.Close = price;                                                        // 對引用數據的收盤價更新操做。
                }
        
                auto bar = _rs[_rs.size()-1];                                               // 取最後一柱數據 ,賦值給 bar 變量
                json point = {bar.Time, bar.Open, bar.High, bar.Low, bar.Close};            // 構造一個 json 類型數據
                if (c != nullptr) {                                                         // 圖表對象指針不等於 空指針,執行如下。
                   if (newBar) {                                                            // 根據標記判斷,若是出現新Bar 
                        c->add(chartIdx, point);                                            // 調用圖表對象成員函數add,向圖表對象中插入數據(新增K線bar)
                        c->reset(1000);                                                     // 只保留1000 bar的數據
                    } else {
                        c->add(chartIdx, point, -1);                                        // 不然就更新(不是新bar),這個點(更新這個bar)。
                    } 
                }
            }
            Records & get() {                                                               // 成員函數,獲取K線數據的方法。
                return _rs;                                                                 // 返回對象的私有變量 _rs 。(即 生成的K線數據)
            }
        private:
            int _period;
            Records _rs;
    };

 


這個類主要就是負責把獲取到的 tick 數據加工成差價K線,用於驅動策略對衝邏輯。
可能有的讀者有疑問,爲何要用 tick 數據呢?爲何要構造一個 這樣的K線數據生成器呢?直接用K線數據很差麼?這樣的疑問三連發,在我當初寫一些對衝策略的時候也有迸發出來過。在寫過差價布林對衝策略時就找到了答案。因爲單個合約的K線數據是這一個合約在必定週期內的價格變化統計。
而兩個合約的差價 的K線數據則是在必定週期內的差價價格變化統計,所以不能簡單的拿兩個合約各自的K線數據作減法運算,計算每根K線Bar上各個數據的差值,當作差價。這樣最明顯的錯誤就例如,兩個合約的最高價、最低價,並不必定是同一時刻的。因此相減出來的數值沒有太大意義。
所以咱們須要使用實時的tick數據,實時計算差價,實時統計成必定週期內的價格變更(即K線柱上的高開低收)。這樣咱們就須要一個K線數據生成器,單獨做爲一個類,很好的進行處理邏輯分離。

對衝類學習

    class Hedge {                                                                           // 對衝類,策略主要邏輯。
      public:
        Hedge() {                                                                           // 構造函數
            ...
        };
        
        State getState(string &symbolA, Depth &depthA, string &symbolB, Depth &depthB) {        // 獲取狀態,參數: 合約A名稱 、合約A深度數據, 合約B名稱、 合約B深度數據
            
            ...
        }
        bool Loop(string &symbolA, Depth &depthA, string &symbolB, Depth &depthB, string extra="") {       // 開平倉 策略主要邏輯
            
            ...
        }    

      private:
        vector<double> _addArr;                                     // 對衝加倉列表
        string _state_desc[4] = {"NA", "IDLE", "LONG", "SHORT"};    // 狀態值 描述信息
        int _countOpen = 0;                                 // 開倉次數
        int _countCover = 0;                                // 平倉次數
        int _lastCache = 0;                                 // 
        int _hedgeCount = 0;                                // 對衝次數
        int _loopCount = 0;                                 // 循環計數(循環累計次數)
        double _holdPrice = 0;                              // 持倉價格
        BarFeeder _feederA = BarFeeder(DPeriod);            // A合約 行情 K線生成器
        BarFeeder _feederB = BarFeeder(DPeriod);            // B合約 行情 K線生成器
        State _st = STATE_NA;                               // 對衝類型 對象的 對衝持倉狀態
        string _cfgStr;                                     // 圖表配置 字符串
        double _holdAmount = 0;                             // 持倉量
        bool _isCover = false;                              // 是否平倉 標記
        bool _needCheckOrder = true;                        // 設置是否 檢查訂單
        Chart _c = Chart("");                               // 圖表對象,並初始化
    };

 



因爲代碼比較長,省略了一部分,主要顯示一下這個對衝類的結構,構造函數Hedge就不說了,主要是對象初始化。剩下,主要有2個功能函數。
- getState

這個函數主要處理訂單檢測,訂單撤銷,倉位檢測,倉位平衡等工做。由於在對衝交易過程當中,是避免不了單腿的狀況(即一個合約成交了,一個合約沒有成交),若是在下單邏輯中進行檢測,而後處理追單或者平倉,策略邏輯會比較亂。因此設計這部分的時候採起了另外一種思路。若是觸發對衝操做,下單一次,不管是否出現單腿對衝的狀況,默認是對衝成功了,而後在getState函數中檢測倉位平衡,獨立出來檢測處理平衡的這個邏輯。

- Loop

策略的交易邏輯封裝在這個函數中,其中調用 getState , 使用K線數據生成器對象生成 差價的K線數據,進行對衝開倉、平倉、加倉邏輯的判斷。還有一些對於圖表的數據更新操做。

策略主函數優化

 

    void main() {  
    
        ...
        
        string realSymbolA = exchange.SetContractType(symbolA)["instrument"];    // 獲取設置的A合約(this_week / next_week / quarter ) ,在 OKEX 合約 當週、次周、季度 對應的真實合約ID 。
        string realSymbolB = exchange.SetContractType(symbolB)["instrument"];    // ...
        
        string qs = urlencode(json({{"op", "subscribe"}, {"args", {"futures/depth5:" + realSymbolA, "futures/depth5:" + realSymbolB}}}).dump());    // 對 ws 接口的要傳的參數進行 json 編碼、 url 編碼
        Log("try connect to websocket");                                                                                                            // 打印鏈接 WS接口的信息。
        auto ws = Dial("wss://real.okex.com:10442/ws/v3|compress=gzip_raw&mode=recv&reconnect=true&payload="+qs);     // 調用FMZ API Dial 函數 訪問  OKEX 期貨的 WS 接口
        Log("connect to websocket success");
        
        Depth depthA, depthB;                               // 聲明兩個 深度數據結構的變量 用於儲存A合約和B合約 的深度數據
        auto fillDepth = [](json &data, Depth &d) {         // 用接口返回的json 數據,構造 Depth 數據的代碼。
            d.Valid = true;
            d.Asks.clear();
            d.Asks.push_back({atof(string(data["asks"][0][0]).c_str()), atof(string(data["asks"][0][1]).c_str())});
            d.Bids.clear();
            d.Bids.push_back({atof(string(data["bids"][0][0]).c_str()), atof(string(data["bids"][0][1]).c_str())});
        };
        string timeA;   // 時間 字符串 A 
        string timeB;   // 時間 字符串 B 
        while (true) {
            auto buf = ws.read();                           // 讀取 WS接口 推送來的數據
            
            ...
            
    }

 




策略啓動後是從main 函數開始執行,策略在main 函數的初始化工做中,訂閱了websocket 接口的tick行情。main 函數最主要的工做就是構造了一個主循環,不停的接收交易所websocket 接口推送來的tick行情,而後調用對衝類對象的成員函數: Loop 函數。由行情數據 驅動 Loop 函數中的交易邏輯。
須要說明的一點,上文中說道的tick行情,實際是訂閱的訂單薄深度數據接口,獲取的是每檔的訂單薄數據。可是策略中只是使用了第一檔的數據,其實就和tick行情數據差很少了,策略中並無用其它檔的數據,也沒有用第一檔的訂單量數值。
詳細看下 策略是如何訂閱 websocket 接口的數據,又是如何設置的。


string qs = urlencode(json({{"op", "subscribe"}, {"args", {"futures/depth5:" + realSymbolA, "futures/depth5:" + realSymbolB}}}).dump());
Log("try connect to websocket");
auto ws = Dial("wss://real.okex.com:10442/ws/v3|compress=gzip_raw&mode=recv&reconnect=true&payload="+qs);
Log("connect to websocket success");


首先要對訂閱的接口所傳的訂閱消息 json 參數進行 url 編碼,也就是 payload 參數的值。而後比較重要的一步就是調用 發明者量化交易平臺的 API 接口函數 Dial 函數。Dial 函數可用於訪問交易所 websocket 接口。咱們這裏進行一些設置,讓即將建立的 websocket 鏈接控制對象 ws 具備斷線自動重連(訂閱消息依然使用 payload 參數的值 qs字符串),實現這個功能就須要在 Dial 函數的參數字符串中增長配置選項。

Dial函數參數的開頭部分以下:


wss://real.okex.com:10442/ws/v3

是須要訪問的websocket接口地址,以後用 | 分隔。
compress=gzip_raw&mode=recv&reconnect=true&payload="+qs 都是配置參數。

這樣設置後,即便websocket 鏈接斷開,發明者量化交易平臺 託管者底層系統也會進行自動重連,及時地獲取到最新的行情數據。
抓住每次差價波動,快速捕獲到合適的對衝行情。

倉位控制

倉位控制採用相似 「波菲納契」數列的對衝倉位比例,進行控制。

  for (int i = 0; i < AddMax + 1; i++) {                                          // 構造 控制加倉數量的數據結構,相似 波菲納契數列 對衝數量 比例。
      if (_addArr.size() < 2) {                                                   // 前兩次加倉量變化爲: 加一倍對衝數量 遞增
          _addArr.push_back((i+1)*OpenAmount);
      }
      _addArr.push_back(_addArr[_addArr.size()-1] + _addArr[_addArr.size()-2]);   // 最後 兩個加倉數量相加,算出當前的加倉數量儲存到 _addArr數據結構中。
  }

 



能夠看到每次增長的加倉倉位數量都是最近的上兩個倉位的和。
這樣的倉位控制能夠實現差價越大,套利對衝數量相對增長,對倉位進行分散,從而把握住小差價波動小倉位,大差價波動倉位適當增大。

平倉:止損止盈

固定的止盈差價,止損差價。
持倉差價到達止盈位置、止損位置即進行止盈、止損。

入市、離市 週期設計

參數 NPeriod 控制的週期對策略的開倉平倉進行必定的動態控制。

策略圖表

策略自動生成差價 K線圖表,標記相關交易信息。

C++策略自定義圖表畫圖操做,也很是簡便,能夠看到在 對衝類的構造函數中,咱們使用了寫好的圖表配置字符串_cfgStr配置給了圖表對象_c , _c 是對衝類 私有成員初始化時調用發明者量化自定義圖表API接口函數 Chart 函數構造的圖表對象。

  _cfgStr = R"EOF(
  [{
  "extension": { "layout": "single", "col": 6, "height": "500px"},
  "rangeSelector": {"enabled": false},
  "tooltip": {"xDateFormat": "%Y-%m-%d %H:%M:%S, %A"},
  "plotOptions": {"candlestick": {"color": "#d75442", "upColor": "#6ba583"}},
  "chart":{"type":"line"},
  "title":{"text":"Spread Long"},
  "xAxis":{"title":{"text":"Date"}},
  "series":[
      {"type":"candlestick", "name":"Long Spread","data":[], "id":"dataseriesA"},
      {"type":"flags","data":[], "onSeries": "dataseriesA"}
      ]
  },
  {
  "extension": { "layout": "single", "col": 6, "height": "500px"},
  "rangeSelector": {"enabled": false},
  "tooltip": {"xDateFormat": "%Y-%m-%d %H:%M:%S, %A"},
  "plotOptions": {"candlestick": {"color": "#d75442", "upColor": "#6ba583"}},
  "chart":{"type":"line"},
  "title":{"text":"Spread Short"},
  "xAxis":{"title":{"text":"Date"}},
  "series":[
      {"type":"candlestick", "name":"Long Spread","data":[], "id":"dataseriesA"},
      {"type":"flags","data":[], "onSeries": "dataseriesA"}
      ]
  }
  ]
  )EOF";
  _c.update(_cfgStr);                 // 用圖表配置 更新圖表對象
  _c.reset();                         // 重置圖表數據。

 




- 調用 _c.update(_cfgStr); 使用 _cfgStr 配置到圖表對象。
- 調用 _c.reset(); 重置圖表數據。

在策略代碼須要給圖表插入數據時,也是經過 直接調用 _c 對象的成員函數,或者 把 _c 的引用做爲參數傳遞,進而調用_c的對象成員函數(方法)去進行圖表數據更新,插入操做。
例如:

_c.add(chartIdx, {{"x", UnixNano()/1000000}, {"title", action}, {"text", format("diff: %f", opPrice)}, {"color", color}});

下單交易後,在K線圖表上打上標籤記錄。

以下,繪製K線是經過 調用 BarFeeder 類的 成員函數 feed 時,把圖表對象 _c 的引用 做爲參數 傳入。

void feed(double price, Chart *c=nullptr, int chartIdx=0)

即feed函數的形參 c 。

  json point = {bar.Time, bar.Open, bar.High, bar.Low, bar.Close};            // 構造一個 json 類型數據
  if (c != nullptr) {                                                         // 圖表對象指針不等於 空指針,執行如下。
     if (newBar) {                                                            // 根據標記判斷,若是出現新Bar 
          c->add(chartIdx, point);                                            // 調用圖表對象成員函數add,向圖表對象中插入數據(新增K線bar)
          c->reset(1000);                                                     // 只保留1000 bar個數據
      } else {
          c->add(chartIdx, point, -1);                                        // 不然就更新(不是新bar),這個點(更新這個bar)。
      } 
  }

 



經過調用 圖表對象 _c 的add成員函數,向圖表中插入新的K線Bar數據。
代碼:c->add(chartIdx, point);

回測


本策略僅爲學習交流使用,實盤使用時請根據實盤實際狀況,自行修改優化。

策略地址:https://www.fmz.com/strategy/163447

更多有趣的策略盡在「發明者量化交易平臺」 : https://www.fmz.com

相關文章
相關標籤/搜索