多市场(沪深 / 港股 / 美股 / 虚拟货币)投研工具,也是我自己每天在用的个人投资 AI 助理,代码全部开源。
它要回答的不是"这支股票好不好",而是**"今天我该做什么"**——每个交易日开盘前推一份操作清单到微信:买哪支、什么价位区间、买多少股多少钱、止损放哪、盈亏比多少、胜率要超过多少才是正期望。我拿着它在券商手动下单,系统全程跟踪,触发止损或目标位时立刻提醒。
它不做的事:不替你下单(AI 模拟盘除外,那是隔离的虚拟资金),不给"稳赚"承诺,不在数学期望没验证出来之前假装它已经验证过。
-
能用代码算的,不交给模型推断。止损位、目标位、盈亏比、仓位、买入价格区间全是本地算的,AI 只负责判断和解释。踩过一次教训:招股章程原文里明明写着"視乎超額配股權行使與否而定",模型仍然输出"没有绿鞋安排"——术语一变它就认不出来了。能确定性判定的事实,就用代码判定,把结论直接告诉模型。
-
数学期望是个数字,不是形容词。期望 = 胜率 × 平均盈利 + 败率 × 平均亏损。这个数长期为正,重复执行才会赚钱。
expectancy.py按正确的复核窗口过滤样本、分档算胜率盈亏比,样本不足时返回"尚未验证"而不是一个看起来精确的数字——基于噪音算出的期望值比没有数字更危险,它会让人以为自己有依据。 -
胜率没验证时,用盈亏比兜底。目标价距离是止损距离的 3 倍时,胜率超过 25% 期望就是正的。所以清单只列盈亏比达标的机会,每条写明"胜率超过 X% 就是正期望"——把"猜这次能不能对"换成"算赔率够不够",这是胜率未知时唯一数学上站得住的做法。
-
时间尺度必须对齐。做 5-10 天短线就不能用 12 个月的机构目标价算赔率,也不能用四周持有的 2 倍 ATR 止损。这个错误犯过两次:英伟达的盈亏比因此从 7.0:1 虚高,改用近期阻力位和匹配周期的止损后是 0.3:1。周期一变,止损、目标位、复核窗口全都要跟着变。
-
数据源缺口不等于事实。"富途没有泡泡玛特的半年报"和"泡泡玛特不发半年报"是两回事,把前者当后者讲给用户,他就会以为这家公司真的不披露。接口拿不到就去公开网页找,找不到就说找不到,不猜。
-
不绑定单一数据源。每类数据按时效、完整度、可用性和口径一致性选择主源,并保留独立兜底;实时港美行情优先 Futu,沪深历史行情优先 BaoStock,公开资料不足再走网页检索。某一源慢、断或字段不完整时,应切换到同口径的可用源并标明时效,不能为了“统一”而返回旧数据或空数据。
-
诚实的追踪记录。每次判断记录当时价格,满 7 天回填实际价格算方向一致率。复核窗口曾经因为一个 bug 实际只有 1 天——用一天的价格噪音去检验一个按季度起作用的信号,测到的不是信号质量。这类错误的可怕之处在于它会产出一份看起来很专业的回测报告。
每个交易日按北京时间自动跑,结果推到微信:
| 时间 | 动作 |
|---|---|
| 06:30 | 全市场初筛和数据更新,为当天清单提供候选池 |
| 08:15 | 港股开盘前可执行清单 |
| 21:00 | 美股开盘前可执行清单 |
| 交易时段每 10 分钟 | 盘中盯盘:触发条件才推,纯脚本不调 AI |
| 17:50 | 记录期望值验证进度,不发送交易提示 |
两份盘前清单是唯一会调用 AI 的推送,用完整候选池、最新行情和硬风控闸门 跑两次高质量筛选,不把额度消耗在安静时段的轮询上。盘中盯盘虽然每 10 分钟跑 一次,但整条链路不经过模型(见下一节),休市直接跳过,所以它既不花钱也不会 在没事发生时打扰你。午盘/收盘/泛资讯这类定时播报仍然不发。
清单里每支给的是可以直接下单的参数:
思格新能(06656·HK)买入 84分
昨收 361.2
买入区间 344.5 ~ 372.8
高于 372.8 就别追了——那个价位盈亏比会跌破2:1
买入 100 股(约 3,281 元),每手100股
[超短线1-2天] 止损 337.78(-6.5%) 目标 368.0(5日高点) 0.3:1
[短线5-10天] 止损 326.07(-9.7%) 目标 387.2(10日高点) 0.7:1
- “买入”不等于可下单:首页研究榜只展示研究观点;只有当天盘前清单同时具备“可执行”状态、买入区间、按实际手数取整的股数、止损、目标与不低于风险档案门槛的盈亏比,才会被列为可直接下单标的。缺任一项就只作为观察,不会伪装成正式推荐。
- 买入上限是解出来的:盈亏比随买入价上升而恶化,令 (目标−买入)/(买入−止损) = 风险档案中的最低盈亏比,反解得到"买到这个价以上这笔不值得做"的分界线。跳空高开时尤其有用。
- 仓位按显式风险档案反推:新开仓只有在
data/risk_profile.json已配置时才会给出数量;档案定义起始资金及币种、单笔风险、单票上限、日损失上限和最低盈亏比。资金会按生成清单时的实时汇率换算成人民币;缺失时仍可研究和观察,但不会假定你的承受能力来生成买入数量;示例见risk_profile.example.json。 - 两档周期并排:同一支票超短线和短线的结论经常相反——刚冲高的票超短线该等回踩,一周维度上趋势没坏。给模糊的中间答案不如把两档都摆出来。
- 区分"不该买"和"买不起":盈亏比够但一手就超过仓位上限的单独列一档,注明是资金规模的约束而不是系统不看好。
交易时段每 10 分钟检查一次,只在真正需要动的时候出声。五类触发条件:
| 类型 | 条件 | 面向 |
|---|---|---|
| 到价 | 用户自己设的"涨到/跌到 X" | 任意标的 |
| 止损 | 跌破早上清单给的止损位 | 持仓 |
| 目标 | 触及目标价 | 持仓 |
| 急涨急跌 | 当日涨跌超阈值但还没到止损/目标 | 持仓 |
| 回到买点 | 候选股跌到接近清单里的止损位 | 自选 |
到价提醒是一次性的:触发后自动停用,不是每天重推。价格在某条线附近来回 波动时每天推一次,用户会关掉通知,然后真出事的那条也一起被埋掉。
整条链路不经过模型,也不消耗任何 AI 额度——推送文案是纯脚本拼的(见下面 "推送为什么不经过模型")。休市时两层保险直接跳过:cron 的小时段只覆盖交易 时段,脚本自己再查一次开市状态,节假日和午休 2 秒返回"跳过",不拉行情也不 发消息。
0-100 分,六个维度加权:基本面 22 / 价格位置 20 / 技术面 20 / 筹码面 20 / 分析师预期 8 / 数据确定性 10。权重按短线重排过——5-10 天的窗口里季度级别的基本面改善不会兑现,所以基本面从主线退成准入门槛,技术面和筹码面升为主线。
喂给模型的数据:财务摘要(年报关键指标 + 最近四个季度趋势)、估值分位、技术面、52 周位置、筹码面(主力资金 / 空头持仓 / 机构持股 / 内部人交易)、分析师一致预期与逐家评级、晨星估值、市场环境(大盘方向 / 所属板块今日 / 南向资金)、公司概况、营收构成、近 10 日主力资金趋势、美联储点阵图,以及公开网页材料。
市场环境是补的最后一块:六个维度原本全在看个股自己,没有一处问"现在整体是什么环境"。大盘系统性下跌时基本面再扎实的票也跟着跌——先看环境再看个股,是专业和业余最容易分辨的分界线。
- AI 咨询(右下角):多轮聊天,能看你自己的持仓和历史判断,接了实时行情/财务/估值分位/新闻/观察池排名共 5 个工具。硬性红线:公司的业务、上市时间、财务数字、可比公司估值只能来自工具查询,查不到就说查不到——这条是实测踩出来的,问一只不存在的股票时它曾编出完整的业务描述和财务数据。
- AI 模拟炒股:内置 AI 用虚拟资金自主管理一个富途 SIMULATE 模拟盘(永远不碰真实资金)。盯盘器每 2 分钟只拉行情不调 AI,异动超过阈值或距上轮超过 15 分钟才唤醒决策——比固定间隔既省调用又反应更快。持仓、收益曲线、完整交易记录对所有访客公开。
- AI 战绩墙:每条判断在生成时记下当时价格,到期由系统按固定回看窗口自动补录事后价格,逐条公开"当时说买入 / 后来涨了还是跌了",亏的一条不藏。胜率只统计买入/卖出这类声称了方向的判断,分母直接写在标题里("方向判断胜率(131条中说对49条)")——持有/观望没有方向可对错,不计入。
- AI 基金经理:模拟盘的净值曲线叠加标普500基准(两条都归一到100),加年化收益/最大回撤/夏普/胜率/平均持仓/区间换手。样本不够的指标一律不显示并写明还差几个交易日:把一两天的涨跌按 252 个交易日外推出来的"年化"不是收益率,是放大后的噪声。
- 组合风险体检(持仓页):市场/币种暴露、两两相关性矩阵、年化波动率、对基准的 Beta 与压力测试、历史 VaR/CVaR、按 HHI 折算的有效分散度。全部本地 numpy/pandas 算,不经过 AI。港币和美元合并成一个美元敞口(联系汇率制),否则会让人以为分散在两个币种。
- 市场全景(行情页):跨资产温度计(VIX / 美债10年期 / 美元指数 / 金油铜,六项都能点进详情页看 K 线和"这是什么/怎么看")+ 板块热力图(面积=成交额,颜色深浅=涨跌幅度)。
- 新股认购(港股 / 沪深 / 美股三个标签切换):
- 港股:招股要素(保荐人/基石/超额认购/绿鞋/回拨)+ AI 打新判断 + 近 120 天首日表现统计(均值/中位数/上涨占比/破发率——均值和中位数都给,因为首日收益是典型长尾分布,实测 57 只的均值 +52.3% 是中位数 +11.6% 的 4.5 倍)+ 打新收益测算器(把孖展最容易被忽略的一点显式算出来:利息按认购额里借的那部分计,收益只按中签金额算)+ 开盘 vs 收盘散点图(实测持到收盘更划算的只占 53%,基本是抛硬币)。
- 美股:同一套首日统计和散点图,但 AI 简报和测算器都是按美股机制重写的,不是港股那份换个币种——美股由承销商配售,没有"中签率"和回拨机制,简报改看承销商档次 / 基石锚定 / 锁定期与流通盘,测算器把"中签率"换成"获配比例"、砍掉孖展(美股 IPO 认购是现金冻结),改为并排算开盘就卖 vs 持到收盘两种卖法的预期收益。
- 沪深:形态不同是数据决定的,不是偷懒——这个 Futu 账号没有沪深行情权限(实测
get_stock_filter/get_market_snapshot/request_history_kline对 SH/SZ 一律返回"行情权限不足",只有get_ipo_list/get_plate_list/get_stock_basicinfo这类静态数据放行),取不到历史新股的首日 K 线就算不出首日表现统计。改成用接口直接给的发行 PE vs 行业 PE,那是沪深打新判断贵贱的通行口径。
- 虚拟货币:20 个主流币行情。24 小时不休市,周末其它三个市场全是死数据时这一栏是唯一活的。
- 截图记账:在券商下完单,把成交截图发微信给 OpenClaw,它读图后调
quick_record.py写库,回显完整持仓让你核对,错了回一句"撤销"。 - 宏观议题 / 财经日历 / 热门板块 / 指数详情页。
app.py # 入口:全部 UI 渲染
assets/theme.css # 全站唯一的样式来源:设计 token + 所有选择器
data_sources.py # 所有外部数据源:AkShare/BaoStock/新浪/东财/Futu OpenAPI
advisor.py # 判断引擎:六维打分、多空辩论、组合分析、全市场初筛
assistant.py # AI 咨询窗后端:多轮对话 + 工具调用
analysis.py # 详情页流式分析
charts.py # K线/均线/MACD/成交量/热力图/散点,本地算不经过 AI
tracker.py # SQLite:判断记录、持仓、到价提醒、追踪一致率
portfolio_risk.py # 组合风险:相关性/波动率/Beta/VaR/HHI(纯计算,可脱离 Streamlit 测)
sim_metrics.py # 模拟盘指标:年化/回撤/夏普/胜率/换手(同上,样本不够就不给数)
ipo_calc.py # 打新收益测算:孖展利息、盈亏平衡涨幅、历史分布摘要
daily_plan.py # 每日操作清单:止损/目标/盈亏比/仓位/买入区间
plan_push.py # 开盘前把清单推微信(纯脚本,不经模型)
intraday_watch.py # 盘中盯盘:触发条件才推
position_report.py # 港美股午盘/收盘持仓报告 + 收益结算
expectancy.py # 数学期望验证:分档胜率、盈亏比、维度预测力
quick_record.py # 命令行记一笔买卖,给微信截图代填用
sim_trader.py # 模拟盘下单层(硬编码 SIMULATE,安全边界)
sim_agent.py # 模拟盘决策循环
sim_watch.py # 模拟盘事件驱动盯盘器
trade_alert_push.py # 模拟盘成交推送
ipo_brief.py # 新股简报(港股/美股各一套提示词)
macro_brief.py # 宏观议题解读
web_research.py # 网页兜底取数:接口拿不到的去公开网页找
warm_home_cache.py # 每分钟预热首页指数快照
ops/intraday_watch.cron # 盘中盯盘的 crontab 配置与依据(已安装)
openclaw-skill-invest-agent.md # OpenClaw 的项目使用说明
三个 *_risk.py / *_metrics.py / *_calc.py 刻意只做算术:不碰 Streamlit、
不自己取数,取数在 app.py、渲染也在 app.py。这样这些数学能脱离页面单独跑
测试——portfolio_risk 是拿合成因子模型反解验证的(设定载荷 1.2/1.1,反解出
Beta 1.19/1.12),sim_metrics 是拿"30个交易日每天恰好+1%"的构造数据验的。
清单和报告里全是要照着下单的价位和盈亏金额。让模型复述一遍,等于给每个数字加一次被改写的机会——它没有理由把 154.00 写成 145.00,但它确实会。所以 plan_push / position_report / intraday_watch / trade_alert_push 全是纯脚本直出,数字从数据库到微信中间没有第二次生成。
这条规矩是踩出来的:早期成交提醒交给 agent 处理,提示词写了"没有待推送就只输出 NO_REPLY",模型不照做、先说了句开场白,而 cron 的 announce 会把最终回复原样发出去——微信上每 5 分钟收到一条"我来检查AI模拟炒股的成交通知情况。"
- 数据源:Futu OpenAPI(港美股实时行情/财报/筹码/新股,需本地跑 OpenD 网关)+ AkShare + BaoStock(沪深)+ 东财 + 新浪;接口拿不到的走
web_research.py(DuckDuckGo + r.jina.ai,都免密钥) - AI:故障转移链 —— Gemini 免费档 → Gemini 付费档,统一走
chat_with_failover;多空辩论里空头单独从智谱起步(prefer只调整起点、不裁剪链条),免得两方退化成同一个模型的左右手互搏。任何调用点都不该自己复刻供应商选择逻辑,这条是踩了三次才立的规矩:额度预检查、多空辩论、最终裁决三处各自维护了一份"供应商清单",加第三家时全部漏掉,前两家欠费时功能整个失效却够不到第三家 - 前端:Streamlit(
@st.fragment拆分刷新粒度)。视觉走一套 CSS 设计 token(间距 6 档 / 字号 6 档 / 字重 3 档 / 文字色 3 档 / 圆角 2 种),全部定义在assets/theme.css的:root里,.streamlit/config.toml的主题配置跟它一一对应——那边管 Streamlit 自己渲染的控件(含 CSS 够不着的下拉弹层、tooltip、dataframe 网格线),并且在首屏绘制前就生效,两边同步才不会闪一下。文字色最低 4.9:1(改造前最高频的那档是 2.19:1,AA 下限 4.5) - 调度:两套并存,改之前先确认东西在哪一套里。系统 crontab(
CRON_TZ=Asia/Shanghai)跑warm_home_cache/advisor/expectancy/macro_brief/ipo_brief/market_recommendation/intraday_watch;OpenClaw 自己的调度表(~/.openclaw/state/openclaw.sqlite的cron_jobs)跑ai-sim-agent/ai-sim-snapshot/sim-agent-*/hk|us-mentor-scan/stock-advisor-*等 20 多个任务。只看crontab -l会以为一堆任务没在跑。市场时段类任务的统一口径是*/N 0-5,9-16,21-23(北京时间,对应港股沪深 09-16、美股 21-23 和次日 00-05)。服务器时区已设为 Asia/Shanghai,所有面向用户的时间统一北京时间,数据库存储仍用 UTC - 部署:VPS + Nginx + systemd,GitHub Actions 自动部署
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# .streamlit/secrets.toml 填 GEMINI_API_KEY / GEMINI_FREE_API_KEY(AI 主链)
# SUPABASE_URL / SUPABASE_KEY(登录)
# SERPER_API_KEY(网页检索,可选,没有就退回 DuckDuckGo)
# ZHIPU_API_KEY(可选,多空辩论让空头从独立供应商起步)
streamlit run app.py港美股实时行情需要另外跑 Futu OpenD 网关(127.0.0.1:11111),没有时自动退化成延迟行情。
独立脚本单独跑:
python3 daily_plan.py # 生成今天的操作清单
python3 expectancy.py # 看策略验证进度
python3 position_report.py --session hk-mid --dry-run
python3 quick_record.py --buy --symbol 00700 --shares 100 --price 442.8写独立脚本时注意:Futu SDK 的线程不是 daemon 线程,不强制退出会挂住,所以结尾要 sys.stdout.flush() 之后再 os._exit(0)——顺序不能反,os._exit 会跳过 stdout 缓冲区刷新,管道输出时内容会整个丢掉。
- 东财接口有限流保护(请求间隔+重试);akshare 的 V8 反爬引擎和 BaoStock 会话状态都加了全局锁串行化
- 宽基指数成分股用"当前涨幅最大一批股票"代理,非严格官方名单;行业主题指数用手动维护名单,按季度调整
- 港美股实时数据依赖本地 Futu OpenD 网关,所有查询统一走一个常驻 worker 线程(跨线程调用会导致 SDK 内部状态错乱卡死),进程刚起来时连接握手要几十秒到几分钟;这个进程本身有非daemon线程,会导致部署重启时进程无法优雅退出,systemd 要等满优雅退出超时才强杀
- 只支持股票/指数/虚拟货币,不支持基金/债券/期货/期权(AI 模拟盘的候选池里有几支跨资产 ETF,但那是当普通股票交易的)
- 策略的数学期望目前还没验证完成。复核窗口在 2026-09-04 之前实际只有 1 天(bug),那批数据口径不对不能用;按正确的 7 天窗口重新积累的样本还不够,
expectancy.py会如实返回"尚未验证"。在验证跑出结果之前,清单是筛选线索和执行参数参考,不是已证明有效的策略 - 财报数据只有年报 + 季报(部分港股中小盘富途只收录年报,那种情况会退回网页搜索兜底),没有实时的业绩快报
- 美股指数(SPX/DJI/IXIC)富途不支持,大盘参照用 SPY 代替
- 网页兜底依赖 DuckDuckGo,调用频繁会被反爬拦截(响应里带 anomaly 字样),限流时会在日志里明说而不是静默返回空——静默的话,"被挡住了"和"网上确实没这条信息"长得一模一样
- 本地 Python 3.14 / 线上 3.10,语法检查必须拿线上解释器跑。本地
ast.parse通过不代表线上能跑:f-string 表达式里的转义引号在 3.12+ 合法、3.10 直接SyntaxError,而那是导入期错误,推上去就是全站白屏 - 富途模拟环境只有 1 个账户(实测
get_acc_list返回一条 SIMULATE 记录)。所以"多个 AI 策略同台比赛"没法各用一个真账户跑,要做得改成纯账本式的虚拟账户 - 港股暗盘、超额认购倍数、保荐人历史战绩这三项拿不到:富途接口没有这些字段,akshare 的
stock_ipo_hk_ths名字带 hk 但实测返回的是 沪深数据(代码是 001246/301716 这种),港交所披露易要逐份 PDF 解析 - 图表一律只读(
fixedrange=True+dragmode=False),不能缩放拖动——这是刻意的,不是没做
-
Futu 连接和调用不在同一线程会直接卡死不返回:十几处调用点全部踩了同一个坑,改成一个常驻 worker 线程统一排队处理,从架构上保证连接和调用永远同一线程。后续又加了看门狗线程盯心跳,真卡死时自动换代自愈。
-
一个查不到的股票代码能拖垮整个批量请求:知名股名单扩容时手滑加了一个 Futu 查不到的代码,
get_market_snapshot是整批一起查的,一个代码无效导致其余几十支全部失败,静默掉进 akshare 全市场快照兜底(实测能到 100 秒以上,因为 akshare 那条路径本身是分页拉全市场)。这个回归上线好几天都没被发现——批量接口的单点故障排查起来比想象中隐蔽,加代码时应该逐个验证而不是攒一批一起加。 -
多线程写 stdout 全缓冲,外部监控看不到任何进度会误判成卡死:脚本被 cron/exec 起来(非交互式终端)时 stdout 默认全缓冲不是行缓冲,一个批量任务如果中间十几分钟不产生新的一整块缓冲区,外部监控完全看不到输出,判定"卡住了"直接杀掉进程——而脚本自己其实在正常跑,只是输出还没冲刷。加
sys.stdout.reconfigure(line_buffering=True)+ 周期性打印进度两条修复,缺一不可。 -
回填价格的时间窗跟固定时刻的定时任务撞在了一起:某个"次日回填"逻辑用的是"距上次任务启动正好 N 小时"做判断依据,但产生数据的那一步排在整个任务流程的后段,落库时间总比任务启动时刻晚一截——这个固定偏移量导致回填条件永远差一点点达不到,不是偶尔慢,是每次都会卡住,回填命中率长期是 0 而没人发现。
-
AI 聊天接工具调用时,"先探测要不要调用工具"的写法会让首字延迟从毫秒级退化到秒级:先做一次不流式的探测请求判断模型要不要调用工具,再决定走哪条分支——这一步本身可能要好几秒,且完全没有流式反馈,看起来跟卡死一样。改成直接发一次带
tools参数的流式请求,边收边判断是文本增量还是分片的tool_calls(后者要按 index 累积),多数不需要工具的问题体验上跟没接工具之前完全一样快。 -
交互式组件不包独立刷新单元,一次输入会拖着整页重跑:聊天输入框这类高频交互组件如果跟其他慢速渲染逻辑写在同一个脚本作用域里,没有用框架自带的"局部刷新"机制包起来,每次提交都会触发整页从头重新执行——哪怕其他部分都有缓存,重新走一遍渲染逻辑本身也有实打实的耗时,用户会感觉"发个消息都要等好久"。
-
AI 分析概率性返回空内容:模型隐藏的思考过程和正式回答共用同一个
max_tokens预算,思考过程吃满预算时正式回答一个字都不剩,看起来像接口失败实际是预算问题,多个调用点因此漏排查过。 -
详情页反射型 XSS 叠加 token 写回 URL,能被用来接管账号:股票名称直接从URL参数渲染没转义,同时登录 token 又被拼进几乎所有跳转链接——独立 Agent 审查揪出,转义修复+token 改用浏览器端
history.replaceState摘除,不再触发 Streamlit rerun。 -
内部页面切换时上一页内容残留:加载遮罩注入的 JS 内容字节不变时 Streamlit 不会重新挂载,嵌入时间戳注释强制每次都重新执行。
-
三处数据源裸调用没有异常保护,包括 沪深详情页默认打开就看到的分时图——接口一抖整页崩掉,补上兜底后统一走已有的降级展示。
-
GitHub Actions 配置时顺带发现 math-agent 的部署密钥已经失效一个多月:服务器迁移后忘了同步更新部署密钥,静默失败没有任何提醒。
-
富途
get_market_snapshot有"每 30 秒最多 60 次"的限流,一个 20+ 支的自选清单每 3 秒刷新一次直接踩爆:持仓/自选页每支股票各自单独调一次快照接口,清单一大(20+ 支)等于每 30 秒发出去接近 200 次请求,远超限流阈值,请求卡住不返回,页面表现为长时间转圈。改成把整批代码打包进一次get_market_snapshot调用,不管清单多长,每次刷新固定只占 1 次调用额度。两个各自独立刷新的 fragment(持仓行情/今日收益)之前靠"共用同一个函数的 3 秒缓存"这种隐性假设互相省请求,其中一个换成批量接口后这层隐性耦合就断了、另一个会退化回单独查询重新撞限流——批量化要两处一起改,不能只改一边。 -
富途模拟账户自己的持仓市值/浮动盈亏字段不是实时重估的:
position_list_query返回的market_val/pl_val是账户侧记账用的估值,实测同一支持仓连续查了 45 分钟数字纹丝不动,哪怕对应股票当时在正常交易——直接拿这两个字段画收益曲线,走势会长期是一条死直线。改成查到持仓清单后,再用一次真实报价快照按最新成交价重新计算市值和浮盈,不信账户侧给的这两个字段。 -
AI 调用没设超时,卡住的线程占着连接一直不放:
judge_stock_with_debate/judge_stock这类调用之前完全没配超时,个别股票的输入内容会让模型挂住不返回,SDK 默认 600 秒兜底,ThreadPoolExecutor里真正卡住的线程还杀不掉,是真实的资源泄漏,不只是那几支票判断不出来这么简单。修复分三轮才对:第一次只加超时参数没生效,因为客户端配了max_retries=2,SDK 内部重试把超时预算又乘了 2-3 倍;第二次关掉重试后又刚好卡在外层 deadline 失败,因为主力超时+兜底调用耗时经常刚好逼近外层预算;第三次把两个超时数字分开设置(互不冲突)、且给多头/空头两个供应商都补上失败互相回落的兜底(原来只有一边有),才真正解决。 -
AI 聊天窗每条消息都重新握手连接:
_client()每次调用都new一个OpenAI(...),底层是httpx.Client,等于每条消息都要走一次全新的 TCP+TLS 握手——多轮连续对话里这个延迟会被反复叠加。单轮分析场景(analysis.py/advisor.py)问题不大,但聊天窗是这个项目里少数真正高频连续调用的场景,改用st.cache_resource把客户端缓存成整个 app 共享的单例,底层连接池能被 keep-alive 复用。 -
OpenClaw 自己的模型配置分散在两份文件里,只改一份会留半个坑:给某个模型换端点/改 ID 时,
~/.openclaw/openclaw.json的models.providers和~/.openclaw/agents/main/agent/models.json是两份独立配置,agents.defaults.subagents.model.primary、某个插件自己的model字段等好几处分别引用不同的模型 ID 字符串——只改主模型那一处、留着别处引用一个已经不存在的旧模型 ID,会导致特定 cron 报"model not found"这种表面看跟改动无关的错误,且只在那条特定链路触发,不会立刻暴露。换模型/端点时要全局搜一遍所有引用点,不能只改"看起来是这个"的那一处。 -
富途模拟账户会在没有任何下单记录的情况下自己丢持仓:某天美股两支模拟持仓(连同应有的现金余额)无预兆消失,账户被恢复成默认初始现金——查遍
simulated_orders表完全没有对应的卖出记录,用户也确认没有手动操作过,判断是富途平台侧的问题,不是代码逻辑或者 AI 决策导致的。这类第三方模拟盘的账户状态不能假设是只增不减的可信数据源,重要节点(比如改动预算/重置账户前)最好都留一份独立于账户本身的记账副本(这个项目里是sim_virtual_cash_hkd+sim_equity_snapshots),账户自己报的数字出问题时才有得对账、能修正。 -
后台调试脚本忘了清理,占着的连接会在几小时后变成别的故障的根因:排查某个超时问题时起了一批一次性调试脚本(
nohup ... &),问题解决后忘了 kill,脚本本身没有退出逻辑,一直挂在后台占着 Futu 连接——几个小时后另一个完全不相关的功能开始出现莫名其妙的查询超时/数据异常,排查了很久才发现是这批忘记清理的僵尸进程在抢连接。用nohup/disown起后台调试脚本时,问题一旦解决应该立刻回去确认进程真的退出了,不能假设"脚本跑完自然会退"。 -
周期一变,所有派生参数都要跟着变:把持有周期从"1-4周"缩短到"5-10天",如果只改提示词里的措辞,止损还是 2 倍 ATR(那是四周持有的波动幅度)、目标位还是 20 日高点(四周才可能摸到),算出来的盈亏比会系统性偏低——承担四周的下行、只吃一周的上行。改完之后 Nutanix 的盈亏比从 0.2 变成 1.8,不是票变好了,是之前的口径本来就不对。
-
时间尺度错配比数字错误更隐蔽:机构一致预期普遍是 12 个月目标价,拿它给 5-10 天的交易算赔率,英伟达会显示 7.0:1(换成近期阻力位后是 0.3:1)。数字本身是真的、来源是权威的、计算是正确的,唯一的问题是两个时间尺度对不上。这类错误不会报警,只会让人对着一个虚高的赔率下单。
-
"报告期"和"公告日"差最多 191 天:SK海力士 2025 年报的报告期末是 2025-12-31,实际公告日是 2026-07-10。按报告期算"距今 249 天"会把一份七月才公开、市场刚消化完的财报,误判成八个月前的老数据。报告期末那天数据还没公开,市场不可能反应——公告日才是信息进入市场的时刻。
-
缓存住一次失败,会污染一整天:行业分类查询挂着 24 小时缓存,某次网络抖动返回空、这个空结果被钉了一整天,表现为"这支股票查不到行业",单独重查却查得到。改成空结果抛异常——
st.cache_data不缓存抛异常的调用,正好是要的语义。 -
同一个概念在不同数据源叫不同名字,模型认不出来:招股章程原文写"視乎發售量調整權及超額配股權行使與否而定"(超额配股权就是绿鞋),第三方聚合站写"绿鞋 無"。把两份材料一起喂给模型并在提示词里说明"这两个词是一回事",它仍然输出"没有绿鞋安排"。最后改成代码层面直接检测那个短语,把结论写成一句话塞进材料——能用代码确定性判定的事实,不要交给模型从长文本里推断,尤其是材料里还存在相反说法的时候。
-
模型会用真实的数字回答错误的问题:问"周五收盘价多少",助手答了一个真实存在的价格,但那是"昨收"字段(周四收盘),而周五收盘价在"最新价"字段里。盘后场景下"最新价"才是当日收盘价。数字能在数据里对上,日期也说得出来,唯独选错了字段——这比编造数字更难被发现。修法是在工具返回里写清每个字段的含义,而不是指望模型自己理解字段名。
-
给了"要全能"的指令,等于解除了"不许编"的约束:为了让助手别动不动说"看不到",提示词写了"网站数据范围外的股票也要正常凭知识回答"。结果问一只不存在的股票时,它编出了完整的业务描述、借壳上市时间、营收增速、可比公司及其估值倍数。反直觉的是,同一个问题裸调模型(不带这段提示词)反而是安全的——它会指出代码不符合编码规则。修法是把边界写死:允许"用工具查了再分析",不允许"凭印象生成"。
-
服务器时区不是 UTC 也不是本地时区:VPS 系统时区是 America/Los_Angeles,按"8:25"写的开盘前推送实际会在北京时间 23:25 触发——港股早收盘了。改系统时区一次覆盖所有
astimezone(),比逐个文件写死ZoneInfo可靠:后者每加一个新脚本都要记得写,迟早会漏。 -
只做语法检查发现不了跨模块的名字错误:把两个函数名加进了错误模块的 import 块,
py_compile全部通过(它只做语法检查,不解析跨模块名字),部署后整站 ImportError 白屏。正确做法是静态解析所有 from-import 的名字,去目标模块里确认存在——直接import app不行,那会把整个页面也执行一遍,报的错跟导入无关。 -
读倒序列表的错误一端,会算出一个像模像样的错数字:净值快照的查询是
ORDER BY snapshot_at DESC(最新在前),而指标模块里写了.iloc[-1]当"最新值",取到的是最老那条。页面上"区间收益 -0.07%"和同一屏的"累计收益率 -0.31%"自相矛盾了一整天没人发现——因为 -0.07% 本身是个完全合理的数字。同一个 bug 让最大回撤在时间倒序的序列上跑cummax,算出来的是"倒着看的最大涨幅",照样输出一个像模像样的负数。取首尾之前先显式排序,不要依赖调用方的返回顺序。 -
同一份数据两条取数路径,早晚会漂:模拟盘顶部净值走"按成交流水核对过的 AI 自有持仓",下面的资产分布饼图直接用了账户原始持仓——同一屏上"总额 $9,969"和"总资产 $11,995"并排显示,差额正好是两笔不属于 AI 的遗留仓位,而顶部还写着"这两笔不计入净值"。过滤逻辑写好了却只用在一处,比完全没写更难发现。
-
Streamlit 的 markdown 会把成对的
$当 LaTeX 吃掉:一条提示里出现两个HK$("借来的 HK$22,500"和"净亏 HK$122"),中间整段被渲染成数学公式,两个美元符号直接消失。单独一个$不成对时不触发,所以旁边"入场费 HK$6,162"一直是好的,很容易误判成不存在这个问题。st.metric不走 markdown,那里反而不能转义。 -
关掉缩放的同时也关掉了唯一的逃生口:图表 config 里写了
scrollZoom: False以为就够了,但 plotly 的笛卡尔图默认dragmode='zoom',鼠标框选和触控板两指都能缩放;而同一份 config 里的doubleClick: False又把"双击复位"堵死了。结果是用户误触缩放后卡在一个空白区间退不回来。真正的开关是轴上的fixedrange=True——范围锁死之后就没有东西需要复位了。 -
日志噪声会在几个月后变成磁盘问题:定时脚本每跑一次往日志写 69 行 streamlit 的 "No runtime found"、8.6KB,有效输出只有 1 行。排查了三轮才找对位置:那些行不是运行时打的,是
import data_sources时约 70 个@st.cache_data装饰器各打一行;而advisor.py自己第 91 行就 import 它,所以在任何调用方里补救都太晚。正解是在 advisor 顶部、它 import data_sources 之前先 import streamlit 把 logger 建出来再压级别。另外两条死路记下来免得重走:只设父 loggerstreamlit没用(streamlit 给子 logger 显式设过 INFO,父级压不住)、设STREAMLIT_LOGGER_LEVEL=error也没用。
MIT License
- Heliotrope
- Claude Code
- Codex