网格挂单,一单成交才挂下一个
This commit is contained in:
parent
3aa332a210
commit
c6f90843f5
@ -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);
|
||||
|
||||
|
||||
@ -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)));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user