ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Cursor实战案例-AI大模型-17-舆情量化选股:用OpenAI API与TaoToken统一通道搭建金融新闻情感评分选股系统

2026/9/27 22:07:11 拓冰建站 浏览量
Cursor实战案例-AI大模型-17-舆情量化选股:用OpenAI API与TaoToken统一通道搭建金融新闻情感评分选股系统 1. 舆情因子为什么值得做以及这套系统到底解决什么问题财经新闻里藏着大量非结构化信息一条“净利润大增但高管对下季度悲观”的公告用关键词匹配很容易被误判成利好。传统情感字典法识别不了双重否定、讽刺语气和金融语境里的逻辑转折评分误差大到没法直接进策略。而通用大语言模型能读懂这些复杂语义把一条新闻压缩成一个 [-1.0, 1.0] 的浮点分数这就是舆情因子的原始素材。这套系统要做的事情很明确把每天抓到的财经新闻逐条送进大模型打分再按股票代码聚合出日度舆情因子最后按得分排序选出多头候选。适合谁用做日线或分钟级盘后清洗的量化研究者、想给现有选股模型加一路另类因子的开发者以及刚接触 Cursor 想跑通一个完整 AI 应用闭环的人。整条链路在 Cursor 里写代码、调接口、跑回测不需要来回切工具。我试过把新闻标题和正文拼在一起送进去比只送标题的区分度高不少尤其是那种标题平淡、正文藏雷的公告。下面从接入通道开始一步步把配置、代码、验证和排障都走一遍。2. TaoToken 统一通道一个 Key 打通 OpenAI 兼容接口做这类项目最烦的是模型来源换来换去今天用这个、明天换那个每换一次就要改 base_url、改 key、改模型名。TaoToken 的思路是提供一个 OpenAI 兼容的统一通道你只维护一份 Key 和一份 base_url模型名在请求里指定就行。对量化这种需要批量、稳定调用的场景少一层配置就少一类线上事故。接入步骤不复杂。先到官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册账号然后进控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建 API Key。拿到 Key 之后在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 可以随时查看和轮换。接口地址统一用 https://taotoken.net/api注意这个地址后面不加任何 UTM 参数直接作为 base_url 使用。提示Key 只存在环境变量或本地配置文件里别硬编码进要提交到 Git 的脚本。量化脚本经常要分享给同事Key 泄露比代码泄露麻烦得多。如果你只是想先验证模型能不能正确输出 JSON 评分可以直接用模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 手动发一条新闻试试确认返回格式符合预期再写进代码。长期跑批量任务、或者要把这套打分逻辑接进 Agent 工作流建议看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite配额和并发策略更适合持续调用。接入细节和参数说明都在接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里遇到字段对不上先翻文档比瞎试快。3. 可复制配置config.toml 与 settings.json 骨架在 Cursor 里建项目先放两个配置文件。一个是给 Python 读的 config.toml一个是给 Cursor 编辑器/调试器读的 settings.json。两者分开的好处是业务参数改 toml编辑器行为改 json互不干扰。config.toml 骨架如下把 api_key 换成你自己的# config.toml [llm] base_url https://taotoken.net/api api_key sk-你的TaoToken密钥 model gpt-4o-mini temperature 0.0 max_tokens 200 timeout 30 [scoring] score_min -1.0 score_max 1.0 sleep_between_calls 0.5 max_retries 5 [selector] top_n 2settings.json 放在 .vscode 目录下主要给 Cursor 的 Python 调试和解释器路径用{ python.defaultInterpreterPath: ${workspaceFolder}/.venv/bin/python, python.terminal.activateEnvironment: true, python.analysis.extraPaths: [${workspaceFolder}/src], editor.formatOnSave: true, files.exclude: { **/__pycache__: true, **/.pytest_cache: true } }读取 config.toml 用 Python 3.11 自带的 tomllib不用额外装包import tomllib from pathlib import Path def load_config(path: str config.toml) - dict: with open(Path(path), rb) as f: return tomllib.load(f) cfg load_config() client_kwargs { api_key: cfg[llm][api_key], base_url: cfg[llm][base_url], timeout: cfg[llm][timeout], }这样 Key 和地址都从配置来代码里不出现明文换环境只改 toml。Cursor 的 AI 补全在写这段读取逻辑时挺顺手但记得让它别把 Key 直接写进函数默认值。4. 核心代码新闻打分与舆情因子聚合打分器的关键是 System Prompt 和 JSON 模式。Prompt 里把角色、输出范围、输出格式三件事说死temperature 设 0.0 保证同一批新闻多次跑分一致。下面这段可以直接放进 src/scorer.pyimport json import time from typing import List, Dict import pandas as pd from loguru import logger from openai import OpenAI class NewsSentimentScorer: def __init__(self, client_kwargs: dict, model: str): self.client OpenAI(**client_kwargs) self.model model self.system_prompt ( 你是一名资深量化交易员和金融语言学专家。\n 阅读给定财经新闻评估其对公司短期股价的影响。\n 只输出 JSON格式为\n {reason: 50字以内金融逻辑, score: 0.85}\n score 范围 [-1.0, 1.0]禁止输出 markdown 围栏。 ) def score_one(self, text: str) - dict: try: resp self.client.chat.completions.create( modelself.model, messages[ {role: system, content: self.system_prompt}, {role: user, content: f请评分\n{text}}, ], temperature0.0, max_tokens200, response_format{type: json_object}, ) raw resp.choices[0].message.content.strip() data json.loads(raw) data[score] float(data.get(score, 0.0)) return data except Exception as e: logger.error(f打分失败: {e}) return {reason: 调用或解析异常, score: 0.0}聚合层负责把多条新闻按股票合并成日度因子。同一只股票一天可能有多条新闻取均值能平滑单条噪声class SentimentStockSelector: def __init__(self, scorer: NewsSentimentScorer, sleep_s: float 0.5): self.scorer scorer self.sleep_s sleep_s def build_factor(self, news_list: List[Dict[str, str]]) - pd.DataFrame: rows [] for i, news in enumerate(news_list): text f【{news[title]}】 {news[content]} res self.scorer.score_one(text) rows.append({ ts_code: news[ts_code], score: res[score], reason: res[reason], }) logger.info(f[{i1}/{len(news_list)}] {news[ts_code]} 得分 {res[score]:.2f}) time.sleep(self.sleep_s) df pd.DataFrame(rows) factor df.groupby(ts_code).agg( score(score, mean), reason(reason, first), ).reset_index() return factor def select_top(self, factor: pd.DataFrame, top_n: int 2) - pd.DataFrame: portfolio factor.sort_values(score, ascendingFalse).head(top_n) for _, r in portfolio.iterrows(): logger.info(f入选 {r[ts_code]} 得分 {r[score]:.2f} 理由 {r[reason]}) return portfolio主流程把配置、打分器、选股器串起来用样例新闻跑一遍if __name__ __main__: cfg load_config() scorer NewsSentimentScorer(client_kwargs, cfg[llm][model]) selector SentimentStockSelector(scorer, cfg[scoring][sleep_between_calls]) mock_news [ {ts_code: 600519.SH, title: 一季报净利润大增25%超预期, content: 高端直营渠道扩张带动净收入远超分析师估算。}, {ts_code: 000063.SZ, title: 传遭供应链制裁面临芯片断供, content: 核心射频芯片采购受阻产业链担忧升温。}, {ts_code: 300750.SZ, title: 发布凝聚态电池能量密度翻倍, content: 新电池能量密度达500Wh/kg计划年内量产装车。}, {ts_code: 600519.SH, title: 辟谣珍藏产品降价传闻, content: 官方称出厂价坚挺网络零售崩盘说法不实。}, ] factor selector.build_factor(mock_news) selector.select_top(factor, cfg[selector][top_n])跑之前确认依赖装好pip install openai pandas loguru。Python 版本建议 3.10 以上tomllib 需要 3.11低版本换成 tomli 即可。5. 验证请求跑通打分与选股回测先做最小验证确认通道通、JSON 能解析。单独发一条新闻from openai import OpenAI client OpenAI(api_keysk-你的Key, base_urlhttps://taotoken.net/api) resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: 只输出JSON{\score\: 0.5}}], response_format{type: json_object}, ) print(resp.choices[0].message.content)返回能正常解析成字典说明 Key、base_url、模型名三者对上了。然后跑完整脚本预期日志类似[1/4] 600519.SH 得分 0.85 [2/4] 000063.SZ 得分 -0.90 [3/4] 300750.SZ 得分 0.90 [4/4] 600519.SH 得分 0.20 入选 300750.SZ 得分 0.90 入选 600519.SH 得分 0.52注意 600519.SH 两条新闻一条 0.85 一条 0.20均值 0.52这就是聚合层在起作用。如果只取第一条会高估利好只取最后一条会低估。均值让因子更稳。回测验证不用接真实行情也能做。把 factor 里的 score 当作信号构造一个简单的 T1 收益对照假设得分前 2 的股票次日平均收益为正得分后 2 的为负用历史新闻和对应日收益做相关性检验。核心是确认因子方向和强度而不是追求精确收益。你可以把 factor 导出成 CSV和行情数据按 ts_code、日期对齐算 IC 值factor.to_csv(sentiment_factor.csv, indexFalse) # 与行情对齐后计算 rank IC ic factor.merge(returns, onts_code)[[score, next_ret]].corr(methodspearman) print(ic)IC 稳定为正说明舆情因子有选股价值接近 0 或为负就要回头调 Prompt 或换模型。6. 本篇常见错排查报错一JSONDecodeError返回内容带前缀杂质。即使设了 response_format模型偶尔仍会输出“Here is the JSON”之类的前缀或被截断成残缺 JSON。解决方式是在解析前用正则提取花括号区域import re m re.search(r\{.*\}, raw, re.S) data json.loads(m.group(0)) if m else {score: 0.0, reason: 解析失败}同时把 System Prompt 里的“禁止 markdown 围栏”写得更直白temperature 保持 0.0。报错二429 RateLimitError批量调用被限流。新闻抓取是爆发式的几百条一起发很容易触发限流。除了在循环里加 time.sleep更稳的做法是用 tenacity 做指数退避重试from tenacity import retry, wait_random_exponential, stop_after_attempt retry(waitwait_random_exponential(min1, max60), stopstop_after_attempt(5)) def score_with_retry(self, text): return self.score_one(text)报错三得分超出 [-1, 1] 范围。模型偶尔会输出 85.0 这种溢出值直接进聚合会污染因子。在 score_one 里加一道钳制data[score] max(-1.0, min(1.0, data[score]))报错四base_url 写错导致 404。统一通道地址是 https://taotoken.net/api不要在后面拼 /v1 或加查询参数。如果请求返回 404先检查 base_url 是否被编辑器自动补全改过。报错五模型名不存在。不同模型名对应不同能力写错会直接报 model not found。在模型对话页面先手动确认模型名可用再写进 config.toml。7. 下一步从单点评分到多智能体协作单点打分能给出日度个股观点但生成不了周度行业综述和投顾报告。要把“行情收集、舆情打分、文本撰写”串成流水线需要多智能体编排。这套打分器可以作为其中一个 Agent 的打分节点输出结构化因子供下游消费。长期跑这类编码和 Agent 任务Coding Plan 的配额策略比按次调用更省心接入方式在接入文档里有完整说明。先把今天这套 config.toml settings.json 打分脚本跑通再往上叠协作层地基才稳。