avatarB/O Trading Blog

Summary

The provided content outlines a comprehensive guide to building a free momentum screener for over 13,000 cryptocurrency coins using Python, which utilizes moving averages and the Relative Strength Index (RSI) to identify coins with strong upward trends.

Abstract

The article details the creation of a momentum-based cryptocurrency screener that analyzes a vast array of coins to pinpoint those experiencing a rebound after market downturns, indicating potential investment opportunities. The strategy employs a combination of three moving averages (60, 40, and 15 periods) and the RSI to detect recent upward trends. The screener fetches coin data from the CoinGecko API, circumventing rate limits by caching the list of coins and throttling requests. Technical indicators are calculated using the pandas-ta library, and the results are used to evaluate a set of predefined strategy conditions. Coins that meet these conditions are then detailed in an HTML report generated by the Yattag Python library. The report includes general coin information, links, social media handles, sentiment analysis, and rankings, along with price graphs generated by Plotly. The article emphasizes the importance of the screener as a tool for traders and investors, providing a means to sift through the vast crypto market efficiently.

Opinions

  • The author emphasizes the educational purpose of the screener and cautions against using it as a sole source for trading decisions.
  • The strategy is designed to catch coins with recent price surges, suggesting a belief in the momentum factor as a predictor of future performance.
  • The use of CoinGecko's API is praised for its generosity and ease of access, facilitating the development of such tools for the trading community.
  • The author values the importance of visual data representation, as evidenced by the inclusion of detailed price graphs in the HTML report.
  • By sharing the full implementation on a GitHub repository, the author demonstrates a commitment to open-source collaboration and community learning.
  • The article subtly promotes the author's other content and platforms, indicating a desire to build a following and establish thought leadership in the algorithmic trading space.

Build a Momentum Screener for over 13K Crypto Coins for Free (in Python)

Photo by Kanchanara on Unsplash.com

That’s right, folks. I’m not messing with you. A free screener for 13,418 crypto coins. Sweet!

So, what are am I really trying to achieve this post? I was initially trying to build a screener that would catch coins bouncing back after the market turndown making them attractive for investment.

This story is solely for general information purposes, and should not be relied upon for trading recommendations or financial advice. Source code and information is provided for educational purposes only, and should not be relied upon to make an investment decision. Please review my full cautionary guidance before continuing.

However, I realized that the screener can be repurposed for other tasks, e.g.

  • Pull coin details from CoinGecko.
  • Perform technical analysis on the stock price.
  • Hook it up to a news sentiment analysis
  • Feed the info to a scraper to fetch data from the coin websites
  • Building a coin info database, etc.

Trade Ideas provides AI stock suggestions, AI alerts, scanning, automated trading, real-time stock market data, charting, educational resources, and more. Get a 15% discount with promo code ‘BOTRADING15’.

The Strategy

The purpose of this screener is to identify coins, that are in the process of bouncing back from the recent market turndown. So the core of my momentum strategy is using Moving Averages (MAs) to detect upward trends.

I’m using three different MAs: 60, 40 and 15 periods. The 30 day price data from CoinGecko is fetched in 4 hour intervals. That gives us 180 periods.

We need to consider that some lead periods are used to calculate the MA. I chose the three MA periods because I wanted to detect fairly recent upward trends. 60 periods equate to 10 days considering the 4 hour intervals. Adding the other two MAs will allow us to detect the more recent upwards trends in the last few days.

The second component of the strategy is the RSI. The RSI is traditionally used to identify overbought assets. However, in cases of strong upward trends the RSI can stay in the high numbers (e.g. over 80) for extended periods.

Combining both components should help us identify coins with a strong upwards price trends.

Here the strategy rules I used:

  • 40 MA has to be greater 60 MA
  • 60 MA has to be greater than 60 MA 2 days ago
  • 15 MA has to be greater than 40MA
  • Close price has to be greater 60 MA
  • Close price has to be greater 40 MA
  • Close price has to be greater 15 MA
  • Close price has to be within 30 of the 180 period low price
  • RSI has to be greater or equal to 80.

Technologies Used

So how do we make this magic happen? This screener is possible thanks to the generous folks at CoinGecko and their CoinGecko API. They provide a number of APIs for coin info, coin prices, and market information for free. You don’t even have to register.

