Chapter 3: Building Trading Signals¶
Transform data into actionable trading signals.
What is a Signal?¶
A signal is a numerical score that predicts expected returns:
- Higher score → expect higher returns → buy more
- Lower score → expect lower returns → sell/short
Text Only
signal momentum:
// Stocks with higher past returns get higher scores
emit zscore(ret(prices, 60))
Signal Properties¶
Good Signals Have:¶
| Property | Description | Example |
|---|---|---|
| Predictive | Forecasts future returns | High momentum → high future returns |
| Stable | Doesn't change too rapidly | Monthly, not minute-by-minute |
| Diversified | Works across many assets | Not just one stock |
| Economically sensible | Has logical explanation | "Winners keep winning" |
Signal Quality Metrics¶
Information Coefficient (IC): Correlation between signal and forward returns.
Text Only
IC = 0.05 → Good signal
IC = 0.10 → Excellent signal
IC > 0.15 → Suspicious (check for errors)
Building Your First Signal¶
Step 1: Start with a Hypothesis¶
"Stocks with strong recent performance continue to outperform."
Step 2: Implement Mathematically¶
Text Only
signal momentum:
// Calculate 60-day return
past_return = ret(prices, 60)
// Standardize cross-sectionally
emit zscore(past_return)
Step 3: Test and Validate¶
Text Only
portfolio test_momentum:
weights = rank(momentum).long_short(top=0.2, bottom=0.2)
backtest from 2015-01-01 to 2024-12-31
Common Signal Types¶
Momentum Signals¶
Past returns predict future returns:
Text Only
// Simple momentum
signal momentum_simple:
emit zscore(ret(prices, 60))
// 12-1 momentum (skip last month)
signal momentum_12_1:
ret_12m = ret(prices, 252)
ret_1m = ret(prices, 21)
emit zscore(ret_12m - ret_1m)
// Relative strength
signal relative_strength:
stock_ret = ret(prices, 60)
market_ret = ret(market, 60)
emit zscore(stock_ret - market_ret)
Value Signals¶
Cheap stocks outperform:
Text Only
// Book-to-market
signal value_btm:
emit zscore(book_value / market_cap)
// Earnings yield
signal value_ey:
emit zscore(earnings / prices)
// Combined value
signal value_composite:
btm = zscore(book_value / market_cap)
ey = zscore(earnings / prices)
emit 0.5 * btm + 0.5 * ey
Quality Signals¶
High-quality companies outperform:
Text Only
// Profitability
signal quality_roe:
emit zscore(net_income / equity)
// Earnings stability
signal quality_stability:
emit -zscore(rolling_std(earnings, 8))
// Combined quality
signal quality_composite:
roe = zscore(net_income / equity)
stability = -zscore(rolling_std(earnings, 8))
emit 0.6 * roe + 0.4 * stability
Low Volatility Signals¶
Low-risk stocks outperform (risk-adjusted):
Text Only
// Inverse volatility
signal low_vol:
vol = rolling_std(ret(prices, 1), 60) * sqrt(252)
emit -zscore(vol)
// Beta
signal low_beta:
stock_ret = ret(prices, 1)
market_ret = ret(market, 1)
beta = rolling_cov(stock_ret, market_ret, 60) / rolling_var(market_ret, 60)
emit -zscore(beta)
Mean Reversion Signals¶
Extreme moves revert:
Text Only
// Price vs moving average
signal mean_reversion:
ma = rolling_mean(prices, 20)
deviation = (prices - ma) / ma
emit -zscore(deviation)
// RSI-based
signal rsi_reversion:
change = diff(prices, 1)
gains = where(change > 0, change, 0)
losses = where(change < 0, -change, 0)
rs = ema(gains, 14) / (ema(losses, 14) + 0.0001)
rsi = 100 - 100 / (1 + rs)
emit zscore(50 - rsi)
Signal Transformation¶
Standardization¶
Make signals comparable:
Text Only
// Z-score (mean=0, std=1)
signal standardized:
emit zscore(raw_signal)
// Rank (uniform distribution)
signal ranked:
emit rank(raw_signal) / count(raw_signal)
Winsorization¶
Handle outliers:
Text Only
signal winsorized:
lower = quantile(raw_signal, 0.01)
upper = quantile(raw_signal, 0.99)
emit clip(raw_signal, lower, upper)
Sector Neutralization¶
Remove sector bias:
Text Only
signal sector_neutral:
// Demean within each sector
emit neutralize(raw_signal, by=sectors)
Smoothing¶
Reduce noise:
Combining Signals¶
Simple Average¶
Weighted Average¶
IC-Weighted¶
Weight by predictive power:
Text Only
signal combined_ic:
// Weights based on historical IC
ic_mom = 0.04
ic_val = 0.03
ic_qual = 0.02
total = ic_mom + ic_val + ic_qual
emit (ic_mom * momentum + ic_val * value + ic_qual * quality) / total
Rank Then Average¶
Text Only
signal combined_rank:
r_mom = rank(momentum)
r_val = rank(value)
r_qual = rank(quality)
emit (r_mom + r_val + r_qual) / 3
Signal Decay¶
Signals lose predictive power over time:
Managing Decay¶
Text Only
// More frequent rebalancing
portfolio fast_decay:
weights = ...
backtest rebal=5 from 2015-01-01 to 2024-12-31 // Every 5 days
// Less frequent for slow decay
portfolio slow_decay:
weights = ...
backtest rebal=21 from 2015-01-01 to 2024-12-31 // Monthly
Complete Example¶
Multi-Factor Signal¶
Text Only
data:
source = "prices_fundamentals.parquet"
format = parquet
// Individual factors
signal momentum:
ret_12m = ret(prices, 252)
ret_1m = ret(prices, 21)
emit neutralize(zscore(ret_12m - ret_1m), by=sectors)
signal value:
btm = zscore(book_value / market_cap)
ey = zscore(earnings / prices)
raw = 0.5 * btm + 0.5 * ey
emit neutralize(raw, by=sectors)
signal quality:
prof = zscore(net_income / equity)
stab = -zscore(rolling_std(earnings, 8))
raw = 0.6 * prof + 0.4 * stab
emit neutralize(raw, by=sectors)
signal low_vol:
vol = rolling_std(ret(prices, 1), 60) * sqrt(252)
emit neutralize(-zscore(vol), by=sectors)
// Combined signal
signal multi_factor:
emit 0.30 * momentum + 0.30 * value + 0.25 * quality + 0.15 * low_vol
// Portfolio
portfolio main:
weights = rank(multi_factor).long_short(top=0.2, bottom=0.2, cap=0.03)
constraints:
max_sector = 0.20
costs = tc.bps(10)
backtest from 2015-01-01 to 2024-12-31
Best Practices¶
1. Start Simple¶
Text Only
// Start here
signal simple:
emit zscore(ret(prices, 60))
// Not here
signal complex:
emit complicated_formula(many, parameters, here)
2. Use Economic Intuition¶
Ask: "Why should this work?"
3. Normalize Everything¶
4. Sector Neutralize¶
5. Test Out-of-Sample¶
Use walk-forward validation to avoid overfitting.
Exercises¶
- Build a momentum signal using 12-1 month returns
- Create a value signal combining book-to-market and earnings yield
- Combine momentum and value with equal weights
- Sector-neutralize your combined signal
Next Chapter¶
Continue to Chapter 4: The sigc Language to master the DSL syntax.