Golden Cross Futures Trading Strategy(MTX)

Photo by rc.xyz NFT gallery on Unsplash

Introduction

The concept of the Moving Average (MA) originates from the Dow Theory developed in the early 20th century. Dow Theory emphasizes that markets exhibit trends, and such trends can be observed through price movements themselves. However, in the early days, price data was highly volatile and lacked smoothing tools. Analysts therefore began using the average price over a certain period to reduce random noise—this marked the beginning of the moving average.

With advancements in statistics and computer computation, the moving average gradually became one of the most fundamental technical indicators in quantitative trading. From the Simple Moving Average (SMA) and Exponential Moving Average (EMA) to more sophisticated variants such as the Double Exponential Moving Average (DEMA) and Hull Moving Average (HMA), all these improvements aim to address the inherent lag problem of traditional moving averages, allowing them to reflect price trends more quickly or more smoothly.

Practical Applications

In practice, moving average strategies are widely used across different markets:

  • Single Moving Average Breakout: The price breaking above or below a long-term moving average (such as the 200-day MA) is used to determine bullish or bearish conditions.
  • Dual Moving Average Crossover: The crossover between short-term and long-term moving averages serves as an entry or exit signal.
  • Multiple Moving Average Alignment: The arrangement of multiple MAs indicates whether the market is in a bullish or bearish configuration, helping to confirm the strength of a trend.
  • Integration with Risk Management: Moving average signals are often combined with stop-loss rules, volatility filters, or other conditions to enhance robustness.

Among these, the most representative examples are the Golden Cross and Death Cross. When a short-term moving average crosses above a long-term moving average, it is often regarded as the beginning of a bullish trend; conversely, when it crosses below, it signals a potential bearish trend. However, relying solely on crossover signals in a sideways or choppy market can lead to false breakouts. Therefore, in practice, traders often incorporate layered stop-loss mechanisms, volatility filters, and futures rollover adjustments to improve the strategy’s feasibility and stability.

This strategy, built within the Golden Cross framework, integrates trailing stop-losses and dynamic filtering for risk management, and applies continuous contract data to handle futures rollovers, ensuring consistency between backtesting and live trading performance.

Investment Targets and Backtesting Period

  • Instrument: Mini Taiwan Stock Index Futures (MTX, continuous contract)
  • Data Frequency: Daily (closing price)
  • Benchmark: Taiwan Capitalization Weighted Stock Index Total Return (IR0001)
  • Backtesting Period: 2020-01-02 to 2025-09-26
  • Contract Rollover: Continuous contract constructed using calendar roll with adjustment = add, ensuring performance continuity across contract expirations.

Core Logic

1. Indicator System

  • The strategy identifies market trends using MA3 and MA10.
    Golden Cross serves as the buy trigger, while a Death Cross acts as the exit signal.
    To reduce noise, a 1.0001 buffer condition is applied in the program, requiring the short-term moving average to be significantly higher than the long-term moving average before confirming a Golden Cross.

2. Filter Mechanism (Core Risk Management)

  • Activation condition: When the benchmark’s 2-day decline is ≤ −5%, the filter turns ON.
  • Deactivation conditions (as currently implemented):
  • Market rebound: When the benchmark’s 2-day gain is ≥ +5%, the filter turns OFF.
  • Automatic deactivation: The filter automatically turns OFF after being active for 200 days.
    When the filter is ON:
  • Any existing position is immediately liquidated (forced exit).
  • After liquidation, the system must wait 5 trading days before entering again (same cooldown as stop-loss).

3. Entry Logic (Executed Only When Filter = OFF)

  • Golden Cross Buy Rule:
    • Condition: (MA3[-2] < MA10[-2]) and (MA3[-1] > MA10[-1] * 1.0001)
    • Action: Open full position with 1.8x leverage
    • Restriction: If the previous exit was due to stop-loss or forced liquidation, wait 5 days before re-entry

4. Exit and Risk Management

  • When the Filter is ON:
  • Any open position is forcefully closed immediately.
  • When the Filter is OFF:
    1. Stop-loss Conditions:
      • 1-day return < −5%
      • 5-day return < −10%
      • 10-day return < −15%
    2. Death Cross Exit: (MA3[-2] > MA10[-2]) and (MA3[-1] < MA10[-1]) → Close position
    3. Time Stop::Close the position if it has been held for more than 200 days.

5. Contract Rolling

  • The strategy uses continuous MTX contracts to automatically handle rollovers, preventing price discontinuities and performance distortions caused by contract expiration.

Trading Flowchart

# Trading Flowchart

