iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Software Development

量化交易入門:從 K 線到可組合的交易策略引擎系列 第 12

Day 11:均價要用哪個?VWAP 與成交量加權的資金流視角"

  • 分享至 

  • xImage
  •  

有人買在 65,000,有人買在 64,500,那「現在」是賺還是賠

Day 10 的 OBI 有一個很硬的限制:現貨的掛單簿歷史在免費資料源裡不存在,所以它只有我們自己錄的那 45 分鐘。今天換到另一邊——成交資料。掛單可以撤,成交撤不掉,而且 Day 09 那條 aggTrades 路徑要幾天有幾天。

先看一個具體的問題。2026-07-15 那天的 BTC/USDT 現貨,1 分鐘 K 線的收盤價從 65,200 附近走到 64,750 附近。「那天的平均價」是多少?

最直覺的算法是把 1,440 根 K 線的收盤價加起來除以 1,440。但這個算法有一個很明顯的問題:那 1,440 分鐘裡,有些分鐘成交了 200 顆 BTC,有些分鐘成交了 0.25 顆(那天最冷清的一分鐘)。算術平均把它們當成一樣重要。

用工程的話講:算術平均假設每個樣本的權重相同,而市場資料的樣本權重顯然不同。正確的權重是成交量。 這就是 VWAP(Volume Weighted Average Price,成交量加權平均價):

VWAP = Σ(價格 × 成交量) / Σ成交量

一行公式,但它換掉的問題比看起來多。均線問的是「價格往哪走」,VWAP 問的是**「現在的價格,對已經進場的人來說是賺還是賠」**——因為 VWAP 就是那些人的平均成本。

今天要處理三件事:兩種累積方式(它們是兩個不同的工具,不是同一個工具的兩種設定)、一個會吃掉數值精度的實作陷阱,以及把 VWAP 變成策略真正能用的形狀。

交易概念補課

為什麼權重是成交量:把時間軸換成成交量軸

「用成交量加權」聽起來像一個修正項,好像是在原本的平均上打個補丁。它其實換掉的是更底層的東西——用什麼當作「一個樣本」

K 線是照時間切的。切法決定了每根 K 線代表一段等長的時間,而時間本身並不是市場的計量單位。市場真正在計量的是成交:一筆成交發生時,買賣雙方同意了一個價格,那是市場對「現在值多少」的一次表態;一分鐘裡完全沒有成交,市場什麼也沒說。

算術平均把 1,440 根 K 線當成 1,440 個等權樣本,等於宣稱「每一分鐘的意見一樣重要」。可是其中有一分鐘成交了 200 顆 BTC(幾千筆成交的共識),另一分鐘成交了 0.25 顆(可能只有兩三筆)。這兩個樣本的資訊量差了三個數量級。

成交量加權做的事,換個角度看就是把時間軸換成成交量軸。VWAP 不問「這一分鐘的價格是多少」,它問「每成交一顆 BTC,平均成交在什麼價格」。這條軸上每一單位都是等重的,因為每一單位都是同樣多的真實成交。

這個視角在後面幾天會反覆出現。Day 12 量活躍度,本質上就是在量「時間軸與成交量軸的換算率」——同樣一分鐘,有時候裝進去很多成交,有時候幾乎是空的。Day 14 的 Volume Profile 更直接:它乾脆把時間軸整個丟掉,只問「哪些價格區間累積了最多成交」。

VWAP 為什麼是機構的參考基準

一個要買進 500 顆 BTC 的機構不可能一次下單——那會把價格推上去。實際做法是拆成幾百張小單,在一段時間裡慢慢買。

買完之後怎麼評估執行得好不好?跟收盤價比沒有意義(收盤價只是最後一個瞬間),跟最高最低比也沒有意義——那兩個數字是事後才知道的,而且只有一筆成交碰得到。合理的基準是同一段時間裡市場的平均成本,也就是 VWAP。買在 VWAP 之下代表買得比市場平均便宜。

這件事的因果方向值得留意:不是先有 VWAP 這個指標,然後有人拿它當基準;是先有「怎麼衡量拆單執行的品質」這個需求,而 VWAP 是那個需求的答案。它是一個執行品質的量尺,被拿來當進出場訊號是後來的事。這也解釋了為什麼它的定義只有一種,沒有參數可調——量尺如果人人一把不同的,就沒有量尺的功能了。

