exness-data-preprocess 0.1.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.
- exness_data_preprocess/__init__.py +51 -0
- exness_data_preprocess/api.py +216 -0
- exness_data_preprocess/cli.py +199 -0
- exness_data_preprocess/processor.py +899 -0
- exness_data_preprocess-0.1.0.dist-info/METADATA +506 -0
- exness_data_preprocess-0.1.0.dist-info/RECORD +10 -0
- exness_data_preprocess-0.1.0.dist-info/WHEEL +4 -0
- exness_data_preprocess-0.1.0.dist-info/entry_points.txt +2 -0
- exness_data_preprocess-0.1.0.dist-info/licenses/AUTHORS.md +24 -0
- exness_data_preprocess-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exness Data Preprocessing
|
|
3
|
+
|
|
4
|
+
Professional forex tick data preprocessing with optimal compression and DuckDB OHLC generation.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Optimal compression: Parquet with Zstd-22 (9% smaller than ZIP, lossless)
|
|
8
|
+
- DuckDB OHLC generation with embedded metadata
|
|
9
|
+
- Direct queryability (no decode step needed)
|
|
10
|
+
- On-demand tick analysis workflow
|
|
11
|
+
- Production-grade atomic operations
|
|
12
|
+
|
|
13
|
+
Quick Start:
|
|
14
|
+
>>> import exness_data_preprocess as edp
|
|
15
|
+
>>>
|
|
16
|
+
>>> # Download and preprocess Exness data
|
|
17
|
+
>>> processor = edp.ExnessDataProcessor()
|
|
18
|
+
>>> result = processor.process_month(year=2024, month=8)
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Query OHLC data
|
|
21
|
+
>>> df_ohlc = edp.query_ohlc(year=2024, month=8, timeframe='1m')
|
|
22
|
+
>>>
|
|
23
|
+
>>> # On-demand tick analysis
|
|
24
|
+
>>> df_ticks = edp.analyze_ticks(year=2024, month=8)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
__author__ = "Terry Li <terry@eonlabs.com>"
|
|
29
|
+
__license__ = "MIT"
|
|
30
|
+
|
|
31
|
+
from exness_data_preprocess.api import (
|
|
32
|
+
analyze_ticks,
|
|
33
|
+
get_storage_stats,
|
|
34
|
+
process_date_range,
|
|
35
|
+
process_month,
|
|
36
|
+
query_ohlc,
|
|
37
|
+
)
|
|
38
|
+
from exness_data_preprocess.processor import ExnessDataProcessor
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Main class
|
|
42
|
+
"ExnessDataProcessor",
|
|
43
|
+
# Simple API functions
|
|
44
|
+
"process_month",
|
|
45
|
+
"process_date_range",
|
|
46
|
+
"query_ohlc",
|
|
47
|
+
"analyze_ticks",
|
|
48
|
+
"get_storage_stats",
|
|
49
|
+
# Package metadata
|
|
50
|
+
"__version__",
|
|
51
|
+
]
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple API for Exness data preprocessing.
|
|
3
|
+
|
|
4
|
+
This module provides function-based wrappers around ExnessDataProcessor
|
|
5
|
+
for common use cases.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from exness_data_preprocess.processor import ExnessDataProcessor
|
|
14
|
+
|
|
15
|
+
# Global processor instance (lazy initialization)
|
|
16
|
+
_processor = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_processor(base_dir: Optional[Path] = None) -> ExnessDataProcessor:
|
|
20
|
+
"""Get or create global processor instance."""
|
|
21
|
+
global _processor
|
|
22
|
+
if _processor is None or base_dir is not None:
|
|
23
|
+
_processor = ExnessDataProcessor(base_dir=base_dir)
|
|
24
|
+
return _processor
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def process_month(
|
|
28
|
+
year: int,
|
|
29
|
+
month: int,
|
|
30
|
+
pair: str = "EURUSD",
|
|
31
|
+
delete_zip: bool = True,
|
|
32
|
+
base_dir: Optional[Path] = None,
|
|
33
|
+
) -> Dict[str, Any]:
|
|
34
|
+
"""
|
|
35
|
+
Process one month of Exness forex tick data.
|
|
36
|
+
|
|
37
|
+
This is the primary API for downloading and preprocessing Exness data.
|
|
38
|
+
Downloads monthly ZIP, converts to Parquet (Zstd-22), generates DuckDB OHLC.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
year: Year (e.g., 2024)
|
|
42
|
+
month: Month (1-12)
|
|
43
|
+
pair: Currency pair (default: EURUSD)
|
|
44
|
+
delete_zip: Whether to delete ZIP after processing (default: True)
|
|
45
|
+
base_dir: Base directory for data storage (default: ~/eon/exness-data-preprocess/data)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Dictionary with processing results:
|
|
49
|
+
- parquet_path: Path to Parquet file
|
|
50
|
+
- duckdb_path: Path to DuckDB file
|
|
51
|
+
- tick_count: Number of ticks
|
|
52
|
+
- ohlc_bar_count: Number of 1-minute bars
|
|
53
|
+
- parquet_size_mb: Parquet file size in MB
|
|
54
|
+
- duckdb_size_mb: DuckDB file size in MB
|
|
55
|
+
- compression_ratio: Parquet size / ZIP size
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> import exness_data_preprocess as edp
|
|
59
|
+
>>> result = edp.process_month(2024, 8)
|
|
60
|
+
>>> print(f"Processed {result['tick_count']:,} ticks")
|
|
61
|
+
>>> print(f"Storage: {result['parquet_size_mb']:.2f} MB")
|
|
62
|
+
"""
|
|
63
|
+
processor = _get_processor(base_dir)
|
|
64
|
+
return processor.process_month(year, month, pair, delete_zip)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def process_date_range(
|
|
68
|
+
start_year: int,
|
|
69
|
+
start_month: int,
|
|
70
|
+
end_year: int,
|
|
71
|
+
end_month: int,
|
|
72
|
+
pair: str = "EURUSD",
|
|
73
|
+
delete_zip: bool = True,
|
|
74
|
+
base_dir: Optional[Path] = None,
|
|
75
|
+
) -> List[Dict[str, Any]]:
|
|
76
|
+
"""
|
|
77
|
+
Process multiple months of Exness data.
|
|
78
|
+
|
|
79
|
+
Downloads and processes all months in the specified range.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
start_year: Start year
|
|
83
|
+
start_month: Start month (1-12)
|
|
84
|
+
end_year: End year
|
|
85
|
+
end_month: End month (1-12)
|
|
86
|
+
pair: Currency pair (default: EURUSD)
|
|
87
|
+
delete_zip: Whether to delete ZIPs after processing
|
|
88
|
+
base_dir: Base directory for data storage
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of result dictionaries (one per month)
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> import exness_data_preprocess as edp
|
|
95
|
+
>>> results = edp.process_date_range(2024, 1, 2024, 12)
|
|
96
|
+
>>> total_ticks = sum(r['tick_count'] for r in results)
|
|
97
|
+
>>> print(f"Processed {total_ticks:,} ticks across {len(results)} months")
|
|
98
|
+
"""
|
|
99
|
+
processor = _get_processor(base_dir)
|
|
100
|
+
results = []
|
|
101
|
+
|
|
102
|
+
# Generate month sequence
|
|
103
|
+
current_year = start_year
|
|
104
|
+
current_month = start_month
|
|
105
|
+
|
|
106
|
+
while (current_year < end_year) or (current_year == end_year and current_month <= end_month):
|
|
107
|
+
try:
|
|
108
|
+
result = processor.process_month(current_year, current_month, pair, delete_zip)
|
|
109
|
+
results.append(result)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
print(f"ā Failed to process {current_year}-{current_month:02d}: {e}")
|
|
112
|
+
|
|
113
|
+
# Move to next month
|
|
114
|
+
if current_month == 12:
|
|
115
|
+
current_month = 1
|
|
116
|
+
current_year += 1
|
|
117
|
+
else:
|
|
118
|
+
current_month += 1
|
|
119
|
+
|
|
120
|
+
return results
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def query_ohlc(
|
|
124
|
+
year: int,
|
|
125
|
+
month: int,
|
|
126
|
+
pair: str = "EURUSD",
|
|
127
|
+
timeframe: str = "1m",
|
|
128
|
+
base_dir: Optional[Path] = None,
|
|
129
|
+
) -> pd.DataFrame:
|
|
130
|
+
"""
|
|
131
|
+
Query OHLC data from DuckDB.
|
|
132
|
+
|
|
133
|
+
Loads pre-computed OHLC data from DuckDB storage. Supports resampling
|
|
134
|
+
to higher timeframes on the fly.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
year: Year
|
|
138
|
+
month: Month (1-12)
|
|
139
|
+
pair: Currency pair (default: EURUSD)
|
|
140
|
+
timeframe: Timeframe (1m, 5m, 15m, 30m, 1h, 4h, 1d)
|
|
141
|
+
base_dir: Base directory for data storage
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
DataFrame with OHLC data (Timestamp, Open, High, Low, Close, spread_avg, tick_count)
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
>>> import exness_data_preprocess as edp
|
|
148
|
+
>>> # Query 1-hour OHLC bars
|
|
149
|
+
>>> df = edp.query_ohlc(2024, 8, timeframe='1h')
|
|
150
|
+
>>> print(df.head())
|
|
151
|
+
>>> print(f"Average spread: {df['spread_avg'].mean():.5f}")
|
|
152
|
+
"""
|
|
153
|
+
processor = _get_processor(base_dir)
|
|
154
|
+
return processor.query_ohlc(year, month, pair, timeframe)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def analyze_ticks(
|
|
158
|
+
year: int,
|
|
159
|
+
month: int,
|
|
160
|
+
pair: str = "EURUSD",
|
|
161
|
+
base_dir: Optional[Path] = None,
|
|
162
|
+
) -> pd.DataFrame:
|
|
163
|
+
"""
|
|
164
|
+
Load tick data for analysis.
|
|
165
|
+
|
|
166
|
+
Provides on-demand access to raw tick data from Parquet storage.
|
|
167
|
+
Useful for microstructure analysis, spread statistics, etc.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
year: Year
|
|
171
|
+
month: Month (1-12)
|
|
172
|
+
pair: Currency pair (default: EURUSD)
|
|
173
|
+
base_dir: Base directory for data storage
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
DataFrame with tick data (Timestamp, Bid, Ask)
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
>>> import exness_data_preprocess as edp
|
|
180
|
+
>>> df_ticks = edp.analyze_ticks(2024, 8)
|
|
181
|
+
>>> # Calculate spread statistics
|
|
182
|
+
>>> spreads = df_ticks['Ask'] - df_ticks['Bid']
|
|
183
|
+
>>> print(f"Mean spread: {spreads.mean():.5f}")
|
|
184
|
+
>>> print(f"Median spread: {spreads.median():.5f}")
|
|
185
|
+
>>> print(f"95th percentile: {spreads.quantile(0.95):.5f}")
|
|
186
|
+
"""
|
|
187
|
+
processor = _get_processor(base_dir)
|
|
188
|
+
return processor.analyze_ticks(year, month, pair)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_storage_stats(base_dir: Optional[Path] = None) -> Dict[str, Any]:
|
|
192
|
+
"""
|
|
193
|
+
Get storage statistics for all processed data.
|
|
194
|
+
|
|
195
|
+
Returns counts and total sizes for Parquet and DuckDB files.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
base_dir: Base directory for data storage
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Dictionary with storage stats:
|
|
202
|
+
- parquet_count: Number of Parquet files
|
|
203
|
+
- parquet_total_mb: Total Parquet storage (MB)
|
|
204
|
+
- duckdb_count: Number of DuckDB files
|
|
205
|
+
- duckdb_total_mb: Total DuckDB storage (MB)
|
|
206
|
+
- total_mb: Total storage (MB)
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
>>> import exness_data_preprocess as edp
|
|
210
|
+
>>> stats = edp.get_storage_stats()
|
|
211
|
+
>>> print(f"Total storage: {stats['total_mb']:.2f} MB")
|
|
212
|
+
>>> print(f"Parquet files: {stats['parquet_count']}")
|
|
213
|
+
>>> print(f"DuckDB files: {stats['duckdb_count']}")
|
|
214
|
+
"""
|
|
215
|
+
processor = _get_processor(base_dir)
|
|
216
|
+
return processor.get_storage_stats()
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for Exness data preprocessing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from exness_data_preprocess import api
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def process_command(args):
|
|
13
|
+
"""Handle 'process' command."""
|
|
14
|
+
if args.range:
|
|
15
|
+
# Process date range
|
|
16
|
+
start_year, start_month = map(int, args.range[0].split("-"))
|
|
17
|
+
end_year, end_month = map(int, args.range[1].split("-"))
|
|
18
|
+
|
|
19
|
+
results = api.process_date_range(
|
|
20
|
+
start_year=start_year,
|
|
21
|
+
start_month=start_month,
|
|
22
|
+
end_year=end_year,
|
|
23
|
+
end_month=end_month,
|
|
24
|
+
pair=args.pair,
|
|
25
|
+
delete_zip=not args.keep_zip,
|
|
26
|
+
base_dir=Path(args.base_dir) if args.base_dir else None,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
print(f"\n{'=' * 70}")
|
|
30
|
+
print(f"Processed {len(results)} months")
|
|
31
|
+
total_ticks = sum(r["tick_count"] for r in results)
|
|
32
|
+
total_size = sum(r["parquet_size_mb"] + r["duckdb_size_mb"] for r in results)
|
|
33
|
+
print(f"Total ticks: {total_ticks:,}")
|
|
34
|
+
print(f"Total storage: {total_size:.2f} MB")
|
|
35
|
+
print(f"{'=' * 70}")
|
|
36
|
+
else:
|
|
37
|
+
# Process single month
|
|
38
|
+
result = api.process_month(
|
|
39
|
+
year=args.year,
|
|
40
|
+
month=args.month,
|
|
41
|
+
pair=args.pair,
|
|
42
|
+
delete_zip=not args.keep_zip,
|
|
43
|
+
base_dir=Path(args.base_dir) if args.base_dir else None,
|
|
44
|
+
)
|
|
45
|
+
print(f"\nā Successfully processed {args.pair} {args.year}-{args.month:02d}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def query_command(args):
|
|
49
|
+
"""Handle 'query' command."""
|
|
50
|
+
df = api.query_ohlc(
|
|
51
|
+
year=args.year,
|
|
52
|
+
month=args.month,
|
|
53
|
+
pair=args.pair,
|
|
54
|
+
timeframe=args.timeframe,
|
|
55
|
+
base_dir=Path(args.base_dir) if args.base_dir else None,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if args.output:
|
|
59
|
+
df.to_csv(args.output, index=False)
|
|
60
|
+
print(f"ā Saved {len(df)} bars to {args.output}")
|
|
61
|
+
else:
|
|
62
|
+
print(df.head(args.head))
|
|
63
|
+
print(f"\nTotal bars: {len(df)}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def analyze_command(args):
|
|
67
|
+
"""Handle 'analyze' command."""
|
|
68
|
+
df = api.analyze_ticks(
|
|
69
|
+
year=args.year,
|
|
70
|
+
month=args.month,
|
|
71
|
+
pair=args.pair,
|
|
72
|
+
base_dir=Path(args.base_dir) if args.base_dir else None,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Calculate spread statistics
|
|
76
|
+
spreads = df["Ask"] - df["Bid"]
|
|
77
|
+
|
|
78
|
+
print(f"\n{'=' * 70}")
|
|
79
|
+
print(f"Tick Analysis: {args.pair} {args.year}-{args.month:02d}")
|
|
80
|
+
print(f"{'=' * 70}")
|
|
81
|
+
print(f"Total ticks: {len(df):,}")
|
|
82
|
+
print("\nSpread Statistics (base units):")
|
|
83
|
+
print(f" Mean: {spreads.mean():.5f}")
|
|
84
|
+
print(f" Median: {spreads.median():.5f}")
|
|
85
|
+
print(f" Std Dev: {spreads.std():.5f}")
|
|
86
|
+
print(f" Min: {spreads.min():.5f}")
|
|
87
|
+
print(f" Max: {spreads.max():.5f}")
|
|
88
|
+
print(f" 25th percentile: {spreads.quantile(0.25):.5f}")
|
|
89
|
+
print(f" 75th percentile: {spreads.quantile(0.75):.5f}")
|
|
90
|
+
print(f" 95th percentile: {spreads.quantile(0.95):.5f}")
|
|
91
|
+
|
|
92
|
+
if args.output:
|
|
93
|
+
df.to_csv(args.output, index=False)
|
|
94
|
+
print(f"\nā Saved {len(df)} ticks to {args.output}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def stats_command(args):
|
|
98
|
+
"""Handle 'stats' command."""
|
|
99
|
+
stats = api.get_storage_stats(base_dir=Path(args.base_dir) if args.base_dir else None)
|
|
100
|
+
|
|
101
|
+
print(f"\n{'=' * 70}")
|
|
102
|
+
print("Storage Statistics")
|
|
103
|
+
print(f"{'=' * 70}")
|
|
104
|
+
print(f"Parquet files: {stats['parquet_count']}")
|
|
105
|
+
print(f"Parquet storage: {stats['parquet_total_mb']:.2f} MB")
|
|
106
|
+
print(f"DuckDB files: {stats['duckdb_count']}")
|
|
107
|
+
print(f"DuckDB storage: {stats['duckdb_total_mb']:.2f} MB")
|
|
108
|
+
print(f"{'=' * 70}")
|
|
109
|
+
print(f"Total storage: {stats['total_mb']:.2f} MB")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main():
|
|
113
|
+
"""Main entry point for CLI."""
|
|
114
|
+
parser = argparse.ArgumentParser(
|
|
115
|
+
description="Exness Forex Tick Data Preprocessing Tool",
|
|
116
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
117
|
+
epilog="""
|
|
118
|
+
Examples:
|
|
119
|
+
# Process single month
|
|
120
|
+
exness-preprocess process --year 2024 --month 8
|
|
121
|
+
|
|
122
|
+
# Process date range
|
|
123
|
+
exness-preprocess process --range 2024-01 2024-12
|
|
124
|
+
|
|
125
|
+
# Query 1-hour OHLC
|
|
126
|
+
exness-preprocess query --year 2024 --month 8 --timeframe 1h
|
|
127
|
+
|
|
128
|
+
# Analyze tick data
|
|
129
|
+
exness-preprocess analyze --year 2024 --month 8
|
|
130
|
+
|
|
131
|
+
# Show storage statistics
|
|
132
|
+
exness-preprocess stats
|
|
133
|
+
""",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
137
|
+
|
|
138
|
+
# Process command
|
|
139
|
+
process_parser = subparsers.add_parser("process", help="Process Exness tick data")
|
|
140
|
+
process_parser.add_argument("--year", type=int, help="Year to process")
|
|
141
|
+
process_parser.add_argument("--month", type=int, help="Month to process (1-12)")
|
|
142
|
+
process_parser.add_argument(
|
|
143
|
+
"--range", nargs=2, metavar=("START", "END"), help="Date range (YYYY-MM YYYY-MM)"
|
|
144
|
+
)
|
|
145
|
+
process_parser.add_argument("--pair", default="EURUSD", help="Currency pair (default: EURUSD)")
|
|
146
|
+
process_parser.add_argument(
|
|
147
|
+
"--keep-zip", action="store_true", help="Keep ZIP files after processing"
|
|
148
|
+
)
|
|
149
|
+
process_parser.add_argument("--base-dir", help="Base directory for data storage")
|
|
150
|
+
process_parser.set_defaults(func=process_command)
|
|
151
|
+
|
|
152
|
+
# Query command
|
|
153
|
+
query_parser = subparsers.add_parser("query", help="Query OHLC data")
|
|
154
|
+
query_parser.add_argument("--year", type=int, required=True, help="Year")
|
|
155
|
+
query_parser.add_argument("--month", type=int, required=True, help="Month (1-12)")
|
|
156
|
+
query_parser.add_argument("--pair", default="EURUSD", help="Currency pair (default: EURUSD)")
|
|
157
|
+
query_parser.add_argument(
|
|
158
|
+
"--timeframe",
|
|
159
|
+
default="1m",
|
|
160
|
+
choices=["1m", "5m", "15m", "30m", "1h", "4h", "1d"],
|
|
161
|
+
help="Timeframe (default: 1m)",
|
|
162
|
+
)
|
|
163
|
+
query_parser.add_argument("--output", help="Output CSV file")
|
|
164
|
+
query_parser.add_argument(
|
|
165
|
+
"--head", type=int, default=10, help="Number of rows to display (default: 10)"
|
|
166
|
+
)
|
|
167
|
+
query_parser.add_argument("--base-dir", help="Base directory for data storage")
|
|
168
|
+
query_parser.set_defaults(func=query_command)
|
|
169
|
+
|
|
170
|
+
# Analyze command
|
|
171
|
+
analyze_parser = subparsers.add_parser("analyze", help="Analyze tick data")
|
|
172
|
+
analyze_parser.add_argument("--year", type=int, required=True, help="Year")
|
|
173
|
+
analyze_parser.add_argument("--month", type=int, required=True, help="Month (1-12)")
|
|
174
|
+
analyze_parser.add_argument("--pair", default="EURUSD", help="Currency pair (default: EURUSD)")
|
|
175
|
+
analyze_parser.add_argument("--output", help="Output CSV file")
|
|
176
|
+
analyze_parser.add_argument("--base-dir", help="Base directory for data storage")
|
|
177
|
+
analyze_parser.set_defaults(func=analyze_command)
|
|
178
|
+
|
|
179
|
+
# Stats command
|
|
180
|
+
stats_parser = subparsers.add_parser("stats", help="Show storage statistics")
|
|
181
|
+
stats_parser.add_argument("--base-dir", help="Base directory for data storage")
|
|
182
|
+
stats_parser.set_defaults(func=stats_command)
|
|
183
|
+
|
|
184
|
+
# Parse and execute
|
|
185
|
+
args = parser.parse_args()
|
|
186
|
+
|
|
187
|
+
if not args.command:
|
|
188
|
+
parser.print_help()
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
args.func(args)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
print(f"ā Error: {e}", file=sys.stderr)
|
|
195
|
+
sys.exit(1)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
main()
|