The previous section introduced quantitative trading tools and basic research. This article covers the practical implementation of quantitative trading strategies and their performance.

By writing trading strategies using GPT-4.0, you can enter the financial programming field at zero cost. If the results are not ideal, you can also have it analyze the tabular files returned by the Interactive Brokers API to optimize parameters (watch out for overfitting).

Disclaimer: The following is based on my personal research, and the code was generated by GPT. It does not constitute any investment advice. Please invest with caution.

Personal Repository

Quant

Quick Start

  1. Register an Interactive Brokers account and download Gateway

  2. Install the backtrader library (quantitative backtesting framework)

  3. Install the ib_insync library (for interfacing with Interactive Brokers Gateway; the developer erdewit passed away on March 11, 2024, RIP)

  4. Write a sample SMA strategy using the ib_insync documentation or GPT

  5. Run and observe the charts

backtrader is a traditional strategy framework. Rather than diving into AI hype right away, I prefer to master traditional frameworks first. Developed by a German programmer, its code style, structure, and design are very suitable for learning.

qlib AI quantitative framework

IB Gateway Integration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import backtrader as bt
from ib_insync import IB, Stock, util
import pandas as pd

class IBStore:
def __init__(self, host='127.0.0.1', port=7497, clientId=1):
self.ib = IB()
self.ib.connect(host, port, clientId)

# 默认回测一年,每一天为一个bar
def get_data(self, symbol, durationStr='1 Y', barSizeSetting='1 day', whatToShow='MIDPOINT'):
contract = Stock(symbol, 'SMART', 'USD')
bars = self.ib.reqHistoricalData(
contract, endDateTime='', durationStr=durationStr,
barSizeSetting=barSizeSetting, whatToShow=whatToShow, useRTH=True)
df = util.df(bars)
# 输出csv,用于GPT解释或优化参数
df.to_csv('~/' + symbol + '.csv', sep=',', index=False, header=True)
return df

# 这里写策略类
# class SmaCross(bt.Strategy):

if __name__ == '__main__':
# 对接IB gateway API,端口改为IB gateway设置中的端口
ibstore = IBStore('127.0.0.1', 4002, clientId=1)
# 选股,建议多选几种走势的股票
df = ibstore.get_data('NVDA') # NVDA单调递增
# df = ibstore.get_data('AAPL') # AAPL波动剧烈

# Ensure the 'date' column is in the correct datetime format
df['date'] = pd.to_datetime(df['date'])
df.set_index('date', inplace=True)

# Load data into Backtrader
data = bt.feeds.PandasData(dataname=df)

# Initialize and run Cerebro
cerebro = bt.Cerebro(stdstats=True, cheat_on_open=True, optreturn=False)
cerebro.addstrategy(SmaCross) # 这里改为你的策略类名
cerebro.broker.set_cash(100000) # 本金
# cerebro.broker.setcommission(commission=0.001) #交易手续费
cerebro.adddata(data)
cerebro.run()
cerebro.plot()

SMA Strategy

Introduction

SMA (Simple Moving Average) sounds simple, as its name suggests, making it ideal for beginners to quickly run through a backtesting process.

SMA is calculated by taking the closing prices over a past period of time and averaging them.

For example, to calculate a security’s 20-day SMA, add up the closing prices of the past 20 days and divide by 20.

Similarly, to calculate a security’s 200-day SMA, add up the closing prices of the past 200 days and divide by 200.

Each data point in an SMA has equal weight. However, investors might consider recent data to be more important than older data and prefer to assign higher weight to recent prices. Therefore, they often favor another form of moving average, the EMA (Exponential Moving Average). EMA can provide a better reference when the market experiences rapid and sharp fluctuations.

Trend Identification

A security trading above its SMA is in an uptrend, while one trading below its SMA is in a downtrend.

It can also be used to identify support and resistance levels.

Crossovers

Bullish Crossovers and Bearish Crossovers

Investment strategies involving SMA generally rely on bullish crossovers and bearish crossovers.

A bullish crossover occurs when a security’s price falls below the SMA and then moves back above it. This signifies the end of a pullback and signals the start of an uptrend. Conversely, a bearish crossover signals the beginning of a pullback.

