Files
2026-07-13 13:26:28 +08:00

214 lines
7.5 KiB
Python

"""Crypto loaders: market (OHLCV, premium index) and on-chain (DefiLlama TVL, CoinGecko)."""
from typing import Literal
import polars as pl
from data.exceptions import DataNotFoundError
from utils import ML4T_DATA_PATH
from utils.data_quality import apply_max_symbols
def list_crypto_perps() -> list[str]:
"""List perpetual-futures symbols available in the local data store.
Returns:
Sorted list of Binance USDT perps (e.g., ``["AAVEUSDT", ..., "XRPUSDT"]``).
Raises:
DataNotFoundError: If ``crypto/perps_1h.parquet`` is missing.
Example:
>>> list_crypto_perps()[:3]
['AAVEUSDT', 'ADAUSDT', 'APTUSDT']
"""
path = ML4T_DATA_PATH / "crypto" / "market" / "perps_1h.parquet"
if not path.exists():
raise DataNotFoundError(
dataset_name="Crypto Perpetuals OHLCV",
path=path,
download_script="data/crypto/market/download.py",
readme="data/crypto/README.md",
)
return pl.scan_parquet(path).select("symbol").unique().collect().to_series().sort().to_list()
def load_crypto_premium(
frequency: Literal["1h", "8h"] = "8h",
symbols: list[str] | None = None,
start_date: str | None = None,
end_date: str | None = None,
max_symbols: int = 0,
) -> pl.DataFrame:
"""Load crypto premium index for funding rate arbitrage case study.
Args:
frequency: Data frequency. "8h" aligns with Binance funding settlement times
(00:00, 08:00, 16:00 UTC). Default is "8h".
symbols: Optional list of symbols to filter (e.g., ["BTCUSDT", "ETHUSDT"])
start_date: Optional start date (YYYY-MM-DD format)
end_date: Optional end date (YYYY-MM-DD format)
max_symbols: Limit to N random symbols (0 = all). Seed-deterministic.
Returns:
DataFrame with columns: timestamp, symbol, premium_index_open/high/low/close
"""
filename = f"premium_index_{frequency}.parquet"
path = ML4T_DATA_PATH / "crypto" / "market" / filename
if not path.exists():
raise DataNotFoundError(
dataset_name="Crypto Premium Index",
path=path,
download_script="data/crypto/market/download.py --premium",
readme="data/crypto/README.md",
)
df = pl.read_parquet(path)
# Apply filters
if symbols:
df = df.filter(pl.col("symbol").is_in(symbols))
if start_date:
df = df.filter(pl.col("timestamp").dt.date() >= pl.lit(start_date).str.to_date())
if end_date:
df = df.filter(pl.col("timestamp").dt.date() <= pl.lit(end_date).str.to_date())
return apply_max_symbols(df, max_symbols)
def load_crypto_perps(
frequency: Literal["1h", "8h"] = "1h",
symbols: list[str] | None = None,
start_date: str | None = None,
end_date: str | None = None,
max_symbols: int = 0,
) -> pl.DataFrame:
"""Load crypto perpetual futures OHLCV data.
Args:
frequency: Data frequency. "1h" for raw hourly data, "8h" for funding-aligned
8-hour bars (00:00, 08:00, 16:00 UTC - standard funding settlement times).
symbols: Optional list of symbols to filter (e.g., ["BTCUSDT", "ETHUSDT"])
start_date: Optional start date (YYYY-MM-DD format)
end_date: Optional end date (YYYY-MM-DD format)
max_symbols: Limit to N random symbols (0 = all). Seed-deterministic.
Returns:
DataFrame with columns: timestamp, symbol, open, high, low, close, volume
"""
# Always load from 1h source
filename = "perps_1h.parquet"
path = ML4T_DATA_PATH / "crypto" / "market" / filename
if not path.exists():
raise DataNotFoundError(
dataset_name="Crypto Perpetuals OHLCV",
path=path,
download_script="data/crypto/market/download.py",
readme="data/crypto/README.md",
)
lf = pl.scan_parquet(path)
ts_type = lf.collect_schema()["timestamp"]
tz = getattr(ts_type, "time_zone", None)
def _ts_lit(d: str) -> pl.Expr:
e = pl.lit(d).str.to_datetime()
return e.dt.replace_time_zone(tz) if tz else e
# Apply filters before resampling (parquet pushdown via row-group pruning)
if symbols:
lf = lf.filter(pl.col("symbol").is_in(symbols))
if start_date:
lf = lf.filter(pl.col("timestamp") >= _ts_lit(start_date))
if end_date:
# Include the entire end_date for intraday
lf = lf.filter(pl.col("timestamp") < _ts_lit(end_date) + pl.duration(days=1))
df = lf.collect()
# Apply max_symbols before resampling
df = apply_max_symbols(df, max_symbols)
if frequency == "8h":
# Resample to 8H aligned with funding settlement times (00:00, 08:00, 16:00 UTC)
df = (
df.sort(["symbol", "timestamp"])
.group_by_dynamic(
"timestamp",
every="8h",
period="8h",
by="symbol",
closed="left",
label="left",
)
.agg(
pl.col("open").first(),
pl.col("high").max(),
pl.col("low").min(),
pl.col("close").last(),
pl.col("volume").sum(),
)
.sort(["symbol", "timestamp"])
)
# Join premium index data (8H aligned, same schedule as funding settlements)
premium_path = ML4T_DATA_PATH / "crypto" / "market" / "premium_index_8h.parquet"
if premium_path.exists():
premium = pl.read_parquet(premium_path)
if symbols:
premium = premium.filter(pl.col("symbol").is_in(df["symbol"].unique()))
df = df.join(premium, on=["symbol", "timestamp"], how="left")
return df
# --- On-chain / DeFi metrics ---
def load_defillama_chain_tvl(chain: str = "total") -> pl.DataFrame:
"""Load historical Total Value Locked (TVL) from DefiLlama.
Produced by `data/crypto/onchain/download.py`.
Args:
chain: "total" for aggregate DeFi TVL across all chains (default),
or a chain name like "Ethereum", "Solana", "BSC", "Arbitrum".
Matches the filename suffix (lowercased).
Returns:
DataFrame with `timestamp` (Date) and `tvl_usd` (float) columns.
"""
suffix = chain.lower()
path = ML4T_DATA_PATH / "crypto" / "onchain" / f"defillama_tvl_{suffix}.parquet"
if not path.exists():
chains_flag = "" if suffix == "total" else f" --chains {chain}"
raise DataNotFoundError(
dataset_name=f"DefiLlama TVL ({chain})",
path=path,
download_script=f"data/crypto/onchain/download.py --dataset defillama{chains_flag}",
readme="data/crypto/onchain/README.md",
)
return pl.read_parquet(path)
def load_coingecko_ohlcv(coin: str = "ethereum") -> pl.DataFrame:
"""Load daily prices/volume for one coin from CoinGecko.
Produced by `data/crypto/onchain/download.py --dataset coingecko`.
Free-tier window is 365 days; re-run the downloader to refresh.
Args:
coin: CoinGecko coin id (lowercase). Defaults to "ethereum".
Returns:
DataFrame with `timestamp` (Date), `price_usd`, `volume_usd`.
"""
path = ML4T_DATA_PATH / "crypto" / "onchain" / f"coingecko_{coin.lower()}.parquet"
if not path.exists():
raise DataNotFoundError(
dataset_name=f"CoinGecko OHLCV ({coin})",
path=path,
download_script=f"data/crypto/onchain/download.py --dataset coingecko --coins {coin}",
readme="data/crypto/onchain/README.md",
)
return pl.read_parquet(path)