概要
本文是JS的合并tick数据位日线Bar的实现。同时提供笔者自己整理过的几个农产品主力合约从2020年到2024年的tick数据资源,更多品种tick数据资源可以通过笔者上传资源进行下载,后续笔者会不断补充上传,可关注本号方便收到最新更新上传的其他分享。
背景
我们从网上下载的Tick数据总会担心是否可靠,其中最简单常用的办法就是合并成K线图进行验证。通常的交易软件对于历史合约不会显示太久远的分钟线(毕竟存储成本太高了),但是多数软件都支持历史合约的日线显示。比如文华就可以显示日线,例如下图为文华中09合约的2022年的交易日线图。
测试数据
测试用数据是已经整理好导入MySQL5.7版本数据库豆粕期货1月、5月、9月三个主力合约从2020年到2024年的tick数据,数据可以结合本文描述的测试程序和文华历史数据对比进行验证。
百度网盘连接(测试目的,请勿商用):
链接:https://pan.baidu.com/s/1JG1eXB3MBJ9c8TTxHF1vZQ
提取码:ezme
验证程序
考虑到Python的mpl_finance如果是数据比对的目的从显示和选择等各个方面都比较麻烦,笔者直接使用Echart来作为tick数据验证。后端使用一段简单的express直接访问数据库,做CRUD的透传:
const express = require('express');
const mysql = require('mysql');
const app = express();
async function runQuery(dbname, query) {
return new Promise((resolve) => {
//主要为了示意目的.
// 对于不同的品种采用了分库分表, 为了测试方便没有使用预创建连接池的方式,
// 而是简单粗暴的使用了每次请求都创建连接,
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '123456',
database: dbname
});
console.log(query)
connection.connect((err) => {
if (err) {
resolve(500);
// return res.status(500).send('Database connection failed');
}
connection.query(query, (error, results) => {
connection.end(); // 执行完查询后关闭连接
if (error) {
resolve(500);
}
resolve(results);
});
});
});
}
app.get('/getDatas', async (req, res) => {
//---------------------------------------------------
// 从请求的查询参数中获取值
const {db, tbl, exchg, code, start, end} = req.query
//转换起止日期
const {startDate, endDate} = convertStartEnd(start, end);
// 构建SQL查询语句
let query = `SELECT * FROM `;
if (tbl === 'bar15m') {
query += tbl + ` WHERE datetime BETWEEN '${startDate}' AND '${endDate}' AND symbol='${symbol}' `;
} else {
query += tbl + ` WHERE datetime BETWEEN '${startDate}' AND '${endDate}' `;
}
query += ' ORDER BY `datetime`;';
const resp = await runQuery(db, query);
return res.json({data: resp});
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
注意, 数据使用了每个品种分库, 每个合约分表的方式. 数据验证程序后端中直接在请求中建立连接。这只是为了比较简单的去实现和演示功能。如果为了测试方便可以考虑自行提前建立连接的方式。
同时前端使用最简单输入框进行查询参数选择:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户端测试</title>
<!-- 引入 ECharts -->
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.3.2/echarts.min.js"></script>
</head>
<body>
<!-- 添加输入框 -->
<div>
<label for="y-extends">K线图Y轴扩展系数 (0~1):</label>
<input type="number" id="y-extends" min="0" max="1" step="0.01" value="0.05">
<br> <br>
<!-- 略 -->
</div>
<!-- 用于显示图表的div元素 -->
<div id="mainChart" style="width: 600px;height:400px;"></div>
<script type="module">
import {fetchAndShowDatas, setChart} from './functions.js';
document.getElementById('fetchDataBtn').addEventListener('click', function () {
fetchAndShowDatas().then(
// 略....
});
</script>
</body>
</html>
functions.js文件中, 接口fetchAndShowDatas()主要操作包括:
- 读取几个input的数值
- 组成请求字符串, 下载Tick数据
- 合并K线
- Echart显示
对于比较简单的内容不再赘述,仅分享一下合并K线的逻辑:
function mergeDayBars(tickDatas) {
// UTC时间转为北京时间(UTC+8),并获取日期和小时
const processTick = (tick) => {
let ts = new Date(tick.datetime).toLocaleString();
const [dateStr, timeStr] = ts.split(' ');
const [hour] = timeStr.split(':');
return {dateStr, hour: parseInt(hour)};
};
const makeCurrentBar = (barData, tick, dateStr) => {
if (!barData) {
return {
date: dateStr,
open: tick.price,
high: tick.price,
low: tick.price,
close: tick.price,
vol: tick.vol
}
} else {
barData.date = dateStr;
barData.close = tick.price;
barData.high = Math.max(barData.high, tick.price);
barData.low = Math.min(barData.low, tick.price);
barData.vol = tick.vol;
return barData;
}
}
const dayBars = [];
let lastTick = {date: null, hour: null};
let currentBar = {open: null, high: null, low: null, close: null, vol: 0};
const closeForOneDay = () => {
console.log(`Day bar ends: `, currentBar);
dayBars.push(currentBar);
currentBar = {open: null, high: null, low: null, close: null, vol: 0};
};
console.log('first tick:', tickDatas[0]);
console.log('last tick:', tickDatas[tickDatas.length-1]);
// 主函数逻辑
tickDatas.forEach((tick) => {
const {dateStr, hour} = processTick(tick);
if (!tick.price || tick.amount === 0) {
return;
}
if (hour < 20 && hour > 16) { //丢弃非交易时间的数据
return;
}
//----------------------------------------------
//核心逻辑
//第一个Tick, 无需特殊处理直接存入当前bar中
if (!lastTick.date || !lastTick.hour) {
currentBar = makeCurrentBar(null, tick, dateStr);
lastTick.date = dateStr;
lastTick.hour = hour;
return;
}
const curDayTrade = hour < 19 && hour > 7;
const lastDayTrade = lastTick.hour < 19 && lastTick.hour > 7;
if (lastTick.date === dateStr && curDayTrade === lastDayTrade) {
//同一日, 直接存储即可
currentBar = makeCurrentBar(currentBar, tick, dateStr);
} else if (curDayTrade) {
if (lastDayTrade) {
//不是同一日, 上一个Tick是日盘, 说明当前bar没有夜盘, 存储bar, 并且生成新的bar
closeForOneDay();
console.log(`Day bar ends: next tick ${tick}, last tick=${lastTick.date}:${lastTick.hour}`);
currentBar = makeCurrentBar(null, tick, dateStr);
} else {
//不是同一日, 上一个Tick是夜盘, 直接使用当前bar日期去更新正在处理的bar, 同时正常计算
currentBar = makeCurrentBar(currentBar, tick, dateStr);
}
} else {
//如果进入夜盘, 且日期不是同一天, 那么无论如何都应该保存旧的bar, 并且生成新的bar
closeForOneDay();
console.log(`Day bar ends: next tick ${tick}, last tick=${lastTick.date}:${lastTick.hour}`);
currentBar = makeCurrentBar(null, tick, dateStr);
}
//更新上一个Tick信息
lastTick.date = dateStr;
lastTick.hour = hour;
});
return dayBars;
}
代码核心核心方法是遍历tick的时候,循环中每次迭代生成一个currentBar,然后根据日夜盘的转换去决定是否将currentBar写入最后结果。使用lastTick记录了上一个tick的日期和时间。算法考虑了日盘和夜盘未开盘的情况。
源码资源
下面连接是本文中测试目的的数据验证代码的资源连接:
前文中网盘链接里面的测试数据下载后导入MySQL5.7以后版本结合此代码直接可验证tick数据。