However, in a range-bound (oscillating) market, this indicator is less effective.

Golden Cross and Death Cross

A Golden Cross occurs when a short-term SMA crosses above a long-term SMA. For example, when the 50-day SMA crosses above the 200-day SMA, a Golden Cross appears, which is a bullish signal. The opposite is a Death Cross, which is a bearish signal.

When a Death Cross occurs, investors might consider temporarily pulling out of the market. (Reflecting on the pharma fund I bought—even though it had been in a Death Cross for two years, I was still stubborn enough to gamble on it…)

Code

Generated by GPT-4. Note that for simplicity, the code executes all-in buy and sell orders when a crossover occurs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class SmaCross(bt.Strategy):
params = dict(pfast=13, pslow=25)

# Define trading strategy
def __init__(self):
sma1 = bt.ind.SMA(period=self.p.pfast)
sma2 = bt.ind.SMA(period=self.p.pslow)
self.crossover = bt.ind.CrossOver(sma1, sma2)

# Custom trade tracking
self.trade_data = []

# Execute trades
def next(self):
# Trading the entire portfolio
size = int(self.broker.get_cash() / self.data.close[0])

if not self.position:
if self.crossover > 0:
self.buy(size=size)
self.entry_bar = len(self) # Record entry bar index
elif self.crossover < 0:
self.close()

# Record trade details
def notify_trade(self, trade):
if trade.isclosed:
exit_bar = len(self)
holding_period = exit_bar - self.entry_bar
trade_record = {
'entry': self.entry_bar,
'exit': exit_bar,
'duration': holding_period,
'profit': trade.pnl
}
self.trade_data.append(trade_record)

# Caclulating holding periods
def stop(self):
# Calculate and print average holding periods
total_holding = sum([trade['duration'] for trade in self.trade_data])
total_trades = len(self.trade_data)
avg_holding_period = round(total_holding / total_trades) if total_trades > 0 else 0

# Calculating for winners and losers separately
winners = [trade for trade in self.trade_data if trade['profit'] > 0]
losers = [trade for trade in self.trade_data if trade['profit'] < 0]
avg_winner_holding = round(sum(trade['duration'] for trade in winners) / len(winners))if winners else 0
avg_loser_holding = round(sum(trade['duration'] for trade in losers) / len(losers)) if losers else 0

# Display average holding period statistics
print('Average Holding Period:', avg_holding_period)
print('Average Winner Holding Period:', avg_winner_holding)
print('Average Loser Holding Period:', avg_loser_holding)

Performance

NVDA Stock
NVDA Stock
AAPL
AAPL

Analysis

SMA performed well on NVDA, a stock in a monotonic uptrend. NVDA rode the AI wave all the way up, but how many such hype waves are there really?

More common are fluctuating trends like AAPL’s, where SMA actually resulted in a loss of around 8%.

MACD Strategy

Introduction

MACD (Moving Average Convergence/Divergence) is known as the moving average convergence/divergence indicator. It is visible on most exchange charts and is often dubbed the King of Technical Indicators.

The MACD indicator consists of three main components: the DIF line, the DEA line, and the MACD histogram. The standard parameters are 12, 26, and 9, as shown in the code below.

  • The DIF line is the fast exponential moving average (typically 12 days) minus the slow exponential moving average (typically 26 days).
  • The DEA line is the 9-day weighted moving average of the DIF line.
  • The MACD histogram represents the difference between the DIF line and the DEA line, used to show the degree of divergence between the DIF and DEA lines.

Crossover Strategy

The basic principle of MACD relies on golden crosses and death crosses:

  • A golden cross occurs when the DIF line crosses above the DEA line, indicating strengthening bullish momentum and suggesting a potential market uptrend, which serves as a buy signal.
  • A death cross occurs when the DIF line crosses below the DEA line, indicating strengthening bearish momentum and suggesting a potential market downtrend, which serves as a sell signal.

Divergence Strategy

Red/Green Bar Divergence

Red Bar Bearish Divergence: The stock price hits a new high, but the red bars fail to reach a new high. Upward momentum is weakening, which may signal a major correction.

