中心化交易所现货与杠杆业务

加密货币交易所是高性能金融系统的典型代表,撮合引擎、订单簿、风控系统都是面试高频考点

🎯 面试重点


📖 一、现货交易(Spot Trading)

1.1 现货交易基础

定义:用户用自有资金买卖加密货币,即时交割,无杠杆。

核心流程

用户下单 → 订单校验 → 进入订单簿 → 撮合引擎匹配 → 成交 → 更新资产 → 清算结算

关键概念

1.2 订单类型

订单类型 说明 适用场景
限价单(Limit) 指定价格成交,不保证成交 控制成本,做 Maker 赚手续费返佣
市价单(Market) 以当前最优价立即成交 急需成交,追求速度
止损单(Stop-Loss) 价格触发后转为市价单 风险控制,自动止损
止盈止损(TP/SL) 同时设置止盈和止损价格 自动化交易策略
冰山单(Iceberg) 大单拆分为多个小单逐步成交 隐藏真实交易量,减少市场冲击
FOK(Fill or Kill) 全部成交或全部取消 要求完全成交
IOC(Immediate or Cancel) 立即成交剩余取消 部分成交也可接受

1.3 撮合引擎(Matching Engine)

核心职责:将买单和卖单按价格优先、时间优先原则进行匹配。

价格优先、时间优先原则

买单:价格高的优先(买得起的优先)
卖单:价格低的优先(卖得便宜的优先)
同价格:先下单的优先

示例:
买盘(Bids):         卖盘(Asks):
100.5  1000          100.6  500
100.4  2000          100.7  1000
100.3  1500          100.8  2000

新买单:价格 100.6,数量 800
→ 与卖一 100.6 匹配成交 500
→ 剩余 300 成为新的买一(100.6)

撮合引擎架构

public class MatchingEngine {
    
    // 订单簿:买盘的优先级队列(价格降序,同价格时间升序)
    private PriorityQueue<Order> bidBook = new PriorityQueue<>((a, b) -> {
        int priceCompare = b.getPrice().compareTo(a.getPrice());
        return priceCompare != 0 ? priceCompare : 
               Long.compare(a.getTimestamp(), b.getTimestamp());
    });
    
    // 订单簿:卖盘的优先级队列(价格升序,同价格时间升序)
    private PriorityQueue<Order> askBook = new PriorityQueue<>((a, b) -> {
        int priceCompare = a.getPrice().compareTo(b.getPrice());
        return priceCompare != 0 ? priceCompare : 
               Long.compare(a.getTimestamp(), b.getTimestamp());
    });
    
    // 撮合入口
    public List<Trade> match(Order newOrder) {
        List<Trade> trades = new ArrayList<>();
        
        if (newOrder.getSide() == Side.BUY) {
            // 买单与卖盘撮合
            trades = matchWithAsks(newOrder);
        } else {
            // 卖单与买盘撮合
            trades = matchWithBids(newOrder);
        }
        
        return trades;
    }
    
    private List<Trade> matchWithAsks(Order buyOrder) {
        List<Trade> trades = new ArrayList<>();
        
        while (!buyOrder.isFilled() && !askBook.isEmpty()) {
            Order bestAsk = askBook.peek();
            
            // 价格是否匹配(买价 >= 卖价)
            if (buyOrder.getPrice().compareTo(bestAsk.getPrice()) < 0) {
                break;  // 无法成交
            }
            
            // 撮合
            BigDecimal tradeQty = buyOrder.getRemainingQty()
                .min(bestAsk.getRemainingQty());
            BigDecimal tradePrice = bestAsk.getPrice();  // 以卖价成交
            
            Trade trade = new Trade(buyOrder, bestAsk, tradeQty, tradePrice);
            trades.add(trade);
            
            // 更新订单状态
            buyOrder.fill(tradeQty);
            bestAsk.fill(tradeQty);
            
            // 完全成交的订单从订单簿移除
            if (bestAsk.isFilled()) {
                askBook.poll();
            }
        }
        
        // 未完全成交的限价单进入订单簿
        if (!buyOrder.isFilled() && buyOrder.getType() == OrderType.LIMIT) {
            bidBook.offer(buyOrder);
        }
        
        return trades;
    }
}

