← 返回教程库

日志里写着「买入信号」,账户里却什么都没发生

最后更新 2026-08-19
📚 量化与 AI 投研 📖 E 线 · 从规则到代码 ⏱ 约 22 分钟 R2 · 注意风险
你将学到
  • 说得出从信号到持仓中间的五个环节,以及每个环节各自会怎么失败
  • 看到「有信号但没成交」「成交了但数字对不上」这类现象时,能定位到是哪一环出的问题
  • 理解为什么把信号逻辑和下单逻辑写在一起,会让两边都变得没法单独验证

你的日志里明明白白打了一行:某某标的,买入信号成立。

你去看持仓,空的。

你以为是程序崩了,翻上翻下找异常,一个红字都没有。往下多滚几屏,发现日志还在正常地一轮一轮往下走,仿佛刚才那行字只是它自言自语了一句。

这个场景几乎每个刚开始写交易程序的人都会遇到一次,而且第一反应通常都是错的——大部分人会去怀疑信号那段代码,把指标反复打印出来核对。可问题往往根本不在那里。信号是成立的,日志没骗你。只是「信号成立」和「持仓里多了一笔」之间,还隔着好几道关,而每一道关都可以在不报错的情况下把这件事拦下来。

这一篇要做的就是把这几道关拆开摆平。不讲怎么调用接口下单,只讲一件事:从一个念头到一笔持仓,中间到底发生了什么,以及每一步各自会怎么失败。 这个拆分不是为了让你多背几个名词——它直接决定了你的代码该怎么切分,以及日后你能不能把哪一半单独拿出来验证。

先说人话:点外卖的五个环节

把「我想吃碗面」变成「面在我桌上」,中间是这么几步:

第一步,你想吃了。 这是个念头。它成立与否只取决于你饿不饿,跟这个世界上有没有面、你钱包里有没有钱,一点关系都没有。

第二步,你决定点。 这里第一次有外部条件插进来:钱够不够?这家现在营业吗?你今天是不是已经点过三顿了、跟自己说好一天最多两顿?念头成立,不代表你会真的按下单。很多念头就死在这一步,而且死得悄无声息——你不会为「今天不点了」这件事写一条记录。

第三步,订单发出去,商家要接。 你按了下单,钱扣了,但商家那边可能拒单:这个菜卖完了,起送价不够,超出配送范围。这一步的失败是对方拒绝,跟你想不想吃、有没有钱都无关。

第四步,等着。 接单了不等于面到了。可能等很久,可能商家跟你说汤面没了只能给你干拌,可能你等不及了想取消——而你点取消的那一秒,骑手正好按下了送达。

第五步,面到了,但和你想的不一样。 分量比图片小,或者店家把你加的蛋算成了单独一份从你付的钱里扣掉。东西是到了,但账对不上你脑子里那本。

这五步,每一步都可能出问题,而且每一步的问题性质完全不同:第一步是你的判断,第二步是你的约束,第三步是对方的规则,第四步是时间和运气,第五步是记账。

把这五步混成一件事想,出了问题你就没法定位。交易系统里,一模一样。

投资层:五道关,五种翻车现场

现在换成交易的说法。这五步分别叫:信号 → 意图 → 委托 → 成交 → 持仓

先把几个词就地说清楚,免得后面绕:

  • 信号:你那套规则在这一刻给出的判断,「该买」或者「该卖」。它是个是非题的答案,没有金额、没有价格、没有数量。
  • 意图:你决定真的要做这件事,并且定下了做多大——买多少钱、买多少股。
  • 委托(也叫下单、报单):你把这个意图变成一张单子递出去,交给交易所或者券商。
  • 成交:这张单子在市场上真的和对手方碰上了,钱货两清。
  • 持仓:成交沉淀下来的结果,你账户里实际拿着的东西。

这五个词很多人是混着用的——「我买了」这句话里,可能指的是上面任何一个。而在写代码的时候混用,等于把五种完全不同的失败原因搅成一锅

下面一道关一道关看。

关卡一:信号有了,但意图没生成

这是开头那个场景最常见的答案。信号是真的,但它没能变成一个下单意图,原因通常是这么几类:

钱不够。 你的规则说该买,但账户里的可用资金已经被前面几笔占掉了。注意这里有个容易忽略的细节:「账面上的钱」和「现在能用的钱」不是一回事。 你昨天卖出的那笔可能还在结算途中,你挂着没成交的那张委托也已经把一部分钱冻结住了。程序看到的可用资金,往往比你心算的少。

