TABLE OF CONTENTS

How to Build a News-Based AI Crypto Trading Bot in Python

TL;DR

  • CoinGecko API powers a news-driven trading bot that pulls headlines from the Crypto News endpoint, scores them with an LLM, validates signals against live market data, and executes paper trades locally.

  • The bot cross-checks every coin mentioned in a headline against market cap and trading volume from CoinGecko’s Coins List with Market Data endpoint before sizing a trade.

Crypto price moves are often triggered by events such as regulatory rulings, exchange listings, protocol exploits, or sudden ETF inflows. A news-based trading bot targets these catalysts directly. It reads each headline as it appears in the news feed, assesses its likely price impact, and acts on a predefined rule instead of waiting for the price to move first.

In this guide, you'll build a Python trading bot using the CoinGecko API that reads real-time crypto news, scores each headline with a large language model (LLM), confirms the signal against live market data, and records simulated trades in a local paper-trading portfolio.

Full Disclosure: All trades in this guide are paper trades with no exchange keys or real orders involved. This guide is for educational purposes only and does not constitute financial advice. All trading involves risk. Please do your own research before making any investment decisions.

CoinGecko API guide banner for building a news-based crypto trading bot in Python with an LLM and paper trading.


Prerequisites & Setup

You'll need a CoinGecko API key. The Crypto News endpoint is available exclusively on the Analyst plan and above, which also includes 500K monthly API credits, 500 calls per minute, and access to 80+ endpoints.

If you're using the free Demo API, follow the guide to get a free Demo API key. You can also connect individual RSS feeds from news publishers to build the bot for free, although this requires more setup and does not provide the structured coin-filtering available through CoinGecko’s news endpoint.

This guide uses Claude as the example, but the same approach works with any LLM that supports structured or JSON output, such as OpenAI and Gemini. Swap the client setup and model name, and the rest of the pipeline stays unchanged.

1
2
3
4
requests
pandas
anthropic
python-dotenv
1
2
3
COINGECKO_API_KEY=CG-your_api_key_here
COINGECKO_PLAN=analyst
ANTHROPIC_API_KEY=sk-ant-your_key_here

Install the dependencies.

pip install -r requirements.txt

Next, the configuration module below switches the API host and authentication header based on your plan, allowing the same codebase to run on either the Demo API or a paid plan. It also keeps the strategy thresholds in one place, letting you tune the bot without changing the trading logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv("COINGECKO_API_KEY", "")
PLAN = os.getenv("COINGECKO_PLAN", "demo")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")

if PLAN in ("analyst", "pro"):
    BASE_URL = "https://pro-api.coingecko.com/api/v3"
    HEADERS = {"x-cg-pro-api-key": API_KEY}
else:
    BASE_URL = "https://api.coingecko.com/api/v3"
    HEADERS = {"x-cg-demo-api-key": API_KEY}

# Fix these before testing so you are not tuning against results
# you have already seen.
MIN_SENTIMENT_SCORE = 0.35
MIN_CONFIDENCE = 0.60

# Universe filters.
MIN_MARKET_CAP_USD = 100_000_000
MIN_VOLUME_USD = 5_000_000

STARTING_CASH_USD = 10_000.0
POSITION_PCT = 0.05          # 5% of equity per trade
COOLDOWN_HOURS = 6           # per coin, prevents stacking one news cycle
FEE_PCT = 0.001              # 0.1% per side
SLIPPAGE_PCT = 0.002         # 0.2% assumed adverse fill
💡 Pro tip: Every endpoint in this guide can also be called using CoinGecko's official Python SDK or TypeScript SDK instead of raw HTTP requests. The SDK handles the base URL, headers, and authentication for you.

How to Fetch Crypto News in Python

CoinGecko API returns structured crypto news through the Crypto News endpoint, aggregating articles from 100+ curated sources including Decrypt, Bankless, BeInCrypto, and delivering them as JSON for direct parsing. Each article includes the headline, publisher, ISO 8601 timestamp, article URL, thumbnail, and a list of CoinGecko coin IDs matched to the article.

The endpoint supports page and per_page for pagination, coin_id for asset-level filtering, language for localized articles, and type for distinguishing news from guides. Results can be paginated across up to 20 pages of 20 articles, allowing up to 400 articles in a single pass.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
from config import BASE_URL, HEADERS

