幣圈量化:數字貨幣多平臺對衝穩定套利 V2.1 (註釋版)

步入量化學習艾莉絲發表於2018-11-10

多平臺對衝穩定套利 V2.1 (註釋版)

對衝策略是風險較小,較為穩健的一類策略,和俗稱“搬磚策略”有些類似,區別是搬磚需要轉移資金,提幣 ,充幣。在這個過程中容易出現價格波動引起虧損。對衝是通過在不同市場同時買賣交易,在交易所資金分配上實現把幣“搬”到價格低的,把錢“流向”價格高的交易所,實現盈利。 程式邏輯流程


幣圈量化:數字貨幣多平臺對衝穩定套利 V2.1 (註釋版)


註釋版原始碼:

var initState;
var isBalance = true;
var feeCache = new Array();
var feeTimeout = optFeeTimeout * 60000;
var lastProfit = 0;                       // 全域性變數 記錄上次盈虧
var lastAvgPrice = 0;
var lastSpread = 0;
var lastOpAmount = 0;
function adjustFloat(v) {                 // 處理資料的自定義函式 ,可以把引數 v 處理 返回 保留3位小數(floor向下取整)
    return Math.floor(v*1000)/1000;       // 先乘1000 讓小數位向左移動三位,向下取整 整數,捨去所有小數部分,再除以1000 , 小數點向右移動三位,即保留三位小數。
}

function isPriceNormal(v) {               // 判斷是否價格正常, StopPriceL 是跌停值,StopPriceH 是漲停值,在此區間返回 true  ,超過這個 區間 認為價格異常 返回false
    return (v >= StopPriceL) && (v <= StopPriceH);  // 在此區間
}

function stripTicker(t) {                           // 根據引數 t , 格式化 輸出關於t的資料。
    return 'Buy: ' + adjustFloat(t.Buy) + ' Sell: ' + adjustFloat(t.Sell);
}

function updateStatePrice(state) {        // 更新 價格
    var now = (new Date()).getTime();     // 記錄 當前時間戳
    for (var i = 0; i < state.details.length; i++) {    // 根據傳入的引數 state(getExchangesState 函式的返回值),遍歷 state.details
        var ticker = null;                              // 宣告一個 變數 ticker
        var key = state.details[i].exchange.GetName() + state.details[i].exchange.GetCurrency();  // 獲取當前索引 i  的 元素,使用其中引用的交易所物件 exchange ,呼叫GetName、GetCurrency函式
                                                                                                  // 交易所名稱 + 幣種 字串 賦值給 key ,作為鍵
        var fee = null;                                                                           // 宣告一個變數 Fee
        while (!(ticker = state.details[i].exchange.GetTicker())) {                               // 用當前 交易所物件 呼叫 GetTicker 函式獲取 行情,獲取失敗,執行迴圈
            Sleep(Interval);                                                                      // 執行 Sleep 函式,暫停 Interval 設定的毫秒數
        }

        if (key in feeCache) {                                                                    // 在feeCache 中查詢,如果找到 key
            var v = feeCache[key];                                                                // 取出 鍵名為 key 的變數值
            if ((now - v.time) > feeTimeout) {                                                    // 根據行情的記錄時間 和 now 的差值,如果大於 手續費更新週期
                delete feeCache[key];                                                             // 刪除 過期的 費率 資料
            } else {
                fee = v.fee;                                                                      // 如果沒大於更新週期, 取出v.fee 賦值給 fee
            }
        }
        if (!fee) {                                                                               // 如果沒有找到 fee 還是初始的null , 則觸發if 
            while (!(fee = state.details[i].exchange.GetFee())) {                                 // 呼叫 當前交易所物件 GetFee 函式 獲取 費率
                Sleep(Interval);
            }
            feeCache[key] = {fee: fee, time: now};                                                // 在費率快取 資料結構 feeCache 中儲存 獲取的 fee 和 當前的時間戳
        }
        // Buy-=fee Sell+=fee
        state.details[i].ticker = {Buy: ticker.Buy * (1-(fee.Sell/100)), Sell: ticker.Sell * (1+(fee.Buy/100))};   // 通過對行情價格處理 得到排除手續費後的 價格用於計算差價
        state.details[i].realTicker = ticker;                                                                      // 實際的 行情價格
        state.details[i].fee = fee;                                                                                // 費率
    }
}

