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