跟隨大戶的交易策略

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 = ['年化報酬(%)','年化標準差(%)','夏普比率'])

回測績效分析

觀察累積報酬率圖以及績效表格,我們可以看到跟隨大戶策略於 2020 年獲得了 133% 的年化報酬率,且夏普比率有大於 3 的水準。整體投組雖然在年初承受了小幅的虧損,不過相較於市場在 3 月因為新冠疫情的恐慌造成逾 20% 的回檔,此策略反倒在 1 月底即提早停損出場,反映大戶提早規避下行風險的可能。另外,在 9 月前此策略的績效與大盤相互消長,不過 9 月後即正式超越大盤,長榮於 2020 下半年的成長可見一斑。

結論

本文利用法人合計買賣超與法人合計持股率觀察籌碼的動向,判斷股價的起漲點與大戶可能開始到貨的時間,藉此建構跟隨大戶的交易策略。投過回測績效分析,本策略確實可觀察到大戶提早規避市場空頭走勢與提早佈局後續漲勢的現象,且本策略於 2020 年擁有超越大盤的績效表現。

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

完整程式碼

延伸閱讀

相關連結

返回總覽頁
Procesing