实现一个多品种并发的商品期货策略

实现一个多品种并发的商品期货策略(8)

当一把造物主,做个程序员真好!实现 《商品期货多品种海龟策略》机器人

  • 海龟交易法起源

这个交易系统的诞生来源与两个交易老手的争论,一方觉得交易这种技能是后天习得的,另一方觉得这是先天所决定的。1983 年,在新加坡海龟农场,两位神一样的交易员 Dennis、Eckhardt 有了分歧。E 神说,交易员是天生的;D 神说,就像这一大缸海龟一样,交易员可以培养。

于是,D 神掏出来白花花的银子,要做实验,要打赌。他们在华尔街日报、纽约时报等等放出广告,说 D 神要搞培训班了,给每个人 100 万美元的账户,手把手地教,不限专业,无须经验,皆可报名。有千余人投了简历,40 人进入面试,23 人被留下考察,13 人进入培训班。

这 13 个人来自各行各业,多数都没有交易经验,是一群尚未成功的普通人。他们被培训了两个星期,然后放出去交易,在接下来的四年半里,创出了 80% 的年均收益率。培训内容,叫做《海龟交易法则》;培训学员,被称为“海龟”。

尽管有人质疑样本的随机性,这场试验应该算 D 神胜利了。

  • 海龟交易系统是一个完整的交易系统, 它有一个完整的交易系统所应该有的所有成分,涵盖了成功交易中的每一个必要决策:

市场:买卖什么?
头寸规模:买卖多少? Unit=(1%∗Account)/N
入市:什么时候买卖?
止损:什么时候放弃一个亏损的头寸?
退出:什么时候退出一个盈利的头寸?
战术:怎么买卖?
核心围绕: N 值,海龟的止损、加仓、头寸规模 都是基于 N 值计算, 有些海龟交易系统用的是 ATR 来代替 N 值,ATR 为真实波幅的 20 日平均。
“海龟们从不去预测市场的动向,而是会寻找市场处于某种特定状态的指示信号。优秀的交易者不会试着预测市场下一步会怎么样;相反,他们会观察指示信号,判断市场现在正处于什么样的状态中。”

对于 “海龟交易法”感到陌生的读者可以看这篇文章: https://www.botvs.com/bbs-topic/609
也可以 知乎 或者 百度 搜索,有很多文章介绍,老白就不做赘述了。

  • 作为 本系列文章的最后收尾一篇,我们就来动手实践一个海龟策略,当然我们要有创新,我们实现一个“海龟群”。

    说点题外的,之前的几篇文章记录的都是老白当时学习时的心路历程,学习量化、程序化没办法一蹴而就,只能脚踏实地,耐住性子一点点进步。老白开始的时候也感觉思考问题、找 BUG 、写程序晕头转向的。但是,慢慢的我发现学习是个加速度,开始很慢,积累越多越轻松。一个完全零基础的朋友经常和我说:“越是感觉自己要放弃的时候,越是应该跟困难死磕的时候!”

    言归正传, 为什么我们要使用海龟群呢?
    当然是为了尽可能的分散风险,即使是大名鼎鼎的海龟策略,当年也曾经有过大幅回撤,甚至亏损本金。任何交易系统都是有一定风险的。多品种的好处就是“把鸡蛋放在不同的篮子里”。当然也有缺点,那就是需要不小的资金量。资金量小了,可能只能交易几个品种,降低了分散风险的能力。
    还有一点需要牢记:任何时候都可能飞出一只黑天鹅! (如商品期货 16 年底黑色星期五 全线暴跌。)

注释版源代码:

/*  
参数:
Instruments             合约列表                  字符串(string)      MA701,CF701,zn1701,SR701,pp1701,l1701,hc1610,ni1701,i1701,v1701,rb1610,jm1701,ag1612,al1701,jd1701,cs1701,p1701
LoopInterval            轮询周期(秒)              数字型(number)       3
RiskRatio               % Risk Per N ( 0 - 100) 数字型(number)       1
ATRLength               ATR计算周期               数字型(number)      20
EnterPeriodA            系统一入市周期             数字型(number)      20
LeavePeriodA            系统一离市周期             数字型(number)      10
EnterPeriodB            系统二入市周期             数字型(number)      55
LeavePeriodB            系统二离市周期             数字型(number)      20
UseEnterFilter          使用入市过滤              布尔型(true/false)   true
IncSpace                加仓间隔(N的倍数)          数字型(number)      0.5
StopLossRatio           止损系数(N的倍数)          数字型(number)      2
MaxLots                 单品种加仓次数             数字型(number)      4
RMode                   进度恢复模式              下拉框(selected)     自动|手动
VMStatus@RMode==1       手动恢复字符串             字符串(string)      {}
WXPush                  推送交易信息              布尔型(true/false)   true
MaxTaskRetry            开仓最多重试次数           数字型(number)       5
KeepRatio               预留保证金比例             数字型(number)      10 
*/

var _bot = $.NewPositionManager();                                                          // 调用CTP商品期货交易类库 的导出函数 生成一个用于单个品种交易的对象 

