OASIS BLOG
← 回到七日冲刺← D8 笔记 · D10 笔记 →
写给没做过量化的 Java 程序员

D9 零基础速成笔记

按一套量化系统的五个环节来讲:数据 → 回测 → 策略 → 模拟盘 → 实盘。历史 K 线怎么拉、怎么洗;双均线和海龟交易法则这两个经典趋势策略一步步算;回测怎么骗人、指标怎么算;套利和做市一笔笔算;怎么控制亏损;最后怎么一步步放到真钱上。页面里带下划虚线的词,点一下就能看解释。

怎么读按顺序读,后面的章节都建立在前面的基础上。一共 13 节,大约 4 小时。每节标题下有一条进度条,标出这节属于五个环节里的哪一环。
每个概念都分四层大白话 → 生活例子 → 算一遍 → Java 类比。前两层看懂就够了。
先说清楚本页没有任何"稳赚"的策略。每种策略都会讲它在什么行情下亏钱,这比它怎么赚钱更重要。
1

量化交易是什么:一套系统的五个环节

本节新词量化交易 · 回测 · 策略 · 信号 · 仓位管理 · 下单执行 · 风控

一套完整的量化系统就五个环节:数据 → 回测 → 策略 → 模拟盘 → 实盘。你在 D1–D8 已经零零散散碰过其中几环,这一页把它们串成一条线。

量化交易 quantitative trading / quant

大白话
不靠感觉拍脑袋,而是先写下明确的规则,再让程序严格照着规则下单。
生活例子
做菜靠手感,每次味道都不一样;照菜谱做,盐 3 克、油 10 毫升,谁做都一样,还能事后检查"是不是盐放多了"。量化就是交易的菜谱。
Java 类比
把业务里散落的 if-else 手工判断,整理成一套明确的规则配置,交给规则引擎执行。好处一样:能复现、能测试、能回放。
 +------+    +----------+    +----------+    +---------------+    +------+
 | data | -> | backtest | -> | strategy | -> | paper trading | -> | live |
 +------+    +----------+    +----------+    +---------------+    +------+
    ^             ^                |                                  |
    |             +---- tune ------+                                  |
    +------------ new data, fills, live vs backtest gaps -------------+

data = 数据;backtest = 回测;strategy = 策略;paper trading = 模拟盘;live = 实盘;tune = 调整策略后再回测;最下面一行 = 实盘产生的新数据、成交记录、实盘和回测的差距,又流回第一环。这不是一条走一遍就结束的直线,而是一个转圈的循环。

环节大白话本站在哪讲这一环做完的标志
① 数据把行情收下来、洗干净、存好。分实时(盯盘用)和历史(回测用)两种D3 实时行情;本页第 3 节 历史 K 线随便拿一段历史,没有缺口、没有重复,时间全是 UTC
② 回测拿历史数据假装策略当时在跑,算出资金曲线和指标本页第 6、7 节同一段数据跑两次结果一模一样;扣了手续费和滑点
③ 策略具体的买卖规则D1 网格和马丁;本页第 2 节地图,第 4 节双均线、第 5 节海龟交易法则、第 8 节套利、第 9 节做市规则能一条条写下来,每个参数说得出为什么是这个数
④ 模拟盘真行情、假钱,程序真的连着行情跑D4–D5 本地模拟撮合;D10 测试网;本页第 12 节跑一段时间,和同一段时间的回测结果对得上
⑤ 实盘真钱,先放一小部分D10;总页"进组以后";本页第 12 节实盘和回测的差距,每一笔都解释得清

为什么是这个顺序

大白话
先有数据,才谈得上回测;先有一个公平的回测"考场",才能把策略放进去反复考;考过了才上模拟盘;模拟盘跑稳了才放真钱。任何一环偷懒,后面那环的结论就不可信:数据缺了几根,回测收益就是假的。
Java 类比
和软件交付流水线一样:准备测试数据 → 写好回归测试 → 改业务代码并跑测试 → 部署到预发环境 → 灰度上线。数据对应测试数据,回测对应回归测试,模拟盘对应预发,实盘对应灰度。
本页的读法
讲解顺序稍微不同:第 2 节先给你一张策略地图,第 3 节讲数据,第 4、5 节先认识两个具体的策略,第 6、7 节再讲回测怎么检验它们。没有具体的策略,回测讲起来太空。每节标题下面有一条小进度条,告诉你这一节属于哪一环。

策略这一环,拆开是四件事:信号、仓位、执行、风控

信号
什么时候买卖。比如"价格碰到某一格"就是网格的信号,"快线上穿慢线"就是双均线的信号。一套完整的买卖规则(信号 + 后面三件事)叫一个策略。
仓位管理
买多少。网格里是每格固定 0.01 个 BTC。第 5 节的海龟和第 10 节会讲更讲究的算法:先想好最多亏多少,再倒推买多少。
下单执行
怎么把单子下出去。用限价单还是市价单,大单要不要拆开分批下。网格用的是限价挂单。第 11 节细讲。
风控
亏到多少必须停。比如单日亏损到本金的 3% 就全部停掉。这是 D4 那个机器人唯一没有的零件:价格一路跌出区间,它只会傻等。
 market data --> SIGNAL --> POSITION SIZE --> EXECUTION --> exchange
    (D3)         when?       how much?         how to place     |
                   ^                                            | fills (D4)
                   |                                            v
                   +----------------- RISK CONTROL <-----------+
                              stop loss / max position / kill switch

market data = 行情;SIGNAL = 信号;POSITION SIZE = 仓位管理;EXECUTION = 下单执行;exchange = 交易所;fills = 成交回报;RISK CONTROL = 风控(止损 / 最大持仓 / 一键停止)。风控是一个环:成交之后的结果回来,风控决定下一轮还能不能继续。

进组以后留意一件事:你们组是"公司自己拿钱跑策略",还是"给用户提供策略工具"(网格、定投、跟单这类产品),还是两样都有。关心的重点不一样,各家分工也不同。

2

常见策略地图

所在环节1 数据→2 回测→3 策略→4 模拟盘→5 实盘
本节新词趋势跟踪 · 均线 · 突破 · 唐奇安通道 · 均值回归 · 做市 · 套利 · 统计套利 · 配对交易

每种策略赚的钱,都来自市场上的某种"规律"。这种规律消失的时候,就是它亏钱的时候。

趋势跟踪 trend following

大白话
涨起来了就跟着买,跌下去了就跟着卖,赌"涨的会继续涨"。
均线
最常用的工具是均线:最近 N 天收盘价的平均值。算一遍:最近 5 天收盘 100、102、104、103、106,5 日均线 = (100+102+104+103+106) ÷ 5 = 515 ÷ 5 = 103。今天收盘 106 在均线上方,常被当作"在涨"的信号。拿一快一慢两条均线比较,就是第 4 节的双均线。
突破
另一种常见信号是突破:过去 20 天最高价是 62,000,今天冲过了 62,000,就买入。过去 N 天最高价和最低价画成的带子叫唐奇安通道,第 5 节的海龟交易法则就是用它入场。
什么时候亏
来回震荡的行情。刚冲过 62,000 你买了,马上又跌回来,你止损;再冲上去,又买……反复被"假突破"打脸。

均值回归 mean reversion

大白话
赌"跑远了会回来":跌多了买,涨多了卖。网格就属于这一类。
什么时候亏
单边大涨或大跌,价格一去不回(D1 讲的踏空和被套)。它和趋势跟踪正好相反:一个赚钱的行情,往往就是另一个亏钱的行情。

统计套利 和 配对交易 statistical arbitrage / pairs trading

大白话
统计套利:找两个历史上关系很稳定的东西,关系偏离了就押它回来。最简单的一种叫配对交易。
例子
假设过去一年 BTC 价格 ÷ ETH 价格一直在 19 到 21 之间晃。今天到了 23,说明 BTC 相对贵了:做空 BTC、同时做多 ETH,等比值回到 20 附近再平掉。
什么时候亏
关系真的断了。比如其中一个币出了事故,比值一路走到 30 也不回来。

做市(同时挂买单和卖单,赚中间的差价)和套利(同一个东西在两个地方价格不一样,低买高卖)是交易所里最常见的两类,第 8、9 节专门一笔笔算。

策略靠什么赚钱什么行情会亏例子
趋势跟踪涨跌能持续一段来回震荡、反复假突破双均线(第 4 节)、海龟交易法则(唐奇安通道突破,第 5 节)
均值回归价格来回晃单边大涨大跌网格、配对交易
做市买卖价差 + 挂单返佣单边行情里货越积越多;被消息更快的人吃单同时挂买一、卖一
套利同一个东西两处价格不一样价差没收回来反而扩大;手续费和速度拼不过别人资金费率套利、搬砖
统计套利历史上稳定的关系关系断了配对交易