1.4 订单簿(Order Book)实现

核心挑战

数据结构选择

方案 查询最优价 插入/删除 内存占用 适用场景
红黑树(TreeMap) O(log n) O(log n) 通用,Java 常用
跳表(SkipList) O(log n) O(log n) 并发性能好
数组 + 哈希表 O(1) O(1) 价格范围有限时

高性能实现(价格分桶)

// 假设价格精度为 0.01,价格范围 0-100000
// 使用数组索引直接定位,O(1) 复杂度
public class PriceLadder {
    
    private static final int PRICE_PRECISION = 2;  // 小数位
    private static final BigDecimal PRICE_TICK = new BigDecimal("0.01");
    private static final int MAX_PRICE_INDEX = 10_000_000;  // 支持到 100000.00
    
    // 每个价格档的订单链表
    private PriceLevel[] bidLevels = new PriceLevel[MAX_PRICE_INDEX];
    private PriceLevel[] askLevels = new PriceLevel[MAX_PRICE_INDEX];
    
    // 当前最优价索引
    private int bestBidIndex = 0;
    private int bestAskIndex = MAX_PRICE_INDEX - 1;
    
    // 价格 → 索引转换
    private int priceToIndex(BigDecimal price) {
        return price.movePointRight(PRICE_PRECISION).intValue();
    }
    
    // O(1) 插入订单
    public void addOrder(Order order) {
        int index = priceToIndex(order.getPrice());
        PriceLevel level = order.getSide() == Side.BUY ? 
                          bidLevels[index] : askLevels[index];
        if (level == null) {
            level = new PriceLevel(order.getPrice());
            if (order.getSide() == Side.BUY) {
                bidLevels[index] = level;
                bestBidIndex = Math.max(bestBidIndex, index);
            } else {
                askLevels[index] = level;
                bestAskIndex = Math.min(bestAskIndex, index);
            }
        }
        level.addOrder(order);
    }
    
    // O(1) 获取最优价
    public BigDecimal getBestBid() {
        return new BigDecimal(bestBidIndex).movePointLeft(PRICE_PRECISION);
    }
}

1.5 资产系统(Wallet)

账户模型

用户账户
├── 现货账户(Spot)
│   ├── BTC:1.5
│   ├── ETH:10.0
│   └── USDT:50000.0
├── 杠杆账户(Margin)
│   ├── 已借 BTC:0.5
│   ├── 已借 USDT:10000
│   └── 保证金:20000 USDT
└── 合约账户(Futures)
    └── ...

资产操作原子性

// 成交后的资产划转必须原子执行
@Transactional
public void settleTrade(Trade trade) {
    String buyerId = trade.getBuyerId();
    String sellerId = trade.getSellerId();
    String baseCurrency = trade.getBaseCurrency();   // BTC
    String quoteCurrency = trade.getQuoteCurrency(); // USDT
    BigDecimal qty = trade.getQuantity();
    BigDecimal price = trade.getPrice();
    BigDecimal amount = qty.multiply(price);
    
    // 买方:减少 USDT,增加 BTC
    walletService.deductAvailable(buyerId, quoteCurrency, amount);
    walletService.addAvailable(buyerId, baseCurrency, qty);
    
    // 卖方:减少 BTC,增加 USDT
    walletService.deductAvailable(sellerId, baseCurrency, qty);
    walletService.addAvailable(sellerId, quoteCurrency, amount);
    
    // 记录成交明细
    tradeHistoryService.save(trade);
}

📖 二、杠杆交易(Margin Trading)

2.1 杠杆交易基础

定义:用户借入资金进行交易,放大收益和风险。

核心概念

杠杆计算示例

用户本金:10000 USDT
杠杆倍数:5x
可借资金:40000 USDT(总仓位 50000 USDT)