var TTManager = {                                                                           // 海龟策略 控制器
    New: function(needRestore, symbol, keepBalance, riskRatio, atrLen, enterPeriodA, leavePeriodA, enterPeriodB, leavePeriodB, useFilter,
        multiplierN, multiplierS, maxLots) {
        // 该控制器对象 TTManager 的属性 New 赋值一个 匿名函数(构造海龟的函数,即:构造函数),用于创建 海龟任务,参数分别是:
        // needRestore: 是否需要恢复,symbol:合约代码,keepBalance:必要的预留的资金,riskRatio:风险系数, atrLen:ATR指标(参数)周期。enterPeriodA:入市周期A
        // leavePeriodA:离市周期A , enterPeriodB:入市周期B, leavePeriodB:离市周期B,useFilter:使用过滤,multiplierN:加仓系数,multiplierS:止损系数,maxLots:最大加仓次数

        // subscribe
        var symbolDetail = _C(exchange.SetContractType, symbol);                            
        // 声明一个局部变量 symbolDetail 用于接受API SetContractType 函数的返回值(值为symbol的合约的详细信息,symbol 是 "MA709",返回的就是甲醇709合约的详细信息),
        // 调用API SetContractType 订阅并切换合约为 symbol 变量值的合约。 _C() 函数的作用是 对 SetContractType 合约容错处理,即如果 SetContractType返回null 会循环重试。
        if (symbolDetail.VolumeMultiple == 0 || symbolDetail.MaxLimitOrderVolume == 0 || symbolDetail.MinLimitOrderVolume == 0 || symbolDetail.LongMarginRatio == 0 || symbolDetail.ShortMarginRatio == 0) {
        // 如果 返回的合约信息对象symbolDetail 中 VolumeMultiple、MaxLimitOrderVolume 等数据异常,则调用 throw 抛出错误,终止程序。
            Log(symbolDetail);
            throw "合约信息异常";
        } else {                                                                             // 检索的数据没有异常则,输出部分合约信息。
            Log("合约", symbolDetail.InstrumentName, "一手", symbolDetail.VolumeMultiple, "份, 最大下单量", symbolDetail.MaxLimitOrderVolume, "保证金率:", _N(symbolDetail.LongMarginRatio), _N(symbolDetail.ShortMarginRatio), "交割日期", symbolDetail.StartDelivDate);
        }

        var ACT_IDLE = 0;                                                                    // 定义一些宏 (标记)
        var ACT_LONG = 1;
        var ACT_SHORT = 2;
        var ACT_COVER = 3;                                                                   // 动作宏


        var ERR_SUCCESS = 0;                                                                 // 错误宏
        var ERR_SET_SYMBOL = 1;
        var ERR_GET_ORDERS = 2;
        var ERR_GET_POS = 3;
        var ERR_TRADE = 4;
        var ERR_GET_DEPTH = 5;
        var ERR_NOT_TRADING = 6;
        var errMsg = ["成功", "切换合约失败", "获取订单失败", "获取持仓失败", "交易下单失败", "获取深度失败", "不在交易时间"];  // 错误宏的值 对应该数组的索引,对应索引的值就是翻译

        var obj = {                                // 声明一个对象,构造完成后返回。单个的海龟策略控制对象。
            symbol: symbol,                        // 合约代码         构造函数执行时的参数传入
            keepBalance: keepBalance,              // 预留的资金       构造函数执行时的参数传入
            riskRatio: riskRatio,                  // 风险系数         构造函数执行时的参数传入
            atrLen: atrLen,                        // ATR 长度         构造函数执行时的参数传入
            enterPeriodA: enterPeriodA,            // 入市周期A        构造函数执行时的参数传入
            leavePeriodA: leavePeriodA,            // 离市周期A        构造函数执行时的参数传入
            enterPeriodB: enterPeriodB,            // 入市周期B        构造函数执行时的参数传入
            leavePeriodB: leavePeriodB,            // 离市周期B        构造函数执行时的参数传入
            useFilter: useFilter,                  // 使用入市过滤条件  构造函数执行时的参数传入
            multiplierN: multiplierN,              // 加仓系数 基于N   构造函数执行时的参数传入
            multiplierS: multiplierS               // 止损系数 基于N   构造函数执行时的参数传入
        };
        obj.task = {                               // 给 obj对象添加一个 task 属性(值也是一个对象),用来保存 海龟的任务状态数据。
            action: ACT_IDLE,                      // 执行动作
            amount: 0,                             // 操作量
            dealAmount: 0,                         // 已经处理的操作量 
            avgPrice: 0,                           // 成交均价
            preCost: 0,                            // 前一次交易成交的额度
            preAmount: 0,                          // 前一次成交的量
            init: false,                           // 是否初始化
            retry: 0,                              // 重试次数
            desc: "空闲",                           // 描述信息
            onFinish: null                         // 处理完成时的 回调函数,即可以自行设定一个 回调函数在完成当前 action 记录的任务后执行的代码。
        }
        obj.maxLots = maxLots;                     // 赋值 最大加仓次数  构造函数执行时的参数传入
        obj.lastPrice = 0;                         // 最近成交价,用于计算 持仓盈亏。
        obj.symbolDetail = symbolDetail;           // 储存 合约的详细信息 到obj 对象的 symbolDetail 属性
        obj.status = {                             // 状态数据
            symbol: symbol,                        // 合约代码
            recordsLen: 0,                         // K线长度
            vm: [],                                // 持仓状态 , 用来储存 每个品种的 ,手动恢复字符串。
            open: 0,                               // 开仓次数
            cover: 0,                              // 平仓次数
            st: 0,                                 // 止损平仓次数
            marketPosition: 0,                     // 加仓次数
            lastPrice: 0,                          // 最近成交价价格
            holdPrice: 0,                          // 持仓均价
            holdAmount: 0,                         // 持仓数量
            holdProfit: 0,                         // 浮动持仓盈亏
            N: 0,                                  // N值 ,  即ATR
            upLine: 0,                             // 上线
            downLine: 0,                           // 下线
            symbolDetail: symbolDetail,            // 合约详细信息
            lastErr: "",                           // 上次错误
            lastErrTime: "",                       // 上次错误时间信息
            stopPrice: '',                         // 止损价格
            leavePrice: '',                        // 
            isTrading: false                       // 是否在交易时间
        };

        obj.setLastError = function(err) {         // 给obj对象添加方法,设置 最近一次的错误信息
            if (typeof(err) === 'undefined' || err === '') {                                     // 如果参数未传入,或者 错误信息为 空字符串
                obj.status.lastErr = "";                                                         // 清空 obj 对象的 status 属性的 对象的lastErr属性
                obj.status.lastErrTime = "";                                                     // 清空
                return;                                                                          // 返回
            }
            var t = new Date();                                                                  // 获取新时间
            obj.status.lastErr = err;                                                            // 设置错误信息
            obj.status.lastErrTime = t.toLocaleString();                                         // toLocaleString()    根据本地时间格式,把 Date 对象转换为字符串。
        };
        obj.reset = function(marketPosition, openPrice, N, leavePeriod, preBreakoutFailure) {    // 给obj对象添加方法,恢复仓位。
            // 参数,marketPosition:加仓次数,openPrice:最后一次加仓价, N:N值, leavePeriod:离市周期,preBreakoutFailure:是否上次突破失败
            if (typeof(marketPosition) !== 'undefined') {                                        // 如果 第一个参数不是未定义 ,传入参数
                obj.marketPosition = marketPosition;                                             // 给obj 添加属性 marketPosition : 加仓次数 正数为多仓,负数为空仓
                obj.openPrice = openPrice;                                                       // 最后一次加仓价
                obj.preBreakoutFailure = preBreakoutFailure;                                     // 是否上次突破失败
                obj.N = N;                                                                       // N值
                obj.leavePeriod = leavePeriod;                                                   // 离市周期
                var pos = _bot.GetPosition(obj.symbol, marketPosition > 0 ? PD_LONG : PD_SHORT); // 调用 模板类库生成的 交易控制对象的成员函数GetPosition 获取 持仓信息
                if (pos) {                                                                       // 如果获取到持仓信息
                    obj.holdPrice = pos.Price;                                                   // 根据获取的持仓信息 给obj 属性赋值
                    obj.holdAmount = pos.Amount;                                                 // 同上
                    Log(obj.symbol, "仓位", pos);                                                 // 输出显示当前仓位
                } else {                                                                         // 如果GetPosition 返回null ,没有找到持仓信息。
                    throw "恢复" + obj.symbol + "的持仓状态出错, 没有找到仓位信息";                    // 抛出异常
                }
                Log("恢复", obj.symbol, "加仓次数", obj.marketPosition, "持仓均价:", obj.holdPrice, "持仓数量:", obj.holdAmount, "最后一次加仓价", obj.openPrice, "N值", obj.N, "离市周期:", leavePeriod, "上次突破:", obj.preBreakoutFailure ? "失败" : "成功");
                // 输出恢复的 相关参数,数据。
                obj.status.open = 1;                                                              // 设置 开仓 计数为1
                obj.status.vm = [obj.marketPosition, obj.openPrice, obj.N, obj.leavePeriod, obj.preBreakoutFailure];  // 储存 手动恢复字符串 数据。
            } else {                                                                              // 没有传入参数,即不恢复, 全部初始化。
                obj.marketPosition = 0;                                                           // 初始化各项变量
                obj.holdPrice = 0;
                obj.openPrice = 0;
                obj.holdAmount = 0;
                obj.holdProfit = 0;
                obj.preBreakoutFailure = true; // test system A                                   // 此处设置true  会使策略 尝试 突破系统A 
                obj.N = 0;
                obj.leavePeriod = leavePeriodA;                                                   // 用系统A 的离市周期 赋值
            }
            obj.holdProfit = 0;                                                                   // 初始化
            obj.lastErr = "";
            obj.lastErrTime = "";
        };

        obj.Status = function() {                                                                 // 给Obj 添加 Status 函数, 把Obj 的一些属性值 赋值给 Obj.status 同样意义的属性
            obj.status.N = obj.N;                                                                 // 给 obj.status 赋值
            obj.status.marketPosition = obj.marketPosition;
            obj.status.holdPrice = obj.holdPrice;
            obj.status.holdAmount = obj.holdAmount;
            obj.status.lastPrice = obj.lastPrice;
            if (obj.lastPrice > 0 && obj.holdAmount > 0 && obj.marketPosition !== 0) {            // 如果有持仓
                obj.status.holdProfit = _N((obj.lastPrice - obj.holdPrice) * obj.holdAmount * symbolDetail.VolumeMultiple, 4) * (obj.marketPosition > 0 ? 1 : -1);
                // 计算持仓盈亏 = (最近成交价 - 持仓价格)* 持仓量 * 一手合约份数 , 计算出来 保留4位小数, 用 obj.marketPosition(加仓次数) 属性的 正负 去修正,计算结果的正负(做空按照这个算法是相反的负数,所以要用-1修正)。
            } else {
                // 如果没有持仓,浮动盈亏赋值为0
                obj.status.holdProfit = 0;
            }
            return obj.status;                                                                    // 返回这个 obj.status 对象(用于显示在界面状态栏?)
        };
        obj.setTask = function(action, amount, onFinish) {                                        // 给obj 对象添加 方法,设置任务
            // 参数,action:执行动作,amount:数量,onFinish: 回调函数
            obj.task.init = false;                                                                // 重置 初次执行标记 为false 
            obj.task.retry = 0;                                                                   // 重置..
            obj.task.action = action;                                                             // 参数传来的 动作指令 赋值
            obj.task.preAmount = 0;                                                               // 重置
            obj.task.preCost = 0;
            obj.task.amount = typeof(amount) === 'number' ? amount : 0;                           // 如果没传入参数 ,设置 0
            obj.task.onFinish = onFinish;
            if (action == ACT_IDLE) {                                                             // 如果 动作指令是 空闲
                obj.task.desc = "空闲";                                                            // 描述变量  赋值为  “空闲”
                obj.task.onFinish = null;                                                         // 赋值为 null
            } else {                                                                               // 其他动作
                if (action !== ACT_COVER) {                                                        // 如果不等于 平仓动作
                    obj.task.desc = (action == ACT_LONG ? "加多仓" : "加空仓") + "(" + amount + ")"; // 根据 action 设置描述 信息
                } else {                                                                           // 如果是平仓 动作 设置描述信息为 “平仓”
                    obj.task.desc = "平仓";
                }
                Log("接收到任务", obj.symbol, obj.task.desc);                                        // 输出日志 显示 接收到任务。
                // process immediately
                obj.Poll(true);                                                                    // 调用 obj 对象的方法 处理 任务,参数是 true , 参数为true ,控制Poll 只执行 一部分(子过程)
            }
        };
        obj.processTask = function() {                                                              // 处理 交易任务 
            var insDetail = exchange.SetContractType(obj.symbol);                                   // 切换 要操作的合约
            if (!insDetail) {                                                                       // 切换失败 返回错误
                return ERR_SET_SYMBOL;
            }
            var SlideTick = 1;                                                                      // 滑价设置为1 个 PriceTick
            var ret = false;                                                                        // 声明返回值  初始false
            if (obj.task.action == ACT_COVER) {                                                     // 处理 指令为全平的 任务,这部分处理 类似 商品期货交易类库 不再赘述,可以参见 商品期货交易类库注释版
                var hasPosition = false;
                do {
                    if (!$.IsTrading(obj.symbol)) {
                        return ERR_NOT_TRADING;
                    }
                    hasPosition = false;
                    var positions = exchange.GetPosition();
                    if (!positions) {
                        return ERR_GET_POS;
                    }
                    var depth = exchange.GetDepth();
                    if (!depth) {
                        return ERR_GET_DEPTH;
                    }
                    var orderId = null;
                    for (var i = 0; i < positions.length; i++) {
                        if (positions[i].ContractType !== obj.symbol) {
                            continue;
                        }
                        var amount = Math.min(insDetail.MaxLimitOrderVolume, positions[i].Amount);
                        if (positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD) {
                            exchange.SetDirection(positions[i].Type == PD_LONG ? "closebuy_today" : "closebuy");
                            orderId = exchange.Sell(_N(depth.Bids[0].Price - (insDetail.PriceTick * SlideTick), 2), Math.min(amount, depth.Bids[0].Amount), obj.symbol, positions[i].Type == PD_LONG ? "平今" : "平昨", 'Bid', depth.Bids[0]);
                            hasPosition = true;
                        } else if (positions[i].Type == PD_SHORT || positions[i].Type == PD_SHORT_YD) {
                            exchange.SetDirection(positions[i].Type == PD_SHORT ? "closesell_today" : "closesell");
                            orderId = exchange.Buy(_N(depth.Asks[0].Price + (insDetail.PriceTick * SlideTick), 2), Math.min(amount, depth.Asks[0].Amount), obj.symbol, positions[i].Type == PD_SHORT ? "平今" : "平昨", 'Ask', depth.Asks[0]);
                            hasPosition = true;
                        }
                    }
                    if (hasPosition) {
                        if (!orderId) {
                            return ERR_TRADE;
                        }
                        Sleep(1000);
                        while (true) {
                            // Wait order, not retry
                            var orders = exchange.GetOrders();
                            if (!orders) {
                                return ERR_GET_ORDERS;
                            }
                            if (orders.length == 0) {
                                break;
                            }
                            for (var i = 0; i < orders.length; i++) {
                                exchange.CancelOrder(orders[i].Id);
                                Sleep(500);
                            }
                        }
                    }
                } while (hasPosition);
                ret = true;
            } else if (obj.task.action == ACT_LONG || obj.task.action == ACT_SHORT) {                   // 处理 建/加多仓 任务  或者  处理 建/加空仓 任务,这部分处理 类似 商品期货交易类库 不再赘述,可以参见 商品期货交易类库注释版。(此策略没有使用商品期货交易类库的交易功能,在次直接植入了处理代码)
                do {
                    if (!$.IsTrading(obj.symbol)) {
                        return ERR_NOT_TRADING;
                    }
                    Sleep(1000);
                    while (true) {
                        // Wait order, not retry
                        var orders = exchange.GetOrders();
                        if (!orders) {
                            return ERR_GET_ORDERS;
                        }
                        if (orders.length == 0) {
                            break;
                        }
                        for (var i = 0; i < orders.length; i++) {
                            exchange.CancelOrder(orders[i].Id);
                            Sleep(500);
                        }
                    }
                    var positions = exchange.GetPosition();
                    // Error
                    if (!positions) {
                        return ERR_GET_POS;
                    }
                    // search position
                    var pos = null;
                    for (var i = 0; i < positions.length; i++) {
                        if (positions[i].ContractType == obj.symbol && (((positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD) && obj.task.action == ACT_LONG) || ((positions[i].Type == PD_SHORT || positions[i].Type == PD_SHORT_YD) && obj.task.action == ACT_SHORT))) {
                            if (!pos) {
                                pos = positions[i];
                                pos.Cost = positions[i].Price * positions[i].Amount;
                            } else {
                                pos.Amount += positions[i].Amount;
                                pos.Profit += positions[i].Profit;
                                pos.Cost += positions[i].Price * positions[i].Amount;
                            }
                        }
                    }
                    // record pre position
                    if (!obj.task.init) {
                        obj.task.init = true;
                        if (pos) {
                            obj.task.preAmount = pos.Amount;
                            obj.task.preCost = pos.Cost;
                        } else {
                            obj.task.preAmount = 0;
                            obj.task.preCost = 0;
                        }
                    }
                    var remain = obj.task.amount;
                    if (pos) {
                        obj.task.dealAmount = pos.Amount - obj.task.preAmount;
                        remain = parseInt(obj.task.amount - obj.task.dealAmount);
                        if (remain <= 0 || obj.task.retry >= MaxTaskRetry) {
                            ret = {
                                price: (pos.Cost - obj.task.preCost) / (pos.Amount - obj.task.preAmount),
                                amount: (pos.Amount - obj.task.preAmount),
                                position: pos
                            };
                            break;
                        }
                    } else if (obj.task.retry >= MaxTaskRetry) {
                        ret = null;
                        break;
                    }

                    var depth = exchange.GetDepth();
                    if (!depth) {
                        return ERR_GET_DEPTH;
                    }
                    var orderId = null;
                    if (obj.task.action == ACT_LONG) {
                        exchange.SetDirection("buy");
                        orderId = exchange.Buy(_N(depth.Asks[0].Price + (insDetail.PriceTick * SlideTick), 2), Math.min(remain, depth.Asks[0].Amount), obj.symbol, 'Ask', depth.Asks[0]);
                    } else {
                        exchange.SetDirection("sell");
                        orderId = exchange.Sell(_N(depth.Bids[0].Price - (insDetail.PriceTick * SlideTick), 2), Math.min(remain, depth.Bids[0].Amount), obj.symbol, 'Bid', depth.Bids[0]);
                    }
                    // symbol not in trading or other else happend
                    if (!orderId) {
                        obj.task.retry++;
                        return ERR_TRADE;
                    }
                } while (true);
            }
            if (obj.task.onFinish) {
                obj.task.onFinish(ret);
            }
            obj.setTask(ACT_IDLE);                                                                 // 任务执行完成(中间没有被 错误 return),重设为 空闲任务
            return ERR_SUCCESS;
        };
        obj.Poll = function(subroutine) {                                                           // 处理海龟交易法  策略逻辑, 参数:  子程序?
            obj.status.isTrading = $.IsTrading(obj.symbol);                                         // 调用 模板的导出函数 $.IsTrading 检测 obj.symbol 记录的品种是否在交易时间,结果赋值给obj.status.isTrading
            if (!obj.status.isTrading) {                                                            // 如果 obj.status.isTrading 是 false 即 不在交易时间内, return 返回
                return;
            }
            if (obj.task.action != ACT_IDLE) {                                                      // 如果 任务属性的 执行动作属性 不等于 等待标记(宏)
                var retCode = obj.processTask();                                                    // 就调用 当前obj 对象的processTask函数 执行 task 记录的任务。 
                if (obj.task.action != ACT_IDLE) {                                                  // 如果 调用 processTask 函数后 task属性的action 属性不等于 等待标记,即证明任务没有处理成功。
                    obj.setLastError("任务没有处理成功: " + errMsg[retCode] + ", " + obj.task.desc + ", 重试: " + obj.task.retry);
                    // 此时调用 setLastError 记录 并 显示 任务 没有处理成功, 错误代码, 任务描述、重试次数
                } else {
                    obj.setLastError();                                                             // 调用 setLastError 不传参数, 不传参数 用空内容(字符串,详见函数setLastError)刷新。
                }
                return;                                                                             // 执行完 任务 返回
            }
            if (typeof(subroutine) !== 'undefined' && subroutine) {                                 // 参数 subroutine 不为null 且 已定义, 比如在调用 setTask 后会执行Poll,到此就返回
                return;                                                                             // 返回
            }
            // Loop
            var suffix = WXPush ? '@' : '';                                                         // 界面参数如果开启 微信推送, suffix 会被赋值 "@"(微信推送功能 只用在API: Log函数后加 "@"字符即可), 否则空字符。
            // switch symbol
            _C(exchange.SetContractType, obj.symbol);                                               // 切换 合约 为 obj.symbol 记录的合约代码
            var records = exchange.GetRecords();                                                    // 获取K线数据
            if (!records) {                                                                         // 如果 K线获取到  null 值
                obj.setLastError("获取K线失败");                                                      // 设置失败信息,并返回。
                return;
            }
            obj.status.recordsLen = records.length;                                                 // 记录K线长度
            if (records.length < obj.atrLen) {                                                      // 如果 K线长度小于  ATR指标参数(小于的话 无法计算出ATR指标 即N值)
                obj.setLastError("K线长度小于 " + obj.atrLen);                                        // 设置错误信息,并返回。
                return;
            }
            var opCode = 0; // 0: IDLE, 1: LONG, 2: SHORT, 3: CoverALL                              // 声明一个临时变量  操作代码 有4种操作
            var lastPrice = records[records.length - 1].Close;                                      // 声明一个临时变量  用K线 最后一个柱 的收盘价给其赋值,(K线最后一个柱的收盘价是实时更新的是最新价格)
            obj.lastPrice = lastPrice;                                                              // 赋值给  obj.lastPrice
            if (obj.marketPosition === 0) {                                                         // 如果当前 海龟策略 控制对象的加仓次数 为0 ,即没持仓。
                obj.status.stopPrice = '--';                                                        // 给止损价 赋值 '--'
                obj.status.leavePrice = '--';                                                       // 用于显示 状态的表格 对象 status的 leavePrice属性赋值 "--"  (因为没有持仓,所以没有 离市价)
                obj.status.upLine = 0;                                                              // 赋值 上线,(这里如果不明白 这些变量控制那些显示,可以实际运行一个模拟盘 ,看下界面对比分析更好理解。)
                obj.status.downLine = 0;                                                            // 赋值 下线
                for (var i = 0; i < 2; i++) {                                                       // 在当前的分支条件内,是没有持仓的,这里循环两次,用来检测2个突破系统的触发。
                    if (i == 0 && obj.useFilter && !obj.preBreakoutFailure) {                       // 如果是第一次循环,并且启用了入市条件过滤,并且上次突破没有失败。
                        continue;                                                                   // 跳过本次循环
                    }
                    var enterPeriod = i == 0 ? obj.enterPeriodA : obj.enterPeriodB;                 // 用 ? :  三元条件表达式,选择使用的  突破系统 参数,即当 i == 0 时 使用 系统A
                    if (records.length < (enterPeriod + 1)) {                                       // 限制 当前 K线周期 bar 长度 必须大于 突破系统的入市周期加1
                        continue;                                                                   // 跳过本次循环
                    }
                    var highest = TA.Highest(records, enterPeriod, 'High');                         // 计算enterPeriod周期内所有最高价的 最大值
                    var lowest = TA.Lowest(records, enterPeriod, 'Low');                            // 计算enterPeriod周期内所有最低价的 最小值
                    obj.status.upLine = obj.status.upLine == 0 ? highest : Math.min(obj.status.upLine, highest);      // 取两次 系统A 和系统B 获取的 highest中 最小的值
                    obj.status.downLine = obj.status.downLine == 0 ? lowest : Math.max(obj.status.downLine, lowest);  // 取两次 系统A 和系统B 获取的 lowest中 最大的值
                    /*
                    if (lastPrice > highest) {                                                                        // 最新的 价格 如果向上突破 对应周期内的最高价
                        opCode = 1;                                                                                   // 操作值 赋值1
                    } else if (lastPrice < lowest) {                                                                  // 最新的 价格 如果向下突破 对应周期内的最低价
                        opCode = 2;                                                                                   // 操作值 赋值2
                    }
                    obj.leavePeriod = (enterPeriod == obj.enterPeriodA) ? obj.leavePeriodA : obj.leavePeriodB;        // 
                    */
                    if (lastPrice > highest) {                                                                        // 修改以上注释
                        opCode = 1;
                    } else if (lastPrice < lowest) {
                        opCode = 2;
                    }
                    if (opCode != 0) {
                        obj.leavePeriod = (enterPeriod == obj.enterPeriodA) ? obj.leavePeriodA : obj.leavePeriodB;
                        break;
                    }
                }
            } else {                                                                                                  // 如果持有仓位
                var spread = obj.marketPosition > 0 ? (obj.openPrice - lastPrice) : (lastPrice - obj.openPrice);      // 计算单价盈亏 做多 盈利是负值 亏损是正值,因为要做和止损单价的对比,所以取反, 做空同理
                obj.status.stopPrice = _N(obj.openPrice + (obj.N * StopLossRatio * (obj.marketPosition > 0 ? -1 : 1)));  // 计算止损价 做多的时候: 用开仓价 减去 N值 乘 止损系数, 做空: 用开仓价 加上 N值 乘止 损系数。
                if (spread > (obj.N * StopLossRatio)) {                                                                  // 检测 单价盈亏 是否大于 设定的 盈亏限制(即 止损系数 * N值)
                    opCode = 3;                                                                                          // 触发 止损 操作代码 赋值 3
                    obj.preBreakoutFailure = true;                                                                       // 触发止损  ,标记 上次突破失败为真
                    Log(obj.symbolDetail.InstrumentName, "止损平仓", suffix);                                             // 打印 该品种 合约名 止损, 如果开启微信推送,则推送到微信。
                    obj.status.st++;                                                                                     // 止损计数 累计
                } else if (-spread > (IncSpace * obj.N)) {                                                               // 如果单价盈亏(取反 得 正 盈利数,负亏损数) 大于加仓系数 * N值, 触发加仓操作
                    opCode = obj.marketPosition > 0 ? 1 : 2;                                                             // 0: IDLE, 1: LONG, 2: SHORT, 3: CoverALL 
                } else if (records.length > obj.leavePeriod) {                                                           // 只要 K线周期 长度大于 离市 周期,可以计算离市价格
                    // obj.status.leavePrice = TA.Lowest(records, obj.leavePeriod, obj.marketPosition > 0 ? 'Low' : 'High') // 问题2
                    obj.status.leavePrice = obj.marketPosition > 0 ? TA.Lowest(records, obj.leavePeriod, 'Low') : TA.Highest(records, obj.leavePeriod, 'High');
                    if ((obj.marketPosition > 0 && lastPrice < obj.status.leavePrice) ||                                 // 做多 或者 做空 如果触发了  离市价
                        (obj.marketPosition < 0 && lastPrice > obj.status.leavePrice)) {
                        obj.preBreakoutFailure = false;                                                                  // 上次突破失败 赋值为 false ,即 没失败
                        Log(obj.symbolDetail.InstrumentName, "正常平仓", suffix);                                         //  打印信息 平仓,可微信推送
                        opCode = 3;                                                                                      // 给操作 赋值 3 
                        obj.status.cover++;                                                                               // 平仓计数累计
                    }
                }
            }

            if (opCode == 0) {                                                                                            // 如果是  等待 代码  则返回
                return;
            }
            if (opCode == 3) {                                                                                            // 如果是 全平仓  代码 
                obj.setTask(ACT_COVER, 0, function(ret) {                                                                 // 调用 obj 海龟控制对象的成员函数 setTask 设置任务 (全平仓)并自定义一个回调函数(第三个参数 function(ret){...} 就是匿名函数。)
                    obj.reset();                                                                                          // 回调函数 会在setTask 函数中 设置任务后 调用的 Poll 的函数中 通过 processTask 函数 执行该任务完成后 ,触发回调函数。
                    _G(obj.symbol, null);                                                                                 // 回调函数 调用了 不传参数的 reset函数,执行控制对象 变量重置工作,清空 _G 保存的 本地永久 数据(用于恢复,因为已经平仓了,所以需要清空)
                });
                return;                                                                                                   // 回调函数是在任务完成后(即 全部海龟头寸 平仓后 才触发,此处只是预设)
            }
            // Open
            if (Math.abs(obj.marketPosition) >= obj.maxLots) {                                                            // 建仓 或者 加仓处理, 这里判断如果 加仓次数 大于等于 最大允许加仓次数
                obj.setLastError("禁止开仓, 超过最大持仓 " + obj.maxLots);                                                    // 设置错误信息,然后返回。
                return;
            }
            var atrs = TA.ATR(records, atrLen);                                                                            // 计算ATR 指标
            var N = _N(atrs[atrs.length - 1], 4);                                                                          // 获取 当前ATR指标值 ,即 N值

            var account = _bot.GetAccount();                                                                               // 调用 模板 生成的 交易控制对象的 成员函数 GetAccount
            var currMargin = JSON.parse(exchange.GetRawJSON()).CurrMargin;                                                 // 获取当前 保证金数值
            var unit = parseInt((account.Balance+currMargin-obj.keepBalance) * (obj.riskRatio / 100) / N / obj.symbolDetail.VolumeMultiple);  
            // 计算 总 可用资金对应 N值 计算出的  一个头寸的 大小(手数)。可以看原版的海龟交易法  关于 unit 的计算,知乎上也有相关文章。 
            var canOpen = parseInt((account.Balance-obj.keepBalance) / (opCode == 1 ? obj.symbolDetail.LongMarginRatio : obj.symbolDetail.ShortMarginRatio) / (lastPrice * 1.2) / obj.symbolDetail.VolumeMultiple);
            // 根据 要做 多仓 或者 空仓 的保证金率 计算 可用资金 可以开 的手数,可开量。
            unit = Math.min(unit, canOpen);                                                                                // 最终头寸大小 取 unit, canOpen 中最小值
            if (unit < obj.symbolDetail.MinLimitOrderVolume) {                                                             // 如果 计算出的 头寸大小  小于 合约规定的限价单 最小下单量,则
                obj.setLastError("可开 " + unit + " 手 无法开仓, " + (canOpen >= obj.symbolDetail.MinLimitOrderVolume ? "风控触发" : "资金限制"));  // 设置最新错误信息
                return;                                                                                                                        // 返回
            }
            obj.setTask((opCode == 1 ? ACT_LONG : ACT_SHORT), unit, function(ret) {                                        // 根据  opCode 设定,  调用 setTask 函数 设定任务
                if (!ret) {                                                                                                // 同样 第三个参数 是回调函数,回调函数中 ret 是触发 调用回调函数时传入的参数,任务的执行返回值。 
                    obj.setLastError("下单失败");
                    return;
                }
                Log(obj.symbolDetail.InstrumentName, obj.marketPosition == 0 ? "开仓" : "加仓", "离市周期", obj.leavePeriod, suffix);  // 任务成功完成,回调函数会执行此 输出
                obj.N = N;                                                                                                          // 开仓 或者 加仓后 更新N值 
                obj.openPrice = ret.price;                                                                                          // 更新 开仓价格
                obj.holdPrice = ret.position.Price;                                                                                 // 更新持仓均价,根据 任务执行的ret。
                if (obj.marketPosition == 0) {                                                                                      // 如果此时 加仓次数是0, 即代表本次是 建仓
                    obj.status.open++;                                                                                              // 开仓计数 累计
                }
                obj.holdAmount = ret.position.Amount;                                                                               // 更新持仓量
                obj.marketPosition += opCode == 1 ? 1 : -1;                                                                         // 根据 做多 或者 做空 累计 加仓次数
                obj.status.vm = [obj.marketPosition, obj.openPrice, N, obj.leavePeriod, obj.preBreakoutFailure];                    // 更新 用于恢复的 字符串 ,属性vm
                _G(obj.symbol, obj.status.vm);                                                                                      // 本地持久化储存 当前持仓信息。
            });
        };                                                                                                                          // Poll 函数结束
        var vm = null;                                                                                                              // 在New 构造函数中 声明一个 局部变量 vm 区别于obj.vm
        if (RMode === 0) {                                                                                                          // 如果进度恢复模式为 自动,下拉框第一个索引是0 ,设置为第一个时 下拉框参数就返回0 ,第二个 返回下一个索引1,以此类推。
            vm = _G(obj.symbol);                                                                                                    // 取回 持久化储存的数据 赋值给 局部变量vm
        } else {                                                                                                                    // 否则 恢复模式为 手动
            vm = JSON.parse(VMStatus)[obj.symbol];                                                                                  // 取手动恢复字符串 JSON解析后的数组中的对应于合约类型 obj.symbol 的 数据。
        }
        if (vm) {                                                                                                                   // 如果获取的有 数据
            Log("准备恢复进度, 当前合约状态为", vm);                                                                                     // 输出恢复的 合约状态
            obj.reset(vm[0], vm[1], vm[2], vm[3], vm[4]);                                                                            // 调用重设 函数 重新设置 恢复状态
        } else {                                                                                                                     // 如果vm 没有数据
            if (needRestore) {                                                                                                       // 需要恢复 则输出 没找到进度的信息, (有可能是 合约列表 中 有新的合约代码,则不需要恢复)
                Log("没有找到" + obj.symbol + "的进度恢复信息");
            }
            obj.reset();                                                                                                             // reset 不传参数 ,即重置
        }
        return obj;                                                                                                                  // 返回 构造完成的对象。
    }
};