这张表进组后很有用:听到任何一个策略,先问"它靠什么赚钱"和"什么行情会亏"。

3

数据:历史 K 线从哪来、怎么洗

所在环节1 数据→2 回测→3 策略→4 模拟盘→5 实盘
本节新词历史 K 线 · UTC · 缺口 · 脏数据 · Parquet

回测的结论,最多和数据一样可靠。数据里少一根 K 线,回测就可能凭空多出、或者漏掉一笔成交。

历史 K 线 historical klines

大白话
过去每一段时间一根的 K 线(D3 讲过:开、高、低、收,再加成交量),按时间排好存下来。它是回测的原料。
从哪来
最常见的是交易所的公开接口,不用账号:告诉它"交易对 + 周期 + 从什么时候开始 + 要几根",它返回一串 K 线。每次最多给几百到一千根(以各家文档为准),所以要按时间一页一页往后翻着拉。另外也有交易所的历史数据下载页和第三方数据商,各家不同。
算一遍
一年的 1 小时 K 线 = 365 × 24 = 8,760 根。每次最多 1,000 根,要拉 ⌈8,760 ÷ 1,000⌉ = 9 次。每次请求都算限频的额度(D3),拉得太快会被拒,所以程序里每页之间主动歇一下。
Java 类比
就是分页拉一个外部接口的全量数据:用"上一页最后一条的时间 + 1 个周期"当下一页的起点,而不是用页码,这样中途失败了也能从断点接着拉。

UTC 时间 Coordinated Universal Time

大白话
UTC 是全世界统一的"零时区"时间,北京时间 = UTC + 8 小时。交易所接口里的时间,几乎都是 UTC 的毫秒时间戳:从 1970-01-01 00:00 UTC 起一共过了多少毫秒。
算一遍
1727740800000 毫秒 = 2024-10-01 00:00 UTC = 北京时间 2024-10-01 08:00。1 小时 = 60 × 60 × 1,000 = 3,600,000 毫秒,所以下一根 1 小时 K 线的开盘时间是 1727744400000。
为什么要紧
常见做法是日线在 UTC 0 点切换,也就是北京时间早上 8 点(各家可能不同)。你要是按北京时间 0 点切日线,自己算出来的"日线"和交易所的对不上,回测和实盘看的就不是同一根 K 线。
规矩
存储和计算一律用 UTC 毫秒,只在页面上显示时才换成北京时间。
Java 类比
存 long 毫秒或 Instant,不要存 LocalDateTime:后者不带时区,换一台时区设置不同的服务器就错了。

下面是分页下载的骨架。Kline 就是一根 K 线,第 6 节的回测直接用它;KlineDownloader 负责分页拉取和检查缺口。解析 JSON 用的 Jackson 在 D10 加过依赖。接口地址和参数名以交易所文档为准,访问外部网站要遵守所在地和公司的规定。

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;
import java.util.ArrayList;
import java.util.List;

/** 一根 K 线:开盘时间(UTC 毫秒)、开、高、低、收 */
record Kline(long openTime, BigDecimal open, BigDecimal high, BigDecimal low, BigDecimal close) {}

public class KlineDownloader {
    static final String BASE = "https://api.binance.com";    // 举例:公开接口,不用 Key;以文档为准
    static final long HOUR = 3_600_000L;                    // 1 小时 = 3,600,000 毫秒
    final HttpClient http = HttpClient.newHttpClient();
    final ObjectMapper json = new ObjectMapper();

    /** 从 start 拉到 end(都是 UTC 毫秒),每页最多 1000 根,按时间往后翻 */
    public List<Kline> download(String symbol, long start, long end) throws Exception {
        List<Kline> all = new ArrayList<>();
        long from = start;
        while (from < end) {
            String url = BASE + "/api/v3/klines?symbol=" + symbol + "&interval=1h"
                    + "&startTime=" + from + "&endTime=" + end + "&limit=1000";
            HttpResponse<String> resp = http.send(
                    HttpRequest.newBuilder(URI.create(url)).GET().build(),
                    HttpResponse.BodyHandlers.ofString());
            if (resp.statusCode() == 429) { Thread.sleep(60_000); continue; }  // 被限频:歇一分钟,重拉同一页
            if (resp.statusCode() != 200) throw new IllegalStateException("HTTP " + resp.statusCode());
            List<List<Object>> rows = json.readValue(resp.body(), List.class);
            if (rows.isEmpty()) break;                              // 这段时间没有数据了
            for (List<Object> r : rows) {                           // 每行:[开盘时间, "开", "高", "低", "收", ...]
                all.add(new Kline(((Number) r.get(0)).longValue(),
                        new BigDecimal((String) r.get(1)), new BigDecimal((String) r.get(2)),
                        new BigDecimal((String) r.get(3)), new BigDecimal((String) r.get(4))));
            }
            from = all.get(all.size() - 1).openTime() + HOUR;      // 下一页从最后一根的下一小时开始
            Thread.sleep(200);                                      // 主动慢一点,别碰限频
        }
        return all;
    }

    /** 逐根检查相邻两根的开盘时间差:正常应该正好差 step */
    public static List<String> checkGaps(List<Kline> ks, long step) {
        List<String> problems = new ArrayList<>();
        for (int i = 1; i < ks.size(); i++) {
            long d = ks.get(i).openTime() - ks.get(i - 1).openTime();
            if (d == 0)        problems.add("重复 " + ks.get(i).openTime());
            else if (d < 0)    problems.add("乱序 " + ks.get(i).openTime());
            else if (d > step) problems.add("缺 " + (d / step - 1) + " 根,在 " + ks.get(i - 1).openTime() + " 之后");
        }
        return problems;
    }
}

价格用字符串构造 BigDecimal,原因和 D2 一样:double 会带来误差。json.readValue(..., List.class) 会有一个"未检查的类型转换"编译警告,能正常运行;想干净可以用 Jackson 的 TypeReference。

脏数据 dirty data

大白话
下载回来的数据不一定是对的。有问题的数据统称脏数据,回测之前必须先查出来。
缺 K 线
相邻两根之间少了几根,叫缺口。常见原因:交易所停机维护、你的下载程序中途失败。处理:先查出来;缺一两根可以用"开高低收都等于上一根收盘、成交量为 0"补上,缺一大段就把那段标成不可用,回测跳过。
重复、乱序
翻页边界上多拉了一根,或者两次下载拼接时顺序乱了。处理:按开盘时间去重、排序。
异常值
某根 K 线的最高价比前后都高出 20%。处理:先标出来人工看,不要自动删。D2 讲过插针是真实发生过的,它真的触发过强平;删掉它,回测反而更乐观。
时间没对齐
一个数据源用 UTC、另一个用北京时间;或者有的用开盘时间、有的用收盘时间当这根 K 线的时间。处理:统一成"开盘时间、UTC 毫秒"。
Java 类比
数据入库前的校验和清洗,和做 ETL 一样:先校验、出报告,再决定补、删还是人工处理。

检查缺口,算一遍

例子
下载了 7 行 1 小时 K 线,开盘时间(UTC)依次是:00:00、01:00、02:00、04:00、05:00、05:00、06:00。
数行数
从 00:00 到 06:00 应该有 (6 − 0) ÷ 1 + 1 = 7 根,实际也是 7 行。只数行数,会以为没问题。
逐根比
相邻两根的差:1 小时、1 小时、2 小时(缺了 03:00 那 1 根)、1 小时、0(05:00 重复了)、1 小时。一缺一多,行数正好抵消。所以必须逐根比相邻的时间差,也就是上面代码里的 checkGaps。
缺口检查器1 小时 K 线 · 每行一个开盘时间(UTC 毫秒)
行数–
首尾之间应有–
缺几根–
重复–
乱序–

怎么玩:默认就是上面的 7 行。先看"行数"和"首尾之间应有"都是 7,再看表格里标出来的那两行:一行缺了 1 根,一行重复。然后把重复的那行删掉、在中间补一行 1727751600000(03:00),看问题全部消失。表格里同时显示 UTC 和北京时间,对照一下相差 8 小时。

存在哪里

先用 CSV
一年的 1 小时 K 线 8,760 行,存成 CSV 不到 1 MB,Excel 都能打开看。刚开始完全够用。
量大了
1 分钟 K 线一年 365 × 24 × 60 = 525,600 根,100 个币就是 5,256 万行。这时换 Parquet(一种按列压缩存储的文件格式,只读某几列时很快,体积也小)或者时序数据库。