情况1:BTC 上涨 20%
  仓位价值:50000 × 1.2 = 60000 USDT
  还款后:60000 - 40000 = 20000 USDT
  收益率:(20000 - 10000) / 10000 = 100%(放大5倍)

情况2:BTC 下跌 20%
  仓位价值:50000 × 0.8 = 40000 USDT
  还款后:40000 - 40000 = 0 USDT
  收益率:-100%(爆仓,本金亏光)

2.2 保证金系统

全仓保证金(Cross Margin)

逐仓保证金(Isolated Margin)

保证金计算公式

// 全仓模式
public class CrossMarginCalculator {
    
    // 计算账户权益(总资产 - 总负债)
    public BigDecimal calculateEquity(Account account) {
        BigDecimal totalAsset = account.getBalances().values().stream()
            .map(Balance::getTotal)  // 可用 + 冻结
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        BigDecimal totalDebt = account.getDebts().values().stream()
            .map(Debt::getPrincipal)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        return totalAsset.subtract(totalDebt);
    }
    
    // 计算保证金率
    public BigDecimal calculateMarginRatio(Account account) {
        BigDecimal equity = calculateEquity(account);
        BigDecimal totalPositionValue = calculateTotalPositionValue(account);
        
        if (totalPositionValue.compareTo(BigDecimal.ZERO) == 0) {
            return BigDecimal.valueOf(Long.MAX_VALUE);  // 无仓位
        }
        
        return equity.divide(totalPositionValue, 4, RoundingMode.DOWN);
    }
    
    // 检查是否触发强平
    public boolean isLiquidationTriggered(Account account) {
        BigDecimal marginRatio = calculateMarginRatio(account);
        BigDecimal maintenanceMarginRate = account.getMaintenanceMarginRate();  // 如 0.05 (5%)
        
        return marginRatio.compareTo(maintenanceMarginRate) < 0;
    }
}

2.3 强平(Liquidation)机制

强平触发条件

保证金率 = 账户权益 / 持仓总价值

当 保证金率 < 维持保证金率(如 5%)时,触发强平

示例:
  本金:10000 USDT
  借入:40000 USDT
  持仓:50000 USDT 的 BTC
  
  BTC 下跌 19%:
    持仓价值:50000 × 0.81 = 40500 USDT
    账户权益:40500 - 40000 = 500 USDT
    保证金率:500 / 40500 = 1.23% < 5%
    → 触发强平

强平流程

@Service
public class LiquidationService {
    
    @Autowired
    private RiskEngine riskEngine;
    
    @Autowired
    private MatchingEngine matchingEngine;
    
    // 定时扫描或事件驱动检查
    @Scheduled(fixedDelay = 100)  // 每100ms检查一次
    public void checkLiquidation() {
        List<Account> highRiskAccounts = riskEngine.getHighRiskAccounts();
        
        for (Account account : highRiskAccounts) {
            if (shouldLiquidate(account)) {
                executeLiquidation(account);
            }
        }
    }
    
    private void executeLiquidation(Account account) {
        try {
            // 1. 冻结账户,禁止新下单
            account.freeze();
            
            // 2. 取消所有未成交订单
            orderService.cancelAllOrders(account.getId());
            
            // 3. 市价平仓(以当前最优价立即卖出)
            List<Position> positions = account.getPositions();
            for (Position position : positions) {
                Order liquidationOrder = Order.builder()
                    .accountId(account.getId())
                    .symbol(position.getSymbol())
                    .side(position.getSide() == Side.BUY ? Side.SELL : Side.BUY)
                    .type(OrderType.MARKET)  // 市价单
                    .quantity(position.getQuantity())
                    .build();
                
                // 发送到撮合引擎
                matchingEngine.submitOrder(liquidationOrder);
            }
            
            // 4. 归还借款
            settleDebt(account);
            
            // 5. 剩余资金返还用户
            BigDecimal remaining = account.getEquity();
            if (remaining.compareTo(BigDecimal.ZERO) > 0) {
                walletService.transferToSpot(account.getId(), remaining);
            }
            
            // 6. 记录强平日志
            liquidationLogService.save(new LiquidationLog(account, remaining));
            
        } catch (Exception e) {
            // 强平失败,触发保险基金或自动减仓
            insuranceFundService.handleFailedLiquidation(account);
        }
    }
}

