量化交易是什么:一套系统的五个环节
一套完整的量化系统就五个环节:数据 → 回测 → 策略 → 模拟盘 → 实盘。你在 D1–D8 已经零零散散碰过其中几环,这一页把它们串成一条线。
量化交易 quantitative trading / quant
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 节 | 实盘和回测的差距,每一笔都解释得清 |
为什么是这个顺序
策略这一环,拆开是四件事:信号、仓位、执行、风控
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 = 风控(止损 / 最大持仓 / 一键停止)。风控是一个环:成交之后的结果回来,风控决定下一轮还能不能继续。
进组以后留意一件事:你们组是"公司自己拿钱跑策略",还是"给用户提供策略工具"(网格、定投、跟单这类产品),还是两样都有。关心的重点不一样,各家分工也不同。
常见策略地图
每种策略赚的钱,都来自市场上的某种"规律"。这种规律消失的时候,就是它亏钱的时候。
趋势跟踪 trend following
均值回归 mean reversion
统计套利 和 配对交易 statistical arbitrage / pairs trading
做市(同时挂买单和卖单,赚中间的差价)和套利(同一个东西在两个地方价格不一样,低买高卖)是交易所里最常见的两类,第 8、9 节专门一笔笔算。
| 策略 | 靠什么赚钱 | 什么行情会亏 | 例子 |
|---|---|---|---|
| 趋势跟踪 | 涨跌能持续一段 | 来回震荡、反复假突破 | 双均线(第 4 节)、海龟交易法则(唐奇安通道突破,第 5 节) |
| 均值回归 | 价格来回晃 | 单边大涨大跌 | 网格、配对交易 |
| 做市 | 买卖价差 + 挂单返佣 | 单边行情里货越积越多;被消息更快的人吃单 | 同时挂买一、卖一 |
| 套利 | 同一个东西两处价格不一样 | 价差没收回来反而扩大;手续费和速度拼不过别人 | 资金费率套利、搬砖 |
| 统计套利 | 历史上稳定的关系 | 关系断了 | 配对交易 |
这张表进组后很有用:听到任何一个策略,先问"它靠什么赚钱"和"什么行情会亏"。
数据:历史 K 线从哪来、怎么洗
回测的结论,最多和数据一样可靠。数据里少一根 K 线,回测就可能凭空多出、或者漏掉一笔成交。
历史 K 线 historical klines
UTC 时间 Coordinated Universal Time
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
检查缺口,算一遍
checkGaps。怎么玩:默认就是上面的 7 行。先看"行数"和"首尾之间应有"都是 7,再看表格里标出来的那两行:一行缺了 1 根,一行重复。然后把重复的那行删掉、在中间补一行 1727751600000(03:00),看问题全部消失。表格里同时显示 UTC 和北京时间,对照一下相差 8 小时。
存在哪里
数据本身也会让回测骗人
双均线:最经典的趋势策略
双均线只有一条规则:短期平均价从下往上穿过长期平均价就买,从上往下穿过就卖。几行代码就能写完,但趋势策略的优点和毛病它全都有。
先把均线算一遍 moving average
双均线、快线和慢线 dual moving average crossover
下面这张表用 3 日均线当快线、5 日均线当慢线。前 4 天慢线还算不出来(不满 5 天),不比较;第 5 天两条线都有了;从第 6 天起才能和前一天比,看有没有穿过去。
| 第几天 | 收盘 | 3 日均线(快) | 5 日均线(慢) | 快线在慢线 | 信号 |
|---|---|---|---|---|---|
| 1–4 | 100 | 第 3 天起 100.00 | – | – | |
| 5 | 100 | 100.00 | 100.00 | 相等 | |
| 6 | 100 | 100.00 | 100.00 | 相等 | |
| 7 | 103 | 101.00 | 100.60 | 上方 | 金叉:买 |
| 8 | 106 | 103.00 | 101.80 | 上方 | |
| 9 | 109 | 106.00 | 103.60 | 上方 | |
| 10 | 112 | 109.00 | 106.00 | 上方 | 价格最高点 |
| 11 | 111 | 110.67 | 108.20 | 上方 | |
| 12 | 108 | 110.33 | 109.20 | 上方 | |
| 13 | 104 | 107.67 | 108.80 | 下方 | 死叉:卖 |
| 14 | 100 | 104.00 | 107.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。
信号出来以后,哪天才能成交
滞后 和 来回打脸 lag / whipsaw
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"根,和上表一致。
图里细实线是价格,粗线是快线,虚线是慢线;朝上的小三角是金叉,朝下的小三角是死叉;浅色底纹是拿着 BTC 的日子。规则:第 t 天收盘出信号,第 t + 1 天起才吃涨跌。两个收益都从慢线算得出来的那天起算。怎么玩:① 默认是"先横盘,再大涨,再大跌",只交叉一两次,双均线拿住了中间的大涨、躲开了后面的大跌,明显跑赢一直拿着。② 切到"来回震荡",交叉一下子变多,底纹碎成一小段一小段,收益变成负的,这就是来回打脸。③ 在震荡行情里把快线、慢线都调大,交叉变少、亏得少一点,但换回趋势行情会发现进出场更晚了,这就是滞后的代价。
海龟交易法则:一整套能照着执行的规则
海龟法则的价值不在某个神奇指标,而在于它把"什么时候进、买多少、什么时候加、什么时候认赔、什么时候走"全写成了规则,没有一步需要"看感觉"。
海龟交易法则 Turtle trading rules
唐奇安通道 和 两套系统 Donchian channel / System 1, System 2
真实波幅 和 N 值 true range / N (ATR)
换一天:昨收 60,000,今天最高 63,000、最低 62,000(一开盘就跳上去了):最高 − 最低只有 1,000,但 |63,000 − 60,000| = 3,000,TR = 3,000。
头寸单位:波动越大,买得越少 unit
加仓 和 止损 pyramiding / 2N stop
| 第几个单位 | 买入价 | 加完后所有单位的止损 | 这时打到止损一共亏 |
|---|---|---|---|
| 1 | 60,000 | 60,000 − 4,800 = 55,200 | 4,800 × 0.041667 ≈ 200.00 U(2%) |
| 2 | 61,200 | 61,200 − 4,800 = 56,400 | (3,600 + 4,800) × 0.041667 ≈ 350.00 U |
| 3 | 62,400 | 62,400 − 4,800 = 57,600 | (2,400 + 3,600 + 4,800) × 0.041667 ≈ 450.00 U |
| 4 | 63,600 | 63,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。
什么行情会亏
图里每根竖线是一天的最高到最低,折线连着每天的收盘价;点线是过去 20 天最高价(入场线),实线是过去 10 天最低价(离场线),虚线是止损线;带数字的圆圈是第几个单位买在哪里,最后的方块是离场点。怎么玩:滚到这里会自动一步步播放(系统开了"减少动态效果"时不自动播放,点"播放"即可),随时可以暂停,用"上一步 / 下一步"来回看。盯住两件事:止损线每加一次仓就往上跳一格;离场线在价格回落前已经悄悄抬到了止损线上方很远,所以最后是离场线先被碰到,而不是止损。
回测:拿过去检验策略,以及它怎么骗人
回测就是拿过去的行情,假装策略当时就在跑,看它会赚还是亏。回测能帮你淘汰坏策略,但回测好看不等于策略好。
回测 backtest
好消息: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 线里的先后顺序:回测最隐蔽的坑
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) 那一行。不扣手续费的回测,结论基本没法用。网格这种限价挂单:成交价就是你挂的价,严格说没有滑点,但价格碰到你的价,不等于轮到你成交,前面可能还排着别人。保守做法是价格要再穿过去一点才算你成交。
坑一:未来函数 look-ahead bias
k.close() 或者下一根 K 线的数据,就是未来函数。两条规则只差一个字:偷看版是"今天收盘比开盘高,就今天开盘买、收盘卖";老实版是"昨天收盘比开盘高,就今天开盘买、收盘卖"。行情是随机生成的,没有任何规律。怎么玩:看图里那条粗线(偷看版)和细线(老实版),再点几次"换一段行情"。偷看版每次都一飞冲天,买入的日子几乎天天赚钱(只有涨幅不到手续费的那几天除外),老实版在 0 附近晃,扣了手续费通常还亏。这就是为什么回测收益"好得不像真的"时,第一件事是查未来函数。
坑二:过拟合 overfitting
规则:快线在慢线上方就持有 BTC,否则拿着 U;每次换仓扣 0.1%。这和第 4 节"金叉买、死叉卖"是同一件事。行(竖着那列数字)是快线天数,列(顶上那行数字)是慢线天数;格子里是这组参数的收益,底色越深表示赚得(或亏得)越多,加粗描边的那格是 A 段收益最高的那组。怎么玩:看描边那格在 B 段排第几,再点几次"换一段行情"。因为行情本来就是随机的,A 段的"最优"只是碰巧,到 B 段排名经常掉到中间甚至垫底。这就是过拟合。
坑三:幸存者偏差 survivorship bias
小结:回测会骗人的三种情况是未来函数、过拟合、幸存者偏差,再加上 K 线里的先后顺序。遇到回测收益特别好看的时候,逐条排查一遍。
怎么判断一个策略好不好
只看赚了多少不够。还要看:中间最难熬的时候亏了多少,以及每天的收益稳不稳。
资金曲线 equity curve
10,000 → 10,300 → 10,100 → 10,600 → 10,000 → 9,800 → 10,400 → 10,700第 0 天到第 7 天,单位 U。
总收益 和 年化收益 total return / annualized return
这个数显然不可信:7 天的运气被放大了 52 倍。时间越短,年化越会骗人。
回撤 和 最大回撤 drawdown / max drawdown
第 3 天 10,600 是最高点,之后第 5 天跌到 9,800。回撤 = (10,600 − 9,800) ÷ 10,600 = 800 ÷ 10,600 ≈ 7.55%。这是整段里最大的一次,所以最大回撤就是 7.55%。
peak = max(peak, e),每一步算 (peak − e) / peak,取最大值。一次循环,O(n)。标准差 和 夏普比率 standard deviation / Sharpe ratio
② 平均值 = 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。
怎么玩:滚到这里会自动一步步播放(系统开了"减少动态效果"时不自动播放,点"播放"即可),右边列表里高亮的是当前这一步;想停下来细看就点"暂停",再用"上一步 / 下一步"来回看,也可以直接点列表里的某一步。看图里的虚线:它就是"到目前为止的最高点",阴影框就是最大回撤那一段。下面三个数字会随着步骤一个个算出来,和上面的手算对一下。
胜率 和 盈亏比 win rate / profit-loss ratio
胜率不到一半也能赚钱;反过来,胜率 90% 但每次亏的是赚的 20 倍,照样亏光。两个数要一起看。
套利,算一遍
套利不是"不会亏",而是"不赌涨跌"。它赚的是一个很小、但看得见的差价,所以手续费、速度、意外情况决定了它最后赚不赚钱。
先复习:资金费率
对冲 和 Delta 中性 hedge / delta neutral
资金费率套利 funding rate arbitrage / cash and carry
② 永续开空同样数量,名义价值(仓位按市价值多少钱)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% 每次。
资金费率套利什么时候会亏
怎么玩:默认值就是上面那个例子,先核对净收益 70 U、年化约 5.68%。然后把"每次费率"改成 −0.01(费率变负),看净收益变成多少;再把费率改回 0.01,"基差盈亏"填 −10,看基差扩大吃掉多少。最后把杠杆改成 1,看占用资金和年化怎么变:杠杆低更安全,但收益率更低。
期现套利 和 交割合约 basis trade / delivery futures
搬砖 和 三角套利 cross-exchange arbitrage / triangular arbitrage
做市:两边都挂,赚中间那点差
做市就是在订单簿上同时挂一张买单和一张卖单,谁来都跟他成交,赚的是买价和卖价中间那点差。做市的人让订单簿变厚,所以交易所欢迎他们。
做市 market making
挂单手续费假设 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,做一轮亏一轮。
库存风险 inventory risk
逆向选择 adverse selection
仓位和风控:先想好最多亏多少
风控先回答一个问题:最坏的时候,我最多亏多少?这个问题答不出来,策略就不能上线。
止损 stop loss
单笔风险:先定亏多少,再倒推买多少 risk per trade / position sizing
入场 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,还剩九成多,还能继续。
熔断 circuit breaker / kill switch
CircuitBreaker:错误率超过阈值就断开,不再放请求过去,等半开状态试探。给 D4 的网格机器人加风控,最简单的一步就是:价格跌出区间下沿 3% 就停止挂新单并告警。凯利公式 Kelly criterion
怎么玩:默认值就是上面的例子,先核对 0.0833 个、约 5,000 U。然后把止损价挪近到 59,400(只跌 1%),看"该买多少"翻倍:止损越近,能买得越多,但越容易被正常波动碰到。再把胜率改成 20%,看凯利比例变成负数,意思是这个策略根本不该做。
大单怎么下:拆开慢慢买
单子越大,你自己就把价格推得越远。拆成小单分批下,是拿时间换价格。
冲击成本 market impact
拆单:TWAP、VWAP、冰山单 order slicing
从模拟盘到实盘:一步一步放钱
没有哪个策略是回测完直接上大钱的。每一步都要有明确的"过关数字",达到了才进下一步。
先分清三种"假"和"真"
PaperMatcher)。模拟盘换成测试网或实盘,要多会一件事:私有接口 D10 细讲
| 阶段 | 要验证什么 | 过关看什么 |
|---|---|---|
| 回测 | 规则本身有没有道理 | 扣了手续费和滑点还赚;样本外不亏;最大回撤在能接受的范围里;排查过未来函数 |
| 模拟盘 | 程序逻辑在真实行情下对不对 | 成交次数、盈亏和同一段时间的回测结果差得不多 |
| 测试网 | 接口、签名、推送、对账 | 每一张单都能按 clientOid 查到、对上;断线重连、重启后状态一致(D5) |
| 小资金实盘 | 真实的滑点、排队、延迟 | 实盘和回测的差距解释得清楚;没有出现任何"不知道为什么"的单 |
| 放大 | 规模变大后冲击成本 | 逐步加钱,每次加完观察一段时间 |
一键全撤全平 kill switch
回测和实盘为什么总对不上
词典和自测
本页的新词,以及前几天学过、这里又用到的词,都能在这里查到。
自测 10 题
回到 七日冲刺 D9,按这个顺序做(总页 D9 还有"今日实操":从 data.binance.vision 下载真实日 K,导入回测器跑三种策略):
- 回测器里把策略在"网格 / 双均线 / 海龟"之间切换,三段行情各跑一遍,对比谁在单边行情里赚、谁在来回震荡里赚。你会看到本页反复说的那句话:一个赚钱的行情,往往就是另一个策略亏钱的行情。再把"只做多"勾上看看趋势策略少了哪一半。
- 网格那边把格数拉到 120、手续费在 0 和 0.20% 之间来回拖,看利润怎么被手续费吃掉;资金费率套利计算器把费率改成负数试一次。
- 做三道编程题:
maCross(第 4 节的金叉死叉,表格那组数就是它的一个测试)、turtlePlan(第 5 节的头寸单位、加仓价和止损)、metrics(第 7 节手算过的总收益、最大回撤和夏普比率)。 - 最后打卡。