0bps

Strategy vault / Market making

Bollinger Band Limit Market Maker Strategy

This is a market maker strategy that uses Bollinger Bands as entries, moving average as closes, and simple percentage stop loss. It was extremely profitable on the XBTUSD contract in June 2022.

Author
ChaoZhang
Complexity
Intermediate · 95 lines
Parameters
18
Backtest bar
1h · intraday
Tested on
BTC/USDT · futures

IMG

Overview

This is a market maker strategy that uses Bollinger Bands as entries, moving average as closes, and simple percentage stop loss. It was extremely profitable on the XBTUSD contract in June 2022.

Strategy Logic

The strategy uses the upper and lower bands of Bollinger Bands as opportunity areas to enter positions. Specifically, when the price is below the lower band, it will long to open long positions; when the price is above the upper band, it will short to open short positions.

In addition, the strategy also uses moving average as the benchmark for closing positions. When holding long positions, if the price is above the moving average, it will choose to close longs; similarly, when holding short positions, if the price is below the moving average, it will also choose to close shorts.

For stop loss, it uses a simple percentage trailing stop loss based on the entry price. This can effectively avoid huge losses in trending markets.

Advantage Analysis

The main advantages of this strategy are:

  1. Using Bollinger Bands can effectively capture price volatility and get more trading opportunities when volatility increases.
  2. Market maker strategies can benefit from the bid-ask spread by trading both sides.
  3. Percentage stop loss can proactively control risks and avoid huge losses in trending markets.

Risk Analysis

There are also some risks with this strategy:

  1. Bollinger Bands are not always reliable entry signals and can give false signals sometimes.
  2. Market maker strategies are vulnerable to being whipsawed in ranging markets.
  3. Percentage stop loss may be too brute force and unable to adapt to complex market situations.

To mitigate these risks, we may consider adding other filters, optimizing stop loss settings, or properly limiting position sizes.

Optimization Directions

There is room for further optimization:

  1. We can test different parameter combinations to find the optimal parameters.
  2. We can add more filters for multi-factor verification.
  3. We can use machine learning methods to auto-optimize parameters.
  4. We can consider more sophisticated stop loss methods like parabolic SAR.

Conclusion

Overall this is a very profitable high-frequency market making strategy. It capitalizes on Bollinger Bands for trading signals and controls risk. But we also need to be aware of its flaws and verify carefully in live trading. With further optimizations, this strategy has the potential to generate even more stable and outsized returns.

Parameters

NameDefaultWhat it controls
v_input_1200length
v_input_2_hlc30Source: hlc3 | high | low | open | hl2 | close | hlcc4 | ohlc4
v_input_344bb mult (0.1%)
v_input_40Trend source: sma | ema | rma | wma
v_input_50Dev source: stdev | dev
v_input_60.5Spread
v_input_7truePyramiding
v_input_8trueuseStopLoss
v_input_915StopLoss 0.1%
v_input_10falseCustom Backtesting Dates
v_input_112018Backtest Start Year
v_input_12trueBacktest Start Month
v_input_13trueBacktest Start Day
v_input_14falseBacktest Start Hour
v_input_152018Backtest Stop Year
v_input_1612Backtest Stop Month
v_input_1714Backtest Stop Day
v_input_1814Backtest Stop Hour

Source Pine Script

/*backtest
start: 2023-12-24 00:00:00
end: 2024-01-23 00:00:00
period: 1h
basePeriod: 15m
exchanges: [{"eid":"Futures_Binance","currency":"BTC_USDT"}]
*/

//@version=3
strategy(shorttitle="BBL", title="BB limit", overlay = true)


length = input(200, minval=1)
src = input(hlc3, title="Source")
xmult = input(44, minval=0.001, maxval=5000, title = "bb mult (0.1%)")
s = input(title="Trend source", defval = "sma", options = ["ema", "sma", "rma", "wma"])
basis = s == "ema" ? ema(src, length) : s == "sma" ? sma(src, length) : s =="rma" ? rma(src, length) : wma(src, length)
sd = input(title="Dev source", defval = "stdev", options = ["stdev", "dev"])
mult = xmult / 10  
dev = sd == "stdev" ? mult * stdev(src, length) : mult * dev(src, length)
diff = input(0.5, title = "Spread")
LongPrice(p) =>
    LongPrice = diff == 0 ? p : floor(p / diff) * diff

ShortPrice(p) =>
    ShortPrice = diff == 0 ? p : ceil(p / diff) * diff

pyr = input(1, title = "Pyramiding")
useStopLoss = input(true)
stoploss_xmult = input(15, minval=0.001, maxval=5000, title = "StopLoss 0.1%")
stopLoss_mult = sd == "simple" ? 1 + stoploss_xmult / 10 / 100 : stoploss_xmult / 10  
dev2 = sd == "stdev" ? stopLoss_mult * stdev(src, length) : sd == "dev" ? stopLoss_mult * dev(src, length) : (stopLoss_mult - 1) * basis
upper = basis + (1*dev)
lower = basis - (1*dev)
plot(basis, color=fuchsia, linewidth=2)
plot(upper, color=green, linewidth=2)
plot(lower, color=green, linewidth=2)


