iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0
Python

時空序列分析-關鍵籌碼分析系列 第 28

番外篇1 - 用 python 程式撰寫一般交易回測-Moving Average Strategy(2/2) 策略優化與視覺化

  • 分享至 

  • xImage
  •  

昨天遇到的這個問題,

KeyError: 'Close'
---> signals = generate_signals(all_data, short_window=40, long_window=100)

確認一下上面的欄位和處理過的資料

print(cleaned_data.columns)

Index(['Volume', '成交金額', 'Open', 'High', 'Low', 'Close', '漲跌價差', '成交筆數'], dtype='object')

print(all_data)

日期 成交股數 成交金額 開盤價 最高價 最低價 收盤價 漲跌價差 成交筆數
0 2019/01/02 0.113 5.948 52.6 53.2 52.4 52.8 0.2 82.0
1 2019/01/03 0.226 11.979 52.8 53.2 52.8 52.8 0.0 122.0
2 2019/01/04 0.739 39.114 53.0 53.1 52.5 53.0 0.2 259.0
3 2019/01/07 0.295 15.643 53.2 53.2 52.7 53.0 0.0 151.0
4 2019/01/08 3.262 180.218 53.0 57.8 52.7 56.6 3.6 1863.0
... ... ... ... ... ... ... ... ... ...
1368 2024/08/21 3.565 2524.408 692.0 720.0 678.0 715.0 27.0 4694.0
1369 2024/08/22 5.574 4162.601 723.0 776.0 723.0 742.0 27.0 7835.0
1370 2024/08/23 6.501 5037.483 727.0 814.0 726.0 807.0 65.0 7660.0
1371 2024/08/26 7.497 6126.218 809.0 860.0 771.0 798.0 -9.0 9371.0
1372 2024/08/27 4.870 3931.727 806.0 834.0 788.0 814.0 16.0 6800.0

[1373 rows x 9 columns]
註: 這邊是使用上櫃公司,旺矽(6223), 2019~2024 年全部的股價資料

發現欄位名稱不知為何沒有被正常轉換,再額外轉換一次

all_data = all_data.rename(columns={
    '開盤價': 'Open',
    '最高價': 'High',
    '最低價': 'Low',
    '收盤價': 'Close',
    '成交張數': 'Volume'
})
all_data

日期 成交股數 成交金額 Open High Low Close 漲跌價差 成交筆數
0 2019/01/02 0.113 5.948 52.6 53.2 52.4 52.8 0.2 82.0
1 2019/01/03 0.226 11.979 52.8 53.2 52.8 52.8 0.0 122.0
2 2019/01/04 0.739 39.114 53.0 53.1 52.5 53.0 0.2 259.0
3 2019/01/07 0.295 15.643 53.2 53.2 52.7 53.0 0.0 151.0
4 2019/01/08 3.262 180.218 53.0 57.8 52.7 56.6 3.6 1863.0
... ... ... ... ... ... ... ... ... ...
1368 2024/08/21 3.565 2524.408 692.0 720.0 678.0 715.0 27.0 4694.0
1369 2024/08/22 5.574 4162.601 723.0 776.0 723.0 742.0 27.0 7835.0
1370 2024/08/23 6.501 5037.483 727.0 814.0 726.0 807.0 65.0 7660.0
1371 2024/08/26 7.497 6126.218 809.0 860.0 771.0 798.0 -9.0 9371.0
1372 2024/08/27 4.870 3931.727 806.0 834.0 788.0 814.0 16.0 6800.0
1373 rows × 9 columns

把昨天的訊號再印出來一次,用40MA 和 100MA 來進行策略

# 計算移動平均線
def moving_average(data, window):
    return data.rolling(window=window).mean()

