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

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.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);

View File

@ -32,6 +32,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
* 基于 ATR 的动态网格策略(只做多)。
* <p>
* 网格间距 = 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<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() {
try {
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>ATR:").append(formatPrice(currentAtr)).append("</div>");
sb.append("<div>网格间距:").append(formatPrice(currentGridGap)).append("</div>");
sb.append("<div>待成交买单:").append(pendingBuyOrders.size())
.append(" / ").append(gridCount).append(" 笔</div>");
sb.append("<div>持仓待止盈:").append(openPositions.size()).append(" 笔</div>");
sb.append("<div>待成交买单:").append(pendingBuyOrders.size()).append(" / 1 笔</div>");
sb.append("<div>持仓待止盈:").append(openPositions.size())
.append(" / ").append(gridCount).append(" 层</div>");
sb.append("<div>上限暂停:").append(upperBoundPaused ? "是" : "否").append("</div>");
sb.append("<div>下限暂停:").append(lowerBoundPaused ? "是" : "否").append("</div>");
@ -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)));
}