2.4 风险限额(Risk Limit)

目的:限制用户持仓规模,降低系统性风险。

分级制度

Level 1: 持仓 ≤ 100 BTC,维持保证金率 2.5%,最高杠杆 125x
Level 2: 持仓 ≤ 500 BTC,维持保证金率 5%,最高杠杆 100x
Level 3: 持仓 ≤ 1000 BTC,维持保证金率 10%,最高杠杆 50x
...

风险限额检查

public class RiskLimitService {
    
    public void checkRiskLimit(String accountId, String symbol, 
                               BigDecimal newPosition) {
        RiskLimit limit = riskLimitRepository.findByAccountId(accountId);
        
        // 检查持仓限额
        if (newPosition.compareTo(limit.getPositionLimit()) > 0) {
            throw new RiskLimitExceededException(
                "持仓超过风险限额: " + limit.getPositionLimit());
        }
        
        // 检查杠杆倍数
        BigDecimal leverage = calculateLeverage(accountId);
        if (leverage.compareTo(limit.getMaxLeverage()) > 0) {
            throw new RiskLimitExceededException(
                "杠杆超过限制: " + limit.getMaxLeverage());
        }
    }
}

📖 三、交易所核心系统架构

3.1 整体架构

┌─────────────────────────────────────────────────────────────┐
│                        接入层                                 │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐        │
│  │  Web    │  │  App    │  │  API    │  │  WS     │        │
│  │  前端   │  │  客户端 │  │  接口   │  │  推送   │        │
│  └────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘        │
└───────┼────────────┼────────────┼────────────┼──────────────┘
        │            │            │            │
        └────────────┴────────────┴────────────┘
                          │
┌─────────────────────────▼───────────────────────────────────┐
│                        网关层                                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │ 负载均衡    │  │ 限流熔断    │  │ 认证鉴权            │  │
│  │ (Nginx/ALB) │  │ (Sentinel)  │  │ (JWT/API Key)       │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────┬───────────────────────────────────┘
                          │
┌─────────────────────────▼───────────────────────────────────┐
│                        业务层                                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │ 订单服务    │  │ 撮合引擎    │  │ 资产服务            │  │
│  │ (Order)     │  │ (Matching)  │  │ (Wallet)            │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │ 杠杆服务    │  │ 风控服务    │  │ 清算服务            │  │
│  │ (Margin)    │  │ (Risk)      │  │ (Settlement)        │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────┬───────────────────────────────────┘
                          │
┌─────────────────────────▼───────────────────────────────────┐
│                        数据层                                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │ 订单簿      │  │ 资产数据    │  │ 行情数据            │  │
│  │ (Redis)     │  │ (MySQL)     │  │ (Kafka + TSDB)      │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

3.2 撮合引擎性能优化

挑战