In particular I’m using the following APIs:

  • /coins/list — Fetches the list of coins and CoinGecko id. We will retrieve the results from my GitHub repo instead for faster performance.
  • /coins/{id}/ohlc — To retrieve coin pricing info
  • /coins/{id} — To retrieve general coin information

The only big restriction here is a 50-call per minute limit. I’m getting around that by throttling my calls to CoinGecko.

Here the link to the official CoinGeck API documentation.

We will be using an unofficial CoinGecko client for Python called ‘coingecko’. Here the GitHub documentation for the library.

To calculate the technical indicators, I used the pandas-ta library.

To generate the HTML page we will be using the Python library Yattag and the docs can be found here.

Implementation

You can find the full implementation of the screener on my repo here.

Here the required Python imports. You may have to install them first.

from datetime import datetime
import pandas as pd
import pandas_ta as ta
import datetime
from pandas.tseries.holiday import USFederalHolidayCalendar
from pandas.tseries.offsets import CustomBusinessDay
US_BUSINESS_DAY = CustomBusinessDay(calendar=USFederalHolidayCalendar())
import matplotlib.pyplot as plt
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from pycoingecko import CoinGeckoAPI
import time
from yattag import Doc
import requests

This function fetches the list of coins. I placed the result JSON on my GitHub repo for faster performance.

def get_coin_ids():
    try:
        #  Slow response on Coingecko
        # uri = 'https://api.coingecko.com/api/v3/coins/list'
        uri = 'https://raw.githubusercontent.com/justmobiledev/python-algorithmic-trading/main/data/coingecko-list.json'
        response = requests.get(uri).json()
        return response
    except Exception as e:
        print('Failed to look up Coinbase assets', e)
        return None

This function calculates the Moving Averages and RSI using the pandas-ta library.

def calculate_technical_indicators(df):
    df['60_MA'] = df['close'].rolling(window=60).mean()
    df['40_MA'] = df['close'].rolling(window=40).mean()
    df['15_MA'] = df['close'].rolling(window=15).mean()
    df['RSI'] = ta.rsi(df["close"], length=14)
    return df

The next function calculates the metrics we need for our strategy.

def calculate_metrics(df):
    #  Values 2 days ago. 6 values per day (data every 4 hours)
    df['60_MA_2d_ago'] = df.iloc[-12]['60_MA']
    df['40_MA_2d_ago'] = df.iloc[-12]['40_MA']
    df['RSI_2d_ago'] = df.iloc[-12]['RSI']
    #  Values 5 days ago. 6 values per day (data every 4 hours)
    df['60_MA_5d_ago'] = df.iloc[-30]['60_MA']
    df['40_MA_5d_ago'] = df.iloc[-30]['40_MA']
    df['RSI_5d_ago'] = df.iloc[-30]['RSI']
    #  min and max
    df['min_close'] = df['close'].min()
    df['max_close'] = df['close'].max()
    # Condition 6: Current Price is at least 30% above 52-week low
    df['within_close_low'] = df['min_close'] * 1.30
    # Condition 7: Current Price is within 90% of 52-week high
    df['within_close_high'] = df['max_close'] * 0.90
    return df

The next block evaluates the different rules we defined for our strategy (see Strategy section above).

def evaluate_conditions(df):
    df['condition1'] = (df['close'] > df['60_MA']) & (df['close'] > df['60_MA'])
    df['condition2'] = df['40_MA'] > df['60_MA']
    df['condition3'] = df['60_MA'] > df['60_MA_2d_ago']
    df['condition4'] = (df['40_MA'] > df['60_MA']) & (df['15_MA'] > df['40_MA'])
    df['condition5'] = df['close'] > df['15_MA']
    df['condition6'] = df['close'] < df['within_close_low']
    df['condition7'] = df['RSI_2d_ago'] >= 80
    #  Select stocks where all conditions are met
    query = "condition1 == True & condition2 == True & condition3 == True & condition4 == True & \
            condition5 == True & condition6 == True & condition7 == True"
    selection_df = df.query(query)
    return selection_df

Here we are fetching the coin price data for the last 30 days in open, high, low, close format from CoinGecko.