def generate_signals(data, short_window, long_window):
    signals = pd.DataFrame(index=data.index)
    signals['price'] = data['Close']
    signals['short_mavg'] = moving_average(data['Close'], short_window)
    signals['long_mavg'] = moving_average(data['Close'], long_window)
    
    signals['signal'] = 0.0
    signals['signal'][short_window:] = np.where(signals['short_mavg'][short_window:] > signals['long_mavg'][short_window:], 1.0, 0.0)   
    signals['positions'] = signals['signal'].diff()
    return signals

# 短MA 採用 40天,長MA 採用 100天
signals = generate_signals(all_data, short_window=40, long_window=100)
signals

price short_mavg long_mavg signal positions
0 52.8 NaN NaN 0.0 NaN
1 52.8 NaN NaN 0.0 0.0
2 53.0 NaN NaN 0.0 0.0
3 53.0 NaN NaN 0.0 0.0
4 56.6 NaN NaN 0.0 0.0
... ... ... ... ... ...
1368 715.0 577.5375 491.890 1.0 0.0
1369 742.0 582.6875 496.220 1.0 0.0
1370 807.0 589.3875 501.245 1.0 0.0
1371 798.0 595.8625 506.105 1.0 0.0
1372 814.0 602.9625 511.035 1.0 0.0
1373 rows × 5 columns

然後就有東西了~

# 定義執行回測的函式,在這邊設定訊號、起始資金設10萬
def backtest(data, signals, initial_capital=100000.0):
    positions = pd.DataFrame(index=signals.index).fillna(0.0) # 處理空值 
    positions['6223'] = 100 * signals['signal']  # 每次交易 100 股
    portfolio = positions.multiply(data['Close'], axis=0)
    pos_diff = positions.diff() # 每天持倉的變化
    
    # 持有的股票倉位變化
    portfolio['holdings'] = (positions.multiply(data['Close'], axis=0)).sum(axis=1)
    # 現金水位 = 初始資金 - 持倉變化(ex. +100)*買或賣的價格(ex. 99)
    portfolio['cash'] = initial_capital - (pos_diff.multiply(data['Close'], axis=0)).sum(axis=1).cumsum()
    
    # 總資產的價值 = 現金 + 股票價值
    portfolio['total'] = portfolio['cash'] + portfolio['holdings']
    return portfolio
# 執行回測
portfolio = backtest(all_data, signals)
portfolio

 6223 holdings cash total
0 0.0 0.0 100000.0 100000.0
1 0.0 0.0 100000.0 100000.0
2 0.0 0.0 100000.0 100000.0
3 0.0 0.0 100000.0 100000.0
4 0.0 0.0 100000.0 100000.0
... ... ... ... ...
1368 71500.0 71500.0 83160.0 154660.0
1369 74200.0 74200.0 83160.0 157360.0
1370 80700.0 80700.0 83160.0 163860.0
1371 79800.0 79800.0 83160.0 162960.0
1372 81400.0 81400.0 83160.0 164560.0

[1373 rows x 4 columns]

看到資料,訊號都有依照雙均線的策略進行買進和賣出!

接著計算這些買賣的績效

用以下幾個常見的評估指標:
報酬(returns)年化報酬率(annual_return)
年化波動率(annual_volatility)夏普比率(sharpe_ratio)最大回撤(drawdown)
特別解釋一下這幾個,
年化波動率: 可以衡量這個策略的風險,波動率高,代表風險高,但也意味著可以賺取更多報酬。
夏普比率: 每承擔一個單位的風險所能帶來的超額報酬,夏普比率越高,代表 報酬>風險。
最大回撤: 投資報酬從最高到最低會虧損的最大金額,可以用來衡量一個策略的泛化能力。

# 計算績效指標 
def calculate_performance(portfolio):
    returns = portfolio['total'].pct_change()
    annual_return = (1 + returns.mean()) ** 252 - 1
    annual_volatility = returns.std() * np.sqrt(252)
    sharpe_ratio = returns.mean() / returns.std() * np.sqrt(252)
    drawdown = (portfolio['total'].cummax() - portfolio['total']).max()
    return annual_return, annual_volatility, sharpe_ratio, drawdown


