kaganseyda commited on
Commit
741903a
·
verified ·
1 Parent(s): a511c2e

Delete src

Browse files
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")