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,657 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Модуль тестирования гипотез для торговых стратегий BQuant
|
|
3
|
+
|
|
4
|
+
Адаптировано из scripts/research/hypothesis_testing.py с улучшениями для новой архитектуры.
|
|
5
|
+
Предоставляет комплексные статистические тесты для анализа зон и торговых паттернов.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy import stats
|
|
11
|
+
from scipy.stats import ttest_ind, mannwhitneyu, chi2_contingency
|
|
12
|
+
from typing import Dict, Any, Optional, List, Union
|
|
13
|
+
import warnings
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
from ...core.logging_config import get_logger
|
|
18
|
+
from ...core.exceptions import StatisticalAnalysisError
|
|
19
|
+
from .. import AnalysisResult
|
|
20
|
+
|
|
21
|
+
# Получаем логгер для модуля
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
warnings.filterwarnings('ignore')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class HypothesisTestResult:
|
|
29
|
+
"""
|
|
30
|
+
Результат статистического теста гипотезы.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
hypothesis: Описание тестируемой гипотезы
|
|
34
|
+
test_type: Тип статистического теста
|
|
35
|
+
statistic: Значение тестовой статистики
|
|
36
|
+
p_value: p-значение теста
|
|
37
|
+
significant: Является ли результат значимым (p < alpha)
|
|
38
|
+
alpha: Уровень значимости
|
|
39
|
+
effect_size: Размер эффекта (если применимо)
|
|
40
|
+
confidence_interval: Доверительный интервал (если применимо)
|
|
41
|
+
sample_size: Размер выборки
|
|
42
|
+
metadata: Дополнительные метаданные теста
|
|
43
|
+
"""
|
|
44
|
+
hypothesis: str
|
|
45
|
+
test_type: str
|
|
46
|
+
statistic: float
|
|
47
|
+
p_value: float
|
|
48
|
+
significant: bool
|
|
49
|
+
alpha: float = 0.05
|
|
50
|
+
effect_size: Optional[float] = None
|
|
51
|
+
confidence_interval: Optional[tuple] = None
|
|
52
|
+
sample_size: Optional[int] = None
|
|
53
|
+
metadata: Dict[str, Any] = None
|
|
54
|
+
|
|
55
|
+
def __post_init__(self):
|
|
56
|
+
if self.metadata is None:
|
|
57
|
+
self.metadata = {}
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
60
|
+
"""Конвертация результата в словарь."""
|
|
61
|
+
return {
|
|
62
|
+
'hypothesis': self.hypothesis,
|
|
63
|
+
'test_type': self.test_type,
|
|
64
|
+
'statistic': self.statistic,
|
|
65
|
+
'p_value': self.p_value,
|
|
66
|
+
'significant': self.significant,
|
|
67
|
+
'alpha': self.alpha,
|
|
68
|
+
'effect_size': self.effect_size,
|
|
69
|
+
'confidence_interval': self.confidence_interval,
|
|
70
|
+
'sample_size': self.sample_size,
|
|
71
|
+
'metadata': self.metadata
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class HypothesisTestSuite:
|
|
76
|
+
"""
|
|
77
|
+
Набор статистических тестов для анализа торговых зон и паттернов.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, alpha: float = 0.05):
|
|
81
|
+
"""
|
|
82
|
+
Инициализация набора тестов.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
alpha: Уровень значимости для всех тестов
|
|
86
|
+
"""
|
|
87
|
+
self.alpha = alpha
|
|
88
|
+
self.logger = get_logger(f"{__name__}.HypothesisTestSuite")
|
|
89
|
+
|
|
90
|
+
self.logger.info(f"Initialized hypothesis test suite with alpha={alpha}")
|
|
91
|
+
|
|
92
|
+
def test_zone_duration_hypothesis(self, zones_features: List[Dict[str, Any]]) -> HypothesisTestResult:
|
|
93
|
+
"""
|
|
94
|
+
Тест гипотезы о влиянии длительности зон на последующее движение цены.
|
|
95
|
+
|
|
96
|
+
H0: Длительность зоны не влияет на доходность
|
|
97
|
+
H1: Длительные зоны дают другую доходность чем короткие
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
zones_features: Список словарей с характеристиками зон
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
HypothesisTestResult с результатами теста
|
|
104
|
+
"""
|
|
105
|
+
self.logger.info("Testing zone duration hypothesis")
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
df_features = pd.DataFrame(zones_features)
|
|
109
|
+
|
|
110
|
+
if 'duration' not in df_features.columns or 'price_return' not in df_features.columns:
|
|
111
|
+
raise StatisticalAnalysisError("Missing required columns: 'duration' or 'price_return'")
|
|
112
|
+
|
|
113
|
+
# Определяем длинные и короткие зоны (верхние и нижние 20%)
|
|
114
|
+
long_threshold = df_features['duration'].quantile(0.8)
|
|
115
|
+
short_threshold = df_features['duration'].quantile(0.2)
|
|
116
|
+
|
|
117
|
+
long_zones = df_features[df_features['duration'] >= long_threshold]
|
|
118
|
+
short_zones = df_features[df_features['duration'] <= short_threshold]
|
|
119
|
+
|
|
120
|
+
if len(long_zones) == 0 or len(short_zones) == 0:
|
|
121
|
+
raise StatisticalAnalysisError("Insufficient data: need both long and short zones")
|
|
122
|
+
|
|
123
|
+
# Выполняем t-тест
|
|
124
|
+
t_stat, p_value = stats.ttest_ind(
|
|
125
|
+
long_zones['price_return'].dropna(),
|
|
126
|
+
short_zones['price_return'].dropna()
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Вычисляем размер эффекта (Cohen's d)
|
|
130
|
+
pooled_std = np.sqrt(
|
|
131
|
+
((len(long_zones) - 1) * long_zones['price_return'].var() +
|
|
132
|
+
(len(short_zones) - 1) * short_zones['price_return'].var()) /
|
|
133
|
+
(len(long_zones) + len(short_zones) - 2)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
effect_size = (long_zones['price_return'].mean() - short_zones['price_return'].mean()) / pooled_std
|
|
137
|
+
|
|
138
|
+
# Метаданные теста
|
|
139
|
+
metadata = {
|
|
140
|
+
'long_zones_count': len(long_zones),
|
|
141
|
+
'short_zones_count': len(short_zones),
|
|
142
|
+
'long_zones_mean_return': long_zones['price_return'].mean(),
|
|
143
|
+
'short_zones_mean_return': short_zones['price_return'].mean(),
|
|
144
|
+
'long_threshold': long_threshold,
|
|
145
|
+
'short_threshold': short_threshold,
|
|
146
|
+
'long_zones_std': long_zones['price_return'].std(),
|
|
147
|
+
'short_zones_std': short_zones['price_return'].std()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return HypothesisTestResult(
|
|
151
|
+
hypothesis="Zone duration affects price returns",
|
|
152
|
+
test_type="Independent t-test",
|
|
153
|
+
statistic=t_stat,
|
|
154
|
+
p_value=p_value,
|
|
155
|
+
significant=p_value < self.alpha,
|
|
156
|
+
alpha=self.alpha,
|
|
157
|
+
effect_size=effect_size,
|
|
158
|
+
sample_size=len(long_zones) + len(short_zones),
|
|
159
|
+
metadata=metadata
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
self.logger.error(f"Zone duration hypothesis test failed: {e}")
|
|
164
|
+
raise StatisticalAnalysisError(f"Zone duration test failed: {e}")
|
|
165
|
+
|
|
166
|
+
def test_histogram_slope_hypothesis(self, zones_features: List[Dict[str, Any]]) -> HypothesisTestResult:
|
|
167
|
+
"""
|
|
168
|
+
Тест гипотезы о корреляции между наклоном гистограммы MACD и длительностью зоны.
|
|
169
|
+
|
|
170
|
+
H0: Наклон гистограммы не коррелирует с длительностью зоны
|
|
171
|
+
H1: Существует значимая корреляция
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
zones_features: Список словарей с характеристиками зон
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
HypothesisTestResult с результатами теста
|
|
178
|
+
"""
|
|
179
|
+
self.logger.info("Testing histogram slope hypothesis")
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
df_features = pd.DataFrame(zones_features)
|
|
183
|
+
|
|
184
|
+
required_cols = ['hist_slope', 'duration']
|
|
185
|
+
missing_cols = [col for col in required_cols if col not in df_features.columns]
|
|
186
|
+
if missing_cols:
|
|
187
|
+
raise StatisticalAnalysisError(f"Missing required columns: {missing_cols}")
|
|
188
|
+
|
|
189
|
+
# Убираем NaN значения
|
|
190
|
+
clean_data = df_features[['hist_slope', 'duration']].dropna()
|
|
191
|
+
|
|
192
|
+
if len(clean_data) < 3:
|
|
193
|
+
raise StatisticalAnalysisError("Insufficient data for correlation test (need at least 3 points)")
|
|
194
|
+
|
|
195
|
+
# Вычисляем корреляцию Пирсона
|
|
196
|
+
correlation, p_value = stats.pearsonr(clean_data['hist_slope'], clean_data['duration'])
|
|
197
|
+
|
|
198
|
+
# Вычисляем t-статистику для корреляции
|
|
199
|
+
n = len(clean_data)
|
|
200
|
+
t_stat = correlation * np.sqrt((n - 2) / (1 - correlation**2))
|
|
201
|
+
|
|
202
|
+
# Доверительный интервал для корреляции (Fisher's z-transform)
|
|
203
|
+
z = np.arctanh(correlation)
|
|
204
|
+
se = 1 / np.sqrt(n - 3)
|
|
205
|
+
z_ci = stats.norm.interval(1 - self.alpha, loc=z, scale=se)
|
|
206
|
+
ci = (np.tanh(z_ci[0]), np.tanh(z_ci[1]))
|
|
207
|
+
|
|
208
|
+
metadata = {
|
|
209
|
+
'correlation': correlation,
|
|
210
|
+
'sample_size': n,
|
|
211
|
+
'degrees_of_freedom': n - 2,
|
|
212
|
+
'hist_slope_mean': clean_data['hist_slope'].mean(),
|
|
213
|
+
'hist_slope_std': clean_data['hist_slope'].std(),
|
|
214
|
+
'duration_mean': clean_data['duration'].mean(),
|
|
215
|
+
'duration_std': clean_data['duration'].std()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return HypothesisTestResult(
|
|
219
|
+
hypothesis="Histogram slope correlates with zone duration",
|
|
220
|
+
test_type="Pearson correlation test",
|
|
221
|
+
statistic=t_stat,
|
|
222
|
+
p_value=p_value,
|
|
223
|
+
significant=p_value < self.alpha,
|
|
224
|
+
alpha=self.alpha,
|
|
225
|
+
effect_size=correlation, # Корреляция как размер эффекта
|
|
226
|
+
confidence_interval=ci,
|
|
227
|
+
sample_size=n,
|
|
228
|
+
metadata=metadata
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
self.logger.error(f"Histogram slope hypothesis test failed: {e}")
|
|
233
|
+
raise StatisticalAnalysisError(f"Histogram slope test failed: {e}")
|
|
234
|
+
|
|
235
|
+
def test_bull_bear_asymmetry_hypothesis(self, zones_features: List[Dict[str, Any]]) -> HypothesisTestResult:
|
|
236
|
+
"""
|
|
237
|
+
Тест гипотезы об асимметрии между бычьими и медвежьими зонами.
|
|
238
|
+
|
|
239
|
+
H0: Нет различий между бычьими и медвежьими зонами
|
|
240
|
+
H1: Существуют значимые различия
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
zones_features: Список словарей с характеристиками зон
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
HypothesisTestResult с результатами теста
|
|
247
|
+
"""
|
|
248
|
+
self.logger.info("Testing bull-bear asymmetry hypothesis")
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
df_features = pd.DataFrame(zones_features)
|
|
252
|
+
|
|
253
|
+
required_cols = ['type', 'duration', 'price_return']
|
|
254
|
+
missing_cols = [col for col in required_cols if col not in df_features.columns]
|
|
255
|
+
if missing_cols:
|
|
256
|
+
raise StatisticalAnalysisError(f"Missing required columns: {missing_cols}")
|
|
257
|
+
|
|
258
|
+
bull_zones = df_features[df_features['type'] == 'bull']
|
|
259
|
+
bear_zones = df_features[df_features['type'] == 'bear']
|
|
260
|
+
|
|
261
|
+
if len(bull_zones) == 0 or len(bear_zones) == 0:
|
|
262
|
+
raise StatisticalAnalysisError("Insufficient data: need both bull and bear zones")
|
|
263
|
+
|
|
264
|
+
# Тест асимметрии по длительности
|
|
265
|
+
duration_stat, duration_p = stats.ttest_ind(
|
|
266
|
+
bull_zones['duration'].dropna(),
|
|
267
|
+
bear_zones['duration'].dropna()
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Тест асимметрии по доходности
|
|
271
|
+
return_stat, return_p = stats.ttest_ind(
|
|
272
|
+
bull_zones['price_return'].dropna(),
|
|
273
|
+
bear_zones['price_return'].dropna()
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Комбинированный p-value (метод Бонферрони)
|
|
277
|
+
combined_p = min(duration_p * 2, return_p * 2, 1.0)
|
|
278
|
+
|
|
279
|
+
# Размер эффекта для длительности (Cohen's d)
|
|
280
|
+
duration_pooled_std = np.sqrt(
|
|
281
|
+
((len(bull_zones) - 1) * bull_zones['duration'].var() +
|
|
282
|
+
(len(bear_zones) - 1) * bear_zones['duration'].var()) /
|
|
283
|
+
(len(bull_zones) + len(bear_zones) - 2)
|
|
284
|
+
)
|
|
285
|
+
duration_effect = (bull_zones['duration'].mean() - bear_zones['duration'].mean()) / duration_pooled_std
|
|
286
|
+
|
|
287
|
+
metadata = {
|
|
288
|
+
'duration_test': {
|
|
289
|
+
't_statistic': duration_stat,
|
|
290
|
+
'p_value': duration_p,
|
|
291
|
+
'significant': duration_p < self.alpha,
|
|
292
|
+
'bull_mean': bull_zones['duration'].mean(),
|
|
293
|
+
'bear_mean': bear_zones['duration'].mean(),
|
|
294
|
+
'effect_size': duration_effect
|
|
295
|
+
},
|
|
296
|
+
'return_test': {
|
|
297
|
+
't_statistic': return_stat,
|
|
298
|
+
'p_value': return_p,
|
|
299
|
+
'significant': return_p < self.alpha,
|
|
300
|
+
'bull_mean': bull_zones['price_return'].mean(),
|
|
301
|
+
'bear_mean': bear_zones['price_return'].mean()
|
|
302
|
+
},
|
|
303
|
+
'bull_zones_count': len(bull_zones),
|
|
304
|
+
'bear_zones_count': len(bear_zones)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return HypothesisTestResult(
|
|
308
|
+
hypothesis="Bullish and bearish zones are asymmetric",
|
|
309
|
+
test_type="Multiple t-tests with Bonferroni correction",
|
|
310
|
+
statistic=max(abs(duration_stat), abs(return_stat)),
|
|
311
|
+
p_value=combined_p,
|
|
312
|
+
significant=combined_p < self.alpha,
|
|
313
|
+
alpha=self.alpha,
|
|
314
|
+
effect_size=duration_effect,
|
|
315
|
+
sample_size=len(bull_zones) + len(bear_zones),
|
|
316
|
+
metadata=metadata
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
self.logger.error(f"Bull-bear asymmetry hypothesis test failed: {e}")
|
|
321
|
+
raise StatisticalAnalysisError(f"Bull-bear asymmetry test failed: {e}")
|
|
322
|
+
|
|
323
|
+
def test_sequence_hypothesis(self, zones_features: List[Dict[str, Any]]) -> HypothesisTestResult:
|
|
324
|
+
"""
|
|
325
|
+
Тест гипотезы о неслучайности последовательностей зон.
|
|
326
|
+
|
|
327
|
+
H0: Последовательности зон случайны
|
|
328
|
+
H1: Последовательности следуют неслучайным паттернам
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
zones_features: Список словарей с характеристиками зон
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
HypothesisTestResult с результатами теста
|
|
335
|
+
"""
|
|
336
|
+
self.logger.info("Testing sequence hypothesis")
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
if 'type' not in zones_features[0]:
|
|
340
|
+
raise StatisticalAnalysisError("Missing 'type' field in zone features")
|
|
341
|
+
|
|
342
|
+
# Создаем последовательность типов зон
|
|
343
|
+
zone_types = [zone['type'] for zone in zones_features]
|
|
344
|
+
|
|
345
|
+
if len(zone_types) < 3:
|
|
346
|
+
raise StatisticalAnalysisError("Need at least 3 zones for sequence analysis")
|
|
347
|
+
|
|
348
|
+
# Подсчитываем переходы
|
|
349
|
+
transitions = {}
|
|
350
|
+
for i in range(len(zone_types) - 1):
|
|
351
|
+
transition = f"{zone_types[i]}_to_{zone_types[i+1]}"
|
|
352
|
+
transitions[transition] = transitions.get(transition, 0) + 1
|
|
353
|
+
|
|
354
|
+
# Тест хи-квадрат на равномерность переходов
|
|
355
|
+
observed_freq = list(transitions.values())
|
|
356
|
+
|
|
357
|
+
if len(observed_freq) < 2:
|
|
358
|
+
raise StatisticalAnalysisError("Need at least 2 different transition types")
|
|
359
|
+
|
|
360
|
+
# Ожидаемая частота при равномерном распределении
|
|
361
|
+
total_transitions = sum(observed_freq)
|
|
362
|
+
expected_freq = total_transitions / len(observed_freq)
|
|
363
|
+
|
|
364
|
+
chi2_stat, chi2_p = stats.chisquare(observed_freq, [expected_freq] * len(observed_freq))
|
|
365
|
+
|
|
366
|
+
# Дополнительный тест: runs test для проверки случайности
|
|
367
|
+
# Преобразуем в бинарную последовательность
|
|
368
|
+
binary_sequence = [1 if zone_type == 'bull' else 0 for zone_type in zone_types]
|
|
369
|
+
runs_stat, runs_p = self._runs_test(binary_sequence)
|
|
370
|
+
|
|
371
|
+
# Комбинированный p-value
|
|
372
|
+
combined_p = min(chi2_p * 2, runs_p * 2, 1.0)
|
|
373
|
+
|
|
374
|
+
metadata = {
|
|
375
|
+
'transitions': transitions,
|
|
376
|
+
'total_transitions': total_transitions,
|
|
377
|
+
'chi2_statistic': chi2_stat,
|
|
378
|
+
'chi2_p_value': chi2_p,
|
|
379
|
+
'runs_statistic': runs_stat,
|
|
380
|
+
'runs_p_value': runs_p,
|
|
381
|
+
'sequence_length': len(zone_types),
|
|
382
|
+
'unique_transitions': len(transitions)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return HypothesisTestResult(
|
|
386
|
+
hypothesis="Zone sequences follow non-random patterns",
|
|
387
|
+
test_type="Chi-square and runs tests",
|
|
388
|
+
statistic=chi2_stat,
|
|
389
|
+
p_value=combined_p,
|
|
390
|
+
significant=combined_p < self.alpha,
|
|
391
|
+
alpha=self.alpha,
|
|
392
|
+
sample_size=len(zone_types),
|
|
393
|
+
metadata=metadata
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
self.logger.error(f"Sequence hypothesis test failed: {e}")
|
|
398
|
+
raise StatisticalAnalysisError(f"Sequence test failed: {e}")
|
|
399
|
+
|
|
400
|
+
def test_volatility_hypothesis(self, zones_features: List[Dict[str, Any]]) -> HypothesisTestResult:
|
|
401
|
+
"""
|
|
402
|
+
Тест гипотезы о влиянии волатильности на характеристики зон.
|
|
403
|
+
|
|
404
|
+
H0: Волатильность не влияет на характеристики зон
|
|
405
|
+
H1: Существует значимая связь
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
zones_features: Список словарей с характеристиками зон
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
HypothesisTestResult с результатами теста
|
|
412
|
+
"""
|
|
413
|
+
self.logger.info("Testing volatility hypothesis")
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
df_features = pd.DataFrame(zones_features)
|
|
417
|
+
|
|
418
|
+
# Определяем прокси волатильности
|
|
419
|
+
if 'price_return_atr' in df_features.columns:
|
|
420
|
+
volatility_proxy = df_features['price_return_atr']
|
|
421
|
+
vol_column = 'price_return_atr'
|
|
422
|
+
elif 'atr' in df_features.columns:
|
|
423
|
+
volatility_proxy = df_features['atr']
|
|
424
|
+
vol_column = 'atr'
|
|
425
|
+
else:
|
|
426
|
+
volatility_proxy = df_features['price_return'].abs()
|
|
427
|
+
vol_column = 'abs_price_return'
|
|
428
|
+
|
|
429
|
+
correlations = {}
|
|
430
|
+
|
|
431
|
+
# Тестируем корреляции с различными характеристиками зон
|
|
432
|
+
test_columns = ['duration', 'macd_amplitude', 'price_return']
|
|
433
|
+
available_columns = [col for col in test_columns if col in df_features.columns]
|
|
434
|
+
|
|
435
|
+
if not available_columns:
|
|
436
|
+
raise StatisticalAnalysisError("No suitable columns for volatility correlation test")
|
|
437
|
+
|
|
438
|
+
significant_correlations = 0
|
|
439
|
+
p_values = []
|
|
440
|
+
|
|
441
|
+
for col in available_columns:
|
|
442
|
+
clean_data = df_features[[vol_column, col]].dropna()
|
|
443
|
+
|
|
444
|
+
if len(clean_data) >= 3:
|
|
445
|
+
corr, p_val = stats.pearsonr(volatility_proxy.dropna(), clean_data[col])
|
|
446
|
+
correlations[f'volatility_{col}_correlation'] = {
|
|
447
|
+
'correlation': corr,
|
|
448
|
+
'p_value': p_val,
|
|
449
|
+
'significant': p_val < self.alpha,
|
|
450
|
+
'sample_size': len(clean_data)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if p_val < self.alpha:
|
|
454
|
+
significant_correlations += 1
|
|
455
|
+
p_values.append(p_val)
|
|
456
|
+
|
|
457
|
+
# Объединенный тест: корректировка для множественных сравнений (Holm-Bonferroni)
|
|
458
|
+
if p_values:
|
|
459
|
+
from statsmodels.stats.multitest import multipletests
|
|
460
|
+
corrected_p = multipletests(p_values, alpha=self.alpha, method='holm')[1]
|
|
461
|
+
combined_p = min(corrected_p) if corrected_p.size > 0 else 1.0
|
|
462
|
+
else:
|
|
463
|
+
combined_p = 1.0
|
|
464
|
+
|
|
465
|
+
# Суммарная статистика
|
|
466
|
+
avg_correlation = np.mean([corr_data['correlation'] for corr_data in correlations.values()])
|
|
467
|
+
|
|
468
|
+
metadata = {
|
|
469
|
+
'volatility_proxy': vol_column,
|
|
470
|
+
'correlations': correlations,
|
|
471
|
+
'significant_correlations': significant_correlations,
|
|
472
|
+
'total_correlations_tested': len(available_columns),
|
|
473
|
+
'volatility_mean': volatility_proxy.mean(),
|
|
474
|
+
'volatility_std': volatility_proxy.std(),
|
|
475
|
+
'individual_p_values': p_values
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return HypothesisTestResult(
|
|
479
|
+
hypothesis="Volatility affects zone characteristics",
|
|
480
|
+
test_type="Multiple correlation tests with Holm-Bonferroni correction",
|
|
481
|
+
statistic=avg_correlation,
|
|
482
|
+
p_value=combined_p,
|
|
483
|
+
significant=combined_p < self.alpha,
|
|
484
|
+
alpha=self.alpha,
|
|
485
|
+
effect_size=avg_correlation,
|
|
486
|
+
sample_size=len(df_features),
|
|
487
|
+
metadata=metadata
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
except Exception as e:
|
|
491
|
+
self.logger.error(f"Volatility hypothesis test failed: {e}")
|
|
492
|
+
raise StatisticalAnalysisError(f"Volatility test failed: {e}")
|
|
493
|
+
|
|
494
|
+
def _runs_test(self, binary_sequence: List[int]) -> tuple:
|
|
495
|
+
"""
|
|
496
|
+
Runs test для проверки случайности бинарной последовательности.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
binary_sequence: Последовательность из 0 и 1
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Tuple (z_statistic, p_value)
|
|
503
|
+
"""
|
|
504
|
+
n = len(binary_sequence)
|
|
505
|
+
n1 = sum(binary_sequence)
|
|
506
|
+
n0 = n - n1
|
|
507
|
+
|
|
508
|
+
if n1 == 0 or n0 == 0:
|
|
509
|
+
return 0.0, 1.0
|
|
510
|
+
|
|
511
|
+
# Подсчет runs (серий)
|
|
512
|
+
runs = 1
|
|
513
|
+
for i in range(1, n):
|
|
514
|
+
if binary_sequence[i] != binary_sequence[i-1]:
|
|
515
|
+
runs += 1
|
|
516
|
+
|
|
517
|
+
# Ожидаемое количество runs
|
|
518
|
+
expected_runs = (2 * n1 * n0) / n + 1
|
|
519
|
+
|
|
520
|
+
# Дисперсия
|
|
521
|
+
variance = (2 * n1 * n0 * (2 * n1 * n0 - n)) / (n**2 * (n - 1))
|
|
522
|
+
|
|
523
|
+
if variance <= 0:
|
|
524
|
+
return 0.0, 1.0
|
|
525
|
+
|
|
526
|
+
# Z-статистика
|
|
527
|
+
z = (runs - expected_runs) / np.sqrt(variance)
|
|
528
|
+
|
|
529
|
+
# p-value (двусторонний тест)
|
|
530
|
+
p_value = 2 * (1 - stats.norm.cdf(abs(z)))
|
|
531
|
+
|
|
532
|
+
return z, p_value
|
|
533
|
+
|
|
534
|
+
def run_all_tests(self, zones_features: List[Dict[str, Any]]) -> AnalysisResult:
|
|
535
|
+
"""
|
|
536
|
+
Выполнить все тесты гипотез.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
zones_features: Список словарей с характеристиками зон
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
AnalysisResult с результатами всех тестов
|
|
543
|
+
"""
|
|
544
|
+
self.logger.info("Running all hypothesis tests")
|
|
545
|
+
|
|
546
|
+
if not zones_features:
|
|
547
|
+
raise StatisticalAnalysisError("No zone features provided")
|
|
548
|
+
|
|
549
|
+
tests = {}
|
|
550
|
+
|
|
551
|
+
# Выполняем все тесты
|
|
552
|
+
test_methods = [
|
|
553
|
+
('zone_duration', self.test_zone_duration_hypothesis),
|
|
554
|
+
('histogram_slope', self.test_histogram_slope_hypothesis),
|
|
555
|
+
('bull_bear_asymmetry', self.test_bull_bear_asymmetry_hypothesis),
|
|
556
|
+
('sequence_patterns', self.test_sequence_hypothesis),
|
|
557
|
+
('volatility_effects', self.test_volatility_hypothesis)
|
|
558
|
+
]
|
|
559
|
+
|
|
560
|
+
for test_name, test_method in test_methods:
|
|
561
|
+
try:
|
|
562
|
+
result = test_method(zones_features)
|
|
563
|
+
tests[test_name] = result.to_dict()
|
|
564
|
+
except Exception as e:
|
|
565
|
+
self.logger.warning(f"Test {test_name} failed: {e}")
|
|
566
|
+
tests[test_name] = {
|
|
567
|
+
'error': str(e),
|
|
568
|
+
'test_type': test_name,
|
|
569
|
+
'significant': False
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
# Подсчет значимых результатов
|
|
573
|
+
significant_count = sum(1 for test_result in tests.values()
|
|
574
|
+
if test_result.get('significant', False))
|
|
575
|
+
|
|
576
|
+
# Сводка
|
|
577
|
+
summary = {
|
|
578
|
+
'total_tests': len(tests),
|
|
579
|
+
'significant_tests': significant_count,
|
|
580
|
+
'significance_rate': significant_count / len(tests) if tests else 0,
|
|
581
|
+
'alpha_level': self.alpha,
|
|
582
|
+
'total_zones': len(zones_features)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
results = {
|
|
586
|
+
'tests': tests,
|
|
587
|
+
'summary': summary
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
metadata = {
|
|
591
|
+
'analyzer': 'HypothesisTestSuite',
|
|
592
|
+
'alpha': self.alpha,
|
|
593
|
+
'timestamp': datetime.now().isoformat()
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return AnalysisResult(
|
|
597
|
+
analysis_type='hypothesis_testing',
|
|
598
|
+
results=results,
|
|
599
|
+
data_size=len(zones_features),
|
|
600
|
+
metadata=metadata
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
# Удобные функции для быстрого использования
|
|
605
|
+
def run_all_hypothesis_tests(zones_features: List[Dict[str, Any]], alpha: float = 0.05) -> Dict[str, Any]:
|
|
606
|
+
"""
|
|
607
|
+
Выполнить все тесты гипотез (совместимость с оригинальным API).
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
zones_features: Список словарей с характеристиками зон
|
|
611
|
+
alpha: Уровень значимости
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Словарь с результатами всех тестов
|
|
615
|
+
"""
|
|
616
|
+
test_suite = HypothesisTestSuite(alpha=alpha)
|
|
617
|
+
analysis_result = test_suite.run_all_tests(zones_features)
|
|
618
|
+
return analysis_result.results
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def test_single_hypothesis(zones_features: List[Dict[str, Any]],
|
|
622
|
+
test_type: str,
|
|
623
|
+
alpha: float = 0.05) -> HypothesisTestResult:
|
|
624
|
+
"""
|
|
625
|
+
Выполнить один конкретный тест гипотезы.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
zones_features: Список словарей с характеристиками зон
|
|
629
|
+
test_type: Тип теста ('duration', 'slope', 'asymmetry', 'sequence', 'volatility')
|
|
630
|
+
alpha: Уровень значимости
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
HypothesisTestResult с результатами теста
|
|
634
|
+
"""
|
|
635
|
+
test_suite = HypothesisTestSuite(alpha=alpha)
|
|
636
|
+
|
|
637
|
+
test_mapping = {
|
|
638
|
+
'duration': test_suite.test_zone_duration_hypothesis,
|
|
639
|
+
'slope': test_suite.test_histogram_slope_hypothesis,
|
|
640
|
+
'asymmetry': test_suite.test_bull_bear_asymmetry_hypothesis,
|
|
641
|
+
'sequence': test_suite.test_sequence_hypothesis,
|
|
642
|
+
'volatility': test_suite.test_volatility_hypothesis
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if test_type not in test_mapping:
|
|
646
|
+
raise ValueError(f"Unknown test type: {test_type}. Available: {list(test_mapping.keys())}")
|
|
647
|
+
|
|
648
|
+
return test_mapping[test_type](zones_features)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
# Экспорт
|
|
652
|
+
__all__ = [
|
|
653
|
+
'HypothesisTestResult',
|
|
654
|
+
'HypothesisTestSuite',
|
|
655
|
+
'run_all_hypothesis_tests',
|
|
656
|
+
'test_single_hypothesis'
|
|
657
|
+
]
|