仓位格子满了。 你事先约定最多同时持有几笔,占满了就不再开新的。这不是 bug,这是你自己定的风控在生效。但如果你没意识到它在生效,你会以为是信号出了问题。

这笔太小了,小到不值得做。 交易所和券商对单笔委托都有最小规模的要求,低于这个数根本递不进去。你的资金被分成很多份之后,每一份可能都掉到了门槛以下。

同一个标的已经有仓位了。 你的规则里,重复信号是加仓、是忽略、还是先平再开?这个问题在 「均线金叉就买入」这句话,程序其实读不懂 里作为一个必填的槽位讲过——如果你填的是「忽略」,那么持仓期间所有的买入信号都会被静静丢掉,日志里照样打,账户里什么都不发生。

这一类失败的共同特征:它们全都不是错误,是约束。 程序没有理由报异常,因为一切都按你说的在跑。也正因如此,这一类最难被发现——你需要主动去记录「信号出现了但被约束挡下了」这件事,否则它连痕迹都不留。

一个立刻能做的动作:给每一个「信号成立但没下单」的时刻打一条日志,并且写清是被哪个约束挡的。这条日志的价值,比你打印十次指标数值大得多。

关卡二:意图有了,但委托被拒

意图变成一张单子递出去,对方可以直接说不。常见的理由:

数量或价格的精度不对。 每个市场对「最小交易单位」和「最小报价单位」都有规定。你算出来要买某个金额,除以价格得到一个带一长串小数的数量,直接递出去会被拒。这个数必须先按对方的规矩截齐——而且截的方向很重要,往上截可能就超了你的资金

这个标的现在不让交易。 停牌、临时停牌、退市整理、还没到交易时段。你的规则里可能压根没考虑「今天它不交易」这种情况,因为在历史数据里,停牌那几天要么被填成了平的、要么干脆没有那几行。这块的细节在 行情数据里那些会让你回测结果全错的坑 里拆得更细。

价格越界。 A 股有涨跌停,你报的价超出当日允许的价格带,单子进不去。这一条特别值得留神:涨跌停不是「难成交」,是「这张单子在法律上不成立」,两者在代码里要走完全不同的分支。

账户状态问题。 权限没开、风险测评过期、这个品种需要单独签协议。这一类在实盘才会遇到,回测里完全不存在。

这一类失败的共同特征:对方会明确告诉你被拒了,而且通常有个原因码。 也就是说,只要你的代码认真处理了返回值,这一类是最容易被发现的。可惜的是,很多人写下单那段时是「发出去就不管了」——把返回值扔掉,然后在几个小时后对着空账户挠头。

关卡三:委托发出去了,但没成交(或者只成交了一部分)

单子被接受了,进了队列,然后就是等。这里有三种局面:

一直挂着不成交。 你报的价没人愿意碰。这是限价单的固有代价,限价单与市价单:一个保价格,一个保成交 那篇把这个取舍算得很细。对代码来说,这带来一个必须回答的问题:挂多久算超时?超时之后是撤、是改价追、还是就这么挂着? 这个决定不做,你的系统里就会慢慢堆起一批僵尸委托,占着资金什么也不干。

只成交了一部分。 你想买一千股,市场上那个价位只有三百股,剩下七百继续挂着。现在你的状态很尴尬:你既不是「有仓位」也不是「没仓位」,你是「有三成仓位,另外七成还在路上」。 如果你的代码只有「持仓 / 空仓」两种状态,这里就会开始出乱子——止损该按三百股算还是一千股算?这时候如果出了卖出信号,你卖的是已经到手的那三百股,还是要连带把没成交的委托一起撤掉?

部分成交还藏着一个更阴的坑:你撤单的时候,已经成交的那部分是撤不回来的。 如果那部分小到不够一手、或者小到低于最小交易规模,你会得到一笔卖不掉的持仓——想清掉它,市场规则不允许你递这么小的单子。freqtrade 在处理撤销买入委托时专门为这个情况留了一个判断:如果已成交的那点金额小于最小交易规模,它宁可不撤这张单,因为撤了会留下一个出不去的仓位。这个判断的存在本身就说明,这不是理论上的边角,是真会发生的事。

