區塊鏈資產量化策略之 多平臺對沖穩定套利 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 fuck...
    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 策略 可以實現 多個 數字貨幣現貨平臺的對沖交易,代碼比較簡潔,具備基礎的對沖功能。由於該版本是基礎教學版本,所以優化空間比較大,對於初學BotVS 策略程序編寫的新用戶、新開發者可以很好的提供一種策略編寫思路範例,能快速的學習到策略編寫的一些技巧,對於掌握量化策略編寫技術很有幫助。

    策略可以實盤,不過由於是最基礎教學版本,可擴展性還很大,對於掌握了思路的同學也可以嘗試 重構 該策略。

    https://dn-filebox.qbox.me/9eae75080bdad97c6bdbd46dbb9a9295606c01ad.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章