数据本身也会让回测骗人

只下现在的币
你按"交易所现在的交易对列表"去下载历史,那些中途下架、归零的币根本不在里面。这就是第 6 节要讲的幸存者偏差,它在下载数据这一步就埋下了。
修正过的数据
有的数据源会事后修掉插针、改成交量。回测里看到的,就不是当时真实发生的价格。
只有 K 线
K 线里没有订单簿,你不知道那个价位当时有多少量、轮不轮得到你。第 6 节会讲这个坑。
Tips · 先查缺口,再回测一年日线应该是 365 根(闰年 366)。少一根,那一天的涨跌会被算进下一天,下一天看起来像一次暴涨;多一根重复,20 日均线其实只有 19 天的信息。下载完先按时间排序、去重、数根数,再和"应有根数"比一下,对不上就不要开始回测。
4

双均线:最经典的趋势策略

所在环节1 数据→2 回测→3 策略→4 模拟盘→5 实盘
本节新词双均线 · 快线 · 慢线 · 金叉 · 死叉 · 来回打脸 · 滞后

双均线只有一条规则:短期平均价从下往上穿过长期平均价就买,从上往下穿过就卖。几行代码就能写完,但趋势策略的优点和毛病它全都有。

先把均线算一遍 moving average

大白话
均线(第 2 节提过)= 最近 N 天收盘价的平均。每过一天,窗口往后挪一格:扔掉最老的一天,加上今天。
算一遍
为了好算,用一个价格在 100 附近的币。第 5 到第 9 天的收盘价是 100、100、103、106、109,第 9 天的 5 日均线 = (100 + 100 + 103 + 106 + 109) ÷ 5 = 518 ÷ 5 = 103.60。第 10 天收盘 112:扔掉最老的 100,加上 112,5 日均线 = (518 − 100 + 112) ÷ 5 = 530 ÷ 5 = 106.00。
Java 类比
滑动窗口求和:一个队列加一个累计和,进一个、出一个,每天 O(1) 更新,不用每天重新加 N 个数。

双均线、快线和慢线 dual moving average crossover

大白话
双均线:同时画两条均线。天数少的叫快线(比如 3 日),跟价格跟得紧;天数多的叫慢线(比如 5 日),更平滑、反应更慢。
金叉
前一天快线 ≤ 慢线,今天快线 > 慢线:快线从下往上穿过了慢线,叫金叉,意思是"最近涨得比以前快了",买入。
死叉
前一天快线 ≥ 慢线,今天快线 < 慢线:从上往下穿过,叫死叉,卖出。
只发一次
信号只在"穿过去的那一天"发一次。之后快线一直在上面,不再重复发买入信号。
生活例子
看一家店最近 3 天的平均客流,和最近 5 天的平均客流。3 天的平均刚超过 5 天的,说明生意正在变好。

下面这张表用 3 日均线当快线、5 日均线当慢线。前 4 天慢线还算不出来(不满 5 天),不比较;第 5 天两条线都有了;从第 6 天起才能和前一天比,看有没有穿过去。

第几天收盘3 日均线(快)5 日均线(慢)快线在慢线信号
1–4100第 3 天起 100.00––
5100100.00100.00相等
6100100.00100.00相等
7103101.00100.60上方金叉:买
8106103.00101.80上方
9109106.00103.60上方
10112109.00106.00上方价格最高点
11111110.67108.20上方
12108110.33109.20上方
13104107.67108.80下方死叉:卖
14100104.00107.00下方

第 7 天:前一天快线 100.00 ≤ 慢线 100.00,今天 101.00 > 100.60,金叉。第 13 天:前一天 110.33 ≥ 109.20,今天 107.67 < 108.80,死叉。3 日均线 = 最近 3 天收盘相加 ÷ 3,比如第 11 天 = (109 + 112 + 111) ÷ 3 = 332 ÷ 3 ≈ 110.67。总页编程题 maCross 有一个测试就是这组数,天数从 0 数:金叉是 i = 6,死叉是 i = 12。

信号出来以后,哪天才能成交

规则
第 7 天的均线要等第 7 天收盘才算得出来。所以最干净的写法是:第 t 天收盘算信号,第 t + 1 天才成交。
算一遍
这里只有收盘价,就用第二天收盘价近似成交价:第 8 天按 106 买,第 14 天按 100 卖,每个币亏 6。价格明明从 100 涨到了 112,你却亏了钱。
写错会怎样
如果回测写成"第 7 天开盘就买",那是在开盘时用了当天的收盘价,就是第 6 节的未来函数,回测收益会好得不像真的。

滞后 和 来回打脸 lag / whipsaw

滞后
均线是过去几天的平均,价格掉头以后,要过几天均线才跟着掉头,这叫滞后。上表里价格第 10 天就见顶了,死叉到第 13 天才出现,这时价格已经从 112 掉到 104。慢线越慢,假信号越少,但进场和离场都越晚。
什么时候赚
一段足够长的单边行情。金叉以后一路涨了 50%,你前后各晚了几天、少赚一点,照样拿住了中间一大段。上表那段只涨了 12% 就掉头,滞后把它全吃掉了。
什么时候亏
来回震荡的行情:刚金叉买入就掉头死叉卖出,刚卖又金叉……每一轮都是买得稍高、卖得稍低,再白交两次手续费,这叫来回打脸。
算一遍
一个月里金叉、死叉各出现 5 次。每一轮买卖价差亏 0.5%,手续费一买一卖各 0.1%,合计每轮亏 0.7%,5 轮下来约亏 3.5%(数字只是举例)。
参数
日线上常见的组合有 10 / 30、20 / 60,没有标准答案。第 6 节的过拟合热力图扫的就是这两个参数,它会告诉你"挑历史上最好的那组"有多危险。

Java 写法:MovingAverage 用一个 ArrayDeque 维护滑动窗口;MaCrossSignal 每根 K 线收盘时调用一次,记住上一根的快线和慢线,只在穿过去的那一根返回买或卖。Side 就是 D4 的那个枚举。

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayDeque;

/** N 日均线:每来一个收盘价 O(1) 更新 */
class MovingAverage {
    private final int n;
    private final ArrayDeque<BigDecimal> window = new ArrayDeque<>();
    private BigDecimal sum = BigDecimal.ZERO;
    MovingAverage(int n) { this.n = n; }

    /** 加入今天的收盘价;窗口还没满 n 天时返回 null */
    BigDecimal add(BigDecimal close) {
        window.addLast(close);
        sum = sum.add(close);
        if (window.size() > n) sum = sum.subtract(window.removeFirst());   // 挤掉最老的那天
        return window.size() < n ? null : sum.divide(BigDecimal.valueOf(n), 8, RoundingMode.HALF_UP);
    }
}

/** 双均线信号:收盘后调用,返回 BUY / SELL / null;下单放到下一根 K 线 */
class MaCrossSignal {
    private final MovingAverage fast, slow;
    private BigDecimal lastFast, lastSlow;          // 上一根的快线、慢线
    MaCrossSignal(int fastDays, int slowDays) {
        fast = new MovingAverage(fastDays);
        slow = new MovingAverage(slowDays);
    }

    Side onClose(BigDecimal close) {
        BigDecimal f = fast.add(close), s = slow.add(close);
        Side signal = null;
        if (f != null && s != null && lastFast != null && lastSlow != null) {
            if (lastFast.compareTo(lastSlow) <= 0 && f.compareTo(s) > 0) signal = Side.BUY;    // 金叉
            if (lastFast.compareTo(lastSlow) >= 0 && f.compareTo(s) < 0) signal = Side.SELL;   // 死叉
        }
        lastFast = f;
        lastSlow = s;
        return signal;
    }
}

比较 BigDecimal 用 compareTo,不要用 equals:equals 会把 100.0 和 100.00 当成不相等。要求上一根的慢线也已经算出来,所以第一次能发信号的最早一根是第"慢线天数 + 1"根,和上表一致。

双均线在两种行情里模拟行情 · 只做多 · 每次买或卖扣 0.1%
金叉次数–
死叉次数–
双均线收益–
一直拿着的收益–

图里细实线是价格,粗线是快线,虚线是慢线;朝上的小三角是金叉,朝下的小三角是死叉;浅色底纹是拿着 BTC 的日子。规则:第 t 天收盘出信号,第 t + 1 天起才吃涨跌。两个收益都从慢线算得出来的那天起算。怎么玩:① 默认是"先横盘,再大涨,再大跌",只交叉一两次,双均线拿住了中间的大涨、躲开了后面的大跌,明显跑赢一直拿着。② 切到"来回震荡",交叉一下子变多,底纹碎成一小段一小段,收益变成负的,这就是来回打脸。③ 在震荡行情里把快线、慢线都调大,交叉变少、亏得少一点,但换回趋势行情会发现进出场更晚了,这就是滞后的代价。

