Python

Implementing UT Bot Strategy in Python with vectorbt

Implementing UT Bot Strategy in Python with vectorbt

UT Bot Strategy is one of the most popular scripts I published on TradingView. I was asked many time if I have a version of this strategy in Python. Recently I started using vectorbt and I decided finally to implement this strategy in Python.

First let’s import all the libraries we’ll use in this code:

import vectorbt as vbt
import pandas as pd
import numpy as np
import talib
import datetime as dt

URL = 'https://api.binance.com/api/v3/klines'

intervals_to_secs = {
    '1m':60,
    '3m':180,
    '5m':300,
    '15m':900,
    '30m':1800,
    '1h':3600,
    '2h':7200,
    '4h':14400,
    '6h':21600,
    '8h':28800,
    '12h':43200,
    '1d':86400,
    '3d':259200,
    '1w':604800,
    '1M':2592000
}

def download_kline_data(start: dt.datetime, end:dt.datetime ,ticker:str, interval:str)-> pd.DataFrame:
    start = int(start.timestamp()*1000)
    end = int(end.timestamp()*1000)
    full_data = pd.DataFrame()
    
    while start < end:
        par = {'symbol': ticker, 'interval': interval, 'startTime': str(start), 'endTime': str(end), 'limit':1000}
        data = pd.DataFrame(json.loads(requests.get(URL, params= par).text))

        data.index = [dt.datetime.fromtimestamp(x/1000.0) for x in data.iloc[:,0]]
        data=data.astype(float)
        full_data = pd.concat([full_data,data])
        
        start+=intervals_to_secs[interval]*1000*1000
        
    full_data.columns = ['Datetime', 'Open', 'High', 'Low', 'Close', 'Volume','Close_time', 'Qav', 'Num_trades','Taker_base_vol', 'Taker_quote_vol', 'Ignore']
    
    return full_data

I’m importing vectorbt, pandas and numpy for some custom computations, binance api to get the candles for the backtest and talib for access to technical indicators in Python.

Next let’s define the parameters I want to use for my backtest. If you want to change any parameters for your backtest change them here:

# UT Bot Parameters
SENSITIVITY = 1
ATR_PERIOD = 10

# Ticker and timeframe
TICKER = "BTCUSDT"
INTERVAL = "1d"

# Backtest start/end date
START = dt.datetime(2017,8,17)
END   = dt.datetime.now()

Next let’s import data from Binance, with library I imported you can do it with 1 line of code:

# Get data from Binance
pd_data = download_kline_data(START, END, TICKER, INTERVAL)

Next I’ll compute ATR and NLoss variables. I’m using ATR function from TA lib to compute average true range, there are many other useful technical indicators available in this lib. Also I’ll delete all NAs to have a clean dataset.

# Compute ATR And nLoss variable
pd_data["xATR"] = talib.ATR(pd_data["High"], pd_data["Low"], pd_data["Close"], timeperiod=ATR_PERIOD)
pd_data["nLoss"] = SENSITIVITY * pd_data["xATR"]

#Drop all rows that have nan, X first depending on the ATR preiod for the moving average
pd_data = pd_data.dropna()
pd_data = pd_data.reset_index()

Next here is a function and a loop I’m using to compute ATRTrailingStop variable. It’s not the most efficient way to do that, but it 1 to 1 replicated logic from TradingView. I’ll take a look later how it can be computed without the loop.

# Function to compute ATRTrailingStop
def xATRTrailingStop_func(close, prev_close, prev_atr, nloss):
    if close > prev_atr and prev_close > prev_atr:
        return max(prev_atr, close - nloss)
    elif close < prev_atr and prev_close < prev_atr:
        return min(prev_atr, close + nloss)
    elif close > prev_atr:
        return close - nloss
    else:
        return close + nloss

# Filling ATRTrailingStop Variable
pd_data["ATRTrailingStop"] = [0.0] + [np.nan for i in range(len(pd_data) - 1)]

for i in range(1, len(pd_data)):
    pd_data.loc[i, "ATRTrailingStop"] = xATRTrailingStop_func(
        pd_data.loc[i, "Close"],
        pd_data.loc[i - 1, "Close"],
        pd_data.loc[i - 1, "ATRTrailingStop"],
        pd_data.loc[i, "nLoss"],
    )

In the following code I’ll compute “Buy” and “Sell” signals for my UT BOT Strategy:

# Calculating signals
ema = vbt.MA.run(pd_data["Close"], 1, short_name='EMA', ewm=True)

pd_data["Above"] = ema.ma_crossed_above(pd_data["ATRTrailingStop"])
pd_data["Below"] = ema.ma_crossed_below(pd_data["ATRTrailingStop"])

