TQuant Lab 損失規避策略 — 真實波動幅度均值

損失規避策略
Photo by 向左看:認知與管理損失規避偏誤 on PIMCO

本文重點概要

  • 文章難度:★★☆☆☆
  • 以真實波動幅度均值指標(ATR)判斷與設置止損點
  • 本文改編自如何避免投資人常犯錯誤-損失規避,以 TQuant Lab 回測平台撰寫交易策略並回測風險與績效。

前言

什麼是行為財務學?是指投資人受到心理偏誤的影響而有不理性的行為,與現代經濟學理論的假設相左。當Daniel Kahneman(康納曼)和他的合作者Amos Tversky(特沃斯基)於2002年獲得諾貝爾獎時,他們被公認為推動了行為經濟學的發展。他們率先觀察到一個人類行為的現象:當人們面臨相等金額的損失和收益時,他們對於損失的痛苦感受比獲得收益時的快樂感覺更加強烈,這一現象被稱為「損失厭惡」。他們以及其他研究學者發現,人們需要比失去的金額高出至少一倍的收益,才能在心理上平衡對風險的承受能力。為了避免量化投資的過度理性,以及對損失波動的忽略,「停損點設置」在策略應用中是不可忽視的一環。

真實波動幅度均值(Average True Range,ATR)是一種衡量金融資產價格波動性的指標。它由 J. Welles Wilder 開發,旨在衡量特定時間段內資產價格的波動程度。ATR通常用作技術分析的工具,幫助交易者了解特定資產的波動情況,進而決定進場、出場和止損點。

ATR的計算基於「真實波動幅度」(True Range)。真實波幅代表特定時間內的最大價格變動範圍,通常由以下三個差值中的最大值來確定:

  1. 當日最高價與最低價之差
  2. 前日收盤價與當日最高價之差
  3. 前日收盤價與當日最低價之差

計算 ATR 的一般步驟如下:

  1. 計算「真實波動幅度」(True Range,TR)。
  2. 對 TR 取移動平均數,得到 ATR。

ATR 能夠幫助交易者評估資產的波動情況,當 ATR 值較高時,表示該資產價格波動較大;而 ATR 值較低時,代表該資產價格較為穩定。

布林通道 + ATR 損失規避策略

  • 若當日收盤價跌破布林下軌且現金水位足夠時,於隔日買入1000股。
  • 若當日收盤價突破布林上軌且持有部位時,於隔日出清部位。
  • 若當日收盤價跌破布林下軌、現金水位足夠,且當日收盤價未跌破停損點,隔日加碼1000股。
  • 若當日收盤價跌破停損點且持有部位時,於隔日出清部位。
    ※停損點計算:當日收盤價 – k × ATR ( k 值依股價波動度而定,若波動度較大,可設定較大的k值,反之亦然。)

本文將以「布林通道 + ATR 損失規避策略」作為實驗組;以一般常見之「布林通道」策略作為對照組,觀察利用損失規避策略的實驗組能否獲得較好的績效表現。

編輯環境與模組需求

本文使用 Windows 11 並以 Jupyter Lab 作為編輯器。

資料導入與環境設定

資料期間從 2022–01–01 至 2023–01–01,以台積電 (2330) 作為實例。

import os
import tejapi
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# set tej_key and base
tej_key = 'Your Key'
api_base = 'https://api.tej.com.tw'

os.environ['TEJAPI_KEY'] = tej_key
os.environ['TEJAPI_BASE'] = api_base

# set date
start = '2020-01-01'
end = '2023-01-01'

os.environ['mdate'] = start + ' ' + end
os.environ['ticker'] = '2330'

!zipline ingest -b tquant

建立 Pipeline 函式

Pipeline() 提供使用者快速處理多檔標的的量化指標與價量資料的功能,於本次案例我們用以處理:

  1. 利用客製化因子的函式計算ATR (客製化因子函式的詳細操作方式可參考:Custom Factors)
  2. 計算布林通道上、中、下軌 (以20日為計算窗格)
from zipline.data import bundles
from zipline.pipeline import Pipeline
from zipline.TQresearch.tej_pipeline import run_pipeline
from zipline.pipeline.data import EquityPricing, TWEquityPricing
from zipline.pipeline.factors import BollingerBands, TrueRange, CustomFactor
from zipline.utils.math_utils import nanmax
from numpy import dstack

start_time = pd.Timestamp(start, tz = 'UTC')
end_time = pd.Timestamp(end,tz = 'UTC')
bundle = bundles.load('tquant')