annual_return, annual_volatility, sharpe_ratio, drawdown = calculate_performance(portfolio)

# 輸出績效指標
print(f'Annual Return: {annual_return:.2f}')
print(f'Annual Volatility: {annual_volatility:.2f}')
print(f'Sharpe Ratio: {sharpe_ratio:.2f}')
print(f'Max Drawdown: {drawdown:.2f}')

Annual Return: 0.10
Annual Volatility: 0.09
Sharpe Ratio: 1.05
Max Drawdown: 16000.00

可以看到用雙均線策略對 6223 旺矽 這4年多來可以獲得大概10%的年化報酬率!

把績效圖和股價一起視覺化,用直覺一點的方式來看:

# 繪製結果圖,用兩個子圖組成一個大圖,分別顯示 訊號買賣點、資產總值的變化
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 14))

# 繪製收盤價、短MA 和 長MA
all_data['Close'].plot(ax=ax1, color='black', lw=2.)
signals[['short_mavg', 'long_mavg']].plot(ax=ax1, lw=2.)

# 繪製買賣訊號
# 子圖ax1放收盤價、短MA、長MA、訊號標記
ax1.plot(signals.loc[signals.positions == 1.0].index, 
         signals.short_mavg[signals.positions == 1.0],
         '^', markersize=10, color='m', label='buy signal')
ax1.plot(signals.loc[signals.positions == -1.0].index, 
         signals.short_mavg[signals.positions == -1.0],
         'v', markersize=10, color='k', label='sell signal')
ax1.set_title('Moving Average Crossover Strategy')
ax1.set_ylabel('Price in NT$')
ax1.legend()

# 繪製資產變化
# 子圖ax2放回測期間資產的變化
portfolio['total'].plot(ax=ax2, lw=2.)
ax2.set_title('Portfolio Value')
ax2.set_ylabel('Total Value in NT$')
plt.show()

https://ithelp.ithome.com.tw/upload/images/20240828/20168322OgylJlCUk3.png

上面子圖的X軸用 回測的紀錄數 不太對,改用日期

在畫圖之前加入這行,把日期欄位設成索引,再畫一次圖:

all_data.set_index('日期', inplace=True)

https://ithelp.ithome.com.tw/upload/images/20240828/20168322Kt0In4kyTw.png

40MA和100MA的雙均線策略,後面一直沒有出現賣出訊號,
就直接結算到最後一天,難怪後面跟收盤價的走勢很像。

如果調整成 10MA 和 20MA 去做策略

Annual Return: 0.08
Annual Volatility: 0.08
Sharpe Ratio: 1.02
Max Drawdown: 10900.00

https://ithelp.ithome.com.tw/upload/images/20240828/20168322aS4sQy4GkL.png

回測證明,一直買進和賣出可能績效還比較差,因為可能錯過某一部分的行情。

或許可以用不同的策略去做搭配,其實還有細節需要微調,
像是還要考慮交易成本和交易稅。

參考文章&資料來源:

  1. 初學者的Python金融分析日記 EP5 – 移動平均、指數移動平均、MACD

  2. Python新手教學(Part 5):如何衡量風險與報酬?夏普比率告訴你

  3. 【量化交易】如何利用Python编程计算年化收益率、最大回撤、波动率、夏普比率


每日記錄:
加權指數收在22370.66點,上漲185.66點。
明天就要開NVDA財報了,未看先猜,應該是符合預期的,財報依然很好。

另外,還記得因為凱基經常會隔日沖,導致投資人以為市場看壞,
跟著殺進殺出,造成當天K棒收黑的可能性提高。
因此又有線型破壞者之稱。


上一篇
番外篇1 - 用 python 程式撰寫一般交易回測-Moving Average Strategy(1/2)
下一篇
番外篇2 - 用簡單的LSTM模型預測股價
系列文
時空序列分析-關鍵籌碼分析31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言