1
行情是什么
本节新词 行情 · 成交流 · 最新价 · 24 小时涨跌
行情就是交易所不停往外播报的"刚刚发生了什么":谁和谁成交了、成交价是多少、订单簿上哪一档变了。你的机器人所有的判断,都建立在行情上。
行情 market data
大白话 交易所对外公开的实时数据。不需要账户,谁都能看。
生活例子 像股票软件上那个不停跳动的价格,或者体育比赛的文字直播:"第 32 分钟,张三进球"。行情就是交易所的文字直播。
Java 类比 一个只读的事件流。你是消费者,交易所是生产者,你只能听,不能改。
成交流 trades
大白话 每成交一笔,就播报一条:什么时间、什么价格、成交了多少个,以及这一笔是"主动买"还是"主动卖"。
主动买 / 主动卖 看是谁"撞上来"促成了这笔成交。买方下单直接吃掉了挂着的卖单,叫主动买 ;卖方下单直接吃掉了挂着的买单,叫主动卖 。也就是 D1 讲的吃单那一方是买还是卖。今天用不到,知道有这个字段就行。
例子 10:00:03 · 60,010 · 0.2 个,意思是 10 点 0 分 3 秒,有 0.2 个 BTC 以 60010 U 成交。一个热门交易对,一秒钟可能有几十上百条。
最新价 和 24 小时涨跌 last price / 24h change
大白话 最新价 :最后一笔成交的价格。24 小时涨跌 :和 24 小时前的价格比,涨了还是跌了多少。这两个加上 24 小时最高、最低、成交量,通常打包成一条"行情摘要"(英文叫 ticker)一起推送。
算一遍 24 小时前价格 58000,现在最新价 60010:涨了 2010 U,涨幅 2010 ÷ 58000 ≈ +3.47% 。
除了成交流和行情摘要,还有两种行情:K 线 (下一节)和订单簿 (D1 讲过,第 4 节起重点讲怎么在本地维护它)。
2
K 线:把一段时间压成四个数
本节新词 K 线 · 开盘价 · 最高价 · 最低价 · 收盘价 · 成交量 · 阳线 · 阴线 · 实体 · 上影线 · 下影线 · K 线周期
一分钟里可能有几百笔成交,人看不过来。K 线就是把这一分钟的所有成交,压缩成四个价格加一个数量:开、高、低、收、量。
K 线的五个数
成交量 这段时间里一共成交了多少个 (所有成交数量加起来)。
算一遍 10:00 这一分钟有 4 笔成交:60010、60040、59980、59995。 开 = 60010,高 = 60040,低 = 59980,收 = 59995。收比开低,这根 K 线是"跌"的。
Java 类比 一个按时间窗口分组的聚合:trades.stream().collect(groupingBy(t -> t.time() / 60, ...)),每组取 first、max、min、last、sum。
| ← high 最高价
+--+--+ ← close 收盘价 (green: close > open, price went up)
| |
| |
+--+--+ ← open 开盘价
| ← low 最低价
red candle: open on top, close at bottom ← 收比开低,这段时间跌了
一根 K 线画成一个"蜡烛":粗的部分从开盘价到收盘价,细线从最高价到最低价。涨是绿色,跌是红色(有的软件反过来,国内股票软件通常红涨绿跌)。
阳线、阴线 和 影线 candle body / wick
大白话 收盘价高于开盘价叫阳线 (这段时间涨了),低于开盘价叫阴线 (跌了)。开盘价和收盘价之间那段粗的叫实体 ;实体上面的细线叫上影线 ,画到最高价;下面的叫下影线 ,画到最低价。
算一遍 用上面 10:00 这一根:开 60010、高 60040、低 59980、收 59995。收 < 开,是阴线 。 实体 = 60010 − 59995 = 15 ;上影线 = 最高价 − 实体顶端(开、收里大的那个)= 60040 − 60010 = 30 ;下影线 = 实体底端(开、收里小的那个)− 最低价 = 59995 − 59980 = 15 。
怎么读 影线说的是"这段时间里去过、但没站住的价格"。上影线很长:冲上去过,被卖回来了;下影线很长:跌下去过,又被买回来了。单独一根说明不了太多,要结合前后几根一起看。
周期 一根 K 线代表多长时间叫 K 线周期 :1 分钟、1 小时、4 小时、1 天都很常用。同一段行情,1 分钟图上是几十根乱跳的小线,日线上可能只是一根。交易软件左上角可以切换周期。
Java 类比 实体 = abs(close − open);上影线 = high − max(open, close);下影线 = min(open, close) − low。
Tips · 看图前先确认颜色 多数交易所和国际软件是
绿涨红跌 ,国内股票软件是
红涨绿跌 ,很多交易所 App 可以在设置里切换。先看清是哪种,别把跌看成涨。怎么看 K 线图、画支撑线和趋势线、加均线和 RSI 这类指标,放在
D11 讲。
拖一下:看 K 线怎么一笔一笔长出来 10:00:00 – 10:02:59 的 24 笔成交(演示数据)
已经发生了几笔成交 10
每根 K 线多长:
1 分钟
30 秒
往右拖,注意最后一根 K 线是怎么变的:还没到时间的那一根叫"正在形成",它的收盘价会随着每一笔新成交变化,最高价、最低价也可能被刷新。再点"30 秒":同样这 24 笔成交会被切成 6 根更短的 K 线,每根里的成交更少,所以更能看出价格在一分钟里是怎么上下晃的。
3
两种拿数据的方式:你去问,还是它来推
本节新词 REST 接口 · 轮询 · WebSocket · 订阅 · 限频
REST 接口 和 轮询
大白话 REST 接口 :你发一个请求问"现在价格多少",它回答一次,就结束了。轮询 :想一直知道最新情况,就只能每隔一段时间再问一次。
生活例子 打电话问快递到哪了。想知道最新位置,就得每隔 10 分钟打一次。两次电话之间发生了什么,你不知道。
Java 类比 就是你天天写的 HttpClient 调接口,加一个 ScheduledExecutorService 定时去调。
WebSocket 和 订阅
大白话 WebSocket :连上之后连接一直保持着,交易所一有变化就主动推给你。订阅 :连上之后告诉它"我要 BTC-USDT 的订单簿变化",它以后就只推这个给你。
生活例子 关注了一个公众号。它一发文章你就收到,不用你天天去问"有新文章吗"。
Java 类比 一条长连接,像 Netty 的 Channel,或者 Java 11 的 HttpClient.newWebSocketBuilder()。你写一个 onMessage 回调,消息来了就被调用。
限频 rate limit
大白话 交易所规定每个人每秒(或每分钟)最多能调几次接口,超了就拒绝你,严重的会暂时封掉。具体限多少各家不同,以文档为准。
为什么重要 轮询问得越勤,越容易撞上限频;问得越慢,错过的变化越多。这是轮询的死结,所以实时行情基本都用 WebSocket。REST 只用来做"一次性"的事,比如拉一次快照(第 5 节)。
轮询 vs 推送:10 秒里价格变了 40 次 演示数据
每隔多久问一次 500 ms
看到了这次变化 错过了(还没问到就又变了) 一次轮询请求
轮询:发了几次请求 –
轮询:看到几次变化 –
轮询:平均晚知道 –
推送:看到几次变化 40 / 40
推送:平均晚知道 ≈ 网络延迟
怎么看:上面一行是轮询,每个点是一次价格变化,绿点是你问到了、红点是你还没来得及问它就又变了(这个价格你永远不知道)。灰色竖线是你发出的每一次请求。"平均晚知道"是价格变了以后,平均要过多久你才问到它。 先看默认的 500 ms:红点不少。再把间隔拖到最小(100 ms):错过的少了,但 10 秒要发 100 次请求,很快撞上限频。下面一行推送只需要连一次,所有变化都能收到。
4
为什么要在自己这边存一份订单簿
本节新词 盘口 · 档位 · 本地订单簿
盘口 和 档位
大白话 盘口 :订单簿最靠近成交价的那几行,也就是买一、买二…卖一、卖二…。平时说"看盘口",就是看订单簿。档位 :订单簿里的一行,一个价格一行。同一个价格上可能有很多人挂单,合起来算一档,只显示总数量。
例子 卖一档:60010 · 0.3 个。可能是 A 挂了 0.1、B 挂了 0.2,行情里只告诉你这一档一共 0.3。
price size
60,030 1.0 ← 卖三
60,020 0.5 ← 卖二
60,010 0.3 ← 卖一 (best ask)
-----------------------
60,000 0.4 ← 买一 (best bid)
59,990 0.8 ← 买二
59,980 1.2 ← 买三
本地订单簿 local order book
大白话 在你自己程序的内存里,维护一份和交易所一模一样的订单簿。策略要看盘口时,直接读内存,不去问交易所。
算一遍 一个网格机器人每秒要看几百次盘口。如果每次都用 REST 去问:一次请求往返至少几十毫秒(比如 50 ms),一秒最多问 20 次,还会撞限频。读内存只要不到 1 微秒。
生活例子 记账:你不会每次想知道余额都去银行柜台查,而是自己记一本账,银行每发生一笔就发短信给你,你照着短信改自己的账本。本地订单簿就是这本账,行情推送就是这些短信。
Java 类比 本地缓存 + 变更通知。跟"MySQL 是源头,本地缓存靠 binlog 同步"是一个思路。难点也一样:怎么保证缓存和源头一致。后面几节讲的全是这件事。
5
全量快照 + 增量更新
本节新词 全量快照 · 增量更新
先拿一份完整的订单簿(快照),然后只接收"哪一档变了"(增量),自己在快照上一条条改。
全量快照 snapshot
大白话 某一时刻订单簿的完整拷贝,通过 REST 接口拉一次。
增量更新 delta / update
大白话 通过 WebSocket 推过来的一条条"修改记录":哪一边(买或卖)、哪个价格、这一档现在一共 多少。
三种情况 ① 这个价格原来就有 → 把数量改成新的; ② 这个价格原来没有 → 新增一档; ③ 数量是 0 → 删掉这一档 (挂单都被吃光或者都撤了)。
容易误会 常见做法是增量给的是"这一档现在一共有多少",不是"加了多少"。卖一原来 0.3,来一条"卖 60010 → 0.5",意思是现在变成 0.5,不是 0.8。具体以各家文档为准。
生活例子 Excel 的修改记录:"B3 单元格改成 0.5""删除第 7 行"。拿着原文件加上所有修改记录,就能还原出最新的表。
Java 类比 MySQL 的全量备份 + binlog。恢复数据库时,先导入备份,再按顺序重放 binlog。
下面的演示里会看到 seq 这个字段:它是每份快照、每条增量身上带的编号,一直往上加 1。现在只要记住"编号越大越新",快照 seq = 100 的意思是"包含到第 100 号变化为止"。第 6 节会专门讲它怎么用。
一步一步应用增量 快照 seq = 100
上一条
应用下一条
重来
怎么看:每点一次"应用下一条",右边列表里高亮的那条就被改到左边的订单簿上,左边被改动的那一档会变色,下面的文字解释这条增量属于"改数量、新增一档、删除一档"里的哪一种。点完 6 条,左边就是交易所 seq = 106 时的样子。
6
序列号:怎么知道有没有漏
本节新词 序列号 · 跳号
每条增量都带一个一直加 1 的编号。你只要检查"这条的编号是不是上一条加 1",就知道中间有没有漏掉。
序列号 sequence number
大白话 交易所给每一次订单簿变化编的号,101、102、103……一直往上加。快照也带一个编号,表示"这份快照包含到第几号为止的所有变化"。
叫法 各家叫法不同,有的叫 sequence,有的叫 u / U。有的交易所一条增量里会打包好几次变化,于是给一个"这条从第几号到第几号"的起止编号,判断规则会稍微变一下。思路都一样:检查新来的这条能不能和上一条首尾接上。具体规则以各家文档为准。
生活例子 银行排队的号码。你手里是 102 号,下一个叫到的应该是 103。如果直接叫到了 105,说明 103、104 你没听到。
Java 类比 TCP 的序号、MySQL binlog 的位点、Kafka 的 offset。都是用一个递增编号来保证"不漏、不乱"。
跳号 gap
算一遍 本地订单簿当前是 104。收到 105,正常,应用,变成 105。下一条收到 107:期望 106,实际 107,跳号了,106 丢了 。 收到 103 呢?它比 105 还小,是旧消息(比如网络重发的),直接扔掉 ,不算跳号。
三条规则
seq ≤ 当前 → 旧的,扔掉
seq = 当前 + 1 → 正常,应用
seq > 当前 + 1 → 跳号,本地订单簿不能再用了(第 8 节讲怎么办)
7
正确的同步顺序:先缓冲,再拉快照
本节新词 缓冲
直觉上应该"先拉快照,再订阅增量",但这样一定会漏掉中间那一段,而且你还不知道漏了。正确做法反过来:先订阅,把收到的增量先存起来,再去拉快照。
缓冲 buffer
大白话 收到的增量先不用,放进一个队列里存着,等快照到了再处理。
生活例子 搬家期间先让快递放到驿站,等新家收拾好了一次性取回来,一件都不会丢。
Java 类比 一个 ArrayDeque<Delta>,快照没到之前只 offer 不 poll。
还有一个检查 快照到了以后,要确认它和缓冲区接得上 :缓冲区里最早那条的编号,必须 ≤ 快照编号 + 1。比如快照 seq = 104,缓冲区从 103 开始:103、104 扔掉,105 接着用,接得上。但如果快照 seq = 100,缓冲区却从 103 才开始(订阅建立得太晚),101、102 两边都没有,接不上,只能再拉一次快照。
1. open WebSocket, subscribe, BUFFER every delta ← 先订阅,收到的先存着
2. GET snapshot via REST (it says: seq = 104) ← 再拉快照
3. drop buffered deltas with seq <= 104 ← 快照里已经包含了,扔掉
4. apply 105, 106, ... one by one ← 按顺序补上
5. from now on: every new delta must be last + 1 ← 之后每条都检查
gap? -> throw the book away, go back to 1 ← 跳号就从头来
两种顺序对比:一步一步点 每个时刻交易所发生一次变化:101、102、103……
先订阅缓冲,再拉快照(正确)
先拉快照,再订阅(错误)
还没发生
收到了,先缓冲
已经包含在快照里
104 划掉:缓冲过,但快照里已经有了,扔掉
已应用到本地
漏掉了
上一步
下一步
重来
滚到这里会自动一格一格播放;点任何按钮就停下,换你自己点。怎么用:先在"正确"模式下一直点"下一步"点到时刻 12,看 101–112 每一格是什么颜色(没有红色);再切到"错误"模式点一遍,看 102–105 变成红色的"漏掉了",而且时刻 6 以后程序照样在应用,自己并不知道。
Tips · 同步顺序口诀:订阅、缓冲、快照、丢旧、接上、应用 ① 先订阅增量;② 收到的先缓冲;③ 再拉全量快照;④ 缓冲里编号 ≤ 快照编号的丢掉;⑤ 检查剩下第一条能不能和快照接上;⑥ 按顺序应用。任何一步对不上,就回到第 ① 步。进组后读行情模块的代码,就按这 6 步去找每一步写在哪。
8
跳号了怎么办:整本扔掉重来
本节新词 重新同步 · 盘口不一致
为什么不能"只补那一条"
大白话 交易所一般不提供"把第 106 号单独再发我一次"的功能。丢了就是丢了。
唯一的办法 重新同步 :把本地订单簿整本扔掉,回到第 7 节的第 1 步,重新缓冲、重新拉快照。这期间本地没有可信的盘口,策略必须暂停下单 。
生活例子 对账时发现流水缺了一页,你不能凭感觉把余额补上。只能去银行打一份最新的完整对账单重新开始。
盘口不一致:忽略跳号的后果
大白话 如果发现跳号但假装没看见,继续应用后面的增量,本地订单簿和交易所的就对不上了,而且会一直错下去,直到那一档再次被更新。
算一遍 丢的那条是"卖 60010 → 0"(卖一被吃光了)。本地没收到,还以为卖一是 60010。策略想"以卖一价买入",按 60010 下了单,可实际上最便宜的卖单已经是 60020 了:你的限价单成交不了,挂在那里干等;或者策略基于错误的价差做了错误的判断。
原则 宁可暂停几百毫秒,也不能拿着错的盘口做决策。这就像数据库主从同步断了,宁可只读主库,也不能读一个不知道错在哪的从库。
动手:丢一条试试 左边是交易所的真实订单簿,右边是你的本地订单簿
处理方式:
发现跳号就重新同步(正确)
忽略跳号继续用(错误)
来一条增量
让下一条在网络上丢掉
拉快照,重新同步
建议顺序:先点几次"来一条增量",看两边同步变化;再点"让下一条丢掉",然后"来一条增量";最后切到"忽略跳号"再丢一次,看右边标红的不一致档位。
9
行情延迟:你看到的是过去
本节新词 行情延迟 · 只做挂单
行情延迟 latency
大白话 交易所那边发生变化,到你的程序收到并更新完本地订单簿,中间花的时间。网络慢、程序处理不过来、垃圾回收卡顿,都会让延迟变大。
怎么量 很多行情消息带有交易所生成它的时间。用你收到的时间减去它,就大致知道延迟多少(前提是两边时钟对得准)。
生活例子 看直播比现场晚了 5 秒。现场已经进球了,你还在看带球。
只做挂单 post-only
大白话 D2 第 3 节讲过的 post-only,这里再说一遍。下单时加一个要求:"我只想当挂单(D1 讲过,手续费更低)。如果我这张单一进去就会马上成交变成吃单,那就直接拒绝,别成交。"
为什么网格常用 网格每一轮只赚一点差价,手续费高一点就可能白干,所以很多网格单会用只做挂单来保证拿到低费率。
延迟会造成什么
算一遍 你看到买一 60000、卖一 60010,决定在 60000 挂一张买单。但行情延迟了 500 ms,这期间价格其实已经跌了 15 U,真实的卖一变成了 59995。 你的买单出价 60000 ≥ 真实卖一 59995 → 一进去就成交,变成吃单 ,付了更高的手续费;如果用的是只做挂单,这张单直接被拒 ,还白白消耗了一次下单机会。
对策 一直监控行情延迟,超过阈值(比如 200 ms,具体由团队定)就暂停下单,等延迟恢复再继续。
拖一拖:延迟多少会出事 价格正在往下走 · 数字只为演示
行情延迟 500 ms
行情有多剧烈:
平静
普通
剧烈
你看到的 买一 / 卖一 60,000 / 60,010
真实的 买一 / 卖一 –
期间价格变了 –
延迟监控(阈值 200 ms) –
怎么用:默认是延迟 500 ms、行情普通,真实卖一已经跌到 59995,比你的出价 60000 还低,你的买单一进去就会成交(和第 9 节"算一遍"是同一个例子)。把延迟往左拖到 100 ms 左右,看它什么时候变回正常的挂单;再点"剧烈",看同样的延迟会差出多少钱。
常见误区 · 直接拿本机时间减消息时间算延迟 交易所的时间戳和你机器的时钟不一定对得准。本机时钟慢了几百毫秒,算出来的延迟可能是负数,或者一直偏大。服务器要开时间同步(NTP);延迟监控看的是趋势和 P99(最慢的那 1%),不要被单个数字吓到或骗到。
10
在 Java 里怎么写
本节新词 TreeMap 存盘口 · 单线程处理
用 TreeMap 存盘口
为什么不用 PriorityQueue D1 讲撮合时用的是 PriorityQueue,因为那里存的是一张张订单,只需要取队头。本地订单簿不一样:增量会说"把 59990 这一档改成 0.3",你要能按价格直接找到某一档去改、去删 。PriorityQueue 删任意元素要一个个找,TreeMap 按 key 找、改、删都很快,还能直接拿到最大或最小的 key(也就是买一、卖一)。
买和卖怎么排 买单:价格从高到低 排,第一个就是买一。卖单:价格从低到高 排,第一个就是卖一。
容易踩的坑 价格用 BigDecimal,别用 double(D2 讲过)。但 BigDecimal 的 equals 会比较小数位数:60000.0 和 60000.00 用 equals 比较是不相等的。TreeMap 按 compareTo 比较,认为它们相等,没问题;要是用了 HashMap,同一个价格就会变成两档。
单线程处理
大白话 同一个交易对的所有增量,只在一个线程里按顺序处理。
为什么 增量必须严格按编号顺序应用。多个线程同时改,105 可能比 104 先改完,盘口就乱了,而且很难复现。单线程天然有序,也不用加锁。
Java 类比 一个交易对一个 Executors.newSingleThreadExecutor(),或者一个线程循环从队列里取消息处理。策略如果在别的线程读盘口,就读一份拷贝,不要直接读正在被修改的 TreeMap。
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.TreeMap;
// 一条增量:编号、买还是卖、哪个价格、这一档现在一共多少
record Delta(long seq, boolean isBid, BigDecimal price, BigDecimal size) {}
// 买:价格从高到低;卖:价格从低到高
TreeMap<BigDecimal, BigDecimal> bids = new TreeMap<>(Comparator.reverseOrder());
TreeMap<BigDecimal, BigDecimal> asks = new TreeMap<>();
long lastSeq; // 本地订单簿当前包含到第几号
// 只在一个线程里调用
void onDelta(Delta d) {
if (d.seq() <= lastSeq) return; // 旧的,扔掉
if (d.seq() != lastSeq + 1) { resync(); return; } // 跳号:暂停策略,重新同步
TreeMap<BigDecimal, BigDecimal> side = d.isBid() ? bids : asks;
if (d.size().signum() == 0) side.remove(d.price()); // 数量为 0:删掉这一档
else side.put(d.price(), d.size()); // 否则:改成新数量
lastSeq = d.seq();
}
// resync():把 bids、asks 清空,策略暂停,回到第 7 节第 1 步重新缓冲、拉快照(自己实现)
BigDecimal bestBid() { return bids.firstKey(); } // 买一价(订单簿为空时 firstKey 会抛异常,实际要先判空)
BigDecimal bestAsk() { return asks.firstKey(); } // 卖一价
七日冲刺 D3 那道编程题叫 applyDeltas("应用一串增量"),就是用 JavaScript 写这个 onDelta:输入一份快照和一串增量,按第 6 节的三条规则处理。
11
词典和自测
今天用到的词都在这里,包括 D1 学过的几个。
自测 8 题
读完以后
回到 七日冲刺 D3 ,按这个顺序过一遍:先对照第 7 节看那张同步流程图;再玩盘口动画,正确做法丢一次包、勾上"忽略跳号"再丢一次;最后做 applyDeltas 编程题,它考的就是第 6 节的三条规则。