class AverageTrueRange(CustomFactor):

    inputs = (
        EquityPricing.high,
        EquityPricing.low,
        EquityPricing.close,
    )
    
    window_length = 20

    outputs = ["TR", "SMA_ATR"]
    
    def compute(self, today, assets, out, highs, lows, closes):

        high_to_low = highs[1:] - lows[1:]
        high_to_prev_close = abs(highs[1:] - closes[:-1])
        low_to_prev_close = abs(lows[1:] - closes[:-1])
        tr_current = nanmax(
            dstack(
                (
                    high_to_low,
                    high_to_prev_close,
                    low_to_prev_close,
                )
            ),
            2,
        )

        sma_atr_values = np.mean(tr_current, axis=0)
        
        out.TR = tr_current[-1]
        out.SMA_ATR = sma_atr_values

def make_pipeline():
    
    ATR = AverageTrueRange(inputs = [TWEquityPricing.high,
                                     TWEquityPricing.low,
                                     TWEquityPricing.close])
    perf = BollingerBands(inputs=[EquityPricing.close], window_length=20, k=2)
    upper,middle,lower = perf.upper, perf.middle, perf.lower
    curr_price = EquityPricing.close.latest
    
    return Pipeline(
        columns={
            'SMA_ATR': ATR.SMA_ATR,
            'upper': upper,
            'middle': middle,
            'lower': lower,
            'curr_price': curr_price
        },
    )

建立 initialize 函式

initialize() 函式用於定義交易開始前的每日交易環境,於此例中我們設置:

  • 流動性滑價
  • 交易手續費
  • 買入持有台積電(2330)的報酬作為基準
  • 將 Pipeline 導入交易流程中
  • 設定 context.last_buy_price 來記錄前一次交易的價格
  • 設定 context.stop_loss 記錄停損點價位
from zipline.api import *
from zipline.finance import commission, slippage

def initialize(context):
    set_slippage(slippage.VolumeShareSlippage())
    set_commission(commission.PerShare(cost = 0.001425 + 0.003 / 2))
    set_benchmark(symbol('2330'))
    attach_pipeline(make_pipeline(), 'mystrategy')
    context.last_buy_price = 0
    context.stop_loss = 0

建立 handle_data 函式

handle_data() 為構建交易策略的重要函式,會在回測開始後每天被呼叫,主要任務為設定交易策略、下單與紀錄交易資訊。

def handle_data(context, data):
    out_dir = pipeline_output('mystrategy')  # 取得每天 pipeline 的布林通道上中下軌 & ATR

    for i in out_dir.index: 
        curr_price = out_dir.loc[i, 'curr_price']
        upper = out_dir.loc[i, 'upper']
        lower = out_dir.loc[i, 'lower']
        atr = out_dir.loc[i, 'SMA_ATR']
        
        cash_position = context.portfolio.cash  # 記錄現金水位
        stock_position = context.portfolio.positions[i].amount  # 記錄股票部位

        loss_stopped, buy, sell = False, False, False
        record(price = curr_price, upper = upper, lower = lower, stop_loss = context.stop_loss, buy = buy, sell = sell, loss_stopped = loss_stopped)

        # 收盤價 > 止損價的狀況:
        if (curr_price > context.stop_loss) or (context.last_buy_price == 0):
            
            # 若收盤價 <= 布林下軌,則買入或加碼
            if (curr_price <= lower) and (cash_position >= curr_price * 1000):
                order(i, 1000)
                buy = True
                record(buy = buy)
                context.stop_loss = curr_price - (0.1 * atr)
                
            # 若收盤價 >= 布林上軌,則出清部位
            elif (curr_price >= upper) and (stock_position >= 1000):
                order_target(i, 0)
                sell = True
                record(sell = sell)
            else:
                pass
        else:
            pass

        # 若收盤價 <= 停損價,則出清部位
        if (curr_price <= context.stop_loss) and (stock_position > 0):
            order_target(i, 0)
            sell = True
            loss_stopped = True
            record(sell = sell, loss_stopped = loss_stopped)
        else:
            pass

建立 analyze 函式

analyze() 主要用於回測後視覺化策略績效與風險,這裡我們以 matplotlib 繪製投組價值表並畫出台積電股價走勢、布林上下軌以及買賣時機點。

def analyze(context, perf):
    fig = plt.figure()
    ax1 = fig.add_subplot(211)
    perf.portfolio_value.plot(ax=ax1)
    ax1.set_ylabel("Portfolio value (NTD)")
    ax2 = fig.add_subplot(212)
    ax2.set_ylabel("Stock Price (NTD)")
    perf.price.plot(ax=ax2)
    perf.upper.plot(ax=ax2)
    perf.lower.plot(ax=ax2)
    
    buy_status, sell_status = perf.buy, perf.sell
    buy_status.fillna(False, inplace=True)
    sell_status.fillna(False, inplace=True)
    
    ax2.plot( # 繪製買入訊號
        perf.index[perf.buy],
        perf.loc[perf.buy, 'price'],
        '^',
        markersize=5,
        color='red'
    )
    ax2.plot( # 繪製賣出訊號
        perf.index[perf.sell],
        perf.loc[perf.sell, 'price'],
        'v',
        markersize=5,
        color='green'
    )
    
    plt.legend(loc=0)
    plt.gcf().set_size_inches(18,8)
    plt.show()

