OASIS BLOG
← 回到七日冲刺← D9 笔记
写给没碰过股票、期货、合约的 Java 程序员 · D10

D10 零基础速成笔记

前九天学的是"为什么",今天练的是"手上怎么做"。先弄清哪些事绝对不能碰,再在测试网上用假钱把现货和合约都真的操作一遍,最后用 Java 签名下单。入职以后拿到币,你已经走过一遍了。带下划虚线的词,点一下就能看解释。

怎么读第 1–3 节先读完;第 4–6 节开着测试网边读边操作;第 8–10 节开着 IDE 写代码。全部做完大约要一整天,可以拆成两三次。
每个练习都分四步怎么操作 → 先自己算出预期 → 去界面哪里看 → 对应前面哪天的知识。数字对不上,才是真正学到东西的时候。
读完以后回到七日冲刺的 D10:看"开一单合约"的录屏式演示,用签名计算器验算,做 checkOrder 编程题,把实操数字填进记录表。
1

实战前的红线

本节新词内部信息 · 老鼠仓 · 员工交易政策 · 合规 · 提现权限 · IP 白名单

老板说"进来给你发币去实战",这很正常:交易所希望你用用户的眼睛把业务走一遍。但交易所员工有几条线,跨过去就不是技术问题了。先把线认清,再动手。

内部信息 和 老鼠仓 insider information · front-running

大白话
内部信息:你因为在公司上班才知道、外面的人还不知道的消息。拿它去给自己下单,行内叫老鼠仓,是交易所员工最典型的红线,被查到通常直接开除。
哪些算
哪个币要上线或下架;大客户的挂单和持仓;带单员还没公开的仓位;策略参数;风控阈值;还没发布的活动和费率调整。
怎么算都不行
用自己的钱、用家人朋友的账户、在本交易所、在别家交易所,都一样。告诉别人让别人去下单,也一样。
算一遍
你在群里看到"明天 10 点上线 XYZ"。外面的人不知道,你今天买入 1,000 U,上线后涨 30% 卖出,赚 300 U。这 300 U 来自其他不知情的用户,这就是为什么它是红线。

员工交易政策 employee trading policy

大白话
公司写好的"员工自己能不能炒币、怎么炒"的规定。大多数交易所都有,内容各家不同。
常见内容
个人交易账户要报备;有的品种禁止交易;某些时间段不能交易(比如上币前后);买入后要持有一段时间才能卖。
你要做的
入职第一周主动找 HR 或合规同事要来看一遍。没看过之前,个人账户什么都别动。

公司发的币怎么用

先问清
① 是测试环境的假币,还是正式环境的真币?② 亏了算谁的,赚了归谁?③ 能不能转出,能不能转到个人账户?④ 能交易哪些品种,杠杆上限多少?最好留下书面或聊天记录。
默认做法
当成公司资产:不转出、不和个人账户混用、不上高杠杆。目的是熟悉系统,不是赚钱。

API Key 的安全

大白话
程序下单要用一对"账号 + 密码":API Key 和密钥(第 9 节细讲)。它能直接动账户里的钱,要比你的登录密码还小心。
三条规矩
① 只开读取和交易,不开提现权限,这样就算泄露,别人也转不走钱;② 绑定 IP 白名单,只允许你那台机器的 IP 调用;③ 密钥放在环境变量里,不写进代码、不提交到 Git、不贴进聊天工具和外部 AI。

合规:你自己要弄清楚的事 compliance

大白话
合规就是"做的事符合法律和监管要求"。虚拟货币在不同国家和地区的监管差别非常大,你个人的风险取决于具体情况。
要问清
公司在哪里注册、有没有牌照;服务哪些地区的用户;你本人在哪里工作;劳动合同和哪家公司签、工资怎么发。
这里不下结论
这份笔记不能替你做法律判断。拿不准就花点钱问一次律师,比事后补救便宜得多。公司代码和内部数据也不要贴进任何外部 AI 工具。
一句话记住:在公司知道的事不拿来交易;公司的币按公司的规矩用;Key 不开提现、不进仓库;合规主体自己问清楚。
2

测试网:真操作、假钱

本节新词测试网 · 模拟交易 · 正式环境

测试网是交易所自己搭的一套"练习场":界面、下单接口、撮合都是真的,钱是假的。D4 的模拟盘是你在本地假装成交,测试网是交易所真的在帮你撮合。

测试网 和 模拟交易 testnet · demo trading

大白话
测试网:和正式交易所隔开的另一套环境,给开发者练接口用。模拟交易:有的交易所在网页里直接给一个"模拟"开关,用假钱点界面。两者目的一样:不花真钱把流程走通。
生活例子
驾校考场:场地、车、考官都是真的,只是不上真马路。D4 的模拟盘更像在家用方向盘玩赛车游戏。
对比
模拟盘(D4):行情是真的,成交是你自己的程序判断的。测试网:成交是交易所的撮合引擎做的,会遇到真实的拒单、部分成交、限频。正式环境:真钱、真用户。
Java 类比
模拟盘是单元测试里的 mock;测试网是公司的 test / staging 环境;正式环境是 prod。
常见例子现货合约备注
Binance现货测试网 testnet.binance.vision合约测试网 testnet.binancefuture.com主要给接口练习用
OKX网页和 App 里有模拟交易模式,接口也有对应的模拟环境在正式账户里切换
Bybit有独立的测试网站点单独注册

网址、注册方式、赠送的假币数量和规则经常变,以各家官网文档为准。访问任何交易所网站,都要遵守你所在地和公司的规定。入职以后,以公司自己的测试环境为准:你们自己的交易所一定有测试或预发环境,问带你的同事要地址和测试账号。

测试网的几个"不像真的"的地方

价格会偏
测试网的用户很少,价格可能和正式环境差几百甚至几千 U。练习时以测试网界面上的价格为准,别拿外面的价格去算。
盘口很薄
挂单的人少,一张稍大的市价单就能吃穿好几档,滑点会比正式环境夸张得多。这反而是练习滑点的好机会。
会被重置
余额和订单可能定期清空,别把测试网当成长期存东西的地方。
规则可能简化
资金费、强平、手续费的规则可能和正式环境不完全一样。对不上的时候,先怀疑是不是测试网的设置不同。
  • 选一家同时有现货和合约测试环境、并且有下单接口的
  • 拿到假币后,先截图记下初始余额(后面对账用)
  • 在测试网上建一个 API Key,按第 1 节的三条规矩设置权限(第 9 节会用)
