# Руководство по расширению API BQuant ## 📚 Обзор Это руководство поможет вам расширить функциональность BQuant, создавая собственные индикаторы, анализаторы, визуализации и модули данных. ## 🎯 Принципы расширения ### Модульность - Каждый новый компонент должен быть независимым - Используйте интерфейсы и абстрактные классы - Минимизируйте зависимости между модулями ### Совместимость - Следуйте существующим паттернам API - Используйте стандартные типы данных - Поддерживайте обратную совместимость ### Производительность - Используйте NumPy для вычислений - Оптимизируйте для больших данных - Применяйте кэширование где возможно ## 🏗️ Создание собственного индикатора ### Шаг 1: Наследование от BaseIndicator ```python from bquant.indicators.base import ( BaseIndicator, CustomIndicator as BQuantCustomIndicator, IndicatorResult, IndicatorSource, ) import pandas as pd import numpy as np class CustomIndicator(BQuantCustomIndicator): """Кастомный индикатор""" def __init__(self, param1=10, param2=20): parameters = { "param1": param1, "param2": param2, } # Наследуемся от BQuant CustomIndicator, чтобы фабрика могла создавать экземпляры super().__init__("CustomIndicator", parameters) self.params = self.config.parameters def get_output_columns(self): return ["custom_indicator"] def get_description(self): return "Документированный пример пользовательского индикатора" def get_required_columns(self): return ["close", "volume"] def calculate(self, data): """Расчет индикатора""" if not self.validate_data(data): raise ValueError("Invalid data for CustomIndicator") # Ваша логика расчета result = self._calculate_indicator(data) result_frame = pd.DataFrame({"custom_indicator": result}, index=data.index) return IndicatorResult( name=self.name, data=result_frame, config=self.config, metadata={"calculated_at": pd.Timestamp.utcnow()}, ) def _calculate_indicator(self, data): """Внутренний метод расчета""" param1 = self.params["param1"] param2 = self.params["param2"] # Пример расчета indicator = (data["close"] * data["volume"]).rolling(window=param1, min_periods=1).mean() return indicator / max(param2, 1) ``` ### Шаг 2: Регистрация в фабрике ```python from bquant.indicators.base import IndicatorFactory # Регистрация индикатора (обновленный API v2.1 использует классовые методы) IndicatorFactory.register_indicator("custom_indicator", CustomIndicator) # Использование indicator = IndicatorFactory.create('custom', 'custom_indicator', param1=15, param2=25) result = indicator.calculate(data) ``` ## 🔬 Создание собственного анализатора ### Шаг 1: Наследование от BaseAnalyzer ```python from bquant.analysis import BaseAnalyzer, AnalysisResult import numpy as np class CustomAnalyzer(BaseAnalyzer): """Кастомный анализатор""" def __init__(self, analysis_type='default'): super().__init__('CustomAnalyzer', {'analysis_type': analysis_type}) self.params = self.config # сохраняем ссылку, как в исходном примере def analyze(self, data): """Выполнение анализа""" if not self.validate_data(data): raise ValueError("Invalid data for CustomAnalyzer") # Ваша логика анализа analysis_result = self._perform_analysis(data) return AnalysisResult( analysis_type=self.params['analysis_type'], results=analysis_result['statistics'], data_size=len(data), metadata={'series_tail': analysis_result['data'].tail(5).to_dict()} ) def validate_data(self, data): """Валидация данных""" return len(data) > 0 and 'close' in data.columns def _perform_analysis(self, data): """Внутренний метод анализа""" analysis_type = self.params['analysis_type'] if analysis_type == 'volatility': result = self._analyze_volatility(data) elif analysis_type == 'trend': result = self._analyze_trend(data) else: result = self._analyze_default(data) return result def _analyze_volatility(self, data): """Анализ волатильности""" returns = data['close'].pct_change().fillna(0) volatility = returns.rolling(window=20, min_periods=5).std().fillna(0) return { 'data': volatility, 'statistics': { 'mean_volatility': float(volatility.mean()), 'max_volatility': float(volatility.max()), 'current_volatility': float(volatility.iloc[-1]) } } ``` ### Шаг 2: Интеграция с системой ```python # Использование анализатора analyzer = CustomAnalyzer(analysis_type='volatility') result = analyzer.analyze(data) print(f"Mean volatility: {result.results['mean_volatility']:.4f}") ``` ## 🎨 Создание пользовательских стратегий (новое в этапе 3) > **Стабильность API:** 🟢 STABLE — интерфейс паттерна стратегий зафиксирован ### Обзор BQuant использует паттерн Strategy для расширяемого расчёта метрик. Вы можете добавлять собственные стратегии, не изменяя базовые анализаторы. **Преимущества:** - Добавляйте новые метрики без изменения `ZoneFeaturesAnalyzer` - Переключайте алгоритмы через конфигурацию - Проводите A/B-тестирование разных подходов - Поддерживайте несколько стратегий одновременно ### Типы стратегий | Тип стратегии | Назначение | Протокол | |---------------|------------|----------| | **SwingCalculationStrategy** | Обнаружение свингов/импульсов в движении цены | 23 метрики | | **ShapeCalculationStrategy** | Анализ формы гистограммы индикатора | 3 метрики | | **DivergenceCalculationStrategy** | Поиск дивергенций между ценой и индикатором | 4 метрики | | **VolatilityCalculationStrategy** | Оценка волатильности рынка | 10 метрик | | **VolumeCalculationStrategy** | Анализ объёмных паттернов | 4 метрики | ### Пошагово: создание пользовательской свинговой стратегии #### Шаг 1: импорт протокола и dataclass ```python from bquant.analysis.zones.strategies.base import ( SwingCalculationStrategy, SwingMetrics ) from bquant.analysis.zones.strategies.registry import StrategyRegistry import pandas as pd import numpy as np ``` #### Шаг 2: реализация класса стратегии ```python class MyCustomSwingStrategy(SwingCalculationStrategy): """My custom swing detection algorithm.""" def __init__(self, threshold: float = 0.02): """ Initialize strategy. Args: threshold: Minimum price movement to consider as swing (e.g., 0.02 = 2%) """ self.threshold = threshold def calculate_swings(self, data: pd.DataFrame) -> SwingMetrics: """ Calculate swing metrics. Args: data: DataFrame with OHLC columns (high, low, close) Returns: SwingMetrics with all 23 fields populated """ if len(data) < self.min_required_length: # Graceful degradation for short zones return self._empty_metrics() # Your algorithm here (упрощенная реализация для документации) price = data['close'] returns = price.pct_change().fillna(0) rallies = returns[returns >= self.threshold] drops = -returns[returns <= -self.threshold] rally_stats = self._stats(rallies) drop_stats = self._stats(drops) duration = max(len(data), 1) rally_speed = rally_stats['avg'] / duration if duration else 0.0 drop_speed = drop_stats['avg'] / duration if duration else 0.0 metrics = SwingMetrics( num_swings=rally_stats['count'] + drop_stats['count'], avg_rally_pct=rally_stats['avg'], avg_drop_pct=drop_stats['avg'], max_rally_pct=rally_stats['max'], max_drop_pct=drop_stats['max'], rally_to_drop_ratio=(rally_stats['avg'] / drop_stats['avg']) if drop_stats['avg'] else 1.0, rally_count=rally_stats['count'], drop_count=drop_stats['count'], min_rally_pct=rally_stats['min'], min_drop_pct=drop_stats['min'], rally_amplitude_std=rally_stats['std'], drop_amplitude_std=drop_stats['std'], rally_amplitude_median=rally_stats['median'], drop_amplitude_median=drop_stats['median'], avg_rally_duration_bars=rally_stats['duration'], avg_drop_duration_bars=drop_stats['duration'], max_rally_duration_bars=rally_stats['max_duration'], max_drop_duration_bars=drop_stats['max_duration'], avg_rally_speed_pct_per_bar=rally_speed, avg_drop_speed_pct_per_bar=drop_speed, max_rally_speed_pct_per_bar=rally_stats['max_speed'], max_drop_speed_pct_per_bar=drop_stats['max_speed'], duration_symmetry=(rally_stats['duration'] / drop_stats['duration']) if drop_stats['duration'] else 1.0, strategy_name='MyCustomSwing', strategy_params={'threshold': self.threshold} ) metrics.validate() return metrics def calculate(self, data: pd.DataFrame) -> SwingMetrics: """Совместимость с ZoneFeaturesAnalyzer (ожидает метод calculate).""" return self.calculate_swings(data) def _stats(self, series: pd.Series) -> dict: if series.empty: return { 'count': 0, 'avg': 0.0, 'max': 0.0, 'min': 0.0, 'std': 0.0, 'median': 0.0, 'duration': 0.0, 'max_duration': 0, 'max_speed': 0.0, } durations = max(1, len(series)) return { 'count': int(series.count()), 'avg': float(series.mean()), 'max': float(series.max()), 'min': float(series.min()), 'std': float(series.std(ddof=0)) if series.count() > 1 else 0.0, 'median': float(series.median()), 'duration': float(durations / max(series.count(), 1)), 'max_duration': int(durations), 'max_speed': float(series.max()), } def _empty_metrics(self) -> SwingMetrics: return SwingMetrics( num_swings=0, avg_rally_pct=0.0, avg_drop_pct=0.0, max_rally_pct=0.0, max_drop_pct=0.0, rally_to_drop_ratio=1.0, rally_count=0, drop_count=0, min_rally_pct=0.0, min_drop_pct=0.0, rally_amplitude_std=0.0, drop_amplitude_std=0.0, rally_amplitude_median=0.0, drop_amplitude_median=0.0, avg_rally_duration_bars=0.0, avg_drop_duration_bars=0.0, max_rally_duration_bars=0, max_drop_duration_bars=0, avg_rally_speed_pct_per_bar=0.0, avg_drop_speed_pct_per_bar=0.0, max_rally_speed_pct_per_bar=0.0, max_drop_speed_pct_per_bar=0.0, duration_symmetry=1.0, strategy_name='MyCustomSwing', strategy_params={'threshold': self.threshold} ) def get_metadata(self) -> dict: return { 'strategy': 'MyCustomSwing', 'threshold': self.threshold, 'algorithm': 'Custom threshold-based swing detection' } def get_name(self) -> str: """Return strategy name.""" return 'MyCustomSwing' def get_metadata(self) -> dict: """Return strategy metadata.""" return { 'strategy': 'MyCustomSwing', 'threshold': self.threshold, 'algorithm': 'Custom threshold-based swing detection', 'description': 'Detects swings when price movement exceeds threshold' } ``` #### Шаг 3: регистрация стратегии ```python # Option A: Добавьте декоратор к определению класса выше # @StrategyRegistry.register_swing_strategy('my_custom') # class MyCustomSwingStrategy(SwingCalculationStrategy): # ... # Option B: Manual registration StrategyRegistry.register_swing_strategy('my_custom')(MyCustomSwingStrategy) # Verify registration print(StrategyRegistry.list_swing_strategies()) # Output: ['zigzag', 'find_peaks', 'pivot_points', 'my_custom'] ``` #### Шаг 4: использование стратегии ```python from bquant.analysis.zones import ZoneFeaturesAnalyzer # By name (from registry) analyzer = ZoneFeaturesAnalyzer(swing_strategy='my_custom') # By instance (with custom parameters) strategy = MyCustomSwingStrategy(threshold=0.03) analyzer = ZoneFeaturesAnalyzer(swing_strategy=strategy) # Extract features features = analyzer.extract_zone_features(zone_dict) # Access swing metrics swing_metrics = features.metadata['swing_metrics'] print(f"Swings detected: {swing_metrics['num_swings']}") print(f"Avg rally: {swing_metrics['avg_rally_pct']:.2%}") print(f"Strategy used: {swing_metrics['strategy_name']}") ``` ### Создание стратегий других типов Процесс идентичен для остальных типов стратегий — достаточно заменить протокол и dataclass: #### Пример стратегии формы ```python from typing import Optional from bquant.analysis.zones.strategies.base import ShapeCalculationStrategy, ShapeMetrics @StrategyRegistry.register_shape_strategy('my_shape') class MyShapeStrategy: def calculate_shape(self, data: pd.DataFrame, indicator_col: Optional[str] = None) -> ShapeMetrics: """ Calculate shape metrics for ANY oscillator (v2.1 universal). Args: data: Zone data with OHLCV + oscillator columns indicator_col: Oscillator column name (e.g., 'RSI_14', 'AO_5_34', 'MY_OSC') If None, strategy should auto-detect or raise error Returns: ShapeMetrics with calculated shape characteristics Examples: # Works with ANY oscillator metrics = strategy.calculate_shape(data, indicator_col='RSI_14') metrics = strategy.calculate_shape(data, indicator_col='macd_hist') metrics = strategy.calculate_shape(data, indicator_col='CUSTOM_OSC') """ if indicator_col is None or indicator_col not in data.columns: raise ValueError(f"indicator_col required and must exist in data") # Your universal implementation (works with ANY column!) oscillator = data[indicator_col] # Calculate skewness, kurtosis, smoothness for your indicator hist_skewness = oscillator.skew() hist_kurtosis = oscillator.kurtosis() hist_smoothness = 1.0 - oscillator.diff().abs().mean() / oscillator.abs().mean() metrics = ShapeMetrics( hist_skewness=hist_skewness, hist_kurtosis=hist_kurtosis, hist_smoothness=hist_smoothness, strategy_name='MyShape', strategy_params={'indicator_col': indicator_col} # ← Track which indicator used ) metrics.validate() return metrics def calculate(self, data: pd.DataFrame, indicator_col: Optional[str] = None) -> ShapeMetrics: """Совместимость с ZoneFeaturesAnalyzer (ожидает метод calculate).""" return self.calculate_shape(data, indicator_col=indicator_col) def get_name(self) -> str: return 'MyShape' def get_metadata(self) -> dict: return {'strategy': 'MyShape', 'algorithm': 'Custom shape analysis'} ``` **Рекомендация v2.1:** всегда сохраняйте `indicator_col` в `strategy_params`, чтобы обеспечить трассируемость! #### Пример стратегии дивергенций ```python from typing import Optional from bquant.analysis.zones.strategies.base import DivergenceCalculationStrategy, DivergenceMetrics @StrategyRegistry.register_divergence_strategy('my_divergence') class MyDivergenceStrategy: def calculate_divergence(self, data: pd.DataFrame, indicator_col: Optional[str] = None, indicator_line_col: Optional[str] = None) -> DivergenceMetrics: """ Calculate divergence for ANY oscillator (v2.1 universal). Args: data: Zone data with OHLCV + oscillator columns indicator_col: Primary oscillator column (e.g., 'RSI_14', 'macd_hist') indicator_line_col: Secondary line for 2-line indicators (e.g., 'macd_signal') Returns: DivergenceMetrics with divergence information Examples: # Single-line oscillator (RSI, AO) metrics = strategy.calculate_divergence(data, indicator_col='RSI_14') # 2-line indicator (MACD with signal) metrics = strategy.calculate_divergence(data, indicator_col='macd', indicator_line_col='macd_signal') """ if indicator_col is None or indicator_col not in data.columns: raise ValueError(f"indicator_col required and must exist in data") # Your universal implementation (works with ANY oscillator!) oscillator = data[indicator_col] price = data['close'] # Detect divergences between price and indicator # ... your divergence logic here ... metrics = DivergenceMetrics( divergence_type='regular', # or 'hidden', 'mixed', 'none' divergence_count=1, divergence_strength=0.75, divergence_direction='bullish', strategy_name='MyDivergence', strategy_params={ 'indicator_col': indicator_col, # ← Track primary indicator 'indicator_line_col': indicator_line_col # ← Track signal line (if any) } ) metrics.validate() return metrics def get_name(self) -> str: return 'MyDivergence' def get_metadata(self) -> dict: return {'strategy': 'MyDivergence', 'supports_2line': True} ``` **Рекомендация v2.1:** отслеживайте и `indicator_col`, и `indicator_line_col` (если применимо) в `strategy_params`! ### Тестирование вашей стратегии ```python import numpy as np import pandas as pd import pytest def test_my_custom_strategy(): """Unit test for custom strategy.""" strategy = MyCustomSwingStrategy(threshold=0.02) # Create test data dates = pd.date_range('2024-01-01', periods=50, freq='1h') data = pd.DataFrame({ 'high': np.random.randn(50).cumsum() + 2000, 'low': np.random.randn(50).cumsum() + 1990, 'close': np.random.randn(50).cumsum() + 1995 }, index=dates) # Calculate swing metrics result = strategy.calculate_swings(data) # Validate contract (all required fields present) assert isinstance(result, SwingMetrics) assert result.num_swings >= 0 assert result.rally_count >= 0 assert result.drop_count >= 0 assert result.strategy_name == 'MyCustomSwing' assert 'threshold' in result.strategy_params # Validate data quality if result.num_swings > 0: assert result.avg_rally_pct >= 0 assert result.avg_drop_pct >= 0 assert result.rally_to_drop_ratio > 0 ``` ### Интеграционное тестирование ```python def test_strategy_with_analyzer(): """Test strategy integration with ZoneFeaturesAnalyzer.""" from bquant.analysis.zones import ZoneFeaturesAnalyzer analyzer = ZoneFeaturesAnalyzer(swing_strategy='my_custom') zone_dict = { 'zone_id': 'test_1', 'type': 'bull', 'duration': 20, 'data': data # your test data } features = analyzer.extract_zone_features(zone_dict) # Verify swing metrics present assert 'swing_metrics' in features.metadata assert features.metadata['swing_metrics']['strategy_name'] == 'MyCustomSwing' ``` ### Лучшие практики #### 1. Плавная деградация Аккуратно обрабатывайте крайние случаи: ```python def calculate_swings(self, data: pd.DataFrame) -> SwingMetrics: # Check data sufficiency if len(data) < self.min_required_length: return self._empty_metrics() # Return zeros # Check required columns required_cols = ['high', 'low', 'close'] if not all(col in data.columns for col in required_cols): raise ValueError(f"Missing required columns: {required_cols}") # Your algorithm... ``` #### 2. Содержательные метаданные Всегда сохраняйте конфигурацию стратегии: ```python def get_metadata(self) -> dict: return { 'strategy': self.get_name(), 'version': '1.0.0', 'algorithm': 'Description of your algorithm', 'parameters': { 'threshold': self.threshold, # ... all parameters }, 'requirements': ['high', 'low', 'close'], 'optional_columns': ['volume'], 'best_for': 'trending markets with clear swings' } ``` #### 3. Оптимизация производительности ```python # Use NumPy for vectorized operations amplitudes = np.abs(np.diff(data['close'].values)) # Avoid loops where possible # BAD: for i in range(len(data)): result.append(calculate_something(data.iloc[i])) # GOOD: result = data['close'].rolling(5).apply(calculate_something) ``` #### 4. Валидируйте входные данные ```python def _validate_data(self, data: pd.DataFrame) -> None: """Validate input data.""" if data.empty: raise ValueError("Data is empty") required = ['high', 'low', 'close'] missing = [col for col in required if col not in data.columns] if missing: raise ValueError(f"Missing columns: {missing}") if data[required].isnull().any().any(): raise ValueError("Data contains NaN values") ``` ### Сравнение стратегий (A/B-тестирование) ```python from bquant.analysis.zones import ZoneFeaturesAnalyzer # Test multiple strategies strategies = ['zigzag', 'find_peaks', 'pivot_points', 'my_custom'] results = {} for strategy_name in strategies: analyzer = ZoneFeaturesAnalyzer(swing_strategy=strategy_name) features = analyzer.extract_zone_features(zone_dict) swing_metrics = features.metadata['swing_metrics'] results[strategy_name] = { 'num_swings': swing_metrics.num_swings, 'avg_rally': swing_metrics.avg_rally_pct, 'avg_drop': swing_metrics.avg_drop_pct } # Compare results import pandas as pd comparison = pd.DataFrame(results).T print(comparison) ``` ### Встроенные стратегии Полную документацию по всем восьми встроенным стратегиям смотрите здесь: - [Справочник по API стратегий](analysis/strategies.md) - Примеры: `tests/unit/test_*_strategy.py` - Реализации: `bquant/analysis/zones/strategies/` ### API реестра ```python from bquant.analysis.zones.strategies.registry import StrategyRegistry # List available strategies print(StrategyRegistry.list_swing_strategies()) print(StrategyRegistry.list_shape_strategies()) print(StrategyRegistry.list_divergence_strategies()) print(StrategyRegistry.list_volatility_strategies()) print(StrategyRegistry.list_volume_strategies()) # Get strategy class SwingClass = StrategyRegistry.get_swing_strategy('zigzag') strategy_instance = SwingClass(legs=10, deviation=0.05) # Registry stats stats = StrategyRegistry.get_registry_stats() print(f"Total strategies: {stats['total']}") print(f"By type: {stats['by_type']}") ``` ### Конфигурация фабрики Добавьте свою стратегию в конфигурацию: ```python # In bquant/core/config.py ANALYSIS_CONFIG = { 'strategies': { 'swing': { 'default': 'zigzag', 'my_custom': { 'threshold': 0.02, 'class': 'MyCustomSwingStrategy' } } } } # Then use factory from bquant.core.config import create_swing_strategy strategy = create_swing_strategy('my_custom') ``` --- ## 📊 Создание собственной визуализации ### Шаг 1: Наследование от BaseChart ```python from bquant.visualization.charts import ChartBuilder from bquant.visualization.themes import ChartThemes import plotly.graph_objects as go class CustomChart(ChartBuilder): """Кастомный график""" def __init__(self, theme='default'): super().__init__(backend='plotly') self.theme_name = theme self.themes = ChartThemes() def create_chart(self, data, title="Custom Chart", **kwargs): """Создание графика""" self.validate_data(data, ["close"]) fig = self._build_chart(data, title, **kwargs) self._apply_theme(fig) return fig def _build_chart(self, data, title, **kwargs): """Построение графика""" fig = go.Figure() fig.add_trace( go.Scatter( x=data.index, y=data['close'], mode='lines', name='Close Price', line=dict(color=kwargs.get('color', '#00A3E0')) ) ) fig.update_layout( title=title, xaxis_title="Date", yaxis_title="Price", height=kwargs.get('height', 600) ) return fig def _apply_theme(self, fig): """Применение темы""" self.themes.apply_theme_to_figure(fig, self.theme_name) ``` ### Шаг 2: Использование ```python # Создание и использование графика chart = CustomChart(theme='dark') fig = chart.create_chart(data, title="My Custom Chart") fig.show() ``` ## 📥 Создание собственного загрузчика данных ### Шаг 1: Реализация адаптера DataLoader ```python from bquant.data import loader import pandas as pd class CustomDataLoader: """Кастомный загрузчик данных""" def __init__(self, source_type='custom_csv'): self.source_type = source_type def load(self, source, *, validate=True, **kwargs): """Загрузка данных""" if self.source_type == 'custom_csv': data = loader.load_ohlcv_data(source, validate_data=validate, **kwargs) return self._standardize_columns(data) return loader.load_ohlcv_data(source, validate_data=validate, **kwargs) def _standardize_columns(self, data): """Стандартизация колонок""" column_mapping = { 'Date': 'time', 'Open': 'open', 'High': 'high', 'Low': 'low', 'Close': 'close', 'Volume': 'volume' } standardized = data.rename(columns=column_mapping) if 'time' in standardized.columns: standardized['time'] = pd.to_datetime(standardized['time']) standardized.set_index('time', inplace=True) standardized = standardized.sort_index() return standardized ``` ## 🔧 Создание собственного процессора данных ### Шаг 1: Реализация адаптера DataProcessor ```python from bquant.data import processor import pandas as pd import numpy as np class CustomDataProcessor: """Кастомный процессор данных""" def __init__(self, *, remove_outliers=True, add_features=True, normalize=False): self.remove_outliers = remove_outliers self.add_features = add_features self.normalize = normalize def process(self, data): """Обработка данных""" processed_data = processor.clean_ohlcv_data(data, remove_outliers=self.remove_outliers) if self.add_features: processed_data = self._add_features(processed_data) if self.normalize: processed_data = self._normalize_data(processed_data) return processed_data def _add_features(self, data): """Добавление признаков""" result = data.copy() result['sma_20'] = result['close'].rolling(window=20, min_periods=5).mean() result['sma_50'] = result['close'].rolling(window=50, min_periods=5).mean() result['rsi_14'] = self._calculate_rsi(result['close']) return result def _calculate_rsi(self, prices, period=14): """Расчет RSI""" delta = prices.diff() gain = delta.clip(lower=0).rolling(window=period, min_periods=period).mean() loss = (-delta.clip(upper=0)).rolling(window=period, min_periods=period).mean() rs = gain / loss.replace(0, np.nan) rsi = 100 - (100 / (1 + rs)) return rsi.fillna(50) def _normalize_data(self, data): """Нормализация данных""" normalized = data.copy() for col in ['open', 'high', 'low', 'close']: if col in normalized.columns: normalized[col] = (normalized[col] - normalized[col].mean()) / normalized[col].std() return normalized ``` ## 🧪 Тестирование расширений ### Создание тестов ```python import numpy as np import pandas as pd import pytest from my_bquant_extension.indicators.custom_indicator import CustomIndicator from my_bquant_extension.analyzers.custom_analyzer import CustomAnalyzer class TestCustomIndicator: """Тесты для кастомного индикатора""" @pytest.fixture def sample_data(self): """Тестовые данные""" dates = pd.date_range('2024-01-01', periods=100, freq='H') data = pd.DataFrame({ 'close': np.random.randn(100).cumsum() + 100, 'volume': np.random.randint(1000, 10000, 100) }, index=dates) return data def test_indicator_calculation(self, sample_data): """Тест расчета индикатора""" indicator = CustomIndicator(param1=10, param2=20) result = indicator.calculate(sample_data) assert result.name == 'CustomIndicator' assert len(result.data) == len(sample_data) assert not result.data['custom_indicator'].isna().all() def test_indicator_validation(self, sample_data): """Тест валидации данных""" indicator = CustomIndicator() # Тест с валидными данными assert indicator.validate_data(sample_data) is True # Тест с невалидными данными invalid_data = sample_data.drop(columns=['close']) assert indicator.validate_data(invalid_data) == False class TestCustomAnalyzer: """Тесты для кастомного анализатора""" @pytest.fixture def sample_data(self): """Тестовые данные""" dates = pd.date_range('2024-01-01', periods=100, freq='H') data = pd.DataFrame({ 'close': np.random.randn(100).cumsum() + 100 }, index=dates) return data def test_analyzer_volatility(self, sample_data): """Тест анализа волатильности""" analyzer = CustomAnalyzer(analysis_type='volatility') result = analyzer.analyze(sample_data) assert result.analysis_type == 'volatility' assert 'mean_volatility' in result.results assert result.results['mean_volatility'] >= 0 ``` ### Запуск тестов ```bash # Запуск всех тестов pytest tests/test_custom_extensions.py -v # Запуск с покрытием pytest tests/test_custom_extensions.py --cov=bquant --cov-report=html ``` ## 📦 Упаковка расширений ### Структура пакета ``` my_bquant_extension/ ├── setup.py ├── README.md ├── requirements.txt ├── my_bquant_extension/ │ ├── __init__.py │ ├── indicators/ │ │ ├── __init__.py │ │ └── custom_indicator.py │ ├── analyzers/ │ │ ├── __init__.py │ │ └── custom_analyzer.py │ └── visualizations/ │ ├── __init__.py │ └── custom_chart.py └── tests/ ├── __init__.py ├── test_indicators.py ├── test_analyzers.py └── test_visualizations.py ``` ### Файл setup.py ```python from setuptools import setup, find_packages setup( name="my-bquant-extension", version="0.1.0", description="Custom extension for BQuant", author="Your Name", author_email="your.email@example.com", packages=find_packages(), install_requires=[ "bquant>=0.0.0", "pandas>=1.3.0", "numpy>=1.20.0", "plotly>=5.0.0" ], extras_require={ "dev": [ "pytest>=6.0.0", "pytest-cov>=2.0.0" ] }, python_requires=">=3.8", classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Financial and Insurance Industry", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ] ) ``` ### Автоматическая регистрация ```python # my_bquant_extension/__init__.py from .indicators.custom_indicator import CustomIndicator from .analyzers.custom_analyzer import CustomAnalyzer from .visualizations.custom_chart import CustomChart # Локальный реестр анализаторов расширения (пример интеграции) ANALYZERS_REGISTRY = {} # Автоматическая регистрация при импорте def register_extensions(): """Регистрация расширений""" from bquant.indicators.base import IndicatorFactory # Регистрация индикаторов в глобальной фабрике BQuant IndicatorFactory.register_indicator('custom_indicator', CustomIndicator) # Регистрация анализаторов в собственном реестре расширения ANALYZERS_REGISTRY['CustomAnalyzer'] = CustomAnalyzer # Автоматическая регистрация при импорте модуля register_extensions() ``` ## 🔗 Интеграция с существующим API ### Использование в скриптах ```python # Использование кастомных компонентов from my_bquant_extension import CustomIndicator, CustomAnalyzer, CustomChart from bquant.data.samples import get_sample_data # Загрузка данных data = get_sample_data('tv_xauusd_1h') # Использование кастомного индикатора indicator = CustomIndicator(param1=15, param2=25) indicator_result = indicator.calculate(data) # Использование кастомного анализатора analyzer = CustomAnalyzer(analysis_type='volatility') analysis_result = analyzer.analyze(data) # Использование кастомного графика chart = CustomChart(theme='dark') fig = chart.create_chart(data, title="Custom Analysis") fig.show() ``` ### Интеграция с CLI ```python # scripts/analysis/custom_analysis.py import argparse from my_bquant_extension import CustomIndicator, CustomAnalyzer from bquant.data.samples import get_sample_data def main(): parser = argparse.ArgumentParser(description="Custom analysis script") parser.add_argument("--dataset", default="tv_xauusd_1h", help="Dataset name") parser.add_argument("--param1", type=int, default=15, help="Parameter 1") parser.add_argument("--param2", type=int, default=25, help="Parameter 2") args = parser.parse_args() # Загрузка данных data = get_sample_data(args.dataset) # Кастомный анализ indicator = CustomIndicator(param1=args.param1, param2=args.param2) indicator_result = indicator.calculate(data) analyzer = CustomAnalyzer(analysis_type='volatility') analysis_result = analyzer.analyze(data) # Вывод результатов print(f"Indicator result: {indicator_result.data.tail()}") print(f"Analysis result: {analysis_result.results}") if __name__ == "__main__": main() ``` ## 🚀 Лучшие практики ### Производительность ```python # Используйте NumPy для быстрых вычислений import numpy as np def fast_calculation(data): """Быстрый расчет с NumPy""" prices = data['close'].values # NumPy array returns = np.diff(prices) / prices[:-1] volatility = np.std(returns) return volatility # Используйте векторизацию def vectorized_operation(data): """Векторизованная операция""" return data['close'].rolling(window=20).mean() ``` ### Обработка ошибок ```python from bquant.core.exceptions import BQuantError, DataError class CustomError(BQuantError): """Кастомное исключение""" pass def safe_calculation(data): """Безопасный расчет с обработкой ошибок""" try: if data.empty: raise DataError("Empty dataset provided") if 'close' not in data.columns: raise DataError("Missing 'close' column") result = perform_calculation(data) return result except Exception as e: raise CustomError(f"Calculation failed: {str(e)}") ``` ### Документация ```python class CustomIndicator(BaseIndicator): """ Кастомный индикатор для анализа финансовых данных. Этот индикатор рассчитывает специальный показатель на основе цены закрытия и объема торгов. Parameters ---------- param1 : int, default=10 Первый параметр индикатора param2 : int, default=20 Второй параметр индикатора Examples -------- >>> indicator = CustomIndicator(param1=15, param2=25) >>> result = indicator.calculate(data) >>> print(result.data.tail()) Notes ----- Индикатор использует скользящее среднее для сглаживания данных. """ def calculate(self, data): """ Расчет индикатора. Parameters ---------- data : pd.DataFrame DataFrame с OHLCV данными Returns ------- IndicatorResult Результат расчета индикатора Raises ------ DataError Если данные некорректны """ # Реализация pass ``` ## 📚 Дополнительные ресурсы - **[Core Modules](../core/README.md)** - Базовые модули для расширения - **[Indicators](../indicators/README.md)** - Примеры индикаторов - **[Analysis](../analysis/README.md)** - Примеры анализаторов - **[Visualization](../visualization/README.md)** - Примеры визуализаций --- **Следующий шаг:** Изучите существующие модули и создайте свое первое расширение! 🚀