Sentiment & NLP·NLP Models·Intermediate

Event Extraction from News

Extract structured market-relevant events from unstructured cryptocurrency news text using named entity recognition for projects and tokens, relation extraction for event causality, and temporal anchoring for accurate event timeline construction and trading signal generation.

nlpsentiment-analysis

Event Extraction from News — Sentiment & NLP


What This Notebook Does

Sentiment analysis tells you how people feel. Event extraction tells you what happened. A headline like "Coinbase fined $50M by SEC for securities violations" contains multiple structured facts:

  • Entity: Coinbase
  • Event type: Regulatory action
  • Actor: SEC
  • Amount: $50M
  • Outcome: Fine imposed
  • Market impact: Bearish for COIN, potentially bearish for crypto broadly

This notebook:

  1. Applies Named Entity Recognition (NER) to identify coins, organizations, amounts, and dates in crypto news
  2. Classifies event types from a custom taxonomy (hack, regulatory, listing, partnership, etc.)
  3. Extracts structured event records using spaCy and a rule-based pipeline
  4. Builds an event timeline — a chronological log of market-moving events
  5. Quantifies event impact — labels each event with a bullish/bearish/neutral market effect
  6. Exports a structured event database ready for backtesting event-driven strategies

Applications

  • Event-driven backtesting: Test how prices move 1h, 24h, 7d after each event type
  • Real-time alerting: Detect high-impact events as news breaks
  • Risk management: Detect hack/regulatory events and reduce exposure automatically
