首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >盘中候选股如何批量取行情并用 Pandas 本地排序

盘中候选股如何批量取行情并用 Pandas 本地排序

原创
作者头像
用户9138916
发布2026-09-19 12:08:52
发布2026-09-19 12:08:52
1230
举报

盘中候选股排序的核心问题不是“怎么把公式写得更复杂”,而是如何稳定地拿到同一批股票的行情快照,并在本地用可解释的规则完成过滤和排序。

本文给出一个可复用的实现思路:先批量获取候选股实时行情,再用 Pandas 做字段校验、缺失值处理、过滤条件和排序打分。这样做的好处是接口调用层保持简单,策略规则层留在本地代码里,便于调试、回放和审计。

示例使用 quantdash==0.1.0 的实时行情接口作为数据来源。代码没有假设固定刷新频率、固定批量上限或固定延迟;实际盘中使用时,还需要根据权限、接口限流和交易所行情状态做运行层面的控制。

适用场景和基本原则

盘中候选股排序通常出现在这类任务里:

  • 已经有一个候选池,例如自选股、行业成分股、昨日涨停池、模型初筛结果;
  • 盘中定时刷新这些标的的最新价、涨跌幅、成交量或成交额;
  • 在本地根据流动性、涨跌幅区间、价格位置等规则筛掉不合适的股票;
  • 对剩余股票打分排序,输出 Top N 给看板、告警或后续研究模块。

这类任务建议遵循三个原则。

第一,行情接口只负责取数,不把策略规则散落在多处接口调用参数里。第二,Pandas 本地过滤必须先做字段校验和单位处理,尤其是 change_pct 这类比例字段。第三,排序结果必须带上时间戳或刷新批次,避免把不同时间点的快照混在一起比较。

运行环境与依赖

示例环境建议如下:

组件

建议版本

Python

3.10 或更高版本

pandas

2.x

quantdash

0.1.0

安装依赖时可使用:pip install pandas quantdash==0.1.0

QuantDash SDK 支持从环境变量读取 API Key,变量名为 QUANTDASH_API_KEY。也可以在初始化时传入 api_key="YOUR_API_KEY",但不建议把真实密钥写进代码仓库。

第一步:批量获取候选股行情快照

假设候选池已经由其他模块生成,这里只关心行情刷新和排序。QuantDash 的实时行情接口可以按 symbols 批量查询,也可以按 universes 查询标的池;二者是两种查询方式,不建议混用。对于候选股排序,更常见的是传入明确的候选列表。

代码语言:python
复制
from quantdash import QuantDash

qd = QuantDash()  # 默认读取环境变量 QUANTDASH_API_KEY

symbols = [
    "600519.SH",
    "000001.SZ",
    "00700.HK",
    "AAPL.US",
]

quotes = qd.quotes.get(
    symbols=symbols,
    to_dataframe=True,
)

print(quotes.columns.tolist())
print(quotes.head())

这里需要注意两个字段层面的事实:

  • 行情基础字段通常包括 symbollast_priceprev_closevolume 等;
  • 扩展字段可能以 ext.nameext.change_pct 这类展开列名出现,实际使用前应打印列名确认。

change_pct 在接口定义中按小数表示,例如 0.01 表示 1%,不要再除以 100。如果看板展示需要百分号,可以在展示层乘以 100。

第二步:把行情 DataFrame 标准化

盘中排序最容易出错的地方不是排序公式,而是字段不稳定、类型不统一、缺失值没有处理。下面的函数把实时行情快照整理成后续过滤所需的最小字段集。

代码语言:python
复制
import pandas as pd


