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,547 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Модуль анализа зон BQuant
|
|
3
|
+
|
|
4
|
+
Предоставляет функции для анализа различных типов зон в финансовых данных:
|
|
5
|
+
- MACD зоны
|
|
6
|
+
- Price action зоны
|
|
7
|
+
- Поддержка и сопротивление
|
|
8
|
+
- Волновой анализ
|
|
9
|
+
- Зоны накопления/распределения
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Dict, List, Any, Optional, Tuple
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import numpy as np
|
|
15
|
+
from datetime import datetime, timedelta
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from ...core.logging_config import get_logger
|
|
19
|
+
from .. import BaseAnalyzer, AnalysisResult
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
# Версия модуля анализа зон
|
|
24
|
+
__version__ = "0.0.0"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Zone:
|
|
29
|
+
"""
|
|
30
|
+
Базовый класс для представления зоны.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
zone_id: Уникальный идентификатор зоны
|
|
34
|
+
zone_type: Тип зоны (support, resistance, accumulation, etc.)
|
|
35
|
+
start_time: Время начала зоны
|
|
36
|
+
end_time: Время окончания зоны
|
|
37
|
+
start_price: Цена начала зоны
|
|
38
|
+
end_price: Цена окончания зоны
|
|
39
|
+
strength: Сила зоны (0-1)
|
|
40
|
+
confidence: Уверенность в зоне (0-1)
|
|
41
|
+
metadata: Дополнительные метаданные
|
|
42
|
+
"""
|
|
43
|
+
zone_id: str
|
|
44
|
+
zone_type: str
|
|
45
|
+
start_time: datetime
|
|
46
|
+
end_time: datetime
|
|
47
|
+
start_price: float
|
|
48
|
+
end_price: float
|
|
49
|
+
strength: float = 0.0
|
|
50
|
+
confidence: float = 0.0
|
|
51
|
+
metadata: Dict[str, Any] = None
|
|
52
|
+
|
|
53
|
+
def __post_init__(self):
|
|
54
|
+
if self.metadata is None:
|
|
55
|
+
self.metadata = {}
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def duration(self) -> timedelta:
|
|
59
|
+
"""Длительность зоны."""
|
|
60
|
+
return self.end_time - self.start_time
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def price_range(self) -> float:
|
|
64
|
+
"""Ценовой диапазон зоны."""
|
|
65
|
+
return abs(self.end_price - self.start_price)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def mid_price(self) -> float:
|
|
69
|
+
"""Средняя цена зоны."""
|
|
70
|
+
return (self.start_price + self.end_price) / 2
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
73
|
+
"""Конвертация в словарь."""
|
|
74
|
+
return {
|
|
75
|
+
'zone_id': self.zone_id,
|
|
76
|
+
'zone_type': self.zone_type,
|
|
77
|
+
'start_time': self.start_time.isoformat(),
|
|
78
|
+
'end_time': self.end_time.isoformat(),
|
|
79
|
+
'start_price': self.start_price,
|
|
80
|
+
'end_price': self.end_price,
|
|
81
|
+
'duration_hours': self.duration.total_seconds() / 3600,
|
|
82
|
+
'price_range': self.price_range,
|
|
83
|
+
'mid_price': self.mid_price,
|
|
84
|
+
'strength': self.strength,
|
|
85
|
+
'confidence': self.confidence,
|
|
86
|
+
'metadata': self.metadata
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ZoneAnalyzer(BaseAnalyzer):
|
|
91
|
+
"""
|
|
92
|
+
Базовый класс для анализа зон.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
96
|
+
"""
|
|
97
|
+
Инициализация анализатора зон.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
config: Конфигурация анализатора
|
|
101
|
+
"""
|
|
102
|
+
super().__init__("ZoneAnalyzer", config)
|
|
103
|
+
|
|
104
|
+
# Параметры по умолчанию
|
|
105
|
+
self.min_zone_duration = self.config.get('min_zone_duration', 5)
|
|
106
|
+
self.min_strength_threshold = self.config.get('min_strength_threshold', 0.3)
|
|
107
|
+
self.min_confidence_threshold = self.config.get('min_confidence_threshold', 0.5)
|
|
108
|
+
|
|
109
|
+
def identify_support_resistance(self, data: pd.DataFrame,
|
|
110
|
+
window: int = 20,
|
|
111
|
+
min_touches: int = 2) -> List[Zone]:
|
|
112
|
+
"""
|
|
113
|
+
Определение зон поддержки и сопротивления.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
data: DataFrame с OHLCV данными
|
|
117
|
+
window: Окно для поиска локальных экстремумов
|
|
118
|
+
min_touches: Минимальное количество касаний для подтверждения зоны
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Список зон поддержки и сопротивления
|
|
122
|
+
"""
|
|
123
|
+
zones = []
|
|
124
|
+
|
|
125
|
+
if len(data) < window * 2:
|
|
126
|
+
self.logger.warning(f"Insufficient data for support/resistance analysis: {len(data)} < {window * 2}")
|
|
127
|
+
return zones
|
|
128
|
+
|
|
129
|
+
# Ищем локальные минимумы (поддержка)
|
|
130
|
+
support_levels = self._find_local_extrema(data['low'], window, 'min')
|
|
131
|
+
support_zones = self._validate_zones(data, support_levels, 'support', min_touches)
|
|
132
|
+
zones.extend(support_zones)
|
|
133
|
+
|
|
134
|
+
# Ищем локальные максимумы (сопротивление)
|
|
135
|
+
resistance_levels = self._find_local_extrema(data['high'], window, 'max')
|
|
136
|
+
resistance_zones = self._validate_zones(data, resistance_levels, 'resistance', min_touches)
|
|
137
|
+
zones.extend(resistance_zones)
|
|
138
|
+
|
|
139
|
+
self.logger.debug(f"Identified {len(zones)} support/resistance zones")
|
|
140
|
+
return zones
|
|
141
|
+
|
|
142
|
+
def _find_local_extrema(self, series: pd.Series, window: int,
|
|
143
|
+
extrema_type: str) -> List[Tuple[int, float]]:
|
|
144
|
+
"""
|
|
145
|
+
Поиск локальных экстремумов.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
series: Временной ряд
|
|
149
|
+
window: Размер окна
|
|
150
|
+
extrema_type: Тип экстремума ('min' или 'max')
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Список (индекс, значение) экстремумов
|
|
154
|
+
"""
|
|
155
|
+
extrema = []
|
|
156
|
+
|
|
157
|
+
for i in range(window, len(series) - window):
|
|
158
|
+
window_data = series.iloc[i-window:i+window+1]
|
|
159
|
+
center_value = series.iloc[i]
|
|
160
|
+
|
|
161
|
+
if extrema_type == 'min':
|
|
162
|
+
if center_value == window_data.min():
|
|
163
|
+
extrema.append((i, center_value))
|
|
164
|
+
elif extrema_type == 'max':
|
|
165
|
+
if center_value == window_data.max():
|
|
166
|
+
extrema.append((i, center_value))
|
|
167
|
+
|
|
168
|
+
return extrema
|
|
169
|
+
|
|
170
|
+
def _validate_zones(self, data: pd.DataFrame, extrema: List[Tuple[int, float]],
|
|
171
|
+
zone_type: str, min_touches: int) -> List[Zone]:
|
|
172
|
+
"""
|
|
173
|
+
Валидация и создание зон на основе экстремумов.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
data: DataFrame с данными
|
|
177
|
+
extrema: Список экстремумов
|
|
178
|
+
zone_type: Тип зоны
|
|
179
|
+
min_touches: Минимальное количество касаний
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Список валидных зон
|
|
183
|
+
"""
|
|
184
|
+
zones = []
|
|
185
|
+
|
|
186
|
+
if len(extrema) < min_touches:
|
|
187
|
+
return zones
|
|
188
|
+
|
|
189
|
+
# Группируем близкие экстремумы
|
|
190
|
+
tolerance = data['close'].std() * 0.1 # 10% от стандартного отклонения
|
|
191
|
+
|
|
192
|
+
grouped_extrema = self._group_extrema(extrema, tolerance)
|
|
193
|
+
|
|
194
|
+
for group in grouped_extrema:
|
|
195
|
+
if len(group) >= min_touches:
|
|
196
|
+
zone = self._create_zone_from_group(data, group, zone_type)
|
|
197
|
+
if zone:
|
|
198
|
+
zones.append(zone)
|
|
199
|
+
|
|
200
|
+
return zones
|
|
201
|
+
|
|
202
|
+
def _group_extrema(self, extrema: List[Tuple[int, float]],
|
|
203
|
+
tolerance: float) -> List[List[Tuple[int, float]]]:
|
|
204
|
+
"""
|
|
205
|
+
Группировка близких экстремумов.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
extrema: Список экстремумов
|
|
209
|
+
tolerance: Допустимое отклонение для группировки
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Список групп экстремумов
|
|
213
|
+
"""
|
|
214
|
+
if not extrema:
|
|
215
|
+
return []
|
|
216
|
+
|
|
217
|
+
# Сортируем по цене
|
|
218
|
+
sorted_extrema = sorted(extrema, key=lambda x: x[1])
|
|
219
|
+
|
|
220
|
+
groups = []
|
|
221
|
+
current_group = [sorted_extrema[0]]
|
|
222
|
+
|
|
223
|
+
for i in range(1, len(sorted_extrema)):
|
|
224
|
+
curr_price = sorted_extrema[i][1]
|
|
225
|
+
prev_price = sorted_extrema[i-1][1]
|
|
226
|
+
|
|
227
|
+
if abs(curr_price - prev_price) <= tolerance:
|
|
228
|
+
current_group.append(sorted_extrema[i])
|
|
229
|
+
else:
|
|
230
|
+
if len(current_group) > 1:
|
|
231
|
+
groups.append(current_group)
|
|
232
|
+
current_group = [sorted_extrema[i]]
|
|
233
|
+
|
|
234
|
+
# Добавляем последнюю группу
|
|
235
|
+
if len(current_group) > 1:
|
|
236
|
+
groups.append(current_group)
|
|
237
|
+
|
|
238
|
+
return groups
|
|
239
|
+
|
|
240
|
+
def _create_zone_from_group(self, data: pd.DataFrame,
|
|
241
|
+
group: List[Tuple[int, float]],
|
|
242
|
+
zone_type: str) -> Optional[Zone]:
|
|
243
|
+
"""
|
|
244
|
+
Создание зоны из группы экстремумов.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
data: DataFrame с данными
|
|
248
|
+
group: Группа экстремумов
|
|
249
|
+
zone_type: Тип зоны
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Объект Zone или None
|
|
253
|
+
"""
|
|
254
|
+
if not group:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
# Вычисляем параметры зоны
|
|
258
|
+
indices = [idx for idx, _ in group]
|
|
259
|
+
prices = [price for _, price in group]
|
|
260
|
+
|
|
261
|
+
start_idx = min(indices)
|
|
262
|
+
end_idx = max(indices)
|
|
263
|
+
|
|
264
|
+
start_time = data.index[start_idx]
|
|
265
|
+
end_time = data.index[end_idx]
|
|
266
|
+
|
|
267
|
+
min_price = min(prices)
|
|
268
|
+
max_price = max(prices)
|
|
269
|
+
|
|
270
|
+
# Вычисляем силу зоны (на основе количества касаний)
|
|
271
|
+
strength = min(len(group) / 5.0, 1.0) # Нормализуем к 1.0
|
|
272
|
+
|
|
273
|
+
# Вычисляем уверенность (на основе плотности касаний)
|
|
274
|
+
time_span = (end_time - start_time).total_seconds() / 3600 # в часах
|
|
275
|
+
if time_span > 0:
|
|
276
|
+
touch_density = len(group) / time_span
|
|
277
|
+
confidence = min(touch_density / 2.0, 1.0) # Нормализуем к 1.0
|
|
278
|
+
else:
|
|
279
|
+
confidence = 1.0
|
|
280
|
+
|
|
281
|
+
zone_id = f"{zone_type}_{start_time.strftime('%Y%m%d_%H%M')}_{end_time.strftime('%Y%m%d_%H%M')}"
|
|
282
|
+
|
|
283
|
+
zone = Zone(
|
|
284
|
+
zone_id=zone_id,
|
|
285
|
+
zone_type=zone_type,
|
|
286
|
+
start_time=start_time,
|
|
287
|
+
end_time=end_time,
|
|
288
|
+
start_price=min_price,
|
|
289
|
+
end_price=max_price,
|
|
290
|
+
strength=strength,
|
|
291
|
+
confidence=confidence,
|
|
292
|
+
metadata={
|
|
293
|
+
'touch_count': len(group),
|
|
294
|
+
'price_std': np.std(prices),
|
|
295
|
+
'indices': indices
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return zone
|
|
300
|
+
|
|
301
|
+
def analyze_zone_breaks(self, data: pd.DataFrame, zones: List[Zone]) -> Dict[str, Any]:
|
|
302
|
+
"""
|
|
303
|
+
Анализ пробоев зон.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
data: DataFrame с данными
|
|
307
|
+
zones: Список зон для анализа
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Результаты анализа пробоев
|
|
311
|
+
"""
|
|
312
|
+
break_analysis = {
|
|
313
|
+
'total_zones': len(zones),
|
|
314
|
+
'broken_zones': 0,
|
|
315
|
+
'false_breaks': 0,
|
|
316
|
+
'clean_breaks': 0,
|
|
317
|
+
'break_details': []
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for zone in zones:
|
|
321
|
+
# Найдем данные после окончания зоны
|
|
322
|
+
zone_end_idx = data.index.get_loc(zone.end_time)
|
|
323
|
+
if zone_end_idx >= len(data) - 1:
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
post_zone_data = data.iloc[zone_end_idx + 1:]
|
|
327
|
+
|
|
328
|
+
# Определяем пробой
|
|
329
|
+
if zone.zone_type == 'support':
|
|
330
|
+
# Пробой поддержки - цена идет ниже
|
|
331
|
+
break_condition = post_zone_data['low'] < zone.start_price
|
|
332
|
+
else: # resistance
|
|
333
|
+
# Пробой сопротивления - цена идет выше
|
|
334
|
+
break_condition = post_zone_data['high'] > zone.end_price
|
|
335
|
+
|
|
336
|
+
if break_condition.any():
|
|
337
|
+
break_analysis['broken_zones'] += 1
|
|
338
|
+
|
|
339
|
+
# Анализируем тип пробоя
|
|
340
|
+
break_idx = break_condition.idxmax()
|
|
341
|
+
break_details = {
|
|
342
|
+
'zone_id': zone.zone_id,
|
|
343
|
+
'zone_type': zone.zone_type,
|
|
344
|
+
'break_time': break_idx,
|
|
345
|
+
'break_price': post_zone_data.loc[break_idx, 'close'],
|
|
346
|
+
'zone_strength': zone.strength,
|
|
347
|
+
'zone_confidence': zone.confidence
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
break_analysis['break_details'].append(break_details)
|
|
351
|
+
|
|
352
|
+
self.logger.debug(f"Analyzed breaks for {len(zones)} zones")
|
|
353
|
+
return break_analysis
|
|
354
|
+
|
|
355
|
+
def analyze(self, data: pd.DataFrame, **kwargs) -> AnalysisResult:
|
|
356
|
+
"""
|
|
357
|
+
Выполнение комплексного анализа зон.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
data: DataFrame с OHLCV данными
|
|
361
|
+
**kwargs: Дополнительные параметры
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
AnalysisResult с результатами анализа зон
|
|
365
|
+
"""
|
|
366
|
+
if not self.validate_data(data):
|
|
367
|
+
raise ValueError("Data validation failed")
|
|
368
|
+
|
|
369
|
+
# Проверяем наличие необходимых колонок
|
|
370
|
+
required_columns = ['open', 'high', 'low', 'close']
|
|
371
|
+
missing_columns = [col for col in required_columns if col not in data.columns]
|
|
372
|
+
if missing_columns:
|
|
373
|
+
raise ValueError(f"Missing required columns: {missing_columns}")
|
|
374
|
+
|
|
375
|
+
results = {}
|
|
376
|
+
|
|
377
|
+
# Анализ зон поддержки и сопротивления
|
|
378
|
+
window = kwargs.get('window', 20)
|
|
379
|
+
min_touches = kwargs.get('min_touches', 2)
|
|
380
|
+
|
|
381
|
+
zones = self.identify_support_resistance(data, window, min_touches)
|
|
382
|
+
|
|
383
|
+
# Фильтруем зоны по силе и уверенности
|
|
384
|
+
strong_zones = [z for z in zones if z.strength >= self.min_strength_threshold]
|
|
385
|
+
confident_zones = [z for z in zones if z.confidence >= self.min_confidence_threshold]
|
|
386
|
+
|
|
387
|
+
results['zones'] = {
|
|
388
|
+
'all_zones': [zone.to_dict() for zone in zones],
|
|
389
|
+
'strong_zones': [zone.to_dict() for zone in strong_zones],
|
|
390
|
+
'confident_zones': [zone.to_dict() for zone in confident_zones],
|
|
391
|
+
'zone_count': len(zones),
|
|
392
|
+
'strong_zone_count': len(strong_zones),
|
|
393
|
+
'confident_zone_count': len(confident_zones)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# Анализ пробоев зон
|
|
397
|
+
if zones:
|
|
398
|
+
break_analysis = self.analyze_zone_breaks(data, zones)
|
|
399
|
+
results['break_analysis'] = break_analysis
|
|
400
|
+
|
|
401
|
+
# Статистика зон
|
|
402
|
+
if zones:
|
|
403
|
+
zone_stats = self._calculate_zone_statistics(zones)
|
|
404
|
+
results['zone_statistics'] = zone_stats
|
|
405
|
+
|
|
406
|
+
metadata = {
|
|
407
|
+
'analyzer': 'ZoneAnalyzer',
|
|
408
|
+
'window': window,
|
|
409
|
+
'min_touches': min_touches,
|
|
410
|
+
'min_strength_threshold': self.min_strength_threshold,
|
|
411
|
+
'min_confidence_threshold': self.min_confidence_threshold,
|
|
412
|
+
'config': self.config
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return AnalysisResult(
|
|
416
|
+
analysis_type='zones',
|
|
417
|
+
results=results,
|
|
418
|
+
data_size=len(data),
|
|
419
|
+
metadata=metadata
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
def _calculate_zone_statistics(self, zones: List[Zone]) -> Dict[str, Any]:
|
|
423
|
+
"""
|
|
424
|
+
Вычисление статистик по зонам.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
zones: Список зон
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Статистики зон
|
|
431
|
+
"""
|
|
432
|
+
if not zones:
|
|
433
|
+
return {}
|
|
434
|
+
|
|
435
|
+
# Группируем по типам
|
|
436
|
+
support_zones = [z for z in zones if z.zone_type == 'support']
|
|
437
|
+
resistance_zones = [z for z in zones if z.zone_type == 'resistance']
|
|
438
|
+
|
|
439
|
+
# Общие статистики
|
|
440
|
+
strengths = [z.strength for z in zones]
|
|
441
|
+
confidences = [z.confidence for z in zones]
|
|
442
|
+
durations = [z.duration.total_seconds() / 3600 for z in zones] # в часах
|
|
443
|
+
|
|
444
|
+
stats = {
|
|
445
|
+
'total_zones': len(zones),
|
|
446
|
+
'support_zones': len(support_zones),
|
|
447
|
+
'resistance_zones': len(resistance_zones),
|
|
448
|
+
'avg_strength': np.mean(strengths),
|
|
449
|
+
'avg_confidence': np.mean(confidences),
|
|
450
|
+
'avg_duration_hours': np.mean(durations),
|
|
451
|
+
'max_strength': max(strengths),
|
|
452
|
+
'max_confidence': max(confidences),
|
|
453
|
+
'max_duration_hours': max(durations)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return stats
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def get_zone_analyzers() -> Dict[str, str]:
|
|
460
|
+
"""
|
|
461
|
+
Получить список доступных анализаторов зон.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Словарь {анализатор: описание}
|
|
465
|
+
"""
|
|
466
|
+
return {
|
|
467
|
+
'zone': 'Комплексный анализ зон поддержки/сопротивления',
|
|
468
|
+
'support_resistance': 'Анализ уровней поддержки и сопротивления',
|
|
469
|
+
'macd_zones': 'Анализ MACD зон',
|
|
470
|
+
'price_action': 'Анализ зон price action'
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Удобные функции
|
|
475
|
+
def find_support_resistance(data: pd.DataFrame, window: int = 20,
|
|
476
|
+
min_touches: int = 2) -> List[Zone]:
|
|
477
|
+
"""
|
|
478
|
+
Быстрый поиск уровней поддержки и сопротивления.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
data: DataFrame с OHLCV данными
|
|
482
|
+
window: Окно для поиска экстремумов
|
|
483
|
+
min_touches: Минимальное количество касаний
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Список зон
|
|
487
|
+
"""
|
|
488
|
+
analyzer = ZoneAnalyzer()
|
|
489
|
+
return analyzer.identify_support_resistance(data, window, min_touches)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# Импорт расширенных модулей анализа зон
|
|
493
|
+
try:
|
|
494
|
+
from .zone_features import (
|
|
495
|
+
ZoneFeatures,
|
|
496
|
+
ZoneFeaturesAnalyzer,
|
|
497
|
+
analyze_zones_distribution,
|
|
498
|
+
extract_zone_features
|
|
499
|
+
)
|
|
500
|
+
_zone_features_available = True
|
|
501
|
+
logger.info("Zone features module loaded successfully")
|
|
502
|
+
except ImportError as e:
|
|
503
|
+
logger.warning(f"Zone features module not available: {e}")
|
|
504
|
+
_zone_features_available = False
|
|
505
|
+
|
|
506
|
+
try:
|
|
507
|
+
from .sequence_analysis import (
|
|
508
|
+
TransitionAnalysis,
|
|
509
|
+
ClusterAnalysis,
|
|
510
|
+
ZoneSequenceAnalyzer,
|
|
511
|
+
create_zone_sequence_analysis,
|
|
512
|
+
cluster_zone_shapes
|
|
513
|
+
)
|
|
514
|
+
_sequence_analysis_available = True
|
|
515
|
+
logger.info("Sequence analysis module loaded successfully")
|
|
516
|
+
except ImportError as e:
|
|
517
|
+
logger.warning(f"Sequence analysis module not available: {e}")
|
|
518
|
+
_sequence_analysis_available = False
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# Экспорт базового функционала
|
|
522
|
+
__all__ = [
|
|
523
|
+
'Zone',
|
|
524
|
+
'ZoneAnalyzer',
|
|
525
|
+
'get_zone_analyzers',
|
|
526
|
+
'find_support_resistance',
|
|
527
|
+
'__version__'
|
|
528
|
+
]
|
|
529
|
+
|
|
530
|
+
# Добавляем zone features если доступен
|
|
531
|
+
if _zone_features_available:
|
|
532
|
+
__all__.extend([
|
|
533
|
+
'ZoneFeatures',
|
|
534
|
+
'ZoneFeaturesAnalyzer',
|
|
535
|
+
'analyze_zones_distribution',
|
|
536
|
+
'extract_zone_features'
|
|
537
|
+
])
|
|
538
|
+
|
|
539
|
+
# Добавляем sequence analysis если доступен
|
|
540
|
+
if _sequence_analysis_available:
|
|
541
|
+
__all__.extend([
|
|
542
|
+
'TransitionAnalysis',
|
|
543
|
+
'ClusterAnalysis',
|
|
544
|
+
'ZoneSequenceAnalyzer',
|
|
545
|
+
'create_zone_sequence_analysis',
|
|
546
|
+
'cluster_zone_shapes'
|
|
547
|
+
])
|