跳至正文
返回文库全部文档

分析中如何选择复权或未复权的ETF价格

代码 《交易机器学习》

总结

文档介绍如何加载 ETF 市场数据,并提供可选的代码和日期筛选,以及对返回代码数量的确定性限制。核心分析要点是根据测量对象选择价格序列:收益计算适合使用复权价格,而换手率筛选或按股佣金等金额和股数计算则需要未复权价格。

文档解释了为何在价格比率中相互抵消的复权因子,可能会扭曲以货币计价的数值。示例比较了某个较早历史日期的复权和实际交易 SPY 收盘价,说明随着复权累积,价差会扩大。加载器还会将时间戳标准化为日期,并按代码和时间对未复权结果排序。文档不介绍交易策略,也不评估数据质量;实用建议仅限于为计算选择合适的价格序列,并从本地存储获取数据。

核心观点

  • 复权价格适合计算收益,因为相同的复权因子会在价格比率中抵消。
  • 金额和股数计算需要未复权价格,包括换手率和按股成本计算。
  • 历史复权收盘价可能与实际成交价存在显著差异。
  • 加载器支持按代码和日期筛选,也可确定性地限制代码集合。

标签

全文
# loader.py


```py
"""ETF universe loader."""

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_etfs() -> list[str]:
    """List ETF symbols available in the local data store.

    Returns:
        Sorted list of ETF tickers (e.g., ``["ACWI", "AGG", ..., "XLK"]``).

    Raises:
        DataNotFoundError: If ``etfs/etf_universe.parquet`` is missing.

    Example:
        >>> list_etfs()[:3]
        ['ACWI', 'ACWX', 'AGG']
    """
    path = ML4T_DATA_PATH / "etfs" / "market" / "etf_universe.parquet"
    if not path.exists():
        raise DataNotFoundError(
            dataset_name="ETF Universe",
            path=path,
            download_script="data/etfs/market/download.py",
            readme="data/etfs/README.md",
        )
    return pl.scan_parquet(path).select("symbol").unique().collect().to_series().sort().to_list()


def load_etfs(
    symbols: list[str] | None = None,
    start_date: str | None = None,
    end_date: str | None = None,
    max_symbols: int = 0,
) -> pl.DataFrame:
    """Load ETF universe for momentum case study.

    Args:
        symbols: Optional list of symbols to filter (e.g., ["SPY", "QQQ"])
        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
    """
    path = ML4T_DATA_PATH / "etfs" / "market" / "etf_universe.parquet"
    if not path.exists():
        raise DataNotFoundError(
            dataset_name="ETF Universe",
            path=path,
            download_script="data/etfs/market/download.py",
            readme="data/etfs/README.md",
        )

    lf = pl.scan_parquet(path)
    ts_type = lf.collect_schema()["timestamp"]

    # Apply filters lazily (parquet pushdown / row-group pruning)
    if symbols:
        lf = lf.filter(pl.col("symbol").is_in(symbols))
    if start_date:
        lit = (
            pl.lit(start_date).str.to_date()
            if ts_type == pl.Date
            else pl.lit(start_date).str.to_datetime()
        )
        lf = lf.filter(pl.col("timestamp") >= lit)
    if end_date:
        lit = (
            pl.lit(end_date).str.to_date()
            if ts_type == pl.Date
            else pl.lit(end_date).str.to_datetime()
        )
        lf = lf.filter(pl.col("timestamp") <= lit)

    # Normalize daily data to Date type (post-filter so pushdown works on raw type)
    if ts_type != pl.Date:
        lf = lf.with_columns(pl.col("timestamp").cast(pl.Date))

    return apply_max_symbols(lf.collect(), max_symbols)


def load_etfs_unadjusted(
    symbols: list[str] | None = None,
    start_date: str | None = None,
    end_date: str | None = None,
) -> pl.DataFrame:
    """Load the traded close and share count, unadjusted for splits and distributions.

    :func:`load_etfs` returns an adjusted panel, which is what a return needs: the
    adjustment divides out distributions, and dividing both ends of a ratio by the same
    factor leaves the ratio alone. A dollar amount has no such cancellation. An adjusted
    close on an early session sits well below what the fund traded at - $87.23 against
    $126.70 for SPY on 2006-01-03 - so a turnover screen or a per-share commission read
    off it is wrong by the cumulative adjustment, and wrong by more the further back it
    looks.

    Use this series for anything denominated in dollars or in shares, and
    :func:`load_etfs` for anything denominated in returns.

    Args:
        symbols: Optional list of symbols to filter (e.g., ["SPY", "QQQ"])
        start_date: Optional start date (YYYY-MM-DD format)
        end_date: Optional end date (YYYY-MM-DD format)

    Returns:
        DataFrame with columns: timestamp, symbol, close, volume

    Raises:
        DataNotFoundError: If ``etfs/etf_universe_unadjusted.parquet`` is missing.
    """
    path = ML4T_DATA_PATH / "etfs" / "market" / "etf_universe_unadjusted.parquet"
    if not path.exists():
        raise DataNotFoundError(
            dataset_name="ETF Universe (unadjusted)",
            path=path,
            download_script="data/etfs/market/download.py",
            readme="data/etfs/README.md",
        )

    lf = pl.scan_parquet(path)
    ts_type = lf.collect_schema()["timestamp"]

    if symbols:
        lf = lf.filter(pl.col("symbol").is_in(symbols))
    if start_date:
        lit = (
            pl.lit(start_date).str.to_date()
            if ts_type == pl.Date
            else pl.lit(start_date).str.to_datetime()
        )
        lf = lf.filter(pl.col("timestamp") >= lit)
    if end_date:
        lit = (
            pl.lit(end_date).str.to_date()
            if ts_type == pl.Date
            else pl.lit(end_date).str.to_datetime()
        )
        lf = lf.filter(pl.col("timestamp") <= lit)

    if ts_type != pl.Date:
        lf = lf.with_columns(pl.col("timestamp").cast(pl.Date))

    return lf.collect().sort(["symbol", "timestamp"])

```

在遵守原作品许可的前提下,附作者信息全文展示。 许可协议: MIT

此摘要由 Stratmill 研究智能体根据原文撰写,并非原文副本。