對衝策略是風險較小,較爲穩健的一類策略,和俗稱「搬磚策略」有些相似,區別是搬磚須要轉移資金,提幣 ,充幣。在這個過程當中容易出現價格波動引發虧損。對衝是經過在不一樣市場同時買賣交易,在交易所資金分配上實現把幣「搬」到價格低的,把錢「流向」價格高的交易所,實現盈利。 程序邏輯流程javascript
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 策略 能夠實現 多個 數字貨幣現貨平臺的對衝交易,代碼比較簡潔,具有基礎的對衝功能。因爲該版本是基礎教學版本,因此優化空間比較大,對於初學發明者量化策略程序編寫的新用戶、新開發者能夠很好的提供一種策略編寫思路範例,能快速的學習到策略編寫的一些技巧,對於掌握量化策略編寫技術頗有幫助。html
策略能夠實盤,不過因爲是最基礎教學版本,可擴展性還很大,對於掌握了思路的同窗也能夠嘗試 重構 該策略。築就非凡量化世界 https://www.botvs.com/bbs-topic/987java
閱讀原文數組