网格变形策略之单边网格 (注释版)

Author: 小小梦, Created: 2017-09-06 18:39:54, Updated: 2019-07-31 18:19:33

网格变形策略之单边网格 (注释版)

策略介绍

  • 网格可以自定义方向

  • 先买后卖: 网格会从首价格开始向下挂买单, 每个买单间隔 “价格间隔” 这个参数, 挂单数量为"单笔数量", 挂够 “总数量” 个买单, 有任意买单成交以后, 程序会在买价基础上加 “价差(元)” 这个参数的的值的价格挂出卖单, 卖出, 卖出以后,重新按原来这个网格的价格挂买入单

  • 先卖后买: 操作刚好相反

  • 策略最大的风险就是单边行情, 价格波动超出网格范围.

  • 网格带有自动止损和移动功能

  • 注释心得:

    策略使用了 虚拟挂单设计, 对于交易所 限制挂单数量,做出了很棒的 处理,灵活的解决了该问题。

    网格逻辑设计灵活,结构巧妙。

    盈亏计算,各个数值统计 算法可借鉴,各个条件检测设计严谨。(能尽量 减少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 - 

Related

More

活着 可以留个联系方式吗,大佬

活着 这个策略可以设置有利位置最大边界自动移动,如果是不利位置可以移动吗,还是只能靠止损重新撒网,但是重新撒网好像平衡有问题,无法平衡到策略初始状态。我平衡了几次都是币不足只能创建两三格网格。

18180828122 不支持合约吗,回测合约提示订阅失败

kkms mark

jjkk 当网格比较多的时候,下单超过api调用频率了,在什么地方加个延时呀??

逍遥侯CC 楼主能留个联系方式吗

nxtplayer 666,学习一下代码思路

小小梦 V : DoMyBestForeverAgo

小小梦 这个策略应该是个现货策略。

jjkk 具体在什么地方呀,找不到呀

小小梦 控制下 轮询 间隔时间 或者 , 下单 时候的 间隔时间。

小小梦 BotVS QQ群 可以QQ我 ^^ : 小小梦