CHAPTER 3

Strategy Development

From Models to Strategies

Having predictive models is just the beginning. This chapter covers how to translate model predictions into actionable trading strategies with proper signal generation, position sizing, and portfolio construction.

Alpha Generation

What is Alpha?

Alpha represents excess returns above a benchmark after adjusting for risk. In quantitative trading, alpha is the holy grail - the edge that allows you to outperform the market consistently.

Alpha Formula: α = Portfolio Return - (Risk-Free Rate + β × Market Risk Premium)

Sources of Alpha

Signal Generation

Converting model predictions into trading signals requires careful design:

Continuous Signals

Raw model predictions (e.g., predicted returns) used directly for ranking and weighting

# Rank-based portfolio construction
predictions = model.predict(features)
ranks = predictions.rank(pct=True)  # 0 to 1 ranking

# Long top 20%, short bottom 20%
signals = pd.Series(0, index=ranks.index)
signals[ranks >= 0.8] = 1   # Long
signals[ranks <= 0.2] = -1  # Short

# Position sizing proportional to signal strength
position_sizes = signals * ranks.abs()

Binary Signals

Discrete buy/sell decisions based on thresholds

# Threshold-based signals
signal = 0
if predicted_return > 0.01:  # 1% threshold
    signal = 1  # Buy
elif predicted_return < -0.01:
    signal = -1  # Sell
else:
    signal = 0  # Hold

Probabilistic Signals

Using predicted probabilities for directional trading

# Classification model output
probs = model.predict_proba(features)
p_up = probs[:, 2]    # P(price up)
p_down = probs[:, 0]  # P(price down)

# Trade only high-confidence signals
if p_up > 0.7:
    signal = p_up - 0.5  # Scaled by confidence
elif p_down > 0.7:
    signal = -(p_down - 0.5)
else:
    signal = 0  # No trade

Position Sizing

How much capital to allocate to each trade is critical for risk management and return optimization.

Fixed Fractional Position Sizing

Risk a fixed percentage of capital per trade

def fixed_fractional_size(capital, risk_per_trade=0.02,
                         entry_price, stop_loss):
    """Risk 2% of capital per trade"""
    risk_amount = capital * risk_per_trade
    price_risk = abs(entry_price - stop_loss)
    shares = risk_amount / price_risk
    return shares

Kelly Criterion

Maximize long-term growth rate (use fractional Kelly for safety)

def kelly_position_size(win_rate, avg_win, avg_loss):
    """Kelly criterion for position sizing"""
    if avg_loss == 0:
        return 0
    win_loss_ratio = avg_win / abs(avg_loss)
    kelly = (win_rate * win_loss_ratio - (1 - win_rate)) / win_loss_ratio

    # Use half-Kelly for safety
    return max(0, min(kelly * 0.5, 0.25))  # Cap at 25%

Volatility-Based Sizing

Inverse volatility weighting for risk parity

def volatility_size(predictions, volatilities, target_vol=0.15):
    """Size positions inversely to volatility"""
    # Target portfolio volatility
    inverse_vol = 1 / volatilities
    weights = predictions * inverse_vol
    weights = weights / weights.abs().sum()

    # Scale to target volatility
    portfolio_vol = (weights ** 2 * volatilities ** 2).sum() ** 0.5
    scale = target_vol / portfolio_vol
    return weights * scale

Portfolio Construction

Mean-Variance Optimization

Classic Markowitz portfolio optimization balancing return and risk

import cvxpy as cp

def optimize_portfolio(expected_returns, cov_matrix,
                       risk_aversion=1.0):
    """Markowitz mean-variance optimization"""
    n = len(expected_returns)
    weights = cp.Variable(n)

    # Objective: maximize return - risk_aversion * variance
    ret = expected_returns @ weights
    risk = cp.quad_form(weights, cov_matrix)
    objective = cp.Maximize(ret - risk_aversion * risk)

    # Constraints
    constraints = [
        cp.sum(weights) == 1,   # Fully invested
        weights >= -1,           # Max 100% short
        weights <= 1             # Max 100% long
    ]

    problem = cp.Problem(objective, constraints)
    problem.solve()
    return weights.value

Risk Parity

Equal risk contribution from each asset

Black-Litterman

Combines market equilibrium with investor views

Factor Models

Fama-French Factors

Custom ML Factors

# Example: ML-based momentum factor
class MLMomentumFactor:
    def __init__(self):
        self.model = LSTMPredictor(...)

    def calculate_factor(self, data):
        """Generate momentum scores using ML"""
        predictions = self.model.predict(data)

        # Rank and normalize
        factor_scores = predictions.rank(pct=True) - 0.5

        # Long top quintile, short bottom quintile
        long_threshold = factor_scores.quantile(0.8)
        short_threshold = factor_scores.quantile(0.2)

        positions = pd.Series(0, index=factor_scores.index)
        positions[factor_scores >= long_threshold] = 1
        positions[factor_scores <= short_threshold] = -1

        return positions

Complete Strategy Example

class AITradingStrategy:
    def __init__(self, model, risk_per_trade=0.02):
        self.model = model
        self.risk_per_trade = risk_per_trade
        self.positions = {}

    def generate_signals(self, market_data):
        """Generate trading signals from market data"""
        # Feature engineering
        features = self.engineer_features(market_data)

        # ML predictions
        predictions = self.model.predict(features)

        # Convert to signals
        signals = self.predictions_to_signals(predictions)

        return signals

    def size_positions(self, signals, portfolio_value, volatilities):
        """Calculate position sizes"""
        positions = {}

        for asset, signal in signals.items():
            if signal == 0:
                positions[asset] = 0
                continue

            # Volatility-adjusted sizing
            vol = volatilities[asset]
            base_size = portfolio_value * self.risk_per_trade
            position_size = (base_size / vol) * signal

            # Apply position limits
            max_position = portfolio_value * 0.10  # 10% max
            positions[asset] = np.clip(position_size,
                                      -max_position, max_position)

        return positions

    def rebalance(self, current_positions, target_positions):
        """Generate rebalancing orders"""
        orders = []

        for asset, target in target_positions.items():
            current = current_positions.get(asset, 0)
            diff = target - current

            if abs(diff) > threshold:  # Avoid small trades
                orders.append({
                    'asset': asset,
                    'quantity': diff,
                    'type': 'market'
                })

        return orders

Strategy Optimization

Parameter Optimization

Walk-Forward Optimization

  1. Optimize parameters on training period
  2. Test on out-of-sample period
  3. Roll forward to next period
  4. Re-optimize and test again
  5. Aggregate out-of-sample results
Warning: Over-optimization is dangerous! More parameters = more overfitting risk. Keep strategies simple and robust.