deltafq 0.4.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.
Files changed (42) hide show
  1. deltafq/__init__.py +29 -0
  2. deltafq/backtest/__init__.py +32 -0
  3. deltafq/backtest/engine.py +145 -0
  4. deltafq/backtest/metrics.py +74 -0
  5. deltafq/backtest/performance.py +350 -0
  6. deltafq/charts/__init__.py +14 -0
  7. deltafq/charts/performance.py +319 -0
  8. deltafq/charts/price.py +64 -0
  9. deltafq/charts/signals.py +181 -0
  10. deltafq/core/__init__.py +18 -0
  11. deltafq/core/base.py +21 -0
  12. deltafq/core/config.py +62 -0
  13. deltafq/core/exceptions.py +34 -0
  14. deltafq/core/logger.py +44 -0
  15. deltafq/data/__init__.py +16 -0
  16. deltafq/data/cleaner.py +39 -0
  17. deltafq/data/fetcher.py +58 -0
  18. deltafq/data/storage.py +264 -0
  19. deltafq/data/validator.py +51 -0
  20. deltafq/indicators/__init__.py +14 -0
  21. deltafq/indicators/fundamental.py +28 -0
  22. deltafq/indicators/talib_indicators.py +67 -0
  23. deltafq/indicators/technical.py +251 -0
  24. deltafq/live/__init__.py +16 -0
  25. deltafq/live/connection.py +235 -0
  26. deltafq/live/data_feed.py +158 -0
  27. deltafq/live/monitoring.py +191 -0
  28. deltafq/live/risk_control.py +192 -0
  29. deltafq/strategy/__init__.py +12 -0
  30. deltafq/strategy/base.py +34 -0
  31. deltafq/strategy/signals.py +193 -0
  32. deltafq/trader/__init__.py +16 -0
  33. deltafq/trader/broker.py +119 -0
  34. deltafq/trader/engine.py +174 -0
  35. deltafq/trader/order_manager.py +110 -0
  36. deltafq/trader/position_manager.py +92 -0
  37. deltafq-0.4.0.dist-info/METADATA +115 -0
  38. deltafq-0.4.0.dist-info/RECORD +42 -0
  39. deltafq-0.4.0.dist-info/WHEEL +5 -0
  40. deltafq-0.4.0.dist-info/entry_points.txt +2 -0
  41. deltafq-0.4.0.dist-info/licenses/LICENSE +21 -0
  42. deltafq-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,34 @@