def fetch_latest_news(per_page=10, coin_id=None):
    """Fetch the most recent crypto news articles."""
    params = {"per_page": per_page, "page": 1}

    if coin_id:
        params["coin_id"] = coin_id

    response = requests.get(f"{BASE_URL}/news", headers=HEADERS, params=params)
    response.raise_for_status()

    # SDK equivalent: client.news.get(per_page=10)
    # The endpoint returns a bare JSON array, not an object with a data key
    return response.json()

if __name__ == "__main__":
    for article in fetch_latest_news(per_page=5):
        print(f"[{article['posted_at']}] {article['source_name']}")
        print(f"  {article['title']}")
        print(f"  coins: {article['related_coin_ids']}\n")

Running this script produces output like the following:

Terminal output of fetch_news.py showing five real crypto news headlines with source, timestamp, and related coin IDs.


How to Turn a News Headline Into a Trading Signal

CoinGecko API provides the live market data needed to turn news into trading signals. The bot sends each headline to an LLM, which scores the news and returns structured values the trading strategy can use, including a sentiment score from -1 to 1, a confidence score from 0 to 1, and a short explanation for logging. The sentiment and confidence scores drive the trading decision, while the bot uses CoinGecko market data to confirm the signal, with only signals that pass both checks reaching the paper trader.

How to Score News Sentiment with an LLM

First, the model should score the potential price impact of the news, not whether the news is generally positive or negative. For example, a regulatory crackdown on a competing chain could be positive for the asset being analyzed.

Second, the model should be allowed to return a score of zero when a headline has no clear directional impact. Forcing a positive or negative score can turn neutral news into false trading signals. Below is an example of a simple scoring layer alongside system prompts for the downstream LLMs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import json
import anthropic
from config import ANTHROPIC_API_KEY

client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)

SYSTEM_PROMPT = """You score crypto news headlines for short-term price impact.

Return JSON only, matching this schema:
{"score": float, "confidence": float, "reasoning": string}

score: -1.0 (strongly bearish) to 1.0 (strongly bullish) for the coin named
confidence: 0.0 to 1.0

Score the likely effect on price, not whether the news is good in general.
Return score 0.0 and confidence 0.0 when a headline carries no clear
directional signal. Most headlines are noise, and scoring them as neutral
is the correct answer."""

def score_headline(title, source_name):
    """Convert a headline into a numeric trading signal."""
    # Source is included because a wire service and an unattributed blog
    # do not deserve the same weight
    message = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=300,
        system=SYSTEM_PROMPT,
        messages=[{
            "role": "user",
            "content": f"Source: {source_name}\nHeadline: {title}"
        }],
    )
    return json.loads(message.content[0].text)

if __name__ == "__main__":
    samples = [
        ("Bitcoin Demand Strengthens as ETFs Add $865M and New Wallets Hit One-Year High", "Blockonomi"),
        ("Eliza Labs founder sells $25M in ElizaOS tokens as project collapses after lawsuit", "Crypto Briefing"),
        ("Crypto card spending hits $759 million in July 2026, led by USDC and USDT", "COINTURK NEWS"),
    ]
    for title, source in samples:
        result = score_headline(title, source)
        print(f"{result['score']:+.2f}  conf {result['confidence']:.2f}  {title[:58]}")
        print(f"       {result['reasoning']}\n")

Here's what the output looks like: Claude scores a bullish Bitcoin ETF headline +0.68 with 0.81 confidence, citing sustained spot demand as the catalyst.

A headline does not always capture the full sentiment, nuance, or context of an article, so treat these scores as an initial signal rather than a definitive assessment. For a stronger signal, fetch and analyze the full article text with tools such as Firecrawl, Jina Reader, or Tavily, and use that context to refine the sentiment and confidence scores, where the publisher’s terms of use and crawling policies allow it.

Alternatively, CoinGecko’s Coin Insights endpoint can provide an additional layer of structured, AI-generated coin-level context, reducing the need to build and maintain a separate article-crawling workflow. Coin Insights is currently available to Enterprise clients; developers interested in accessing the endpoint can submit their interest here.

CoinGecko API: stay ahead of the markets with accurate and reliable crypto data.

How to Confirm a Trading Signal Against Live Market Data

The CoinGecko API’s price data adds market context before a trade is placed. A positive headline score reflects what the LLM thinks the news could mean for the asset, while live price data shows how the market is responding. The signal only qualifies for execution when both the news assessment and market data align, helping filter out signals that may already be reflected in the price.

