加密货币交易 bot 每 5 分钟轮询价格数据,但 API 的 limit 参数未配合显式开始时间,导致静默返回错误窗口。真实踩坑案例,教训:API 参数语义需严格验证。
Another one from the same one-person-AI-company setup I've written about before: Claude Code builds and maintains the code, several agents run unattended on a schedule, nobody's watching in real time. This one's from earlier in the project than the incidents I've posted about before, and it's the simplest of the bunch - which is what makes it worth writing up.
这是同一个"一人 AI 公司"配置的另一个故事——我之前也写过相关的文章:Claude Code 构建和维护代码,多个 agent 按计划无人值守运行,没有人实时盯着。这次的事件发生在之前发的那几个事故更早的阶段,也是其中最简单的一个——正因为简单,才值得写出来。
A crypto trading bot polls a broker's API every 5 minutes, pulls the last hour of price bars, and decides whether to buy, sell, or hold based on recent price movement. Every cycle, it logs its decision - symbol, price, reasoning - to a file. That log was the only thing anyone (human or otherwise) was checking to confirm the bot was doing its job.
一个加密货币交易 bot 每 5 分钟轮询一次券商的 API,获取最近一小时的价格 K 线,根据近期价格走势决定买入、卖出还是持有。每个周期,它都会把决策——标的、价格、推理过程——记录到一个文件里。这个日志是所有人(包括人类和其他程序)确认 bot 是否正常运行的唯一依据。
What actually happened
The code asked the broker's API for "the last N bars" by passing a limit parameter and nothing else - no explicit start time. That seemed like a reasonable way to ask for "the most recent bars." It wasn't. Without an explicit start time, the API silently returned a fixed window - bars starting from midnight UTC that day, sorted oldest-first - instead of the most recent N bars. Two very different requests that happen to share a limit parameter, with no error, no warning, nothing in the response shape that would tip you off.
代码通过一个 limit 参数向券商 API 请求"最近 N 根 K 线",但没有传明确的开始时间。这看起来像是获取"最近 K 线"的合理方式,但实际上不是。没有明确的开始时间,API 默然返回了固定窗口——从当天 UTC 午夜开始的 K 线,按从旧到新排序——而不是最近的 N 根 K 线。这是两个完全不同的请求,只是恰好共用了一个 limit 参数,既没有报错,也没有警告,响应结构中也没有任何能让你察觉端倪的提示。
Practical effect: the bot kept asking every 5 minutes, kept getting an answer, and for over an hour that answer was exactly the same set of bars - the same price, to three decimal places, cycle after cycle. The log showed a fresh timestamp and a fresh "decision" every 5 minutes the whole time. Nothing about the log looked wrong. It just wasn't true - the bot wasn't deciding anything based on current information, it was re-deciding the same stale snapshot over and over and calling it live.
实际效果:bot 每 5 分钟持续请求,持续得到回复,在超过一小时的时间里,回复的 K 线数据完全相同——价格精确到三位小数,每个周期都一样。日志显示每 5 分钟都有新的时间戳和新的"决策"。日志看起来没有任何问题,只是并非事实——bot 并没有根据当前信息做决策,而是在反复对同一份过时的快照做决策,却声称这是实时的。
I only caught it by actually looking at the price values across consecutive log entries during an unrelated review and noticing they were identical - not "similar," identical to the decimal.
我是在一次无关的代码审查中,偶然看到连续几条日志记录中的价格数值时才发现这个问题的——它们完全一样,不是"相似",是精确到小数点后都相同。
Why this one's worth separating from the others
I've written before about two other incidents from this same project - a safety check that fired correctly but never got logged, and a scheduled task that crashed for three weeks while still reporting success. Those both involved something breaking. This one didn't. The bug was in a single missing keyword argument to an API call, the API itself never errored, the process never crashed, nothing timed out. Every individual component did exactly what it was told to do. The bot was, by every internal measure it had, working.
这个事件之所以值得单独拿出来讲,是因为之前我写过同一项目的另外两个事故——一个安全检查正确触发了但从未被记录,以及一个定时任务崩溃了三周却仍在报告成功。那两个事件都涉及某些东西坏了,而这次没有。问题出在一次 API 调用的某个关键字参数缺失,API 从未报错,进程从未崩溃,没有任何超时。每个独立组件都完全按照指令执行了。从 bot 自身的各项指标来看,它都在正常运行。
That's the part that generalizes past this one API's quirk: "the log says something happened every cycle" and "something meaningfully different happened every cycle" are not the same claim, and nothing about a healthy-looking log distinguishes them. A monitoring setup built around "did the process log something recently" - which is most of what I had at the time - is structurally blind to this exact failure mode. It would need to check whether the content changed, not just whether output kept arriving on schedule.
这就是这件事能超出这一个 API 的怪癖推广到一般情况的部分:"日志显示每个周期都发生了某件事"与"每个周期都发生了有意义的不同事情"是两个截然不同的命题,但一份看起来健康的日志无法区分它们。当时我的监控设置是围绕"进程最近是否输出了日志"建立的,而这在结构上对这个特定故障模式完全视而不见。它需要检查内容是否发生了变化,而不仅仅是输出是否按时到达。
What I'm taking from it
Between this and the other two incidents, I've now got three distinct ways an unattended agent looked completely fine from the outside while doing nothing useful: a real result that never got recorded, a crash that got recorded as success, and correct-looking output that was quietly frozen. Three different bugs, same underlying gap - nothing was checking "is the actual work still happening," only "is the process still running."
结合这次和另外两个事故,我现在有了三种不同的方式,让一个无人值守的 agent 看起来完全正常,实际上却在做无用功:真实结果从未被记录,崩溃被记录为成功,以及看似正确但悄然冻结的输出。三种不同的 bug,暴露了同一个根本问题——没有任何东西在检查"实际工作是否仍在进行",只在检查"进程是否仍在运行"。
Curious whether others running scheduled/unattended agents have run into the frozen-but-technically-successful version specifically - it's the quietest of the three failure modes I've hit, in the sense that there's no error anywhere to eventually trip over. You'd only catch it by actually reading the values, which is exactly the kind of check nobody does once something's been running fine for weeks.
很好奇其他运行定时或无人值守 agent 的人是否也遇到过这种"冻结但技术上成功"的版本——这是我所经历的三种故障模式中最安静的一种,因为它在任何地方都不会产生错误。你只有通过实际读取数值才能发现它,而这恰恰是没有人会对已稳定运行数周的东西进行的检查。