於是 VWAP 有一個其他指標沒有的性質:它是很多人真的在拿來當標準的數字。 均線的參數(20 還是 60)是各人自選的,而以 VWAP 為目標的執行演算法會照著它下單。這讓「價格在 VWAP 之上或之下」多了一層自我實現的成分——不是因為它有什麼神秘的預測力,而是因為有一群人的決策確實掛在它上面。

價格在 VWAP 之上或之下的意義

VWAP 是平均成本,所以:

  • 價格在 VWAP 之上:這段期間進場的人平均是獲利的。
  • 價格在 VWAP 之下:這段期間進場的人平均是虧損的,而套在上面的人有回本就賣的動機。

第二種情況是「壓力」這個詞在資料上的樣子。它不保證價格會往下,但它描述了一群人的處境,而那個處境會影響他們的行為。

這裡有一個要小心的地方:「平均成本」是一個統計上的重心,不是任何一個人的成本。真實的持倉分布可能是兩群人——一群買在 65,200,一群買在 64,300——而平均值落在中間那個沒什麼人成交的價位。用一個數字概括整個分布,就會漏掉「分布長什麼形狀」這件事。Day 14 的 Volume Profile 正是為了補這個缺口:它不給一個平均值,它給整條分布。

實測 2026-07-15 那天:價格在日內 VWAP 之上的時間佔 45.14%。也就是說那天多數時間裡,當天進場的人平均是虧的。

日內 VWAP 有一個均線沒有的性質:它越走越鈍

均線有固定的視窗——20 根就是 20 根,昨天如此,明天也如此。日內 VWAP 不是,它的視窗從當天開盤起算,一路長大

這件事的後果比聽起來大。早上第一根 K 線,VWAP 就等於那根的價格;第十根的時候它只累積了十根,新的一根有十分之一的影響力;到了晚上第一千根,新的一根只有千分之一的影響力。也就是說,同一條線在一天的開頭像一條很短的均線,到了晚上像一條很長的均線。

由此可以推出兩件之後會在圖上看到的事,而且不必等到看圖:

  • 每天開頭幾根,VWAP 幾乎黏著價格走,而它的標準差通道非常窄(樣本太少)。窄通道會讓標準化的偏離度特別容易破 2——這是模式的性質,不是行情變激烈。
  • 跨過 UTC 午夜的那一根,線會斷開重來,因為分母歸零了。一條會在半夜跳一下的線,拿去跟連續的均線做交叉判斷會出事。

滾動視窗沒有這兩個性質,代價是它不對應任何一群人的實際成本——「過去 60 根的平均成本」不是任何人的持倉均價,它只是一個統計量。

這就是為什麼今天堅持說這兩者是兩個工具,而不是同一個工具的兩種設定。它們的視窗行為不同、可解釋性不同、通道寬度的成因也不同。

資金流:把方向加進成交量

成交量本身沒有方向——一顆 BTC 換手,同時有人買也有人賣。要讓成交量帶方向,得靠 Day 09 講的 taker 概念:主動吃單的那一方是誰。買賣雙方在數量上永遠相等,但誰比較急這件事不對稱,而那就是方向的來源。

Day 09 已經把 taker_buy_base_volume 存進 candles 了(官方 K 線的 12 欄之一),所以「這一分鐘的主動買佔多少」是查得到的。Day 09 那兩根 K 線就是這麼比出來的:一根 80.6%、一根 4.7%。

要留意這個標記能宣稱的範圍:它說的是「哪一方主動」,不是「哪一方比較聰明」。一個急著停損出場的人跟一個看到好消息急著進場的人,在資料上都是主動方。方向資訊是真的,方向的含義要靠上下文,而那個上下文就是 Day 13 的題目——同樣是主動買,突破時出現與盤整時出現是兩回事。

今天的 VWAP 不區分方向(它是所有成交的加權平均),但這個概念今天要先建立。

工程實作

今天的程式要做什麼

