Select Page

The cryptocurrency market is very volatile. How do you take advantage and earn extra income from it?  You buy low and sell high. Easy to say, but what would be your first step?  Which trading pairs would you trade?

A strategy that comes to mind is range trading.   When a cryptocurrency trading pair has been moving sideways for a couple of days, you identify a range at which you will buy and sell at over a short period of time. Let’s say you buy an asset at $1 and sell it at $1.10.  You believe it is going to trade again at $1 then rise again to $1.10. You repeat this process until you think the price will no longer trade in this range.

There is a volatility indicator designed to determine if the market is choppy (trading sideways) or not choppy (trading within a trend in either direction). It is called the Choppiness Index (CHOP).  CHOP is not meant to predict future market direction, it is a metric to be used for defining the market’s trendiness only. 

The focus of this tutorial is to automate the computation for the Choppiness Index of all the  cryptocurrency trading pairs of the exchange of your choice.  Values range between 0 and 100, with low values indicating a strong trend and high values signaling consolidation. If the Choppiness Index for the pair is equal to 61.8 or higher, they will be included in the list of pairs that you can potentially place trades on. This does NOT include identifying significant price levels that you can use to enter and exit trades.

To calculate Choppinness Index:

100 * LOG10( SUM(ATR(1), n) / ( MaxHi(n) - MinLo(n) ) ) / LOG10(n)

n = User defined period length.
LOG10(n) = base-10 LOG of n
ATR(1) = Average True Range (Period of 1)
SUM(ATR(1), n) = Sum of the Average True Range over past n bars MaxHi(n) = The highest high over past n bars

Here is a sample web application:

The Open Chart link opens a new web page displaying a simple chart of Choppiness Index along with a line chart of the cryptocurrency trading pair’s closing prices.

You may compare the application’s charts side by side with TradingView chart, adding the indicator called “Choppiness Index (CHOP)”

Below are the sections of this tutorial:

  • Part 1 Setup API Keys in your Cryptocurrency Exchange (Binance US and Kraken)
  • Part 2 Setup Environment Variables
  • Part 3 Setup Python project workspace
  • Part 4 Writing the Python programs
  • Part 5 Running the application

Part 1 – Setup API Keys in your Cryptocurrency Exchange

Part 1A – Setting up API Keys in Binance US

Step 1. Login to your Binance US account.

Step 2. Click “API Management” under the email address drop-down.


Step 3. Enter the API key label. Click “Create” when done.


Step 4. An email is sent to confirm that you created the API key.


Step 5. Open your email. Click “Confirm API Key Creationbutton.


Step 6. The newly-created API key will be displayed. Make sure to save the Secret Key somewhere safe. It will disappear after refreshing the page.

Part 1B – Setting up API Keys in Kraken

Step 1. Login to your Kraken account in https://www.kraken.com

Step 2. Click on your name on the top right panel of the website. Select “API” from the list.

Step 3. Click the “Add key” link.

Step 4. Select at least one key permission. Then click the “Generate key” button.

Step 5. The API key and Private key will be displayed. Save these information in a safe place.

PART 2 – Setup Environment Variables

Step 1. If you are using Windows, select Control Panel -> System and Security -> System -> Advanced system settings -> Environment Variables..

Step 2. Click “New…” In the Variable Name and Variable Value fields, enter these data:

Variable nameNotes
BINANCE_US_API_KEYCopy the API Key from Part 1A Step 6
BINANCE_US_SECRET_KEYCopy the Secret Key from Part 1A Step 6
KRAKEN_APICopy the API key from Part 1B Step 5
KRAKEN_PRIVATECopy the Private key from Part 1B Step 5

PART 3 – Setup Python project workspace

Step 1. Install Pycharm Community Edition in https://www.jetbrains.com/pycharm/download/
This tutorial uses the latest stable release pycharm-community-2021.1.2.exe

Step 2. Open Pycharm. Create New Project with a Virtual environment. The latest Pycharm installer will always have new virtual environment installed.

