TQuant Lab Bollinger Bands Trading Strategy

TQuant Lab 布林通道交易策略
Photo by Maxim Hopman on Unsplash


  • Difficulty: ★☆☆☆☆
  • Bollinger band consists mainly of the stock price moving average and its positive, and negative standard deviation. With the above three lines, we can draw the upper, lower, and middle bounds for determining when to long or short stocks.
  • This article is revised from Bollinger Band Trading Strategy via TQuant Lab.


Bollinger Band is a technical indicator that John Bollinger invented in the 1980s. Bollinger Band consists of the concepts of statistics and moving averages. The moving Average(MA) is the average closing price of a past period. Usually, the period of MA in Bollinger Band is 20 days, and Standard Deviation(SD) is usually represented by σ in mathematical sign, which is used to evaluate the data’s degree of discrete.

Bollinger Band is composed of three tracks:
● The upper track:20 MA+double standard deviation
● The middle track:20 MA
● The lower track:20 MA+double standard deviation

The investment target price distribution during the long-term observation period will be Normal Distribution. According to statistics, there is a 95% probability that the price will present between μ − 2σ and μ + 2σ, which is also called a 95% Confidence Interval(CI). Bollinger Band is the technical indicator based on the theories above.

Trading Strategy

  • When today`s close price touches the upper bound and currently holds the position, sell the asset tomorrow.
  • When today`s close price touches the lower bound and the cash position is positive, buy the asset tomorrow.
  • When today`s close price touches the lower bound, the cash position is positive, and the current close price is lower than the last buy price, buy the asset tomorrow.

The Editing Environment and Module Required

This article uses Windows 11 as a system and jupyter notebook as an editor.

Data and Module Import

The backtesting time period is between 2021/04/01 to 2022/12/31, and we take AUO(2409) as an example.

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

os.environ['TEJAPI_BASE'] = 'https://api.tej.com.tw'
os.environ['TEJAPI_KEY'] = 'Your Key'
os.environ['mdate'] = '20210401 20221231'
os.environ['ticker'] = '2409'

# 使用 ingest 將股價資料導入暫存,並且命名該股票組合 (bundle) 為 tquant
!zipline ingest -b tquant 

from zipline.api import set_slippage, set_commission, set_benchmark, attach_pipeline, order, order_target, symbol, pipeline_output, record
from zipline.finance import commission, slippage
from zipline.data import bundles
from zipline import run_algorithm
from zipline.pipeline import Pipeline
from zipline.pipeline.filters import StaticAssets
from zipline.pipeline.factors import BollingerBands
from zipline.pipeline.data import EquityPricing

Create Pipeline function

Pipeline() enables users to quickly process multiple assets’ trading-related data. In today`s article, we use it to process:

  • Upper bound for Bollinger Bands in 20 days duration.
  • Middle bound for Bollinger Bands in 20 days duration.
  • Lower bound for Bollinger Bands in 20 days duration.
  • Current close price.
