| New file |
| | |
| | | package com.xcong.excoin.modules.gateApi.simulation; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.*; |
| | | |
| | | /** |
| | | * 网格策略模拟器 — 模拟 GateGridTradeService 完整生命周期并发现逻辑 BUG。 |
| | | * |
| | | * <h3>预设参数</h3> |
| | | * shortBaseEntryPrice = 100.0, gridRate = 0.005 (0.5%), step = 0.5, priceScale = 1, baseQuantity = 10, quantity = 1 |
| | | */ |
| | | public class GridSimulator { |
| | | |
| | | static int SIM_PRICE_PREC = 1; |
| | | static BigDecimal BASE_PRICE = new BigDecimal("100.0"); |
| | | static BigDecimal STEP = BASE_PRICE.multiply(new BigDecimal("0.005")).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP); // 0.5 |
| | | static int BASE_QTY = 10; |
| | | static int QTY = 1; |
| | | static int GRID_QUEUE_SIZE = 10; // 模拟用缩减队列 |
| | | |
| | | // ---- 网格数据结构 ---- |
| | | static class Grid { |
| | | int id; |
| | | BigDecimal price; |
| | | boolean hasLongOrder, hasShortOrder; |
| | | String longOrderId, shortOrderId; |
| | | String longTpOrderId, shortTpOrderId; // 止盈 |
| | | String longSlOrderId, shortSlOrderId; // 止损 |
| | | BigDecimal longTpPrice, shortTpPrice; |
| | | int longFilledQty = QTY, shortFilledQty = QTY; |
| | | |
| | | Grid(int id, BigDecimal price) { |
| | | this.id = id; this.price = price; |
| | | this.longTpPrice = price.add(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP); |
| | | this.shortTpPrice = price.subtract(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP); |
| | | } |
| | | @Override public String toString() { |
| | | return String.format("G[%2d] p=%.1f | L:o=%s tp=%s sl=%s | S:o=%s tp=%s sl=%s", |
| | | id, price, |
| | | hasLongOrder ? "Y" : "N", longTpOrderId != null ? longTpOrderId : "-", longSlOrderId != null ? longSlOrderId : "-", |
| | | hasShortOrder ? "Y" : "N", shortTpOrderId != null ? shortTpOrderId : "-", shortSlOrderId != null ? shortSlOrderId : "-"); |
| | | } |
| | | } |
| | | |
| | | static Map<Integer, Grid> INDEX = new LinkedHashMap<>(); |
| | | static List<Grid> gridElements = new ArrayList<>(); |
| | | |
| | | // ---- 队列 ---- |
| | | static List<BigDecimal> shortQueue = new ArrayList<>(); // 降序 |
| | | static List<BigDecimal> longQueue = new ArrayList<>(); // 升序 |
| | | static List<BigDecimal> totalShortQ = new ArrayList<>(); // 降序 |
| | | static List<BigDecimal> totalLongQ = new ArrayList<>(); // 升序 |
| | | |
| | | // ---- 状态 ---- |
| | | enum State { WAITING_KLINE, OPENING, ACTIVE, STOPPED } |
| | | static State state = State.WAITING_KLINE; |
| | | static boolean baseLongOpened = false, baseShortOpened = false; |
| | | static boolean longActive = false, shortActive = false; |
| | | static BigDecimal longPositionSize = BigDecimal.ZERO; |
| | | static BigDecimal shortPositionSize = BigDecimal.ZERO; |
| | | static BigDecimal longEntryPrice = BigDecimal.ZERO; |
| | | static BigDecimal shortEntryPrice = BigDecimal.ZERO; |
| | | static BigDecimal cumulativePnl = BigDecimal.ZERO; |
| | | static BigDecimal unrealizedPnl = BigDecimal.ZERO; |
| | | static BigDecimal shortBaseEntryPrice = BigDecimal.ZERO; |
| | | static BigDecimal longBaseEntryPrice = BigDecimal.ZERO; |
| | | |
| | | static int orderIdCounter = 1000; |
| | | static List<String> eventLog = new ArrayList<>(); |
| | | static List<String> bugLog = new ArrayList<>(); |
| | | |
| | | static void log(String msg) { eventLog.add(msg); System.out.println(" " + msg); } |
| | | static void bug(String msg) { String s = " [BUG] " + msg; bugLog.add(s); System.out.println(s); } |
| | | static String newOrderId() { return "O-" + (orderIdCounter++); } |
| | | static String repeat(String s, int n) { |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (int i = 0; i < n; i++) sb.append(s); |
| | | return sb.toString(); |
| | | } |
| | | |
| | | // ================================================================ |
| | | // 步骤1: 初始化 |
| | | // ================================================================ |
| | | static void init() { |
| | | log("=== Phase 1: 初始化 ==="); |
| | | log("参数: basePrice=" + BASE_PRICE + " step=" + STEP + " baseQty=" + BASE_QTY + " qty=" + QTY); |
| | | state = State.WAITING_KLINE; |
| | | } |
| | | |
| | | // ================================================================ |
| | | // 步骤2: 首根K线 → 市价双开基底 |
| | | // ================================================================ |
| | | static void onFirstKline() { |
| | | log("\n=== Phase 2: 首根K线到达,市价双开基底 ==="); |
| | | state = State.OPENING; |
| | | log("市价开多 " + BASE_QTY + " 张,开空 " + BASE_QTY + " 张"); |
| | | |
| | | // 模拟成交 |
| | | longPositionSize = new BigDecimal(BASE_QTY); |
| | | shortPositionSize = new BigDecimal(BASE_QTY); |
| | | longEntryPrice = BASE_PRICE; |
| | | shortEntryPrice = BASE_PRICE; |
| | | longActive = true; |
| | | shortActive = true; |
| | | |
| | | // 模拟 onPositionUpdate → 基底成交 |
| | | log("基底多成交价: " + BASE_PRICE); |
| | | longBaseEntryPrice = BASE_PRICE; |
| | | baseLongOpened = true; |
| | | log("基底空成交价: " + BASE_PRICE); |
| | | shortBaseEntryPrice = BASE_PRICE; |
| | | baseShortOpened = true; |
| | | |
| | | tryGenerateQueues(); |
| | | } |
| | | |
| | | // ================================================================ |
| | | // 步骤3: 生成队列和网格 |
| | | // ================================================================ |
| | | static void tryGenerateQueues() { |
| | | if (!baseLongOpened || !baseShortOpened) return; |
| | | log("\n=== Phase 3: 生成网格队列 ==="); |
| | | |
| | | // 生成队列 |
| | | generateShortQueue(); |
| | | generateLongQueue(); |
| | | updateGridElements(); |
| | | |
| | | // 标记基底元素 (模拟 baseLongTraderParam/baseShortTraderParam) |
| | | Grid baseG = INDEX.get(0); |
| | | baseG.hasLongOrder = true; |
| | | baseG.longOrderId = "BASE-LONG-IOC"; |
| | | baseG.hasShortOrder = true; |
| | | baseG.shortOrderId = "BASE-SHORT-IOC"; |
| | | log("基底元素 ID=0 标记 hasLongOrder=true, hasShortOrder=true"); |
| | | |
| | | // 初始化止损单 |
| | | initStopLossOrders(); |
| | | |
| | | state = State.ACTIVE; |
| | | log("状态切换为 ACTIVE"); |
| | | printGridState(); |
| | | } |
| | | |
| | | static void generateShortQueue() { |
| | | shortQueue.clear(); |
| | | BigDecimal elem = BASE_PRICE.subtract(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP); |
| | | for (int i = 0; i < GRID_QUEUE_SIZE; i++) { |
| | | shortQueue.add(elem); |
| | | totalLongQ.add(elem); |
| | | totalShortQ.add(elem); |
| | | elem = elem.subtract(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP); |
| | | if (elem.compareTo(BigDecimal.ZERO) <= 0) break; |
| | | } |
| | | shortQueue.sort((a, b) -> b.compareTo(a)); |
| | | log("空仓队列(降序): " + shortQueue); |
| | | } |
| | | |
| | | static void generateLongQueue() { |
| | | longQueue.clear(); |
| | | BigDecimal elem = BASE_PRICE.add(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP); |
| | | for (int i = 0; i < GRID_QUEUE_SIZE; i++) { |
| | | longQueue.add(elem); |
| | | totalLongQ.add(elem); |
| | | totalShortQ.add(elem); |
| | | elem = elem.add(STEP).setScale(SIM_PRICE_PREC, RoundingMode.HALF_UP); |
| | | } |
| | | longQueue.sort(BigDecimal::compareTo); |
| | | log("多仓队列(升序): " + longQueue); |
| | | } |
| | | |
| | | static void updateGridElements() { |
| | | gridElements.clear(); |
| | | INDEX.clear(); |
| | | int shortSize = shortQueue.size(); |
| | | int longSize = longQueue.size(); |
| | | |
| | | // 空仓区域: id 从 -1 自减 |
| | | for (int i = 0; i < shortSize; i++) { |
| | | int id = -(i + 1); |
| | | BigDecimal price = shortQueue.get(i); |
| | | Grid g = new Grid(id, price); |
| | | g.longFilledQty = QTY; |
| | | g.shortFilledQty = QTY; |
| | | gridElements.add(g); |
| | | INDEX.put(id, g); |
| | | } |
| | | |
| | | // 位置 0 |
| | | Grid g0 = new Grid(0, BASE_PRICE); |
| | | g0.longFilledQty = QTY; |
| | | g0.shortFilledQty = QTY; |
| | | gridElements.add(g0); |
| | | INDEX.put(0, g0); |
| | | |
| | | // 多仓区域: id 从 1 自增 |
| | | for (int i = 0; i < longSize; i++) { |
| | | int id = i + 1; |
| | | BigDecimal price = longQueue.get(i); |
| | | Grid g = new Grid(id, price); |
| | | g.longFilledQty = QTY; |
| | | g.shortFilledQty = QTY; |
| | | gridElements.add(g); |
| | | INDEX.put(id, g); |
| | | } |
| | | |
| | | log("网格元素: " + gridElements.size() + " 个"); |
| | | } |
| | | |
| | | static void initStopLossOrders() { |
| | | log("\n--- 初始化止损单 ---"); |
| | | // 空仓止损: ID 2 到 BASE_QTY+1 |
| | | int shortTime = BASE_QTY + 1; |
| | | for (int id = 2; id <= shortTime; id++) { |
| | | Grid g = INDEX.get(id); |
| | | if (g == null) continue; |
| | | String slId = newOrderId(); |
| | | g.shortSlOrderId = slId; |
| | | log(String.format(" 空仓止损 gridId:%d price:%.1f slId:%s (CLOSE_SHORT NUMBER_1)", id, g.price, slId)); |
| | | } |
| | | |
| | | // 多仓止损: ID -2 到 -(BASE_QTY+1) |
| | | int longTime = BASE_QTY + 1; |
| | | for (int id = -2; id >= -longTime; id--) { |
| | | Grid g = INDEX.get(id); |
| | | if (g == null) continue; |
| | | String slId = newOrderId(); |
| | | g.longSlOrderId = slId; |
| | | log(String.format(" 多仓止损 gridId:%d price:%.1f slId:%s (CLOSE_LONG NUMBER_2)", id, g.price, slId)); |
| | | } |
| | | log("止损单初始化完成"); |
| | | } |
| | | |
| | | // ================================================================ |
| | | // 步骤4: onKline ACTIVE 状态下的网格处理 |
| | | // ================================================================ |
| | | static void onKline(BigDecimal closePrice) { |
| | | if (state != State.ACTIVE) return; |
| | | |
| | | if (!longActive && longPositionSize.compareTo(BigDecimal.ZERO) == 0) { |
| | | processShortGrid(closePrice); |
| | | } |
| | | if (!shortActive && shortPositionSize.compareTo(BigDecimal.ZERO) == 0) { |
| | | processLongGrid(closePrice); |
| | | } |
| | | } |
| | | |
| | | static void processShortGrid(BigDecimal currentPrice) { |
| | | log("\n--- processShortGrid (多仓归零, 触发空仓队列) 当前价:" + currentPrice + " ---"); |
| | | BigDecimal matched = BigDecimal.ZERO; |
| | | for (BigDecimal p : totalLongQ) { |
| | | if (p.compareTo(currentPrice) >= 0) { |
| | | matched = p; |
| | | break; |
| | | } |
| | | } |
| | | if (matched.compareTo(BigDecimal.ZERO) == 0) { |
| | | log(" 未匹配到价格"); |
| | | return; |
| | | } |
| | | log(" 匹配价格: " + matched); |
| | | Grid matchedG = findByPrice(matched); |
| | | if (matchedG == null) { log(" Grid不存在"); return; } |
| | | |
| | | if (!matchedG.hasLongOrder) { |
| | | // 在 upId 位置挂多单 |
| | | int upId = matchedG.id + 1; // upId逻辑简化 |
| | | Grid newEntryG = INDEX.get(upId); |
| | | if (newEntryG != null && !newEntryG.hasLongOrder) { |
| | | String oid = newOrderId(); |
| | | newEntryG.hasLongOrder = true; |
| | | newEntryG.longOrderId = oid; |
| | | newEntryG.longFilledQty = BASE_QTY; |
| | | log(String.format(" 在gridId:%d挂多单(size=%d) orderId:%s", upId, BASE_QTY, oid)); |
| | | } |
| | | |
| | | // 取消upId+1位置的多单 |
| | | int cancelId = upId + 1; |
| | | Grid cancelG = INDEX.get(cancelId); |
| | | if (cancelG != null && cancelG.hasLongOrder) { |
| | | log(String.format(" 取消gridId:%d的多单 orderId:%s", cancelId, cancelG.longOrderId)); |
| | | cancelG.hasLongOrder = false; |
| | | cancelG.longOrderId = null; |
| | | } |
| | | |
| | | // BUG: 这里新挂的单 size = BASE_QTY,但如果之前已有其他追单 |
| | | // 可能导致持仓远超预期 |
| | | } |
| | | } |
| | | |
| | | static void processLongGrid(BigDecimal currentPrice) { |
| | | log("\n--- processLongGrid (空仓归零, 触发多仓队列) 当前价:" + currentPrice + " ---"); |
| | | BigDecimal matched = BigDecimal.ZERO; |
| | | for (BigDecimal p : totalShortQ) { |
| | | if (p.compareTo(currentPrice) <= 0) { |
| | | matched = p; |
| | | break; |
| | | } |
| | | } |
| | | if (matched.compareTo(BigDecimal.ZERO) == 0) { |
| | | log(" 未匹配到价格"); |
| | | return; |
| | | } |
| | | log(" 匹配价格: " + matched); |
| | | Grid matchedG = findByPrice(matched); |
| | | if (matchedG == null) { log(" Grid不存在"); return; } |
| | | |
| | | if (!matchedG.hasShortOrder) { |
| | | int downId = matchedG.id - 1; |
| | | Grid newEntryG = INDEX.get(downId); |
| | | if (newEntryG != null && !newEntryG.hasShortOrder) { |
| | | String oid = newOrderId(); |
| | | newEntryG.hasShortOrder = true; |
| | | newEntryG.shortOrderId = oid; |
| | | newEntryG.shortFilledQty = BASE_QTY; |
| | | log(String.format(" 在gridId:%d挂空单(size=%d) orderId:%s", downId, BASE_QTY, oid)); |
| | | } |
| | | |
| | | int cancelId = downId - 1; |
| | | Grid cancelG = INDEX.get(cancelId); |
| | | if (cancelG != null && cancelG.hasShortOrder) { |
| | | log(String.format(" 取消gridId:%d的空单 orderId:%s", cancelId, cancelG.shortOrderId)); |
| | | cancelG.hasShortOrder = false; |
| | | cancelG.shortOrderId = null; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ================================================================ |
| | | // 步骤5: onAutoOrder 条件单成交回调 |
| | | // ================================================================ |
| | | static void onAutoOrder(String orderId, String status, String orderType, String tradeId) { |
| | | if (state == State.STOPPED) return; |
| | | if (!"finished".equals(status)) return; |
| | | |
| | | log("\n--- onAutoOrder: id=" + orderId + " type=" + orderType + " tradeId=" + tradeId + " ---"); |
| | | |
| | | // 1. 检查是否是止损单成交 |
| | | Grid longSl = findByLongSlOrderId(orderId); |
| | | if (longSl != null && tradeId != null && !"0".equals(tradeId)) { |
| | | handleLongStopLossTriggered(longSl); |
| | | return; |
| | | } |
| | | Grid shortSl = findByShortSlOrderId(orderId); |
| | | if (shortSl != null && tradeId != null && !"0".equals(tradeId)) { |
| | | handleShortStopLossTriggered(shortSl); |
| | | return; |
| | | } |
| | | |
| | | // 2. 检查是否是空仓挂单成交 |
| | | Grid shortG = findByShortOrderId(orderId); |
| | | if (shortG != null && shortG.hasShortOrder && tradeId != null && !"0".equals(tradeId)) { |
| | | int filledQty = shortG.shortFilledQty; |
| | | shortG.hasShortOrder = false; |
| | | shortG.shortOrderId = null; |
| | | extendShortStopLoss(filledQty); |
| | | log(String.format(" 空单成交 gridId:%d filledQty:%d", shortG.id, filledQty)); |
| | | |
| | | // 新逻辑: 持仓超过baseQuantity时,从gridId-2开始追挂止盈 |
| | | BigDecimal baseQty = new BigDecimal(BASE_QTY); |
| | | BigDecimal qty = new BigDecimal(QTY); |
| | | if (shortPositionSize.compareTo(baseQty) > 0) { |
| | | BigDecimal excess = shortPositionSize.subtract(baseQty); |
| | | int excessCount = excess.divide(qty, 0, RoundingMode.DOWN).intValue(); |
| | | for (int i = 0; i < excessCount; i++) { |
| | | int tpId = shortG.id - 2 - i; |
| | | Grid tpG = INDEX.get(tpId); |
| | | if (tpG == null || tpG.shortTpOrderId != null) continue; |
| | | String tpIdStr = newOrderId(); |
| | | tpG.shortTpOrderId = tpIdStr; |
| | | log(String.format(" 空仓止盈挂单 gridId:%d price:%.1f tpId:%s", tpId, tpG.price, tpIdStr)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 3. 检查是否是多仓挂单成交 |
| | | Grid longG = findByLongOrderId(orderId); |
| | | if (longG != null && longG.hasLongOrder && tradeId != null && !"0".equals(tradeId)) { |
| | | int filledQty = longG.longFilledQty; |
| | | longG.hasLongOrder = false; |
| | | longG.longOrderId = null; |
| | | extendLongStopLoss(filledQty); |
| | | log(String.format(" 多单成交 gridId:%d filledQty:%d", longG.id, filledQty)); |
| | | |
| | | BigDecimal baseQty = new BigDecimal(BASE_QTY); |
| | | BigDecimal qty = new BigDecimal(QTY); |
| | | if (longPositionSize.compareTo(baseQty) > 0) { |
| | | BigDecimal excess = longPositionSize.subtract(baseQty); |
| | | int excessCount = excess.divide(qty, 0, RoundingMode.DOWN).intValue(); |
| | | for (int i = 0; i < excessCount; i++) { |
| | | int tpId = longG.id + 2 + i; |
| | | Grid tpG = INDEX.get(tpId); |
| | | if (tpG == null || tpG.longTpOrderId != null) continue; |
| | | String tpIdStr = newOrderId(); |
| | | tpG.longTpOrderId = tpIdStr; |
| | | log(String.format(" 多仓止盈挂单 gridId:%d price:%.1f tpId:%s", tpId, tpG.price, tpIdStr)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | static void handleLongStopLossTriggered(Grid g) { |
| | | log(" 多仓止损触发 gridId:" + g.id); |
| | | g.longSlOrderId = null; |
| | | // 模拟平仓: longPositionSize 减少 |
| | | longPositionSize = longPositionSize.subtract(new BigDecimal(QTY)); |
| | | if (longPositionSize.compareTo(BigDecimal.ZERO) < 0) longPositionSize = BigDecimal.ZERO; |
| | | |
| | | // 在gridId+1位置挂新追单 |
| | | int newId = g.id + 1; |
| | | Grid newG = INDEX.get(newId); |
| | | if (newG == null) { |
| | | log(" gridId:" + newId + " 不存在,止损追单失败"); |
| | | return; |
| | | } |
| | | |
| | | // 计算size |
| | | BigDecimal baseQty = new BigDecimal(BASE_QTY); |
| | | BigDecimal subtract = baseQty.subtract(longPositionSize); |
| | | int size = QTY + 1; |
| | | if (subtract.compareTo(BigDecimal.ZERO) >= 0) { |
| | | size = subtract.intValue() + 1; |
| | | } |
| | | log(String.format(" 挂多单 gridId:%d size:%d (pos=%s)", newId, size, longPositionSize)); |
| | | newG.hasLongOrder = true; |
| | | newG.longOrderId = newOrderId(); |
| | | newG.longFilledQty = size; |
| | | |
| | | // 取消gridId+2的多单 |
| | | int cancelId = g.id + 2; |
| | | Grid cancelG = INDEX.get(cancelId); |
| | | if (cancelG != null && cancelG.hasLongOrder) { |
| | | log(String.format(" 取消gridId:%d的多单 orderId:%s", cancelId, cancelG.longOrderId)); |
| | | cancelG.hasLongOrder = false; |
| | | cancelG.longOrderId = null; |
| | | } |
| | | |
| | | // 取消最远多仓止盈 |
| | | Grid farthest = null; |
| | | for (Grid e : gridElements) { |
| | | if (e.longTpOrderId != null) { |
| | | if (farthest == null || e.price.compareTo(farthest.price) > 0) { |
| | | farthest = e; |
| | | } |
| | | } |
| | | } |
| | | if (farthest != null) { |
| | | log(String.format(" 取消最远多仓止盈 gridId:%d orderId:%s", farthest.id, farthest.longTpOrderId)); |
| | | farthest.longTpOrderId = null; |
| | | } |
| | | |
| | | // 持仓归零检查 |
| | | if (longPositionSize.compareTo(BigDecimal.ZERO) == 0) { |
| | | longActive = false; |
| | | log(" 多仓已全部止损,longActive=false"); |
| | | } |
| | | } |
| | | |
| | | static void handleShortStopLossTriggered(Grid g) { |
| | | log(" 空仓止损触发 gridId:" + g.id); |
| | | g.shortSlOrderId = null; |
| | | shortPositionSize = shortPositionSize.subtract(new BigDecimal(QTY)); |
| | | if (shortPositionSize.compareTo(BigDecimal.ZERO) < 0) shortPositionSize = BigDecimal.ZERO; |
| | | |
| | | int newId = g.id - 1; |
| | | Grid newG = INDEX.get(newId); |
| | | if (newG == null) { |
| | | log(" gridId:" + newId + " 不存在,止损追单失败"); |
| | | return; |
| | | } |
| | | |
| | | BigDecimal baseQty = new BigDecimal(BASE_QTY); |
| | | BigDecimal subtract = baseQty.subtract(shortPositionSize); |
| | | int size = QTY + 1; |
| | | if (subtract.compareTo(BigDecimal.ZERO) >= 0) { |
| | | size = subtract.intValue() + 1; |
| | | } |
| | | log(String.format(" 挂空单 gridId:%d size:%d (pos=%s)", newId, size, shortPositionSize)); |
| | | newG.hasShortOrder = true; |
| | | newG.shortOrderId = newOrderId(); |
| | | newG.shortFilledQty = size; |
| | | |
| | | int cancelId = g.id - 2; |
| | | Grid cancelG = INDEX.get(cancelId); |
| | | if (cancelG != null && cancelG.hasShortOrder) { |
| | | log(String.format(" 取消gridId:%d的空单 orderId:%s", cancelId, cancelG.shortOrderId)); |
| | | cancelG.hasShortOrder = false; |
| | | cancelG.shortOrderId = null; |
| | | } |
| | | |
| | | Grid farthest = null; |
| | | for (Grid e : gridElements) { |
| | | if (e.shortTpOrderId != null) { |
| | | if (farthest == null || e.price.compareTo(farthest.price) < 0) { |
| | | farthest = e; |
| | | } |
| | | } |
| | | } |
| | | if (farthest != null) { |
| | | log(String.format(" 取消最远空仓止盈 gridId:%d orderId:%s", farthest.id, farthest.shortTpOrderId)); |
| | | farthest.shortTpOrderId = null; |
| | | } |
| | | |
| | | if (shortPositionSize.compareTo(BigDecimal.ZERO) == 0) { |
| | | shortActive = false; |
| | | log(" 空仓已全部止损,shortActive=false"); |
| | | } |
| | | } |
| | | |
| | | static void extendLongStopLoss(int filledQty) { |
| | | // 找到最远止损 |
| | | int furthestId = 0; |
| | | for (Grid e : gridElements) { |
| | | if (e.longSlOrderId != null && e.id < furthestId) { |
| | | furthestId = e.id; |
| | | } |
| | | } |
| | | if (furthestId == 0) furthestId = -11; |
| | | for (int i = 0; i < filledQty; i++) { |
| | | int newId = furthestId - i - 1; |
| | | Grid g = INDEX.get(newId); |
| | | if (g == null) continue; |
| | | String slId = newOrderId(); |
| | | g.longSlOrderId = slId; |
| | | log(String.format(" 多仓止损追加 gridId:%d slId:%s", newId, slId)); |
| | | } |
| | | } |
| | | |
| | | static void extendShortStopLoss(int filledQty) { |
| | | int furthestId = 0; |
| | | for (Grid e : gridElements) { |
| | | if (e.shortSlOrderId != null && e.id > furthestId) { |
| | | furthestId = e.id; |
| | | } |
| | | } |
| | | if (furthestId == 0) furthestId = 11; |
| | | for (int i = 0; i < filledQty; i++) { |
| | | int newId = furthestId + i + 1; |
| | | Grid g = INDEX.get(newId); |
| | | if (g == null) continue; |
| | | String slId = newOrderId(); |
| | | g.shortSlOrderId = slId; |
| | | log(String.format(" 空仓止损追加 gridId:%d slId:%s", newId, slId)); |
| | | } |
| | | } |
| | | |
| | | // ================================================================ |
| | | // 模拟 onPositionUpdate |
| | | // ================================================================ |
| | | static void onPositionUpdate(boolean isLong, BigDecimal size, BigDecimal entryPrice) { |
| | | if (isLong) { |
| | | if (size.compareTo(BigDecimal.ZERO) > 0) { |
| | | longActive = true; |
| | | longPositionSize = size; |
| | | longEntryPrice = entryPrice; |
| | | } else { |
| | | longActive = false; |
| | | longPositionSize = BigDecimal.ZERO; |
| | | longEntryPrice = BigDecimal.ZERO; |
| | | } |
| | | } else { |
| | | if (size.compareTo(BigDecimal.ZERO) > 0) { |
| | | shortActive = true; |
| | | shortPositionSize = size; |
| | | shortEntryPrice = entryPrice; |
| | | } else { |
| | | shortActive = false; |
| | | shortPositionSize = BigDecimal.ZERO; |
| | | shortEntryPrice = BigDecimal.ZERO; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ================================================================ |
| | | // 查找方法 |
| | | // ================================================================ |
| | | static Grid findByPrice(BigDecimal price) { |
| | | for (Grid g : gridElements) { |
| | | if (g.price.compareTo(price) == 0) return g; |
| | | } |
| | | return null; |
| | | } |
| | | static Grid findByLongSlOrderId(String oid) { |
| | | for (Grid g : gridElements) if (oid.equals(g.longSlOrderId)) return g; |
| | | return null; |
| | | } |
| | | static Grid findByShortSlOrderId(String oid) { |
| | | for (Grid g : gridElements) if (oid.equals(g.shortSlOrderId)) return g; |
| | | return null; |
| | | } |
| | | static Grid findByLongOrderId(String oid) { |
| | | for (Grid g : gridElements) if (oid.equals(g.longOrderId)) return g; |
| | | return null; |
| | | } |
| | | static Grid findByShortOrderId(String oid) { |
| | | for (Grid g : gridElements) if (oid.equals(g.shortOrderId)) return g; |
| | | return null; |
| | | } |
| | | |
| | | // ================================================================ |
| | | // 打印 |
| | | // ================================================================ |
| | | static void printGridState() { |
| | | System.out.println("\n === 网格状态 ==="); |
| | | System.out.println(" 多仓: pos=" + longPositionSize + " active=" + longActive + " entryPrice=" + longEntryPrice); |
| | | System.out.println(" 空仓: pos=" + shortPositionSize + " active=" + shortActive + " entryPrice=" + shortEntryPrice); |
| | | for (Grid g : gridElements) { |
| | | System.out.println(" " + g); |
| | | } |
| | | } |
| | | |
| | | // ================================================================ |
| | | // 主模拟流程 |
| | | // ================================================================ |
| | | public static void main(String[] args) { |
| | | System.out.println(repeat("=",70)); |
| | | System.out.println("Gate 网格策略模拟器"); |
| | | System.out.println("参数: basePrice=100.0, step=0.5, baseQty=10, qty=1, gridSize=10"); |
| | | System.out.println(repeat("=",70)); |
| | | |
| | | init(); |
| | | |
| | | // ---- 场景 A: 价格稳定 → 基底双开 → 进入ACTIVE ---- |
| | | System.out.println("\n\n◆◆◆ 场景A: 首根K线,基底双开 ◆◆◆"); |
| | | onFirstKline(); |
| | | // 模拟 onPositionUpdate 设置基底持仓 |
| | | onPositionUpdate(true, new BigDecimal(BASE_QTY), BASE_PRICE); |
| | | onPositionUpdate(false, new BigDecimal(BASE_QTY), BASE_PRICE); |
| | | |
| | | printGridState(); |
| | | |
| | | // ---- 场景 B: 价格下跌触发多仓止损 ---- |
| | | System.out.println("\n\n◆◆◆ 场景B: 价格跌至99.0,触发多仓止损(gridId:-2) ◆◆◆"); |
| | | Grid gMinus2 = INDEX.get(-2); |
| | | String slOrderId = gMinus2.longSlOrderId; |
| | | if (slOrderId != null) { |
| | | // 模拟止损成交 |
| | | onAutoOrder(slOrderId, "finished", "plan-close-long-position", "T001"); |
| | | // 模拟 onPositionUpdate: 多仓从10降到9 |
| | | onPositionUpdate(true, new BigDecimal("9"), new BigDecimal("99.5")); |
| | | printGridState(); |
| | | } else { |
| | | log(" gridId:-2 无止损单!"); |
| | | } |
| | | |
| | | // ---- 场景 C: 价格持续下跌,再次触发止损 ---- |
| | | System.out.println("\n\n◆◆◆ 场景C: 价格跌至98.5,再次触发多仓止损(gridId:-3) ◆◆◆"); |
| | | Grid gMinus3 = INDEX.get(-3); |
| | | slOrderId = gMinus3.longSlOrderId; |
| | | if (slOrderId != null) { |
| | | onAutoOrder(slOrderId, "finished", "plan-close-long-position", "T002"); |
| | | onPositionUpdate(true, new BigDecimal("8"), new BigDecimal("99.0")); |
| | | printGridState(); |
| | | } |
| | | |
| | | // ---- 场景 D: 止损追单成交,测试止盈追挂 ---- |
| | | System.out.println("\n\n◆◆◆ 场景D: 追单成交,触发止盈追挂 ◆◆◆"); |
| | | // 找到最近挂的多单 |
| | | Grid pendingLong = null; |
| | | for (Grid g : gridElements) { |
| | | if (g.hasLongOrder) { |
| | | pendingLong = g; |
| | | break; |
| | | } |
| | | } |
| | | if (pendingLong != null) { |
| | | // 模拟追单成交,追了2张 |
| | | log("\n模拟追单成交: gridId:" + pendingLong.id + " orderId:" + pendingLong.longOrderId); |
| | | longPositionSize = longPositionSize.add(new BigDecimal(pendingLong.longFilledQty)); |
| | | log("多仓持仓变为: " + longPositionSize); |
| | | onAutoOrder(pendingLong.longOrderId, "finished", "", "T003"); |
| | | onPositionUpdate(true, longPositionSize, BASE_PRICE); |
| | | printGridState(); |
| | | } |
| | | |
| | | // ---- 场景 E: 多仓全部止损归零 ---- |
| | | System.out.println("\n\n◆◆◆ 场景E: 价格继续暴跌,多仓全部止损归零 ◆◆◆"); |
| | | log("模拟多仓全部平仓..."); |
| | | onPositionUpdate(true, BigDecimal.ZERO, BigDecimal.ZERO); |
| | | log("多仓归零, longActive=" + longActive + ", pos=" + longPositionSize); |
| | | |
| | | // ACTIVE状态下多仓归零 → 下一根K线触发processShortGrid |
| | | System.out.println("\n 下一根K线触发 processShortGrid:"); |
| | | onKline(new BigDecimal("95.0")); |
| | | printGridState(); |
| | | |
| | | // ---- 场景 F: 空仓止损触发 ---- |
| | | System.out.println("\n\n◆◆◆ 场景F: 价格暴涨至101.0,触发空仓止损(gridId:2) ◆◆◆"); |
| | | Grid g2 = INDEX.get(2); |
| | | if (g2 != null && g2.shortSlOrderId != null) { |
| | | onAutoOrder(g2.shortSlOrderId, "finished", "plan-close-short-position", "T004"); |
| | | onPositionUpdate(false, new BigDecimal("9"), new BigDecimal("100.5")); |
| | | printGridState(); |
| | | } |
| | | |
| | | // ================================================================ |
| | | // BUG 分析 |
| | | // ================================================================ |
| | | System.out.println("\n\n" + repeat("=",70)); |
| | | System.out.println("BUG 分析报告"); |
| | | System.out.println(repeat("=",70)); |
| | | |
| | | int bugNum = 1; |
| | | |
| | | // BUG 1 |
| | | System.out.println("\n[BUG-" + (bugNum++) + "] 基座IOC成交不触发onAutoOrder,hasLongOrder/hasShortOrder永久为true"); |
| | | System.out.println(" 位置: GateGridTradeService.onKline() + tryGenerateQueues()"); |
| | | System.out.println(" 描述: 基底市价IOC开仓后,tryGenerateQueues中在ID=0设置了hasLongOrder=true/hasShortOrder=true。"); |
| | | System.out.println(" 但IOC成交不会触发onAutoOrder(条件单回调),导致ID=0的挂单状态永远不会被清除。"); |
| | | System.out.println(" 影响: ID=0的hasLongOrder永远为true。当止损触发在gridId=-2时,会尝试取消gridId=0的多单"); |
| | | System.out.println(" (cancelGridId = -2 + 2 = 0),由于IOC已成交,取消会失败但只产生warn日志,无严重后果。"); |
| | | System.out.println(" 但状态不一致可能影响后续processShortGrid/processLongGrid的匹配逻辑。"); |
| | | System.out.println(" 修复: tryGenerateQueues中不要用IOC的orderId标记hasLongOrder,或者增加onOrderUpdate回调处理普通订单。"); |
| | | |
| | | // BUG 2 |
| | | System.out.println("\n[BUG-" + (bugNum++) + "] handleShortStopLossTriggered 使用了longPositionSize计算size"); |
| | | System.out.println(" 位置: handleShortStopLossTriggered() line 1176"); |
| | | System.out.println(" 代码: BigDecimal subtract = baseQuantity.subtract(longPositionSize);"); |
| | | System.out.println(" 描述: 空仓止损触发时,用 longPositionSize 来计算追单size,应该用 shortPositionSize。"); |
| | | System.out.println(" 影响: 空仓止损追单的size计算基于多头持仓而非空头持仓,导致补仓数量错误。"); |
| | | System.out.println(" 例如多仓持仓5,空仓持仓9,空仓止损触发时: subtract = 10 - 5 = 5, size = 5+1 = 6."); |
| | | System.out.println(" 应该是: subtract = 10 - 9 = 1, size = 1+1 = 2."); |
| | | System.out.println(" 修复: 将 longPositionSize 改为 shortPositionSize。"); |
| | | |
| | | // BUG 3 |
| | | System.out.println("\n[BUG-" + (bugNum++) + "] onAutoOrder中持仓判断存在竞态问题"); |
| | | System.out.println(" 位置: onAutoOrder() 追挂止盈逻辑 (line 617/655)"); |
| | | System.out.println(" 描述: onAutoOrder被调用时,longPositionSize/shortPositionSize可能尚未被onPositionUpdate更新。"); |
| | | System.out.println(" WebSocket消息顺序虽然是顺序的,但onAutoOrder基于orders/autoorders频道,"); |
| | | System.out.println(" onPositionUpdate基于positions频道,两者到达顺序不一定严格匹配。"); |
| | | System.out.println(" 影响: 追挂止盈的excessCount计算可能偏小(持仓尚未更新),导致少挂止盈单。"); |
| | | System.out.println(" 下次fill时会再次尝试追挂,重复计算可能产生多余止盈单。"); |
| | | System.out.println(" 修复: 考虑在onPositionUpdate中做止盈追挂,或基于filledQty累加预估新持仓。"); |
| | | |
| | | // BUG 4 |
| | | System.out.println("\n[BUG-" + (bugNum++) + "] 止盈追挂从gridId±2开始,可能覆盖已有止损单的网格位置"); |
| | | System.out.println(" 位置: onAutoOrder() 止盈追挂逻辑"); |
| | | System.out.println(" 描述: 止盈追挂从 filledGridId+2(多)/filledGridId-2(空) 开始。"); |
| | | System.out.println(" 但这些位置可能已经被止损单占用(如initStopLossOrders在ID 2~11和-2~-11挂止损)。"); |
| | | System.out.println(" 虽然止盈和止损是不同的订单(分别存在takeProfitOrderId和stopLossOrderId中),"); |
| | | System.out.println(" 但同一个GridElement同时有止盈和止损,在状态追踪上可能模糊。"); |
| | | System.out.println(" 影响: 同一个网格位置可能同时有止盈和止损订单,当订单成交回调时可能匹配错误。"); |
| | | System.out.println(" 风险: 中低。onAutoOrder先用findByLongStopLossOrderId匹配止损,再匹配入口单,止盈不会被匹配。"); |
| | | |
| | | // BUG 5 → 已确认为设计意图,移除 |
| | | System.out.println("\n[OK-5] processShortGrid/processLongGrid 只在对方持仓归零时触发 — 属于设计意图,非BUG"); |
| | | System.out.println(" 位置: onKline() line 389-400"); |
| | | System.out.println(" 描述: 策略有两套驱动机制:有持仓时走订单驱动(onAutoOrder处理挂单成交/止损追单),"); |
| | | System.out.println(" 当某方向持仓归零后切换到价格驱动(onKline→processGrid),挂新入场单后回到订单驱动。"); |
| | | System.out.println(" 两套机制交替运作,确保仓位归零后能基于当前价格重新入场。"); |
| | | |
| | | // BUG 6 |
| | | System.out.println("\n[BUG-" + (bugNum++) + "] processShortGrid/processLongGrid 中 size 使用 BASE_QTY 而非 QTY"); |
| | | System.out.println(" 位置: processShortGrid() line 1027, processLongGrid() line 1077"); |
| | | System.out.println(" 描述: 仓位归零后重新挂单时,使用 baseQuantity 而不是 quantity。"); |
| | | System.out.println(" 例如 baseQuantity=10,会在新位置挂10张,而其他网格层级只挂1张。"); |
| | | System.out.println(" 影响: 每次仓位归零后的重新入场都是10张,可能导致持仓量远超预期。"); |
| | | System.out.println(" 风险: 可能是设计意图(仓位归零后需要重建基底仓位),但需确认。"); |
| | | |
| | | // BUG 7 |
| | | System.out.println("\n[BUG-" + (bugNum++) + "] extendLongStopLoss/extendShortStopLoss 不停向外扩展"); |
| | | System.out.println(" 位置: extendLongStopLoss() line 1216, extendShortStopLoss() line 1249"); |
| | | System.out.println(" 描述: 每次成交都调用extend,从当前最远止损位置继续向外挂filledQty个止损单。"); |
| | | System.out.println(" 如果成交次数多,止损单会无限向外扩展(比如挂到gridId=-100)。"); |
| | | System.out.println(" 影响: 大量止损单可能超出交易所限制(Gate条件单上限通常200个)。并且止损位置离当前价"); |
| | | System.out.println(" 越来越远,实际作用不大但占用资源。"); |
| | | System.out.println(" 风险: 低中。止损位置越来越远意味着回撤越来越大,风控上可能已经触发maxLoss停止策略。"); |
| | | |
| | | // BUG 8 → 已确认为设计意图,移除 |
| | | System.out.println("\n[OK-8] onPositionUpdate 中两仓归零异步重启策略 — 属于设计意图,非BUG"); |
| | | System.out.println(" 位置: onPositionUpdate() line 522-538"); |
| | | System.out.println(" 描述: 当 shortActive==false && longActive==false 时,取消所有条件单并平仓,"); |
| | | System.out.println(" 然后异步sleep 3s后调用startGrid重启;双仓归零后重新开启一轮新策略。"); |
| | | |
| | | // BUG 9 |
| | | System.out.println("\n[BUG-" + (bugNum++) + "] onAutoOrder中stopLoss匹配缺少positionSize校验"); |
| | | System.out.println(" 位置: onAutoOrder() line 595-604"); |
| | | System.out.println(" 描述: 原始代码有 longPositionSize > 0 的校验但被注释掉了:"); |
| | | System.out.println(" // if (longStopLossElem != null && longPositionSize.compareTo(BigDecimal.ZERO) > 0 ..."); |
| | | System.out.println(" 现在只要tradeId非空非0就触发handler。"); |
| | | System.out.println(" 影响: 如果多仓已全部平仓但止损单尚未取消(异步延迟),止损单的finished回调"); |
| | | System.out.println(" 仍会触发handleLongStopLossTriggered,产生多余的追单操作。"); |
| | | System.out.println(" 修复: 恢复positionSize > 0的校验。"); |
| | | |
| | | // BUG 10 |
| | | System.out.println("\n[BUG-" + (bugNum++) + "] tryGenerateQueues中上/下ID分配有误导性命名"); |
| | | System.out.println(" 位置: updateGridElements() line 960-967"); |
| | | System.out.println(" 描述: ID=0的upId=1(指向高价), downId=-1(指向低价)。"); |
| | | System.out.println(" 在注释和文档中写的是 upId=-1, downId=1,但代码实际是反过来的。"); |
| | | System.out.println(" 实际语义: upId=\"向上的价格\"(更大ID), downId=\"向下的价格\"(更小ID)。"); |
| | | System.out.println(" 影响: 代码实际工作正确,但注释和代码不一致,容易误导维护者。"); |
| | | System.out.println(" 状态: 可能是早期代码重构遗留,实际语义是\"up=价格向上, down=价格向下\"。"); |
| | | |
| | | // BUG 11 |
| | | System.out.println("\n[BUG-" + (bugNum++) + "] currentLongOrderIds/currentShortOrderIds 声明但从未使用"); |
| | | System.out.println(" 位置: GateGridTradeService line 119-121"); |
| | | System.out.println(" 描述: currentLongOrderIds 和 currentShortOrderIds 是 synchronized LinkedHashMap,"); |
| | | System.out.println(" 声明但代码中没有任何put操作,只有clear。"); |
| | | System.out.println(" 影响: 死代码,占用内存但无实际作用。代码中对此Map的使用意图已过时。"); |
| | | |
| | | // BUG 12 |
| | | System.out.println("\n[BUG-" + (bugNum++) + "] onAutoOrder中已成交的挂单仍通过isHasLongOrder检测"); |
| | | System.out.println(" 位置: onAutoOrder() line 608/617"); |
| | | System.out.println(" 描述: 当查找shortGridElement时,检查 isHasShortOrder() 为true才处理。"); |
| | | System.out.println(" 但同一个orderId被匹配到说明它确实是挂单,检查 isHasShortOrder 可能因"); |
| | | System.out.println(" placeEntryOrderWithPreFlag的竞态导致问题(API失败→回滚→但WS已推送)。"); |
| | | System.out.println(" 影响: 如果挂单API失败但WS已推送finished回调,isHasShortOrder可能已回滚为false,"); |
| | | System.out.println(" 导致成交回调被忽略,持仓状态与实际不符。"); |
| | | System.out.println(" 风险: 极低(需要API失败+WS推送的精确时序)。"); |
| | | } |
| | | } |