請更新您的瀏覽器

您使用的瀏覽器版本較舊,已不再受支援。建議您更新瀏覽器版本,以獲得最佳使用體驗。

理財

黃金交叉期貨交易策略(MTX)

TEJ 台灣經濟新報

更新於 2025年10月30日12:01 • 發布於 2025年10月23日06:00
Photo by rc.xyz NFT gallery on Unsplash

前言

移動平均線(Moving Average, MA)的思想源於 20 世紀初的 道氏理論(Dow Theory)。道氏理論強調市場具有趨勢性,且趨勢可透過價格本身來觀察,但早期價格資料波動劇烈,缺乏平滑工具。分析者因此開始利用一段期間的平均價格來減少隨機雜訊,這便是移動平均線的濫觴。 隨著統計學與電腦運算的進步,移動平均線逐漸成為量化交易最基礎的技術指標之一。從簡單移動平均(SMA)、指數移動平均(EMA),到更複雜的變體如雙指數移動平均(DEMA)、Hull MA 等,均線的改良目的皆在於解決傳統均線的「延遲性」問題,使其更快速或更平滑地反映價格趨勢。

在應用層面,均線策略廣泛存在於不同市場:

  • 單均線突破:價格突破長期均線(如 200 日)來判斷多空。
  • 雙均線交叉:短期均線與長期均線交叉作為進出場訊號。
  • 多均線排列:觀察均線是否呈現多頭或空頭排列,以確認趨勢強弱。
  • 均線與風控結合:均線訊號搭配停損、波動率或濾網,增強穩健性。

其中,最具代表性的莫過於「黃金交叉(Golden Cross)」與「死亡交叉(Death Cross)」。當短期均線上穿長期均線,往往被視為多頭趨勢啟動;反之則意味空頭來臨。然而,單純依賴交叉訊號在震盪市容易產生假突破,因此實務上常搭配多層停損、風控濾網與期貨換月處理,以提高策略的可行性與穩健性。

本策略即在黃金交叉框架下,結合移動停損與濾網動態管控風險,並應用連續合約處理期貨換月,使回測與實盤保持一致性。

投資標的與回測期間

  • 標的:小台指期(MTX 連續合約)
  • 資料頻率:日線(收盤價)
  • 基準(Benchmark)台灣加權報酬指數 (IR0001)
  • 回測區間2020-01-02 ~ 2025-09-26
  • 合約轉倉:使用連續合約(calendar roll, adjustment=add),確保到期換月的績效連續性

核心邏輯

1. 指標系統 (Indicator System)

  • 使用 MA3 / MA10 辨識趨勢。
  • 黃金交叉為買進觸發;死亡交叉為出場訊號。
  • 為降低雜訊,黃金交叉在程式端加入 1.0001 緩衝條件(短均需明顯高於長均)。

2. 濾網機制(核心風險管理)

  • 開啟條件:當 benchmark 近 2 日跌幅 ≤ −5% 時,濾網 ON

  • 關閉條件(依目前程式實作):

  • 市場反彈關閉benchmark 近 2 日漲幅 ≥ +5% → 濾網 OFF

    • 自動關閉:濾網開啟 200 天 後自動 OFF
  • 濾網 ON 時處置:只要有持倉,無條件強制平倉;且(與停損相同)需等待 5 個交易日 才可再度進場。

3. 進場邏輯(僅在濾網 OFF 時執行)

  • 黃金交叉買進

  • 條件:(MA3[-2] < MA10[-2]) and (MA3[-1] > MA10[-1] * 1.0001)

    • 動作:全倉買進槓桿 1.8 倍
    • 限制:若前次為停損或濾網強平,需 等待 5 日 才能再次進場

4. 出場與風險管理

  • 濾網觸發(ON):有倉位即 強制平倉

  • 濾網未觸發(OFF)時

  • 停損平倉

  • 1 日:return_1d < −5%

    • 5 日:return_5d < −10%
    • 10 日:return_10d < −15%
  • 死亡交叉:(MA3[-2] > MA10[-2]) and (MA3[-1] < MA10[-1]) → 平倉

  • 時間停損持有超過 200 天 → 平倉

