$npx -y skills add agiprolabs/claude-trading-skills --skill ohlcv-processingMarket data preparation including OHLCV resampling, gap handling, anomaly detection, normalization, and multi-source merging
| 1 | # OHLCV Processing — Market Data Preparation |
| 2 | |
| 3 | Clean, consistent OHLCV data is the foundation of every trading analysis. Garbage in, garbage out — a single anomalous candle can trigger false signals, corrupt indicator calculations, and produce misleading backtest results. This skill covers the full data preparation pipeline: validation, cleaning, resampling, normalization, and multi-source merging. |
| 4 | |
| 5 | **Why this matters**: Crypto OHLCV data is messier than traditional markets. 24/7 trading means no official close, DEX aggregators disagree on prices, low-liquidity tokens produce impossible candles, and API outages create gaps. Every analysis workflow should start with this pipeline. |
| 6 | |
| 7 | ## Quick Start |
| 8 | |
| 9 | ### 1. Install Dependencies |
| 10 | |
| 11 | ```bash |
| 12 | uv pip install pandas numpy httpx |
| 13 | ``` |
| 14 | |
| 15 | ### 2. Standard OHLCV DataFrame Format |
| 16 | |
| 17 | All processing functions expect this canonical format: |
| 18 | |
| 19 | ```python |
| 20 | import pandas as pd |
| 21 | |
| 22 | # Canonical OHLCV DataFrame |
| 23 | # - DatetimeIndex in UTC |
| 24 | # - Columns: open, high, low, close, volume (lowercase) |
| 25 | # - Sorted ascending by timestamp |
| 26 | # - No duplicate timestamps |
| 27 | |
| 28 | df = pd.DataFrame({ |
| 29 | "open": [1.10, 1.12, 1.11], |
| 30 | "high": [1.15, 1.14, 1.13], |
| 31 | "low": [1.08, 1.10, 1.09], |
| 32 | "close": [1.12, 1.11, 1.12], |
| 33 | "volume": [50000, 48000, 52000], |
| 34 | }, index=pd.to_datetime([ |
| 35 | "2025-01-01 00:00:00", |
| 36 | "2025-01-01 00:01:00", |
| 37 | "2025-01-01 00:02:00", |
| 38 | ], utc=True)) |
| 39 | df.index.name = "timestamp" |
| 40 | ``` |
| 41 | |
| 42 | ### 3. Full Processing Pipeline |
| 43 | |
| 44 | ```python |
| 45 | import pandas as pd |
| 46 | import numpy as np |
| 47 | |
| 48 | def process_ohlcv(df: pd.DataFrame) -> pd.DataFrame: |
| 49 | """Run complete OHLCV processing pipeline.""" |
| 50 | df = standardize_columns(df) |
| 51 | df = validate_ohlcv(df) |
| 52 | df = handle_gaps(df, method="ffill") |
| 53 | df = detect_and_flag_anomalies(df) |
| 54 | return df |
| 55 | ``` |
| 56 | |
| 57 | ## Data Validation |
| 58 | |
| 59 | ### Column Checks |
| 60 | |
| 61 | ```python |
| 62 | REQUIRED_COLUMNS = {"open", "high", "low", "close", "volume"} |
| 63 | |
| 64 | def standardize_columns(df: pd.DataFrame) -> pd.DataFrame: |
| 65 | """Normalize column names to lowercase standard.""" |
| 66 | df.columns = df.columns.str.lower().str.strip() |
| 67 | # Common renames |
| 68 | rename_map = {"vol": "volume", "v": "volume", "o": "open", |
| 69 | "h": "high", "l": "low", "c": "close"} |
| 70 | df = df.rename(columns=rename_map) |
| 71 | missing = REQUIRED_COLUMNS - set(df.columns) |
| 72 | if missing: |
| 73 | raise ValueError(f"Missing columns: {missing}") |
| 74 | return df[["open", "high", "low", "close", "volume"]] |
| 75 | ``` |
| 76 | |
| 77 | ### Structural Validation |
| 78 | |
| 79 | ```python |
| 80 | def validate_ohlcv(df: pd.DataFrame) -> pd.DataFrame: |
| 81 | """Validate OHLCV structural integrity.""" |
| 82 | # Ensure DatetimeIndex in UTC |
| 83 | if not isinstance(df.index, pd.DatetimeIndex): |
| 84 | df.index = pd.to_datetime(df.index, utc=True) |
| 85 | if df.index.tz is None: |
| 86 | df.index = df.index.tz_localize("UTC") |
| 87 | |
| 88 | # Sort and deduplicate |
| 89 | df = df.sort_index() |
| 90 | dupes = df.index.duplicated(keep="last") |
| 91 | if dupes.any(): |
| 92 | print(f"Warning: Removed {dupes.sum()} duplicate timestamps") |
| 93 | df = df[~dupes] |
| 94 | |
| 95 | # Type enforcement |
| 96 | for col in ["open", "high", "low", "close", "volume"]: |
| 97 | df[col] = pd.to_numeric(df[col], errors="coerce") |
| 98 | |
| 99 | return df |
| 100 | ``` |
| 101 | |
| 102 | ### Impossible Candle Detection |
| 103 | |
| 104 | ```python |
| 105 | def find_impossible_candles(df: pd.DataFrame) -> pd.DataFrame: |
| 106 | """Find candles that violate OHLC constraints.""" |
| 107 | issues = pd.DataFrame(index=df.index) |
| 108 | issues["high_lt_low"] = df["high"] < df["low"] |
| 109 | issues["high_lt_open"] = df["high"] < df["open"] |
| 110 | issues["high_lt_close"] = df["high"] < df["close"] |
| 111 | issues["low_gt_open"] = df["low"] > df["open"] |
| 112 | issues["low_gt_close"] = df["low"] > df["close"] |
| 113 | issues["negative_price"] = (df[["open", "high", "low", "close"]] < 0).any(axis=1) |
| 114 | issues["negative_volume"] = df["volume"] < 0 |
| 115 | issues["any_issue"] = issues.any(axis=1) |
| 116 | return issues[issues["any_issue"]] |
| 117 | ``` |
| 118 | |
| 119 | ## Gap Handling |
| 120 | |
| 121 | Crypto trades 24/7, but gaps still occur from API outages, low liquidity, or aggregator downtime. |
| 122 | |
| 123 | ### Detect Gaps |
| 124 | |
| 125 | ```python |
| 126 | def detect_gaps(df: pd.DataFrame, expected_freq: str = "1min") -> pd.Series: |
| 127 | """Find missing timestamps based on expected frequency.""" |
| 128 | full_index = pd.date_range( |
| 129 | start=df.index.min(), end=df.index.max(), freq=expected_freq, tz="UTC" |
| 130 | ) |
| 131 | missing = full_index.difference(df.index) |
| 132 | return missing |
| 133 | ``` |
| 134 | |
| 135 | ### Fill Gaps |
| 136 | |
| 137 | ```python |
| 138 | def handle_gaps( |
| 139 | df: pd.DataFrame, |
| 140 | freq: str = "1min", |
| 141 | method: str = "ffill", |
| 142 | max_gap: int = 5, |
| 143 | ) -> pd.DataFrame: |
| 144 | """Fill gaps in OHLCV data. |
| 145 | |
| 146 | Args: |
| 147 | df: OHLCV DataFrame with DatetimeIndex. |
| 148 | freq: Expected bar frequency. |
| 149 | method: 'ffill' (forward fill) or 'interpolate'. |
| 150 | max_gap: Maximum consecutive bars to fill. Larger gaps are left as NaN. |
| 151 | """ |
| 152 | full_index = pd.date_range( |
| 153 | start=df.index.min(), end=df.index.max(), freq=freq, tz="UTC" |
| 154 | ) |