基于大语言模型的小市值股票交易决策系统构建

基于大语言模型的小市值股票交易决策系统构建 小市值策略在量化研究里一直是个很特殊的领域弹性大、修复快、容易出超额收益但噪音也大基本面数据稀疏传统因子经常失灵。过去做小市值大家主要依赖财务因子和量价技术指标新闻、公告、宏观数据这类非结构化信息很难被系统化纳入。大语言模型Large Language ModelLLM出现后这个方向有了新的解法可以直接把新闻情绪、宏观经济指标和技术信号放进同一个决策框架里让模型输出可解释、可回测的交易信号。这篇文章基于一个实际研究项目整理而成完整拆解如何用 LLM 构建一个面向小市值股票的交易决策系统。内容包含数据获取、新闻情绪分析、宏观指标处理、技术信号计算、信号融合和简易回测每个环节都会给出可运行的 Python 示例和注释说明。适合有一定 Python 基础、想了解大模型在金融场景如何落地的开发者阅读。需要提前说明的是本文只做技术研究和教学演示不构成任何投资建议实盘涉及的风险需要你自己严格评估。1. 背景与核心概念1.1 为什么小市值交易需要 LLM小市值股票的特点是信息不对称程度高机构覆盖少价格对新闻和情绪的敏感度比大盘股更强。传统量化模型处理的是结构化数据比如价格、成交量、市盈率、净资产收益率但真正驱动小市值短期波动的往往是“公司出了什么新闻”和“市场怎么理解这条新闻”。这些信息以文本形式存在于公告、财经新闻、互动平台和社交媒体中。过去处理文本情绪主要靠词典法比如提前维护一份正面词和负面词表对文本做分词匹配。问题很明显一词多义、否定句式、反讽语气都处理不了而且金融领域的专业表达和日常用语差异很大词典法很容易给出错误判断。LLM 的优势在于它具备上下文理解能力能结合前后文判断“业绩预增”“股东减持”这些句子在不同语境下的真实情绪方向。更重要的是LLM 可以把非结构化文本转成结构化的情绪分数例如 -1 到 1 之间的浮点数这个分数可以直接和宏观指标、技术指标做加权融合形成一个统一的多因子信号。1.2 三类信号的分工与定位本文方案的核心是把三类信息源组合起来每一类的定位都不同。新闻情绪信号解决的是“短期事件驱动”问题。小市值股票对个别新闻非常敏感比如中标公告、监管问询、股东减持计划这些信息会直接改变市场短期预期。情绪分数体现了这条新闻对股价的潜在影响方向和强度。宏观经济指标解决的是“系统性风险”问题。小市值股票对利率、流动性、市场风险偏好的敏感度远高于大盘蓝筹。当宏观环境收紧时即使个股新闻偏正面也很难走出独立行情。宏观指标的引入相当于给整个策略加了一个“环境阀门”。技术信号解决的是“进场时机”问题。新闻情绪能告诉你该关注哪些股票但无法告诉你什么时候买。均线排列、相对强弱指标RSI、成交量变化这些技术指标可以帮助确认趋势和拐点避免在下跌趋势中过早入场。这三类信号的频率、语义和覆盖范围完全不同融合的目标不是让它们互相证明而是让它们各自贡献一部分信息最终合成一个综合评分。1.3 整体架构设计整个系统可以拆成五层每一层职责单一方便单独测试和替换。数据获取层 - 特征加工层 - LLM 分析层 - 信号融合层 - 回测验证层数据获取层负责拉取个股行情、新闻标题与正文、宏观指标数据。特征加工层把行情数据转成技术指标把宏观数据归一化成标准分。LLM 分析层对新闻文本做情绪打分输出结构化 JSON。信号融合层把三类分数按照权重合成最终信号并给出买入、卖出或观望的决策。回测验证层负责评估策略在历史数据上的表现并检查是否存在前视偏差、幸存者偏差等问题。这个架构最重要的特点是每层之间通过标准数据结构通信。比如 LLM 分析层的输出一定是{sentiment: positive, score: 0.85}这种 JSON这样可以避免模型输出格式不稳定导致下游解析失败。2. 环境准备与版本说明2.1 运行环境与依赖本文示例代码使用 Python 3.10 以上版本操作系统不限Windows、macOS、Linux 都可以运行。建议使用虚拟环境隔离依赖避免和本机其他项目冲突。核心依赖列表如下依赖库用途说明pandas数据处理核心数据容器numpy数值计算指标计算辅助akshareA股行情数据开源接口无需 tokenopenaiLLM API 调用兼容 OpenAI 协议的模型服务python-dotenv环境变量管理保存 API Keypyyaml配置文件解析管理参数版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。akshare 的接口变更比较频繁建议安装后先查看对应函数的文档LLM 模型服务选择兼容 OpenAI Chat Completions 协议即可国内可用通义千问、DeepSeek、智谱 GLM 等国际可用 GPT、Claude 等具体模型名以你的服务商为准。2.2 项目目录结构建议按下面的目录组织代码这样每个模块职责清楚后续扩展单因子或者接入实盘接口都比较方便。llm_trading/ ├── config/ │ └── config.yaml # 策略参数配置 ├── data/ │ ├── fetcher.py # 行情数据获取 │ ├── news.py # 新闻数据获取与预处理 │ └── macro.py # 宏观指标获取 ├── features/ │ ├── technical.py # 技术指标计算 │ └── macro_score.py # 宏观指标打分 ├── llm_engine/ │ ├── client.py # LLM 客户端封装 │ └── sentiment.py # 新闻情绪分析 ├── strategy/ │ └── fusion.py # 信号融合与决策 ├── backtest/ │ └── engine.py # 简易回测引擎 ├── output/ # 结果输出目录 ├── .env # API Key不要提交到 git └── main.py # 主程序入口目录结构本身不复杂但模块化边界很重要。数据层只负责返回干净的 DataFrame特征层只负责加工指标LLM 层只负责把文本变成分数这样任何一个环节出现异常都能快速定位。3. 核心模块设计与原理解析3.1 数据获取层的设计数据获取层要解决两个问题一是不同来源的数据时间频率不一致二是数据缺失和脏数据清洗。行情数据是小市值策略的基础。以 A 股为例akshare 可以拿到日线级别的前复权行情。小市值股票经常出现停牌、涨跌停、低流动性等问题获取后必须检查日期连续性和成交量异常值。新闻数据获取是整个项目里最需要谨慎处理的部分。公开新闻源可能有访问频率限制而且新闻文本往往混入广告、重复内容和无关链接。在进入 LLM 之前需要做基础清洗去除 HTML 标签、去除超链接、去重、过滤过短文本。宏观指标数据频率通常低于行情数据比如 PMI、CPI 每月公布一次利率调整不定期。处理宏观数据时不能直接和日频数据对齐需要做“向前填充”也就是把最新公布的宏观数据应用到它公布之后的每个交易日避免用到未来数据。3.2 新闻情绪分析层的设计新闻情绪分析是 LLM 发挥作用最核心的环节。基本流程是把新闻文本和任务指令组合成 Prompt发送给 LLM解析返回的 JSON得到情绪分数。这里有两个设计要点。第一是 Prompt 必须要求结构化输出。如果不限定格式LLM 可能输出这条消息偏正面这种自然语言下游很难解析。建议在 Prompt 中明确要求返回 JSON并给出字段说明和取值范围。更稳妥的做法是使用模型服务商提供的 JSON Mode 或结构化输出能力从协议层面保证输出是合法 JSON。第二是要给模型足够的判断上下文。直接丢一句公司发布业绩预增公告给模型模型只能做通用判断如果补充说明目标公司是小市值股票、当前市场环境、新闻发布日期模型的判断会更贴近实际情况。但也不要塞入过多历史行情数据否则容易让模型产生幻觉反而把简单任务复杂化。情绪分数建议控制在 -1 到 1 之间负数代表负面情绪正数代表正面情绪0 代表中性。同时让模型输出一个 reason 字段方便调试和人工复核。3.3 宏观经济指标处理层宏观指标这一步不涉及 LLM主要是规则判断和标准化。常见做法是把宏观指标映射到 -1 到 1 的分数区间。比如 PMI 以 50 为荣枯线高于 50 说明制造业扩张对市场偏正面低于 50 说明收缩偏负面。可以把 (40, 60) 区间线性映射到 (-1, 1)。CPI 则要看趋势温和通胀对股市影响不大过高通胀会引发紧缩担忧这需要结合历史分位数判断。宏观信号在融合时的权重不宜过高。因为宏观数据月度更新反映的是慢变量而小市值策略的调仓周期通常较短宏观信号更多是作为过滤器和环境判断而不是主要驱动因子。3.4 技术信号层技术信号是从行情数据里计算出来的常见指标包括双均线、RSI、MACD、布林带、成交量变化率。技术指标计算用 pandas 就能完成不需要专门的 TA-Lib 库虽然 TA-Lib 更快但在原型阶段用 pandas 会少很多安装和兼容性问题。小市值股票的技术指标要注意失真问题。由于流动性差某些交易日可能只有很少的成交量导致量比指标突变涨跌停时会没有成交导致 RSI 计算出现极端值。建议在计算技术信号前先剔除停牌日和零成交量日期并且对极端值做截断处理。3.5 信号融合与决策信号融合是策略的核心逻辑。本文采用最直观的加权评分法综合得分 新闻情绪分 × W1 宏观环境分 × W2 技术信号分 × W3权重 W1、W2、W3 之和等于 1具体数值通过参数寻优确定或者根据经验预设。在原型阶段我建议把新闻情绪权重设置得高一些因为这是 LLM 能带来增量信息的主要来源宏观权重低一些技术权重居中。融合之后是决策规则。最简单的规则是综合得分大于阈值就买入小于负阈值就卖出或回避其余时间观望。更复杂的做法是结合持仓周期、止损线和仓位管理但那是实盘阶段的事。4. 完整实战代码下面用一套简化但完整可运行的代码把整个流程串起来。为了突出重点代码做了一些简化行情数据用 akshare 获取新闻数据用本地示例文本代替宏观数据用模拟值演示。实际项目中你需要接入真实的新闻源和宏观数据库。4.1 初始化与全局配置先写配置文件config/config.yamlstrategy: sentiment_weight: 0.4 macro_weight: 0.2 technical_weight: 0.4 buy_threshold: 0.3 sell_threshold: -0.3 llm: model: your-model-name temperature: 0.1 max_tokens: 300 data: start_date: 20240101 end_date: 20240630 stock_pools: - 300001 # 示例股票代码实际使用时替换为你的股票池 - 300002再写主程序入口负责加载配置、串联各模块# main.py import os import yaml from dotenv import load_dotenv from data.fetcher import fetch_stock_daily from data.news import load_sample_news from data.macro import load_macro_indicator from features.technical import add_technical_signals from features.macro_score import score_macro from llm_engine.sentiment import analyze_news from strategy.fusion import fuse_signals, generate_decision load_dotenv() API_KEY os.getenv(LLM_API_KEY) API_BASE os.getenv(LLM_API_BASE) def main(): with open(config/config.yaml, r, encodingutf-8) as f: config yaml.safe_load(f) symbol config[data][stock_pools][0] print(f处理股票: {symbol}) # 1. 获取行情数据 df_daily fetch_stock_daily( symbolsymbol, start_dateconfig[data][start_date], end_dateconfig[data][end_date] ) print(f行情数据: {len(df_daily)} 个交易日) # 2. 获取新闻数据 news_list load_sample_news() # 3. LLM 情绪分析 news_scores analyze_news(news_list, modelconfig[llm][model]) print(f新闻情绪平均分: {news_scores[avg_score]:.4f}) # 4. 宏观指标处理 macro_df load_macro_indicator() macro_score score_macro(macro_df, ref_datedf_daily[date].iloc[-1]) print(f宏观环境分: {macro_score:.4f}) # 5. 技术指标 df_signal add_technical_signals(df_daily) last_bar df_signal.iloc[-1] technical_score last_bar[tech_score] print(f技术信号分: {technical_score:.4f}) # 6. 信号融合 decision generate_decision( sentiment_scorenews_scores[avg_score], macro_scoremacro_score, technical_scoretechnical_score, configconfig ) print(f最终决策: {decision}) if __name__ __main__: main()4.2 行情数据获取模块写data/fetcher.py。akshare 的字段名是中文字段需要统一改成英文字段方便后续处理。# data/fetcher.py import akshare as ak import pandas as pd def fetch_stock_daily(symbol: str, start_date: str, end_date: str) - pd.DataFrame: 使用 akshare 获取 A 股日线前复权行情。 注意akshare 接口字段和频率可能随版本变化请以实际返回为准。 raw ak.stock_zh_a_hist( symbolsymbol, perioddaily, start_datestart_date, end_dateend_date, adjustqfq ) if raw is None or raw.empty: raise ValueError(f未获取到股票 {symbol} 的行情数据) df raw.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume, 成交额: amount }) df[date] pd.to_datetime(df[date]) df df[[date, open, high, low, close, volume, amount]] df df.sort_values(date).reset_index(dropTrue) # 剔除停牌日成交量与成交额均为 0 的交易日 df df[(df[volume] 0) | (df[amount] 0)] return df4.3 新闻情绪分析模块写llm_engine/sentiment.py。这个模块封装了 LLM 调用和结果解析。# llm_engine/sentiment.py import json import time from openai import OpenAI def analyze_news(news_list: list, model: str your-model-name) - dict: 对新闻列表逐条做情绪分析返回聚合结果。 使用 OpenAI 兼容协议需要提前设置环境变量 LLM_API_KEY 和 LLM_API_BASE。 client OpenAI() scores [] details [] for item in news_list: title item.get(title, ) content item.get(content, ) news_date item.get(date, ) prompt f 你是一名金融新闻分析助手专注于小市值股票研究。 请阅读下面的新闻判断它对目标上市公司股价的潜在情绪影响。 新闻发布日期{news_date} 新闻标题{title} 新闻正文{content} 请只输出 JSON不要输出其他内容格式如下 {{ sentiment: positive 或 neutral 或 negative, score: -1.0 到 1.0 之间的浮点数, reason: 不超过50字说明判断依据 }} for retry in range(3): try: response client.chat.completions.create( modelmodel, messages[{role: user, content: prompt}], temperature0.1, max_tokens300 ) text response.choices[0].message.content.strip() # 兼容模型偶尔用 json 包裹输出的情况 if text.startswith(): text text.strip() if text.startswith(json): text text[4:] result json.loads(text) score float(result[score]) score max(-1.0, min(1.0, score)) scores.append(score) details.append({ title: title, result: result[sentiment], score: score, reason: result[reason] }) break except (json.JSONDecodeError, KeyError, ValueError): if retry 2: details.append({ title: title, result: parse_error, score: 0.0, reason: LLM 返回结果解析失败 }) time.sleep(2) except Exception as e: if retry 2: details.append({ title: title, result: api_error, score: 0.0, reason: str(e) }) time.sleep(2) avg_score sum(scores) / len(scores) if scores else 0.0 return { avg_score: avg_score, detail: details, count: len(scores) }这里有几个细节值得注意。一是重试机制LLM API 在网络抖动或限流时返回可能失败重试三次能显著提高整体成功率二是解析失败时不能直接抛异常否则整条链路中断应该记录一条错误详情并把该条新闻的情绪分置为 0保持流程继续三是温度参数设置成 0.1情绪分析是主观判断任务但我们需要尽量稳定可复现的结果温度越低越稳定。4.4 技术指标计算模块写features/technical.py计算双均线、RSI 和量比并合成一个技术信号分。# features/technical.py import pandas as pd import numpy as np def add_technical_signals(df: pd.DataFrame) - pd.DataFrame: 输入行情 DataFrame输出带技术指标和综合技术分的 DataFrame。 技术分范围约在 -1 到 1 之间。 df df.copy() # 双均线MA5 与 MA20 df[ma5] df[close].rolling(5).mean() df[ma20] df[close].rolling(20).mean() # RSI(14) delta df[close].diff() gain delta.clip(lower0) loss -delta.clip(upper0) avg_gain gain.rolling(14).mean() avg_loss loss.rolling(14).mean() rs avg_gain / avg_loss.replace(0, np.nan) df[rsi] 100 - (100 / (1 rs)) # 量比当日成交量 / 近5日平均成交量 - 1 df[vol_ratio] df[volume] / df[volume].rolling(5).mean() - 1 # 均线趋势分MA5 在 MA20 上方为 1下方为 -1 ma_trend np.where(df[ma5] df[ma20], 1.0, -1.0) # RSI 分70 以上偏过热给负分30 以下偏超卖给正分中间线性插值 rsi_score np.where( df[rsi] 70, -1.0, np.where(df[rsi] 30, 1.0, (50 - df[rsi]) / 40) ) # 量比分放量给正分缩量给负分截断到 [-1, 1] vol_score np.clip(df[vol_ratio], -1, 1) df[tech_score] 0.5 * ma_trend 0.3 * rsi_score 0.2 * vol_score df[tech_score] df[tech_score].clip(-1, 1) return df技术指标合成的权重同样是经验值。均线趋势在趋势行情里最有效RSI 在震荡行情里有参考价值量比用来捕捉放量异动。可以在回测阶段做参数敏感性分析但要注意避免在同一个数据集上反复调参导致过拟合。4.5 信号融合与决策模块写strategy/fusion.py。这一层把三类信号组合起来并映射为交易决策。# strategy/fusion.py def fuse_signals(sentiment_score, macro_score, technical_score, config): 加权融合三路信号返回综合得分。 w1 config[strategy][sentiment_weight] w2 config[strategy][macro_weight] w3 config[strategy][technical_weight] composite ( w1 * sentiment_score w2 * macro_score w3 * technical_score ) return composite def generate_decision(sentiment_score, macro_score, technical_score, config): 根据综合得分和阈值生成交易决策。 composite fuse_signals( sentiment_score, macro_score, technical_score, config ) buy_threshold config[strategy][buy_threshold] sell_threshold config[strategy][sell_threshold] # 宏观环境过滤宏观分过低时即使技术面偏多也不开新仓 if macro_score -0.5: return HOLD if composite buy_threshold: return BUY elif composite sell_threshold: return SELL else: return HOLD宏观过滤逻辑值得单独说明。在实际研究中我们发现宏观环境极差时小市值股票的新闻利好和技术突破往往持续性很差此时强行交易会频繁止损。把macro_score -0.5作为硬性过滤条件等于在策略层面加了一个系统性风险开关。4.6 简易回测引擎回测是验证策略是否有效的重要环节。下面写一个最简化的单标的回测框架帮助理解回测的核心逻辑。# backtest/engine.py import pandas as pd def run_backtest(df_signal, config): 极简回测只在 BUY 信号出现后满仓持有SELL 信号出现后清仓。 真实环境中需要加入手续费、滑点、涨跌停限制和持仓比例这里只演示原理。 df df_signal.copy() df[position] 0 df[daily_return] df[close].pct_change().fillna(0) holding False for i in range(len(df)): if df.loc[i, tech_score] config[strategy][buy_threshold]: holding True elif df.loc[i, tech_score] config[strategy][sell_threshold]: holding False df.loc[i, position] 1 if holding else 0 df[strategy_return] df[position].shift(1) * df[daily_return] df[cum_benchmark] (1 df[daily_return]).cumprod() df[cum_strategy] (1 df[strategy_return]).cumprod() return df这个回测极度简化明显会高估收益。真实回测至少要处理T1 交易规则、买卖手续费和印花税、涨跌停无法成交、持仓市值占比限制、停牌无法卖出等问题。简化版只用于理解逻辑不能用来评估真实策略优劣。5. 常见问题与排查思路在开发和测试这个系统的过程中我整理了几个高频问题供大家参考。问题现象常见原因排查与解决思路LLM 返回的 JSON 解析失败模型输出了多余自然语言或 JSON 字段名不一致在 Prompt 中强制要求只输出 JSON使用服务商的 JSON Mode解析失败时记录原始文本并重试新闻日期和行情日期对不上新闻可能发生在非交易日或当天股票停牌按交易日历对齐数据只取每个交易日最近一条新闻停牌日剔除交易信号技术指标出现 NaN滚动窗口初期数据不足使用min_periods参数或直接丢弃前 20 条数据回测结果异常偏高前视偏差、幸存者偏差、忽略交易成本严格检查信号生成时是否只用当时已公开数据股票池按当时市值动态构建加入手续费和滑点LLM API 限流单次请求过多、频率超限增加并发控制加入指数退避重试对已分析新闻做本地缓存akshare 接口字段变化数据源升级导致字段名或返回值格式变动查看最新文档在数据获取层加字段适配不要在上层代码里硬编码字段名前视偏差是回测中危害最大的问题。一个典型错误是用当天收盘后的情绪分数去预测当天的涨跌幅这等于让策略提前看到了收盘结果。正确做法是第 N 天的信号只能使用第 N 天及之前已公开的信息然后判断第 N1 天及之后的表现。宏观数据也是一样必须使用公布日之后的数据不能用事后统计值。另一个容易被忽略的问题是新闻源选择偏差。如果只选入利好才会被报道的股票样本天然不均衡情绪分析结果会整体偏多。建议新闻源覆盖公告、财经媒体、交易所监管信息等多渠道并且记录新闻未被收录的日期方便判断缺失是否会造成偏差。6. 最佳实践与工程建议6.1 数据与缓存设计金融接口调用成本高、限流多强烈建议对每次数据获取做本地缓存。行情数据按股票代码和日期范围缓存到 Parquet 文件LLM 情绪分析结果按新闻正文哈希缓存到数据库。这样重复实验时不需要重新调用昂贵的模型接口也方便人工复盘模型判断是否有误。6.2 Prompt 工程与管理情绪分析 Prompt 应该作为独立配置文件管理不要硬编码在代码里。随着策略迭代Prompt 会不断修改建议给 Prompt 加版本号并在每次分析时记录使用的 Prompt 版本。这样当情绪打分行为发生变化时你能快速定位是模型变化还是 Prompt 变化导致的。6.3 日志与可解释性交易决策必须有完整的日志链路。至少要记录每条新闻的原始文本、LLM 输出的情绪分和理由、宏观指标数值和映射分数、技术指标原始值、三个权重、最终综合分和决策。这样任何一笔交易都能回溯也方便用人工标注的样本评估 LLM 情绪分析的准确率。6.4 风险控制边界小市值股票流动性差冲击成本高下单时不能只看模型信号还要检查盘口深度和近期成交额。建议设置单只股票仓位上限、单笔交易额占总成交额比例上限并在策略中加入最大回撤止损机制。模拟盘运行至少一个完整市场周期再考虑实盘。6.5 API Key 安全管理LLM 的 API Key 必须放在环境变量或密钥管理服务中严禁提交到 Git 仓库。代码中的错误信息也不要直接打印完整请求内容避免泄露 Prompt 中的敏感信息和 Key。7. 总结与下一步学习方向到这里一个完整的 LLM 驱动小市值交易决策系统已经跑通从数据获取、LLM 新闻情绪分析、宏观指标处理、技术指标计算到信号融合和回测验证每一层都有对应的代码实现和设计思路。这套方案的核心价值在于把非结构化的新闻文本转化成了结构化的情绪分数从而和传统量化信号放在同一个框架下做融合。下一步可以往三个方向深入。第一个方向是把单股票决策扩展成股票池打分排序每日对候选池所有股票计算综合分选出得分最高的前几只做组合持仓第二个方向是引入更丰富的文本来源比如互动平台问答、研报摘要、监管函件让情绪信号更全面第三个方向是改进回测引擎加入交易成本、滑点、涨跌停约束和组合层面的风险控制让回测结果更接近真实表现。如果你准备在真实项目中落地这套方案优先把精力花在数据质量和回测真实性上这两个环节决定了策略的可靠性。模型选型、权重参数这些可以慢慢调但数据对齐错误或回测偏差造成的误导往往是策略上线后才发现的大坑。建议从模拟盘开始先跑三个月完整周期积累情绪分析和实际行情的对照数据再逐步提高资金投入。动手写代码之前记得先把你关心的股票池和新闻源准备好数据通顺了策略才谈得上验证。