function getProfit(stateInit, stateNow, coinPrice) {                // 獲取 當前計算盈虧的函式 
    var netNow = stateNow.allBalance + (stateNow.allStocks * coinPrice);          // 計算當前賬戶的總資產市值
    var netInit =  stateInit.allBalance + (stateInit.allStocks * coinPrice);      // 計算初始賬戶的總資產市值
    return adjustFloat(netNow - netInit);                                         // 當前的 減去 初始的  即是 盈虧,return 這個盈虧
}

function getExchangesState() {                                      // 獲取 交易所狀態 函式
    var allStocks = 0;                                              // 所有的幣數
    var allBalance = 0;                                             // 所有的錢數
    var minStock = 0;                                               // 最小交易 幣數
    var details = [];                                               // details 儲存詳細內容 的陣列。
    for (var i = 0; i < exchanges.length; i++) {                    // 遍歷 交易所物件陣列
        var account = null;                                         // 每次 迴圈宣告一個 account 變數。
        while (!(account = exchanges[i].GetAccount())) {            // 使用exchanges 陣列內的 當前索引值的 交易所物件,呼叫其成員函式,獲取當前交易所的賬戶資訊。返回給 account 變數,!account為真則一直獲取。
            Sleep(Interval);                                        // 如果!account 為真,即account獲取失敗,則呼叫Sleep 函式 暫停 Interval 設定的 毫秒數 時間,重新迴圈,直到獲取到有效的賬戶資訊。 
        }
        allStocks += account.Stocks + account.FrozenStocks;         // 累計所有 交易所幣數
        allBalance += account.Balance + account.FrozenBalance;      // 累計所有 交易所錢數
        minStock = Math.max(minStock, exchanges[i].GetMinStock());  // 設定最小交易量minStock  為 所有交易所中 最小交易量最大的值
        details.push({exchange: exchanges[i], account: account});   // 把每個交易所物件 和 賬戶資訊 組合成一個物件壓入陣列 details 
    }
    return {allStocks: adjustFloat(allStocks), allBalance: adjustFloat(allBalance), minStock: minStock, details: details};   // 返回 所有交易所的 總幣數,總錢數 ,所有最小交易量中的最大值, details陣列
}

function cancelAllOrders() {                                        // 取消所有訂單函式
    for (var i = 0; i < exchanges.length; i++) {                    // 遍歷交易所物件陣列(就是在新建機器人時新增的交易所,對應的物件)
        while (true) {                                              // 遍歷中每次進入一個 while 迴圈
            var orders = null;                                      // 宣告一個 orders 變數,用來接收 API 函式 GetOrders  返回的 未完成的訂單 資料。
            while (!(orders = exchanges[i].GetOrders())) {          // 使用 while 迴圈 檢測 API 函式 GetOrders 是否返回了有效的資料(即 如果 GetOrders 返回了null 會一直執行while 迴圈,並重新檢測)
                                                                    // exchanges[i] 就是當前迴圈的 交易所物件,我們通過呼叫API GetOrders (exchanges[i] 的成員函式) ,獲取未完成的訂單。 
                Sleep(Interval);                                    // Sleep 函式根據 引數 Interval 的設定 ,讓程式暫停 設定的 毫秒數(1000毫秒 = 1秒)。
            }

            if (orders.length == 0) {                               // 如果 獲取到的未完成的訂單陣列 非null , 即通過上邊的while 迴圈, 但是 orders.length 等於 0(空陣列,沒有掛單了)。  
                break;                                              // 執行 break 跳出 當前的 while 迴圈(即 沒有要取消的訂單)
            }

            for (var j = 0; j < orders.length; j++) {               // 遍歷orders  陣列, 根據掛出 訂單ID,逐個呼叫 API 函式 CancelOrder 撤銷掛單 
                exchanges[i].CancelOrder(orders[j].Id, orders[j]);
            }
        }
    }
}

