时间序列挖掘的数据标签(第2部分):使用Python制作带有趋势标记的数据集·进阶篇
📘

时间序列挖掘的数据标签(第2部分):使用Python制作带有趋势标记的数据集·进阶篇

第 2/3 篇

「把 MT5 拉出的黄金数组改成 pandas 时序」

从 MT5 终端用 copy_rates_from_pos 抓 GOLD_micro 的 M15 数据,返回的是 numpy.ndarray,结构形如 (时间戳, 开, 高, 低, 收, 成交量, spread, 保留位)。原始样例里首行时间是 1692368100(对应 2023-08-18 20:15 UTC),收盘 1893.88,最后一行 1693825200 收盘 1943.85,跨度约 170 万秒、近 15 天。 这类 ndarray 不能直接喂给 pytrendseries 这类趋势库,后者要求输入是 pandas.DataFrame,且至少有一列 float/int 的观测值、时间作索引。第一步用 pd.DataFrame(rts) 把数组包成表,就能 head(10) 看到前 10 行原始字段。 但 time 列还是 int 秒戳,需转成可读时间:rts_fm['time']=pd.to_datetime(rts_fm['time'], unit='s')。转换后取 [['time','close']].set_index('time'),输出变成 2023-08-18 20:45:00 1888.82 这种格式。注意 td_data 只是算趋势的过渡产物,不是最终落库样式。 外汇与贵金属杠杆高、跳空频繁,GOLD_micro 虽点值小仍可能短时间内反向数十点,转完数据先本地核对时间轴是否连续,再进下一步分析。

