Spaces:
Sleeping
Sleeping
Delete src
Browse files- src/__init__.py +0 -7
- src/__pycache__/__init__.cpython-311.pyc +0 -0
- src/__pycache__/bayesian_engine.cpython-311.pyc +0 -0
- src/__pycache__/config.cpython-311.pyc +0 -0
- src/__pycache__/data_fetcher.cpython-311.pyc +0 -0
- src/__pycache__/ml_models.cpython-311.pyc +0 -0
- src/__pycache__/monte_carlo.cpython-311.pyc +0 -0
- src/__pycache__/pattern_recognition.cpython-311.pyc +0 -0
- src/__pycache__/pdf_report.cpython-311.pyc +0 -0
- src/__pycache__/spectral_analyzer.cpython-311.pyc +0 -0
- src/__pycache__/visualization.cpython-311.pyc +0 -0
- src/bayesian_engine.py +0 -293
- src/config.py +0 -101
- src/data_fetcher.py +0 -192
- src/ml_models.py +0 -240
- src/monte_carlo.py +0 -339
- src/pattern_recognition.py +0 -395
- src/pdf_report.py +0 -238
- src/spectral_analyzer.py +0 -262
- src/visualization.py +0 -508
src/__init__.py
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Professional Quantitative Finance Analysis Platform
|
| 3 |
-
Advanced spectral analysis, Bayesian inference, and ML-based momentum prediction
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
__version__ = "2.0.0"
|
| 7 |
-
__author__ = "Quant Analysis Team"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/__pycache__/__init__.cpython-311.pyc
DELETED
|
Binary file (374 Bytes)
|
|
|
src/__pycache__/bayesian_engine.cpython-311.pyc
DELETED
|
Binary file (12.8 kB)
|
|
|
src/__pycache__/config.cpython-311.pyc
DELETED
|
Binary file (3.63 kB)
|
|
|
src/__pycache__/data_fetcher.cpython-311.pyc
DELETED
|
Binary file (12.2 kB)
|
|
|
src/__pycache__/ml_models.cpython-311.pyc
DELETED
|
Binary file (11.9 kB)
|
|
|
src/__pycache__/monte_carlo.cpython-311.pyc
DELETED
|
Binary file (13.9 kB)
|
|
|
src/__pycache__/pattern_recognition.cpython-311.pyc
DELETED
|
Binary file (20.2 kB)
|
|
|
src/__pycache__/pdf_report.cpython-311.pyc
DELETED
|
Binary file (16.8 kB)
|
|
|
src/__pycache__/spectral_analyzer.cpython-311.pyc
DELETED
|
Binary file (12 kB)
|
|
|
src/__pycache__/visualization.cpython-311.pyc
DELETED
|
Binary file (8.73 kB)
|
|
|
src/bayesian_engine.py
DELETED
|
@@ -1,293 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Bayesian inference engine for probabilistic market analysis
|
| 3 |
-
"""
|
| 4 |
-
import numpy as np
|
| 5 |
-
import pandas as pd
|
| 6 |
-
import pymc as pm
|
| 7 |
-
import arviz as az
|
| 8 |
-
from typing import Dict, Tuple, Optional
|
| 9 |
-
from src.config import config
|
| 10 |
-
|
| 11 |
-
class BayesianAnalyzer:
|
| 12 |
-
"""
|
| 13 |
-
Bayesian inference for momentum prediction and parameter optimization
|
| 14 |
-
Uses PyMC for probabilistic modeling
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
def __init__(self):
|
| 18 |
-
self.model = None
|
| 19 |
-
self.trace = None
|
| 20 |
-
self.posterior_predictive = None
|
| 21 |
-
|
| 22 |
-
def build_momentum_model(
|
| 23 |
-
self,
|
| 24 |
-
features: np.ndarray,
|
| 25 |
-
target: np.ndarray,
|
| 26 |
-
prior_means: Optional[Dict] = None
|
| 27 |
-
) -> Tuple[pm.Model, az.InferenceData]:
|
| 28 |
-
"""
|
| 29 |
-
Build Bayesian linear regression model for momentum prediction
|
| 30 |
-
|
| 31 |
-
Args:
|
| 32 |
-
features: Feature matrix (n_samples, n_features)
|
| 33 |
-
target: Target momentum values
|
| 34 |
-
prior_means: Optional prior means for coefficients
|
| 35 |
-
|
| 36 |
-
Returns:
|
| 37 |
-
Tuple of (model, trace)
|
| 38 |
-
"""
|
| 39 |
-
n_features = features.shape[1]
|
| 40 |
-
|
| 41 |
-
with pm.Model() as self.model:
|
| 42 |
-
# Priors for regression coefficients
|
| 43 |
-
if prior_means is not None:
|
| 44 |
-
alpha = pm.Normal('alpha', mu=prior_means.get('alpha', 0), sigma=10)
|
| 45 |
-
beta = pm.Normal('beta', mu=prior_means.get('beta', np.zeros(n_features)),
|
| 46 |
-
sigma=10, shape=n_features)
|
| 47 |
-
else:
|
| 48 |
-
alpha = pm.Normal('alpha', mu=0, sigma=10)
|
| 49 |
-
beta = pm.Normal('beta', mu=0, sigma=10, shape=n_features)
|
| 50 |
-
|
| 51 |
-
# Prior for noise
|
| 52 |
-
sigma = pm.HalfNormal('sigma', sigma=1)
|
| 53 |
-
|
| 54 |
-
# Expected value (linear model)
|
| 55 |
-
mu = alpha + pm.math.dot(features, beta)
|
| 56 |
-
|
| 57 |
-
# Likelihood
|
| 58 |
-
y_obs = pm.Normal('y_obs', mu=mu, sigma=sigma, observed=target)
|
| 59 |
-
|
| 60 |
-
# Sampling
|
| 61 |
-
self.trace = pm.sample(
|
| 62 |
-
draws=config.BAYESIAN_DRAWS,
|
| 63 |
-
tune=config.BAYESIAN_TUNE,
|
| 64 |
-
chains=config.BAYESIAN_CHAINS,
|
| 65 |
-
return_inferencedata=True,
|
| 66 |
-
progressbar=True
|
| 67 |
-
)
|
| 68 |
-
|
| 69 |
-
# Posterior predictive sampling
|
| 70 |
-
pm.sample_posterior_predictive(
|
| 71 |
-
self.trace,
|
| 72 |
-
extend_inferencedata=True,
|
| 73 |
-
progressbar=True
|
| 74 |
-
)
|
| 75 |
-
|
| 76 |
-
return self.model, self.trace
|
| 77 |
-
|
| 78 |
-
def predict(self, features: np.ndarray) -> Dict:
|
| 79 |
-
"""
|
| 80 |
-
Make predictions using trained Bayesian model
|
| 81 |
-
|
| 82 |
-
Args:
|
| 83 |
-
features: Feature matrix for prediction
|
| 84 |
-
|
| 85 |
-
Returns:
|
| 86 |
-
Dictionary with predictions and uncertainty estimates
|
| 87 |
-
"""
|
| 88 |
-
if self.model is None or self.trace is None:
|
| 89 |
-
raise ValueError("Model not trained yet. Call build_momentum_model first.")
|
| 90 |
-
|
| 91 |
-
with self.model:
|
| 92 |
-
# Get posterior samples
|
| 93 |
-
alpha_samples = self.trace.posterior['alpha'].values.flatten()
|
| 94 |
-
beta_samples = self.trace.posterior['beta'].values.reshape(-1, features.shape[1])
|
| 95 |
-
|
| 96 |
-
# Calculate predictions for each posterior sample
|
| 97 |
-
predictions = []
|
| 98 |
-
for i in range(len(alpha_samples)):
|
| 99 |
-
pred = alpha_samples[i] + features @ beta_samples[i]
|
| 100 |
-
predictions.append(pred)
|
| 101 |
-
|
| 102 |
-
predictions = np.array(predictions)
|
| 103 |
-
|
| 104 |
-
# Calculate statistics
|
| 105 |
-
mean_pred = np.mean(predictions, axis=0)
|
| 106 |
-
std_pred = np.std(predictions, axis=0)
|
| 107 |
-
|
| 108 |
-
# Credible intervals (95%)
|
| 109 |
-
lower_ci = np.percentile(predictions, 2.5, axis=0)
|
| 110 |
-
upper_ci = np.percentile(predictions, 97.5, axis=0)
|
| 111 |
-
|
| 112 |
-
return {
|
| 113 |
-
'mean': mean_pred,
|
| 114 |
-
'std': std_pred,
|
| 115 |
-
'lower_95': lower_ci,
|
| 116 |
-
'upper_95': upper_ci,
|
| 117 |
-
'samples': predictions
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
def update_priors(self, new_data_features: np.ndarray, new_data_target: np.ndarray):
|
| 121 |
-
"""
|
| 122 |
-
Update model with new data (online learning)
|
| 123 |
-
Uses previous posterior as new prior
|
| 124 |
-
|
| 125 |
-
Args:
|
| 126 |
-
new_data_features: New feature data
|
| 127 |
-
new_data_target: New target data
|
| 128 |
-
"""
|
| 129 |
-
if self.trace is None:
|
| 130 |
-
# First time - no prior information
|
| 131 |
-
return self.build_momentum_model(new_data_features, new_data_target)
|
| 132 |
-
|
| 133 |
-
# Extract posterior means as new priors
|
| 134 |
-
alpha_mean = float(self.trace.posterior['alpha'].mean())
|
| 135 |
-
beta_mean = self.trace.posterior['beta'].mean(dim=['chain', 'draw']).values
|
| 136 |
-
|
| 137 |
-
prior_means = {
|
| 138 |
-
'alpha': alpha_mean,
|
| 139 |
-
'beta': beta_mean
|
| 140 |
-
}
|
| 141 |
-
|
| 142 |
-
# Build new model with updated priors
|
| 143 |
-
return self.build_momentum_model(new_data_features, new_data_target, prior_means)
|
| 144 |
-
|
| 145 |
-
def estimate_regime_probabilities(
|
| 146 |
-
self,
|
| 147 |
-
volatility: float,
|
| 148 |
-
adx: float,
|
| 149 |
-
returns: np.ndarray
|
| 150 |
-
) -> Dict[str, float]:
|
| 151 |
-
"""
|
| 152 |
-
Estimate probabilities of different market regimes using Bayesian approach
|
| 153 |
-
|
| 154 |
-
Args:
|
| 155 |
-
volatility: Current volatility measure
|
| 156 |
-
adx: Current ADX value
|
| 157 |
-
returns: Recent returns series
|
| 158 |
-
|
| 159 |
-
Returns:
|
| 160 |
-
Dictionary of regime probabilities
|
| 161 |
-
"""
|
| 162 |
-
with pm.Model() as regime_model:
|
| 163 |
-
# Define regime categories
|
| 164 |
-
# 0: Range-bound, 1: Trending, 2: High volatility
|
| 165 |
-
|
| 166 |
-
# Prior probabilities (equal initially)
|
| 167 |
-
p = pm.Dirichlet('p', a=np.ones(3))
|
| 168 |
-
|
| 169 |
-
# Regime indicators
|
| 170 |
-
regime = pm.Categorical('regime', p=p, shape=len(returns))
|
| 171 |
-
|
| 172 |
-
# Likelihood parameters for each regime
|
| 173 |
-
# Range-bound: low volatility, low ADX
|
| 174 |
-
mu_range = pm.Normal('mu_range', mu=0, sigma=0.5)
|
| 175 |
-
sigma_range = pm.HalfNormal('sigma_range', sigma=0.3)
|
| 176 |
-
|
| 177 |
-
# Trending: moderate volatility, high ADX
|
| 178 |
-
mu_trend = pm.Normal('mu_trend', mu=0, sigma=1.0)
|
| 179 |
-
sigma_trend = pm.HalfNormal('sigma_trend', sigma=0.5)
|
| 180 |
-
|
| 181 |
-
# High vol: high volatility, variable ADX
|
| 182 |
-
mu_highvol = pm.Normal('mu_highvol', mu=0, sigma=2.0)
|
| 183 |
-
sigma_highvol = pm.HalfNormal('sigma_highvol', sigma=1.0)
|
| 184 |
-
|
| 185 |
-
# Mixture likelihood
|
| 186 |
-
mus = pm.math.stack([mu_range, mu_trend, mu_highvol])
|
| 187 |
-
sigmas = pm.math.stack([sigma_range, sigma_trend, sigma_highvol])
|
| 188 |
-
|
| 189 |
-
# Observed returns
|
| 190 |
-
y = pm.Normal('y', mu=mus[regime], sigma=sigmas[regime], observed=returns)
|
| 191 |
-
|
| 192 |
-
# Sample
|
| 193 |
-
trace = pm.sample(
|
| 194 |
-
draws=2000,
|
| 195 |
-
tune=1000,
|
| 196 |
-
chains=2,
|
| 197 |
-
return_inferencedata=True,
|
| 198 |
-
progressbar=False
|
| 199 |
-
)
|
| 200 |
-
|
| 201 |
-
# Extract regime probabilities
|
| 202 |
-
regime_probs = trace.posterior['p'].mean(dim=['chain', 'draw']).values
|
| 203 |
-
|
| 204 |
-
return {
|
| 205 |
-
'range_bound': float(regime_probs[0]),
|
| 206 |
-
'trending': float(regime_probs[1]),
|
| 207 |
-
'high_volatility': float(regime_probs[2])
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
def optimize_parameters_bayesian(
|
| 211 |
-
self,
|
| 212 |
-
historical_signals: pd.DataFrame,
|
| 213 |
-
forward_returns: pd.Series
|
| 214 |
-
) -> Dict:
|
| 215 |
-
"""
|
| 216 |
-
Optimize signal parameters using Bayesian optimization
|
| 217 |
-
|
| 218 |
-
Args:
|
| 219 |
-
historical_signals: DataFrame with signal features
|
| 220 |
-
forward_returns: Actual forward returns
|
| 221 |
-
|
| 222 |
-
Returns:
|
| 223 |
-
Optimized parameter distributions
|
| 224 |
-
"""
|
| 225 |
-
with pm.Model() as param_model:
|
| 226 |
-
# Parameters to optimize
|
| 227 |
-
momentum_threshold = pm.Normal('momentum_threshold', mu=-0.3, sigma=0.2)
|
| 228 |
-
price_threshold = pm.Normal('price_threshold', mu=-0.05, sigma=0.05)
|
| 229 |
-
cutoff_freq = pm.Beta('cutoff_freq', alpha=2, beta=5)
|
| 230 |
-
|
| 231 |
-
# Generate signals based on parameters
|
| 232 |
-
# (simplified for demonstration)
|
| 233 |
-
signal_strength = (
|
| 234 |
-
historical_signals['momentum'] - momentum_threshold
|
| 235 |
-
) * (
|
| 236 |
-
historical_signals['price_change'] - price_threshold
|
| 237 |
-
)
|
| 238 |
-
|
| 239 |
-
# Likelihood: forward returns should be positive when signal is strong
|
| 240 |
-
pm.Normal(
|
| 241 |
-
'forward_returns',
|
| 242 |
-
mu=signal_strength * 0.05, # Expected 5% return on strong signal
|
| 243 |
-
sigma=0.1,
|
| 244 |
-
observed=forward_returns
|
| 245 |
-
)
|
| 246 |
-
|
| 247 |
-
# Sample
|
| 248 |
-
trace = pm.sample(
|
| 249 |
-
draws=3000,
|
| 250 |
-
tune=1500,
|
| 251 |
-
chains=4,
|
| 252 |
-
return_inferencedata=True,
|
| 253 |
-
progressbar=True
|
| 254 |
-
)
|
| 255 |
-
|
| 256 |
-
# Extract optimal parameters
|
| 257 |
-
optimal_params = {
|
| 258 |
-
'momentum_threshold': {
|
| 259 |
-
'mean': float(trace.posterior['momentum_threshold'].mean()),
|
| 260 |
-
'std': float(trace.posterior['momentum_threshold'].std()),
|
| 261 |
-
'hdi_95': tuple(az.hdi(trace, var_names=['momentum_threshold'], hdi_prob=0.95)['momentum_threshold'].values)
|
| 262 |
-
},
|
| 263 |
-
'price_threshold': {
|
| 264 |
-
'mean': float(trace.posterior['price_threshold'].mean()),
|
| 265 |
-
'std': float(trace.posterior['price_threshold'].std()),
|
| 266 |
-
'hdi_95': tuple(az.hdi(trace, var_names=['price_threshold'], hdi_prob=0.95)['price_threshold'].values)
|
| 267 |
-
},
|
| 268 |
-
'cutoff_freq': {
|
| 269 |
-
'mean': float(trace.posterior['cutoff_freq'].mean()),
|
| 270 |
-
'std': float(trace.posterior['cutoff_freq'].std()),
|
| 271 |
-
'hdi_95': tuple(az.hdi(trace, var_names=['cutoff_freq'], hdi_prob=0.95)['cutoff_freq'].values)
|
| 272 |
-
}
|
| 273 |
-
}
|
| 274 |
-
|
| 275 |
-
return optimal_params
|
| 276 |
-
|
| 277 |
-
def get_diagnostics(self) -> Dict:
|
| 278 |
-
"""
|
| 279 |
-
Get MCMC diagnostics
|
| 280 |
-
|
| 281 |
-
Returns:
|
| 282 |
-
Dictionary with diagnostic metrics
|
| 283 |
-
"""
|
| 284 |
-
if self.trace is None:
|
| 285 |
-
return {}
|
| 286 |
-
|
| 287 |
-
diagnostics = {
|
| 288 |
-
'r_hat': az.rhat(self.trace).to_dict(),
|
| 289 |
-
'ess': az.ess(self.trace).to_dict(),
|
| 290 |
-
'divergences': self.trace.sample_stats.diverging.sum().item()
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
return diagnostics
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/config.py
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Configuration management for the quantitative finance platform
|
| 3 |
-
"""
|
| 4 |
-
import os
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
from typing import Dict, List, Tuple
|
| 7 |
-
|
| 8 |
-
@dataclass
|
| 9 |
-
class FrequencyBand:
|
| 10 |
-
"""Frequency band configuration"""
|
| 11 |
-
name: str
|
| 12 |
-
min_period: float # days
|
| 13 |
-
max_period: float # days
|
| 14 |
-
cutoff_range: Tuple[float, float]
|
| 15 |
-
filter_order: int
|
| 16 |
-
|
| 17 |
-
@dataclass
|
| 18 |
-
class AnalysisConfig:
|
| 19 |
-
"""Main analysis configuration"""
|
| 20 |
-
|
| 21 |
-
# Frequency bands (Low, Mid, High)
|
| 22 |
-
FREQUENCY_BANDS: Dict[str, FrequencyBand] = None
|
| 23 |
-
|
| 24 |
-
# Bayesian configuration
|
| 25 |
-
BAYESIAN_DRAWS: int = 5000
|
| 26 |
-
BAYESIAN_TUNE: int = 2000
|
| 27 |
-
BAYESIAN_CHAINS: int = 4
|
| 28 |
-
|
| 29 |
-
# Monte Carlo configuration
|
| 30 |
-
MC_SIMULATIONS: int = 10000
|
| 31 |
-
MC_TIME_HORIZON: int = 30 # days
|
| 32 |
-
|
| 33 |
-
# Self-supervised learning
|
| 34 |
-
ROLLING_WINDOW_SIZE: int = 252 # 1 year
|
| 35 |
-
FORWARD_TEST_SIZE: int = 21 # 1 month
|
| 36 |
-
OPTIMIZATION_ITERATIONS: int = 50
|
| 37 |
-
|
| 38 |
-
# Pattern recognition
|
| 39 |
-
PATTERN_WINDOW_MIN: int = 5
|
| 40 |
-
PATTERN_WINDOW_MAX: int = 60
|
| 41 |
-
PATTERN_SIMILARITY_THRESHOLD: float = 0.75
|
| 42 |
-
|
| 43 |
-
# Machine Learning
|
| 44 |
-
ML_VALIDATION_SPLITS: int = 5
|
| 45 |
-
ML_TEST_SIZE: float = 0.2
|
| 46 |
-
ML_RANDOM_STATE: int = 42
|
| 47 |
-
|
| 48 |
-
# Visualization
|
| 49 |
-
PLOT_DPI: int = 300
|
| 50 |
-
PLOT_WIDTH: int = 1400
|
| 51 |
-
PLOT_HEIGHT: int = 800
|
| 52 |
-
|
| 53 |
-
# PDF Report
|
| 54 |
-
PDF_PAGE_SIZE: str = "A4"
|
| 55 |
-
PDF_FONT_SIZE: int = 10
|
| 56 |
-
PDF_TITLE_SIZE: int = 16
|
| 57 |
-
|
| 58 |
-
def __post_init__(self):
|
| 59 |
-
if self.FREQUENCY_BANDS is None:
|
| 60 |
-
self.FREQUENCY_BANDS = {
|
| 61 |
-
'low': FrequencyBand(
|
| 62 |
-
name='Low Frequency (Long-term)',
|
| 63 |
-
min_period=20.0,
|
| 64 |
-
max_period=252.0,
|
| 65 |
-
cutoff_range=(0.02, 0.05),
|
| 66 |
-
filter_order=6
|
| 67 |
-
),
|
| 68 |
-
'mid': FrequencyBand(
|
| 69 |
-
name='Mid Frequency (Medium-term)',
|
| 70 |
-
min_period=5.0,
|
| 71 |
-
max_period=20.0,
|
| 72 |
-
cutoff_range=(0.05, 0.15),
|
| 73 |
-
filter_order=4
|
| 74 |
-
),
|
| 75 |
-
'high': FrequencyBand(
|
| 76 |
-
name='High Frequency (Short-term)',
|
| 77 |
-
min_period=1.0,
|
| 78 |
-
max_period=5.0,
|
| 79 |
-
cutoff_range=(0.15, 0.4),
|
| 80 |
-
filter_order=3
|
| 81 |
-
)
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
# Global configuration instance
|
| 85 |
-
config = AnalysisConfig()
|
| 86 |
-
|
| 87 |
-
# Time intervals for different analyses
|
| 88 |
-
TIMEFRAMES = {
|
| 89 |
-
'1d': {'name': '1 Day', 'interval': '1d', 'period': '2y'},
|
| 90 |
-
'1h': {'name': '1 Hour', 'interval': '1h', 'period': '60d'},
|
| 91 |
-
'15m': {'name': '15 Minutes', 'interval': '15m', 'period': '7d'},
|
| 92 |
-
'5m': {'name': '5 Minutes', 'interval': '5m', 'period': '5d'},
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
# Market regime thresholds
|
| 96 |
-
REGIME_THRESHOLDS = {
|
| 97 |
-
'trending': 25.0, # ADX threshold
|
| 98 |
-
'range_bound': 20.0,
|
| 99 |
-
'high_volatility': 0.30,
|
| 100 |
-
'low_volatility': 0.15
|
| 101 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/data_fetcher.py
DELETED
|
@@ -1,192 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Data fetching and preprocessing module
|
| 3 |
-
"""
|
| 4 |
-
import yfinance as yf
|
| 5 |
-
import pandas as pd
|
| 6 |
-
import numpy as np
|
| 7 |
-
from typing import Tuple, Optional
|
| 8 |
-
from datetime import datetime, timedelta
|
| 9 |
-
import warnings
|
| 10 |
-
warnings.filterwarnings('ignore')
|
| 11 |
-
|
| 12 |
-
class DataFetcher:
|
| 13 |
-
"""Fetches and preprocesses financial market data"""
|
| 14 |
-
|
| 15 |
-
def __init__(self):
|
| 16 |
-
self.data = None
|
| 17 |
-
self.symbol = None
|
| 18 |
-
self.interval = None
|
| 19 |
-
|
| 20 |
-
def fetch_data(self, symbol: str, interval: str = '1d', period: str = '2y') -> Tuple[bool, str]:
|
| 21 |
-
"""
|
| 22 |
-
Fetch market data from Yahoo Finance
|
| 23 |
-
|
| 24 |
-
Args:
|
| 25 |
-
symbol: Stock ticker symbol
|
| 26 |
-
interval: Data interval (1d, 1h, 15m, 5m)
|
| 27 |
-
period: Historical period to fetch
|
| 28 |
-
|
| 29 |
-
Returns:
|
| 30 |
-
Tuple of (success: bool, message: str)
|
| 31 |
-
"""
|
| 32 |
-
try:
|
| 33 |
-
symbol = symbol.strip().upper()
|
| 34 |
-
self.symbol = symbol
|
| 35 |
-
self.interval = interval
|
| 36 |
-
|
| 37 |
-
ticker = yf.Ticker(symbol)
|
| 38 |
-
self.data = ticker.history(period=period, interval=interval)
|
| 39 |
-
|
| 40 |
-
if len(self.data) < 60:
|
| 41 |
-
return False, f"Insufficient data: Only {len(self.data)} records found for {symbol}"
|
| 42 |
-
|
| 43 |
-
# Add technical indicators
|
| 44 |
-
self._add_technical_indicators()
|
| 45 |
-
|
| 46 |
-
return True, f"{symbol} data successfully fetched: {len(self.data)} records ({interval} interval)"
|
| 47 |
-
|
| 48 |
-
except Exception as e:
|
| 49 |
-
return False, f"Data fetch error: {str(e)}"
|
| 50 |
-
|
| 51 |
-
def _add_technical_indicators(self):
|
| 52 |
-
"""Add common technical indicators to the dataset"""
|
| 53 |
-
if self.data is None or len(self.data) == 0:
|
| 54 |
-
return
|
| 55 |
-
|
| 56 |
-
# Returns
|
| 57 |
-
self.data['Returns'] = self.data['Close'].pct_change()
|
| 58 |
-
|
| 59 |
-
# Log returns
|
| 60 |
-
self.data['LogReturns'] = np.log(self.data['Close'] / self.data['Close'].shift(1))
|
| 61 |
-
|
| 62 |
-
# Simple Moving Averages
|
| 63 |
-
for period in [5, 10, 20, 50, 200]:
|
| 64 |
-
if len(self.data) > period:
|
| 65 |
-
self.data[f'SMA_{period}'] = self.data['Close'].rolling(window=period).mean()
|
| 66 |
-
|
| 67 |
-
# Exponential Moving Averages
|
| 68 |
-
for period in [12, 26]:
|
| 69 |
-
if len(self.data) > period:
|
| 70 |
-
self.data[f'EMA_{period}'] = self.data['Close'].ewm(span=period, adjust=False).mean()
|
| 71 |
-
|
| 72 |
-
# Bollinger Bands
|
| 73 |
-
if len(self.data) > 20:
|
| 74 |
-
sma_20 = self.data['Close'].rolling(window=20).mean()
|
| 75 |
-
std_20 = self.data['Close'].rolling(window=20).std()
|
| 76 |
-
self.data['BB_Upper'] = sma_20 + (std_20 * 2)
|
| 77 |
-
self.data['BB_Lower'] = sma_20 - (std_20 * 2)
|
| 78 |
-
self.data['BB_Width'] = (self.data['BB_Upper'] - self.data['BB_Lower']) / sma_20
|
| 79 |
-
|
| 80 |
-
# RSI (Relative Strength Index)
|
| 81 |
-
if len(self.data) > 14:
|
| 82 |
-
delta = self.data['Close'].diff()
|
| 83 |
-
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
|
| 84 |
-
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
|
| 85 |
-
rs = gain / loss
|
| 86 |
-
self.data['RSI'] = 100 - (100 / (1 + rs))
|
| 87 |
-
|
| 88 |
-
# MACD
|
| 89 |
-
if len(self.data) > 26:
|
| 90 |
-
ema_12 = self.data['Close'].ewm(span=12, adjust=False).mean()
|
| 91 |
-
ema_26 = self.data['Close'].ewm(span=26, adjust=False).mean()
|
| 92 |
-
self.data['MACD'] = ema_12 - ema_26
|
| 93 |
-
self.data['MACD_Signal'] = self.data['MACD'].ewm(span=9, adjust=False).mean()
|
| 94 |
-
self.data['MACD_Hist'] = self.data['MACD'] - self.data['MACD_Signal']
|
| 95 |
-
|
| 96 |
-
# Average True Range (ATR)
|
| 97 |
-
if len(self.data) > 14:
|
| 98 |
-
high_low = self.data['High'] - self.data['Low']
|
| 99 |
-
high_close = np.abs(self.data['High'] - self.data['Close'].shift())
|
| 100 |
-
low_close = np.abs(self.data['Low'] - self.data['Close'].shift())
|
| 101 |
-
ranges = pd.concat([high_low, high_close, low_close], axis=1)
|
| 102 |
-
true_range = ranges.max(axis=1)
|
| 103 |
-
self.data['ATR'] = true_range.rolling(window=14).mean()
|
| 104 |
-
|
| 105 |
-
# Volume indicators
|
| 106 |
-
if 'Volume' in self.data.columns:
|
| 107 |
-
self.data['Volume_SMA'] = self.data['Volume'].rolling(window=20).mean()
|
| 108 |
-
self.data['Volume_Ratio'] = self.data['Volume'] / self.data['Volume_SMA']
|
| 109 |
-
|
| 110 |
-
def get_clean_prices(self) -> np.ndarray:
|
| 111 |
-
"""Get clean closing prices without NaN values"""
|
| 112 |
-
return self.data['Close'].fillna(method='ffill').fillna(method='bfill').values
|
| 113 |
-
|
| 114 |
-
def get_returns(self) -> np.ndarray:
|
| 115 |
-
"""Get returns series"""
|
| 116 |
-
returns = self.data['Returns'].fillna(0).values
|
| 117 |
-
return returns
|
| 118 |
-
|
| 119 |
-
def get_ohlcv(self) -> pd.DataFrame:
|
| 120 |
-
"""Get OHLCV data"""
|
| 121 |
-
return self.data[['Open', 'High', 'Low', 'Close', 'Volume']].copy()
|
| 122 |
-
|
| 123 |
-
def calculate_volatility(self, window: int = 20) -> pd.Series:
|
| 124 |
-
"""Calculate rolling volatility (annualized)"""
|
| 125 |
-
returns = self.data['Close'].pct_change()
|
| 126 |
-
|
| 127 |
-
# Determine scaling factor based on interval
|
| 128 |
-
if self.interval == '1d':
|
| 129 |
-
scale = np.sqrt(252)
|
| 130 |
-
elif self.interval == '1h':
|
| 131 |
-
scale = np.sqrt(252 * 6.5) # 6.5 trading hours
|
| 132 |
-
elif self.interval == '15m':
|
| 133 |
-
scale = np.sqrt(252 * 6.5 * 4) # 4 periods per hour
|
| 134 |
-
elif self.interval == '5m':
|
| 135 |
-
scale = np.sqrt(252 * 6.5 * 12) # 12 periods per hour
|
| 136 |
-
else:
|
| 137 |
-
scale = np.sqrt(252)
|
| 138 |
-
|
| 139 |
-
volatility = returns.rolling(window=window).std() * scale
|
| 140 |
-
|
| 141 |
-
# Fill initial NaN values
|
| 142 |
-
first_valid = volatility.first_valid_index()
|
| 143 |
-
if first_valid is not None:
|
| 144 |
-
first_vol = volatility.loc[first_valid]
|
| 145 |
-
volatility = volatility.fillna(first_vol)
|
| 146 |
-
else:
|
| 147 |
-
volatility = pd.Series(0.2, index=self.data.index)
|
| 148 |
-
|
| 149 |
-
return volatility
|
| 150 |
-
|
| 151 |
-
def detect_market_regime(self, lookback: int = 50) -> str:
|
| 152 |
-
"""
|
| 153 |
-
Detect market regime using ADX (Average Directional Index)
|
| 154 |
-
|
| 155 |
-
Returns:
|
| 156 |
-
Market regime: 'Trending', 'Range-Bound', or 'Transitional'
|
| 157 |
-
"""
|
| 158 |
-
high = self.data['High']
|
| 159 |
-
low = self.data['Low']
|
| 160 |
-
close = self.data['Close']
|
| 161 |
-
|
| 162 |
-
# True Range
|
| 163 |
-
tr1 = high - low
|
| 164 |
-
tr2 = abs(high - close.shift())
|
| 165 |
-
tr3 = abs(low - close.shift())
|
| 166 |
-
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
| 167 |
-
|
| 168 |
-
# Directional Movement
|
| 169 |
-
up_move = high - high.shift()
|
| 170 |
-
down_move = low.shift() - low
|
| 171 |
-
|
| 172 |
-
plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0)
|
| 173 |
-
minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0)
|
| 174 |
-
|
| 175 |
-
# ATR and Directional Indicators
|
| 176 |
-
atr = tr.rolling(window=lookback).mean()
|
| 177 |
-
plus_di = pd.Series(plus_dm).rolling(window=lookback).mean() / atr * 100
|
| 178 |
-
minus_di = pd.Series(minus_dm).rolling(window=lookback).mean() / atr * 100
|
| 179 |
-
|
| 180 |
-
# ADX
|
| 181 |
-
dx = abs(plus_di - minus_di) / (plus_di + minus_di) * 100
|
| 182 |
-
adx = dx.rolling(window=lookback).mean()
|
| 183 |
-
|
| 184 |
-
adx = adx.fillna(20)
|
| 185 |
-
current_adx = adx.iloc[-1]
|
| 186 |
-
|
| 187 |
-
if current_adx > 25:
|
| 188 |
-
return "Trending"
|
| 189 |
-
elif current_adx < 20:
|
| 190 |
-
return "Range-Bound"
|
| 191 |
-
else:
|
| 192 |
-
return "Transitional"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/ml_models.py
DELETED
|
@@ -1,240 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Machine Learning models with self-supervised learning
|
| 3 |
-
"""
|
| 4 |
-
import numpy as np
|
| 5 |
-
import pandas as pd
|
| 6 |
-
from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor
|
| 7 |
-
from sklearn.model_selection import TimeSeriesSplit, GridSearchCV, RandomizedSearchCV
|
| 8 |
-
from sklearn.preprocessing import StandardScaler
|
| 9 |
-
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
|
| 10 |
-
import xgboost as xgb
|
| 11 |
-
import lightgbm as lgb
|
| 12 |
-
from typing import Dict, Tuple, List
|
| 13 |
-
from src.config import config
|
| 14 |
-
|
| 15 |
-
class MLMomentumPredictor:
|
| 16 |
-
"""
|
| 17 |
-
Self-supervised learning for momentum prediction
|
| 18 |
-
Continuously learns from past predictions
|
| 19 |
-
"""
|
| 20 |
-
|
| 21 |
-
def __init__(self, model_type: str = 'xgboost'):
|
| 22 |
-
self.model_type = model_type
|
| 23 |
-
self.model = None
|
| 24 |
-
self.scaler = StandardScaler()
|
| 25 |
-
self.feature_importance = None
|
| 26 |
-
self.validation_history = []
|
| 27 |
-
self.optimal_params = None
|
| 28 |
-
|
| 29 |
-
def prepare_features(self, data: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]:
|
| 30 |
-
"""
|
| 31 |
-
Engineer features from market data
|
| 32 |
-
|
| 33 |
-
Returns:
|
| 34 |
-
Tuple of (features, target)
|
| 35 |
-
"""
|
| 36 |
-
features_list = []
|
| 37 |
-
|
| 38 |
-
# Price-based features
|
| 39 |
-
features_list.append(data['Returns'].values.reshape(-1, 1))
|
| 40 |
-
features_list.append(data['LogReturns'].values.reshape(-1, 1))
|
| 41 |
-
|
| 42 |
-
# Technical indicators
|
| 43 |
-
for col in data.columns:
|
| 44 |
-
if col.startswith(('SMA_', 'EMA_', 'RSI', 'MACD', 'ATR', 'BB_')):
|
| 45 |
-
features_list.append(data[col].values.reshape(-1, 1))
|
| 46 |
-
|
| 47 |
-
# Lag features
|
| 48 |
-
for lag in [1, 2, 3, 5, 10]:
|
| 49 |
-
lag_returns = data['Returns'].shift(lag).values.reshape(-1, 1)
|
| 50 |
-
features_list.append(lag_returns)
|
| 51 |
-
|
| 52 |
-
# Rolling statistics
|
| 53 |
-
for window in [5, 10, 20]:
|
| 54 |
-
roll_mean = data['Close'].rolling(window).mean().pct_change().values.reshape(-1, 1)
|
| 55 |
-
roll_std = data['Close'].rolling(window).std().values.reshape(-1, 1)
|
| 56 |
-
features_list.append(roll_mean)
|
| 57 |
-
features_list.append(roll_std)
|
| 58 |
-
|
| 59 |
-
# Combine all features
|
| 60 |
-
X = np.hstack(features_list)
|
| 61 |
-
|
| 62 |
-
# Target: future returns (5 days ahead)
|
| 63 |
-
y = data['Close'].pct_change(5).shift(-5).values
|
| 64 |
-
|
| 65 |
-
# Remove NaN rows
|
| 66 |
-
valid_idx = ~(np.isnan(X).any(axis=1) | np.isnan(y))
|
| 67 |
-
X = X[valid_idx]
|
| 68 |
-
y = y[valid_idx]
|
| 69 |
-
|
| 70 |
-
return X, y
|
| 71 |
-
|
| 72 |
-
def self_supervised_training(
|
| 73 |
-
self,
|
| 74 |
-
X: np.ndarray,
|
| 75 |
-
y: np.ndarray,
|
| 76 |
-
optimize_params: bool = True
|
| 77 |
-
) -> Dict:
|
| 78 |
-
"""
|
| 79 |
-
Train model using rolling window walk-forward validation
|
| 80 |
-
Self-supervised: learns from past predictions
|
| 81 |
-
|
| 82 |
-
Returns:
|
| 83 |
-
Training metrics
|
| 84 |
-
"""
|
| 85 |
-
n_splits = max(3, len(X) // config.ROLLING_WINDOW_SIZE)
|
| 86 |
-
tscv = TimeSeriesSplit(n_splits=n_splits)
|
| 87 |
-
|
| 88 |
-
all_predictions = []
|
| 89 |
-
all_actuals = []
|
| 90 |
-
|
| 91 |
-
for train_idx, val_idx in tscv.split(X):
|
| 92 |
-
X_train, X_val = X[train_idx], X[val_idx]
|
| 93 |
-
y_train, y_val = y[train_idx], y[val_idx]
|
| 94 |
-
|
| 95 |
-
# Scale features
|
| 96 |
-
X_train_scaled = self.scaler.fit_transform(X_train)
|
| 97 |
-
X_val_scaled = self.scaler.transform(X_val)
|
| 98 |
-
|
| 99 |
-
# Train model
|
| 100 |
-
if optimize_params and self.optimal_params is None:
|
| 101 |
-
self._optimize_hyperparameters(X_train_scaled, y_train)
|
| 102 |
-
|
| 103 |
-
model = self._create_model()
|
| 104 |
-
model.fit(X_train_scaled, y_train)
|
| 105 |
-
|
| 106 |
-
# Predict
|
| 107 |
-
y_pred = model.predict(X_val_scaled)
|
| 108 |
-
|
| 109 |
-
# Store results
|
| 110 |
-
all_predictions.extend(y_pred)
|
| 111 |
-
all_actuals.extend(y_val)
|
| 112 |
-
|
| 113 |
-
# Calculate validation metrics
|
| 114 |
-
val_metrics = {
|
| 115 |
-
'mse': mean_squared_error(y_val, y_pred),
|
| 116 |
-
'mae': mean_absolute_error(y_val, y_pred),
|
| 117 |
-
'r2': r2_score(y_val, y_pred)
|
| 118 |
-
}
|
| 119 |
-
self.validation_history.append(val_metrics)
|
| 120 |
-
|
| 121 |
-
# Train final model on all data
|
| 122 |
-
X_scaled = self.scaler.fit_transform(X)
|
| 123 |
-
self.model = self._create_model()
|
| 124 |
-
self.model.fit(X_scaled, y)
|
| 125 |
-
|
| 126 |
-
# Feature importance
|
| 127 |
-
if hasattr(self.model, 'feature_importances_'):
|
| 128 |
-
self.feature_importance = self.model.feature_importances_
|
| 129 |
-
|
| 130 |
-
# Overall metrics
|
| 131 |
-
overall_metrics = {
|
| 132 |
-
'avg_mse': np.mean([v['mse'] for v in self.validation_history]),
|
| 133 |
-
'avg_mae': np.mean([v['mae'] for v in self.validation_history]),
|
| 134 |
-
'avg_r2': np.mean([v['r2'] for v in self.validation_history]),
|
| 135 |
-
'final_mse': mean_squared_error(all_actuals, all_predictions),
|
| 136 |
-
'final_r2': r2_score(all_actuals, all_predictions)
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
return overall_metrics
|
| 140 |
-
|
| 141 |
-
def _create_model(self):
|
| 142 |
-
"""Create ML model based on type"""
|
| 143 |
-
if self.model_type == 'xgboost':
|
| 144 |
-
params = self.optimal_params if self.optimal_params else {
|
| 145 |
-
'n_estimators': 200,
|
| 146 |
-
'max_depth': 5,
|
| 147 |
-
'learning_rate': 0.05,
|
| 148 |
-
'subsample': 0.8
|
| 149 |
-
}
|
| 150 |
-
return xgb.XGBRegressor(**params, random_state=config.ML_RANDOM_STATE)
|
| 151 |
-
|
| 152 |
-
elif self.model_type == 'lightgbm':
|
| 153 |
-
params = self.optimal_params if self.optimal_params else {
|
| 154 |
-
'n_estimators': 200,
|
| 155 |
-
'max_depth': 5,
|
| 156 |
-
'learning_rate': 0.05,
|
| 157 |
-
'subsample': 0.8
|
| 158 |
-
}
|
| 159 |
-
return lgb.LGBMRegressor(**params, random_state=config.ML_RANDOM_STATE, verbose=-1)
|
| 160 |
-
|
| 161 |
-
elif self.model_type == 'gradient_boosting':
|
| 162 |
-
params = self.optimal_params if self.optimal_params else {
|
| 163 |
-
'n_estimators': 200,
|
| 164 |
-
'max_depth': 5,
|
| 165 |
-
'learning_rate': 0.05,
|
| 166 |
-
'subsample': 0.8
|
| 167 |
-
}
|
| 168 |
-
return GradientBoostingRegressor(**params, random_state=config.ML_RANDOM_STATE)
|
| 169 |
-
|
| 170 |
-
else: # random_forest
|
| 171 |
-
params = self.optimal_params if self.optimal_params else {
|
| 172 |
-
'n_estimators': 200,
|
| 173 |
-
'max_depth': 10
|
| 174 |
-
}
|
| 175 |
-
return RandomForestRegressor(**params, random_state=config.ML_RANDOM_STATE)
|
| 176 |
-
|
| 177 |
-
def _optimize_hyperparameters(self, X: np.ndarray, y: np.ndarray):
|
| 178 |
-
"""Optimize hyperparameters using GridSearch"""
|
| 179 |
-
if self.model_type == 'xgboost':
|
| 180 |
-
param_grid = {
|
| 181 |
-
'n_estimators': [100, 200, 500],
|
| 182 |
-
'max_depth': [3, 5, 7],
|
| 183 |
-
'learning_rate': [0.01, 0.05, 0.1],
|
| 184 |
-
'subsample': [0.7, 0.8, 0.9]
|
| 185 |
-
}
|
| 186 |
-
base_model = xgb.XGBRegressor(random_state=config.ML_RANDOM_STATE)
|
| 187 |
-
|
| 188 |
-
elif self.model_type == 'lightgbm':
|
| 189 |
-
param_grid = {
|
| 190 |
-
'n_estimators': [100, 200, 500],
|
| 191 |
-
'max_depth': [3, 5, 7],
|
| 192 |
-
'learning_rate': [0.01, 0.05, 0.1],
|
| 193 |
-
'subsample': [0.7, 0.8, 0.9]
|
| 194 |
-
}
|
| 195 |
-
base_model = lgb.LGBMRegressor(random_state=config.ML_RANDOM_STATE, verbose=-1)
|
| 196 |
-
|
| 197 |
-
else:
|
| 198 |
-
param_grid = {
|
| 199 |
-
'n_estimators': [100, 200, 500],
|
| 200 |
-
'max_depth': [3, 5, 7, 10],
|
| 201 |
-
'learning_rate': [0.01, 0.05, 0.1]
|
| 202 |
-
}
|
| 203 |
-
base_model = GradientBoostingRegressor(random_state=config.ML_RANDOM_STATE)
|
| 204 |
-
|
| 205 |
-
# Use RandomizedSearch for efficiency
|
| 206 |
-
search = RandomizedSearchCV(
|
| 207 |
-
base_model,
|
| 208 |
-
param_grid,
|
| 209 |
-
n_iter=20,
|
| 210 |
-
cv=3,
|
| 211 |
-
scoring='neg_mean_squared_error',
|
| 212 |
-
random_state=config.ML_RANDOM_STATE,
|
| 213 |
-
n_jobs=-1
|
| 214 |
-
)
|
| 215 |
-
|
| 216 |
-
search.fit(X, y)
|
| 217 |
-
self.optimal_params = search.best_params_
|
| 218 |
-
|
| 219 |
-
def predict(self, X: np.ndarray) -> np.ndarray:
|
| 220 |
-
"""Make predictions"""
|
| 221 |
-
if self.model is None:
|
| 222 |
-
raise ValueError("Model not trained yet")
|
| 223 |
-
|
| 224 |
-
X_scaled = self.scaler.transform(X)
|
| 225 |
-
return self.model.predict(X_scaled)
|
| 226 |
-
|
| 227 |
-
def get_feature_importance(self, feature_names: List[str] = None) -> pd.DataFrame:
|
| 228 |
-
"""Get feature importance rankings"""
|
| 229 |
-
if self.feature_importance is None:
|
| 230 |
-
return pd.DataFrame()
|
| 231 |
-
|
| 232 |
-
if feature_names is None:
|
| 233 |
-
feature_names = [f'feature_{i}' for i in range(len(self.feature_importance))]
|
| 234 |
-
|
| 235 |
-
importance_df = pd.DataFrame({
|
| 236 |
-
'feature': feature_names,
|
| 237 |
-
'importance': self.feature_importance
|
| 238 |
-
}).sort_values('importance', ascending=False)
|
| 239 |
-
|
| 240 |
-
return importance_df
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/monte_carlo.py
DELETED
|
@@ -1,339 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Monte Carlo simulation engine for probabilistic forecasting
|
| 3 |
-
"""
|
| 4 |
-
import numpy as np
|
| 5 |
-
import pandas as pd
|
| 6 |
-
from typing import Dict, List, Tuple
|
| 7 |
-
from scipy import stats
|
| 8 |
-
from src.config import config
|
| 9 |
-
|
| 10 |
-
class MonteCarloEngine:
|
| 11 |
-
"""
|
| 12 |
-
Advanced Monte Carlo simulation with multiple stochastic models
|
| 13 |
-
Supports GBM, Jump Diffusion, and GARCH-based simulations
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
def __init__(self):
|
| 17 |
-
self.simulations = None
|
| 18 |
-
self.statistics = None
|
| 19 |
-
|
| 20 |
-
def geometric_brownian_motion(
|
| 21 |
-
self,
|
| 22 |
-
S0: float,
|
| 23 |
-
mu: float,
|
| 24 |
-
sigma: float,
|
| 25 |
-
T: int,
|
| 26 |
-
n_sims: int = None
|
| 27 |
-
) -> np.ndarray:
|
| 28 |
-
"""
|
| 29 |
-
Geometric Brownian Motion simulation
|
| 30 |
-
|
| 31 |
-
Args:
|
| 32 |
-
S0: Initial price
|
| 33 |
-
mu: Drift (expected return)
|
| 34 |
-
sigma: Volatility
|
| 35 |
-
T: Time horizon (days)
|
| 36 |
-
n_sims: Number of simulations
|
| 37 |
-
|
| 38 |
-
Returns:
|
| 39 |
-
Array of simulated price paths (n_sims, T)
|
| 40 |
-
"""
|
| 41 |
-
if n_sims is None:
|
| 42 |
-
n_sims = config.MC_SIMULATIONS
|
| 43 |
-
|
| 44 |
-
dt = 1 # Daily steps
|
| 45 |
-
paths = np.zeros((n_sims, T))
|
| 46 |
-
paths[:, 0] = S0
|
| 47 |
-
|
| 48 |
-
for t in range(1, T):
|
| 49 |
-
Z = np.random.standard_normal(n_sims)
|
| 50 |
-
paths[:, t] = paths[:, t-1] * np.exp(
|
| 51 |
-
(mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * Z
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
return paths
|
| 55 |
-
|
| 56 |
-
def jump_diffusion(
|
| 57 |
-
self,
|
| 58 |
-
S0: float,
|
| 59 |
-
mu: float,
|
| 60 |
-
sigma: float,
|
| 61 |
-
lambda_jump: float,
|
| 62 |
-
jump_mean: float,
|
| 63 |
-
jump_std: float,
|
| 64 |
-
T: int,
|
| 65 |
-
n_sims: int = None
|
| 66 |
-
) -> np.ndarray:
|
| 67 |
-
"""
|
| 68 |
-
Merton's Jump Diffusion model
|
| 69 |
-
|
| 70 |
-
Args:
|
| 71 |
-
S0: Initial price
|
| 72 |
-
mu: Drift
|
| 73 |
-
sigma: Diffusion volatility
|
| 74 |
-
lambda_jump: Jump intensity (jumps per day)
|
| 75 |
-
jump_mean: Mean jump size
|
| 76 |
-
jump_std: Jump size standard deviation
|
| 77 |
-
T: Time horizon
|
| 78 |
-
n_sims: Number of simulations
|
| 79 |
-
|
| 80 |
-
Returns:
|
| 81 |
-
Array of simulated price paths
|
| 82 |
-
"""
|
| 83 |
-
if n_sims is None:
|
| 84 |
-
n_sims = config.MC_SIMULATIONS
|
| 85 |
-
|
| 86 |
-
dt = 1
|
| 87 |
-
paths = np.zeros((n_sims, T))
|
| 88 |
-
paths[:, 0] = S0
|
| 89 |
-
|
| 90 |
-
for t in range(1, T):
|
| 91 |
-
# Diffusion component
|
| 92 |
-
Z = np.random.standard_normal(n_sims)
|
| 93 |
-
diffusion = (mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * Z
|
| 94 |
-
|
| 95 |
-
# Jump component
|
| 96 |
-
N = np.random.poisson(lambda_jump * dt, n_sims) # Number of jumps
|
| 97 |
-
jump_sizes = np.random.normal(jump_mean, jump_std, n_sims) * N
|
| 98 |
-
|
| 99 |
-
# Combined evolution
|
| 100 |
-
paths[:, t] = paths[:, t-1] * np.exp(diffusion + jump_sizes)
|
| 101 |
-
|
| 102 |
-
return paths
|
| 103 |
-
|
| 104 |
-
def garch_simulation(
|
| 105 |
-
self,
|
| 106 |
-
S0: float,
|
| 107 |
-
returns: np.ndarray,
|
| 108 |
-
T: int,
|
| 109 |
-
n_sims: int = None
|
| 110 |
-
) -> np.ndarray:
|
| 111 |
-
"""
|
| 112 |
-
GARCH(1,1) based simulation
|
| 113 |
-
|
| 114 |
-
Args:
|
| 115 |
-
S0: Initial price
|
| 116 |
-
returns: Historical returns for parameter estimation
|
| 117 |
-
T: Time horizon
|
| 118 |
-
n_sims: Number of simulations
|
| 119 |
-
|
| 120 |
-
Returns:
|
| 121 |
-
Array of simulated price paths
|
| 122 |
-
"""
|
| 123 |
-
if n_sims is None:
|
| 124 |
-
n_sims = config.MC_SIMULATIONS
|
| 125 |
-
|
| 126 |
-
# Estimate GARCH parameters
|
| 127 |
-
omega, alpha, beta = self._estimate_garch_params(returns)
|
| 128 |
-
|
| 129 |
-
paths = np.zeros((n_sims, T))
|
| 130 |
-
paths[:, 0] = S0
|
| 131 |
-
|
| 132 |
-
# Initialize volatility
|
| 133 |
-
sigma_sq = np.var(returns)
|
| 134 |
-
|
| 135 |
-
for sim in range(n_sims):
|
| 136 |
-
prices = [S0]
|
| 137 |
-
var = sigma_sq
|
| 138 |
-
|
| 139 |
-
for t in range(1, T):
|
| 140 |
-
# Generate return with time-varying volatility
|
| 141 |
-
epsilon = np.random.standard_normal()
|
| 142 |
-
ret = np.sqrt(var) * epsilon
|
| 143 |
-
|
| 144 |
-
# Update price
|
| 145 |
-
new_price = prices[-1] * (1 + ret)
|
| 146 |
-
prices.append(new_price)
|
| 147 |
-
|
| 148 |
-
# Update variance (GARCH dynamics)
|
| 149 |
-
var = omega + alpha * (ret**2) + beta * var
|
| 150 |
-
|
| 151 |
-
paths[sim, :] = prices
|
| 152 |
-
|
| 153 |
-
return paths
|
| 154 |
-
|
| 155 |
-
def _estimate_garch_params(self, returns: np.ndarray) -> Tuple[float, float, float]:
|
| 156 |
-
"""
|
| 157 |
-
Simple GARCH(1,1) parameter estimation using method of moments
|
| 158 |
-
|
| 159 |
-
Returns:
|
| 160 |
-
Tuple of (omega, alpha, beta)
|
| 161 |
-
"""
|
| 162 |
-
# Simple estimation (could be improved with MLE)
|
| 163 |
-
variance = np.var(returns)
|
| 164 |
-
|
| 165 |
-
# Typical values for GARCH(1,1)
|
| 166 |
-
alpha = 0.1
|
| 167 |
-
beta = 0.85
|
| 168 |
-
omega = variance * (1 - alpha - beta)
|
| 169 |
-
|
| 170 |
-
return omega, alpha, beta
|
| 171 |
-
|
| 172 |
-
def heston_model(
|
| 173 |
-
self,
|
| 174 |
-
S0: float,
|
| 175 |
-
v0: float,
|
| 176 |
-
kappa: float,
|
| 177 |
-
theta: float,
|
| 178 |
-
sigma_v: float,
|
| 179 |
-
rho: float,
|
| 180 |
-
mu: float,
|
| 181 |
-
T: int,
|
| 182 |
-
n_sims: int = None
|
| 183 |
-
) -> Tuple[np.ndarray, np.ndarray]:
|
| 184 |
-
"""
|
| 185 |
-
Heston stochastic volatility model
|
| 186 |
-
|
| 187 |
-
Args:
|
| 188 |
-
S0: Initial price
|
| 189 |
-
v0: Initial variance
|
| 190 |
-
kappa: Mean reversion speed
|
| 191 |
-
theta: Long-term variance
|
| 192 |
-
sigma_v: Volatility of volatility
|
| 193 |
-
rho: Correlation between price and volatility
|
| 194 |
-
mu: Drift
|
| 195 |
-
T: Time horizon
|
| 196 |
-
n_sims: Number of simulations
|
| 197 |
-
|
| 198 |
-
Returns:
|
| 199 |
-
Tuple of (price_paths, variance_paths)
|
| 200 |
-
"""
|
| 201 |
-
if n_sims is None:
|
| 202 |
-
n_sims = config.MC_SIMULATIONS
|
| 203 |
-
|
| 204 |
-
dt = 1
|
| 205 |
-
price_paths = np.zeros((n_sims, T))
|
| 206 |
-
var_paths = np.zeros((n_sims, T))
|
| 207 |
-
|
| 208 |
-
price_paths[:, 0] = S0
|
| 209 |
-
var_paths[:, 0] = v0
|
| 210 |
-
|
| 211 |
-
for t in range(1, T):
|
| 212 |
-
# Correlated random variables
|
| 213 |
-
Z1 = np.random.standard_normal(n_sims)
|
| 214 |
-
Z2 = rho * Z1 + np.sqrt(1 - rho**2) * np.random.standard_normal(n_sims)
|
| 215 |
-
|
| 216 |
-
# Variance process (with truncation to ensure positivity)
|
| 217 |
-
var_paths[:, t] = np.maximum(
|
| 218 |
-
var_paths[:, t-1] + kappa * (theta - var_paths[:, t-1]) * dt +
|
| 219 |
-
sigma_v * np.sqrt(var_paths[:, t-1] * dt) * Z2,
|
| 220 |
-
1e-10
|
| 221 |
-
)
|
| 222 |
-
|
| 223 |
-
# Price process
|
| 224 |
-
price_paths[:, t] = price_paths[:, t-1] * np.exp(
|
| 225 |
-
(mu - 0.5 * var_paths[:, t-1]) * dt +
|
| 226 |
-
np.sqrt(var_paths[:, t-1] * dt) * Z1
|
| 227 |
-
)
|
| 228 |
-
|
| 229 |
-
return price_paths, var_paths
|
| 230 |
-
|
| 231 |
-
def simulate_all_models(
|
| 232 |
-
self,
|
| 233 |
-
S0: float,
|
| 234 |
-
returns: np.ndarray,
|
| 235 |
-
T: int = None,
|
| 236 |
-
n_sims: int = None
|
| 237 |
-
) -> Dict[str, np.ndarray]:
|
| 238 |
-
"""
|
| 239 |
-
Run all Monte Carlo models and return results
|
| 240 |
-
|
| 241 |
-
Args:
|
| 242 |
-
S0: Current price
|
| 243 |
-
returns: Historical returns
|
| 244 |
-
T: Forecast horizon
|
| 245 |
-
n_sims: Number of simulations
|
| 246 |
-
|
| 247 |
-
Returns:
|
| 248 |
-
Dictionary with results from each model
|
| 249 |
-
"""
|
| 250 |
-
if T is None:
|
| 251 |
-
T = config.MC_TIME_HORIZON
|
| 252 |
-
if n_sims is None:
|
| 253 |
-
n_sims = config.MC_SIMULATIONS
|
| 254 |
-
|
| 255 |
-
# Estimate parameters from historical data
|
| 256 |
-
mu = np.mean(returns)
|
| 257 |
-
sigma = np.std(returns)
|
| 258 |
-
|
| 259 |
-
# Estimate jump parameters
|
| 260 |
-
# Identify potential jumps (returns > 3 std)
|
| 261 |
-
jump_threshold = 3 * sigma
|
| 262 |
-
jumps = returns[np.abs(returns) > jump_threshold]
|
| 263 |
-
lambda_jump = len(jumps) / len(returns) # Jumps per day
|
| 264 |
-
jump_mean = np.mean(jumps) if len(jumps) > 0 else 0
|
| 265 |
-
jump_std = np.std(jumps) if len(jumps) > 1 else sigma
|
| 266 |
-
|
| 267 |
-
# Run simulations
|
| 268 |
-
results = {
|
| 269 |
-
'gbm': self.geometric_brownian_motion(S0, mu, sigma, T, n_sims),
|
| 270 |
-
'jump_diffusion': self.jump_diffusion(
|
| 271 |
-
S0, mu, sigma, lambda_jump, jump_mean, jump_std, T, n_sims
|
| 272 |
-
),
|
| 273 |
-
'garch': self.garch_simulation(S0, returns, T, n_sims)
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
# Heston model (with estimated parameters)
|
| 277 |
-
v0 = sigma**2
|
| 278 |
-
kappa = 2.0 # Mean reversion speed
|
| 279 |
-
theta = sigma**2 # Long-term variance
|
| 280 |
-
sigma_v = 0.3 # Vol of vol
|
| 281 |
-
rho = -0.7 # Typical negative correlation
|
| 282 |
-
|
| 283 |
-
price_paths_heston, var_paths_heston = self.heston_model(
|
| 284 |
-
S0, v0, kappa, theta, sigma_v, rho, mu, T, n_sims
|
| 285 |
-
)
|
| 286 |
-
results['heston'] = price_paths_heston
|
| 287 |
-
results['heston_variance'] = var_paths_heston
|
| 288 |
-
|
| 289 |
-
self.simulations = results
|
| 290 |
-
return results
|
| 291 |
-
|
| 292 |
-
def calculate_statistics(self, model_name: str = 'gbm') -> Dict:
|
| 293 |
-
"""
|
| 294 |
-
Calculate statistics from simulations
|
| 295 |
-
|
| 296 |
-
Args:
|
| 297 |
-
model_name: Which model to analyze
|
| 298 |
-
|
| 299 |
-
Returns:
|
| 300 |
-
Dictionary of statistics
|
| 301 |
-
"""
|
| 302 |
-
if self.simulations is None or model_name not in self.simulations:
|
| 303 |
-
raise ValueError(f"Model '{model_name}' not simulated yet")
|
| 304 |
-
|
| 305 |
-
paths = self.simulations[model_name]
|
| 306 |
-
final_prices = paths[:, -1]
|
| 307 |
-
|
| 308 |
-
statistics = {
|
| 309 |
-
'mean_final_price': float(np.mean(final_prices)),
|
| 310 |
-
'median_final_price': float(np.median(final_prices)),
|
| 311 |
-
'std_final_price': float(np.std(final_prices)),
|
| 312 |
-
'percentile_5': float(np.percentile(final_prices, 5)),
|
| 313 |
-
'percentile_25': float(np.percentile(final_prices, 25)),
|
| 314 |
-
'percentile_75': float(np.percentile(final_prices, 75)),
|
| 315 |
-
'percentile_95': float(np.percentile(final_prices, 95)),
|
| 316 |
-
'prob_profit': float(np.mean(final_prices > paths[0, 0])),
|
| 317 |
-
'expected_return': float(np.mean((final_prices - paths[0, 0]) / paths[0, 0])),
|
| 318 |
-
'var_95': float(np.percentile(final_prices, 5)), # Value at Risk
|
| 319 |
-
'cvar_95': float(np.mean(final_prices[final_prices <= np.percentile(final_prices, 5)])) # CVaR
|
| 320 |
-
}
|
| 321 |
-
|
| 322 |
-
self.statistics = statistics
|
| 323 |
-
return statistics
|
| 324 |
-
|
| 325 |
-
def get_probability_distribution(self, model_name: str = 'gbm', bins: int = 50) -> Tuple[np.ndarray, np.ndarray]:
|
| 326 |
-
"""
|
| 327 |
-
Get probability distribution of final prices
|
| 328 |
-
|
| 329 |
-
Returns:
|
| 330 |
-
Tuple of (bin_centers, probabilities)
|
| 331 |
-
"""
|
| 332 |
-
if self.simulations is None or model_name not in self.simulations:
|
| 333 |
-
raise ValueError(f"Model '{model_name}' not simulated yet")
|
| 334 |
-
|
| 335 |
-
final_prices = self.simulations[model_name][:, -1]
|
| 336 |
-
counts, bin_edges = np.histogram(final_prices, bins=bins, density=True)
|
| 337 |
-
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
| 338 |
-
|
| 339 |
-
return bin_centers, counts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/pattern_recognition.py
DELETED
|
@@ -1,395 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Pattern recognition module for technical analysis
|
| 3 |
-
Includes candlestick patterns, chart patterns, and ML-based clustering
|
| 4 |
-
"""
|
| 5 |
-
import numpy as np
|
| 6 |
-
import pandas as pd
|
| 7 |
-
from typing import List, Dict, Tuple
|
| 8 |
-
from scipy.spatial.distance import euclidean
|
| 9 |
-
from scipy.signal import find_peaks, argrelextrema
|
| 10 |
-
from tslearn.clustering import TimeSeriesKMeans
|
| 11 |
-
from tslearn.metrics import dtw
|
| 12 |
-
from src.config import config
|
| 13 |
-
|
| 14 |
-
class PatternRecognizer:
|
| 15 |
-
"""
|
| 16 |
-
Advanced pattern recognition combining technical patterns and ML
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
def __init__(self):
|
| 20 |
-
self.patterns_found = []
|
| 21 |
-
self.clusters = None
|
| 22 |
-
self.cluster_model = None
|
| 23 |
-
|
| 24 |
-
def detect_candlestick_patterns(self, ohlc: pd.DataFrame) -> List[Dict]:
|
| 25 |
-
"""
|
| 26 |
-
Detect common candlestick patterns
|
| 27 |
-
|
| 28 |
-
Args:
|
| 29 |
-
ohlc: DataFrame with Open, High, Low, Close columns
|
| 30 |
-
|
| 31 |
-
Returns:
|
| 32 |
-
List of detected patterns
|
| 33 |
-
"""
|
| 34 |
-
patterns = []
|
| 35 |
-
|
| 36 |
-
for i in range(2, len(ohlc)):
|
| 37 |
-
pattern_info = {
|
| 38 |
-
'index': i,
|
| 39 |
-
'date': ohlc.index[i],
|
| 40 |
-
'pattern': None,
|
| 41 |
-
'signal': None
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
# Get candles
|
| 45 |
-
c0 = ohlc.iloc[i] # Current
|
| 46 |
-
c1 = ohlc.iloc[i-1] # Previous
|
| 47 |
-
c2 = ohlc.iloc[i-2] # Before previous
|
| 48 |
-
|
| 49 |
-
# Doji
|
| 50 |
-
if self._is_doji(c0):
|
| 51 |
-
pattern_info['pattern'] = 'Doji'
|
| 52 |
-
pattern_info['signal'] = 'Neutral/Reversal'
|
| 53 |
-
patterns.append(pattern_info.copy())
|
| 54 |
-
|
| 55 |
-
# Hammer
|
| 56 |
-
if self._is_hammer(c0):
|
| 57 |
-
pattern_info['pattern'] = 'Hammer'
|
| 58 |
-
pattern_info['signal'] = 'Bullish Reversal'
|
| 59 |
-
patterns.append(pattern_info.copy())
|
| 60 |
-
|
| 61 |
-
# Shooting Star
|
| 62 |
-
if self._is_shooting_star(c0):
|
| 63 |
-
pattern_info['pattern'] = 'Shooting Star'
|
| 64 |
-
pattern_info['signal'] = 'Bearish Reversal'
|
| 65 |
-
patterns.append(pattern_info.copy())
|
| 66 |
-
|
| 67 |
-
# Engulfing patterns
|
| 68 |
-
if self._is_bullish_engulfing(c1, c0):
|
| 69 |
-
pattern_info['pattern'] = 'Bullish Engulfing'
|
| 70 |
-
pattern_info['signal'] = 'Bullish Reversal'
|
| 71 |
-
patterns.append(pattern_info.copy())
|
| 72 |
-
|
| 73 |
-
if self._is_bearish_engulfing(c1, c0):
|
| 74 |
-
pattern_info['pattern'] = 'Bearish Engulfing'
|
| 75 |
-
pattern_info['signal'] = 'Bearish Reversal'
|
| 76 |
-
patterns.append(pattern_info.copy())
|
| 77 |
-
|
| 78 |
-
# Morning Star (3-candle pattern)
|
| 79 |
-
if self._is_morning_star(c2, c1, c0):
|
| 80 |
-
pattern_info['pattern'] = 'Morning Star'
|
| 81 |
-
pattern_info['signal'] = 'Bullish Reversal'
|
| 82 |
-
patterns.append(pattern_info.copy())
|
| 83 |
-
|
| 84 |
-
# Evening Star
|
| 85 |
-
if self._is_evening_star(c2, c1, c0):
|
| 86 |
-
pattern_info['pattern'] = 'Evening Star'
|
| 87 |
-
pattern_info['signal'] = 'Bearish Reversal'
|
| 88 |
-
patterns.append(pattern_info.copy())
|
| 89 |
-
|
| 90 |
-
self.patterns_found.extend(patterns)
|
| 91 |
-
return patterns
|
| 92 |
-
|
| 93 |
-
def _is_doji(self, candle: pd.Series, threshold: float = 0.001) -> bool:
|
| 94 |
-
"""Check if candle is a Doji"""
|
| 95 |
-
body = abs(candle['Close'] - candle['Open'])
|
| 96 |
-
range_size = candle['High'] - candle['Low']
|
| 97 |
-
return body / (range_size + 1e-10) < threshold
|
| 98 |
-
|
| 99 |
-
def _is_hammer(self, candle: pd.Series) -> bool:
|
| 100 |
-
"""Check if candle is a Hammer"""
|
| 101 |
-
body = abs(candle['Close'] - candle['Open'])
|
| 102 |
-
lower_shadow = min(candle['Open'], candle['Close']) - candle['Low']
|
| 103 |
-
upper_shadow = candle['High'] - max(candle['Open'], candle['Close'])
|
| 104 |
-
|
| 105 |
-
return (lower_shadow > 2 * body and
|
| 106 |
-
upper_shadow < body and
|
| 107 |
-
body > 0)
|
| 108 |
-
|
| 109 |
-
def _is_shooting_star(self, candle: pd.Series) -> bool:
|
| 110 |
-
"""Check if candle is a Shooting Star"""
|
| 111 |
-
body = abs(candle['Close'] - candle['Open'])
|
| 112 |
-
upper_shadow = candle['High'] - max(candle['Open'], candle['Close'])
|
| 113 |
-
lower_shadow = min(candle['Open'], candle['Close']) - candle['Low']
|
| 114 |
-
|
| 115 |
-
return (upper_shadow > 2 * body and
|
| 116 |
-
lower_shadow < body and
|
| 117 |
-
body > 0)
|
| 118 |
-
|
| 119 |
-
def _is_bullish_engulfing(self, prev: pd.Series, curr: pd.Series) -> bool:
|
| 120 |
-
"""Check for Bullish Engulfing pattern"""
|
| 121 |
-
prev_bearish = prev['Close'] < prev['Open']
|
| 122 |
-
curr_bullish = curr['Close'] > curr['Open']
|
| 123 |
-
|
| 124 |
-
return (prev_bearish and curr_bullish and
|
| 125 |
-
curr['Open'] < prev['Close'] and
|
| 126 |
-
curr['Close'] > prev['Open'])
|
| 127 |
-
|
| 128 |
-
def _is_bearish_engulfing(self, prev: pd.Series, curr: pd.Series) -> bool:
|
| 129 |
-
"""Check for Bearish Engulfing pattern"""
|
| 130 |
-
prev_bullish = prev['Close'] > prev['Open']
|
| 131 |
-
curr_bearish = curr['Close'] < curr['Open']
|
| 132 |
-
|
| 133 |
-
return (prev_bullish and curr_bearish and
|
| 134 |
-
curr['Open'] > prev['Close'] and
|
| 135 |
-
curr['Close'] < prev['Open'])
|
| 136 |
-
|
| 137 |
-
def _is_morning_star(self, c1: pd.Series, c2: pd.Series, c3: pd.Series) -> bool:
|
| 138 |
-
"""Check for Morning Star pattern"""
|
| 139 |
-
first_bearish = c1['Close'] < c1['Open']
|
| 140 |
-
second_small = abs(c2['Close'] - c2['Open']) < abs(c1['Close'] - c1['Open']) * 0.3
|
| 141 |
-
third_bullish = c3['Close'] > c3['Open']
|
| 142 |
-
|
| 143 |
-
gap_down = c2['Open'] < c1['Close']
|
| 144 |
-
gap_up = c3['Open'] > c2['Close']
|
| 145 |
-
|
| 146 |
-
return first_bearish and second_small and third_bullish and gap_down and gap_up
|
| 147 |
-
|
| 148 |
-
def _is_evening_star(self, c1: pd.Series, c2: pd.Series, c3: pd.Series) -> bool:
|
| 149 |
-
"""Check for Evening Star pattern"""
|
| 150 |
-
first_bullish = c1['Close'] > c1['Open']
|
| 151 |
-
second_small = abs(c2['Close'] - c2['Open']) < abs(c1['Close'] - c1['Open']) * 0.3
|
| 152 |
-
third_bearish = c3['Close'] < c3['Open']
|
| 153 |
-
|
| 154 |
-
gap_up = c2['Open'] > c1['Close']
|
| 155 |
-
gap_down = c3['Open'] < c2['Close']
|
| 156 |
-
|
| 157 |
-
return first_bullish and second_small and third_bearish and gap_up and gap_down
|
| 158 |
-
|
| 159 |
-
def detect_chart_patterns(self, prices: np.ndarray, dates: pd.DatetimeIndex) -> List[Dict]:
|
| 160 |
-
"""
|
| 161 |
-
Detect chart patterns (Head & Shoulders, Double Top/Bottom, etc.)
|
| 162 |
-
|
| 163 |
-
Args:
|
| 164 |
-
prices: Price series
|
| 165 |
-
dates: Date index
|
| 166 |
-
|
| 167 |
-
Returns:
|
| 168 |
-
List of detected chart patterns
|
| 169 |
-
"""
|
| 170 |
-
patterns = []
|
| 171 |
-
|
| 172 |
-
# Find local maxima and minima
|
| 173 |
-
maxima_idx = argrelextrema(prices, np.greater, order=5)[0]
|
| 174 |
-
minima_idx = argrelextrema(prices, np.less, order=5)[0]
|
| 175 |
-
|
| 176 |
-
# Head and Shoulders
|
| 177 |
-
hs_patterns = self._find_head_shoulders(prices, maxima_idx, dates)
|
| 178 |
-
patterns.extend(hs_patterns)
|
| 179 |
-
|
| 180 |
-
# Inverse Head and Shoulders
|
| 181 |
-
ihs_patterns = self._find_inverse_head_shoulders(prices, minima_idx, dates)
|
| 182 |
-
patterns.extend(ihs_patterns)
|
| 183 |
-
|
| 184 |
-
# Double Top
|
| 185 |
-
double_top = self._find_double_top(prices, maxima_idx, dates)
|
| 186 |
-
patterns.extend(double_top)
|
| 187 |
-
|
| 188 |
-
# Double Bottom
|
| 189 |
-
double_bottom = self._find_double_bottom(prices, minima_idx, dates)
|
| 190 |
-
patterns.extend(double_bottom)
|
| 191 |
-
|
| 192 |
-
# Triangle patterns
|
| 193 |
-
triangles = self._find_triangles(prices, maxima_idx, minima_idx, dates)
|
| 194 |
-
patterns.extend(triangles)
|
| 195 |
-
|
| 196 |
-
return patterns
|
| 197 |
-
|
| 198 |
-
def _find_head_shoulders(self, prices: np.ndarray, peaks: np.ndarray, dates: pd.DatetimeIndex) -> List[Dict]:
|
| 199 |
-
"""Detect Head and Shoulders pattern"""
|
| 200 |
-
patterns = []
|
| 201 |
-
|
| 202 |
-
for i in range(len(peaks) - 2):
|
| 203 |
-
left_shoulder = peaks[i]
|
| 204 |
-
head = peaks[i + 1]
|
| 205 |
-
right_shoulder = peaks[i + 2]
|
| 206 |
-
|
| 207 |
-
# Check if head is higher than shoulders
|
| 208 |
-
if (prices[head] > prices[left_shoulder] and
|
| 209 |
-
prices[head] > prices[right_shoulder] and
|
| 210 |
-
abs(prices[left_shoulder] - prices[right_shoulder]) / prices[head] < 0.03):
|
| 211 |
-
|
| 212 |
-
patterns.append({
|
| 213 |
-
'pattern': 'Head and Shoulders',
|
| 214 |
-
'signal': 'Bearish Reversal',
|
| 215 |
-
'left_shoulder': {'date': dates[left_shoulder], 'price': prices[left_shoulder]},
|
| 216 |
-
'head': {'date': dates[head], 'price': prices[head]},
|
| 217 |
-
'right_shoulder': {'date': dates[right_shoulder], 'price': prices[right_shoulder]},
|
| 218 |
-
'neckline': float(min(prices[left_shoulder], prices[right_shoulder]))
|
| 219 |
-
})
|
| 220 |
-
|
| 221 |
-
return patterns
|
| 222 |
-
|
| 223 |
-
def _find_inverse_head_shoulders(self, prices: np.ndarray, troughs: np.ndarray, dates: pd.DatetimeIndex) -> List[Dict]:
|
| 224 |
-
"""Detect Inverse Head and Shoulders pattern"""
|
| 225 |
-
patterns = []
|
| 226 |
-
|
| 227 |
-
for i in range(len(troughs) - 2):
|
| 228 |
-
left_shoulder = troughs[i]
|
| 229 |
-
head = troughs[i + 1]
|
| 230 |
-
right_shoulder = troughs[i + 2]
|
| 231 |
-
|
| 232 |
-
if (prices[head] < prices[left_shoulder] and
|
| 233 |
-
prices[head] < prices[right_shoulder] and
|
| 234 |
-
abs(prices[left_shoulder] - prices[right_shoulder]) / prices[head] < 0.03):
|
| 235 |
-
|
| 236 |
-
patterns.append({
|
| 237 |
-
'pattern': 'Inverse Head and Shoulders',
|
| 238 |
-
'signal': 'Bullish Reversal',
|
| 239 |
-
'left_shoulder': {'date': dates[left_shoulder], 'price': prices[left_shoulder]},
|
| 240 |
-
'head': {'date': dates[head], 'price': prices[head]},
|
| 241 |
-
'right_shoulder': {'date': dates[right_shoulder], 'price': prices[right_shoulder]},
|
| 242 |
-
'neckline': float(max(prices[left_shoulder], prices[right_shoulder]))
|
| 243 |
-
})
|
| 244 |
-
|
| 245 |
-
return patterns
|
| 246 |
-
|
| 247 |
-
def _find_double_top(self, prices: np.ndarray, peaks: np.ndarray, dates: pd.DatetimeIndex) -> List[Dict]:
|
| 248 |
-
"""Detect Double Top pattern"""
|
| 249 |
-
patterns = []
|
| 250 |
-
|
| 251 |
-
for i in range(len(peaks) - 1):
|
| 252 |
-
peak1 = peaks[i]
|
| 253 |
-
peak2 = peaks[i + 1]
|
| 254 |
-
|
| 255 |
-
# Check if peaks are similar in height
|
| 256 |
-
if abs(prices[peak1] - prices[peak2]) / prices[peak1] < 0.02:
|
| 257 |
-
patterns.append({
|
| 258 |
-
'pattern': 'Double Top',
|
| 259 |
-
'signal': 'Bearish Reversal',
|
| 260 |
-
'first_top': {'date': dates[peak1], 'price': prices[peak1]},
|
| 261 |
-
'second_top': {'date': dates[peak2], 'price': prices[peak2]}
|
| 262 |
-
})
|
| 263 |
-
|
| 264 |
-
return patterns
|
| 265 |
-
|
| 266 |
-
def _find_double_bottom(self, prices: np.ndarray, troughs: np.ndarray, dates: pd.DatetimeIndex) -> List[Dict]:
|
| 267 |
-
"""Detect Double Bottom pattern"""
|
| 268 |
-
patterns = []
|
| 269 |
-
|
| 270 |
-
for i in range(len(troughs) - 1):
|
| 271 |
-
trough1 = troughs[i]
|
| 272 |
-
trough2 = troughs[i + 1]
|
| 273 |
-
|
| 274 |
-
if abs(prices[trough1] - prices[trough2]) / prices[trough1] < 0.02:
|
| 275 |
-
patterns.append({
|
| 276 |
-
'pattern': 'Double Bottom',
|
| 277 |
-
'signal': 'Bullish Reversal',
|
| 278 |
-
'first_bottom': {'date': dates[trough1], 'price': prices[trough1]},
|
| 279 |
-
'second_bottom': {'date': dates[trough2], 'price': prices[trough2]}
|
| 280 |
-
})
|
| 281 |
-
|
| 282 |
-
return patterns
|
| 283 |
-
|
| 284 |
-
def _find_triangles(self, prices: np.ndarray, peaks: np.ndarray, troughs: np.ndarray, dates: pd.DatetimeIndex) -> List[Dict]:
|
| 285 |
-
"""Detect triangle patterns (ascending, descending, symmetrical)"""
|
| 286 |
-
patterns = []
|
| 287 |
-
|
| 288 |
-
if len(peaks) >= 2 and len(troughs) >= 2:
|
| 289 |
-
# Simple triangle detection based on trendlines
|
| 290 |
-
peak_slope = (prices[peaks[-1]] - prices[peaks[0]]) / (peaks[-1] - peaks[0])
|
| 291 |
-
trough_slope = (prices[troughs[-1]] - prices[troughs[0]]) / (troughs[-1] - troughs[0])
|
| 292 |
-
|
| 293 |
-
if abs(peak_slope) < 0.001 and trough_slope > 0:
|
| 294 |
-
patterns.append({
|
| 295 |
-
'pattern': 'Ascending Triangle',
|
| 296 |
-
'signal': 'Bullish Continuation'
|
| 297 |
-
})
|
| 298 |
-
elif abs(trough_slope) < 0.001 and peak_slope < 0:
|
| 299 |
-
patterns.append({
|
| 300 |
-
'pattern': 'Descending Triangle',
|
| 301 |
-
'signal': 'Bearish Continuation'
|
| 302 |
-
})
|
| 303 |
-
elif peak_slope < 0 and trough_slope > 0:
|
| 304 |
-
patterns.append({
|
| 305 |
-
'pattern': 'Symmetrical Triangle',
|
| 306 |
-
'signal': 'Breakout Expected'
|
| 307 |
-
})
|
| 308 |
-
|
| 309 |
-
return patterns
|
| 310 |
-
|
| 311 |
-
def ml_pattern_clustering(self, price_windows: np.ndarray, n_clusters: int = 5) -> Dict:
|
| 312 |
-
"""
|
| 313 |
-
Use ML to cluster similar price patterns using DTW
|
| 314 |
-
|
| 315 |
-
Args:
|
| 316 |
-
price_windows: Array of price windows (n_samples, window_size)
|
| 317 |
-
n_clusters: Number of clusters
|
| 318 |
-
|
| 319 |
-
Returns:
|
| 320 |
-
Dictionary with cluster information
|
| 321 |
-
"""
|
| 322 |
-
# Normalize windows
|
| 323 |
-
normalized_windows = []
|
| 324 |
-
for window in price_windows:
|
| 325 |
-
if len(window) > 0 and np.std(window) > 0:
|
| 326 |
-
norm_window = (window - np.mean(window)) / np.std(window)
|
| 327 |
-
normalized_windows.append(norm_window)
|
| 328 |
-
|
| 329 |
-
if len(normalized_windows) == 0:
|
| 330 |
-
return {}
|
| 331 |
-
|
| 332 |
-
normalized_windows = np.array(normalized_windows)
|
| 333 |
-
|
| 334 |
-
# Time series K-means with DTW
|
| 335 |
-
self.cluster_model = TimeSeriesKMeans(
|
| 336 |
-
n_clusters=n_clusters,
|
| 337 |
-
metric="dtw",
|
| 338 |
-
max_iter=10,
|
| 339 |
-
random_state=config.ML_RANDOM_STATE
|
| 340 |
-
)
|
| 341 |
-
|
| 342 |
-
labels = self.cluster_model.fit_predict(normalized_windows)
|
| 343 |
-
|
| 344 |
-
# Analyze clusters
|
| 345 |
-
cluster_info = {}
|
| 346 |
-
for cluster_id in range(n_clusters):
|
| 347 |
-
cluster_windows = normalized_windows[labels == cluster_id]
|
| 348 |
-
|
| 349 |
-
if len(cluster_windows) > 0:
|
| 350 |
-
cluster_info[f'cluster_{cluster_id}'] = {
|
| 351 |
-
'size': int(np.sum(labels == cluster_id)),
|
| 352 |
-
'centroid': self.cluster_model.cluster_centers_[cluster_id].flatten().tolist(),
|
| 353 |
-
'avg_shape': np.mean(cluster_windows, axis=0).tolist()
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
self.clusters = {
|
| 357 |
-
'labels': labels.tolist(),
|
| 358 |
-
'cluster_info': cluster_info,
|
| 359 |
-
'n_clusters': n_clusters
|
| 360 |
-
}
|
| 361 |
-
|
| 362 |
-
return self.clusters
|
| 363 |
-
|
| 364 |
-
def find_similar_patterns(self, current_window: np.ndarray, historical_windows: np.ndarray, top_n: int = 10) -> List[Dict]:
|
| 365 |
-
"""
|
| 366 |
-
Find historical patterns similar to current pattern using DTW
|
| 367 |
-
|
| 368 |
-
Args:
|
| 369 |
-
current_window: Current price pattern
|
| 370 |
-
historical_windows: Historical price patterns
|
| 371 |
-
top_n: Number of similar patterns to return
|
| 372 |
-
|
| 373 |
-
Returns:
|
| 374 |
-
List of similar patterns with distances
|
| 375 |
-
"""
|
| 376 |
-
# Normalize current window
|
| 377 |
-
if np.std(current_window) > 0:
|
| 378 |
-
norm_current = (current_window - np.mean(current_window)) / np.std(current_window)
|
| 379 |
-
else:
|
| 380 |
-
norm_current = current_window
|
| 381 |
-
|
| 382 |
-
# Calculate DTW distances
|
| 383 |
-
distances = []
|
| 384 |
-
for i, hist_window in enumerate(historical_windows):
|
| 385 |
-
if np.std(hist_window) > 0:
|
| 386 |
-
norm_hist = (hist_window - np.mean(hist_window)) / np.std(hist_window)
|
| 387 |
-
else:
|
| 388 |
-
norm_hist = hist_window
|
| 389 |
-
|
| 390 |
-
distance = dtw(norm_current, norm_hist)
|
| 391 |
-
distances.append({'index': i, 'distance': float(distance)})
|
| 392 |
-
|
| 393 |
-
# Sort by distance and return top N
|
| 394 |
-
distances.sort(key=lambda x: x['distance'])
|
| 395 |
-
return distances[:top_n]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/pdf_report.py
DELETED
|
@@ -1,238 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
PDF Report Generation Module
|
| 3 |
-
"""
|
| 4 |
-
from reportlab.lib.pagesizes import A4, letter
|
| 5 |
-
from reportlab.lib import colors
|
| 6 |
-
from reportlab.lib.units import inch
|
| 7 |
-
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak, Image
|
| 8 |
-
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 9 |
-
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
| 10 |
-
from datetime import datetime
|
| 11 |
-
import os
|
| 12 |
-
from typing import Dict, List
|
| 13 |
-
import numpy as np
|
| 14 |
-
|
| 15 |
-
class PDFReportGenerator:
|
| 16 |
-
"""
|
| 17 |
-
Generate comprehensive PDF reports with charts and analysis
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
def __init__(self, filename: str = None):
|
| 21 |
-
if filename is None:
|
| 22 |
-
filename = f"analysis_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
| 23 |
-
|
| 24 |
-
self.filename = filename
|
| 25 |
-
self.doc = SimpleDocTemplate(
|
| 26 |
-
filename,
|
| 27 |
-
pagesize=A4,
|
| 28 |
-
rightMargin=0.75*inch,
|
| 29 |
-
leftMargin=0.75*inch,
|
| 30 |
-
topMargin=1*inch,
|
| 31 |
-
bottomMargin=0.75*inch
|
| 32 |
-
)
|
| 33 |
-
self.story = []
|
| 34 |
-
self.styles = getSampleStyleSheet()
|
| 35 |
-
self._setup_custom_styles()
|
| 36 |
-
|
| 37 |
-
def _setup_custom_styles(self):
|
| 38 |
-
"""Setup custom paragraph styles"""
|
| 39 |
-
self.styles.add(ParagraphStyle(
|
| 40 |
-
name='CustomTitle',
|
| 41 |
-
parent=self.styles['Heading1'],
|
| 42 |
-
fontSize=24,
|
| 43 |
-
textColor=colors.HexColor('#1976d2'),
|
| 44 |
-
spaceAfter=30,
|
| 45 |
-
alignment=TA_CENTER
|
| 46 |
-
))
|
| 47 |
-
|
| 48 |
-
self.styles.add(ParagraphStyle(
|
| 49 |
-
name='SectionHeader',
|
| 50 |
-
parent=self.styles['Heading2'],
|
| 51 |
-
fontSize=16,
|
| 52 |
-
textColor=colors.HexColor('#1976d2'),
|
| 53 |
-
spaceAfter=12,
|
| 54 |
-
spaceBefore=12
|
| 55 |
-
))
|
| 56 |
-
|
| 57 |
-
def add_title_page(self, symbol: str, analysis_date: str):
|
| 58 |
-
"""Add title page"""
|
| 59 |
-
title = Paragraph(
|
| 60 |
-
f"<b>Quantitative Finance Analysis Report</b>",
|
| 61 |
-
self.styles['CustomTitle']
|
| 62 |
-
)
|
| 63 |
-
self.story.append(title)
|
| 64 |
-
self.story.append(Spacer(1, 0.3*inch))
|
| 65 |
-
|
| 66 |
-
subtitle = Paragraph(
|
| 67 |
-
f"<b>Symbol:</b> {symbol}<br/>"
|
| 68 |
-
f"<b>Analysis Date:</b> {analysis_date}<br/>"
|
| 69 |
-
f"<b>Report Generated:</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
| 70 |
-
self.styles['Normal']
|
| 71 |
-
)
|
| 72 |
-
self.story.append(subtitle)
|
| 73 |
-
self.story.append(PageBreak())
|
| 74 |
-
|
| 75 |
-
def add_executive_summary(self, summary_data: Dict):
|
| 76 |
-
"""Add executive summary section"""
|
| 77 |
-
self.story.append(Paragraph("Executive Summary", self.styles['SectionHeader']))
|
| 78 |
-
|
| 79 |
-
summary_text = f"""
|
| 80 |
-
<b>Current Price:</b> ${summary_data.get('current_price', 0):.2f}<br/>
|
| 81 |
-
<b>Market Regime:</b> {summary_data.get('market_regime', 'N/A')}<br/>
|
| 82 |
-
<b>Volatility:</b> {summary_data.get('volatility', 0):.2%}<br/>
|
| 83 |
-
<b>Momentum Status:</b> {summary_data.get('momentum_status', 'N/A')}<br/>
|
| 84 |
-
<b>ML Prediction Accuracy:</b> {summary_data.get('ml_r2', 0):.2%}<br/>
|
| 85 |
-
"""
|
| 86 |
-
|
| 87 |
-
self.story.append(Paragraph(summary_text, self.styles['Normal']))
|
| 88 |
-
self.story.append(Spacer(1, 0.2*inch))
|
| 89 |
-
|
| 90 |
-
def add_frequency_analysis(self, spectral_data: Dict):
|
| 91 |
-
"""Add spectral analysis section"""
|
| 92 |
-
self.story.append(Paragraph("Multi-Frequency Spectral Analysis", self.styles['SectionHeader']))
|
| 93 |
-
|
| 94 |
-
for band_name, band_data in spectral_data.items():
|
| 95 |
-
self.story.append(Paragraph(f"<b>{band_name.upper()} Frequency Band</b>", self.styles['Heading3']))
|
| 96 |
-
|
| 97 |
-
# Dominant frequencies table
|
| 98 |
-
if 'dominant_frequencies' in band_data and band_data['dominant_frequencies']:
|
| 99 |
-
freq_data = [['Frequency (cycles/day)', 'Period (days)', 'Amplitude', 'Significance']]
|
| 100 |
-
|
| 101 |
-
for freq in band_data['dominant_frequencies'][:3]:
|
| 102 |
-
freq_data.append([
|
| 103 |
-
f"{freq['frequency']:.4f}",
|
| 104 |
-
f"{freq['period_days']:.1f}",
|
| 105 |
-
f"{freq['amplitude']:.0f}",
|
| 106 |
-
f"{freq['significance']:.2%}"
|
| 107 |
-
])
|
| 108 |
-
|
| 109 |
-
table = Table(freq_data)
|
| 110 |
-
table.setStyle(TableStyle([
|
| 111 |
-
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
| 112 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
| 113 |
-
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 114 |
-
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 115 |
-
('FONTSIZE', (0, 0), (-1, 0), 10),
|
| 116 |
-
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
| 117 |
-
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
|
| 118 |
-
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
| 119 |
-
]))
|
| 120 |
-
|
| 121 |
-
self.story.append(table)
|
| 122 |
-
self.story.append(Spacer(1, 0.2*inch))
|
| 123 |
-
|
| 124 |
-
def add_monte_carlo_results(self, mc_stats: Dict):
|
| 125 |
-
"""Add Monte Carlo simulation results"""
|
| 126 |
-
self.story.append(Paragraph("Monte Carlo Simulation Results", self.styles['SectionHeader']))
|
| 127 |
-
|
| 128 |
-
for model_name, stats in mc_stats.items():
|
| 129 |
-
self.story.append(Paragraph(f"<b>{model_name.upper()} Model</b>", self.styles['Heading3']))
|
| 130 |
-
|
| 131 |
-
stats_text = f"""
|
| 132 |
-
<b>Expected Final Price:</b> ${stats.get('mean_final_price', 0):.2f}<br/>
|
| 133 |
-
<b>Median Price:</b> ${stats.get('median_final_price', 0):.2f}<br/>
|
| 134 |
-
<b>95% Confidence Interval:</b> ${stats.get('percentile_5', 0):.2f} - ${stats.get('percentile_95', 0):.2f}<br/>
|
| 135 |
-
<b>Probability of Profit:</b> {stats.get('prob_profit', 0):.2%}<br/>
|
| 136 |
-
<b>Expected Return:</b> {stats.get('expected_return', 0):.2%}<br/>
|
| 137 |
-
<b>Value at Risk (95%):</b> ${stats.get('var_95', 0):.2f}<br/>
|
| 138 |
-
<b>Conditional VaR:</b> ${stats.get('cvar_95', 0):.2f}<br/>
|
| 139 |
-
"""
|
| 140 |
-
|
| 141 |
-
self.story.append(Paragraph(stats_text, self.styles['Normal']))
|
| 142 |
-
self.story.append(Spacer(1, 0.15*inch))
|
| 143 |
-
|
| 144 |
-
def add_bayesian_analysis(self, bayesian_data: Dict):
|
| 145 |
-
"""Add Bayesian analysis results"""
|
| 146 |
-
self.story.append(Paragraph("Bayesian Inference Results", self.styles['SectionHeader']))
|
| 147 |
-
|
| 148 |
-
if 'regime_probabilities' in bayesian_data:
|
| 149 |
-
probs = bayesian_data['regime_probabilities']
|
| 150 |
-
|
| 151 |
-
regime_text = f"""
|
| 152 |
-
<b>Market Regime Probabilities:</b><br/>
|
| 153 |
-
• Range-Bound: {probs.get('range_bound', 0):.1%}<br/>
|
| 154 |
-
• Trending: {probs.get('trending', 0):.1%}<br/>
|
| 155 |
-
• High Volatility: {probs.get('high_volatility', 0):.1%}<br/>
|
| 156 |
-
"""
|
| 157 |
-
|
| 158 |
-
self.story.append(Paragraph(regime_text, self.styles['Normal']))
|
| 159 |
-
|
| 160 |
-
if 'optimal_params' in bayesian_data:
|
| 161 |
-
self.story.append(Paragraph("<b>Optimized Parameters (Bayesian):</b>", self.styles['Heading3']))
|
| 162 |
-
|
| 163 |
-
params = bayesian_data['optimal_params']
|
| 164 |
-
param_data = [['Parameter', 'Mean', 'Std Dev', '95% HDI']]
|
| 165 |
-
|
| 166 |
-
for param_name, param_info in params.items():
|
| 167 |
-
hdi = param_info.get('hdi_95', (0, 0))
|
| 168 |
-
param_data.append([
|
| 169 |
-
param_name.replace('_', ' ').title(),
|
| 170 |
-
f"{param_info.get('mean', 0):.4f}",
|
| 171 |
-
f"{param_info.get('std', 0):.4f}",
|
| 172 |
-
f"[{hdi[0]:.4f}, {hdi[1]:.4f}]"
|
| 173 |
-
])
|
| 174 |
-
|
| 175 |
-
table = Table(param_data)
|
| 176 |
-
table.setStyle(TableStyle([
|
| 177 |
-
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
| 178 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
| 179 |
-
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 180 |
-
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 181 |
-
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
| 182 |
-
]))
|
| 183 |
-
|
| 184 |
-
self.story.append(table)
|
| 185 |
-
|
| 186 |
-
def add_pattern_detection(self, patterns: List[Dict]):
|
| 187 |
-
"""Add detected patterns"""
|
| 188 |
-
self.story.append(Paragraph("Pattern Recognition Results", self.styles['SectionHeader']))
|
| 189 |
-
|
| 190 |
-
if not patterns:
|
| 191 |
-
self.story.append(Paragraph("No significant patterns detected in the analysis period.",
|
| 192 |
-
self.styles['Normal']))
|
| 193 |
-
return
|
| 194 |
-
|
| 195 |
-
# Group by pattern type
|
| 196 |
-
pattern_counts = {}
|
| 197 |
-
for p in patterns:
|
| 198 |
-
ptype = p.get('pattern', 'Unknown')
|
| 199 |
-
pattern_counts[ptype] = pattern_counts.get(ptype, 0) + 1
|
| 200 |
-
|
| 201 |
-
pattern_text = "<b>Detected Patterns:</b><br/>"
|
| 202 |
-
for ptype, count in pattern_counts.items():
|
| 203 |
-
pattern_text += f"• {ptype}: {count} occurrence(s)<br/>"
|
| 204 |
-
|
| 205 |
-
self.story.append(Paragraph(pattern_text, self.styles['Normal']))
|
| 206 |
-
|
| 207 |
-
def add_chart_image(self, image_path: str, width: float = 6*inch):
|
| 208 |
-
"""Add chart image to report"""
|
| 209 |
-
if os.path.exists(image_path):
|
| 210 |
-
img = Image(image_path, width=width, height=width*0.6)
|
| 211 |
-
self.story.append(img)
|
| 212 |
-
self.story.append(Spacer(1, 0.2*inch))
|
| 213 |
-
|
| 214 |
-
def add_risk_disclaimer(self):
|
| 215 |
-
"""Add risk disclaimer"""
|
| 216 |
-
self.story.append(PageBreak())
|
| 217 |
-
self.story.append(Paragraph("Risk Disclaimer", self.styles['SectionHeader']))
|
| 218 |
-
|
| 219 |
-
disclaimer = """
|
| 220 |
-
<b>Important Notice:</b> This report is for informational and educational purposes only.
|
| 221 |
-
It does not constitute financial advice, investment recommendations, or an offer to buy or sell securities.
|
| 222 |
-
<br/><br/>
|
| 223 |
-
Past performance is not indicative of future results. All investments carry risk, including potential loss of principal.
|
| 224 |
-
The models and predictions presented in this report are based on historical data and assumptions that may not hold in the future.
|
| 225 |
-
<br/><br/>
|
| 226 |
-
Always conduct your own research and consult with a qualified financial advisor before making investment decisions.
|
| 227 |
-
<br/><br/>
|
| 228 |
-
<b>Model Limitations:</b> The quantitative models used in this analysis have inherent limitations and may not account
|
| 229 |
-
for all market factors, unexpected events, or regime changes. Results should be interpreted with appropriate caution.
|
| 230 |
-
"""
|
| 231 |
-
|
| 232 |
-
self.story.append(Paragraph(disclaimer, self.styles['Normal']))
|
| 233 |
-
|
| 234 |
-
def generate(self):
|
| 235 |
-
"""Generate the PDF report"""
|
| 236 |
-
self.add_risk_disclaimer()
|
| 237 |
-
self.doc.build(self.story)
|
| 238 |
-
return self.filename
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/spectral_analyzer.py
DELETED
|
@@ -1,262 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Advanced spectral analysis with multi-frequency decomposition
|
| 3 |
-
"""
|
| 4 |
-
import numpy as np
|
| 5 |
-
import pandas as pd
|
| 6 |
-
from scipy import signal
|
| 7 |
-
from scipy.fft import fft, fftfreq
|
| 8 |
-
from scipy.stats import norm
|
| 9 |
-
from typing import Dict, List, Tuple
|
| 10 |
-
from src.config import config, FrequencyBand
|
| 11 |
-
|
| 12 |
-
class SpectralAnalyzer:
|
| 13 |
-
"""
|
| 14 |
-
Multi-frequency spectral analysis engine
|
| 15 |
-
Decomposes price signals into Low, Mid, and High frequency bands
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
def __init__(self):
|
| 19 |
-
self.original_prices = None
|
| 20 |
-
self.filtered_bands = {} # Stores filtered prices for each band
|
| 21 |
-
self.spectrum_bands = {} # Stores spectrum for each band
|
| 22 |
-
self.frequencies_bands = {} # Stores frequencies for each band
|
| 23 |
-
self.dominant_frequencies = {} # Stores dominant freqs per band
|
| 24 |
-
|
| 25 |
-
def analyze(self, prices: np.ndarray) -> Dict:
|
| 26 |
-
"""
|
| 27 |
-
Perform complete spectral analysis across all frequency bands
|
| 28 |
-
|
| 29 |
-
Args:
|
| 30 |
-
prices: Array of price data
|
| 31 |
-
|
| 32 |
-
Returns:
|
| 33 |
-
Dictionary containing analysis results for all bands
|
| 34 |
-
"""
|
| 35 |
-
self.original_prices = prices
|
| 36 |
-
results = {}
|
| 37 |
-
|
| 38 |
-
for band_name, band_config in config.FREQUENCY_BANDS.items():
|
| 39 |
-
# Apply band-specific filtering
|
| 40 |
-
filtered = self._apply_bandpass_filter(prices, band_config)
|
| 41 |
-
self.filtered_bands[band_name] = filtered
|
| 42 |
-
|
| 43 |
-
# Perform spectral analysis
|
| 44 |
-
spectrum, frequencies = self._perform_fft(filtered)
|
| 45 |
-
self.spectrum_bands[band_name] = spectrum
|
| 46 |
-
self.frequencies_bands[band_name] = frequencies
|
| 47 |
-
|
| 48 |
-
# Find dominant frequencies
|
| 49 |
-
dominant = self._find_dominant_frequencies(
|
| 50 |
-
spectrum, frequencies, band_config, top_n=5
|
| 51 |
-
)
|
| 52 |
-
self.dominant_frequencies[band_name] = dominant
|
| 53 |
-
|
| 54 |
-
results[band_name] = {
|
| 55 |
-
'filtered_prices': filtered,
|
| 56 |
-
'spectrum': spectrum,
|
| 57 |
-
'frequencies': frequencies,
|
| 58 |
-
'dominant_frequencies': dominant,
|
| 59 |
-
'band_config': band_config
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
return results
|
| 63 |
-
|
| 64 |
-
def _apply_bandpass_filter(self, prices: np.ndarray, band: FrequencyBand) -> np.ndarray:
|
| 65 |
-
"""
|
| 66 |
-
Apply butterworth bandpass filter for specific frequency band
|
| 67 |
-
|
| 68 |
-
Args:
|
| 69 |
-
prices: Input price series
|
| 70 |
-
band: Frequency band configuration
|
| 71 |
-
|
| 72 |
-
Returns:
|
| 73 |
-
Filtered price series
|
| 74 |
-
"""
|
| 75 |
-
nyquist = 0.5
|
| 76 |
-
low_cutoff = band.cutoff_range[0] / nyquist
|
| 77 |
-
high_cutoff = band.cutoff_range[1] / nyquist
|
| 78 |
-
|
| 79 |
-
# Ensure cutoffs are in valid range (0, 1)
|
| 80 |
-
low_cutoff = max(0.001, min(0.999, low_cutoff))
|
| 81 |
-
high_cutoff = max(0.001, min(0.999, high_cutoff))
|
| 82 |
-
|
| 83 |
-
if low_cutoff >= high_cutoff:
|
| 84 |
-
# Use lowpass filter instead
|
| 85 |
-
b, a = signal.butter(band.filter_order, high_cutoff, btype='low')
|
| 86 |
-
else:
|
| 87 |
-
b, a = signal.butter(band.filter_order, [low_cutoff, high_cutoff], btype='band')
|
| 88 |
-
|
| 89 |
-
filtered = signal.filtfilt(b, a, prices)
|
| 90 |
-
return filtered
|
| 91 |
-
|
| 92 |
-
def _perform_fft(self, prices: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
| 93 |
-
"""
|
| 94 |
-
Perform Fast Fourier Transform
|
| 95 |
-
|
| 96 |
-
Returns:
|
| 97 |
-
Tuple of (spectrum, frequencies)
|
| 98 |
-
"""
|
| 99 |
-
n = len(prices)
|
| 100 |
-
spectrum = fft(prices)
|
| 101 |
-
frequencies = fftfreq(n, d=1.0)
|
| 102 |
-
|
| 103 |
-
# Keep only positive frequencies
|
| 104 |
-
positive_mask = frequencies > 0
|
| 105 |
-
spectrum = spectrum[positive_mask]
|
| 106 |
-
frequencies = frequencies[positive_mask]
|
| 107 |
-
|
| 108 |
-
return spectrum, frequencies
|
| 109 |
-
|
| 110 |
-
def _find_dominant_frequencies(
|
| 111 |
-
self,
|
| 112 |
-
spectrum: np.ndarray,
|
| 113 |
-
frequencies: np.ndarray,
|
| 114 |
-
band: FrequencyBand,
|
| 115 |
-
top_n: int = 5
|
| 116 |
-
) -> List[Dict]:
|
| 117 |
-
"""
|
| 118 |
-
Identify dominant frequencies with statistical significance
|
| 119 |
-
|
| 120 |
-
Returns:
|
| 121 |
-
List of dominant frequency components
|
| 122 |
-
"""
|
| 123 |
-
magnitude = np.abs(spectrum)
|
| 124 |
-
|
| 125 |
-
# Estimate noise floor
|
| 126 |
-
noise_floor = np.percentile(magnitude, 75)
|
| 127 |
-
|
| 128 |
-
# Find significant frequencies
|
| 129 |
-
significant_mask = magnitude > noise_floor * 1.5
|
| 130 |
-
significant_freqs = frequencies[significant_mask]
|
| 131 |
-
significant_mags = magnitude[significant_mask]
|
| 132 |
-
|
| 133 |
-
if len(significant_freqs) == 0:
|
| 134 |
-
significant_freqs = frequencies
|
| 135 |
-
significant_mags = magnitude
|
| 136 |
-
|
| 137 |
-
# Get top N by magnitude
|
| 138 |
-
top_indices = np.argsort(significant_mags)[-top_n:][::-1]
|
| 139 |
-
|
| 140 |
-
dominant_freqs = []
|
| 141 |
-
for idx in top_indices:
|
| 142 |
-
freq = significant_freqs[idx]
|
| 143 |
-
amplitude = significant_mags[idx]
|
| 144 |
-
period_days = 1.0 / freq if freq != 0 else np.inf
|
| 145 |
-
|
| 146 |
-
# Get phase
|
| 147 |
-
phase_idx = np.where(frequencies == freq)[0]
|
| 148 |
-
if len(phase_idx) > 0:
|
| 149 |
-
phase = np.angle(spectrum[phase_idx[0]])
|
| 150 |
-
else:
|
| 151 |
-
phase = 0.0
|
| 152 |
-
|
| 153 |
-
# Calculate statistical significance
|
| 154 |
-
z_score = (amplitude - noise_floor) / (np.std(magnitude) + 1e-10)
|
| 155 |
-
p_value = 1 - norm.cdf(z_score)
|
| 156 |
-
significance = 1 - p_value
|
| 157 |
-
|
| 158 |
-
dominant_freqs.append({
|
| 159 |
-
'frequency': float(freq),
|
| 160 |
-
'period_days': float(period_days),
|
| 161 |
-
'amplitude': float(amplitude),
|
| 162 |
-
'phase': float(phase),
|
| 163 |
-
'significance': float(significance),
|
| 164 |
-
'z_score': float(z_score),
|
| 165 |
-
'band': band.name
|
| 166 |
-
})
|
| 167 |
-
|
| 168 |
-
return dominant_freqs
|
| 169 |
-
|
| 170 |
-
def calculate_band_momentum(self, band_name: str, window_size: int = 30) -> np.ndarray:
|
| 171 |
-
"""
|
| 172 |
-
Calculate momentum signal for specific frequency band
|
| 173 |
-
|
| 174 |
-
Args:
|
| 175 |
-
band_name: Name of frequency band ('low', 'mid', 'high')
|
| 176 |
-
window_size: Rolling window for momentum calculation
|
| 177 |
-
|
| 178 |
-
Returns:
|
| 179 |
-
Momentum signal array
|
| 180 |
-
"""
|
| 181 |
-
if band_name not in self.filtered_bands:
|
| 182 |
-
raise ValueError(f"Band '{band_name}' not analyzed yet")
|
| 183 |
-
|
| 184 |
-
filtered_prices = self.filtered_bands[band_name]
|
| 185 |
-
dominant_freqs = self.dominant_frequencies[band_name]
|
| 186 |
-
|
| 187 |
-
if not dominant_freqs:
|
| 188 |
-
return self._simple_momentum(filtered_prices, window_size)
|
| 189 |
-
|
| 190 |
-
momentum_values = []
|
| 191 |
-
|
| 192 |
-
# Calculate weights based on amplitude and significance
|
| 193 |
-
total_weight = sum(f['amplitude'] * f['significance'] for f in dominant_freqs[:3])
|
| 194 |
-
weights = [(f['amplitude'] * f['significance']) / total_weight for f in dominant_freqs[:3]]
|
| 195 |
-
|
| 196 |
-
for i in range(window_size, len(filtered_prices)):
|
| 197 |
-
window_data = filtered_prices[i-window_size:i]
|
| 198 |
-
window_spectrum = fft(window_data)
|
| 199 |
-
window_freqs = fftfreq(len(window_data), d=1.0)
|
| 200 |
-
|
| 201 |
-
weighted_momentum = 0
|
| 202 |
-
for j, freq_info in enumerate(dominant_freqs[:3]):
|
| 203 |
-
closest_idx = np.argmin(np.abs(window_freqs - freq_info['frequency']))
|
| 204 |
-
phase = np.angle(window_spectrum[closest_idx])
|
| 205 |
-
|
| 206 |
-
# Calculate distance to next peak
|
| 207 |
-
next_peak_distance = (2*np.pi - phase) / (2*np.pi)
|
| 208 |
-
freq_momentum = np.cos(phase) * (1 - next_peak_distance)
|
| 209 |
-
|
| 210 |
-
weighted_momentum += freq_momentum * weights[j]
|
| 211 |
-
|
| 212 |
-
momentum_values.append(weighted_momentum)
|
| 213 |
-
|
| 214 |
-
# Prepend zeros for initial window
|
| 215 |
-
momentum_signal = np.concatenate([
|
| 216 |
-
np.zeros(window_size),
|
| 217 |
-
np.array(momentum_values)
|
| 218 |
-
])
|
| 219 |
-
|
| 220 |
-
return momentum_signal
|
| 221 |
-
|
| 222 |
-
def _simple_momentum(self, prices: np.ndarray, window_size: int) -> np.ndarray:
|
| 223 |
-
"""Fallback simple momentum calculation"""
|
| 224 |
-
momentum = np.zeros(len(prices))
|
| 225 |
-
|
| 226 |
-
for i in range(window_size, len(prices)):
|
| 227 |
-
window = prices[i-window_size:i]
|
| 228 |
-
momentum[i] = (prices[i] - np.mean(window)) / (np.std(window) + 1e-10)
|
| 229 |
-
|
| 230 |
-
return momentum
|
| 231 |
-
|
| 232 |
-
def get_multi_band_momentum(self, window_size: int = 30) -> Dict[str, np.ndarray]:
|
| 233 |
-
"""
|
| 234 |
-
Calculate momentum for all frequency bands
|
| 235 |
-
|
| 236 |
-
Returns:
|
| 237 |
-
Dictionary mapping band names to momentum signals
|
| 238 |
-
"""
|
| 239 |
-
momentum_signals = {}
|
| 240 |
-
|
| 241 |
-
for band_name in self.filtered_bands.keys():
|
| 242 |
-
momentum_signals[band_name] = self.calculate_band_momentum(band_name, window_size)
|
| 243 |
-
|
| 244 |
-
return momentum_signals
|
| 245 |
-
|
| 246 |
-
def get_composite_momentum(self, window_size: int = 30) -> np.ndarray:
|
| 247 |
-
"""
|
| 248 |
-
Calculate composite momentum combining all frequency bands
|
| 249 |
-
|
| 250 |
-
Returns:
|
| 251 |
-
Composite momentum signal
|
| 252 |
-
"""
|
| 253 |
-
band_momentums = self.get_multi_band_momentum(window_size)
|
| 254 |
-
|
| 255 |
-
# Weight bands: Low=0.5, Mid=0.3, High=0.2 (emphasize longer-term trends)
|
| 256 |
-
weights = {'low': 0.5, 'mid': 0.3, 'high': 0.2}
|
| 257 |
-
|
| 258 |
-
composite = np.zeros(len(self.original_prices))
|
| 259 |
-
for band_name, momentum in band_momentums.items():
|
| 260 |
-
composite += momentum * weights.get(band_name, 0.33)
|
| 261 |
-
|
| 262 |
-
return composite
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/visualization.py
DELETED
|
@@ -1,508 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Advanced visualization module using Plotly
|
| 3 |
-
"""
|
| 4 |
-
import plotly.graph_objects as go
|
| 5 |
-
from plotly.subplots import make_subplots
|
| 6 |
-
import numpy as np
|
| 7 |
-
import pandas as pd
|
| 8 |
-
from typing import Dict, List, Tuple
|
| 9 |
-
from src.config import config
|
| 10 |
-
|
| 11 |
-
class Visualizer:
|
| 12 |
-
"""Create professional interactive visualizations"""
|
| 13 |
-
|
| 14 |
-
def __init__(self):
|
| 15 |
-
self.figures = {}
|
| 16 |
-
|
| 17 |
-
def create_price_decomposition_chart(
|
| 18 |
-
self,
|
| 19 |
-
dates: pd.DatetimeIndex,
|
| 20 |
-
original_prices: np.ndarray,
|
| 21 |
-
spectral_results: Dict,
|
| 22 |
-
buy_signals: List[Dict] = None
|
| 23 |
-
) -> go.Figure:
|
| 24 |
-
"""
|
| 25 |
-
Chart 1: Price decomposition with anti-aliased bands and buy signals
|
| 26 |
-
"""
|
| 27 |
-
fig = go.Figure()
|
| 28 |
-
|
| 29 |
-
# Original price (raw)
|
| 30 |
-
fig.add_trace(go.Scatter(
|
| 31 |
-
x=dates,
|
| 32 |
-
y=original_prices,
|
| 33 |
-
name='Original Price',
|
| 34 |
-
line=dict(color='rgba(128,128,128,0.3)', width=1),
|
| 35 |
-
hovertemplate='%{x}<br>Price: $%{y:.2f}<extra></extra>'
|
| 36 |
-
))
|
| 37 |
-
|
| 38 |
-
# Anti-aliased filtered bands
|
| 39 |
-
colors = {
|
| 40 |
-
'low': '#0066CC', # Blue
|
| 41 |
-
'mid': '#00AA44', # Green
|
| 42 |
-
'high': '#EE4400' # Red
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
for band_name, result in spectral_results.items():
|
| 46 |
-
filtered = result['filtered_prices']
|
| 47 |
-
band_config = result['band_config']
|
| 48 |
-
|
| 49 |
-
fig.add_trace(go.Scatter(
|
| 50 |
-
x=dates,
|
| 51 |
-
y=filtered,
|
| 52 |
-
name=f'{band_config.name}',
|
| 53 |
-
line=dict(color=colors[band_name], width=2.5),
|
| 54 |
-
hovertemplate=f'{band_config.name}<br>%{{x}}<br>Filtered: $%{{y:.2f}}<extra></extra>'
|
| 55 |
-
))
|
| 56 |
-
|
| 57 |
-
# Add buy signals
|
| 58 |
-
if buy_signals:
|
| 59 |
-
signal_dates = [pd.to_datetime(s['date']) for s in buy_signals]
|
| 60 |
-
signal_prices = [s['price'] for s in buy_signals]
|
| 61 |
-
|
| 62 |
-
fig.add_trace(go.Scatter(
|
| 63 |
-
x=signal_dates,
|
| 64 |
-
y=signal_prices,
|
| 65 |
-
mode='markers',
|
| 66 |
-
name='Buy Signals',
|
| 67 |
-
marker=dict(
|
| 68 |
-
size=15,
|
| 69 |
-
color='lime',
|
| 70 |
-
symbol='triangle-up',
|
| 71 |
-
line=dict(color='darkgreen', width=2)
|
| 72 |
-
),
|
| 73 |
-
hovertemplate='BUY SIGNAL<br>%{x}<br>Price: $%{y:.2f}<extra></extra>'
|
| 74 |
-
))
|
| 75 |
-
|
| 76 |
-
fig.update_layout(
|
| 77 |
-
title='Multi-Frequency Price Decomposition with Anti-Aliasing',
|
| 78 |
-
xaxis_title='Date',
|
| 79 |
-
yaxis_title='Price ($)',
|
| 80 |
-
hovermode='x unified',
|
| 81 |
-
height=600,
|
| 82 |
-
template='plotly_white',
|
| 83 |
-
legend=dict(x=0.01, y=0.99, bgcolor='rgba(255,255,255,0.8)')
|
| 84 |
-
)
|
| 85 |
-
|
| 86 |
-
return fig
|
| 87 |
-
|
| 88 |
-
def create_frequency_spectrum_chart(self, spectral_results: Dict) -> go.Figure:
|
| 89 |
-
"""
|
| 90 |
-
Chart 2: Frequency spectrum with dominant frequencies highlighted
|
| 91 |
-
"""
|
| 92 |
-
fig = make_subplots(
|
| 93 |
-
rows=3, cols=1,
|
| 94 |
-
subplot_titles=['Low Frequency Spectrum', 'Mid Frequency Spectrum', 'High Frequency Spectrum'],
|
| 95 |
-
vertical_spacing=0.1
|
| 96 |
-
)
|
| 97 |
-
|
| 98 |
-
colors = {'low': '#0066CC', 'mid': '#00AA44', 'high': '#EE4400'}
|
| 99 |
-
row_map = {'low': 1, 'mid': 2, 'high': 3}
|
| 100 |
-
|
| 101 |
-
for band_name, result in spectral_results.items():
|
| 102 |
-
frequencies = result['frequencies']
|
| 103 |
-
magnitude = np.abs(result['spectrum'])
|
| 104 |
-
dominant_freqs = result['dominant_frequencies']
|
| 105 |
-
|
| 106 |
-
row = row_map[band_name]
|
| 107 |
-
|
| 108 |
-
# Spectrum line
|
| 109 |
-
fig.add_trace(go.Scatter(
|
| 110 |
-
x=frequencies,
|
| 111 |
-
y=magnitude,
|
| 112 |
-
name=f'{band_name.capitalize()} Spectrum',
|
| 113 |
-
line=dict(color=colors[band_name], width=2),
|
| 114 |
-
fill='tozeroy',
|
| 115 |
-
fillcolor=f'rgba{tuple(list(int(colors[band_name][i:i+2], 16) for i in (1, 3, 5)) + [0.3])}',
|
| 116 |
-
hovertemplate='Freq: %{x:.4f} cycles/day<br>Magnitude: %{y:.0f}<extra></extra>'
|
| 117 |
-
), row=row, col=1)
|
| 118 |
-
|
| 119 |
-
# Mark dominant frequencies
|
| 120 |
-
if dominant_freqs:
|
| 121 |
-
dom_freqs = [d['frequency'] for d in dominant_freqs[:3]]
|
| 122 |
-
dom_mags = [d['amplitude'] for d in dominant_freqs[:3]]
|
| 123 |
-
dom_periods = [d['period_days'] for d in dominant_freqs[:3]]
|
| 124 |
-
|
| 125 |
-
fig.add_trace(go.Scatter(
|
| 126 |
-
x=dom_freqs,
|
| 127 |
-
y=dom_mags,
|
| 128 |
-
mode='markers+text',
|
| 129 |
-
name=f'{band_name.capitalize()} Dominant',
|
| 130 |
-
marker=dict(size=12, color='red', symbol='star'),
|
| 131 |
-
text=[f'{p:.1f}d' for p in dom_periods],
|
| 132 |
-
textposition='top center',
|
| 133 |
-
hovertemplate='Period: %{text}<br>Freq: %{x:.4f}<br>Amplitude: %{y:.0f}<extra></extra>'
|
| 134 |
-
), row=row, col=1)
|
| 135 |
-
|
| 136 |
-
fig.update_xaxes(title_text='Frequency (cycles/day)')
|
| 137 |
-
fig.update_yaxes(title_text='Magnitude')
|
| 138 |
-
|
| 139 |
-
fig.update_layout(
|
| 140 |
-
height=900,
|
| 141 |
-
title_text='Frequency Domain Analysis - Dominant Cycles Identified',
|
| 142 |
-
showlegend=True,
|
| 143 |
-
template='plotly_white'
|
| 144 |
-
)
|
| 145 |
-
|
| 146 |
-
return fig
|
| 147 |
-
|
| 148 |
-
def create_phase_angle_chart(self, spectral_results: Dict, dates: pd.DatetimeIndex) -> go.Figure:
|
| 149 |
-
"""
|
| 150 |
-
Chart 3: Phase angle evolution over time
|
| 151 |
-
"""
|
| 152 |
-
fig = make_subplots(
|
| 153 |
-
rows=3, cols=1,
|
| 154 |
-
subplot_titles=['Low Frequency Phase', 'Mid Frequency Phase', 'High Frequency Phase'],
|
| 155 |
-
vertical_spacing=0.1
|
| 156 |
-
)
|
| 157 |
-
|
| 158 |
-
colors = {'low': '#0066CC', 'mid': '#00AA44', 'high': '#EE4400'}
|
| 159 |
-
row_map = {'low': 1, 'mid': 2, 'high': 3}
|
| 160 |
-
|
| 161 |
-
for band_name, result in spectral_results.items():
|
| 162 |
-
dominant_freqs = result['dominant_frequencies']
|
| 163 |
-
if not dominant_freqs:
|
| 164 |
-
continue
|
| 165 |
-
|
| 166 |
-
# Get the most dominant frequency
|
| 167 |
-
main_freq = dominant_freqs[0]
|
| 168 |
-
freq_val = main_freq['frequency']
|
| 169 |
-
period = main_freq['period_days']
|
| 170 |
-
|
| 171 |
-
row = row_map[band_name]
|
| 172 |
-
|
| 173 |
-
# Calculate phase angle over time
|
| 174 |
-
n_points = len(dates)
|
| 175 |
-
phase_angles = np.zeros(n_points)
|
| 176 |
-
|
| 177 |
-
for i in range(n_points):
|
| 178 |
-
phase_angles[i] = (main_freq['phase'] + 2 * np.pi * freq_val * i) % (2 * np.pi)
|
| 179 |
-
|
| 180 |
-
# Convert to degrees
|
| 181 |
-
phase_degrees = np.degrees(phase_angles)
|
| 182 |
-
|
| 183 |
-
# Phase angle line
|
| 184 |
-
fig.add_trace(go.Scatter(
|
| 185 |
-
x=dates,
|
| 186 |
-
y=phase_degrees,
|
| 187 |
-
name=f'{band_name.capitalize()} Phase',
|
| 188 |
-
line=dict(color=colors[band_name], width=2),
|
| 189 |
-
hovertemplate='%{x}<br>Phase: %{y:.1f}°<extra></extra>'
|
| 190 |
-
), row=row, col=1)
|
| 191 |
-
|
| 192 |
-
# Add horizontal lines for key phases
|
| 193 |
-
fig.add_hline(y=0, line_dash="dash", line_color="gray", opacity=0.5, row=row, col=1)
|
| 194 |
-
fig.add_hline(y=90, line_dash="dash", line_color="green", opacity=0.5, row=row, col=1)
|
| 195 |
-
fig.add_hline(y=180, line_dash="dash", line_color="gray", opacity=0.5, row=row, col=1)
|
| 196 |
-
fig.add_hline(y=270, line_dash="dash", line_color="red", opacity=0.5, row=row, col=1)
|
| 197 |
-
|
| 198 |
-
# Add annotations
|
| 199 |
-
fig.add_annotation(
|
| 200 |
-
x=dates[-1], y=phase_degrees[-1],
|
| 201 |
-
text=f"Period: {period:.1f}d",
|
| 202 |
-
showarrow=True,
|
| 203 |
-
arrowhead=2,
|
| 204 |
-
row=row, col=1
|
| 205 |
-
)
|
| 206 |
-
|
| 207 |
-
fig.update_xaxes(title_text='Date')
|
| 208 |
-
fig.update_yaxes(title_text='Phase Angle (degrees)', range=[0, 360])
|
| 209 |
-
|
| 210 |
-
fig.update_layout(
|
| 211 |
-
height=900,
|
| 212 |
-
title_text='Phase Angle Evolution - Cycle Timing Analysis',
|
| 213 |
-
template='plotly_white',
|
| 214 |
-
showlegend=True
|
| 215 |
-
)
|
| 216 |
-
|
| 217 |
-
return fig
|
| 218 |
-
|
| 219 |
-
def create_momentum_signals_chart(
|
| 220 |
-
self,
|
| 221 |
-
dates: pd.DatetimeIndex,
|
| 222 |
-
momentum_signals: Dict[str, np.ndarray],
|
| 223 |
-
buy_signals: List[Dict] = None
|
| 224 |
-
) -> go.Figure:
|
| 225 |
-
"""
|
| 226 |
-
Chart 4: Momentum signals across all frequency bands
|
| 227 |
-
"""
|
| 228 |
-
fig = go.Figure()
|
| 229 |
-
|
| 230 |
-
colors = {'low': '#0066CC', 'mid': '#00AA44', 'high': '#EE4400', 'composite': '#9933FF'}
|
| 231 |
-
|
| 232 |
-
for band_name, momentum in momentum_signals.items():
|
| 233 |
-
fig.add_trace(go.Scatter(
|
| 234 |
-
x=dates,
|
| 235 |
-
y=momentum,
|
| 236 |
-
name=f'{band_name.capitalize()} Momentum',
|
| 237 |
-
line=dict(color=colors.get(band_name, 'gray'), width=2),
|
| 238 |
-
hovertemplate=f'{band_name.capitalize()}<br>%{{x}}<br>Momentum: %{{y:.3f}}<extra></extra>'
|
| 239 |
-
))
|
| 240 |
-
|
| 241 |
-
# Zero line
|
| 242 |
-
fig.add_hline(y=0, line_dash="dash", line_color="black", opacity=0.5)
|
| 243 |
-
|
| 244 |
-
# Threshold lines
|
| 245 |
-
fig.add_hline(y=0.5, line_dash="dot", line_color="green", opacity=0.3, annotation_text="Bullish")
|
| 246 |
-
fig.add_hline(y=-0.5, line_dash="dot", line_color="red", opacity=0.3, annotation_text="Bearish")
|
| 247 |
-
|
| 248 |
-
# Mark buy signals
|
| 249 |
-
if buy_signals:
|
| 250 |
-
signal_dates = [pd.to_datetime(s['date']) for s in buy_signals]
|
| 251 |
-
signal_momentum = [s['momentum'] for s in buy_signals]
|
| 252 |
-
|
| 253 |
-
fig.add_trace(go.Scatter(
|
| 254 |
-
x=signal_dates,
|
| 255 |
-
y=signal_momentum,
|
| 256 |
-
mode='markers',
|
| 257 |
-
name='Buy Signal Points',
|
| 258 |
-
marker=dict(size=12, color='lime', symbol='star', line=dict(color='darkgreen', width=1)),
|
| 259 |
-
hovertemplate='BUY SIGNAL<br>%{x}<br>Momentum: %{y:.3f}<extra></extra>'
|
| 260 |
-
))
|
| 261 |
-
|
| 262 |
-
fig.update_layout(
|
| 263 |
-
title='Multi-Frequency Momentum Signals',
|
| 264 |
-
xaxis_title='Date',
|
| 265 |
-
yaxis_title='Momentum Value',
|
| 266 |
-
hovermode='x unified',
|
| 267 |
-
height=600,
|
| 268 |
-
template='plotly_white',
|
| 269 |
-
legend=dict(x=0.01, y=0.99, bgcolor='rgba(255,255,255,0.8)')
|
| 270 |
-
)
|
| 271 |
-
|
| 272 |
-
return fig
|
| 273 |
-
|
| 274 |
-
def create_monte_carlo_chart(self, mc_results: Dict, current_price: float) -> go.Figure:
|
| 275 |
-
"""
|
| 276 |
-
Chart 5: Monte Carlo simulation paths with confidence bands
|
| 277 |
-
"""
|
| 278 |
-
fig = go.Figure()
|
| 279 |
-
|
| 280 |
-
model_colors = {
|
| 281 |
-
'gbm': '#0066CC',
|
| 282 |
-
'jump_diffusion': '#FF6600',
|
| 283 |
-
'garch': '#00AA44',
|
| 284 |
-
'heston': '#9933FF'
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
for model_name, paths in mc_results.items():
|
| 288 |
-
if 'variance' in model_name:
|
| 289 |
-
continue
|
| 290 |
-
|
| 291 |
-
n_paths, n_days = paths.shape
|
| 292 |
-
x_days = list(range(n_days))
|
| 293 |
-
|
| 294 |
-
# Sample paths (show 50 for clarity)
|
| 295 |
-
sample_indices = np.random.choice(n_paths, min(50, n_paths), replace=False)
|
| 296 |
-
for idx in sample_indices:
|
| 297 |
-
fig.add_trace(go.Scatter(
|
| 298 |
-
x=x_days,
|
| 299 |
-
y=paths[idx],
|
| 300 |
-
mode='lines',
|
| 301 |
-
line=dict(color=model_colors.get(model_name, 'gray'), width=0.5),
|
| 302 |
-
opacity=0.15,
|
| 303 |
-
showlegend=False,
|
| 304 |
-
hoverinfo='skip'
|
| 305 |
-
))
|
| 306 |
-
|
| 307 |
-
# Mean path
|
| 308 |
-
mean_path = np.mean(paths, axis=0)
|
| 309 |
-
fig.add_trace(go.Scatter(
|
| 310 |
-
x=x_days,
|
| 311 |
-
y=mean_path,
|
| 312 |
-
name=f'{model_name.upper()} Mean',
|
| 313 |
-
line=dict(color=model_colors.get(model_name, 'gray'), width=3),
|
| 314 |
-
hovertemplate=f'{model_name.upper()}<br>Day: %{{x}}<br>Price: $%{{y:.2f}}<extra></extra>'
|
| 315 |
-
))
|
| 316 |
-
|
| 317 |
-
# Confidence bands (95%)
|
| 318 |
-
percentile_5 = np.percentile(paths, 5, axis=0)
|
| 319 |
-
percentile_95 = np.percentile(paths, 95, axis=0)
|
| 320 |
-
|
| 321 |
-
fig.add_trace(go.Scatter(
|
| 322 |
-
x=x_days + x_days[::-1],
|
| 323 |
-
y=percentile_95.tolist() + percentile_5.tolist()[::-1],
|
| 324 |
-
fill='toself',
|
| 325 |
-
fillcolor=f'rgba{tuple(list(int(model_colors.get(model_name, "#000000")[i:i+2], 16) for i in (1, 3, 5)) + [0.1])}',
|
| 326 |
-
line=dict(color='rgba(255,255,255,0)'),
|
| 327 |
-
name=f'{model_name.upper()} 95% CI',
|
| 328 |
-
showlegend=True,
|
| 329 |
-
hoverinfo='skip'
|
| 330 |
-
))
|
| 331 |
-
|
| 332 |
-
# Current price line
|
| 333 |
-
fig.add_hline(
|
| 334 |
-
y=current_price,
|
| 335 |
-
line_dash="dash",
|
| 336 |
-
line_color="black",
|
| 337 |
-
annotation_text=f"Current: ${current_price:.2f}",
|
| 338 |
-
annotation_position="right"
|
| 339 |
-
)
|
| 340 |
-
|
| 341 |
-
fig.update_layout(
|
| 342 |
-
title='Monte Carlo Price Simulations (4 Models, 10,000 Paths Each)',
|
| 343 |
-
xaxis_title='Days Forward',
|
| 344 |
-
yaxis_title='Price ($)',
|
| 345 |
-
height=700,
|
| 346 |
-
template='plotly_white',
|
| 347 |
-
hovermode='x unified',
|
| 348 |
-
legend=dict(x=0.01, y=0.99, bgcolor='rgba(255,255,255,0.8)')
|
| 349 |
-
)
|
| 350 |
-
|
| 351 |
-
return fig
|
| 352 |
-
|
| 353 |
-
def create_buy_signals_frequency_chart(
|
| 354 |
-
self,
|
| 355 |
-
buy_signals: List[Dict],
|
| 356 |
-
spectral_results: Dict,
|
| 357 |
-
dates: pd.DatetimeIndex
|
| 358 |
-
) -> go.Figure:
|
| 359 |
-
"""
|
| 360 |
-
Chart 6: Buy signals mapped to frequency domain
|
| 361 |
-
"""
|
| 362 |
-
if not buy_signals:
|
| 363 |
-
# Return empty figure with message
|
| 364 |
-
fig = go.Figure()
|
| 365 |
-
fig.add_annotation(
|
| 366 |
-
text="No buy signals detected in this period",
|
| 367 |
-
xref="paper", yref="paper",
|
| 368 |
-
x=0.5, y=0.5, showarrow=False,
|
| 369 |
-
font=dict(size=20, color="gray")
|
| 370 |
-
)
|
| 371 |
-
fig.update_layout(height=400, template='plotly_white')
|
| 372 |
-
return fig
|
| 373 |
-
|
| 374 |
-
fig = make_subplots(
|
| 375 |
-
rows=2, cols=1,
|
| 376 |
-
subplot_titles=['Buy Signals in Time Domain', 'Buy Signal Frequency Distribution'],
|
| 377 |
-
vertical_spacing=0.15,
|
| 378 |
-
specs=[[{'type': 'scatter'}], [{'type': 'bar'}]]
|
| 379 |
-
)
|
| 380 |
-
|
| 381 |
-
# Time domain signals
|
| 382 |
-
signal_dates = [pd.to_datetime(s['date']) for s in buy_signals]
|
| 383 |
-
signal_prices = [s['price'] for s in buy_signals]
|
| 384 |
-
signal_momentum = [s['momentum'] for s in buy_signals]
|
| 385 |
-
|
| 386 |
-
fig.add_trace(go.Scatter(
|
| 387 |
-
x=signal_dates,
|
| 388 |
-
y=signal_prices,
|
| 389 |
-
mode='markers',
|
| 390 |
-
marker=dict(
|
| 391 |
-
size=[abs(m)*30 for m in signal_momentum],
|
| 392 |
-
color=signal_momentum,
|
| 393 |
-
colorscale='RdYlGn',
|
| 394 |
-
showscale=True,
|
| 395 |
-
colorbar=dict(title="Momentum", x=1.1),
|
| 396 |
-
line=dict(color='black', width=1)
|
| 397 |
-
),
|
| 398 |
-
name='Buy Signals',
|
| 399 |
-
hovertemplate='Date: %{x}<br>Price: $%{y:.2f}<br>Momentum: %{marker.color:.3f}<extra></extra>'
|
| 400 |
-
), row=1, col=1)
|
| 401 |
-
|
| 402 |
-
# Frequency distribution of signals (by month/week)
|
| 403 |
-
signal_series = pd.Series(1, index=signal_dates)
|
| 404 |
-
signal_freq = signal_series.resample('W').sum()
|
| 405 |
-
|
| 406 |
-
fig.add_trace(go.Bar(
|
| 407 |
-
x=signal_freq.index,
|
| 408 |
-
y=signal_freq.values,
|
| 409 |
-
name='Signals per Week',
|
| 410 |
-
marker=dict(color='#00AA44'),
|
| 411 |
-
hovertemplate='Week: %{x}<br>Signals: %{y}<extra></extra>'
|
| 412 |
-
), row=2, col=1)
|
| 413 |
-
|
| 414 |
-
fig.update_xaxes(title_text='Date', row=1, col=1)
|
| 415 |
-
fig.update_yaxes(title_text='Price ($)', row=1, col=1)
|
| 416 |
-
fig.update_xaxes(title_text='Week', row=2, col=1)
|
| 417 |
-
fig.update_yaxes(title_text='Number of Signals', row=2, col=1)
|
| 418 |
-
|
| 419 |
-
fig.update_layout(
|
| 420 |
-
height=800,
|
| 421 |
-
title_text='Buy Signal Analysis - Frequency Domain Mapping',
|
| 422 |
-
template='plotly_white',
|
| 423 |
-
showlegend=False
|
| 424 |
-
)
|
| 425 |
-
|
| 426 |
-
return fig
|
| 427 |
-
|
| 428 |
-
def create_comprehensive_dashboard(
|
| 429 |
-
self,
|
| 430 |
-
dates: pd.DatetimeIndex,
|
| 431 |
-
original_prices: np.ndarray,
|
| 432 |
-
spectral_results: Dict,
|
| 433 |
-
momentum_signals: Dict,
|
| 434 |
-
mc_results: Dict,
|
| 435 |
-
buy_signals: List[Dict],
|
| 436 |
-
current_price: float
|
| 437 |
-
) -> Dict[str, go.Figure]:
|
| 438 |
-
"""
|
| 439 |
-
Create all charts and return as dictionary
|
| 440 |
-
"""
|
| 441 |
-
charts = {}
|
| 442 |
-
|
| 443 |
-
# Chart 1: Price decomposition with anti-aliasing
|
| 444 |
-
charts['price_decomposition'] = self.create_price_decomposition_chart(
|
| 445 |
-
dates, original_prices, spectral_results, buy_signals
|
| 446 |
-
)
|
| 447 |
-
|
| 448 |
-
# Chart 2: Frequency spectrum
|
| 449 |
-
charts['frequency_spectrum'] = self.create_frequency_spectrum_chart(spectral_results)
|
| 450 |
-
|
| 451 |
-
# Chart 3: Phase angles
|
| 452 |
-
charts['phase_angles'] = self.create_phase_angle_chart(spectral_results, dates)
|
| 453 |
-
|
| 454 |
-
# Chart 4: Momentum signals
|
| 455 |
-
charts['momentum_signals'] = self.create_momentum_signals_chart(
|
| 456 |
-
dates, momentum_signals, buy_signals
|
| 457 |
-
)
|
| 458 |
-
|
| 459 |
-
# Chart 5: Monte Carlo
|
| 460 |
-
charts['monte_carlo'] = self.create_monte_carlo_chart(mc_results, current_price)
|
| 461 |
-
|
| 462 |
-
# Chart 6: Buy signals in frequency domain
|
| 463 |
-
charts['buy_signals_freq'] = self.create_buy_signals_frequency_chart(
|
| 464 |
-
buy_signals, spectral_results, dates
|
| 465 |
-
)
|
| 466 |
-
|
| 467 |
-
self.figures = charts
|
| 468 |
-
return charts
|
| 469 |
-
|
| 470 |
-
def export_to_html(self, charts: Dict[str, go.Figure], filename: str):
|
| 471 |
-
"""Export all charts to a single HTML file"""
|
| 472 |
-
with open(filename, 'w', encoding='utf-8') as f:
|
| 473 |
-
f.write("""
|
| 474 |
-
<!DOCTYPE html>
|
| 475 |
-
<html>
|
| 476 |
-
<head>
|
| 477 |
-
<meta charset="utf-8">
|
| 478 |
-
<title>Quantitative Finance Analysis Report</title>
|
| 479 |
-
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
| 480 |
-
<style>
|
| 481 |
-
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
|
| 482 |
-
.chart-container { background: white; margin: 20px 0; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
| 483 |
-
h1 { color: #333; text-align: center; }
|
| 484 |
-
h2 { color: #666; border-bottom: 2px solid #0066CC; padding-bottom: 10px; }
|
| 485 |
-
</style>
|
| 486 |
-
</head>
|
| 487 |
-
<body>
|
| 488 |
-
<h1>📊 Professional Quantitative Finance Analysis Report</h1>
|
| 489 |
-
""")
|
| 490 |
-
|
| 491 |
-
for chart_name, fig in charts.items():
|
| 492 |
-
title = chart_name.replace('_', ' ').title()
|
| 493 |
-
f.write(f'<div class="chart-container"><h2>{title}</h2>')
|
| 494 |
-
f.write(fig.to_html(include_plotlyjs=False, div_id=chart_name))
|
| 495 |
-
f.write('</div>')
|
| 496 |
-
|
| 497 |
-
f.write("""
|
| 498 |
-
</body>
|
| 499 |
-
</html>
|
| 500 |
-
""")
|
| 501 |
-
|
| 502 |
-
def export_to_image(self, fig: go.Figure, filename: str, format: str = 'png'):
|
| 503 |
-
"""Export figure to static image"""
|
| 504 |
-
try:
|
| 505 |
-
fig.write_image(filename, format=format, width=1400, height=800, scale=2)
|
| 506 |
-
except Exception as e:
|
| 507 |
-
print(f"Warning: Could not export image: {e}")
|
| 508 |
-
print("Install kaleido for image export: pip install kaleido")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|