5. 合約轉倉 (Contract Rolling)

  • 使用 連續合約(MTX) 自動處理換月,避免因到期造成的價格斷層與績效失真。

交易流程圖

策略特色

  • 動態風險管理:濾網在市場急跌時強化資本保護(ON 強平 + 5 日冷卻)。
  • 避免雜訊交易:黃金交叉加入 MA10[-1] * 1.0001` 設定,降低假突破。
  • 多層出場保護:三段式停損 + 技術指標 + 時間停損。
  • 嚴格風控執行:濾網觸發即時清倉,避免承擔下行尾部風險。
  • 轉倉一致性:連續合約確保回測與實盤的一致與可解釋性。

載入套件

#%% Setup ticker1 = 'IR0001 IX0001' ticker2 = 'MTX MSCI NYF' # 環境變數 import os import sys # import time # 未使用 import yaml ''' ------------------- 不使用 config.yaml 管理 API KEY 的使用者可以忽略以下程式碼 -------------------''' notebook_dir = os.path.dirname(os.path.abspath(file)) if 'file' in globals() else os.getcwd() yaml_path = os.path.join(notebook_dir, '..', 'config.yaml') yaml_path = os.path.abspath(os.path.join(notebook_dir, '..', 'config.yaml')) with open(yaml_path, 'r') as tejapi_settings: config = yaml.safe_load(tejapi_settings) ''' ------------------- 不使用 config.yaml 管理 API KEY 的使用者可以忽略以上程式碼 -------------------''' # -------------------------------------------------------------------------------------------------- os.environ['TEJAPI_BASE'] = config['TEJAPI_BASE'] # = "https://api.tej.com.tw" os.environ['TEJAPI_KEY'] = config['TEJAPI_KEY'] # = "YOUR_API_KEY" # -------------------------------------------------------------------------------------------------- os.environ['ticker'] = ticker1 os.environ['future'] = ticker2 os.environ['mdate'] = '20180101 20251002' !zipline ingest -b tquant_future # 數據分析套件 import warnings import numpy as np import pandas as pd import matplotlib.pyplot as plt from logbook import Logger, StderrHandler, INFO # tquant 相關套件 # import tejapi # import TejToolAPI import zipline import pyfolio as pf from zipline.utils.calendar_utils import get_calendar from zipline.utils.events import date_rules, time_rules from zipline.finance.commission import ( PerContract ) from zipline.finance.slippage import ( FixedSlippage ) from zipline import run_algorithm from zipline.api import ( record, schedule_function, set_slippage, set_commission, order_value, set_benchmark, symbol, get_datetime, date_rules, time_rules, continuous_future ) from pyfolio.utils import extract_rets_pos_txn_from_zipline # logbook 設定 warnings.filterwarnings('ignore') print(sys.executable) print(sys.version) print(sys.prefix) log_handler = StderrHandler( format_string = ( '[{record.time:%Y-%m-%d %H:%M:%S.%f}]: ' + '{record.level_name}: {record.func_name}: {record.message}' ), level=INFO ) log_handler.push_application() log = Logger('Algorithm')

整理資料

#%% Strategy 1.3 # 目前最讚的版本的修改版 + 濾網天數限制 '''=========================================================================================''' # 策略參數 #--------------------------------------------------------------------------------------------- ''' 未觸發濾網時 MA3 & MA10 黃金交叉 => 買進(全倉,槓桿 1.8倍),濾網開啟 or 死亡交叉 or 觸發停損條件 or 超過持有天數 => 平倉 ''' # 均線參數 SHORT_MA = 3 LONG_MA = 10 # 停損參數 MAX_HOLD_DAYS = 200 STOPLOSS_1D = 5 STOPLOSS_5D = 10 STOPLOSS_10D = 15 # 濾網參數 FILLTER_ON_ACCDAY = 2 FILLTER_OF_ACCDAY = 3 FILLTER_ON_TRIGGER = -5 FILLTER_OF_TRIGGER = 5 FILLTER_AUTO_OFF_DAYS = 200 # 濾網開啟後自動關閉天數 # 槓桿參數 LEVERAGE = 1.8 MAX_LEVERAGE = LEVERAGE + 0.05 # 其他 MTX_CONTRACT_MULTIPLIER = 50 # 小台乘數(請依你行情系統設定) CAPITAL_BASE = 1e6 START_DT = pd.Timestamp('2020-01-02', tz='utc') END_DT = pd.Timestamp('2025-09-26', tz='utc') print(f'最大槓桿:{MAX_LEVERAGE} 倍') print("[Trading log---------------]: ----------------------------- | signal ----- |action|Original posi| Note") #--------------------------------------------------------------------------------------------- def initialize(context): set_benchmark(symbol('IR0001')) set_commission( futures=PerContract( cost = {'MTX': 15}, exchange_fee = 0 ) ) set_slippage( futures=FixedSlippage(spread=1.0)) # set_max_leverage(MAX_LEVERAGE) context.asset = continuous_future('MTX', offset=0, roll='calendar', adjustment='add') context.universe = [context.asset] context.in_position = False context.hold_days = 0 context.wait_after_stoploss = 0 context.filter_triggered = 0 # 添加濾網狀態變數 context.filter_days = 0 # 濾網開啟天數計數器 schedule_function(ma_strategy, date_rules.every_day(), time_rules.market_close()) def ma_strategy(context, data): # 獲取足夠的歷史資料 hist = data.history(context.asset, ['close'], bar_count=max(LONG_MA+2, 15), frequency='1d') close = hist['close'] # 獲取基準指標歷史資料 benchmark_hist = data.history(symbol('IR0001'), ['close'], bar_count=7, frequency='1d') benchmark_close = benchmark_hist['close'] # 計算策略累計報酬 return_1d = (close.iloc[-1] - close.iloc[-2]) / close.iloc[-2] * 100 return_5d = (close.iloc[-1] - close.iloc[-6]) / close.iloc[-6] * 100 return_10d = (close.iloc[-1] - close.iloc[-11]) / close.iloc[-11] * 100 # 計算指數累計報酬 benchmark_return_on = (benchmark_close.iloc[-1] - benchmark_close.iloc[-(FILLTER_ON_ACCDAY+1)]) / benchmark_close.iloc[-(FILLTER_ON_ACCDAY+1)] * 100 benchmark_return_of = (benchmark_close.iloc[-1] - benchmark_close.iloc[-(FILLTER_OF_ACCDAY+1)]) / benchmark_close.iloc[-(FILLTER_OF_ACCDAY+1)] * 100 # 計算均線 short_ma = close.rolling(window=SHORT_MA).mean() long_ma = close.rolling(window=LONG_MA).mean() # 判斷是否有持倉 contract = data.current(context.asset, 'contract') open_positions = context.portfolio.positions in_position = contract in open_positions and open_positions[contract].amount != 0 value = context.portfolio.portfolio_value # 計算黃金交叉訊號 # golden_cross = (short_ma.iloc[-2] < long_ma.iloc[-2]) and (short_ma.iloc[-1] > long_ma.iloc[-1]) golden_cross = (short_ma.iloc[-2] < long_ma.iloc[-2]) and (short_ma.iloc[-1] > long_ma.iloc[-1] * 1.0001) # 計算死亡交叉訊號 death_cross = (short_ma.iloc[-2] > long_ma.iloc[-2]) and (short_ma.iloc[-1] < long_ma.iloc[-1]) # 計算停損訊號 stop_loss_flag = (return_1d < -STOPLOSS_1D) or (return_5d < -STOPLOSS_5D) or (return_10d < -STOPLOSS_10D) # 計算濾網訊號 if context.filter_triggered == 0: if benchmark_return_on <= FILLTER_ON_TRIGGER: context.filter_triggered = 1 context.filter_days = 0 # 重置濾網天數計數器 #----------------------------------|--------------| log.info(f'{get_datetime().date()} | Filter ON | Benchmark {FILLTER_ON_ACCDAY}d return: {benchmark_return_on:.2f}%') else: # context.filter_triggered == 1 context.filter_days += 1 # 濾網天數計數器增加 # 檢查濾網自動關閉條件:天數達到上限 if context.filter_days >= FILLTER_AUTO_OFF_DAYS: context.filter_triggered = 0 context.filter_days = 0 #----------------------------------|--------------| log.info(f'{get_datetime().date()} | Filter OFF | Auto off after {FILLTER_AUTO_OFF_DAYS} days') # 檢查濾網關閉條件:市場反彈 elif benchmark_return_of >= FILLTER_OF_TRIGGER: context.filter_triggered = 0 context.filter_days = 0 #----------------------------------|--------------| log.info(f'{get_datetime().date()} | Filter OFF | Benchmark {FILLTER_OF_ACCDAY}d return: {benchmark_return_of:.2f}%') # 計算進出場訊號 # 濾網未觸發時,依策略進出場 if context.filter_triggered == 0: # 有部位 => 判斷出場時機 if in_position: context.hold_days += 1 # 移動停損 if stop_loss_flag: order_value(contract, 0) #----------------------------------|--------------| log.info(f'{get_datetime().date()} | STOP_LOSS | sell | position: {open_positions[contract].amount if contract in open_positions else 0} | Hold Days: {context.hold_days}') context.hold_days = 0 context.wait_after_stoploss = 5 # 死亡交叉 elif death_cross: order_value(contract, 0) #----------------------------------|--------------| log.info(f'{get_datetime().date()} | Death Cross | sell | position: {open_positions[contract].amount if contract in open_positions else 0} | Hold Days: {context.hold_days}') context.hold_days = 0 # 超過持有天數限制 elif context.hold_days >= MAX_HOLD_DAYS: order_value(contract, 0) #----------------------------------|--------------| log.info(f'{get_datetime().date()} | Max Hold | sell | position: {open_positions[contract].amount if contract in open_positions else 0} | Hold Days: {context.hold_days}') context.hold_days = 0 # 無部位 => 判斷進場時機 else: context.hold_days = 0 if context.wait_after_stoploss > 0: context.wait_after_stoploss -= 1 elif golden_cross: order_value(contract, value * LEVERAGE) #----------------------------------|--------------| log.info(f'{get_datetime().date()} | Golden Cross | buy | position: {open_positions[contract].amount if contract in open_positions else 0} | Amount: {value * LEVERAGE}') context.hold_days = 0 # 濾網觸發時,無條件直接平倉 else: # context.filter_triggered == 1 if in_position: context.hold_days += 1 order_value(contract, 0) #----------------------------------|--------------| log.info(f'{get_datetime().date()} | Filter Close | sell | position: {open_positions[contract].amount if contract in open_positions else 0} | Hold Days: {context.hold_days} | Filter Days: {context.filter_days}') context.hold_days = 0 context.wait_after_stoploss = 5 record(close=data.current(context.asset, 'price')) record(position=open_positions[contract].amount if contract in open_positions else 0) record(filter_status=context.filter_triggered) # 記錄濾網狀態 def analyze(context=None, results=None): close = results['close'] ma3 = close.rolling(window=3).mean() ma10 = close.rolling(window=10).mean() position = results['position'] filter_status = results.get('filter_status', pd.Series(0, index=results.index)) # 濾網狀態 fig, (ax1, ax2, ax3) = plt.subplots( 3, 1, figsize=(16, 9), sharex=True, gridspec_kw={'height_ratios': [4, 2, 2]} ) # 上方:策略與基準績效 results.algorithm_period_return.plot(label='Strategy', ax=ax1) results.benchmark_period_return.plot(label='Benchmark', ax=ax1) ax1.set_title('MTX 3MA/10MA Crossover Strategy with Filter (Dynamic Contracts)') ax1.legend(loc="upper left") ax1.grid(True) # 中間:收盤價、均線與持倉區塊 # ax2.plot(close.index, close, label='MTX Close') ma_diff = ma3 - ma10 ax2.set_title('MTX Close, MA3, MA10 with Position Highlight') ax2.bar(ma_diff.index, ma_diff, width=0.8, alpha=0.7, label='MA3-MA10', color=['black' if x > 0 else 'black' for x in ma_diff]) ax2.axhline(y=0, color='black', linewidth=2, linestyle='-') ax2.set_ylabel('MA3 - MA10') ax2.legend(loc="upper left") ax2.grid(True) # ax2.plot(ma3.index, ma3, label='MA3') # ax2.plot(ma10.index, ma10, label='MA10') # 下方:濾網狀態 ax3.plot(filter_status.index, filter_status, label='Filter Status', color='red', linewidth=2) ax3.fill_between(filter_status.index, 0, filter_status, alpha=0.3, color='red', where=(filter_status > 0), label='Filter ON') ax3.set_ylim(-0.1, 1.1) ax3.set_ylabel('Filter Status') ax3.set_title('Filter Status (0=OFF, 1=ON)') ax3.legend(loc="upper left") ax3.grid(True) # 添加持倉區塊到所有圖表 in_position = (position > 0) start = None for i in range(len(in_position)): if in_position.iloc[i] and start is None: start = i elif not in_position.iloc[i] and start is not None: ax1.axvspan(close.index[start], close.index[i-1], color='orange', alpha=0.3) ax2.axvspan(close.index[start], close.index[i-1], color='orange', alpha=0.3) ax3.axvspan(close.index[start], close.index[i-1], color='orange', alpha=0.3) start = None if start is not None: ax1.axvspan(close.index[start], close.index[-1], color='orange', alpha=0.3) ax2.axvspan(close.index[start], close.index[-1], color='orange', alpha=0.3) ax3.axvspan(close.index[start], close.index[-1], color='orange', alpha=0.3) plt.tight_layout() plt.show() results = run_algorithm( start = START_DT, end = END_DT, initialize = initialize, capital_base = CAPITAL_BASE, analyze = analyze, data_frequency = 'daily', bundle = 'tquant_future', trading_calendar = get_calendar('TEJ'), ) #%% pyfolio '''=========================================================================================''' plt.rcParams['font.sans-serif'] = ['Arial', 'Noto Sans CJK TC', 'SimHei'] plt.rcParams['axes.unicode_minus'] = False try: returns, positions, transactions = extract_rets_pos_txn_from_zipline(results) # print('returns:', returns.head()) # print('positions:', positions.head()) # print('transactions:', transactions.head()) except Exception as e: print('extract_rets_pos_txn_from_zipline error:', e) returns = results.get('algorithm_period_return', None) positions = None transactions = None if returns is not None: print('returns (fallback):', returns.head()) benchmark_rets = getattr(results, 'benchmark_return', None) if benchmark_rets is None and hasattr(results, 'benchmark_period_return'): benchmark_rets = results.benchmark_period_return if benchmark_rets is not None: print('benchmark_rets:', benchmark_rets.head()) else: print('No benchmark returns found!') if returns is not None: pf.tears.create_full_tear_sheet( returns=returns, positions=positions, transactions=transactions, benchmark_rets=benchmark_rets ) else: print('No returns data for pyfolio.') #%% Summary '''=========================================================================================''' summary_strategy = pf.timeseries.perf_stats(returns, factor_returns=benchmark_rets) summary_benchmark = pf.timeseries.perf_stats(benchmark_rets) summary = pd.concat([summary_strategy, summary_benchmark], axis=1) summary.columns = ['Strategy', 'Benchmark'] summary = summary.round(4) summary

benchmark_rets: 2020-01-02 00:00:00+00:00 0.008614 2020-01-03 00:00:00+00:00 0.000823 2020-01-06 00:00:00+00:00 -0.012970 2020-01-07 00:00:00+00:00 -0.006110 2020-01-08 00:00:00+00:00 -0.005322 Name: benchmark_return, dtype: float64

Start date 2020-01-02 End date 2025-09-26 Total months 66 Backtest Annual return 29.162% Cumulative returns 312.722% Annual volatility 13.704% Sharpe ratio 1.94 Calmar ratio 2.67 Stability 0.81 Max drawdown -10.902% Omega ratio 1.88 Sortino ratio 3.28 Skew 0.54 Kurtosis 13.65 Tail ratio 1.96 Daily value at risk -1.621% Gross leverage 0.61 Daily turnover 0.156% Alpha 0.24 Beta 0.28 Worst drawdown periods Net drawdown in % Peak date Valley date Recovery date Duration 0 10.90 2024-07-11 2024-09-04 2025-05-09 198 1 10.30 2023-07-14 2023-08-25 2024-05-13 201 2 9.16 2021-04-20 2022-07-06 2022-07-20 309 3 6.69 2020-10-12 2020-10-30 2020-11-06 20 4 6.09 2023-03-07 2023-07-10 2023-07-14 88

Top 10 long positions of all time max sid MTX202009 64.14% MTX202007 64.14% MTX202401 63.85% MTX202011 63.68% MTX202309 63.38% MTX202308 63.30% MTX202101 63.26% MTX202010 63.23% MTX202207 63.08% MTX202312 63.05% Top 10 short positions of all time max sid Top 10 positions of all time max sid MTX202009 64.14% MTX202007 64.14% MTX202401 63.85% MTX202011 63.68% MTX202309 63.38% MTX202308 63.30% MTX202101 63.26% MTX202010 63.23% MTX202207 63.08% MTX202312 63.05% Strategy Benchmark Annual return 0.2916 0.1881 Cumulative returns 3.1272 1.5977 Annual volatility 0.1370 0.1978 Sharpe ratio 1.9365 0.9707 Calmar ratio 2.6749 0.6587 Stability 0.8124 0.8224 Max drawdown -0.1090 -0.2855 Omega ratio 1.8757 1.1919 Sortino ratio 3.2789 1.3591 Skew 0.5411 -0.5698 Kurtosis 13.6454 8.1531 Tail ratio 1.9604 1.0142 Daily value at risk -0.0162 -0.0242 Alpha 0.2355 NaN Beta 0.2803 NaN

結論

本策略以 MA3/MA10 黃金交叉為核心,加入 1.0001 緩衝減少假突破,搭配「急跌濾網」(基準 2 日跌幅≦−5% 即強平+5 日冷卻)、三段式停損與時間停損,在順勢時能放大趨勢、遇劇烈下跌能優先保本;採連續合約處理換月,提升回測與實盤一致性。優點是訊號簡潔、風控層次明確、1.8× 槓桿配合嚴格停損可穩定風險報酬;限制在於均線遲滯與盤整期易被鞭打、濾網門檻與參數對績效敏感。建議後續進行走時序與敏感度測試(MA 期間、緩衝值、濾網阈值),引入波動分層或 ADX 篩選只在趨勢期啟動,並納入成本滑價評估以強化實務可行性。

溫馨提醒,本次分析僅供參考,不代表任何商品或投資上建議。

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

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

點我註冊會員,開始試用

GitHub 原始碼

點擊前往 Github

延伸閱讀

相關連結

查看原始文章

更多理財相關文章

01

她退休11年總花費只有314萬!居無定所走到哪、玩到哪、住到哪,壯遊世界把旅行當生活

幸福熟齡 X 今周刊
02

年薪破300萬!黃仁勳點未來搶手「3職業」成金飯碗:寫程式不是唯一出路

三立新聞網
03

破天荒!台北101「15.1%股權」標售 最低5.7億當「國家門面」房東

太報
04

賣藍莓先看台灣!外媒曝市場的秘密

自由電子報
05

政府打房建商慘1/新舊青安累計撥款2.55兆 兩顆未爆彈全民挫咧等

鏡週刊
06

「陶朱隱園」沒有蓋牌! 17樓見實登「單價飆364萬」

ETtoday新聞雲
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...