def get_ohlc(cg, coin_id, vs_currency, days):
    try:
        response = cg.get_coin_ohlc_by_id(coin_id, vs_currency=vs_currency, days=days)
        ohlc_df = pd.DataFrame()
        for ohlc in response:
            row = pd.DataFrame(
                {'epoch': [ohlc[0]], 'open': [ohlc[1]], 'high': [ohlc[2]], 'low': [ohlc[3]], 'close': [ohlc[4]]})
            ohlc_df = pd.concat([ohlc_df, row], axis=0, ignore_index=True)
        return ohlc_df
    except Exception as e:
        print('Failed to fetch CoinGecko price info', e)
        return None

This next function retrieves general coin information from CoinGecko and parses the results. We are going to use this information to build out HTML report.

def get_coin_info(cg, coin_id):
    try:
        response = cg.get_coin_by_id(coin_id)
        categories = response['categories']
        public_notice = response['public_notice']
        name = response['name']
        description = response['description']['en']
        links = response['links']
        homepage_link = links['homepage']
        blockchain_site = links['blockchain_site']
        if blockchain_site is not None:
            blockchain_site = ",".join(blockchain_site)
        official_forum_url = links['official_forum_url']
        chat_url = links['chat_url']
        announcement_url = links['announcement_url']
        twitter_screen_name = links['twitter_screen_name']
        facebook_username = links['facebook_username']
        telegram_channel_identifier = links['telegram_channel_identifier']
        subreddit_url = links['subreddit_url']
        sentiment_votes_up_percentage = response['sentiment_votes_up_percentage']
        sentiment_votes_down_percentage = response['sentiment_votes_down_percentage']
        market_cap_rank = response['market_cap_rank']
        coingecko_rank = response['coingecko_rank']
        coingecko_score = response['coingecko_score']
        community_score = response['community_score']
        liquidity_score = response['liquidity_score']
        public_interest_score = response['public_interest_score']
        row = pd.DataFrame(
            {'id': [coin_id], 'name': [name], 'categories': [categories], 'public_notice': [public_notice], \
             'description': [description], 'homepage_link': [homepage_link], \
             'blockchain_site': [blockchain_site], 'official_forum_url': [official_forum_url], \
             'chat_url': [chat_url], 'announcement_url': [announcement_url], \
             'twitter_screen_name': [twitter_screen_name], 'facebook_username': [facebook_username], \
             'telegram_channel_identifier': [telegram_channel_identifier], 'subreddit_url': [subreddit_url], \
             'sentiment_votes_up_percentage': [sentiment_votes_up_percentage], \
             'sentiment_votes_down_percentage': [sentiment_votes_down_percentage], \
             'market_cap_rank': [market_cap_rank], \
             'coingecko_rank': [coingecko_rank], \
             'coingecko_score': [coingecko_score], 'coingecko_score': [coingecko_score], \
             'community_score': [community_score], 'liquidity_score': [liquidity_score], \
             'public_interest_score': [public_interest_score]})
        row = row.set_index('id')
        return row
    except Exception as e:
        print('Failed to fetch CoinGecko coin info', e)
        return None

This function builds the HTML report of the coins that match our criteria using the Yattag Python library. The report is stored in the same directory with the file name ‘screener-coins-report.html’.