常见误区 · 测试网成交了,实盘也会一样测试网人少、盘口薄:挂单常常等不到对手,市价单又可能一下吃穿好几档;价格有时和正式环境差得很远。它只适合练流程、核对公式、验证代码,成交速度、滑点、手续费等级都不能当成实盘的参考。
3

看懂合约交易界面

本节新词最新价 · 当前委托 · 历史委托 · 成交记录 · 资金流水 · 合约张数 · 条件单 · 止盈止损 · 开多 · 开空 · 保证金率 · ROE

各家交易所的永续合约界面布局大同小异,都是七块:顶部行情栏、K 线、订单簿、最近成交、下单面板、仓位栏、下面几个记录标签页。认得这七块,换哪家都不慌。

+---------------------------------------------------------------------+
| [1] BTCUSDT Perp | Last | Mark | Index | Funding + countdown | 24h  |
+-----------------------------+--------------+------------------------+
| [2] Chart (candles)         | [3] Order    | [5] Order form         |
|                             |     book     |  Isolated/Cross | 5x   |
|                             +--------------+  Limit/Market/Stop     |
|                             | [4] Recent   |  Price  Qty  TP/SL     |
|                             |     trades   |  Reduce-only Post-only |
|                             |              | [Open Long][Open Short]|
+-----------------------------+--------------+------------------------+
| [6] Positions: size entry mark liq margin ratio uPnL ROE            |
+---------------------------------------------------------------------+
| [7] Open orders | Order history | Trade history | Transaction log   |
+---------------------------------------------------------------------+

1 顶部行情栏:交易对(Perp = 永续)、最新价、标记价格、指数价格、资金费率和结算倒计时、24 小时成交量 · 2 K 线 · 3 订单簿 · 4 最近成交 · 5 下单面板:逐仓 / 全仓、杠杆、限价 / 市价 / 条件单、价格、数量、止盈止损、只减仓、只做挂单、开多 / 开空 · 6 仓位栏:数量、开仓均价、标记价格、强平价、保证金、保证金率、未实现盈亏、收益率 · 7 当前委托 / 历史委托 / 成交记录 / 资金流水。

几个一看就懂、但容易看错的字段

三个价格
最新价是最后一笔成交的价格,跳得最快;标记价格用来算盈亏和强平;指数价格是几家现货的加权价(D2)。界面上最大、最醒目的那个数通常是最新价,不是算强平用的那个。
开多 / 开空
开多:买入开仓,赌涨。开空:卖出开仓,赌跌。平仓时方向反过来:平多是卖,平空是买。有的界面直接写"买入 / 卖出",要看旁边有没有"开仓 / 平仓"的切换。
数量单位
有的交易所用币的个数(0.01 BTC),有的用合约张数:一张代表多少币各家不同,比如一张 = 0.01 BTC 或一张 = 100 U。下单前一定看清单位,这是新手最常见的"下错十倍"。
条件单
先不挂进订单簿,等价格碰到你设的触发价才变成真正的订单。止盈止损就是最常用的条件单:涨到某价自动平仓锁利润,跌到某价自动平仓限亏损。
保证金率
仓位离强平还有多远。各家定义不同,常见一种是"维持保证金 ÷(保证金 + 未实现盈亏)",越大越危险,到 100% 就强平。也有交易所反过来定义,越小越危险,看界面上的说明。
ROE
收益率 = 未实现盈亏 ÷ 初始保证金。它是按你押的保证金算的,不是按仓位价值算的,所以 5 倍杠杆下价格涨 1%,ROE 就是 +5%。
四个记录页
当前委托:还挂着的订单。历史委托:已经结束的订单(全部成交、撤销、被拒)。成交记录:每一笔成交,一张订单可能有好几条(D2:订单和成交是两回事)。资金流水:每一笔钱的进出,包括手续费、资金费、已实现盈亏(D8 的账本在界面上的样子)。
点一点:通用合约交易界面无品牌示意 · BTCUSDT 永续 · 逐仓 5 倍多单 0.01 BTC

怎么玩:依次点 1 到 7 号区域,下面会解释这一块看什么、新手最容易看错什么、对应前面哪天。默认选中的是 6 号仓位栏:你会看到强平价 48,241.2 和 ROE +5% 是怎么来的,这两个数和第 5 节练习 1 的算例一致。

4

现货手动练习

本节新词冻结 · 可用余额 · 手续费币种

先从现货开始:没有杠杆、没有强平,最适合练"下单以后钱去哪儿了"。每个练习都先把预期算出来,再去界面上核对。

冻结 和 可用余额 frozen / locked · available

大白话
挂一张买单,交易所先把这张单要花的钱冻结起来,防止你用同一笔钱再下别的单。可用余额 = 总余额 − 冻结。撤单就解冻,成交就从冻结里真正扣掉(D8 讲过后台怎么做)。
生活例子
酒店入住押金:前台先在你信用卡上"预授权"500 元,钱还在卡里但不能花;退房时按实际消费扣,剩下的解冻。
Java 类比
账户表里有 available 和 frozen 两列,下单时在一个事务里 available -= x; frozen += x。

练习 1:挂一张不会成交的买单,再撤掉

操作
现货 BTCUSDT,限价买入 0.01 BTC,价格填当前价的九折左右(比如当前价 60,000 就填 54,000)。挂上以后看资产页,再撤单,再看一次。如果被价格保护拒了(第 6 节),就把价格往当前价靠近一点,比如九五折。
先算
冻结 = 0.01 × 54,000 = 540 U。假设你有 1,000 USDT:挂单后可用 460、冻结 540;撤单后可用 1,000、冻结 0。
看哪里
资产页的"可用 / 冻结"(有的叫"下单锁定");当前委托里出现这张单,撤单后移到历史委托,状态是"已撤销"。
对应
D2 订单状态机;D8 资产冻结。

