输入/搜索内容
1
关注
280
关注者
和老白一起玩转JavaScript -- 创造一个会做买卖的小伙伴(8)实现一个多品种并发的商品期货策略
商品期货
创建于 2017-03-18 17:20:46  更新于 2017-10-11 10:38:05
 1
 4722

实现一个多品种并发的商品期货策略(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年底黑色星期五 全线暴跌。)

注释版源代码:

javascript
/* 参数: 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.youquant.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.youquant.com/strategy/17289

附个 simnow 模拟盘测试

img

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

https://www.youquant.com/bbs-topic/745
img

程序员 littleDream 原创

相关推荐
评论
全部评论 (1)

    詳細

    8 年前
  • 1
iPhone 下载
社区
回测系统
© 2015 - ∞ YouQuant 豫ICP备19046564号