jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Đầu tư Cổ phiếu · Phần 26 — Backtest không tự lừa mình

Thiết kế backtest point-in-time, mô phỏng tín hiệu và execution, xử lý bias, costs, out-of-sample, robustness và tiêu chí quyết định keep/revise/retire.

Một equity curve đẹp có thể được tạo bởi edge, may mắn, lỗi dữ liệu hoặc một trăm lần thử rồi chỉ giữ lần đẹp nhất. Backtest không phải máy chứng minh chiến lược đúng. Nó là thí nghiệm cố gắng làm chiến lược thất bại trước khi tiền thật làm việc đó.

Bailey và López de Prado mô tả backtest overfitting là nguồn của những “tối ưu” thống kê nhưng hoạt động kém ngoài đời. Vì vậy bài này ưu tiên audit trail hơn Sharpe đẹp.

1. Research contract trước khi chạy

RESEARCH ID: TPC-01-R001
Hypothesis:
Universe / market / period:
Signal timestamp:
Execution assumption:
Risk/exit rules:
Primary metric:
Secondary metrics:
Minimum sample:
In-sample / validation / test split:
Costs model:
Acceptance criteria:
Rejection criteria:
Parameters allowed to vary:
Number of trials budget:

Viết trước giúp phân biệt confirmatory test với exploration. Exploration được phép; chỉ cần không giả vờ kết quả sau khám phá là kiểm định độc lập.

2. Event timeline — xương sống chống nhìn trước

Với signal cuối ngày:

t close:      bar hoàn thành
t + latency:  indicator/signal được tính
t+1 open:     thời điểm sớm nhất order có thể tới thị trường
t+1 session:  fill/partial fill/cancel
future bars:  stop/trail/time exit theo thứ tự sự kiện

Mỗi trường dữ liệu phải có hai thời điểm:

  • effective_at: nó nói về kỳ nào.
  • known_at: thị trường/researcher biết nó lúc nào.

BCTC quý kết thúc 31/03 nhưng công bố 25/04 không được dùng ngày 31/03. Thành phần index công bố sau kỳ cũng vậy.

3. Dữ liệu đúng

Checklist universe

☐ Bao gồm mã hủy niêm yết/phá sản nếu thuộc universe lịch sử
☐ Thành phần index theo từng ngày, không dùng membership hôm nay
☐ IPO chỉ xuất hiện sau listing và đủ lookback
☐ Suspended/limited trading được phản ánh
☐ Symbol change/merger được map đúng

Giá điều chỉnh

Giá raw cần cho execution thực tế; giá adjusted tiện tính return qua split/dividend. Không được trộn adjusted entry với raw stop. Lưu riêng:

raw OHLCV
adjustment factor
adjusted series
corporate action table

Dữ liệu bẩn

Phát hiện high < low, volume âm, return cực đoan, missing bar, duplicated timestamp. Không tự winsorize mọi outlier: một số là crash thật. Mọi correction có log nguồn, ngày và lý do.

4. Bias ledger

BiasCách nó lọt vàoKiểm soát
Look-aheadEntry tại close dùng để tạo signalLag và event timeline
SurvivorshipChỉ test mã còn sốngPoint-in-time universe
SelectionChọn thị trường/giai đoạn đẹpPre-register phạm vi; multi-market
Multiple testingThử nhiều biến thể, báo một biến thểTrial ledger; holdout; correction
Data snoopingXem test set nhiều lầnTest set khóa; tạo test mới nếu mở
Fill biasLimit chạm low là coi khớp đủQueue/volume/priority assumption
Cost omissionP&L grossFees, spread, slippage, impact, tax
DelistingBỏ return cuốiEvent/delist treatment
RebalancingDùng weight chưa thể biếtTimestamp và execution lag

Bias ledger phải nằm cạnh report, không ở trí nhớ.

5. Backtest engine tối thiểu

for each trading_day t:
  load only data known by decision_time(t)
  update universe and regime
  process exits and corporate events by declared priority
  create signals eligible at t
  generate orders for earliest actionable time
  simulate fills with volume/price constraints
  recompute cash, positions, stops, open risk, heat
  charge all costs
  save immutable event log and daily equity

Thứ tự intrabar

