File size: 4,086 Bytes
d4e3353
a4d1e62
 
 
d4e3353
a4d1e62
d4e3353
a4d1e62
 
 
d4e3353
a4d1e62
 
 
a11ca8d
 
 
 
a4d1e62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f53bc3
 
 
 
64ea316
a4d1e62
9f53bc3
a4d1e62
9f53bc3
d4e3353
a4d1e62
 
9f53bc3
d4e3353
a4d1e62
 
 
 
 
 
 
 
 
 
 
 
 
d4e3353
 
a4d1e62
 
 
 
d4e3353
9f53bc3
a4d1e62
9f53bc3
a4d1e62
 
9f53bc3
 
 
a4d1e62
9f53bc3
a4d1e62
 
 
9f53bc3
d4e3353
a4d1e62
9f53bc3
a4d1e62
d4e3353
a4d1e62
9f53bc3
 
a4d1e62
 
 
 
9f53bc3
 
d4e3353
a4d1e62
 
 
 
9f53bc3
a4d1e62
 
 
 
9f53bc3
a4d1e62
 
 
 
 
9f53bc3
 
a4d1e62
 
9f53bc3
 
 
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
# processor.py
# Processes earnings call transcripts into sentiment features using the
# hosted HuggingFace model (Ani-404/finbert-model), and fetches post-earnings
# stock returns as prediction targets.

import os
import re
from datetime import datetime, timedelta
from functools import lru_cache

import numpy as np
import pandas as pd
import yfinance as yf

# Financial sentiment model. Defaults to the project's fine-tuned model
# (gated on HF, so needs an authorized token or the repo ungated). Override
# with FINANCIAL_MODEL to point at a different/public model.
MODEL_NAME = os.getenv("FINANCIAL_MODEL", "Ani-404/finbert-model")


@lru_cache(maxsize=1)
def get_classifier():
    """Load and cache the financial sentiment classifier pipeline."""
    from transformers import pipeline

    return pipeline(
        "text-classification",
        model=MODEL_NAME,
        tokenizer=MODEL_NAME,
    )


def ingest_transcripts(project_root: str) -> pd.DataFrame:
    """Load the sample earnings-call transcripts."""
    file_path = os.path.join(project_root, "finance", "sample_transcripts_advanced.csv")
    try:
        return pd.read_csv(file_path)
    except FileNotFoundError:
        raise FileNotFoundError(f"Could not find transcript data at '{file_path}'.")


def preprocess_and_split(text):
    """Clean text and split it into sentences."""
    if not isinstance(text, str):
        return []
    text = re.sub(r"\s+", " ", text).strip()
    sentences = re.split(r"(?<=[.!?])\s+", text)
    return [s.strip() for s in sentences if s.strip()]


def _label_to_score(label: str) -> int:
    """Map a sentiment label to a simplified -1 / 0 / +1 score."""
    l = label.lower()
    if "pos" in l:
        return 1
    if "neg" in l:
        return -1
    return 0


def score_sentences(sentences, classifier=None):
    """Run the classifier on sentences and return a -1/0/+1 score each."""
    if not sentences:
        return []
    classifier = classifier or get_classifier()
    results = classifier(sentences, truncation=True, max_length=128)
    return [_label_to_score(r["label"]) for r in results]


def aggregate_sentiment_features(scores):
    """Aggregate sentence scores into summary features."""
    if not scores:
        return {"mean_score": 0.0, "positive_ratio": 0.0, "negative_ratio": 0.0}

    total_sentences = len(scores)
    positive_sentences = sum(1 for s in scores if s > 0)
    negative_sentences = sum(1 for s in scores if s < 0)

    return {
        "mean_score": float(np.mean(scores)),
        "positive_ratio": positive_sentences / total_sentences,
        "negative_ratio": negative_sentences / total_sentences,
    }


def get_stock_returns(ticker, earnings_date_str):
    """Fetch 1-day and 5-day returns following an earnings date."""
    try:
        earnings_date = datetime.strptime(earnings_date_str, "%Y-%m-%d")
        start_date = earnings_date - timedelta(days=1)
        end_date = earnings_date + timedelta(days=10)

        stock_data = yf.download(
            ticker, start=start_date, end=end_date, auto_adjust=True, progress=False
        )
        if stock_data.empty:
            return None

        # Recent yfinance returns MultiIndex columns even for a single ticker.
        if isinstance(stock_data.columns, pd.MultiIndex):
            stock_data.columns = stock_data.columns.get_level_values(0)

        price_on_date_series = stock_data.loc[stock_data.index >= earnings_date]
        if price_on_date_series.empty:
            return None
        price_on_date = float(price_on_date_series.iloc[0]["Close"])

        post_earnings_prices = stock_data.loc[stock_data.index > earnings_date]
        if len(post_earnings_prices) < 5:
            return None

        price_1d_after = float(post_earnings_prices.iloc[0]["Close"])
        price_5d_after = float(post_earnings_prices.iloc[4]["Close"])

        return {
            "return_1d": (price_1d_after - price_on_date) / price_on_date,
            "return_5d": (price_5d_after - price_on_date) / price_on_date,
        }
    except Exception:
        return None