资源加载中... loading...

和老白一起玩转JavaScript -- 创造一个会做买卖的小伙伴(8)实现一个多品种并发的商品期货策略

Author: 扫地僧, Created: 2017-03-18 17:20:46, Updated: 2017-10-11 10:38:05

实现一个多品种并发的商品期货策略(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.youquant.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”;


更多内容

haocow 詳細