動手之前先把工作攤開來。今天要算的是兩個特徵:一個是平均成本本身,一個是「現在離它多遠」的無單位版本。

  1. 決定每一根 K 線用哪個價格代表——收盤價只採樣一個瞬間,所以預設用典型價(高、低、收三個取平均)。這是一個選擇,要能換。
  2. 算加權平均,得到 VWAP——把每一根的「價格 × 成交量」累積起來,除以成交量的累積和。累積的範圍有兩種:日內從 UTC 午夜起一路長大,或是固定往前看 N 根。這兩種是兩個不同的工具,各算各的。
  3. 算加權標準差——權重跟 VWAP 用同一組,但要先把價格平移到 0 附近再算,否則在 BTC 的價位上會掉精度。
  4. 算偏離度——價格離 VWAP 多遠,除以標準差變成無單位的值。這一個才是策略真正用得上的形狀。標準差是 0 的時候回 NaN,NEVER 回無限大。
  5. 統計並排報表——最後一根的 VWAP 與價格、價格在 VWAP 之上的比例、偏離超過 1 與 2 個標準差的根數與比例。兩種累積方式各跑一次,並排比較。
  6. 畫圖——上格 K 線疊上 VWAP 與 ±1、±2 標準差通道,下格是偏離度。

第 2 步與第 3 步要能各自取用:通道要標準差、偏離度要標準差,而報表裡兩個都要印。第 4 步則完全建在前兩步之上,它自己不重算任何東西——「哪一種累積、哪一個價格」只在第 1、2 步決定一次。

兩種累積方式,兩個工具

Σ(價格 × 成交量) / Σ成交量 這個式子有一個沒說的地方:Σ 從哪裡加到哪裡。

前一節已經把兩種答案的性質講完了,這裡只補命名與一個約定。專案裡它是一個值(VWAPModeSESSIONROLLING),並且進到特徵的名字(vwap_sessionvwap_rolling_60),設定檔上看得見。理由等一下實跑那節會兌現:兩者的觸發頻率差將近一倍,藏在設定裡的差異遲早會變成一次對不起來的回測。

約定的部分是「一天」怎麼切。股市的日內 VWAP 有明確的物理意義:開盤到收盤是一個完整的交易時段,收盤之後所有人都停下來。加密貨幣 24/7 不休市,所謂「當日」是我們自己在 UTC 午夜切一刀。切在 UTC 午夜、紐約午夜還是台北午夜,會得到三條不同的線,而沒有哪一條比較「正確」。

這不代表日內 VWAP 在加密貨幣上沒用——很多人確實看 UTC 日界,所以它有那層自我實現的成分。但它是一個約定,寫程式的人要知道自己選了什麼。

實作上分日用 DatetimeIndex.normalize() 而不是 index.date。後者看起來更直覺,但它把索引轉成一整欄 Python date 物件(object dtype),groupby 因此慢好幾倍,而且時區資訊在那一步就掉了。normalize() 保留型別與時區,只把時間部分歸零。

用哪個價格代表一根 K 線

第二個沒說的地方:價格 是哪個價格。

收盤價是預設的直覺選擇,但它只採樣一個瞬間——一根長影線的 K 線,收盤價完全看不出那一分鐘走過的範圍。成交量加權的計算慣例用典型價 (高 + 低 + 收) / 3,把區間也算進去。

典型價是慣例,不是定理。它的邏輯呼應本篇第一節:那一分鐘的成交散落在整個價格區間裡,不是全部發生在收盤價上,所以用單一瞬間代表整根會系統性地偏掉。至於為什麼是這三個而不是開高低收四個平均,答案就只是慣例,沒有更深的理由。

真正精確的做法是拿逐筆成交去算,Day 09 那條路徑有那份資料,而那正是 Day 14 要做的事——用 tick 精算跟用 K 線近似的差別,那天會有數字。

順帶一句很划算的檢查:官方 K 線的 12 欄裡有 quote_volume(以報價幣計的成交額),而 quote_volume / volume 就是那一根真正的 VWAP,不需要任何近似。單根 K 線內的近似誤差可以直接跟它對,Day 14 會用到。

在專案裡這個選擇也是一個值(PriceSourceCLOSETYPICAL)而不是一個布林參數,因為之後還會有第三種。

加權平均:兩種累積只差一個方法

