盘中候选股排序的核心问题不是“怎么把公式写得更复杂”,而是如何稳定地拿到同一批股票的行情快照,并在本地用可解释的规则完成过滤和排序。
本文给出一个可复用的实现思路:先批量获取候选股实时行情,再用 Pandas 做字段校验、缺失值处理、过滤条件和排序打分。这样做的好处是接口调用层保持简单,策略规则层留在本地代码里,便于调试、回放和审计。
示例使用 quantdash==0.1.0 的实时行情接口作为数据来源。代码没有假设固定刷新频率、固定批量上限或固定延迟;实际盘中使用时,还需要根据权限、接口限流和交易所行情状态做运行层面的控制。
盘中候选股排序通常出现在这类任务里:
这类任务建议遵循三个原则。
第一,行情接口只负责取数,不把策略规则散落在多处接口调用参数里。第二,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 查询标的池;二者是两种查询方式,不建议混用。对于候选股排序,更常见的是传入明确的候选列表。
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())这里需要注意两个字段层面的事实:
symbol、last_price、prev_close、volume 等;ext.name、ext.change_pct 这类展开列名出现,实际使用前应打印列名确认。change_pct 在接口定义中按小数表示,例如 0.01 表示 1%,不要再除以 100。如果看板展示需要百分号,可以在展示层乘以 100。
盘中排序最容易出错的地方不是排序公式,而是字段不稳定、类型不统一、缺失值没有处理。下面的函数把实时行情快照整理成后续过滤所需的最小字段集。
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 去重,保留最后一条;本地过滤的目标不是预测未来,而是把明显不符合当前观察条件的标的排除掉。例如:
下面是一个偏工程化的过滤函数。阈值只是示例,不构成交易建议,实际应由研究口径决定。
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 过滤可以保留完整快照,方便后续复盘。
排序分数应尽量简单、可解释、可复现。下面的示例使用三个维度:
维度 | 含义 | 处理方式 |
|---|---|---|
涨跌幅排名 | 当前相对强弱 |
|
成交额或成交量排名 | 流动性 | 有 |
振幅排名 | 盘中活跃度 | 有 |
为了避免不同指标量纲不一致,先转成百分位排名,再加权合成分数。
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)这里没有使用复杂模型,而是选择了一个透明的排名方案。盘中看板和告警任务通常更需要稳定、可解释和可回放,而不是很难排查的黑箱分数。
把取数、清洗、过滤、排序组合起来,就可以形成一次盘中刷新。
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 仍是小数口径;如果要把上面的单次刷新放进盘中定时任务,还需要额外处理几个边界。
行情接口可能返回权限或限流相关错误。常见情况包括:
状态码 | 含义 | 处理建议 |
|---|---|---|
401 | API Key 缺失或无效 | 检查环境变量和密钥配置 |
403 | 当前权限不包含目标功能或市场 | 检查所需市场和功能权限 |
429 | 请求频率超限 | 若响应包含 |
不要在盘中任务里无限重试。更稳妥的做法是设置最大重试次数,并把失败批次写入日志。
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 和网络异常,避免对权限错误做无意义重试。
盘中排序比较的是一个刷新批次内的快照。如果行情结果中包含 timestamp 字段,可以保留下来用于排查数据是否来自明显不同的时间点。不要把 9:35、10:20、14:50 的结果混在一个排序表里直接比较。
如果要保存历史刷新结果,建议增加本地采集时间:
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 记录本地采集时间,展示层再转换为目标市场或用户所在时区。这样可以减少跨市场股票同时展示时的时区混乱。
按全市场标的池刷新很方便,但盘中排序未必需要每次都扫全市场。更常见的工程设计是:
这样可以降低接口压力,也能让排序结果更符合当前策略关注范围。
0.03 当成 0.03%在示例接口中,change_pct、amplitude、turnover_rate 这类比例字段按小数表示。0.03 表示 3%,展示时再乘以 100。过滤条件也应使用小数口径,例如 max_change_pct=0.08 表示涨幅不超过 8%。
只按涨跌幅排序很容易把成交稀疏、价格跳动异常的标的排到前面。至少应加入成交量或成交额过滤;如果有成交额字段,通常比单纯成交量更适合跨价格区间比较。
盘中看板只展示 Top N 是不够的。研究和排查时通常需要知道某个股票为什么被过滤掉。因此建议保存标准化后的完整快照,排序结果只是派生表。
本文讨论的是数据工程和本地排序实现,不涉及个性化投资建议。盘中排名只能说明某一套规则下的当前快照排序,不能直接代表未来收益。
可以来自自选股、行业股票池、盘前模型、历史 K 线筛选结果或人工维护列表。本文只处理“候选池已存在后,如何盘中批量刷新行情并排序”的问题。
symbols 还是 universes?如果已经有候选股列表,优先用 symbols。如果要拉取某个官方标的池,可以使用 universes。两种方式语义不同,实际调用时应二选一。
可以,但不建议一开始就复杂化。先使用涨跌幅、成交额或成交量、振幅这类容易解释的字段,确认数据质量和刷新链路稳定后,再加入更多研究因子。
刷新周期取决于看板用途、候选池大小、接口权限和限流约束。工程上应避免盲目高频刷新,并对 429 限流响应做退避处理。
本文方案只覆盖行情获取、Pandas 本地过滤和排序,不包含下单、账户、风控或实盘执行。任何交易系统都需要独立的权限控制、风控校验和合规评估。
盘中候选股排序可以拆成四步:批量取行情、标准化字段、本地过滤、可解释打分排序。接口层越简单,Pandas 规则层越清晰,后续调试和复盘就越容易。
在实现时尤其要注意三个细节:比例字段使用小数口径,过滤前先做字段和类型校验,排序结果必须绑定刷新时间或批次。这样得到的候选股排序表才适合作为盘中看板、告警或进一步研究的数据基础。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。