优化策略

  1. 内存计算
    // 订单簿完全在内存中,避免数据库访问
    public class InMemoryOrderBook {
        
     // 使用 Disruptor 无锁队列处理订单
     private Disruptor<OrderEvent> disruptor;
        
     // 订单簿数据结构
     private ConcurrentSkipListMap<BigDecimal, PriceLevel> bids;
     private ConcurrentSkipListMap<BigDecimal, PriceLevel> asks;
        
     // 撮合核心:单线程处理,避免锁竞争
     public void onEvent(OrderEvent event, long sequence, boolean endOfBatch) {
         Order order = event.getOrder();
         List<Trade> trades = match(order);
            
         // 批量发送成交结果
         if (endOfBatch) {
             tradeProducer.sendBatch(trades);
         }
     }
    }
    
  2. 异步持久化
    // 撮合结果先写内存,异步批量写入数据库
    @Service
    public class AsyncSettlementService {
        
     private BlockingQueue<Trade> tradeQueue = new LinkedBlockingQueue<>();
        
     // 撮合线程只负责撮合,不落库
     public void onTrade(Trade trade) {
         tradeQueue.offer(trade);
         // 立即返回,不等待数据库
     }
        
     // 独立线程批量落库
     @Scheduled(fixedDelay = 10)
     public void batchPersist() {
         List<Trade> batch = new ArrayList<>();
         tradeQueue.drainTo(batch, 1000);
            
         if (!batch.isEmpty()) {
             tradeRepository.batchInsert(batch);
         }
     }
    }
    
  3. 读写分离
    写入路径:订单 → 撮合引擎 → 内存订单簿 → 异步落库
    读取路径:行情查询 → 内存订单簿(无锁读取)
    

3.3 冷热钱包架构

资金分层

用户充值
   ↓
热钱包(在线)─── 日常提现、交易结算
   │                  占比:5% 资金
   │                  存储:HSM 加密机
   ↓
温钱包(半离线)── 定期归集、大额提现审批
   │                  占比:15% 资金
   │                  存储:离线签名机
   ↓
冷钱包(完全离线)── 长期存储、多签管理
                      占比:80% 资金
                      存储:纸钱包、硬件钱包、银行保险柜

安全机制


📖 四、面试真题

Q1: 撮合引擎的核心设计原则是什么?

答: 撮合引擎是交易所的核心,设计原则包括:

  1. 正确性:价格优先、时间优先原则必须严格遵守,不能出现错配、漏配。

  2. 高性能
    • 内存计算:订单簿完全在内存中
    • 无锁设计:单线程处理或使用无锁数据结构
    • 批量处理:批量落库、批量推送行情
  3. 低延迟
    • 目标:< 1ms 从订单接收到成交
    • 手段:CPU 亲和性绑定、禁用 GC、使用 Disruptor
  4. 高可用
    • 主备架构:主撮合故障时秒级切换
    • 状态复制:订单簿状态实时同步到备机
  5. 数据一致性
    • 撮合结果必须持久化,不能丢单
    • 使用 WAL(Write Ahead Log)保证

Q2: 如何实现订单簿的快速查询和更新?

答: 订单簿需要支持高频查询(最优价、深度)和更新(挂单、撤单、成交)。

方案对比

数据结构 最优价查询 插入/删除 内存占用 适用场景
TreeMap/红黑树 O(log n) O(log n) 通用实现
SkipList O(log n) O(log n) 高并发
数组索引 O(1) O(1) 价格范围有限

最佳实践(数组索引 + 链表)

// 价格精度 0.01,范围 0-100000,共 1000 万个价格档
// 使用 int[] 索引,O(1) 定位

public class FastOrderBook {
    
    // 每个价格档是一个链表(同价格按时间排序)
    private Order[] bidLevels = new Order[10_000_000];
    private Order[] askLevels = new Order[10_000_000];
    
    // 当前最优价索引
    private int bestBid = 0;
    private int bestAsk = 9_999_999;
    
    // O(1) 插入
    public void addOrder(Order order) {
        int index = priceToIndex(order.getPrice());
        Order[] levels = order.isBuy() ? bidLevels : askLevels;
        
        // 头插法(时间最新在最前)
        order.next = levels[index];
        levels[index] = order;
        
        // 更新最优价
        if (order.isBuy() && index > bestBid) bestBid = index;
        if (!order.isBuy() && index < bestAsk) bestAsk = index;
    }
    
    // O(1) 获取最优价
    public BigDecimal getBestBid() {
        return indexToPrice(bestBid);
    }
}

Q3: 杠杆交易的强平机制是如何设计的?

答: 强平机制是杠杆系统的风险控制核心。

触发条件

保证金率 = 账户权益 / 持仓总价值 < 维持保证金率(如 5%)

