海龟交易法和唐奇安通道

ZaynPei Lv6

海龟交易法的传奇起源于这样一个问题:交易员是天生的还是后天培养的? 传奇交易员理查德·丹尼斯 (Richard Dennis) 相信,成功的交易员可以像海龟一样被“培育”出来。他认为,只要有一套明确、量化的规则,并严格遵守,普通人也能成为顶尖的交易员。这个实验的产物——海龟交易系统,证明了他的观点,并成为了量化交易领域的传世经典。

海龟交易法则的本质是一个趋势跟踪 (Trend Following) 策略。它基于一个哲学信念:市场价格倾向于在一段时间内向某个方向持续运动,形成趋势。该策略的目标就是尽早识别出新趋势的苗头,建立头寸,并尽可能地持有,直到有明确的信号表明趋势已经结束

它的“完整性”和“机械化”体现在它为一笔交易的全生命周期都提供了清晰的、不带感情色彩的规则:

  • 买卖什么? (市场)

  • 买卖多少? (头寸规模)

  • 何时买卖? (入市信号)

  • 何时止损? (风险控制)

  • 何时止盈? (退出信号)

策略的五大支柱

我们可以将海龟交易法则拆解为五个环环相扣的核心组成部分。

头寸规模:买卖多少? (风险管理的核心)

这是海龟交易法则最核心、最天才的部分。它不问“这个信号能赚多少钱?”,而是先问“如果这个信号错了,我会亏多少钱?”。这一切都围绕着一个核心概念——波动性 (Volatility)。

  • 定义波动性 N (Average True Range - ATR): 策略首先需要量化市场的日常波动幅度。这通过计算平均真实波幅 (ATR) 来实现。

    • 真实波幅 (True Range, TR) 的计算如下,取三者中的最大值:

      • 当日最高价 - 当日最低价

      • 当日最高价 - 上一交易日收盘价 |
      • 当日最低价 - 上一交易日收盘价 |
    • 平均真实波幅 (N 或 ATR) 则是过去 n 天 TR 值的简单移动平均值。

    • 计算 N 的目的是为了让所有的交易决策都基于市场的真实波动性,而不是主观臆断。

定义头寸单位 (Unit): 海龟法则不以固定的“手数”来下单,而是以一个标准化的“单位”(Unit)来下单。一个 Unit 的设计原则是:当价格向不利方向变动 1N 时,账户总资金的损失恰好是 1%。

  • 价值波动量 (Dollar Volatility, DV):首先计算 1N 的波动对应多少现金价值。

DV = N × 合约每点价值

  • 计算单位 (Unit):用总资产的1%除以价值波动量,得到每次应该交易的合约数量。

  • 这是风险标准化的关键。通过这个公式,无论是在波动剧烈的原油市场还是在波动平缓的玉米市场,一个 Unit 所承担的风险都是完全一致的,即总资产的1%。

入市信号:何时买入/卖出?

海龟法则使用唐奇安通道 (Donchian Channel) 来识别趋势的突破。唐奇安通道由过去 n 天最高价(上轨)和最低价(下轨)构成。

系统一 (System 1):短期系统

  • 入市信号:当价格向上突破过去 20日 的最高价时,买入一个 Unit。当价格向下突破过去 20日 的最低价时,卖出一个 Unit。

系统二 (System 2):中长期系统

  • 入市信号:当价格向上突破过去 55日 的最高价时,买入一个 Unit。当价格向下突破过去 55日 的最低价时,卖出一个 Unit。

突破前期高点或低点被认为是市场原有均衡被打破的标志,可能预示着一轮新趋势的开启。

加仓:如何扩大优势?

海龟法则采用金字塔式加仓 (Pyramiding),在盈利的头寸上追加投资,让利润奔跑。

加仓规则:在建立第一个 Unit 的头寸后,如果市场价格继续沿着盈利方向移动 1/2N,则加仓一个 Unit。

虽然这种加仓方式比较激进,但它是基于波动性的,只有当趋势得到确认(价格持续朝有利方向移动)时才增加风险暴露

止损:何时承认错误?

严格的止损是系统的安全带。

止损规则:任何一笔交易的初始止损位都设置在距离入场价 2N 的位置。

风险上限:由于初始头寸是一个 Unit(代表1%的风险),而止损位在 2N,这意味着任何一笔初始交易的最大亏损被严格限制在总资产的 2%

