帶你學習硬核策略 之 「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