强平流程

  1. 冻结账户:禁止新下单,防止风险扩大
  2. 取消挂单:撤销所有未成交订单,释放保证金
  3. 市价平仓:以当前最优价立即卖出持仓
  4. 归还借款:优先归还借入的资金
  5. 剩余返还:如有剩余资金,返还到用户现货账户

风险控制

Q4: 如何保证撮合引擎的高可用和数据一致性?

答: 撮合引擎的高可用和数据一致性需要多层面保障。

高可用架构

主撮合引擎 ── 实时复制状态 ── 备撮合引擎
     │                           │
     └────── 共享存储(WAL)──────┘

数据一致性保障

  1. WAL(Write Ahead Log)
    • 撮合前先写日志
    • 日志顺序写入,性能高
    • 故障恢复时重放日志
  2. 状态快照
    • 定期对订单簿打快照
    • 故障时从快照恢复 + 重放增量日志
  3. 主备切换
    • 主故障时,备机从共享存储读取最新状态
    • 秒级切换,用户无感知
  4. 对账机制
    • 每日对账:订单簿状态 vs 数据库状态
    • 发现不一致时告警并修复

Q5: 交易所如何防止女巫攻击和洗钱?

答: 女巫攻击(Sybil Attack)和洗钱(Money Laundering)是交易所面临的安全挑战。

防御措施

  1. KYC(Know Your Customer)
    • 实名认证:身份证、人脸识别
    • 地址证明:水电账单、银行对账单
    • 风险评级:根据用户行为和资金来源评级
  2. 链上监控
    • 黑名单地址:与已知黑客地址、混币器地址关联
    • 资金溯源:追踪资金来源,识别可疑交易
    • 大额告警:超过阈值的交易触发人工审核
  3. 行为分析
    • 异常交易模式:如频繁小额转账(分层)
    • 多账户关联:同一 IP、设备、身份关联多个账户
    • 交易对手分析:识别高风险对手方
  4. 合规合作
    • 与 Chainalysis、Elliptic 等链上分析公司合作
    • 与监管机构共享可疑交易信息

Q6: 冷热钱包的资金流转是如何设计的?

答: 冷热钱包的资金流转需要兼顾安全性和效率。

归集流程(热 → 冷)

1. 监控热钱包余额
2. 超过阈值(如 100 BTC)触发归集
3. 生成冷钱包地址(离线)
4. 热钱包发起转账(多签)
5. 等待链上确认(6个区块)
6. 更新系统余额

提现流程(冷 → 热)

1. 用户发起提现申请
2. 风控审核(大额人工审核)
3. 从冷钱包转账到热钱包(离线签名)
4. 热钱包发起链上转账给用户
5. 等待确认,标记提现完成

安全机制

Q7: 交易所的费率模型是如何设计的?

答: 费率模型影响交易所的收入和流动性。

费率结构

Maker 费率:0.02% - 0.1%(挂单,提供流动性,费率低)
Taker 费率:0.04% - 0.2%(吃单,消耗流动性,费率高)

费率优惠

费率计算示例

public class FeeCalculator {
    
    public Fee calculateFee(Order order, User user) {
        // 基础费率
        BigDecimal baseRate = order.isMaker() ? 
            new BigDecimal("0.0002") : new BigDecimal("0.0004");
        
        // VIP 折扣
        BigDecimal vipDiscount = getVipDiscount(user.getVipLevel());
        
        // 平台币抵扣折扣
        BigDecimal tokenDiscount = user.isUseTokenFee() ? 
            new BigDecimal("0.25") : BigDecimal.ZERO;
        
        // 最终费率
        BigDecimal finalRate = baseRate
            .multiply(BigDecimal.ONE.subtract(vipDiscount))
            .multiply(BigDecimal.ONE.subtract(tokenDiscount));
        
        // 计算手续费
        BigDecimal fee = order.getAmount().multiply(finalRate);
        
        return new Fee(fee, finalRate);
    }
}

📚 延伸阅读