教他点简单的知识(均线应用)
上一章节我们在沙盒系统里面实践了不少有趣的代码,今天我们需要让我们的小程序从沙盒中走出来,看看外面的世界,当然我们要先教它一点东西!
-
均线买卖逻辑
这个买卖逻辑可能是程序化交易、量化交易世界里面最简单的也是最基础的策略了, 原理很简单,运用的就是历史价格的均值对比从而判断标的物价格的未来走向。
即:一个标的物比如 MA705合约 (甲醇合约) 周期为10个Bar(K线柱)的均值 和周期为 8个Bar的均值,对比的差别。以下简称10个周期(长周期)的为慢线,8个周期(相对短周期)的为快线,可能有读者不明白,没关系,我们让小伙伴在沙盒系统中画出来(看明白了上一章的画图代码再画图简直so easy!),为了方便使用,上一期那一大堆画图的代码都封装成了一个文件了。这个画图模板地址:https://www.botvs.com/strategy/27293
复制了就出现在“模板引用”栏里面了,直接用,记得勾选上。
代码很简单:
var PreRecordTime = 0;
var isFirst = true;
function MainLoop(){
var info = exchange.SetContractType("MA705");
if(!info){
return;
}
var records = exchange.GetRecords();
if(!records || records.length < 10){ // 因为长周期 为10 所以要计算10个Bar的均值, 必须要有10个K线才能计算出来。
return; // 不满足的情况,返回,重新来。
}
if(isFirst){
PreRecordTime = records[records.length - 1].Time;
isFirst = false;
}
var fastLine = TA.MA(records, 8); // 均线指标,计算出给定 的K线数据 8个 Bar 的均值, 按顺序压入数组(随时间序列,和K线Bar时间同步形成一条线,所以叫快线) 返回这个数组给变量fastLine
var slowLine = TA.MA(records, 10); // 同上 区别是10个Bar ,所以叫慢线(周期大,均值变化的慢)。
var current_fastLine = fastLine[fastLine.length - 1]; // 这个数组的 倒数第一个索引 fastLine.length - 1 ,也就是表示的 快线的最后一个值 ,就是当前K线 对应的 快线均值。
var current_slowLine = slowLine[slowLine.length - 1]; // 同上
if(PreRecordTime !== records[records.length - 1].Time){ // K线更新了才确定当前最新的前一根Bar
$.PlotLine("fastLine", fastLine[fastLine.length - 2], PreRecordTime); // 引用了模板,可以直接使用这个模块导出函数 画线。(其实就是封装成 模块了)
$.PlotLine("slowLine", slowLine[slowLine.length - 2], PreRecordTime); // 同上 画指标线
PreRecordTime = records[records.length - 1].Time;
$.PlotLine("fastLine", current_fastLine, records[records.length - 1].Time);
$.PlotLine("slowLine", current_slowLine, records[records.length - 1].Time);
}else{
$.PlotLine("fastLine", current_fastLine, records[records.length - 1].Time); // 当前的不停在变动。
$.PlotLine("slowLine", current_slowLine, records[records.length - 1].Time);
}
$.PlotRecords(records, "MA705"); // 画K线
}
var cfg = $.GetCfg();
function main() {
var status = null;
cfg.yAxis = [{
title: {text: 'K线'}, //标题
style: {color: '#4572A7'}, //样式
opposite: false //生成右边Y轴
},
{
title:{text: "另一个Y轴"},
opposite: true //生成右边Y轴 ceshi
}
];
while(true){
status = exchange.IO("status"); // 调用API 确定连接状态
if(status === true){ // 判断状态
// LogStatus("已连接!"); // 在回测或者实际运行中显示一些实时数据、信息。
// 由于MainLoop 中用到了LogStatus 所以这个地方的要先注释掉, 以免覆盖掉信息
MainLoop(); // 连接上 交易所服务器后,执行主要工作函数。
}else{ // 如果没有连接上 即 exchange.IO("status") 函数返回 false
LogStatus("未连接状态!"); // 显示 未连接状态。
}
Sleep(1000); // 封装的睡眠函数,需要有轮询间隔, 以免访问过于频繁。CTP协议是每秒推送2次数据。
}
}
沙盒系统中运行出来看下:
可见在局部上涨的时候会出现快线从下上穿慢线,这种形态在代码中怎么描述呢?可以这样写:
// 判断指标形态
if(fastLine.length > 3 && slowLine.length > 3 && fastLine[fastLine.length - 3] < slowLine[slowLine.length - 3] && fastLine[fastLine.length - 2] > slowLine[slowLine.length - 2]){
$.PlotFlag(records[records.length - 2].Time, '已经交叉', 'X'); // 在图表上打个标记
}
快线从上下穿慢线也是同理。
我们就按照这个逻辑给程序添加一段代码。
-
教程序学会均线操作买卖
这里用到另外一个JavaScript写的模块,不过我这次先不引用,直接嵌入到我们的程序中
依然使用 MA705 合约(甲醇)作为交易标的物。
// --------交易模块-------------
var Interval = 500;
var SlideTick = 1;
var RiskControl = false;
var __orderCount = 0
var __orderDay = 0
function CanTrade(tradeAmount) {
if (!RiskControl) {
return true
}
if (typeof(tradeAmount) == 'number' && tradeAmount > MaxTradeAmount) {
Log("风控模块限制, 超过最大下单量", MaxTradeAmount, "#ff0000 @");
throw "中断执行"
return false;
}
var nowDay = new Date().getDate();
if (nowDay != __orderDay) {
__orderDay = nowDay;
__orderCount = 0;
}
__orderCount++;
if (__orderCount > MaxTrade) {
Log("风控模块限制, 不可交易, 超过最大下单次数", MaxTrade, "#ff0000 @");
throw "中断执行"
return false;
}
return true;
}
function init() {
if (typeof(SlideTick) === 'undefined') {
SlideTick = 1;
} else {
SlideTick = parseInt(SlideTick);
}
Log("商品交易类库加载成功");
}
function GetPosition(e, contractType, direction, positions) {
var allCost = 0;
var allAmount = 0;
var allProfit = 0;
var allFrozen = 0;
var posMargin = 0;
if (typeof(positions) === 'undefined' || !positions) {
positions = _C(e.GetPosition);
}
for (var i = 0; i < positions.length; i++) {
if (positions[i].ContractType == contractType &&
(((positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD) && direction == PD_LONG) || ((positions[i].Type == PD_SHORT || positions[i].Type == PD_SHORT_YD) && direction == PD_SHORT))
) {
posMargin = positions[i].MarginLevel;
allCost += (positions[i].Price * positions[i].Amount);
allAmount += positions[i].Amount;
allProfit += positions[i].Profit;
allFrozen += positions[i].FrozenAmount;
}
}
if (allAmount === 0) {
return null;
}
return {
MarginLevel: posMargin,
FrozenAmount: allFrozen,
Price: _N(allCost / allAmount),
Amount: allAmount,
Profit: allProfit,
Type: direction,
ContractType: contractType
};
}
function Open(e, contractType, direction, opAmount) {
var initPosition = GetPosition(e, contractType, direction);
var isFirst = true;
var initAmount = initPosition ? initPosition.Amount : 0;
var positionNow = initPosition;
while (true) {
var needOpen = opAmount;
if (isFirst) {
isFirst = false;
} else {
positionNow = GetPosition(e, contractType, direction);
if (positionNow) {
needOpen = opAmount - (positionNow.Amount - initAmount);
}
}
var insDetail = _C(e.SetContractType, contractType);
//Log("初始持仓", initAmount, "当前持仓", positionNow, "需要加仓", needOpen);
if (needOpen < insDetail.MinLimitOrderVolume) {
break;
}
if (!CanTrade(opAmount)) {
break;
}
var depth = _C(e.GetDepth);
var amount = Math.min(insDetail.MaxLimitOrderVolume, needOpen);
e.SetDirection(direction == PD_LONG ? "buy" : "sell");
var orderId;
if (direction == PD_LONG) {
orderId = e.Buy(depth.Asks[0].Price + (insDetail.PriceTick * SlideTick), Math.min(amount, depth.Asks[0].Amount), contractType, 'Ask', depth.Asks[0]);
} else {
orderId = e.Sell(depth.Bids[0].Price - (insDetail.PriceTick * SlideTick), Math.min(amount, depth.Bids[0].Amount), contractType, 'Bid', depth.Bids[0]);
}
// CancelPendingOrders
while (true) {
Sleep(Interval);
var orders = _C(e.GetOrders);
if (orders.length === 0) {
break;
}
for (var j = 0; j < orders.length; j++) {
e.CancelOrder(orders[j].Id);
if (j < (orders.length - 1)) {
Sleep(Interval);
}
}
}
}
var ret = {
price: 0,
amount: 0,
position: positionNow
};
if (!positionNow) {
return ret;
}
if (!initPosition) {
ret.price = positionNow.Price;
ret.amount = positionNow.Amount;
} else {
ret.amount = positionNow.Amount - initPosition.Amount;
ret.price = _N(((positionNow.Price * positionNow.Amount) - (initPosition.Price * initPosition.Amount)) / ret.amount);
}
return ret;
}
function Cover(e, contractType) {
var insDetail = _C(e.SetContractType, contractType);
while (true) {
var n = 0;
var opAmount = 0;
var positions = _C(e.GetPosition);
for (var i = 0; i < positions.length; i++) {
if (positions[i].ContractType != contractType) {
continue;
}
var amount = Math.min(insDetail.MaxLimitOrderVolume, positions[i].Amount);
var depth;
if (positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD) {
depth = _C(e.GetDepth);
opAmount = Math.min(amount, depth.Bids[0].Amount);
if (!CanTrade(opAmount)) {
return;
}
e.SetDirection(positions[i].Type == PD_LONG ? "closebuy_today" : "closebuy");
e.Sell(depth.Bids[0].Price - (insDetail.PriceTick * SlideTick), opAmount, contractType, positions[i].Type == PD_LONG ? "平今" : "平昨", 'Bid', depth.Bids[0]);
n++;
} else if (positions[i].Type == PD_SHORT || positions[i].Type == PD_SHORT_YD) {
depth = _C(e.GetDepth);
opAmount = Math.min(amount, depth.Asks[0].Amount);
if (!CanTrade(opAmount)) {
return;
}
e.SetDirection(positions[i].Type == PD_SHORT ? "closesell_today" : "closesell");
e.Buy(depth.Asks[0].Price + (insDetail.PriceTick * SlideTick), opAmount, contractType, positions[i].Type == PD_SHORT ? "平今" : "平昨", 'Ask', depth.Asks[0]);
n++;
}
}
if (n === 0) {
break;
}
while (true) {
Sleep(Interval);
var orders = _C(e.GetOrders);
if (orders.length === 0) {
break;
}
for (var j = 0; j < orders.length; j++) {
e.CancelOrder(orders[j].Id);
if (j < (orders.length - 1)) {
Sleep(Interval);
}
}
}
}
}
var PositionManager = (function() {
function PositionManager(e) {
if (typeof(e) === 'undefined') {
e = exchange;
}
if (e.GetName() !== 'Futures_CTP') {
throw 'Only support CTP';
}
this.e = e;
this.account = null;
}
// Get Cache
PositionManager.prototype.Account = function() {
if (!this.account) {
this.account = _C(this.e.GetAccount);
}
return this.account;
};
PositionManager.prototype.GetAccount = function(getTable) {
this.account = _C(this.e.GetAccount);
if (typeof(getTable) !== 'undefined' && getTable) {
return AccountToTable(this.e.GetRawJSON())
}
return this.account;
};
PositionManager.prototype.GetPosition = function(contractType, direction, positions) {
return GetPosition(this.e, contractType, direction, positions);
};
PositionManager.prototype.OpenLong = function(contractType, shares) {
if (!this.account) {
this.account = _C(this.e.GetAccount);
}
return Open(this.e, contractType, PD_LONG, shares);
};
PositionManager.prototype.OpenShort = function(contractType, shares) {
if (!this.account) {
this.account = _C(this.e.GetAccount);
}
return Open(this.e, contractType, PD_SHORT, shares);
};
PositionManager.prototype.Cover = function(contractType) {
if (!this.account) {
this.account = _C(this.e.GetAccount);
}
return Cover(this.e, contractType);
};
PositionManager.prototype.CoverAll = function() {
if (!this.account) {
this.account = _C(this.e.GetAccount);
}
while (true) {
var positions = _C(this.e.GetPosition)
if (positions.length == 0) {
break
}
for (var i = 0; i < positions.length; i++) {
// Cover Hedge Position First
if (positions[i].ContractType.indexOf('&') != -1) {
Cover(this.e, positions[i].ContractType)
Sleep(1000)
}
}
for (var i = 0; i < positions.length; i++) {
if (positions[i].ContractType.indexOf('&') == -1) {
Cover(this.e, positions[i].ContractType)
Sleep(1000)
}
}
}
};
PositionManager.prototype.Profit = function(contractType) {
var accountNow = _C(this.e.GetAccount);
return _N(accountNow.Balance - this.account.Balance);
};
return PositionManager;
})();
$.NewPositionManager = function(e) {
return new PositionManager(e);
};
// Via: http://mt.sohu.com/20160429/n446860150.shtml
$.IsTrading = function(symbol) {
var now = new Date();
var day = now.getDay();
var hour = now.getHours();
var minute = now.getMinutes();
if (day === 0 || (day === 6 && (hour > 2 || hour == 2 && minute > 30))) {
return false;
}
symbol = symbol.replace('SPD ', '').replace('SP ', '');
var p, i, shortName = "";
for (i = 0; i < symbol.length; i++) {
var ch = symbol.charCodeAt(i);
if (ch >= 48 && ch <= 57) {
break;
}
shortName += symbol[i].toUpperCase();
}
var period = [
[9, 0, 10, 15],
[10, 30, 11, 30],
[13, 30, 15, 0]
];
if (shortName === "IH" || shortName === "IF" || shortName === "IC") {
period = [
[9, 30, 11, 30],
[13, 0, 15, 0]
];
} else if (shortName === "TF" || shortName === "T") {
period = [
[9, 15, 11, 30],
[13, 0, 15, 15]
];
}
if (day >= 1 && day <= 5) {
for (i = 0; i < period.length; i++) {
p = period[i];
if ((hour > p[0] || (hour == p[0] && minute >= p[1])) && (hour < p[2] || (hour == p[2] && minute < p[3]))) {
return true;
}
}
}
var nperiod = [
[
['AU', 'AG'],
[21, 0, 02, 30]
],
[
['CU', 'AL', 'ZN', 'PB', 'SN', 'NI'],
[21, 0, 01, 0]
],
[
['RU', 'RB', 'HC', 'BU'],
[21, 0, 23, 0]
],
[
['P', 'J', 'M', 'Y', 'A', 'B', 'JM', 'I'],
[21, 0, 23, 30]
],
[
['SR', 'CF', 'RM', 'MA', 'TA', 'ZC', 'FG', 'IO'],
[21, 0, 23, 30]
],
];
for (i = 0; i < nperiod.length; i++) {
for (var j = 0; j < nperiod[i][0].length; j++) {
if (nperiod[i][0][j] === shortName) {
p = nperiod[i][1];
var condA = hour > p[0] || (hour == p[0] && minute >= p[1]);
var condB = hour < p[2] || (hour == p[2] && minute < p[3]);
// in one day
if (p[2] >= p[0]) {
if ((day >= 1 && day <= 5) && condA && condB) {
return true;
}
} else {
if (((day >= 1 && day <= 5) && condA) || ((day >= 2 && day <= 6) && condB)) {
return true;
}
}
return false;
}
}
}
return false;
};
// --------交易模块完结----------
var PreRecordTime = 0;
var isFirst = true;
var IDLE = 0;
var OPENLONG = 1;
var COVER = 2;
var STATE = IDLE;
var InitAccount = null;
function MainLoop(){
var info = exchange.SetContractType("MA705");
if(!info){
return;
}
var records = exchange.GetRecords();
if(!records || records.length < 10){ // 因为长周期 为10 所以要计算10个Bar的均值, 必须要有10个K线才能计算出来。
return; // 不满足的情况,返回,重新来。
}
if(isFirst){
PreRecordTime = records[records.length - 1].Time;
InitAccount = exchange.GetAccount();
isFirst = false;
}
var fastLine = TA.MA(records, 5); // 均线指标,计算出给定 的K线数据 8个 Bar 的均值, 按顺序压入数组(随时间序列,和K线Bar时间同步形成一条线,所以叫快线) 返回这个数组给变量fastLine
var slowLine = TA.MA(records, 10); // 同上 区别是10个Bar ,所以叫慢线(周期大,均值变化的慢)。
var current_fastLine = fastLine[fastLine.length - 1]; // 这个数组的 倒数第一个索引 fastLine.length - 1 ,也就是表示的 快线的最后一个值 ,就是当前K线 对应的 快线均值。
var current_slowLine = slowLine[slowLine.length - 1]; // 同上
if(PreRecordTime !== records[records.length - 1].Time){ // K线更新了才确定当前最新的前一根Bar
$.PlotLine("fastLine", fastLine[fastLine.length - 2], PreRecordTime); // 引用了模板,可以直接使用这个模块导出函数 画线。(其实就是封装成 模块了)
$.PlotLine("slowLine", slowLine[slowLine.length - 2], PreRecordTime); // 同上 画指标线
PreRecordTime = records[records.length - 1].Time;
$.PlotLine("fastLine", current_fastLine, records[records.length - 1].Time);
$.PlotLine("slowLine", current_slowLine, records[records.length - 1].Time);
}else{
$.PlotLine("fastLine", current_fastLine, records[records.length - 1].Time); // 当前的不停在变动。
$.PlotLine("slowLine", current_slowLine, records[records.length - 1].Time);
}
$.PlotRecords(records, "MA705"); // 画K线
// 判断指标形态
if(STATE === IDLE && fastLine.length > 3 && slowLine.length > 3 && fastLine[fastLine.length - 3] < slowLine[slowLine.length - 3] && fastLine[fastLine.length - 2] > slowLine[slowLine.length - 2]){
P.OpenLong("MA705", 1);
STATE = OPENLONG;
$.PlotFlag(new Date().getTime(), '快线上传慢线', 'OPENLONG');
}
if(STATE === OPENLONG && fastLine[fastLine.length - 3] > slowLine[slowLine.length - 3] && fastLine[fastLine.length - 2] < slowLine[slowLine.length - 2]){
P.Cover("MA705");
STATE = COVER;
$.PlotFlag(new Date().getTime(), '快线下传慢线', 'COVER');
}
if(STATE === COVER){
var nowAccount = exchange.GetAccount();
LogProfit(nowAccount.Balance - InitAccount.Balance, nowAccount);
STATE = IDLE;
}
}
var cfg = $.GetCfg();
var P = null;
function main() {
var status = null;
P = $.NewPositionManager(exchange); // 构造一个 用于控制交易细节的对象。
cfg.yAxis = [{
title: {text: 'K线'}, //标题
style: {color: '#4572A7'}, //样式
opposite: false //生成右边Y轴
},
{
title:{text: "另一个Y轴"},
opposite: true //生成右边Y轴 ceshi
}
];
while(true){
status = exchange.IO("status"); // 调用API 确定连接状态
if(status === true){ // 判断状态
// LogStatus("已连接!"); // 在回测或者实际运行中显示一些实时数据、信息。
// 由于MainLoop 中用到了LogStatus 所以这个地方的要先注释掉, 以免覆盖掉信息
MainLoop(); // 连接上 交易所服务器后,执行主要工作函数。
}else{ // 如果没有连接上 即 exchange.IO("status") 函数返回 false
LogStatus("未连接状态!"); // 显示 未连接状态。
}
Sleep(1000); // 封装的睡眠函数,需要有轮询间隔, 以免访问过于频繁。CTP协议是每秒推送2次数据。
}
}
沙盒回测如图:
回测中震荡市回撤很大,可见这个交易逻辑很不完善,在上升趋势中是正向收益,不代表没有问题(震荡行情可能被震死 =_= )。所以这里只是研究学习,别用于实盘。
-
蹦出沙盒,看看外面的世界 --- 用simnow上期技术 提供的仿真期货模拟账户玩一把。
详细的申请 simnow 商品期货 模拟账号可以看这个帖子: https://www.botvs.com/bbs-topic/325 直接可以使用CTP协议程序化模拟交易。
程序跑了一段时间,策略本身是演示性质的,大家仅供参考,如需完善还可以加入 仓位控制、止盈止损、爆仓预警等等 ~(这就看 原生JS编程语言的发挥了,自由度很高,编写策略还是很有乐趣的。)
仿真账户,可以放在这测试。(不是真钱!果然心态不一样,✧(≖ ◡ ≖✿)嘿嘿)