基于AI大模型的A股自选股分析系统:全流程实现与部署指南 简介金融数据分析中利用AI大模型自动处理海量行情数据已成为提升决策效率的关键路径。本文从技术指标计算与结构化信号提取的通用原理出发介绍如何基于Python和AKShare构建数据管道并借助大模型API生成盘面解读。通过设计反幻觉Prompt与多级分析策略系统将行情指标转化为可操作的决策仪表盘HTML报告并支持企业微信、飞书、邮箱等多渠道推送实现从数据获取、指标计算、AI分析到报告分发的完整自动化闭环。同时文章还涵盖了定时调度、容器化部署等工程实践细节帮助读者快速搭建一套可自动运行的A股自选股分析工具为个人投资决策提供结构化信息支撑。 先说明一下我最近手头一直在跑一个自用的A股自选股分析项目基于AI大模型每天收盘后自动拉数据、算指标、调用大模型做解读最后生成一份「决策仪表盘」HTML报告推送到企业微信和邮箱。朋友看到后一直在问能不能把源码和部署过程整理出来正好这次把整个思路、核心代码和踩坑记录都写清楚。无论你是想自己搭一套盘中/盘后分析工具还是单纯想研究“大模型金融数据”怎么落地这篇内容应该都能给你省不少时间。整个项目技术栈不复杂Python 3.10、AKShare/Tushare拉行情、Pandas算技术指标、大模型API做文本解读、HTML模板生成报告、企业微信/飞书/邮箱做推送通道。难的不是某个环节而是怎么把这几个环节串成一个每天自动跑的流水线并且保证输出稳定、不报错、不幻觉。下面从设计思路开始讲。1. 项目整体设计与模块拆解1.1 核心需求从“手动看盘”到“自动生成决策参考”做这个系统的起因很简单手动盯盘太累而且容易漏信息。每天收盘后我需要花大量时间翻自选股行情、看技术指标、读公告和新闻再凭感觉判断明天怎么走。这个过程中有两个痛点一是重复劳动多二是信息过载导致决策质量下降。所以这个系统要解决的核心问题就三个自动拉取自选股的行情、资金流向、技术指标数据用AI大模型对多维度数据做阅读理解生成“人话”版的盘面解读和风险提示把数据、指标、解读汇总成一份结构化报告推送到微信、飞书、邮箱随时随地能看。说白了就是把我以前每天收盘后做的事交给一个Python定时任务加一个大模型API去完成。系统本身不预测涨跌它做的是“信息压缩”和“结构化呈现”把几十只股票的几百个数据点压成一张仪表盘、几段关键结论。1.2 整体架构数据层、分析层、推送层三层分离系统设计上我刻意做了三层拆分这是整个项目最核心的架构决策层级职责核心组件数据层获取行情、资金、财务数据AKShare / Tushare / Baostock分析层计算技术指标、生成AI解读、产出HTML报告Pandas、TA-Lib、大模型API、Jinja2模板推送层将报告分发到各终端企业微信机器人、飞书机器人、SMTP邮箱这个拆法的好处很明显数据源接口变了只需改数据层大模型厂商换了只需改分析层推送渠道新增了只需在推送层加一个类。每一层之间通过标准化的数据结构交互不至于牵一发动全身。另外在调度上我用的是系统自带的crontab加APScheduler双保险避免单点失效。主流程是每天14:50触发一次盘中快照15:10触发一次收盘分析A股15:00收盘后数据才稳定19:00再触发一次晚间新闻补充分析。这个时间点设计是在实盘中调出来的后面专门讲。1.3 技术选型为什么是Python AKShare 大模型API技术选型这里我直接给出对比结论省得大家再踩一遍坑。Python是金融数据分析的事实标准Pandas处理表格数据、requests拉接口、Jinja2渲染报告生态太成熟了。为什么不选Node.js或Go不是说不能用而是Python在数据清洗和可视化这块的库积累目前还是最省事的。对于这种“数据获取—处理—生成报告”的典型批处理任务Python的代码量和调试成本最低。数据源方面我对比了三个AKShare完全免费、开源、接口丰富A股行情/资金/公告都能拿缺点是接口变动频繁偶尔需要跟着升级Tushare Pro数据质量高、稳定性好但部分接口需要积分积分又要充值或做任务个人用户门槛略高Baostock免费稳定但接口相对少只覆盖基础行情不适合需要资金流、龙虎榜等场景的。最终我主用AKShareTushare做备用数据源。原因很简单这个系统最怕的是数据源挂了导致整条链路中断AKShare免费且更新频率高社区活跃遇到接口变动也好查文档。大模型这块我的选择标准是首先必须支持OpenAI兼容的接口格式这样代码里只需要改base_url和api_key就能切换模型其次上下文长度要够因为一条分析任务要把多只股票的行情摘要拼进去最少也要8K最后是成本要可控。目前DeepSeek-V3、通义千问qwen-plus、Kimi这些国内模型都能满足我用DeepSeek跑主力任务qwen做备选。关键点是大模型在这个系统里不是用来“算命”的而是用来做“结构化归纳与报告生成”的所以对模型能力的要求集中在文本压缩和格式遵循能力上不需要它有多深的金融知识。2. 核心功能模块的实现细节2.1 自选股配置用一份JSON管住所有股票整个系统的入口是一份自选股配置文件我放在项目根目录的watchlist.json里。格式很简单{ stocks: [ {code: 600519, name: 贵州茅台, market: SH, reason: 白酒龙头观察消费复苏}, {code: 000858, name: 五粮液, market: SZ, reason: 白酒二龙头估值观察}, {code: 300750, name: 宁德时代, market: SZ, reason: 新能源电池龙头} ], risk_level: conservative, max_holding_days: 20 }每个股票条目里的reason字段很重要它是给大模型看的“关注逻辑”比如“白酒龙头观察消费复苏”。在生成AI解读时这个字段会作为上下文拼进Prompt让模型知道你为什么关注这只票从而给出更有针对性的分析而不是泛泛而谈。risk_level和max_holding_days是给报告里的建议模块用的。比如你是稳健型投资者大模型在给出操作倾向时会更保守强调仓位控制和止损纪律。这个参数在Prompt里会体现为系统级指令而不是让AI自由发挥。这里有一个踩过的坑千万不要用股票名称做唯一标识一定要用代码市场前缀。原因很简单A股存在不同市场代码重复的情况比如600开头的沪市和000开头的深市完全不一样但名称可能相近。用600519SH这样的组合键保证在拼接行情数据、生成图表路径时不会串数据。2.2 行情数据获取AKShare接口的封装与容错行情获取是整个系统的地基这一层如果有问题后面全白搭。我对AKShare的调用做了统一封装核心思路是“接口隔离异常降级数据校验”。先说接口隔离。AKShare的接口名称经常变比如历史行情接口从stock_zh_a_hist改成过stock_zh_a_hist_tx如果不做一层封装你改接口的时候要动全项目。所以我定义了一个DataFetcher类所有业务代码只跟这个类打交道class DataFetcher: def get_daily_kline(self, code: str, market: str, days: int 120) - pd.DataFrame: symbol self._to_akshare_symbol(code, market) df ak.stock_zh_a_hist(symbolsymbol, perioddaily, adjustqfq) return self._normalize(df) def get_realtime_quote(self, code: str, market: str) - dict: ... def get_capital_flow(self, code: str, market: str) - pd.DataFrame: ..._to_akshare_symbol负责把统一的code market格式转成AKShare需要的格式比如600519 SH要转成sh600519000858 SZ要转成sz000858。这个映射关系写错API会直接返回空数据还不会报错很隐蔽。再说异常降级。AKShare偶尔会因为网络问题、对方服务器波动、接口限频而抛异常所以每个方法里都套了重试和降级逻辑def _safe_call(self, func, *args, **kwargs): for attempt in range(3): try: return func(*args, **kwargs) except Exception as e: logger.warning(f调用失败第{attempt 1}次重试: {e}) time.sleep(2 ** attempt) # 指数退避 # 全部失败后返回空DataFrame并在报告中标红 logger.error(f数据获取失败: {func}, {args}) return pd.DataFrame()这里的退避策略是重点第一次失败等2秒第二次等4秒第三次就直接放弃返回空的DataFrame。要注意不要无限重试否则整个定时任务会卡在数据获取阶段导致后续环节全部延迟。数据校验也很关键。我遇到过AKShare返回的数据里某只股票连续两天停牌K线数据直接缺行。所以拿到数据后我会检查行数是否足够至少要有60个交易日否则技术指标计算会失真、是否有明显的价格异常比如最新价是0或者负值。校验不通过的股票会被标记为“数据异常”在报告中单独列出而不是混进正常分析里干扰模型判断。2.3 技术指标计算MACD/KDJ/RSI/均线系统的实现在调用大模型之前需要先把原始行情数据加工成技术指标。我用了TA-Lib库来算但这里有个大坑TA-Lib的安装非常折腾Windows上经常编译失败。所以我的建议是先用纯Pandas实现一套指标计算这也是项目源码默认的方式可以在任何环境直接跑起来。我封装了一个TechnicalIndicators类class TechnicalIndicators: staticmethod def add_macd(df: pd.DataFrame, fast12, slow26, signal9) - pd.DataFrame: df[ema_fast] df[close].ewm(spanfast, adjustFalse).mean() df[ema_slow] df[close].ewm(spanslow, adjustFalse).mean() df[dif] df[ema_fast] - df[ema_slow] df[dea] df[dif].ewm(spansignal, adjustFalse).mean() df[macd] (df[dif] - df[dea]) * 2 return dfMACD的核心逻辑是计算快线12日EMA和慢线26日EMA的差离值DIF再用DIF的9日EMA作为信号线DEA最后用(DIF-DEA)*2得到柱状图。这里乘2是为了跟国内股票软件的显示习惯对齐否则柱状图数值会跟你在行情软件里看到的对不上。策略信号我组合了几组常用判断均线系统5日、10日、20日、60日均线的多头/空头排列MACDDIF是否上穿DEA金叉/死叉柱状图是否在放量KDJK值、D值、J值特别是超买超卖区间J值100为超买0为超卖RSI6日、14日RSI是否进入超买70或超卖30区域成交量当日成交量 vs 5日均量的比值判断放量/缩量。每个信号我都会输出结构化数据存储成JSON作为大模型的输入。结构大概是{ code: 600519, name: 贵州茅台, signals: { ma: 多头排列, macd: 金叉, kdj: 超买, rsi_14: 68.5, volume_ratio: 1.35 }, latest_price: 1688.0, change_pct: 2.15 }这样设计的好处是大模型拿到的是已经清洗好的、结构化的信号描述而不是让它去从一堆数字里自己找规律能显著降低AI幻觉的概率。如果你直接把1000多行数字扔给模型它很容易编造出根本不存在的“趋势线突破”之类的内容。2.4 AI大模型接入Prompt设计与JSON结构化输出大模型接入这块是整个项目里最需要调优的部分下面重点展开。2.4.1 接口适配层兼容多家模型厂商因为国内大模型厂商的API基本都兼容OpenAI格式所以我基于openaiPython SDK做了统一适配from openai import OpenAI class LLMClient: def __init__(self, provider: str, api_key: str, model: str, base_url: str): self.client OpenAI(api_keyapi_key, base_urlbase_url) self.model model def analyze(self, system_prompt: str, user_content: str, temperature: float 0.3) - dict: resp self.client.chat.completions.create( modelself.model, messages[ {role: system, content: system_prompt}, {role: user, content: user_content} ], temperaturetemperature, response_format{type: json_object} # 强制JSON输出 ) return json.loads(resp.choices[0].message.content)配置上放在config.yaml里llm: provider: deepseek api_key: sk-xxx model: deepseek-chat base_url: https://api.deepseek.com你如果想切到通义千问只需要改provider为qwenbase_url改为https://dashscope.aliyuncs.com/compatible-mode/v1model改成qwen-plus其他代码不用动。temperature参数我调到了0.3这是反复试验出来的temperature太高比如0.8以上模型输出的风格会更加“创作化”容易偏离事实一本正经地胡扯太低比如0则可能输出过于死板缺少必要的“分析感”。0.2~0.4这个区间目前是我觉得最适合金融分析场景的稳定性和可读性兼顾。2.4.2 让大模型“自知之明”反幻觉设计这应该是这个项目里最有价值的一部分设计。大模型在处理金融数据时特别容易出现“AI幻觉”——就是模型一本正经地编造它并不知道的信息比如错误的涨跌幅、编造的新闻事件、甚至是虚构的上市公司公告。这种幻觉放到投资决策场景里是要出事的。所以我在Prompt里加入了几个强制约束你是一名A股市场分析助理。请注意以下铁律 1. 你只能基于用户提供的数据进行分析禁止编造任何数据、新闻、公告或事实。 2. 如果提供的数据不足以支撑某个判断你必须明确回答“数据不足无法判断”。 3. 禁止给出绝对化的涨跌预测例如“明天必涨”“一定突破XX元”。可以给出情景分析但要注明概率和前提条件。 4. 所有建议仅为技术交流参考不构成投资建议。 5. 输出必须为JSON格式字段包括summary、signals、news_impact、risk_tips、scenario_analysis、operation_suggestion。这里的核心是两条一是“数据不足时必须承认”二是“禁止绝对化预测”。前者直接缓解了模型编造数据的冲动后者让它在输出建议时更谨慎从根源上降低误导风险。2.4.3 分段分析 汇总分析的两级Prompt策略一开始我尝试让大模型一次性分析全部自选股结果输出质量很差因为股票多每个股票的上下文会被截断模型只能概括性说两句毫无参考价值。后来改成“先个股分析、后汇总分析”的两级方案第一级每只股票单独调用一次大模型输入该股的行情摘要、技术信号、近期消息面输出个股结构化分析字段固定第二级把全部个股的分析结果拼接起来让大模型做一次横向对比找出值得重点关注的方向和风格切换的信号。注意个股分析并行的上限根据模型限流情况调整——DeepSeek的并发限制比较紧实测下来一次分析10只股票串行大约要1-2分钟并行反而容易触发限流。所以最终我选择了串行指数退避重试。这里还有一个细节大模型的输入token是有上限的如果自选股数量很多比如50只第一级分析结果的拼接会超过上下文长度。我的做法是对分析结果做“再压缩”只保留每只股票的summary、risk_tips、operation_suggestion三个字段参与汇总分析其余字段留在个股报告里单独展示。2.5 决策仪表盘生成HTML模板 自动化图表报告生成是决定“有没有人愿意看”的关键。我见过很多项目数据很全但整个页面跟Excel截图一样完全没有可读性。我在这个项目里做的是用Jinja2模板渲染HTML配合ECharts做交互图表生成的是一个单文件HTML不需要本地起服务双击就能看。仪表盘设计成四个区域自上而下总览区今日自选股整体表现用涨跌分布、涨停/跌停数量、资金净流入TOP5配一个概览表格个股详情区每只股票的K线图用ECharts的K线图、技术指标信号、AI个股解读做成折叠面板点击展开AI综合研判区大模型基于全市场自选股生成的综合Summary、板块热度和风险提示操作提醒区根据自定义的风险偏好列明触发关注/止损/止盈的股票并附上触发条件。HTML模板的关键在于Jinja2的渲染逻辑。我定义了一个ReportGenerator类class ReportGenerator: def __init__(self, template_dir: str): self.env Environment(loaderFileSystemLoader(template_dir)) def generate(self, context: dict, output_path: str): template self.env.get_template(dashboard.html) html template.render(context) with open(output_path, w, encodingutf-8) as f: f.write(html)模板里ECharts的K线图是通过注入JSON数据来渲染的const klineData {{ kline_json | safe }}; const chart echarts.init(document.getElementById(kline_{{ code }})); chart.setOption({ title: { text: {{ name }} ({{ code }}) }, xAxis: { type: category, data: klineData.categories }, yAxis: { scale: true }, series: [{ type: candlestick, data: klineData.values, ... }] });注意| safe这个过滤器它告诉Jinja2不要对JSON字符串做HTML转义否则JavaScript里会解析出错。这里有一个安全提示不要在报告里渲染用户可控的、未经过滤的HTML内容避免XSS注入。由于数据源都是行情接口返回的纯数字和有限字符串实际风险很低但如果你在后面扩展了公告抓取、新闻评论等文本字段一定要做好转义。这个项目里我用的是markupsafe库做白名单处理。2.6 推送通道企业微信/飞书/邮箱三种渠道的实现推送层是系统真正产生价值的终点。报告生成得再好推不到你手机上就没有意义。我实现了三个渠道接口设计上统一成push方法。2.6.1 企业微信机器人推送企业微信是目前国内职场用户活跃度最高的IM之一公司内部基本都是企业微信的场景下推送到企微群是最合适的。配置方式在企微群里添加一个群机器人拿到Webhook地址然后post一个JSON消息体。class WeComPusher: def __init__(self, webhook_url: str): self.webhook_url webhook_url def push_markdown(self, title: str, markdown_content: str): payload { msgtype: markdown, markdown: { content: f## {title}\n{markdown_content} } } resp requests.post(self.webhook_url, jsonpayload, timeout10) return resp.json()企业微信markdown消息只支持部分Markdown语法加粗、标题到4级、引用、链接、字体颜色仅3种默认色是支持的但表格和图片不支持也没办法直接在消息里附件HTML。我现在的做法是报告生成后把HTML文件复制到服务器上一个静态目录同时把链接发到群里。这样点击链接触发浏览器打开完整报告群消息里放一个精简版的文字分析摘要。这里有个企业微信特有的问题Webhook会限频每个机器人每分钟最多20条消息。如果自选股太多一条条推个股详情很容易触发限频。我的方案是“先汇总推送、后分板块推送”每次整体推送控制在1条群消息1条链接再针对重点信号单独推一条。2.6.2 飞书机器人推送飞书的API设计跟企业微信类似也是通过Webhook发消息不过消息体格式用的是飞书自定义的卡片格式。实现上我封装成了class FeishuPusher: def __init__(self, webhook_url: str): self.webhook_url webhook_url def push_text(self, text: str): payload { msg_type: text, content: {text: text} } ... def push_interactive_card(self, title: str, content: str, link: str): payload { msg_type: interactive, card: { header: {title: {tag: plain_text, content: title}}, elements: [ {tag: markdown, content: content}, {tag: action, actions: [ {tag: button, text: {tag: plain_text, content: 查看完整报告}, type: primary, url: link} ]} ] } }飞书卡片支持markdown和按钮展现效果比企业微信的纯文本强不少可以直接在消息卡片里展示分析结论按钮跳转报告链接。推个人飞书用的是open_id推群用的是chat_id这两个参数在企业微信/飞书后台申请应用的时候都要注意区分我一开始就栽在这里后面会专门讲。2.6.3 SMTP邮件推送邮箱是兼容性最强的兜底通道哪怕微信和飞书都挂了邮件也能到。我用标准库smtplib加email.mime实现class EmailPusher: def __init__(self, smtp_host: str, smtp_port: int, username: str, password: str): ... def push_report(self, to_addr: str, subject: str, html_path: str): with open(html_path, r, encodingutf-8) as f: html_content f.read() msg MIMEMultipart(alternative) msg[Subject] Header(subject, utf-8) msg[From] self.username msg[To] to_addr part MIMEText(html_content, html, utf-8) msg.attach(part) with smtplib.SMTP_SSL(self.smtp_host, self.smtp_port) as server: server.login(self.username, self.password) server.sendmail(self.username, [to_addr], msg.as_string())这里一个很隐蔽的坑是QQ邮箱和网易163邮箱的SMTP密码不是登录密码而是“授权码”需要在邮箱设置里单独生成。另外邮件正文直接放完整HTML报告可以但很多邮箱客户端会拦截内嵌的ECharts脚本所以我把ECharts的CDN链接放到了HTML模板里而不是内嵌本地库。如果你在纯内网环境跑这个系统建议把ECharts的JS文件下载到本地静态目录再引用。3. 完整部署实操从零开始跑起来3.1 环境准备Python版本、虚拟环境与依赖安装建议使用Python 3.10及以上版本我自己跑在3.10.12上3.9也兼容但低于3.9有些依赖装不上。项目用pip管理依赖直接安装git clone https://github.com/yourname/stock-ai-dashboard.git cd stock-ai-dashboard python3 -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install -r requirements.txtrequirements.txt里的核心依赖如下akshare1.12.0 pandas2.0.0 numpy1.24.0 requests2.28.0 openai1.30.0 jinja23.1.0 pyyaml6.0 apscheduler3.10.0安装完成后先跑一个最简单的冒烟测试验证AKShare能不能拉到数据python -c import akshare as ak; df ak.stock_zh_a_hist(symbolsh600519, perioddaily, adjustqfq); print(df.tail(5))如果能正常打印出贵州茅台的K线说明网络和AKShare都正常。这一步一定要先做因为AKShare首次安装后经常因为缺依赖比如lxml、py-mini-racer而报错提前暴露问题会省很多调试时间。3.2 配置文件准备API Key、Webhook、邮箱参数项目根目录下有一个config.example.yaml先复制成config.yaml再编辑watchlist_file: watchlist.json data: source: akshare tushare_token: # 备用不需要可以不填 llm: provider: deepseek api_key: sk-xxxxxxxxxxxxxxxx model: deepseek-chat base_url: https://api.deepseek.com temperature: 0.3 push: wecom: enabled: true webhook_url: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxxx feishu: enabled: false webhook_url: email: enabled: true smtp_host: smtp.qq.com smtp_port: 465 username: yourqq.com password: 授权码不是QQ密码 to_addr: receiptexample.com schedule: trading_only: true times: - 14:50 # 盘中快照 - 15:10 # 收盘分析 - 19:00 # 晚间补充trading_only设为true时调度器会判断当天是否为A股交易日非交易日自动跳过。这个判断逻辑很关键我用的是exchange_calendars库节假日休市不会误触发。不过节假日数据可以提前在库里配置避免额外调用接口。注意配置文件里所有敏感信息都不要提交到Git仓库。项目.gitignore里已经加了config.yaml和watchlist.json后者可能包含你的投资逻辑偏好但最好还是用config.example.yaml作为模板提交实际配置在本地生成。3.3 第一次运行手动执行主流程验证配置好后先别直接上定时任务手动跑一遍主流程python main.py --mode once这个命令会完整执行“拉数据→算指标→AI分析→生成报告→推送”整条链路。正常情况下你会看到类似这样的日志INFO - 开始获取行情数据: 3只自选股 INFO - 600519 数据获取成功最新收盘价 1688.00 INFO - 000858 数据获取成功最新收盘价 129.80 INFO - 300750 数据获取成功最新收盘价 175.30 INFO - 技术指标计算完成MACD金叉信号: 1只均线多头排列: 2只 INFO - 调用大模型分析 600519请求耗时 3.2s输出JSON解析成功 INFO - 调用大模型分析 000858请求耗时 2.8s输出JSON解析成功 INFO - 调用大模型分析 300750请求耗时 4.1s输出JSON解析成功 INFO - 汇总分析完成生成综合研判 INFO - 报告已生成: reports/dashboard_20250115_1510.html INFO - 企业微信推送成功 INFO - 邮件发送成功如果这里某一步卡住或报错多半是API Key配置错误、Webhook地址不对、或者AKShare接口变动。别急第4部分有完整的排查速查表。3.4 定时调度Linux crontab与Windows任务计划手动跑通后就可以上定时任务了。Linux环境下用crontab最方便crontab -e # 周一到周五的 14:50、15:10、19:00 运行 50 14 * * 1-5 cd /path/to/stock-dashboard /usr/bin/python3 main.py --mode scheduled logs/cron.log 21 10 15 * * 1-5 cd /path/to/stock-dashboard /usr/bin/python3 main.py --mode scheduled logs/cron.log 21 0 19 * * 1-5 cd /path/to/stock-dashboard /usr/bin/python3 main.py --mode scheduled logs/cron.log 21注意这里日志必须重定向到文件否则crontab的输出会在服务器邮件系统里找很不方便排查。Windows环境可以用“任务计划程序”操作路径是控制面板→管理工具→任务计划程序→创建基本任务。触发器选择“按一周中的某天”设置周一到周五时间设为15:10操作选择“启动程序”程序填python的绝对路径参数填main.py --mode scheduled起始于填项目目录。我强烈建议把日志级别调到INFO以上并启动定时清理脚本避免长时间运行日志文件暴涨。清理可以根据日志日期字段用logrotate或者一个简单的shell脚本每天压缩归档保留最近30天。3.5 容器化部署Docker Compose一键启动如果你有公网服务器或者NAS用Docker Compose会更省心尤其适合不想直接装Python环境的场景。FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ ./src/ COPY main.py . COPY config.yaml . COPY watchlist.json . CMD [python, main.py, --mode, scheduled]docker-compose.yml里这样定义version: 3.8 services: stock-ai: build: . container_name: stock-dashboard environment: - TZAsia/Shanghai volumes: - ./reports:/app/reports - ./logs:/app/logs restart: unless-stopped这里有一个细节必须设置TZAsia/Shanghai否则容器默认UTC时区定时任务会在北京时间的22:50、23:10触发完全错位。我在这上面损失过半天时间第一次部署时没注意到时区问题日志显示一直在跑但推送时间对不上。磁盘挂载也很重要reports和logs这两个目录如果数据只在容器里容器重建后历史报告和日志会全丢。挂载到宿主机目录后报告可以做Web服务对外访问比如部署到Nginx下的静态目录也方便事后复盘。4. 常见问题与排查技巧实录从实际跑这个系统到现在我整理了几个高频问题和对应的排查方法。这些问题如果不去看源码和日志真的很折磨人。放在这里当速查表用。4.1 数据获取失败AKShare接口变动、网络超时、格式变化现象日志中出现数据获取失败或空DataFrame。排查顺序先检查网络curl -I https://www.baidu.com是否通如果服务器在境外访问AKShare的源站可能会有问题再检查AKShare版本pip show akshare看版本去GitHub对比一下接口是否有改动。AKShare更新很频繁接口变动是常态然后手动在Python环境试一下具体接口ak.stock_zh_a_hist(symbolsh600519, perioddaily, adjustqfq)看是否真的报错最后确认symbol格式是sh600519还是600519不同接口要求不一样我的封装里已经统一处理但你要确保没有绕过封装直接调用原始接口。避坑技巧AKShare的接口返回列名可能变化比如日期列可能会变成date或日期收盘列可能是close也可能是收盘价。所以我的normalize方法里做了列名映射用正则把常见的列名统一成英文def _normalize(self, df: pd.DataFrame) - pd.DataFrame: rename_dict { 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume } df df.rename(columnsrename_dict) return df4.2 大模型输出JSON解析失败格式漂移、字段缺失、内容幻觉现象日志中显式JSON解析失败或字段缺失。这是我最常见的问题几乎没有之一。大模型即使加了response_format: json_object偶尔也会输出多余的markdown代码块标记或者在JSON里多了/少了字段。解决方案解析失败后做一个“清理重试”的兜底逻辑def _parse_llm_json(text: str) - dict: text text.strip() # 去掉可能的 json 代码块标记 text re.sub(r^(?:json)?|$, , text, flagsre.MULTILINE).strip() try: return json.loads(text) except json.JSONDecodeError: # 去掉最外层非JSON的说明文字再试一次 start, end text.find({), text.rfind(}) if start 0 and end start: return json.loads(text[start:end1]) raise如果清理后还是解析失败就重新调用一次大模型。重试设计成最多2次超过就直接跳过这只股票的AI分析在报告里标注“AI分析暂不可用”而不是让整个任务挂掉。这里还有一个经验解析出的JSON要做一次严格字段校验至少确认summary、signals、risk_tips这几个关键字段非空。如果字段缺失宁可就地降级为“该股数据正常AI解读暂缺”也不要让模型随便编一段填充进去。AI幻觉的针对性解决我前面已经说了“数据不足必须承认”的Prompt约束这里再补充一个实操把给大模型的数据摘要里的数字做四舍五入只保留两位小数并明确标注为“已脱敏处理仅供分析参考”。实测发现数字越小越精确模型越容易在解读时“强行找规律”反而造成幻觉。保留两位小数已经足够看出来趋势。4.3 定时任务不执行或执行两次现象设定15:10触发但日志里没有或者没有重复执行。排查思路crontab的时区问题date命令确认服务器时区如果是UTC需要把时间换算成UTC时间或者用TZ环境变量指定crontab里的路径问题crontab中Python和项目目录要用绝对路径因为PATH环境变量可能没有你shell里的那些目录用flock防止命令重叠如果上一个任务还没跑完下一个任务的触发时间已经到了会造成任务并发执行。加一个文件锁更稳妥50 14 * * 1-5 flock -n /tmp/stock_dashboard.lock -c cd /path/to/project /usr/bin/python3 main.py --mode scheduled logs/cron.log 214.4 企业微信Webhook报错invalid webhook url或group robot is not authorized原因最常见的是Webhook地址配置错或者Webhook被群主/管理员移除了机器人。企业微信群机器人的Webhook地址一旦在群里被删除原来的地址会立即失效需要重新添加机器人获取新地址。另一个坑是IP白名单企业微信机器人默认允许服务器直接调用但如果你在企业微信管理后台开启了“企业可信IP”限制那服务器IP不在白名单里就会被拒绝。这种场景下需要在企业微信后台里添加服务器的公网IP否则推送必然失败。排查时不要只凭返回码判断把企业微信的完整返回信息打出来看。企业微信返回的errmsg通常很明确比如invalid webhook url、ip not in whitelist照着处理即可。4.5 SMTP邮件被判定为垃圾邮件原因新域名、服务器IP段被标记、邮件内容里包含大量链接和图片都会导致较低送达率。建议优先用腾讯企业邮或阿里企业邮等成熟服务它们的域名信誉比较好设置SPF和DKIM记录如果你用自建域名邮件里避免过多追加大尺寸HTML可以在邮件里只放摘要文字和报告链接推送时间延后1分钟避免多个服务商在同一秒发送被限流。4.6 报告HTML打开白屏、图表不显示最常见的原因是ECharts CDN加载失败。在内网环境、或者服务器在公司防火墙后面外网CDN可能被拦截。解决思路是把ECharts的JS下载到本地静态目录script srcstatic/echarts.min.js/script还有一个坑是Jinja2模板中的变量名冲突比如你有一个变量叫data模板里也用到了data如果不加命名空间管理渲染出来的HTML可能被意外覆盖成字符串。我的做法是渲染前把所有图表数据包装在chart_data这个字典里模板统一从根级字典取值。5. 风险提示与使用边界把话说在前面写到这里必须明确一个立场这个系统本质是一个“信息整理与展示工具”它能帮你把自选股的行情、技术指标、AI解读汇总到一块看板里但它的输出绝对不构成投资建议更不承诺任何收益。任何基于此工具做出的买卖决策风险由使用者自己承担。我在这里从系统设计的角度给出了三道“防火墙”你也可以理解为底线第一道数据可信性。报告完全基于行情接口的数据AI模型不“读过”任何新闻或公告它的分析仅仅是基于你提供的结构化数据。所以一旦数据源本身有延迟或错误分析结论的可靠性就打了折扣。第二道决策边界。A股市场受宏观面、政策面、情绪面影响极大纯技术指标的准确率在真实市场中并不高。这套系统的定位是辅助你“更快看完信息”不是替你做决策。第三道模型幻觉残留。尽管我已经做了反幻觉设计大模型仍然可能偶尔给出不准确的表述。每次推送的报告里我都保留了“AI分析可能存在偏差请结合自身判断”的提示没有抹掉作为对使用者的最后一道提醒。用这个系统三个月个人最深的感受是它带来的最大价值不是“预测涨跌”而是强制你每天用同一个标准审视一遍自选股这种纪律性本身才是有效的。市场里不存在稳赢的策略但存在减少犯错的流程。最后分享一个后续扩展的方向如果你希望这个系统更进一步可以考虑把“多因子打分”模块加进来将估值因子、动量因子、波动率因子组合成综合评分再把大模型生成的分析摘要作为文本因子参与打分。这样整个系统就从“展示信息”进化到“生成参考评分”实用价值会再上一个台阶。我已经把扩展接口留在了代码的strategies/目录下有兴趣的朋友可以直接在这个框架上继续开发。本文还有配套的精品资源点击获取