Daily bar có high và low nhưng không nói cái nào xảy ra trước. Nếu cả target và stop cùng nằm trong bar, không được luôn chọn target. Chọn policy:

  • Conservative: stop trước.
  • Dùng intraday data để tái dựng.
  • Đánh dấu ambiguous và báo khoảng kết quả.

Policy phải áp dụng đều.

6. Execution và costs

P&L thật:

Net P&L = Gross P&L
          - commissions
          - exchange/broker fees
          - taxes/levies nếu áp dụng
          - spread cost
          - slippage
          - market impact
          - borrow/funding cost nếu có

SEC lưu ý limit order có execution uncertainty và adverse selection; thống kê execution cần xét effective/realized spread, time-to-execution và loại order, không chỉ giá cuối ngày (SEC order-execution disclosure).

Ba kịch bản cost

ScenarioSpread/slippageDùng để
Optimistic but possibleEstimate thấp có bằng chứngUpper bound
BaseMedian/conditional estimateMain report
Stress2–3× base, liquidity xấuFragility test

Nếu edge biến mất ở tăng chi phí nhỏ, capacity thấp hoặc execution model là phần cốt lõi của “edge”.

Participation cap

shares_filled ≤ participation_rate × observed_volume

Không dùng volume cả ngày để giả định khớp ở open mà không có intraday profile. Với daily research, dùng cap thận trọng và capacity bands.

7. Position accounting

Mỗi fill tạo event, không chỉ một trade row:

ORDER_SUBMITTED
PARTIAL_FILL
FILL
STOP_UPDATED
DIVIDEND/CORPORATE_ACTION
EXIT_FILL
CANCEL/REJECT

Kiểm tra invariant hằng ngày:

ending_cash = beginning_cash + sells - buys + income - costs
equity = cash + market_value_positions
position_qty = prior_qty + buys - sells ± corporate_actions

Nếu không reconcile, performance report chưa đáng tin.

8. Chia mẫu theo thời gian

Random split phá cấu trúc thời gian. Dùng:

TRAIN/RESEARCH: phát triển giả thuyết và khoảng tham số
VALIDATION: chọn giữa số ít specification đã định
TEST/HOLDOUT: mở một lần để đánh giá cuối
FORWARD/PAPER: dữ liệu đến sau, gần thực tế nhất

Walk-forward:

Train 2012–2016 → Test 2017
Train 2013–2017 → Test 2018
...

Nếu label/position overlap, cần purge/embargo phù hợp để thông tin của event sát biên không rò giữa folds.

9. Metric pack — không có một con số thống trị

Return và risk

CAGR = (ending_equity / starting_equity)^(1/years) - 1
Drawdown_t = equity_t / running_peak_t - 1
Max drawdown = min(drawdown_t)
Volatility = std(period returns) × annualization factor
Sharpe = annualized excess mean / annualized volatility

Sharpe giả định và độ tin cậy phụ thuộc dữ liệu; autocorrelation, skew, tails và sample nhỏ làm nó dễ bị hiểu quá mức.

Trade distribution

Expectancy_R = win_rate × avg_win_R - loss_rate × avg_loss_R
Profit factor = gross_profit / abs(gross_loss)
Payoff ratio = avg_win / abs(avg_loss)

Win rate đứng một mình vô nghĩa. 80% thắng vẫn tệ nếu thắng +0,2R và thua -2R.

Operational

  • Turnover và trades/year.
  • Exposure/time in market.
  • Capacity theo liquidity band.
  • Slippage median/P90.
  • Rejection/partial-fill rate.
  • P&L concentration ở top 5 trades.

Uncertainty

Báo confidence interval hoặc bootstrap phù hợp chuỗi thời gian, không chỉ point estimate. Với ít trade, kết luận phải yếu tương ứng.

10. Worked example TPC-01

Giả sử kết quả minh họa, không phải dữ liệu thật:

In-sampleValidationHoldout
Trades420135142
Expectancy gross+0,24R+0,13R+0,09R
Costs-0,07R-0,08R-0,09R
Expectancy net+0,17R+0,05R0,00R
Max DD-11R-8R-14R

Kết luận không phải “700 trades nên chắc chắn”. Edge net không tồn tại ở holdout; cần fail/revise, không chọn lại tham số trên holdout rồi gọi đó là test.

Phân rã cho thấy:

Liquid large-cap: +0,08R net
Illiquid tail:    -0,19R net
Risk-on:          +0,11R net
Caution:          -0,07R net