练习 2:小额市价买,看滑点和手续费

操作
市价买入 0.002 BTC。下单前先截图订单簿的卖一到卖三。
先算
假设卖一 60,000 有 0.001 个,卖二 60,050 有 0.001 个。你会吃掉两档:0.001 × 60,000 + 0.001 × 60,050 = 120.05 U,均价 60,025,比卖一贵 25 U,也就是贵了 0.042%(约 4.2 个基点,1 个基点 = 0.01%)。测试网盘口薄,滑点常比这个还大。
看哪里
成交记录里会有两条(每吃一档一条);订单详情里的"成交均价";手续费那一列。
对应
D1 滑点、吃单;D2 一张订单对应多笔成交。

练习 3:挂单和吃单的手续费对比,外加一个坑

操作
用"只做挂单"下一张会慢慢成交的限价买单,再用市价下一张同样数量的买单。成交后对比两条成交记录的手续费。
先算
现货手续费常见在 0.1% 上下,挂单常比吃单便宜或一样,各家不同。买 0.001 BTC、费率 0.1%:手续费 0.000001 BTC。
坑:手续费币种
买入时,手续费常从买到的币里扣(有的交易所从平台币或 USDT 里扣)。你买了 0.001 BTC,实际到账 0.000999。紧接着想卖 0.001,会报"余额不足"。先看资产页的真实数量再卖。
对应
D1 挂单 / 吃单、手续费;D8 手续费和 VIP 等级。

练习 4:部分成交后撤单

操作
在买一附近挂一张大一点的限价买单(比如 0.01 BTC),等它成交一部分就撤单。测试网人少,不一定等得到,等不到就看下面的录屏演示。
先算
挂 0.01 @ 59,990,冻结 599.90 U。成交 0.004 后,冻结里扣掉 0.004 × 59,990 = 239.96 U,还剩 359.94 U 冻结。撤单后,剩下的 359.94 全部解冻,可用 = 1,000 − 239.96 = 760.04 U。
看哪里
历史委托里这张单的状态是"已撤销",但"已成交数量"是 0.004。这就是 D2 说的最容易漏的状态。
对应
D2 部分成交后撤单;D5 对账。
录屏:一张现货买单的一生假设初始 1,000 USDT · 忽略手续费

下单面板

资产

委托 / 成交

怎么玩:它会自己一步步播放,像一段录屏。看被框起来的那一块,三个数字"可用 / 冻结 / BTC"每一步怎么变。想停下来看就点"暂停",再用上一步 / 下一步。最后一步把账对平:花掉的 239.96 U 正好换来了 0.004 BTC。

Tips · 手续费扣完后剩下的"粉尘"买 0.001 BTC、手续费 0.1% 从币里扣,到账 0.000999。想全部卖掉时,要按 lotSize(比如 0.00001)向下取整成 0.00099,剩下 0.000009 BTC 卖不掉,这就是"粉尘"。不少交易所有"小额资产兑换"功能处理它;写机器人时,卖出数量一律用"真实余额向下取整",别用"当初买了多少"。
5

合约手动练习

本节新词触发价 · 双向持仓 · 单向持仓 · positionSide

合约练习的目标只有一个:界面上的每个数,你都能自己算出来,或者说得出为什么算不出来。下面的例子统一用:BTCUSDT 永续,逐仓 5 倍,限价开多 0.01 BTC @ 60,000。

练习 1:开一张最小的逐仓多单,核对强平价

操作
切到逐仓,杠杆设 5 倍,限价开多 0.01 BTC(测试网价格不是 60,000 就用它的价格,数量用它允许的最小值也行)。成交后把仓位栏的开仓均价、强平价、保证金抄下来。
先算
仓位价值 = 0.01 × 60,000 = 600 U。保证金 = 600 ÷ 5 = 120 U。
强平价(D2 公式,维持保证金率按 0.5% 举例)= 60,000 × (1 − 1/5) ÷ (1 − 0.5%) = 48,000 ÷ 0.995 ≈ 48,241.21。
对不上怎么办
差几十 U 很正常:维持保证金率是分档的,小仓位可能不是 0.5%;有的交易所会预留平仓手续费;有的把开仓手续费从保证金里扣。把界面上的维持保证金率找出来(通常在合约信息或杠杆档位里),代进公式再算一次。
对应
D2 强平价推导;D8 阶梯保证金。

练习 2:挂止盈止损

操作
给这张多单设止盈 63,000、止损 57,000。注意触发价的类型选项:按最新价触发,还是按标记价格触发。
先算
止盈触发:盈亏 = (63,000 − 60,000) × 0.01 = +30 U,ROE = 30 ÷ 120 = +25%。止损触发:−30 U,ROE −25%。
坑
按最新价触发,一根插针就可能把你止损出去,然后价格又弹回来;按标记价格触发更稳,但触发时成交价可能比止损价差一些。止盈止损单一般自带"只减仓",不会反手开出新仓位。
对应
D2 三个价格和插针、reduce-only。

练习 3:等一次资金费结算

操作
看顶部的资金费率和倒计时,在结算时刻之前持有仓位,结算后去资金流水里找"资金费"那一行。
先算
资金费 = 仓位价值 × 资金费率。假设结算时标记价格 60,600,费率 +0.01%:0.01 × 60,600 × 0.0001 = 0.0606 U。费率为正,多头付给空头,所以你的资金流水是 −0.0606。
坑
只有结算那一刻持仓才收付,差一秒平仓就不收也不付。结算间隔常见每 8 小时,也有 4 小时、1 小时的,各家不同。
对应
D2 资金费率;D8 资金费结算任务。

练习 4:手动平仓,看已实现盈亏