def build_report(coin_ids, info_map):
    # doc, tag, text, line = Doc().tagtext()
    doc, tag, text, line = Doc().ttl()
    now_str = datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")
    with tag('html'):
        with tag('head'):
            with tag('style'):
                text("""div{ width: 100%; }
                        html { margin: 0; padding: 10px; background: #1e81b0;}
                        body { font: 12px verdana, sans-serif;line-height: 1.88889; margin: 5%; background: #ffffff; padding: 1%; width: 90%; }
                        p { margin-top: 5px; text-align: justify; font: normal 0.9em verdana, sans-serif;color:#484848}
                        li { font: normal 0.8em verdana, sans-serif;color:#484848}
                        h1 { font: normal 1.8em verdana, sans-serif; letter-spacing: 1px; margin-bottom: 0; color: #063970,}
                        h2 { font: normal 1.6em verdana, sans-serif; letter-spacing: 1px; margin-bottom: 0; color: #154c79}
                        h3 { font: normal 1.6em verdana, sans-serif; letter-spacing: 1px; margin-bottom: 0; color: #154c79}
                        p.bold_text{ font: normal 0.9em verdana, sans-serif; letter-spacing: 1px; margin-bottom: 0; color: #154c79; font-weight: bold}""")
        with tag('body', id='body'):
            with tag('h1'):
                text(f"Trending Crypto Report for {now_str}")
            with tag('hr'):
                text('')
            for coin_id in coin_ids:
                coin_df = info_map[coin_id]
                with tag('h2'):
                    text(f"{coin_id.upper()}")
                with tag('div'):
                    line('p', f"Name: {coin_df['name'].values[0]}", klass="bold_text")
                    line('p', coin_df['name'].values[0])
                    line('p', f"Description:", klass="bold_text")
                    line('p', coin_df['description'].values[0])
                    line('p', f"Categories:", klass="bold_text")
                    category_list = ""
                    if coin_df['categories'].values[0] is not None:
                        for category_str in coin_df['categories'].values[0]:
                            if category_str is None:
                                continue
                            if len(category_list) > 0:
                                category_list += ", "
                            category_list += category_str
                    line('p', f"{category_list}")
                    line('p', f"Public Notice:", klass="bold_text")
                    line('p', f"{coin_df['public_notice'].values[0]}")
                    line('p', f"Links:", klass="bold_text")
                    with tag('ul', id='links-list'):
                        url_list = ""
                        if coin_df['homepage_link'].values[0] is not None:
                            for link_str in coin_df['homepage_link'].values[0]:
                                url_list += link_str
                        line('li', f"Home Page: {url_list}")
                        line('li', f"Blockchain Site: {coin_df['blockchain_site'].values[0]}")
                        url_list = ""
                        if coin_df['official_forum_url'].values[0] is not None:
                            for link_str in coin_df['official_forum_url'].values[0]:
                                url_list += link_str
                        line('li', f"Official Forum URLs: {url_list}")
                        url_list = ""
                        if coin_df['chat_url'].values[0] is not None:
                            for link_str in coin_df['chat_url'].values[0]:
                                url_list += link_str
                        line('li', f"Chat URLs: {url_list}")
                    line('p', f"Social Media:", klass="bold_text")
                    with tag('ul', id='social-list'):
                        line('li', f"Twitter: {coin_df['twitter_screen_name'].values[0]}")
                        line('li', f"Facebook: {coin_df['facebook_username'].values[0]}")
                        line('li', f"Telegram: {coin_df['telegram_channel_identifier'].values[0]}")
                    line('p', f"Sentiment:", klass="bold_text")
                    with tag('ul', id='sentiment-list'):
                        line('li', f"Votes Up: {coin_df['sentiment_votes_up_percentage'].values[0]}")
                        line('li', f"Votes Down: {coin_df['sentiment_votes_down_percentage'].values[0]}")
                    line('p', f"Ranks:", klass="bold_text")
                    with tag('ul', id='sentiment-list'):
                        line('li', f"Market Cap Rank: {coin_df['market_cap_rank'].values[0]}")
                        line('li', f"Gecko Rank: {coin_df['coingecko_rank'].values[0]}")
                        line('li', f"Gecko Score: {coin_df['coingecko_score'].values[0]}")
                        line('li', f"Community Score: {coin_df['community_score'].values[0]}")
                        line('li', f"Public Interest Score: {coin_df['public_interest_score'].values[0]}")
                    with tag('div', id='photo-container'):
                        line('p', f"Day Plot:", klass="bold_text")
                        doc.stag('img', src=f"{coin_id}-ohlc-day.png", klass="day_plot")
                        line('p', f"Month Plot:", klass="bold_text")
                        doc.stag('img', src=f"{coin_id}-ohlc-month.png", klass="day_plot")
                    with tag('hr'):
                        text('')
    #  Save report
    report_file = open("screener-coins-report.html", "w")
    report_file.write(doc.getvalue())
    report_file.close()

This last part is the main() function, which performs the following steps:

  • Fetch the list of coins
  • Iterate through the list of coins
  • Fetch monthly pricing info for the coin
  • Calculate technical indicators
  • Evaluate our strategy conditions
  • In case the strategy matches, get coin detail information
  • Build the HTML with the matched coins.

