策略介绍 - 网格可以自定义方向 - 先买后卖: 网格会从首价格开始向下挂买单, 每个买单间隔 “价格间隔” 这个参数, 挂单数量为”单笔数量”, 挂够 “总数量” 个买单, 有任意买单成交以后, 程序会在买价基础上加 “价差(元)” 这个参数的的值的价格挂出卖单, 卖出, 卖出以后,重新按原来这个网格的价格挂买入单 - 先卖后买: 操作刚好相反 - 策略最大的风险就是单边行情, 价格波动超出网格范围. - 网格带有自动止损和移动功能
策略使用了 虚拟挂单设计, 对于交易所 限制挂单数量,做出了很棒的 处理,灵活的解决了该问题。
网格逻辑设计灵活,结构巧妙。
盈亏计算,各个数值统计 算法可借鉴,各个条件检测设计严谨。(能尽量 减少BUG 出现可能)
源代码 非常值得学习。
”`
/* 界面参数 (代码中体现为全局变量)
变量 描述 类型 默认值
OpType 网格方向 下拉框(selected) 先买后卖|先卖后买
FirstPriceAuto 首价格自动 布尔型(true/false) true
FirstPrice@!FirstPriceAuto 首价格 数字型(number) 100
AllNum 总数量 数字型(number) 10
PriceGrid 价格间隔 数字型(number) 1
PriceDiff 价差(元) 数字型(number) 2
AmountType 订单大小 下拉框(selected) 买卖同量|自定义量
AmountOnce@AmountType==0 单笔数量 数字型(number) 0.1
BAmountOnce@AmountType==1 买单大小 数字型(number) 0.1
SAmountOnce@AmountType==1 卖单大小 数字型(number) 0.1
AmountCoefficient@AmountType==0 量差 字符串(string) *1
AmountDot 量小数点最长位数 数字型(number) 3
EnableProtectDiff 开启价差保护 布尔型(true/false) false
ProtectDiff@EnableProtectDiff 入市价差保护 数字型(number) 20
CancelAllWS 停止时取消所有挂单 布尔型(true/false) true
CheckInterval 轮询间隔 数字型(number) 2000
Interval 失败重试间隔 数字型(number) 1300
RestoreProfit 恢复上次盈利 布尔型(true/false) false
LastProfit@RestoreProfit 上次盈利 数字型(number) 0
ProfitAsOrg@RestoreProfit 上次盈利算入均价 布尔型(true/false) false
EnableAccountCheck 启用资金检验 布尔型(true/false) true
EnableStopLoss@EnableAccountCheck 开启止损 布尔型(true/false) false
StopLoss@EnableStopLoss 最大浮动亏损(元) 数字型(number) 100
StopLossMode@EnableStopLoss 止损后操作 下拉框(selected) 回收并退出|回收再撒网
EnableStopWin@EnableAccountCheck 开启止盈 布尔型(true/false) false
StopWin@EnableStopWin 最大浮动盈利(元) 数字型(number) 120
StopWinMode@EnableStopWin 止盈后操作 下拉框(selected) 回收并退出|回收再撒网
AutoMove@EnableAccountCheck 自动移动 布尔型(true/false) false
MaxDistance@AutoMove 最大距离(元) 数字型(number) 20
MaxIdle@AutoMove 最大空闲(秒) 数字型(number) 7200
EnableDynamic 开启动态挂单 布尔型(true/false) false
DynamicMax@EnableDynamic 订单失效距离(元) 数字型(number) 30
ResetData 启动时清空所有数据 布尔型(true/false) true
Precision 价格小数位长度 数字型(number) 5
*/
function hasOrder(orders, orderId) { // 检测 参数 orders 中 是否有 ID 为 orderId 的订单 for (var i = 0; i < orders.length; i++) { // 遍历 orders 检测 是否 有相同的 id , 找到 返回 true if (orders[i].Id == orderId) { return true; } } return false; // 全部遍历完 ,没有触发 if 则 没有找到 ID 为 orderId 的订单, 返回 false }
function cancelPending() { // 取消所有挂单 函数 var ret = false; // 设置 返回成功 标记变量 while (true) { // while 循环 if (ret) { // 如果 ret 为 true 则 Sleep 一定时间 Sleep(Interval); } var orders = _C(exchange.GetOrders); // 调用 API 获取 交易所 未完成的订单信息 if (orders.length == 0) { // 如果返回的是 空数组, 即 交易所 没有未完成的订单。 break; // 跳出 while 循环 }
for (var j = 0; j < orders.length; j++) { // 遍历 未完成的 订单数组, 并根据索引j 逐个使用 orders[j].Id 去 取消订单。
exchange.CancelOrder(orders[j].Id, orders[j]);
ret = true; // 一旦有取消操作, ret 赋值 为 true 。用于触发 以上 Sleep , 等待后重新 exchange.GetOrders 检测
}
}
return ret; // 返回 ret
}
function valuesToString(values, pos) { // 值 转换 为字符串 var result = “; // 声明一个用于返回的 空字符串 result if (typeof(pos) === ‘undefined’) { // 如果 没有传入 pos 这个参数 ,给 pos 赋值 0 pos = 0; } for (var i = pos; i < values.length; i++) { // 根据 传入的 pos 处理 values 数组 if (i > pos) { // 除了第一次 循环 之后 在result 字符串后 加上 ‘ ’ 一个空格 result += ‘ ‘; } if (values[i] === null) { // 如果 values (函数 参数列表 数组) 当前的索引的 元素 为 null 则 result 添加 ‘null’字符串 result += ‘null’; } else if (typeof(values[i]) == ‘undefined’) { // 如果 是 未定义的, 则添加 ‘undefined’ result += ‘undefined’; } else { // 剩余类型 做 switch 检测 分别处理 switch (values[i].constructor.name) { // 检查 values[i] 的 constructor 的 name 属性, 即类型 名称 case ‘Date’: case ‘Number’: case ‘String’: case ‘Function’: result += values[i].toString(); // 如果是 日期类型、数值类型、字符串类型、函数类型 ,调用其 toString 函数 转换为字符串 后,添加 break; default: result += JSON.stringify(values[i]); // 其他情况 则 使用JSON.stringify 函数 转换为 JSON 字符串 添加到 result break; } } } return result; // 返回 result }
function Trader() { // Trader 函数 ,使用闭包。
var vId = 0; // 订单递增ID
var orderBooks = []; // 订单薄
var hisBooks = []; // 历史订单薄
var orderBooksLen = 0; // 订单薄长度
this.Buy = function(price, amount, extra) { // 买函数, 参数: 价格 、数量、扩展信息
if (typeof(extra) === ‘undefined’) { // 如果参数 extra 未传入,即 typeof 返回 undefined
extra = “; // 给 extra 赋值空字符串
} else {
extra = valuesToString(arguments, 2); // 调用 this.Buy 函数 时传入的参数 arguments ,传入 valuesToString函数中
}
vId++; //
var orderId = “V” + vId; //
orderBooks[orderId] = { // 向订单薄 数组中添加 属性 orderId, 用构造的 对象 对其初始化。
Type: ORDER_TYPE_BUY, // 构造的对象 Type 属性: 类型 买单
Status: ORDER_STATE_PENDING, // 状态 挂起
Id: 0, // 订单ID 0
Price: price, // 价格 参数 price
Amount: amount, // 订单量 参数 amount
Extra: extra // 扩展信息 经valuesToString 处理过的字符串。
};
orderBooksLen++; // 订单薄的长度 累计加1
return orderId; // 返回 本次构造的订单的 orderId (非交易所订单ID ,别混淆。)
};
this.Sell = function(price, amount, extra) { // 和 thie.Buy 基本类似, 构造卖单。
if (typeof(extra) === ‘undefined’) {
extra = “;
} else {
extra = valuesToString(arguments, 2);
}
vId++;
var orderId = “V” + vId;
orderBooks[orderId] = {
Type: ORDER_TYPE_SELL,
Status: ORDER_STATE_PENDING,
Id: 0,
Price: price,
Amount: amount,
Extra: extra
};
orderBooksLen++;
return orderId;
};
this.GetOrders = function() { // 获取未完成的订单信息
var orders = _C(exchange.GetOrders); // 调用 API GetOrders 获取 未完成的订单信息 赋值给 orders
for (orderId in orderBooks) { // 遍历 Trader 对象中的 orderBooks
var order = orderBooks[orderId]; // 根据 orderId 取出 订单
if (order.Status !== ORDER_STATE_PENDING) { // 如果 order 的状态不等于 挂起状态 ,就跳过本次循环
continue;
}
var found = false; // 初始化 found 变量(标记 是否找到) 为 true
for (var i = 0; i < orders.length; i++) { // 遍历 API 返回的未完成的订单的数据
if (orders[i].Id == order.Id) { // 找到 和 orderBooks 中 未完成订单 , id 相同的订单时,给found 赋值 true,代表找到。
found = true;
break; // 跳出当前循环
}
}
if (!found) { // 如果 没有找到,则 向 orders push orderBooks[orderId]。
orders.push(orderBooks[orderId]); // 为何要这样 push ?
}
}
return orders; // 返回 orders
}
this.GetOrder = function(orderId) { // 获取订单
if (typeof(orderId) === ‘number’) { // 如果传入的 参数 orderId 是数值类型
return exchange.GetOrder(orderId); // 调用 API GetOrder 根据 orderId 获取 订单信息并返回。
}
if (typeof(hisBooks[orderId]) !== ‘undefined’) { // typeof(hisBooks[orderId]) 如果不等于 未定义的
return hisBooks[orderId]; // 返回 hisBooks 中 属性为 orderId 的数据
}
if (typeof(orderBooks[orderId]) !== ‘undefined’) { // 同上, orderBooks 中如果有 属性为 orderId的值存在, 返回这个数据。
return orderBooks[orderId];
}
return null; // 如果不符合上述条件触发, 返回 null
};
this.Len = function() { // 返回 Trader 的 orderBookLen 变量, 即返回订单薄长度。
return orderBooksLen;
};
this.RealLen = function() { // 返回 订单薄中 激活订单数量。
var n = 0; // 初始计数 为 0
for (orderId in orderBooks) { // 遍历 订单薄
if (orderBooks[orderId].Id > 0) { // 如果 在遍历中 当前 的订单的 Id 大于0 ,即 非初始时的0, 表明订单已下单,该订单已经激活。
n++; // 累计 已经激活的订单
}
}
return n; // 返回 n值, 即返回 真实 订单薄长度。(已激活订单数量)
};
this.Poll = function(ticker, priceDiff) { //
var orders = _C(exchange.GetOrders); // 获取 所有未完成的订单
for (orderId in orderBooks) { // 遍历 订单薄
var order = orderBooks[orderId]; // 取出当前 的订单 赋值给 order
if (order.Id > 0) { // 如果订单 为 激活状态,即 order.Id 不为0(已经下过单)
var found = false; // 变量 found(标记找到) 为 false
for (var i = 0; i < orders.length; i++) { // 在交易所返回的 未完成订单信息中 查找 相同的订单号
if (order.Id == orders[i].Id) { // 如果查找到, 给found 赋值 true ,代表已找到。
found = true;
}
}
if (!found) { // 如果当前的 orderId 代表的订单 没有在 交易所返回的未完成订单数组orders中找到对应的。
order.Status = ORDER_STATE_CLOSED; // 给 orderBooks 中对应 orderId 的订单(即当前的order变量)更新,Status 属性更新为 ORDER_STATE_CLOSED (即 已关闭)
hisBooks[orderId] = order; // 完成的订单 记录在 历史订单薄里,即 hisBooks ,统一,且唯一的订单号 orderId
delete(orderBooks[orderId]); // 删除 订单薄的 名为 orderId值的 属性。(完成的订单 从中 删除)
orderBooksLen–; // 订单薄 长度自减
continue; // 以下代码 跳过继续循环。
}
}
var diff = _N(order.Type == ORDER_TYPE_BUY ? (ticker.Buy - order.Price) : (order.Price - ticker.Sell));
// diff 为 当前订单薄 中 订单的 计划开仓价和 当前实时开仓价格的差值。
var pfn = order.Type == ORDER_TYPE_BUY ? exchange.Buy : exchange.Sell; // 根据订单的类型,给 pfn 赋值相应的 API 函数 引用。
// 即 如果 order 的类型是买单 , pfn 就是 exchange.Buy 函数的引用, 卖单同理。
if (order.Id == 0 && diff <= priceDiff) { // 如果 订单薄中的订单 order 没有激活(即Id 等于0 ) 并且 当前价格距离 订单计划价格 小于等于 参数传入的 priceDiff
var realId = pfn(order.Price, order.Amount, order.Extra + "(距离: " + diff + (order.Type == ORDER_TYPE_BUY ? (" 买一: " + ticker.Buy) : (" 卖一: " + ticker.Sell))+")");
// 执行下单函数 ,参数传入 价格、数量、 订单扩展信息 + 挂单距离 + 行情数据(买一 或者 卖一),返回 交易所 订单id
if (typeof(realId) === 'number') { // 如果 返回的 realId 是数值类型
order.Id = realId; // 赋值给 订单薄 当前的订单 order的 Id 属性。
}
} else if (order.Id > 0 && diff > (priceDiff + 1)) { // 如果订单 处于激活状态, 并且 当前距离 大于 参数传入的 距离
var ok = true; // 声明一个 用于标记的变量 初始 true
do { // 先执行 do 再判断 while
ok = true; // ok 赋值 true
exchange.CancelOrder(order.Id, "不必要的" + (order.Type == ORDER_TYPE_BUY ? "买单" : "卖单"), "委托价:", order.Price, "量:", order.Amount, ", 距离:", diff, order.Type == ORDER_TYPE_BUY ? ("买一: " + ticker.Buy) : ("卖一: " + ticker.Sell));
// 取消 当前 超出 范围的挂单, 在取消订单这条日志后 打印 当前订单的信息、当前 距离 diff。
Sleep(200); // 等待 200 毫秒
orders = _C(exchange.GetOrders); // 调用 API 获取 交易所 中 未完成的订单。
for (var i = 0; i < orders.length; i++) { // 遍历 这些未完成的订单。
if (orders[i].Id == order.Id) { // 如果找到 取消的订单 还在 交易所未完成的订单数组中
ok = false; // 给 ok 这个变量赋值 false , 即没有 取消成功
}
}
} while (!ok); // 如果 ok 为 false,则 !ok 为 true ,while 就会继续重复循环,继续取消这个订单,并检测是否取消成功
order.Id = 0; // 给 order.Id 赋值 0 , 代表 当前这个订单 是未激活的。
}
}
};
}
function balanceAccount(orgAccount, initAccount) { // 平衡账户 函数 参数 策略启动时最初始的账户信息 , 本次撒网前初始账户信息 cancelPending(); // 调用自定义函数 cancelPending() 取消所有挂单。 var nowAccount = _C(exchange.GetAccount); // 声明一个 变量 nowAccount 用来记录 此刻 账户的最新信息。 var slidePrice = 0.2; // 设置下单时 的滑价 为 0.2 var ok = true; // 标记变量 初始 true while (true) { // while 循环 var diff = _N(nowAccount.Stocks - initAccount.Stocks); // 计算出 当前 账户 和 初始账户 的币差 diff if (Math.abs(diff) < exchange.GetMinStock()) { // 如果 币差的绝对值 小于 交易所 的最小交易量,break 跳出循环,不进行平衡操作。 break; } var depth = _C(exchange.GetDepth); // 获取 交易所深度信息 赋值给 声明的 depth 变量 var books = diff > 0 ? depth.Bids : depth.Asks; // 根据 币差 的大于0 或者 小于 0 ,提取 depth 中的 买单数组 或者 卖单数组(等于0 不会处理,在判断小于GetMinStock 的时候已经break) // 币差大于0 要卖出平衡,所以看买单数组, 币差小于0 相反。 var n = 0; // 声明 n 初始为 0 var price = 0; // 声明 price 初始 0 for (var i = 0; i < books.length; i++) { // 遍历 买单 或者 卖单 数组 n += books[i].Amount; // 根据 遍历的索引 i , 累计每次的 订单的Amount (订单量) if (n >= Math.abs(diff)) { // 如果 累计的 订单量 n 大于等于 币差,则: price = books[i].Price; // 获取 当前索引的订单的 价格,赋值给 price break; // 跳出 当前 for 遍历循环 } } var pfn = diff > 0 ? exchange.Sell : exchange.Buy; // 根据 币差 大于0 或者 小于 0 , 将 下卖单 API(exchange.Sell) 或者 下买单 API(exchange.Buy) 引用传递给 声明的 pfn var amount = Math.abs(diff); // 将要平衡操作的 下单量 为 diff 即 币差, 赋值给 声明的 amount 变量 var price = diff > 0 ? (price - slidePrice) : (price + slidePrice); // 根据币差 决定的 买卖方向 ,在 price 的基础上 增加 或者 减去 滑价(滑价是为了更容易成交),再赋值给 price Log(“开始平衡”, (diff > 0 ? “卖出” : “买入”), amount, “个币”); // 输出 日志 平衡的 币数。 if (diff > 0) { // 根据币差 决定的 买卖方向 , 检测账户币数 或者 钱数是否足够。 amount = Math.min(nowAccount.Stocks, amount); // 确保下单量 amount 不会超过 当前 账户 的可用币数。 } else { amount = Math.min(nowAccount.Balance / price, amount); // 确保下单量 amount 不会超过 当前 账户 的可用钱数。 } if (amount < exchange.GetMinStock()) { // 检测 最终下单数量 是否 小于 交易所 允许的最小下单量 Log(“资金不足, 无法平衡到初始状态”); // 如果 下单量过小,则打印 信息。 ok = false; // 标记 平衡失败 break; // 跳出 while 循环 } pfn(price, amount); // 执行 下单 API (pfn 引用) Sleep(1000); // 暂停 1 秒 cancelPending(); // 取消所有挂单。 nowAccount = _C(exchange.GetAccount); // 获取当前 最新账户信息 } if (ok) { // 当 ok 为 true (平衡成功) 时执行 花括号内代码 LogProfit(_N(nowAccount.Balance - orgAccount.Balance)); // 用传入的参数 orgAccount (平衡前的账户信息)的Balance 属性 减去当前的 账户信息的 Balance 属性,即 钱数之差, // 也就是 盈亏 (因币数不变,略有误差 因为有些很小的 量不能平衡) Log(“平衡完成”, nowAccount); // 输出日志 平衡完成。 } }
var STATE_WAIT_OPEN = 0; // 用于 fishTable 中每个 节点的 状态 var STATE_WAIT_COVER = 1; // … var STATE_WAIT_CLOSE = 2; // … var ProfitCount = 0; // 盈亏次数 记录 var BuyFirst = true; // 初始 界面参数 var IsSupportGetOrder = true; // 交易所 是否支持 GetOrder API 函数, 全局变量, 用于 main 函数开始的判断 var LastBusy = 0; // 记录上次 处理的时间对象
function setBusy() { // 设置 Busy 时间 LastBusy = new Date(); // 给 LastBusy 赋值当前的时间对象 }
function isTimeout() { // 判断是否超时 if (MaxIdle <= 0) { // 最大空闲时间(基于 是否自动 移动网格), 如果 最大空闲时间 MaxIdle 设置小于等于0 return false; // 返回 false, 不判断 超时。即 总是返回false 未超时。 } var now = new Date(); // 获取当前时间对象 if (((now.getTime() - LastBusy.getTime()) / 1000) >= MaxIdle) { // 使用当前时间对象的 getTime 函数 获取时间戳 与 LastBusy 的时间戳 计算差值, // 除以1000 算出 两个时间对象间 相差的秒数。 判断是否大于 最大空闲时间MaxIdle LastBusy = now; // 如果是大于, 更新 LastBusy 为当前时间对象 now return true; // 返回 true ,即超时。 } return false; // 返回 false 未超时 }
function onexit() { // 程序 退出 时的收尾函数。 if (CancelAllWS) { // 设置了 停止时取消所有挂单,则 调用 cancelPending() 函数 取消所有挂单 Log(“正在退出, 尝试取消所有挂单”); cancelPending(); } Log(“策略成功停止”); Log(_C(exchange.GetAccount)); // 打印退出程序时的 账户持仓信息。 }
function fishing(orgAccount, fishCount) { // 撒网 参数 : 账户信息 ,撒网次数 setBusy(); // 设置 LastBuys 为当前 时间戳 var account = _C(exchange.GetAccount); // 声明一个 account 变量 , 获取当前 账户信息 并 赋值。 Log(account); // 输出 本次调用 fishing 函数 开始 时的账户信息。 var InitAccount = account; // 声明一个 变量 InitAccount 并用 account 赋值。 此处是 记录 本次 撒网 前的 初始账户资金,用于计算 浮动盈亏。 var ticker = _C(exchange.GetTicker); // 获取 行情 赋值给 声明的 ticker 变量 var amount = _N(AmountOnce); // 根据 界面参数 单笔数量,使用 _N 处理小数位(_N 默认 保留2位),赋值给 amount 。 var amountB = [amount]; // 声明一个 变量 叫 amountB 是一个数组,用 amount 初始化 一个元素 var amountS = [amount]; // 声明一个 变量 叫 amountS … if (typeof(AmountType) !== ‘undefined’ && AmountType == 1) { // 按自定义量 ,订单大小类型 这个界面参数如果不是未定义的, //并且 AmountType 在界面上设定为 自定义量,即AmountType 值为 1 (下拉框的索引) for (var idx = 0; idx < AllNum; idx++) { // AllNum 总数量。 如果是设置自定义量, 根据总数量 循环一定次数 给amountB/amountS 即买卖单量数组赋值 amountB[idx] = BAmountOnce; // 使用界面参数 给买单量数组 赋值 amountS[idx] = SAmountOnce; // … 给卖单… } } else { // 其它 for (var idx = 1; idx < AllNum; idx++) { // 根据网格总数量 循环。 switch (AmountCoefficient[0]) { // 根据界面参数 差量 这个字符串的 第一个 字符,即 AmountCoefficient[0] 是 ‘+‘、’-‘、’‘、’/’ case ‘+’: // 根据 界面参数 进行 构造 下单量加法递增的网格。 amountB[idx] = amountB[idx - 1] + parseFloat(AmountCoefficient.substring(1)); break; case ‘-’: // … amountB[idx] = amountB[idx - 1] - parseFloat(AmountCoefficient.substring(1)); break; case ‘’: amountB[idx] = amountB[idx - 1] * parseFloat(AmountCoefficient.substring(1)); break; case ‘/’: amountB[idx] = amountB[idx - 1] / parseFloat(AmountCoefficient.substring(1)); break; } amountB[idx] = _N(amountB[idx], AmountDot); // 买单 、买单 量相同,处理好数据小数位。 amountS[idx] = amountB[idx]; // 赋值。 } } if (FirstPriceAuto) { // 如果界面参数设置了 首价格自动 为 true ,执行 if 花括号内代码。 FirstPrice = BuyFirst ? _N(ticker.Buy - PriceGrid, Precision) : _N(ticker.Sell + PriceGrid, Precision); // 界面参数 FirstPrice 根据 BuyFirst全局变量(声明初始为true,在main开始已经根据OpType赋值)设定第一个价格,用此刻行情 ticker 和 界面参数 PriceGrid 价格间距去设定。 } // Initialize fish table 初始化网格 var fishTable = {}; // 声明一个 网格对象 var uuidTable = {}; // 识别码 表格对象 var needStocks = 0; // 所需币数 变量 var needMoney = 0; // 所需 钱 变量 var actualNeedMoney = 0; // 实际需要的 钱 var actualNeedStocks = 0; // 实际需要的 币 var notEnough = false; // 资金不足 标记变量, 初始设置为false var canNum = 0; // 可用 网格 for (var idx = 0; idx < AllNum; idx++) { // 根据 网格数 AllNum 去遍历 构造。 var price = _N((BuyFirst ? FirstPrice - (idx * PriceGrid) : FirstPrice + (idx * PriceGrid)), Precision); // 遍历构造时,当前的索引idx 的 价格 设置 根据 BuyFirst 去设置。 每个索引价格 之间的间距 为 PriceGrid . needStocks += amountS[idx]; // 卖出所需 币数 随着 循环逐步 累计。(由 卖单量数组逐个累计到 needStocks) needMoney += price * amountB[idx]; // 买入所需 钱数 随着 循环逐步 累计。(…. 买单量数组逐个累计…) if (BuyFirst) { // 处理 先买 if (_N(needMoney) <= _N(account.Balance)) { // 如果 网格所需的钱 小于 账户上的可用钱数 actualNeedMondy = needMoney; // 赋值给 实际所需要的钱数 actualNeedStocks = needStocks; // 赋值给 实际所需要的币数 该出有些问题? canNum++; // 累计 可用网格数 } else { // _N(needMoney) <= _N(account.Balance) 该条件不满足,则设置 资金不足标记变量 为 true notEnough = true; } } else { // 处理 先卖 if (_N(needStocks) <= _N(account.Stocks)) { // 检测 所需币数 是不是 小于 账户 可用币数 actualNeedMondy = needMoney; // 赋值 actualNeedStocks = needStocks; canNum++; // 累计可用网格数 } else { notEnough = true; // 不满足资金条件 ,就设置 true } } fishTable[idx] = STATE_WAIT_OPEN; // 根据当前索引idx,设置网格对象的idx成员(网格结点)的状态,初始为STATE_WAIT_OPEN(等待开仓) uuidTable[idx] = -1; // 编号对象 也根据当前 idx 初始化 自己的idx 值(对应 fishTable 的节点)为 -1 } if (!EnableAccountCheck && (canNum < AllNum)) { // 如果不启用资金检验, 并且 可开 节点 小于 界面参数设置的网格数量(节点总数)时。 Log(“警告, 当前资金只可做”, canNum, “个网格, 全网共需”, (BuyFirst ? needMoney : needStocks), “请保持资金充足”); // Log 输出 警告信息。 canNum = AllNum; // 更新可开数量 为界面参数的设置 } if (BuyFirst) { // 先买 if (EnableProtectDiff && (FirstPrice - ticker.Sell) > ProtectDiff) { // 开启差价保护 并且 入市价格 减去 此刻卖一 大于 入市差价保护 throw “首次买入价比市场卖1价高” + _N(FirstPrice - ticker.Sell, Precision) + ‘ 元’; // 抛出错误 信息。 } else if (EnableAccountCheck && account.Balance < _N(needMoney)) { // 如果启用资金检验 并且 账户 可用钱数 小于 网格所需资金钱数。 if (fishCount == 1) { // 如果是第一次撒网 throw “资金不足, 需要” + _N(needMoney) + “元”; // 抛出错误 资金不足 } else { Log(“资金不足, 需要”, _N(needMoney), “元, 程序只做”, canNum, “个网格 #ff0000”); // 如果不是 第一次 撒网, 输出提示信息。 } } else { // 其他情况, 没有开启 资金检验 、差价保护 等 Log(‘预计动用资金: ‘, _N(needMoney), “元”); // 输出 预计动用资金。 } } else { // 先卖, 一下类似 先买 if (EnableProtectDiff && (ticker.Buy - FirstPrice) > ProtectDiff) { throw “首次卖出价比市场买1价高 ” + _N(ticker.Buy - FirstPrice, Precision) + ’ 元’; } else if (EnableAccountCheck && account.Stocks < _N(needStocks)) { if (fishCount == 1) { throw “币数不足, 需要 ” + _N(needStocks) + “ 个币”; } else { Log(“资金不足, 需要”, _N(needStocks), “个币, 程序只做”, canNum, “个网格 #ff0000”); } } else { Log(‘预计动用币数: ‘, _N(needStocks), “个, 约”, _N(needMoney), “元”); } }
var trader = new Trader(); // 构造一个 Trader 对象, 赋值给 此处声明的 trader 变量。
var OpenFunc = BuyFirst ? exchange.Buy : exchange.Sell; // 根据 是否先买后卖 ,设定开仓函数OpenFunc 是 引用 exchange.Buy 还是 exchange.Sell
var CoverFunc = BuyFirst ? exchange.Sell : exchange.Buy; // 同上
if (EnableDynamic) { // 根据界面参数 EnableDynamic (是否动态挂单) 是否开启, 去再次 设定 OpenFunc/CoverFunc
OpenFunc = BuyFirst ? trader.Buy : trader.Sell; // 引用 trader 对象的 成员函数 Buy 用于 动态挂单 (主要是由于一些交易所 限制挂单数量,所以就需要虚拟动态挂单)
CoverFunc = BuyFirst ? trader.Sell : trader.Buy; // 同上
}
var ts = new Date(); // 创建此刻时间对象(赋值给ts),用于记录此刻时间。
var preMsg = ""; // 声明一个 变量 用于记录 上次信息, 初始 空字符串
var profitMax = 0; // 最大收益
while (true) { // 网格 撒网后的 主要 逻辑
var now = new Date(); // 记录 当前循环 开始的时的时间
var table = null; // 声明一个 变量
if (now.getTime() - ts.getTime() > 5000) { // 计算当前 时间 now 和 记录的时间 ts 之间的差值 是否大于 5000 毫秒
if (typeof(GetCommand) == 'function' && GetCommand() == "收网") { // 检测是否 接收到 策略 交互控件 命令 “收网”,停止并平衡到初始状态
Log("开始执行命令进行收网操作"); // 输出 信息
balanceAccount(orgAccount, InitAccount); // 执行平衡函数 ,平衡币数 到初始状态
return false; // 本次 撒网函数 fishing 返回 false
}
ts = now; // 用当前时间 now 更新 ts,用于下次比对时间
var nowAccount = _C(exchange.GetAccount); // 声明 nowAccount 变量 并初始为当前最新 账户信息。
var ticker = _C(exchange.GetTicker); // 声明 ticker 变量 并初始为当前行情信息
if (EnableDynamic) { // 如果开启动态挂单
trader.Poll(ticker, DynamicMax); // 调用 trader 对象的 Poll 函数 ,根据 当前 ticker 行情,和 界面参数 DynamicMax(订单失效距离)检测 并 处理 所有订单。
}
var amount_diff = (nowAccount.Stocks + nowAccount.FrozenStocks) - (InitAccount.Stocks + InitAccount.FrozenStocks); // 计算当前的 币差
var money_diff = (nowAccount.Balance + nowAccount.FrozenBalance) - (InitAccount.Balance + InitAccount.FrozenBalance); // 计算当前的 钱差
var floatProfit = _N(money_diff + (amount_diff * ticker.Last)); // 计算 当前 本次撒网 的浮动盈亏
var floatProfitAll = _N((nowAccount.Balance + nowAccount.FrozenBalance - orgAccount.Balance - orgAccount.FrozenBalance) + ((nowAccount.Stocks + nowAccount.FrozenStocks - orgAccount.Stocks - orgAccount.FrozenStocks) * ticker.Last));
// 计算 总体的浮动盈亏
var isHold = Math.abs(amount_diff) >= exchange.GetMinStock(); // 如果 此刻 币差 绝对值 大于 交易所最小 交易量 ,代表已经持仓
if (isHold) { // 已经持仓 则执行 setBusy() 函数,该函数会给 LastBusy 更新时间。
setBusy(); // 即 开仓后开始 启动开仓机制。
}
profitMax = Math.max(floatProfit, profitMax); // 刷新 最大浮动盈亏
if (EnableAccountCheck && EnableStopLoss) { // 如果启动账户检测 并且 启动 止损
if ((profitMax - floatProfit) >= StopLoss) { // 如果 最大浮动盈亏 减去 当前浮动盈亏 大于等于 最大浮动亏损值,则执行 花括号内代码
Log("当前浮动盈亏", floatProfit, "利润最高点: ", profitMax, "开始止损"); // 输出信息
balanceAccount(orgAccount, InitAccount); // 平衡账户
if (StopLossMode == 0) { // 根据 止损模式 处理, 如果 StopLossMode 等于 0 ,即 止损后退出程序。
throw "止损退出"; // 抛出错误 “止损退出” 策略停止。
} else {
return true; // 除了 止损后退出模式, 即 : 止损后重新撒网。
}
}
}
if (EnableAccountCheck && EnableStopWin) { // 如果开启了 检测账户 并且 开启了 止盈
if (floatProfit > StopWin) { // 如果 浮动盈亏 大于 止盈
Log("当前浮动盈亏", floatProfit, "开始止盈"); // 输出日志
balanceAccount(orgAccount, InitAccount); // 平衡账户 恢复初始 (止盈)
if (StopWinMode == 0) { // 根据止盈模式 处理。
throw "止盈退出"; // 止盈后退出
} else {
return true; // 止盈后 返回 true , 继续撒网
}
}
}
var distance = 0; // 声明 一个 变量 用来 记录 距离
if (EnableAccountCheck && AutoMove) { // 如果开启 账户检测 并且 网格自动移动
if (BuyFirst) { // 如果是 先买后卖
distance = ticker.Last - FirstPrice; // 给 distance 赋值 : 当前的价格 减去 首价格,算出距离
} else { // 其他情况 : 先卖后买
distance = FirstPrice - ticker.Last; // 给 distance 赋值 : 首价格 减去 当前价格,算出距离
}
var refish = false; // 是否重新撒网 标记变量
if (!isHold && isTimeout()) { // 如果没有持仓(isHold 为 false) 并且 超时(isTimeout 返回 true)
Log("空仓过久, 开始移动网格");
refish = true; // 标记 重新撒网
}
if (distance > MaxDistance) { // 如果 当前 的距离 大于 界面参数设定的最大距离, 标记 重新撒网
Log("价格超出网格区间过多, 开始移动网格, 当前距离: ", _N(distance, Precision), "当前价格:", ticker.Last);
refish = true;
}
if (refish) { // 如果 refish 是 true ,则执行 平衡函数
balanceAccount(orgAccount, InitAccount);
return true; // 本次 撒网函数 返回 true
}
}
var holdDirection, holdAmount = "--", // 声明 三个 变量,持仓方向、持仓数量、持仓价格
holdPrice = "--";
if (isHold) { // 持仓时
if (RestoreProfit && ProfitAsOrg) { // 如果 开启 恢复上次盈利 并且 上次盈利算入均价
if (BuyFirst) { // 如果是先买后卖
money_diff += LastProfit; // 把上次盈利 加入 money_diff ,即 上次收益 折合入 钱差(在先买的情况,钱差为负值,即花费的),折合入开仓成本。
} else { // 如果是先卖后买
money_diff -= LastProfit; // 先卖后买 钱差 为 正值 , why - ?
}
}
// 处理先买后卖
holdAmount = amount_diff; // 币差 赋值 给持仓数量 (此刻币差 即 持仓)
holdPrice = (-money_diff) / amount_diff; // 用 钱差 除以 币差 算出 持仓均价,
// 注意 : 如果 money_diff 为 负值 ,则amount_diff 一定为正值,所以一定要在 money_diff 前加 负号,这样算出的价格才是 正数
// 处理先卖后买
if (!BuyFirst) { // 如果是 先卖后买 则触发 更新 持仓量 和 持仓均价
holdAmount = -amount_diff; // 币差为负数 ,所以取反
holdPrice = (money_diff) / -amount_diff; // 计算持仓均价。
}
holdAmount = _N(holdAmount, 4); // 持仓量,保留4位小数。
holdPrice = _N(holdPrice, Precision); // 持仓均价, 保留 Precision 位小数。
holdDirection = BuyFirst ? "多" : "空"; // 根据 先买后卖 或者 先卖后买 给 holdDirection 赋值 多 或者 空
} else { // 如果 isHold 为false ,给holdDirection 赋值 "--"
holdDirection = "--";
}
table = { // 给声明 的 table 变量 赋值一个 对象,用于在 发明者量化 机器人 状态栏上显示 表格信息
type: 'table', // 详见 API 文档 LogStatus 函数, 这里给 type 属性 初始化 'table' 用于在状态栏显示成表格
title: '运行状态', // 表格的 标题
cols: ['动用资金', '持有仓位', '持仓大小', '持仓均价', '总浮动盈亏', '当前网格盈亏', '撒网次数', '网格偏移', '真实委托', '最新币价'], // 表格的 列名
rows: [ // 表格的 逐行的数据
[_N(actualNeedMondy, 4), holdDirection, holdAmount, holdPrice, _N(floatProfitAll, 4) + ' ( ' + _N(floatProfitAll * 100 / actualNeedMondy, 4) + ' % )', floatProfit, fishCount, (AutoMove && distance > 0) ? ((BuyFirst ? "向上" : "向下") + "偏离: " + _N(distance) + " 元") : "--", trader.RealLen(), ticker.Last]
// 一行数据
]
};
} // 每间隔 5 秒处理 一些任务, 并更新 机器人状态栏 表格对象 table
var orders = _C(trader.GetOrders); // 获取 所有未完成的订单
if (table) { // 如果 table 已经被 赋值表格对象
if (!EnableDynamic) { // 如果没有开启动态挂单
table.rows[0][8] = orders.length; // 在状态栏 表格 第一行 第9列 位置 更新 挂单数组的长度
}
LogStatus('`' + JSON.stringify(table) + '`'); // 调用 发明者量化 平台 API LogStatus 显示 设置的状态栏表格
}
for (var idx = 0; idx < canNum; idx++) { // 遍历 可用的 网格节点数量。
var openPrice = _N((BuyFirst ? FirstPrice - (idx * PriceGrid) : FirstPrice + (idx * PriceGrid)), Precision); // 随着 节点 索引 idx 遍历,构造每个节点的 开仓价 (方向由 先买后卖,或者先卖后买 决定)
var coverPrice = _N((BuyFirst ? openPrice + PriceDiff : openPrice -
活着 可以留个联系方式吗,大佬
活着 这个策略可以设置有利位置最大边界自动移动,如果是不利位置可以移动吗,还是只能靠止损重新撒网,但是重新撒网好像平衡有问题,无法平衡到策略初始状态。我平衡了几次都是币不足只能创建两三格网格。
18180828122 不支持合约吗,回测合约提示订阅失败
kkms mark
jjkk 当网格比较多的时候,下单超过api调用频率了,在什么地方加个延时呀??
逍遥侯CC 楼主能留个联系方式吗
nxtplayer 666,学习一下代码思路
发明者量化-小小梦 V : DoMyBestForeverAgo
发明者量化-小小梦 这个策略应该是个现货策略。
jjkk 具体在什么地方呀,找不到呀
发明者量化-小小梦 控制下 轮询 间隔时间 或者 , 下单 时候的 间隔时间。
发明者量化-小小梦 BotVS QQ群 可以QQ我 ^^ : 小小梦