bquant 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. bquant/__init__.py +31 -0
  2. bquant/analysis/__init__.py +297 -0
  3. bquant/analysis/candlestick/__init__.py +100 -0
  4. bquant/analysis/chart/__init__.py +98 -0
  5. bquant/analysis/statistical/__init__.py +414 -0
  6. bquant/analysis/statistical/hypothesis_testing.py +657 -0
  7. bquant/analysis/technical/__init__.py +98 -0
  8. bquant/analysis/timeseries/__init__.py +102 -0
  9. bquant/analysis/zones/__init__.py +547 -0
  10. bquant/analysis/zones/sequence_analysis.py +661 -0
  11. bquant/analysis/zones/zone_features.py +520 -0
  12. bquant/cli.py +160 -0
  13. bquant/core/__init__.py +112 -0
  14. bquant/core/cache.py +534 -0
  15. bquant/core/config.py +328 -0
  16. bquant/core/exceptions.py +351 -0
  17. bquant/core/logging_config.py +368 -0
  18. bquant/core/numpy_fix.py +99 -0
  19. bquant/core/performance.py +554 -0
  20. bquant/core/utils.py +327 -0
  21. bquant/data/__init__.py +85 -0
  22. bquant/data/loader.py +436 -0
  23. bquant/data/processor.py +644 -0
  24. bquant/data/samples/__init__.py +442 -0
  25. bquant/data/samples/datasets.py +246 -0
  26. bquant/data/samples/embedded/__init__.py +16 -0
  27. bquant/data/samples/embedded/mt_xauusd_m15.py +9018 -0
  28. bquant/data/samples/embedded/tv_xauusd_1h.py +17018 -0
  29. bquant/data/samples/utils.py +393 -0
  30. bquant/data/schemas.py +285 -0
  31. bquant/data/validator.py +498 -0
  32. bquant/indicators/__init__.py +115 -0
  33. bquant/indicators/base.py +487 -0
  34. bquant/indicators/calculators.py +404 -0
  35. bquant/indicators/library.py +514 -0
  36. bquant/indicators/loaders.py +413 -0
  37. bquant/indicators/macd.py +754 -0
  38. bquant/ml/__init__.py +19 -0
  39. bquant/visualization/__init__.py +316 -0
  40. bquant/visualization/charts.py +616 -0
  41. bquant/visualization/statistical.py +812 -0
  42. bquant/visualization/themes.py +599 -0
  43. bquant/visualization/zones.py +775 -0
  44. bquant-0.0.0.dist-info/METADATA +167 -0
  45. bquant-0.0.0.dist-info/RECORD +49 -0
  46. bquant-0.0.0.dist-info/WHEEL +5 -0
  47. bquant-0.0.0.dist-info/entry_points.txt +2 -0
  48. bquant-0.0.0.dist-info/licenses/LICENSE +21 -0
  49. bquant-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,661 @@