Một giả thuyết v0.2 hợp lý là universe/permission khác, nhưng phải có lý do kinh tế và test trên dữ liệu mới. Không xóa nhóm thua chỉ vì nó xấu.

11. Robustness tests

Parameter sensitivity

Thay volume ratio 1,1/1,2/1,3; pullback 2–4/2–5/2–6; stop buffer 0/0,1/0,2 ATR. Tìm plateau, không tìm đỉnh.

Ablation

Loại từng filter:

Base
- regime filter
- volume condition
- trend condition
- time stop

Nếu một filter không đóng góp hoặc chỉ giảm sample để Sharpe đẹp, bỏ nó.

Subperiod và regime

Đánh giá bull, bear, range, shock, early/late sample. Một chiến lược chỉ lời ở một năm nhờ hai trade phải mang nhãn như vậy.

Placebo

Randomize entry trong cùng universe/regime hoặc shift signal. Nếu chiến lược không khác placebo đáng kể, “edge” có thể chỉ là market beta.

Cost/capacity stress

Tăng spread/slippage, giảm participation cap, trì hoãn entry một bar. Edge mong manh với delay nhỏ khó triển khai.

12. Multiple testing ledger

trial_id | date | hypothesis | parameters | sample_seen | result | decision
001      | ...  | base       | ...        | train       | ...    | keep
002      | ...  | vol 1.1    | ...        | train       | ...    | reject
...

“Tôi chỉ chạy một chiến lược” là sai nếu bạn đã thử 50 filter trong đầu/code. Ghi mọi trial đủ ảnh hưởng quyết định. Holdout đã xem trở thành validation; muốn kiểm định mới cần dữ liệu tương lai hoặc thị trường độc lập.

13. Acceptance gate

Một gate minh họa:

DATA
☐ Point-in-time universe, corporate actions, no reconciliation errors

IMPLEMENTATION
☐ Independent rule review; unit tests; ambiguous bars handled

PERFORMANCE
☐ Net expectancy > 0 ở đa số walk-forward windows
☐ Không phụ thuộc vài outlier
☐ Drawdown nằm trong risk mandate

ROBUSTNESS
☐ Parameter plateau
☐ Survives stressed costs/delay
☐ Sensible regime/subperiod behavior

GOVERNANCE
☐ Trial ledger complete
☐ Holdout untouched until final
☐ Reproduction from config + data version

Kết quả cuối chỉ có bốn nhãn:

  • KEEP FOR PAPER: qua gate, chưa phải live.
  • REVISE: có giả thuyết sửa cụ thể; tạo version mới.
  • REJECT: evidence chống lại thesis.
  • INCONCLUSIVE: sample/data chưa đủ.

14. Báo cáo backtest một trang

Research ID / code commit / data version
Question and pre-registered criteria
Universe and exact timestamps
Signal → order → fill → exit timeline
Costs and capacity model
IS / validation / holdout / walk-forward table
Equity, drawdown and monthly-return distributions
Trade R distribution, MFE/MAE, P&L concentration
Regime/subperiod/parameter/ablation results
Bias ledger and known limitations
Trial count and discarded variants
Decision: keep/revise/reject/inconclusive
Next evidence required

15. Bài kiểm tra

  1. Signal dùng closing price và backtest mua đúng closing price: look-ahead/execution bias trừ khi chứng minh có thể tính và gửi trước close với dữ liệu phù hợp.
  2. Universe là VN30 hiện tại cho dữ liệu 10 năm: survivorship/membership bias.
  3. Limit buy bằng daily low nên coi khớp: không đủ; thiếu path, queue và volume.
  4. Thử 200 tham số, chọn tốt nhất rồi test trên chính kỳ đó: multiple testing/overfit.
  5. Gross expectancy +0,08R, stressed cost -0,10R: không triển khai theo assumptions này.

Nguồn chuẩn và đọc thêm

Kết luận

Backtest đáng tin không bắt đầu bằng code nhanh; nó bắt đầu bằng timeline đúng và kết thúc bằng bằng chứng có thể tái tạo. Equity curve là một output. Bias ledger, trial history, costs, capacity và holdout mới cho biết output đó đáng tin đến đâu.

Phần 27 đi sâu vào đoạn thường bị một backtest nến ngày giấu mất: order book, spread, queue, fill, adverse selection, market impact và transaction-cost analysis.