The function below runs three checks in order. If any check fails, the bot skips the trade:

  1. Signal strength: The sentiment score and confidence must meet the thresholds in config.py.

  2. Price direction: The coin’s price over the past hour should move in the same direction as the signal. If the news is bullish but the price is falling, the bot skips the trade.

  3. Recent price move: If the coin has already moved more than MAX_ALREADY_MOVED_PCT in the past hour, the bot assumes the initial move may have already happened and skips the trade.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from config import MIN_SENTIMENT_SCORE, MIN_CONFIDENCE

MAX_ALREADY_MOVED_PCT = 8.0

def should_trade(score, confidence, coin):
    """Apply every gate between a scored headline and an order."""
    if score < MIN_SENTIMENT_SCORE or confidence < MIN_CONFIDENCE:
        return False, "below score or confidence threshold"

    change_1h = coin.get("price_change_percentage_1h_in_currency") or 0.0

    # A bullish story on a falling coin means the market knows something
    # the headline does not
    if change_1h < 0:
        return False, f"price disagrees with signal ({change_1h:+.1f}% 1h)"

    if change_1h > MAX_ALREADY_MOVED_PCT:
        return False, f"move already priced in ({change_1h:+.1f}% 1h)"

    return True, "confirmed"

def should_exit(score, confidence, coin):
    """A simple, illustrative exit rule: reverse out of a position on a
    sufficiently bearish, price-confirmed signal. This is not a production
    exit strategy -- see the note below the paper-trading example for what
    a real one needs."""
    change_1h = coin.get("price_change_percentage_1h_in_currency") or 0.0
    return (score <= -MIN_SENTIMENT_SCORE and confidence >= MIN_CONFIDENCE
            and change_1h < 0)

if __name__ == "__main__":
    cases = [
        ("agrees, not yet moved", 0.68, 0.81, {"price_change_percentage_1h_in_currency": 1.2}),
        ("price disagrees",       0.68, 0.81, {"price_change_percentage_1h_in_currency": -0.6}),
        ("already priced in",     0.68, 0.81, {"price_change_percentage_1h_in_currency": 9.4}),
        ("low confidence",        0.41, 0.30, {"price_change_percentage_1h_in_currency": 0.5}),
    ]
    for label, score, confidence, coin in cases:
        ok, reason = should_trade(score, confidence, coin)
        print(f"{'TRADE' if ok else 'SKIP ':<6} {label:<24} {reason}")

    exit_cases = [
        ("bearish, price confirms", -0.71, 0.79, {"price_change_percentage_1h_in_currency": -1.4}),
        ("bearish, not yet moved",  -0.71, 0.79, {"price_change_percentage_1h_in_currency": 0.3}),
    ]
    for label, score, confidence, coin in exit_cases:
        exit_now = should_exit(score, confidence, coin)
        change_1h = coin["price_change_percentage_1h_in_currency"]
        print(f"{'EXIT' if exit_now else 'HOLD':<6} {label:<24} score {score:+.2f}, {change_1h:+.1f}% 1h")

The example below shows how each condition can block a trade. Each rejection maps to a specific check in the function, making it clear why the trade was skipped. When the bot rejects a trade later, the log shows which condition blocked it, making the decision easier to review and debug.

should_trade confirms one signal while should_exit triggers on a -0.71 score paired with a -1.4% 1h price move.


How to Paper Trade a News Strategy in Python

A paper trading bot keeps a local record of cash and open positions and simulates trades using real market prices without sending orders to an exchange. CoinGecko API provides the market data, while the bot handles the simulated trading locally.

For more realistic results, the bot uses the next candle’s open price to simulate each trade. This gives the market time to react to the news before the simulated entry and avoids making the strategy look more profitable than it would be in practice.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import sqlite3
from datetime import datetime, timedelta, timezone
from config import (STARTING_CASH_USD, POSITION_PCT, COOLDOWN_HOURS,
                    FEE_PCT, SLIPPAGE_PCT)