strategy.cancel_all()

if strategy.position_size > 0 and close <= basis + diff * 2
    strategy.order("Close long", strategy.short, strategy.position_size, limit = ShortPrice(basis))
else 
    if strategy.position_size < 0 and close >= basis - diff * 2
        strategy.order("Close short", strategy.long, -strategy.position_size, limit = LongPrice(basis))
            
stopLossPrice1 = na
stopLossPrice2 = na
add = na
openOrderCondition = close > lower - 2 * diff and (strategy.opentrades < pyr or (strategy.position_size < 0 and strategy.position_avg_price > lower * (1 + stopLoss_mult / 100)))
if openOrderCondition
    add := strategy.position_size > 0 ? -strategy.position_size : close >= basis - diff * 2 ? 0 : -strategy.position_size
    strategy.order("Open long", strategy.long, strategy.equity / pyr / lower + add, limit = LongPrice(lower))
if useStopLoss and (strategy.position_size > 0 or openOrderCondition)
    add = openOrderCondition ? strategy.equity / pyr / lower : 0
    posPrice = strategy.position_size <= 0 ? lower : strategy.position_avg_price
    posSize = strategy.position_size <= 0 ? 0 : strategy.position_size
    stopLossPrice1 := posPrice * (1 - stopLoss_mult / 100)
    strategy.order("StopLoss open short ", strategy.short, posSize + add + strategy.equity / pyr / stopLossPrice1, stop = ShortPrice(stopLossPrice1))


openOrderCondition := close < upper + 2 * diff and (strategy.opentrades < pyr or (strategy.position_size > 0 and strategy.position_avg_price * (1 + stopLoss_mult / 100) < upper))
if openOrderCondition
    add := strategy.position_size < 0 ? strategy.position_size : close <= basis + diff * 2 ? 0 : strategy.position_size
    strategy.order("Open short", strategy.short, strategy.equity / pyr / upper + add, limit = ShortPrice(upper))
if useStopLoss and (strategy.position_size < 0 or openOrderCondition)
    add = openOrderCondition ? strategy.equity / pyr / upper : 0
    posPrice = strategy.position_size >= 0 ? upper : strategy.position_avg_price
    posSize = strategy.position_size >= 0 ? 0 : -strategy.position_size
    stopLossPrice2 := posPrice * (1 + stopLoss_mult / 100)
    strategy.order("StopLoss open long", strategy.long, posSize + add + strategy.equity / pyr / stopLossPrice2, stop = LongPrice(stopLossPrice2))

plot(not useStopLoss ? na : stopLossPrice1, color=red, linewidth=2)
plot(not useStopLoss ? na : stopLossPrice2, color=red, linewidth=2)

// === Backtesting Dates ===
testPeriodSwitch = input(false, "Custom Backtesting Dates")
testStartYear = input(2018, "Backtest Start Year")
testStartMonth = input(1, "Backtest Start Month")
testStartDay = input(1, "Backtest Start Day")
testStartHour = input(0, "Backtest Start Hour")
testPeriodStart = timestamp(testStartYear,testStartMonth,testStartDay,testStartHour,0)
testStopYear = input(2018, "Backtest Stop Year")
testStopMonth = input(12, "Backtest Stop Month")
testStopDay = input(14, "Backtest Stop Day")
testStopHour = input(14, "Backtest Stop Hour")
testPeriodStop = timestamp(testStopYear,testStopMonth,testStopDay,testStopHour,0)
testPeriod() =>
    time >= testPeriodStart and time <= testPeriodStop ? true : false
isPeriod = testPeriodSwitch == true ? testPeriod() : true
// === /END
if not isPeriod
    strategy.cancel_all()
    strategy.close_all()

Published by ChaoZhang on the FMZ strategy square · last modified 2024-01-24. Collected in the Quant Trading Vault. Unvalidated research: re-test with your own data and costs before trusting it.

Build this with AgenKit

A spec is not a trading system. Paste this into your coding agent and AgenKit runs a specialist team — architecture, test-first build, risk gates, review — with paper trading on by default.

in Claude Code, Cursor, Codex & 4 more
/agenkit build a market making bot with inventory skew limits and order deduplication from strategies/Bollinger-Band-Limit-Market-Maker-Strategy.md
  1. npx agenkit install engineering-kit in your project
  2. Paste the command into your agent and approve each phase
  3. Get tests, backtest, risk controls and a paper-trading switch

Understand it first

Before you run this, read the lesson behind the idea and how to backtest it honestly. Specs like this one are the survivors of an unknown number of trials; deflate their results accordingly.

Related specs

esc

Try , , or .

↑↓ move↵ open