#!/usr/bin/env python3 """ Retrain sentiment model from BASE FinBERT with CORRECT label mapping. FinBERT native: 0=negative(Bearish), 1=neutral(Neutral), 2=positive(Bullish) """ import json import torch import numpy as np from pathlib import Path from typing import List from torch.utils.data import Dataset from transformers import ( AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, EarlyStoppingCallback ) from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, f1_score from sklearn.utils.class_weight import compute_class_weight # CORRECT label mapping matching FinBERT's native order # FinBERT: 0=negative, 1=neutral, 2=positive SENTIMENT_LABELS = ["Bearish", "Neutral", "Bullish"] # Order matches FinBERT! SENTIMENT_MAP = {"Bearish": 0, "Neutral": 1, "Bullish": 2} def load_labeled_data(label_file: str): """Load verified labeled data from labeling pipeline""" texts, labels = [], [] with open(label_file) as f: for line in f: r = json.loads(line) if r.get('verified', False): texts.append(r['text']) labels.append(SENTIMENT_MAP[r['labels']['sentiment']]) return texts, labels def build_augmented_data(): """Build comprehensive training data""" bearish_texts = [ # From labeled verified data "Major hack: Radiant Capital loses $50M in exploit. Attacker exploits rounding error in lending market. Funds moved to Tornado Cash.", "Curve Finance hit by $50M exploit. Vyper compiler bug affects multiple pools. CRV drops 20%.", "Wintermute market maker loses $20M in exploit. Private key compromise suspected. Funds returned.", "SEC sues Kraken for operating unregistered securities exchange. Alleged commingling of customer funds.", "SEC charges Uniswap Labs with operating unregistered securities exchange. UNI drops 15%.", "Binance delists Monero (XMR), Zcash (ZEC), and 4 other privacy coins. Cites regulatory compliance review.", "OKX delists USDT trading pairs in EEA region. MiCA compliance cited. USDT/USD pairs remain.", "Solana network experiences 5-hour outage. Validators restart cluster. SOL drops 8% on news.", "Circle USDC depegs to $0.97 after SVB exposure revealed. $3.3B reserves stuck at SVB.", # Expanded bearish "Bitcoin crashes 30% in hours as leverage flushes out longs", "Massive liquidation cascade wipes out $500M in longs across exchanges", "Exchange hacked, $100M stolen, users panic selling", "Regulatory crackdown: SEC files enforcement action against major DeFi protocol", "Rug pull: Dev team abandons project, drains liquidity pool", "Bankruptcy filing: Major crypto lender files Chapter 11", "Stablecoin depeg: USDT drops to $0.95 on redemption fears", "Smart contract vulnerability discovered, $50M at risk", "Market structure breakdown: Order books thin, spreads widen", "Forced liquidations trigger death spiral in lending protocol", "Contagion risk: Major fund exposure to failed protocol revealed", "Bear market confirmed: Lower highs, lower lows on weekly chart", "Institutional outflows: ETF sees record redemptions for 5th week", "Mining capitulation: Hash rate drops 20% as price falls below cost", ] bullish_texts = [ # From labeled verified data "Bitcoin hits new all-time high of $108,000 as institutional inflows surge. BlackRock IBIT ETF sees record $1.2B daily inflow.", "Bitcoin ETF inflows hit record $2.1B in single week. IBIT alone sees $1.2B. Cumulative AUM passes $50B.", "Bitcoin hits $100,000 for first time ever. MicroStrategy, ETFs, and sovereign buying drive rally.", "Pump.fun revenue hits $100M in 30 days. Memecoin factory launches 50k tokens/day. SOL fees surge.", "MicroStrategy buys additional 12,000 BTC at $61M. Total holdings now 190,000 BTC. Stock MSTR up 15% premarket.", "Arbitrum DAO approves $200M ARB grant program for gaming ecosystem. Voting passes with 92% approval.", "EigenLayer restaking TVL hits $20B. ETH restaking becomes largest DeFi category. Points season 2 announced.", "Hyperliquid DEX launches HYPE token airdrop. $1.2B TVL locked. Points program drives volume.", "dYdX chain migration to Cosmos complete. V4 mainnet launches with 0.02s block times. DYDX token migration.", # Expanded bullish "Institutional adoption accelerates: Fortune 500 companies adding BTC to treasury", "ETF approval drives massive inflows: $10B in first month", "Golden cross confirmed on Bitcoin weekly chart, technical breakout", "Supply shock: Exchange balances hit 5-year low as holders accumulate", "Layer 2 adoption surges: Arbitrum and Optimism TVL doubles", "Real yield protocols attract TradFi capital seeking returns", "Token unlock schedule favorable: Low float, high demand dynamics", "Major partnership: TradFi giant integrates blockchain settlement", "Sovereign wealth fund announces Bitcoin allocation", "Hash rate hits all-time high, mining investment surges", "Developer activity reaches record highs across major ecosystems", "Stablecoin supply grows 50% YoY, indicating fresh capital entry", "Options market signaling upside: Call skew at multi-year highs", "Macro tailwinds: Rate cuts expected, dollar weakening", ] neutral_texts = [ # From labeled verified data "Ethereum Dencun upgrade goes live on mainnet. Proto-Danksharding (EIP-4844) activates, reducing L2 transaction fees by 90%.", "SEC approves spot Bitcoin ETFs for 11 issuers including BlackRock, Fidelity, ARK. Trading begins Thursday.", "Coinbase lists Pepe (PEPE) and Bonk (BONK) memecoins. Trading opens with 100x volume spike.", "Ethereum Pectra upgrade activated. EIP-7702 account abstraction live. EOAs can now batch transactions.", # Expanded neutral "Bitcoin consolidates in tight range between $50k-$52k", "Ethereum gas fees stable at 15 gwei amid low activity", "Market awaits FOMC decision, volumes below average", "Trading range established: Support at $48k, resistance at $55k", "Altcoin season index neutral at 50, no clear trend", "Funding rates flat across perpetual futures markets", "On-chain metrics show equilibrium: Inflows match outflows", "Derivatives open interest stable, no excessive leverage", "Stablecoin market cap flat month-over-month", "Developer conference announces roadmap, no token news", "Governance proposal passes: Parameter change only, no value accrual", "Exchange lists new token, volume modest, no price impact", "Research report: Fair value estimate $55k-$65k range", "Whale wallet rotates positions, no net accumulation or distribution", ] texts = [] labels = [] for t in bearish_texts: texts.append(t); labels.append(0) # Bearish = 0 for t in bullish_texts: texts.append(t); labels.append(2) # Bullish = 2 (positive in FinBERT) for t in neutral_texts: texts.append(t); labels.append(1) # Neutral = 1 return texts, labels class SentimentDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len=128): self.texts = texts self.labels = labels self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text = self.texts[idx] label = self.labels[idx] encoding = self.tokenizer( text, truncation=True, max_length=self.max_len, padding="max_length", return_tensors="pt" ) return { "input_ids": encoding["input_ids"].squeeze(0), "attention_mask": encoding["attention_mask"].squeeze(0), "token_type_ids": encoding.get("token_type_ids", torch.zeros_like(encoding["input_ids"])).squeeze(0), "labels": torch.tensor(label, dtype=torch.long) } def compute_metrics(eval_pred): predictions, labels = eval_pred predictions = np.argmax(predictions, axis=1) return { "accuracy": accuracy_score(labels, predictions), "f1_macro": f1_score(labels, predictions, average="macro"), "f1_per_class": f1_score(labels, predictions, average=None).tolist() } def main(): print("="*60) print("RETRAINING SENTIMENT FROM BASE FINBERT - CORRECT LABELS") print("="*60) # 1. Load verified labeled data print("\n1. Loading verified labeled data...") verified_texts, verified_labels = load_labeled_data("data/labeled_verified.jsonl") print(f" Verified samples: {len(verified_texts)}") # 2. Build augmented data print("\n2. Building augmented training data...") aug_texts, aug_labels = build_augmented_data() print(f" Augmented samples: {len(aug_texts)}") # 3. Combine (weight verified 3x) all_texts = verified_texts * 3 + aug_texts all_labels = verified_labels * 3 + aug_labels print(f"\n3. Total training samples: {len(all_texts)}") print(f" Bearish(0): {all_labels.count(0)}, Neutral(1): {all_labels.count(1)}, Bullish(2): {all_labels.count(2)}") # 4. Train/val/test split train_texts, temp_texts, train_labels, temp_labels = train_test_split( all_texts, all_labels, test_size=0.3, random_state=42, stratify=all_labels ) val_texts, test_texts, val_labels, test_labels = train_test_split( temp_texts, temp_labels, test_size=0.5, random_state=42, stratify=temp_labels ) print(f" Train: {len(train_texts)}, Val: {len(val_texts)}, Test: {len(test_texts)}") # 5. Load BASE FinBERT (not fine-tuned) print("\n4. Loading BASE FinBERT...") tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert") model = AutoModelForSequenceClassification.from_pretrained( "ProsusAI/finbert", num_labels=3, id2label={0: "Bearish", 1: "Neutral", 2: "Bullish"}, label2id={"Bearish": 0, "Neutral": 1, "Bullish": 2} ) # 6. Create datasets train_dataset = SentimentDataset(train_texts, train_labels, tokenizer, max_len=128) val_dataset = SentimentDataset(val_texts, val_labels, tokenizer, max_len=128) # 7. Class weights class_weights = compute_class_weight("balanced", classes=np.array([0,1,2]), y=np.array(train_labels)) class_weights = torch.tensor(class_weights, dtype=torch.float) print(f" Class weights: {class_weights}") # 8. Training arguments training_args = TrainingArguments( output_dir="./models/finbert-crypto-sentiment-v2", num_train_epochs=5, per_device_train_batch_size=8, per_device_eval_batch_size=16, gradient_accumulation_steps=4, warmup_ratio=0.1, learning_rate=2e-5, lr_scheduler_type="cosine", eval_strategy="epoch", save_strategy="epoch", load_best_model_at_end=True, metric_for_best_model="f1_macro", greater_is_better=True, fp16=False, dataloader_num_workers=0, logging_steps=10, save_total_limit=2, remove_unused_columns=False, report_to="none", weight_decay=0.01, ) class WeightedTrainer(Trainer): def compute_loss(self, model, inputs, return_outputs=False, **kwargs): labels = inputs.get("labels") outputs = model(**inputs) logits = outputs.get("logits") loss_fct = torch.nn.CrossEntropyLoss(weight=class_weights.to(logits.device)) loss = loss_fct(logits.view(-1, 3), labels.view(-1)) return (loss, outputs) if return_outputs else loss trainer = WeightedTrainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=val_dataset, tokenizer=tokenizer, compute_metrics=compute_metrics, callbacks=[EarlyStoppingCallback(early_stopping_patience=2)] ) print("\n5. Training (5 epochs)...") trainer.train() # 9. Evaluate on test set print("\n6. Evaluating on test set...") test_dataset = SentimentDataset(test_texts, test_labels, tokenizer, max_len=128) test_results = trainer.evaluate(test_dataset) print(f" Test results: {test_results}") # 10. Save best model print("\n7. Saving improved model...") model.save_pretrained("./models/finbert-crypto-sentiment") tokenizer.save_pretrained("./models/finbert-crypto-sentiment") print(" ✅ Model saved to models/finbert-crypto-sentiment/") # 11. Quick inference test print("\n8. Quick inference test...") model.eval() test_cases = [ ("Bitcoin surges to $108k as institutional inflows surge", 2), # Bullish ("Bitcoin crashes 50% in hours, massive selloff", 0), # Bearish ("BTC at $50k, ETH at $3k, market consolidating", 1), # Neutral ] for text, expected in test_cases: inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128, padding=True) with torch.no_grad(): outputs = model(**inputs) probs = torch.softmax(outputs.logits, dim=-1).numpy()[0] pred = np.argmax(probs) polarity = probs[2] - probs[0] # Bullish - Bearish print(f" '{text[:50]}...' -> {SENTIMENT_LABELS[pred]} (polarity={polarity:.2f}) expected={SENTIMENT_LABELS[expected]}") print("\n" + "="*60) print("SENTIMENT MODEL RETRAINING COMPLETE!") print("="*60) if __name__ == "__main__": main()