操作
用市价单平掉这张多单(平多 = 卖出),假设成交在 60,600。然后看资金流水。
先算
价差盈亏 = (60,600 − 60,000) × 0.01 = +6 U。手续费举例:开仓那张限价单是挂单,600 × 0.02% = 0.12 U;平仓用市价单是吃单,606 × 0.05% = 0.303 U。
净结果 = 6 − 0.12 − 0.303 − 0.0606(练习 3 的资金费)= +5.5164 U。和总页 D10 录屏最后一步是同一笔账。
如果改用限价单挂在 60,600 等成交,平仓就是挂单,手续费变成 606 × 0.02% = 0.1212 U,净结果 +5.6982 U。差的这 0.18 U 就是挂单和吃单的区别。
坑
界面上的"已实现盈亏"有的只算价差(+6),有的扣了手续费,有的连资金费也算进去。以资金流水里一笔一笔的记录为准,自己加一遍。
对应
D1 已实现盈亏;D8 清算和账本。

练习 5:切到双向持仓,同时拿一多一空 hedge mode

大白话
单向持仓:一个币只有一个净仓位,先买 0.01 再卖 0.01 就是平掉。双向持仓:多仓和空仓分开记,可以同时拿着 0.01 多和 0.01 空。
操作
在偏好设置里切到双向持仓(常见要求先平掉所有仓位、撤掉所有挂单才能切)。开多 0.01,再开空 0.01,看仓位栏出现两行。
坑
双向模式下,平仓要说清楚平哪一边。接口里用 positionSide 字段(LONG / SHORT)区分:平多 = 卖出 + positionSide=LONG。写程序时漏了这个字段,常见结果是下单被拒。
对应
D8 单向和双向持仓。

练习 6:同一张单,逐仓和全仓的强平价差多少

操作
平掉上面的仓位,切到全仓,再开一张同样的 0.01 多单,对比强平价。
先算
全仓时整个账户余额都在给这张单兜底。只有这一张仓位时,强平条件是:余额 + (P − 60,000) × 0.01 = 0.5% × P × 0.01。
余额 200 U:P = (600 − 200) ÷ (0.01 × 0.995) ≈ 40,201.01,比逐仓的 48,241.21 远得多。
余额 1,000 U:算出来 P 是负数,也就是价格跌到 0 也不会强平,界面上的强平价会显示成"--"。
对应
D2 逐仓和全仓。
核对器:把测试网上的数填进来单个仓位 · 维持保证金率按单档算 · 手续费按仓位价值算
初始保证金–
逐仓强平价–
全仓强平价(只这一张)–
未实现盈亏(按标记价)–
ROE–
一次资金费–
开仓 + 平仓手续费–
平仓后净结果–

怎么玩:默认值就是本节练习 1、3、4、6 的算例,打开页面你就能看到 120 U、48,241.21、40,201.01、+6 U、+5%、−0.0606、净 +5.5164(平仓按吃单 0.05%)。去测试网操作完以后,把界面上的真实数字填进来,看看哪一个对不上,再按练习 1 的"对不上怎么办"找原因。资金费按标记价计算,平仓后净结果 = 价差盈亏 − 两次手续费 − 多头付出去的资金费(费率为负时变成加)。

Tips · 每天按资金流水对一次账界面上的"已实现盈亏"有的只算价差,有的扣了手续费,有的连资金费都算进去,各家不同。最可靠的做法:把资金流水按类型加起来。本页那一笔就是 价差 +6 − 开仓手续费 0.12 − 平仓手续费 0.303 − 资金费 0.0606 = +5.5164 U。和余额变化对上了,才算真的弄懂了这一笔。
6

故意犯错练习

本节新词拒单 · 价格保护 · 最小下单金额 · checkOrder

真正干过的人,都知道交易所会因为哪些原因拒单。今天故意把每种错都犯一遍,看交易所怎么回。以后线上用户说"下不了单",你第一反应就知道查哪几项。

下面这张表里的原因代码,是我们自己起的名字,不是哪家交易所的真实错误码(各家的错误码和提示文字都不一样)。它们是简化版规则,真实规则各家不同,细节以文档为准。回到总页 D10 的编程题 checkOrder,就是把这张表写成代码:下单前自己先挡一遍,别等交易所来拒。

原因代码什么意思在测试网上怎么制造对应
PRICE_TICK价格不是 tickSize 的整数倍tickSize 是 0.1 时,价格填 60,000.05D2 精度
QTY_LOT数量不是 lotSize 的整数倍lotSize 是 0.001 时,数量填 0.0015D2 精度
MIN_NOTIONAL价格 × 数量小于最小下单金额下一张只值几 U 的单D2 最小下单金额
PRICE_BAND价格离标记价格太远,触发价格保护买价填标记价的 1.2 倍D8 价格保护
POST_ONLY_TAKER只做挂单的订单一下单就会吃掉对手单勾选只做挂单,买价填卖一或更高D2 post-only
REDUCE_ONLY只减仓的订单没有仓位可减、方向和仓位同向,或者数量超过仓位持有 0.01 多,挂只减仓卖 0.02D2 reduce-only
MARGIN保证金不够:价格 × 数量 ÷ 杠杆 > 可用余额下一张远超余额的开仓单D2 初始保证金

价格保护 和 拒单

大白话
拒单:交易所收到了你的订单,但检查不通过,直接退回,订单根本没进订单簿。价格保护:限价单的价格离标记价格太远(比如超过 ±5%),交易所就拒掉,防止手滑把 60,000 输成 6,000 一下把盘口砸穿。
算一遍
标记价 60,000,保护范围 ±5%:买价上限 60,000 × 1.05 = 63,000,卖价下限 60,000 × 0.95 = 57,000。买价 63,500 会被拒,卖价 56,900 也会被拒。
坑:不是所有错都会被拒
有的交易所遇到 reduce-only 超量不拒单,而是自动把数量缩到仓位大小;有的遇到精度不对会自动取整。你在测试网看到的行为,就是这家交易所的真实规则,记下来。
下单检查器:先自己挡一遍tickSize 0.1 · lotSize 0.001 · 最小 100 U(为了演示设大一点)· 标记价 60,000 · 保护 ±5% · 买一 59,999.9 / 卖一 60,000.0 · 可用 100 U · 5 倍 · 持有 0.01 多

怎么玩:点上面的七个按钮,每个都会填好一张"犯了某种错"的单子,下面列出它会被哪条规则拦下、为什么。也可以自己改价格和数量。一张单可能同时犯好几种错,比如"精度不对"那个会同时报 PRICE_TICK 和 QTY_LOT。检查顺序和总页 checkOrder 编程题一样。