MQL5 / C++
# Copyright class="num">2021, MetaQuotes Ltd.
# [MQL5官方文档]
class="kw">import MetaTrader5 as mt
class="kw">import pandas as pd
if not mt.initialize("D:\\Project\\mt\\MT5\\terminal64.exe"):
    print(&class="macro">#x27;initialize() failed!&class="macro">#x27;)
else:
    print(mt.version())
    sb=mt.symbols_total()
    rts=None
    if sb > class="num">0:
        rts=mt.copy_rates_from_pos("GOLD_micro",mt.TIMEFRAME_M15,class="num">0,class="num">1000) 
    mt.shutdown()
    rts_fm=pd.DataFrame(rts)
    rts_fm[&class="macro">#x27;time&class="macro">#x27;]=pd.to_datetime(rts_fm[&class="macro">#x27;time&class="macro">#x27;], unit=&class="macro">#x27;s&class="macro">#x27;)
    td_data=rts_fm[[&class="macro">#x27;time&class="macro">#x27;,&class="macro">#x27;close&class="macro">#x27;]].set_index(&class="macro">#x27;time&class="macro">#x27;)
    print(td_data.head(class="num">10))

◍ 把趋势段打上可训练标签

用 pytrendseries 拉出趋势后,原始输出只是「从哪到哪、涨跌幅多少」的段落表。以 2023-08-21 黄金 1 小时数据为例,detecttrend 在 window=120、limit=6 下识别出 15 段下跌,最大 drawdown 出现在第 8 段(2023-08-22 13:00 至 16:45),回撤 0.00694;同向 uptrend 里第 13 段(2023-08-29 15:30 至 30 17:00)drawup 达 0.01713,跨度 98 根 K。这些数字直接能贴回 MT5 的 K 线时间去核对。 光有段落不够,做监督学习得给每根 bar 标趋势归属。逻辑拆成四块:数据开头到首段下跌前算默认上升;中间下跌段标 ②;下跌间的反弹标 ③;末段下跌结束标 ④。遍历 trends 拿每段的 index_from / index_to,对非首尾段显式写 trend 列,首尾若非下跌则假定上升。 校验时抽前 25 行看 trend 与 trend_index 两列:2023-08-22 13:00 起 trend 由 0 翻 1,到 17:00 又回 0,和前面 detecttrend 的第 8 段下跌区间(index 153–168)完全对得上。若你觉得「头尾强制上升」太粗,for 循环后删掉首尾段即可。 标完直接落盘。演示用 to_csv 就够,要接网页端可用 to_html,接 API 用 to_json。下面这段是可复跑的最小代码,limit=6 指趋势最少跨 6 根 K,wd=120 是回看窗口上限。

MQL5 / C++
class="kw">import pytrendseries as pts
td=&class="macro">#x27;downtrend&class="macro">#x27; # or "uptrend"
wd=class="num">120
limit=class="num">6
trends=pts.detecttrend(td_data,trend=td,limit=limit,window=wd)
print(trends.head(class="num">15))
pts.vizplot.plot_trend(td_data,trends)
td="uptrend"
wd=class="num">120
limit=class="num">6
trends=pts.detecttrend(td_data,trend=td,limit=limit,window=wd)
print(trends.head(class="num">15))
pts.vizplot.plot_trend(td_data,trends)

趋势区段索引的边界回填逻辑

把分段趋势映射到逐根 K 线的索引序列时,头尾两段最容易漏填。上面这段处理思路是:先给 rts_fm['trend'] 和 rts_fm['trend_index'] 置 0,再拿 trends 表的 index_from / index_to 去切分回填。 当 trend 序号为 1 且起点不在 0(start!=0),说明数据开头是一段未标记的震荡或反向段,直接把 [0:start] 填成 range(0,start)。若最后一段(trend[0]==max_len)终点没覆盖到 rts_fm 末尾(end!=max_len_rts-1),则从 last_end+1 填到表尾,长度 = max_len_rts-last_end-1。 中间段走 else 分支:把上一段尾 last_end+1 到本段起点 start 之间的空隙填成 range(0, start-last_end-1)。这样整张表除了明确趋势段外,边界空隙都有了连续索引,后续做价格行为统计不会断档。 开 MT5 用 Python 脚本来回测时,先 print(max_len_rts) 和 len(trends),确认末尾空隙长度不为负;若 end 算错成越界,range 会直接抛 ValueError。

MQL5 / C++
rts_fm[&class="macro">#x27;trend&class="macro">#x27;]=class="num">0
rts_fm[&class="macro">#x27;trend_index&class="macro">#x27;]=class="num">0
max_len_rts=len(rts_fm)
max_len=len(trends)
last_start=class="num">0
last_end=class="num">0
for trend in trends.iterrows():
    pass
for trend in trends.iterrows():
    start=trend[class="num">1][&class="macro">#x27;index_from&class="macro">#x27;]
    end=trend[class="num">1][&class="macro">#x27;index_to&class="macro">#x27;]
for trend in trends.iterrows():
    start=trend[class="num">1][&class="macro">#x27;index_from&class="macro">#x27;]
    end=trend[class="num">1][&class="macro">#x27;index_to&class="macro">#x27;]
    if trend[class="num">0]==class="num">1 and start!=class="num">0:
        # Since the rts_fm["trend"] itself has been initialized to class="num">0, there is no need to change the "trend" column
        rts_fm[&class="macro">#x27;trend_index&class="macro">#x27;][class="num">0:start]=list(range(class="num">0,start))
for trend in trends.iterrows():
    start=trend[class="num">1][&class="macro">#x27;index_from&class="macro">#x27;]
    end=trend[class="num">1][&class="macro">#x27;index_to&class="macro">#x27;]
    if trend[class="num">0]==class="num">1 and start!=class="num">0:
        # Since the rts_fm["trend"] itself has been initialized to class="num">0, there is no need to change the "trend" column
        rts_fm[&class="macro">#x27;trend_index&class="macro">#x27;][class="num">0:start]=list(range(class="num">0,start))
    elif trend[class="num">0]==max_len and end!=max_len_rts-class="num">1:
        class="macro">#we need to see if it ends in a downtrend at the end of the data
        rts_fm[&class="macro">#x27;trend_index&class="macro">#x27;][last_end+class="num">1:len(rts_fm)]=list(range(class="num">0,max_len_rts-last_end-class="num">1))
for trend in trends.iterrows():
    start=trend[class="num">1][&class="macro">#x27;index_from&class="macro">#x27;]
    end=trend[class="num">1][&class="macro">#x27;index_to&class="macro">#x27;]
    if trend[class="num">0]==class="num">1 and start!=class="num">0:
        # Since the rts_fm["trend"] itself has been initialized to class="num">0, there is no need to change the "trend" column
        rts_fm[&class="macro">#x27;trend_index&class="macro">#x27;][class="num">0:start]=list(range(class="num">0,start))
    elif trend[class="num">0]==max_len and end!=max_len_rts-class="num">1:
        class="macro">#we need to see if it ends in a downtrend at the end of the data
        rts_fm[&class="macro">#x27;trend_index&class="macro">#x27;][last_end+class="num">1:len(rts_fm)]=list(range(class="num">0,max_len_rts-last_end-class="num">1))
    else:
        class="macro">#Process the uptrend segments other than the beginning and end of the data
        rts_fm["trend_index"][last_end+class="num">1:start]=list(range(class="num">0,start-last_end-class="num">1))
for trend in trends.iterrows():
    start=trend[class="num">1][&class="macro">#x27;index_from&class="macro">#x27;]
    end=trend[class="num">1][&class="macro">#x27;index_to&class="macro">#x27;]
    if trend[class="num">0]==class="num">1 and start!=class="num">0:
        # Since the rts_fm["trend"] itself has been initialized to class="num">0, there is no need to change the "trend" column
        rts_fm[&class="macro">#x27;trend_index&class="macro">#x27;][class="num">0:start]=list(range(class="num">0,start))
    elif trend[class="num">0]==max_len and end!=max_len_rts-class="num">1:
        class="macro">#we need to see if it ends in a downtrend at the end of the data
        rts_fm[&class="macro">#x27;trend_index&class="macro">#x27;][last_end+class="num">1:len(rts_fm)]=list(range(class="num">0,max_len_rts-last_end-class="num">1))
    else:

「把趋势段切成可回放的索引序列」

这段逻辑干的事,是把已识别出的上升/下降段重新映射到一张逐根 K 线的帧表 rts_fm 上,给每段标 trend(0 为上升、1 为下降)和段内 trend_index。 初始化时整张表 trend 与 trend_index 全置 0,max_len_rts 取 rts_fm 行数、max_len 取 trends 段数,last_start/last_end 从 0 起步,准备在循环里滚动记录上一段边界。 遍历 trends 每一行:start、end 来自该段的 index_from / index_to。首段若是上升且 start 不为 0,就把 [0:start] 的 trend_index 顺填 range(0,start);末段若是下降且 end 没到表尾,则把 last_end+1 到表尾填成连续段内序号。 其余情况走 else:先填 last_end+1:start 的上升过渡区序号,再把 start:end+1 整段标 trend=1 并填段内序号,随后更新 last_start、last_end。循环结束后用 iloc 按首段起点裁掉表头冗余,head(25) 可肉眼抽查,to_csv 落地为 GOLD_micro_M15.csv 供 MT5 外复盘。 外汇与贵金属属高杠杆品种,这类帧表只描述历史分段,不预示后续方向,实盘前请在 M15 上用真实 tick 复核切片边界。

MQL5 / C++
rts_fm[&class="macro">#x27;trend&class="macro">#x27;]=class="num">0
rts_fm[&class="macro">#x27;trend_index&class="macro">#x27;]=class="num">0
max_len_rts=len(rts_fm)
max_len=len(trends)
last_start=class="num">0
last_end=class="num">0
for trend in trends.iterrows():
    start=trend[class="num">1][&class="macro">#x27;index_from&class="macro">#x27;]
    end=trend[class="num">1][&class="macro">#x27;index_to&class="macro">#x27;]
    if trend[class="num">0]==class="num">1 and start!=class="num">0:
        # Since the rts_fm["trend"] itself has been initialized to class="num">0, there is no need to change the "trend" column
        rts_fm[&class="macro">#x27;trend_index&class="macro">#x27;][class="num">0:start]=list(range(class="num">0,start))
    elif trend[class="num">0]==max_len and end!=max_len_rts-class="num">1:
        class="macro">#we need to see if it ends in a downtrend at the end of the data
        rts_fm[&class="macro">#x27;trend_index&class="macro">#x27;][last_end+class="num">1:len(rts_fm)]=list(range(class="num">0,max_len_rts-last_end-class="num">1))
    else:
        class="macro">#Process the uptrend segments other than the beginning and end of the data
        rts_fm["trend_index"][last_end+class="num">1:start]=list(range(class="num">0,start-last_end-class="num">1))

    class="macro">#Process each segments of the downtrend
    rts_fm["trend"][start:end+class="num">1]=class="num">1
    rts_fm["trend_index"][start:end+class="num">1]=list(range(class="num">0,end-start+class="num">1))
    last_start=start
    last_end=end
rts_fm=rts_fm.iloc[trends.iloc[class="num">0,:][&class="macro">#x27;index_from&class="macro">#x27;]:end,:]
rts_fm.head(class="num">25)
rts_fm.to_csv(&class="macro">#x27;GOLD_micro_M15.csv&class="macro">#x27;)

常见问题

用 pandas 的 DatetimeIndex 把时间戳列设为索引,再将价格列转为 float 类型,即可形成标准时序对象用于后续处理。
可设定阈值如区间回撤小于 2% 且高低点逐级上移记为上升趋势,否则标为震荡,具体参数按品种波动调。
可以,小布能读取你的品种数据并按规则自动标注趋势段、生成可回放索引,省去手写 Python 的重复劳动。
在分段函数里对首尾索引做前向和后向填充各 N 根,确保标签覆盖完整波段,避免训练时边界样本缺失。
它能让你按段重放行情测策略,可用 Python 循环读取每段索引在图上复盘,确认标签与肉眼趋势一致。