function balanceAccounts() {          // 平衡交易所 賬戶 錢數 幣數
    // already balance
    if (isBalance) {                  // 如果 isBalance 為真 , 即 平衡狀態,則無需平衡,立即返回
        return;
    }

    cancelAllOrders();                // 在平衡前 要先取消所有交易所的掛單

    var state = getExchangesState();  // 呼叫 getExchangesState 函式 獲取所有交易所狀態(包括賬戶資訊)
    var diff = state.allStocks - initState.allStocks;      // 計算當前獲取的交易所狀態中的 總幣數與初始狀態總幣數 只差(即 初始狀態 和 當前的 總幣差)
    var adjustDiff = adjustFloat(Math.abs(diff));          // 先呼叫 Math.abs 計算 diff 的絕對值,再呼叫自定義函式 adjustFloat 保留3位小數。 
    if (adjustDiff < state.minStock) {                     // 如果 處理後的 總幣差資料 小於 滿足所有交易所最小交易量的資料 minStock,即不滿足平衡條件
        isBalance = true;                                  // 設定 isBalance 為 true ,即平衡狀態
    } else {                                               //  adjustDiff >= state.minStock  的情況 則:
        Log('初始幣總數量:', initState.allStocks, '現在幣總數量: ', state.allStocks, '差額:', adjustDiff);
        // 輸出要平衡的資訊。
        // other ways, diff is 0.012, bug A only has 0.006 B only has 0.006, all less then minstock
        // we try to statistical orders count to recognition this situation
        updateStatePrice(state);                           // 更新 ,並獲取 各個交易所行情
        var details = state.details;                       // 取出 state.details 賦值給 details
        var ordersCount = 0;                               // 宣告一個變數 用來記錄訂單的數量
        if (diff > 0) {                                    // 判斷 幣差 是否大於 0 , 即 是否是 多幣。賣掉多餘的幣。
            var attr = 'Sell';                             // 預設 設定 即將獲取的 ticker 屬性為 Sell  ,即 賣一價
            if (UseMarketOrder) {                          // 如果 設定 為 使用市價單, 則 設定 ticker 要獲取的屬性 為 Buy 。(通過給atrr賦值實現)
                attr = 'Buy';
            }
            // Sell adjustDiff, sort by price high to low
            details.sort(function(a, b) {return b.ticker[attr] - a.ticker[attr];}); // return 大於0,則 b 在前,a在後, return 小於0 則 a 在前 b在後,陣列中元素,按照 氣泡排序進行。
                                                                                    // 此處 使用 b - a ,進行排序就是 details 陣列 從高到低排。
            for (var i = 0; i < details.length && adjustDiff >= state.minStock; i++) {     // 遍歷 details 陣列 
                if (isPriceNormal(details[i].ticker[attr]) && (details[i].account.Stocks >= state.minStock)) {    // 判斷 價格是否異常, 並且 當前賬戶幣數是否大於最小可以交易量
                    var orderAmount = adjustFloat(Math.min(AmountOnce, adjustDiff, details[i].account.Stocks));
                    // 給下單量 orderAmount 賦值 , 取 AmountOnce 單筆交易數量, 幣差 , 當前交易所 賬戶 幣數 中的 最小的。   因為details已經排序過,開始的是價格最高的,這樣就是從最高的交易所開始出售
                    var orderPrice = details[i].realTicker[attr] - SlidePrice;               // 根據 實際的行情價格(具體用賣一價Sell 還是 買一價Buy 要看UseMarketOrder的設定了)
                                                                                             // 因為是要下賣出單 ,減去滑價 SlidePrice 。設定好下單價格
                    if ((orderPrice * orderAmount) < details[i].exchange.GetMinPrice()) {    // 判斷 當前索引的交易所的最小交易額度 是否 足夠本次下單的 金額。
                        continue;                                                            // 如果小於 則 跳過 執行下一個索引。
                    }
                    ordersCount++;                                                           // 訂單數量 計數 加1
                    if (details[i].exchange.Sell(orderPrice, orderAmount, stripTicker(details[i].ticker))) {   // 按照 以上程式既定的 價格 和 交易量 下單, 並且輸出 排除手續費因素後處理過的行情資料。
                        adjustDiff = adjustFloat(adjustDiff - orderAmount);                  // 如果 下單API 返回訂單ID , 根據本次既定下單量更新 未平衡的量
                    }
                    // only operate one platform                                             // 只在一個平臺 操作平衡,所以 以下 break 跳出本層for迴圈
                    break;
                }
            }
        } else {                                           // 如果 幣差 小於0 , 即 缺幣  要進行補幣操作
            var attr = 'Buy';                              // 同上
            if (UseMarketOrder) {
                attr = 'Sell';
            }
            // Buy adjustDiff, sort by sell-price low to high
            details.sort(function(a, b) {return a.ticker[attr] - b.ticker[attr];});           // 價格從小到大 排序,因為從價格最低的交易所 補幣
            for (var i = 0; i < details.length && adjustDiff >= state.minStock; i++) {        // 迴圈 從價格小的開始
                if (isPriceNormal(details[i].ticker[attr])) {                                 // 如果價格正常 則執行  if {} 內程式碼
                    var canRealBuy = adjustFloat(details[i].account.Balance / (details[i].ticker[attr] + SlidePrice));
                    var needRealBuy = Math.min(AmountOnce, adjustDiff, canRealBuy);
                    var orderAmount = adjustFloat(needRealBuy * (1+(details[i].fee.Buy/100)));  // 因為買入扣除的手續費 是 幣數,所以 要把手續費計算在內。
                    var orderPrice = details[i].realTicker[attr] + SlidePrice;
                    if ((orderAmount < details[i].exchange.GetMinStock()) ||
                        ((orderPrice * orderAmount) < details[i].exchange.GetMinPrice())) {
                        continue;
                    }
                    ordersCount++;
                    if (details[i].exchange.Buy(orderPrice, orderAmount, stripTicker(details[i].ticker))) {
                        adjustDiff = adjustFloat(adjustDiff - needRealBuy);
                    }
                    // only operate one platform
                    break;
                }
            }
        }
        isBalance = (ordersCount == 0);                                                         // 是否 平衡, ordersCount  為 0 則 ,true
    }

    if (isBalance) {
        var currentProfit = getProfit(initState, state, lastAvgPrice);                          // 計算當前收益
        LogProfit(currentProfit, "Spread: ", adjustFloat((currentProfit - lastProfit) / lastOpAmount), "Balance: ", adjustFloat(state.allBalance), "Stocks: ", adjustFloat(state.allStocks));
        // 列印當前收益資訊
        if (StopWhenLoss && currentProfit < 0 && Math.abs(currentProfit) > MaxLoss) {           // 超過最大虧損停止程式碼塊
            Log('交易虧損超過最大限度, 程式取消所有訂單後退出.');
            cancelAllOrders();                                                                  // 取消所有 掛單
            if (SMSAPI.length > 10 && SMSAPI.indexOf('http') == 0) {                            // 簡訊通知 程式碼塊
                HttpQuery(SMSAPI);
                Log('已經簡訊通知');
            }
            throw '已停止';                                                                      // 丟擲異常 停止策略
        }
        lastProfit = currentProfit;                                                             // 用當前盈虧數值 更新 上次盈虧記錄
    }
}