pd_data["Buy"] = (pd_data["Close"] > pd_data["ATRTrailingStop"]) & (pd_data["Above"]==True)
pd_data["Sell"] = (pd_data["Close"] < pd_data["ATRTrailingStop"]) & (pd_data["Below"]==True)

Next we’re ready to run the strategy itself:

# Run the strategy
pf = vbt.Portfolio.from_signals(
    pd_data["Close"],
    entries=pd_data["Buy"],
    short_entries=pd_data["Sell"],
    upon_opposite_entry='ReverseReduce', 
    freq = "d"
)

To check the stats we can use stats() method:

pf.stats()

These will output us following stats for BTCUSDT 1d timeframe:

Start                                                  0
End                                                 1808
Period                                1809 days 00:00:00
Start Value                                        100.0
End Value                                     311.947976
Total Return [%]                              211.947976
Benchmark Return [%]                           435.91175
Max Gross Exposure [%]                             100.0
Total Fees Paid                                      0.0
Max Drawdown [%]                               60.805122
Max Drawdown Duration                  774 days 00:00:00
Total Trades                                         198
Total Closed Trades                                  197
Total Open Trades                                      1
Open Trade PnL                                  1.963699
Win Rate [%]                                   35.532995
Best Trade [%]                                126.663976
Worst Trade [%]                               -21.189107
Avg Winning Trade [%]                          15.214053
Avg Losing Trade [%]                           -5.996914
Avg Winning Trade Duration    14 days 20:54:51.428571428
Avg Losing Trade Duration      5 days 21:21:15.590551181
Profit Factor                                   1.096022
Expectancy                                       1.06591
Sharpe Ratio                                    0.672734
Calmar Ratio                                    0.424353
Omega Ratio                                     1.108914
Sortino Ratio                                   1.055846
dtype: object

If we’re compare this metrics to TradingView’s metrics we’ll see that we’re matching exactly, this means that we implemented strategy correctly:

To plot the chart with P&L/signals and trades you can use following code:

# Show the chart 
fig = pf.plot(subplots=['orders','trade_pnl','cum_returns'])
ema.ma.vbt.plot(fig=fig)
fig.show()

As you can see it’s not very complicated to code in Python even pretty advanced strategies like UT BOT. If you want me to implement other strategies in Python – let me know.


Follow me on TradingView and YouTube.

This image has an empty alt attribute; its file name is wide.png

