Best Practices

This guide covers recommended practices for getting the best results from GRU Dynamic Beta.

Data Requirements

Minimum Data Size

For reliable results:

# Minimum recommended
min_samples = lookback + initial_train_size + 200

# Example: lookback=90, initial_train_size=500
# Minimum: 90 + 500 + 200 = 790 samples (~3 years daily data)

# Ideal for robust estimation
ideal_samples = 2000+  # ~8 years daily data

Data Quality

  1. No gaps: Fill or interpolate missing dates

  2. Clean returns: Cap extreme values

    returns = np.clip(returns, -0.5, 0.5)  # Cap at ±50%
    
  3. Consistent dates: Stock and market must align

    # Align on common dates
    common = stock_df.merge(market_df, on='Date', how='inner')
    

Parameter Selection

Lookback Window

Lambda Beta (Stability)

# Start with default
lambda_beta = 0.05

# If beta is too noisy, increase
lambda_beta = 0.1 to 0.2

# If beta is too smooth (missing regime changes), decrease
lambda_beta = 0.01 to 0.03

Lambda Alpha (Sparsity)

# For efficient markets (alpha should be ~0)
lambda_alpha = 0.5 to 1.0

# If stock has persistent alpha
lambda_alpha = 0.1 to 0.3

# For alpha estimation focus
lambda_alpha = 0.05

Lambda Alpha Smooth (Temporal Smoothness)

# For efficient markets (alpha should be stable near zero)
lambda_alpha_smooth = 0.1 to 0.2

# If stock has genuine time-varying alpha
lambda_alpha_smooth = 0.02 to 0.05

# For maximum smoothness
lambda_alpha_smooth = 0.2

Walk-Forward Step Size

# More frequent retraining = more adaptive, slower
wf_step_size = 21   # Monthly
wf_step_size = 63   # Quarterly
wf_step_size = 126  # Semi-annual (default)
wf_step_size = 252  # Annual

Recommendation: Start with 126, reduce if you need more adaptability.

Avoiding Common Pitfalls

1. Lookahead Bias

Always verify:

from grubeta.utils import validate_no_lookahead

passed = validate_no_lookahead(
    results['beta'].values,
    results['stock_return'].values,
    results['market_return'].values,
    initial_size=config.initial_train_size
)

assert passed, "Lookahead bias detected!"

Common causes:

  • Using current-day features (not lagging)

  • Fitting scalers on full dataset

  • Using in-sample predictions

2. Overfitting

Signs of overfitting:

  • In-sample R² much higher than out-of-sample

  • Beta volatility too low (memorizing)

  • Poor performance on new data

Solutions:

# Increase regularization
config = DynamicBetaConfig(
    dropout_rate=0.3,  # Increase from 0.2
    lambda_beta=0.1,   # Increase stability penalty
)

# Use smaller network
config = DynamicBetaConfig(
    gru_units=64,  # Reduce from 128
)

# Reduce training epochs
config = DynamicBetaConfig(
    epochs_init=30,
    epochs_retrain=2,
)

3. Insufficient Training

Signs:

  • Beta stuck near initial values

  • High prediction variance

  • Poor convergence

Solutions:

# Increase initial training
config = DynamicBetaConfig(
    initial_train_size=750,  # From 500
    epochs_init=60,          # From 40
)

# Adjust learning rate
config = DynamicBetaConfig(
    learning_rate=5e-5,  # Slower but more stable
)

4. Regime Changes

Beta can change dramatically during market regime changes.

Detection:

# Monitor rolling beta volatility
rolling_std = results['beta'].rolling(60).std()

# High volatility = potential regime change
regime_changes = rolling_std > 2 * rolling_std.mean()

Handling:

# Faster adaptation
config = DynamicBetaConfig(
    wf_step_size=21,  # Monthly retraining
    lambda_beta=0.02, # More responsive
)

Production Deployment

Model Versioning

from datetime import datetime