執行交易策略

使用 run_algorithm() 執行上述設定的交易策略,設置交易期間為 start_time(2022-01-01) 到 end_time(2022-12-31),所使用資料集為 tquant,初始資金為 10,000,000 元。其中輸出的 results 就是每日績效與交易的明細表。

因本文利用實驗組與對照組來觀察設定止損點的效果,因此以下將分別呈現兩個組別的結果,以利我們進行止損效果的分析。

from zipline import run_algorithm

results = run_algorithm(
    start = start_time,
    end = end_time,
    initialize=initialize,
    bundle='tquant',
    analyze=analyze,
    capital_base=1e7,
    handle_data = handle_data
)

results
損失規避策略
實驗組投組資產價值、交易買賣點與交易結果
損失規避策略
對照組投組資產價值、交易買賣點與交易結果

將實驗組與對照組兩相比對之下,很明顯地我們可以發現實驗組的資產價值因為有設定止損點,所以僅有小幅的虧損,且整體資產有向上獲利的空間。反觀對照組的資產價值大部分的時間都在承受虧損,最大的虧損超過6%。

再來我們可以透過交易買賣點的圖表觀察實驗組止損的時機。可以看到3月時,我們的策略進行了兩次止損(綠色箭頭為賣出,紅色為買入),使虧損的幅度僅有 0.5%,且很好的規避了4~5 月的空頭走勢。另外,9月及10月也分別進行了一次止損,反觀未進行止損的對照組,在持續的加碼攤平之下,資產價值也逐漸縮水,雖然最後出場時有賺到波段價差,使資產回復初始的水位,但投資人也必須承受9至10月的下行風險。

利用 pyfolio 進行績效評估

進行績效評估前,我們需要先將results中的資料細分為以下部分:

  • return: 投組每日報酬
  • positions: 持有部位資料表
  • transactions: 交易明細資料表
  • benchmark_rets: benchmark 買進持有的報酬率
import pyfolio as pf 
returns, positions, transactions = pf.utils.extract_rets_pos_txn_from_zipline(results)
benchmark_rets = results['benchmark_return']

繪製夏普比率比較圖

平均而言,有進行止損的實驗組夏普比率有 0.9 左右,而對照組僅有 0.1,且實驗組的夏普比率在回測期間皆大於 0,對照組則低至 – 0.1 左右

from pyfolio.plotting import plot_rolling_sharpe
plot_rolling_sharpe(returns, factor_returns = benchmark_rets)
損失規避策略
實驗組夏普比率

損失規避策略
對照組夏普比率

比較前 5 大交易回撤期間

從圖表中可以看到沒有設定止損的對照組通常會有較大的交易回撤,最大的回撤達到 7.14%,而實驗組的最大回撤則只有 2.8%,且除了最大回撤之外,其餘回撤都小於 1%

from pyfolio.plotting import show_worst_drawdown_periods
show_worst_drawdown_periods(returns, top=5)
損失規避策略
實驗組前 5 大交易回撤期間

損失規避策略
對照組前 5 大交易回撤期間

結論

本次實作為了達成「損失規避」的效果,我們將布林通道策略搭配真實波動幅度均值(ATR),以達到止損的目的,同時我們將沒有設定止損的布林策略當作對照組,用以凸顯ATR止損的成效。

從以上的分析結果,我們可以看到 ATR 的確幫助我們達成「損失規避」,尤其在空頭走勢中,止損點的設立讓我們有效的避開資產縮水的風險。搭配 TQuant Lab 中的 Pyfolio 績效評估工具來看,我們也可以發現損失規避策略幫助我們有效地將平均夏普比率從 0.1 提升到 0.9,意味著投資人在承受相同的風險下,能獲取較高的報酬。此外,損失規避策略也能大幅縮小交易期間的回撤,使投資人的資金不至於過度流失。

溫馨提醒,本次策略與標的僅供參考,不代表任何商品或投資上的建議。之後也會介紹使用TEJ資料庫來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購 TQuant Lab 的相關方案,用高品質的資料庫,建構出適合自己的交易策略。

【TQuant Lab回測系統】解決你的量化金融痛點

全方位提供交易回測所需工具

原始碼

延伸閱讀

相關連結

返回總覽頁
Procesing