常见误区 · 收盘出信号,当天收盘就成交第 t 天收盘那一刻才知道出现了金叉,这时候当天已经收盘了,最早只能在第 t+1 天成交。回测里写成"第 t 天收盘价买入",每笔看似只差一点,几十笔叠起来就是一条漂亮的假曲线,这就是未来函数。总页回测器和编程题都按"第 t 天出信号、第 t+1 天成交"。
5

海龟交易法则:一整套能照着执行的规则

所在环节1 数据→2 回测→3 策略→4 模拟盘→5 实盘
本节新词海龟交易法则 · 唐奇安通道 · 系统一 · 系统二 · 真实波幅 · N 值 · 头寸单位 · 加仓

海龟法则的价值不在某个神奇指标,而在于它把"什么时候进、买多少、什么时候加、什么时候认赔、什么时候走"全写成了规则,没有一步需要"看感觉"。

海龟交易法则 Turtle trading rules

来历
1983 年,期货交易员 Richard Dennis 和 William Eckhardt 打赌"交易能不能教会",招了一批没经验的人,教给他们一整套规则,再拿钱让他们真做,这批学员被叫作"海龟"。规则后来被公开出版。当年用在期货上,搬到币圈时参数要重新回测检验。
大白话
一个趋势跟踪策略,包含五样东西:入场(通道突破)、仓位(按波动大小定)、加仓、止损、离场。下面一样一样讲。

唐奇安通道 和 两套系统 Donchian channel / System 1, System 2

大白话
把过去 20 天的最高价画一条线、最低价画一条线,中间这条带子叫唐奇安通道。价格冲出通道上沿,就是第 2 节讲的突破。
系统一
价格突破过去 20 天最高价,做多;做多以后,跌破过去 10 天最低价,离场。做空反过来。
系统二
55 天突破入场,20 天反向突破离场。天数更多,信号更少、更慢,抓的是更大的趋势。
算一遍
过去 20 天最高价是 60,000。今天价格冲到 60,800,突破了 → 按 60,000 触发买入。(原版系统一还有一条"上一次突破如果赚钱,这次就跳过"的过滤规则,这里先不展开。)
和双均线比
同样是趋势跟踪:双均线看"平均价有没有变快",通道突破看"有没有创出一段时间的新高"。两者在单边行情里都赚,在震荡行情里都被来回打脸。

真实波幅 和 N 值 true range / N (ATR)

大白话
海龟不按"固定金额"买,而是先量一量这个币一天通常晃多少,晃得凶就少买。量出来的数叫 N 值(也叫 ATR,平均真实波幅)。
真实波幅
一天真实晃了多大,叫真实波幅(TR),取三个数里最大的:最高 − 最低、|最高 − 昨收|、|最低 − 昨收|。后两个是为了把"开盘就跳了一大截"也算进去。
算一遍
昨收 60,000,今天最高 61,500、最低 59,200:最高 − 最低 = 2,300;|61,500 − 60,000| = 1,500;|59,200 − 60,000| = 800。TR = 2,300。
换一天:昨收 60,000,今天最高 63,000、最低 62,000(一开盘就跳上去了):最高 − 最低只有 1,000,但 |63,000 − 60,000| = 3,000,TR = 3,000。
N 怎么更新
N = (19 × 昨天的 N + 今天的 TR) ÷ 20,相当于最近 20 天 TR 的平滑平均。昨天 N = 2,400,今天 TR = 3,000:(19 × 2,400 + 3,000) ÷ 20 = (45,600 + 3,000) ÷ 20 = 48,600 ÷ 20 = 2,430。
Java 类比
和监控里的指数滑动平均(EWMA)一样:新值只占一小份权重,一次毛刺不会让它跳太多。

头寸单位:波动越大,买得越少 unit

公式
1 个头寸单位的数量 = 账户权益 × 1% ÷ N。
算一遍
权益 10,000 U,BTC 现价 60,000,N = 2,400 U(约 4%)。1 个单位 = 10,000 × 1% ÷ 2,400 = 100 ÷ 2,400 ≈ 0.041667 BTC,名义价值 ≈ 0.041667 × 60,000 ≈ 2,500 U。
含义
价格动 1 个 N(2,400 U),这 1 个单位赚或亏 0.041667 × 2,400 ≈ 100 U,正好是账户的 1%。换一个 N 更大的币,单位数量自动变小,每个单位的风险还是 1%。
和第 10 节的关系
第 10 节讲"先定亏多少,再倒推买多少"。海龟就是这个思路的一个具体版本:拿 N 当尺子。

加仓 和 止损 pyramiding / 2N stop

加仓
价格每往有利方向走 0.5 个 N,就再买 1 个单位,最多 4 个。这叫加仓:在赚钱的方向上越买越多。它和 D1 的补仓(越跌越买)方向正好相反。
止损
止损放在最近一次买入价下方 2 个 N。每加一次仓,所有单位的止损一起往上挪。
算一遍
接上例,0.5N = 1,200,2N = 4,800。下表里"一共亏"指的是:加完这一个单位后,价格掉头直接打到止损,所有单位加起来亏多少(数量按 0.041667 算)。
第几个单位买入价加完后所有单位的止损这时打到止损一共亏
160,00060,000 − 4,800 = 55,2004,800 × 0.041667 ≈ 200.00 U(2%)
261,20061,200 − 4,800 = 56,400(3,600 + 4,800) × 0.041667 ≈ 350.00 U
362,40062,400 − 4,800 = 57,600(2,400 + 3,600 + 4,800) × 0.041667 ≈ 450.00 U
463,60063,600 − 4,800 = 58,800(1,200 + 2,400 + 3,600 + 4,800) × 0.041667 ≈ 500.00 U(5%)

满 4 个单位时打到 58,800:第 1 个单位(60,000 买)亏 1,200 × 0.041667 ≈ 50 U,第 2 个亏约 100 U,第 3 个约 150 U,第 4 个约 200 U,合计 500 U。此时一共持有 4 × 0.041667 = 0.166668 BTC,名义价值 ≈ 0.041667 × (60,000 + 61,200 + 62,400 + 63,600) = 0.041667 × 247,200 ≈ 10,300 U,大约 1 倍,不需要杠杆。总页编程题 turtlePlan 对 (10000, 60000, 2400) 应该返回:单位数量 0.041667、四个加仓价 60,000 / 61,200 / 62,400 / 63,600、每次加完后的止损 55,200 / 56,400 / 57,600 / 58,800、满仓打止损的总亏损 500。

什么行情会亏

假突破
来回震荡的行情里,刚突破新高就掉头,打到止损;过几天又突破,又掉头。和双均线的来回打脸是一回事。
胜率低
这类突破策略的胜率常常只有三四成(不同市场差别很大),靠少数几次大趋势把钱赚回来。所以它要求你扛得住一长串连续亏损。
算一遍
连续 10 次假突破,每次都在第 1 个单位就打到止损、亏当时权益的 2%:10,000 × 0.98¹⁰ ≈ 8,170.73 U。账户少了约 18%,这时候还得照样执行下一次突破,大多数人就是在这里放弃规则的。
动图:海龟系统一的一次完整交易权益 10,000 U · N 按 2,400 固定(实际每天更新)
现在是第几根–
持有几个单位–
持有 BTC–
统一止损价–
离场线(过去 10 天最低)–
盈亏 (U)–

图里每根竖线是一天的最高到最低,折线连着每天的收盘价;点线是过去 20 天最高价(入场线),实线是过去 10 天最低价(离场线),虚线是止损线;带数字的圆圈是第几个单位买在哪里,最后的方块是离场点。怎么玩:滚到这里会自动一步步播放(系统开了"减少动态效果"时不自动播放,点"播放"即可),随时可以暂停,用"上一步 / 下一步"来回看。盯住两件事:止损线每加一次仓就往上跳一格;离场线在价格回落前已经悄悄抬到了止损线上方很远,所以最后是离场线先被碰到,而不是止损。

常见误区 · 海龟每笔最多亏 2%那是只有 1 个单位的时候(10,000 U 账户亏 200 U)。加满 4 个单位、止损统一上移到 58,800 后,打止损一共亏 500 U,是账户的 5%。如果同时在几个品种上都加满,风险还要加起来算,所以海龟原版规则还限制了同方向、相关品种的总单位数。
6

