大模型驱动的股票分析系统:从指标计算到容器化部署实战
在实际的股票分析场景里数据获取、指标计算和结论解读是三个难度完全不同的环节。行情数据可以通过 CSV 或接口落地MA、RSI、MACD 这类技术指标也有成熟公式真正麻烦的是拿到一堆指标后怎么把它们组合成一段有逻辑、有依据、可阅读的分析结论。大模型擅长处理“结构化数据转自然语言”的任务所以把它引入股票分析系统是一个很自然的方向。这篇 FDE 前沿部署实战记录围绕一条完整链路展开用本地行情数据构造指标快照通过 OpenAI 兼容接口让大模型生成分析报告再用 Streamlit 和 FastAPI 搭建展示与部署环境最终用 Docker Compose 把整套系统跑起来。整个过程强调可复现代码片段可以直接在本地实验。FDE 在这里不是某个固定框架的缩写而是这次实战的核心目标Front Deployment Experience前沿部署体验。它关注的不只是算法本身而是大模型应用从本地脚本走向可部署服务的全过程。下面所有内容都会围绕这个主线展开先讲清楚大模型在分析系统中的定位再给最小可运行实现最后落到部署和排错。1. 为什么用大模型做股票分析而不是固定规则1.1 固定规则能算指标但生成不了“解释”技术分析里有一类确定性的计算任务比如 5 日均线、14 日 RSI、MACD 的 DIF/DEA 柱状图。这类任务用 pandas 几行就能完成结果在相同输入下也是确定的。但真正的分析报告不止包含数字。人工阅读时会把“MA5 上穿 MA20”“RSI 处于 70 以上”“MACD 红柱连续放大”这些现象组合成一段话趋势处于什么状态短期动量是否增强当前处于什么风险位置。这种组合能力如果完全用规则实现需要写大量条件分支。if ma5 ma20 and ma10 ma20: if rsi 70: return 短期超买注意回调风险 else: return 均线多头排列趋势偏强规则一多边界情况会指数级增加。比如均线纠缠、数据不足、停牌导致的缺失值都会让规则维护成本变高。大模型不是用来替代这个计算的而是用自然语言把指标快照里的关键特征总结出来减少人工编写规则的工作量。1.2 大模型的角色不是替代计算而是补充解读大模型并不擅长精确的数值计算。让模型直接计算 RSI 72.35 并不可靠它更适合处理“RSI 为 72.35MA5 为 10.28MA20 为 9.91”这类已经算好的事实并生成解释性文本。这是整个系统设计的关键所有数值计算都在 pandas 中完成大模型只接收经过结构化的指标摘要不直接面对原始 K 线。这样能显著降低幻觉风险也让大模型输出的内容更容易验证。从架构角度看系统被拆成四个模块数据层加载 CSV 或行情接口统一成 DataFrame。指标层计算 MA、RSI、MACD 等特征。解读层构造指标快照调用大模型生成报告。展示层通过 Streamlit 或 FastAPI 把结果呈现给用户。每一层职责单一替换起来也方便。今天用云端 API明天切换成本地模型只需要改解读层的配置不影响指标计算和展示部分。1.3 这样做能带来什么实际价值对个人研究者来说这套系统的价值在于把“读数据”变成“读报告”。常见股票的指标算完后不再需要人工盯着五六个数字判断而是让大模型先给一版结构化描述再由人做最终决策。对开发团队来说大模型负责的是高重复性的解释工作例如周报生成、异动说明、指标摘要。真正的量化策略仍然需要严谨回测和人工验证不能直接把模型输出当交易信号。本文示例用于学习技术链路。涉及股票的内容仅作技术演示不构成投资建议。2. 环境准备与依赖选型2.1 运行环境要求不同阶段的运行环境要求不同。下面是推荐配置落地前要结合自己的操作系统和实际数据量调整。环境最低配置建议配置用途本地学习Python 3.104GB 内存内存 8GB 以上跑通指标计算和少量 LLM 请求本地模型部署内存 16GB 左右独立 GPU 或 32GB 内存运行 7B 级别量化模型服务部署2 核 4GB4 核 8GB 加监控运行 FastAPI、Streamlit 和模型服务如果使用云端大模型 API开发机只需要普通配置即可如果使用本地部署模型需要重点关注内存和磁盘占用。2.2 Python 依赖清单项目使用 Python 生态主要依赖如下pandas2.1.4 numpy1.26.2 plotly5.18.0 streamlit1.29.0 fastapi0.104.1 uvicorn[standard]0.24.0 openai1.6.1 pydantic2.5.0 python-dotenv1.0.0将这些内容保存到requirements.txt。安装命令pip install -r requirements.txt如果只是做快速实验也可以只安装 pandas、openai 和 streamlit其余依赖按需补充。不要一次性装太多不相关的包否则排错时会很难定位问题。2.3 大模型接入方案对比大模型接入有几种常见方式选型直接影响部署成本和开发复杂度。方案适用阶段优点注意点云端商用 API快速原型、个人实验调用简单无需本地资源需要密钥按 token 计费Ollama 本地部署私有化、离线环境数据不出内网部署简单需要内存/显卡模型切换需下载vLLM 服务生产环境高并发推理吞吐高支持连续部署配置复杂需要 GPU为了让代码不绑定具体供应商下面统一采用 OpenAI 兼容接口方式。用环境变量控制base_url、api_key和model_name。这样无论是云端 API 还是本地 Ollama都能用同一套调用代码。3. 实现一个最小可运行的股票分析系统3.1 项目结构为了避免所有逻辑堆在一个文件里项目可以按下面结构组织stock-analyzer/ ├── data/ │ └── sample_stock.csv ├── src/ │ ├── __init__.py │ ├── loader.py │ ├── indicators.py │ ├── prompt.py │ └── llm_client.py ├── web/ │ ├── api.py │ └── dashboard.py ├── deploy/ │ ├── Dockerfile.api │ ├── Dockerfile.dashboard │ └── docker-compose.yml ├── .env.example ├── requirements.txt └── README.md项目中src存放纯逻辑代码web存放接口和展示层deploy存放容器化配置。这样设计的原因是指标计算和大模型调用都不依赖 Web 框架可以直接在 Notebook 或命令行中测试。3.2 行情数据准备与加载为了复现方便本文使用本地 CSV 作为数据源。这样可以避免不同行情接口的限流、字段差异和密钥问题。先把流程跑通再替换成真实数据源。样例数据格式如下date,open,high,low,close,volume 2024-01-02,10.00,10.30,9.90,10.20,1500000 2024-01-03,10.20,10.60,10.10,10.50,1800000 2024-01-04,10.50,10.80,10.30,10.40,1200000 ...加载代码写在src/loader.pyimport pandas as pd def load_stock_data(file_path: str) - pd.DataFrame: df pd.read_csv(file_path) df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) numeric_cols [open, high, low, close, volume] df[numeric_cols] df[numeric_cols].apply(pd.to_numeric, errorscoerce) df df.dropna(subset[date, close]).reset_index(dropTrue) return df if __name__ __main__: data load_stock_data(data/sample_stock.csv) print(data.tail())这段代码做三件最基本的事把日期转成时间类型、按时间排序、把核心行情字段转成数值。排序很重要指标计算依赖时间顺序如果原始数据倒序均线结果会是错的。3.3 技术指标计算指标逻辑放在src/indicators.py。这里实现最常用的三个指标移动平均线、RSI、MACD。import pandas as pd def add_ma(df: pd.DataFrame, windows(5, 10, 20)) - pd.DataFrame: for window in windows: df[fma{window}] df[close].rolling(windowwindow).mean() return df def add_rsi(df: pd.DataFrame, period: int 14) - pd.DataFrame: delta df[close].diff() gain delta.where(delta 0, 0.0) loss -delta.where(delta 0, 0.0) avg_gain gain.rolling(windowperiod).mean() avg_loss loss.rolling(windowperiod).mean() rs avg_gain / avg_loss df[frsi{period}] 100 - (100 / (1 rs)) df.loc[df[frsi{period}].isna(), frsi{period}] 50 return df def add_macd(df: pd.DataFrame) - pd.DataFrame: ema12 df[close].ewm(span12, adjustFalse).mean() ema26 df[close].ewm(span26, adjustFalse).mean() dif ema12 - ema26 dea dif.ewm(span9, adjustFalse).mean() df[macd_dif] dif df[macd_dea] dea df[macd_hist] (dif - dea) * 2 return df这里的实现有几个关键点rolling默认使用窗口内数据窗口期前面的结果为NaN。RSI 计算中avg_loss可能为 0公式会出现除零。示例里用NaN填 50表示中性状态。实际项目要考虑数据长度是否足够。MACD 使用指数移动平均adjustFalse确保从第一根 K 线开始计算。调用方式def prepare_indicators(df: pd.DataFrame) - pd.DataFrame: df add_ma(df) df add_rsi(df) df add_macd(df) return df3.4 构造大模型可读的市场快照大模型不需要接收所有历史数据。通常只需要最近几天的关键数值以及近期的趋势状态。在src/prompt.py中实现一个函数把最后几行指标转成结构化摘要import json import pandas as pd def build_market_snapshot(df: pd.DataFrame, periods: int 3) - dict: recent df.tail(periods) snapshot { last_date: str(df[date].max().date()), close: float(df[close].iloc[-1]), ma5: float(df[ma5].iloc[-1]) if not pd.isna(df[ma5].iloc[-1]) else None, ma20: float(df[ma20].iloc[-1]) if not pd.isna(df[ma20].iloc[-1]) else None, rsi14: float(df[rsi14].iloc[-1]) if not pd.isna(df[rsi14].iloc[-1]) else None, macd_dif: float(df[macd_dif].iloc[-1]), macd_dea: float(df[macd_dea].iloc[-1]), macd_hist: float(df[macd_hist].iloc[-1]), recent_rows: recent[[date, close, volume]].astype(str).to_dict(records), } return snapshot def build_analysis_prompt(snapshot: dict) - str: prompt f 你是一名股票数据分析助理。请基于以下技术指标快照生成一份简洁的研究说明。 要求 1. 从趋势、动量、风险三个角度描述。 2. 不要给出买卖建议不要预测未来具体价格。 3. 全文不超过 300 字。 4. 以 JSON 格式输出字段为 trend、momentum、risk、summary。 指标快照 {json.dumps(snapshot, ensure_asciiFalse, indent2)} return prompt把最近数据和指标单独封装成 JSON可以避免在提示词中塞入完整 K 线。这样 token 消耗低模型也更容易聚焦。3.5 调用大模型生成分析报告调用层放在src/llm_client.py。为了适配不同供应商使用 openai 库并指定base_url。import os import json from openai import OpenAI def create_llm_client(): return OpenAI( api_keyos.getenv(LLM_API_KEY, ollama), base_urlos.getenv(LLM_BASE_URL, http://localhost:11434/v1), ) def chat_with_json(client: OpenAI, prompt: str, model: str None, temperature: float 0.2): model model or os.getenv(LLM_MODEL, qwen2.5:7b) resp client.chat.completions.create( modelmodel, messages[ {role: system, content: 你只输出合法 JSON不要输出额外说明。}, {role: user, content: prompt}, ], temperaturetemperature, ) text resp.choices[0].message.content return parse_json(text) def parse_json(text: str): text text.strip().strip(json).strip().strip() try: return json.loads(text) except json.JSONDecodeError: start text.find({) end text.rfind(}) if start ! -1 and end ! -1: return json.loads(text[start:end 1]) raise ValueError(f无法从模型输出中解析 JSON: {text[:200]})temperature设为 0.2可以让输出更稳定。提示词里明确要求只输出 JSON解析端再做一层容错。很多问题都出在模型输出多余文字所以解析函数必须足够健壮。4. 用 AI 编程提升开发效率4.1 用大模型生成指标计算模块的提示词示例在开发过程中可以使用大模型辅助生成代码。以 RSI 计算为例提示词可以这样写我在写一个股票分析系统使用 pandas DataFrame 存储每日行情字段有 date、open、high、low、close、volume。 请实现一个 add_rsi 函数计算 14 日 RSI并且处理 avg_loss 为 0 的情况。 要求 - 返回新增 rsi14 列的 DataFrame。 - 对前 13 行填充中性值 50。 - 给出简单的测试用例。将这段提示词输入大模型通常可以得到可运行代码。但直接复制前必须检查三点是否处理了空值和除零。是否改变了原 DataFrame 的结构。数值公式是否与常用定义一致。AI 生成的代码可能能运行但业务正确性需要人来确认。4.2 如何审查 AI 生成的代码以 RSI 为例常见错误写法是rs avg_gain / avg_loss df[rsi14] 100 - (100 / (1 rs))这个写法在avg_loss为 0 时会出现inf不是崩溃就是计算出错误值。审查时重点看边界条件窗口不足、除数为 0、空值填充策略。另一个常见问题是指标函数直接修改传入的 DataFrame。如果后面有缓存或并发场景这种副作用可能引发很难排查的 bug。建议在函数内部使用df.copy()或者明确约定函数返回新 DataFrame。4.3 调试大模型输出格式调用模型时最常遇到的是输出格式问题。模型可能输出这是分析结果 { trend: 趋势偏强, ... }或者把 JSON 包在 Markdown 代码块里。解析时不能直接json.loads需要先清理前缀。上面parse_json函数已经做了两层处理先去掉 Markdown 代码块标记再截取首尾大括号。如果仍解析失败可以把原始输出记录到日志中方便定位是模型问题还是提示词问题。调试阶段的建议是把 prompt 和 response 都打印出来print(PROMPT:, prompt) print(RESPONSE:, text)先看模型收到什么再看模型回了什么很多问题会变得清楚。5. 前端展示与交互5.1 使用 Streamlit 搭建分析面板Streamlit 适合快速搭建数据应用。在web/dashboard.py中实现一个简单面板import os import sys import pandas as pd import streamlit as st sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from src.loader import load_stock_data from src.indicators import prepare_indicators from src.prompt import build_analysis_prompt, build_market_snapshot from src.llm_client import create_llm_client, chat_with_json st.set_page_config(page_title股票分析系统, layoutwide) st.title(FDE 股票分析系统) uploaded_file st.file_uploader(上传 CSV 行情文件, type[csv]) if uploaded_file is not None: df load_stock_data(uploaded_file) df prepare_indicators(df) st.subheader(原始行情) st.dataframe(df.tail(20))这里load_stock_data接收文件对象pandas 可以直接读取。上传 CSV 后系统会展示处理后的行情表。5.2 展示 K 线与指标K 线图使用 plotly 的Candlestick。示例代码import plotly.graph_objects as go from plotly.subplots import make_subplots def plot_kline_with_indicators(df: pd.DataFrame): fig make_subplots( rows2, cols1, shared_xaxesTrue, row_heights[0.7, 0.3], vertical_spacing0.05, ) fig.add_trace( go.Candlestick( xdf[date], opendf[open], highdf[high], lowdf[low], closedf[close], nameK线, ), row1, col1, ) fig.add_trace(go.Scatter(xdf[date], ydf[ma5], nameMA5), row1, col1) fig.add_trace(go.Scatter(xdf[date], ydf[ma20], nameMA20), row1, col1) colors [red if v 0 else green for v in df[macd_hist]] fig.add_trace( go.Bar(xdf[date], ydf[macd_hist], nameMACD, marker_colorcolors), row2, col1, ) return fig fig plot_kline_with_indicators(df) st.plotly_chart(fig, use_container_widthTrue)中国 A 股习惯红花绿跌这里的颜色只在示例中使用。实际项目中可以根据所在市场习惯调整。5.3 增加报告生成与下载在面板中加入大模型分析按钮st.subheader(大模型分析报告) if st.button(生成分析报告): snapshot build_market_snapshot(df) prompt build_analysis_prompt(snapshot) with st.spinner(正在调用大模型...): client create_llm_client() report chat_with_json(client, prompt) st.json(report) st.download_button( 下载报告, datastr(report).encode(utf-8), file_nameanalysis_report.json, mimeapplication/json, )按钮触发后系统先构造指标快照再调用大模型最后把 JSON 报告展示出来。这里的st.download_button让用户可以把结果保存到本地。6. 后端部署实战从本地脚本到容器服务6.1 使用 FastAPI 封装分析接口直接让 Streamlit 调用本地函数在单机场景可行但要部署成服务还需要一套接口层。在web/api.py中定义/analyze接口。import os import sys sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from fastapi import FastAPI, HTTPException from pydantic import BaseModel from src.loader import load_stock_data from src.indicators import prepare_indicators from src.prompt import build_analysis_prompt, build_market_snapshot from src.llm_client import create_llm_client, chat_with_json app FastAPI(titleStock Analyzer API) class AnalyzeRequest(BaseModel): file_path: str model: str None class AnalyzeResponse(BaseModel): report: dict snapshot: dict app.post(/analyze, response_modelAnalyzeResponse) def analyze(request: AnalyzeRequest): if not os.path.exists(request.file_path): raise HTTPException(status_code404, detail文件不存在) df load_stock_data(request.file_path) df prepare_indicators(df) snapshot build_market_snapshot(df) prompt build_analysis_prompt(snapshot) try: client create_llm_client() report chat_with_json(client, prompt, modelrequest.model) except Exception as exc: raise HTTPException(status_code500, detailf大模型调用失败: {str(exc)}) return AnalyzeResponse(reportreport, snapshotsnapshot)生产环境不会把文件路径暴露给接口。这里的file_path只是演示。正式服务应该把上传文件保存到指定目录做路径校验和权限控制再传给分析逻辑。6.2 编写 DockerfileAPI 服务的 Dockerfile 放在deploy/Dockerfile.apiFROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src ./src COPY web ./web EXPOSE 8000 CMD [uvicorn, web.api:app, --host, 0.0.0.0, --port, 8000]Streamlit 面板的 Dockerfile 放在deploy/Dockerfile.dashboardFROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src ./src COPY web ./web COPY data ./data EXPOSE 8501 CMD [streamlit, run, web/dashboard.py, --server.port8501, --server.address0.0.0.0]requirements.txt同时包含 API 和面板依赖。如果担心镜像过大可以拆分成两个 requirements 文件。这里保持简单。6.3 使用 Docker Compose 一键部署deploy/docker-compose.yml示例version: 3.8 services: api: build: context: .. dockerfile: deploy/Dockerfile.api env_file: - ../.env volumes: - ../data:/app/data ports: - 8000:8000 restart: unless-stopped dashboard: build: context: .. dockerfile: deploy/Dockerfile.dashboard env_file: - ../.env volumes: - ../data:/app/data ports: - 8501:8501 depends_on: - api restart: unless-stopped ollama: image: ollama/ollama:latest profiles: [local-model] volumes: - ollama_data:/root/.ollama ports: - 11434:11434 restart: unless-stopped volumes: ollama_data:使用本地模型服务时在根目录执行docker compose --profile local-model up -d然后进入 ollama 容器拉取模型docker compose exec ollama ollama pull qwen2.5:7b没有 GPU 的机器不建议跑 7B 模型可以改用 3B 或 1.5B 量化版本。如果使用云端 API则不需要启动ollama服务只需在.env中配置LLM_BASE_URL和LLM_API_KEY。6.4 生产环境部署检查清单从学习环境过渡到生产环境至少检查以下项目检查项说明配置外置模型地址、密钥、温度参数不要写死在代码里数据目录权限服务账户只能访问指定数据目录请求超时大模型接口设置读超时和连接超时并发控制限制上传文件大小和请求频率日志记录请求 ID、模型名称、输入输出摘要监控监控 API 延迟、token 消耗、内存占用回滚容器镜像打版本标签便于快速回滚7. 常见问题与排查链路7.1 大模型返回内容不符合预期现象模型输出的报告和指标明显不符例如 RSI 为 35 却说“超买”。排查顺序检查提示词是否把指标值放进去。检查模型是否看到完整快照。检查 temperature 是否过高。检查是否用了错误的模型名称。解决方案在提示词中明确“只能根据给定指标快照下结论”并把 temperature 调低到 0.1 至 0.3。7.2 数据缺失或指标计算报错现象ValueError: Length of values does not match length of index或大量指标为 NaN。可能原因CSV 表头与代码读取字段不一致。数据量太少rolling(20)前 19 行都是空值。日期存在重复或未排序。检查方式print(df.head()) print(df.dtypes) print(df.isna().sum())解决方案加载后先排序指标计算后统一填充或丢弃空值。数据少于 30 天时建议给出提示而不是继续生成报告。7.3 本地模型加载慢、内存占用高现象首次请求等待几十秒容器启动后内存占用超过 80%。可能原因模型体积太大。没有设置量化。服务并发请求导致多个模型实例加载。排查命令docker compose exec ollama ollama ps解决方案切换到更小的量化模型减少并发请求或者使用云端 API 做对比。本地部署不是越快越好需要根据机器资源选择合适模型。7.4 API 请求超时与并发限制现象局域网部署后页面一直转圈后端日志出现timeout。检查方向LLM_BASE_URL是否正确。容器是否能访问宿主机或外部 API。模型服务是否已启动。单次生成是否超过接口超时时间。解决方案在 openai 客户端中设置timeout60并增加重试机制。重试时要处理限流错误等待时间可以按retry-after响应头计算。8. 最佳实践、安全边界与下一步扩展8.1 控制幻觉和事实依据大模型生成的分析报告只能作为辅助研究材料不能直接视为投资建议。落地时可以在报告模板中增加“数据来源”和“指标计算时间”字段让用户能回溯到源头。推荐的报告结构{ data_date: 2024-03-01, trend: ..., momentum: ..., risk: ..., disclaimer: 本报告由大模型基于技术指标自动生成仅供学习研究不构成投资建议。 }字段越少越容易检查。不要要求模型输出精确预测值例如“明天会涨到 10.5 元”这类内容既不可靠也容易引发合规问题。8.2 数据与成本控制大模型 API 按 token 计费报告生成频繁时会积累费用。可以采用以下措施相同日期和相同参数的分析结果做缓存。控制快照长度只传最近 3 到 5 天指标。设置单日请求配额。把模型输出保存到本地便于审计。简单缓存可以用字典实现cache {} def get_report(snapshot_key: str): if snapshot_key in cache: return cache[snapshot_key] report chat_with_json(client, prompt) cache[snapshot_key] report return report生产环境建议使用 Redis 或数据库做持久化缓存。8.3 合规说明如果你准备发布包含股票分析的公开系统必须明确声明内容的风险等级。涉及金融分析的工具应把重点放在“数据处理和指标解释”而不是“荐股”或“收益保证”。不要在界面中输出“买入”“卖出”等指令性语言。在 Streamlit 页面底部加上提示数据和技术指标来自历史行情大模型生成内容仅用于技术研究和学习不构成任何投资建议。8.4 下一步扩展方向当前这个系统已经具备完整的从数据到部署链路可以继续扩展的方向包括接入实时行情 API把历史 CSV 替换成定时任务。加入更多技术指标如布林带、成交量均线、市场情绪指标。使用向量数据库保存历史报告支持问答式复盘。引入多模型投票综合不同大模型的分析结果。用 CI/CD 流水线自动构建镜像并部署到云服务器。对刚入门的人来说最有价值的练习是先不要急着接入复杂模型而是把第 3 章的指标计算和提示词工程跑通。等到切换模型时你会真正理解大模型应用的边界什么时候该让它写代码什么时候该让它分析文本什么时候应该把它放在流程之外。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →