f5f6005f80
- Implement RustObjective trait for GIL-free parallelization
- Add 5 benchmark functions: Sphere, Rosenbrock, Rastrigin, Ackley, Griewank
* Each implements RustObjective with evaluate(), dimension(), global_optimum()
* Exposed to Python with __call__ method
- Add parallel_differential_evolution_rust() function:
* Uses Rayon for parallel population evaluation
* Works with RustObjective implementations only
* Eliminates Python GIL overhead for 10-100× speedup
* Supports all DE strategies and adaptive parameters
- Create comprehensive examples:
* parallel_de_benchmark.py: Performance benchmarks showing speedup
* polaroid_optimizr_integration.py: 4 workflows combining Polaroid + OptimizR
- Regime detection with HMM
- Strategy parameter optimization
- Portfolio risk analysis
- Pairs trading pipeline
- Module integration:
* Export benchmark functions in Python API
* Export parallel_differential_evolution_rust
* Update __init__.py and core.py with new functions
- Technical implementation:
* RustObjective trait in src/rust_objectives.rs
* Parallel evaluation uses par_iter() from Rayon
* Per-thread RNG seeding for reproducibility
* Maintains same API as standard DE for easy comparison
Part of Priority 2: Enable Rust parallelization (Enhancement Strategy)
Expected speedup: 10-100× on multi-core systems for pure Rust objectives
500 lines
17 KiB
Python
500 lines
17 KiB
Python
"""
|
||
Polaroid + OptimizR Integration Examples
|
||
========================================
|
||
|
||
Demonstrates workflows combining Polaroid's time-series operations with OptimizR's
|
||
optimization and statistical inference capabilities.
|
||
|
||
Workflows:
|
||
1. Regime Detection: Polaroid features → OptimizR HMM
|
||
2. Strategy Optimization: Polaroid backtesting → OptimizR DE
|
||
3. Risk Analysis: Polaroid data processing → OptimizR risk metrics
|
||
4. Pairs Trading: Combined feature engineering and parameter optimization
|
||
|
||
Prerequisites:
|
||
- Polaroid gRPC server running (or data files available)
|
||
- OptimizR installed with time-series helpers
|
||
"""
|
||
|
||
import numpy as np
|
||
import optimizr
|
||
from typing import List, Tuple
|
||
|
||
|
||
def workflow1_regime_detection_with_features():
|
||
"""
|
||
Workflow 1: Regime Detection with Feature Engineering
|
||
|
||
Uses OptimizR's time-series helpers (which could integrate with Polaroid's
|
||
lag/diff/pct_change operations) to prepare features for HMM regime detection.
|
||
"""
|
||
print("\n" + "=" * 70)
|
||
print("Workflow 1: Regime Detection with Feature Engineering")
|
||
print("=" * 70)
|
||
|
||
# Simulate price data (in production, this comes from Polaroid)
|
||
np.random.seed(42)
|
||
|
||
# Generate regime-switching prices
|
||
prices = [100.0]
|
||
regime = 0 # 0=bull, 1=bear, 2=sideways
|
||
for _ in range(200):
|
||
if np.random.random() < 0.05: # 5% chance of regime switch
|
||
regime = (regime + 1) % 3
|
||
|
||
if regime == 0: # Bull
|
||
ret = np.random.normal(0.001, 0.015)
|
||
elif regime == 1: # Bear
|
||
ret = np.random.normal(-0.001, 0.02)
|
||
else: # Sideways
|
||
ret = np.random.normal(0, 0.01)
|
||
|
||
prices.append(prices[-1] * (1 + ret))
|
||
|
||
# Step 1: Feature engineering with OptimizR helpers
|
||
print("\n1. Feature Engineering:")
|
||
features = optimizr.prepare_for_hmm_py(prices, lag_periods=[1, 2, 3])
|
||
print(f" Created feature matrix: {len(features)} rows × {len(features[0])} columns")
|
||
print(" Features: returns, log_returns, volatility, lag1, lag2, lag3")
|
||
|
||
# Step 2: Train HMM for regime detection
|
||
print("\n2. Training HMM (3 regimes):")
|
||
|
||
# Extract returns for HMM (first column of feature matrix)
|
||
returns = [row[0] for row in features]
|
||
|
||
# Initialize and train HMM
|
||
hmm = optimizr.HMM(n_states=3)
|
||
hmm.fit(returns, n_iterations=50, tolerance=1e-4)
|
||
|
||
print(f" Training complete after {50} iterations")
|
||
print(f" Log-likelihood: {hmm.log_likelihood(returns):.2f}")
|
||
|
||
# Step 3: Predict regimes
|
||
print("\n3. Regime Prediction:")
|
||
states = hmm.predict(returns)
|
||
|
||
# Analyze regime statistics
|
||
unique_states, counts = np.unique(states, return_counts=True)
|
||
print(f" Detected {len(unique_states)} regimes:")
|
||
for state, count in zip(unique_states, counts):
|
||
pct = count / len(states) * 100
|
||
print(f" - Regime {state}: {count} periods ({pct:.1f}%)")
|
||
|
||
# Step 4: Regime characteristics
|
||
print("\n4. Regime Characteristics:")
|
||
for state in unique_states:
|
||
regime_returns = [r for r, s in zip(returns, states) if s == state]
|
||
mean, std, skew, kurt, sharpe = optimizr.return_statistics_py(regime_returns)
|
||
print(f" Regime {state}:")
|
||
print(f" Mean return: {mean*100:.3f}% (annualized: {mean*252*100:.1f}%)")
|
||
print(f" Volatility: {std*100:.3f}% (annualized: {std*np.sqrt(252)*100:.1f}%)")
|
||
print(f" Sharpe: {sharpe:.2f}")
|
||
|
||
print("\n✅ Workflow 1 complete! Use regimes for regime-switching strategies.")
|
||
return states, returns
|
||
|
||
|
||
def workflow2_strategy_optimization():
|
||
"""
|
||
Workflow 2: Strategy Parameter Optimization
|
||
|
||
Uses Differential Evolution to optimize trading strategy parameters,
|
||
with Polaroid handling data operations and OptimizR handling optimization.
|
||
"""
|
||
print("\n" + "=" * 70)
|
||
print("Workflow 2: Moving Average Crossover Strategy Optimization")
|
||
print("=" * 70)
|
||
|
||
# Simulate OHLC data
|
||
np.random.seed(42)
|
||
n_days = 500
|
||
prices = [100.0]
|
||
for _ in range(n_days - 1):
|
||
ret = np.random.normal(0.0005, 0.02)
|
||
prices.append(prices[-1] * (1 + ret))
|
||
|
||
prices = np.array(prices)
|
||
|
||
def moving_average_strategy(params: List[float], prices: np.ndarray) -> float:
|
||
"""
|
||
Simulate MA crossover strategy.
|
||
params = [short_window, long_window, stop_loss]
|
||
Returns: negative Sharpe ratio (for minimization)
|
||
"""
|
||
short_win = int(params[0])
|
||
long_win = int(params[1])
|
||
stop_loss = params[2]
|
||
|
||
# Calculate moving averages
|
||
short_ma = np.convolve(prices, np.ones(short_win)/short_win, mode='valid')
|
||
long_ma = np.convolve(prices, np.ones(long_win)/long_win, mode='valid')
|
||
|
||
# Align arrays
|
||
n = min(len(short_ma), len(long_ma))
|
||
short_ma = short_ma[-n:]
|
||
long_ma = long_ma[-n:]
|
||
aligned_prices = prices[-n:]
|
||
|
||
# Generate signals
|
||
position = 0
|
||
returns = []
|
||
entry_price = 0
|
||
|
||
for i in range(1, n):
|
||
if short_ma[i] > long_ma[i] and short_ma[i-1] <= long_ma[i-1]:
|
||
# Buy signal
|
||
position = 1
|
||
entry_price = aligned_prices[i]
|
||
elif short_ma[i] < long_ma[i] and short_ma[i-1] >= long_ma[i-1]:
|
||
# Sell signal
|
||
position = 0
|
||
|
||
# Stop loss
|
||
if position == 1 and entry_price > 0:
|
||
drawdown = (aligned_prices[i] - entry_price) / entry_price
|
||
if drawdown < -stop_loss:
|
||
position = 0
|
||
|
||
# Calculate returns
|
||
if position == 1:
|
||
ret = (aligned_prices[i] - aligned_prices[i-1]) / aligned_prices[i-1]
|
||
returns.append(ret)
|
||
else:
|
||
returns.append(0)
|
||
|
||
if len(returns) < 10:
|
||
return 999.0 # Penalty for invalid parameters
|
||
|
||
# Calculate Sharpe ratio
|
||
mean_ret = np.mean(returns)
|
||
std_ret = np.std(returns)
|
||
if std_ret == 0:
|
||
return 999.0
|
||
|
||
sharpe = mean_ret / std_ret * np.sqrt(252)
|
||
return -sharpe # Negative for minimization
|
||
|
||
print("\n1. Setting up optimization:")
|
||
print(" Parameters: [short_window, long_window, stop_loss]")
|
||
print(" Bounds: short=[5, 50], long=[20, 200], stop_loss=[0.02, 0.15]")
|
||
|
||
# Define objective function for OptimizR
|
||
def objective(x: List[float]) -> float:
|
||
return moving_average_strategy(x, prices)
|
||
|
||
# Optimize with Differential Evolution
|
||
print("\n2. Running Differential Evolution:")
|
||
result = optimizr.differential_evolution(
|
||
objective,
|
||
bounds=[(5, 50), (20, 200), (0.02, 0.15)],
|
||
strategy="best1",
|
||
max_iterations=50,
|
||
population_size=20,
|
||
convergence_threshold=1e-6
|
||
)
|
||
|
||
print(f" Optimization complete!")
|
||
print(f" Best parameters:")
|
||
print(f" Short window: {int(result['x'][0])} days")
|
||
print(f" Long window: {int(result['x'][1])} days")
|
||
print(f" Stop loss: {result['x'][2]*100:.1f}%")
|
||
print(f" Best Sharpe ratio: {-result['fun']:.3f}")
|
||
print(f" Iterations: {result['nit']}")
|
||
|
||
print("\n✅ Workflow 2 complete! Optimal strategy parameters found.")
|
||
return result
|
||
|
||
|
||
def workflow3_risk_analysis():
|
||
"""
|
||
Workflow 3: Comprehensive Risk Analysis
|
||
|
||
Combines Polaroid's data processing with OptimizR's risk metrics
|
||
for portfolio risk assessment.
|
||
"""
|
||
print("\n" + "=" * 70)
|
||
print("Workflow 3: Portfolio Risk Analysis")
|
||
print("=" * 70)
|
||
|
||
# Simulate multi-asset portfolio returns
|
||
np.random.seed(42)
|
||
n_days = 252
|
||
n_assets = 3
|
||
|
||
print("\n1. Simulating 3-asset portfolio (1 year daily data):")
|
||
|
||
# Generate correlated returns
|
||
corr_matrix = np.array([
|
||
[1.0, 0.6, 0.3],
|
||
[0.6, 1.0, 0.4],
|
||
[0.3, 0.4, 1.0]
|
||
])
|
||
|
||
# Cholesky decomposition for correlation
|
||
L = np.linalg.cholesky(corr_matrix)
|
||
uncorrelated = np.random.randn(n_days, n_assets) * 0.015
|
||
returns = uncorrelated @ L.T
|
||
|
||
# Add drift
|
||
returns[:, 0] += 0.0008 # Asset 1: 20% annual
|
||
returns[:, 1] += 0.0004 # Asset 2: 10% annual
|
||
returns[:, 2] += 0.0006 # Asset 3: 15% annual
|
||
|
||
print(f" Asset 1: Expected 20% annual return")
|
||
print(f" Asset 2: Expected 10% annual return")
|
||
print(f" Asset 3: Expected 15% annual return")
|
||
|
||
# Step 2: Individual asset statistics
|
||
print("\n2. Individual Asset Analysis:")
|
||
for i in range(n_assets):
|
||
mean, std, skew, kurt, sharpe = optimizr.return_statistics_py(
|
||
returns[:, i].tolist()
|
||
)
|
||
print(f"\n Asset {i+1}:")
|
||
print(f" Return (annual): {mean*252*100:.1f}%")
|
||
print(f" Volatility (annual): {std*np.sqrt(252)*100:.1f}%")
|
||
print(f" Skewness: {skew:.3f}")
|
||
print(f" Kurtosis: {kurt:.3f}")
|
||
print(f" Sharpe ratio: {sharpe:.3f}")
|
||
|
||
# Step 3: Mean-reversion analysis
|
||
print("\n3. Mean-Reversion Analysis:")
|
||
prices = [np.cumprod(1 + returns[:, i]) * 100 for i in range(n_assets)]
|
||
|
||
for i in range(n_assets):
|
||
hurst = optimizr.rolling_hurst_exponent_py(
|
||
returns[:, i].tolist(),
|
||
window_size=60
|
||
)
|
||
avg_hurst = np.mean(hurst)
|
||
|
||
half_life = optimizr.rolling_half_life_py(
|
||
prices[i].tolist(),
|
||
window_size=60
|
||
)
|
||
# Filter out infinities
|
||
finite_hl = [hl for hl in half_life if np.isfinite(hl)]
|
||
avg_hl = np.mean(finite_hl) if finite_hl else float('inf')
|
||
|
||
print(f"\n Asset {i+1}:")
|
||
print(f" Hurst exponent: {avg_hurst:.3f}", end="")
|
||
if avg_hurst < 0.45:
|
||
print(" (mean-reverting)")
|
||
elif avg_hurst > 0.55:
|
||
print(" (trending)")
|
||
else:
|
||
print(" (random walk)")
|
||
|
||
if np.isfinite(avg_hl):
|
||
print(f" Half-life: {avg_hl:.1f} days")
|
||
|
||
# Step 4: Correlation analysis
|
||
print("\n4. Correlation Matrix (rolling 60-day):")
|
||
for i in range(n_assets):
|
||
for j in range(i+1, n_assets):
|
||
corr = optimizr.rolling_correlation_py(
|
||
returns[:, i].tolist(),
|
||
returns[:, j].tolist(),
|
||
window_size=60
|
||
)
|
||
avg_corr = np.mean(corr)
|
||
print(f" Asset {i+1} ↔ Asset {j+1}: {avg_corr:.3f}")
|
||
|
||
# Step 5: Portfolio optimization weights (equal risk contribution)
|
||
print("\n5. Portfolio Construction:")
|
||
weights = [1/n_assets] * n_assets
|
||
portfolio_returns = returns @ np.array(weights)
|
||
|
||
mean, std, skew, kurt, sharpe = optimizr.return_statistics_py(
|
||
portfolio_returns.tolist()
|
||
)
|
||
|
||
print(f" Equal-weight portfolio:")
|
||
print(f" Return (annual): {mean*252*100:.1f}%")
|
||
print(f" Volatility (annual): {std*np.sqrt(252)*100:.1f}%")
|
||
print(f" Sharpe ratio: {sharpe:.3f}")
|
||
|
||
print("\n✅ Workflow 3 complete! Comprehensive risk analysis finished.")
|
||
|
||
|
||
def workflow4_pairs_trading_pipeline():
|
||
"""
|
||
Workflow 4: Complete Pairs Trading Pipeline
|
||
|
||
End-to-end pairs trading: cointegration check, parameter optimization,
|
||
and risk management using OptimizR's integrated tools.
|
||
"""
|
||
print("\n" + "=" * 70)
|
||
print("Workflow 4: Pairs Trading Pipeline")
|
||
print("=" * 70)
|
||
|
||
# Generate cointegrated pair
|
||
np.random.seed(42)
|
||
n_days = 500
|
||
|
||
# Asset 1: Random walk with drift
|
||
returns1 = np.random.normal(0.0003, 0.015, n_days)
|
||
prices1 = 100 * np.cumprod(1 + returns1)
|
||
|
||
# Asset 2: Cointegrated with Asset 1
|
||
spread_noise = np.random.normal(0, 0.01, n_days)
|
||
prices2 = prices1 * 0.9 + np.cumsum(spread_noise)
|
||
|
||
# Calculate spread
|
||
spread = prices1 - prices2
|
||
|
||
print("\n1. Cointegration Analysis:")
|
||
|
||
# Check mean-reversion
|
||
spread_returns = np.diff(spread) / spread[:-1]
|
||
hurst = optimizr.rolling_hurst_exponent_py(
|
||
spread_returns.tolist(),
|
||
window_size=60
|
||
)
|
||
avg_hurst = np.mean(hurst)
|
||
print(f" Hurst exponent: {avg_hurst:.3f}", end="")
|
||
|
||
if avg_hurst < 0.5:
|
||
print(" ✅ Mean-reverting (good for pairs trading)")
|
||
else:
|
||
print(" ⚠️ Not clearly mean-reverting")
|
||
|
||
# Estimate half-life
|
||
half_lives = optimizr.rolling_half_life_py(
|
||
spread.tolist(),
|
||
window_size=60
|
||
)
|
||
finite_hl = [hl for hl in half_lives if np.isfinite(hl) and hl > 0]
|
||
avg_hl = np.mean(finite_hl) if finite_hl else float('inf')
|
||
|
||
if np.isfinite(avg_hl):
|
||
print(f" Half-life: {avg_hl:.1f} days (reversion speed)")
|
||
|
||
# Correlation check
|
||
returns2 = np.diff(prices2) / prices2[:-1]
|
||
corr = optimizr.rolling_correlation_py(
|
||
returns1[1:].tolist(),
|
||
returns2.tolist(),
|
||
window_size=60
|
||
)
|
||
avg_corr = np.mean(corr)
|
||
print(f" Correlation: {avg_corr:.3f}", end="")
|
||
|
||
if avg_corr > 0.7:
|
||
print(" ✅ Strong correlation")
|
||
elif avg_corr > 0.5:
|
||
print(" ⚠️ Moderate correlation")
|
||
else:
|
||
print(" ❌ Weak correlation")
|
||
|
||
# Step 2: Optimize strategy parameters
|
||
print("\n2. Strategy Parameter Optimization:")
|
||
|
||
def pairs_strategy(params: List[float]) -> float:
|
||
"""
|
||
Pairs trading with mean-reversion.
|
||
params = [entry_z, exit_z, stop_loss]
|
||
Returns: negative Sharpe (for minimization)
|
||
"""
|
||
entry_z = params[0]
|
||
exit_z = params[1]
|
||
stop_loss = params[2]
|
||
|
||
# Calculate z-score
|
||
window = 20
|
||
spread_ma = np.convolve(spread, np.ones(window)/window, mode='valid')
|
||
spread_std = np.array([
|
||
np.std(spread[i:i+window])
|
||
for i in range(len(spread) - window + 1)
|
||
])
|
||
|
||
aligned_spread = spread[window-1:]
|
||
z_score = (aligned_spread - spread_ma) / (spread_std + 1e-6)
|
||
|
||
# Trading logic
|
||
position = 0 # 1 = long spread, -1 = short spread
|
||
returns = []
|
||
entry_value = 0
|
||
|
||
for i in range(1, len(z_score)):
|
||
# Entry signals
|
||
if z_score[i] > entry_z and position == 0:
|
||
position = -1 # Short spread (short asset1, long asset2)
|
||
entry_value = aligned_spread[i]
|
||
elif z_score[i] < -entry_z and position == 0:
|
||
position = 1 # Long spread (long asset1, short asset2)
|
||
entry_value = aligned_spread[i]
|
||
|
||
# Exit signals
|
||
if abs(z_score[i]) < exit_z and position != 0:
|
||
position = 0
|
||
|
||
# Stop loss
|
||
if position != 0 and entry_value != 0:
|
||
pnl = position * (aligned_spread[i] - entry_value) / abs(entry_value)
|
||
if pnl < -stop_loss:
|
||
position = 0
|
||
|
||
# Calculate returns
|
||
if position != 0:
|
||
spread_ret = (aligned_spread[i] - aligned_spread[i-1]) / aligned_spread[i-1]
|
||
returns.append(position * spread_ret)
|
||
else:
|
||
returns.append(0)
|
||
|
||
if len(returns) < 10:
|
||
return 999.0
|
||
|
||
mean_ret = np.mean(returns)
|
||
std_ret = np.std(returns)
|
||
if std_ret == 0:
|
||
return 999.0
|
||
|
||
sharpe = mean_ret / std_ret * np.sqrt(252)
|
||
return -sharpe
|
||
|
||
print(" Optimizing: [entry_z, exit_z, stop_loss]")
|
||
|
||
result = optimizr.differential_evolution(
|
||
pairs_strategy,
|
||
bounds=[(1.5, 3.0), (0.1, 1.0), (0.02, 0.1)],
|
||
strategy="best1",
|
||
max_iterations=30,
|
||
population_size=15
|
||
)
|
||
|
||
print(f" Optimal parameters:")
|
||
print(f" Entry z-score: {result['x'][0]:.2f}")
|
||
print(f" Exit z-score: {result['x'][1]:.2f}")
|
||
print(f" Stop loss: {result['x'][2]*100:.1f}%")
|
||
print(f" Expected Sharpe: {-result['fun']:.3f}")
|
||
|
||
print("\n✅ Workflow 4 complete! Pairs trading strategy optimized.")
|
||
return result
|
||
|
||
|
||
if __name__ == "__main__":
|
||
print("=" * 70)
|
||
print("Polaroid + OptimizR Integration Examples")
|
||
print("=" * 70)
|
||
print("\nDemonstrating 4 integrated workflows combining time-series")
|
||
print("operations with optimization and statistical inference.")
|
||
|
||
# Run all workflows
|
||
workflow1_regime_detection_with_features()
|
||
workflow2_strategy_optimization()
|
||
workflow3_risk_analysis()
|
||
workflow4_pairs_trading_pipeline()
|
||
|
||
print("\n" + "=" * 70)
|
||
print("✅ All integration workflows completed successfully!")
|
||
print("=" * 70)
|
||
print("\nThese examples show how to combine:")
|
||
print(" • Polaroid's time-series operations (lag, diff, pct_change)")
|
||
print(" • OptimizR's optimization (DE, grid search)")
|
||
print(" • OptimizR's inference (HMM, MCMC)")
|
||
print(" • OptimizR's time-series helpers (Hurst, half-life, etc.)")
|
||
print("\nFor production use, connect to Polaroid gRPC for data processing.")
|
||
print("=" * 70)
|