1
+ """
2
+ Модуль анализа последовательностей зон BQuant
3
+
4
+ Адаптировано из scripts/research/macd_analysis.py с улучшениями для новой архитектуры.
5
+ Предоставляет функции для анализа переходов между зонами и кластеризации.
6
+ """
7
+
8
+ import pandas as pd
9
+ import numpy as np
10
+ from scipy import stats
11
+ from sklearn.cluster import KMeans
12
+ from sklearn.preprocessing import StandardScaler
13
+ from typing import Dict, Any, List, Optional, Union, Tuple
14
+ from datetime import datetime
15
+ from dataclasses import dataclass
16
+
17
+ from ...core.logging_config import get_logger
18
+ from ...core.exceptions import AnalysisError
19
+ from .. import AnalysisResult, BaseAnalyzer
20
+ from .zone_features import ZoneFeatures
21
+
22
+ # Получаем логгер для модуля
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ @dataclass
27
+ class TransitionAnalysis:
28
+ """
29
+ Результат анализа переходов между зонами.
30
+
31
+ Attributes:
32
+ transition_type: Тип перехода (e.g., 'bull_to_bear')
33
+ count: Количество таких переходов
34
+ probability: Вероятность такого перехода
35
+ avg_duration_before: Средняя длительность предыдущей зоны
36
+ avg_duration_after: Средняя длительность следующей зоны
37
+ avg_return_before: Средняя доходность предыдущей зоны
38
+ avg_return_after: Средняя доходность следующей зоны
39
+ metadata: Дополнительные метаданные
40
+ """
41
+ transition_type: str
42
+ count: int
43
+ probability: float
44
+ avg_duration_before: Optional[float] = None
45
+ avg_duration_after: Optional[float] = None
46
+ avg_return_before: Optional[float] = None
47
+ avg_return_after: Optional[float] = None
48
+ metadata: Dict[str, Any] = None
49
+
50
+ def __post_init__(self):
51
+ if self.metadata is None:
52
+ self.metadata = {}
53
+
54
+
55
+ @dataclass
56
+ class ClusterAnalysis:
57
+ """
58
+ Результат кластеризации зон.
59
+
60
+ Attributes:
61
+ cluster_id: ID кластера
62
+ size: Количество зон в кластере
63
+ centroid: Центроид кластера (средние значения признаков)
64
+ characteristics: Основные характеристики кластера
65
+ dominant_type: Преобладающий тип зон в кластере
66
+ avg_duration: Средняя длительность зон в кластере
67
+ avg_return: Средняя доходность зон в кластере
68
+ metadata: Дополнительные метаданные
69
+ """
70
+ cluster_id: int
71
+ size: int
72
+ centroid: Dict[str, float]
73
+ characteristics: Dict[str, Any]
74
+ dominant_type: str
75
+ avg_duration: float
76
+ avg_return: float
77
+ metadata: Dict[str, Any] = None
78
+
79
+ def __post_init__(self):
80
+ if self.metadata is None:
81
+ self.metadata = {}
82
+
83
+
84
+ class ZoneSequenceAnalyzer(BaseAnalyzer):
85
+ """
86
+ Анализатор последовательностей торговых зон.
87
+
88
+ Предоставляет методы для:
89
+ - Анализа переходов между зонами
90
+ - Вычисления вероятностей переходов
91
+ - Кластеризации зон по форме и характеристикам
92
+ - Выявления паттернов в последовательностях
93
+ """
94
+
95
+ def __init__(self, min_sequence_length: int = 3):
96
+ """
97
+ Инициализация анализатора.
98
+
99
+ Args:
100
+ min_sequence_length: Минимальная длина последовательности для анализа
101
+ """
102
+ super().__init__("ZoneSequenceAnalyzer")
103
+ self.min_sequence_length = min_sequence_length
104
+ self.logger = get_logger(f"{__name__}.ZoneSequenceAnalyzer")
105
+
106
+ self.logger.info(f"Initialized zone sequence analyzer with min_sequence_length={min_sequence_length}")
107
+
108
+ def analyze_zone_transitions(self, zones_features: List[Union[ZoneFeatures, Dict[str, Any]]]) -> AnalysisResult:
109
+ """
110
+ Анализ переходов между зонами.
111
+
112
+ Args:
113
+ zones_features: Список объектов ZoneFeatures или словарей
114
+
115
+ Returns:
116
+ AnalysisResult с анализом переходов
117
+ """
118
+ try:
119
+ self.logger.info(f"Analyzing transitions for {len(zones_features)} zones")
120
+
121
+ if len(zones_features) < self.min_sequence_length:
122
+ raise AnalysisError(f"Need at least {self.min_sequence_length} zones for sequence analysis")
123
+
124
+ # Конвертируем в DataFrame
125
+ features_dicts = []
126
+ for zone in zones_features:
127
+ if isinstance(zone, ZoneFeatures):
128
+ features_dicts.append(zone.to_dict())
129
+ elif isinstance(zone, dict):
130
+ features_dicts.append(zone)
131
+ else:
132
+ raise AnalysisError(f"Invalid zone features type: {type(zone)}")
133
+
134
+ df_features = pd.DataFrame(features_dicts)
135
+
136
+ # Создаем последовательность типов зон
137
+ zone_sequence = df_features['zone_type'].tolist()
138
+
139
+ # Анализируем переходы
140
+ transitions = self._calculate_transitions(df_features)
141
+ transition_probabilities = self._calculate_transition_probabilities(transitions)
142
+ transition_details = self._analyze_transition_details(df_features, transitions)
143
+
144
+ # Анализ паттернов
145
+ patterns = self._find_sequence_patterns(zone_sequence)
146
+
147
+ # Статистические тесты
148
+ randomness_tests = self._test_sequence_randomness(zone_sequence)
149
+
150
+ # Марковский анализ
151
+ markov_analysis = self._markov_chain_analysis(zone_sequence)
152
+
153
+ results = {
154
+ 'sequence_summary': {
155
+ 'total_zones': len(zone_sequence),
156
+ 'total_transitions': len(zone_sequence) - 1,
157
+ 'unique_transition_types': len(transitions),
158
+ 'sequence_length': len(zone_sequence)
159
+ },
160
+ 'transitions': transitions,
161
+ 'transition_probabilities': transition_probabilities,
162
+ 'transition_details': transition_details,
163
+ 'patterns': patterns,
164
+ 'randomness_tests': randomness_tests,
165
+ 'markov_analysis': markov_analysis
166
+ }
167
+
168
+ metadata = {
169
+ 'analyzer': 'ZoneSequenceAnalyzer',
170
+ 'analysis_method': 'zone_transitions',
171
+ 'min_sequence_length': self.min_sequence_length,
172
+ 'timestamp': datetime.now().isoformat()
173
+ }
174
+
175
+ return AnalysisResult(
176
+ analysis_type='zone_transitions',
177
+ results=results,
178
+ data_size=len(zones_features),
179
+ metadata=metadata
180
+ )
181
+
182
+ except Exception as e:
183
+ self.logger.error(f"Zone transitions analysis failed: {e}")
184
+ raise AnalysisError(f"Zone transitions analysis failed: {e}")
185
+
186
+ def cluster_zones(self, zones_features: List[Union[ZoneFeatures, Dict[str, Any]]],
187
+ n_clusters: int = 3,
188
+ features_to_use: Optional[List[str]] = None) -> AnalysisResult:
189
+ """
190
+ Кластеризация зон по характеристикам.
191
+
192
+ Args:
193
+ zones_features: Список объектов ZoneFeatures или словарей
194
+ n_clusters: Количество кластеров
195
+ features_to_use: Список признаков для кластеризации (если None, используются по умолчанию)
196
+
197
+ Returns:
198
+ AnalysisResult с результатами кластеризации
199
+ """
200
+ try:
201
+ self.logger.info(f"Clustering {len(zones_features)} zones into {n_clusters} clusters")
202
+
203
+ if len(zones_features) < n_clusters:
204
+ raise AnalysisError(f"Cannot create {n_clusters} clusters from {len(zones_features)} zones")
205
+
206
+ # Конвертируем в DataFrame
207
+ features_dicts = []
208
+ for zone in zones_features:
209
+ if isinstance(zone, ZoneFeatures):
210
+ features_dicts.append(zone.to_dict())
211
+ elif isinstance(zone, dict):
212
+ features_dicts.append(zone)
213
+
214
+ df_features = pd.DataFrame(features_dicts)
215
+
216
+ # Выбираем признаки для кластеризации
217
+ if features_to_use is None:
218
+ features_to_use = [
219
+ 'duration', 'macd_amplitude', 'hist_amplitude',
220
+ 'price_range_pct', 'correlation_price_hist'
221
+ ]
222
+ # Добавляем дополнительные признаки если они доступны
223
+ if 'num_peaks' in df_features.columns:
224
+ features_to_use.append('num_peaks')
225
+ if 'num_troughs' in df_features.columns:
226
+ features_to_use.append('num_troughs')
227
+
228
+ # Проверяем доступность признаков
229
+ available_features = [f for f in features_to_use if f in df_features.columns]
230
+ if not available_features:
231
+ raise AnalysisError("No clustering features available in data")
232
+
233
+ self.logger.info(f"Using features for clustering: {available_features}")
234
+
235
+ # Подготавливаем данные для кластеризации
236
+ clustering_data = df_features[available_features].fillna(0)
237
+
238
+ # Нормализация признаков
239
+ scaler = StandardScaler()
240
+ features_scaled = scaler.fit_transform(clustering_data)
241
+
242
+ # Кластеризация
243
+ kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
244
+ cluster_labels = kmeans.fit_predict(features_scaled)
245
+
246
+ # Добавляем метки кластеров
247
+ df_features['cluster'] = cluster_labels
248
+
249
+ # Анализ кластеров
250
+ clusters_analysis = self._analyze_clusters(df_features, available_features, kmeans, scaler)
251
+
252
+ # Валидация кластеризации
253
+ clustering_quality = self._evaluate_clustering_quality(features_scaled, cluster_labels, n_clusters)
254
+
255
+ results = {
256
+ 'clustering_summary': {
257
+ 'n_clusters': n_clusters,
258
+ 'features_used': available_features,
259
+ 'total_zones': len(df_features),
260
+ 'clustering_quality': clustering_quality
261
+ },
262
+ 'cluster_labels': cluster_labels.tolist(),
263
+ 'clusters_analysis': clusters_analysis,
264
+ 'feature_importance': self._calculate_feature_importance(features_scaled, cluster_labels, available_features)
265
+ }
266
+
267
+ metadata = {
268
+ 'analyzer': 'ZoneSequenceAnalyzer',
269
+ 'analysis_method': 'zone_clustering',
270
+ 'n_clusters': n_clusters,
271
+ 'features_used': available_features,
272
+ 'timestamp': datetime.now().isoformat()
273
+ }
274
+
275
+ return AnalysisResult(
276
+ analysis_type='zone_clustering',
277
+ results=results,
278
+ data_size=len(zones_features),
279
+ metadata=metadata
280
+ )
281
+
282
+ except Exception as e:
283
+ self.logger.error(f"Zone clustering failed: {e}")
284
+ raise AnalysisError(f"Zone clustering failed: {e}")
285
+
286
+ def _calculate_transitions(self, df_features: pd.DataFrame) -> Dict[str, int]:
287
+ """Подсчет переходов между зонами."""
288
+ zone_sequence = df_features['zone_type'].tolist()
289
+ transitions = {}
290
+
291
+ for i in range(len(zone_sequence) - 1):
292
+ current = zone_sequence[i]
293
+ next_zone = zone_sequence[i + 1]
294
+ transition = f"{current}_to_{next_zone}"
295
+
296
+ transitions[transition] = transitions.get(transition, 0) + 1
297
+
298
+ return transitions
299
+
300
+ def _calculate_transition_probabilities(self, transitions: Dict[str, int]) -> Dict[str, float]:
301
+ """Вычисление вероятностей переходов."""
302
+ total_transitions = sum(transitions.values())
303
+ if total_transitions == 0:
304
+ return {}
305
+
306
+ return {transition: count / total_transitions
307
+ for transition, count in transitions.items()}
308
+
309
+ def _analyze_transition_details(self, df_features: pd.DataFrame,
310
+ transitions: Dict[str, int]) -> Dict[str, TransitionAnalysis]:
311
+ """Детальный анализ переходов."""
312
+ zone_sequence = df_features['zone_type'].tolist()
313
+ transition_details = {}
314
+
315
+ # Собираем данные о переходах
316
+ transition_data = {transition: {'before': [], 'after': []}
317
+ for transition in transitions.keys()}
318
+
319
+ for i in range(len(zone_sequence) - 1):
320
+ current = zone_sequence[i]
321
+ next_zone = zone_sequence[i + 1]
322
+ transition = f"{current}_to_{next_zone}"
323
+
324
+ if transition in transition_data:
325
+ transition_data[transition]['before'].append(i)
326
+ transition_data[transition]['after'].append(i + 1)
327
+
328
+ # Анализируем каждый тип перехода
329
+ total_transitions = sum(transitions.values())
330
+
331
+ for transition, count in transitions.items():
332
+ before_indices = transition_data[transition]['before']
333
+ after_indices = transition_data[transition]['after']
334
+
335
+ avg_duration_before = None
336
+ avg_duration_after = None
337
+ avg_return_before = None
338
+ avg_return_after = None
339
+
340
+ if before_indices and 'duration' in df_features.columns:
341
+ avg_duration_before = float(df_features.iloc[before_indices]['duration'].mean())
342
+
343
+ if after_indices and 'duration' in df_features.columns:
344
+ avg_duration_after = float(df_features.iloc[after_indices]['duration'].mean())
345
+
346
+ if before_indices and 'price_return' in df_features.columns:
347
+ avg_return_before = float(df_features.iloc[before_indices]['price_return'].mean())
348
+
349
+ if after_indices and 'price_return' in df_features.columns:
350
+ avg_return_after = float(df_features.iloc[after_indices]['price_return'].mean())
351
+
352
+ transition_details[transition] = TransitionAnalysis(
353
+ transition_type=transition,
354
+ count=count,
355
+ probability=count / total_transitions,
356
+ avg_duration_before=avg_duration_before,
357
+ avg_duration_after=avg_duration_after,
358
+ avg_return_before=avg_return_before,
359
+ avg_return_after=avg_return_after,
360
+ metadata={
361
+ 'before_indices': before_indices,
362
+ 'after_indices': after_indices
363
+ }
364
+ )
365
+
366
+ return {k: v.__dict__ for k, v in transition_details.items()}
367
+
368
+ def _find_sequence_patterns(self, zone_sequence: List[str]) -> Dict[str, Any]:
369
+ """Поиск паттернов в последовательностях."""
370
+ patterns = {}
371
+
372
+ # Анализ длин серий
373
+ current_type = zone_sequence[0]
374
+ current_length = 1
375
+ series_lengths = {current_type: []}
376
+
377
+ for i in range(1, len(zone_sequence)):
378
+ if zone_sequence[i] == current_type:
379
+ current_length += 1
380
+ else:
381
+ series_lengths[current_type].append(current_length)
382
+ current_type = zone_sequence[i]
383
+ if current_type not in series_lengths:
384
+ series_lengths[current_type] = []
385
+ current_length = 1
386
+
387
+ # Добавляем последнюю серию
388
+ series_lengths[current_type].append(current_length)
389
+
390
+ # Статистика серий
391
+ patterns['series_analysis'] = {}
392
+ for zone_type, lengths in series_lengths.items():
393
+ if lengths:
394
+ patterns['series_analysis'][zone_type] = {
395
+ 'avg_series_length': np.mean(lengths),
396
+ 'max_series_length': max(lengths),
397
+ 'min_series_length': min(lengths),
398
+ 'total_series': len(lengths),
399
+ 'std_series_length': np.std(lengths)
400
+ }
401
+
402
+ # Поиск триплетов (последовательности из 3 зон)
403
+ if len(zone_sequence) >= 3:
404
+ triplets = {}
405
+ for i in range(len(zone_sequence) - 2):
406
+ triplet = f"{zone_sequence[i]}-{zone_sequence[i+1]}-{zone_sequence[i+2]}"
407
+ triplets[triplet] = triplets.get(triplet, 0) + 1
408
+
409
+ patterns['triplet_patterns'] = triplets
410
+
411
+ return patterns
412
+
413
+ def _test_sequence_randomness(self, zone_sequence: List[str]) -> Dict[str, Any]:
414
+ """Тестирование случайности последовательности."""
415
+ randomness_tests = {}
416
+
417
+ # Runs test
418
+ binary_sequence = [1 if zone == 'bull' else 0 for zone in zone_sequence]
419
+ runs_result = self._runs_test(binary_sequence)
420
+ randomness_tests['runs_test'] = runs_result
421
+
422
+ # Chi-square test для равномерности
423
+ bull_count = zone_sequence.count('bull')
424
+ bear_count = zone_sequence.count('bear')
425
+
426
+ if bull_count > 0 and bear_count > 0:
427
+ expected = len(zone_sequence) / 2
428
+ chi2_stat = ((bull_count - expected)**2 / expected +
429
+ (bear_count - expected)**2 / expected)
430
+ chi2_p = 1 - stats.chi2.cdf(chi2_stat, df=1)
431
+
432
+ randomness_tests['uniformity_test'] = {
433
+ 'chi2_statistic': chi2_stat,
434
+ 'p_value': chi2_p,
435
+ 'is_uniform': chi2_p > 0.05,
436
+ 'bull_count': bull_count,
437
+ 'bear_count': bear_count
438
+ }
439
+
440
+ return randomness_tests
441
+
442
+ def _runs_test(self, binary_sequence: List[int]) -> Dict[str, Any]:
443
+ """Runs test для проверки случайности."""
444
+ n = len(binary_sequence)
445
+ n1 = sum(binary_sequence)
446
+ n0 = n - n1
447
+
448
+ if n1 == 0 or n0 == 0:
449
+ return {'error': 'All values are the same'}
450
+
451
+ # Подсчет runs
452
+ runs = 1
453
+ for i in range(1, n):
454
+ if binary_sequence[i] != binary_sequence[i-1]:
455
+ runs += 1
456
+
457
+ # Ожидаемое количество runs
458
+ expected_runs = (2 * n1 * n0) / n + 1
459
+
460
+ # Дисперсия
461
+ variance = (2 * n1 * n0 * (2 * n1 * n0 - n)) / (n**2 * (n - 1))
462
+
463
+ if variance <= 0:
464
+ return {'error': 'Cannot calculate variance'}
465
+
466
+ # Z-статистика
467
+ z = (runs - expected_runs) / np.sqrt(variance)
468
+
469
+ # p-value
470
+ p_value = 2 * (1 - stats.norm.cdf(abs(z)))
471
+
472
+ return {
473
+ 'runs_count': runs,
474
+ 'expected_runs': expected_runs,
475
+ 'z_statistic': z,
476
+ 'p_value': p_value,
477
+ 'is_random': p_value > 0.05
478
+ }
479
+
480
+ def _markov_chain_analysis(self, zone_sequence: List[str]) -> Dict[str, Any]:
481
+ """Анализ последовательности как цепи Маркова."""
482
+ if len(zone_sequence) < 2:
483
+ return {'error': 'Sequence too short for Markov analysis'}
484
+
485
+ # Матрица переходов
486
+ states = ['bull', 'bear']
487
+ transition_matrix = np.zeros((2, 2))
488
+ state_to_index = {'bull': 0, 'bear': 1}
489
+
490
+ # Подсчет переходов
491
+ for i in range(len(zone_sequence) - 1):
492
+ current_state = zone_sequence[i]
493
+ next_state = zone_sequence[i + 1]
494
+
495
+ if current_state in state_to_index and next_state in state_to_index:
496
+ current_idx = state_to_index[current_state]
497
+ next_idx = state_to_index[next_state]
498
+ transition_matrix[current_idx, next_idx] += 1
499
+
500
+ # Нормализация для получения вероятностей
501
+ row_sums = transition_matrix.sum(axis=1, keepdims=True)
502
+ row_sums[row_sums == 0] = 1 # Избегаем деления на ноль
503
+ transition_probabilities = transition_matrix / row_sums
504
+
505
+ # Стационарное распределение (если цепь эргодична)
506
+ try:
507
+ eigenvalues, eigenvectors = np.linalg.eig(transition_probabilities.T)
508
+ stationary_idx = np.argmax(eigenvalues.real)
509
+ stationary_distribution = np.abs(eigenvectors[:, stationary_idx].real)
510
+ stationary_distribution = stationary_distribution / stationary_distribution.sum()
511
+ except:
512
+ stationary_distribution = None
513
+
514
+ return {
515
+ 'transition_matrix': transition_matrix.tolist(),
516
+ 'transition_probabilities': transition_probabilities.tolist(),
517
+ 'states': states,
518
+ 'stationary_distribution': stationary_distribution.tolist() if stationary_distribution is not None else None
519
+ }
520
+
521
+ def _analyze_clusters(self, df_features: pd.DataFrame,
522
+ available_features: List[str],
523
+ kmeans: KMeans,
524
+ scaler: StandardScaler) -> Dict[str, ClusterAnalysis]:
525
+ """Анализ результатов кластеризации."""
526
+ clusters_analysis = {}
527
+
528
+ for cluster_id in range(kmeans.n_clusters):
529
+ cluster_data = df_features[df_features['cluster'] == cluster_id]
530
+
531
+ if len(cluster_data) == 0:
532
+ continue
533
+
534
+ # Центроид (обратная нормализация)
535
+ centroid_scaled = kmeans.cluster_centers_[cluster_id]
536
+ centroid_original = scaler.inverse_transform([centroid_scaled])[0]
537
+ centroid_dict = {feature: float(centroid_original[i])
538
+ for i, feature in enumerate(available_features)}
539
+
540
+ # Характеристики кластера
541
+ characteristics = {}
542
+ for feature in available_features:
543
+ if feature in cluster_data.columns:
544
+ characteristics[f'{feature}_mean'] = float(cluster_data[feature].mean())
545
+ characteristics[f'{feature}_std'] = float(cluster_data[feature].std())
546
+
547
+ # Преобладающий тип
548
+ type_counts = cluster_data['zone_type'].value_counts()
549
+ dominant_type = type_counts.index[0] if len(type_counts) > 0 else 'unknown'
550
+
551
+ # Средние метрики
552
+ avg_duration = float(cluster_data['duration'].mean()) if 'duration' in cluster_data.columns else 0
553
+ avg_return = float(cluster_data['price_return'].mean()) if 'price_return' in cluster_data.columns else 0
554
+
555
+ clusters_analysis[f'cluster_{cluster_id}'] = ClusterAnalysis(
556
+ cluster_id=cluster_id,
557
+ size=len(cluster_data),
558
+ centroid=centroid_dict,
559
+ characteristics=characteristics,
560
+ dominant_type=dominant_type,
561
+ avg_duration=avg_duration,
562
+ avg_return=avg_return,
563
+ metadata={
564
+ 'bull_ratio': (cluster_data['zone_type'] == 'bull').mean() if 'zone_type' in cluster_data.columns else 0,
565
+ 'bear_ratio': (cluster_data['zone_type'] == 'bear').mean() if 'zone_type' in cluster_data.columns else 0
566
+ }
567
+ )
568
+
569
+ return {k: v.__dict__ for k, v in clusters_analysis.items()}
570
+
571
+ def _evaluate_clustering_quality(self, features_scaled: np.ndarray,
572
+ cluster_labels: np.ndarray,
573
+ n_clusters: int) -> Dict[str, float]:
574
+ """Оценка качества кластеризации."""
575
+ from sklearn.metrics import silhouette_score, calinski_harabasz_score, davies_bouldin_score
576
+
577
+ quality_metrics = {}
578
+
579
+ try:
580
+ if n_clusters > 1 and len(np.unique(cluster_labels)) > 1:
581
+ quality_metrics['silhouette_score'] = float(silhouette_score(features_scaled, cluster_labels))
582
+ quality_metrics['calinski_harabasz_score'] = float(calinski_harabasz_score(features_scaled, cluster_labels))
583
+ quality_metrics['davies_bouldin_score'] = float(davies_bouldin_score(features_scaled, cluster_labels))
584
+ except Exception as e:
585
+ logger.warning(f"Failed to calculate clustering quality metrics: {e}")
586
+
587
+ return quality_metrics
588
+
589
+ def _calculate_feature_importance(self, features_scaled: np.ndarray,
590
+ cluster_labels: np.ndarray,
591
+ feature_names: List[str]) -> Dict[str, float]:
592
+ """Вычисление важности признаков для кластеризации."""
593
+ feature_importance = {}
594
+
595
+ try:
596
+ for i, feature_name in enumerate(feature_names):
597
+ feature_values = features_scaled[:, i]
598
+
599
+ # Вычисляем дисперсию между кластерами
600
+ cluster_means = []
601
+ for cluster_id in np.unique(cluster_labels):
602
+ cluster_mask = cluster_labels == cluster_id
603
+ if np.sum(cluster_mask) > 0:
604
+ cluster_mean = np.mean(feature_values[cluster_mask])
605
+ cluster_means.append(cluster_mean)
606
+
607
+ if len(cluster_means) > 1:
608
+ between_cluster_variance = np.var(cluster_means)
609
+ feature_importance[feature_name] = float(between_cluster_variance)
610
+ else:
611
+ feature_importance[feature_name] = 0.0
612
+
613
+ except Exception as e:
614
+ logger.warning(f"Failed to calculate feature importance: {e}")
615
+
616
+ return feature_importance
617
+
618
+
619
+ # Удобные функции для быстрого использования
620
+ def create_zone_sequence_analysis(zones_features: List[Union[ZoneFeatures, Dict[str, Any]]],
621
+ min_sequence_length: int = 3) -> Dict[str, Any]:
622
+ """
623
+ Анализ последовательностей зон (совместимость с оригинальным API).
624
+
625
+ Args:
626
+ zones_features: Список характеристик зон
627
+ min_sequence_length: Минимальная длина последовательности
628
+
629
+ Returns:
630
+ Словарь с результатами анализа
631
+ """
632
+ analyzer = ZoneSequenceAnalyzer(min_sequence_length=min_sequence_length)
633
+ analysis_result = analyzer.analyze_zone_transitions(zones_features)
634
+ return analysis_result.results
635
+
636
+
637
+ def cluster_zone_shapes(zones_features: List[Union[ZoneFeatures, Dict[str, Any]]],
638
+ n_clusters: int = 3) -> Dict[str, Any]:
639
+ """
640
+ Кластеризация зон по форме (совместимость с оригинальным API).
641
+
642
+ Args:
643
+ zones_features: Список характеристик зон
644
+ n_clusters: Количество кластеров
645
+
646
+ Returns:
647
+ Словарь с результатами кластеризации
648
+ """
649
+ analyzer = ZoneSequenceAnalyzer()
650
+ analysis_result = analyzer.cluster_zones(zones_features, n_clusters=n_clusters)
651
+ return analysis_result.results
652
+
653
+
654
+ # Экспорт
655
+ __all__ = [
656
+ 'TransitionAnalysis',
657
+ 'ClusterAnalysis',
658
+ 'ZoneSequenceAnalyzer',
659
+ 'create_zone_sequence_analysis',
660
+ 'cluster_zone_shapes'
661
+ ]