ohlcv-processing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOHLCV Processing — Market Data Preparation
OHLCV数据处理 — 市场数据预处理
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.
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.
干净、一致的OHLCV数据是所有交易分析的基础。垃圾进垃圾出——一根异常K线就可能触发错误信号、破坏指标计算,并产生误导性的回测结果。本技能涵盖完整的数据预处理流程:验证、清洗、重采样、标准化以及多源数据合并。
重要性说明:加密货币OHLCV数据比传统市场数据更杂乱。7×24小时交易意味着没有官方收盘时间,DEX聚合器对价格的统计存在差异,低流动性代币会生成不符合逻辑的K线,API中断还会造成数据缺口。任何分析工作流都应从该预处理流程开始。
Quick Start
快速开始
1. Install Dependencies
1. 安装依赖
bash
uv pip install pandas numpy httpxbash
uv pip install pandas numpy httpx2. Standard OHLCV DataFrame Format
2. 标准OHLCV DataFrame格式
All processing functions expect this canonical format:
python
import pandas as pd所有处理函数都要求使用以下标准格式:
python
import pandas as pdCanonical OHLCV DataFrame
Canonical OHLCV DataFrame
- DatetimeIndex in UTC
- DatetimeIndex in UTC
- Columns: open, high, low, close, volume (lowercase)
- Columns: open, high, low, close, volume (lowercase)
- Sorted ascending by timestamp
- Sorted ascending by timestamp
- No duplicate timestamps
- No duplicate timestamps
df = pd.DataFrame({
"open": [1.10, 1.12, 1.11],
"high": [1.15, 1.14, 1.13],
"low": [1.08, 1.10, 1.09],
"close": [1.12, 1.11, 1.12],
"volume": [50000, 48000, 52000],
}, index=pd.to_datetime([
"2025-01-01 00:00:00",
"2025-01-01 00:01:00",
"2025-01-01 00:02:00",
], utc=True))
df.index.name = "timestamp"
undefineddf = pd.DataFrame({
"open": [1.10, 1.12, 1.11],
"high": [1.15, 1.14, 1.13],
"low": [1.08, 1.10, 1.09],
"close": [1.12, 1.11, 1.12],
"volume": [50000, 48000, 52000],
}, index=pd.to_datetime([
"2025-01-01 00:00:00",
"2025-01-01 00:01:00",
"2025-01-01 00:02:00",
], utc=True))
df.index.name = "timestamp"
undefined3. Full Processing Pipeline
3. 完整处理流程
python
import pandas as pd
import numpy as np
def process_ohlcv(df: pd.DataFrame) -> pd.DataFrame:
"""Run complete OHLCV processing pipeline."""
df = standardize_columns(df)
df = validate_ohlcv(df)
df = handle_gaps(df, method="ffill")
df = detect_and_flag_anomalies(df)
return dfpython
import pandas as pd
import numpy as np
def process_ohlcv(df: pd.DataFrame) -> pd.DataFrame:
"""Run complete OHLCV processing pipeline."""
df = standardize_columns(df)
df = validate_ohlcv(df)
df = handle_gaps(df, method="ffill")
df = detect_and_flag_anomalies(df)
return dfData Validation
数据验证
Column Checks
列检查
python
REQUIRED_COLUMNS = {"open", "high", "low", "close", "volume"}
def standardize_columns(df: pd.DataFrame) -> pd.DataFrame:
"""Normalize column names to lowercase standard."""
df.columns = df.columns.str.lower().str.strip()
# Common renames
rename_map = {"vol": "volume", "v": "volume", "o": "open",
"h": "high", "l": "low", "c": "close"}
df = df.rename(columns=rename_map)
missing = REQUIRED_COLUMNS - set(df.columns)
if missing:
raise ValueError(f"Missing columns: {missing}")
return df[["open", "high", "low", "close", "volume"]]python
REQUIRED_COLUMNS = {"open", "high", "low", "close", "volume"}
def standardize_columns(df: pd.DataFrame) -> pd.DataFrame:
"""Normalize column names to lowercase standard."""
df.columns = df.columns.str.lower().str.strip()
# Common renames
rename_map = {"vol": "volume", "v": "volume", "o": "open",
"h": "high", "l": "low", "c": "close"}
df = df.rename(columns=rename_map)
missing = REQUIRED_COLUMNS - set(df.columns)
if missing:
raise ValueError(f"Missing columns: {missing}")
return df[["open", "high", "low", "close", "volume"]]Structural Validation
结构验证
python
def validate_ohlcv(df: pd.DataFrame) -> pd.DataFrame:
"""Validate OHLCV structural integrity."""
# Ensure DatetimeIndex in UTC
if not isinstance(df.index, pd.DatetimeIndex):
df.index = pd.to_datetime(df.index, utc=True)
if df.index.tz is None:
df.index = df.index.tz_localize("UTC")
# Sort and deduplicate
df = df.sort_index()
dupes = df.index.duplicated(keep="last")
if dupes.any():
print(f"Warning: Removed {dupes.sum()} duplicate timestamps")
df = df[~dupes]
# Type enforcement
for col in ["open", "high", "low", "close", "volume"]:
df[col] = pd.to_numeric(df[col], errors="coerce")
return dfpython
def validate_ohlcv(df: pd.DataFrame) -> pd.DataFrame:
"""Validate OHLCV structural integrity."""
# Ensure DatetimeIndex in UTC
if not isinstance(df.index, pd.DatetimeIndex):
df.index = pd.to_datetime(df.index, utc=True)
if df.index.tz is None:
df.index = df.index.tz_localize("UTC")
# Sort and deduplicate
df = df.sort_index()
dupes = df.index.duplicated(keep="last")
if dupes.any():
print(f"Warning: Removed {dupes.sum()} duplicate timestamps")
df = df[~dupes]
# Type enforcement
for col in ["open", "high", "low", "close", "volume"]:
df[col] = pd.to_numeric(df[col], errors="coerce")
return dfImpossible Candle Detection
异常K线检测
python
def find_impossible_candles(df: pd.DataFrame) -> pd.DataFrame:
"""Find candles that violate OHLC constraints."""
issues = pd.DataFrame(index=df.index)
issues["high_lt_low"] = df["high"] < df["low"]
issues["high_lt_open"] = df["high"] < df["open"]
issues["high_lt_close"] = df["high"] < df["close"]
issues["low_gt_open"] = df["low"] > df["open"]
issues["low_gt_close"] = df["low"] > df["close"]
issues["negative_price"] = (df[["open", "high", "low", "close"]] < 0).any(axis=1)
issues["negative_volume"] = df["volume"] < 0
issues["any_issue"] = issues.any(axis=1)
return issues[issues["any_issue"]]python
def find_impossible_candles(df: pd.DataFrame) -> pd.DataFrame:
"""Find candles that violate OHLC constraints."""
issues = pd.DataFrame(index=df.index)
issues["high_lt_low"] = df["high"] < df["low"]
issues["high_lt_open"] = df["high"] < df["open"]
issues["high_lt_close"] = df["high"] < df["close"]
issues["low_gt_open"] = df["low"] > df["open"]
issues["low_gt_close"] = df["low"] > df["close"]
issues["negative_price"] = (df[["open", "high", "low", "close"]] < 0).any(axis=1)
issues["negative_volume"] = df["volume"] < 0
issues["any_issue"] = issues.any(axis=1)
return issues[issues["any_issue"]]Gap Handling
缺口处理
Crypto trades 24/7, but gaps still occur from API outages, low liquidity, or aggregator downtime.
加密货币全天候交易,但API中断、低流动性或聚合器停机仍会导致数据缺口。
Detect Gaps
检测缺口
python
def detect_gaps(df: pd.DataFrame, expected_freq: str = "1min") -> pd.Series:
"""Find missing timestamps based on expected frequency."""
full_index = pd.date_range(
start=df.index.min(), end=df.index.max(), freq=expected_freq, tz="UTC"
)
missing = full_index.difference(df.index)
return missingpython
def detect_gaps(df: pd.DataFrame, expected_freq: str = "1min") -> pd.Series:
"""Find missing timestamps based on expected frequency."""
full_index = pd.date_range(
start=df.index.min(), end=df.index.max(), freq=expected_freq, tz="UTC"
)
missing = full_index.difference(df.index)
return missingFill Gaps
填充缺口
python
def handle_gaps(
df: pd.DataFrame,
freq: str = "1min",
method: str = "ffill",
max_gap: int = 5,
) -> pd.DataFrame:
"""Fill gaps in OHLCV data.
Args:
df: OHLCV DataFrame with DatetimeIndex.
freq: Expected bar frequency.
method: 'ffill' (forward fill) or 'interpolate'.
max_gap: Maximum consecutive bars to fill. Larger gaps are left as NaN.
"""
full_index = pd.date_range(
start=df.index.min(), end=df.index.max(), freq=freq, tz="UTC"
)
df = df.reindex(full_index)
df.index.name = "timestamp"
# Mark which bars were filled
df["is_filled"] = df["close"].isna()
if method == "ffill":
# Forward fill OHLC (flat candle), zero volume
df[["open", "high", "low", "close"]] = (
df[["open", "high", "low", "close"]].ffill(limit=max_gap)
)
df["volume"] = df["volume"].fillna(0)
elif method == "interpolate":
df[["open", "high", "low", "close"]] = (
df[["open", "high", "low", "close"]].interpolate(
method="time", limit=max_gap
)
)
df["volume"] = df["volume"].fillna(0)
return dfpython
def handle_gaps(
df: pd.DataFrame,
freq: str = "1min",
method: str = "ffill",
max_gap: int = 5,
) -> pd.DataFrame:
"""Fill gaps in OHLCV data.
Args:
df: OHLCV DataFrame with DatetimeIndex.
freq: Expected bar frequency.
method: 'ffill' (forward fill) or 'interpolate'.
max_gap: Maximum consecutive bars to fill. Larger gaps are left as NaN.
"""
full_index = pd.date_range(
start=df.index.min(), end=df.index.max(), freq=freq, tz="UTC"
)
df = df.reindex(full_index)
df.index.name = "timestamp"
# Mark which bars were filled
df["is_filled"] = df["close"].isna()
if method == "ffill":
# Forward fill OHLC (flat candle), zero volume
df[["open", "high", "low", "close"]] = (
df[["open", "high", "low", "close"]].ffill(limit=max_gap)
)
df["volume"] = df["volume"].fillna(0)
elif method == "interpolate":
df[["open", "high", "low", "close"]] = (
df[["open", "high", "low", "close"]].interpolate(
method="time", limit=max_gap
)
)
df["volume"] = df["volume"].fillna(0)
return dfAnomaly Detection
异常检测
See for the complete anomaly taxonomy.
references/data_quality.md完整的异常分类请参考。
references/data_quality.mdPrice Spike Detection
价格 spike 检测
python
def detect_price_spikes(
df: pd.DataFrame, window: int = 20, threshold: float = 3.0
) -> pd.Series:
"""Flag bars where return exceeds threshold * rolling std."""
returns = df["close"].pct_change()
rolling_std = returns.rolling(window, min_periods=5).std()
spike = returns.abs() > (threshold * rolling_std)
return spike.fillna(False)python
def detect_price_spikes(
df: pd.DataFrame, window: int = 20, threshold: float = 3.0
) -> pd.Series:
"""Flag bars where return exceeds threshold * rolling std."""
returns = df["close"].pct_change()
rolling_std = returns.rolling(window, min_periods=5).std()
spike = returns.abs() > (threshold * rolling_std)
return spike.fillna(False)Zero Volume Detection
零交易量检测
python
def detect_zero_volume(df: pd.DataFrame, min_volume: float = 0) -> pd.Series:
"""Flag bars with zero or below-minimum volume."""
return df["volume"] <= min_volumepython
def detect_zero_volume(df: pd.DataFrame, min_volume: float = 0) -> pd.Series:
"""Flag bars with zero or below-minimum volume."""
return df["volume"] <= min_volumeComposite Anomaly Flagging
复合异常标记
python
def flag_anomalies(df: pd.DataFrame) -> pd.DataFrame:
"""Add anomaly flag columns to DataFrame."""
df["anomaly_spike"] = detect_price_spikes(df)
df["anomaly_zero_vol"] = detect_zero_volume(df)
impossible = find_impossible_candles(df)
df["anomaly_impossible"] = False
if not impossible.empty:
df.loc[impossible.index, "anomaly_impossible"] = True
df["anomaly_any"] = (
df["anomaly_spike"] | df["anomaly_zero_vol"] | df["anomaly_impossible"]
)
return dfpython
def flag_anomalies(df: pd.DataFrame) -> pd.DataFrame:
"""Add anomaly flag columns to DataFrame."""
df["anomaly_spike"] = detect_price_spikes(df)
df["anomaly_zero_vol"] = detect_zero_volume(df)
impossible = find_impossible_candles(df)
df["anomaly_impossible"] = False
if not impossible.empty:
df.loc[impossible.index, "anomaly_impossible"] = True
df["anomaly_any"] = (
df["anomaly_spike"] | df["anomaly_zero_vol"] | df["anomaly_impossible"]
)
return dfResampling
重采样
See for detailed guidance.
references/resampling_guide.md详细指南请参考。
references/resampling_guide.mdStandard Resample
标准重采样
python
OHLCV_RESAMPLE_RULES = {
"open": "first",
"high": "max",
"low": "min",
"close": "last",
"volume": "sum",
}
def resample_ohlcv(df: pd.DataFrame, target_freq: str) -> pd.DataFrame:
"""Resample OHLCV to a coarser timeframe.
Args:
df: OHLCV DataFrame (must be finer than target_freq).
target_freq: Pandas frequency string ('5min', '15min', '1h', '4h', '1D').
Returns:
Resampled OHLCV DataFrame with no NaN rows.
"""
ohlcv_cols = ["open", "high", "low", "close", "volume"]
resampled = df[ohlcv_cols].resample(target_freq).agg(OHLCV_RESAMPLE_RULES)
return resampled.dropna(subset=["close"])python
OHLCV_RESAMPLE_RULES = {
"open": "first",
"high": "max",
"low": "min",
"close": "last",
"volume": "sum",
}
def resample_ohlcv(df: pd.DataFrame, target_freq: str) -> pd.DataFrame:
"""Resample OHLCV to a coarser timeframe.
Args:
df: OHLCV DataFrame (must be finer than target_freq).
target_freq: Pandas frequency string ('5min', '15min', '1h', '4h', '1D').
Returns:
Resampled OHLCV DataFrame with no NaN rows.
"""
ohlcv_cols = ["open", "high", "low", "close", "volume"]
resampled = df[ohlcv_cols].resample(target_freq).agg(OHLCV_RESAMPLE_RULES)
return resampled.dropna(subset=["close"])Common Timeframe Ladder
常用时间阶梯
python
TIMEFRAME_LADDER = ["1min", "5min", "15min", "1h", "4h", "1D"]
def resample_ladder(df: pd.DataFrame) -> dict[str, pd.DataFrame]:
"""Resample 1-minute data to all standard timeframes."""
results = {"1min": df.copy()}
for tf in TIMEFRAME_LADDER[1:]:
results[tf] = resample_ohlcv(df, tf)
return resultspython
TIMEFRAME_LADDER = ["1min", "5min", "15min", "1h", "4h", "1D"]
def resample_ladder(df: pd.DataFrame) -> dict[str, pd.DataFrame]:
"""Resample 1-minute data to all standard timeframes."""
results = {"1min": df.copy()}
for tf in TIMEFRAME_LADDER[1:]:
results[tf] = resample_ohlcv(df, tf)
return resultsVWAP Calculation
VWAP 计算
python
def compute_vwap(df: pd.DataFrame) -> pd.Series:
"""Compute cumulative VWAP over the DataFrame."""
typical_price = (df["high"] + df["low"] + df["close"]) / 3
cum_vol = df["volume"].cumsum()
cum_tp_vol = (typical_price * df["volume"]).cumsum()
return cum_tp_vol / cum_volpython
def compute_vwap(df: pd.DataFrame) -> pd.Series:
"""Compute cumulative VWAP over the DataFrame."""
typical_price = (df["high"] + df["low"] + df["close"]) / 3
cum_vol = df["volume"].cumsum()
cum_tp_vol = (typical_price * df["volume"]).cumsum()
return cum_tp_vol / cum_volNormalization
标准化
python
def normalize_prices(
df: pd.DataFrame, method: str = "returns"
) -> pd.DataFrame:
"""Normalize OHLCV price columns.
Methods:
'returns' — Percentage returns (close-to-close).
'log_returns' — Log returns.
'minmax' — Min-max scale to [0, 1].
'zscore' — Z-score normalization.
"""
price_cols = ["open", "high", "low", "close"]
result = df.copy()
if method == "returns":
for col in price_cols:
result[f"{col}_ret"] = result[col].pct_change()
elif method == "log_returns":
for col in price_cols:
result[f"{col}_logret"] = np.log(result[col] / result[col].shift(1))
elif method == "minmax":
for col in price_cols:
cmin, cmax = result[col].min(), result[col].max()
result[f"{col}_norm"] = (result[col] - cmin) / (cmax - cmin)
elif method == "zscore":
for col in price_cols:
result[f"{col}_z"] = (
(result[col] - result[col].mean()) / result[col].std()
)
return resultpython
def normalize_prices(
df: pd.DataFrame, method: str = "returns"
) -> pd.DataFrame:
"""Normalize OHLCV price columns.
Methods:
'returns' — Percentage returns (close-to-close).
'log_returns' — Log returns.
'minmax' — Min-max scale to [0, 1].
'zscore' — Z-score normalization.
"""
price_cols = ["open", "high", "low", "close"]
result = df.copy()
if method == "returns":
for col in price_cols:
result[f"{col}_ret"] = result[col].pct_change()
elif method == "log_returns":
for col in price_cols:
result[f"{col}_logret"] = np.log(result[col] / result[col].shift(1))
elif method == "minmax":
for col in price_cols:
cmin, cmax = result[col].min(), result[col].max()
result[f"{col}_norm"] = (result[col] - cmin) / (cmax - cmin)
elif method == "zscore":
for col in price_cols:
result[f"{col}_z"] = (
(result[col] - result[col].mean()) / result[col].std()
)
return resultMulti-Source Merging
多源数据合并
When combining data from multiple sources (e.g., Birdeye + DexScreener), timestamps may not align and prices may differ due to different DEX aggregation.
python
def merge_ohlcv_sources(
primary: pd.DataFrame,
secondary: pd.DataFrame,
tolerance: str = "30s",
) -> pd.DataFrame:
"""Merge two OHLCV sources, preferring the higher-volume source per bar.
Args:
primary: First OHLCV source.
secondary: Second OHLCV source.
tolerance: Maximum time difference for alignment.
"""
merged = pd.merge_asof(
primary.sort_index(),
secondary.sort_index(),
left_index=True, right_index=True,
tolerance=pd.Timedelta(tolerance),
suffixes=("_pri", "_sec"),
)
# Use higher-volume source per bar
use_secondary = merged["volume_sec"] > merged["volume_pri"]
for col in ["open", "high", "low", "close", "volume"]:
merged[col] = np.where(
use_secondary, merged[f"{col}_sec"], merged[f"{col}_pri"]
)
merged["source"] = np.where(use_secondary, "secondary", "primary")
return merged[["open", "high", "low", "close", "volume", "source"]]当合并来自多个数据源(如Birdeye + DexScreener)的数据时,时间戳可能无法对齐,且由于不同的DEX聚合逻辑,价格也可能存在差异。
python
def merge_ohlcv_sources(
primary: pd.DataFrame,
secondary: pd.DataFrame,
tolerance: str = "30s",
) -> pd.DataFrame:
"""Merge two OHLCV sources, preferring the higher-volume source per bar.
Args:
primary: First OHLCV source.
secondary: Second OHLCV source.
tolerance: Maximum time difference for alignment.
"""
merged = pd.merge_asof(
primary.sort_index(),
secondary.sort_index(),
left_index=True, right_index=True,
tolerance=pd.Timedelta(tolerance),
suffixes=("_pri", "_sec"),
)
# Use higher-volume source per bar
use_secondary = merged["volume_sec"] > merged["volume_pri"]
for col in ["open", "high", "low", "close", "volume"]:
merged[col] = np.where(
use_secondary, merged[f"{col}_sec"], merged[f"{col}_pri"]
)
merged["source"] = np.where(use_secondary, "secondary", "primary")
return merged[["open", "high", "low", "close", "volume", "source"]]Timezone Handling
时区处理
Standard: Always store and process in UTC. Convert only for display.
python
def ensure_utc(df: pd.DataFrame) -> pd.DataFrame:
"""Ensure DatetimeIndex is UTC."""
if df.index.tz is None:
df.index = df.index.tz_localize("UTC")
elif str(df.index.tz) != "UTC":
df.index = df.index.tz_convert("UTC")
return df标准规范:始终使用UTC时区存储和处理数据,仅在展示时进行转换。
python
def ensure_utc(df: pd.DataFrame) -> pd.DataFrame:
"""Ensure DatetimeIndex is UTC."""
if df.index.tz is None:
df.index = df.index.tz_localize("UTC")
elif str(df.index.tz) != "UTC":
df.index = df.index.tz_convert("UTC")
return dfData Quality Report
数据质量报告
python
def quality_report(df: pd.DataFrame) -> dict:
"""Generate a data quality summary."""
total = len(df)
return {
"total_bars": total,
"date_range": f"{df.index.min()} → {df.index.max()}",
"missing_values": int(df[["open", "high", "low", "close"]].isna().sum().sum()),
"zero_volume_bars": int((df["volume"] == 0).sum()),
"impossible_candles": int((df["high"] < df["low"]).sum()),
"duplicate_timestamps": int(df.index.duplicated().sum()),
"negative_prices": int((df[["open", "high", "low", "close"]] < 0).any(axis=1).sum()),
"completeness_pct": round((1 - df["close"].isna().mean()) * 100, 2),
}python
def quality_report(df: pd.DataFrame) -> dict:
"""Generate a data quality summary."""
total = len(df)
return {
"total_bars": total,
"date_range": f"{df.index.min()} → {df.index.max()}",
"missing_values": int(df[["open", "high", "low", "close"]].isna().sum().sum()),
"zero_volume_bars": int((df["volume"] == 0).sum()),
"impossible_candles": int((df["high"] < df["low"]).sum()),
"duplicate_timestamps": int(df.index.duplicated().sum()),
"negative_prices": int((df[["open", "high", "low", "close"]] < 0).any(axis=1).sum()),
"completeness_pct": round((1 - df["close"].isna().mean()) * 100, 2),
}Files
文件说明
References
参考文档
- — Anomaly types, detection methods, correction strategies, crypto-specific data issues
references/data_quality.md - — Resample rules, timeframe use cases, partial bar handling, VWAP resampling, multi-timeframe alignment
references/resampling_guide.md
- — 异常类型、检测方法、修正策略、加密货币特有的数据问题
references/data_quality.md - — 重采样规则、时间框架用例、部分K线处理、VWAP重采样、多时间框架对齐
references/resampling_guide.md
Scripts
脚本文件
- — Full processing pipeline: validate, clean, resample, normalize with anomaly reporting (run with
scripts/process_ohlcv.pyfor synthetic data)--demo - — Multi-source OHLCV merging with conflict resolution and discrepancy reporting (run with
scripts/merge_sources.py)--demo
- — 完整处理流程:验证、清洗、重采样、标准化及异常报告(添加
scripts/process_ohlcv.py参数可使用合成数据运行)--demo - — 多源OHLCV数据合并,含冲突解决和差异报告(添加
scripts/merge_sources.py参数可运行)--demo