跟隨大戶的交易策略

Photo Creds: Usplash

前言

一般來說,三大法人、關鍵內部人或是其他千張大戶相較於散戶會擁有較多的資訊,因此較有可能挑選出潛力股或是避開地雷股。金管會為了降低資訊不對等,便要求公司或券商公布每日買賣資料,使得投資人能藉由觀察大戶們的買賣動向去分析股價的未來走勢,然而這就是所謂的籌碼分析。

常見的籌碼分析指標包含三大法人買(賣)超、連買(賣)天數、法人成交比重、董監持股比率、券資比等等,TEJ API資料庫已涵蓋許多籌碼指標,不需要自行計算即可進行股票的篩選與報酬回測。此文章可以銜接 【新手上路(五)】開始體驗TEJ免費資料庫 的內容,以下我們仍然會使用試用資料庫進行簡單的策略回測範例,讀者可以在申請試用金鑰後也跟著一起做!

編輯環境及模組需求

本文使用 Windows OS 並以 Jupyter Notebook 作為編輯器

import tejapi
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
tejapi.ApiConfig.api_key = “Your Key”
tejapi.ApiConfig.ignoretz = True

Note: 將 Your Key 替換成當初申請試用的金鑰,最後一行代表不顯示時區

本文重點概要

  • 認識籌碼面指標
  • 回測交易策略報酬率

試用資料庫

  • 上市(櫃)未調整股價(日): 資料庫編碼為 ‘TRAIL/TAPRCD’,收錄所有上市櫃公司的股價、成交量與本益比等資料
  • 三大法人買賣超: 資料庫編碼為 ‘TRAIL/TATINST1’,收錄上市櫃公司每日的大戶買賣超、持股率與週轉率等資料

籌碼面指標應用

  • 法人合計持股率: 為外資、投信與自營商持股比重的加總,可藉此比率觀察每檔股票持有者的分布狀況
  • 法人合計買賣超: 為外資、投信與自營商買賣超股數的加總,數值為正代表該股當天為買超,反之則為賣超,並可藉此觀察大戶持股的流向

交易策略設計與實作

買入訊號 : 當法人合計買超,且法人合計持股率低於近五日平均時,代表後續或許有一波漲勢,故給予買入訊號

賣出訊號 : 當法人合計賣超,且法人合計持股率高於近五日平均時,代表法人可能準備出貨,故給予賣出訊號

假設買點成立時,次日開盤買入股票並持有到賣出訊號出現,期間最多持有一股,而選擇次日開盤是因為全日買賣超資料於盤後公布,所以隔日才可按照此公開資訊操作。本次回測有考慮交易手續費 (0.1425%)與證交稅 (0.3%)

Step 1. 導入模組後,開始撈取資料

evergreen_chip = tejapi.get('TRAIL/TATINST1',
coid = '2603',
opts = {'columns':['coid','mdate',
'ttl_ex','fld024']},
chinese_column_name = True)

首先選取籌碼面資料,這次的公司一樣選擇長榮 (2603),並根據資料庫欄位說明內的欄位名稱,選取合計買賣超與合計持股率欄位

evergreen_price = tejapi.get('TRAIL/TAPRCD',
coid = '2603',
opts = {'columns':['mdate','open_d']},
chinese_column_name = True)

為了計算報酬率,我們需要撈取股價資料,並且利用 shift(-1) 將開盤價上移一列,形成次日開盤價欄位以利後續的報酬計算

evergreen_price['次日開盤價'] = evergreen_price['開盤價(元)'].shift(-1)
market = tejapi.get('TRAIL/TAPRCD',
coid = 'Y9997',
opts = {'columns':['mdate', 'roi']},
chinese_column_name = True)

為了對比出交易策略的表現,我們需要台灣加權報酬指數的報酬率作為基準,並且以 rename() 將報酬率欄位名稱改成市場報酬率,以避免與交易策略報酬混淆

market = market.rename(columns = {'報酬率%':'市場報酬率%'})

Step 2. 合併資料

evergreen = evergreen_chip.merge(evergreen_price, on = '年月日')
evergreen = evergreen.merge(market, on = '年月日')

利用 merge() 將籌碼、股價與市場報酬合併,第一個參數為欲合併資料,on 則表示以該共通欄位進行合併

Step 3. 建立訊號判斷欄位

evergreen['合計買賣超'] = np.where(evergreen['合計買賣超(千股)'] >= 0, 1, 0)

這邊利用 np.where(),若符合法人合計買超則在合計買賣超 (訊號判斷) 欄位填入1,反之為 0

evergreen['持股率_5日MA'] = evergreen['合計持股率%'].rolling(5).mean()
evergreen['合計持股變化'] = np.where(evergreen['合計持股率%'] - evergreen['持股率_5日MA'] > 0, 1, 0)

而為了計算合計持股變化,我們以五日移動平均作為基準,若當日合計持股率相對五日移動平均高,則在合計持股變化欄填入1,反之則為 0

evergreen = evergreen.dropna().reset_index(drop=True)

