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,520 @@
|
|
|
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 scipy.signal import find_peaks
|
|
12
|
+
from typing import Dict, Any, List, Optional, Union
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from ...core.logging_config import get_logger
|
|
17
|
+
from ...core.exceptions import AnalysisError
|
|
18
|
+
from .. import AnalysisResult, BaseAnalyzer
|
|
19
|
+
|
|
20
|
+
# Получаем логгер для модуля
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ZoneFeatures:
|
|
26
|
+
"""
|
|
27
|
+
Характеристики торговой зоны.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
zone_id: Уникальный идентификатор зоны
|
|
31
|
+
zone_type: Тип зоны ('bull' или 'bear')
|
|
32
|
+
duration: Длительность зоны в периодах
|
|
33
|
+
start_price: Цена в начале зоны
|
|
34
|
+
end_price: Цена в конце зоны
|
|
35
|
+
price_return: Доходность за зону
|
|
36
|
+
macd_amplitude: Амплитуда MACD в зоне
|
|
37
|
+
hist_amplitude: Амплитуда гистограммы MACD
|
|
38
|
+
price_range_pct: Ценовой диапазон в процентах
|
|
39
|
+
atr_normalized_return: Доходность, нормализованная на ATR
|
|
40
|
+
correlation_price_hist: Корреляция между ценой и гистограммой
|
|
41
|
+
num_peaks: Количество пиков в зоне
|
|
42
|
+
num_troughs: Количество впадин в зоне
|
|
43
|
+
drawdown_from_peak: Просадка от пика (для бычьих зон)
|
|
44
|
+
rally_from_trough: Отскок от минимума (для медвежьих зон)
|
|
45
|
+
metadata: Дополнительные метаданные
|
|
46
|
+
"""
|
|
47
|
+
zone_id: str
|
|
48
|
+
zone_type: str
|
|
49
|
+
duration: int
|
|
50
|
+
start_price: float
|
|
51
|
+
end_price: float
|
|
52
|
+
price_return: float
|
|
53
|
+
macd_amplitude: float
|
|
54
|
+
hist_amplitude: float
|
|
55
|
+
price_range_pct: float
|
|
56
|
+
atr_normalized_return: Optional[float] = None
|
|
57
|
+
correlation_price_hist: Optional[float] = None
|
|
58
|
+
num_peaks: Optional[int] = None
|
|
59
|
+
num_troughs: Optional[int] = None
|
|
60
|
+
drawdown_from_peak: Optional[float] = None
|
|
61
|
+
rally_from_trough: Optional[float] = None
|
|
62
|
+
metadata: Dict[str, Any] = None
|
|
63
|
+
|
|
64
|
+
def __post_init__(self):
|
|
65
|
+
if self.metadata is None:
|
|
66
|
+
self.metadata = {}
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
69
|
+
"""Конвертация в словарь."""
|
|
70
|
+
result = {}
|
|
71
|
+
for field, value in self.__dict__.items():
|
|
72
|
+
if isinstance(value, (int, float, str, bool)) or value is None:
|
|
73
|
+
result[field] = value
|
|
74
|
+
elif isinstance(value, dict):
|
|
75
|
+
result[field] = value
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ZoneFeaturesAnalyzer(BaseAnalyzer):
|
|
80
|
+
"""
|
|
81
|
+
Анализатор характеристик торговых зон.
|
|
82
|
+
|
|
83
|
+
Предоставляет методы для:
|
|
84
|
+
- Извлечения признаков зон
|
|
85
|
+
- Анализа распределения характеристик
|
|
86
|
+
- Статистического анализа зон
|
|
87
|
+
- Сравнительного анализа типов зон
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, min_duration: int = 2, min_amplitude: float = 0.001):
|
|
91
|
+
"""
|
|
92
|
+
Инициализация анализатора.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
min_duration: Минимальная длительность зоны
|
|
96
|
+
min_amplitude: Минимальная амплитуда для значимой зоны
|
|
97
|
+
"""
|
|
98
|
+
super().__init__("ZoneFeaturesAnalyzer")
|
|
99
|
+
self.min_duration = min_duration
|
|
100
|
+
self.min_amplitude = min_amplitude
|
|
101
|
+
self.logger = get_logger(f"{__name__}.ZoneFeaturesAnalyzer")
|
|
102
|
+
|
|
103
|
+
self.logger.info(f"Initialized zone features analyzer with min_duration={min_duration}, min_amplitude={min_amplitude}")
|
|
104
|
+
|
|
105
|
+
def extract_zone_features(self, zone_info: Dict[str, Any]) -> ZoneFeatures:
|
|
106
|
+
"""
|
|
107
|
+
Извлечение признаков из информации о зоне.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
zone_info: Словарь с информацией о зоне
|
|
111
|
+
- zone_id: ID зоны
|
|
112
|
+
- type: Тип зоны ('bull'/'bear')
|
|
113
|
+
- duration: Длительность
|
|
114
|
+
- data: DataFrame с OHLCV + MACD + ATR
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
ZoneFeatures: Объект с характеристиками зоны
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
data = zone_info['data']
|
|
121
|
+
zone_type = zone_info['type']
|
|
122
|
+
zone_id = zone_info.get('zone_id', f"{zone_type}_{len(data)}")
|
|
123
|
+
|
|
124
|
+
if len(data) < self.min_duration:
|
|
125
|
+
raise AnalysisError(f"Zone duration {len(data)} is less than minimum {self.min_duration}")
|
|
126
|
+
|
|
127
|
+
# Базовые характеристики
|
|
128
|
+
start_price = float(data['close'].iloc[0])
|
|
129
|
+
end_price = float(data['close'].iloc[-1])
|
|
130
|
+
price_return = (end_price / start_price) - 1
|
|
131
|
+
|
|
132
|
+
# MACD характеристики
|
|
133
|
+
max_macd = float(data['macd'].max())
|
|
134
|
+
min_macd = float(data['macd'].min())
|
|
135
|
+
macd_amplitude = max_macd - min_macd
|
|
136
|
+
|
|
137
|
+
# Гистограмма MACD
|
|
138
|
+
max_hist = float(data['macd_hist'].max())
|
|
139
|
+
min_hist = float(data['macd_hist'].min())
|
|
140
|
+
hist_amplitude = max_hist - min_hist
|
|
141
|
+
|
|
142
|
+
# Ценовые характеристики
|
|
143
|
+
max_price = float(data['high'].max())
|
|
144
|
+
min_price = float(data['low'].min())
|
|
145
|
+
price_range_pct = (max_price / min_price) - 1
|
|
146
|
+
|
|
147
|
+
# ATR нормализация
|
|
148
|
+
atr_normalized_return = None
|
|
149
|
+
if 'atr' in data.columns and data['atr'].iloc[0] > 0:
|
|
150
|
+
atr_normalized_return = price_return / float(data['atr'].iloc[0])
|
|
151
|
+
|
|
152
|
+
# Корреляция цены и гистограммы
|
|
153
|
+
correlation_price_hist = None
|
|
154
|
+
if len(data) >= 3:
|
|
155
|
+
try:
|
|
156
|
+
correlation_price_hist = float(data['close'].corr(data['macd_hist']))
|
|
157
|
+
except:
|
|
158
|
+
correlation_price_hist = None
|
|
159
|
+
|
|
160
|
+
# Анализ пиков и впадин
|
|
161
|
+
num_peaks = None
|
|
162
|
+
num_troughs = None
|
|
163
|
+
try:
|
|
164
|
+
peaks, _ = find_peaks(data['high'].values, height=data['high'].mean())
|
|
165
|
+
troughs, _ = find_peaks(-data['low'].values, height=-data['low'].mean())
|
|
166
|
+
num_peaks = len(peaks)
|
|
167
|
+
num_troughs = len(troughs)
|
|
168
|
+
except:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
# Специфичные для типа зоны метрики
|
|
172
|
+
drawdown_from_peak = None
|
|
173
|
+
rally_from_trough = None
|
|
174
|
+
|
|
175
|
+
if zone_type == 'bull':
|
|
176
|
+
# Просадка от пика
|
|
177
|
+
drawdown_from_peak = (end_price / max_price) - 1
|
|
178
|
+
elif zone_type == 'bear':
|
|
179
|
+
# Отскок от минимума
|
|
180
|
+
rally_from_trough = (end_price / min_price) - 1
|
|
181
|
+
|
|
182
|
+
# Метаданные
|
|
183
|
+
metadata = {
|
|
184
|
+
'data_points': len(data),
|
|
185
|
+
'start_timestamp': str(data.index[0]) if hasattr(data.index[0], '__str__') else None,
|
|
186
|
+
'end_timestamp': str(data.index[-1]) if hasattr(data.index[-1], '__str__') else None,
|
|
187
|
+
'max_macd': max_macd,
|
|
188
|
+
'min_macd': min_macd,
|
|
189
|
+
'avg_macd': float(data['macd'].mean()),
|
|
190
|
+
'macd_std': float(data['macd'].std()),
|
|
191
|
+
'max_hist': max_hist,
|
|
192
|
+
'min_hist': min_hist,
|
|
193
|
+
'avg_hist': float(data['macd_hist'].mean()),
|
|
194
|
+
'hist_std': float(data['macd_hist'].std()),
|
|
195
|
+
'max_price': max_price,
|
|
196
|
+
'min_price': min_price,
|
|
197
|
+
'price_range': max_price - min_price
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if 'atr' in data.columns:
|
|
201
|
+
metadata.update({
|
|
202
|
+
'atr_start': float(data['atr'].iloc[0]),
|
|
203
|
+
'atr_end': float(data['atr'].iloc[-1]),
|
|
204
|
+
'avg_atr': float(data['atr'].mean())
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
return ZoneFeatures(
|
|
208
|
+
zone_id=zone_id,
|
|
209
|
+
zone_type=zone_type,
|
|
210
|
+
duration=len(data),
|
|
211
|
+
start_price=start_price,
|
|
212
|
+
end_price=end_price,
|
|
213
|
+
price_return=price_return,
|
|
214
|
+
macd_amplitude=macd_amplitude,
|
|
215
|
+
hist_amplitude=hist_amplitude,
|
|
216
|
+
price_range_pct=price_range_pct,
|
|
217
|
+
atr_normalized_return=atr_normalized_return,
|
|
218
|
+
correlation_price_hist=correlation_price_hist,
|
|
219
|
+
num_peaks=num_peaks,
|
|
220
|
+
num_troughs=num_troughs,
|
|
221
|
+
drawdown_from_peak=drawdown_from_peak,
|
|
222
|
+
rally_from_trough=rally_from_trough,
|
|
223
|
+
metadata=metadata
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
self.logger.error(f"Failed to extract zone features: {e}")
|
|
228
|
+
raise AnalysisError(f"Failed to extract zone features: {e}")
|
|
229
|
+
|
|
230
|
+
def analyze_zones_distribution(self, zones_features: List[Union[ZoneFeatures, Dict[str, Any]]]) -> AnalysisResult:
|
|
231
|
+
"""
|
|
232
|
+
Анализ распределения характеристик зон.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
zones_features: Список объектов ZoneFeatures или словарей
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
AnalysisResult с анализом распределения
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
self.logger.info(f"Analyzing distribution of {len(zones_features)} zones")
|
|
242
|
+
|
|
243
|
+
if not zones_features:
|
|
244
|
+
raise AnalysisError("No zones features provided")
|
|
245
|
+
|
|
246
|
+
# Конвертируем в DataFrame
|
|
247
|
+
features_dicts = []
|
|
248
|
+
for zone in zones_features:
|
|
249
|
+
if isinstance(zone, ZoneFeatures):
|
|
250
|
+
features_dicts.append(zone.to_dict())
|
|
251
|
+
elif isinstance(zone, dict):
|
|
252
|
+
features_dicts.append(zone)
|
|
253
|
+
else:
|
|
254
|
+
raise AnalysisError(f"Invalid zone features type: {type(zone)}")
|
|
255
|
+
|
|
256
|
+
df_features = pd.DataFrame(features_dicts)
|
|
257
|
+
|
|
258
|
+
# Разделяем по типу зон
|
|
259
|
+
bull_zones = df_features[df_features['zone_type'] == 'bull']
|
|
260
|
+
bear_zones = df_features[df_features['zone_type'] == 'bear']
|
|
261
|
+
|
|
262
|
+
# Общая статистика
|
|
263
|
+
total_stats = {
|
|
264
|
+
'total_zones': len(df_features),
|
|
265
|
+
'bull_zones_count': len(bull_zones),
|
|
266
|
+
'bear_zones_count': len(bear_zones),
|
|
267
|
+
'bull_ratio': len(bull_zones) / len(df_features) if len(df_features) > 0 else 0,
|
|
268
|
+
'bear_ratio': len(bear_zones) / len(df_features) if len(df_features) > 0 else 0
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Статистика по длительности
|
|
272
|
+
duration_stats = self._calculate_distribution_stats(df_features, bull_zones, bear_zones, 'duration')
|
|
273
|
+
|
|
274
|
+
# Статистика по доходности
|
|
275
|
+
return_stats = self._calculate_distribution_stats(df_features, bull_zones, bear_zones, 'price_return')
|
|
276
|
+
|
|
277
|
+
# Статистика по амплитуде MACD
|
|
278
|
+
macd_amplitude_stats = self._calculate_distribution_stats(df_features, bull_zones, bear_zones, 'macd_amplitude')
|
|
279
|
+
|
|
280
|
+
# Статистика по амплитуде гистограммы
|
|
281
|
+
hist_amplitude_stats = self._calculate_distribution_stats(df_features, bull_zones, bear_zones, 'hist_amplitude')
|
|
282
|
+
|
|
283
|
+
# Дополнительные метрики
|
|
284
|
+
additional_stats = {}
|
|
285
|
+
|
|
286
|
+
# Корреляции
|
|
287
|
+
if 'correlation_price_hist' in df_features.columns:
|
|
288
|
+
correlation_data = df_features['correlation_price_hist'].dropna()
|
|
289
|
+
if len(correlation_data) > 0:
|
|
290
|
+
additional_stats['price_hist_correlation'] = {
|
|
291
|
+
'mean': float(correlation_data.mean()),
|
|
292
|
+
'std': float(correlation_data.std()),
|
|
293
|
+
'positive_correlations': len(correlation_data[correlation_data > 0]),
|
|
294
|
+
'negative_correlations': len(correlation_data[correlation_data < 0]),
|
|
295
|
+
'strong_correlations': len(correlation_data[abs(correlation_data) > 0.7])
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# Пики и впадины
|
|
299
|
+
if 'num_peaks' in df_features.columns and 'num_troughs' in df_features.columns:
|
|
300
|
+
peaks_data = df_features['num_peaks'].dropna()
|
|
301
|
+
troughs_data = df_features['num_troughs'].dropna()
|
|
302
|
+
|
|
303
|
+
if len(peaks_data) > 0 and len(troughs_data) > 0:
|
|
304
|
+
additional_stats['peaks_troughs'] = {
|
|
305
|
+
'avg_peaks_per_zone': float(peaks_data.mean()),
|
|
306
|
+
'avg_troughs_per_zone': float(troughs_data.mean()),
|
|
307
|
+
'zones_with_peaks': len(peaks_data[peaks_data > 0]),
|
|
308
|
+
'zones_with_troughs': len(troughs_data[troughs_data > 0])
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# Объединяем все результаты
|
|
312
|
+
results = {
|
|
313
|
+
'total_statistics': total_stats,
|
|
314
|
+
'duration_distribution': duration_stats,
|
|
315
|
+
'return_distribution': return_stats,
|
|
316
|
+
'macd_amplitude_distribution': macd_amplitude_stats,
|
|
317
|
+
'hist_amplitude_distribution': hist_amplitude_stats,
|
|
318
|
+
'additional_metrics': additional_stats
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
metadata = {
|
|
322
|
+
'analyzer': 'ZoneFeaturesAnalyzer',
|
|
323
|
+
'analysis_method': 'zones_distribution',
|
|
324
|
+
'min_duration': self.min_duration,
|
|
325
|
+
'min_amplitude': self.min_amplitude,
|
|
326
|
+
'timestamp': datetime.now().isoformat()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return AnalysisResult(
|
|
330
|
+
analysis_type='zones_distribution',
|
|
331
|
+
results=results,
|
|
332
|
+
data_size=len(zones_features),
|
|
333
|
+
metadata=metadata
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
self.logger.error(f"Zones distribution analysis failed: {e}")
|
|
338
|
+
raise AnalysisError(f"Zones distribution analysis failed: {e}")
|
|
339
|
+
|
|
340
|
+
def _calculate_distribution_stats(self, df_all: pd.DataFrame,
|
|
341
|
+
df_bull: pd.DataFrame,
|
|
342
|
+
df_bear: pd.DataFrame,
|
|
343
|
+
column: str) -> Dict[str, Any]:
|
|
344
|
+
"""
|
|
345
|
+
Вычисление статистики распределения для конкретной характеристики.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
df_all: Все зоны
|
|
349
|
+
df_bull: Бычьи зоны
|
|
350
|
+
df_bear: Медвежьи зоны
|
|
351
|
+
column: Название колонки для анализа
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Словарь со статистикой распределения
|
|
355
|
+
"""
|
|
356
|
+
stats_dict = {}
|
|
357
|
+
|
|
358
|
+
# Общая статистика
|
|
359
|
+
if column in df_all.columns:
|
|
360
|
+
all_data = df_all[column].dropna()
|
|
361
|
+
if len(all_data) > 0:
|
|
362
|
+
stats_dict['overall'] = {
|
|
363
|
+
'mean': float(all_data.mean()),
|
|
364
|
+
'median': float(all_data.median()),
|
|
365
|
+
'std': float(all_data.std()),
|
|
366
|
+
'min': float(all_data.min()),
|
|
367
|
+
'max': float(all_data.max()),
|
|
368
|
+
'q25': float(all_data.quantile(0.25)),
|
|
369
|
+
'q75': float(all_data.quantile(0.75)),
|
|
370
|
+
'skewness': float(all_data.skew()),
|
|
371
|
+
'kurtosis': float(all_data.kurtosis())
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# Статистика для бычьих зон
|
|
375
|
+
if column in df_bull.columns and len(df_bull) > 0:
|
|
376
|
+
bull_data = df_bull[column].dropna()
|
|
377
|
+
if len(bull_data) > 0:
|
|
378
|
+
stats_dict['bull'] = {
|
|
379
|
+
'mean': float(bull_data.mean()),
|
|
380
|
+
'median': float(bull_data.median()),
|
|
381
|
+
'std': float(bull_data.std()),
|
|
382
|
+
'min': float(bull_data.min()),
|
|
383
|
+
'max': float(bull_data.max()),
|
|
384
|
+
'count': len(bull_data)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
# Статистика для медвежьих зон
|
|
388
|
+
if column in df_bear.columns and len(df_bear) > 0:
|
|
389
|
+
bear_data = df_bear[column].dropna()
|
|
390
|
+
if len(bear_data) > 0:
|
|
391
|
+
stats_dict['bear'] = {
|
|
392
|
+
'mean': float(bear_data.mean()),
|
|
393
|
+
'median': float(bear_data.median()),
|
|
394
|
+
'std': float(bear_data.std()),
|
|
395
|
+
'min': float(bear_data.min()),
|
|
396
|
+
'max': float(bear_data.max()),
|
|
397
|
+
'count': len(bear_data)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# Сравнительная статистика
|
|
401
|
+
if ('bull' in stats_dict and 'bear' in stats_dict and
|
|
402
|
+
column in df_bull.columns and column in df_bear.columns):
|
|
403
|
+
|
|
404
|
+
bull_data = df_bull[column].dropna()
|
|
405
|
+
bear_data = df_bear[column].dropna()
|
|
406
|
+
|
|
407
|
+
if len(bull_data) > 1 and len(bear_data) > 1:
|
|
408
|
+
try:
|
|
409
|
+
# t-тест для сравнения средних
|
|
410
|
+
t_stat, p_value = stats.ttest_ind(bull_data, bear_data)
|
|
411
|
+
|
|
412
|
+
stats_dict['comparison'] = {
|
|
413
|
+
't_statistic': float(t_stat),
|
|
414
|
+
'p_value': float(p_value),
|
|
415
|
+
'significant_difference': p_value < 0.05,
|
|
416
|
+
'bull_vs_bear_ratio': stats_dict['bull']['mean'] / stats_dict['bear']['mean'] if stats_dict['bear']['mean'] != 0 else None
|
|
417
|
+
}
|
|
418
|
+
except Exception as e:
|
|
419
|
+
self.logger.warning(f"Failed to calculate comparison stats for {column}: {e}")
|
|
420
|
+
|
|
421
|
+
return stats_dict
|
|
422
|
+
|
|
423
|
+
def get_zone_features_summary(self, zones_features: List[Union[ZoneFeatures, Dict[str, Any]]]) -> Dict[str, Any]:
|
|
424
|
+
"""
|
|
425
|
+
Получение краткой сводки по характеристикам зон.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
zones_features: Список объектов ZoneFeatures или словарей
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Словарь с краткой сводкой
|
|
432
|
+
"""
|
|
433
|
+
try:
|
|
434
|
+
if not zones_features:
|
|
435
|
+
return {'error': 'No zones features provided'}
|
|
436
|
+
|
|
437
|
+
# Конвертируем в DataFrame
|
|
438
|
+
features_dicts = []
|
|
439
|
+
for zone in zones_features:
|
|
440
|
+
if isinstance(zone, ZoneFeatures):
|
|
441
|
+
features_dicts.append(zone.to_dict())
|
|
442
|
+
elif isinstance(zone, dict):
|
|
443
|
+
features_dicts.append(zone)
|
|
444
|
+
|
|
445
|
+
df_features = pd.DataFrame(features_dicts)
|
|
446
|
+
|
|
447
|
+
bull_zones = df_features[df_features['zone_type'] == 'bull']
|
|
448
|
+
bear_zones = df_features[df_features['zone_type'] == 'bear']
|
|
449
|
+
|
|
450
|
+
summary = {
|
|
451
|
+
'total_zones': len(df_features),
|
|
452
|
+
'bull_zones': len(bull_zones),
|
|
453
|
+
'bear_zones': len(bear_zones),
|
|
454
|
+
'avg_duration': float(df_features['duration'].mean()) if 'duration' in df_features.columns else None,
|
|
455
|
+
'avg_return': float(df_features['price_return'].mean()) if 'price_return' in df_features.columns else None,
|
|
456
|
+
'positive_returns': len(df_features[df_features['price_return'] > 0]) if 'price_return' in df_features.columns else None,
|
|
457
|
+
'negative_returns': len(df_features[df_features['price_return'] < 0]) if 'price_return' in df_features.columns else None
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if len(bull_zones) > 0:
|
|
461
|
+
summary['bull_avg_duration'] = float(bull_zones['duration'].mean()) if 'duration' in bull_zones.columns else None
|
|
462
|
+
summary['bull_avg_return'] = float(bull_zones['price_return'].mean()) if 'price_return' in bull_zones.columns else None
|
|
463
|
+
|
|
464
|
+
if len(bear_zones) > 0:
|
|
465
|
+
summary['bear_avg_duration'] = float(bear_zones['duration'].mean()) if 'duration' in bear_zones.columns else None
|
|
466
|
+
summary['bear_avg_return'] = float(bear_zones['price_return'].mean()) if 'price_return' in bear_zones.columns else None
|
|
467
|
+
|
|
468
|
+
return summary
|
|
469
|
+
|
|
470
|
+
except Exception as e:
|
|
471
|
+
self.logger.error(f"Failed to get zone features summary: {e}")
|
|
472
|
+
return {'error': str(e)}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# Удобные функции для быстрого использования
|
|
476
|
+
def analyze_zones_distribution(zones_features: List[Union[ZoneFeatures, Dict[str, Any]]],
|
|
477
|
+
min_duration: int = 2,
|
|
478
|
+
min_amplitude: float = 0.001) -> Dict[str, Any]:
|
|
479
|
+
"""
|
|
480
|
+
Анализ распределения зон (совместимость с оригинальным API).
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
zones_features: Список характеристик зон
|
|
484
|
+
min_duration: Минимальная длительность зоны
|
|
485
|
+
min_amplitude: Минимальная амплитуда
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
Словарь с результатами анализа
|
|
489
|
+
"""
|
|
490
|
+
analyzer = ZoneFeaturesAnalyzer(min_duration=min_duration, min_amplitude=min_amplitude)
|
|
491
|
+
analysis_result = analyzer.analyze_zones_distribution(zones_features)
|
|
492
|
+
return analysis_result.results
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def extract_zone_features(zone_info: Dict[str, Any],
|
|
496
|
+
min_duration: int = 2,
|
|
497
|
+
min_amplitude: float = 0.001) -> Dict[str, Any]:
|
|
498
|
+
"""
|
|
499
|
+
Извлечение признаков зоны (совместимость с оригинальным API).
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
zone_info: Информация о зоне
|
|
503
|
+
min_duration: Минимальная длительность зоны
|
|
504
|
+
min_amplitude: Минимальная амплитуда
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Словарь с характеристиками зоны
|
|
508
|
+
"""
|
|
509
|
+
analyzer = ZoneFeaturesAnalyzer(min_duration=min_duration, min_amplitude=min_amplitude)
|
|
510
|
+
zone_features = analyzer.extract_zone_features(zone_info)
|
|
511
|
+
return zone_features.to_dict()
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# Экспорт
|
|
515
|
+
__all__ = [
|
|
516
|
+
'ZoneFeatures',
|
|
517
|
+
'ZoneFeaturesAnalyzer',
|
|
518
|
+
'analyze_zones_distribution',
|
|
519
|
+
'extract_zone_features'
|
|
520
|
+
]
|
bquant/cli.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BQuant CLI Module
|
|
3
|
+
|
|
4
|
+
Простой CLI интерфейс для основных функций BQuant.
|
|
5
|
+
Работает "из коробки" без внешних зависимостей.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .core.logging_config import get_logger
|
|
13
|
+
from .data.samples import get_sample_data, list_datasets, print_sample_data_status
|
|
14
|
+
from .indicators import MACD
|
|
15
|
+
from .visualization import charts
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def analyze_macd(dataset_name: str = 'tv_xauusd_1h', output_file: Optional[str] = None):
|
|
21
|
+
"""
|
|
22
|
+
Простой анализ MACD для sample данных.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
dataset_name: Название датасета для анализа
|
|
26
|
+
output_file: Путь для сохранения графика (опционально)
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
print(f"🔍 Анализ MACD для датасета: {dataset_name}")
|
|
30
|
+
|
|
31
|
+
# Загружаем данные
|
|
32
|
+
print("📊 Загрузка данных...")
|
|
33
|
+
data = get_sample_data(dataset_name)
|
|
34
|
+
print(f" Загружено {len(data)} записей")
|
|
35
|
+
|
|
36
|
+
# Рассчитываем MACD
|
|
37
|
+
print("📈 Расчет MACD...")
|
|
38
|
+
macd = MACD()
|
|
39
|
+
result = macd.calculate(data)
|
|
40
|
+
print(" MACD рассчитан успешно")
|
|
41
|
+
|
|
42
|
+
# Создаем график
|
|
43
|
+
print("📊 Создание графика...")
|
|
44
|
+
try:
|
|
45
|
+
# Создаем простой график цен с MACD
|
|
46
|
+
fig = charts.create_price_chart(data, chart_type='line', title=f"Price Chart - {dataset_name}")
|
|
47
|
+
|
|
48
|
+
if output_file:
|
|
49
|
+
fig.write_html(output_file)
|
|
50
|
+
print(f" График сохранен в: {output_file}")
|
|
51
|
+
else:
|
|
52
|
+
fig.show()
|
|
53
|
+
print(" График отображен в браузере")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f" ⚠️ Не удалось создать график: {e}")
|
|
56
|
+
print(" ✅ Анализ MACD завершен успешно (без графика)")
|
|
57
|
+
|
|
58
|
+
print("✅ Анализ завершен успешно!")
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"❌ Ошибка при анализе: {e}")
|
|
62
|
+
logger.error(f"MACD analysis failed: {e}")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def list_available_data():
|
|
67
|
+
"""Показать список доступных sample данных."""
|
|
68
|
+
try:
|
|
69
|
+
print("📋 Доступные sample данные:")
|
|
70
|
+
print("=" * 50)
|
|
71
|
+
|
|
72
|
+
datasets = list_datasets()
|
|
73
|
+
for i, dataset in enumerate(datasets, 1):
|
|
74
|
+
print(f"{i}. {dataset['title']} ({dataset['name']})")
|
|
75
|
+
print(f" Символ: {dataset['symbol']} | Таймфрейм: {dataset['timeframe']}")
|
|
76
|
+
print(f" Записей: {dataset['rows']:,} | Размер: {dataset['size_kb']} KB")
|
|
77
|
+
print()
|
|
78
|
+
|
|
79
|
+
print(f"Всего доступно датасетов: {len(datasets)}")
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"❌ Ошибка при получении списка данных: {e}")
|
|
83
|
+
logger.error(f"Failed to list datasets: {e}")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def show_data_status():
|
|
88
|
+
"""Показать статус всех sample данных."""
|
|
89
|
+
try:
|
|
90
|
+
print_sample_data_status()
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"❌ Ошибка при получении статуса: {e}")
|
|
93
|
+
logger.error(f"Failed to show data status: {e}")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main():
|
|
98
|
+
"""Основная функция CLI."""
|
|
99
|
+
parser = argparse.ArgumentParser(
|
|
100
|
+
description="BQuant - Quantitative Research Toolkit",
|
|
101
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
102
|
+
epilog="""
|
|
103
|
+
Примеры использования:
|
|
104
|
+
bquant analyze # Анализ MACD с данными по умолчанию
|
|
105
|
+
bquant analyze mt_xauusd_m15 # Анализ MACD с конкретным датасетом
|
|
106
|
+
bquant analyze --output chart.html # Сохранение графика в файл
|
|
107
|
+
bquant list # Список доступных данных
|
|
108
|
+
bquant status # Статус всех данных
|
|
109
|
+
"""
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
subparsers = parser.add_subparsers(dest='command', help='Доступные команды')
|
|
113
|
+
|
|
114
|
+
# Команда analyze
|
|
115
|
+
analyze_parser = subparsers.add_parser('analyze', help='Анализ MACD')
|
|
116
|
+
analyze_parser.add_argument(
|
|
117
|
+
'dataset',
|
|
118
|
+
nargs='?',
|
|
119
|
+
default='tv_xauusd_1h',
|
|
120
|
+
help='Название датасета (по умолчанию: tv_xauusd_1h)'
|
|
121
|
+
)
|
|
122
|
+
analyze_parser.add_argument(
|
|
123
|
+
'--output', '-o',
|
|
124
|
+
help='Путь для сохранения графика (HTML файл)'
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Команда list
|
|
128
|
+
subparsers.add_parser('list', help='Список доступных данных')
|
|
129
|
+
|
|
130
|
+
# Команда status
|
|
131
|
+
subparsers.add_parser('status', help='Статус всех данных')
|
|
132
|
+
|
|
133
|
+
args = parser.parse_args()
|
|
134
|
+
|
|
135
|
+
if not args.command:
|
|
136
|
+
parser.print_help()
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
if args.command == 'analyze':
|
|
141
|
+
analyze_macd(args.dataset, args.output)
|
|
142
|
+
elif args.command == 'list':
|
|
143
|
+
list_available_data()
|
|
144
|
+
elif args.command == 'status':
|
|
145
|
+
show_data_status()
|
|
146
|
+
else:
|
|
147
|
+
print(f"❌ Неизвестная команда: {args.command}")
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
except KeyboardInterrupt:
|
|
151
|
+
print("\n⚠️ Операция прервана пользователем")
|
|
152
|
+
sys.exit(0)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
print(f"❌ Неожиданная ошибка: {e}")
|
|
155
|
+
logger.error(f"Unexpected error in CLI: {e}")
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == '__main__':
|
|
160
|
+
main()
|