回测:拿过去检验策略,以及它怎么骗人

所在环节1 数据→2 回测→3 策略→4 模拟盘→5 实盘
本节新词回测 · 历史数据 · 未来函数 · 过拟合 · 样本内 · 样本外 · 幸存者偏差

回测就是拿过去的行情,假装策略当时就在跑,看它会赚还是亏。回测能帮你淘汰坏策略,但回测好看不等于策略好。

回测 backtest

大白话
把过去一段时间的行情(历史数据,比如过去一年每小时一根的 K 线)一条条喂给策略,按规则模拟成交、记账,最后看资金变成多少。
生活例子
考前做往年真题。做得好说明有点希望,但今年的题不一样。
Java 类比
生产流量回放:把录下来的请求按原来的顺序重放给新版本服务,看它表现如何。

好消息:D4 写的零件一个都不用改。GridStrategy(网格策略)和 PaperMatcher(模拟撮合器)本来就只认"价格事件",把真行情换成历史 K 线就是回测。Kline 直接用第 3 节定义的那个(一根 K 线:开盘时间、开、高、低、收),新写的只有 GridBacktest,也就是回测循环本身。pathInside 这个方法负责把一根 K 线拆成几个价格依次喂进去,为什么要拆、拆的顺序为什么是个问题,看代码下面的解释。

import java.math.BigDecimal;
import java.util.*;

// Kline 用第 3 节 KlineDownloader.java 里定义的 record,同一个包里直接能用

public class GridBacktest {
    static final BigDecimal FEE = new BigDecimal("0.0002");     // 挂单手续费 0.02%,只是举例

    /** 返回每根 K 线收盘时的账户总值,也就是资金曲线 */
    public static List<BigDecimal> run(List<Kline> klines, List<BigDecimal> levels, BigDecimal qty) {
        BigDecimal start = klines.get(0).open();
        GridState state = new GridState(levels, start);          // D4 的类,原样用
        GridStrategy strategy = new GridStrategy("BT", state);
        PaperMatcher matcher = new PaperMatcher();

        BigDecimal cash = new BigDecimal("10000"), btc = BigDecimal.ZERO;
        for (int i = 0; i < levels.size(); i++) {              // 和 D4 的 Main 一样挂初始单
            if (state.cells[i] == Cell.EMPTY) continue;
            Side side = state.cells[i] == Cell.BUY ? Side.BUY : Side.SELL;
            if (side == Side.SELL) {                              // 备货:卖单要的币按开盘价先买好
                BigDecimal amt = start.multiply(qty);
                cash = cash.subtract(amt).subtract(amt.multiply(FEE));
                btc = btc.add(qty);
            }
            matcher.place(new Order("BT-" + i + "-" + side.name().charAt(0) + "-0", side, levels.get(i), qty, i));
        }

        List<BigDecimal> equity = new ArrayList<>();
        for (Kline k : klines) {
            for (BigDecimal p : pathInside(k)) {                    // 一根 K 线拆成 4 个价格
                for (Fill f : matcher.onTrade(p)) {
                    BigDecimal amt = f.price().multiply(f.qty());
                    BigDecimal fee = amt.multiply(FEE);             // 每笔成交都扣手续费
                    if (f.side() == Side.BUY) { cash = cash.subtract(amt).subtract(fee); btc = btc.add(f.qty()); }
                    else                      { cash = cash.add(amt).subtract(fee);      btc = btc.subtract(f.qty()); }
                    strategy.nextOrderOnFill(f).ifPresent(matcher::place);   // 挂反向单
                }
            }
            equity.add(cash.add(btc.multiply(k.close())));          // 收盘时记一次账户总值
        }
        return equity;
    }

    /** 一根 K 线里价格先到最高还是先到最低,我们不知道,只能假设 */
    static List<BigDecimal> pathInside(Kline k) {
        boolean up = k.close().compareTo(k.open()) >= 0;
        return up ? List.of(k.open(), k.low(), k.high(), k.close())   // 收盘比开盘高:假设先探底再冲高
                  : List.of(k.open(), k.high(), k.low(), k.close());  // 收盘比开盘低:假设先冲高再探底
    }
}

和 D4 放在同一个包里(D4 的类型都是 public,直接能用)。输入的 K 线就是第 3 节下载、查过缺口的那一份。算出来的资金曲线,就是第 7 节算指标用的那一串数。

K 线里的先后顺序:回测最隐蔽的坑

问题
一根 K 线只告诉你开、高、低、收四个数,不告诉你先到最高还是先到最低。同一根 K 线,顺序不同,结果可能完全相反。
说明什么
K 线越粗(比如用日线),里面藏的来回就越多,回测越不准。常见做法:换更细的 K 线(1 分钟)或逐笔成交数据;或者同一根 K 线里两个价都碰到时,一律按对自己不利的那种算。
 position:  LONG 0.01 BTC @ 60000
 take-profit 60500        stop-loss 59500
 candle:    open 60000  high 60600  low 59400  close 60100

 path A: open -> high -> low -> close    hits 60500 first  => +5 U
 path B: open -> low -> high -> close    hits 59500 first  => -5 U

position = 仓位(做多 0.01 个,60,000 买入);take-profit = 止盈价;stop-loss = 止损价;candle = 这根 K 线;path = 价格在 K 线里走的顺序。同一根 K 线,先碰最高价就是止盈赚 500 × 0.01 = 5 U,先碰最低价就是止损亏 5 U。只看 K 线,你分不出是哪一种。

手续费和滑点在回测里怎么算

手续费
每笔成交按"成交额 × 费率"扣。上面代码里就是 amt.multiply(FEE) 那一行。不扣手续费的回测,结论基本没法用。
滑点
市价单:按订单簿的厚度估一个成交均价,或者简单地每笔多算 0.05% 左右的不利价格(数字只是举例)。
网格这种限价挂单:成交价就是你挂的价,严格说没有滑点,但价格碰到你的价,不等于轮到你成交,前面可能还排着别人。保守做法是价格要再穿过去一点才算你成交。

坑一:未来函数 look-ahead bias

大白话
回测里不小心用到了"当时还不可能知道"的数据,这叫未来函数。
生活例子
考试前偷看了答案,考 100 分不说明你会。
常见写法
"如果今天收盘比开盘高,就在今天开盘买入"。第 4 节的双均线也一样:第 t 天收盘才知道金叉,第 t + 1 天才能成交,写成"第 t 天开盘就买"就中招了。开盘那一刻,收盘价还没出来。代码里只要在做决定的时刻,读了 k.close() 或者下一根 K 线的数据,就是未来函数。
Java 类比
单元测试里断言用的期望值,是从被测代码的输出里抄来的,测试永远通过,但什么也没测。
偷看一眼收盘价,收益能差多少同一段行情 · 两种规则 · 每天一买一卖
偷看版:最终收益–
偷看版:赚钱天数占比–
老实版:最终收益–
老实版:赚钱天数占比–

两条规则只差一个字:偷看版是"今天收盘比开盘高,就今天开盘买、收盘卖";老实版是"昨天收盘比开盘高,就今天开盘买、收盘卖"。行情是随机生成的,没有任何规律。怎么玩:看图里那条粗线(偷看版)和细线(老实版),再点几次"换一段行情"。偷看版每次都一飞冲天,买入的日子几乎天天赚钱(只有涨幅不到手续费的那几天除外),老实版在 0 附近晃,扣了手续费通常还亏。这就是为什么回测收益"好得不像真的"时,第一件事是查未来函数。

坑二:过拟合 overfitting

大白话
参数反复调,调到正好适合过去那段行情,换一段就不灵了,这叫过拟合。
生活例子
把去年的真题答案全背下来,去年的卷子能考满分,今年换了题就傻眼。背答案不等于学会了方法。
样本内和样本外
把历史数据切成两段:前一段叫样本内,用来调参数;后一段叫样本外,调参时一眼都不看,最后只用来检验一次。比如用 2023 年调参数,用 2024 年检验。样本外也不亏,才有一点点可信度。
Java 类比
只针对某几个测试用例写死返回值,测试全绿,但代码逻辑是错的。
过拟合:在 A 段挑出的"最优参数",放到 B 段还剩多少双均线策略(第 4 节)· 36 组参数 · 行情随机生成
A 段(样本内,用来挑参数)
B 段(样本外,只用来检验)
A 段最优那组–
它在 A 段的收益–
同一组在 B 段的收益–
在 B 段 36 组里排第几–

