更新於 2024/10/22閱讀時間約 15 分鐘

加速防禦雙動能投資法

主要是讀了99趴大大的動能致富一書簡單寫的筆記:

加速防禦雙動能策略是一個有效的風險管理方法,透過結合相對動能和絕對動能兩種方式,讓投資者在市場上漲時能夠抓住機會,而在市場下跌時則能降低風險,從而達到「進可攻,退可守」的效果。

雙動能策略的基礎的核心原理是「強者恆強」,即過去表現好的資產未來有可能繼續表現良好。雙動能策略運用了兩個概念:

  • 相對動能:比較不同資產在過去一段時間的表現,選擇表現較好的進行投資。例如,投資者可以比較美股 ETF(如 SPY/VOO)和國際小型股 ETF(如 SCZ)的報酬率,選擇近一個月、三個月或六個月報酬率平均的表現較佳的資產進行配置,會選擇國際小型股(如SCZ)的邏輯是因為和SPY/VOO的走勢相關係數較低。
  • 絕對動能:當市場處於下跌趨勢時,避免投資表現不佳的資產,將資金轉向債券或現金等避險資產(例如 TLT,美國長期國債 ETF),藉以減少下行風險。

整個運作方式如下:

  1. 篩選表現較佳的資產:根據近一個月、三個月和六個月的報酬率平均,選擇表現較好的股票資產(例如 SPY 或 SCZ)。
  2. 判斷市場情況:當股市下行時,絕對動能策略會自動判斷市場趨勢。如果選擇的股票資產表現不佳,資金會轉向避險資產(如 TLT),以保護投資組合免受市場波動的影響。
  3. 加入現金避險選項:如果避險資產如債券的表現也不理想,那麼策略可以進一步轉向現金,這是市場出現極端風險時的最後防線。


為什麼加速防禦雙動能有效?

這種策略之所以有效,主要有兩個原因:

  • 動態調整資產配置:加速防禦雙動能策略能夠根據市場狀況靈活調整資產配置,在市場上漲時抓住機會,而在市場下跌時轉向債券或現金,降低損失。
  • 降低投資組合波動性:單純持有股票(如 SPY)在市場回調時可能會經歷較大損失,防禦雙動能策略通過債券或現金的避險,可以在市場不穩定時保持穩定的資本保護。


儘管防禦雙動能策略在風險管理方面具有顯著優勢,但也有一些潛在的缺點:

  • 牛市中可能錯過增長:當市場快速上升時,如果過早轉向避險資產,可能會錯過一些市場的上升潛力。
  • 依賴過去數據:動能策略依賴於過去的資產表現來進行投資決策,但過去的表現未必能夠保證未來的回報,在市場快速反轉時策略可能無法立即做出反應。
  • 金融市場對事件的反應速度愈來愈快:報酬率計算權動可能要適時調整,不然會跟不上市場的變化。


防禦雙動能策略是一個簡單、有效的投資方法,特別適合希望在市場不穩定時保護資本的投資者,通過靈活調整股票與避險資產的配置,這種策略能夠在不同的市場環境中達到較好的風險管理效果,相較於單純的 Buy-and-Hold 策略,防禦雙動能策略雖然可能在牛市中略顯保守,但其在熊市中的防禦能力使得它成為一個值得考慮的長期投資策略。


基於查證精神用 Portfolio Visualizer 驗證看看策略的有效性,把時間拉到2024之後…表現比Buy and Hold VOO差,呃……

ANYWAY,分享一下加速防禦雙動能的判斷小程式(每個月初執行一次)

import yfinance as yf
import pandas as pd
import sqlite3
from datetime import datetime, timedelta

# 設定 ETF 代號
symbols = ['VOO', 'SCZ', 'TLT']

# 建立 SQLite 連接
conn = sqlite3.connect('etf_data.db')
c = conn.cursor()

# 建表格(如果尚未存在)儲存每月的收盤價
c.execute('''
CREATE TABLE IF NOT EXISTS etf_monthly_close (
symbol TEXT,
date TEXT,
close_price REAL
)
''')

# 抓取 ETF 的歷史資料(過去 7 個月)
def fetch_and_store_data(symbols, conn):
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=7*30)).strftime('%Y-%m-%d') # 抓取過去7個月的數據

for symbol in symbols:
#print(f"正在取得 {symbol} 的資料...")
# 抓取數據
data = yf.download(symbol, start=start_date, end=end_date)
if data.empty:
print(f"未能取得 {symbol} 的資料")
#else:
#print(f"成功取得 {symbol} 的資料:\n{data.tail()}")

data.reset_index(inplace=True)
data['Date'] = pd.to_datetime(data['Date'])

