本文重點概要
- 文章難度:★★★☆☆
- 介紹一目均衡表及移動止損。
- 以 TQuant Lab 回測平台撰寫一目均衡表策略並回測風險與績效。
前言
一目均衡表(又稱雲帶圖,いちもくきんこうひょう、英:Ichimoku Kinko Hyo) 是由日本記者細田悟一 (ほそだごいち/ Hosoda Goichi) 發明,但當時並沒有對外公開。過了 40 年後,在 1975 年以後的 6 年間,細田悟一用筆名一目山人 (いちもくさんじん / Ichimoku Sanjin)以「一目均衡表」為名發表了七本系列著作,在其中詳細講解了這個指標與其交易系統背後的哲學思想。
與一般股票價格受價量關係、買賣供需、企業基本面等傳統觀念不同,一目均衡表認為,時間是影響價格變化的重要因素,尤其是在時間的影響下,價格實際上有週期性的變化。市場總會有股票漲久了就擔心會跌,跌久了就漲的心理預期。而市場整體心理預期反應在市場行為上造就了一種波動的規律,這個規律波動的週期,是一目均衡表「時間理論」的基礎。
除此之外,一目均衡表其實是相當具有哲學性的的一個技術分析指標,與其說是一種指標,筆者認為更應該將其視為細田悟一對市場乃至於對社會的剖析與反思,其中思想相當值得我們深入了解。由於篇幅原因,本文不在此贅述,筆者認為一目均衡表理論與市場平衡對於一目均衡表有不錯的見解,有興趣者可以參考。
一目均衡表架構介紹
時間理論
一目均衡表的時間論是分析時間與市場轉換點的方法。「9、17、26」為基本數値,可分析在這期間市場是否容易出現轉換,為重要的數値。
解讀方法
- 轉換線(tankan_sen):轉換線(轉折線)為過去 9 天最高價與最低價的平均線。若轉換線上行,則判斷上升的力道大,若下行則判斷下跌走勢強。由於是根據 9 天的短期間算出,因此被用來分析短期間的趨勢。
- 基準線(kijun_sen):基準線為過去 26 天最高價與最低價的平均線。若基準線上行,則判斷上升力道大,若下行則判斷下跌走勢強。由於是以 26 天的期間算出,因此被用來分析中期的趨勢。
- 先行帶 A / B(senkou_span_a/b):先行帶 A 是以轉換線與基準線平均値預測未來 26 天的趨勢。先行帶 B 是以過去 52 天的最高價與最低價的平均値預測未來 26 天趨勢的線。由於兩者皆顯示平均値未來 26 天的趨勢,因此有助於分析未來的價格變動。先行帶 A 與先行帶 B 之間塗上色後,該部份就稱作「雲區或雲帶」,A < B的部分為綠色,A > B的部分則為紅色。
- 延遲線(chikou_span):延遲線為當天收盤價前 26 天的股價。延遲線若位於 K 線上方,表示市場走勢的上升力道強,若位於 K 線下方,則市場走勢的下跌走勢強。
三役好轉與三役逆轉
一目均衡表中還有所謂的三役好轉與三役逆轉的判讀方法。三役好轉代表買進的訊號,三役逆轉則是賣出的訊號。通常在股價盤整區間外,若滿足三役好轉會有較強烈的上升趨勢,反之亦然。本文會根據三役好轉微調後進行買進訊號的產生。
三役好轉的條件:
- 轉換線超出基準線
- 延遲線(遲行帶)在股價之上
- 價格超出雲區(雲帶)
三役逆轉的條件:
- 轉換線低於基準線
- 延遲線(遲行帶)在股價下
- 價格低於雲區(雲帶)
編輯環境與模組需求
本文使用 Mac OS 以及 Jupyter Notebook 作為編輯器。
資料導入及使用 get_universe 函式取得股票池
本策略先從 2018-04-01 到 2019-03-31 共一年,抓取包含上市櫃之普通股所有股票代碼。
import os import tejapi import pandas as pd import numpy as np os.environ['TEJAPI_KEY'] = "your key" os.environ['TEJAPI_BASE'] = "https://api.tej.com.tw" start = '2018-04-01' end = '2019-03-31' from zipline.sources.TEJ_Api_Data import get_universe pool = get_universe(start, end, mkt = ['TWSE', 'OTC'], stktp_e=['Common Stock-Foreign', 'Common Stock']) len(pool)
使用 TEJToolAPI 抓取 MSCI 成分股
一目均衡表策略從上述的股票代碼中,利用 TEJToolAPI 篩選出 MSCI 成分股作為回測成分股。
average_vol = df.groupby('股票代碼')['成交量_千股'].mean().reset_index() average_vol = average_vol.rename(columns={'成交量_千股': '平均成交量'}) high_price_stocks = df.groupby('股票代碼')['最低價'].min().reset_index() high_price_stocks = high_price_stocks[high_price_stocks['最低價'] >= 50] merge_data = pd.merge(average_vol, high_price_stocks, on='股票代碼', how='inner') top_100_vol = merge_data.sort_values(by='平均成交量', ascending=False).head(100) top_100_vol
df = df.drop_duplicates(subset=['coid']) df_filtered = df[df['Component_Stock_of_MSCI_TW_Fg'] == 'Y'] new_pool = df_filtered['coid'].to_list() len(new_pool)
剔除重複欄位,篩選出共 91 檔成分股
將資料導入 bundle
為避免前視偏誤,本策略之回測期間從 2019-04-01 至 2024-04-01,共五年,並使用上述之 91 檔成分股,並加入台股加權報酬指數指數 IR0001,作為大盤比較。
下圖為 91 檔成分股列表及加權報酬指數:
!zipline ingest -b tquant !zipline bundle-info
建立 Pipeline 函式
建立 Custom Factor 函數
CustomFactor 可以讓使用者自行設計所需的客製化因子,於本次案例我們用以處理:
- 真實波動幅度均值(AverageTrueRange):作為移動止損點之計算
Pipeline() 提供使用者快速處理多檔標的的量化指標與價量資料的功能,於本次案例我們用以處理:
- 轉換線(tankan_sen)
- 基準線(kijun_sen)
- 先行帶 A / B(senkou_span_a/b)
- 雲區或雲帶(cloud_red)
- 延遲線(chikou_span)from zipline.pipeline import Pipeline from zipline.TQresearch.tej_pipeline import run_pipeline def make_pipeline(): Ich = IchimokuKinkoHyo( inputs = [TWEquityPricing.high, TWEquityPricing.low, TWEquityPricing.close], window_length = 52, ) atr = AverageTrueRange(inputs = [TWEquityPricing.high, TWEquityPricing.low, TWEquityPricing.close], window_length = 52, ) return Pipeline( columns = { 'curr_price': TWEquityPricing.close.latest, "tenkan_sen": Ich.tenkan_sen, "kijun_sen": Ich.kijun_sen, "senkou_span_a": Ich.senkou_span_a, "senkou_span_b": Ich.senkou_span_b, 'cloud_red': Ich.senkou_span_a < Ich.senkou_span_b, "chikou_span": Ich.chikou_span, 'stop_loss': atr.ATR, }, # screen = ~StaticAssets([benchmark_asset]) screen = ~StaticAssets([benchmark_asset]) & (Ich.senkou_span_a > 0) & (Ich.senkou_span_b > 0) ) my_pipeline = run_pipeline(make_pipeline(), start_dt, end_dt) my_pipeline
建立 initialize 函式
initialize() 函式用於定義交易開始前的每日交易環境,與此例中我們設置:
- 滑價成本
- 台股市場手續費模型
- 加權報酬指數 ( IR0001 ) 作為 Benchmark
- 將 Pipeline 設計的一目均衡表指標導入交易流程中
- 設定 context.stop_loss 變數,將回測中的止損點紀錄
- 設定 context.holding 變數,紀錄是否持有該股票部位
- 設定 context.trailing_stop 變數,紀錄是否進行移動止損
- 設定 context.last_buy_price 變數,紀錄最後買入價格
- 設定 context.trailing_count 變數,紀錄移動止損次數
- 設定 context.buy_count 變數,限制每檔股票最大交易單數from zipline.finance import slippage, commission from zipline.api import * def initialize(context): set_slippage(slippage.VolumeShareSlippage()) set_commission(commission.Custom_TW_Commission(min_trade_cost = 20, discount = 1.0, tax = 0.003)) attach_pipeline(make_pipeline(), 'mystrats') set_benchmark(symbol('IR0001')) context.stop_loss = {} context.trailing_stop = {} context.last_buy_price = {} context.trailing_count = {} context.holding = {} context.buy_count = {}
建立 handle_data 函式
handle_data() 為構建一目均衡表策略的重要函式,會在回測開始後每天被呼叫,主要任務為設定交易策略、下單與紀錄交易資訊。
註:為避免一目均衡表在股價盤整區間發出錯誤進場訊號的可能,本文將 tenkan_sen > kijun_sen 做額外 *1.01 的方式,期望在上升趨勢足夠明顯時才入場,達到一定程度的迴避盤整區間。
關於本策略的交易詳細規則請至:一目均衡表.ipynb
# 三役好轉 (tenkan_sen > kijun_sen*1.015 : avoid the Darvas Box Theory) if (curr_price > senkou_span_b) and (cloud_red == True) and (tenkan_sen > kijun_sen*1.01) and (context.buy_count[f'{i}'] <= 5): order_percent(i, 0.01) buy = True context.stop_loss[f'{i}'] = curr_price - (1.25 * stop_loss) context.last_buy_price[f'{i}'] = curr_price record( **{ f'buy{sym}':buy } ) context.holding[f'{i}'] = True context.buy_count[f'{i}'] += 1 # reset stop loss point if (curr_price >= (1.3**context.trailing_count[f'{i}'])*context.last_buy_price[f'{i}']) and (context.holding[f'{i}'] == True) and (context.trailing_stop[f'{i}'] == False): context.stop_loss[f'{i}'] = 1.3*context.stop_loss[f'{i}'] context.trailing_stop[f'{i}'] = True context.trailing_count[f'{i}'] += 1 elif (curr_price >= (1.3**context.trailing_count[f'{i}'])*context.last_buy_price[f'{i}']) and (context.holding[f'{i}'] == True) and (context.trailing_stop[f'{i}'] == True): context.stop_loss[f'{i}'] = 1.3*context.stop_loss[f'{i}'] context.trailing_count[f'{i}'] += 1 if (curr_price <= context.stop_loss[f'{i}']) and (context.holding[f'{i}'] == True): order_target(i, 0) sell = True context.stop_loss[f'{i}'] = None context.trailing_stop[f'{i}'] = None context.trailing_count[f'{i}'] = None record( **{ f'sell{sym}':sell } ) context.holding[f'{i}'] = None context.buy_count[f'{i}'] = None
執行交易策略
使用 run_algorithm() 執行上述設定的一目均衡表策略,設置交易期間為 start_dt(2019-04-01) 到 end_dt(2024-04-01),使用資料集 tquant,初始資金為兩千萬元。其中輸出的 results 就是每日績效與交易的明細表。
import matplotlib.pyplot as plt from zipline import run_algorithm results = run_algorithm( start = start_dt, end = end_dt, initialize = initialize, bundle = 'tquant', analyze = analyze, capital_base = 2e7, handle_data = handle_data ) results
利用 Pyfolio 進行績效評估
import pyfolio as pf returns, positions, transactions = pf.utils.extract_rets_pos_txn_from_zipline(results) benchmark_rets = results['benchmark_return'] pf.tears.create_full_tear_sheet(returns=returns, positions=positions, transactions=transactions, benchmark_rets=benchmark_rets )
可以看到一目均衡表策略在這 58 個月中我們獲得了 23.554% 的年化報酬,累積報酬 178.194%,獲利表現相當優秀;夏普率在 1.38 更是相當好的數值,顯示策略報酬相對較高而風險較低;而在 β 值上,β 值在 0.66 代表該策略波動相對總體市場的波動算是相當不敏感,反映出一目均衡表本身抽離市場的核心概念。另外在最大回撤這部分,可以看到比較主要的下跌集中在 22 年至 23 年間疫情間導致的系統性風險提升,其他的波段都處於穩定成長的趨勢,可以推論利用一目均衡表之三役好轉的進場,搭配 ATR 移動止損的策略,是一個長期下來比較穩健且能穩定獲取超越大盤報酬的好策略。
可以看到每檔持有時間都相當固定,不太會有過度偏向哪一支股票的問題。
查看特定股票之一目均衡表策略表現
graph(2330, True)
由於是本策略是一個選股回測,並非單股回測,因此無法快速確認每檔股票在一目均衡表中的實際進出場情況如何,因此筆者額外寫了一個 function,方便我們查看個股交易情況(詳情請至GitHub原始碼):
比較值得注意的是,一目均衡表三役好轉的進場方式不包含出場判斷,筆者實際測試用三役逆轉的方式進行出場則會損失許多波段的收益,因此本策略使用移動止損的方式進行出場,例如在上圖就可以看到在 2021 年初策略就在台積電(2330)有一個止損的賣出,賺取超過半年的波段。不過除了移動止損的方式,也可以加入止盈或是其他可以停利的手段,以及加入多空並行的策略,有待將來可以再做討論。
結論
本次策略使用一目均衡表進行回測模擬,運用其三役好轉的跡象進行入場,並輔以移動止損進行停利,取得不錯的回測績效,歡迎投資朋友參考。之後也會持續介紹使用 TEJ 資料庫來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購 TQuant Lab 的相關方案,用高品質的資料庫,建構出適合自己的交易策略。
溫馨提醒,本次策略僅供參考,不代表任何商品或投資上的建議。
【TQuant Lab回測系統】解決你的量化金融痛點
全方位提供交易回測所需工具
留言 0