# quantbot/domain/features/volume_weighted_average_price.py
    def _weighted_mean(self, values: pd.Series, volume: pd.Series) -> pd.Series:
        """加權平均。兩種累積方式只差 cumsum 與 rolling().sum()。

        分母是成交量的累積和,所以完全沒有成交的那幾根會讓分母是 0。那時候回 NaN
        而不是 0——「沒有人成交」時平均成本沒有定義,填 0 會在圖上畫出一條掉到
        原點的線,而那條線會被下游當成真的價格。
        """
        weighted = values * volume
        if self.mode is VWAPMode.SESSION:
            session = self._session_of(values.index)
            numerator = weighted.groupby(session).cumsum()
            denominator = volume.groupby(session).cumsum()
        else:
            numerator = weighted.rolling(self.window).sum()
            denominator = volume.rolling(self.window).sum()
        return (numerator / denominator).where(denominator > 0)

全部向量化,沒有一行在遍歷 K 線。日內模式用 groupby(...).cumsum():分組之後各自累積,跨日自然重置。兩種模式在程式上只差一個方法呼叫,在概念上卻是前面講的那兩個不同工具——這種「實作幾乎一樣、語意完全不同」的地方,正是最需要在名字上分開的地方。

加權標準差:課本那條算法在 BTC 的價位上會出事

VWAP 只是第一階的統計量。要回答「現在偏離得多不多」,還需要第二階:加權標準差。

先講一件容易被跳過的事:這裡用的是加權標準差,權重跟 VWAP 是同一組。理由是一致性——中心已經是用成交量加權算出來的,如果離散度改用等權,兩者描述的就不是同一個分布了。用成交量當權重的意思是:離散度衡量的是「每成交一顆 BTC,成交價離平均成本多遠」,跟中心的定義對得上。

課本上的算法是「平方的平均,減掉平均的平方」。它受歡迎的理由很實際:完全向量化得起來,兩個加權平均各算一次就好,不必先算平均再回頭掃第二遍。

問題是它在 BTC 的價位上會出事。BTC 的價格是五位數,平方之後是十位數;而一天之內的變異數可能只有幾十。也就是說那條算法要拿兩個非常大的數相減,去湊出一個很小的結果,而相減的時候前面那些位數會彼此抵銷——float64 的精度就是在這一步被吃掉的。

這種誤差不會噴例外,也不會產生 NaN,它產生一個看起來很正常、但後面幾位是垃圾的數字。更麻煩的是它的嚴重程度跟標的的價位成正比:同一段程式碼在價格 30 的標的上完全沒事,換到 BTC 才開始掉精度,所以它特別容易在開發階段被漏掉。

解法是先把價格平移到 0 附近,讓相減的兩個數量級接近。變異數不受平移影響(每個值都減掉同一個常數,離散程度不變),所以這不是近似,是同一件事的另一種寫法:

# quantbot/domain/features/volume_weighted_average_price.py
    def standard_deviation(self, view: MarketView) -> pd.Series:
        """加權標準差,用來畫通道、也用來標準化偏離程度。

        算法是「加權平方的平均 − 加權平均的平方」,但**先把價格平移到 0 附近**。
        直接對 BTC 的價格用那條算法會出事:五位數的價格平方之後是十位數,而
        變異數只有幾十,拿兩個量級差很多的數相減會把 float64 的精度吃掉大半。
        平移之後兩邊的量級就接近了。

        平移不改變變異數,所以這不是近似,是同一件事的另一種寫法。
        """
        candles = view.candles.frame
        price = self.price_source.of(candles)
        volume = candles["volume"].astype("float64")

        centre = float(price.mean())
        shifted = price - centre
        mean = self._weighted_mean(shifted, volume)
        mean_of_squares = self._weighted_mean(shifted**2, volume)
        # 浮點誤差可能讓變異數變成 -1e-12 這種值,開根號會得到 NaN
        variance = (mean_of_squares - mean**2).clip(lower=0.0)
        # 用 Series.pow 而不是 np.sqrt:後者的回傳型別在 pandas-stubs 下退化成 Any,
        # 於是 mypy --strict 再也看不出這個函式回傳的是 Series
        return variance.pow(0.5).rename(f"{self.name}_standard_deviation")

clip(lower=0.0) 那一行不是防禦性的裝飾。即使平移過,浮點誤差還是可能讓變異數變成 -1e-12 這種值(數學上不可能為負),而 sqrt(-1e-12) 會得到 NaN——一個突然出現的 NaN 會讓下游的偏離度整段消失,而原因非常難查。

