最近看到一个很牛逼的策略,海龟交易策略,其实这个策略也是老古董了,但是它是一套极为完整的交易系统,经过时间的考验后依旧经久不衰,太棒了。

海龟交易策略

总有人说这是个垃圾,但它提供了一套交易思路,最亮眼的就是仓位管理机制。原版的策略里,直观可以理解为:如果从日线的角度来看,通过计算ATR,每一次建仓,每天最极端的变动会使得总资金的变动幅度不超过所设定的risk value。

策略核心理念

趋势跟踪:利用价格突破信号捕捉市场趋势。

仓位管理:通过计算波动性,确保每次建仓时单笔风险不超过预设比例(例如1%)。

严格规则:制定明确的加仓、止损和止盈规则,保证策略的执行纪律。

Average True Range, ATR, 平均真实波幅

下面是原版海龟的ATR计算方法

真实波幅(TR)计算公式:
$$
TR = \max(H - L,, |H - PDC|,, |PDC - L|)
$$

  • H:当日最高价
  • L:当日最低价
  • PDC:前一日收盘价

ATR 计算公式
$$
ATR = \frac{19 \times PATR + TR}{20}
$$

  • PATR:前一日的ATR值
  • 注意:首次计算ATR时,不能使用递推公式,而是用最近20日真实波幅的简单平均值。

从ATR的计算过程可以感受到,如果我想让每一个仓位(Unit,做建仓的股数)每天的变动幅度所带来的影响不超过总资金的百分之一,可以设置risk value为0.01,从而计算:
$$
Unit = \frac{risk\ value \times Total_net}{ATR}
$$
其中,Total_net 表示总资产净值。

Donchian Channel, 唐奇安通道

唐奇安通道规则为:当最高价高于前X个K的最大最高价时,做多;当最低价低于前X个K的最小最低价时,做空。如果你想对往后回溯多少K进行优化,你会发现在不同市场会得到不同的结果,甚至同一市场不同时期最优值也是不同的。但是一般默认值为20。

该指标的计算方法,设定常用参数为20日,则:

  • 上轨(Upper Channel):20日最高价
    $$
    Upper\ Channel = 20\ Day\ High
    $$

  • 下轨(Lower Channel):20日最低价
    $$
    Lower\ Channel = 20\ Day\ Low
    $$

  • 中轨(Middle Channel):上轨与下轨均值
    $$
    Middle\ Channel = \frac{20\ Day\ High + 20\ Day\ Low}{2}
    $$

唐奇安通道

交易流程

海龟策略的交易流程分为建仓、加仓、止损与止盈四个部分。

建仓时机

  • 突破信号:
    • 当前价格突破上轨时,产生买入信号;
    • 当前价格跌破下轨时,产生卖空信号。
  • 初始仓位:建仓时先买入/卖空1个Unit。

加仓规则

  • 多仓加仓:当价格在上一次建仓或加仓后上涨了0.5ATR时,加仓1个Unit。
  • 空仓加仓:当价格在上一次建仓或加仓后下跌了0.5ATR时,加仓1个Unit。

海龟策略本质上是一种“追涨杀跌”的交易方法,通过不断加仓来顺应趋势。

止损策略

  • 多仓止损:若价格在上一次建仓或加仓基础上下跌2N,则卖出所有多仓以止损。
  • 空仓止损:若价格在上一次建仓或加仓基础上上涨2N,则平掉所有空仓以止损。

止盈策略

利用10日唐奇安通道判断止盈时机:

  • 多仓止盈:当价格跌破10日通道的下轨时,清空所有多仓,结束策略。
  • 空仓止盈:当价格突破10日通道的上轨时,清空所有空仓,结束策略。

QuantConnect上简单回测

这是一个量化交易平台,就不介绍了,用这个平台回测2024全年,上述策略在AAPL股票上的表现。

#
#                     _ooOoo_
#                    o8888888o
#                    88" . "88
#                    (| -_- |)
#                     O\ = /O
#                 ____/`---'\____
#               .   ' \\| |// `.
#                / \\||| : |||// \
#              / _||||| -:- |||||- \
#                | | \\\ - /// | |
#              | \_| ''\---/'' | |
#               \ .-\__ `-` ___/-. /
#            ___`. .' /--.--\ `. . __
#         ."" '< `.___\_<|>_/___.' >'"".
#        | | : `- \`.;`\ _ /`;.`/ - ` : | |
#          \ \ `-. \_ __\ /__ _/ .-` / /
#  ======`-.____`-.___\_____/___.-`____.-'======
#                     `=---='
# 
#  .............................................
#           佛祖保佑             永无BUG
#