def normalize_quotes(df: pd.DataFrame) -> pd.DataFrame:
    """
    将实时行情快照整理成候选股排序需要的字段。
    该函数不假设所有扩展字段一定存在。
    """
    if df is None or df.empty:
        return pd.DataFrame()

    data = df.copy()

    required = ["symbol", "last_price", "prev_close", "volume"]
    missing = [col for col in required if col not in data.columns]
    if missing:
        raise ValueError(f"行情数据缺少必要字段: {missing}")

    rename_map = {}
    if "ext.name" in data.columns:
        rename_map["ext.name"] = "name"
    if "ext.change_pct" in data.columns:
        rename_map["ext.change_pct"] = "change_pct"
    if "ext.turnover_rate" in data.columns:
        rename_map["ext.turnover_rate"] = "turnover_rate"
    if "ext.amplitude" in data.columns:
        rename_map["ext.amplitude"] = "amplitude"

    data = data.rename(columns=rename_map)

    numeric_cols = [
        "last_price",
        "prev_close",
        "open",
        "high",
        "low",
        "volume",
        "amount",
        "change_pct",
        "turnover_rate",
        "amplitude",
    ]
    for col in numeric_cols:
        if col in data.columns:
            data[col] = pd.to_numeric(data[col], errors="coerce")

    data = data.drop_duplicates(subset=["symbol"], keep="last")

    if "change_pct" not in data.columns:
        valid = data["prev_close"].notna() & (data["prev_close"] != 0)
        data.loc[valid, "change_pct"] = (
            data.loc[valid, "last_price"] / data.loc[valid, "prev_close"] - 1
        )

    keep_cols = [
        "symbol",
        "name",
        "last_price",
        "prev_close",
        "change_pct",
        "volume",
        "amount",
        "turnover_rate",
        "amplitude",
        "timestamp",
    ]
    keep_cols = [col for col in keep_cols if col in data.columns]

    return data[keep_cols].reset_index(drop=True)

这个标准化过程做了几件关键事情:

  • 先检查必需字段,不让后续排序在缺字段时静默失败;
  • 将价格、成交量、涨跌幅等列统一转为数值类型;
  • 如果没有扩展涨跌幅字段,则用 last_price / prev_close - 1 本地计算;
  • 对重复 symbol 去重,保留最后一条;
  • 只保留排序阶段真正需要的字段。

第三步:用 Pandas 做本地过滤

本地过滤的目标不是预测未来,而是把明显不符合当前观察条件的标的排除掉。例如:

  • 最新价必须有效;
  • 昨收价必须大于 0;
  • 涨跌幅位于一个可接受区间;
  • 成交量或成交额达到最低流动性要求;
  • 可选地使用换手率、振幅等扩展字段。

下面是一个偏工程化的过滤函数。阈值只是示例,不构成交易建议,实际应由研究口径决定。

代码语言:python
复制
def filter_candidates(
    quotes: pd.DataFrame,
    min_volume: float = 100_000,
    min_amount: float | None = None,
    min_change_pct: float = -0.03,
    max_change_pct: float = 0.08,
) -> pd.DataFrame:
    """
    对候选股行情做本地过滤。
    change_pct 使用小数口径,例如 0.03 表示 3%。
    """
    if quotes is None or quotes.empty:
        return pd.DataFrame()

    data = quotes.copy()

    mask = (
        data["symbol"].notna()
        & data["last_price"].notna()
        & data["prev_close"].notna()
        & (data["prev_close"] > 0)
        & data["change_pct"].notna()
        & data["change_pct"].between(min_change_pct, max_change_pct)
        & data["volume"].fillna(0).ge(min_volume)
    )

    if min_amount is not None and "amount" in data.columns:
        mask = mask & data["amount"].fillna(0).ge(min_amount)

    return data.loc[mask].reset_index(drop=True)

为什么建议在本地过滤?因为盘中候选股规则经常调整,例如今天看涨跌幅区间,明天又想叠加成交额、换手率或振幅。如果每次都把规则强绑定在远端查询层,调试成本会更高。本地 DataFrame 过滤可以保留完整快照,方便后续复盘。

第四步:设计一个可解释的排序分数

排序分数应尽量简单、可解释、可复现。下面的示例使用三个维度:

维度

含义

处理方式

涨跌幅排名

当前相对强弱

change_pct 越高排名越靠前

成交额或成交量排名

流动性

amountamount,否则用 volume

振幅排名

盘中活跃度

amplitude 时参与打分

为了避免不同指标量纲不一致,先转成百分位排名,再加权合成分数。

