海龟交易法起源
这个交易系统的诞生来源与两个交易老手的争论,一方觉得交易这种技能是后天习得的,另一方觉得这是先天所决定的。1983年,在新加坡海龟农场,两位神一样的交易员Dennis、Eckhardt有了分歧。E神说,交易员是天生的;D神说,就像这一大缸海龟一样,交易员可以培养。
于是,D神掏出来白花花的银子,要做实验,要打赌。他们在华尔街日报、纽约时报等等放出广告,说D神要搞培训班了,给每个人100万美元的账户,手把手地教,不限专业,无须经验,皆可报名。有千余人投了简历,40人进入面试,23人被留下考察,13人进入培训班。
这13个人来自各行各业,多数都没有交易经验,是一群尚未成功的普通人。他们被培训了两个星期,然后放出去交易,在接下来的四年半里,创出了80%的年均收益率。培训内容,叫做《海龟交易法则》;培训学员,被称为“海龟”。
尽管有人质疑样本的随机性,这场试验应该算D神胜利了。
海龟交易系统是一个完整的交易系统,它有一个完整的交易系统所应该有的所有成分,涵盖了成功交易中的每一个必要决策:
市场:买卖什么?
头寸规模:买卖多少? Unit=(1%∗Account)/N
入市:什么时候买卖?
止损:什么时候放弃一个亏损的头寸?
退出:什么时候退出一个盈利的头寸?
战术:怎么买卖?
核心围绕: N值,海龟的止损、加仓、头寸规模 都是基于N值计算, 有些海龟交易系统用的是ATR来代替N值,ATR为真实波幅的20日平均。
“海龟们从不去预测市场的动向,而是会寻找市场处于某种特定状态的指示信号。优秀的交易者不会试着预测市场下一步会怎么样;相反,他们会观察指示信号,判断市场现在正处于什么样的状态中。”
对于 “海龟交易法”感到陌生的读者可以看这篇文章: 发明者量化
也可以知乎或者百度搜索,有很多文章介绍,老白就不做赘述了。
作为本系列文章的最后收尾一篇,我们就来动手实践一个海龟策略,当然我们要有创新,我们实现一个“海龟群”。
说点题外的,之前的几篇文章记录的都是老白当时学习时的心路历程,学习量化、程序化没办法一蹴而就,只能脚踏实地,耐住性子一点点进步。老白开始的时候也感觉思考问题、找BUG 、写程序晕头转向的。但是,慢慢的我发现学习是个加速度,开始很慢,积累越多越轻松。一个完全零基础的朋友经常和我说:“越是感觉自己要放弃的时候,越是应该跟困难死磕的时候!”
言归正传, 为什么我们要使用海龟群呢?
当然是为了尽可能的分散风险,即使是大名鼎鼎的海龟策略,当年也曾经有过大幅回撤,甚至亏损本金。任何交易系统都是有一定风险的。多品种的好处就是“把鸡蛋放在不同的篮子里”。当然也有缺点,那就是需要不小的资金量。资金量小了,可能只能交易几个品种,降低了分散风险的能力。
还有一点需要牢记:任何时候都可能飞出一只黑天鹅! (如商品期货 16年底黑色星期五 全线暴跌。)
请下载最新托管者并比较版本号是否最新
$ ./robot -v
发明者量化 docker 3.0 compiled at 2016-07-05T09:56:18+0800
注释版多品种海龟源码
/*
参数:
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.fmz.cn/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 访问过于频繁
}
}
以上是上期 simnow 期货仿真账户测试。鉴于海龟策略 在一定行情中回撤还是有的,风险意识要强。本文章策略代码 是掌握海龟思路,深入学习程序化、量化策略代码设计的很好资料。
最后!一句很重要的话“敬畏市场!!!”
欢迎读者给我留言!提出建议和意见,如果感觉好玩可以分享给更多热爱程序热爱交易的朋友