國安基金進場後持有正2
平常持有 0050,國安基金宣布進場(啟動日)後 250 個交易日改持有 00631L(元大台灣50正2),期滿換回 0050。2014 年至 2026 年台股回測年化報酬 38.6%、日夏普 1.37、最大回撤 -40.3%;同視窗 0050 含息年化 20.3%、日夏普 1.07。2014 年後國安基金只進場 4 次,樣本很少;若進場後市場繼續崩跌,正2 的跌幅會比 0050 深很多。
投資警語:本文僅供教學參考,不構成投資建議。過去績效不代表未來表現,投資有風險。
回測績效
- 年化報酬率
- 38.6%
- 日夏普
- 1.37
- 最大回撤
- -40.3%
- 總報酬率
- 4916%
- 平均持股
- 1.0 檔
回測含手續費 0.1425% 與賣出證交稅 0.3%(finlab 套件 sim() 預設成本),日夏普未扣無風險利率。策略淨值自資料可得日(2014-10-31)起算。
淨值曲線(對數座標)
國安基金進場後持有正20050 買進持有(含息)
與 0050 買進持有(含息)對照
| 對象 | 年化報酬率 | 日夏普 | 最大回撤 | 統計視窗 |
|---|---|---|---|---|
| 國安基金進場後持有正2 | 38.6% | 1.37 | -40.3% | 2014-10-31 〜 2026-10-30 |
| 0050 買進持有(含息) | 20.3% | 1.07 | -34.0% | 2014-10-31 〜 2026-10-30 |
完整策略程式碼
安裝 finlab 套件後即可直接執行重現(finlab.login() 會自動引導登入,不需手動填 token)。想用 AI 輔助安裝與跑第一次回測,可以走安裝教學。
展開程式碼(strategy.py)
"""國安基金進場後一年改抱正 2(/blog/national-stabilization-fund-00631l-strategy 與同名策略頁的數字來源)。
規則:
- 平常持有元大台灣50(0050)
- 國安基金宣布進場(national_security_fund 的「啟動時間」)起 250 個交易日,改持有元大台灣50正2(00631L)
- 訊號日的下一個交易日收盤換股,sim() 預設手續費 0.1425%、證交稅 0.3%
- 從 00631L 上市的 2014-10-31 起算;2014 年以後國安基金進場 4 次(2015、2020、2022、2025),樣本很少
統計口徑:日夏普 = 日報酬平均 / 標準差 × √252,不扣無風險利率;
0050 含息 = etl:adj_close 還原價買進持有,與策略同一段期間、不扣成本。
三項驗證(9 組鄰近參數、前後兩段、成本對照)的程式在
https://finlab.finance/blog/taiwan-market-timing-signals-backtest/research.py。
執行後會在目前目錄輸出 data.csv(淨值曲線)與 metrics.json(統計)。
pip install finlab
python strategy.py
finlab 需要資料時會自動引導登入,不需要在程式裡填 token。
"""
import json
import pandas as pd
import finlab
from finlab import data
from finlab.backtest import sim
STRATEGY_NAME = "國安基金進場後持有正2"
START = "2014-10-31"
HOLD_DAYS = 250 # 國安基金進場後持有正 2 的交易日數
def build_report():
dates = data.get("price:收盤價").loc[START:].index
nsf = data.get("national_security_fund")
# 國安基金啟動日起 HOLD_DAYS 個交易日為 True
leveraged = pd.Series(False, index=dates)
for start in pd.to_datetime(nsf["啟動時間"]):
if start >= dates[0]:
i = dates.searchsorted(start)
leveraged.iloc[i:i + HOLD_DAYS] = True
position = pd.DataFrame({
"00631L": leveraged.astype(float),
"0050": (~leveraged).astype(float),
})
return sim(position, upload=False, name=STRATEGY_NAME)
def curve_stats(curve: pd.Series) -> dict:
curve = curve / curve.iloc[0]
daily = curve.pct_change().dropna()
years = (curve.index[-1] - curve.index[0]).days / 365.25
return {
"cagr": curve.iloc[-1] ** (1 / years) - 1,
"daily_sharpe": daily.mean() / daily.std() * 252 ** 0.5,
"max_drawdown": (curve / curve.cummax() - 1).min(),
"total_return": curve.iloc[-1] - 1,
}
def rounded(stats: dict) -> dict:
return {k: round(float(v), 6) for k, v in stats.items()}
def main() -> None:
finlab.login()
adj = data.get("etl:adj_close")
report = build_report()
creturn = report.creturn.loc[START:].dropna()
bench = adj["0050"].ffill().loc[creturn.index[0]:creturn.index[-1]]
lev = adj["00631L"].ffill().loc[creturn.index[0]:creturn.index[-1]]
strategy_stats = curve_stats(creturn)
strategy_stats["avg_holdings"] = 1.0
bench_stats = curve_stats(bench)
lev_stats = curve_stats(lev)
start, end = creturn.index[0].date(), creturn.index[-1].date()
print(f"期間 {start} 至 {end}")
for label, s in [("策略", strategy_stats), ("0050 含息", bench_stats), ("00631L 買進持有", lev_stats)]:
print(f"{label}:年化 {s['cagr']:.2%},日夏普 {s['daily_sharpe']:.2f},最大回撤 {s['max_drawdown']:.2%}")
curves = pd.DataFrame({
STRATEGY_NAME: creturn / creturn.iloc[0],
"0050含息": bench / bench.iloc[0],
"00631L買進持有": lev / lev.iloc[0],
})
curves.index.name = "date"
curves.round(6).to_csv("data.csv")
metrics = {
"window": f"{start}/{end}",
"star": STRATEGY_NAME,
"variants": {STRATEGY_NAME: rounded(strategy_stats)},
"benchmark_0050": rounded(bench_stats),
"buy_hold_00631L": rounded(lev_stats),
"run_date": str(pd.Timestamp.today().date()),
"finlab_version": finlab.__version__,
"generated_by": "static/blog/national-stabilization-fund-00631l-strategy/strategy.py",
}
with open("metrics.json", "w", encoding="utf-8") as f:
json.dump(metrics, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
main()
在 Studio 裡用同一套真實資料調整條件、重跑回測。