跟随大户的交易策略

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

结论

看到这边相信各位对于筹码指标与回测已有进一步的理解了!不过需要注意的是此策略只是简单示范,以上回测结果并不代表其适用于所有公司、任意区间,故还是需要搭配其他指标判断。如果想要回测更长的时间区间、利用更丰富的筹码分析指标,推荐使用达人方案组合,自行设计您专属的最佳买卖点!

完整程式码

延伸阅读

相关连结

返回总览页