海龟交易系统的实现
前言
海龟交易系统本质上是一个趋势跟随的系统,但是最值得我们学习的,是资金管理尤其是分批建仓及动态止损的部分
一、趋势捕捉
** 唐奇安通道**
该指标是有Richard Donchian发明的,是有3条不同颜色的曲线组成的,该指标用周期(一般都是20)内的最高价和最低价来显示市场价格的波动性,当其通道窄时表示市场波动较小,反之通道宽则表示市场波动比较大。 如图所示:
该具体分析为:
当价格冲冲破上轨是就是可能的买的信号;反之,冲破下轨时就是可能的卖的信号。
该指标的计算方法为:
上线=Max(最高低,n)
下线=Min(最低价,n)
中线=(上线+下线)/2
海龟交易就是利用唐奇安通道的价格突破来捕捉趋势。
不过我们在向下突破10日唐奇安下沿卖出。
二、资金管理
2.1、N值计算
N值是仓位管理的核心,涉及加仓及止损。另外,N值与技术指标平均真实波幅 ATR很相似
首先介绍真实波幅: 真实波幅是以下三个值中的最大值
1、当前交易日最高价和最低价的波幅
2、前一交易日的收盘价与当前交易日最高价的波幅
3、前一交易日的收盘价与当前交易日最低价的波幅
用公式写就是:
TrueRange=Max(High−Low,abs(High−PreClose),abs(PreClose−Low))
接下来,N值计算公式为:
N=PreN[−19:]+TrueRange20
其中 preN为前面N值,TrueRange为当前的真实波幅,此公式的真是含义为计算之前20天(包括今天在内)的N的平均值
另外,有些海龟交易系统用的是ATR来代替N值,ATR为真实波幅的20日平均。
2.2 买卖单位及首次建仓
先给出公式:
Unit=1N
首次建仓的时候,当捕捉到趋势,即价格突破唐奇安上轨时,买入1个unit。
其意义就是,让一个N值的波动与你总资金1%的波动对应,如果买入1unit单位的资产,当天震幅使得总资产的变化不超过1%。例如:
现在你有10万元资金,1%波动就是1000元。假如标X的N值为0.2元,1000元÷0.2元=5000股。也就是说,你的第一笔仓位应该是在其突破上轨(假设为5元)时立刻买入5000股,耗资25000元。
2.3 加仓
若股价在上一次买入(或加仓)的基础上上涨了0.5N,则加仓一个Unit。
接上面的例子:假如N值仍为0.2。
价格来到 5 + 0.2*0.5 = 5.1时,加仓1个Unit,买入5000股,耗资25500元,剩余资金 49500元
价格来到 5.1 + 0.2*0.5 = 5.2 时再加仓1个unit。买入5000股,耗资26000元,剩余资金 23500元
2.4 动态止损
当价格比最后一次买入价格下跌2N时,则卖出全部头寸止损。
接上面的例子,最后一次加仓价格为5.2。假如此时N值0.2元。 当价格下跌到 5.2 - 2*0.2 = 4.8元时,清仓。
持仓成本为 (5+5.1+5.2)*5000/15000 = 5.1元。 此时亏损 (5.1-4.8)*15000 = 4500元 对于10万来说 这波亏损4.5%
2.5 止盈
当股价跌破10日唐奇安通道下沿,清空头寸结束本次交易
三、代码实现
本代码用ATR代替N值进行计算,其他逻辑不变:
ATR=MA(TrueRange,20)
我们以单只股票为标,建立海龟交易系统,当然,可以将总资产均分为n份,同时交易n个标。
计算ATR值用日线数据,监控价格突破采用分钟线
0 初始化参数,在initialize(account)写入
1
def initialize(account):
2
account.last_buy_prcie = 0 #上一次买入价
3
account.hold_flag = False # 是否持有头寸标志
4
account.limit_unit = 4 # 限制最多买入的单元数
5
account.unit = 0 # 现在买入1单元的股数
6
1 唐奇安通道计算及判断入场离场:
我们设计个函数,传入值为回测中 account.get_history()取得的某单个股票的历史数据、股票现价、T为计算唐奇安通道的数据长度,转化为dataframe格式
1
def IN_OR_OUT(data,price,T):
2
up = max(data['highPrice'].iloc[-T:])
3
down = min(data['lowPrice'].iloc[-int(T/2):]) # 这里是10日唐奇安下沿
4
if price>up:
5
return 1
6
elif price<down:
7
return -1
8
else:
9
return 0
2. ATR值计算:
1
def CalcATR(data):
2
TR_List = []
3
for i in range(1,21):
4
TR = max(data['highPrice'].iloc[i]-data['lowPrice'].iloc[i],abs(data['highPrice'].iloc[i]-data['closePrice'].iloc[i-1]),abs(data['closePrice'].iloc[i-1]-data['lowPrice'].iloc[i]))
5
TR_List.append(TR)
6
ATR = np.array(TR_List).mean()
7
return ATR
3. 计算unit,注意股数为100的整数倍
1
def CalcUnit(perValue,ATR):
2
return int((perValue/ATR)/100)*100
4.判断是否加仓或止损:
当价格相对上个买入价上涨 0.5ATR时,再买入一个unit
当价格相对上个买入价下跌 2ATR时,清仓
1
def Add_OR_Stop(price,lastprice,ATR):
2
if price >= lastprice + 0.5*ATR:
3
return 1
4
elif price <= lastprice - 2*ATR:
5
return -1
6
else:
7
return 0
5 判断上次卖出操作是否成功(可能出现当日买进,之后却判断需要卖出)
1
def SellComplete(hold_flag,security_position):
2
if len(security_position)>0 and hold_flag==False:
3
return True
4
else:
5
return False
构建策略
分钟线回测时间略长啊~
先把上面写的函数集中下,方便微核充启后运行函数
1
################################################### 计算、判断函数 #####################################################################
2
def IN_OR_OUT(data,price,T):
3
up = max(data['highPrice'].iloc[-T:])
4
down = min(data['lowPrice'].iloc[-int(T/2):]) # 这里是10日唐奇安下沿
5
if price>up:
6
return 1
7
elif price<down:
8
return -1
9
else:
10
return 0
11
12
def CalcATR(data):
13
TR_List = []
14
for i in range(1,21):
15
TR = max(data['highPrice'].iloc[i]-data['lowPrice'].iloc[i],abs(data['highPrice'].iloc[i]-data['closePrice'].iloc[i-1]),abs(data['closePrice'].iloc[i-1]-data['lowPrice'].iloc[i]))
16
TR_List.append(TR)
17
ATR = np.array(TR_List).mean()
18
return ATR
19
20
def CalcUnit(perValue,ATR):
21
return int((perValue/ATR)/100)*100
22
23
def Add_OR_Stop(price,lastprice,ATR):
24
if price >= lastprice + 0.5*ATR:
25
return 1
26
elif price <= lastprice - 2*ATR:
27
return -1
28
else:
29
return 0
30
31
def SellComplete(hold_flag,security_position):
32
if len(security_position)>0 and hold_flag==False:
33
return True
34
else:
35
return False
1
import numpy as np
2
import pandas as pd
3
from __future__ import division
4
from CAL.PyCAL import *
5
import matplotlib.pyplot as plt
6
7
start = '2012-01-01' # 回测起始时间
8
end = '2016-01-01' # 回测结束时间
9
benchmark = '000001.XSHE'
10
universe = ['000001.XSHE']
11
capital_base = 100000 # 起始资金
12
freq = 'm' # 策略类型,'d'表示日间策略使用日线回测,'m'表示日内策略使用分钟线回测
13
refresh_rate = 1 # 调仓频率,表示执行handle_data的时间间隔,若freq = 'd'时间间隔的单位为交易日,若freq = 'm'时间间隔为分钟
14
15
16
#----------------------------------- 记录部分数据 -----------------------------
17
global record
18
record = {'break_up':{},'break_down':{},'stop_loss':{},'position':{},'ATR':{}} # 记录入场、离常、止损点、持仓比、ATR
19
#---------------------------------------------------------------------------------------
20
21
#****************************************** 策略主体 ********************************************
22
23
def initialize(account): # 初始化虚拟账户状态
24
account.last_buy_prcie = 0 #上一次买入价
25
account.hold_flag = False # 是否持有头寸标志
26
account.limit_unit = 4 # 限制最多买入的单元数
27
account.unit = 0 # 现在买入1单元的股数
28
account.add_time = 0 # 买入次数
29
30
31
def handle_data(account): # 每个交易日的买入卖出指令
32
T = 20
33
data = account.get_daily_history(T+1)
34
stk = universe[0]
35
data = data[stk]
36
data = pd.DataFrame(data)
37
prices = account.reference_price[stk]
38
today = Date.fromDateTime(account.current_date)
39
today = today.toISO()
40
41
# 0 如果停牌,直接跳过
42
if np.isnan(prices) or prices == 0: # 停牌或是还没有上市等原因不能交易
43
return
44
45
# 1 计算ATR
46
ATR = CalcATR(data)
47
record['ATR'].update({today:ATR})
48
49
# 2 判断上次卖出是否成功,若不成功,再次卖出
50
if SellComplete(account.hold_flag,account.security_position):
51
for stk in account.security_position:
52
order_to(stk,0)
53
54
# 3 判断加仓或止损
55
if account.hold_flag==True and len(account.security_position)>0: # 先判断是否持仓
56
temp = Add_OR_Stop(prices,account.last_buy_prcie,ATR)
57
if temp ==1and account.add_time<account.limit_unit: # 判断加仓
58
order_num = min(account.unit,account.cash) # 不够1unit时买入剩下全部
59
order_to(stk,account.unit)
60
account.last_buy_prcie = prices
61
account.add_time += 1
62
elif temp== -1: # 判断止损
63
order_to(stk,0)
64
initialize(account) # 重新初始化参数 very important here!
65
record['stop_loss'].update({today:prices})
66
67
# 4 判断入场离场
68
out = IN_OR_OUT(data,prices,T)
69
if out ==1 and account.hold_flag==False: #入场
70
value = account.reference_portfolio_value * 0.01
71
account.unit = CalcUnit(value,ATR)
72
order_to(stk,account.unit)
73
account.add_time = 1
74
account.hold_flag = True
75
account.last_buy_prcie = prices
76
record['break_up'].update({today:prices})
77
78
elif out==-1 and account.hold_flag ==True: #离场
79
order_to(stk,0)
80
initialize(account) # 重新初始化参数 very important here!
81
record['break_down'].update({today:prices})
82
83
# 5 计算持仓比
84
ratio = 1 - account.cash/account.reference_portfolio_value
85
record['position'].update({today:ratio}) # 虽然每分钟重算,但因为key是日期,最后覆盖为当日最终持仓比
86
87
return
88
- 年化收益率8.6%
- 基准年化收益率17.1%
- 阿尔法2.9%
- 贝塔0.17
- 夏普比率0.49
- 收益波动率10.5%
- 信息比率-0.43
- 最大回撤11.8%
- 年化换手率--
累计收益率策略基准2012-012012-072013-012013-072014-012014-072015-012015-072016-01-50.00%0.00%50.00%100.00%150.00%200.00%2015-03-12策略: 27.17%基准: 85.35%
WARNING: refresh_rate的值仅作用于分钟线。若想对日线进行控制,请使用如下定义: refresh_rate = (日线refresh_rate, 分钟线refresh_rate)
我们发现,收益基本上处于阶梯状上升。但是几年下来收益也并不高,我们来看看记录下来的数据,分析下整个过程:
1
r = pd.DataFrame(record)
2
adj_price = DataAPI.MktEqudAdjGet(secID=u"000001.XSHE",ticker=u"",beginDate='20120101',endDate='20160101',isOpen="",field=u"",pandas="1")
3
adj_price = adj_price.set_index('tradeDate')
把图画出来:
红色点为入场点;
蓝色点为离场点;
绿色点位止损点
1
plt.figure(figsize=(20,10))
2
r['ATR'].plot(label='ATR')
3
adj_price['closePrice'].plot(label='adj price')
4
adj_price['highestPrice'].plot(label='high price')
5
adj_price['lowestPrice'].plot(label='low price')
6
for i in range(len(r)):
7
plt.plot(i,r['break_up'].iloc[i],'.r',markersize=13)
8
plt.plot(i,r['break_down'].iloc[i],'.b',markersize=13)
9
plt.plot(i,r['stop_loss'].iloc[i],'.g',markersize=13)
10
plt.legend(loc=0)
<matplotlib.legend.Legend at 0x468a6c50>
可以发现:
ATR波形有些异常,有些地方会直线上升。分析后发现:因为quartz 中,account.get_daily_history()取得的最高最低价中,对停牌的情况处理为了0!
我们调整下策略:
在计算ATR时,剔除最高最低为0的部分,再做平均。
1
import numpy as np
2
import pandas as pd
3
from __future__ import division
4
from CAL.PyCAL import *
5
import matplotlib.pyplot as plt
6
7
start = '2012-01-01' # 回测起始时间
8
end = '2016-01-01' # 回测结束时间
9
benchmark = '000001.XSHE'
10
universe = ['000001.XSHE']
11
capital_base = 100000 # 起始资金
12
freq = 'm' # 策略类型,'d'表示日间策略使用日线回测,'m'表示日内策略使用分钟线回测
13
refresh_rate = 1 # 调仓频率,表示执行handle_data的时间间隔,若freq = 'd'时间间隔的单位为交易日,若freq = 'm'时间间隔为分钟
14
15
16
#----------------------------------- 记录部分数据 -----------------------------
17
global record
18
record = {'break_up':{},'break_down':{},'stop_loss':{},'position':{},'ATR':{}} # 记录入场、离常、止损点、持仓比、ATR
19
#---------------------------------------------------------------------------------------
20
21
22
#****************************************** 策略主体 ********************************************
23
24
def initialize(account): # 初始化虚拟账户状态
25
account.last_buy_prcie = 0 #上一次买入价
26
account.hold_flag = False # 是否持有头寸标志
27
account.limit_unit = 4 # 限制最多买入的单元数
28
account.unit = 0 # 现在买入1单元的股数
29
account.add_time = 0 # 买入次数
30
31
32
def handle_data(account): # 每个交易日的买入卖出指令
33
T = 20
34
data = account.get_daily_history(T+1)
35
stk = universe[0]
36
data = data[stk]
37
#---------------------- 修改部分 ----------------------
38
data = pd.DataFrame(data)
39
data['highPrice'] = data['highPrice'].replace(0,np.nan)
40
data = data.dropna()
41
if len(data)<T+1:
42
delta = T+1 - len(data)
43
m = T+1+delta
44
while delta > 0: # 直到取满20个不为停牌的数据
45
m += delta
46
data = account.get_daily_history(m)
47
data = data[stk]
48
data = pd.DataFrame(data)
49
data['highPrice'] = data['highPrice'].replace(0,np.nan)
50
data = data.dropna()
51
delta = T+1 - len(data)
52
#---------------------------------------------------------
53
prices = account.reference_price[stk]
54
today = Date.fromDateTime(account.current_date)
55
today = today.toISO()
56
57
# 0 如果停牌,直接跳过
58
if np.isnan(prices) or prices == 0: # 停牌或是还没有上市等原因不能交易
59
return
60
61
# 1 计算ATR
62
ATR = CalcATR(data)
63
record['ATR'].update({today:ATR})
64
65
# 2 判断上次卖出是否成功,若不成功,再次卖出
66
if SellComplete(account.hold_flag,account.security_position):
67
for stk in account.security_position:
68
order_to(stk,0)
69
70
# 3 判断加仓或止损
71
if account.hold_flag==True and len(account.security_position)>0: # 先判断是否持仓
72
temp = Add_OR_Stop(prices,account.last_buy_prcie,ATR)
73
if temp ==1and account.add_time<account.limit_unit: # 判断加仓
74
order_num = min(account.unit,account.cash) # 不够1unit时买入剩下全部
75
order_to(stk,account.unit)
76
account.last_buy_prcie = prices
77
account.add_time += 1
78
elif temp== -1: # 判断止损
79
order_to(stk,0)
80
initialize(account) # 重新初始化参数 very important here!
81
record['stop_loss'].update({today:prices})
82
83
# 4 判断入场离场
84
out = IN_OR_OUT(data,prices,T)
85
if out ==1 and account.hold_flag==False: #入场
86
value = account.reference_portfolio_value * 0.01
87
account.unit = CalcUnit(value,ATR)
88
order_to(stk,account.unit)
89
account.add_time = 1
90
account.hold_flag = True
91
account.last_buy_prcie = prices
92
record['break_up'].update({today:prices})
93
94
elif out==-1 and account.hold_flag ==True: #离场
95
order_to(stk,0)
96
initialize(account) # 重新初始化参数 very important here!
97
record['break_down'].update({today:prices})
98
99
# 5 计算持仓比
100
ratio = 1 - account.cash/account.reference_portfolio_value
101
record['position'].update({today:ratio}) # 虽然每分钟重算,但因为key是日期,最后覆盖为当日最终持仓比
102
103
return
- 年化收益率8.7%
- 基准年化收益率17.1%
- 阿尔法2.9%
- 贝塔0.17
- 夏普比率0.48
- 收益波动率10.9%
- 信息比率-0.43
- 最大回撤11.8%
- 年化换手率--
累计收益率策略基准2012-012012-072013-012013-072014-012014-072015-012015-072016-01-50.00%0.00%50.00%100.00%150.00%200.00%
WARNING: refresh_rate的值仅作用于分钟线。若想对日线进行控制,请使用如下定义: refresh_rate = (日线refresh_rate, 分钟线refresh_rate)
累计收益相差不多,我们再来看看记录的数据。
红色点为入场点;
蓝色点为离场点;
绿色点位止损点
1
r = pd.DataFrame(record)
2
adj_price = DataAPI.MktEqudAdjGet(secID=u"000001.XSHE",ticker=u"",beginDate='20120101',endDate='20160101',isOpen="",field=u"",pandas="1")
3
adj_price = adj_price.set_index('tradeDate')
4
adj_price['highestPrice'] = adj_price['highestPrice'].replace(0,np.nan)
5
adj_price['lowestPrice'] = adj_price['lowestPrice'].replace(0,np.nan)
6
7
plt.figure(figsize=(20,10))
8
r['ATR'].plot(label='ATR')
9
adj_price['closePrice'].plot(label='adj price')
10
adj_price['highestPrice'].plot(label='high price')
11
adj_price['lowestPrice'].plot(label='low price')
12
for i in range(len(r)):
13
plt.plot(i,r['break_up'].iloc[i],'.r',markersize=13)
14
plt.plot(i,r['break_down'].iloc[i],'.b',markersize=13)
15
plt.plot(i,r['stop_loss'].iloc[i],'.g',markersize=13)
16
plt.legend(loc=0)
<matplotlib.legend.Legend at 0x54b4f490>
-
这次发现,ATR波形比较正常,在波动剧烈的时候增大。
-
观察入场、离场、止损点发现,海龟交易系统捕捉到了大的上涨趋势,在震荡市中不断试错止损。
-
上涨过程中出现回调容易震出,减少了回撤的同时也减小了收益。
再看看仓位情况
1
r['position'].plot(kind='bar',figsize=(200,5))
<matplotlib.axes._subplots.AxesSubplot at 0xb28f9cd0>
-
可以发现,大部分持有情况下仓位在0.5左右,甚至低于半仓,少数高于半仓的情况最高不超过0.8。因此,收益不高也是正常了。
总结
-
本文主要介绍了海龟交易的细节,不过是面向一个投资目标的。当想投多只股票时,可以先设定几个坑位,平分资金,然后对每个坑位采用海龟交易策略。
-
海龟交易系统通常会用两个趋势捕捉系统,不同之处在于价格突破的上下线计算。系统1:突破上线20日最高买,突破下线10日最低卖;系统2:突破上线55日最高买,突破下线20日最低卖。 这部分可以通过修改参数实现。
-
原始的海龟交易采用唐奇安通道来捕捉趋势,虽然能捕捉到大趋势,但是在震荡的情况下表现不如人意,不过这也是所有趋势型策略的通病。
-
海龟交易策略的核心在于资金管理,可以看出策略的回撤比较小,并且还有优化的空间。资金管理不一定要与趋势型策略结合,是不是可以用到多因子策略上?动量反转?均值回归?这些就留给读者们自行尝试了~