A production-ready, local-first cryptocurrency market data pipeline for Windows that collects, transforms, validates, and stores real-time trading data from Binance.
Data Flow: Exchange β Collector β Transformer β Parquet Storage β DuckDB β Analysis
- Collector: Real-time WebSocket connection to Binance for trade and quote data
- Transformer: Converts raw events into 1-second OHLCV bars with L1 quotes
- Validator: Ensures data quality, continuity, and schema integrity
- Compactor: Merges partitioned files into daily Parquet archives with hash verification
- β Live streaming data collection (trades + top-of-book quotes)
- β Historical backfill via Binance REST API (up to 6 months)
- β Unified orchestrator with auto-transform scheduling
- β Macro/FX data collection (1-minute from yfinance)
- β Auto-reconnect with exponential backoff
- β Rotating JSONL file output (60-second intervals)
- β 1-second OHLCV aggregation with VWAP and spread
- β Partitioned Parquet storage (year/month/day)
- β Data validation (gaps, duplicates, schema checks)
- β Daily file compaction with SHA256 hashing
- β Comprehensive logging with 14-day rotation
- β Full test coverage with pytest
- Python 3.12 (compatible with 3.11+)
- aiohttp + websockets: Async WebSocket client
- pandas + pyarrow: Data transformation and Parquet I/O
- duckdb: Fast analytical queries on Parquet files
- loguru: Structured logging
- pytest: Unit and integration testing
python -m venv venv
venv\Scripts\activate # On Windows
# source venv/bin/activate # On Linux/Mac
pip install -r requirements.txt# Copy the example config
cp config.yml.example config.ymlEdit config.yml to set:
base_path: Local data lake directory (default:./data- relative, works anywhere)symbols: Trading pairs to track (13 crypto pairs pre-configured)macro_minute.tickers: Stock/FX/commodity tickers (17 pre-configured)
# Collect live data (run continuously)
python main.py --mode collector
# Transform raw data to Parquet (run every 5 min or on-demand)
python main.py --mode transformer --date 2025-10-20
# Validate data quality
python main.py --mode validate --date 2025-10-20
# Compact daily files
python main.py --mode compact --date 2025-10-20crypto-lake/
βββ main.py # CLI entrypoint
βββ config.yml # Runtime configuration
βββ collector/
β βββ collector.py # WebSocket data collection
βββ transformer/
β βββ transformer.py # Raw β Parquet transformation
βββ storage/
β βββ compactor.py # Daily file compaction
βββ tools/
β βββ common.py # Shared utilities
β βββ validator.py # Data quality checks
β βββ backfill.py # Historical data via REST API
β βββ scheduler.py # Windows Task Scheduler integration
βββ sql/
β βββ query_duckdb.sql # Example analytical queries
βββ tests/
βββ test_collector.py # Collector unit tests
βββ test_transformer.py # Transformer unit tests
D:/CryptoDataLake/raw/binance/SOLUSDT/2025-10-20/part_001.jsonl
Each line: {symbol, ts_event, ts_recv, price, qty, side, bid, ask, stream}
D:/CryptoDataLake/parquet/binance/SOLUSDT/year=2025/month=10/day=20/*.parquet
Columns: symbol, window_start, open, high, low, close, volume_base, volume_quote, trade_count, vwap, bid, ask, spread
D:/CryptoDataLake/parquet/binance/SOLUSDT/2025-10-20.parquet
Run the full test suite:
pytest -vCurrent Status: β All 4 tests passing
test_parse_event_trade_and_quote: Binance message parsingtest_rotating_writer: JSONL file rotation logictest_transformer_integration: End-to-end pipeline testtest_aggregate_bars_1s_basic: OHLCV aggregation correctness
Date: 2025-10-20 Data Quality:
- β 321 rows collected (SOLUSDT)
- β 0 missing seconds
- β 0 duplicates
- β All schema columns present
- β
SHA256 hash:
49d27af23d4e64ddea78a63a30ed32064d742a1e9fcc2320fb29c9c4f5816458
Set up automated pipeline execution:
python tools/scheduler.py --action setup_allThis creates Windows Task Scheduler jobs:
- Collector: Runs on system startup
- Transformer: Runs every 5 minutes
- Compactor: Runs daily at 1:30 AM
The orchestrator is a single long-running process that manages both crypto and macro data collection, with built-in health monitoring.
The orchestrator runs:
- Binance WebSocket Collector - Continuous crypto streaming (19 symbols)
- Macro/FX Data Fetcher - Scheduled 1-minute data collection every 15 minutes (SPY, UUP, ES=F, FX pairs)
- Transformer - Automatic transformation of raw data to Parquet every 60 minutes
- One-Time Startup Backfill - Fetches 7 days of historical macro data on launch
- Health Monitoring - Writes metrics every 60 seconds to JSON and Markdown files
Using config.yml (recommended):
python main.py --mode orchestrateWith CLI arguments:
python main.py --mode orchestrate \
--macro_tickers SPY,UUP,ES=F,EURUSD=X,GBPUSD=X,USDJPY=X \
--macro_interval_min 15 \
--macro_lookback_startup_days 7 \
--macro_runtime_lookback_days 1Edit config.yml to set default orchestrator parameters:
transformer:
resample_interval_sec: 1
parquet_compression: "snappy"
schedule_minutes: 60 # Run transformer every 60 minutes (0 to disable)
macro_minute:
tickers:
- SPY # S&P 500 ETF
- UUP # US Dollar Index Fund
- ES=F # E-mini S&P 500 Futures
- EURUSD=X # EUR/USD
- GBPUSD=X # GBP/USD
- USDJPY=X # USD/JPY
- USDCAD=X # USD/CAD
- USDCHF=X # USD/CHF
schedule_minutes: 15 # Fetch macro data every 15 minutes
startup_backfill_days: 7 # One-time backfill on startup
runtime_lookback_days: 1 # Fetch last 1 day on each runThe orchestrator writes health metrics to two files:
JSON Heartbeat (D:/CryptoDataLake/logs/health/heartbeat.json):
{
"ts_utc": "2025-10-22T12:00:00Z",
"collector": {
"status": "running",
"last_latency_p50_ms": 10.5,
"last_latency_p95_ms": 25.3,
"last_seen_ts": "2025-10-22T11:59:00Z"
},
"macro_minute": {
"status": "idle",
"last_run_start": "2025-10-22T11:45:00Z",
"last_run_end": "2025-10-22T11:46:00Z",
"last_run_rows_written": 1500,
"last_error": null
},
"transformer": {
"status": "idle",
"last_run_start": "2025-10-22T12:00:00Z",
"last_run_end": "2025-10-22T12:02:00Z",
"last_error": null
},
"files": {
"raw_count_today": 100,
"parquet_1s_rows_today": 50000,
"macro_min_rows_today": 2000
}
}Markdown Report (D:/CryptoDataLake/reports/health.md):
- Overall system status (HEALTHY / ERROR / STOPPED)
- Real-time crypto collector metrics
- Macro/FX fetcher metrics
- Today's data volume statistics
Press Ctrl+C to gracefully stop the orchestrator. All threads will shut down cleanly within 10 seconds, and a final heartbeat will be written.
- Crypto Data: Continuous streaming, files rotate every 60 seconds
- Macro Data:
- Startup: 7-day backfill (happens once when orchestrator starts)
- Runtime: Fetches every 15 minutes (or as configured)
- Each run: Fetches last 1 day with automatic deduplication
- Macro data is fetched via yfinance REST API (1-minute granularity, max 7 days)
- Crypto data is collected via Binance WebSocket (real-time streaming)
- Both data types are stored in partitioned Parquet format (year/month/day)
- Health metrics update every 60 seconds (configurable in code)
- All timestamps are UTC timezone-aware
Testing mode allows you to validate the entire Crypto Lake system in minutes instead of hours by compressing time-based intervals while maintaining all production logic, data integrity checks, and warnings.
- π Validate end-to-end pipeline after code changes
- π Quick smoke test before production deployment
- π§ͺ Development and debugging with accelerated cycles
- π Verify all components work together correctly
Testing mode applies time compression to scheduled operations:
| Operation | Production | Testing | Speedup |
|---|---|---|---|
| Transform Interval | 60 min | 2 min | 30x faster |
| Macro Fetch Interval | 15 min | 1 min | 15x faster |
| Macro Startup Backfill | 7 days | 1 day | 7x less data |
| Backfill Lookback | 180 days | 3 days | 60x less data |
What Stays the Same:
- β All business logic and data processing
- β All validation rules and integrity checks
- β All warnings and error handling
- β Full 19-symbol collection (no shortcuts)
- β Real-time WebSocket streaming
- β Parquet schema and partitioning
What Changes:
- π Output directory:
D:/CryptoDataLake/test/(isolated) - π Log files:
D:/CryptoDataLake/test/logs/test/(isolated) - β±οΈ Intervals compressed for speed
- π·οΈ All health reports labeled "MODE: TEST"
- β‘ Forced initial transform after 2-minute warmup
Method 1: Shorthand flag
python main.py --mode testMethod 2: Orchestrate with testing flag
python main.py --mode orchestrate --testingMethod 3: Enable in config.yml
testing:
enabled: true # Activates testing mode globallyA typical 5-minute test run validates:
- Startup (0:00-0:30): Orchestrator initializes, starts WebSocket collector
- Macro Backfill (0:30-1:00): One-time 1-day macro data fetch
- Forced Transform (2:00): Guaranteed transform cycle runs
- Scheduled Transform (4:00): Second transform cycle completes
- Macro Fetch (3:00): Scheduled macro data fetch
- Shutdown (5:00): User presses Ctrl+C, test summary prints
When you stop the orchestrator with Ctrl+C, you'll see:
================================================================================
TEST COMPLETE
================================================================================
Duration: 300.5s
Transform cycles run: 2
Macro fetches: 2
Files written: 16
Warnings: None
================================================================================
All testing parameters are defined in config.yml:
testing:
enabled: false # Can be overridden by CLI flags
transform_interval_min: 2 # Transform every 2 minutes
macro_interval_min: 1 # Fetch macro data every 1 minute
macro_lookback_startup_days: 1 # Backfill 1 day on startup
macro_runtime_lookback_days: 1 # Fetch 1 day on each run
backfill_days: 3 # Backfill 3 days for testing
base_path: "D:/CryptoDataLake/test" # Isolated test directoryAfter running in test mode, validate the output:
# Run final validation on test directory
python -m tools.final_validation
# Check health report
cat D:/CryptoDataLake/test/reports/health.md
# Inspect test logs
cat D:/CryptoDataLake/test/logs/test/orchestrator.logThe validation script automatically detects test mode from config.yml and validates the test directory.
- Isolation: Test mode uses completely separate directories - production data is never touched
- No Shortcuts: All 19 symbols are collected, all validations run, all warnings fire
- Realistic Testing: Only time intervals are compressed - everything else is production-grade
- Clean State: Delete
D:/CryptoDataLake/test/between test runs for clean testing - Performance: Expect ~50-100 MB of test data after a 5-minute run
Launch the interactive dashboard for data visualization and quality monitoring:
streamlit run gui/app.pyFeatures:
- Real-time Candlestick Charts: View OHLC data across multiple timeframes (1s, 1m, 5m, 15m, 1h)
- Volume Analysis: Secondary volume bars color-coded by price direction
- Spread Visualization: Track bid-ask spread over time
- Data Quality Metrics: Monitor gaps, completeness, and continuity
- Auto-Refresh: Enable overnight monitoring with configurable intervals (15s - 5m)
- Export: Download filtered data as CSV
Access: http://localhost:8501 after launch
Tips:
- Use auto-refresh with 60s interval for overnight monitoring
- All timestamps displayed in UTC
- Efficiently queries large datasets using DuckDB with time-based filtering
The crypto-lake includes an analysis-ready layer with research views, data slicing tools, and comprehensive validation:
Pre-built views make analysis "one SELECT away". All views use window_start as the timestamp field (UTC timezone-aware).
Available Views:
- bars_1s - Base 1-second bars from collector (with exchange literal)
- bars_1m - 1-minute rollup from 1s bars
- klines_1m - Official Binance 1m klines (if available)
- compare_our_vs_kline_1m - Comparison view for data quality checks
- funding_oi_hourly - Funding rates and open interest (if derivatives data exists)
- macro_minute - 1-minute macro data (SPY, UUP, ES=F, etc.)
- dxy_synthetic_minute - DXY synthetic index (if available)
Location: sql/views.sql (uses @@BASE@@ placeholder for base path)
Note: The views use a @@BASE@@ placeholder that is replaced at runtime with the actual base_path from config.yml. This allows the SQL views to be portable across different installations.
Export timeboxed datasets for analysis in Parquet or CSV format:
# Export 1-minute bars for specific symbols and time range
python main.py --mode slice \
--symbols SOLUSDT,SUIUSDT \
--start 2025-10-21T00:00:00Z \
--end 2025-10-21T23:59:00Z \
--tf 1m \
--source bars \
--out data/extracts/sol_1m_2025-10-21.parquet \
--format parquet
# Export to CSV instead
python main.py --mode slice \
--symbols SOLUSDT \
--start 2025-10-21T00:00:00Z \
--end 2025-10-21T01:00:00Z \
--tf 1s \
--source bars \
--out data/extracts/sol_1s_sample.csv \
--format csvParameters:
--symbols: Comma-separated list of symbols (e.g., SOLUSDT,SUIUSDT)--start/--end: Time range in ISO format with Z suffix--tf: Timeframe (1s or 1m)--source: Data source (bars or klines)--out: Output file path--format: Output format (parquet or csv)
Run comprehensive data quality checks and generate Markdown reports:
# Run validation rulepack
python main.py --mode validate_rules \
--symbols SOLUSDT,SUIUSDT,ADAUSDT \
--start 2025-10-21T00:00:00Z \
--end 2025-10-21T14:40:00Z \
--tf 1s \
--source bars \
--report reports/sanity_2025-10-21.mdValidation Rules:
- R1: OHLC ordering (low β€ open,close β€ high)
- R2: Non-negative prices (OHLC > 0)
- R3: Ask β₯ Bid (bars only, ignores NaN)
- R4: No NaNs in OHLC (allows NaN in bid/ask/spread)
- R5: Timestamp continuity and UTC minute alignment
- R6: Spread sanity (spread β₯ 0, < 5% mid for >99.9% of rows)
- R7: Parity check with official klines (optional, if available)
Report Output:
- Summary table with violation counts per rule
- Top 25 offending rows per rule
- Overall assessment: PASS / INVESTIGATE / FAIL
- Detailed statistics (spread outliers, volume differences, etc.)
Example Report Structure:
# Data Quality Validation Report
## Run Metadata
- Symbols: SOLUSDT, SUIUSDT, ADAUSDT
- Time Range: 2025-10-21T00:00:00Z to 2025-10-21T14:40:00Z
- Timeframe: 1s
- Total Rows Checked: 157,680
## Validation Summary
| Rule | Description | Total Checked | Violations | % Violated |
|------|-------------|---------------|------------|------------|
| R1 | OHLC ordering | 157,680 | 0 | 0.00% |
| R2 | Positive prices | 157,680 | 0 | 0.00% |
...
## Overall Assessment
β
PASS - All validation rules passed successfully.Fetch historical 1-minute OHLCV data from Binance REST API to backfill up to 6 months of data:
# Backfill 180 days of historical data
python main.py --mode backfill_binance \
--symbols SOLUSDT,BTCUSDT,ETHUSDT \
--lookback_days 180 \
--interval 1m- REST API Integration: Uses Binance
/api/v3/klinesendpoint - Daily Chunking: Fetches data in daily chunks to respect API limits
- Automatic Deduplication: Checks existing data and only writes new timestamps
- Rate Limit Handling: Exponential backoff with retry logic (up to 5 attempts)
- Partitioned Storage: Writes to
D:/CryptoDataLake/backfill/binance/{SYMBOL}/year=YYYY/month=MM/day=DD/ - UTC Timestamps: All timestamps normalized to UTC timezone
- open_time: Candle open timestamp (UTC)
- open, high, low, close: Price data
- volume: Base asset volume
- close_time: Candle close timestamp (UTC)
- quote_volume: Quote asset volume
- trades: Number of trades in candle
- taker_base_vol, taker_quote_vol: Taker volume metrics
- Historical Analysis: Backfill 6 months for backtesting strategies
- Gap Filling: Fill gaps from collector downtime
- Bootstrap: Initialize database before starting real-time collection
- Binance limits: 1000 klines per request, 1200 requests/minute
- Rate limiting: 0.1s sleep between requests (10 req/s max)
- Deduplication: Existing timestamps are automatically skipped
- Storage: Separate from real-time data (
backfill/vsparquet/)
Collect 1-minute macro data (stocks, ETFs, futures) using yfinance:
# Fetch SPY and UUP data for last 7 days
python main.py --mode macro_minute \
--tickers SPY,UUP,ES=F \
--lookback_days 7Storage: D:/CryptoDataLake/macro/minute/{ticker}/year=2025/month=10/day=21/
Schema: ticker, ts, open, high, low, close, volume (all UTC timezone-aware)
Load and analyze data:
-- Load 1-second bars
SELECT * FROM read_parquet('D:/CryptoDataLake/parquet/**/*.parquet');
-- Aggregate to 1-minute bars
SELECT
symbol,
date_trunc('minute', window_start) AS ts,
first(open) AS open,
max(high) AS high,
min(low) AS low,
last(close) AS close,
sum(volume_base) AS volume
FROM read_parquet('D:/CryptoDataLake/parquet/**/*.parquet')
GROUP BY symbol, ts
ORDER BY ts;The codebase has been fully tested and verified:
- β Environment: Python 3.12.6, all dependencies installed
- β Static Analysis: All modules import successfully, no syntax errors
- β Unit Tests: 4/4 passing
- β Functional Tests: All 4 pipeline modes verified with live data
- β Data Quality: Zero gaps, zero duplicates, full schema compliance
See docs/ for the complete build verification report.
- High latency warnings during collection are normal (caused by Binance event timestamp metadata)
- The collector auto-reconnects on network failures with exponential backoff
- Logs rotate daily and are retained for 14 days
- Parquet files use Snappy compression for optimal size/speed tradeoff
MIT