网格变形策略之单边网格 (注释版)
策略介绍
-
网格可以自定义方向
-
先买后卖:
网格会从首价格开始向下挂买单, 每个买单间隔 "价格间隔" 这个参数, 挂单数量为"单笔数量", 挂够 "总数量" 个买单, 有任意买单成交以后, 程序会在买价基础上加 "价差(元)" 这个参数的的值的价格挂出卖单, 卖出, 卖出以后,重新按原来这个网格的价格挂买入单 -
先卖后买:
操作刚好相反 -
策略最大的风险就是单边行情, 价格波动超出网格范围.
-
网格带有自动止损和移动功能
-
注释心得:
策略使用了 虚拟挂单设计, 对于交易所 限制挂单数量,做出了很棒的 处理,灵活的解决了该问题。
网格逻辑设计灵活,结构巧妙。
盈亏计算,各个数值统计 算法可借鉴,各个条件检测设计严谨。(能尽量 减少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 变量 赋值一个 对象,用于在 BotVS 机器人 状态栏上显示 表格信息
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) + '`'); // 调用 BotVS 平台 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 - PriceDiff), Precision); // 开仓平仓价差,即 每个节点的盈利空间
var state = fishTable[idx]; // 赋值 渔网 节点的状态
var fishId = uuidTable[idx]; // 编号
// 此处判断作用为: 过滤 未完成的订单
if (hasOrder(orders, fishId)) { // 如果 所有未完成订单,即挂单数组 中有ID为 fishId 的订单
continue; // 跳过本次循环 继续循环
}
if (fishId != -1 && IsSupportGetOrder) { // 网格 节点 id 不等于 初始值,即下过订单,并且 交易所支持 GetOrder
var order = trader.GetOrder(fishId); // 获取 该 fishId 号 的订单
// 此处判断作用为: 过滤 没有找到订单 的 网格节点,以下判断(state == STATE_WAIT_COVER) 等等 的逻辑不会触发
if (!order) { // 如果 !order 为真 即获取订单失败
Log("获取订单信息失败, ID: ", fishId); // 输出日志
continue; // 跳过本次循环 继续循环
}
// 此处判断作用为: 过滤 处于挂起状态,未成交,或者 未完全成交的 网格节点, 以下判断(state == STATE_WAIT_COVER) 等等 的逻辑不会触发
if (order.Status == ORDER_STATE_PENDING) { // 如果订单状态 是在交易所 挂起状态
//Log("订单状态为未完成, ID: ", fishId);
continue; // 跳过本次循环 继续循环
}
}
if (state == STATE_WAIT_COVER) { // 如果 当前节点 状态是 等待平仓
var coverId = CoverFunc(coverPrice, (BuyFirst ? amountS[idx] : amountB[idx]), (BuyFirst ? '完成买单:' : '完成卖单:'), openPrice, '量:', (BuyFirst ? amountB[idx] : amountS[idx]));
// 调用 平仓 函数 CoverFunc 挂出 平仓单
if (typeof(coverId) === 'number' || typeof(coverId) === 'string') { // 判断 如果 平仓函数 返回的 Id 为 数值(由BotVS API 直接返回) 或者 字符串(由 trader 对象的 Buy/Sell函数返回)
fishTable[idx] = STATE_WAIT_CLOSE; // 已经挂出 平仓单, 更新状态为 : STATE_WAIT_CLOSE 即等待 节点任务完成
uuidTable[idx] = coverId; // 把 订单号 储存在 uuidTable 对应的 idx 位置上。
}
} else if (state == STATE_WAIT_OPEN || state == STATE_WAIT_CLOSE) { // 如果状态是 等待开仓 或者 等待完成
var openId = OpenFunc(openPrice, BuyFirst ? amountB[idx] : amountS[idx]); // 下开仓单。
if (typeof(openId) === 'number' || typeof(openId) === 'string') { // 判断是否下单成功
fishTable[idx] = STATE_WAIT_COVER; // 更新状态 为等待平仓
uuidTable[idx] = openId; // 记录当前 节点 订单ID
if (state == STATE_WAIT_CLOSE) { // 如果是等待完成 (开仓订单下了后 才会触发)
ProfitCount++; // 累计盈利次数
var account = _C(exchange.GetAccount); // 获取当前账户信息
var ticker = _C(exchange.GetTicker); // 获取当前行情信息
var initNet = _N(((InitAccount.Stocks + InitAccount.FrozenStocks) * ticker.Buy) + InitAccount.Balance + InitAccount.FrozenBalance, 8);
// 计算 初始 资产 净值
var nowNet = _N(((account.Stocks + account.FrozenStocks) * ticker.Buy) + account.Balance + account.FrozenBalance, 8);
// 计算 当前 资产 净值
var actualProfit = _N(((nowNet - initNet)) * 100 / initNet, 8); // 计算 收益率
if (AmountType == 0) { // 根据 买卖同量 , 自定义量 不同的处理。
var profit = _N((ProfitCount * amount * PriceDiff) + LastProfit, 8); // 计算: 所有盈利节点的 盈亏 和 上次撒网盈亏 之和 即 总盈亏
Log((BuyFirst ? '完成卖单:' : '完成买单:'), coverPrice, '量:', (BuyFirst ? amountS[idx] : amountB[idx]), '平仓收益', profit);
// 输出 订单完成信息
} else {
Log((BuyFirst ? '完成卖单:' : '完成买单:'), coverPrice, '量:', (BuyFirst ? amountS[idx] : amountB[idx]));
}
}
}
}
}
Sleep(CheckInterval); // 网格逻辑 主要 while 循环检测, 每次 暂停一定时间 CheckInterval 即:检测间隔
}
return true; // 本次撒网完成 返回 true
}
function main() { // 策略主函数,程序从这里开始执行。
if (ResetData) { // RestData 为界面参数, 默认 true , 控制 启动时 是否清空所有数据。默认全部清空。
LogProfitReset(); // 执行 API LogProfitReset 函数,清空 所有收益。
LogReset(); // 执行 API LogReset 函数, 清空 所有日志。
}
// exchange.SetMaxDigits(Precision) // 已废弃,使用 exchange.SetPrecision 代替。
exchange.SetPrecision(Precision, 3) // exchange.SetPrecision(2, 3); // 设置价格小数位精度为2位, 品种下单量小数位精度为3位
// Precision 为界面参数。
if (typeof(AmountType) === 'undefined') { // 订单 数量类型, 0:“买卖同量” , 1:“自定义量” , 检测 如果该参数是 未定义的,默认设置 0 。
AmountType = 0; // typeof 会 检测 AmountType 的类型, 如果是 undefined 即 “未定义” ,则给 AmountType 赋值 0。
}
if (typeof(AmountDot) === 'undefined') { // 订单量 小数点 最长位数 AmountDot 如果是 未定义的, 设置 AmountDot 为 3 。
AmountDot = 3; // 其实已经由 exchange.SetPrecision(Precision, 3) 设置过了,在底层会截断处理。
}
if (typeof(EnableDynamic) === 'undefined') { // 检测 是否 开启动态挂单 参数, 如果 EnableDynamic 是未定义的, 设置 为 false 即 不开启。
EnableDynamic = false;
}
if (typeof(AmountCoefficient) === 'undefined') { // 如果未定义, 默认设置 "*1"
AmountCoefficient = "*1";
}
if (typeof(EnableAccountCheck) === 'undefined') {// 如果未定义, 启用资金检验 参数 设置为 true ,即 开启。
EnableAccountCheck = true;
}
BuyFirst = (OpType == 0); // 根据 OpType 的设置 去 给BuyFirst 赋值, OpType 设置网格类型, 0: 先买后卖, 1: 先卖后买
IsSupportGetOrder = exchange.GetName().indexOf('itstamp') == -1; // 检测 交易所 名称, 如果是 Bitstamp 则提醒
if (!IsSupportGetOrder) {
Log(exchange.GetName(), "不支持GetOrder, 可能影响策略稳定性.");
}
SetErrorFilter("502:|503:|S_U_001|unexpected|network|timeout|WSARecv|Connect|GetAddr|no such|reset|http|received|refused|EOF|When");
// SetErrorFilter 过滤错误信息
exchange.SetRate(1);
Log('已经禁用汇率转换, 当前货币为', exchange.GetBaseCurrency()); // 禁用汇率转换
if (!RestoreProfit) { // 恢复上次盈利 若果是 false 则 给 LastProfit 赋值 0 , 即不恢复。
LastProfit = 0;
}
var orgAccount = _C(exchange.GetAccount); // 获取账户信息, 此处记录 策略开始运行时的 初始账户信息 ,用于 计算一些收益,如: 总体浮动盈亏 等。本策略有几个参数 都是 该变量传入。
var fishCount = 1; // 撒网次数 初始1
while (true) { // 策略 主循环
if (!fishing(orgAccount, fishCount)) { // 撒网函数 fishing
break;
}
fishCount++; // 撒网次数 累计
Log("第", fishCount, "次重新撒网..."); // 输出 撒网信息。
FirstPriceAuto = true; // 重置 首价格自动 为true
Sleep(1000); // 轮询间隔 1000毫秒
}
}
网友评论