代码语言:python
复制
def rank_candidates(
    candidates: pd.DataFrame,
    top_n: int = 20,
    weight_change: float = 0.5,
    weight_liquidity: float = 0.4,
    weight_activity: float = 0.1,
) -> pd.DataFrame:
    """
    对过滤后的候选股打分排序。
    分数越高,排序越靠前。
    """
    if candidates is None or candidates.empty:
        return pd.DataFrame()

    data = candidates.copy()

    liquidity_col = "amount" if "amount" in data.columns else "volume"

    data["rank_change"] = data["change_pct"].rank(pct=True, method="average")
    data["rank_liquidity"] = data[liquidity_col].rank(pct=True, method="average")

    if "amplitude" in data.columns:
        data["rank_activity"] = data["amplitude"].rank(pct=True, method="average")
    else:
        data["rank_activity"] = 0.0
        weight_activity = 0.0

    total_weight = weight_change + weight_liquidity + weight_activity
    if total_weight <= 0:
        raise ValueError("排序权重之和必须大于 0")

    data["score"] = (
        weight_change * data["rank_change"]
        + weight_liquidity * data["rank_liquidity"]
        + weight_activity * data["rank_activity"]
    ) / total_weight

    data["change_pct_display"] = data["change_pct"] * 100

    sort_cols = ["score", "change_pct", liquidity_col]
    data = data.sort_values(sort_cols, ascending=[False, False, False])

    display_cols = [
        "symbol",
        "name",
        "last_price",
        "change_pct_display",
        liquidity_col,
        "score",
    ]
    display_cols = [col for col in display_cols if col in data.columns]

    return data[display_cols].head(top_n).reset_index(drop=True)

这里没有使用复杂模型,而是选择了一个透明的排名方案。盘中看板和告警任务通常更需要稳定、可解释和可回放,而不是很难排查的黑箱分数。

第五步:串起来形成一次刷新任务

把取数、清洗、过滤、排序组合起来,就可以形成一次盘中刷新。

代码语言:python
复制
from quantdash import QuantDash
import pandas as pd


def refresh_intraday_ranking(symbols: list[str], top_n: int = 20) -> pd.DataFrame:
    qd = QuantDash()

    raw_quotes = qd.quotes.get(
        symbols=symbols,
        to_dataframe=True,
    )

    normalized = normalize_quotes(raw_quotes)

    filtered = filter_candidates(
        normalized,
        min_volume=100_000,
        min_amount=None,
        min_change_pct=-0.03,
        max_change_pct=0.08,
    )

    ranked = rank_candidates(filtered, top_n=top_n)

    return ranked


if __name__ == "__main__":
    candidate_symbols = [
        "600519.SH",
        "000001.SZ",
        "00700.HK",
        "AAPL.US",
    ]

    result = refresh_intraday_ranking(candidate_symbols, top_n=10)
    print(result)

这段代码的验证重点不是某一次输出的 Top N 是否“正确”,而是以下条件是否成立:

  • 返回结果只包含候选池中的标的;
  • change_pct_display 是百分比展示值,而内部 change_pct 仍是小数口径;
  • 成交量或成交额过滤确实生效;
  • 分数相同或接近时,有明确的二级排序字段;
  • 任何缺字段、空结果、非数值字段都不会被静默吞掉。

盘中循环刷新时要处理的工程边界

如果要把上面的单次刷新放进盘中定时任务,还需要额外处理几个边界。

1. 限流与错误码

行情接口可能返回权限或限流相关错误。常见情况包括:

状态码

含义

处理建议

401

API Key 缺失或无效

检查环境变量和密钥配置

403

当前权限不包含目标功能或市场

检查所需市场和功能权限

429

请求频率超限

若响应包含 retry_after_ms,按毫秒等待后重试

不要在盘中任务里无限重试。更稳妥的做法是设置最大重试次数,并把失败批次写入日志。

代码语言:python
复制
import time


def safe_fetch_quotes(qd: QuantDash, symbols: list[str], max_retries: int = 3) -> pd.DataFrame:
    last_error = None

    for attempt in range(max_retries):
        try:
            return qd.quotes.get(symbols=symbols, to_dataframe=True)
        except Exception as exc:
            last_error = exc
            sleep_seconds = min(2 ** attempt, 10)
            time.sleep(sleep_seconds)

    raise RuntimeError("多次获取行情失败") from last_error

上面的重试代码是通用保护层,不代表 SDK 内置固定重试策略。真实项目中还应区分 401、403、429 和网络异常,避免对权限错误做无意义重试。

2. 时间戳与快照一致性

盘中排序比较的是一个刷新批次内的快照。如果行情结果中包含 timestamp 字段,可以保留下来用于排查数据是否来自明显不同的时间点。不要把 9:35、10:20、14:50 的结果混在一个排序表里直接比较。

如果要保存历史刷新结果,建议增加本地采集时间:

