多平臺對沖穩定套利 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