# 找到每個月最後一個交易日的資料
monthly_data = data.groupby(data['Date'].dt.to_period('M')).tail(1)

#print(f"{symbol} 每月最後一個交易日的資料:\n{monthly_data}")

# 檢查資料庫中是否已有該月份的資料,沒有的話才寫入
for _, row in monthly_data.iterrows():
c.execute('''
SELECT * FROM etf_monthly_close
WHERE symbol = ? AND date = ?
''', (symbol, row['Date'].strftime('%Y-%m-%d')))
result = c.fetchone()

if result is None:
# 插入新的數據
c.execute('''
INSERT INTO etf_monthly_close (symbol, date, close_price)
VALUES (?, ?, ?)
''', (symbol, row['Date'].strftime('%Y-%m-%d'), row['Close']))
#print(f"插入 {symbol} 的 {row['Date'].strftime('%Y-%m-%d')} 資料")

conn.commit()

# 計算報酬率的函數
def calculate_returns(df, symbol):
df_symbol = df[df['symbol'] == symbol].copy()
df_symbol.set_index('date', inplace=True)

# 計算 1 個月、3 個月、6 個月報酬率
df_symbol['month_1_return'] = df_symbol['close_price'].pct_change(1)
df_symbol['month_3_return'] = df_symbol['close_price'].pct_change(3)
df_symbol['month_6_return'] = df_symbol['close_price'].pct_change(6)

# 刪除 NaN
df_symbol.dropna(inplace=True)

return df_symbol

# 取得資料並存入資料庫
fetch_and_store_data(symbols, conn)

# 從資料庫中讀取所有 symbol 的資料
query = '''
SELECT symbol, date, close_price
FROM etf_monthly_close
ORDER BY symbol, date
'''
df = pd.read_sql(query, conn)
df['date'] = pd.to_datetime(df['date'])

# 確認已經讀取了 VOOSCZTLT 的資料
#print(f"從資料庫讀取的資料:\n{df}")

# 檢查每個 symbol 的資料是否足夠計算報酬率
for symbol in symbols:
symbol_data = df[df['symbol'] == symbol]
#print(f"{symbol} 的資料量: {len(symbol_data)} 個資料點")
#print(symbol_data)

# 計算每個 ETF 的報酬率
voo_returns = calculate_returns(df, 'VOO')
scz_returns = calculate_returns(df, 'SCZ')
tlt_returns = calculate_returns(df, 'TLT')

# 計算 VOOSCZTLT 的報酬率平均值(1 個月、3 個月、6 個月的平均)
voo_avg_return = voo_returns[['month_1_return', 'month_3_return', 'month_6_return']].mean(skipna=True).mean()
scz_avg_return = scz_returns[['month_1_return', 'month_3_return', 'month_6_return']].mean(skipna=True).mean()
tlt_avg_return = tlt_returns[['month_1_return', 'month_3_return', 'month_6_return']].mean(skipna=True).mean()

# 檢查報酬率是否有 NaN
print("VOO 的報酬率:")
print(voo_returns[['month_1_return', 'month_3_return', 'month_6_return']])

print("SCZ 的報酬率:")
print(scz_returns[['month_1_return', 'month_3_return', 'month_6_return']])

print("TLT 的報酬率:")
print(tlt_returns[['month_1_return', 'month_3_return', 'month_6_return']])


# 比較 VOOSCZ 的平均報酬率
if not voo_returns.empty and voo_avg_return > scz_avg_return:
if voo_avg_return > 0:
result = 'VOO'
else:
if not tlt_returns.empty and 'month_1_return' in tlt_returns.columns and not tlt_returns['month_1_return'].isna().all():
if tlt_returns['month_1_return'].iloc[-1] > 0:
result = 'TLT'
else:
result = 'CASH'
else:
result = 'CASH'
else:
if scz_avg_return > 0:
result = 'SCZ'
else:
if not tlt_returns.empty and 'month_1_return' in tlt_returns.columns and not tlt_returns['month_1_return'].isna().all():
if tlt_returns['month_1_return'].iloc[-1] > 0:
result = 'TLT'
else:
result = 'CASH'
else:
result = 'CASH'

# 回傳結果及報酬率
print(f"VOO 平均報酬率: {voo_avg_return:.4f}")
print(f"SCZ 平均報酬率: {scz_avg_return:.4f}")
print(f"TLT 平均報酬率: {tlt_avg_return:.4f}")
print(f"最終選擇: {result}")

# 關閉資料庫連接
conn.close()


強烈懷疑 Buy-and-Hold VOO 是真王道,持續反覆打臉自己為什麼不加入大盤到底!?

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.