Portfolio Operators¶
Operators for constructing portfolio weights.
Long-Short Portfolio¶
long_short(top, bottom, cap)¶
Create dollar-neutral long-short weights.
Parameters:
top: Fraction of assets to long (e.g., 0.2 = top 20%)bottom: Fraction of assets to short (e.g., 0.2 = bottom 20%)cap: Maximum position size (optional)
Text Only
portfolio main:
// Long top 20%, short bottom 20%
weights = rank(signal).long_short(top=0.2, bottom=0.2)
backtest from 2024-01-01 to 2024-12-31
Weight Distribution¶
With long_short(top=0.2, bottom=0.2) and 100 assets:
| Position | Count | Individual Weight | Total |
|---|---|---|---|
| Long | 20 | +5% | +100% |
| Neutral | 60 | 0% | 0% |
| Short | 20 | -5% | -100% |
| Net | 0% | ||
| Gross | 200% |
With Position Cap¶
Text Only
portfolio capped:
// No position > 5%
weights = rank(signal).long_short(top=0.2, bottom=0.2, cap=0.05)
backtest from 2024-01-01 to 2024-12-31
The cap limits individual position sizes while maintaining the long-short structure.
Examples¶
Text Only
// Basic long-short
weights = rank(signal).long_short(top=0.2, bottom=0.2)
// Wider spread
weights = rank(signal).long_short(top=0.3, bottom=0.3)
// Asymmetric (more longs than shorts)
weights = rank(signal).long_short(top=0.3, bottom=0.1)
// With position cap
weights = rank(signal).long_short(top=0.2, bottom=0.2, cap=0.05)
Neutralization¶
neutralize(x, by)¶
Remove group exposure by demeaning within groups.
Text Only
signal sector_neutral:
raw = zscore(ret(prices, 60))
// Demean within each sector
neutral = neutralize(raw, by=sectors)
emit neutral
After neutralization, each group (sector) has mean = 0.
Use Cases¶
Sector Neutralization:
Text Only
signal sector_neutral:
momentum = zscore(ret(prices, 60))
neutral = neutralize(momentum, by=sectors)
emit neutral
Industry Neutralization:
Text Only
signal industry_neutral:
value = zscore(book_to_market)
neutral = neutralize(value, by=industries)
emit neutral
Country Neutralization:
Text Only
signal country_neutral:
momentum = zscore(ret(prices, 60))
neutral = neutralize(momentum, by=countries)
emit neutral
Clipping¶
clip(x, lo, hi)¶
Bound values to a range.
Text Only
signal bounded:
raw = zscore(ret(prices, 20))
// Clip to [-3, 3]
bounded = clip(raw, -3, 3)
emit bounded
For Weights¶
Text Only
portfolio constrained:
raw_weights = some_weight_calculation
// Limit position sizes
bounded_weights = clip(raw_weights, -0.05, 0.05)
Common Patterns¶
Basic Long-Short¶
Text Only
signal momentum:
emit zscore(ret(prices, 60))
portfolio basic:
weights = rank(momentum).long_short(top=0.2, bottom=0.2)
backtest from 2024-01-01 to 2024-12-31
Sector-Neutral Long-Short¶
Text Only
signal sector_neutral_mom:
raw = zscore(ret(prices, 60))
neutral = neutralize(raw, by=sectors)
emit neutral
portfolio sector_neutral:
weights = rank(sector_neutral_mom).long_short(top=0.2, bottom=0.2)
backtest from 2024-01-01 to 2024-12-31
Position-Capped¶
Text Only
signal signal:
emit zscore(ret(prices, 60))
portfolio capped:
weights = rank(signal).long_short(top=0.2, bottom=0.2, cap=0.03)
backtest from 2024-01-01 to 2024-12-31
Combined Signals¶
Text Only
signal momentum:
emit zscore(ret(prices, 60))
signal value:
emit zscore(book_to_market)
signal combined:
emit 0.5 * momentum + 0.5 * value
portfolio multi_factor:
weights = rank(combined).long_short(top=0.2, bottom=0.2)
backtest from 2024-01-01 to 2024-12-31
Long-Only (Conceptual)¶
Text Only
signal signal:
scores = zscore(ret(prices, 60))
// Only keep positive scores
long_only = where(scores > 0, scores, 0)
emit long_only
portfolio long:
// Use rank for top selection
weights = rank(signal).long_short(top=0.2, bottom=0)
backtest from 2024-01-01 to 2024-12-31
Why Use Rank?¶
rank() before long_short() provides:
- Robustness: Not sensitive to outliers
- Uniform Distribution: Equal weight to each position
- Consistency: Same number of positions regardless of signal scale
Without Rank (Signal-Weighted)¶
With Rank (Equal-Weighted)¶
Text Only
// Equal weight to each position
// Robust to outliers
weights = rank(signal).long_short(top=0.2, bottom=0.2)
Weight Properties¶
Dollar Neutral¶
With equal top and bottom:
Text Only
weights = rank(signal).long_short(top=0.2, bottom=0.2)
// Sum of long weights = Sum of short weights (absolute)
// Net exposure = 0
Gross Exposure¶
Adjusting Exposure¶
To achieve specific gross exposure:
Text Only
// For 150% gross (75% long, 75% short)
weights = rank(signal).long_short(top=0.2, bottom=0.2)
scaled_weights = weights * 0.75
Integration with Backtest¶
Text Only
portfolio full_example:
weights = rank(signal).long_short(top=0.2, bottom=0.2, cap=0.05)
costs = tc.bps(5) + slippage.model("square-root", coef=0.1)
backtest rebal=21 benchmark=SPY from 2020-01-01 to 2024-12-31
Best Practices¶
1. Always Use Rank¶
Text Only
// Good: Robust to outliers
weights = rank(signal).long_short(top=0.2, bottom=0.2)
// Risky: Sensitive to signal scale
weights = signal.long_short(top=0.2, bottom=0.2)
2. Apply Position Caps¶
3. Consider Sector Neutrality¶
Text Only
// If sector exposure is undesirable
neutral_signal = neutralize(raw_signal, by=sectors)
weights = rank(neutral_signal).long_short(top=0.2, bottom=0.2)
4. Match Rebalancing to Signal¶
Text Only
// Fast signal: frequent rebalancing
backtest rebal=5 from ...
// Slow signal: infrequent rebalancing
backtest rebal=21 from ...
Next Steps¶
- Portfolio Section - Full portfolio syntax
- Backtesting - Running backtests
- Constraints - Advanced constraints