# region imports
from AlgorithmImports import *
from datetime import timedelta
# endregion
class TurtleTrading(QCAlgorithm):
    def initialize(self):
        self.set_start_date(2024, 1, 1)
        self.set_end_date(2025, 1, 31)
        self.set_cash(10000000)

        self.aapl = self.add_equity('AAPL', Resolution.DAILY, data_normalization_mode=DataNormalizationMode.RAW).symbol

        self.entry_length = 20
        self.exit_length = 10
        self.atr_length = 14
        self.risk_percent = 0.01
        self.max_position = 10
        self.donchian_entry = self.dch(self.aapl, self.entry_length, self.entry_length)
        self.donchian_exit = self.dch(self.aapl, self.exit_length, self.exit_length)

        self.long_entry = float('inf')
        self.short_entry = 0
        self.long_exit = 0
        self.short_exit = float('inf')

        self.set_warm_up(50)
        self._atr = self.atr(self.aapl, self.atr_length, MovingAverageType.Simple, Resolution.DAILY)

        self.entry_price = 0
        self.position_count = 0
        self.stop_loss = 0
        self.consolidate(self.aapl, timedelta(days=1), self.on_consolidated)

    def on_consolidated(self, bar: TradeBar):
        # Plot the candlestick data
        self.plot("DonchianChannel", "AAPL", bar)


    def on_data(self, data: Slice):
        if not (self.donchian_entry.is_ready and self.donchian_exit.is_ready and self._atr.is_ready):
            return

        current_price = self.securities[self.aapl].price

        if self.donchian_entry.is_ready:
            # self.debug('dc is ready')
            # The current value of self._dch is represented by self._dch.current.value
            self.plot("DonchianChannel", "dch", self.donchian_entry.current.value)
            # Plot all attributes of self._dch
            self.plot("DonchianChannel", "upper_band", self.donchian_entry.upper_band.current.value)
            self.plot("DonchianChannel", "lower_band", self.donchian_entry.lower_band.current.value)


        if self._atr.is_ready:
            # The current value of self._atr is represented by self._atr.current.value
            self.plot("AverageTrueRange", "atr", self._atr.current.value)

        holdings = self.portfolio[self.aapl].quantity
        dollar_risk = self.portfolio.total_portfolio_value * self.risk_percent
        atr_value = self._atr.current.value
        contract_value = current_price
        position_size = dollar_risk / (atr_value)

        if holdings == 0 and current_price > self.long_entry:
            self.debug('long')
            self.entry_price = current_price
            self.stop_loss = current_price - 2 * atr_value
            self.position_count = 1
            self.market_order(self.aapl, position_size)
        elif holdings == 0 and current_price < self.short_entry:
            self.debug('short')
            self.entry_price = current_price
            self.stop_loss = current_price + 2 * atr_value
            self.position_count = 1
            self.market_order(self.aapl, -position_size)
        elif abs(holdings) < self.max_position * position_size:
            if (holdings > 0 and current_price > self.entry_price + 0.5 * atr_value) or \
               (holdings < 0 and current_price < self.entry_price - 0.5 * atr_value):
                self.position_count += 1
                self.entry_price = current_price
                self.market_order(self.aapl, position_size if holdings > 0 else -position_size)
        if (holdings > 0 and current_price < self.stop_loss) or \
           (holdings < 0 and current_price > self.stop_loss):
            self.liquidate(self.aapl)
            self.position_count = 0
        if holdings > 0 and current_price < self.long_exit:
            self.liquidate(self.aapl)
        elif holdings < 0 and current_price > self.short_exit:
            self.liquidate(self.aapl)

        self.long_entry = self.donchian_entry.upper_band.current.value
        self.short_entry = self.donchian_entry.lower_band.current.value
        self.long_exit = self.donchian_exit.lower_band.current.value
        self.short_exit = self.donchian_exit.upper_band.current.value

原版策略

可以看到,原版的策略表现并不好,但依旧提供了一个不错的思路以及在仓位管理上的启发,理解整体思路后完全可以自己继续改进策略,我们以后再见。