Step 3. Create new file with filename requirements.txt. Enter the following library names

ccxt
pandas
datetime
requests
flask
numpy
matplotlib

Step 4. Select “Terminal” on the bottom panel. This will launch a command line. Execute the command to install the libraries listed in requirements.txt

pip3 install -r requirements.txt

PART 4 – Writing the Python programs

Step 1. Create new Python files called config.py. This will contain most of the settings for the application.

config.py

import os

EXCHANGE_USED = "BINANCEUS"

BINANCE_US_API_KEY = os.environ.get('BINANCE_US_API_KEY')
BINANCE_US_SECRET_KEY = os.environ.get('BINANCE_US_SECRET_KEY')
KRAKEN_API = os.environ.get('KRAKEN_API')
KRAKEN_PRIVATE = os.environ.get('KRAKEN_PRIVATE')

CHART_TIMEFRAME = '1d'
CHOPPINESS_DAYS = 14
LIMIT = CHOPPINESS_DAYS * 24
START_DATE = '20220101'
RANGING_INDEX = 61.8
TRENDING_INDEX = 38.2

EXCLUDE_CURRENCY = ['GBP', 'JPY', 'EUR', 'CAD', 'AUD', 'ZUSD', 'CHF']
PRICE_PRECISION = ".8f"
CHOP_INDEX_PRECISION = 2

Step 2. Create a new file called app.py. It will have all logic from connecting to the cryptocurrency exchange, to loading the exchange’s trading pairs, calculating CHOP indices and categorizing the trading pair to Ranging, Trending or neither of the two. This file also handles the plotting of chart into HTML file.

app.py

from flask import Flask, render_template
import ccxt
import config
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from io import BytesIO
import base64
import datetime, time
from pytz import timezone

app = Flask(__name__)


class Asset:
    def __init__(self, symbol, chop_indicator, min_range_price, max_range_price, chop_chart_url):
        self.symbol = symbol
        self.chop_indicator = chop_indicator
        self.min_range_price = min_range_price
        self.max_range_price = max_range_price
        self.chop_chart_url = chop_chart_url


@app.route('/', methods=['GET'])
def get_sideways_crypto_pairs():
    print(f'default exchange {config.EXCHANGE_USED}')
    return get_sideways_crypto_pairs_exchange(config.EXCHANGE_USED)


# If you want to override the default exchange defined in config.py, pass the exchange name in the URL
@app.route('/cryptoPairsSideways/<param_exchange>', methods=['GET'])
def get_sideways_crypto_pairs_exchange(param_exchange):
    start_time = time.time()
    if param_exchange is not None:
        config.EXCHANGE_USED = param_exchange
    print(config.EXCHANGE_USED)
    exchange = init_exchange(param_exchange)

    if exchange is None:
        return f'Unable to connect to the Cryptocurrency Exchange settings for {param_exchange}.'
    markets = exchange.load_markets()
    symbols = [markets[market]['id'] for market in markets]
    exclude = config.EXCLUDE_CURRENCY
    symbols = [symbol for symbol in symbols if all(excludes not in symbol for excludes in exclude)]

    ranging_list = []
    not_ranging_list = []
    neither_list = []
    ctr = 0
    total = len(symbols)
    for symbol in symbols:
        print(f"{ctr} of {total} {symbol}")
        ctr += 1
        data = get_exchange_symbol_data(exchange, symbol)
        if data is not None and data.size > 0:
            if data[-1:]['CHOP'].values[0] >= config.RANGING_INDEX:
                asset = Asset(symbol, round(data[-1:]['CHOP'].values[0], config.CHOP_INDEX_PRECISION),
                              format(data[-config.LIMIT:]['Close'].min(), config.PRICE_PRECISION),
                              format(data[-config.LIMIT:]['Close'].max(), config.PRICE_PRECISION),
                              f"/choppinessChart/{param_exchange}/{symbol}")
                ranging_list.append(asset)
            elif data[-1:]['CHOP'].values[0] >= config.TRENDING_INDEX:
                asset = Asset(symbol, round(data[-1:]['CHOP'].values[0], config.CHOP_INDEX_PRECISION),
                              format(data[-config.LIMIT:]['Close'].min(), config.PRICE_PRECISION),
                              format(data[-config.LIMIT:]['Close'].max(), config.PRICE_PRECISION),
                              f"/choppinessChart/{param_exchange}/{symbol}")
                not_ranging_list.append(asset)
            else:
                asset = Asset(symbol, round(data[-1:]['CHOP'].values[0], config.CHOP_INDEX_PRECISION),
                              format(data[-config.LIMIT:]['Close'].min(), config.PRICE_PRECISION),
                              format(data[-config.LIMIT:]['Close'].max(), config.PRICE_PRECISION),
                              f"/choppinessChart/{param_exchange}/{symbol}")
                neither_list.append(asset)

    html = "<html><head><title>Crypto Trading Pairs on Sideways</title>"
    html += "<link rel='stylesheet' href='/static/sorta.css'>"
    html += "<script src='/static/sort-table.js'></script></head><body>"

    time_now = str(datetime.datetime.now(timezone('EST')))[:-7]
    html += time_now
    html += print_criteria()
    html += print_results(ranging_list, not_ranging_list, neither_list)
    html += "</body></html>"

    end_time = time.time()
    print(f'time total:{end_time - start_time}')
    return html