你想撤的那一刻,它正好成交了。 撤单请求和成交是两件在不同地方发生的事,中间有网络延迟。你发出撤单,对方回你「撤不掉,已经成交了」。这时候你的程序如果假定「我撤了所以没仓位」,就会带着一个错误的世界观继续往下跑,后果可以很严重。freqtrade 的做法是遇到这种竞态直接放弃处理、把这张单留给下一轮循环重新判断——这是个很值得抄的思路:拿不准的时候,不要猜,退回去下一轮再看一眼。

这一类失败的共同特征:程序不会收到任何「失败」的信号。 单子好好的,状态是「未完全成交」,这在系统看来完全正常。是你的业务逻辑需要判断它算不算异常。

关卡四:成交了,但价格和数量都和你以为的不一样

单子成了,这总没问题了吧?还有问题。

成交价不是你报的价。 市价单当然不用说,你根本没报价。但限价单也一样:你报了一个价,实际可能成交在更有利的价位上(对方挂的价比你出的好)。如果你的代码直接用「委托价」去算成本、算止损位,就会和真实情况差一截。

一张单子可能有很多笔成交。 一千股可能是分五次、在五个略微不同的价上凑齐的。你的真实成本是这五笔的加权平均,不是任何一笔的价。

手续费可能从你买到的东西里直接扣。 这一点在数字资产市场特别常见——你花钱买了一个东西,交易所把手续费从那个东西本身里扣掉一小块,于是你实际拿到的数量比订单上写的少。你按订单数量记账,账就永远对不平。freqtrade 为此专门留了一套逻辑,去追查这笔单子的手续费到底是从哪一边扣的、实际到手多少。

这一类失败的共同特征:数字是对的,只是不是你以为的那个数字。 它不会立刻炸,它会在几十笔之后表现为「我的记账和券商对账单对不上」。而对不上多少,取决于你交易的频率——做得越频繁,这道缝张得越大。这也是 交易成本假设 那篇反复强调的:成本不是一个可以事后拍脑袋补上的修正项。

关卡五:持仓这件事本身,也是个需要维护的状态

前面四关都过了,你手上有仓位了。但「有仓位」不是一个静止的事实,它是一个需要不断和外部对齐的状态:

  • 你以为你有,实际可能没有(你手动在券商 App 上卖掉了,程序不知道)
  • 你以为数量是 A,实际是 B(分红送股、手续费扣减)
  • 你的程序重启了,重启之前那些挂在外面的委托,现在是什么状态?

最后这条特别重要:程序停掉的时候,委托不会跟着停。 它们还在交易所那边好好挂着。所以任何一个严肃的交易程序,启动之后要做的第一件事不是找信号,而是先去把外面的实际状态拉回来,和自己数据库里记的对一遍。这个动作的位置,交易机器人开机之后,它一整天到底在忙什么 那篇讲执行循环时提到过——每一轮先看存量再看增量,是同一个道理的不同尺度。

系统层:一个真实框架是怎么切这一刀的

现在回头看代码结构。

如果你去翻 freqtrade(一个用 Python 写的开源自动交易执行框架)的目录,会看到一个挺有说服力的对比:

策略那一侧,接口文件里和「信号」有关的部分非常薄。策略要交出来的东西,本质上就是在每一行数据上标出「这里该进」「这里该出」——是几列是非值。策略不知道你有多少钱,不知道最小交易单位是多少,不知道上一张单子撤没撤掉。它只回答一个问题:按我这套规则,此刻该不该动。

执行那一侧,主程序文件里围绕订单的方法有几十个。翻一眼它们在处理什么:算这笔该下多大、校验这个数量合不合法、下单、查单、超时了要不要撤、撤的时候发现部分成交了怎么办、撤到一半发现已经全成交了怎么办、成交之后手续费从哪边扣的、程序重启后把外面的单子捞回来对账……这一整层,和「该不该买」这个判断,一点关系都没有。

这个对比本身就是答案:信号是一个判断,订单是一段流程。 判断可以很短,流程注定很长。把它们写在一个函数里,那个函数就同时承担了两种完全不同的复杂度。

还有个细节值得单独说:freqtrade 把「这笔下多大」放在了执行侧,但同时留了口子让策略去干预。这个设计其实承认了一件事——仓位大小是个中间物种,它既有策略成分(我对这个信号有多大把握),又有执行成分(我账户里现在有多少钱能用、这个市场的最小单位是多少)。所以它的最终值是「策略提议 + 执行侧按现实约束裁剪」两段合起来的结果,而不是任何一侧单独说了算。你自己写的时候如果遇到「这段该放哪一层」的纠结,多半就是碰上了这类中间物种,答案通常是拆成两段而不是硬塞进一边。

