多平台对冲稳定套利 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 策略 可以实现 多个 数字货币现货平台的对冲交易,代码比较简洁,具备基础的对冲功能。由于该版本是基础教学版本,所以优化空间比较大,对于初学发明者量化策略程序编写的新用户、新开发者可以很好的提供一种策略编写思路范例,能快速的学习到策略编写的一些技巧,对于掌握量化策略编写技术很有帮助。
策略可以实盘,不过由于是最基础教学版本,可扩展性还很大,对于掌握了思路的同学也可以尝试 重构 该策略。筑就非凡量化世界 https://www.botvs.com/bbs-topic/987
来源:oschina
链接:https://my.oschina.net/u/3949328/blog/2874462