用 Java 案例识别盘口异常变动
可以,Java 完全可以实现盘口(Level-2 行情)异常变动识别,下面给一个可运行的案例框架,涵盖常见异常类型和识别逻辑。

盘口异常的主要类型
| 异常类型 | 特征 | 判定思路 |
|---|---|---|
| 大单扫单 | 卖一至卖N被连续吃掉 | 主动买量骤增、委卖锐减 |
| 撤单异常 | 某价位挂单瞬间消失 | 挂单量环比突变 |
| 价差异常 | 买一卖一价差突然扩大/收窄 | spread 突变 |
| 盘口失衡 | 委买委卖比例极端 | order imbalance 阈值 |
| 压单/托单 | 单价位挂单量异常巨大 | 单档量偏离均值 N 倍 |
| 封板异动 | 涨停/跌停封单快速变化 | 封单量变化率 |
核心 Java 案例代码
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
// 1. 盘口快照
class OrderBook {
long timestamp;
int[] bidPrices = new int[10]; // 买1~买10 价格(分)
long[] bidVols = new long[10]; // 买1~买10 量
int[] askPrices = new int[10];
long[] askVols = new long[10];
long lastPrice;
long lastVol;
}
// 2. 异常事件
class Anomaly {
String code;
String type;
long timestamp;
String detail;
Anomaly(String c, String t, long ts, String d) {
code = c; type = t; timestamp = ts; detail = d;
}
@Override public String toString() {
return String.format("[%s] %s @%d : %s", code, type, timestamp, detail);
}
}
// 3. 异常识别器
public class OrderBookAnomalyDetector {
// 阈值(可配置)
private static final double SINGLE_LEVEL_RATIO_THRESHOLD = 5.0; // 单档量/均值 > 5 倍
private static final double IMBALANCE_THRESHOLD = 3.0; // 委买/委卖 > 3 或 < 1/3
private static final double SPREAD_SPIKE_RATIO = 2.5; // 价差突然扩大 2.5 倍
private static final double CANCEL_RATIO_THRESHOLD = 0.7; // 单档量骤减 > 70%
// 每只股票的历史快照,用于环比
private final Map<String, OrderBook> lastBook = new ConcurrentHashMap<>();
public List<Anomaly> detect(String code, OrderBook cur) {
List<Anomaly> result = new ArrayList<>();
OrderBook prev = lastBook.get(code);
// --- (1) 单档挂单异动:某档量远超同侧均值 ---
checkSingleLevelSpike(result, code, cur, cur.bidVols, "BID");
checkSingleLevelSpike(result, code, cur, cur.askVols, "ASK");
// --- (2) 盘口失衡:委买总量 / 委卖总量 ---
long bidSum = Arrays.stream(cur.bidVols).sum();
long askSum = Arrays.stream(cur.askVols).sum();
if (askSum > 0) {
double ratio = (double) bidSum / askSum;
if (ratio > IMBALANCE_THRESHOLD) {
result.add(new Anomaly(code, "BID_IMBALANCE", cur.timestamp,
String.format("委买/委卖=%.2f (买多)", ratio)));
} else if (ratio < 1.0 / IMBALANCE_THRESHOLD) {
result.add(new Anomaly(code, "ASK_IMBALANCE", cur.timestamp,
String.format("委买/委卖=%.2f (卖多)", ratio)));
}
}
// --- (3) 价差异常扩大 ---
int spread = cur.askPrices[0] - cur.bidPrices[0];
if (prev != null) {
int prevSpread = prev.askPrices[0] - prev.bidPrices[0];
if (prevSpread > 0 && spread > prevSpread * SPREAD_SPIKE_RATIO) {
result.add(new Anomaly(code, "SPREAD_SPIKE", cur.timestamp,
String.format("价差 %d->%d", prevSpread, spread)));
}
}
// --- (4) 撤单异常:某档挂单量骤减 ---
if (prev != null) {
for (int i = 0; i < 10; i++) {
checkCancel(result, code, cur.timestamp, "BID-" + (i+1),
prev.bidVols[i], cur.bidVols[i]);
checkCancel(result, code, cur.timestamp, "ASK-" + (i+1),
prev.askVols[i], cur.askVols[i]);
}
}
// --- (5) 大单扫单:成交价击穿多档 + 量放大 ---
if (prev != null && cur.lastVol > 0) {
double avgLastVol = Math.max(prev.lastVol, 1);
if (cur.lastVol > avgLastVol * 5 && cur.lastPrice >= cur.askPrices[0]) {
result.add(new Anomaly(code, "AGGRESSIVE_BUY", cur.timestamp,
"大单主动买 " + cur.lastVol));
}
if (cur.lastVol > avgLastVol * 5 && cur.lastPrice <= cur.bidPrices[0]) {
result.add(new Anomaly(code, "AGGRESSIVE_SELL", cur.timestamp,
"大单主动卖 " + cur.lastVol));
}
}
lastBook.put(code, cur);
return result;
}
private void checkSingleLevelSpike(List<Anomaly> out, String code,
OrderBook ob, long[] vols, String side) {
double avg = Arrays.stream(vols).average().orElse(0);
if (avg <= 0) return;
for (int i = 0; i < vols.length; i++) {
if (vols[i] > avg * SINGLE_LEVEL_RATIO_THRESHOLD) {
out.add(new Anomaly(code, "LEVEL_SPIKE", ob.timestamp,
String.format("%s%d 量=%d 均值=%.0f", side, i+1, vols[i], avg)));
}
}
}
private void checkCancel(List<Anomaly> out, String code, long ts,
String tag, long prevVol, long curVol) {
if (prevVol > 0 && curVol < prevVol * (1 - CANCEL_RATIO_THRESHOLD)) {
out.add(new Anomaly(code, "CANCEL", ts,
String.format("%s %d->%d 撤单率=%.0f%%",
tag, prevVol, curVol, 100.0 * (prevVol - curVol) / prevVol)));
}
}
}
使用示例
public class Demo {
public static void main(String[] args) {
OrderBookAnomalyDetector detector = new OrderBookAnomalyDetector();
OrderBook ob1 = mockBook(10000, 10010, 500, 500);
detector.detect("600000", ob1); // 首笔只记录
// 第二笔:卖档突然出现巨大挂单 + 价差扩大
OrderBook ob2 = mockBook(10000, 10100, 500, 500);
ob2.askVols[0] = 20000;
List<Anomaly> list = detector.detect("600000", ob2);
list.forEach(System.out::println);
}
static OrderBook mockBook(int bid1, int ask1, long v1, long v2) {
OrderBook ob = new OrderBook();
ob.timestamp = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
ob.bidPrices[i] = bid1 - i * 10;
ob.askPrices[i] = ask1 + i * 10;
ob.bidVols[i] = v1;
ob.askVols[i] = v2;
}
ob.lastPrice = ask1;
ob.lastVol = 100;
return ob;
}
}
输出示例:
[600000] LEVEL_SPIKE @... : ASK1 量=20000 均值=2450
[600000] SPREAD_SPIKE @... : 价差 10->100
工程化的关键点
- 数据源:通过行情网关(如 CTP、恒生、迅投 QMT、掘金、WebSocket 推送)拿到 Level-1/Level-2 快照,按股票分片推入检测器。
- 窗口统计:用滑动窗口(如最近 20 笔)计算均值和标准差,动态阈值(
mean ± k*std)比固定阈值更稳。 - 性能:单机可处理数万只股票 tick,用
ConcurrentHashMap+ 无锁结构;高频场景可用 Disruptor。 - 落库/告警:异常事件写入 Kafka/Redis,并触发风控或可视化告警。
- 回测验证:拿历史 tick 回放,统计各类异常与后续收益/波动的关系,防止误报。
局限与注意
- 仅凭快照难以区分"大单成交"和"撤单"造成的盘口变化,需结合 逐笔委托 + 逐笔成交(Level-2)才更准确。
- 单一阈值易误报,建议 多因子组合 + 机器学习(如孤立森林) 提升准确率。
- 注意交易所 集合竞价、涨跌停封板 等特殊阶段的规则差异,需单独处理。
如果你需要,我可以进一步给出:逐笔委托撤单识别、滑动窗口 Z-score 动态阈值版本、或Spring Boot + Kafka 的完整接入方案。
版权声明:本站文章为原创整理,转载请注明出处。