class PaperPortfolio:
    def __init__(self, db_path="portfolio.db"):
        self.conn = sqlite3.connect(db_path)
        self._setup()

    def _setup(self):
        # signal_log records every scored headline, not only executed ones.
        self.conn.executescript("""
            CREATE TABLE IF NOT EXISTS positions (
                coin_id TEXT PRIMARY KEY, qty REAL, entry_price REAL, opened_at TEXT);
            CREATE TABLE IF NOT EXISTS trades (
                id INTEGER PRIMARY KEY, coin_id TEXT, side TEXT, qty REAL,
                price REAL, headline TEXT, score REAL, executed_at TEXT);
            CREATE TABLE IF NOT EXISTS signal_log (
                id INTEGER PRIMARY KEY, coin_id TEXT, headline TEXT, score REAL,
                confidence REAL, executed INTEGER, reason TEXT, logged_at TEXT);
            CREATE TABLE IF NOT EXISTS cash (balance REAL);
        """)
        if not self.conn.execute("SELECT 1 FROM cash").fetchone():
            self.conn.execute("INSERT INTO cash VALUES (?)", (STARTING_CASH_USD,))
        self.conn.commit()

    def in_cooldown(self, coin_id):
        """Stop one news cycle from opening five positions in the same coin."""
        row = self.conn.execute(
            "SELECT executed_at FROM trades WHERE coin_id=? ORDER BY id DESC LIMIT 1",
            (coin_id,)).fetchone()
        if not row:
            return False
        last = datetime.fromisoformat(row[0])
        return datetime.now(timezone.utc) - last < timedelta(hours=COOLDOWN_HOURS)

    def buy(self, coin_id, fill_price, headline, score):
        balance = self.conn.execute("SELECT balance FROM cash").fetchone()[0]
        notional = balance * POSITION_PCT

        # Assume an adverse fill and pay the fee. Optimistic fills are the
        # fastest way to build a strategy that only works on paper
        effective_price = fill_price * (1 + SLIPPAGE_PCT)
        cost = notional * (1 + FEE_PCT)
        if cost > balance:
            return None

        qty = notional / effective_price
        now = datetime.now(timezone.utc).isoformat()
        self.conn.execute("UPDATE cash SET balance = balance - ?", (cost,))
        self.conn.execute("INSERT OR REPLACE INTO positions VALUES (?,?,?,?)",
                          (coin_id, qty, effective_price, now))
        self.conn.execute(
            "INSERT INTO trades (coin_id, side, qty, price, headline, score, executed_at)"
            " VALUES (?,?,?,?,?,?,?)",
            (coin_id, "BUY", qty, effective_price, headline, score, now))
        self.conn.commit()
        return qty

    def sell(self, coin_id, fill_price, headline, score):
        """Close an open position at the given fill price."""
        row = self.conn.execute(
            "SELECT qty, entry_price FROM positions WHERE coin_id=?", (coin_id,)).fetchone()
        if not row:
            return None
        qty, entry_price = row

        # Selling into a move works against you the same way buying does,
        # so slippage and fees are applied on the way out too
        effective_price = fill_price * (1 - SLIPPAGE_PCT)
        proceeds = qty * effective_price * (1 - FEE_PCT)
        now = datetime.now(timezone.utc).isoformat()

        self.conn.execute("UPDATE cash SET balance = balance + ?", (proceeds,))
        self.conn.execute("DELETE FROM positions WHERE coin_id=?", (coin_id,))
        self.conn.execute(
            "INSERT INTO trades (coin_id, side, qty, price, headline, score, executed_at)"
            " VALUES (?,?,?,?,?,?,?)",
            (coin_id, "SELL", qty, effective_price, headline, score, now))
        self.conn.commit()
        return qty

    def has_position(self, coin_id):
        return self.conn.execute(
            "SELECT 1 FROM positions WHERE coin_id=?", (coin_id,)).fetchone() is not None

    def log_signal(self, coin_id, headline, score, confidence, executed, reason):
        """Record every scored headline; skipping this call leaves the false-signal report empty."""
        now = datetime.now(timezone.utc).isoformat()
        self.conn.execute(
            "INSERT INTO signal_log (coin_id, headline, score, confidence,"
            " executed, reason, logged_at) VALUES (?,?,?,?,?,?,?)",
            (coin_id, headline, score, confidence, int(executed), reason, now))
        self.conn.commit()