7

行话对照:别让人一听就是新手

本节新词爆仓 · 插针 · 深度 · 搬砖 · 基点

行话不是为了装,是为了说得准。"价格不对"可能指五种不同的问题,"最新价插针、标记价没动"只指一种。

场景像新手像干过
买入开仓"我买了个合约""开了个 BTC 永续多单,逐仓 5 倍,0.01 个"
把空单买回来"把卖出去的买回来了""平空了" / "空单止盈了"
仓位被强平"钱突然没了""爆仓了,强平价 48,241,离开仓价 −19.6%"
价格瞬间跳一下又回来"价格抽风""刚才插了根针,最新价打到 58,000,标记价没怎么动,所以没触发强平"
挂单的人少"卖的人不多""深度很薄,卖一到卖五加起来才 0.3 个,大市价单滑点会很大"
成交价比看到的差"成交价不对""吃了三档,均价比卖一差 25 U,滑点 4 个基点左右"
多空之间定期转账"要交一笔钱""现在费率万一,多头付空头,下次结算还有 3 小时"
两边差价赚钱"两个地方价格不一样""A 所比 B 所高 30 U,扣掉两边吃单手续费还剩多少?值不值得搬砖"
收益百分比"赚了百分之五""未实现盈亏 +6 U,ROE +5%,按保证金算的"
下了一张不想吃单的单"我下了个单""挂了个 post-only 的限价单,会吃单就直接拒"
止盈单防反手"设了个卖出""止盈单带了 reduce-only,不会反手开空"
费率大小"百分之零点零一""1 个 bp"(1 个基点,万分之一)

这几个词再说一遍

爆仓
就是被强平,口语说法。更严重的是"穿仓":亏损超过了保证金(D2)。
插针
K 线上一根很长的影线,像一根针:价格瞬间跳到很远又马上回来,通常是因为盘口薄、有人砸了大单(D2)。
深度
订单簿里各档挂单加起来有多少。深度好 = 大单也不容易把价格砸穿。
搬砖
跨交易所套利:在便宜的地方买、贵的地方卖(D9)。
基点
万分之一,英文 bp。0.05% = 5 个基点。手续费、资金费、滑点都常用它说。
8

公开接口实操:不用账号也能练

本节新词curl · 公开接口 · exchangeInfo · Jackson

公开接口不需要账号,谁都能调。你可以用它拿到真实的盘口、K 线、标记价格和资金费率,把 D2、D3、D9 学的公式用真数据算一遍。

公开接口 和 curl

大白话
公开接口:交易所给所有人开放的 HTTP 接口,返回行情数据,不用登录。curl 是命令行里发 HTTP 请求的工具,Mac 和 Linux 自带。
Java 类比
就是一个不需要鉴权的 GET 接口,和你调别的服务的 REST 接口没区别。

下面用 Binance 的公开接口举例(路径、参数和返回字段以官方文档为准;把域名换成测试网的,就能拿到测试网的数据):

# 1. 盘口:买卖各 5 档(D3)
curl 'https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=5'

# 2. K 线:最近 3 根 1 小时 K 线(D3),每根是一个数组:
#    [开盘时间, 开, 高, 低, 收, 成交量, 收盘时间, ...]
curl 'https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1h&limit=3'

# 3. 交易规则:tickSize、lotSize、最小下单金额都在 filters 里(D2)
curl 'https://api.binance.com/api/v3/exchangeInfo?symbol=BTCUSDT'

# 4. 永续合约:标记价格、指数价格、当前资金费率、下次结算时间(D2)
curl 'https://fapi.binance.com/fapi/v1/premiumIndex?symbol=BTCUSDT'

# 5. 永续合约:最近 3 次资金费率历史(D9 资金费率套利要用)
curl 'https://fapi.binance.com/fapi/v1/fundingRate?symbol=BTCUSDT&limit=3'

1 depth 盘口 · 2 klines K 线 · 3 exchangeInfo 交易规则 · 4 premiumIndex 标记价格和资金费率 · 5 fundingRate 资金费率历史。网络连不上时,先确认你所在的网络环境是否允许访问,不要为了练习去绕公司的网络规定。

exchangeInfo:下单前必看的"交易规则"

大白话
exchangeInfo 返回每个交易对的规则:价格精度、数量精度、最小下单金额、价格保护范围等。第 6 节的那些拒单,原因全写在这里。
坑
这些规则会变(比如交易所调整精度),程序不能写死,要启动时拉一次、定时刷新。
拆一条返回看看:标记价格和资金费率示例数据,可以改里面的数字

怎么玩:默认是一条示例返回,表里逐个解释字段,下面算出"持 0.01 BTC 多单,下次结算要付多少资金费、还剩多久"。把 lastFundingRate 改成负数(比如 "-0.00010000"),看付钱的方向反过来。字段名是 Binance 的叫法,各家不同。

Java 版本(JDK 11 起自带 HttpClient;解析 JSON 用 D6 已经加过依赖的 Jackson,它是 Java 里最常用的 JSON 库):

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.math.BigDecimal;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class PublicRest {
    public static void main(String[] args) throws Exception {
        HttpClient http = HttpClient.newHttpClient();
        HttpRequest req = HttpRequest.newBuilder(
                URI.create("https://fapi.binance.com/fapi/v1/premiumIndex?symbol=BTCUSDT")).build();
        String body = http.send(req, HttpResponse.BodyHandlers.ofString()).body();

        JsonNode n = new ObjectMapper().readTree(body);
        BigDecimal mark = new BigDecimal(n.path("markPrice").asText());        // 价格是字符串,直接进 BigDecimal
        BigDecimal rate = new BigDecimal(n.path("lastFundingRate").asText());
        BigDecimal qty  = new BigDecimal("0.01");
        // 资金费 = 仓位价值 × 费率;正数表示多头付钱
        System.out.println("标记价 " + mark + ",0.01 BTC 多单下次资金费 " + mark.multiply(qty).multiply(rate));
    }
}
9

签名接口:用 Java 真的下一张单