Green Bar Bullish Divergence: Although the stock price is falling, downward momentum is gradually diminishing. Once bullish momentum gains the upper hand, the market trend will reverse from bearish to bullish.

Yellow/White Line Divergence

The stock price hits a new high/low, but the yellow and white lines fail to hit a new high/low. This signals a major correction or a bullish reversal.

Buy Points

Bullish divergence may recur while prices probe lower, which can lead to getting trapped. Confirmation requires multiple recurrences (for stocks in a downtrend, it is best for bullish divergence to occur at least twice).

Position building should only be considered when the price transitions from a fluctuating trend to sideways consolidation. The optimal buy point is a golden cross above/at the zero line.

If no zero-line golden cross occurs:

  • Look for support levels after a breakout, i.e., positions where resistance turns into support (pin bar bottoming out with a small bullish candle).
  • Look for continuation setups / mid-air refueling points (when the white line starts curling upward).
  • Wait for a high-volume breakout from the current sideways consolidation range.

Build positions gradually; do not go all-in on the first day of a pin-bar bottom.

Sell Points

The stock price hits a new high; while the red and white lines show no bearish divergence, the red bars exhibit bearish divergence, indicating declining upward momentum.

When high volume is accompanied by stagnant prices, violating the logic of volume-price expansion, strong selling pressure is present.

This could mean profit-takers or insiders knowing it’s a peak are selling, and institutional money is exiting—a clear sell signal.

After a double bearish divergence signal appears, a pullback is highly probable to digest the divergence.

Summary

When bearish divergence appears, a correction usually follows: in a best-case scenario, a wave adjustment; in a worst-case scenario, a trend reversal.

When bullish divergence appears, many stocks undergo a long period of accumulation and shakeouts before entering a primary wave of rise.

The more frequently and longer divergence occurs, the stronger the main uptrend will be.

The longer the horizontal consolidation, the higher the vertical rise.

MACD is a trend-following indicator, suitable only for trend stocks rather than sentiment-driven stocks, because it has significant lag. (Just think about those 3 parameters)

There is no method in the market with a 100% success rate.

Summary Formula by Xueqiu Veteran

One Center, Two Basic Points, Four Basic Principles

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 定义策略
class DynamicMACDStrategy(bt.Strategy):
params = (
('fast', 12),
('slow', 26),
('signal', 9),
)

def __init__(self):
self.macd = bt.indicators.MACD(self.data.close,
period_me1=self.params.fast,
period_me2=self.params.slow,
period_signal=self.params.signal)
self.crossover = bt.indicators.CrossOver(self.macd.macd, self.macd.signal)

def next(self):
if not self.position: # 没有持仓
if self.crossover > 0: # MACD线上穿信号线
self.buy()
elif self.crossover < 0: # 已有持仓且MACD线下穿信号线
self.sell()

Performance

NVDA
NVDA
AAPL
AAPL

Analysis

It appears that a monotonic uptrend yields positive returns, whereas frequent fluctuations leave the principal mostly unchanged.

Enhanced MACD Strategy

GPT-4

From the simple MACD strategy code above, we noticed that it executes all-in buy and sell orders whenever a golden cross or death cross occurs.

So I posed this question to GPT-4 to see how it would solve it.

I ran this strategy, but the profit is too low—possibly because it buys and sells the entire position at once. Please optimize the strategy code to achieve better profitability through appropriate position-sizing buy and sell strategies.

Code

GPT allows specifying capital allocation ratios, stop-loss/take-profit ratios, and profit thresholds, which looks much more scientific than going all-in.

Additionally, it provides a method to search for the fast, slow, and signal parameters. You can execute it with default settings first, then check the three parameters output to the console. Next, input these parameters into cerebro.optstrategy(ImprovedMACDStrategy), for example: cerebro.optstrategy(ImprovedMACDStrategy, fast=10, slow=27, signal=5).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