代码语言:python
复制
from datetime import datetime, timezone


def add_local_snapshot_time(df: pd.DataFrame) -> pd.DataFrame:
    data = df.copy()
    data["snapshot_collected_at"] = datetime.now(timezone.utc)
    return data

这里使用 UTC 记录本地采集时间,展示层再转换为目标市场或用户所在时区。这样可以减少跨市场股票同时展示时的时区混乱。

3. 候选池不要无限扩大

按全市场标的池刷新很方便,但盘中排序未必需要每次都扫全市场。更常见的工程设计是:

  • 盘前或低频任务生成候选池;
  • 盘中只刷新候选池行情;
  • 每隔较长时间重新生成一次候选池;
  • 排序结果只作为研究或看板信号,不直接等同于交易指令。

这样可以降低接口压力,也能让排序结果更符合当前策略关注范围。

常见错误

错误一:把 0.03 当成 0.03%

在示例接口中,change_pctamplitudeturnover_rate 这类比例字段按小数表示。0.03 表示 3%,展示时再乘以 100。过滤条件也应使用小数口径,例如 max_change_pct=0.08 表示涨幅不超过 8%

错误二:只按涨幅排序,忽略流动性

只按涨跌幅排序很容易把成交稀疏、价格跳动异常的标的排到前面。至少应加入成交量或成交额过滤;如果有成交额字段,通常比单纯成交量更适合跨价格区间比较。

错误三:没有保留原始快照

盘中看板只展示 Top N 是不够的。研究和排查时通常需要知道某个股票为什么被过滤掉。因此建议保存标准化后的完整快照,排序结果只是派生表。

错误四:把排序结果当成交易建议

本文讨论的是数据工程和本地排序实现,不涉及个性化投资建议。盘中排名只能说明某一套规则下的当前快照排序,不能直接代表未来收益。

FAQ

1. 候选股列表应该从哪里来?

可以来自自选股、行业股票池、盘前模型、历史 K 线筛选结果或人工维护列表。本文只处理“候选池已存在后,如何盘中批量刷新行情并排序”的问题。

2. 应该用 symbols 还是 universes

如果已经有候选股列表,优先用 symbols。如果要拉取某个官方标的池,可以使用 universes。两种方式语义不同,实际调用时应二选一。

3. 排序分数要不要加入更多因子?

可以,但不建议一开始就复杂化。先使用涨跌幅、成交额或成交量、振幅这类容易解释的字段,确认数据质量和刷新链路稳定后,再加入更多研究因子。

4. 多久刷新一次合适?

刷新周期取决于看板用途、候选池大小、接口权限和限流约束。工程上应避免盲目高频刷新,并对 429 限流响应做退避处理。

5. 能不能直接把排序结果接到自动交易?

本文方案只覆盖行情获取、Pandas 本地过滤和排序,不包含下单、账户、风控或实盘执行。任何交易系统都需要独立的权限控制、风控校验和合规评估。

小结

盘中候选股排序可以拆成四步:批量取行情、标准化字段、本地过滤、可解释打分排序。接口层越简单,Pandas 规则层越清晰,后续调试和复盘就越容易。

在实现时尤其要注意三个细节:比例字段使用小数口径,过滤前先做字段和类型校验,排序结果必须绑定刷新时间或批次。这样得到的候选股排序表才适合作为盘中看板、告警或进一步研究的数据基础。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 适用场景和基本原则
  • 运行环境与依赖
  • 第一步:批量获取候选股行情快照
  • 第二步:把行情 DataFrame 标准化
  • 第三步:用 Pandas 做本地过滤
  • 第四步:设计一个可解释的排序分数
  • 第五步:串起来形成一次刷新任务
  • 盘中循环刷新时要处理的工程边界
    • 1. 限流与错误码
    • 2. 时间戳与快照一致性
    • 3. 候选池不要无限扩大
  • 常见错误
    • 错误一:把 0.03 当成 0.03%
    • 错误二:只按涨幅排序,忽略流动性
    • 错误三:没有保留原始快照
    • 错误四:把排序结果当成交易建议
  • FAQ
    • 1. 候选股列表应该从哪里来?
    • 2. 应该用 symbols 还是 universes?
    • 3. 排序分数要不要加入更多因子?
    • 4. 多久刷新一次合适?
    • 5. 能不能直接把排序结果接到自动交易?
  • 小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档