BotVS 数字货币 多平台对冲稳定套利 V2.1 (注释版)

2023-02-22 19:22发布

多平台对冲稳定套利 V2.1 (注释版)对冲策略是风险较小,较为稳健的一类策略,和俗称“搬砖策略”有些类似,区别是搬砖需要转移资金,提币 ,充币。在这个过程中容
1条回答
1楼 · 2023-02-22 20:18.采纳回答

多平台对冲稳定套利 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 策略程序编写的新用户、新开发者可以很好的提供一种策略编写思路范例,能快速的学习到策略编写的一些技巧,对于掌握量化策略编写技术很有帮助。

策略可以实盘,不过由于是最基础教学版本,可扩展性还很大,对于掌握了思路的同学也可以尝试 重构 该策略。


由 发明者量化(FMZ.COM) 授权首发刊载