最後利用 dropna() 去除空值以及 reset_index(drop=True) 將索引重置且使原索引不獨立形成新的一欄

Step 4. 新增訊號欄位

  • 買入
evergreen['訊號'] = np.where((evergreen['合計買賣超'] == 1)&(evergreen['合計持股變化'] == 0), 'Buy', '')
  • 賣出
evergreen['訊號'] = np.where((evergreen['合計買賣超'] == 0)&(evergreen['合計持股變化'] == 1), 'Sell', evergreen['訊號'])

新建立一個訊號欄位,並根據交易策略的買賣判斷填入 Buy, Sell 或是空字串。在判斷賣出訊號時,為了不覆蓋買入訊號判斷結果,必須將訊號欄置於第三個參數,代表不符合賣出條件時保留原欄位資訊

evergreen['訊號'][len(evergreen)-1] = 'Sell'

len(evergreen)代表資料總筆數,扣除 1 即為最後一筆 (12/30) 的索引。這邊將該天訊號自行改為 Sell,表示手中有持股時會以 31日開盤價進行平倉

Step 5. 計算策略報酬率

這裡我們先建立 hold 變數,預設為0,代表尚未持有股票,但當遇上買入訊號且尚未持有股票時,則存入1,若賣出手中持股時,則重置為0

hold = 0

cost 變數預設為0,若碰到買入訊號且手中未有股票時,則存入次日開盤價,當作購買股票成本

cost = 0

接著建立 Return 空列表,遇到賣出訊號且手中有股票時,利用 np.log() 計算這段持有期間的連續報酬 (%),並在扣除手續費與證交稅後搭配 append()加到此列表,其餘情況則填入0,確保報酬筆數與原資料長度相等以利合併

Return = []

利用 for 進行與資料長度相等次數的迴圈,每次迴圈使用 if檢視第 i 天的訊號是否出現買賣點,符合條件時計算報酬率、改變 holdcost 變數值

for i in range(len(evergreen)):
if evergreen['訊號'][i] == '':
Return.append(0)
elif evergreen['訊號'][i] == 'Buy':
if hold == 0:
cost = evergreen['次日開盤價'][i]
hold = 1
Return.append(0)
else:
Return.append(0)
elif evergreen['訊號'][i] == 'Sell':
if hold == 1:
Return.append(100*(np.log(evergreen['次日開盤價'][i]/cost)- 0.001425*2 - 0.003))
hold = 0
else:
Return.append(0)

最後再將Return列表當作新的一欄資料,欄名為籌碼面報酬率

evergreen['籌碼面報酬率(%)'] = Return

Step 6. 計算累積報酬率,並視覺化結果

evergreen['籌碼累積報酬率'] = evergreen['籌碼面報酬率(%)'].apply(lambda x: 0.01*x+1).cumprod()
evergreen['市場累積報酬率'] = evergreen['市場報酬率%'].apply(lambda x: 0.01*x+1).cumprod()

先利用 apply()將欄位裡的報酬率換成非百分比形式再加上本金,然後用 cumprod() 計算累積乘積,此即為累積報酬

plt.plot(evergreen['年月日'], evergreen['籌碼累積報酬率'], label = 'strategy')
plt.plot(evergreen['年月日'], evergreen['市場累積報酬率'], label = 'market')
plt.legend()
plt.show()

最後再用 plt.plot()plt.show()將圖繪出,其中labelplt.legend() 顯示圖例

可以看到在2020年期間,此策略的累積報酬率表現優於市場

Step 7. 績效表格

cagr = [100*(evergreen['籌碼累積報酬率'].values[-1]**(252/len(evergreen)) - 1), 100*(evergreen['市場累積報酬率'].values[-1]**(252/len(evergreen)) - 1)]

首先以 cagr 列表存放籌碼與市場的年化報酬率,利用 values[-1]選出累積報酬率的最後一筆,並且以年交易天數 (252)調整,然後再扣除本金

std = [evergreen['籌碼面報酬率(%)'].std()*(252**0.5),evergreen['市場報酬率%'].std()*(252**0.5)]

std 列表存放的是年化標準差,其由日標準差與年交易天數調整得出

sharpe_ratio = [(cagr[0] - 1)/std[0],(cagr[1] - 1)/std[1]]  

sharpe_ratio 列表的夏普比率是以無風險利率1%作為假設計算,最後再以 pd.DataFrame() 整理成表格

result = pd.DataFrame([cagr,std,sharpe_ratio], columns = ['籌碼面','市場'], index = ['年化報酬(%)','年化標準差(%)','夏普比率'])

結論

看到這邊相信各位對於籌碼指標與回測已有進一步的理解了!不過需要注意的是此策略只是簡單示範,以上回測結果並不代表其適用於所有公司、任意區間,故還是需要搭配其他指標判斷。如果想要回測更長的時間區間、利用更豐富的籌碼分析指標,推薦使用達人方案組合,自行設計您專屬的最佳買賣點!

完整程式碼

延伸閱讀

相關連結

想看更多內容?快來【登入會員】,享受更多閱讀文章的權限喔!
返回總覽頁