Individual alternative data signals are noisy. Insider buying might be bullish, but if sentiment is collapsing and analysts are downgrading, the picture is less clear. The real value comes from combining multiple signals to find alignment—or interesting divergences.
This tutorial shows how to pull multiple FinBrain datasets, normalize them into comparable scores, and combine them into a unified signal.
The Problem with Single Signals
Each alternative data type has limitations:
| Signal | Limitation |
|---|---|
| Insider transactions | Insiders sell for many reasons (diversification, taxes) |
| News sentiment | Can be manipulated, lags price action |
| Put/call ratio | Hedging distorts the signal |
| Analyst ratings | Conflicts of interest, slow to update |
But when multiple independent signals align, conviction increases. An insider buying while sentiment improves and put/call drops is more meaningful than any single data point.
Setup
First, install the FinBrain Python SDK:
pip install finbrain-pythonThen initialize the client:
from finbrain import FinBrainClient
fb = FinBrainClient(api_key="YOUR_API_KEY")Pulling Multiple Signals
Let’s pull four signal types for a single ticker:
import pandas as pdfrom datetime import datetime, timedelta
# Define parametersmarket = "S&P 500"ticker = "AAPL"date_from = (datetime.now() - timedelta(days=90)).strftime("%Y-%m-%d")date_to = datetime.now().strftime("%Y-%m-%d")
# Pull each datasetinsider_df = fb.insider_transactions.ticker(market, ticker, as_dataframe=True)sentiment_df = fb.sentiments.ticker(market, ticker, date_from=date_from, date_to=date_to, as_dataframe=True)options_df = fb.options.put_call(market, ticker, date_from=date_from, date_to=date_to, as_dataframe=True)analyst_df = fb.analyst_ratings.ticker(market, ticker, date_from=date_from, date_to=date_to, as_dataframe=True)Understanding the Response Fields
Each dataset returns specific fields that we’ll use for scoring:
Insider Transactions:
transaction— “Purchase” or “Sale”USDValue— Dollar value of the transactionrelationship— Role of the insider (CEO, CFO, Director, etc.)
Sentiment:
sentimentAnalysis— Date-keyed scores from -1 (bearish) to +1 (bullish)
Put/Call Data:
ratio— Put volume divided by call volumeputCountandcallCount— Raw volumes
Analyst Ratings:
type— “Upgrade”, “Downgrade”, “Reiterated”, etc.signal— “Buy”, “Hold”, “Sell”, etc.targetPrice— Analyst’s price target
Scoring Each Signal
Now let’s convert each raw dataset into a normalized score between -1 (bearish) and +1 (bullish).
Insider Transaction Score
def score_insider_transactions(df): """ Score insider activity: +1 for net buying, -1 for net selling. Weight by USD value and recency. """ if df is None or df.empty: return 0.0
# Calculate net buying buys = df[df['transaction'] == 'Purchase']['USDValue'].sum() sells = df[df['transaction'] == 'Sale']['USDValue'].sum()
net = buys - sells total = buys + sells
if total == 0: return 0.0
# Normalize to -1 to +1 score = net / total return round(score, 3)Sentiment Score
def score_sentiment(df): """ Average recent sentiment scores. Already in -1 to +1 range. """ if df is None or df.empty: return 0.0
# Get recent sentiment values (last 14 days) recent = df.head(14)
# Sentiment values are strings, convert to float if 'sentimentAnalysis' in recent.columns: scores = recent['sentimentAnalysis'].astype(float) else: # Handle date-keyed format scores = recent.iloc[:, 0].astype(float)
return round(scores.mean(), 3)Put/Call Score
def score_put_call(df): """ Score put/call ratio: low ratio = bullish, high ratio = bearish. Typical range is 0.5 to 1.5. """ if df is None or df.empty: return 0.0
# Get recent average ratio recent_ratio = df.head(14)['ratio'].mean()
# Invert and normalize: 0.5 -> +1, 1.0 -> 0, 1.5 -> -1 # Formula: score = (1.0 - ratio) * 2, clamped to [-1, 1] score = (1.0 - recent_ratio) * 2 score = max(-1, min(1, score))
return round(score, 3)Analyst Rating Score
def score_analyst_ratings(df): """ Score analyst activity: upgrades = bullish, downgrades = bearish. """ if df is None or df.empty: return 0.0
# Count upgrades vs downgrades upgrades = len(df[df['type'].str.contains('Upgrade', case=False, na=False)]) downgrades = len(df[df['type'].str.contains('Downgrade', case=False, na=False)])
# Also consider signal distribution buys = len(df[df['signal'].isin(['Buy', 'Strong Buy', 'Outperform'])]) sells = len(df[df['signal'].isin(['Sell', 'Strong Sell', 'Underperform'])])
total_actions = upgrades + downgrades total_signals = buys + sells
# Combine both metrics action_score = (upgrades - downgrades) / max(total_actions, 1) signal_score = (buys - sells) / max(total_signals, 1)
# Average the two score = (action_score + signal_score) / 2 return round(score, 3)Combining Into a Composite Score
Now we combine the individual scores with configurable weights:
def calculate_composite_score(insider_score, sentiment_score, putcall_score, analyst_score, weights=None): """ Combine individual scores into a weighted composite. Default weights are equal. """ if weights is None: weights = { 'insider': 0.30, 'sentiment': 0.25, 'putcall': 0.25, 'analyst': 0.20 }
composite = ( insider_score * weights['insider'] + sentiment_score * weights['sentiment'] + putcall_score * weights['putcall'] + analyst_score * weights['analyst'] )
return round(composite, 3)Putting It All Together
Here’s the complete workflow:
from finbrain import FinBrainClientimport pandas as pdfrom datetime import datetime, timedelta
def analyze_ticker(fb, market, ticker, days_back=90): """ Pull all signals and calculate composite score for a ticker. """ date_from = (datetime.now() - timedelta(days=days_back)).strftime("%Y-%m-%d") date_to = datetime.now().strftime("%Y-%m-%d")
# Pull data insider_df = fb.insider_transactions.ticker(market, ticker, as_dataframe=True) sentiment_df = fb.sentiments.ticker(market, ticker, date_from=date_from, date_to=date_to, as_dataframe=True) options_df = fb.options.put_call(market, ticker, date_from=date_from, date_to=date_to, as_dataframe=True) analyst_df = fb.analyst_ratings.ticker(market, ticker, date_from=date_from, date_to=date_to, as_dataframe=True)
# Score each signal scores = { 'insider': score_insider_transactions(insider_df), 'sentiment': score_sentiment(sentiment_df), 'putcall': score_put_call(options_df), 'analyst': score_analyst_ratings(analyst_df) }
# Calculate composite scores['composite'] = calculate_composite_score( scores['insider'], scores['sentiment'], scores['putcall'], scores['analyst'] )
return scores
# Run analysisfb = FinBrainClient(api_key="YOUR_API_KEY")scores = analyze_ticker(fb, "S&P 500", "AAPL")
print(f"Insider Score: {scores['insider']:+.3f}")print(f"Sentiment Score: {scores['sentiment']:+.3f}")print(f"Put/Call Score: {scores['putcall']:+.3f}")print(f"Analyst Score: {scores['analyst']:+.3f}")print(f"─────────────────────────")print(f"Composite Score: {scores['composite']:+.3f}")Example output:
Insider Score: +0.450Sentiment Score: +0.280Put/Call Score: +0.150Analyst Score: +0.333─────────────────────────Composite Score: +0.314Interpreting the Composite Score
| Score Range | Interpretation |
|---|---|
| +0.5 to +1.0 | Strong bullish alignment |
| +0.2 to +0.5 | Moderately bullish |
| -0.2 to +0.2 | Mixed/neutral signals |
| -0.5 to -0.2 | Moderately bearish |
| -1.0 to -0.5 | Strong bearish alignment |
The composite score is most useful when signals align. A +0.3 composite where all four signals are positive is more meaningful than +0.3 where insider is +0.8 and sentiment is -0.5.
Detecting Signal Divergence
Sometimes the most interesting situations are when signals diverge:
def detect_divergence(scores, threshold=0.4): """ Flag when signals significantly disagree. """ values = [scores['insider'], scores['sentiment'], scores['putcall'], scores['analyst']]
spread = max(values) - min(values)
if spread > threshold: bullish = [k for k, v in scores.items() if k != 'composite' and v > 0.2] bearish = [k for k, v in scores.items() if k != 'composite' and v < -0.2]
return { 'divergence': True, 'spread': round(spread, 3), 'bullish_signals': bullish, 'bearish_signals': bearish }
return {'divergence': False, 'spread': round(spread, 3)}Example divergence scenarios:
| Scenario | What It Might Mean |
|---|---|
| Insider bullish, sentiment bearish | Insiders buying the dip, market hasn’t caught on |
| Sentiment bullish, put/call bearish | Retail optimistic, institutions hedging |
| Analyst bullish, insider bearish | Wall Street pumping, insiders exiting |
Screening Multiple Tickers
Scale this to scan an entire market:
def screen_market(fb, market, tickers): """ Screen multiple tickers and rank by composite score. """ results = []
for ticker in tickers: try: scores = analyze_ticker(fb, market, ticker) scores['ticker'] = ticker results.append(scores) except Exception as e: print(f"Error processing {ticker}: {e}") continue
# Sort by composite score results_df = pd.DataFrame(results) results_df = results_df.sort_values('composite', ascending=False)
return results_df
# Get available tickerstickers_df = fb.available.tickers("daily", as_dataframe=True)sp500_tickers = tickers_df[tickers_df['market'] == 'S&P 500']['ticker'].tolist()[:20]
# Screen top 20rankings = screen_market(fb, "S&P 500", sp500_tickers)print(rankings[['ticker', 'composite', 'insider', 'sentiment', 'putcall', 'analyst']])Customizing Weights
Different strategies call for different weightings:
# Value investor: trust insiders and analystsvalue_weights = { 'insider': 0.40, 'sentiment': 0.10, 'putcall': 0.10, 'analyst': 0.40}
# Momentum trader: trust sentiment and options flowmomentum_weights = { 'insider': 0.15, 'sentiment': 0.35, 'putcall': 0.35, 'analyst': 0.15}
# Contrarian: inverse sentiment, trust insiderscontrarian_weights = { 'insider': 0.50, 'sentiment': -0.20, # Negative weight = contrarian 'putcall': 0.20, 'analyst': 0.10}Adding AI Predictions
You can also incorporate FinBrain’s AI price predictions:
def score_ai_prediction(fb, ticker): """ Score based on AI prediction direction. Uses expectedShort, expectedMid, expectedLong probabilities. """ pred_df = fb.predictions.ticker(ticker, as_dataframe=True)
if pred_df is None or pred_df.empty: return 0.0
# Get probability scores (returned as strings) short = float(pred_df['expectedShort'].iloc[0]) mid = float(pred_df['expectedMid'].iloc[0]) long_term = float(pred_df['expectedLong'].iloc[0])
# Short = bearish, Long = bullish # Score = long - short, normalized by mid uncertainty score = (long_term - short) * (1 - mid)
return round(score, 3)Best Practices
1. Handle Missing Data
Not all tickers have all data types. Always check for empty responses:
def safe_score(scoring_func, df, default=0.0): try: if df is None or df.empty: return default return scoring_func(df) except Exception: return default2. Consider Recency
Weight recent data more heavily than older data:
def apply_recency_weight(df, date_col='date', half_life_days=14): """Apply exponential decay based on data age.""" df['age_days'] = (pd.Timestamp.now() - pd.to_datetime(df[date_col])).dt.days df['weight'] = 0.5 ** (df['age_days'] / half_life_days) return df3. Normalize for Sector
Some sectors naturally have higher insider selling (tech executives) or different put/call norms. Consider sector-relative scoring for cross-sector comparisons.
4. Track Signal Changes
A signal flipping from negative to positive is often more meaningful than a stable positive signal:
def calculate_momentum(current_score, previous_score): """Track how signals are changing.""" return current_score - previous_scoreKey Takeaways
- Individual alternative data signals are noisy—combine multiple sources for conviction
- Normalize each signal to a common scale (-1 to +1) before combining
- Signal alignment is bullish; significant divergence warrants investigation
- Customize weights based on your investment style
- Screen entire markets to find the strongest signal combinations
- Always handle missing data gracefully
- Track signal changes over time, not just absolute levels
Related Resources
- Insider Transactions Dataset — SEC Form 4 filing data
- News Sentiment Dataset — NLP-scored news sentiment
- Put/Call Ratio Dataset — Options market sentiment
- Analyst Ratings Dataset — Wall Street ratings and targets
- AI Forecasts Dataset — Machine learning price predictions
- Python SDK Documentation — Complete SDK reference
Combining alternative data signals transforms noisy individual indicators into a clearer picture of market positioning. The framework above is a starting point—adapt the scoring logic and weights to match your investment process.