规则:快线在慢线上方就持有 BTC,否则拿着 U;每次换仓扣 0.1%。这和第 4 节"金叉买、死叉卖"是同一件事。行(竖着那列数字)是快线天数,列(顶上那行数字)是慢线天数;格子里是这组参数的收益,底色越深表示赚得(或亏得)越多,加粗描边的那格是 A 段收益最高的那组。怎么玩:看描边那格在 B 段排第几,再点几次"换一段行情"。因为行情本来就是随机的,A 段的"最优"只是碰巧,到 B 段排名经常掉到中间甚至垫底。这就是过拟合。

坑三:幸存者偏差 survivorship bias

大白话
只拿"活到今天"的东西做测试,那些中途死掉的根本没进样本,结果自然好看。
例子
它在第 3 节下载数据时就埋下了。想测"每年买入市值前 50 的币拿一年"。如果你用的是今天交易所还在交易的币的历史数据,那些中途下架、归零的币都不在里面,回测收益一定被高估。

小结:回测会骗人的三种情况是未来函数、过拟合、幸存者偏差,再加上 K 线里的先后顺序。遇到回测收益特别好看的时候,逐条排查一遍。

常见误区 · 回测先不算手续费,之后再说手续费和滑点对高频策略是生死线。总页回测器里网格开 120 格,每格只差 100 U(约 0.17%):手续费为 0 时一年约 +3.1%,每边 0.20% 时变成约 −1.5%。回测时先按吃单费率算,再额外加一档滑点;如果加完就不赚了,这个策略在实盘里多半也不赚。
7

怎么判断一个策略好不好

所在环节1 数据→2 回测→3 策略→4 模拟盘→5 实盘
本节新词资金曲线 · 总收益 · 年化收益 · 回撤 · 最大回撤 · 标准差 · 夏普比率 · 胜率 · 盈亏比

只看赚了多少不够。还要看:中间最难熬的时候亏了多少,以及每天的收益稳不稳。

资金曲线 equity curve

大白话
每天收盘时记一次账户总值(现金 + 持有的币按收盘价折算),连起来就是资金曲线。上一节回测代码返回的就是它。
本节的例子
下面所有指标都用同一条 8 天的资金曲线算:
10,000 → 10,300 → 10,100 → 10,600 → 10,000 → 9,800 → 10,400 → 10,700
第 0 天到第 7 天,单位 U。

总收益 和 年化收益 total return / annualized return

算一遍
总收益 = 最后 ÷ 最初 − 1 = 10,700 ÷ 10,000 − 1 = 7%。
年化
年化收益是把收益换算成"按这个速度跑一整年"。最简单的算法:收益率 × 365 ÷ 天数。这里是 7% × 365 ÷ 7 = 365%。
这个数显然不可信:7 天的运气被放大了 52 倍。时间越短,年化越会骗人。

回撤 和 最大回撤 drawdown / max drawdown

大白话
回撤:从之前的最高点跌下来了多少。最大回撤:整段时间里最惨的那一次回撤。它回答"最难熬的时候,我从高点亏掉了百分之几"。
算一遍
一路记下"到目前为止的最高点":10,000 → 10,300 → 10,300 → 10,600 → 10,600 → 10,600 → …
第 3 天 10,600 是最高点,之后第 5 天跌到 9,800。回撤 = (10,600 − 9,800) ÷ 10,600 = 800 ÷ 10,600 ≈ 7.55%。这是整段里最大的一次,所以最大回撤就是 7.55%。
常见算错
拿最低点和起点比:(9,800 − 10,000) ÷ 10,000 = −2%。错在没从"之前的最高点"算起。你是在 10,600 的时候心里最美,跌到 9,800 才最难受。
Java 类比
遍历一次数组,维护一个 peak = max(peak, e),每一步算 (peak − e) / peak,取最大值。一次循环,O(n)。

标准差 和 夏普比率 standard deviation / Sharpe ratio