关于「你以为成交了但其实没有」这一类问题,回测引擎里有一整套对应的假设,那是另一个话题——回测里的成交假设:你以为你买到了,那天可能根本没人卖给你 把常见的几种撮合假设排成了一把尺子。这里只提一句关键的:回测里,上面五道关基本被压缩成了一道。 信号出来,引擎按某个规则判定成交,持仓就变了。中间那些拒单、部分成交、撤单竞态、手续费扣减,回测要么完全没有,要么用一个平均数糊过去了。所以回测跑得再顺,也不构成「实盘也会这么顺」的任何证据——这两件事的失败模式根本不在一个集合里。

为什么这条分界线决定了你的代码分层

说到这里,那个实际的问题该回答了:知道这五道关,对我写代码有什么用?

用处是:它给了你一条天然的切口。

信号那一侧有一个非常好的性质——它是纯的。给定同样一段历史数据,它应该永远给出同样的答案。它不需要网络,不需要账户,不需要知道今天是几号。这意味着你可以拿一段构造出来的数据喂给它,然后断言它该在第几行出信号。

订单那一侧完全相反——它全是副作用。它要发请求,要等回应,要处理超时,要在数据库里改状态。它的正确性不体现在「算得对不对」,而体现在「各种意外情况下会不会把状态搞乱」。验证它需要的是另一套办法:把外部世界换成一个你能控制的假货,然后专门制造那些意外——拒单、部分成交、撤单撞上成交,看看你的代码会不会崩。

这两种验证方式没有任何交集。 所以如果你把它们写在一个函数里,会发生什么?

你想测信号,得先造一个假的交易所。你想测「部分成交怎么处理」,得先想办法让指标算出一个买入信号来。每测一半,都得把另一半也搭起来。 结果就是你两边都懒得测了,然后带着一堆没验证过的分支去跑实盘。

更糟的是第二个后果:混在一起写,你会分不清一个 bug 属于哪一半。 实盘跑了一周结果不对,是信号算错了,还是下单那段在某个边角情况下漏了?如果两段是分开的、各自能单独跑,这个问题十分钟能定位;如果搅在一起,你只能对着日志硬猜。

这条分界线怎么落到具体的代码组织上——什么该抽成参数、什么该换成假货、边界情况怎么造——是 把策略写成能被测试的样子 那篇的活。这里只需要你记住那个判断标准:

拿一段固定的数据喂进去,它是不是永远给同一个答案? 是,它属于信号层;不是,它属于执行层。

拿这个标准去扫一遍你现在的代码,你多半会发现有几行放错了地方——比如在算指标的函数里顺手查了一下账户余额,或者在下单的函数里顺手又算了一遍均线。这两处就是缝合的伤口。

一个能当场做的自查

打开你自己写的那个策略文件,从头到尾扫一遍,给每一行贴个标签:

  1. 这一行只用到了行情数据吗? → 信号层
  2. 这一行用到了「我现在有多少钱 / 有多少仓位 / 有几张单子挂着」吗? → 执行层
  3. 这一行会往外发请求、或者改数据库吗? → 执行层
  4. 贴不上标签的? → 大概率就是那个中间物种(仓位大小、风控拦截),值得单独拎出来想想它该拆成几段

然后数一下:信号层和执行层的代码,是不是交错着写在同一段里? 如果是,你现在还改得动;等到跑了半年、加了七八个补丁之后,这一刀就很难下了。

这一篇没讲什么

另外提醒一句:上面所有关于源码的描述,来自对代码快照的阅读,不是对运行结果的观察。你手上那个版本的具体行为,以你自己跑出来的日志为准。

小结

「我想买」和「我买到了」,中间隔着好几件事:你得真的有钱有额度,单子得被接受,得有人愿意跟你成交,成交完账还得对得上。任何一环出问题,结果都是「什么都没发生」或者「发生了但不是你想的那样」,而且大多数时候没有任何东西会提醒你。

所以写代码的时候,把「判断该不该做」和「把事做成」这两件事分开放。前者只看行情,可以反复重放验证;后者全是和外面世界打交道的脏活,得专门去试那些意外情况。分开了,出事你知道该去哪一半找;混着写,你只能猜。

内容有错、看不懂、或想看下一篇?告诉我们 →

本文为投资教育整理,不构成任何投资建议;不荐个股/基金、不预测点位、不承诺收益。关键数据与结论请结合权威来源自行验证,并见风险免责声明