基于MT5的K线处理逻辑
一、数据存储结构
ConcurrentDictionary<string, LockSortedDictionary<DateTime, PriceInfo>>
该结构主要用来存储如下k线周期数据:
- M1 (1分钟), M5, M15, M30
- H1 (1小时), H2, H4
- D1 (日线), W1 (周线), MN1 (月线)
比如:
lstM1 = {// 第一层:品种维度["EURUSD"] => {// 第二层:时间维度(自动排序)[2025-10-24 09:00:00] => PriceInfo {OpenPrice: 1.0850,HighPrice: 1.0855,LowPrice: 1.0848,ClosePrice: 1.0852,Volume: 125000,PriceTime: 2025-10-24 09:00:00},[2025-10-24 09:01:00] => PriceInfo {OpenPrice: 1.0852,HighPrice: 1.0858,LowPrice: 1.0850,ClosePrice: 1.0856,Volume: 98000,PriceTime: 2025-10-24 09:01:00},[2025-10-24 09:02:00] => PriceInfo { ... },...},["GBPUSD"] => {[2025-10-24 09:00:00] => PriceInfo {OpenPrice: 1.2650,HighPrice: 1.2658,LowPrice: 1.2648,ClosePrice: 1.2655,Volume: 156000,PriceTime: 2025-10-24 09:00:00},[2025-10-24 09:01:00] => PriceInfo { ... },...}
}
lstM5 = {["EURUSD"] => {// 注意:5分钟K线的时间是5的倍数[2025-10-24 09:00:00] => PriceInfo {OpenPrice: 1.0850, // 来自09:00的M1HighPrice: 1.0860, // 09:00-09:04这5根M1的最高价LowPrice: 1.0845, // 09:00-09:04这5根M1的最低价ClosePrice: 1.0858, // 来自09:04的M1Volume: 550000, // 5根M1的Volume累加PriceTime: 2025-10-24 09:00:00},[2025-10-24 09:05:00] => PriceInfo {OpenPrice: 1.0858,HighPrice: 1.0865,LowPrice: 1.0855,ClosePrice: 1.0863,Volume: 480000,PriceTime: 2025-10-24 09:05:00},...}
}
二、 历史数据加载:RefreshChartData
1、根据配置文件获取需要品种加载行情的开始时间和加载月份数
<add key="ChartStartTime" value="2025-08-12 00:00" /> // 从2025-08-12 00:00开始
<add key="ChartFetchMonthEachTime" value="1" /> // 加载一个月的数据
DateTime endSynTime = DateTime.Now.Add(-serverTimeSpan);
auto startTime = symbolFefreshTimes[symbolName];
DateTime endTime = startTime.AddMonths(fetchMonthEachTime) > endSynTime ? endSynTime : startTime.AddMonths(fetchMonthEachTime);
// 如果加载月份超过当前时间,则以当前时间为结束时间
RequestChartData(symbolName, startTime, endTime);
最后调用MT5的ChartRequest接口
2、数据存储在内存中,最多保留6000条 (AddPriceInfo)
由于从MT5获取的是1分钟k线数据,所以转化成5分钟k线、小时k线、日k、周k还涉及到k线数据合并与归档逻辑
处理过程分为 三个阶段:
(1)按周期对时间戳对齐 (ConvertPeriodTime)
获取对应周期的历史数据表
auto symbolPriceList = GetSymbolPriceList(symbolName, period);
GetSymbolPriceList 会:
-
按周期选择对应的全局字典(lstM1, lstH1, lstD1 等)
-
从该字典中取出某个 symbol 对应的价格表(类型为 LockSortedDictionary<DateTime, PriceInfo>)
-
如果不存在则创建一个新的空表
每个 symbol、每个周期都有独立的时间序列表,例如:
lstH1[“EURUSD”] => LockSortedDictionary<时间, PriceInfo>
把任意时刻的数据对齐到周期的开始时间
DateTime keyTime = ConvertPeriodTime(priceKV.Key, period);
| 原始时间 | 周期 | 对齐后时间 |
|---|---|---|
| 2025-10-24 14:07 | M5 | 2025-10-24 14:05 |
| 2025-10-24 14:59 | H1 | 2025-10-24 14:00 |
| 2025-10-24 14:00 | D1 | 2025-10-24 00:00 |
| 2025-10-24 14:00 | W1 | 2025-10-19 (周日) |
| 2025-10-24 14:00 | MN1 | 2025-10-01 |
保证数据被合并进同一K线的时间桶中。
实现逻辑:
- 如果是分钟线:1分钟/5分钟/15分钟/30分钟
计算公式:
当前时间 - (当前时间的分钟数%k线周期)
time.AddMinutes(-time.Minute % (int)period);
比如5分钟:
| 当前时间 | time.Minute | 偏移量 (minute % 5) | 结果时间(周期起点) |
|---|---|---|---|
| 10:03 | 3 | 3 | 10:00 |
| 10:07 | 7 | 2 | 10:05 |
| 10:19 | 19 | 4 | 10:15 |
| 10:30 | 30 | 0 | 10:30 |
- 如果是小时线:
计算公式:
当前时间 - (当前时间的分钟数 + 当前时间的小时数 * 60 % k线周期)
time.AddMinutes(-(time.Minute + time.Hour * 60 % (int)period));
解释:把“当前小时 + 当前分钟”都换算成“从当天 00:00 起的总分钟数”。
举例(H4 = 240分钟):
| 时间 | 从0点起的分钟数 | 偏移量(%240) | 当前属于哪个周期 |
|---|---|---|---|
| 01:15 | 75 | 75 | 第1段 (00:00–03:59) |
| 03:59 | 239 | 239 | 仍在第1段 |
| 04:00 | 240 | 0 | 第2段开始 (04:00–07:59) |
| 06:10 | 370 | 130 | 第2段 (04:00–07:59) |
| 08:01 | 481 | 1 | 第3段 (08:00–11:59) |
- 如果是周线:
计算公式:
当前时间 - 当前是周几 (周日为0,周一为1…) 即对齐到周日
time.Date.AddDays(-(int)time.DayOfWeek);
如果是月线:
计算公式:
当前时间 - 当前是本月的第几天+1,即对齐到本月第一天
time.Date.AddDays(-time.Day + 1);
| 周期类型 | 逻辑 | 示例输入 | 输出(对齐后的起始时间) |
|---|---|---|---|
| W1(周线) | 回退到本周星期日零点 | 2025-10-27(周一) | 2025-10-26(周日) |
| MN1(月线) | 回退到本月第一天零点 | 2025-10-27 | 2025-10-01 |
(2)合并新旧K线数据(更新OHLCV)
对每个 priceKV(时间+PriceInfo):
✅ 如果当前时间桶已有K线:
if (symbolPriceList.TryGetValue(keyTime, out PriceInfo priceInfo))
{priceInfo.ClosePrice = priceKV.Value.ClosePrice;if (priceInfo.LowPrice > priceKV.Value.LowPrice){priceInfo.LowPrice = priceKV.Value.LowPrice;}else if (priceInfo.HighPrice < priceKV.Value.HighPrice){priceInfo.HighPrice = priceKV.Value.HighPrice;}priceInfo.Volume += priceKV.Value.Volume == 0 ? CreateRandomNumber() : priceKV.Value.Volume;
}
则执行 更新逻辑:
- ClosePrice 始终更新为最新
- LowPrice 更新为较低者
- HighPrice 更新为较高者
- Volume 累加,若为0则生成随机数(CreateRandomNumber())
- 不改变Open
❌ 如果不存在该时间桶:
else
{auto addPrice = new PriceInfo(){ClosePrice = priceKV.Value.ClosePrice,OpenPrice = priceKV.Value.OpenPrice,HighPrice = priceKV.Value.HighPrice,LowPrice = priceKV.Value.LowPrice,PriceTime = keyTime,Volume = priceKV.Value.Volume == 0 ? CreateRandomNumber() : priceKV.Value.Volume};symbolPriceList.Add(keyTime, addPrice);
}
就新建一根K线(通常代表新的周期开始)。
(3)清理过期缓存数据(数量控制)
if (symbolPriceList.Count > maxCachedPriceInfoCount + 30)
- 超过缓存阈值则清理多余K线(FIFO原则)
- Take(symbolPriceList.Count - maxCachedPriceInfoCount) 取出最旧的部分删除
这样可避免内存无限增长。
3、支持持久化到文件(ChartLoadModel模式)
需要导出行情的时候使用
if (isChartLoadModel)
{StoreChart(symbolName);base.ClearChart(symbolName);
}
功能:
- 将指定品种(symbolName)的所有周期的K线缓存,打包保存成一个文件。
存储结构:
- 每个品种对应一个独立文件;
- 文件内容是多个周期(M1、M5、H1、D1…)的K线数据集合;
- 每个周期包含 PriceInfo 列表。

三、实时报价处理:AddRealtimeQuote
1、从MT5的行情回调函数OnTick接收到行情数据
public override void OnTick(string symbol, MTTickShort tick)
{
....Quote quote = tick.ToQuote(symbol, manager);
.....
}
2、对价格进行检查(10%、900%波动警告)
3、将TICK数据聚合成1分钟K线
4、更新当前分钟的OHLC(开高低收)
5、通过定时任务PushQuoteToChart 每10秒将实时数据推送到主K线存储
四、k线数据查询:GetChartList
1、LatestCount > 0 的情况下,如果EndTime == default(DateTime), 查询最近LatestCount条记录;否则查询EndTime之前的最新LatestCount条记录。
2、LatestCount <= 0的情况下,查询StartTime,EndTime范围内的记录。
3、每次返回记录不超过1500条。
// 参数校验逻辑
if (!symbolNameList.Contains(request.Symbol))
{log.ErrorFormat("RequestChart Wrong symbol, rquest: {0}", JsonConvert.SerializeObject(request));return null;
}
if (!Enum.IsDefined(typeof(ChartPeriods), request.Period))
{log.ErrorFormat("RequestChart Wrong period, rquest: {0}", JsonConvert.SerializeObject(request));return null;
}
if (request.LatestCount > MAX_RECORD_SIZE)
{log.ErrorFormat("RequestChart Wrong LatestCount, rquest: {0}", JsonConvert.SerializeObject(request));return null;
}
if (request.LatestCount == 0 &&(request.StartTime == default(DateTime) || request.EndTime == default(DateTime) || // 时间之一为空request.StartTime > request.EndTime // 开始时间大于截止时间))
{log.ErrorFormat("RequestChart Wrong, rquest: {0}", JsonConvert.SerializeObject(request));request.LatestCount = MAX_RECORD_SIZE;
}
五、K线数据修复:AddReplaceChart
1、清除指定时间范围的K线数据
ClearChart(request.Symbol, request.StartTime, request.EndTime);
2、自动调整为按月批量重新拉取
while (lastDataTime < request.EndTime)
{lastDataTime = request.StartTime.AddMonths(fetchMonthEachTime) > request.EndTime ? request.EndTime : request.StartTime.AddMonths(fetchMonthEachTime);auto chartData = RequestChartData(request.Symbol, request.StartTime, request.EndTime);...
}
3、从MT5重新拉取该时间段数据
manager.ChartRequest(symbol, from, to, out code);
4、转换为有序字典并去重(价格时间为 key)
5、修复后触发数据完整性检查
六、K线完整性检查:CheckChart
1、遍历所有K线周期(除W1和MN1)
2、根据品种的交易时段检查缺失的K线
3、记录缺失时间段
{List<DateTime> missChart = new List<DateTime>();auto priceTimeList = chartService.GetAllPriceTimeList(symbol.Name, period).Where(x => x > chartCheckStartTime); if (priceTimeList.Count() == 0){return missChart;}DateTime startTime = priceTimeList.First();DateTime endTime = priceTimeList.Last();DateTime checkTime = GetNextPeriodTime(startTime, period);while (checkTime < endTime){bool isQuoteTime = true;if (!priceTimeList.Contains(checkTime)){isQuoteTime = IsQuoteTime(checkTime, symbol, period);if (isQuoteTime){missChart.Add(checkTime);}}checkTime = isQuoteTime ? GetNextPeriodTime(checkTime, period) : GetNextQuoteTimeForPeriod(checkTime, symbol, period);}return missChart;
}
4、提供导出 ExportMissChart和查询接口GetMissSymbolList
5、每天自动检查一次
七、定时任务
1、存储线程: 每60分钟持久化K线到文件 (StoreChart)
2、推送线程: 每10秒将实时报价聚合到K线存储 (PushQuoteToChart)
3、修复线程: 每10秒处理K线修复请求 (ReplaceChart)