1
+ """
2
+ Custom exceptions for DeltaFQ.
3
+ """
4
+
5
+
6
+ class DeltaFQError(Exception):
7
+ """Base exception for DeltaFQ."""
8
+ pass
9
+
10
+
11
+ class DataError(DeltaFQError):
12
+ """Exception raised for data-related errors."""
13
+ pass
14
+
15
+
16
+ class TradingError(DeltaFQError):
17
+ """Exception raised for trading-related errors."""
18
+ pass
19
+
20
+
21
+ class BacktestError(DeltaFQError):
22
+ """Exception raised for backtesting errors."""
23
+ pass
24
+
25
+
26
+ class StrategyError(DeltaFQError):
27
+ """Exception raised for strategy-related errors."""
28
+ pass
29
+
30
+
31
+ class IndicatorError(DeltaFQError):
32
+ """Exception raised for indicator calculation errors."""
33
+ pass
34
+
deltafq/core/logger.py ADDED
@@ -0,0 +1,44 @@
1
+ """
2
+ Logging system for DeltaFQ.
3
+ """
4
+
5
+ import logging
6
+ import sys
7
+
8
+ class Logger:
9
+ """Logger for DeltaFQ components."""
10
+
11
+ def __init__(self, name: str = "deltafq", level: str = "INFO"):
12
+ """Initialize logger."""
13
+ self.logger = logging.getLogger(name)
14
+ self.logger.setLevel(getattr(logging, level.upper()))
15
+
16
+ if not self.logger.handlers:
17
+ handler = logging.StreamHandler(sys.stdout)
18
+ formatter = logging.Formatter(
19
+ '[%(asctime)s] %(name)-20s >>> %(levelname)-8s >>> %(message)s',
20
+ datefmt='%H:%M:%S'
21
+ )
22
+ handler.setFormatter(formatter)
23
+ self.logger.addHandler(handler)
24
+
25
+ def debug(self, message: str):
26
+ """Log debug message."""
27
+ self.logger.debug(message)
28
+
29
+ def info(self, message: str):
30
+ """Log info message."""
31
+ self.logger.info(message)
32
+
33
+ def warning(self, message: str):
34
+ """Log warning message."""
35
+ self.logger.warning(message)
36
+
37
+ def error(self, message: str):
38
+ """Log error message."""
39
+ self.logger.error(message)
40
+
41
+ def critical(self, message: str):
42
+ """Log critical message."""
43
+ self.logger.critical(message)
44
+
@@ -0,0 +1,16 @@
1
+ """
2
+ Data management module for DeltaFQ.
3
+ """
4
+
5
+ from .fetcher import DataFetcher
6
+ from .cleaner import DataCleaner
7
+ from .validator import DataValidator
8
+ from .storage import DataStorage
9
+
10
+ __all__ = [
11
+ "DataFetcher",
12
+ "DataCleaner",
13
+ "DataValidator",
14
+ "DataStorage"
15
+ ]
16
+
@@ -0,0 +1,39 @@
1
+ """
2
+ Data cleaning utilities for DeltaFQ.
3
+ """
4
+
5
+ import pandas as pd
6
+ from typing import Optional
7
+ from ..core.base import BaseComponent
8
+
9
+
10
+ class DataCleaner(BaseComponent):
11
+ """Data cleaning utilities."""
12
+
13
+ def initialize(self) -> bool:
14
+ """Initialize the data cleaner."""
15
+ self.logger.info("Initializing data cleaner")
16
+ return True
17
+
18
+ def dropna(self, data: pd.DataFrame) -> pd.DataFrame:
19
+ """Remove rows with NaN values."""
20
+ cleaned_data = data.dropna()
21
+ self.logger.info(f"Dropped NaN rows: {len(data)} -> {len(cleaned_data)} rows")
22
+ return cleaned_data
23
+
24
+ def fillna(self, data: pd.DataFrame, method: str = "forward") -> pd.DataFrame:
25
+ """Fill missing data using specified method."""
26
+ na_count_before = data.isna().sum().sum()
27
+
28
+ if method == "forward":
29
+ filled_data = data.fillna(method='ffill')
30
+ elif method == "backward":
31
+ filled_data = data.fillna(method='bfill')
32
+ else:
33
+ filled_data = data.fillna(0)
34
+
35
+ na_count_after = filled_data.isna().sum().sum()
36
+ self.logger.info(f"Filled NaN: {na_count_before} -> {na_count_after} (method: {method})")
37
+
38
+ return filled_data
39
+
@@ -0,0 +1,58 @@
1
+ """
2
+ Data fetching interfaces for DeltaFQ.
3
+ """
4
+
5
+ import pandas as pd
6
+ import yfinance as yf
7
+ from typing import List, Optional
8
+ from ..core.base import BaseComponent
9
+ from ..core.exceptions import DataError
10
+ from .cleaner import DataCleaner
11
+ import warnings
12
+ warnings.filterwarnings('ignore')
13
+
14
+
15
+ class DataFetcher(BaseComponent):
16
+ """Data fetcher for various sources."""
17
+
18
+ def __init__(self, source: str = "yahoo", **kwargs):
19
+ """Initialize data fetcher."""
20
+ super().__init__(**kwargs)
21
+ self.source = source
22
+ self.cleaner = None
23
+
24
+ def initialize(self) -> bool:
25
+ """Initialize the data fetcher."""
26
+ self.logger.info(f"Initializing data fetcher with source: {self.source}")
27
+ return True
28
+
29
+ def _ensure_cleaner(self):
30
+ """Lazy initialization of cleaner."""
31
+ if self.cleaner is None:
32
+ self.cleaner = DataCleaner()
33
+ self.cleaner.initialize()
34
+
35
+ def fetch_data(self, symbol: str, start_date: str, end_date: str = None, clean: bool = False) -> pd.DataFrame:
36
+ """Fetch stock data for given symbol."""
37
+ try:
38
+ self.logger.info(f"Fetching data for {symbol} from {start_date} to {end_date}")
39
+
40
+ data = yf.download(symbol, start=start_date, end=end_date, progress=False)
41
+ data = data.droplevel(level=1, axis=1) # Drop the multi-index level
42
+
43
+ if clean:
44
+ self._ensure_cleaner()
45
+ data = self.cleaner.dropna(data)
46
+
47
+ return data
48
+ except Exception as e:
49
+ raise DataError(f"Failed to fetch data for {symbol}: {str(e)}")
50
+
51
+ def fetch_data_multiple(self, symbols: List[str], start_date: str, end_date: str = None, clean: bool = False) -> dict:
52
+ """Fetch data for multiple symbols."""
53
+ data_dict = {}
54
+ for symbol in symbols:
55
+ data_dict[symbol] = self.fetch_data(symbol, start_date, end_date, clean)
56
+ return data_dict
57
+
58
+
@@ -0,0 +1,264 @@
1
+ """
2
+ Data storage management for DeltaFQ.
3
+ """
4
+
5
+ import pandas as pd
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Optional, Dict, Any
9
+ from datetime import datetime
10
+ from ..core.base import BaseComponent
11
+
12
+
13
+ class DataStorage(BaseComponent):
14
+ """
15
+ Data storage manager with categorized storage.
16
+
17
+ Directory structure:
18
+ data_cache/
19
+ ├── price/ # Price data
20
+ │ └── {symbol}/
21
+ ├── backtest/ # Backtest results
22
+ │ └── {symbol}/
23
+ ├── indicators/ # Technical indicators
24
+ └── misc/ # Other data
25
+ """
26
+
27
+ def __init__(self, base_path: str = None, **kwargs):
28
+ """Initialize data storage."""
29
+ super().__init__(**kwargs)
30
+
31
+ # Auto-detect project root if base_path not provided
32
+ if base_path is None:
33
+ project_root = self._get_project_root()
34
+ base_path = project_root / "data_cache"
35
+
36
+ self.base_path = Path(base_path)
37
+ self._init_directories()
38
+
39
+ def _get_project_root(self) -> Path:
40
+ """Get project root directory by finding setup.py or pyproject.toml."""
41
+ current = Path(__file__).resolve()
42
+ # Go up from deltafq/data/storage.py to project root
43
+ for parent in current.parents:
44
+ if (parent / "setup.py").exists() or (parent / "pyproject.toml").exists():
45
+ return parent
46
+ # Fallback to current working directory
47
+ return Path.cwd()
48
+
49
+ def _init_directories(self):
50
+ """Initialize directory structure."""
51
+ self.price_dir = self.base_path / "price"
52
+ self.backtest_dir = self.base_path / "backtest"
53
+ self.indicators_dir = self.base_path / "indicators"
54
+ self.misc_dir = self.base_path / "misc"
55
+
56
+ # Create directories
57
+ for dir_path in [self.price_dir, self.backtest_dir,
58
+ self.indicators_dir, self.misc_dir]:
59
+ dir_path.mkdir(parents=True, exist_ok=True)
60
+
61
+ def initialize(self) -> bool:
62
+ """Initialize the data storage."""
63
+ self.logger.info(f"Initializing data storage at: {self.base_path}")
64
+ return True
65
+
66
+ # ============================================================================
67
+ # Price Data Storage
68
+ # ============================================================================
69
+
70
+ def save_price_data(self, data: pd.DataFrame, symbol: str,
71
+ start_date: Optional[str] = None,
72
+ end_date: Optional[str] = None) -> Path:
73
+ """Save price data to storage."""
74
+ symbol_dir = self.price_dir / symbol.replace('.', '_')
75
+ symbol_dir.mkdir(exist_ok=True)
76
+
77
+ # Generate filename
78
+ if start_date and end_date:
79
+ filename = f"{symbol}_{start_date}_{end_date}.csv"
80
+ else:
81
+ filename = f"{symbol}_{datetime.now().strftime('%Y%m%d')}.csv"
82
+
83
+ filepath = symbol_dir / filename
84
+ data.to_csv(filepath, encoding='utf-8-sig', index=True)
85
+ self.logger.info(f"Saved price data to: {filepath}")
86
+ return filepath
87
+
88
+ def load_price_data(self, symbol: str, start_date: Optional[str] = None,
89
+ end_date: Optional[str] = None) -> Optional[pd.DataFrame]:
90
+ """Load price data from storage."""
91
+ symbol_dir = self.price_dir / symbol.replace('.', '_')
92
+
93
+ if start_date and end_date:
94
+ filename = f"{symbol}_{start_date}_{end_date}.csv"
95
+ else:
96
+ # Try to find the latest file
97
+ files = list(symbol_dir.glob(f"{symbol}_*.csv"))
98
+ if not files:
99
+ self.logger.warning(f"No price data found for {symbol}")
100
+ return None
101
+ filename = sorted(files)[-1].name
102
+
103
+ filepath = symbol_dir / filename
104
+ if filepath.exists():
105
+ data = pd.read_csv(filepath, index_col=0, parse_dates=True)
106
+ self.logger.info(f"Loaded price data from: {filepath}")
107
+ return data
108
+ return None
109
+
110
+ # ============================================================================
111
+ # Backtest Data Storage
112
+ # ============================================================================
113
+
114
+ def save_backtest_results(self, trades_df: pd.DataFrame,
115
+ values_df: pd.DataFrame, symbol: str,
116
+ strategy_name: Optional[str] = None) -> Dict[str, Path]:
117
+ """Save backtest results to storage."""
118
+ symbol_dir = self.backtest_dir / symbol.replace('.', '_')
119
+ symbol_dir.mkdir(exist_ok=True)
120
+
121
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
122
+ strategy_suffix = f"_{strategy_name}" if strategy_name else ""
123
+
124
+ trades_path = symbol_dir / f"{symbol}_trades{strategy_suffix}_{timestamp}.csv"
125
+ values_path = symbol_dir / f"{symbol}_values{strategy_suffix}_{timestamp}.csv"
126
+
127
+ trades_df.to_csv(trades_path, encoding='utf-8-sig', index=False)
128
+ values_df.to_csv(values_path, encoding='utf-8-sig', index=False)
129
+
130
+ self.logger.info(f"Saved backtest results to: {symbol_dir}")
131
+ return {'trades': trades_path, 'values': values_path}
132
+
133
+ def load_backtest_results(self, symbol: str, strategy_name: Optional[str] = None,
134
+ latest: bool = True) -> Optional[Dict[str, pd.DataFrame]]:
135
+ """Load backtest results from storage."""
136
+ symbol_dir = self.backtest_dir / symbol.replace('.', '_')
137
+
138
+ if not symbol_dir.exists():
139
+ self.logger.warning(f"No backtest results found for {symbol}")
140
+ return None
141
+
142
+ # Find trades and values files
143
+ if strategy_name:
144
+ trades_files = list(symbol_dir.glob(f"{symbol}_trades_{strategy_name}_*.csv"))
145
+ values_files = list(symbol_dir.glob(f"{symbol}_values_{strategy_name}_*.csv"))
146
+ else:
147
+ trades_files = list(symbol_dir.glob(f"{symbol}_trades*.csv"))
148
+ values_files = list(symbol_dir.glob(f"{symbol}_values*.csv"))
149
+
150
+ if not trades_files or not values_files:
151
+ self.logger.warning(f"No backtest results found for {symbol}")
152
+ return None
153
+
154
+ if latest:
155
+ trades_file = sorted(trades_files)[-1]
156
+ values_file = sorted(values_files)[-1]
157
+ return {
158
+ 'trades': pd.read_csv(trades_file, encoding='utf-8-sig'),
159
+ 'values': pd.read_csv(values_file, encoding='utf-8-sig')
160
+ }
161
+ else:
162
+ # Return all files
163
+ return {
164
+ 'trades': [pd.read_csv(f, encoding='utf-8-sig') for f in trades_files],
165
+ 'values': [pd.read_csv(f, encoding='utf-8-sig') for f in values_files]
166
+ }
167
+
168
+ # ============================================================================
169
+ # Generic Storage Methods
170
+ # ============================================================================
171
+
172
+ def save_data(self, data: pd.DataFrame, filename: str,
173
+ category: str = "misc", subdir: Optional[str] = None) -> Path:
174
+ """Save data to storage with category."""
175
+ if category == "price":
176
+ target_dir = self.price_dir
177
+ elif category == "backtest":
178
+ target_dir = self.backtest_dir
179
+ elif category == "indicators":
180
+ target_dir = self.indicators_dir
181
+ else:
182
+ target_dir = self.misc_dir
183
+
184
+ if subdir:
185
+ target_dir = target_dir / subdir
186
+ target_dir.mkdir(parents=True, exist_ok=True)
187
+
188
+ filepath = target_dir / filename
189
+ data.to_csv(filepath, encoding='utf-8-sig', index=False)
190
+ self.logger.info(f"Saved data to: {filepath}")
191
+ return filepath
192
+
193
+ def load_data(self, filename: str, category: str = "misc",
194
+ subdir: Optional[str] = None) -> Optional[pd.DataFrame]:
195
+ """Load data from storage."""
196
+ if category == "price":
197
+ target_dir = self.price_dir
198
+ elif category == "backtest":
199
+ target_dir = self.backtest_dir
200
+ elif category == "indicators":
201
+ target_dir = self.indicators_dir
202
+ else:
203
+ target_dir = self.misc_dir
204
+
205
+ if subdir:
206
+ target_dir = target_dir / subdir
207
+
208
+ filepath = target_dir / filename
209
+ if filepath.exists():
210
+ data = pd.read_csv(filepath, encoding='utf-8-sig')
211
+ self.logger.info(f"Loaded data from: {filepath}")
212
+ return data
213
+ else:
214
+ self.logger.warning(f"File not found: {filepath}")
215
+ return None
216
+
217
+ # ============================================================================
218
+ # Utility Methods
219
+ # ============================================================================
220
+
221
+ def list_files(self, category: Optional[str] = None,
222
+ subdir: Optional[str] = None) -> list:
223
+ """List all files in storage."""
224
+ if category == "price":
225
+ target_dir = self.price_dir
226
+ elif category == "backtest":
227
+ target_dir = self.backtest_dir
228
+ elif category == "indicators":
229
+ target_dir = self.indicators_dir
230
+ elif category == "misc":
231
+ target_dir = self.misc_dir
232
+ else:
233
+ target_dir = self.base_path
234
+
235
+ if subdir:
236
+ target_dir = target_dir / subdir
237
+
238
+ if not target_dir.exists():
239
+ return []
240
+
241
+ files = []
242
+ for item in target_dir.rglob('*.csv'):
243
+ if item.is_file():
244
+ files.append(str(item.relative_to(self.base_path)))
245
+ return files
246
+
247
+ def get_storage_info(self) -> Dict[str, Any]:
248
+ """Get storage information."""
249
+ return {
250
+ 'base_path': str(self.base_path),
251
+ 'price_files': len(list(self.price_dir.rglob('*.csv'))),
252
+ 'backtest_files': len(list(self.backtest_dir.rglob('*.csv'))),
253
+ 'indicators_files': len(list(self.indicators_dir.rglob('*.csv'))),
254
+ 'misc_files': len(list(self.misc_dir.rglob('*.csv'))),
255
+ 'total_size_mb': self._calculate_size()
256
+ }
257
+
258
+ def _calculate_size(self) -> float:
259
+ """Calculate total storage size in MB."""
260
+ total_size = 0
261
+ for file_path in self.base_path.rglob('*'):
262
+ if file_path.is_file():
263
+ total_size += file_path.stat().st_size
264
+ return round(total_size / (1024 * 1024), 2)
@@ -0,0 +1,51 @@
1
+ """
2
+ Data validation for DeltaFQ.
3
+ """
4
+
5
+ import pandas as pd
6
+ from typing import List, Optional
7
+ from ..core.base import BaseComponent
8
+ from ..core.exceptions import DataError
9
+
10
+
11
+ class DataValidator(BaseComponent):
12
+ """Data validator for ensuring data quality."""
13
+
14
+ def initialize(self) -> bool:
15
+ """Initialize the data validator."""
16
+ self.logger.info("Initializing data validator")
17
+ return True
18
+
19
+ def validate_price_data(self, data: pd.DataFrame) -> bool:
20
+ """Validate price data structure and values."""
21
+ required_columns = ['open', 'high', 'low', 'close']
22
+
23
+ # Check required columns
24
+ missing_columns = [col for col in required_columns if col not in data.columns]
25
+ if missing_columns:
26
+ raise DataError(f"Missing required columns: {missing_columns}")
27
+
28
+ # Check for negative prices
29
+ for col in required_columns:
30
+ if (data[col] <= 0).any():
31
+ raise DataError(f"Found non-positive values in {col} column")
32
+
33
+ # Check high >= low
34
+ if (data['high'] < data['low']).any():
35
+ raise DataError("Found high < low values")
36
+
37
+ self.logger.info("Price data validation passed")
38
+ return True
39
+
40
+ def validate_data_continuity(self, data: pd.DataFrame, date_column: str = 'date') -> bool:
41
+ """Validate data continuity."""
42
+ if date_column not in data.columns:
43
+ raise DataError(f"Date column '{date_column}' not found")
44
+
45
+ # Check for duplicate dates
46
+ if data[date_column].duplicated().any():
47
+ raise DataError("Found duplicate dates in data")
48
+
49
+ self.logger.info("Data continuity validation passed")
50
+ return True
51
+
@@ -0,0 +1,14 @@
1
+ """
2
+ Technical indicators module for DeltaFQ.
3
+ """
4
+
5
+ from .technical import TechnicalIndicators
6
+ from .talib_indicators import TalibIndicators
7
+ from .fundamental import FundamentalIndicators
8
+
9
+ __all__ = [
10
+ "TechnicalIndicators",
11
+ "TalibIndicators",
12
+ "FundamentalIndicators"
13
+ ]
14
+
@@ -0,0 +1,28 @@
1
+ """
2
+ Fundamental indicators (placeholders).
3
+ """
4
+
5
+ import pandas as pd
6
+ from ..core.base import BaseComponent
7
+
8
+
9
+ class FundamentalIndicators(BaseComponent):
10
+ """Basic fundamental indicators computed from preloaded fundamentals."""
11
+
12
+ def initialize(self) -> bool:
13
+ self.logger.info("Initializing fundamental indicators")
14
+ return True
15
+
16
+ def pe(self, price: pd.Series, eps_ttm: pd.Series) -> pd.Series:
17
+ eps = eps_ttm.reindex(price.index).ffill()
18
+ return price / eps
19
+
20
+ def pb(self, price: pd.Series, bvps: pd.Series) -> pd.Series:
21
+ bvps = bvps.reindex(price.index).ffill()
22
+ return price / bvps
23
+
24
+ def earnings_yield(self, price: pd.Series, eps_ttm: pd.Series) -> pd.Series:
25
+ pe_series = self.pe(price, eps_ttm)
26
+ return 1.0 / pe_series
27
+
28
+
@@ -0,0 +1,67 @@
1
+ """
2
+ Technical indicators using TA-Lib library.
3
+ """
4
+
5
+ import pandas as pd
6
+ import talib
7
+ from ..core.base import BaseComponent
8
+
9
+
10
+ class TalibIndicators(BaseComponent):
11
+ """Technical indicators using TA-Lib library."""
12
+
13
+ def initialize(self) -> bool:
14
+ """Initialize technical indicators."""
15
+ self.logger.info("Initializing TA-Lib technical indicators")
16
+ return True
17
+
18
+ def sma(self, data: pd.Series, period: int) -> pd.Series:
19
+ """Calculate Simple Moving Average (SMA) using TA-Lib."""
20
+ self.logger.info(f"Calculating SMA(period={period})")
21
+ return pd.Series(talib.SMA(data.values.astype(float), timeperiod=period), index=data.index)
22
+
23
+ def ema(self, data: pd.Series, period: int) -> pd.Series:
24
+ """Calculate Exponential Moving Average (EMA) using TA-Lib."""
25
+ self.logger.info(f"Calculating EMA(period={period})")
26
+ return pd.Series(talib.EMA(data.values.astype(float), timeperiod=period), index=data.index)
27
+
28
+ def rsi(self, data: pd.Series, period: int = 14) -> pd.Series:
29
+ """Calculate Relative Strength Index (RSI) using TA-Lib."""
30
+ self.logger.info(f"Calculating RSI(period={period})")
31
+ return pd.Series(talib.RSI(data.values.astype(float), timeperiod=period), index=data.index)
32
+
33
+ def kdj(self, high: pd.Series, low: pd.Series, close: pd.Series,
34
+ n: int = 9, m1: int = 3, m2: int = 3) -> pd.DataFrame:
35
+ """Calculate KDJ indicator using TA-Lib."""
36
+ self.logger.info(f"Calculating KDJ(n={n}, m1={m1}, m2={m2})")
37
+ k, d = talib.STOCH(high.values.astype(float), low.values.astype(float), close.values.astype(float),
38
+ fastk_period=n, slowk_period=m1, slowd_period=m2)
39
+ return pd.DataFrame({
40
+ 'k': pd.Series(k, index=close.index),
41
+ 'd': pd.Series(d, index=close.index),
42
+ 'j': pd.Series(3 * k - 2 * d, index=close.index)
43
+ })
44
+
45
+ def boll(self, data: pd.Series, period: int = 20, std_dev: float = 2) -> pd.DataFrame:
46
+ """Calculate Bollinger Bands using TA-Lib."""
47
+ self.logger.info(f"Calculating BOLL(period={period}, std_dev={std_dev})")
48
+ upper, middle, lower = talib.BBANDS(data.values.astype(float), timeperiod=period,
49
+ nbdevup=std_dev, nbdevdn=std_dev, matype=0)
50
+ return pd.DataFrame({
51
+ 'upper': pd.Series(upper, index=data.index),
52
+ 'middle': pd.Series(middle, index=data.index),
53
+ 'lower': pd.Series(lower, index=data.index)
54
+ })
55
+
56
+ # Note: No separate alias; use boll() for consistency across providers
57
+
58
+ def atr(self, high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series:
59
+ """Calculate Average True Range (ATR) using TA-Lib."""
60
+ self.logger.info(f"Calculating ATR(period={period})")
61
+ return pd.Series(talib.ATR(high.values.astype(float), low.values.astype(float),
62
+ close.values.astype(float), timeperiod=period), index=close.index)
63
+
64
+ def obv(self, close: pd.Series, volume: pd.Series) -> pd.Series:
65
+ """Calculate On-Balance Volume (OBV) using TA-Lib."""
66
+ self.logger.info("Calculating OBV")
67
+ return pd.Series(talib.OBV(close.values.astype(float), volume.values.astype(float)), index=close.index)