function onTick() {                  // 主要迴圈
    if (!isBalance) {                // 判斷 全域性變數 isBalance 是否為 false  (代表不平衡), !isBalance 為 真,執行 if 語句內程式碼。
        balanceAccounts();           // 不平衡 時執行 平衡賬戶函式 balanceAccounts()
        return;                      // 執行完返回。繼續下次迴圈執行 onTick
    }

    var state = getExchangesState(); // 獲取 所有交易所的狀態
    // We also need details of price
    updateStatePrice(state);         // 更新 價格, 計算排除手續費影響的對衝價格值

    var details = state.details;     // 取出 state 中的 details 值
    var maxPair = null;              // 最大   組合
    var minPair = null;              // 最小   組合
    for (var i = 0; i < details.length; i++) {      //  遍歷 details 這個陣列
        var sellOrderPrice = details[i].account.Stocks * (details[i].realTicker.Buy - SlidePrice);    // 計算 當前索引 交易所 賬戶幣數 賣出的總額(賣出價為對手買一減去滑價)
        if (((!maxPair) || (details[i].ticker.Buy > maxPair.ticker.Buy)) && (details[i].account.Stocks >= state.minStock) &&
            (sellOrderPrice > details[i].exchange.GetMinPrice())) { // 首先判斷maxPair 是不是 null ,如果不是null 就判斷 排除手續費因素後的價格 大於 maxPair中行情資料的買一價
                                                                    // 剩下的條件 是 要滿足最小可交易量,並且要滿足最小交易金額,滿足條件執行以下。
            details[i].canSell = details[i].account.Stocks;         // 給當前索引的 details 陣列的元素 增加一個屬性 canSell 把 當前索引交易所的賬戶 幣數 賦值給它
            maxPair = details[i];                                   // 把當前的 details 陣列元素 引用給 maxPair 用於 for 迴圈下次對比,對比出最大的價格的。
        }

        var canBuy = adjustFloat(details[i].account.Balance / (details[i].realTicker.Sell + SlidePrice));   // 計算 當前索引的 交易所的賬戶資金 可買入的幣數
        var buyOrderPrice = canBuy * (details[i].realTicker.Sell + SlidePrice);                             // 計算 下單金額
        if (((!minPair) || (details[i].ticker.Sell < minPair.ticker.Sell)) && (canBuy >= state.minStock) && // 和賣出 部分尋找 最大價格maxPair一樣,這裡尋找最小价格
            (buyOrderPrice > details[i].exchange.GetMinPrice())) {
            details[i].canBuy = canBuy;                             // 增加 canBuy 屬性記錄   canBuy
            // how much coins we real got with fee                  // 以下要計算 買入時 收取手續費後 (買入收取的手續費是扣幣), 實際要購買的幣數。
            details[i].realBuy = adjustFloat(details[i].account.Balance / (details[i].ticker.Sell + SlidePrice));   // 使用 排除手續費影響的價格 計算真實要買入的量
            minPair = details[i];                                   // 符合條件的 記錄為最小价格組合 minPair
        }
    }

    if ((!maxPair) || (!minPair) || ((maxPair.ticker.Buy - minPair.ticker.Sell) < MaxDiff) ||         // 根據以上 對比出的所有交易所中最小、最大價格,檢測是否不符合對衝條件
    !isPriceNormal(maxPair.ticker.Buy) || !isPriceNormal(minPair.ticker.Sell)) {
        return;                                                                                       // 如果不符合 則返回
    }

    // filter invalid price
    if (minPair.realTicker.Sell <= minPair.realTicker.Buy || maxPair.realTicker.Sell <= maxPair.realTicker.Buy) {   // 過濾 無效價格, 比如 賣一價 是不可能小於等於 買一價的。
        return;
    }

    // what a ****...
    if (maxPair.exchange.GetName() == minPair.exchange.GetName()) {                                   // 資料異常,同時 最低 最高都是一個交易所。
        return;
    }

    lastAvgPrice = adjustFloat((minPair.realTicker.Buy + maxPair.realTicker.Buy) / 2);                // 記錄下 最高價  最低價 的平均值
    lastSpread = adjustFloat((maxPair.realTicker.Sell - minPair.realTicker.Buy) / 2);                 // 記錄  買賣 差價

    // compute amount                                                                                 // 計算下單量
    var amount = Math.min(AmountOnce, maxPair.canSell, minPair.realBuy);                              // 根據這幾個 量取最小值,用作下單量
    lastOpAmount = amount;                                                                            // 記錄 下單量到 全域性變數
    var hedgePrice = adjustFloat((maxPair.realTicker.Buy - minPair.realTicker.Sell) / Math.max(SlideRatio, 2))  // 根據 滑價係數 ,計算對衝 滑價  hedgePrice
    if (minPair.exchange.Buy(minPair.realTicker.Sell + hedgePrice, amount * (1+(minPair.fee.Buy/100)), stripTicker(minPair.realTicker))) { // 先下 買單
        maxPair.exchange.Sell(maxPair.realTicker.Buy - hedgePrice, amount, stripTicker(maxPair.realTicker));                               // 買單下之後 下賣單
    }

    isBalance = false;                                                                                // 設定為 不平衡,下次帶檢查 平衡。
}

