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.
- bquant/__init__.py +31 -0
- bquant/analysis/__init__.py +297 -0
- bquant/analysis/candlestick/__init__.py +100 -0
- bquant/analysis/chart/__init__.py +98 -0
- bquant/analysis/statistical/__init__.py +414 -0
- bquant/analysis/statistical/hypothesis_testing.py +657 -0
- bquant/analysis/technical/__init__.py +98 -0
- bquant/analysis/timeseries/__init__.py +102 -0
- bquant/analysis/zones/__init__.py +547 -0
- bquant/analysis/zones/sequence_analysis.py +661 -0
- bquant/analysis/zones/zone_features.py +520 -0
- bquant/cli.py +160 -0
- bquant/core/__init__.py +112 -0
- bquant/core/cache.py +534 -0
- bquant/core/config.py +328 -0
- bquant/core/exceptions.py +351 -0
- bquant/core/logging_config.py +368 -0
- bquant/core/numpy_fix.py +99 -0
- bquant/core/performance.py +554 -0
- bquant/core/utils.py +327 -0
- bquant/data/__init__.py +85 -0
- bquant/data/loader.py +436 -0
- bquant/data/processor.py +644 -0
- bquant/data/samples/__init__.py +442 -0
- bquant/data/samples/datasets.py +246 -0
- bquant/data/samples/embedded/__init__.py +16 -0
- bquant/data/samples/embedded/mt_xauusd_m15.py +9018 -0
- bquant/data/samples/embedded/tv_xauusd_1h.py +17018 -0
- bquant/data/samples/utils.py +393 -0
- bquant/data/schemas.py +285 -0
- bquant/data/validator.py +498 -0
- bquant/indicators/__init__.py +115 -0
- bquant/indicators/base.py +487 -0
- bquant/indicators/calculators.py +404 -0
- bquant/indicators/library.py +514 -0
- bquant/indicators/loaders.py +413 -0
- bquant/indicators/macd.py +754 -0
- bquant/ml/__init__.py +19 -0
- bquant/visualization/__init__.py +316 -0
- bquant/visualization/charts.py +616 -0
- bquant/visualization/statistical.py +812 -0
- bquant/visualization/themes.py +599 -0
- bquant/visualization/zones.py +775 -0
- bquant-0.0.0.dist-info/METADATA +167 -0
- bquant-0.0.0.dist-info/RECORD +49 -0
- bquant-0.0.0.dist-info/WHEEL +5 -0
- bquant-0.0.0.dist-info/entry_points.txt +2 -0
- bquant-0.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
]
|