From 08c5559cf6518272ab9a7b56bd80a76564d6a2f3 Mon Sep 17 00:00:00 2001 From: dongzp_book <90fanhua@gmail.com> Date: Sat, 11 Jul 2026 00:43:35 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BD=91=E6=A0=BC=E5=8D=96=E5=8D=95=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=8C=82=E5=8D=95=E6=88=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../trend/strategy/AutoGridStrategy.java | 384 ++++++++++++------ 2 files changed, 265 insertions(+), 121 deletions(-) diff --git a/pom.xml b/pom.xml index 278c562..65af8f1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.sample trend-grid-strategy - 1.0.0-SNAPSHOT + 1.0.1-SNAPSHOT trend-grid-strategy 趋势网格策略 diff --git a/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java b/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java index 066b98c..b4526e5 100644 --- a/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java +++ b/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java @@ -36,8 +36,8 @@ import java.util.concurrent.CopyOnWriteArrayList; * 网格间距 = atr_multiplier × ATR,并限制在 [min_gap, max_gap] 区间。 * 下方买单始终最多挂 1 笔:成交后再挂下一格,不会批量铺满网格。 * 若现价相对挂单价上行超过间距 × 1.5,撤单并按现价重新挂靠下一格,避免买单悬空。 - * 采用限价挂单买入,市价止盈卖出。 - * 网格间距变化时不撤销已有挂单,已成交仓位按最新间距计算止盈价。 + * 采用限价挂单买入,买单成交后在「入场价 + 间距」挂限价卖单止盈。 + * 网格间距变化时不撤销已有挂单,已挂卖单价格保持不变。 */ public class AutoGridStrategy extends BaseStrategy { @@ -85,14 +85,14 @@ public class AutoGridStrategy extends BaseStrategy { * 待成交买单:key = 网格价格字符串 */ private final Map pendingBuyOrders = new ConcurrentHashMap<>(); + /** + * 待成交卖单:key = clientPositionId + */ + private final Map pendingSellOrders = new ConcurrentHashMap<>(); /** * 已成交待止盈的多头仓位 */ private final List openPositions = new CopyOnWriteArrayList<>(); - /** - * 正在平仓中的仓位,防止重复触发 - */ - private final Map closingPositions = new ConcurrentHashMap<>(); /** * 已处理过的买单成交 orderId,避免 fill / orders 双推重复开仓补单 */ @@ -123,19 +123,35 @@ public class AutoGridStrategy extends BaseStrategy { } } + private static class GridSellOrder { + private final String clientOrderId; + private volatile String exchangeOrderId; + private final String clientPositionId; + private final BigDecimal sellPrice; + private final BigDecimal quantity; + + GridSellOrder(String clientOrderId, String clientPositionId, + BigDecimal sellPrice, BigDecimal quantity) { + this.clientOrderId = clientOrderId; + this.clientPositionId = clientPositionId; + this.sellPrice = sellPrice; + this.quantity = quantity; + } + } + private static class GridPosition { private final String clientPositionId; private final BigDecimal entryPrice; private final BigDecimal quantity; + /** 开仓时锁定的止盈价,不随 gridGap 变化 */ + private final BigDecimal takeProfitPrice; - GridPosition(String clientPositionId, BigDecimal entryPrice, BigDecimal quantity) { + GridPosition(String clientPositionId, BigDecimal entryPrice, BigDecimal quantity, + BigDecimal takeProfitPrice) { this.clientPositionId = clientPositionId; this.entryPrice = entryPrice; this.quantity = quantity; - } - - BigDecimal takeProfitPrice(BigDecimal gridGap) { - return entryPrice.add(gridGap); + this.takeProfitPrice = takeProfitPrice; } } @@ -287,6 +303,7 @@ public class AutoGridStrategy extends BaseStrategy { refreshGridGap(true); syncOpenOrdersFromExchange(); pruneExcessPendingBuys(); + replenishMissingSellOrders(); placeInitialBuyGrids(); createTask(this::syncAccount, 5, 600); @@ -307,7 +324,7 @@ public class AutoGridStrategy extends BaseStrategy { try { checkPriceBounds(); refreshGridGap(false); - processTakeProfit(); + replenishMissingSellOrders(); rebaseStalePendingBuy(); replenishBuyGrids(); updateDashboard(); @@ -382,7 +399,7 @@ public class AutoGridStrategy extends BaseStrategy { if (oldGap != null && oldGap.compareTo(gap) != 0) { logger.info("网格间距调整:{} -> {}(ATR={})", oldGap, gap, atr); submitAlarm("网格间距调整", - String.format("间距 %s -> %s,ATR=%s,已有挂单保持不变,止盈按新间距计算", + String.format("间距 %s -> %s,ATR=%s,已有挂单及卖单价格保持不变", formatPrice(oldGap), formatPrice(gap), formatPrice(atr)), AlarmLevel.INFO); } @@ -589,53 +606,60 @@ public class AutoGridStrategy extends BaseStrategy { } } - // ======================== 止盈(市价平仓) ======================== + // ======================== 限价卖单止盈 ======================== - private void processTakeProfit() { - if (currentGridGap == null || currentPrice == null) { - return; - } - - for (GridPosition position : openPositions) { - BigDecimal tpPrice = position.takeProfitPrice(currentGridGap); - if (currentPrice.compareTo(tpPrice) < 0) { - continue; + /** + * 为缺少卖单的持仓补挂限价止盈单(启动恢复或卖单被撤时)。 + */ + private void replenishMissingSellOrders() { + synchronized (tradeLock) { + for (GridPosition position : openPositions) { + if (!hasSellOrderFor(position.clientPositionId)) { + placeLimitSellOrder(position); + } } - if (closingPositions.putIfAbsent(position.clientPositionId, Boolean.TRUE) != null) { - continue; - } - marketClosePosition(position, tpPrice); } } - private void marketClosePosition(GridPosition position, BigDecimal tpPrice) { - synchronized (tradeLock) { - try { - JSONObject orderInfo = buildOrderRequest("sell", "close", "market", null, - position.quantity, true); - orderInfo.put("clientOid", buildClientOrderId("SELL", position.entryPrice)); - - JSONObject result = bitGetClient.placeOrder(orderInfo); - String exchangeOrderId = result.getString("orderId"); - - logger.info("市价止盈:入场={},止盈价={},当前价={},订单号={}", - position.entryPrice, tpPrice, currentPrice, exchangeOrderId); - - submitAlarm("网格止盈", - String.format("入场 %s,止盈目标 %s,当前价 %s,市价平仓已提交", - formatPrice(position.entryPrice), formatPrice(tpPrice), formatPrice(currentPrice)), - AlarmLevel.INFO); - - submitOrderSync(exchangeOrderId, position.clientPositionId, currentPrice, position.quantity, - OrderSide.SELL, OrderStatus.NEW); - } catch (Exception e) { - closingPositions.remove(position.clientPositionId); - logger.error("市价止盈失败,入场价={}", position.entryPrice, e); - submitAlarm("止盈失败", - String.format("入场 %s:%s", formatPrice(position.entryPrice), e.getMessage()), - AlarmLevel.ERROR); - } + private void placeLimitSellOrder(GridPosition position) { + if (hasSellOrderFor(position.clientPositionId)) { + return; } + BigDecimal sellPrice = position.takeProfitPrice; + if (sellPrice.compareTo(upperBound) > 0) { + logger.warn("止盈价 {} 超过上限 {},跳过挂卖单", sellPrice, upperBound); + return; + } + + String clientOrderId = buildClientOrderId("SELL", position.entryPrice); + try { + JSONObject orderInfo = buildOrderRequest("sell", "close", "limit", sellPrice, + position.quantity, true); + orderInfo.put("clientOid", clientOrderId); + + JSONObject result = bitGetClient.placeOrder(orderInfo); + String exchangeOrderId = result.getString("orderId"); + + GridSellOrder sellOrder = new GridSellOrder(clientOrderId, position.clientPositionId, + sellPrice, position.quantity); + sellOrder.exchangeOrderId = exchangeOrderId; + pendingSellOrders.put(position.clientPositionId, sellOrder); + + logger.info("挂卖单成功:入场={},止盈价={},数量={},订单号={}", + position.entryPrice, sellPrice, position.quantity, exchangeOrderId); + submitOrderSync(exchangeOrderId, clientOrderId, sellPrice, position.quantity, + OrderSide.SELL, OrderStatus.NEW); + } catch (Exception e) { + logger.error("挂卖单失败,入场价={},止盈价={}", position.entryPrice, sellPrice, e); + submitAlarm("挂卖单失败", + String.format("入场 %s,止盈 %s:%s", + formatPrice(position.entryPrice), formatPrice(sellPrice), e.getMessage()), + AlarmLevel.ERROR); + } + } + + private boolean hasSellOrderFor(String clientPositionId) { + return pendingSellOrders.containsKey(clientPositionId); } // ======================== WebSocket 事件处理 ======================== @@ -671,7 +695,7 @@ public class AutoGridStrategy extends BaseStrategy { if (isOpenBuy(side, tradeSide)) { onBuyFilled(price, volume, orderId, clientOid, price); } else if (isCloseSell(side, tradeSide)) { - onSellFilled(price, volume, orderId); + onSellFilled(price, volume, orderId, clientOid); } } @@ -695,6 +719,8 @@ public class AutoGridStrategy extends BaseStrategy { if ("cancelled".equals(status) || "canceled".equals(status)) { if ("buy".equals(side)) { removePendingBuy(orderId, clientOid, orderPrice); + } else if ("sell".equals(side)) { + removePendingSell(orderId, clientOid, orderPrice); } return; } @@ -715,7 +741,7 @@ public class AutoGridStrategy extends BaseStrategy { if (isOpenBuy(side, tradeSide)) { onBuyFilled(fillPrice, volume, orderId, clientOid, orderPrice); } else if (isCloseSell(side, tradeSide)) { - onSellFilled(fillPrice, volume, orderId); + onSellFilled(fillPrice, volume, orderId, clientOid); } } @@ -741,73 +767,83 @@ public class AutoGridStrategy extends BaseStrategy { return; } - BigDecimal gridPrice = pending.buyPrice; - String clientPositionId = "POS_" + gridPrice.toPlainString(); - GridPosition position = new GridPosition(clientPositionId, gridPrice, volume); - openPositions.add(position); + synchronized (tradeLock) { + BigDecimal gridPrice = pending.buyPrice; + String clientPositionId = "POS_" + gridPrice.toPlainString(); + if (currentGridGap == null) { + if (orderId != null) { + processedBuyFillOrderIds.remove(orderId); + } + logger.warn("网格间距未就绪,无法处理买单成交,orderId={}", orderId); + return; + } - BigDecimal tpPrice = currentGridGap != null ? position.takeProfitPrice(currentGridGap) : null; - logger.info("买单成交:网格价={},成交价={},数量={},止盈目标={},orderId={}", - gridPrice, fillPrice, volume, tpPrice, orderId); + BigDecimal tpPrice = normalizePrice(gridPrice.add(currentGridGap)); + GridPosition position = new GridPosition(clientPositionId, gridPrice, volume, tpPrice); + openPositions.add(position); - submitAlarm("网格买入成交", - String.format("买入价 %s,数量 %s,止盈目标 %s(间距 %s)", - formatPrice(gridPrice), volume.toPlainString(), - formatPrice(tpPrice), formatPrice(currentGridGap)), - AlarmLevel.INFO); + logger.info("买单成交:网格价={},成交价={},数量={},止盈价={},orderId={}", + gridPrice, fillPrice, volume, tpPrice, orderId); - submitOrderSync(orderId, clientPositionId, gridPrice, volume, OrderSide.BUY, OrderStatus.FILLED); - submitPositionSync(clientPositionId, gridPrice, volume, true); + submitAlarm("网格买入成交", + String.format("买入价 %s,数量 %s,挂卖单止盈 %s(间距 %s)", + formatPrice(gridPrice), volume.toPlainString(), + formatPrice(tpPrice), formatPrice(currentGridGap)), + AlarmLevel.INFO); - // 成交后再挂下方一格,保证永远只有一笔待成交买单 - placeNextBuyBelow(gridPrice); + submitOrderSync(orderId, clientPositionId, gridPrice, volume, OrderSide.BUY, OrderStatus.FILLED); + submitPositionSync(clientPositionId, gridPrice, volume, true); + + placeLimitSellOrder(position); + placeNextBuyBelow(gridPrice); + } } - private void onSellFilled(BigDecimal price, BigDecimal volume, String orderId) { + private void onSellFilled(BigDecimal price, BigDecimal volume, String orderId, String clientOid) { if (orderId != null && !processedSellFillOrderIds.add(orderId)) { return; } - GridPosition matched = null; - for (String closingId : closingPositions.keySet()) { - for (GridPosition position : openPositions) { - if (position.clientPositionId.equals(closingId)) { - matched = position; - break; + synchronized (tradeLock) { + GridSellOrder pendingSell = removePendingSell(orderId, clientOid, null); + GridPosition matched = null; + if (pendingSell != null) { + for (GridPosition position : openPositions) { + if (position.clientPositionId.equals(pendingSell.clientPositionId)) { + matched = position; + break; + } } } + if (matched == null && clientOid != null) { + BigDecimal entryPrice = parseEntryPriceFromClientOid(clientOid); + if (entryPrice != null) { + String posId = "POS_" + entryPrice.toPlainString(); + for (GridPosition position : openPositions) { + if (position.clientPositionId.equals(posId)) { + matched = position; + break; + } + } + } + } + if (matched != null) { - break; + openPositions.remove(matched); + pendingSellOrders.remove(matched.clientPositionId); + submitPositionSync(matched.clientPositionId, price, matched.quantity, false); } - } - if (matched == null && volume != null) { - for (GridPosition position : openPositions) { - if (position.quantity.compareTo(volume) == 0) { - matched = position; - break; - } - } - } - if (matched == null && !openPositions.isEmpty()) { - matched = openPositions.get(0); - } - if (matched != null) { - openPositions.remove(matched); - closingPositions.remove(matched.clientPositionId); - submitPositionSync(matched.clientPositionId, price, matched.quantity, false); + logger.info("卖单成交(止盈):价格={},数量={},orderId={}", price, volume, orderId); + submitAlarm("网格止盈成交", + String.format("卖出价 %s,数量 %s", formatPrice(price), + volume != null ? volume.toPlainString() : "-"), + AlarmLevel.INFO); + submitOrderSync(orderId, matched != null ? matched.clientPositionId : "CLOSE", price, volume, + OrderSide.SELL, OrderStatus.FILLED); + + replenishBuyGrids(); } - - logger.info("卖单成交(止盈):价格={},数量={},orderId={}", price, volume, orderId); - submitAlarm("网格止盈成交", - String.format("卖出价 %s,数量 %s", formatPrice(price), - volume != null ? volume.toPlainString() : "-"), - AlarmLevel.INFO); - submitOrderSync(orderId, matched != null ? matched.clientPositionId : "CLOSE", price, volume, - OrderSide.SELL, OrderStatus.FILLED); - - // 成交后补充下一格买单 - replenishBuyGrids(); } private String resolveSymbol(JSONObject payload) { @@ -881,6 +917,68 @@ public class AutoGridStrategy extends BaseStrategy { return null; } + private GridSellOrder removePendingSell(String orderId, String clientOid, BigDecimal price) { + if (orderId != null) { + for (Iterator> it = pendingSellOrders.entrySet().iterator(); + it.hasNext(); ) { + Map.Entry entry = it.next(); + GridSellOrder order = entry.getValue(); + if (orderId.equals(order.exchangeOrderId)) { + it.remove(); + return order; + } + } + } + if (clientOid != null) { + for (Iterator> it = pendingSellOrders.entrySet().iterator(); + it.hasNext(); ) { + Map.Entry entry = it.next(); + GridSellOrder order = entry.getValue(); + if (clientOid.equals(order.clientOrderId)) { + it.remove(); + return order; + } + } + } + if (price != null) { + BigDecimal normalized = normalizePrice(price); + for (Iterator> it = pendingSellOrders.entrySet().iterator(); + it.hasNext(); ) { + Map.Entry entry = it.next(); + GridSellOrder order = entry.getValue(); + if (normalized.compareTo(order.sellPrice) == 0) { + it.remove(); + return order; + } + } + } + return null; + } + + /** + * 从 clientOid(SELL_{symbol}_{entryPrice}_{ts})解析入场价。 + */ + private BigDecimal parseEntryPriceFromClientOid(String clientOid) { + if (clientOid == null || !clientOid.startsWith("SELL_")) { + return null; + } + String plainSymbol = getPlainSymbol(); + String prefix = "SELL_" + plainSymbol + "_"; + if (!clientOid.startsWith(prefix)) { + return null; + } + String remainder = clientOid.substring(prefix.length()); + int lastUnderscore = remainder.lastIndexOf('_'); + if (lastUnderscore <= 0) { + return null; + } + try { + return normalizePrice(new BigDecimal(remainder.substring(0, lastUnderscore))); + } catch (NumberFormatException e) { + return null; + } + } + // ======================== 交易所同步 ======================== private void syncOpenOrdersFromExchange() { @@ -889,29 +987,63 @@ public class AutoGridStrategy extends BaseStrategy { if (openOrders == null) { return; } + int buyCount = 0; + int sellCount = 0; for (int i = 0; i < openOrders.size(); i++) { JSONObject order = openOrders.getJSONObject(i); - if (!"buy".equals(order.getString("side"))) { - continue; - } - if (!"open".equals(order.getString("tradeSide"))) { - continue; - } + String side = order.getString("side"); + String tradeSide = order.getString("tradeSide"); BigDecimal price = normalizePrice(order.getBigDecimal("price")); String clientOid = order.getString("clientOid"); - if (clientOid == null) { - clientOid = "SYNC_" + price.toPlainString(); + String exchangeOrderId = order.getString("orderId"); + BigDecimal quantity = firstPositive( + order.getBigDecimal("size"), + order.getBigDecimal("baseVolume")); + + if ("buy".equals(side) && "open".equals(tradeSide)) { + if (clientOid == null) { + clientOid = "SYNC_BUY_" + price.toPlainString(); + } + GridBuyOrder gridBuyOrder = new GridBuyOrder(clientOid, price); + gridBuyOrder.exchangeOrderId = exchangeOrderId; + pendingBuyOrders.put(priceKey(price), gridBuyOrder); + buyCount++; + } else if ("sell".equals(side) && "close".equals(tradeSide)) { + if (clientOid == null) { + clientOid = "SYNC_SELL_" + price.toPlainString(); + } + BigDecimal entryPrice = parseEntryPriceFromClientOid(clientOid); + String clientPositionId = entryPrice != null + ? "POS_" + entryPrice.toPlainString() + : "SYNC_POS_" + price.toPlainString(); + if (quantity == null) { + quantity = baseQuantity; + } + GridSellOrder sellOrder = new GridSellOrder(clientOid, clientPositionId, price, quantity); + sellOrder.exchangeOrderId = exchangeOrderId; + pendingSellOrders.put(clientPositionId, sellOrder); + sellCount++; + + if (entryPrice != null && !hasOpenPosition(clientPositionId)) { + openPositions.add(new GridPosition(clientPositionId, entryPrice, quantity, price)); + } } - GridBuyOrder gridBuyOrder = new GridBuyOrder(clientOid, price); - gridBuyOrder.exchangeOrderId = order.getString("orderId"); - pendingBuyOrders.put(priceKey(price), gridBuyOrder); } - logger.info("同步挂单 {} 笔", pendingBuyOrders.size()); + logger.info("同步挂单:买单 {} 笔,卖单 {} 笔", buyCount, sellCount); } catch (Exception e) { logger.warn("同步挂单失败:{}", e.getMessage()); } } + private boolean hasOpenPosition(String clientPositionId) { + for (GridPosition position : openPositions) { + if (position.clientPositionId.equals(clientPositionId)) { + return true; + } + } + return false; + } + /** * 若交易所上残留多笔买单,只保留最靠近现价(价格最高)的一笔,撤销其余。 */ @@ -1039,6 +1171,7 @@ public class AutoGridStrategy extends BaseStrategy { sb.append("
ATR:").append(formatPrice(currentAtr)).append("
"); sb.append("
网格间距:").append(formatPrice(currentGridGap)).append("
"); sb.append("
待成交买单:").append(pendingBuyOrders.size()).append(" / 1 笔
"); + sb.append("
待成交卖单:").append(pendingSellOrders.size()).append(" 笔
"); sb.append("
持仓待止盈:").append(openPositions.size()) .append(" / ").append(gridCount).append(" 层
"); sb.append("
上限暂停:").append(upperBoundPaused ? "是" : "否").append("
"); @@ -1069,6 +1202,17 @@ public class AutoGridStrategy extends BaseStrategy { rows.add(row); } + List pendingSells = new ArrayList<>(pendingSellOrders.values()); + pendingSells.sort(Comparator.comparing(o -> o.sellPrice, Comparator.reverseOrder())); + for (GridSellOrder order : pendingSells) { + JSONObject row = new JSONObject(); + row.put("type", "待成交卖单"); + row.put("price", formatPrice(order.sellPrice)); + row.put("quantity", order.quantity.toPlainString()); + row.put("takeProfit", formatPrice(order.sellPrice)); + rows.add(row); + } + List positions = new ArrayList<>(openPositions); positions.sort(Comparator.comparing(p -> p.entryPrice, Comparator.reverseOrder())); for (GridPosition position : positions) { @@ -1076,7 +1220,7 @@ public class AutoGridStrategy extends BaseStrategy { row.put("type", "持仓"); row.put("price", formatPrice(position.entryPrice)); row.put("quantity", position.quantity.toPlainString()); - row.put("takeProfit", formatPrice(position.takeProfitPrice(currentGridGap))); + row.put("takeProfit", formatPrice(position.takeProfitPrice)); rows.add(row); }