大白话
标准差衡量每天收益晃得有多厉害。夏普比率 = 平均每天赚多少 ÷ 每天晃多厉害,意思是"每承受一份波动,换来多少收益"。越高越好。
生活例子
两份工作月薪都平均 1 万:一份每月稳定 1 万,一份有时 3 万有时 0。前一份的"夏普"高得多。
算一遍
① 日收益 = 今天 ÷ 昨天 − 1,7 个数:+3.00%、−1.94%、+4.95%、−5.66%、−2.00%、+6.12%、+2.88%。
② 平均值 = 7 个数加起来 ÷ 7 ≈ 1.051%。
③ 标准差:每个数减平均值、平方、加起来,除以 7 − 1 = 6(样本标准差除以 n−1),再开根号 ≈ 4.309%。
④ 每天的比值 = 1.051% ÷ 4.309% ≈ 0.2439。
⑤ 换算成一年:乘以 √365 ≈ 19.105,夏普 ≈ 0.2439 × 19.105 ≈ 4.66。
约定
币圈全年无休,所以乘 √365(股票常用 √252,因为一年大约 252 个交易日)。严格的夏普要先减掉"无风险利率"(比如存银行的利息),这里取 0。如果资金曲线一动不动、标准差是 0,没法除,就规定夏普 = 0。
别当真
4.66 看起来很漂亮,但它只用了 7 个数。样本这么少的夏普没有意义,常见的说法是至少要看几个月到一年的数据。
动图:在资金曲线上一步步算出三个指标同上面那条 8 天的资金曲线
    总收益–
    最大回撤–
    夏普比率–

    怎么玩:滚到这里会自动一步步播放(系统开了"减少动态效果"时不自动播放,点"播放"即可),右边列表里高亮的是当前这一步;想停下来细看就点"暂停",再用"上一步 / 下一步"来回看,也可以直接点列表里的某一步。看图里的虚线:它就是"到目前为止的最高点",阴影框就是最大回撤那一段。下面三个数字会随着步骤一个个算出来,和上面的手算对一下。

    胜率 和 盈亏比 win rate / profit-loss ratio

    大白话
    胜率:赚钱的交易占几成。盈亏比:平均每次赚的 ÷ 平均每次亏的。
    算一遍
    10 笔交易:4 笔各赚 300 U,6 笔各亏 100 U。胜率 = 4 ÷ 10 = 40%,盈亏比 = 300 ÷ 100 = 3。总盈亏 = 4 × 300 − 6 × 100 = +600 U。
    胜率不到一半也能赚钱;反过来,胜率 90% 但每次亏的是赚的 20 倍,照样亏光。两个数要一起看。
    8

    套利,算一遍

    所在环节1 数据→2 回测→3 策略→4 模拟盘→5 实盘
    本节新词对冲 · Delta 中性 · 资金费率套利 · 基差 · 期现套利 · 交割合约 · 搬砖 · 三角套利

    套利不是"不会亏",而是"不赌涨跌"。它赚的是一个很小、但看得见的差价,所以手续费、速度、意外情况决定了它最后赚不赚钱。

    先复习:资金费率

    D2 讲过
    永续合约没有到期日,为了让它的价格贴着现货,交易所定时让多空双方互相转账,这笔钱叫资金费,比例叫资金费率。费率为正时,做多的人付钱给做空的人。结算间隔常见是 8 小时,各家不同。

    对冲 和 Delta 中性 hedge / delta neutral

    大白话
    对冲:同时拿着两个方向相反的仓位,一边赚的正好抵掉另一边亏的。抵得干干净净、价格怎么动都不赚不亏的状态,行话叫 Delta 中性(Delta 就是"价格变 1,你的盈亏变多少")。
    算一遍
    现货买了 0.1 个 BTC,同时开了 0.1 个 BTC 的永续空单。BTC 涨 1,000:现货赚 0.1 × 1,000 = 100 U,空单亏 0.1 × 1,000 = 100 U,合计 0。
    Java 类比
    像两笔互为冲正的记账:一借一贷,余额不变。

    资金费率套利 funding rate arbitrage / cash and carry

    大白话
    现货买入 + 永续开等量空单,价格涨跌抵消掉;费率为正时,空单那一边每 8 小时收一次资金费。赚的就是资金费。
    算一遍
    ① 现货买入 10,000 U 的 BTC(价格 60,000 时约 0.1667 个)。
    ② 永续开空同样数量,名义价值(仓位按市价值多少钱)10,000 U,用 2 倍杠杆,要押保证金 10,000 ÷ 2 = 5,000 U。
    ③ 费率 0.01% 每 8 小时。30 天 = 30 × 3 = 90 次。每次收 10,000 × 0.01% = 1 U,一共 90 U。
    ④ 手续费:开现货、开空单、平现货、平空单一共 4 笔,每笔 10,000 × 0.05% = 5 U,一共 20 U(0.05% 只是举例,各家不同,挂单和吃单也不同)。
    ⑤ 净收益 = 90 − 20 = 70 U。占用资金 = 10,000 + 5,000 = 15,000 U。30 天收益率 = 70 ÷ 15,000 ≈ 0.467%,简单年化 ≈ 0.467% × 365 ÷ 30 ≈ 5.68%。
    ⑥ 保本费率:费率低到多少就白干?20 ÷ (10,000 × 90) ≈ 0.00222% 每次。
    一个简化
    上面假设 30 天里价格不变,所以每次都按 10,000 U 算。实际上名义价值跟着价格变,每次收的钱也会变一点。

    资金费率套利什么时候会亏

    费率变负
    市场情绪反转,做空的人更多,费率变成负的:现在是你这个空单付钱给多头了。
    基差变大
    基差 = 合约价格 − 现货价格。开仓时永续比现货贵 30 U,平仓时变成贵 90 U:你是空永续、多现货,永续那边多涨的 60 U 算你亏的。每个 BTC 亏 60 U,0.1667 个就是亏约 10 U。
    空单被强平
    2 倍杠杆的空单,按 D2 的强平价公式(维持保证金率 0.5%):60,000 × (1 + 1/2) ÷ (1 + 0.5%) ≈ 89,552。BTC 涨约 49% 空单就被强平。现货那边虽然赚了,但它在另一个账户里,救不了空单,对冲就破了。
    其他
    手续费比预想的高、资金放在交易所本身的风险、资金被占着不能干别的。
    资金费率套利计算器现货 + 等量永续空单 · 费率按每 8 小时一次
    持有天数–
    收到的资金费–
    4 笔手续费–
    基差盈亏–
    净收益–
    占用资金–
    期间收益率–
    简单年化–
    保本费率(每次)–

    怎么玩:默认值就是上面那个例子,先核对净收益 70 U、年化约 5.68%。然后把"每次费率"改成 −0.01(费率变负),看净收益变成多少;再把费率改回 0.01,"基差盈亏"填 −10,看基差扩大吃掉多少。最后把杠杆改成 1,看占用资金和年化怎么变:杠杆低更安全,但收益率更低。

    期现套利 和 交割合约 basis trade / delivery futures

    大白话
    交割合约就是 D2 讲过的"有到期日的期货":到了那天按现货价格结算。到期那一刻,它的价格必须等于现货,所以基差最后会变成 0。期现套利就是赚这个"必然收拢"。
    算一遍
    现货 60,000,一个季度后到期的交割合约 61,200,基差 1,200(2%)。买 1 个现货 + 空 1 个交割合约,拿到到期:两边价格收拢到一起,每个 BTC 赚 1,200 U,再减掉手续费。
    什么时候亏
    到期前基差先变大、空单保证金不够被强平;或者中途被迫平仓,没等到收拢那一天。

    搬砖 和 三角套利 cross-exchange arbitrage / triangular arbitrage

    搬砖
    同一个币在两家交易所价格不一样,便宜的那家买、贵的那家卖。算一遍:A 所 60,000,B 所 60,090,差 90 ÷ 60,000 = 0.15%。两边都吃单、手续费各 0.1%,合计 0.2% > 0.15%,做一次亏一次。而且币从 A 转到 B 要时间,所以真做搬砖的人会提前在两边都放好钱和币。
    三角套利
    同一家交易所里,三个交易对的价格对不上。BTC/USDT = 60,000,ETH/USDT = 3,000,按这两个算 ETH/BTC 应该是 3,000 ÷ 60,000 = 0.05,但盘口上是 0.0502。拿 1 个 BTC:卖成 60,000 U → 买 20 个 ETH → 按 0.0502 换回 20 × 0.0502 = 1.004 个 BTC,多了 0.4%。三笔吃单手续费各 0.1%,一共约 0.3%,剩约 0.1%。这种机会通常几毫秒就被别人抢光了。
    9

    做市:两边都挂,赚中间那点差

    所在环节1 数据→2 回测→3 策略→4 模拟盘→5 实盘
    本节新词做市 · 挂单返佣 · 库存风险 · 逆向选择

    做市就是在订单簿上同时挂一张买单和一张卖单,谁来都跟他成交,赚的是买价和卖价中间那点差。做市的人让订单簿变厚,所以交易所欢迎他们。

    做市 market making

    生活例子
    机场换汇小店:美元买入价 7.10、卖出价 7.15。一天里有人来换进、有人来换出,小店赚的就是每一美元 0.05 的差,它并不关心美元明天涨还是跌。
    算一遍
    挂买单 59,990、卖单 60,010,各 0.01 个 BTC。两边都成交一次:差价 = (60,010 − 59,990) × 0.01 = 0.2 U。
    挂单手续费假设 0.02%:买那笔 599.9 × 0.02% ≈ 0.12 U,卖那笔 600.1 × 0.02% ≈ 0.12 U,合计 0.24 U。净收益 0.2 − 0.24 = −0.04 U,做一轮亏一轮。
    挂单返佣
    D8 讲过:成交量大的用户,挂单手续费可以是负的,也就是交易所倒贴钱给你,这叫挂单返佣。假设是 −0.005%:两笔各收回约 0.03 U,净收益 = 0.2 + 0.06 = 0.26 U。做市赚不赚钱,常常就差在这一档费率上(数字只是举例,各家不同)。
    Java 类比
    一个不停刷新的报价服务:行情一变,就撤掉旧的两张单、按新价格重挂两张。

    库存风险 inventory risk

    大白话
    买单成交了,卖单却一直没人来,手里的币越积越多,价格还在往下走。
    算一遍
    买单在 59,990 成交后,价格一路跌到 59,690(跌 300)。手里这 0.01 个 BTC 浮亏 300 × 0.01 = 3 U。按上面带返佣一轮赚 0.26 U 算,3 ÷ 0.26 ≈ 11.5,十一轮多的利润没了。
    常见应对
    手里货多了,就把买价和卖价一起往下挪,让卖单更容易成交、买单更难成交;再设一个库存上限,到了就只卖不买。

    逆向选择 adverse selection

    大白话
    来吃你单子的人,经常是比你先知道消息的人。
    算一遍
    别的交易所 BTC 已经跌到 59,900 了,你的行情慢了一步,买单还挂在 59,990。有人立刻把它吃掉:你用 59,990 买到一个其实只值 59,900 的东西,0.01 个就亏 0.9 U,三四轮的利润一下没了。
    说明什么
    做市拼速度。D3 讲的行情延迟,在做市里会直接变成亏钱。
    Java 类比
    缓存没及时失效:源数据已经变了,你还在对外提供旧值,别人专挑你的旧值来读。
    10

    仓位和风控:先想好最多亏多少

    所在环节1 数据→2 回测→3 策略→4 模拟盘→5 实盘
    本节新词止损 · 单笔风险 · 熔断 · 凯利公式

    风控先回答一个问题:最坏的时候,我最多亏多少?这个问题答不出来,策略就不能上线。

    止损 stop loss

    大白话
    事先定好一个价格,亏到那里就认赔卖掉,不再扛。和 D1 的止盈正好相反。
    例子
    60,000 买入,止损设在 58,800(跌 2%)。跌到 58,800 就卖,不去想"会不会马上涨回来"。

    单笔风险:先定亏多少,再倒推买多少 risk per trade / position sizing

    大白话
    不是"我有多少钱就买多少",而是"这一笔最多只能亏本金的 1%",再根据止损有多远,倒推出该买多少。每笔最多亏的比例叫单笔风险。
    算一遍
    本金 10,000 U,单笔风险 1% → 这笔最多亏 100 U。
    入场 60,000,止损 58,800 → 每个 BTC 从入场到止损亏 1,200 U。
    该买的数量 = 100 ÷ 1,200 ≈ 0.0833 个 BTC,名义价值 ≈ 0.0833 × 60,000 ≈ 5,000 U。
    连续止损 10 次:每次都是剩下本金的 1%,10,000 × 0.99¹⁰ ≈ 9,043.82 U,还剩九成多,还能继续。
    海龟
    第 5 节海龟的头寸单位就是这个思路:止损距离取 2 个 N 值,每个单位风险是账户的 2%(按 1 个 N 算是 1%)。
    对比
    如果一把梭哈 10,000 U(约 0.1667 个),同样跌到止损就亏 200 U;上 10 倍杠杆,价格跌不到 10% 就被强平(D1 讲过)。

    熔断 circuit breaker / kill switch

    大白话
    整个账户层面的刹车:比如单日亏到本金的 3%,就撤掉所有挂单、停掉所有策略,等人工看过再开(数字只是举例,每个团队自己定)。
    其他常见限制
    最大持仓(这个币最多拿多少)、单笔最大下单量、每分钟最多下几张单(防止程序出 bug 疯狂下单)。
    Java 类比
    就是 Resilience4j 里的熔断器 CircuitBreaker:错误率超过阈值就断开,不再放请求过去,等半开状态试探。给 D4 的网格机器人加风控,最简单的一步就是:价格跌出区间下沿 3% 就停止挂新单并告警。

    凯利公式 Kelly criterion

    大白话
    一个"每次该押多大比例"的理论公式:押的比例 = 胜率 − (1 − 胜率) ÷ 盈亏比。
    算一遍
    用第 7 节的例子:胜率 40%、盈亏比 3 → 0.4 − 0.6 ÷ 3 = 0.4 − 0.2 = 20%。
    实务
    只记结论:凯利算出来的比例通常太激进。胜率和盈亏比都是从回测里估出来的,估高一点就会押太多、亏很惨。所以常见做法是打个折,比如只押一半(叫"半凯利"),这里就是 10%。
    仓位计算器忽略手续费和滑点
    这笔最多亏–
    每个 BTC 到止损亏–
    该买多少 BTC–
    名义价值–
    连续止损 10 次后剩–
    凯利比例–
    半凯利–

    怎么玩:默认值就是上面的例子,先核对 0.0833 个、约 5,000 U。然后把止损价挪近到 59,400(只跌 1%),看"该买多少"翻倍:止损越近,能买得越多,但越容易被正常波动碰到。再把胜率改成 20%,看凯利比例变成负数,意思是这个策略根本不该做。

    11

    大单怎么下:拆开慢慢买

    所在环节1 数据→2 回测→3 策略→4 模拟盘→5 实盘
    本节新词冲击成本 · 拆单 · TWAP · VWAP · 冰山单

    单子越大,你自己就把价格推得越远。拆成小单分批下,是拿时间换价格。

    冲击成本 market impact

    大白话
    你自己的大单吃穿了好几档,把价格推走了,多付的那部分钱叫冲击成本。它就是 D1 讲的第一种滑点,只不过是你自己造成的。
    算一遍
    用 D1 那个订单簿:卖单 60,010 有 0.3 个、60,020 有 0.5 个、60,050 有 1.0 个。市价买 1 个:0.3 × 60,010 + 0.5 × 60,020 + 0.2 × 60,050 = 18,003 + 30,010 + 12,010 = 60,023 U,成交均价 60,023,比卖一价多付 13 U。买 10 个的话,要一路吃到很远的档位,多付的钱会大得多。

    拆单:TWAP、VWAP、冰山单 order slicing

    拆单
    把一张大单拆成很多小单,分批下出去,每次只吃一点点,等订单簿自己补回来再下一批。
    TWAP
    按时间平均拆(Time-Weighted Average Price)。算一遍:1 小时内买 10 个 BTC,每 6 分钟买 1 个,一共 10 次。
    VWAP
    按成交量的分布拆(Volume-Weighted Average Price):市场本来交易活跃的时段多买,冷清的时段少买。算一遍:过去一段时间里,每天 9 点到 10 点的成交量占全天的 8%,那一张 100 个的大单,这个小时就下 8 个。
    冰山单
    挂一张 10 个的卖单,订单簿上只露出 0.5 个,这 0.5 个成交了再自动露出下一个 0.5,像冰山只露一角。目的是不让别人看出有个大单在这里。
    代价
    拆得越慢,价格越可能在这段时间里跑掉;拆的节奏太规律,也可能被别人识别出来抢在你前面。
    Java 类比
    批量任务分页处理 + 限流:一次性写 100 万行会把数据库打挂,按每批 1,000 行、每秒 10 批慢慢写。
    12

    从模拟盘到实盘:一步一步放钱

    所在环节1 数据→2 回测→3 策略→4 模拟盘→5 实盘
    本节新词测试网 · 小资金实盘 · 一键全撤全平 · API Key · 密钥 · 签名 · 时间戳 · recvWindow · IP 白名单

    没有哪个策略是回测完直接上大钱的。每一步都要有明确的"过关数字",达到了才进下一步。

    先分清三种"假"和"真"

    模拟盘
    D4 讲过:真行情、假钱,撮合是你自己程序里模拟的(PaperMatcher)。
    测试网
    交易所提供的一整套假环境:下单走真的接口、真的签名、真的撮合,钱是假的。它能检验你的接口调用、签名、推送处理、对账是不是都对,但它的行情往往和真实市场不一样(深度薄、价格怪),只验证流程,不验证赚钱。D10 会实际上手。
    实盘
    真钱。先上小资金实盘:只放计划资金的一小部分(比如一成,每个团队自己定),跑一段时间再决定要不要加。

    模拟盘换成测试网或实盘,要多会一件事:私有接口 D10 细讲

    为什么
    公开行情谁都能看(D3);下单、撤单、查余额,交易所要确认"这真的是账户本人"。
    API Key 和 密钥
    在交易所后台创建一对:API Key 相当于用户名,随请求发出去;密钥相当于密码,永远不在网络上传输,只在本机用来算签名。
    签名
    把请求参数和密钥一起算出一串"指纹"附在请求后面(最常用的算法是 HMAC-SHA256)。交易所用同一把密钥再算一遍,对上了才认。别人截获了请求也改不了参数。
    时间戳
    每个请求带上发出的时刻,交易所发现它和自己的时间相差超过 recvWindow(比如 5,000 毫秒,各家不同)就拒绝,防止旧请求被重放。你电脑时钟不准,所有请求都会被拒。
    限频和推送
    限频常按权重算,每个接口算几分;自己的订单和成交变化通过用户数据流(一条只推你自己消息的 WebSocket)推过来,推送断了就回到 D5 的按编号查询和对账。
    Key 的规矩
    只开"读取 + 交易",永远不开"提现";设 IP 白名单,只允许你服务器的 IP 使用;密钥不写进代码、不进 Git、不贴进外部 AI 工具。完整的 Java 签名下单代码在 D10。
    阶段要验证什么过关看什么
    回测规则本身有没有道理扣了手续费和滑点还赚;样本外不亏;最大回撤在能接受的范围里;排查过未来函数
    模拟盘程序逻辑在真实行情下对不对成交次数、盈亏和同一段时间的回测结果差得不多
    测试网接口、签名、推送、对账每一张单都能按 clientOid 查到、对上;断线重连、重启后状态一致(D5)
    小资金实盘真实的滑点、排队、延迟实盘和回测的差距解释得清楚;没有出现任何"不知道为什么"的单
    放大规模变大后冲击成本逐步加钱,每次加完观察一段时间

    一键全撤全平 kill switch

    大白话
    一个按钮(或一条命令):撤掉这个策略所有挂单,平掉所有仓位,停止下新单。上实盘之前必须先有它,并且亲手演练过一次。
    为什么
    出事的时候没时间写代码。程序疯狂下单、行情剧烈波动、交易所公告维护,你要能在几秒内让策略彻底停下来。

    回测和实盘为什么总对不上

    常见原因
    ① 滑点和排队:回测以为成交了,实盘没轮到你。② 延迟:回测里信号一出就成交,实盘要几十上百毫秒。③ 部分成交:回测常按全部成交算。④ 断线、限频、接口报错:回测里不存在。⑤ 资金费:做合约的回测常常忘了算。
    进组后怎么用
    拿到币实战时,每笔单都记下"想以什么价成交"和"实际成交价",一周后算一算差距。这是最快让你对"真实交易"有感觉的办法。
    13

    词典和自测

    本页的新词,以及前几天学过、这里又用到的词,都能在这里查到。

    自测 10 题

    读完以后

    回到 七日冲刺 D9,按这个顺序做(总页 D9 还有"今日实操":从 data.binance.vision 下载真实日 K,导入回测器跑三种策略):

    1. 回测器里把策略在"网格 / 双均线 / 海龟"之间切换,三段行情各跑一遍,对比谁在单边行情里赚、谁在来回震荡里赚。你会看到本页反复说的那句话:一个赚钱的行情,往往就是另一个策略亏钱的行情。再把"只做多"勾上看看趋势策略少了哪一半。
    2. 网格那边把格数拉到 120、手续费在 0 和 0.20% 之间来回拖,看利润怎么被手续费吃掉;资金费率套利计算器把费率改成负数试一次。
    3. 做三道编程题:maCross(第 4 节的金叉死叉,表格那组数就是它的一个测试)、turtlePlan(第 5 节的头寸单位、加仓价和止损)、metrics(第 7 节手算过的总收益、最大回撤和夏普比率)。
    4. 最后打卡。