function main() {                                         // 策略的入口函式
    if (exchanges.length < 2) {                           // 首先判斷 exchanges 策略新增的交易所物件個數,  exchanges 是一個交易所物件陣列,我們判斷其長度 exchanges.length,如果小於2執行{}內程式碼
        throw "交易所數量最少得兩個才能完成對衝";              // 丟擲一個錯誤,程式停止。
    }

    TickInterval = Math.max(TickInterval, 50);            // TickInterval 是介面上的引數, 檢測頻率, 使用JS 的數學物件Math ,呼叫 函式 max 來限制 TickInterval 的最小值 為 50 。 (單位 毫秒)
    Interval = Math.max(Interval, 50);                    // 同上,限制 出錯重試間隔 這個介面引數, 最小為50 。(單位 毫秒)

    cancelAllOrders();                                    // 在最開始的時候 不能有任何掛單。所以 會檢測所有掛單 ,並取消所有掛單。

    initState = getExchangesState();                      // 呼叫自定義的 getExchangesState 函式獲取到 所有交易所的資訊, 賦值給 initState 
    if (initState.allStocks == 0) {                       // 如果 所有交易所 幣數總和為0  ,丟擲錯誤。
        throw "所有交易所貨幣數量總和為空, 必須先在任一交易所建倉才可以完成對衝";
    }
    if (initState.allBalance == 0) {                      // 如果 所有交易所 錢數總和為0  ,丟擲錯誤。
        throw "所有交易所CNY數量總和為空, 無法繼續對衝";
    }

    for (var i = 0; i < initState.details.length; i++) {  // 遍歷獲取的交易所狀態中的 details陣列。
        var e = initState.details[i];                     // 把當前索引的交易所資訊賦值給e 
        Log(e.exchange.GetName(), e.exchange.GetCurrency(), e.account);   // 呼叫e 中引用的 交易所物件的成員函式 GetName , GetCurrency , 和 當前交易所資訊中儲存的 賬戶資訊 e.account  用Log 輸出。 
    }

    Log("ALL: Balance: ", initState.allBalance, "Stocks: ", initState.allStocks, "Ver:", Version());  // 列印日誌 輸出 所有新增的交易所的總錢數, 總幣數, 託管者版本


    while (true) {                                        // while 迴圈
        onTick();                                         // 執行主要 邏輯函式 onTick 
        Sleep(parseInt(TickInterval));
    }
}
複製程式碼

策略解讀

多平臺對衝2.1 策略 可以實現 多個 數字貨幣現貨平臺的對衝交易,程式碼比較簡潔,具備基礎的對衝功能。由於該版本是基礎教學版本,所以優化空間比較大,對於初學發明者量化策略程式編寫的新使用者、新開發者可以很好的提供一種策略編寫思路範例,能快速的學習到策略編寫的一些技巧,對於掌握量化策略編寫技術很有幫助。


策略可以實盤,不過由於是最基礎教學版本,可擴充套件性還很大,對於掌握了思路的同學也可以嘗試 重構 該策略。築就非凡量化世界 www.botvs.com/bbs-topic/9…


閱讀原文

相關文章