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.
@@ -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()