本节新词API Key · 密钥 · 签名 · HMAC-SHA256 · query string · timestamp · recvWindow · 限频 · TestnetClient

下单、查单、撤单要证明"这个请求真是你发的,而且中途没被改过"。办法是签名:用只有你和交易所知道的密钥,给请求内容算一个指纹,一起发过去。

API Key 和 密钥 API key · secret

大白话
在交易所后台建一对字符串。API Key 像用户名,随请求一起发,告诉交易所"我是谁"。密钥像密码,永远不发出去,只在本地用来算签名。
生活例子
银行柜台:你报卡号(API Key),然后在密码键盘上输密码。柜员看不到密码,只看到"验证通过"。
放哪儿
Binance 把 API Key 放在请求头 X-MBX-APIKEY 里;其他交易所请求头的名字不同,有的还要多一个口令,以文档为准。

签名 和 HMAC-SHA256 signature

大白话
签名:把请求参数和密钥一起放进一个"搅拌机",出来一串固定长度的乱码。HMAC-SHA256 就是这台搅拌机的型号:同样的输入永远得到同样的输出,改一个字符输出就完全不同,而且从输出倒推不出密钥。
query string
签的是什么?就是 URL 问号后面那串 query string,比如 symbol=BTCUSDT&side=BUY&…。你签的字符串和你发出去的必须一个字符都不差,顺序也不能变。
算一遍
用 Binance 文档里公开的示例:密钥 NhqPtmdS…Tj0j,参数 symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559,签名结果是 c8db5682…bd6b71(64 个十六进制字符)。把 quantity=1 改成 2,签名变成 aca62923…be3ea3,完全不一样。
Java 类比
JDK 自带:Mac.getInstance("HmacSHA256")。和你做过的"回调验签""接口防篡改"是同一个东西。

自己在终端验证一下(输出应该是 c8db…6b71):

echo -n "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559" \
  | openssl dgst -sha256 -hmac "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j"

echo -n:不要在末尾加换行(多一个换行,签名就不对了,这是最常见的签名错误之一)。这对示例密钥是文档公开的,不是任何人的真实密钥。

timestamp 和 recvWindow

大白话
timestamp:你发请求时的毫秒时间戳。recvWindow:允许这个请求在路上"走"多久,比如 5000 毫秒。交易所收到时发现已经过期,就拒掉。
为什么要
防止别人把你的请求录下来,过一会儿再原样发一次(重放攻击)。签名防的是"改内容",时间戳防的是"原样重发"。
坑
你电脑的时钟不准,差了几秒,所有请求都会被拒,报错大意是"时间戳不在 recvWindow 内"。先对一下服务器时间(交易所一般有公开的"服务器时间"接口)。上面示例里的时间是 2017 年的,真发出去一定被拒。
录屏:一个签名请求从拼参数到被接受用 Binance 文档的公开示例

怎么玩:自动播放七步,被框起来的是当前这一步,灰的是还没走到的。重点看第 3 步和第 6 步:你这边和交易所那边用同一个密钥、同一串参数各算一次,结果一样才放行。第 7 步演示改了一个字符,签名就完全不同。

限频 rate limit

大白话
限频:交易所限制你每分钟能调多少次。常见按"权重"算:查一张单算 1–4 分,查所有挂单算得更多,每分钟总分有上限,超了就拒,严重的会临时封 IP。
怎么看
很多交易所会在响应头里告诉你"这一分钟已经用了多少",程序要读出来控制节奏。数值各家不同,以文档为准。

下面的 TestnetClient 是 D5 Exchange 接口的真实实现,连的是现货测试网。上层的 placeSafely、Reconciler 一行都不用改,这就是 D5 先抽接口的好处。Order、Side、RemoteOrder 用 D4 / D5 定义好的,名字和参数顺序都一样。

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;

/** 现货测试网上的 Exchange 实现。以 Binance 现货测试网为例,路径、参数、错误码以官方文档为准 */
public class TestnetClient implements Exchange {
    private static final String BASE = "https://testnet.binance.vision";
    private final String apiKey = System.getenv("TESTNET_API_KEY");       // 密钥只从环境变量读,绝不写进代码
    private final String secret = System.getenv("TESTNET_API_SECRET");
    private final HttpClient http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
    private final ObjectMapper json = new ObjectMapper();
    private final String symbol;

    public TestnetClient(String symbol) {
        if (apiKey == null || secret == null)
            throw new IllegalStateException("先设置环境变量 TESTNET_API_KEY 和 TESTNET_API_SECRET");
        this.symbol = symbol;
    }

    @Override
    public Order place(Order o) throws TimeoutException {
        Map<String, String> p = new LinkedHashMap<>();       // LinkedHashMap:保持参数顺序
        p.put("symbol", symbol);
        p.put("side", o.side().name());                     // BUY / SELL
        p.put("type", "LIMIT");
        p.put("timeInForce", "GTC");                        // 一直挂着,直到成交或撤单
        p.put("quantity", o.qty().toPlainString());         // toPlainString:避免 1E-3 这种科学计数法
        p.put("price", o.price().toPlainString());
        p.put("newClientOrderId", o.clientOid());           // 我们自己的编号,D5 的 ClientOid.of(...)
        send("POST", "/api/v3/order", p);                   // 被拒会抛异常;超时抛 TimeoutException
        return o;
    }

    @Override
    public Optional<RemoteOrder> query(String clientOid) {
        Map<String, String> p = new LinkedHashMap<>();
        p.put("symbol", symbol);
        p.put("origClientOrderId", clientOid);              // 按我们的编号查,不需要交易所的 orderId
        try {
            return Optional.of(toRemote(send("GET", "/api/v3/order", p)));
        } catch (NotFound e) {
            return Optional.empty();                        // 交易所确实没有这张单
        } catch (TimeoutException e) {
            // 查询超时 ≠ 查不到。绝不能当成"没有"去重下,交给上层稍后重试
            throw new IllegalStateException("查单超时,稍后重试: " + clientOid, e);
        }
    }