def init_exchange(param_exchange):
    exchange = None
    if param_exchange == "BINANCEUS":
        exchange = ccxt.binanceus({
            'apiKey': config.BINANCE_US_API_KEY,
            'secret': config.BINANCE_US_SECRET_KEY
        })
    elif param_exchange == "KRAKEN":
        exchange = ccxt.kraken({
            'apiKey': config.KRAKEN_API,
            'secret': config.KRAKEN_PRIVATE,
        })
    return exchange


def get_exchange_symbol_data(exchange, symbol):
    from datetime import datetime
    since = int(datetime(int(config.START_DATE[:4]), int(config.START_DATE[4:6]),
                         int(config.START_DATE[6:])).timestamp() * 1000)
    data = None
    try:
        klines = exchange.fetch_ohlcv(symbol, config.CHART_TIMEFRAME, since, config.LIMIT)

        if len(klines) > config.CHOPPINESS_DAYS:
            data = pd.DataFrame(klines, columns=['timestamp', 'Open', 'High', 'Low', 'Close', 'Volume'])
            data['timestamp'] = pd.to_datetime(data['timestamp'], unit='ms')
            data = data.dropna()
            data['CHOP'] = get_choppiness_index(data['High'], data['Low'], data['Close'], config.CHOPPINESS_DAYS)
    except Exception as e:
        pass
    return data


def get_choppiness_index(high, low, close, lookback):
    tr1 = pd.DataFrame(high - low).rename(columns={0: 'tr1'})
    tr2 = pd.DataFrame(abs(high - close.shift(1))).rename(columns={0: 'tr2'})
    tr3 = pd.DataFrame(abs(low - close.shift(1))).rename(columns={0: 'tr3'})
    frames = [tr1, tr2, tr3]
    tr = pd.concat(frames, axis=1, join='inner').dropna().max(axis=1)
    atr = tr.rolling(1).mean()
    highest = high.rolling(lookback).max()
    lowest = low.rolling(lookback).min()
    choppiness_index = 100 * np.log10((atr.rolling(lookback).sum()) / (highest - lowest)) / np.log10(lookback)
    return choppiness_index


def print_criteria():
    html = "<br/><br/><b>SETTINGS</b>"
    html += f"<table border='1'><tr><td>RANGING INDEX</td><td>{config.RANGING_INDEX}</td></tr>" \
            f"<tr><td>TRENDING INDEX</td><td>{config.TRENDING_INDEX}</td></tr>" \
            f"<tr><td>CHART TIMEFRAME</td><td>{config.CHART_TIMEFRAME}</td></tr>" \
            f"<tr><td>CRYPTO EXCHANGE USED</td><td>{config.EXCHANGE_USED}</td></tr>" \
            f"<tr><td>IGNORED QUOTE CURRENCY</td><td>{config.EXCLUDE_CURRENCY}</td></tr>" \
            "</table><br/>"
    return html