The bot fetches the latest news, filters out previously processed articles, scores new headlines, identifies the relevant coin, checks the signal against its price, and records the simulated trade. It also calculates PnL once per cycle using CoinGecko's Simple Price endpoint, which accepts up to 515 coin IDs in a single request, allowing the bot to price all open positions and previously traded coins in one call.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import time
import requests
from config import BASE_URL, HEADERS, MIN_MARKET_CAP_USD, MIN_VOLUME_USD
from fetch_news import fetch_latest_news
from score_news import score_headline
from confirm_signal import should_trade, should_exit
from paper_trader import PaperPortfolio

seen_urls = set()
portfolio = PaperPortfolio()

def get_portfolio_pnl(conn):
    """Realized PnL from closed trades plus unrealized PnL from open
    positions, both powered by a single bulk price call."""
    positions = conn.execute(
        "SELECT coin_id, qty, entry_price FROM positions").fetchall()
    trades = conn.execute(
        "SELECT coin_id, side, qty, price FROM trades").fetchall()

    coin_ids = {row[0] for row in positions} | {row[0] for row in trades}
    if not coin_ids:
        return {"realized_usd": 0.0, "unrealized_usd": 0.0, "positions": []}

    # A single call covers every coin the bot has ever touched;
    # /simple/price accepts up to 515 IDs per request.
    response = requests.get(f"{BASE_URL}/simple/price", headers=HEADERS,
        params={"ids": ",".join(coin_ids), "vs_currencies": "usd"})
    response.raise_for_status()
    prices = {coin_id: data["usd"] for coin_id, data in response.json().items()}

    realized = 0.0
    cost_basis = {}
    for coin_id, side, qty, price in trades:
        if side == "BUY":
            cost_basis.setdefault(coin_id, []).append([qty, price])
        elif side == "SELL":
            remaining = qty
            while remaining > 0 and cost_basis.get(coin_id):
                lot_qty, lot_price = cost_basis[coin_id][0]
                matched = min(remaining, lot_qty)
                realized += matched * (price - lot_price)
                lot_qty -= matched
                remaining -= matched
                if lot_qty <= 0:
                    cost_basis[coin_id].pop(0)
                else:
                    cost_basis[coin_id][0][0] = lot_qty

    unrealized = 0.0
    open_positions = []
    for coin_id, qty, entry_price in positions:
        current_price = prices.get(coin_id)
        if current_price is None:
            continue
        pnl = qty * (current_price - entry_price)
        unrealized += pnl
        open_positions.append({"coin_id": coin_id, "qty": qty, "pnl_usd": pnl})

    return {"realized_usd": realized, "unrealized_usd": unrealized,
            "positions": open_positions}

def run_cycle():
    for article in fetch_latest_news(per_page=20):
        # The same story is syndicated across outlets and reappears on every
        # poll. Without this check the bot buys the same news repeatedly
        if article["url"] in seen_urls:
            continue
        seen_urls.add(article["url"])

        coin_ids = article["related_coin_ids"]
        if not coin_ids:
            portfolio.log_signal(None, article["title"], None, None,
                                 executed=False, reason="no related coin")
            continue

        response = requests.get(f"{BASE_URL}/coins/markets", headers=HEADERS, params={
            "vs_currency": "usd", "ids": ",".join(coin_ids),
            "price_change_percentage": "1h,24h",
        })
        response.raise_for_status()
        candidates = sorted(
            (c for c in response.json()
             if (c["market_cap"] or 0) >= MIN_MARKET_CAP_USD
             and (c["total_volume"] or 0) >= MIN_VOLUME_USD),
            key=lambda c: c["market_cap"], reverse=True)

        if not candidates:
            # Logged with no coin_id, since nothing matched survived the
            # size and liquidity filter at all
            portfolio.log_signal(None, article["title"], None, None,
                                 executed=False, reason="no tradeable coin")
            continue

        signal = score_headline(article["title"], article["source_name"])
        coin = candidates[0]

        if portfolio.has_position(coin["id"]) and should_exit(
                signal["score"], signal["confidence"], coin):
            qty = portfolio.sell(coin["id"], coin["current_price"],
                                article["title"], signal["score"])
            print(f"       SELL {qty:.6f} {coin['symbol'].upper()} @ ${coin['current_price']:,.2f}")
            continue

        ok, reason = should_trade(signal["score"], signal["confidence"], coin)

        if ok and portfolio.in_cooldown(coin["id"]):
            ok, reason = False, "cooldown active"

        portfolio.log_signal(coin["id"], article["title"], signal["score"],
                             signal["confidence"], executed=ok, reason=reason)

        print(f"{signal['score']:+.2f} {coin['id']:<10} {article['title'][:46]}")
        if not ok:
            print(f"       skip: {reason}")
            continue

        qty = portfolio.buy(coin["id"], coin["current_price"],
                            article["title"], signal["score"])
        print(f"       BUY {qty:.6f} {coin['symbol'].upper()} @ ${coin['current_price']:,.2f}")