14 thoughts on “Implementing UT Bot Strategy in Python with vectorbt”

  1. Awesome. I’d bet your profitability would be much better on an instrument with less abrupt price swings, like a stock or forex pair.

  2. ERROR: Could not find a version that satisfies the requirement apis (from versions: none)

    1. It’s a custom code to get data from Binance, here is this function:


      URL = 'https://api.binance.com/api/v3/klines'

      intervals_to_secs = {
      '1m':60,
      '3m':180,
      '5m':300,
      '15m':900,
      '30m':1800,
      '1h':3600,
      '2h':7200,
      '4h':14400,
      '6h':21600,
      '8h':28800,
      '12h':43200,
      '1d':86400,
      '3d':259200,
      '1w':604800,
      '1M':2592000
      }

      def download_kline_data(start: dt.datetime, end:dt.datetime ,ticker:str, interval:str)-> pd.DataFrame:
      start = int(start.timestamp()*1000)
      end = int(end.timestamp()*1000)
      full_data = pd.DataFrame()

      while start < end: par = {'symbol': ticker, 'interval': interval, 'startTime': str(start), 'endTime': str(end), 'limit':1000} data = pd.DataFrame(json.loads(requests.get(URL, params= par).text)) data.index = [dt.datetime.fromtimestamp(x/1000.0) for x in data.iloc[:,0]] data=data.astype(float) full_data = pd.concat([full_data,data]) start+=intervals_to_secs[interval]*1000*1000 full_data.columns = ['Datetime', 'Open', 'High', 'Low', 'Close', 'Volume','Close_time', 'Qav', 'Num_trades','Taker_base_vol', 'Taker_quote_vol', 'Ignore'] return full_data

      1. So.. one question, I’m trying to implement it not as a backtest, but live. I just cannot find the take-profit and stoploss in the code, only the ATRtrailingstop which is used to indicate when/whether to buy and sell.
        When the buy/sell signal is produced, how would you determine the SL/TP?

        Thanks in advance!

        Joy

  3. Thank you, I got it. After executing your code, whole copy paste directly the output is not same or wrong.
    tart 0
    End 2403
    Period 2404 days 00:00:00
    Start Value 100.0
    End Value -1817.297673
    Total Return [%] -1917.297673
    Benchmark Return [%] 1518.093694
    Max Gross Exposure [%] 100.0
    Total Fees Paid 0.0
    Max Drawdown [%] 674.045678
    Max Drawdown Duration 2301 days 00:00:00
    Total Trades 31
    Total Closed Trades 30
    Total Open Trades 1
    Open Trade PnL -2036.013137
    Win Rate [%] 43.333333
    Best Trade [%] 126.663976
    Worst Trade [%] -20.708764
    Avg Winning Trade [%] 22.977098
    Avg Losing Trade [%] -8.584429
    Avg Winning Trade Duration 14 days 14:46:09.230769230
    Avg Losing Trade Duration 5 days 08:28:14.117647058
    Profit Factor 1.363926
    Expectancy 3.957182
    Sharpe Ratio -0.513601
    Calmar Ratio NaN
    Omega Ratio 0.632677
    Sortino Ratio -0.527403
    dtype: object

  4. The data is same, copy paste every thing.
    Do you think is there any mistake in my program?

    import vectorbt as vbt
    import pandas as pd
    import numpy as np
    import talib
    import datetime as dt
    import requests
    import json
    import binance

    URL = ‘https://api.binance.com/api/v3/klines’

    intervals_to_secs = {
    ‘1m’: 60,
    ‘3m’: 180,
    ‘5m’: 300,
    ’15m’: 900,
    ’30m’: 1800,
    ‘1h’: 3600,
    ‘2h’: 7200,
    ‘4h’: 14400,
    ‘6h’: 21600,
    ‘8h’: 28800,
    ’12h’: 43200,
    ‘1d’: 86400,
    ‘3d’: 259200,
    ‘1w’: 604800,
    ‘1M’: 2592000
    }

    def download_kline_data(start: dt.datetime, end: dt.datetime, ticker: str, interval: str) -> pd.DataFrame:
    start = int(start.timestamp() * 1000)
    end = int(end.timestamp() * 1000)
    full_data = pd.DataFrame()

    while start prev_atr and prev_close > prev_atr:
    return max(prev_atr, close – nloss)
    elif close < prev_atr and prev_close prev_atr:
    return close – nloss
    else:
    return close + nloss

    # Filling ATRTrailingStop Variable

    pd_data[“ATRTrailingStop”] = [0.0] + [np.nan for i in range(len(pd_data) – 1)]

    for i in range(1, len(pd_data)):
    pd_data.loc[i, “ATRTrailingStop”] = xATRTrailingStop_func(
    pd_data.loc[i, “Close”],
    pd_data.loc[i – 1, “Close”],
    pd_data.loc[i – 1, “ATRTrailingStop”],
    pd_data.loc[i, “nLoss”],
    )
    # Calculating signals
    ema = vbt.MA.run(pd_data[“Close”], 1, short_name=’EMA’, ewm=True)

    pd_data[“Above”] = ema.ma_crossed_above(pd_data[“ATRTrailingStop”])
    pd_data[“Below”] = ema.ma_crossed_below(pd_data[“ATRTrailingStop”])
    pd_data[“Buy”] = (pd_data[“Close”] > pd_data[“ATRTrailingStop”]) & pd_data[“Above”]
    pd_data[“Sell”] = (pd_data[“Close”] < pd_data["ATRTrailingStop"]) & pd_data["Below"]

    # Run the strategy
    pf = vbt.Portfolio.from_signals(
    pd_data["Close"],
    entries=pd_data["Buy"],
    short_entries=pd_data["Sell"],
    upon_opposite_entry='ReverseReduce',
    freq="d"
    )
    # print(pf.stats())
    pf.stats()
    # Show the chart
    fig = pf.plot(subplots=['orders', 'trade_pnl', 'cum_returns'])
    ema.ma.vbt.plot(fig=fig)
    fig.show()

  5. Awesome . Were you able to find a way to calculate the ATRTrailingStop variables without using the loop?

    1. Hi Federico,
      I’m not sure it’s possible to do that with vector. It’s past dependant.

      1. What is the mathematical algorithm that vectorbt uses to calculate the signals Above & Below?

        # Calculating signals
        ema = vbt.MA.run(pd_data[“Close”], 1, short_name=’EMA’, ewm=True)

        pd_data[“Above”] = ema.ma_crossed_above(pd_data[“ATRTrailingStop”])
        pd_data[“Below”] = ema.ma_crossed_below(pd_data[“ATRTrailingStop”])

        Thank you in advance for your response!

        1. Well, I think it’s simply if first value for previous bar was below the second value, but now it’s above.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

Pine Script Programming Courses
Pine Script Programming Courses
Learn to build your own TradingView Indicators and Strategies
Sidebar Signup Form
If you want to be the first in this business, subscribe to the latest news