Skip to content
Hero Background Light

Combining Alternative Data Signals: A Practical Framework

Combining Alternative Data Signals: A Practical Framework

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:

SignalLimitation
Insider transactionsInsiders sell for many reasons (diversification, taxes)
News sentimentCan be manipulated, lags price action
Put/call ratioHedging distorts the signal
Analyst ratingsConflicts 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:

Terminal window
pip install finbrain-python

Then 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 pd
from datetime import datetime, timedelta
# Define parameters
market = "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 dataset
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)

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 transaction
  • relationship — 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 volume
  • putCount and callCount — 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 FinBrainClient
import pandas as pd
from 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 analysis
fb = 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.450
Sentiment Score: +0.280
Put/Call Score: +0.150
Analyst Score: +0.333
─────────────────────────
Composite Score: +0.314

Interpreting the Composite Score

Score RangeInterpretation
+0.5 to +1.0Strong bullish alignment
+0.2 to +0.5Moderately bullish
-0.2 to +0.2Mixed/neutral signals
-0.5 to -0.2Moderately bearish
-1.0 to -0.5Strong 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:

ScenarioWhat It Might Mean
Insider bullish, sentiment bearishInsiders buying the dip, market hasn’t caught on
Sentiment bullish, put/call bearishRetail optimistic, institutions hedging
Analyst bullish, insider bearishWall 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 tickers
tickers_df = fb.available.tickers("daily", as_dataframe=True)
sp500_tickers = tickers_df[tickers_df['market'] == 'S&P 500']['ticker'].tolist()[:20]
# Screen top 20
rankings = 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 analysts
value_weights = {
'insider': 0.40,
'sentiment': 0.10,
'putcall': 0.10,
'analyst': 0.40
}
# Momentum trader: trust sentiment and options flow
momentum_weights = {
'insider': 0.15,
'sentiment': 0.35,
'putcall': 0.35,
'analyst': 0.15
}
# Contrarian: inverse sentiment, trust insiders
contrarian_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 default

2. 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 df

3. 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_score

Key Takeaways

  1. Individual alternative data signals are noisy—combine multiple sources for conviction
  2. Normalize each signal to a common scale (-1 to +1) before combining
  3. Signal alignment is bullish; significant divergence warrants investigation
  4. Customize weights based on your investment style
  5. Screen entire markets to find the strongest signal combinations
  6. Always handle missing data gracefully
  7. Track signal changes over time, not just absolute levels

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.