网格挂单,一单成交才挂下一个

This commit is contained in:
tony 2026-07-08 23:11:52 +08:00
parent 3aa332a210
commit c6f90843f5
2 changed files with 91 additions and 33 deletions

View File

@ -4,12 +4,18 @@ import vip.uuquant.exhangeapi.core.ExchangeFactory;
import vip.uuquant.exhangeapi.core.HttpClient; import vip.uuquant.exhangeapi.core.HttpClient;
import vip.uuquant.exhangeapi.core.WebSocketManager; import vip.uuquant.exhangeapi.core.WebSocketManager;
import java.net.InetSocketAddress;
import java.net.Proxy;
/** /**
* 交易所客户端全局单例 * 交易所客户端全局单例
*/ */
public class CommonConstant { 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); public static final WebSocketManager webSocketManager = new WebSocketManager(httpClient);

View File

@ -32,6 +32,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
* 基于 ATR 的动态网格策略(只做多)。 * 基于 ATR 的动态网格策略(只做多)。
* <p> * <p>
* 网格间距 = atr_multiplier × ATR,并限制在 [min_gap, max_gap] 区间。 * 网格间距 = atr_multiplier × ATR,并限制在 [min_gap, max_gap] 区间。
* 下方买单始终最多挂 1 笔:成交后再挂下一格,不会批量铺满网格。
* 采用限价挂单买入,市价止盈卖出。 * 采用限价挂单买入,市价止盈卖出。
* 网格间距变化时不撤销已有挂单,已成交仓位按最新间距计算止盈价。 * 网格间距变化时不撤销已有挂单,已成交仓位按最新间距计算止盈价。
*/ */
@ -54,7 +55,7 @@ public class AutoGridStrategy extends BaseStrategy {
private boolean testMode; private boolean testMode;
private int leverage; private int leverage;
/** /**
* 最大待成交买单数量 * 最大持仓层数;达到后不再新建买单。待成交买单始终最多 1 笔。
*/ */
private int gridCount; private int gridCount;
@ -246,6 +247,7 @@ public class AutoGridStrategy extends BaseStrategy {
refreshGridGap(true); refreshGridGap(true);
syncOpenOrdersFromExchange(); syncOpenOrdersFromExchange();
pruneExcessPendingBuys();
placeInitialBuyGrids(); placeInitialBuyGrids();
createTask(this::syncAccount, 5, 600); createTask(this::syncAccount, 5, 600);
@ -299,7 +301,7 @@ public class AutoGridStrategy extends BaseStrategy {
bitGetClient.changeTestMode(testMode); bitGetClient.changeTestMode(testMode);
AccountVO account = getAccount(); AccountVO account = getAccount();
bitGetClient.init(account.getApiKey(), account.getSecretKey(), account.getPassphrase(), bitGetClient.init(account.getApiKey(), account.getSecretKey(), account.getPassphrase(),
null, CommonConstant.webSocketManager); CommonConstant.httpClient, CommonConstant.webSocketManager);
// try { // try {
// bitGetClient.setLeverage(getSymbol(), leverage); // bitGetClient.setLeverage(getSymbol(), leverage);
// } catch (Exception e) { // } catch (Exception e) {
@ -357,19 +359,14 @@ public class AutoGridStrategy extends BaseStrategy {
private BigDecimal fetchAtr() throws Exception { private BigDecimal fetchAtr() throws Exception {
JSONObject params = new JSONObject(); JSONObject params = new JSONObject();
params.put("symbol", getPlainSymbol()); params.put("symbol", getPlainSymbol());
params.put("granularity", normalizeKlineInterval(klineInterval)); params.put("granularity", klineInterval);
params.put("limit", String.valueOf(klinePeriod + 2)); params.put("limit", String.valueOf(klinePeriod + 2));
JSONArray klines = bitGetClient.getKLines(params); JSONArray klines = bitGetClient.getKLines(params);
if (klines == null || klines.isEmpty()) { if (klines == null || klines.isEmpty()) {
return null; return null;
} }
// BitGet 返回倒序,转为升序 return AtrCalculator.calculate(klines, klinePeriod);
JSONArray sorted = new JSONArray();
for (int i = klines.size() - 1; i >= 0; i--) {
sorted.add(klines.getJSONArray(i));
}
return AtrCalculator.calculate(sorted, klinePeriod);
} }
private String normalizeKlineInterval(String interval) { private String normalizeKlineInterval(String interval) {
@ -418,7 +415,7 @@ public class AutoGridStrategy extends BaseStrategy {
// ======================== 网格挂单 ======================== // ======================== 网格挂单 ========================
/** /**
* 初始化仅挂下方一格买单,与运行中补单逻辑一致,避免网格间距调整后批量挂单失配。 * 初始化仅挂下方一格买单,与运行中补单逻辑一致。
*/ */
private void placeInitialBuyGrids() { private void placeInitialBuyGrids() {
if (currentPrice == null || currentGridGap == null) { if (currentPrice == null || currentGridGap == null) {
@ -427,25 +424,22 @@ public class AutoGridStrategy extends BaseStrategy {
replenishBuyGrids(); replenishBuyGrids();
} }
/**
* 无待成交买单时,在现价下方挂一格;已有挂单则不补,保证始终只有 1 笔买单。
*/
private void replenishBuyGrids() { private void replenishBuyGrids() {
if (!canPlaceBuyOrders() || currentGridGap == null) { if (!canPlaceBuyOrders() || currentGridGap == null || currentPrice == null) {
return; return;
} }
synchronized (tradeLock) { synchronized (tradeLock) {
if (pendingBuyOrders.size() >= gridCount) { if (!pendingBuyOrders.isEmpty() || openPositions.size() >= gridCount) {
return; return;
} }
BigDecimal lowestPending = findLowestPendingBuyPrice(); BigDecimal nextBuyPrice = alignPriceDown(currentPrice, currentGridGap);
BigDecimal nextBuyPrice; if (nextBuyPrice.compareTo(currentPrice) >= 0) {
nextBuyPrice = nextBuyPrice.subtract(currentGridGap);
if (lowestPending != null) {
nextBuyPrice = lowestPending.subtract(currentGridGap);
} else if (currentPrice != null) {
nextBuyPrice = alignPriceDown(currentPrice, currentGridGap);
} else {
return;
} }
if (nextBuyPrice.compareTo(lowerBound) >= 0 && !hasBuyOrderAt(nextBuyPrice)) { 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) { private void placeLimitBuyOrder(BigDecimal price) {
if (pendingBuyOrders.size() >= gridCount) { if (!pendingBuyOrders.isEmpty() || openPositions.size() >= gridCount) {
return; return;
} }
BigDecimal normalizedPrice = normalizePrice(price); BigDecimal normalizedPrice = normalizePrice(price);
@ -613,6 +627,9 @@ public class AutoGridStrategy extends BaseStrategy {
submitOrderSync(orderId, clientPositionId, normalizedPrice, volume, OrderSide.BUY, OrderStatus.FILLED); submitOrderSync(orderId, clientPositionId, normalizedPrice, volume, OrderSide.BUY, OrderStatus.FILLED);
submitPositionSync(clientPositionId, normalizedPrice, volume, true); submitPositionSync(clientPositionId, normalizedPrice, volume, true);
// 成交后再挂下方一格,保证永远只有一笔待成交买单
placeNextBuyBelow(normalizedPrice);
} }
private void onSellFilled(BigDecimal price, BigDecimal volume, String orderId) { 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<GridBuyOrder> 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() { private void fetchCurrentPriceFromRest() {
try { try {
JSONObject ticker = bitGetClient.getTicker(getSymbol()); JSONObject ticker = bitGetClient.getTicker(getSymbol());
@ -772,9 +831,9 @@ public class AutoGridStrategy extends BaseStrategy {
sb.append("<div>当前价:").append(formatPrice(currentPrice)).append("</div>"); sb.append("<div>当前价:").append(formatPrice(currentPrice)).append("</div>");
sb.append("<div>ATR:").append(formatPrice(currentAtr)).append("</div>"); sb.append("<div>ATR:").append(formatPrice(currentAtr)).append("</div>");
sb.append("<div>网格间距:").append(formatPrice(currentGridGap)).append("</div>"); sb.append("<div>网格间距:").append(formatPrice(currentGridGap)).append("</div>");
sb.append("<div>待成交买单:").append(pendingBuyOrders.size()) sb.append("<div>待成交买单:").append(pendingBuyOrders.size()).append(" / 1 笔</div>");
.append(" / ").append(gridCount).append(" 笔</div>"); sb.append("<div>持仓待止盈:").append(openPositions.size())
sb.append("<div>持仓待止盈:").append(openPositions.size()).append(" 笔</div>"); .append(" / ").append(gridCount).append(" 层</div>");
sb.append("<div>上限暂停:").append(upperBoundPaused ? "是" : "否").append("</div>"); sb.append("<div>上限暂停:").append(upperBoundPaused ? "是" : "否").append("</div>");
sb.append("<div>下限暂停:").append(lowerBoundPaused ? "是" : "否").append("</div>"); sb.append("<div>下限暂停:").append(lowerBoundPaused ? "是" : "否").append("</div>");
@ -858,13 +917,6 @@ public class AutoGridStrategy extends BaseStrategy {
return orderInfo; return orderInfo;
} }
private BigDecimal findLowestPendingBuyPrice() {
return pendingBuyOrders.values().stream()
.map(o -> o.buyPrice)
.min(BigDecimal::compareTo)
.orElse(null);
}
private boolean hasBuyOrderAt(BigDecimal price) { private boolean hasBuyOrderAt(BigDecimal price) {
return pendingBuyOrders.containsKey(priceKey(normalizePrice(price))); return pendingBuyOrders.containsKey(priceKey(normalizePrice(price)));
} }