    @Override
    public void cancel(String clientOid) {
        Map<String, String> p = new LinkedHashMap<>();
        p.put("symbol", symbol);
        p.put("origClientOrderId", clientOid);
        try {
            send("DELETE", "/api/v3/order", p);
        } catch (NotFound e) {
            // 已经成交或已经撤掉了:撤单是幂等的,不算错
        } catch (TimeoutException e) {
            throw new IllegalStateException("撤单超时,稍后按编号查一下再决定: " + clientOid, e);
        }
    }

    @Override
    public List<RemoteOrder> openOrders() {
        Map<String, String> p = new LinkedHashMap<>();
        p.put("symbol", symbol);
        try {
            List<RemoteOrder> out = new ArrayList<>();
            for (JsonNode n : send("GET", "/api/v3/openOrders", p)) out.add(toRemote(n));
            return out;
        } catch (TimeoutException e) {
            throw new IllegalStateException("查挂单超时,稍后重试", e);
        }
    }

    /** 统一发请求:加 recvWindow 和 timestamp → 签名 → 带上 API Key 发出去 */
    private JsonNode send(String method, String path, Map<String, String> params) throws TimeoutException {
        params.put("recvWindow", "5000");
        params.put("timestamp", String.valueOf(System.currentTimeMillis()));
        String query = params.entrySet().stream()
                .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
                .collect(Collectors.joining("&"));
        String url = BASE + path + "?" + query + "&signature=" + hmacSha256(secret, query);  // 签的和发的是同一串
        HttpRequest req = HttpRequest.newBuilder(URI.create(url))
                .timeout(Duration.ofSeconds(3))
                .header("X-MBX-APIKEY", apiKey)
                .method(method, HttpRequest.BodyPublishers.noBody())
                .build();
        try {
            HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
            JsonNode body = json.readTree(resp.body());
            if (resp.statusCode() == 200) return body;
            int code = body.path("code").asInt();
            if (code == -2013 || code == -2011) throw new NotFound();   // Binance:订单不存在 / 撤单时找不到这张单
            throw new IllegalStateException("交易所拒绝: " + resp.body());
        } catch (HttpTimeoutException e) {
            throw new TimeoutException("请求超时,不知道成没成: " + path);   // 交给 D5 的 placeSafely 按编号去查
        } catch (IOException e) {
            throw new IllegalStateException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(e);
        }
    }

    static String hmacSha256(String secret, String data) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            return HexFormat.of().formatHex(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));   // HexFormat:JDK 17
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    /** 交易所的返回 → D5 的 RemoteOrder;状态翻译成 D5 用的四种 */
    private RemoteOrder toRemote(JsonNode n) {
        String oid = n.path("clientOrderId").asText();
        Order o = new Order(oid, Side.valueOf(n.path("side").asText()),
                new BigDecimal(n.path("price").asText()), new BigDecimal(n.path("origQty").asText()), levelOf(oid));
        String status = switch (n.path("status").asText()) {
            case "NEW" -> "OPEN";
            case "PARTIALLY_FILLED" -> "PARTIALLY_FILLED";
            case "FILLED" -> "FILLED";
            default -> "CANCELED";                          // CANCELED / EXPIRED / REJECTED 都算"不在挂了"
        };
        return new RemoteOrder(o, status);
    }

    /** G1-3-B-7 → 3;不是我们起的编号(比如网页上手动下的单)返回 -1 */
    private static int levelOf(String oid) {
        String[] a = oid.split("-");
        try { return a.length == 4 ? Integer.parseInt(a[1]) : -1; }
        catch (NumberFormatException e) { return -1; }
    }

    private static class NotFound extends RuntimeException {}
}

这段代码要注意的四件事

超时 ≠ 失败
place 超时只抛 TimeoutException,不重试、不猜。D5 的 placeSafely 接住它,用同一个 clientOid 去 query,查到了就接管,查不到才用同一个编号重下。
查询超时 ≠ 查不到
这是最容易写错的一行。如果把查询超时当成 Optional.empty(),上层会以为"没下成功"去重下,而第一张单其实已经挂上了。
撤单幂等
撤一张已经成交或已经撤掉的单,交易所会说"找不到"。这不是错误,吞掉就好。
先拉规则
测试网 BTCUSDT 的 tickSize、lotSize 和本页例子可能不同,价格和数量要先按第 8 节 exchangeInfo 里的规则取整(D2 的 roundToStep),否则会报第 6 节的 PRICE_TICK / QTY_LOT。

跑一下:挂一张不会成交的单 → 查 → 撤 → 再查。价格取测试网当前价的九折左右,数量用规则允许的值。

import java.math.BigDecimal;

public class TestnetDemo {
    public static void main(String[] args) throws Exception {
        Exchange ex = new TestnetClient("BTCUSDT");
        String oid = ClientOid.of("T1", 0, Side.BUY, 1);                   // "T1-0-B-1",D5 的编号规则
        Order o = new Order(oid, Side.BUY, new BigDecimal("54000.00"), new BigDecimal("0.00100"), 0);
        ex.place(o);
        System.out.println(ex.query(oid));   // 期望:status=OPEN
        ex.cancel(oid);
        System.out.println(ex.query(oid));   // 期望:status=CANCELED
        ex.cancel(oid);                      // 再撤一次:不报错(幂等)
    }
}
// 运行前:export TESTNET_API_KEY=...  export TESTNET_API_SECRET=...(只在终端里设,不写进任何文件)

跑完以后去测试网网页的历史委托里,找到编号 T1-0-B-1 的那张单,状态应该是已撤销。网页上能看到你程序下的单,说明整条链路通了。

常见误区 · Key 只要不外传就安全底线三条:只勾交易权限、不勾提现;绑定你的出口 IP;密钥放环境变量或本机配置文件,并写进 .gitignore。万一已经推到了仓库:立刻去交易所删掉这个 Key 再新建一个,只删提交记录没用,别人可能已经拉走了。测试网和正式环境的 Key 分开建,名字里写清是哪个。
10

私有推送,以及把 D4 机器人接到测试网

本节新词私有推送 · 用户数据流 · listenKey · 轮询

D3、D4 用的是公开推送,所有人收到的都一样。你自己的订单什么时候成交,要靠私有推送来告诉你。这一节是进阶,可以选做。