function onexit() {                                                                // 策略程序 退出时执行。
    Log("已退出策略...");
}

function main() {
    if (exchange.GetName().indexOf('CTP') == -1) {                                 // 限定 连接的交易所 必须是 CTP 商品期货 
        throw "只支持商品期货CTP";
    }
    SetErrorFilter("login|ready|流控|连接失败|初始|Timeout");                         // 过滤常规错误
    var mode = exchange.IO("mode", 0);                                             // 设定行情模式 为立即返回模式 参看 API 文档: https://www.botvs.com/api
    if (typeof(mode) !== 'number') {                                               // 如果 切换模式 的API 返回的 不是 数值,即切换失败。
        throw "切换模式失败, 请更新到最新托管者!";                                      // 抛出异常
    }
    while (!exchange.IO("status")) {                                               // 检测 与 行情、交易服务器连接,直到  API  函数  exchange.IO("status") 返回true 连接上,退出循环
        Sleep(3000);
        LogStatus("正在等待与交易服务器连接, " + new Date());                          // 在未连接上时  输出 文本和 当前时间。
    }
    var positions = _C(exchange.GetPosition);                                      // 调用API  GetPosition 函数 获取 持仓信息
    if (positions.length > 0) {                                                    // 返回的数组不是空数组 ,即有持仓
        Log("检测到当前持有仓位, 系统将开始尝试恢复进度...");
        Log("持仓信息", positions);
    }
    Log("风险系数:", RiskRatio, "N值周期:", ATRLength, "系统1: 入市周期", EnterPeriodA, "离市周期", LeavePeriodA, "系统二: 入市周期", EnterPeriodB, "离市周期", LeavePeriodB, "加仓系数:", IncSpace, "止损系数:", StopLossRatio, "单品种最多开仓:", MaxLots, "次");
    // 输出 参数信息。
    var initAccount = _bot.GetAccount();                                           // 获取账户信息
    var initMargin = JSON.parse(exchange.GetRawJSON()).CurrMargin;                 // 调用 API GetRawJSON 函数 获取  : "CurrMargin": "当前保证金总额",
    var keepBalance = _N((initAccount.Balance + initMargin) * (KeepRatio/100), 3); // 根据预留保证金比例 计算出 需要预留的资金。
    Log("资产信息", initAccount, "保留资金:", keepBalance);                           // 输出信息
    
    var tts = [];
    var filter = [];                                                               // 过滤用数组
    var arr = Instruments.split(',');                                              // 合约列表按照逗号分隔 成数组
    for (var i = 0; i < arr.length; i++) {                                         // 遍历分隔后的数组
        var symbol = arr[i].replace(/^\s+/g, "").replace(/\s+$/g, "");             // 正则表达式 匹配 操作, 得出  合约代码
        if (typeof(filter[symbol]) !== 'undefined') {                              // 如果 在过滤数组中 存在 名为 symbol的属性,则显示信息 并跳过。
            Log(symbol, "已经存在, 系统已自动过滤");
            continue;
        }
        filter[symbol] = true;                                                     // 给过滤数组 添加 名为 symbol 的 属性,下次 同样的 合约代码 会被过滤
        var hasPosition = false;                                                   // 初始化 hasPosition 变量 false 代表没有持仓 
        for (var j = 0; j < positions.length; j++) {                               // 遍历 获取到的持仓信息
            if (positions[j].ContractType == symbol) {                             // 如果有持仓信息 合约 名称 和 symbol一样的, 给hasPosition 赋值true 代表有持仓
                hasPosition = true;
                break;
            }
        }
        var obj = TTManager.New(hasPosition, symbol, keepBalance, RiskRatio, ATRLength, EnterPeriodA, LeavePeriodA, EnterPeriodB, LeavePeriodB, UseEnterFilter, IncSpace, StopLossRatio, MaxLots);
        // 根据界面参数 使用 构造函数 New 构造  一个品种的海龟交易策略控制对象
        tts.push(obj);                                                             // 把该对象压入 tts 数组, 最终根据合约列表 ,生成了若干个品种的 控制对象储存在tts数组 
    }
    

    var preTotalHold = -1;
    var lastStatus = '';
    while (true) {                                                                 // 主要循环
        if (GetCommand() === "暂停/继续") {                                         // API GetCommand 函数 获取 程序界面上的 命令。此处 如果 点击了界面上的“暂停/继续”按钮
            Log("暂停交易中...");
            while (GetCommand() !== "暂停/继续") {                                  // 进入等待循环 ,直到再次点击  “暂停/继续” 按钮 退出 等待循环
                Sleep(1000);
            }
            Log("继续交易中...");
        }
        while (!exchange.IO("status")) {                                           // 一旦断开服务器的连接,则尝试重连 并等待。
            Sleep(3000);
            LogStatus("正在等待与交易服务器连接, " + new Date() + "\n" + lastStatus);  // 输出上一次的 状态栏 内容,并 更新时间。
        }
        var tblStatus = {                                                          // 用于显示在状态栏表格上的  持仓信息  对象
            type: "table",
            title: "持仓信息",
            cols: ["合约名称", "持仓方向", "持仓均价", "持仓数量", "持仓盈亏", "加仓次数", "开仓次数", "止损次数", "成功次数", "当前价格", "N"],
            rows: []
        };
        var tblMarket = {                                                          // 用于显示在状态栏表格上的 市场信息 对象
            type: "table",
            title: "运行状态",
            cols: ["合约名称", "合约乘数", "保证金率", "交易时间", "柱线长度", "上线", "下线", "止损价", "离市价", "异常描述", "发生时间"],
            rows: []
        };
        var totalHold = 0;
        var vmStatus = {};
        var ts = new Date().getTime();                                             // 当前时间戳
        var holdSymbol = 0;                                                        // 持有的合约量
        for (var i = 0; i < tts.length; i++) {                                     // 遍历tts数组
            tts[i].Poll();                                                         // 调用每个 合约的海龟管理对象的 Poll 函数
            var d = tts[i].Status();                                               // 更新每个 海龟管理对象的  状态 属性 status 并返回。
            if (d.holdAmount > 0) {                                                // 如果当前索引的对象 有 持仓
                vmStatus[d.symbol] = d.vm;                                         // 给空对象 vmStatus 添加合约名称 为属性名 的属性,并给其赋值 持仓信息vm
                holdSymbol++;                                                      // 给持有的合约品种数量  累计  
            }
            tblStatus.rows.push([d.symbolDetail.InstrumentName, d.holdAmount == 0 ? '--' : (d.marketPosition > 0 ? '多' : '空'), d.holdPrice, d.holdAmount, d.holdProfit, Math.abs(d.marketPosition), d.open, d.st, d.cover, d.lastPrice, d.N]);
            // 压入当前 索引 的 海龟管理对象 的信息 到状态分页表格
            tblMarket.rows.push([d.symbolDetail.InstrumentName, d.symbolDetail.VolumeMultiple, _N(d.symbolDetail.LongMarginRatio, 4) + '/' + _N(d.symbolDetail.ShortMarginRatio, 4), (d.isTrading ? '是#0000ff' : '否#ff0000'), d.recordsLen, d.upLine, d.downLine, d.stopPrice, d.leavePrice, d.lastErr, d.lastErrTime]);
            // 压入当前 索引 的 海龟管理对象 的信息 到行情分页表格
            totalHold += Math.abs(d.holdAmount);                                   // 值为回调函数 的参数ret 的属性 更新,可以参见 回调函数的 传入实参。processTask 函数中的 ret
            // 累计 总持仓手数
        }
        var now = new Date();                                                      // 获取最新时间
        var elapsed = now.getTime() - ts;                                          // 计算主要耗时代码 , 迭代 执行 Poll 函数的 开始与结束的 时间差。
        var tblAssets = _bot.GetAccount(true);                                     // 获取账户详细信息并返回一个表格对象。(因为参数传递的是true, 参见 模板的 GetAccount 函数的 getTable 参数)
        var nowAccount = _bot.Account();                                           // 获取账户信息
       
        if (tblAssets.rows.length > 10) {                                          // 如果获取的 表格的 行数 大于10
            // replace AccountId
            tblAssets.rows[0] = ["InitAccount", "初始资产", initAccount];           // 设置 索引 0 的行数 为 初始资金信息。
        } else {
            tblAssets.rows.unshift(["NowAccount", "当前可用", nowAccount], ["InitAccount", "初始资产", initAccount]); // 往 rows 数组 中开始的位置插入2个元素
        }
        lastStatus = '`' + JSON.stringify([tblStatus, tblMarket, tblAssets]) + '`\n轮询耗时: ' + elapsed + ' 毫秒, 当前时间: ' + now.toLocaleString() + ', 星期' + ['日', '一', '二', '三', '四', '五', '六'][now.getDay()] + ", 持有品种个数: " + holdSymbol;
        // 组合 各种 用于显示在界面的信息。
        if (totalHold > 0) {                                                       // 在有持仓时才 显示 手动恢复字符串(vmStatus JSON序列化)
            lastStatus += "\n手动恢复字符串: " + JSON.stringify(vmStatus);
        }
        LogStatus(lastStatus);                                                     // 调用API 显示在 状态栏
        if (preTotalHold > 0 && totalHold == 0) {                                  // 当全部持仓 平掉 没有持仓时
            LogProfit(nowAccount.Balance - initAccount.Balance - initMargin);      // 输出 盈利, 显示到收益曲线(此种情况 出现概率较低,很难有同时全部都未持仓的状态,所以收益都是 动态的,可以看 账户详细信息分析当前状况)
        }
        preTotalHold = totalHold;                                                  // 每次都更新  确保 输出收益只显示一次。 
        Sleep(LoopInterval * 1000);                                                // 轮询等待。避免API 访问过于频繁
    }                                                                                              // 源码地址 :https://www.botvs.com/strategy/17289
}

欢迎读者给我留言!提出建议和意见,如果感觉好玩可以分享给更多热爱程序热爱交易的朋友

程序员 littleDream 原创