At each daily close → Update filter status  
├─ Is the filter OFF?  
│  └─ No (Filter = ON)  
│     ├─ Have position → Force close (start 5-trading-day cooldown)  
│     └─ No position → No entry  
└─ Yes (Filter = OFF)  
   ├─ Have position  
   │  ├─ Stop-loss (1D/5D/10D) → Close (start 5-day cooldown)  
   │  ├─ Death Cross → Close  
   │  ├─ Holding > 200 days → Close  
   │  └─ Otherwise → Continue holding  
   └─ No position  
      ├─ (If in cooldown) → No entry; cooldown −1  
      ├─ Golden Cross (with 1.0001 buffer) → Enter full position (leverage 1.8×)  
      └─ Else → No entry

Strategy Features

  • Dynamic Risk Management: The filter enhances capital protection during sharp market declines by enforcing forced liquidation (ON) and a 5-day cooldown period before re-entry.
  • Noise Reduction: The Golden Cross condition includes a MA10[-1] * 1.0001 buffer to minimize false breakouts and avoid noise trading.
  • Multi-Layered Exit Protection: Combines three-tier stop-losstechnical indicators, and time-based stops for comprehensive downside risk control.
  • Strict Risk Execution: When the filter is triggered, all positions are immediately closed, preventing exposure to tail-risk drawdowns.
  • Rollover Consistency: Continuous contracts ensure consistency and interpretability between backtesting and live trading performance.

Package Import

#%% 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')

Data Processing



#%% 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 date2020-01-02
End date2025-09-26
Total months66
Backtest
Annual return29.162%
Cumulative returns312.722%
Annual volatility13.704%
Sharpe ratio1.94
Calmar ratio2.67
Stability0.81
Max drawdown-10.902%
Omega ratio1.88
Sortino ratio3.28
Skew0.54
Kurtosis13.65
Tail ratio1.96
Daily value at risk-1.621%
Gross leverage0.61
Daily turnover0.156%
Alpha0.24
Beta0.28
Worst drawdown periodsNet drawdown in %Peak dateValley dateRecovery dateDuration
010.902024-07-112024-09-042025-05-09198
110.302023-07-142023-08-252024-05-13201
29.162021-04-202022-07-062022-07-20309
36.692020-10-122020-10-302020-11-0620
46.092023-03-072023-07-102023-07-1488

Top 10 long positions of all timemax
sid
MTX20200964.14%
MTX20200764.14%
MTX20240163.85%
MTX20201163.68%
MTX20230963.38%
MTX20230863.30%
MTX20210163.26%
MTX20201063.23%
MTX20220763.08%
MTX20231263.05%
Top 10 short positions of all timemax
sid
Top 10 positions of all timemax
sid
MTX20200964.14%
MTX20200764.14%
MTX20240163.85%
MTX20201163.68%
MTX20230963.38%
MTX20230863.30%
MTX20210163.26%
MTX20201063.23%
MTX20220763.08%
MTX20231263.05%
StrategyBenchmark
Annual return0.29160.1881
Cumulative returns3.12721.5977
Annual volatility0.13700.1978
Sharpe ratio1.93650.9707
Calmar ratio2.67490.6587
Stability0.81240.8224
Max drawdown-0.1090-0.2855
Omega ratio1.87571.1919
Sortino ratio3.27891.3591
Skew0.5411-0.5698
Kurtosis13.64548.1531
Tail ratio1.96041.0142
Daily value at risk-0.0162-0.0242
Alpha0.2355NaN
Beta0.2803NaN

Conclusion

This strategy is built around the MA3/MA10 Golden Cross, with a 1.0001 buffer added to reduce false breakouts. It incorporates a sharp-decline filter (triggered when the benchmark’s 2-day drop ≤ −5%, enforcing immediate liquidation and a 5-day cooldown), a three-tier stop-loss system, and a time-based stop. The design allows the strategy to amplify gains during trending markets while prioritizing capital preservation during sharp drawdowns. A continuous contract is used to handle rollovers, improving consistency between backtesting and live trading results.

The advantages include clear trading signalsstructured risk management, and a balanced 1.8× leverage that provides stable risk-adjusted returns under strict stop-loss control. Limitations include lag inherent in moving averagesvulnerability to whipsaws in sideways markets, and sensitivity of performance to filter thresholds and parameter tuning.

Future work should include walk-forward analysis and sensitivity testing (on MA periods, buffer values, and filter thresholds), integrating volatility-based regime segmentation or ADX filters to activate trading only during strong trends, and incorporating transaction cost and slippage evaluation to enhance real-world feasibility.

Note: This analysis is for informational purposes only and does not constitute investment advice or recommendations for any financial product.


【TQuant Lab Backtesting System】Solving Your Quantitative Finance Challenges


Click here to sign up and start your free trial

GitHub Source Code

Click to View on GitHub

Further Reading

Related Links

Back
Processing...