平移救回多少,是一件可以測的事,而測法的形狀值得說明。對照組是逐項展開的定義式——先算出平均,再一項一項量每個價格離它多遠,精度最好但要掃兩遍資料。測試斷言的是:平移過的向量化版本跟它的誤差小於 1e-9,而沒平移的版本誤差至少大一百倍。

這個測試不是在斷言「我的版本是對的」,而是在斷言**「我的版本比那個看起來一樣的寫法準確得多」**。沒有這個對照,兩種寫法都會通過任何「數值範圍合理」的檢查——這正是這類數值缺陷的特徵,它沒有一個會失敗的斷言,除非刻意去造一個。

偏離度:策略真正會用到的那個數字

VWAP 本身不能直接進策略,因為它的單位是價格。「價格比 VWAP 高 120 USDT」在 BTC 上是小事,在 ETH 上是大事,同一個門檻 NEVER 能同時適用於兩個交易對。除以標準差之後它變成無單位的,跨交易對、跨時段才可比:

# quantbot/domain/features/vwap_deviation.py
    def compute(self, view: MarketView) -> pd.Series:
        price = self._vwap.price_source.of(view.candles.frame)
        centre = self._vwap.compute(view)
        spread = self._vwap.standard_deviation(view)
        # 標準差為 0 的時候(整段只有一個成交價)偏離沒有定義,回 NaN 而不是 inf
        return ((price - centre) / spread.where(spread > 0)).rename(self.name)

這也是 Day 18 均值回歸策略裡「價格顯著偏離 VWAP」那句話的實際定義——「顯著」是幾個標準差,是一個要被寫下來的數字,不是一種感覺。

型別上它用組合而不是繼承:VWAPDeviation 收一個 VWAP 進來。這讓「哪一種累積、哪一個價格」只需要決定一次,而且兩者都是 Feature,Day 15 的管線可以把它們當成兩個獨立的特徵各自註冊。

陷阱與驗證

四個會算出「合理數字」的錯

今天的測試守的都是同一類問題:寫錯了也會得到一個看起來很正常的數字。 四個邊界值得逐一寫下來。

一、加權真的有加到。 兩根 K 線,價格 100 與 200、成交量 1 與 99:算術平均是 150,VWAP 是 199。忘記乘權重、或把權重乘在錯的地方,算出來還是一個落在 100 與 200 之間的合理數字,沒有任何跡象。這個測試看起來很淺,但它守的正是這篇文章存在的理由。

二、跨日要重置。 前 24 小時價格 100、後 24 小時價格 200,日內模式在第 24 根(第二天第一根)必須正好等於 200,不能是任何中間值。忘記分組的話那裡會落在 150 附近,而 150 看起來完全像一個合理的 VWAP。

三、沒有成交的那幾根回 NaN 不回 0。 分母是成交量的累積和,完全沒成交時它是 0。理由跟 Day 10 的「兩側都空」一樣:0 是一個有意義的值(「平均成本是零元」顯然不對),NaN 才是「這裡沒有定義」。實務上這會發生在冷門交易對與交易所維護期間;BTC/USDT 不會,但寫特徵的時候不能假設只有 BTC。

四、標準差為 0 時偏離度回 NaN 不回 inf 整段只有一個成交價時標準差是 0,除法會得到 inf,而 inf 比 NaN 危險——它會通過 notna() 檢查,然後在下游的比較運算裡永遠成立。一個「偏離超過 2 個標準差就進場」的條件會在這裡無條件觸發。

這四個加上前面那個精度對照,共同的形狀是:錯誤的版本不會失敗,只會安靜地給出另一個答案。 這類缺陷只有兩種抓法,一種是刻意造一個知道正確答案的極端案例,另一種是等到回測結果不合理再回頭查半天。

標準差是一個尺度,不是一個機率

把偏離度除成「幾個標準差」之後,很容易順手接受一個隱含的假設:2 個標準差 ≈ 很罕見。這個聯想來自常態分布,而偏離度不是常態分布的。

等一下的實測會給出數字:日內模式超過 1 個標準差的比例是 28.70%,接近常態的 31.7%;滾動 60 根是 53.29%,遠遠超出。同一個「2 個標準差」的門檻,在兩種模式下代表的稀有程度完全不同。