def make_pipeline():
    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 = {
            'upper':  upper,
            'middle':  middle,
            'lower':  lower,

Creating Initialize Function

Initialize() enables users to set up the trading environment at the beginning of the backtest period. In this article, we set up :

  • Slippage
  • Commission
  • Set the return of buying and holding AUO as the benchmark
  • Attach Pipline() function into backtesting.
  • Set context.last_signal_price to record the last buy price
def initialize(context):
    context.last_buy_price = 0
    attach_pipeline(make_pipeline(), 'mystrategy')
    context.last_signal_price = 0

Create Handle_data Function

handle_data() is used to process data and make orders daily.

def handle_data(context, data):
    out_dir = pipeline_output('mystrategy') # 取得每天 pipeline 的布林通道上中下軌
    for i in out_dir.index: 
        upper = out_dir.loc[i, 'upper']
        middle = out_dir.loc[i, 'middle']
        lower = out_dir.loc[i, 'lower']
        curr_price = out_dir.loc[i, 'curr_price']
        cash_position = context.portfolio.cash
        stock_position = context.portfolio.positions[i].amount
        buy, sell = False, False
        record(price = curr_price, upper = upper, lower = lower, buy = buy, sell = sell)
        if stock_position == 0:
            if (curr_price <= lower) and (cash_position >= curr_price * 1000):
                order(i, 1000)
                context.last_signal_price = curr_price
                buy = True
                record(buy = buy)
        elif stock_position > 0:
            if (curr_price <= lower) and (curr_price <= context.last_signal_price) and (cash_position >= curr_price * 1000):
                order(i, 1000)
                context.last_signal_price = curr_price
                buy = True
                record(buy = buy)
            elif (curr_price >= upper):
                order_target(i, 0)
                context.last_signal_price = 0
                sell = True
                record(sell = sell)

Creating Analyze Function

Here, we apply matplotlib.pyplot for the trading signals and the portfolio value visualization.

def analyze(context, perf):
    fig = plt.figure()
    ax1 = fig.add_subplot(211)
    ax1.set_ylabel("Portfolio value (NTD)")
    ax2 = fig.add_subplot(212)
    ax2.set_ylabel("Price (NTD)")
    ax2.plot( # 繪製買入訊號
        perf.loc[perf.buy, 'price'],
    ax2.plot( # 繪製賣出訊號
        perf.loc[perf.sell, 'price'],

Run Algorithms

Via run_algorithm(), we can execute the strategy we just built. The backtesting time period is set between 2021-06-01 to 2022-12-31. The data bundle we use is tquant. We assume the initial capital base is 500,000. The output of run_algorithm(), which is results, contains information on daily performance and trading receipts.

By observing the following graph, one can discover that there is an upper trend between 2021/11 to 2021/12. Since the close prices were not able to hit the lower bound, there was no buying transaction in this time period. And that made us fail to earn a profit.

The same issue has happened in the continuously lower trend. A sharp lower trend occurred in 2022-04, which led to consistently touching the lower bound. That means we have bought a bunch of stocks in this time zone. However, after the price recovered shortly, the close price touched the upper bound immediately. As a result, we sell the holding positions and earn a net loss during this time period.

As a matter of fact, due to the latency of 20 days Bollinger band, the band has difficulty reflecting the short-term high volatility price movement. If your target asset is more volatile, we suggested shortening the duration of the Bollinger band or adding trend-related indicators to fine-tune your strategy.

results = run_algorithm(
    start = pd.Timestamp('2021-06-01', tz='UTC'),
    end = pd.Timestamp('2022-12-31', tz ='UTC'),
    handle_data = handle_data

Portfolio value, trading time point, and trading records

Performance Analysis

Then, we used Pyfolio module which came with TQuant Lab to analyze strategy`s performance and risk. First, we use extract_rets_pos_txn_from_zipline() to calculate returns, positions, and trading records.

import pyfolio as pf 
returns, positions, transactions = pf.utils.extract_rets_pos_txn_from_zipline(results)

Daily Returns

Calculating daily portfolio return.

Daily portfolio return

Holding Positions

  • Equity(0 [2406]): AUO
  • Cash
Holding position record

Transaction Record

  • sid: index
  • symbol: ticker symbol
  • price: buy/sell price
  • order_id: order number
  • amount: trading amount
  • commission: commission cost
  • dt: trading date
  • txn_dollar: trading dollar volume
Trading record

Plot Accumulated Return and Benchmark Return

benchmark_rets = results['benchmark_return'] 
pf.plotting.plot_rolling_returns(returns, factor_returns=benchmark_rets)
Figure for strategy returns

Making Performance Table

With show_perf_stats(), one can easily showcase the performance and risk analysis table.

Performance Table


From late 2021 to 2022, the stock price of AUO is clearly in a downward spiral. if choosing buy and hold strategy, the accumulated return would turn out to be a horrible -40% to -50%. On the contrary, with Bollinger band strategy, the performance is way better than the buy and hold strategy.

However, the pure Bollinger Bands strategy tends to have the disadvantage of exiting prematurely during the rebound phase after a significant downward trend and the predicament of entering very rarely during the upward phase. Therefore, for stocks with significant price fluctuations, it is recommended to use other indicators to assess trend strength and optimize their strategy.

Please note that this strategy and the underlying assets are for reference only and do not constitute any recommendations for commodities or investments. In the future, we will also introduce the use of TEJ database to construct various indicators and backtest indicator performance. So, readers interested in various trading backtesting can choose relevant solutions from TEJ E-Shop to build their own trading strategies with high-quality databases.

Source Code

Click here to Github

Extended Reading

Related Link