和老白一起玩转JavaScript -- 创造一个会做买卖的小伙伴(6)开发机器人用过的工具代码
在开发机器人的过程中积累了不少小代码片段,有些是可以借鉴到自己的量化策略程序中的,这里老白拿出自己写的一些代码抛砖引玉,作为初学者写的比较菜,各位读者斧正。
-
1、转换任意K线周期
这个代码模块没有写成类库,只是写了个函数,也方便后期修改扩展。作用就是根据一定周期的基础K线数据合成大周期的K线,在平时看行情、或者使用平台写策略的时候,默认的都是 常用周期的K线,如1天、1小时、1分钟等等, 如果需要处理 其它周期的数据,比如4小时、6小时、8小时K线,那么就需要自己动手撸了。所以就写了这个。
源码地址:https://www.botvs.com/strategy/35986// K线周期合成 扩展为 根据基础K线 合成 为任意周期。
var cloneObj = function(obj) { // 深拷贝 对象函数
var str, newobj = obj.constructor === Array ? [] : {};
if (typeof obj !== 'object') {
return;
} else if (JSON) {
str = JSON.stringify(obj); //系列化对象
newobj = JSON.parse(str); //还原
} else {
for (var i in obj) {
newobj[i] = typeof obj[i] === 'object' ?
cloneObj(obj[i]) : obj[i];
}
}
return newobj;
};
var DAY = 0;
var HOURS = 1;
var MINUTES = 2;
var isFirstFind = true;
var FirstStamp = null;
function GetDHM(objTime, BaseCycle, NewCycleForMS){
var ret = [];
if(BaseCycle % (1000 * 60 * 60 * 24) === 0){
ret[0] = objTime.getDate();
ret[1] = DAY;
}else if(BaseCycle % (1000 * 60 * 60) === 0){
ret[0] = objTime.getHours();
ret[1] = HOURS;
}else if(BaseCycle % (1000 * 60) === 0){
ret[0] = objTime.getMinutes();
ret[1] = MINUTES;
}
if(NewCycleForMS % (1000 * 60 * 60 * 24) === 0){
ret[2] = DAY;
}else if(NewCycleForMS % (1000 * 60 * 60) === 0){
ret[2] = HOURS;
}else if(NewCycleForMS % (1000 * 60) === 0){
ret[2] = MINUTES;
}
return ret;
}
function SearchFirstTime(ret, BaseCycle, NewCycleForMS){
if(ret[1] === DAY && ret[2] === DAY){
var array_day = [];
for(var i = 1 ; i < 29; i += (NewCycleForMS / BaseCycle)){
array_day.push(i);
}
for(var j = 0 ; j < array_day.length; j++ ){
if(ret[0] === array_day[j]){
return true;
}
}
}else if(ret[1] === HOURS && ret[2] === HOURS){
var array_hours = [];
for(var i = 0 ; i < 24; i += (NewCycleForMS / BaseCycle)){
array_hours.push(i);
}
for(var j = 0 ; j < array_hours.length ; j++){
if(ret[0] === array_hours[j]){
return true;
}
}
}else if(ret[1] === MINUTES && ret[2] === MINUTES){
var array_minutes = [];
for(var i = 0; i < 60; i += (NewCycleForMS / BaseCycle)){
array_minutes.push(i);
}
for(var j = 0; j < array_minutes.length; j++){
if(ret[0] === array_minutes[j]){
return true;
}
}
}else{
throw "目标周期与基础周期不匹配!目标周期毫秒数:" + NewCycleForMS + " 基础周期毫秒数: " + BaseCycle;
}
}
function Calc_High(AssRecords, n, BaseCycle, NewCycleForMS){
var max = AssRecords[n].High;
for(var i = 1 ; i < NewCycleForMS / BaseCycle; i++){
max = Math.max(AssRecords[n + i].High, max);
}
return max;
}
function Calc_Low(AssRecords, n, BaseCycle, NewCycleForMS){
var min = AssRecords[n].Low;
for(var i = 1 ; i < NewCycleForMS / BaseCycle; i++){
min = Math.min(AssRecords[n + i].Low, min);
}
return min;
}
function AssembleRecords(records, NewCycleForMS) {
var AssRecords = records.slice(0); // 深拷贝
var AfterAssRecords = [];
if(!records || records.length < 2){
throw (!records) ? "传入的records参数为 错误" + records : "基础K线长度小于2";
}
var BaseCycle = records[records.length - 1].Time - records[records.length - 2].Time;
if(NewCycleForMS % BaseCycle !== 0){
throw "目标周期‘" + NewCycleForMS + "’不是 基础周期 ‘" + BaseCycle + "’ 的整倍数,无法合成!";
}
if(NewCycleForMS / BaseCycle > records.length){
throw "基础K线数量不足,请检查是否基础K线周期过小!";
}
// 判断时间戳, 找到 基础K线 相对于 目标K线的起始时间。
var objTime = new Date();
for (var i = 0; i < AssRecords.length; i++) {
objTime.setTime(AssRecords[i].Time);
var ret = GetDHM(objTime, BaseCycle, NewCycleForMS);
if (isFirstFind === true && SearchFirstTime(ret, BaseCycle, NewCycleForMS) === true) {
FirstStamp = AssRecords[i].Time;
for (j = 0; j < i; j++) {
AssRecords.shift(); // 把目标K线周期前不满足合成的数据排除。
}
isFirstFind = false;
break; // 排除后跳出
}else if(isFirstFind === false){
if((AssRecords[i].Time - FirstStamp) % NewCycleForMS === 0){
for (j = 0; j < i; j++) {
AssRecords.shift(); // 把目标K线周期前不满足合成的数据排除。
}
break;
}
}
}
var BarObj = { // 定义一个 K线柱结构
Time: 0,
Open: 0,
High: 0,
Low: 0,
Close: 0,
Volume: 0,
};
var n = 0;
for (n = 0; n < AssRecords.length - (NewCycleForMS / BaseCycle); n += (NewCycleForMS / BaseCycle)) { // 合成
/*
{
Time :一个时间戳, 精确到毫秒,与Javascript的 new Date().getTime() 得到的结果格式一样
Open :开盘价
High :最高价
Low :最低价
Close :收盘价
Volume :交易量
}
*/
BarObj.Time = AssRecords[n].Time;
BarObj.Open = AssRecords[n].Open;
BarObj.High = Calc_High(AssRecords, n, BaseCycle, NewCycleForMS);
BarObj.Low = Calc_Low(AssRecords, n, BaseCycle, NewCycleForMS);
BarObj.Close = AssRecords[n + (NewCycleForMS / BaseCycle) - 1].Close;
BarObj.Volume = AssRecords[n + (NewCycleForMS / BaseCycle) - 1].Volume;
AfterAssRecords.push(cloneObj(BarObj));
}
BarObj.Time = AssRecords[n - (NewCycleForMS / BaseCycle)].Time + NewCycleForMS; // 最后一根时间不能变,
BarObj.Open = AssRecords[n].Open;
BarObj.Close = AssRecords[AssRecords.length - 1].Close;
BarObj.Volume = AssRecords[AssRecords.length - 1].Volume;
var max = AssRecords[n].High;
var min = AssRecords[n].Low;
for(var index_n = n + 1 ;index_n < AssRecords.length; index_n++){
max = Math.max(max, AssRecords[index_n].High);
min = Math.min(min, AssRecords[index_n].Low);
}
BarObj.High = max;
BarObj.Low = min;
AfterAssRecords.push(cloneObj(BarObj));
return AfterAssRecords;
}
function main() { // 测试代码
while(!exchange.IO("status")){
LogStatus("未连接!");
}
var Info = _C(exchange.SetContractType, "MA705"); // 试试甲醇705合约的K线数据合成
var records = exchange.GetRecords();
while (!records || records.length < 24) {
records = exchange.GetRecords();
}
// 处理界面参数, 如果写到自己的策略里面 可以参考下
var Num_UI_NewCycleForMS = 1;
var arrayNum = UI_NewCycleForMS.split("*");
for(var indexNum = 0 ; indexNum < arrayNum.length ; indexNum++){
Num_UI_NewCycleForMS = Num_UI_NewCycleForMS * Number(arrayNum[indexNum]);
}
Log("自定义周期毫秒时间为:", Num_UI_NewCycleForMS);
while(true){
records = _C(exchange.GetRecords);
// Log("原始K线数据:长度", records.length, "数据:", records);
records = AssembleRecords(records, Num_UI_NewCycleForMS); // 第一个参数是 基础K线, 第二个参数是 要转换的周期的 毫秒数, 1000 * 60 * 20 就是 转换为 20分钟
// Log("转换后K线数据:长度", records.length, "数据:", records);
$.PlotRecords(records, 'BTC');
// throw "stop"; // ceshi
Sleep(1000);
}
}
- #### 2、传统期货差价监控 (CTP)
当需要分析两个品种差价走势的时候,会用上这段代码。有时候也会把这段代码修改集成到自己的策略程序里面(比如跨期对冲策略),代码会绘制出一个差价走势图,在学习如何让机器人程序画图也是很有帮助的,很好的例子。
源码地址: https://www.botvs.com/strategy/5379
var __lastDiff = 0;
var __AType = ["Last", "Buy", "Sell"][AType];
var __BType = ["Last", "Buy", "Sell"][BType];
function _N(v, precision) {
if (typeof(precision) != 'number') {
precision = 4;
}
var d = parseFloat(v.toFixed(Math.max(10, precision + 5)));
s = d.toString().split(".");
if (s.length < 2 || s[1].length <= precision) {
return d;
}
var b = Math.pow(10, precision);
return Math.floor(d * b) / b;
}
function EnsureCall(method) {
var r;
while (!(r = method.apply(this, Array.prototype.slice.call(arguments).slice(1)))) {
Sleep(Interval);
}
return r;
}
function onTick() {
var a = EnsureCall(exchange.SetContractType, AInstrument);
var tickerA = EnsureCall(exchange.GetTicker);
var b = EnsureCall(exchange.SetContractType, BInstrument);
var tickerB = EnsureCall(exchange.GetTicker);
var diff = _N(tickerA[__AType] - tickerB[__BType]);
LogStatus(a.InstrumentName, _N(tickerA[__AType]), b.InstrumentName, _N(tickerB[__BType]), "差价:", diff);
if (__lastDiff != 0) {
if (Math.abs(Math.abs(diff) - Math.abs(__lastDiff)) > 200) {
return;
}
}
if (diff != __lastDiff) {
// add添加数据到series, 参数格式为[series序号, 数据];
__chart.add([0, [new Date().getTime(), diff]]);
__lastDiff = diff;
}
}
function main() {
if (exchange.GetName().indexOf("Futures_CTP") == -1) {
throw "只支持传统期货(CTP)";
}
SetErrorFilter("login|ready|流控|连接失败|Timeout");
// 传给Chart函数的必须是一个与上下文无关的结构体(附合HighStocks规则, 详情参数HighStocks使用方法)
__chart = Chart({
tooltip: {
xDateFormat: '%Y-%m-%d %H:%M:%S, %A'
},
title: {
text: '差价分析图'
},
rangeSelector: {
buttons: [{
type: 'hour',
count: 1,
text: '1h'
}, {
type: 'hour',
count: 3,
text: '3h'
}, {
type: 'hour',
count: 8,
text: '8h'
}, {
type: 'all',
text: 'All'
}],
selected: 0,
inputEnabled: false
},
xAxis: {
type: 'datetime'
},
yAxis: {
plotLines: [{
value: NormalDiff,
color: 'green',
dashStyle: 'shortdash',
width: 1,
}, {
value: HighDiff,
color: 'red',
dashStyle: 'shortdash',
width: 1,
}, {
value: -NormalDiff,
color: 'green',
dashStyle: 'shortdash',
width: 1,
}, {
value: -HighDiff,
color: 'red',
dashStyle: 'shortdash',
width: 1,
}]
},
series: [{
name: '价差',
data: [],
tooltip: {
valueDecimals: 2
}
}]
});
// reset 清空所有图表之前的信息
// __chart.reset();
var a = EnsureCall(exchange.SetContractType, AInstrument);
var b = EnsureCall(exchange.SetContractType, BInstrument);
Log(a.InstrumentName + "." + __AType, "-", b.InstrumentName + "." + __BType, '差价做为收益显示到图表');
TickInterval = Math.max(TickInterval, 50);
Interval = Math.max(Interval, 50);
while (true) {
onTick();
Sleep(TickInterval);
}
}
- #### 3、CTP手动全平CTP商品期货持仓
在Simnow 上测试 商品期货策略时经常需要把已经开过的仓位平掉重新测试代码,这样就需要个类似一键平仓的程序来处理 恢复模拟账号未开仓状态。这里使用了一个交易处理模块: $.NewPositionManager 就是该模块的接口函数,作用是生成一个对象,可以调用该对象的方法处理具体操作,比如 全平仓: CoverAll(); 。 做了一点额外的功能,在全部平仓完以后,会打印出所有交易的标的物名称。
var p = $.NewPositionManager();
function main() {
while(true){
if(exchange.IO("status") === true){
p.CoverAll();
var positions = _C(exchange.GetPosition);
if(positions.length === 0){
Log("positions is :", positions);
break;
}
}else{
LogStatus("未连接服务器,等待");
}
Sleep(2000);
}
var dict = exchange.IO("instruments"); // 返回交易所所有的产品列表{产品名: 详情}字典形式
for(var k in dict){
Log("产品名:", k, "详细信息: 产品名称", dict[k] );
}
Log("exit.", _C(exchange.GetPosition));
}
- #### 4、商品期货主力合约过滤
在处理商品期货合约的连续性时会遇到主力合约的问题,如何更快的过滤识别出主力合约呢? 同样也写了个代码模块,可以改造,嵌入,或者单独使用。
在 filter 变量中指定要 扫描的合约代码头。(即不含日期信息的合约代码的部分)
var str = null;
var strArray = [];
function main() {
var filter = ['MA','CF','zn','SR','pp','l','ni','i','v','jm','al','jd','cs','p'];
var products = [];
Log("等待连接到交易服务器");
while (!exchange.IO("status")) Sleep(1000);
Log("开始获取所有合约");
var instruments = _C(exchange.IO, "instruments");
Log("合约列表获取成功");
var len = 0;
for (var instrumentId in instruments) {
len++;
}
Log("合约列表长度为:", len);
for (var instrumentId in instruments) {
if (instruments[instrumentId].IsTrading) {
var found = false;
for (var i = 0; i < filter.length; i++) {
if (instruments[instrumentId].ProductID == filter[i]) {
found = true;
}
}
if (!found) {
continue;
}
if (typeof(products[instruments[instrumentId].ProductID]) === 'undefined') {
products[instruments[instrumentId].ProductID] = [];
}
products[instruments[instrumentId].ProductID].push(instrumentId);
}
}
for (var product in products) {
var ss = products[product];
Log("订阅", product, "的", ss.length, "种合约, 以识别主力合约");
var vol = 0,
volIdx = 0;
for (var i = 0; i < ss.length; i++) {
_C(exchange.SetContractType, ss[i]);
}
Sleep(5000);
for (var i = 0; i < ss.length; i++) {
_C(exchange.SetContractType, ss[i]);
var ticker = exchange.GetTicker();
if (ticker) {
var obj = JSON.parse(exchange.GetRawJSON());
if (obj.OpenInterest > vol) {
vol = obj.OpenInterest;
volIdx = i;
}
}
}
// 取消订阅行情(之后此合约K线将停止收集), 当然也可以不取消, 这里演示用
for (var i = 0; i < ss.length; i++) {
_C(exchange.SetContractType, "-" + ss[i]);
}
strArray.push(ss[volIdx]);
Log("主力合约为", ss[volIdx], "持仓", vol, '#ff0000');
}
for(var i = 0 ; i < strArray.length; i++){
str += strArray[i] + ',';
}
Log("主力合约:", str);
}
- #### 5、交互模块
有时候需要给机器人交互,需要下命令、改参数、获取详细运行状态参数 就需要交互代码了。
function get_Command(){//负责交互的函数,交互及时更新 相关数值 ,熟悉的用户可以自行扩展
var keyValue = 0;// 命令传来的参数 数值
var way = null; //路由
var cmd = GetCommand(); //获取 交互命令API
if (cmd) {
Log("按下了按钮:",cmd);//日志显示
arrStr = cmd.split(":"); // GetCommand 函数返回的 是一个字符串,这里我处理的麻烦了,因为想熟悉一下JSON
//,所以先对字符串做出处理,把函数返回的字符串以 : 号分割成2个字符串。储存在字符串数组中。
if(arrStr.length === 2){//接受的不是 按钮型的,是数值型。
jsonObjStr = '{' + '"' + arrStr[0] + '"' + ':' + arrStr[1] + '}'; // 把 字符串数组中的元素重新
//拼接 ,拼接成 JSON 字符串 用于转换为JSON 对象。
jsonObj = JSON.parse(jsonObjStr); // 转换为JSON 对象
for(var key in jsonObj){ // 遍历对象中的 成员名
keyValue = jsonObj[key]; //取出成员名对应的 值 , 就是交互按钮的值
}
if(arrStr[0] == "upDateAmount"){// 此处为 数字型 。这里处理分为 按钮 和 数字型 。 详见 策略参数 设置界面 下的 交互设置
way = 1;
}
if(arrStr[0] == "扩展1"){
way = 2;
}
if(arrStr[0] == "扩展2"){
way = 3;
}
if(arrStr[0] == "扩展3"){
way = 4;
}
}else if(arrStr.length === 1){// 此处为 按钮型
//路由
if(cmd == "cmdOpen"){
way = 0;
}
if(cmd == "cmdCover"){
way = 5;
}
}else{
throw "error:" + cmd + "--" + arrStr;
}
switch(way){ // 分支选择 操作
case 0://处理 发出开仓信号
tiaojian = 1;
break;
case 1://处理
Amount = keyValue;//把交互界面设置的 数值 传递给 Amount
Log("开仓量修改为:",Amount);//提示信息
break;
case 2://处理
break;
case 3://处理
break;
case 4://处理
break;
case 5://处理 发出平仓信号
tiaojian = 2;
break;
default: break;
}
}
}
有时我们甚至需要在机器人运行时插入运行JS 代码:
var cmd = GetCommand(); // 调用API 获取界面交互控件的消息。
if (cmd) { // 判断是否有消息
var js = cmd.split(':', 2)[1]; // 分割 返回的消息 字符串, 限制返回2个, 把索引为1的 元素 赋值给 名为js 的变量
Log("执行调试代码:", js); // 输出 执行的代码
try { // 异常检测
eval(js); // 执行 eval函数, 该函数执行传入的参数(代码)。
} catch(e) { // 抛出异常
Log("Exception", e); // 输出错误信息
}
}
当然还有很多代码工具尽在 : https://www.botvs.com/square
#### 先写到这,欢迎读者给我留言!提出建议和意见,如果感觉好玩可以分享给更多热爱程序热爱交易的朋友 https://www.botvs.com/m/dashboard。
### 程序员 littleDream 原创