diff --git a/src/main/java/com/sample/trend/config/CommonConstant.java b/src/main/java/com/sample/trend/config/CommonConstant.java index a54adfc..2447d09 100644 --- a/src/main/java/com/sample/trend/config/CommonConstant.java +++ b/src/main/java/com/sample/trend/config/CommonConstant.java @@ -4,12 +4,18 @@ import vip.uuquant.exhangeapi.core.ExchangeFactory; import vip.uuquant.exhangeapi.core.HttpClient; import vip.uuquant.exhangeapi.core.WebSocketManager; +import java.net.InetSocketAddress; +import java.net.Proxy; + /** * 交易所客户端全局单例 */ public class CommonConstant { - public static final HttpClient httpClient = HttpClient.builder().build(); + /** 全局 HTTP 代理 */ + public static final Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 7897)); + + public static final HttpClient httpClient = HttpClient.builder().proxy(proxy).build(); public static final WebSocketManager webSocketManager = new WebSocketManager(httpClient); diff --git a/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java b/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java index bf7cb1f..f433e84 100644 --- a/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java +++ b/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java @@ -32,6 +32,7 @@ import java.util.concurrent.CopyOnWriteArrayList; * 基于 ATR 的动态网格策略(只做多)。 *

* 网格间距 = atr_multiplier × ATR,并限制在 [min_gap, max_gap] 区间。 + * 下方买单始终最多挂 1 笔:成交后再挂下一格,不会批量铺满网格。 * 采用限价挂单买入,市价止盈卖出。 * 网格间距变化时不撤销已有挂单,已成交仓位按最新间距计算止盈价。 */ @@ -54,7 +55,7 @@ public class AutoGridStrategy extends BaseStrategy { private boolean testMode; private int leverage; /** - * 最大待成交买单数量 + * 最大持仓层数;达到后不再新建买单。待成交买单始终最多 1 笔。 */ private int gridCount; @@ -246,6 +247,7 @@ public class AutoGridStrategy extends BaseStrategy { refreshGridGap(true); syncOpenOrdersFromExchange(); + pruneExcessPendingBuys(); placeInitialBuyGrids(); createTask(this::syncAccount, 5, 600); @@ -299,7 +301,7 @@ public class AutoGridStrategy extends BaseStrategy { bitGetClient.changeTestMode(testMode); AccountVO account = getAccount(); bitGetClient.init(account.getApiKey(), account.getSecretKey(), account.getPassphrase(), - null, CommonConstant.webSocketManager); + CommonConstant.httpClient, CommonConstant.webSocketManager); // try { // bitGetClient.setLeverage(getSymbol(), leverage); // } catch (Exception e) { @@ -357,19 +359,14 @@ public class AutoGridStrategy extends BaseStrategy { private BigDecimal fetchAtr() throws Exception { JSONObject params = new JSONObject(); params.put("symbol", getPlainSymbol()); - params.put("granularity", normalizeKlineInterval(klineInterval)); + params.put("granularity", klineInterval); params.put("limit", String.valueOf(klinePeriod + 2)); JSONArray klines = bitGetClient.getKLines(params); if (klines == null || klines.isEmpty()) { return null; } - // BitGet 返回倒序,转为升序 - JSONArray sorted = new JSONArray(); - for (int i = klines.size() - 1; i >= 0; i--) { - sorted.add(klines.getJSONArray(i)); - } - return AtrCalculator.calculate(sorted, klinePeriod); + return AtrCalculator.calculate(klines, klinePeriod); } private String normalizeKlineInterval(String interval) { @@ -418,7 +415,7 @@ public class AutoGridStrategy extends BaseStrategy { // ======================== 网格挂单 ======================== /** - * 初始化仅挂下方一格买单,与运行中补单逻辑一致,避免网格间距调整后批量挂单失配。 + * 初始化仅挂下方一格买单,与运行中补单逻辑一致。 */ private void placeInitialBuyGrids() { if (currentPrice == null || currentGridGap == null) { @@ -427,25 +424,22 @@ public class AutoGridStrategy extends BaseStrategy { replenishBuyGrids(); } + /** + * 无待成交买单时,在现价下方挂一格;已有挂单则不补,保证始终只有 1 笔买单。 + */ private void replenishBuyGrids() { - if (!canPlaceBuyOrders() || currentGridGap == null) { + if (!canPlaceBuyOrders() || currentGridGap == null || currentPrice == null) { return; } synchronized (tradeLock) { - if (pendingBuyOrders.size() >= gridCount) { + if (!pendingBuyOrders.isEmpty() || openPositions.size() >= gridCount) { return; } - BigDecimal lowestPending = findLowestPendingBuyPrice(); - BigDecimal nextBuyPrice; - - if (lowestPending != null) { - nextBuyPrice = lowestPending.subtract(currentGridGap); - } else if (currentPrice != null) { - nextBuyPrice = alignPriceDown(currentPrice, currentGridGap); - } else { - return; + BigDecimal nextBuyPrice = alignPriceDown(currentPrice, currentGridGap); + if (nextBuyPrice.compareTo(currentPrice) >= 0) { + nextBuyPrice = nextBuyPrice.subtract(currentGridGap); } if (nextBuyPrice.compareTo(lowerBound) >= 0 && !hasBuyOrderAt(nextBuyPrice)) { @@ -454,8 +448,28 @@ public class AutoGridStrategy extends BaseStrategy { } } + /** + * 买单成交后,挂「成交价再下一格」的限价买单。 + */ + private void placeNextBuyBelow(BigDecimal filledPrice) { + if (!canPlaceBuyOrders() || currentGridGap == null || filledPrice == null) { + return; + } + + synchronized (tradeLock) { + if (!pendingBuyOrders.isEmpty() || openPositions.size() >= gridCount) { + return; + } + + BigDecimal nextBuyPrice = normalizePrice(filledPrice.subtract(currentGridGap)); + if (nextBuyPrice.compareTo(lowerBound) >= 0 && !hasBuyOrderAt(nextBuyPrice)) { + placeLimitBuyOrder(nextBuyPrice); + } + } + } + private void placeLimitBuyOrder(BigDecimal price) { - if (pendingBuyOrders.size() >= gridCount) { + if (!pendingBuyOrders.isEmpty() || openPositions.size() >= gridCount) { return; } BigDecimal normalizedPrice = normalizePrice(price); @@ -613,6 +627,9 @@ public class AutoGridStrategy extends BaseStrategy { submitOrderSync(orderId, clientPositionId, normalizedPrice, volume, OrderSide.BUY, OrderStatus.FILLED); submitPositionSync(clientPositionId, normalizedPrice, volume, true); + + // 成交后再挂下方一格,保证永远只有一笔待成交买单 + placeNextBuyBelow(normalizedPrice); } private void onSellFilled(BigDecimal price, BigDecimal volume, String orderId) { @@ -688,6 +705,48 @@ public class AutoGridStrategy extends BaseStrategy { } } + /** + * 若交易所上残留多笔买单,只保留最靠近现价(价格最高)的一笔,撤销其余。 + */ + private void pruneExcessPendingBuys() { + if (pendingBuyOrders.size() <= 1) { + return; + } + + synchronized (tradeLock) { + GridBuyOrder keep = pendingBuyOrders.values().stream() + .max(Comparator.comparing(o -> o.buyPrice)) + .orElse(null); + if (keep == null) { + return; + } + + List extras = new ArrayList<>(); + for (GridBuyOrder order : pendingBuyOrders.values()) { + if (order.buyPrice.compareTo(keep.buyPrice) != 0) { + extras.add(order); + } + } + + for (GridBuyOrder order : extras) { + try { + bitGetClient.cancelOrder(getSymbol(), order.exchangeOrderId, order.clientOrderId, null); + pendingBuyOrders.remove(priceKey(order.buyPrice)); + logger.info("撤销多余买单:价格={},订单号={}", order.buyPrice, order.exchangeOrderId); + } catch (Exception e) { + logger.error("撤销多余买单失败,价格={}", order.buyPrice, e); + } + } + + if (!extras.isEmpty()) { + submitAlarm("清理多余挂单", + String.format("仅保留最靠近现价的买单 %s,已撤销 %d 笔", + formatPrice(keep.buyPrice), extras.size()), + AlarmLevel.INFO); + } + } + } + private void fetchCurrentPriceFromRest() { try { JSONObject ticker = bitGetClient.getTicker(getSymbol()); @@ -772,9 +831,9 @@ public class AutoGridStrategy extends BaseStrategy { sb.append("

当前价:").append(formatPrice(currentPrice)).append("
"); sb.append("
ATR:").append(formatPrice(currentAtr)).append("
"); sb.append("
网格间距:").append(formatPrice(currentGridGap)).append("
"); - sb.append("
待成交买单:").append(pendingBuyOrders.size()) - .append(" / ").append(gridCount).append(" 笔
"); - sb.append("
持仓待止盈:").append(openPositions.size()).append(" 笔
"); + sb.append("
待成交买单:").append(pendingBuyOrders.size()).append(" / 1 笔
"); + sb.append("
持仓待止盈:").append(openPositions.size()) + .append(" / ").append(gridCount).append(" 层
"); sb.append("
上限暂停:").append(upperBoundPaused ? "是" : "否").append("
"); sb.append("
下限暂停:").append(lowerBoundPaused ? "是" : "否").append("
"); @@ -858,13 +917,6 @@ public class AutoGridStrategy extends BaseStrategy { return orderInfo; } - private BigDecimal findLowestPendingBuyPrice() { - return pendingBuyOrders.values().stream() - .map(o -> o.buyPrice) - .min(BigDecimal::compareTo) - .orElse(null); - } - private boolean hasBuyOrderAt(BigDecimal price) { return pendingBuyOrders.containsKey(priceKey(normalizePrice(price))); }