def print_results(ranging_list, not_ranging_list, neither_list):
    html = print_table(ranging_list, "Ranging (Sideways Crypto Pairs)")
    html += print_table(not_ranging_list, "Trending Pairs")
    html += print_table(neither_list, "Neither Ranging nor Trending Pairs")
    return html


def print_table(list, label):
    ctr = 0
    html = f"<br><b>{label}</b><br>"
    html += "<table border='1' class='js-sort-table'><thead><tr><th class='js-sort-number'>#</th>" \
            "<th class='js-sort-string'><b>Pair</b></th>" \
            "<th class='js-sort-number'><b>Choppiness Index</b></th>" \
            "<th class='js-sort-number'><b>Min. Range Price</b></th>" \
            "<th class='js-sort-number'><b>Max. Range Price</b></th>" \
            "<th>Choppiness Indicator Chart</th></tr></thead>"
    for item in list:
        ctr += 1
        html += f"<tr><td>{ctr}</td> " \
                f"<td>{item.symbol}</td>" \
                f"<td align='right'>{item.chop_indicator}</td>" \
                f"<td align='right'>{item.min_range_price}</td>" \
                f"<td align='right'>{item.max_range_price}</td>" \
                f"<td><a href='{item.chop_chart_url}' target='_blank'>Open Chart</a></td></tr>"
    html += "</table>"
    return html


@app.route('/choppinessChart/<param_exchange>/<symbol>', methods=['GET'])
def display_choppiness_chart(param_exchange, symbol):
    exchange = init_exchange(param_exchange)
    data = get_exchange_symbol_data(exchange, symbol)

    ax1 = plt.subplot2grid((11, 1,), (0, 0), rowspan=5, colspan=1)
    ax2 = plt.subplot2grid((11, 1,), (6, 0), rowspan=4, colspan=1)

    ax1.plot(data['Close'], linewidth=1.5, color='#2196f3')
    ax1.set_title(f'{symbol} ({exchange}) CLOSING PRICES')

    ax2.plot(data['CHOP'], linewidth=1.5, color='#fb8c00')
    ax2.axhline(38.2, linestyle='--', linewidth=1.5, color='grey')
    ax2.axhline(61.8, linestyle='--', linewidth=1.5, color='grey')
    ax2.set_title(f'{symbol} CHOPPINESS INDEX')

    img_bytes = BytesIO()
    plt.savefig(img_bytes, format='png')
    plt.close()
    plot_url = base64.b64encode(img_bytes.getvalue()).decode("utf-8").replace("\n", "")
    return render_template('plot.html', plot_url=plot_url)

Step 3. Create a directory called templates, then add a new HTML file called plot.html. This will contain the template for displaying the popup window that contains the line charts for the stock’s prices and Choppines Index

templates/plot.html

<!doctype html>
<head meta charset="utf-8"> <title>Choppiness Index</title></head>

<section>
  <img src="data:image/png;base64, {{ plot_url }}">
</section>

PART 5. Running the application

Step 1. Select “Terminal” on the bottom panel.

flask run

Step 2. Open a browser and enter http://127.0.0.1/ This is load the default cryptocurrency exchange defined in config.py, which is BINANCEUS.

You may switch to another cryptocurrency exchange without restarting your server, by adding /cryptoPairsSideways/<exchangeName>

Example: hhttp://127.0.0.1:5000/cryptoPairsSideways/KRAKEN

References:

Fidelity : Range Trading

Detecting Ranging and Trending Markets with Choppiness Index in Python

Range Trading Crypto Strategy: How to Use and Make Profit

Incredible Charts: Choppiness Index

TradingView : Choppiness Index (CHOP)