网格挂单,一单成交才挂下一个
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.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);
|
||||||
|
|
||||||
|
|||||||
@ -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)));
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user