动态调整:每加仓一次(增加 1/2N 的盈利),整个头寸(包括之前的所有头寸)的止损位也随之提高 1/2N

这种移动止损机制不仅能保护初始资本,还能逐步锁定浮动盈利。

止盈退出:何时兑现利润?

这是海龟法则中最反人性的部分。它没有固定的盈利目标,而是让趋势决定何时离场。

系统一 (System 1) 退出:

  • 多头头寸:当价格跌破过去 10日 的最低价时离场。

  • 空头头寸:当价格涨破过去 10日 的最高价时离场。

系统二 (System 2) 退出:

  • 多头头寸:当价格跌破过去 20日 的最低价时离场。

  • 空头头寸:当价格涨破过去 20日 的最高价时离场。

这种退出机制的哲学是“给趋势足够的呼吸空间”。它允许价格在趋势中有较大幅度的回调而不被震荡出局,目标是完整地抓住一段大趋势,即使这意味着会回吐一部分浮动盈利。

代码示例

下面的代码示例展示了如何实现一个简单的海龟交易策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# coding=utf-8
from __future__ import print_function, absolute_import, unicode_literals
import numpy as np
import pandas as pd
from gm.api import *
'''
以短期为例:20日线
第一步:获取历史数据,计算唐奇安通道和ATR
第二步:当突破唐奇安通道时,开仓。
第三步:计算加仓和止损信号。
'''
def init(context):
# 设置计算唐奇安通道的参数
context.n = 20
# 设置合约标的
context.symbol = 'DCE.i2012'
# 设置交易最大资金比率
context.ratio = 0.8
# 订阅数据
subscribe(symbols=context.symbol, frequency='60s', count=2)
# 获取当前时间
time = context.now.strftime('%H:%M:%S')
# 如果策略执行时间点是交易时间段,则直接执行algo定义atr等参数,以防直接进入on_bar()导致atr等未定义
if '09:00:00' < time < '15:00:00' or '21:00:00' < time < '23:00:00':
algo(context)
# 如果是交易时间段,等到开盘时间确保进入algo()
schedule(schedule_func=algo, date_rule='1d', time_rule='09:00:00')
schedule(schedule_func=algo, date_rule='1d', time_rule='21:00:00')
def algo(context):
# 计算通道的数据:当日最低、最高、上一交易日收盘
# 注:由于talib库计算ATR的结果与公式求得的结果不符,所以这里利用公式计算ATR
# 如果是回测模式,当天的数据直接用history取到
if context.mode == 2:
data = history_n(symbol=context.symbol, frequency='1d', count=context.n+1, end_time=context.now, fields='close,high,low,bob', df=True) # 计算ATR
tr_list = []
for i in range(0, len(data)-1):
tr = max((data['high'].iloc[i] - data['low'].iloc[i]), data['close'].shift(-1).iloc[i] - data['high'].iloc[i],
data['close'].shift(-1).iloc[i] - data['low'].iloc[i])
tr_list.append(tr)
context.atr = int(np.floor(np.mean(tr_list)))
context.atr_half = int(np.floor(0.5 * context.atr))
# 计算唐奇安通道
context.don_open = np.max(data['high'].values[-context.n:])
context.don_close = np.min(data['low'].values[-context.n:])
# 如果是实时模式,当天的数据需要用current取到
if context.mode == 1:
data = history_n(symbol=context.symbol, frequency='1d', count=context.n, end_time=context.now, fields='close,high,low',
df=True) # 计算ATR
current_data = current(symbols=context.symbol) # 最新一个交易日的最高、最低
tr_list = []
for i in range(1, len(data)):
tr = max((data['high'].iloc[i] - data['low'].iloc[i]),
data['close'].shift(-1).iloc[i] - data['high'].iloc[i],
data['close'].shift(-1).iloc[i] - data['low'].iloc[i])
tr_list.append(tr)
# 把最新一期tr加入列表中
tr_new = max((current_data[0]['high'] - current_data[0]['low']),
data['close'].iloc[-1] - current_data[0]['high'],
data['close'].iloc[-1] - current_data[0]['low'])
tr_list.append(tr_new)
context.atr = int(np.floor(np.mean(tr_list)))
context.atr_half = int(np.floor(0.5 * context.atr))
# 计算唐奇安通道
context.don_open = np.max(data['high'].values[-context.n:])
context.don_close = np.min(data['low'].values[-context.n:])
# 计算加仓点和止损点
context.long_add_point = context.don_open + context.atr_half
context.long_stop_loss = context.don_open - context.atr_half
context.short_add_point = context.don_close - context.atr_half
context.short_stop_loss = context.don_close + context.atr_half
def on_bar(context, bars):
# 提取数据
symbol = bars[0]['symbol']
recent_data = context.data(symbol=context.symbol, frequency='60s', count=2, fields='close,high,low')
close = recent_data['close'].values[-1]
# 账户仓位情况
position_long = context.account().position(symbol=symbol, side=PositionSide_Long)
position_short = context.account().position(symbol=symbol, side=PositionSide_Short)
# 当无持仓时
if not position_long and not position_short:
# 如果向上突破唐奇安通道,则开多
if close > context.don_open:
order_volume(symbol=symbol, side=OrderSide_Buy, volume=context.atr, order_type=OrderType_Market, position_effect=PositionEffect_Open)
print('开多仓atr')
# 如果向下突破唐奇安通道,则开空
if close < context.don_close:
order_volume(symbol=symbol, side=OrderSide_Sell, volume=context.atr, order_type=OrderType_Market, position_effect=PositionEffect_Open)
print('开空仓atr')
# 有持仓时
# 持多仓,继续突破(加仓)
if position_long:
# 当突破1/2atr时加仓
if close > context.long_add_point:
order_volume(symbol=symbol, volume=context.atr_half, side=OrderSide_Buy, order_type=OrderType_Market,position_effect=PositionEffect_Open)
print('继续加仓0.5atr')
context.long_add_point += context.atr_half
context.long_stop_loss += context.atr_half
# 持多仓,止损位计算
if close < context.long_stop_loss:
volume_hold = position_long['volume']
if volume_hold >= context.atr_half:
order_volume(symbol=symbol, volume=context.atr_half, side=OrderSide_Sell, order_type=OrderType_Market, position_effect=PositionEffect_Close)
else:
order_volume(symbol=symbol, volume=volume_hold, side=OrderSide_Sell, order_type=OrderType_Market,position_effect=PositionEffect_Close)
print('平多仓0.5atr')
context.long_add_point -= context.atr_half
context.long_stop_loss -= context.atr_half
# 持空仓,继续突破(加仓)
if position_short:
# 当跌破加仓点时加仓
if close < context.short_add_point:
order_volume(symbol = symbol, volume=context.atr_half, side=OrderSide_Sell, order_type=OrderType_Market, position_effect=PositionEffect_Open)
print('继续加仓0.5atr')
context.short_add_point -= context.atr_half
context.short_stop_loss -= context.atr_half
# 持多仓,止损位计算
if close > context.short_stop_loss:
volume_hold = position_short['volume']
if volume_hold >= context.atr_half:
order_volume(symbol=symbol, volume=context.atr_half, side=OrderSide_Buy, order_type=OrderType_Market, position_effect=PositionEffect_Close)
else:
order_volume(symbol=symbol, volume=volume_hold, side=OrderSide_Buy, order_type=OrderType_Market,position_effect=PositionEffect_Close)
print('平空仓0.5atr')
context.short_add_point += context.atr_half
context.short_stop_loss += context.atr_half
if __name__ == '__main__':
'''
strategy_id策略ID,由系统生成
filename文件名,请与本文件名保持一致
mode实时模式:MODE_LIVE回测模式:MODE_BACKTEST
token绑定计算机的ID,可在系统设置-密钥管理中生成
backtest_start_time回测开始时间
backtest_end_time回测结束时间
backtest_adjust股票复权方式不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST
backtest_initial_cash回测初始资金
backtest_commission_ratio回测佣金比例
backtest_slippage_ratio回测滑点比例
'''
run(strategy_id='strategy_id',
filename='main.py',
mode=MODE_BACKTEST,
token='token',
backtest_start_time='2020-02-15 09:15:00',
backtest_end_time='2020-09-01 15:00:00',
backtest_adjust=ADJUST_PREV,
backtest_initial_cash=1000000,
backtest_commission_ratio=0.0001,
backtest_slippage_ratio=0.0001)