# Version your models
version = datetime.now().strftime("%Y%m%d")
model.save(f'./models/AAPL_beta_v{version}')

# Keep metadata
metadata = {
    'version': version,
    'lookback': config.lookback,
    'lambda_beta': config.lambda_beta,
    'train_end_date': str(data['dates'][-1]),
    'performance': metrics
}

Monitoring

def monitor_model_performance(model, new_data, threshold=0.2):
    """Monitor for model degradation."""
    predictions = model.predict(new_data['stock'], new_data['market'])

    # Calculate recent R²
    recent_r2 = compute_r2(
        new_data['stock'][-252:],
        predictions['beta'][-252:] * new_data['market'][-252:]
    )

    if recent_r2 < threshold:
        alert("Model performance degraded - consider retraining")
        return False
    return True

Retraining Schedule

# Automated retraining
def should_retrain(model, metrics_history, threshold=0.1):
    """Determine if model needs retraining."""
    if len(metrics_history) < 2:
        return False

    recent = metrics_history[-1]['systematic_r2']
    baseline = np.mean([m['systematic_r2'] for m in metrics_history[:-1]])

    degradation = (baseline - recent) / baseline
    return degradation > threshold

Performance Optimization

Memory Management

import gc

def process_batch(stocks, market_data):
    """Process stocks with memory cleanup."""
    results = {}

    for stock in stocks:
        model = DynamicBeta(lookback=60)
        result = model.fit_predict(stock_data[stock], market_data)

        # Keep only what we need
        results[stock] = result[['date', 'beta', 'alpha']].copy()

        # Cleanup
        del model
        gc.collect()

    return results

GPU Utilization

import tensorflow as tf

# Enable memory growth
gpus = tf.config.list_physical_devices('GPU')
for gpu in gpus:
    tf.config.experimental.set_memory_growth(gpu, True)

# For multiple GPUs
strategy = tf.distribute.MirroredStrategy()
with strategy.scope():
    model = DynamicBeta(lookback=90)

Batch Processing

from concurrent.futures import ProcessPoolExecutor

def process_stock(args):
    stock_name, stock_data, market_data = args
    model = DynamicBeta(lookback=60)
    return stock_name, model.fit_predict(stock_data, market_data)

# Parallel processing
with ProcessPoolExecutor(max_workers=4) as executor:
    tasks = [(name, data, market) for name, data in stocks.items()]
    results = dict(executor.map(process_stock, tasks))

Validation Checklist

Before deploying a model, verify:

def validate_model(model, results, data):
    """Complete validation checklist."""
    checks = []

    # 1. Lookahead bias
    from grubeta.utils import validate_no_lookahead
    checks.append(('No lookahead bias', validate_no_lookahead(
        results['beta'].values,
        results['stock_return'].values,
        results['market_return'].values,
        500, verbose=False
    )))

    # 2. Reasonable beta range
    beta_valid = results['beta'].dropna()
    checks.append(('Beta in range [0, 3]',
                   (beta_valid >= 0).all() and (beta_valid <= 3).all()))

    # 3. Sufficient R²
    from sklearn.metrics import r2_score
    mask = ~results['beta'].isna()
    r2 = r2_score(
        results['stock_return'][mask],
        results['beta'][mask] * results['market_return'][mask]
    )
    checks.append(('R² > 0.1', r2 > 0.1))

    # 4. Model stability
    beta_stability = results['beta'].diff().std()
    checks.append(('Beta stability < 0.05', beta_stability < 0.05))

    # 5. Alpha near zero
    alpha_mean = abs(results['alpha'].dropna().mean())
    checks.append(('|Alpha mean| < 0.001', alpha_mean < 0.001))

    # Report
    print("Validation Checklist")
    print("=" * 40)
    all_passed = True
    for name, passed in checks:
        status = "✓" if passed else "✗"
        print(f"{status} {name}")
        all_passed = all_passed and passed

    return all_passed