class ImprovedMACDStrategy(bt.Strategy):
params = (
('fast', 12),
('slow', 26),
('signal', 9),
('order_percentage', 0.95), # 买入资金比例
('stop_loss_percentage', 0.95), # 止损比例
('take_profit_percentage', 1.05), # 止盈比例
('partial_sell_percentage', 0.5), # 部分卖出比例
('profit_threshold', 1.02), # 盈利阈值,达到则部分卖出
)

def __init__(self):
self.macd = bt.indicators.MACD(self.data.close,
period_me1=self.params.fast,
period_me2=self.params.slow,
period_signal=self.params.signal)
self.crossover = bt.indicators.CrossOver(self.macd.macd, self.macd.signal)
self.order = None

def notify_order(self, order):
if order.status in [order.Completed]:
if order.isbuy():
self.buy_price = order.executed.price
self.stop_price = self.buy_price * self.params.stop_loss_percentage
self.limit_price = self.buy_price * self.params.take_profit_percentage
self.order = None

def next(self):
if self.order:
return # 如果有未完成的订单,则不执行任何操作

if not self.position: # 没有持仓时
if self.crossover > 0: # MACD线上穿信号线,买入信号
self.order = self.buy(size=(self.broker.get_cash() / self.data.close[0]) * self.params.order_percentage)
else: # 已有持仓时
if self.data.close[0] / self.buy_price > self.params.profit_threshold:
# 达到盈利阈值,部分卖出
self.order = self.sell(size=self.position.size * self.params.partial_sell_percentage)
elif self.crossover < 0 or self.data.close[0] < self.stop_price or self.data.close[0] > self.limit_price:
# MACD线下穿信号线,或触发止损止盈,全仓卖出
self.order = self.close()


if __name__ == '__main__':
ibstore = IBStore('127.0.0.1', 4002, clientId=1)
# df = ibstore.get_data('NVDA')
df = ibstore.get_data('AAPL')

# Ensure the 'date' column is in the correct datetime format
df['date'] = pd.to_datetime(df['date'])
df.set_index('date', inplace=True)

# Load data into Backtrader
data = bt.feeds.PandasData(dataname=df)

# Initialize and run Cerebro
cerebro = bt.Cerebro(stdstats=True, cheat_on_open=True, optreturn=False)

# 1-默认参数
# cerebro.optstrategy(ImprovedMACDStrategy)

# 2-范围里面找最优解,查看控制台With Parameters输出。不同股票需要重新计算。
# cerebro.optstrategy(ImprovedMACDStrategy, fast=range(10, 15), slow=range(20, 30), signal=range(5, 10))

# 3-根据With Parameters输出,配置最优解。这个是AAPL的最优解,不同股票需要重新计算。
# cerebro.optstrategy(ImprovedMACDStrategy, fast=10, slow=27, signal=5)
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
cerebro.broker.set_cash(100000)
# cerebro.broker.setcommission(commission=0.001)
cerebro.adddata(data) # 确保这里的data是bt.feeds对象,不是pandas DataFrame
result = cerebro.run()

best_params = None
best_sharpe = float('-inf') # 初始化为负无穷大

for run in result:
for strategy in run:
sharpe = strategy.analyzers.sharpe.get_analysis()['sharperatio']
params = strategy.params
if sharpe > best_sharpe:
best_sharpe = sharpe
best_params = params

# 根据
print(f"Best Sharpe Ratio: {best_sharpe}")
print(f"With Parameters: Fast={best_params.fast}, Slow={best_params.slow}, Signal={best_params.signal}")
cerebro.plot()

Performance

Default Parameters (12, 26, 9)

NVDA
NVDA
AAPL
AAPL

Optimal Parameters for Individual Stocks

Note: Parameters for different stocks need to be recalculated. If the market is not oscillating/range-bound, parameter calculations might not yield optimization benefits.

NVDA

Best Sharpe Ratio: 0.8460012912318833
With Parameters: Fast=13, Slow=24, Signal=9

NVDA
NVDA

AAPL

Best Sharpe Ratio: 0.801877924498146
With Parameters: Fast=10, Slow=27, Signal=5

AAPL
AAPL

Conclusion

This article built a backtesting toolkit, introduced several common technical indicators, and provided the corresponding code and execution results.

Next, I will continue to optimize MACD and other strategies to achieve better performance.