所以標準化真正買到的是可比性(跨交易對、跨時段的單位統一),不是機率解釋。要知道一個門檻多罕見,唯一可靠的辦法是回頭數這份資料裡它觸發了幾次——這也是 Day 18 訂策略門檻時要做的事,而不是查常態分布表。

兩種累積差多少:實際跑一次

前面說「日內」與「滾動」是兩個不同的工具。這句話用同一天的資料就能量出來。

uv run python -m quantbot.entrypoints.vwap_command \
    --symbol BTC/USDT --market spot --timeframe 1m \
    --start 2026-07-15 --end 2026-07-16 --mode session
1,440 根 K 線:2026-07-15 00:00:00+00:00 → 2026-07-15 23:59:00+00:00
vwap_session:最後一根 64,974.38,價格 64,756.83
價格在 VWAP 之上的比例:45.14%
偏離超過 1 個標準差:413 根(28.70%)
偏離超過 2 個標準差:72 根(5.00%)

換成滾動 60 根(也就是過去一小時):

1,440 根 K 線:2026-07-15 00:00:00+00:00 → 2026-07-15 23:59:00+00:00
vwap_rolling_60:最後一根 64,810.65,價格 64,756.83
價格在 VWAP 之上的比例:48.89%
偏離超過 1 個標準差:736 根(53.29%)
偏離超過 2 個標準差:180 根(13.03%)

這兩份報告的百分比分母不是 1,440。偏離度要有標準差才算得出來,而日內模式當天的第一根、滾動模式的前 59 根都還沒有,所以分母分別是 1,439 與 1,381——拿 1,440 去除會對不起來,差的那一截就是暖機期。報表同時印根數與百分比就是為了這件事:只印百分比的話,分母是什麼再也問不出來。

並排看:

日內重置 滾動 60 根
最後一根的 VWAP 64,974.38 64,810.65
價格在 VWAP 之上 45.14% 48.89%
偏離超過 1 個標準差 28.70% 53.29%
偏離超過 2 個標準差 5.00% 13.03%

同一天、同一根 K 線,兩條線相差 164 USDT(64,974.38 對 64,810.65)。 而「偏離超過一個標準差」的比例從 28.70% 變成 53.29%——差了將近一倍。

差距的來源不是價格,是標準差。日內 VWAP 的標準差累積了整天的價格範圍,通道很寬;滾動 60 根只用最近一小時,通道窄得多,於是價格待在通道外的時間自然多得多。這正是前面「越走越鈍」那一節的直接後果:一個視窗會長大的工具與一個視窗固定的工具,通道寬度的行為本來就不一樣。

實務結論很直接:「價格偏離 VWAP 兩個標準差」在沒有講清楚哪一種累積之前,不是一個定義。 兩個人照同一句話寫程式,會得到兩個觸發頻率差一倍的策略。

拉長到一年半的 1 小時線,滾動 168 根(一週):

13,848 根 K 線:2025-01-01 00:00:00+00:00 → 2026-07-31 23:00:00+00:00
vwap_rolling_168:最後一根 64,164.76,價格 62,920.68
價格在 VWAP 之上的比例:49.05%
偏離超過 1 個標準差:45.87% / 超過 2 個標準差:7.85%

「價格在 VWAP 之上」在三組設定裡都落在 45% 到 49%——這個數字本身沒有交易資訊(它接近 50% 是應該的),但它是一個有用的健康檢查:如果它跑到 70% 或 30%,那八成是計算或對齊出了問題,不是市場的性質。這種「應該接近某個值、否則就是有 bug」的量,在特徵工程裡值得多找幾個出來。

視覺化

輸出是上下兩格:上格 K 線疊上 VWAP 與 ±1、±2 標準差通道,下格是偏離度。通道跟 K 線疊在同一張而不是分開兩張,因為要看的是「價格現在在通道的哪裡」,那是一個相對位置,拆開就得靠眼睛在兩邊來回對時間軸。

上下兩格的分工正是標準化的價值:上格的通道會隨行情呼吸(波動大的時候變寬),所以「離通道多遠」在圖上不好比較;下格的單位是標準差,±2 那條線一整天都在同一個高度。策略要用的是下格那種形狀。

