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.
- deltafq/__init__.py +29 -0
- deltafq/backtest/__init__.py +32 -0
- deltafq/backtest/engine.py +145 -0
- deltafq/backtest/metrics.py +74 -0
- deltafq/backtest/performance.py +350 -0
- deltafq/charts/__init__.py +14 -0
- deltafq/charts/performance.py +319 -0
- deltafq/charts/price.py +64 -0
- deltafq/charts/signals.py +181 -0
- deltafq/core/__init__.py +18 -0
- deltafq/core/base.py +21 -0
- deltafq/core/config.py +62 -0
- deltafq/core/exceptions.py +34 -0
- deltafq/core/logger.py +44 -0
- deltafq/data/__init__.py +16 -0
- deltafq/data/cleaner.py +39 -0
- deltafq/data/fetcher.py +58 -0
- deltafq/data/storage.py +264 -0
- deltafq/data/validator.py +51 -0
- deltafq/indicators/__init__.py +14 -0
- deltafq/indicators/fundamental.py +28 -0
- deltafq/indicators/talib_indicators.py +67 -0
- deltafq/indicators/technical.py +251 -0
- deltafq/live/__init__.py +16 -0
- deltafq/live/connection.py +235 -0
- deltafq/live/data_feed.py +158 -0
- deltafq/live/monitoring.py +191 -0
- deltafq/live/risk_control.py +192 -0
- deltafq/strategy/__init__.py +12 -0
- deltafq/strategy/base.py +34 -0
- deltafq/strategy/signals.py +193 -0
- deltafq/trader/__init__.py +16 -0
- deltafq/trader/broker.py +119 -0
- deltafq/trader/engine.py +174 -0
- deltafq/trader/order_manager.py +110 -0
- deltafq/trader/position_manager.py +92 -0
- deltafq-0.4.0.dist-info/METADATA +115 -0
- deltafq-0.4.0.dist-info/RECORD +42 -0
- deltafq-0.4.0.dist-info/WHEEL +5 -0
- deltafq-0.4.0.dist-info/entry_points.txt +2 -0
- deltafq-0.4.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
|
deltafq/data/__init__.py
ADDED
|
@@ -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
|
+
|
deltafq/data/cleaner.py
ADDED
|
@@ -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
|
+
|
deltafq/data/fetcher.py
ADDED
|
@@ -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
|
+
|
deltafq/data/storage.py
ADDED
|
@@ -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)
|