配方集:照抄就能跑的完整腳本
所屬章節:入門 每一份都是實跑驗證過的完整腳本,貼上換參數就能執行 績效數字為實測值,資料日 2026-09-11
這頁在解決什麼
寫一支最簡單的單檔回測,資訊散在六頁裡:
backtest.md → daily_bars() 範本、為什麼不用 get_stock_ohlc
backtest-strategy.md → 標準骨架、on_init 放參數、暖身期
backtest-order.md → Condition.Global 七個參數
backtest-report.md → unit/capital/tax 陷阱
backtest-tools.md → self.Ind 每根都要呼叫
data-history.md → 日頻 1000 筆上限
這頁把它們拼好。 要改需求時再回去查對應那頁。
共用前置
import sys
sys.path.insert(0, r'<snowyowl_project 路徑>') # 用原始碼樹時才需要;pip 裝的不用
import pandas as pd
import snowyowl as so
api = so.login(type='google') # 或 so.login('user@x.com', 'pwd')
bt = api.bt
取日 K 的標準函式
日 K 一律從 api.MSMP 取,不要用 bt.Tools.get_stock_ohlc——
後者是 1 分 K resample,跟積木的日頻 sig 對不上,還多一次大量下載。
def daily_bars(api, symbol, adj=True):
"""回傳可直接餵 feed_data 的 df。adj=True 用還原價。"""
k = api.MSMP.日_K
p = 'adj_' if adj else ''
cols = {'開盤價': f'{p}Open', '最高價': f'{p}High', '最低價': f'{p}Low',
'收盤價': f'{p}Close', '成交量': f'{p}Volume' if adj else 'Volume'}
df = pd.DataFrame({zh: getattr(k, en)[symbol] for zh, en in cols.items()})
df.index.name = 'DateTime'
return df.dropna().reset_index() # ← dropna 必要,未上市期間是 NaN
要 adjclose 就整組都用
adj_*(含adj_Volume),不要混用還原價與未還原量。
成本速查
| 標的 | config |
|---|---|
| 台股個股(以股計) | fee=0.001425, tax_long=0, tax_short=0.003, unit=1 |
| 台股個股(以張計) | 同上但 unit=1000,qty 要換算成張數 |
| 台股 ETF(0050/0056…) | fee=0.001425, tax_long=0, tax_short=0.001, unit=1 |
| 台指期 | fee=500, tax_long=0.00002, tax_short=0.00002, unit=200, margin_long=0.13 |
| 美股 | fee=0, tax_long=0, tax_short=0, unit=1 |
台股只填 tax_short,tax_long 留 0——兩邊都填會把證交稅收兩次。
配方一:單檔技術指標回測(每次固定金額)
對應 sample.md 案例 1。0050 的 MA5/MA10 多頭排列買、空頭排列賣,每次 10 萬。
SYM, AMOUNT = '0050', 100_000
df = daily_bars(api, SYM, adj=True)
class MaCross(bt.Core):
def on_init(self): # 只放參數與狀態,下單會 RuntimeError
self.n_fast, self.n_slow = 5, 10
self.amount = AMOUNT
self.close_list = []
def on_bar(self, row):
self.close_list.append(row['Close']) # row 的 key 一律英文
fast = self.Ind.rolling_mean(self.close_list, self.n_fast)
slow = self.Ind.rolling_mean(self.close_list, self.n_slow)
self.char['ma5'], self.char['ma10'] = fast[-1], slow[-1] # 進 char 才上圖
if len(self.close_list) < self.n_slow:
return # 暖身期不下單
if fast[-1] > slow[-1] and self.global_qty == 0:
qty = self.amount / row['Close'] # ★ 股數,必須搭 unit=1
self.Condition.Global(True, qty, 0, self.PriceType.market_ioc, None, 'bk')
elif fast[-1] < slow[-1] and self.global_qty > 0:
self.Condition.Global(True, -self.global_qty, 0,
self.PriceType.market_ioc, None, 'bp')
core = MaCross()
core.feed_data(df, ['DateTime', '開盤價', '最高價', '最低價', '收盤價', '成交量'])
cfg = bt.config(name='MA5_10_0050', symbol=SYM,
fee=0.001425, tax_long=0, tax_short=0.001, unit=1) # ETF 稅率
bench = df.set_index('DateTime')['收盤價']
rp = bt.generate_report({cfg: core}, capital=1_000_000, benchmark=bench)
驗證段(每次回測都跑)
COL = 'Strategy' # ★ 單策略時欄名就是這個,不是 config.name
td = rp.trade_detail
gross, cost = (td['pnl'] + td['cost']).sum(), td['cost'].sum()
invest = rp.report.loc['Invest', COL]
print(f'毛損益 {gross:,.0f} / 成本 {cost:,.0f}(佔 Invest {cost/invest*100:.1f}%)')
print(f'期間 {rp.report.loc["StartDate",COL]} ~ {rp.report.loc["EndDate",COL]}')
print(f'曝險率 {(rp.daily["position"].abs()>0).mean()*100:.1f}%')
print(f'策略 {rp.report.loc["TotalReturns",COL]*100:.2f}% '
f'vs benchmark {rp.report.loc["TotalReturns","benchmark"]*100:.2f}%')
rp.dashboard(path='dashboard.html', view=None) # ★ 腳本裡要給 path
實測結果
期間 2022-08-15 ~ 2026-09-11(4.08 年,日頻 1000 筆上限)
策略 0050 B&H
TotalReturns +81.04% +292.84%
CAGR 16.31% 41.71%
MaxDrawdown -14.23% -27.48%
SharpeRatio 1.38 1.56
AnnualisedVol 11.38% 24.28%
毛損益 104,213 / 成本 20,644(佔 Invest 20.0%)
53 筆交易、勝率 48%、盈虧比 2.56、曝險率 61.3%
(2026-09 的輸出;2026-10 起 rp.report 的 MaxDrawdown 改名 MddRatio,另有 MddAmount 最大回撤金額。)
怎麼讀:總報酬輸很多,但 MDD 與波動都只有一半。 曝險率 61.3% 是關鍵——38.7% 的時間空手,而標的同期漲 3 倍。 這是結構問題,不是參數問題(實測掃 95 組 MA 參數,贏過 buy&hold 的是 0 組)。
配方二:積木訊號當進出場
積木選出的
sig是日頻矩陣,與 MSMP 日 K 天然對齊,不需 reindex/ffill。
SYM = '2330'
con = api.Block.con_tw('今日收盤價<>D日(周月)均線', period=20, table='日_K')
r = api.Block.screener_tw([con])
df = daily_bars(api, SYM, adj=True)
df['sig'] = df['DateTime'].map(r['sig'][SYM]).fillna(False)
class BlockStrategy(bt.Core):
def on_bar(self, row):
if row['sig'] and self.global_qty == 0:
self.Condition.Global(True, 1, 0, self.PriceType.market_ioc, None, 'bk')
elif not row['sig'] and self.global_qty > 0:
self.Condition.Global(True, -self.global_qty, 0,
self.PriceType.market_ioc, None, 'bp')
core = BlockStrategy()
core.feed_data(df, ['DateTime', '開盤價', '最高價', '最低價', '收盤價', '成交量'],
columns=['sig']) # ★ 額外欄位一定要宣告,否則 row 裡沒有
cfg = bt.config(name=f'block_{SYM}', symbol=SYM,
fee=0.001425, tax_long=0, tax_short=0.003, unit=1000)
rp = bt.generate_report({cfg: core}, capital=1_000_000)
選股結果不要直接當回測標的 —— 用今天選出的股票回測過去是 look-ahead, 等於事先知道誰會被選上。要嘛用固定清單,要嘛在策略內逐日重算。
配方三:多檔組合(共用資金池)
關鍵:
qty要填 0~1 的比例,capital才會生效。固定口數走向量化路徑,capital完全不參與計算。
SYMS = ['2330', '2317', '2454', '1301', '2882']
W = 1 / len(SYMS) # ★ 1/檔數 是安全上限,見下方說明
class PctStrategy(bt.Core):
def on_init(self):
self.n_fast, self.n_slow = 5, 20
self.close_list = []
def on_bar(self, row):
self.close_list.append(row['Close'])
fast = self.Ind.rolling_mean(self.close_list, self.n_fast)
slow = self.Ind.rolling_mean(self.close_list, self.n_slow)
if len(self.close_list) < self.n_slow:
return
if fast[-1] > slow[-1] and self.global_qty == 0:
self.Condition.Global(True, W, 0, self.PriceType.market_ioc, None, 'bk')
elif fast[-1] < slow[-1] and self.global_qty > 0:
self.Condition.Global(True, -self.global_qty, 0,
self.PriceType.market_ioc, None, 'bp')
core_dic = {}
for s in SYMS:
c = PctStrategy()
c.feed_data(daily_bars(api, s), ['DateTime','開盤價','最高價','最低價','收盤價','成交量'])
core_dic[bt.config(name=s, symbol=s, fee=0.001425,
tax_long=0, tax_short=0.003, unit=1000)] = c
rp = bt.generate_report(core_dic, capital=10_000_000)
print(rp.report['Strategy']) # ★ 只有這一欄是組合,其餘是「該檔獨佔全部資金」
rp.report 逐檔加總 ≠ Strategy。 其餘各欄是單獨視角。
為什麼是 1/檔數:capital 只累加已實現損益、不扣未平倉佔用的資金,
所以 N 檔同時進場時每檔都拿同一份 capital 的比例算口數,
weight × 同時持有檔數 > 1 就是隱性槓桿。
資金不夠時的誤導訊息:capital × weight 買不起一張(台股 1000 股)時,
floor 把口數夾成 0,全部被夾掉會拋
RuntimeError: make_daily: 每日損益全為 0——訊息說是版本問題,
實際上幾乎都是資金不夠。三種解法:提高 capital、減少檔數,
或改用零股 unit=1(檔數多、資金小時這才是正解,見配方五)。
配方四:期貨雙均線多空反手(台指期 TXF)
對應 sample.md 案例 3。60 分 K、快線上穿做多、下穿做空、多空反手、結算日強制出場。 期貨資料在
api.MSMP._fut_TXF(底線私有屬性),不在api.Data的 80 張表裡。
import numpy as np
df1 = api.MSMP._fut_TXF # DateTime, Open, High, Low, Close, TotalVolume, settlement
class DualMA(bt.Core):
def on_init(self):
self.n_fast, self.n_slow = 5, 20
self.tick_map = [(np.inf, 1)] # ★ 期貨最小跳動 1 點,不設會套股票規格
self.C = []
def on_bar(self, row):
self.C.append(row['Close'])
fast = self.Ind.rolling_mean(self.C, self.n_fast)
slow = self.Ind.rolling_mean(self.C, self.n_slow)
if len(self.C) <= self.n_slow:
return # 暖身期不下單
self.char['fast'], self.char['slow'] = fast[-1], slow[-1]
# ★ 「上穿」要比較前一根,不是單純比大小
cross_up = fast[-2] <= slow[-2] and fast[-1] > slow[-1]
cross_dn = fast[-2] >= slow[-2] and fast[-1] < slow[-1]
# ★ 多空反手:一次補齊到目標部位(±1 口),不必先平再開
self.Condition.Global(cross_up and self.global_qty < 1,
-self.global_qty + 1, 0,
self.PriceType.market_ioc, None, 'long')
self.Condition.Global(cross_dn and self.global_qty > -1,
-self.global_qty - 1, 0,
self.PriceType.market_ioc, None, 'short')
# 結算日強制平倉
self.Condition.Global(row['DateTime'] == row['Settlement'] and self.global_qty != 0,
-self.global_qty, row['Close'],
self.PriceType.compel_now, False, '結算')
core = DualMA()
core.set_tick_period(freq=60, need_settlement=True, # 60 分 K,日盤+夜盤
args=['DateTime','Open','High','Low','Close','TotalVolume'])
core.feed_tick_df(df1) # ★ 期貨走 feed_tick_df,不是 feed_data
cfg = bt.config(name='dualma', symbol='TXF', fee=500,
tax_long=0.00002, tax_short=0.00002, unit=200)
rp = bt.generate_report({cfg: core})
多空反手的寫法
-self.global_qty + 1 是「一次補齊到 +1 口」:手上是 -1 口時送 +2、空手時送 +1、
已經 +1 口時條件 global_qty < 1 不成立所以不送。不需要先平倉再開倉。
部位檢查要用 < 1 / > -1,不是 == 0——用 == 0 就只能空手時進場,反不了手。
實測結果(2,589,900 列 1 分 K → 60 分 K)
多空分開看損益,不要只看總和:
td = rp.trade_detail
for d in ('long', 'short'):
s = td[td['direction'] == d]
print(f'{d}: {len(s)} 筆, pnl {s["pnl"].sum():,.0f}, 勝率 {(s["pnl"]>0).mean()*100:.1f}%')
這組參數的空單是淨虧損的——總報酬為正純粹靠多單撐著。 只看總績效會誤以為「多空都做有效」。
期貨四個必做:tick_map=[(np.inf,1)]、fee=500(>=1 自動切換成每口固定金額)、
unit=200、結算日判斷用 row['DateTime'] == row['Settlement']。
要多週期(60 分進場 + 日線過濾)一定要走 set_tick_period(freq=[60,'D']) + feed_tick_df,
feed_data 不會觸發 on_bar2。
配方五:目標權重輪動(選股 + 月頻換股)
對應 sample.md 案例 2、5。文件到處說「要用目標權重不要固定金額」,這是實作。 小資金做多檔輪動一定要用零股,見下方對照。
TOP_N, CAPITAL = 20, 1_000_000
M = api.MSMP
# ── 1) 股池:排除 ETF / KY / DR ─────────────────────────
bad = set(M.list['etf']) | set(M.list['KY']) | set(M.list['DR'])
close = pd.DataFrame(M.日_K.adj_Close)
raw_close = pd.DataFrame(M.日_K.Close) # ★ 未還原價才是真實股價,門檻用它
vol = pd.DataFrame(M.日_K.Volume) # 單位是「張」
univ = [c for c in close.columns if c not in bad]
close, raw_close, vol = close[univ], raw_close[univ], vol[univ]
R = close.index
# ── 2) 可交易性遮罩(排名前先套)────────────────────────
turnover = (raw_close * vol * 1000).rolling(20).mean() # ★ Volume 是張,要 ×1000
tradable = (turnover.rank(axis=1, pct=True) > 0.4) & (raw_close >= 10)
# ── 3) 因子:月營收 YoY,用官方方法轉成公告日 ────────────
feat = pd.DataFrame(M.月_營收.YoY.index_str_to_date()).reindex(columns=univ)
feat = feat.reindex(feat.index.union(R)).ffill().reindex(R) # union→ffill→reindex
# ── 4) 換股日 = 營收公告日,每次選 top_n ─────────────────
pub = pd.DataFrame(M.月_營收.YoY.index_str_to_date()).index
score = feat.where(tradable) # ★ 先遮罩再排名,母體才一致
picks = {d: score.loc[d].dropna().nlargest(TOP_N).index.tolist()
for d in sorted(set(pub) & set(R)) if score.loc[d].notna().any()}
# ── 5) 攤成「每天該持有誰」的布林表 ──────────────────────
all_syms = sorted({s for v in picks.values() for s in v})
hold = pd.DataFrame(False, index=R, columns=all_syms)
cur = []
for d in R:
if d in picks:
cur = picks[d]
if cur:
hold.loc[d, [c for c in cur if c in hold.columns]] = True
# ── 6) 每檔一個 core,qty=1/N 走百分比路徑 ───────────────
W = 1 / TOP_N
class Rotate(bt.Core):
def on_bar(self, row):
if row['sig'] and self.global_qty == 0:
self.Condition.Global(True, W, 0, self.PriceType.market_ioc, None, 'bk')
elif not row['sig'] and self.global_qty > 0:
self.Condition.Global(True, -self.global_qty, 0,
self.PriceType.market_ioc, None, 'bp')
k = M.日_K
core_dic = {}
for s in all_syms:
df = pd.DataFrame({'開盤價': k.adj_Open[s], '最高價': k.adj_High[s],
'最低價': k.adj_Low[s], '收盤價': k.adj_Close[s],
'成交量': k.adj_Volume[s]}).dropna()
if len(df) < 100:
continue
df.index.name = 'DateTime'
df = df.reset_index()
df['sig'] = df['DateTime'].map(hold[s]).fillna(False)
if not df['sig'].any():
continue
c = Rotate()
c.feed_data(df, ['DateTime','開盤價','最高價','最低價','收盤價','成交量'],
columns=['sig'])
core_dic[bt.config(name=s, symbol=s, fee=0.001425 * 0.3, # 手續費 3 折
tax_long=0, tax_short=0.003, unit=1)] = c # ★ unit=1 = 零股
rp = bt.generate_report(core_dic, capital=CAPITAL,
benchmark=pd.DataFrame(M.日_K.adj_Close)['0050'].dropna())
print(rp.report['Strategy']) # ★ 只有這欄是組合
零股 vs 整股:小資金只能做零股
unit=1 是以股計價,也就是零股。這不是變通寫法——
小資金做多檔輪動在現實中本來就是買零股。實測對照(capital 100 萬、20 檔、W=1/20):
| 設定 | 結果 |
|---|---|
unit=1(零股) |
正常跑。單筆進場中位 53,031 元(≈ capital/20)、股數中位 1560 股、最小 120 股 |
lot_size='inf,0,floor,1000'(整股) |
掛掉,全期只成交 2 筆 |
整股版之所以掛掉:capital × 1/20 = 5 萬,買不起一張台股(動輒幾十萬),
floor 把口數夾成 0,然後拋出那個會誤導人的錯誤訊息:
它跟版本無關,就是資金不夠買一個單位。
| 你要的 | 設法 |
|---|---|
| 零股(1 股一取) | unit=1,lot_size 用預設 'inf,0,floor,1' |
| 整股(1000 股一取) | unit=1 + lot_size='inf,0,floor,1000',且 capital × W 要買得起一張 |
| 整張思考(qty 是張數) | unit=1000,qty 自己換算成張數 |
框架沒有「最低手續費」參數。
fee只有費率(<1)與每口固定金額(>=1)兩種模式, 無法表達券商零股常見的 20 元低消。小額零股的真實成本會比回測高, 單筆 5 萬 × 0.1425% × 3 折 ≈ 21 元剛好在低消邊緣,再小就會被低消吃掉。
實測結果(60 檔子集、月營收 YoY 單因子)
股池 2874 → 排除 ETF/KY/DR 後 2331
可交易遮罩:平均每日 1211 檔通過
換股 49 次,每次 20 檔,全期出現過 236 檔
TotalReturns +14.85% vs 0050 benchmark +312.82%
MaxPosition 638,979 / capital 1,000,000 ← capital 確實生效
毛損益 90,947 / 成本 25,348(佔 Invest 5.7%)
TotalTrades 122
大幅落後 0050,原因見配方六的 IC 分析—— 月營收 YoY 單因子的 IC 幾乎為零。這正是 how-to-ask.md 案例 2 「單因子選股輸給大盤」的完整重現。要改善得做多因子合成(sample.md 案例 5)。
MaxPosition < capital是確認capital有生效的指標。 若遠大於 capital,代表走了固定口數路徑,capital根本沒參與計算。
配方六:訊號驗證(IC 與分組單調性)
對應 sample.md 案例 4 的第 ④ 階。在花時間跑回測之前先做這個。
兩個一定要知道的陷阱
① 月頻/季頻的 index 是字串,用 index_str_to_date() 轉,不要自己接。
月_營收 的 index 是 '2026-M08'(營收所屬月份),不是公告日。
直接拿去跟日頻對齊就是未來函數——8 月營收要到 9/10 才公告。
MSMP 的矩陣有現成方法:
rev = api.MSMP.月_營收.YoY # index: '2026-M06', '2026-M07', '2026-M08'
rev_d = rev.index_str_to_date() # index: 2026-07-13, 2026-08-10, 2026-09-10
它做了三件事,自己接 INDEX['M'] 都做不到:
| 做了什麼 | 說明 |
|---|---|
| 月頻 → 次月 10 日 | 月營收法定公告日;12 月跨年處理好;再用 _to_trading_day() 順延到實際交易日(2026-M06 → 07-13 因為 7/10 是週末) |
| 季頻 → 財報公告日 | Q1→5/15、Q2→8/14、Q3→11/14、Q4→次年 3/21。2025-Q4 → 2026-03-23 |
| 自動遮蔽下市後的資料 | 依 delist_dates,下市後布林填 False、數值填 NaN。這是防倖存者偏差的關鍵 |
年頻('2026-Y')同樣支援。一律用它,不要自己拼日期。
② corrwith 會炸,要先 pd.DataFrame() 脫殼。
MSMP 回的是 ProboDataFrame(pd.DataFrame 的子類),它覆寫了 __mul__ / __sub__
等運算子去做自動 reshape。pandas 的 corrwith 內部會做 ldem * rdem,
一旦兩邊 index 型態不同就會走進 reshape 然後爆掉:
解法是脫殼成純 pandas:
一般的矩陣運算(相加、相除、比大小)要保留
ProboDataFrame, 自動對齊很好用。只有在呼叫corrwith這類 pandas 內建統計方法時才需要脫殼。
完整程式碼
import pandas as pd
close = pd.DataFrame(api.MSMP.日_K.adj_Close) # ★ 脫殼
fwd = close.shift(-20) / close - 1 # 未來 20 日報酬
R = close.index
# 特徵:月營收 YoY,轉成公告日後對齊到日頻
feat = pd.DataFrame(api.MSMP.月_營收.YoY.index_str_to_date())
feat = feat.reindex(feat.index.union(R)).ffill().reindex(R) # ★ union→ffill→reindex
# ① IC
ic = feat.corrwith(fwd, axis=1).dropna()
print(f'IC n={len(ic)}, mean={ic.mean():.4f}, IR={ic.mean()/ic.std():.3f}')
# ② 分組單調性(5 組)
q = feat.rank(axis=1, pct=True)
for i in range(5):
mask = (q > i/5) & (q <= (i+1)/5)
print(f' Q{i+1}: {fwd.where(mask).stack().mean()*100:+.2f}%')
實測結果(月營收 YoY)
怎麼讀:IC 接近零卻分組單調,代表這個因子有方向性但訊號很弱,
橫斷面相關係數被極端值稀釋了(月營收 YoY 有 +1,073,121% 這種除以零的值)。
單獨用它選前 20 檔會選到一堆財報異常的公司——這正是
how-to-ask.md 案例 2 「單因子 YoY 選股輸給 0050」的量化原因。
判讀標準:|IC| > 0.03 算有訊號;分組要單調才可信。
兩個都不過就別跑回測了,先換因子或做極端值處理。
配方七:參數尋優
def opt(n_fast=5, n_slow=10):
core = MaCross()
core.n_fast, core.n_slow = n_fast, n_slow # 覆寫 on_init 設的 self.xxx
core.feed_data(df, ['DateTime','開盤價','最高價','最低價','收盤價','成交量'])
return bt.generate_report({cfg: core}, capital=1_000_000)
best = bt.Tools.optimize(opt, {'n_fast': [3, 20, 1], 'n_slow': [10, 60, 5]}, times=50)
rp_best = opt(**best)
print(best, rp_best.report.loc['TotalTrades', 'Strategy']) # ★ 一定要看交易次數
三個警告:
- 每次呼叫都要建新的
core——core 有狀態,重用會累積前一次的部位。 - 評分是
CalmarRatio(CAGR / abs(MddRatio)),完全不看交易次數—— 可能挑出只交易 3 次剛好都對的參數。 - 尋優本身就是過度配適的來源。 至少把資料切前後兩段分別驗。
掃參數之前先看曝險率——低曝險 + 大多頭標的時,掃參數救不了,見配方一的結論。
前後兩段分別驗(sample 案例 3、4 都要求)
文件到處都說「至少切前後兩段」,做法是這樣:
td = rp.trade_detail
d = pd.to_datetime(td['entry_time']) # ★ 時間欄是字串,要先轉
mid = d.min() + (d.max() - d.min()) / 2
for label, mask in (('前段', d < mid), ('後段', d >= mid)):
s = td[mask]
print(f'{label}: {len(s)} 筆, pnl {s["pnl"].sum():,.0f}, '
f'勝率 {(s["pnl"]>0).mean()*100:.1f}%')
實測配方四那組雙均線參數:
兩段差距這麼大就不能只看總績效。 這組參數的獲利幾乎全來自後段, 前段是虧的——代表它吃的是某段特定行情,不是穩定的邊際優勢。
entry_time/exit_time都是strftime過的字串不是datetime64, 直接用.dt會失敗,一定要先pd.to_datetime()。
更嚴格的做法是只在前段尋優、拿後段驗證(真正的樣本外):
分段驗證不過的參數,optimize 給的分數再漂亮都不能用。
跑完一定要回答的三件事
不論哪個配方:
| 問題 | 怎麼算 | 為什麼 |
|---|---|---|
| 毛損益與成本佔比 | (td['pnl']+td['cost']).sum()、cost/Invest |
日頻策略常常毛損益為正、虧損全來自成本。看到負報酬就否定策略是誤判 |
| 回測期間實際多長 | rp.report.loc['StartDate'/'EndDate', COL] |
日頻硬上限 1000 筆(約 4.1 年),使用者以為的「近十年」拿不到 |
| 曝險率 | (rp.daily['position'].abs()>0).mean() |
判斷輸給 buy&hold 是結構問題還是參數問題 |
相關頁
- 怎麼問,才問得到你要的東西:把需求講清楚的三組案例
- 怎麼制定一個 plan:六階段研究流程
- 案例集:小白怎麼問回測:三個案例的完整追問清單
- 商品規格與績效報告:
unit/capital/tax_*、報表欄位定義 - 事件模型與策略撰寫:事件順序、
row欄位、狀態屬性 - 委託、成交價與風控:
PriceType、12 個成交價實測案例