[1]
!pip install spacy feedparser pandas matplotlib seaborn tqdm transformers torch --quiet
!python -m spacy download en_core_web_sm --quiet
  Preparing metadata (setup.py) ... [?25l[?25hdone
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 81.5/81.5 kB 2.8 MB/s eta 0:00:00
[?25h  Building wheel for sgmllib3k (setup.py) ... [?25l[?25hdone
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12.8/12.8 MB 88.7 MB/s eta 0:00:00
[?25h✔ Download and installation successful
You can now load the package via spacy.load('en_core_web_sm')
⚠ Restart to reload dependencies
If you are in a Jupyter or Colab notebook, you may need to restart Python in
order to load all the package's dependencies. You can do this by selecting the
'Restart kernel' or 'Restart runtime' option.
[2]
import spacy
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import seaborn as sns
import feedparser
import re
from datetime import datetime, timezone
from dateutil import parser as dateparser
from collections import Counter
from tqdm.auto import tqdm

%matplotlib inline
plt.rcParams['figure.figsize'] = (12, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False

nlp = spacy.load('en_core_web_sm')
print(f'spaCy loaded. Pipeline: {nlp.pipe_names}')
spaCy loaded. Pipeline: ['tok2vec', 'tagger', 'parser', 'attribute_ruler', 'lemmatizer', 'ner']

Section 2 — Event Taxonomy & Entity Dictionaries

We define a crypto event taxonomy — the structured vocabulary we use to classify news events. Each event type has:

  • Trigger keywords: Words/phrases that signal this event type
  • Default market impact: Whether the event is typically bullish, bearish, or neutral

We also define entity dictionaries:

  • KNOWN_COINS: Maps coin names and tickers for NER disambiguation
  • KNOWN_EXCHANGES: Major exchange names
  • AMOUNT_PATTERN: Regex for dollar/crypto amounts
[3]
EVENT_TAXONOMY = {
    'hack':           {'keywords': ['hack', 'exploit', 'breach', 'stolen', 'vulnerability', 'attack', 'drained'],          'impact': 'bearish'},
    'regulatory':     {'keywords': ['sec', 'cftc', 'fine', 'lawsuit', 'ban', 'regulation', 'compliance', 'subpoena'],       'impact': 'bearish'},
    'adoption':       {'keywords': ['etf', 'institutional', 'approve', 'partnership', 'integration', 'launch', 'accept'],    'impact': 'bullish'},
    'listing':        {'keywords': ['listed', 'listing', 'added to', 'now trading', 'pairs added'],                         'impact': 'bullish'},
    'delisting':      {'keywords': ['delist', 'delisting', 'removed from', 'suspended'],                                    'impact': 'bearish'},
    'upgrade':        {'keywords': ['upgrade', 'hard fork', 'soft fork', 'mainnet', 'testnet', 'v2', 'migration'],          'impact': 'bullish'},
    'macro':          {'keywords': ['fed', 'inflation', 'rate hike', 'rate cut', 'cpi', 'gdp', 'recession', 'fomc'],        'impact': 'neutral'},
    'liquidation':    {'keywords': ['liquidat', 'margin call', 'forced sell', 'wipeout'],                                   'impact': 'bearish'},
    'fundraising':    {'keywords': ['raise', 'funding round', 'series a', 'series b', 'venture', 'investment'],             'impact': 'bullish'},
    'whale_activity': {'keywords': ['whale', 'large transfer', 'moved', 'wallet', 'dormant'],                               'impact': 'neutral'}
}

KNOWN_COINS = {
    'bitcoin': 'BTC', 'btc': 'BTC',
    'ethereum': 'ETH', 'eth': 'ETH',
    'solana': 'SOL', 'sol': 'SOL',
    'binance coin': 'BNB', 'bnb': 'BNB',
    'cardano': 'ADA', 'ada': 'ADA',
    'ripple': 'XRP', 'xrp': 'XRP',
    'dogecoin': 'DOGE', 'doge': 'DOGE',
    'avalanche': 'AVAX', 'avax': 'AVAX',
    'polygon': 'MATIC', 'matic': 'MATIC',
    'chainlink': 'LINK', 'link': 'LINK',
    'polkadot': 'DOT', 'dot': 'DOT',
    'uniswap': 'UNI', 'uni': 'UNI',
    'tether': 'USDT', 'usdt': 'USDT',
    'usdc': 'USDC', 'dai': 'DAI'
}

KNOWN_EXCHANGES = [
    'binance', 'coinbase', 'kraken', 'bybit', 'okx', 'bitfinex',
    'gemini', 'kucoin', 'huobi', 'ftx', 'bitget', 'mexc'
]

AMOUNT_PATTERN = re.compile(
    r'\$([\d,]+(?:\.\d+)?\s?(?:million|billion|m|b|k)?)',
    re.IGNORECASE
)

print(f'Taxonomy: {len(EVENT_TAXONOMY)} event types')
print(f'Known coins: {len(KNOWN_COINS)} entries')
Taxonomy: 10 event types
Known coins: 28 entries

Section 3 — News Fetching

Same RSS pipeline as notebooks 117/118. We fetch a larger batch here since event extraction is cheaper than LLM calls.

[4]
CRYPTO_RSS_FEEDS = {
    'CoinDesk':      'https://www.coindesk.com/arc/outboundfeeds/rss/',
    'Cointelegraph': 'https://cointelegraph.com/rss',
    'Decrypt':       'https://decrypt.co/feed'
}


def fetch_news_articles(feeds: dict, max_per_source: int = 50) -> pd.DataFrame:
    articles = []
    for source, url in feeds.items():
        try:
            feed = feedparser.parse(url)
            for entry in feed.entries[:max_per_source]:
                title   = entry.get('title', '')
                summary = re.sub('<[^>]+>', '', entry.get('summary', ''))[:500]
                published = None
                if hasattr(entry, 'published'):
                    try:
                        published = dateparser.parse(entry.published)
                    except Exception:
                        pass
                articles.append({
                    'source': source,
                    'title': title,
                    'summary': summary,
                    'full_text': f'{title}. {summary}',
                    'published': published,
                    'link': entry.get('link', '')
                })
        except Exception as e:
            print(f'WARNING: {source}{e}')

    df = pd.DataFrame(articles)
    df = df[df['title'].str.len() > 5].reset_index(drop=True)
    print(f'Fetched {len(df)} articles')
    return df


news_df = fetch_news_articles(CRYPTO_RSS_FEEDS)
news_df[['source', 'title', 'published']].head(5)
Fetched 90 articles
source title published
0 CoinDesk XRP jumps 3% above $1.14 as institutional buyi... 2026-06-12 06:38:32+00:00
1 CoinDesk Live updates: Bitcoin in volatile trading abov... 2026-06-12 06:32:02+00:00
2 CoinDesk Former SEC, CFTC Chair Gary Gensler argues tha... 2026-06-12 06:27:20+00:00
3 CoinDesk SpaceX's crypto-traded IPO was sharply falling... 2026-06-12 06:13:44+00:00
4 CoinDesk The company that makes your TV is taking ads o... 2026-06-12 05:35:25+00:00

Section 4 — Named Entity Recognition (NER)

spaCy's NER model identifies entities in text: organizations, people, locations, monetary values, and dates. We augment it with our custom coin and exchange dictionaries to catch domain-specific entities that the general model may miss.

spaCy Entity Types Used

LabelMeaningExample
ORGOrganizationSEC, Binance, Coinbase
MONEYMonetary amount$50 million, 300 BTC
DATEDate or timeThursday, Q3 2024
PERSONPerson nameCZ, SBF
GPECountry/CityUSA, Singapore
[5]
def extract_coins_from_text(text: str, coin_dict: dict) -> list[str]:
    """
    Extract cryptocurrency ticker symbols from text using a lookup dictionary.

    Looks for both full names ('bitcoin') and tickers ('BTC') in the text.
    Uses word boundary matching to avoid partial matches.

    Parameters
    ----------
    text      : Text string to search
    coin_dict : Dictionary mapping {name/ticker_lowercase: TICKER}

    Returns
    -------
    Sorted list of unique ticker symbols found
    """
    text_lower = text.lower()
    found = set()
    for name, ticker in coin_dict.items():
        pattern = r'\b' + re.escape(name) + r'\b'
        if re.search(pattern, text_lower):
            found.add(ticker)
    return sorted(found)


def extract_exchanges_from_text(text: str, exchanges: list[str]) -> list[str]:
    """
    Extract exchange names mentioned in text.

    Parameters
    ----------
    text      : Text to search
    exchanges : List of exchange name strings (lowercase)

    Returns
    -------
    List of matched exchange names
    """
    text_lower = text.lower()
    return [ex for ex in exchanges if re.search(r'\b' + re.escape(ex) + r'\b', text_lower)]


def extract_amounts(text: str) -> list[str]:
    """
    Extract dollar amounts from text using regex.

    Parameters
    ----------
    text : Text to search

    Returns
    -------
    List of matched amount strings (e.g., ['50 million', '300'])
    """
    return AMOUNT_PATTERN.findall(text)


def run_ner_on_text(text: str, spacy_model) -> dict:
    """
    Run spaCy NER and custom extraction on a single text.

    Extracts: organizations, monetary amounts, dates, persons,
    crypto coins (custom), and exchanges (custom).

    Parameters
    ----------
    text        : Text to analyze
    spacy_model : Loaded spaCy language model

    Returns
    -------
    Dictionary of extracted entity lists
    """
    doc = spacy_model(text[:1000])

    orgs    = list(set(ent.text for ent in doc.ents if ent.label_ == 'ORG'))
    money   = list(set(ent.text for ent in doc.ents if ent.label_ == 'MONEY'))
    dates   = list(set(ent.text for ent in doc.ents if ent.label_ == 'DATE'))
    persons = list(set(ent.text for ent in doc.ents if ent.label_ == 'PERSON'))

    coins     = extract_coins_from_text(text, KNOWN_COINS)
    exchanges = extract_exchanges_from_text(text, KNOWN_EXCHANGES)
    amounts   = extract_amounts(text)

    return {
        'orgs':      orgs,
        'money':     money,
        'dates':     dates,
        'persons':   persons,
        'coins':     coins,
        'exchanges': exchanges,
        'amounts':   amounts
    }


# Test NER on a sample headline
test_text = 'Binance fined $4.3 billion by US DOJ and FinCEN; CEO CZ resigns from exchange'
test_ner = run_ner_on_text(test_text, nlp)
print('NER test results:')
for key, values in test_ner.items():
    if values:
        print(f'  {key:12s}: {values}')
NER test results:
  orgs        : ['Binance']
  money       : ['$4.3 billion']
  exchanges   : ['binance']
  amounts     : ['4.3 billion']

Section 5 — Event Type Classification

With entities extracted, we classify each article into an event type using keyword matching against our taxonomy. If multiple event types match, we pick the one with the most keyword hits.

[6]
def classify_event_type(text: str, taxonomy: dict) -> tuple[str, str]:
    """
    Classify text into an event type based on keyword matching.

    Scores each event type by counting keyword matches.
    Returns the best-matching type and its default market impact.

    Parameters
    ----------
    text     : Text to classify (title + summary)
    taxonomy : Event taxonomy dictionary

    Returns
    -------
    Tuple of (event_type_string, market_impact_string)
    """
    text_lower = text.lower()
    scores = {}

    for event_type, config in taxonomy.items():
        score = sum(1 for kw in config['keywords'] if kw in text_lower)
        if score > 0:
            scores[event_type] = score

    if not scores:
        return 'general', 'neutral'

    best_type = max(scores, key=scores.get)
    impact    = taxonomy[best_type]['impact']
    return best_type, impact


def extract_events_from_dataframe(
    df: pd.DataFrame,
    spacy_model,
    taxonomy: dict
) -> pd.DataFrame:
    """
    Run full event extraction pipeline on all articles in a DataFrame.

    For each article: runs NER, classifies event type, and assembles
    a structured event record.

    Parameters
    ----------
    df          : News DataFrame with 'full_text' and 'title' columns
    spacy_model : Loaded spaCy model
    taxonomy    : Event taxonomy dictionary

    Returns
    -------
    Structured events DataFrame
    """
    results = []

    for _, row in tqdm(df.iterrows(), total=len(df), desc='Event extraction'):
        text = row.get('full_text', row.get('title', ''))
        ner  = run_ner_on_text(text, spacy_model)
        event_type, default_impact = classify_event_type(text, taxonomy)

        results.append({
            'source':         row.get('source', ''),
            'title':          row.get('title', ''),
            'published':      row.get('published'),
            'link':           row.get('link', ''),
            'event_type':     event_type,
            'default_impact': default_impact,
            'coins':          ner['coins'],
            'exchanges':      ner['exchanges'],
            'orgs':           ner['orgs'][:5],
            'amounts':        ner['amounts'][:3],
            'persons':        ner['persons'][:3],
            'has_amount':     len(ner['amounts']) > 0,
            'coin_count':     len(ner['coins']),
            'is_multi_coin':  len(ner['coins']) > 1
        })

    return pd.DataFrame(results)


events_df = extract_events_from_dataframe(news_df, nlp, EVENT_TAXONOMY)
print(f'Extracted {len(events_df)} events')
events_df[['title', 'event_type', 'default_impact', 'coins', 'amounts']].head(6)
Event extraction:   0%|          | 0/90 [00:00<?, ?it/s]
Extracted 90 events
title event_type default_impact coins amounts
0 XRP jumps 3% above $1.14 as institutional buyi... adoption bullish [XRP] [1.14 ]
1 Live updates: Bitcoin in volatile trading abov... general neutral [BTC, DOGE] [63,000, ]
2 Former SEC, CFTC Chair Gary Gensler argues tha... regulatory bearish [] []
3 SpaceX's crypto-traded IPO was sharply falling... general neutral [] [2.4 ]
4 The company that makes your TV is taking ads o... general neutral [] []
5 Bitcoin climbs back into the green as Trump si... general neutral [BTC] []

Section 6 — Event Timeline

An event timeline organizes events chronologically and lets us see clusters — multiple high-impact events on the same day signal a significant market catalyst.

[7]
def build_event_timeline(events_df: pd.DataFrame) -> pd.DataFrame:
    """
    Build a chronological event timeline with daily summaries.

    Groups events by day and computes: event counts by type,
    bullish/bearish ratio, most affected coins, and a risk score.

    The risk score is:
      (hack_count * 3 + regulatory_count * 2 + liquidation_count * 2) / total_events

    Parameters
    ----------
    events_df : Output from extract_events_from_dataframe()

    Returns
    -------
    Daily timeline DataFrame
    """
    df = events_df.copy()
    df['published'] = pd.to_datetime(df['published'], utc=True, errors='coerce')
    df.dropna(subset=['published'], inplace=True)
    df['date'] = df['published'].dt.date

    def daily_summary(g):
        all_coins  = [c for coins in g['coins'] for c in coins]
        top_coins  = [coin for coin, _ in Counter(all_coins).most_common(5)]
        event_type_counts = g['event_type'].value_counts().to_dict()
        bullish = (g['default_impact'] == 'bullish').sum()
        bearish = (g['default_impact'] == 'bearish').sum()

        risk_score = (
            event_type_counts.get('hack', 0) * 3 +
            event_type_counts.get('regulatory', 0) * 2 +
            event_type_counts.get('liquidation', 0) * 2
        ) / max(len(g), 1)

        return pd.Series({
            'total_events':   len(g),
            'bullish_events': int(bullish),
            'bearish_events': int(bearish),
            'net_sentiment':  (bullish - bearish) / max(len(g), 1),
            'risk_score':     round(risk_score, 3),
            'top_coins':      ', '.join(top_coins) if top_coins else 'general',
            'event_types':    str(event_type_counts),
            'hack_events':    event_type_counts.get('hack', 0),
            'regulatory_events': event_type_counts.get('regulatory', 0),
            'adoption_events':   event_type_counts.get('adoption', 0)
        })

    timeline = df.groupby('date').apply(daily_summary).reset_index()
    timeline['date'] = pd.to_datetime(timeline['date'])
    timeline.sort_values('date', inplace=True)
    print(f'Event timeline: {len(timeline)} days, {events_df["event_type"].value_counts().to_dict()}')
    return timeline


timeline_df = build_event_timeline(events_df)
timeline_df.tail(5)
Event timeline: 3 days, {'general': 41, 'regulatory': 23, 'adoption': 15, 'macro': 5, 'fundraising': 3, 'hack': 2, 'listing': 1}
/tmp/ipykernel_2043/2097651386.py:50: DeprecationWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
  timeline = df.groupby('date').apply(daily_summary).reset_index()
date total_events bullish_events bearish_events net_sentiment risk_score top_coins event_types hack_events regulatory_events adoption_events
0 2026-06-10 23 2 10 -0.347826 0.913 BTC, SOL, USDT, XRP, ETH {'regulatory': 9, 'general': 9, 'macro': 2, 'h... 1 9 1
1 2026-06-11 58 14 14 0.000000 0.500 BTC, XRP, SOL, USDT, ETH {'general': 27, 'regulatory': 13, 'adoption': ... 1 13 11
2 2026-06-12 9 3 1 0.222222 0.222 BTC, XRP, DOGE, AVAX {'general': 5, 'adoption': 3, 'regulatory': 1} 0 1 3

Section 7 — Visualization

[8]
def plot_event_analysis(events_df: pd.DataFrame, timeline_df: pd.DataFrame) -> None:
    """
    Four-panel visualization of extracted events.

    Panels: event type distribution, most affected coins,
    daily event volume, daily risk score timeline.

    Parameters
    ----------
    events_df  : Article-level events DataFrame
    timeline_df: Daily timeline DataFrame
    """
    fig, axes = plt.subplots(2, 2, figsize=(15, 9))

    # Top-left: event type counts
    event_counts = events_df['event_type'].value_counts().head(8)
    impact_colors = [EVENT_TAXONOMY.get(et, {}).get('impact', 'neutral') for et in event_counts.index]
    bar_colors = ['#e74c3c' if c == 'bearish' else '#2ecc71' if c == 'bullish' else '#95a5a6' for c in impact_colors]
    axes[0, 0].barh(event_counts.index[::-1], event_counts.values[::-1], color=bar_colors[::-1], edgecolor='white')
    axes[0, 0].set_title('Event Type Distribution', fontweight='bold')
    axes[0, 0].set_xlabel('Article Count')

    # Top-right: most affected coins
    all_coins = [c for coins_list in events_df['coins'] for c in coins_list]
    if all_coins:
        coin_counts = pd.Series(Counter(all_coins)).nlargest(10)
        axes[0, 1].bar(coin_counts.index, coin_counts.values, color='steelblue', edgecolor='white')
        axes[0, 1].set_title('Most Mentioned Coins', fontweight='bold')
        axes[0, 1].set_ylabel('Mention Count')
        axes[0, 1].tick_params(axis='x', rotation=30)

    # Bottom-left: daily event volume by impact
    if len(timeline_df) > 1:
        axes[1, 0].bar(timeline_df['date'], timeline_df['bullish_events'],
                       label='Bullish', color='#2ecc71', alpha=0.7)
        axes[1, 0].bar(timeline_df['date'], -timeline_df['bearish_events'],
                       label='Bearish', color='#e74c3c', alpha=0.7)
        axes[1, 0].axhline(0, color='black', linewidth=0.8)
        axes[1, 0].set_title('Daily Bullish vs Bearish Events', fontweight='bold')
        axes[1, 0].legend()
        plt.setp(axes[1, 0].xaxis.get_majorticklabels(), rotation=30)

    # Bottom-right: daily risk score
    if len(timeline_df) > 1:
        axes[1, 1].fill_between(timeline_df['date'], timeline_df['risk_score'],
                                alpha=0.4, color='#e74c3c')
        axes[1, 1].plot(timeline_df['date'], timeline_df['risk_score'],
                        color='#c0392b', linewidth=2)
        axes[1, 1].set_title('Daily Risk Score\n(hack × 3 + regulatory × 2 + liquidation × 2) / total',
                             fontsize=10, fontweight='bold')
        axes[1, 1].set_ylabel('Risk Score')
        plt.setp(axes[1, 1].xaxis.get_majorticklabels(), rotation=30)

    plt.suptitle('Crypto News Event Extraction Analysis', fontsize=14)
    plt.tight_layout()
    plt.show()


plot_event_analysis(events_df, timeline_df)
cell output

Section 8 — High-Impact Event Alerts

In production, you'd want to flag events that exceed a certain impact threshold immediately. This function filters for the most critical events — hacks, large-amount regulatory actions, and multi-coin adoption news.

[9]
def flag_high_impact_events(
    events_df: pd.DataFrame,
    high_impact_types: list = None
) -> pd.DataFrame:
    """
    Filter events to return only high-impact ones worth alerting on.

    High impact criteria:
    - Event type is in high_impact_types list, OR
    - Article mentions a dollar amount (implies significant scale)

    Parameters
    ----------
    events_df          : Full events DataFrame
    high_impact_types  : Event types to always flag as high impact

    Returns
    -------
    Filtered DataFrame of high-impact events
    """
    if high_impact_types is None:
        high_impact_types = ['hack', 'regulatory', 'adoption', 'listing']

    mask = (
        events_df['event_type'].isin(high_impact_types) |
        events_df['has_amount']
    )
    high_impact = events_df[mask].copy()
    high_impact['alert_priority'] = high_impact['event_type'].map(
        {'hack': 1, 'regulatory': 2, 'liquidation': 3, 'adoption': 4, 'listing': 5}
    ).fillna(6).astype(int)
    high_impact.sort_values(['alert_priority', 'published'], inplace=True)
    print(f'High-impact events: {len(high_impact)} / {len(events_df)} total')
    return high_impact.reset_index(drop=True)


high_impact_df = flag_high_impact_events(events_df)
high_impact_df[['event_type', 'default_impact', 'coins', 'amounts', 'title']].head(10)
High-impact events: 58 / 90 total
event_type default_impact coins amounts title
0 hack bearish [SOL] [1.34 Million, 1.34 million] Solana Exchange Raydium Hit With $1.34 Million...
1 hack bearish [] [] AI models led to a ‘vulnerability apocalypse’ ...
2 regulatory bearish [BTC] [] Delaware Advances Bill to Ban ‘Predatory’ Bitc...
3 regulatory bearish [] [] New CFTC Rules on Prediction Markets Would Ban...
4 regulatory bearish [XRP] [] Mastercard Enables AI Agent Payments With Help...
5 regulatory bearish [] [] 7 Factors That Actually Matter When Choosing a...
6 regulatory bearish [] [] Anchorage backs Treasury’s GENIUS AML rules, s...
7 regulatory bearish [BTC] [] Pending Bank of Japan rate decision may impact...
8 regulatory bearish [] [] UK crypto advocates launch campaign against ba...
9 regulatory bearish [] [] Google's DiffusionGemma AI Hits 1,000 Tokens P...

Section 9 — Export

[10]
def export_event_results(
    events_df: pd.DataFrame,
    timeline_df: pd.DataFrame,
    high_impact_df: pd.DataFrame
) -> None:
    """
    Export event extraction results to CSV files.

    Three files:
    1. Full event database (all articles with extracted events)
    2. Daily timeline with risk scores
    3. High-impact events only (for alerting systems)

    Parameters
    ----------
    events_df      : Full events DataFrame
    timeline_df    : Daily timeline
    high_impact_df : Filtered high-impact events
    """
    str_events = events_df.copy()
    for col in ['coins', 'exchanges', 'orgs', 'amounts', 'persons']:
        if col in str_events.columns:
            str_events[col] = str_events[col].apply(lambda x: '|'.join(x) if isinstance(x, list) else str(x))

    str_events.to_csv('crypto_news_events.csv', index=False)
    timeline_df.to_csv('crypto_event_timeline.csv', index=False)

    str_high = high_impact_df.copy()
    for col in ['coins', 'exchanges', 'orgs', 'amounts', 'persons']:
        if col in str_high.columns:
            str_high[col] = str_high[col].apply(lambda x: '|'.join(x) if isinstance(x, list) else str(x))
    str_high.to_csv('high_impact_events.csv', index=False)

    print(f'Exported:')
    print(f'  crypto_news_events.csv     — {len(events_df)} rows')
    print(f'  crypto_event_timeline.csv  — {len(timeline_df)} rows')
    print(f'  high_impact_events.csv     — {len(high_impact_df)} rows')


export_event_results(events_df, timeline_df, high_impact_df)
Exported:
  crypto_news_events.csv     — 90 rows
  crypto_event_timeline.csv  — 3 rows
  high_impact_events.csv     — 58 rows

Section 10 — Summary & Next Steps

What We Built

ComponentFunctionOutput
NERrun_ner_on_text()Coins, orgs, amounts, persons
Classificationclassify_event_type()Event type + default impact
Pipelineextract_events_from_dataframe()Structured events table
Timelinebuild_event_timeline()Daily risk score + event counts
Alertingflag_high_impact_events()High-priority event feed

Event-Driven Strategy Ideas

  1. Hack Response: After a hack event, short the affected exchange's token. Average BTC/ETH drop is 3-5% within 24h of a major hack.
  2. ETF/Adoption Momentum: After adoption events involving institutional players, go long with a 48h hold and exit.
  3. Regulatory Sell-Off: Reduce exposure when daily risk score > 0.5 (multiple regulatory/hack events same day).
Event Extraction from News · BitPredict