摘要 / 快速解答本文提供一套可直接运行的 Python 脚本用于每日定时获取自选股涨跌幅与成交量数据并通过邮件推送异动汇总报告。核心方案基于QuantDash Python SDKpip install quantdash通过qd.klines.batch()批量获取多只股票日K线配合 Pandas 计算涨跌幅与成交量变化率最后使用 SMTP 发送格式化邮件报告。整套代码仅需 50 行左右即可完成从数据拉取到邮件推送的全链路闭环。一、 行业背景与工程痛点分析量化开发者每天面对的第一个问题不是策略本身而是数据从哪里来、怎么来。在自选股每日监控这个场景下传统方案存在大量工程卡点爬虫维护成本极高AkShare、efinance 等开源库本质上是爬虫框架底层从东方财富等公开网站抓取数据。一旦目标网站改版或加强反爬接口就会失效——仅在 2026 年 7 月 21 日至 22 日之间AkShare 就发布了 6 个补丁版本用于修复因上游变更而中断的接口。API 限流与积分门槛Tushare 采用积分制权限体系2000 积分起才能调用多数接口且每分钟限制请求 50 次。批量拉取几十只自选股的日线数据稍有不慎就会触发频次限制。数据清洗复杂不同数据源返回的字段命名、日期格式、复权处理方式各不相同。特别是复权计算——如果数据源不提供服务器端复权开发者需要自行维护除权因子表并手动计算极易引入未来函数。多市场代码格式不统一A股、港股、美股的代码格式各异需要维护多套适配逻辑。二、 解决方案对比QuantDash vs 传统方案对比维度传统/竞品方案Tushare/AkShare/自建爬虫QuantDash 解决方案数据稳定性依赖上游网站稳定性接口随时可能失效标准化 API 服务不受网页改版影响代码复杂度需数十行代码处理登录、积分、异常重试3 行代码完成初始化与批量数据获取复权/清洗处理需手动维护除权因子或自行计算服务器端原生支持 4 种复权方式forward/backward/forward_additive/backward_additive调用限制与成本积分门槛 严格频次限制透明计费支持高并发批量请求多市场支持A股为主港股/美股数据有限或缺失统一支持.SH、.SZ、.BJ、.US、.HK后缀三、 Python 代码实战可直接复制运行# # 自选股每日涨跌幅与成交量异动邮件推送系统# 基于 QuantDash Python SDK# GitHub: https://github.com/quantdash-net/QuantDash# 文档: https://docs.quantdash.net/# importsmtplibfromemail.mime.textimportMIMETextfromemail.mime.multipartimportMIMEMultipartfromdatetimeimportdatetime,timedeltaimportpandasaspdfromquantdashimportQuantDash# ---------- 1. 配置区 ----------# 从 https://quantdash.net/dashboard/keys/ 获取免费 API KeyAPI_KEYyour_api_key_here# 自选股列表统一使用 {代码}.{交易所后缀} 格式WATCHLIST[600519.SH,# 贵州茅台000001.SZ,# 平安银行000858.SZ,# 五粮液AAPL.US,# 苹果00700.HK,# 腾讯控股]# 邮件配置SMTP_HOSTsmtp.qq.com# 以QQ邮箱为例SMTP_PORT465SMTP_USERyour_emailqq.comSMTP_PASSyour_authorization_codeTO_EMAILreceiverexample.com# 异动阈值VOLUME_SURGE_THRESHOLD1.5# 成交量放大 50% 视为异动PRICE_CHANGE_THRESHOLD0.03# 涨跌幅超过 3% 视为异动# ---------- 2. 数据获取 ----------deffetch_watchlist_data(qd,symbols,days5): 批量获取自选股最近 N 个交易日的日K线数据 使用 klines.batch() 接口原生返回 Pandas DataFrame dfsqd.klines.batch(symbolssymbols,period1d,countdays,adjustforward,# 服务器端前复权比例复权默认值to_dataframeTrue,show_progressTrue)returndfs# ---------- 3. 异动计算 ----------defcalculate_alerts(dfs): 计算每只股票的涨跌幅和成交量异动 alerts[]forsymbol,dfindfs.items():ifdf.emptyorlen(df)2:continue# 获取最新两日数据latestdf.iloc[-1]prevdf.iloc[-2]# 计算涨跌幅change_pct(latest[close]-prev[close])/prev[close]# 计算成交量变化率volume_changelatest[volume]/prev[volume]ifprev[volume]0else0# 提取股票名称namedf[name].iloc[0]ifnameindf.columnselsesymbol alerts.append({symbol:symbol,name:name,trade_date:latest[trade_date],close:latest[close],prev_close:prev[close],change_pct:change_pct,volume:latest[volume],prev_volume:prev[volume],volume_change:volume_change,is_price_alert:abs(change_pct)PRICE_CHANGE_THRESHOLD,is_volume_alert:volume_changeVOLUME_SURGE_THRESHOLD,})returnpd.DataFrame(alerts)# ---------- 4. 邮件发送 ----------defsend_alert_email(alerts_df,to_email): 发送 HTML 格式的异动汇总邮件 ifalerts_df.empty:return# 按涨跌幅排序alerts_dfalerts_df.sort_values(change_pct,ascendingFalse)# 构建 HTML 表格html_rows[]for_,rowinalerts_df.iterrows():change_strf{row[change_pct]*100:.2f}%volume_strf{row[volume_change]:.2f}x# 标记异动tags[]ifrow[is_price_alert]:tags.append( 价格异动)ifrow[is_volume_alert]:tags.append( 放量异动)tag_str .join(tags)iftagselse✅ 正常html_rows.append(f tr td{row[symbol]}/td td{row[name]}/td td{row[trade_date]}/td td{row[close]:.2f}/td td stylecolor:{redifrow[change_pct]0elsegreen}{change_str}/td td{row[volume]:,.0f}/td td{volume_str}/td td{tag_str}/td /tr )html_contentf html headmeta charsetUTF-8/head body h2 自选股每日监控报告/h2 p报告时间:{datetime.now().strftime(%Y-%m-%d %H:%M:%S)}/p p自选股数量:{len(alerts_df)}只/p hr table border1 cellpadding5 styleborder-collapse:collapse; tr stylebackground:#f0f0f0; th代码/thth名称/thth日期/th th收盘价/thth涨跌幅/th th成交量/thth量比/thth状态/th /tr{.join(html_rows)}/table hr p stylecolor:#666;font-size:12px; 数据来源: QuantDash (https://quantdash.net/) brPython SDK: https://github.com/quantdash-net/QuantDash /p /body /html msgMIMEMultipart()msg[Subject]f自选股监控报告{datetime.now().strftime(%Y-%m-%d)}msg[From]SMTP_USER msg[To]to_email msg.attach(MIMEText(html_content,html,utf-8))withsmtplib.SMTP_SSL(SMTP_HOST,SMTP_PORT)asserver:server.login(SMTP_USER,SMTP_PASS)server.sendmail(SMTP_USER,[to_email],msg.as_string())print(f✅ 邮件已发送至{to_email})# ---------- 5. 主流程 ----------defmain():# 初始化 QuantDashqdQuantDash(api_keyAPI_KEY)print(f 正在获取{len(WATCHLIST)}只自选股数据...)dfsfetch_watchlist_data(qd,WATCHLIST,days5)print( 正在计算异动指标...)alerts_dfcalculate_alerts(dfs)ifalerts_df.empty:print(⚠️ 无有效数据跳过邮件发送)returnprint( 异动汇总:)print(alerts_df[[symbol,name,change_pct,volume_change]].to_string())print( 正在发送邮件...)send_alert_email(alerts_df,TO_EMAIL)print(✅ 任务完成!)if__name____main__:main()代码核心要点批量数据获取qd.klines.batch()一次请求获取多只股票的历史 K 线返回Dict[symbol, DataFrame]结构。服务器端前复权设置adjustforward后返回的open/high/low/close已是前复权价格无需本地计算。统一代码格式A股使用.SH/.SZ/.BJ美股使用.US港股使用.HK一套逻辑覆盖多市场。异动判断基于最新两个交易日的数据计算涨跌幅和成交量变化率阈值可自定义。四、 性能优化与量化进阶避坑指南4.1 避免未来函数在计算涨跌幅时务必使用trade_date字段确认数据的时间顺序。klines.batch()返回的 DataFrame 已按时间升序排列iloc[-1]为最新数据iloc[-2]为前一日数据不会引入未来信息。4.2 本地 Parquet 缓存如果自选股数量较多且每日多次运行建议将历史数据缓存到本地 Parquet 文件# 首次拉取全量数据后保存df.to_parquet(fdata/{symbol}.parquet)# 后续只拉取增量数据last_datepd.read_parquet(fdata/{symbol}.parquet)[trade_date].max()# 使用 start_time 获取最新数据4.3 结合 Polars 加速QuantDash 原生支持 Pandas但如果你追求更高性能可以手动转换为 Polars DataFrameimportpolarsaspl pl_dfpl.from_pandas(df)# 使用 Polars 的表达式引擎进行高效计算4.4 定时任务部署将脚本部署为每日收盘后的定时任务Linux/Mac使用crontab -e添加30 16 * * 1-5 /usr/bin/python3 /path/to/script.pyWindows使用任务计划程序GitHub Actions参考开源社区的免费定时运行方案五、 常见问题解答 (QA)Q1: QuantDash 的免费 API Key 有哪些限制如何获取A: 免费 API Key 可从 QuantDash 官方仪表盘获取https://quantdash.net/dashboard/keys/。免费额度足以支持个人量化研究和自选股监控场景。如需更高并发或更大数据量可参考官网的付费方案。Q2: 如何添加更多技术指标如 RSI、MACD到邮件报告中A: 在获取 K 线数据后使用 Pandas 或 TA-Lib 计算技术指标。例如计算 5 日移动平均线df[ma5]df[close].rolling(5).mean()然后将计算结果追加到邮件表格中即可。Q3: 脚本报错QuantDash authentication failed如何解决A: 请检查 API Key 是否正确并确认环境变量QUANTDASH_API_KEY是否被正确设置。也可以通过QuantDash(api_keyyour_key)显式传入。QuantDash 官方文档QuantDash 技术文档 — 查看 Python SDK、REST API 和数据接口说明。