if __name__ == "__main__":
    while True:
        run_cycle()
        pnl = get_portfolio_pnl(portfolio.conn)
        print(f"Realized ${pnl['realized_usd']:+,.2f} | "
              f"Unrealized ${pnl['unrealized_usd']:+,.2f}")
        time.sleep(600)

run_cycle() also checks should_exit() before looking for a new entry. If the bot already holds a position and a new headline is sufficiently bearish and the price confirms the move, it sells instead of buying.

Here's what the output looks like:

Paper trading bot buys bitcoin at $64,869, then sells at $64,200 on a bearish reversal, closing -$6.16 realized PnL.

Note: buy() and sell()apply FEE_PCT(0.1%)and SLIPPAGE_PCT(0.2%)from config.py, so PnL already reflects these costs on both entry and exit.

This exit rule is intentionally simple and is not intended for production trading. Before using it, consider:

  • Backtesting against historical data.
  • Analyzing how different news types affect price.
  • Defining an appropriate holding period.
  • Adding independent stop-loss and take-profit rules.
  • Considering other factors relevant to the asset and strategy.

How to Reduce False Signals in a News Trading Bot

To reduce false signals, check which filter rejects the most headlines and tune that filter first. Change one threshold at a time so you can see what each adjustment changes. The bot logs every scored headline through log_signal(), including headlines that do not result in a trade, giving you a clear record to review and tune the strategy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sqlite3
import pandas as pd

def rejection_breakdown(db_path="portfolio.db"):
    """Show which check is filtering out the most signals."""
    conn = sqlite3.connect(db_path)
    df = pd.read_sql("SELECT reason, executed FROM signal_log", conn)

    total = len(df)
    breakdown = (
        df[df["executed"] == 0]["reason"]
        .value_counts()
        .rename_axis("reason")
        .reset_index(name="count")
    )
    breakdown["pct_of_total"] = (breakdown["count"] / total * 100).round(1)
    return total, breakdown

if __name__ == "__main__":
    total, breakdown = rejection_breakdown()
    print(f"{total} headlines scored over the period\n")
    for _, row in breakdown.iterrows():
        print(f"{row['count']:>4}  {row['pct_of_total']:>5.1f}%   {row['reason']}")

Here’s what the bot’s signal_log report looks like: 65% of 412 scored headlines are rejected for having no tradeable coin, the largest false-signal category by far.

CoinGecko webhooks provide a push-based alternative, with the cg.coin.info.updated event covering coin metadata changes such as contract migrations, symbol or logo updates, category changes, and public notices. Newly added public_notices can be scored with the same LLM used for news headlines, giving the bot another source of event-based signals.

Update as of June 2026: CoinGecko webhooks are now available on the Basic plan starting at just $29/month.

For price movements, the cg.coin.price.updated webhook event triggers when a tracked coin crosses a configured price threshold or shows abnormal volatility. This webhook event is currently in private beta – join the waitlist for early access.

CoinGecko Webhooks push real-time crypto data across 250+ networks starting at $29 per month.


Conclusion

With CoinGecko API, you can build a Python trading bot that covers the full news-to-trade workflow, from ingesting headlines and scoring them with an LLM to matching them with tradeable coins, checking live price data, and recording simulated trades in a local ledger. Start with paper trading to test and refine the strategy, identify issues, and evaluate its behavior before connecting it to exchange APIs for live execution.

To combine headlines with price and OHLC data, see our guide on building an AI crypto trading bot. For more on the Crypto News endpoint, including coin filtering and pagination, fetching real-time crypto news with Python covers the details. To test a strategy against historical data, explore our crypto backtesting guide.

Ready to start building? The CoinGecko API Analyst plan unlocks the Crypto News endpoint, 500K monthly call credits, 500 RPM, and full historical depth for backtesting. If you are not ready to subscribe yet, start with a free Demo API key to explore 50+ endpoints with 10K free monthly API call credits.