You can see that I’m also adding a delay of 1.3 between each CoinGecko call to avoid the 50-calls-per-minute limit.

#  Initialize clients
cg = CoinGeckoAPI()
#  Get all coinbase assets
max_coins = 5
month_ohlc_map = {}
info_map = {}
selected_coin_ids = []
coin_list = get_coin_ids()
num_selections = 0
max_selections = 3
i = 0
for coin in coin_list:
    coin_id = coin['id']
    name = coin['name']
    symbol = coin['symbol']
    #  Skip unwanted coins
    if "RealT" in name:
        continue
    print(f"Processing '{name}', ({symbol})")
    #  Get monthly prices - 4 hour period
    ohlc_df = get_ohlc(cg, coin_id, vs_currency='usd', days=30)
    if len(ohlc_df) < 30:
        continue
    ohlc_df = calculate_technical_indicators(ohlc_df)
    #  Calculate SMAs and RSI
    metrics_df = calculate_metrics(ohlc_df)
    selections_df = evaluate_conditions(metrics_df)
    if len(selections_df) > 0:
        #  Get coin info
        time.sleep(1.3)
        info_df = get_coin_info(cg, coin_id)
        if info_df is None:
            continue
        info_map[coin_id] = info_df
        #  Store prices
        ohlc_df.to_csv(f"{coin_id}_ohlc_df.csv")
        print(f"-> Match: {coin_id}, {name}")
        selected_coin_ids.append(coin_id)
        num_selections += 1
        if num_selections >= max_selections:
            break
    time.sleep(1.3)
#  Build report
build_report(selected_coin_ids, info_map)

Results

Let’s take a look at some of the results this screener generated. Keep in mind that the screener will take a while to run due to the CoinGecko API limits. If you let it process all 13K+ coins, it will take about 4.5 hours.

While it is running, the script will print out the different coin names and symbols as it chucks along through the list of coins.

...
Processing 'Ethos Project', (ethos)
Processing 'ETHPad', (ethpad)
Processing 'ETHplode', (ethplo)
Processing 'ETHPlus', (ethp)
Processing 'Eth Shiba', (ethshib)
Processing 'ETHST Governance', (et)
Processing 'ETHUP', (ethup)
Processing 'ETH Variable Leverage Long', (methmoon)
Processing 'Ethverse', (ethv)
...

Once the screener has found three coins that match our strategy, I decided to exit the screening process and build our HTML report. The report contains general information about the coins that match our strategy, e.g.

  • Name
  • Description
  • Categories
  • Links (e.g. hompage, Blockchain URL)
  • Social Media handles
  • Sentiment
  • Rank.

Below a sample output for ‘Coinsale’. You can see the full report on my GitHub repo here.

Image: Custom HTML report

In my last run, I found three coins that matched my criteria: coinsale, cryptopunt and idle-dai-risk-adjusted.

Let’s take a look at the price graphs for those coins.

coinsale

Here in the case of the coinsale coin, the strategy worked out beautifully. We are just catching the coin price after bouncing off the 180 period low (green line).

The RSI graph shows the momentum of the coin by hovering above 80 for the last 30 periods.

Image created with Plotly

cryptopunt

In the case of cryptopunt coin we identified another recent spike in the coin price. However, the price drops back down in the last few periods. Further research may be useful to see what’s going on with this coin.

Image created with Plotly

idle-dai-risk-adjusted

Not much activity here for this coins until the last 70 or so periods. The appears to be a new coin from idle.finance on the Ethereum Blockchain. The price just jumped up from 1.07 to 1.08 in a single transaction.

Image created with Plotly

Wrapping Up

In this post we went over step-by-step instructions to build a cryptocurrency coin momentum screener for 13K+ coins using Python. The script provides us with a HTML report of the coins that match the criteria.

You can find the full implementation of the screener on my repo here.

I hope you found this post worth your time. Thanks for reading.

You can support my writing for free using this link. Don’t miss another story — subscribe to my stories by email. For more premium content, check out my ‘B/O Trading Blog’ on Substack.

This post contains affiliate marketing links.

Have a great day!

Python Trading
Automatic Trading
Algorithmic Trading
Recommended from ReadMedium