File: code/analysts/sentiment_analyst.py
Role: supervised market-aligned sentiment specialist
Branch: qualitative branch
Upstream dependency: FinBERT embeddings + text-market labels
Downstream consumers: Qualitative Analyst and, indirectly, Fusion Engine
The Sentiment Analyst converts FinBERT text embeddings into supervised financial sentiment outputs. It is not a dictionary-based sentiment model and it is not trained on dummy labels. Its labels are built by text_market_label_builder.py from future market behaviour after the SEC filing date. In practical terms, it answers:
Given this financial text representation, what market-aligned sentiment signal does it imply?
The model is deliberately modular. FinBERT remains the general financial-language encoder; the Sentiment Analyst is the task-specific supervised head that maps those text embeddings into sentiment, confidence, uncertainty, and compact downstream sentiment embeddings.
SEC / financial text
↓
FinBERT financial text encoder
↓
Sentiment Analyst
↓
Qualitative Analyst
↓
Fusion Engine
↓
Final Trade Approver
The Sentiment Analyst does not make the final Buy/Hold/Sell decision. It contributes one qualitative signal that is later combined with News Analyst outputs by the Qualitative Analyst.
For each chunk and split:
outputs/embeddings/FinBERT/chunk{N}_{split}_embeddings.npy
outputs/results/analysts/labels/text_market_labels_chunk{N}_{split}.csv
where:
N ∈ {1, 2, 3}
split ∈ {train, val, test}
The embedding file must be row-aligned with the label file. Row i in the embedding matrix must correspond to row i in the label CSV.
Expected embedding shape:
(number_of_text_rows, 256)
Important label columns include:
chunk_id
doc_id
year
form_type
cik
filing_date
accession
source_name
chunk_index
word_count
metadata_row_index
split
ticker
ticker_in_market_panel
label_available
sentiment_score_target
sentiment_class_target
The module follows the project-wide chronological chunks:
| Chunk | Train | Validation | Test |
|---|---|---|---|
| 1 | 2000–2004 | 2005 | 2006 |
| 2 | 2007–2014 | 2015 | 2016 |
| 3 | 2017–2022 | 2023 | 2024 |
Validation and test rows are never used to fit preprocessing statistics, label thresholds, or HPO decisions beyond validation scoring.
Labels are produced by text_market_label_builder.py. The label builder maps each filing to the first trading day strictly after filing_date, then computes forward excess market returns.
The continuous sentiment target is:
sentiment_score_target = tanh(future_excess_log_return / train_fitted_scale)
The class target is based on train-fitted quantile thresholds:
future excess return <= negative threshold → negative class
future excess return >= positive threshold → positive class
otherwise → neutral class
This makes the Sentiment Analyst a market-aligned sentiment model, not a general natural-language polarity model.
Primary model class:
SentimentAnalystMLP
Architecture:
FinBERT embedding, 256-dim
+ optional metadata features
↓
MLP trunk
Linear
LayerNorm
Tanh
Dropout
↓
sentiment representation embedding
↓
polarity head
class head
magnitude head
Outputs:
| Output | Type / range | Purpose |
|---|---|---|
sentiment_score |
[-1, 1] via tanh |
directional market-aligned sentiment |
class_logits |
3 logits | negative / neutral / positive class |
magnitude |
logit | strength of sentiment signal |
sentiment_embedding |
configurable, default 64-dim | compact downstream representation |
The model uses tanh activations in the hidden stack, matching the project’s preference for MLP hidden activations.
The training objective combines polarity regression, class prediction, and magnitude learning:
loss =
polarity_loss_weight × MSE(sentiment_score, sentiment_score_target)
+ class_loss_weight × CE(class_logits, sentiment_class_target)
+ magnitude_loss_weight × BCEWithLogits(magnitude, sentiment_magnitude_target)
Default weights:
polarity_loss_weight = 1.0
class_loss_weight = 0.5
magnitude_loss_weight = 0.25
This design preserves both fine-grained continuous signal and coarse class-level direction.
The core defaults from SentimentConfig include:
input_dim = 256
hidden_dims = [128, 64]
representation_dim = 64
dropout = 0.15
batch_size = 512
eval_batch_size = 1024
epochs = 40
learning_rate = 1e-4
weight_decay = 1e-4
gradient_clip = 1.0
early_stop_patience = 8
mixed_precision = True
Configurable directories:
outputs/embeddings/FinBERT
outputs/results/analysts/labels
outputs/embeddings/analysts/sentiment
outputs/models/analysts/sentiment
outputs/results/analysts/sentiment
outputs/codeResults/analysts/sentiment
Model outputs:
outputs/models/analysts/sentiment/chunk{N}/latest.pt
outputs/models/analysts/sentiment/chunk{N}/best.pt
outputs/models/analysts/sentiment/chunk{N}/metadata_preprocessor.json
Result outputs:
outputs/results/analysts/sentiment/chunk{N}_training_history.csv
outputs/results/analysts/sentiment/chunk{N}_{split}_metrics.json
outputs/results/analysts/sentiment/chunk{N}_{split}_predictions.csv
Embedding outputs:
outputs/embeddings/analysts/sentiment/chunk{N}_{split}_sentiment_embeddings.npy
These sentiment embeddings allow later modules to consume a compact learned sentiment representation instead of raw FinBERT outputs.
The module computes:
loss
polarity_loss
class_loss
magnitude_loss
MSE
MAE
RMSE
correlation
classification accuracy
3-class confusion matrix
The main HPO objective is validation loss.
The Sentiment Analyst exposes interpretability through:
--xai-method, currently supporting gradient and shap.For large-scale runs, gradient-based attribution is the practical default because SHAP can be computationally expensive on hundreds of thousands of text embeddings.
The module protects against leakage by:
using chronological splits only
using train split for fitting metadata preprocessing
using train-fitted market thresholds from the label builder
not fitting scalers on validation/test
requiring row alignment between embeddings and labels
serialising preprocessing state with the model
Because targets are derived from future returns, these controls are essential.
Inspect:
python code/analysts/sentiment_analyst.py inspect --repo-root . --chunk 1
HPO:
python code/analysts/sentiment_analyst.py hpo --repo-root . --chunk 1 --trials 30 --device cuda
Train from best HPO:
python code/analysts/sentiment_analyst.py train-best --repo-root . --chunk 1 --device cuda
Predict one split:
python code/analysts/sentiment_analyst.py predict --repo-root . --chunk 1 --split test --device cuda
Predict all chunks and splits:
python code/analysts/sentiment_analyst.py predict-all --repo-root . --chunks 1,2,3 --splits train,val,test --device cuda
Full rerun after FinBERT regeneration:
python code/analysts/sentiment_analyst.py hpo-all --repo-root . --chunks 1,2,3 --trials 30 --device cuda && python code/analysts/sentiment_analyst.py train-best-all --repo-root . --chunks 1,2,3 --device cuda && python code/analysts/sentiment_analyst.py predict-all --repo-root . --chunks 1,2,3 --splits train,val,test --device cuda
Rerun this module whenever FinBERT embeddings are refreshed. Do not mix old Sentiment Analyst outputs with new News Analyst or Qualitative Analyst outputs. The correct text-side dependency chain is:
FinBERT embeddings
↓
text_market_label_builder.py
↓
Sentiment Analyst
↓
News Analyst
↓
Qualitative Analyst
↓
Fusion Engine
The Sentiment Analyst is a supervised, market-aligned financial text module. It uses FinBERT embeddings, train-only market-derived labels, checkpointing, HPO, explicit confidence/uncertainty outputs, and downstream embedding export. It contributes interpretable sentiment evidence to the qualitative branch without pretending to be the final trading decision-maker.