CoinGecko's Content Editorial Guidelines
CoinGecko’s content aims to demystify the crypto industry. While certain posts you see may be sponsored, we strive to uphold the highest standards of editorial quality and integrity, and do not publish any content that has not been vetted by our editors.
Learn more
Want to be the first to know about upcoming airdrops?
Subscribe to the CoinGecko Daily Newsletter!
Join 600,000+ crypto enthusiasts, traders, and degens in getting the latest crypto news, articles, videos, and reports by subscribing to our FREE newsletter.
Tell us how much you like this article!
Ru Jun Ang
Ru Jun Ang
Ru Jun is a growth marketer who’s curious about crypto, APIs, and the technology behind digital assets, with a focus on market data, on-chain trends, and products shaping the Web3 space.

More Articles

Select Currency
Suggested Currencies
USD
US Dollar
IDR
Indonesian Rupiah
TWD
New Taiwan Dollar
EUR
Euro
KRW
South Korean Won
JPY
Japanese Yen
RUB
Russian Ruble
CNY
Chinese Yuan
Fiat Currencies
AED
United Arab Emirates Dirham
ARS
Argentine Peso
AUD
Australian Dollar
BDT
Bangladeshi Taka
BHD
Bahraini Dinar
BMD
Bermudian Dollar
BRL
Brazil Real
CAD
Canadian Dollar
CHF
Swiss Franc
CLP
Chilean Peso
CZK
Czech Koruna
DKK
Danish Krone
GBP
British Pound Sterling
GEL
Georgian Lari
HKD
Hong Kong Dollar
HUF
Hungarian Forint
ILS
Israeli New Shekel
INR
Indian Rupee
KWD
Kuwaiti Dinar
LKR
Sri Lankan Rupee
MMK
Burmese Kyat
MXN
Mexican Peso
MYR
Malaysian Ringgit
NGN
Nigerian Naira
NOK
Norwegian Krone
NZD
New Zealand Dollar
PHP
Philippine Peso
PKR
Pakistani Rupee
PLN
Polish Zloty
SAR
Saudi Riyal
SEK
Swedish Krona
SGD
Singapore Dollar
THB
Thai Baht
TRY
Turkish Lira
UAH
Ukrainian hryvnia
VND
Vietnamese đồng
ZAR
South African Rand
XDR
IMF Special Drawing Rights
Cryptocurrencies
BTC
Bitcoin
ETH
Ether
LTC
Litecoin
BCH
Bitcoin Cash
BNB
Binance Coin
EOS
EOS
XRP
XRP
XLM
Lumens
LINK
Chainlink
DOT
Polkadot
YFI
Yearn.finance
SOL
Solana
Bitcoin Units
BITS
Bits
SATS
Satoshi
Commodities
XAG
Silver - Troy Ounce
XAU
Gold - Troy Ounce
Select Language
Popular Languages
EN
English
RU
Русский
DE
Deutsch
PL
język polski
ES
Español
VI
Tiếng việt
FR
Français
PT-BR
Português
All Languages
AR
العربية
BG
български
CS
čeština
DA
dansk
EL
Ελληνικά
FI
suomen kieli
HE
עִבְרִית
HI
हिंदी
HR
hrvatski
HU
Magyar nyelv
ID
Bahasa Indonesia
IT
Italiano
JA
日本語
KO
한국어
LT
lietuvių kalba
NL
Nederlands
NO
norsk
RO
Limba română
SK
slovenský jazyk
SL
slovenski jezik
SV
Svenska
TH
ภาษาไทย
TR
Türkçe
UK
украї́нська мо́ва
ZH
简体中文
ZH-TW
繁體中文
Welcome to CoinGecko
Welcome back!
Login or Sign up in seconds
or
Sign in with . Not you?
Forgot your password?
Didn't receive confirmation instructions?
Resend confirmation instructions
Password must contain at least 8 characters including 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character
By continuing, you acknowledge that you've read and agree fully to our Terms of Service and Privacy Policy.
Get Price Alerts with CoinGecko App
Forgot your password?
You will receive an email with instructions on how to reset your password in a few minutes.
Resend confirmation instructions
You will receive an email with instructions for how to confirm your email address in a few minutes.
Get the CoinGecko app.
Scan this QR code to download the app now App QR Code Or check it out in the app stores
Add NFT
CoinGecko
Better on the app
Real-time price alerts and a faster, smoother experience.