畫完之後有一件事值得用眼睛確認:把日內 VWAP 那張圖拉到 UTC 午夜的位置,會看到那條線在那一根斷開重來,而通道在每天開頭幾根特別窄。前面推導過為什麼會這樣,圖只是把它兌現。那個窄通道會讓偏離度在每天開頭特別容易破 2——這是日內模式的已知性質,不是 bug,但拿它當進場條件的人要知道有這件事。

今日交付物

quantbot/
├── domain/
│   ├── values/
│   │   ├── price_source.py                     今天:CLOSE / TYPICAL
│   │   └── vwap_mode.py                        今天:SESSION / ROLLING
│   └── features/
│       ├── volume_weighted_average_price.py    今天:VWAP + 加權標準差
│       └── vwap_deviation.py                   今天:策略要用的無單位版本
├── infrastructure/charting/
│   └── plotly_vwap_chart_renderer.py           今天
├── entrypoints/vwap_command.py                 今天
└── tests/domain/features/
    └── test_volume_weighted_average_price.py   今天

驗收標準

六項全過才算完成:

  1. uv run pytest tests/domain/features/test_volume_weighted_average_price.py 全綠,包含前面列的四個邊界:加權平均不等於算術平均(100/200 配 1/99 要得到 199 而不是 150)、跨日重置(第二天第一根正好等於 200)、零成交量回 NaN、標準差為 0 時偏離回 NaN。滾動模式則要斷言它不認識日界。
  2. 精度那個測試要過:平移過的加權標準差與逐項展開的定義式誤差小於 1e-9,而沒平移的版本誤差至少大一百倍。這一項是今天最容易被當成裝飾而跳過的,但它守的是一個不會報錯的錯誤。
  3. uv run python -m quantbot.entrypoints.vwap_command --symbol BTC/USDT --market spot --timeframe 1m --start 2026-07-15 --end 2026-07-16 --mode session 印出 1,440 根、價格在 VWAP 之上約 45%,並產出 notebooks/day11-spot_BTCUSDT_1m-vwap_session.html
  4. 換成 --mode rolling --window 60 再跑一次,兩者的「偏離超過 1 個標準差」比例要明顯不同(實測 28.70% 對 53.29%)。這個差異是預期的結果,不是哪一邊算錯。
  5. 打開產出的 html:日內模式的 VWAP 線在 UTC 午夜斷開重來,通道在每天開頭幾根明顯偏窄;滾動模式的線連續不斷。
  6. uv run mypy quantbotuv run lint-imports 全過。

第 4 項是今天真正的重點。兩個數字都對,而它們差一倍——這說明「偏離 VWAP 兩個標準差」這句話在沒有指定累積方式之前不構成一個定義。

免責聲明:本文為程式與資料工程的技術分享,所有數字皆為教學範例,不構成投資建議;VWAP 是對已發生成交的描述,不預測後續走勢。

明天

今天的兩個特徵都在回答「價格在哪裡」——相對於平均成本的位置。明天問一個不一樣的問題:這段時間市場有多熱。

這個問題其實是今天第一節那個視角的延伸。既然時間軸與成交量軸是兩條不同的軸,那麼「同樣一分鐘裡裝進了多少成交」本身就是一個值得量的量。

Day 09 那兩根 K 線又要再出場一次。它們的開高低收與成交量幾乎一樣,成交筆數差 4.2 倍。所以「熱不熱」至少有三個角度(成交筆數、成交量、價格實際走了多少),而它們的答案會不一致。

明天 Day 12 會把這三個角度都做成標準化的分數,並且處理一個很容易寫成未來函數的地方:加密貨幣 24/7 不休市,但它有很明顯的時段節奏(實測最冷清與最熱鬧的鐘點差 2.71 倍)。要問「現在真的不尋常嗎」,就得跟同一個鐘點的歷史比,而那個比較只要寫成一行 groupby("hour").transform("mean"),就會把未來的資料算進基準裡。

也會補上 ATR——它是 Day 24 決定停損距離的依據,而它在這個專案裡不是 Indicator 而是 Feature,理由是 Day 04 那個基底類別的契約真的裝不下它。

Reference


上一篇
Day 10:掛單簿買賣兩邊不對稱代表什麼?用 Order Book Imbalance 量化短線壓力
系列文
量化交易入門:從 K 線到可組合的交易策略引擎12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言