格序列相似性分析:DTW與形態(tài)匹配實(shí)踐)
簡(jiǎn)介基于Python的股票價(jià)格序列相似性分析課程設(shè)計(jì)資源包面向金融數(shù)據(jù)分析、Python編程及算法課程設(shè)計(jì)人群圍繞動(dòng)態(tài)時(shí)間彎曲DTW算法實(shí)現(xiàn)股票價(jià)格序列的相似性度量并通過(guò)折線圖直觀呈現(xiàn)對(duì)比結(jié)果解決同類(lèi)課程中序列對(duì)齊與形態(tài)比較的典型問(wèn)題。壓縮包共12個(gè)文件核心為2個(gè)Python源碼Main.py、source.py與1份Word版課程設(shè)計(jì)報(bào)告另含requirements.txt依賴(lài)說(shuō)明、database.db數(shù)據(jù)文件及7張運(yùn)行效果截圖整體大小僅2.13MB結(jié)構(gòu)緊湊便于對(duì)照學(xué)習(xí)。平臺(tái)顯示已有613人學(xué)習(xí)下載認(rèn)可度良好。資源提供了從數(shù)據(jù)讀取、數(shù)據(jù)預(yù)處理到DTW相似性計(jì)算、結(jié)果可視化的完整代碼流程配合報(bào)告中的算法原理講解與截圖展示可幫助讀者快速理解序列相似性分析的實(shí)現(xiàn)思路并直接用于相關(guān)課程設(shè)計(jì)、期末項(xiàng)目或作為后續(xù)研究參考。1. 用 Python 做股票價(jià)格序列相似性分析這個(gè) zip 解決的問(wèn)題比想象中更具體拿到這個(gè) zip 的人多半正在找一種能回答這類(lèi)問(wèn)題的代碼當(dāng)前這 20 根、40 根 K 線歷史上有沒(méi)有出現(xiàn)過(guò)差不多的形態(tài)相似形態(tài)出現(xiàn)之后行情是繼續(xù)走、還是反轉(zhuǎn)“股票價(jià)格序列相似性分析”干的就是這件事——把當(dāng)前一段行情切片和歷史所有切片做匹配按相似度排序。它常見(jiàn)于形態(tài)復(fù)盤(pán)、歷史行情回放也是形態(tài)類(lèi)量化策略的信號(hào)來(lái)源。Python 這條技術(shù)路徑的好處是生態(tài)齊全從數(shù)據(jù)獲取到相似度計(jì)算再到可視化都能在幾十行代碼里跑通。先說(shuō)一個(gè)反直覺(jué)的結(jié)論直接算歐氏距離判斷兩條 K 線像不像在真實(shí)行情里會(huì)頻繁翻車(chē)原因是兩個(gè)序列往往存在相位偏移相似形態(tài)不一定對(duì)齊在相同的時(shí)間點(diǎn)上真正靠譜的是 DTW 這類(lèi)允許時(shí)間軸彎曲的度量方式。2. 從數(shù)據(jù)到序列價(jià)格數(shù)據(jù)獲取與預(yù)處理的兩個(gè)關(guān)鍵選擇2.1 獲取前復(fù)權(quán)行情免費(fèi)數(shù)據(jù)源與復(fù)權(quán)參數(shù)怎么配相似性分析的第一步是拿到干凈、連續(xù)、可對(duì)齊的價(jià)格序列。常見(jiàn)做法是用 akshare 做快速驗(yàn)證免費(fèi)、不需要注冊(cè) token接口返回的就是 pandas DataFrame直接能用如果團(tuán)隊(duì)內(nèi)部已經(jīng)有 tushare pro 的 token也可以用ts.pro_bar拿同樣的數(shù)據(jù)。下面以 akshare 為例取平安銀行 2020 年到 2024 年的日線import akshare as ak # symbol 傳股票代碼period 固定為 dailyadjustqfq 表示前復(fù)權(quán) df ak.stock_zh_a_hist( symbol000001, perioddaily, start_date20200101, end_date20241231, adjustqfq ) # 只需要日期和收盤(pán)價(jià)按時(shí)間升序排好并重建索引 df df.rename(columns{日期: trade_date, 收盤(pán): close}) df df[[trade_date, close]].sort_values(trade_date).reset_index(dropTrue) print(df.head())adjustqfq是這里最關(guān)鍵的參數(shù)。除權(quán)除息日當(dāng)天股價(jià)會(huì)向下跳空不復(fù)權(quán)數(shù)據(jù)里這個(gè)跳空會(huì)被相似性算法識(shí)別成一次劇烈的“下跌”導(dǎo)致所有跨越除權(quán)日的切片距離被拉高匹配結(jié)果里出現(xiàn)一批和形態(tài)毫無(wú)關(guān)系的假相似。前復(fù)權(quán)會(huì)按最新股本把歷史價(jià)格整體調(diào)整保持價(jià)格走勢(shì)連續(xù)是形態(tài)類(lèi)分析用的默認(rèn)設(shè)置。注意它的副作用復(fù)權(quán)時(shí)間久了早期價(jià)格可能被調(diào)整為負(fù)數(shù)做收益率計(jì)算時(shí)要留個(gè)心眼十年以上的長(zhǎng)歷史建議改用adjusthfq后復(fù)權(quán)。另外兩個(gè)容易忽略的細(xì)節(jié)一是停牌日可能沒(méi)有行情記錄讀取后要檢查 NaN用ffill填充或直接剔除二是在 Windows 上解壓這個(gè) zip 時(shí)如果解壓路徑帶中文pandas 讀 CSV 很容易撞上編碼錯(cuò)誤把路徑改成純英文能省掉很多排查時(shí)間。運(yùn)行代碼前確保環(huán)境里已經(jīng)裝好 numpy、pandas 和 akshare裝 numpy 用pip install numpy裝不上時(shí)先檢查 Python 是否在系統(tǒng) PATH 里。2.2 價(jià)格序列還是收益率序列兩種輸入各自的含義和坑拿到收盤(pán)價(jià)之后下一個(gè)選擇直接影響“相似”的定義。直接用價(jià)格序列算相似度不同股票、不同歷史時(shí)段的絕對(duì)價(jià)位差異會(huì)主導(dǎo)距離——茅臺(tái) 800 元時(shí)期的走勢(shì)和一只 8 元股票的走勢(shì)即使形態(tài)一模一樣價(jià)格尺度也差了 100 倍必須做標(biāo)準(zhǔn)化。標(biāo)準(zhǔn)化價(jià)格序列保留趨勢(shì)和幅度信息適合回答“哪段走勢(shì)長(zhǎng)得像當(dāng)前這段”。收益率序列則天然去趨勢(shì)適合回答“哪段走勢(shì)的拐點(diǎn)節(jié)奏和現(xiàn)在一致”。下面兩種預(yù)處理都寫(xiě)出來(lái)import pandas as pd import numpy as np def zscore(s: pd.Series) - pd.Series: # z-score 標(biāo)準(zhǔn)化去掉均值、除以標(biāo)準(zhǔn)差消除絕對(duì)價(jià)位影響 return (s - s.mean()) / s.std() # 方式一標(biāo)準(zhǔn)化價(jià)格序列保留趨勢(shì)形態(tài) price_z zscore(df[close]) # 方式二收益率序列天然去趨勢(shì)pct_change 首行是 NaN需要丟棄 ret df[close].pct_change().dropna()我一般會(huì)把兩種都算一遍分兩個(gè) pipeline 跑然后對(duì)比結(jié)果。原因是它們對(duì)同一段行情的敏感度完全不同標(biāo)準(zhǔn)化價(jià)格對(duì)趨勢(shì)敏感一段橫盤(pán)和一段穩(wěn)步上漲的形態(tài)會(huì)拉開(kāi)距離收益率序列對(duì)單日極端波動(dòng)敏感一個(gè)漲跌停就能把整段序列的重心帶偏但它的好處是跨股票可比。如果你做的是全市場(chǎng)選股想在不同股票之間找相似形態(tài)優(yōu)先用收益率序列如果你做的是單只股票的歷史復(fù)盤(pán)想找“歷史上我見(jiàn)過(guò)這種走法”標(biāo)準(zhǔn)化價(jià)格更直觀。預(yù)處理階段還有一個(gè)容易犯的錯(cuò)窗口內(nèi)的標(biāo)準(zhǔn)化必須在切片內(nèi)部做而不是對(duì)整個(gè)歷史序列統(tǒng)一做標(biāo)準(zhǔn)化。原因是相似性比較看的是切片內(nèi)的相對(duì)形態(tài)如果直接用全序列的均值和標(biāo)準(zhǔn)差去歸一化不同歷史時(shí)期的波動(dòng)率水平會(huì)干擾匹配早期的低波動(dòng)切片會(huì)被錯(cuò)誤放大。3. 相似性度量怎么選DTW 為什么在股票序列上優(yōu)于歐氏距離3.1 三種度量方法對(duì)比距離定義差異決定了匹配質(zhì)量把序列切成片段之后核心問(wèn)題變成怎么定義“像”。三種常用方法各自有不同的距離定義直接決定了匹配效果。歐氏距離要求兩個(gè)序列等長(zhǎng)且逐點(diǎn)對(duì)齊時(shí)間軸上一旦錯(cuò)位距離就急劇放大皮爾遜相關(guān)系數(shù)衡量線性相關(guān)對(duì)幅度不敏感但同樣需要對(duì)齊而且它只能捕捉同步的形態(tài)關(guān)系動(dòng)態(tài)時(shí)間規(guī)整DTW通過(guò)動(dòng)態(tài)規(guī)劃尋找兩個(gè)序列之間的最優(yōu)對(duì)齊路徑允許時(shí)間軸局部拉伸或壓縮天然適配股票數(shù)據(jù)的相位偏移問(wèn)題。三類(lèi)對(duì)比如下度量方法時(shí)間復(fù)雜度對(duì)相位偏移的容忍度適用場(chǎng)景歐氏距離O(n)不支持等長(zhǎng)且對(duì)齊良好的序列實(shí)際行情里很少見(jiàn)皮爾遜相關(guān)系數(shù)O(n)不支持判斷同步漲跌關(guān)系適合因子分析DTWO(n*m)支持歷史形態(tài)匹配允許時(shí)間軸扭曲股票序列里的“相似”本質(zhì)上是形態(tài)相似而不是數(shù)值恰好相同。當(dāng)前 40 日 K 線可能和歷史上某一個(gè) 60 日 K 線的中間段長(zhǎng)得一樣只是當(dāng)時(shí)走得慢、磨得久歐氏距離要求頭對(duì)頭、尾對(duì)尾錯(cuò)一天距離就被放大一倍。DTW 允許路徑往左、往右、往對(duì)角線方向走找到累計(jì)距離最小的對(duì)齊方式形態(tài)相似的兩段序列即使長(zhǎng)度不同、節(jié)奏不同也能被正確識(shí)別。代價(jià)是計(jì)算復(fù)雜度從 O(n) 漲到 O(n*m)二十年的日線全歷史暴力匹配會(huì)有性能壓力這個(gè)坑放在第五章細(xì)說(shuō)。3.2 用 numpy 寫(xiě)一個(gè)最小 DTW 實(shí)現(xiàn)遞推矩陣與路徑約束工程上可以直接調(diào)fastdtw庫(kù)但我還是建議手寫(xiě)一個(gè)最小實(shí)現(xiàn)幾十行代碼能讓你把 DTW 的遞推邏輯徹底搞清楚后面調(diào)參數(shù)、加約束、排查異常匹配都有底。核心是一個(gè)累積代價(jià)矩陣每個(gè)格子的值是當(dāng)前兩個(gè)點(diǎn)的絕對(duì)差加上到達(dá)這個(gè)格子之前最小的累計(jì)代價(jià)import numpy as np def dtw_distance(s1: np.ndarray, s2: np.ndarray, window: int None) - float: 最小 DTW 實(shí)現(xiàn)返回兩個(gè)序列的最小累計(jì)距離。 s1, s2: 一維序列建議已經(jīng)做過(guò)分段內(nèi)標(biāo)準(zhǔn)化 window: 路徑約束限制 i 和 j 的偏移量防止荒謬?yán)? n, m len(s1), len(s2) # 初始化 n1 x m1 矩陣邊界值為正無(wú)窮保證路徑不從邊界外繞行 dtw np.full((n 1, m 1), np.inf) dtw[0, 0] 0.0 for i in range(1, n 1): for j in range(1, m 1): # 如果設(shè)置了 window偏移超過(guò)閾值的格子直接跳過(guò) if window is not None and abs(i - j) window: continue cost abs(float(s1[i - 1]) - float(s2[j - 1])) # 遞推式當(dāng)前點(diǎn)代價(jià) 到達(dá)前一個(gè)點(diǎn)的三條路徑中的最小值 dtw[i, j] cost min( dtw[i - 1, j], # 向上走s2 多對(duì)齊一次 dtw[i, j - 1], # 向右走s1 多對(duì)齊一次 dtw[i - 1, j - 1] # 對(duì)角線走兩點(diǎn)正常對(duì)齊 ) return dtw[n, m]遞推式的幾何意義很重要dtw[i, j]是序列1 前 i 個(gè)點(diǎn)和序列2 前 j 個(gè)點(diǎn)的最小累計(jì)代價(jià)。向上走意味著序列2 的當(dāng)前點(diǎn)重復(fù)對(duì)齊序列1 的當(dāng)前點(diǎn)向右走則反過(guò)來(lái)這種“重復(fù)對(duì)齊”就是 DTW 處理時(shí)間軸扭曲的機(jī)制。window參數(shù)是血淚經(jīng)驗(yàn)換來(lái)的——如果完全不限制路徑DTW 可能通過(guò)極端拉伸找出一條奇怪的路徑讓兩個(gè)完全不相關(guān)的片段也拿到很小的距離。經(jīng)驗(yàn)值取序列長(zhǎng)度的 10%40 日窗口就設(shè)window4。還要注意返回值是累計(jì)代價(jià)不是平均代價(jià)。兩個(gè)序列越長(zhǎng)累計(jì)代價(jià)天然越大跨長(zhǎng)度比較時(shí)必須歸一化用dtw_distance(s1, s2) / (len(s1) len(s2))換算成每個(gè)點(diǎn)的平均代價(jià)否則匹配結(jié)果永遠(yuǎn)偏向短序列。3.3 滑窗切分把歷史行情切成可比片段再匹配有了距離函數(shù)下一步是把歷史行情切成一堆等長(zhǎng)的片段建立歷史形態(tài)庫(kù)。切分用滑窗窗口長(zhǎng)度決定你在找“多長(zhǎng)”的形態(tài)步長(zhǎng)決定匹配密度。常見(jiàn)做法是窗口取 40 個(gè)交易日約兩個(gè)月步長(zhǎng)取 1 或 5前者最細(xì)、計(jì)算量大后者做粗篩。切分時(shí)每個(gè)片段內(nèi)部做 z-score 標(biāo)準(zhǔn)化這一步必須在切片內(nèi)做原因前面已經(jīng)說(shuō)過(guò)import numpy as np import pandas as pd def build_window_library(close: pd.Series, window: int 40, step: int 5): 把收盤(pán)價(jià)序列切成等長(zhǎng)窗口每個(gè)窗口內(nèi)獨(dú)立做 z-score。 返回窗口矩陣和每個(gè)窗口的截止日期索引。 arr close.to_numpy() windows [] end_dates [] # 滑窗切分python 數(shù)組切片左閉右開(kāi)arr[start:startwindow] for start in range(0, len(arr) - window 1, step): seg arr[start:start window] # 每個(gè)窗口獨(dú)立標(biāo)準(zhǔn)化剔除絕對(duì)價(jià)位和波動(dòng)率差異 seg_norm (seg - seg.mean()) / (seg.std() 1e-8) windows.append(seg_norm) end_dates.append(close.index[start window - 1]) return np.vstack(windows), np.array(end_dates)窗口長(zhǎng)度是這個(gè)環(huán)節(jié)最該調(diào)的參數(shù)。20 日太短噪聲占比高隨便兩段隨機(jī)波動(dòng)都可能算出低距離60 日以上自然匹配的片段數(shù)量驟減能選出來(lái)的 TopN 參考意義下降。我一般先跑 40 日窗口 step5 做第一輪粗篩鎖定相似度排名前 100 的片段再把這些片段用原始逐日數(shù)據(jù)做一次精排兼顧效率和精度。step5會(huì)讓相鄰窗口高度重疊匹配結(jié)果里經(jīng)常出現(xiàn)同一段行情被連續(xù)多日命中這是正常的后續(xù)去重按截止日期做就行。4. 從相似度到可復(fù)現(xiàn)結(jié)果TopN 排序、可視化與信號(hào)轉(zhuǎn)換4.1 對(duì)全歷史片段做 TopN 排序歸一化距離再比歷史形態(tài)庫(kù)建好之后拿當(dāng)前最新的窗口去和庫(kù)里所有歷史窗口算距離排序取 TopN。注意比較前必須做歸一化直接用原始 DTW 累計(jì)距離排序結(jié)果會(huì)偏向短序列或低波動(dòng)片段這是新手最容易看錯(cuò)的一步。排序代碼import numpy as np # 取最后一個(gè)窗口作為當(dāng)前形態(tài)假設(shè)當(dāng)前是庫(kù)的最后一段 cur windows[-1] scores [] # 只和歷史窗口比不和自己比倒數(shù)第二個(gè)窗口和當(dāng)前高度重疊也建議跳過(guò) for i in range(len(windows) - 10): d dtw_distance(cur, windows[i], window4) # 歸一化除以?xún)蓚€(gè)序列總長(zhǎng)度換算成平均每點(diǎn)最小代價(jià) norm_d d / (len(cur) len(windows[i])) scores.append((end_dates[i], norm_d)) # 按歸一化距離升序取前 10 個(gè)最相似的時(shí)段 top10 sorted(scores, keylambda x: x[1])[:10] for date, score in top10: print(date, round(score, 4))這里window4對(duì)應(yīng) 40 日窗口的 10% 偏移約束。如果信號(hào)是 20 日窗口則改為window2。排名結(jié)果中排名靠前的片段大概率集中在同一個(gè)歷史牛熊階段的相鄰日期因?yàn)樾星楸旧碛凶韵嚓P(guān)性此時(shí)可以做一次去重如果兩個(gè)命中日期間隔小于窗口長(zhǎng)度保留距離更小的那個(gè)。4.2 可視化當(dāng)前走勢(shì)和 Top3 歷史走勢(shì)并排對(duì)比排名數(shù)字不足以說(shuō)服自己或團(tuán)隊(duì)可視化才是驗(yàn)證相似度計(jì)算是否合理的手段。畫(huà)出當(dāng)前序列和歷史 Top3 序列的對(duì)比圖注意用同一個(gè)縱軸范圍否則標(biāo)準(zhǔn)化后的曲線尺度不一致看起來(lái)會(huì)很誤導(dǎo)import matplotlib.pyplot as plt fig, axes plt.subplots(3, 1, figsize(10, 8)) for idx, (date, score) in enumerate(top10[:3]): hist_start np.where(end_dates date)[0][0] axes[idx].plot(windows[-1], labelcurrent, colorblack) axes[idx].plot(windows[hist_start], labelstr(date), colorsteelblue, linestyle--) axes[idx].set_ylim(-3, 3) axes[idx].legend() plt.tight_layout() plt.show()視覺(jué)確認(rèn)時(shí)要重點(diǎn)看拐點(diǎn)位置對(duì)不對(duì)。DTW 允許時(shí)間軸彎曲意味著兩條曲線高低點(diǎn)不必嚴(yán)格對(duì)齊但拐點(diǎn)順序應(yīng)該一致——如果當(dāng)前序列是“下臺(tái)階后橫盤(pán)”匹配到的歷史片段也應(yīng)該是“下臺(tái)階后橫盤(pán)”而不是“緩慢陰跌”或者“橫盤(pán)后加速”。拐點(diǎn)對(duì)不上說(shuō)明窗口長(zhǎng)度不合適或者 window 約束太松。這種目視抽檢每次至少做 3 個(gè)不同時(shí)間點(diǎn)的匹配只看單次結(jié)果容易陷入“湊巧像”的錯(cuò)覺(jué)。4.3 把相似變成信號(hào)看匹配后的走勢(shì)分布而不是單次結(jié)果相似性分析本身只回答“像不像”不回答“接下來(lái)漲不漲”。要用它輔助決策標(biāo)準(zhǔn)做法是統(tǒng)計(jì)所有相似片段之后一段時(shí)間的收益分布而不是盯住某一次匹配的后續(xù)走勢(shì)。對(duì)每個(gè)命中的歷史片段取它結(jié)束之后未來(lái) 5 日的漲跌幅做分布統(tǒng)計(jì)def match_future_stats(close: pd.Series, windows: np.ndarray, end_dates: np.ndarray, top_k: int 10, future: int 5): 當(dāng)前窗口匹配歷史 TopK統(tǒng)計(jì)每個(gè)歷史片段未來(lái) future 日的漲跌幅。 返回平均漲跌幅、上漲比例、片段數(shù)量。 cur windows[-1] scores [] for i in range(len(windows) - 10): d dtw_distance(cur, windows[i], window4) scores.append((i, d / (len(cur) len(windows[i])))) scores sorted(scores, keylambda x: x[1])[:top_k] rets [] for i, _ in scores: start_pos i * 5 # 窗口構(gòu)建時(shí) step5還原原始序列位置 end_pos start_pos 40 future if end_pos len(close): continue seg close.iloc[start_pos 40: end_pos] rets.append(seg.iloc[-1] / seg.iloc[0] - 1) if not rets: return None return np.mean(rets), np.sum(np.array(rets) 0) / len(rets), len(rets)這段代碼本質(zhì)上是把“相似形態(tài)”翻譯成“后續(xù)收益的樣本分布”已經(jīng)接近一個(gè)形態(tài)類(lèi)量化策略代碼的雛形。但必須強(qiáng)調(diào)統(tǒng)計(jì)結(jié)果只是描述不是預(yù)測(cè)。匹配片段數(shù)量少于 30 次時(shí)均值沒(méi)有統(tǒng)計(jì)意義漲跌幅分布要結(jié)合下一章的隨機(jī)基準(zhǔn)確認(rèn)否則很容易被幸存者偏差誤導(dǎo)。5. 股票序列相似性分析的避坑5 條血淚經(jīng)驗(yàn)里的現(xiàn)象、原因與修復(fù)5.1 未復(fù)權(quán)數(shù)據(jù)在除權(quán)日制造“假相似”現(xiàn)象某只股票除權(quán)除息日之后全歷史相似度排名突然涌出一批和當(dāng)前形態(tài)無(wú)關(guān)的片段把命中片段畫(huà)出來(lái)發(fā)現(xiàn)它們的共同點(diǎn)是都跨過(guò)了一次除權(quán)跳空。原因除權(quán)日的價(jià)格跳空在 DTW 眼里是一個(gè)需要特殊拉伸才能對(duì)齊的突變點(diǎn)。為了繞開(kāi)這個(gè)高成本區(qū)域DTW 會(huì)傾向于把路徑“跳過(guò)”這段導(dǎo)致包含除權(quán)跳空的片段之間距離整體偏小形成假相似。解決統(tǒng)一使用前復(fù)權(quán)數(shù)據(jù)adjustqfq。如果歷史區(qū)間超過(guò)十年改用后復(fù)權(quán)避免早期價(jià)格為負(fù)。另外可以在預(yù)處理里加一道檢查相鄰兩日漲跌幅超過(guò) 15% 且當(dāng)日成交量異常放大優(yōu)先懷疑是除權(quán)未處理直接打印出來(lái)人工確認(rèn)。5.2 回測(cè)勝率虛高歷史形態(tài)庫(kù)里混入了“未來(lái)數(shù)據(jù)”現(xiàn)象用相似形態(tài)做后續(xù)走勢(shì)統(tǒng)計(jì)時(shí)勝率高達(dá) 80% 以上自己也覺(jué)得不真實(shí)換了個(gè)時(shí)間段勝率又掉到 40%結(jié)果飄忽不定。原因構(gòu)建歷史形態(tài)庫(kù)時(shí)沒(méi)有做時(shí)序隔離。如果當(dāng)前時(shí)間點(diǎn)是 T而形態(tài)庫(kù)里包含了 T 時(shí)刻之后才形成的片段匹配時(shí)就會(huì)用到未來(lái)信息這就是未來(lái)函數(shù)回測(cè)曲線再漂亮也是假的。解決匹配第 T 日的形態(tài)時(shí)候選庫(kù)只保留截止日期早于 T 的片段不能用全歷史一次性建庫(kù)再匹配。滾動(dòng)更新庫(kù)每次把當(dāng)前形態(tài)追加進(jìn)庫(kù)同時(shí)對(duì)已有庫(kù)按時(shí)間截?cái)唷E袛嘁粋€(gè)代碼有沒(méi)有未來(lái)函數(shù)看它建庫(kù)和匹配是否在同一段循環(huán)里完成。5.3 序列長(zhǎng)度不一致時(shí)無(wú)腦補(bǔ)齊距離被瘋狂放大現(xiàn)象兩個(gè)序列長(zhǎng)度不同比如一個(gè)是 40 日一個(gè)是 55 日直接拿 55 日序列的前 40 個(gè)點(diǎn)去比較距離異常大補(bǔ)零對(duì)齊后距離更大。原因補(bǔ)零相當(dāng)于給序列兩端人為制造了一段“零收益”DTW 被迫用更長(zhǎng)的路徑繞過(guò)這段假數(shù)據(jù)累計(jì)距離虛高歸一化后依然偏高。長(zhǎng)度不一致時(shí) DTW 的優(yōu)勢(shì)之一就是不用裁剪但前提是參與比較的兩個(gè)序列本身是“完整形態(tài)”而不是硬截?cái)嗟臍埰?。解決做長(zhǎng)度匹配時(shí)要么保持窗口嚴(yán)格等長(zhǎng)要么用 DTW 處理不等長(zhǎng)序列后按平均代價(jià)歸一化。等長(zhǎng)窗口是工程上最省心的方案構(gòu)建歷史形態(tài)庫(kù)時(shí)就固定 window后續(xù)所有比較都在同一長(zhǎng)度下進(jìn)行。5.4 全市場(chǎng)暴力匹配太慢從幾十分鐘到幾秒的剪枝方法現(xiàn)象五千只股票、每只二十年日線、40 日窗口直接全量跑 DTW一次匹配要等十幾分鐘完全沒(méi)法迭代調(diào)參。原因手寫(xiě)雙層循環(huán)本來(lái)就不快再加上 Python 的 GIL 和動(dòng)態(tài)類(lèi)型開(kāi)銷(xiāo)千萬(wàn)次級(jí)別的 DTW 調(diào)用就是災(zāi)難。DTW 本身的 O(n*m) 復(fù)雜度也扛不住全量掃描。解決先降復(fù)雜度再精排。第一步用皮爾遜相關(guān)系數(shù)粗篩相關(guān)系數(shù)高于 0.8 的片段才進(jìn)候選集這一步是向量化的幾秒就能跑完第二步只對(duì)候選集里的前 100 個(gè)片段做 DTW 精排。同時(shí)給 DTW 加 window 參數(shù)把復(fù)雜度從 O(nm) 降到 O(nwindow)。數(shù)據(jù)量再大就上 numba JIT或者用fastdtw的近似算法精度損失在形態(tài)匹配場(chǎng)景下可以接受。5.5 把“相似”當(dāng)“因果”忽略隨機(jī)基準(zhǔn)的比較現(xiàn)象某形態(tài)匹配完成后后續(xù) 5 日平均收益 1.5%看起來(lái)是一個(gè)可以入場(chǎng)的信號(hào)但看一眼全樣本所有歷史片段后續(xù) 5 日的平均收益也是 1.5%根本沒(méi)有超額。原因單次匹配的樣本量小偶然性高很多“有效形態(tài)”只是剛好趕上了一段普漲行情。不做隨機(jī)基準(zhǔn)對(duì)比任何統(tǒng)計(jì)結(jié)果都可能只是運(yùn)氣。解決每次統(tǒng)計(jì)相似片段后續(xù)收益時(shí)從全部歷史片段里隨機(jī)抽同樣數(shù)量做基準(zhǔn)對(duì)比兩組均值差。樣本量少于 50 次的匹配結(jié)論不要下條件允許的話做 bootstrap 置信區(qū)間看看均值差是否顯著不為零。這是判斷“相似性分析到底有沒(méi)有用”的唯一可靠方式。6. 進(jìn)階DTW 窗口約束、多尺度確認(rèn)與一個(gè)最小驗(yàn)證腳本6.1 Sakoe-Chiba 帶給 DTW 加上合理的路徑約束DTW 的window參數(shù)就是 Sakoe-Chiba 帶限制路徑偏移對(duì)角線的最大距離。經(jīng)驗(yàn)值是序列長(zhǎng)度的 10%20 日窗口設(shè) 240 日窗口設(shè) 460 日窗口設(shè) 6。設(shè)置過(guò)小會(huì)把有意義的相位偏移全部截?cái)嘣O(shè)置過(guò)大又回到無(wú)約束狀態(tài)。調(diào)參時(shí)對(duì)比window0和windowlen*0.1的 Top10 結(jié)果如果兩組結(jié)果差異巨大說(shuō)明序列本身噪聲過(guò)高優(yōu)先檢查數(shù)據(jù)而不是繼續(xù)調(diào)參。6.2 多尺度確認(rèn)日線相似加上周線相似結(jié)論才站得住日線噪聲會(huì)導(dǎo)致隨機(jī)匹配一種有效過(guò)濾方式是多尺度確認(rèn)。日線匹配出 Top100 后把命中的時(shí)間段換成周線再算一遍 DTW取兩組結(jié)果的交集。日線和周線都相似的形態(tài)比單一日線相似可靠得多相當(dāng)于增加了一個(gè)獨(dú)立驗(yàn)證維度。這個(gè)做法成本低代碼復(fù)用同一切片邏輯只是把 resample 周期改成周線。6.3 最小回測(cè)腳本把最近 10 次形態(tài)匹配的后續(xù)走勢(shì)跑出來(lái)最后給一個(gè)可以直接用的回測(cè)骨架驗(yàn)證相似度與未來(lái)收益之間是否存在真實(shí)關(guān)聯(lián)。它把每個(gè)歷史日期都當(dāng)作“當(dāng)前日”滾動(dòng)匹配按平均相似度分高低兩組對(duì)比未來(lái)收益def similar_signal_backtest(close: pd.Series, window: int 40, top_k: int 10, future: int 5): arr close.to_numpy() n len(arr) results [] for t in range(window, n - future): # 用 t 日之前的 window 個(gè)點(diǎn)作為當(dāng)前形態(tài)不觸碰未來(lái)數(shù)據(jù) cur arr[t - window:t] cur_z (cur - cur.mean()) / (cur.std() 1e-8) best [] # 在更早的歷史里匹配步長(zhǎng) 5 粗篩 for s in range(0, t - window 1, 5): seg arr[s:s window] seg_z (seg - seg.mean()) / (seg.std() 1e-8) d dtw_distance(cur_z, seg_z, window4) / (2 * window) best.append((d, s)) if len(best) top_k: continue best sorted(best, keylambda x: x[0])[:top_k] # 未來(lái) future 日的實(shí)際漲跌幅作為信號(hào)有效性標(biāo)簽 fwd_ret arr[t future] / arr[t] - 1 avg_sim np.mean([x[0] for x in best]) results.append((fwd_ret, avg_sim)) results np.array(results) median_sim np.median(results[:, 1]) low_group results[results[:, 1] median_sim, 0] high_group results[results[:, 1] median_sim, 0] return low_group.mean(), high_group.mean(), len(results) # 輸出(低相似組未來(lái)均值, 高相似組未來(lái)均值, 樣本量) print(similar_signal_backtest(df[close]))注意這里的輸出不構(gòu)成交易建議它只回答一個(gè)問(wèn)題歷史相似度高低與未來(lái)收益之間有沒(méi)有可觀測(cè)的差異。如果高相似組的未來(lái)收益顯著高于低相似組說(shuō)明這個(gè)形態(tài)窗口有信息量如果沒(méi)有差異說(shuō)明這個(gè)窗口長(zhǎng)度或度量方式不適合當(dāng)前標(biāo)的換參數(shù)重來(lái)。我做這類(lèi)分析最早的教訓(xùn)就是直接用 Top1 相似形態(tài)做預(yù)測(cè)回測(cè)曲線畫(huà)出來(lái)很漂亮后來(lái)對(duì)比隨機(jī)基線才發(fā)現(xiàn)根本沒(méi)有優(yōu)勢(shì)。從那以后我把這個(gè)工具的定位收斂成“形態(tài)定位器”——它負(fù)責(zé)告訴我歷史上什么時(shí)候出現(xiàn)過(guò)類(lèi)似走法以及后來(lái)大概率的路徑分布最終判斷還是交給人和更上層的規(guī)則。每次拿到新的股票代碼我會(huì)先跑一遍第六節(jié)的回測(cè)腳本看相似度有沒(méi)有信息量再?zèng)Q定要不要把參數(shù)調(diào)細(xì)。這個(gè)前置檢查幫我避開(kāi)了不少無(wú)效的形態(tài)研究希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取