私有推送 和 用户数据流 user data stream

大白话
私有推送:只推给你一个人的消息:你的订单状态变了、你的成交、你的余额和仓位变了。Binance 管这条连接叫用户数据流。它就是 D4 说的"成交回报"的真实来源。
怎么连
常见做法:先用 API Key 调一个接口,换一个临时令牌(Binance 叫 listenKey),再用它连 WebSocket;令牌要定时续期,否则过一段时间连接会失效。也有交易所改成直接在 WebSocket 里登录订阅。各家不同,而且会变,以文档为准。
Java 类比
像订阅一个只属于你的 MQ topic,消费者就是 D4 的 BotLoop。

轮询:先用最笨但最稳的办法 polling

大白话
轮询:每隔几秒主动去问一次"我的单子怎么样了"。比私有推送慢,但简单、不怕断线,适合第一版。
算一遍
网格挂着 8 张单,每 2 秒逐张查一次:每分钟 8 × 30 = 240 次查询。如果查一张单算 4 分权重、每分钟上限 6,000 分,那就用了 960 分,占 16%。单子多了或者间隔短了,就会撞限频,这时候再换成私有推送。

接入步骤(每一步都能单独跑通再做下一步):

  1. 下单换实现:D4 的 BotLoop 里直接用的是 PaperMatcher。把"下单"改成调 Exchange.place,传进去的是 new TestnetClient("BTCUSDT");PaperMatcher.onTrade 不再需要,成交改由下一步产生。
  2. 成交从交易所来:第一版用轮询,发现 FILLED 就包成 D4 的 Fill 放进 BotLoop 的队列(代码见下)。
  3. 去重:同一张单可能被连续两次轮询都看到 FILLED,事件循环里按 clientOid 去重,处理过的直接跳过(D6、D7 的幂等)。
  4. 启动先对账:每次启动先跑 D5 的 Reconciler,本地和交易所对齐了再开始挂新单。
  5. 一键停:写一个"撤掉本网格所有挂单"的方法(用 openOrders + ClientOid.belongsTo 过滤),跑实盘前必须有。
// 第一版成交来源:每 2 秒轮询一次本地认为还在挂的单
ScheduledExecutorService poller = Executors.newSingleThreadScheduledExecutor();
poller.scheduleAtFixedRate(() -> {
    for (Order o : List.copyOf(local.openOrders())) {                 // local 是 D5 的 LocalBook
        exchange.query(o.clientOid()).ifPresent(r -> {
            if (r.status().equals("FILLED"))
                loop.submit(new Fill(o.clientOid(), o.side(), o.level(), o.price(), o.qty(), "FILLED"));
        });                                                            // 只放进队列,不在这个线程里改状态(D4 第 8 节)
    }
}, 0, 2, TimeUnit.SECONDS);

这段代码里查询超时会抛异常,而 scheduleAtFixedRate 遇到没捕获的异常会悄悄停止后续执行。真实代码要在循环体外包一层 try-catch,打日志后继续。

11

实操清单,以及入职第一周怎么开口

按下面的顺序做,每一步都记下一个数字。数字填到总页 D10 的实操记录表里(保存在你这台浏览器上),入职以后拿到公司的币再走一遍,对比两边。

#做什么记下哪个数对应本页
1测试网领假币,截图初始余额初始 USDT 余额第 2 节
2现货:挂不会成交的买单 → 撤单冻结金额第 4 节练习 1
3现货:小额市价买成交均价、滑点第 4 节练习 2
4合约:逐仓 5 倍开最小多单开仓价、界面强平价、自己算的强平价、保证金第 5 节练习 1
5合约:设止盈止损止盈、止损各自的预期盈亏第 5 节练习 2
6合约:持仓跨过一次资金费结算资金费扣款第 5 节练习 3
7合约:手动平仓已实现盈亏、开仓手续费、平仓手续费第 5 节练习 4
8故意犯错:至少制造 3 种拒单被拒原因代码(用第 6 节的名字记)第 6 节
9Java:TestnetDemo 下单 → 查 → 撤网页上看到的那张单的编号第 9 节
10带着计划下一单:先写交易计划、用以损定仓算数量,再用计划委托 + 止盈止损执行计划风险(U)、实际盈亏(U)、R 倍数D12 第 3、9、10 节

记录表在这里:七日冲刺 D10 → 实操记录表。每一行分"自己先算"和"界面实际"两栏,差不到 0.5% 会自动显示对上了。记录项是:现货限价买单冻结金额、合约开仓保证金、强平价、某一时刻的未实现盈亏、一次资金费扣款、已实现盈亏、开仓手续费、平仓手续费,最后一行写被拒过的原因代码。

入职第一周怎么开口

问账
"我在测试网开了一单,界面强平价和我按公式算的差了 50 多 U,咱们的维持保证金率是分档的吧?档位配置在哪个服务里?"
问链路
"一张合约单从网关进来到撮合、再到资金流水那一行,中间经过哪几个服务?资金费结算是定时任务跑的吗?"
问可靠性
"下单接口超时的时候,咱们是按 clientOid 查了再决定吗?查询也超时怎么处理?"
问规矩
"公司发的这些币是测试环境还是正式环境的?员工交易政策我去哪里看?"

这些问题的共同点:先说你自己做过什么、看到了什么数,再问一个具体的点。比"能给我讲讲系统架构吗"好回答得多,也显得你是做过功课的。

12

词典和自测

今天用到的词,包括前几天学过的,这里都能查到。

自测 8 题

读完以后

回到 七日冲刺 D10:先看"开一单合约"的录屏式演示,把整个过程在脑子里过一遍;再用签名计算器把第 9 节的示例算一次,改一个字符看签名怎么变;然后做 checkOrder 编程题(就是第 6 节那张表);最后把你在测试网上记下的数字填进实操记录表,勾上打卡。

这一天练的是"交易所算得对不对"。怎么像交易员一样下单(先写计划、以损定仓、计划委托、追踪止损、记日志),以及在模拟盘里亲手建网格、定投、跟单,放在 D12;看盘和指标放在 D11。