還原股價要用向前復權還是向後復權?回測該選哪個?

回測用向後復權(後復權)。差別在錨點:向前復權以最近價格為基準往回調,所以每發生一次除權息,整段歷史價格就被改寫一次——同一份回測今天跑和下個月跑會得到不同數字。向後復權以最早價格為錨,新事件只往後加,歷史不變,回測才可重現。

還原股價

把除權、除息、分割造成的價格跳空接回去,讓不同時間的股價可以互相比較。

先看清楚差在哪:錨點不同,不是精度不同

  • 向前復權(前復權)=以最近一根為錨往回調。每次新的除權息發生,之前所有歷史價格都會被重算一次。
  • 向後復權(後復權)=以最早一根為錨往後調。新事件只影響它之後的價格,已經算好的歷史不動。
  • 兩者畫出來的報酬率曲線形狀一樣,差別在「歷史數字會不會變」。多數看盤軟體顯示前復權,因為它讓最新價格等於實際成交價,看盤方便。

為什麼回測必須用後復權

前復權的歷史是浮動的。你在 1 月跑出一組進出場點與績效,3 月那檔除息一次,同一份程式碼再跑一次,1 月那段的價格已經跟當時不同了——訊號可能沒觸發、停損可能沒打到。回測最基本的要求是可重現,前復權違反的正是這一點。後復權沒有這個問題:新事件只往後加。

還原價不是你能成交的價格

不論哪種復權,還原價都是為了「跨時間比較」而算出來的數字,不是當天市場上真的能買到的價格。用它算報酬率沒問題,但如果你的回測要模擬實際成交、滑價或漲跌停限制,那幾個判斷要回到原始價格去做。

我們供應的是原始價 + 官方調整因子

這裡沒有一支「算好的還原股價」端點。供應的是兩件事:twse-daily-price 的原始開高低收,以及 price-enhanced 的官方除權除息調整因子(來源 TWSE TWT49U)。後復權由你自己算——多一個步驟,但你完全知道每一個數字是怎麼來的,也可以自行驗證。

兩支端點的名字不一樣,連查詢參數都不一樣

  • twse-daily-price:查詢參數用 symbol,回應欄位是 symbol 與 date。
  • price-enhanced:代號參數用 ticker,日期區間用 date_from / date_to,回應欄位是 ticker 與 trade_date。
  • 送錯名字的後果不一樣:對這支送 symbol 會直接回 422,但送 start_date / end_date 不會報錯——它會被靜默丟掉,回給你未過濾的全部歷史。實測用 2020 的區間查,送 start_date/end_date 回了 45 列、橫跨 2003 到 2026;送 date_from/date_to 才正確回 4 列。
  • 所以兩邊 join 起來時,鍵名要各自對應,不能假設同名。這些是實際打過的結果,不是文件上的宣告——值得自己打一次確認。
import requests

HEADERS = {"X-API-Key": "sk_live_你的金鑰"}
BASE = "https://api.twmarketdata.com/v2/datasets"

def rows_of(payload):
"""兩種信封都接:價格端點回 rows,調整因子端點回 envelope.data(皆為實測)。"""
return payload.get("rows") or payload.get("envelope", {}).get("data", [])

factors = rows_of(requests.get(
f"{BASE}/price-enhanced",
headers=HEADERS,
# 這支的參數名跟價格端點都不一樣:代號是 ticker(不是 symbol),
# 日期區間是 date_from / date_to(不是 start_date / end_date)。
params={"ticker": "2330", "date_from": "2024-01-01", "date_to": "2026-07-31"},
timeout=20,
).json())

for f in factors:
print(f["trade_date"], f["event_type"],
"factor", f["factor"],
"事件前收盤", f["pre_event_close"],
"參考價", f["reference_price"])

先驗證因子的方向,再拿它算

在實際回應上,factor 等於 reference_price ÷ pre_event_close(例:參考價 5.84 ÷ 事件前收盤 5.93 = 0.98482293,與該列的 factor 完全相同)。但別只因為這裡這樣寫就相信——每一列都同時給了這兩個欄位,你可以自己對一列做除法確認,再套到整段歷史。

# 用資料自己驗證因子方向:參考價 ÷ 事件前收盤,跟 factor 對照for f in factors[:3]:    pre, ref = f["pre_event_close"], f["reference_price"]    if pre and ref:        print(f["trade_date"], f["event_type"],              "ref/pre =", round(ref / pre, 6),              "| factor =", f["factor"])

算後復權:累乘,且只往後影響

確認方向之後,後復權就是把每個事件的因子累乘上去,套用在事件之後的價格上。錨點放在序列最早那一根,所以新的除權息只會加在尾端,已經算過的歷史不會變。

prices = rows_of(requests.get(    f"{BASE}/twse-daily-price",    headers=HEADERS,    params={"symbol": "2330", "start_date": "2024-01-01", "end_date": "2026-07-31"},    timeout=20,).json()) # 以最早一根為錨,事件因子累乘後只套用到該事件之後的交易日events = sorted(factors, key=lambda f: f["trade_date"])cumulative, applied = 1.0, []for row in sorted(prices, key=lambda r: r["date"]):    while events and events[0]["trade_date"] <= row["date"]:        cumulative *= float(events.pop(0)["factor"])    applied.append({**row, "close_back_adjusted": row["close"] * cumulative}) for row in applied[-3:]:    print(row["date"], "原始", row["close"], "後復權", round(row["close_back_adjusted"], 2))

沒有因子的日子,不會被假裝成有

價格資料每一列都帶 price_method 與 price_confidence,告訴你這個數字是怎麼來的、可信度如何。沒有調整因子的期間就是沒有,不會拿原始收盤價冒充還原價——序列被補得漂亮,回測就會騙你。

我們沒有的東西,先說清楚

  • 沒有一支「算好的還原股價」端點。供應的是原始價與官方調整因子,後復權要你自己算(上面的程式碼就是)。
  • 沒有即時報價。這裡是歷史與盤後資料。
  • 調整因子來自 TWSE 的除權除息資料,涵蓋範圍以資料集頁標示為準,不做推估補值。

常見問題

1為什麼看盤軟體都用前復權?
因為看盤要的是「現在這根等於我真的能買到的價格」,前復權以最近價為錨,剛好滿足這件事。它是為了看盤方便,不是為了回測可重現——兩個目的不同,選擇也就不同。
2後復權的價格看起來跟市價差很多,正常嗎?
正常。後復權以最早一根為錨,累積多年的除權息之後,尾端數字會明顯高於當時的市價。它本來就不是拿來看價位的,是拿來算跨期報酬的。
3含息還是不含息?
看你把哪些 event_type 的因子算進去。要含息報酬就把除息事件一起累乘;只想看價格變化就只算除權與分割。因為因子是你自己套用的,這個選擇握在你手上。
4這跟避免前視偏誤有關係嗎?
有,而且是同一類問題:兩者都是「歷史被事後改掉」。前復權讓歷史價格隨新事件變動,前視偏誤讓歷史資料含有當時不存在的資訊。回測要成立,歷史就必須是當時那個歷史。

想自己查這些資料?免費註冊

相關文章