网格卖单改为挂单成交
This commit is contained in:
parent
8e7e1075f7
commit
08c5559cf6
2
pom.xml
2
pom.xml
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<groupId>com.sample</groupId>
|
<groupId>com.sample</groupId>
|
||||||
<artifactId>trend-grid-strategy</artifactId>
|
<artifactId>trend-grid-strategy</artifactId>
|
||||||
<version>1.0.0-SNAPSHOT</version>
|
<version>1.0.1-SNAPSHOT</version>
|
||||||
<name>trend-grid-strategy</name>
|
<name>trend-grid-strategy</name>
|
||||||
<description>趋势网格策略</description>
|
<description>趋势网格策略</description>
|
||||||
|
|
||||||
|
|||||||
@ -36,8 +36,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||||||
* 网格间距 = atr_multiplier × ATR,并限制在 [min_gap, max_gap] 区间。
|
* 网格间距 = atr_multiplier × ATR,并限制在 [min_gap, max_gap] 区间。
|
||||||
* 下方买单始终最多挂 1 笔:成交后再挂下一格,不会批量铺满网格。
|
* 下方买单始终最多挂 1 笔:成交后再挂下一格,不会批量铺满网格。
|
||||||
* 若现价相对挂单价上行超过间距 × 1.5,撤单并按现价重新挂靠下一格,避免买单悬空。
|
* 若现价相对挂单价上行超过间距 × 1.5,撤单并按现价重新挂靠下一格,避免买单悬空。
|
||||||
* 采用限价挂单买入,市价止盈卖出。
|
* 采用限价挂单买入,买单成交后在「入场价 + 间距」挂限价卖单止盈。
|
||||||
* 网格间距变化时不撤销已有挂单,已成交仓位按最新间距计算止盈价。
|
* 网格间距变化时不撤销已有挂单,已挂卖单价格保持不变。
|
||||||
*/
|
*/
|
||||||
public class AutoGridStrategy extends BaseStrategy {
|
public class AutoGridStrategy extends BaseStrategy {
|
||||||
|
|
||||||
@ -85,14 +85,14 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
* 待成交买单:key = 网格价格字符串
|
* 待成交买单:key = 网格价格字符串
|
||||||
*/
|
*/
|
||||||
private final Map<String, GridBuyOrder> pendingBuyOrders = new ConcurrentHashMap<>();
|
private final Map<String, GridBuyOrder> pendingBuyOrders = new ConcurrentHashMap<>();
|
||||||
|
/**
|
||||||
|
* 待成交卖单:key = clientPositionId
|
||||||
|
*/
|
||||||
|
private final Map<String, GridSellOrder> pendingSellOrders = new ConcurrentHashMap<>();
|
||||||
/**
|
/**
|
||||||
* 已成交待止盈的多头仓位
|
* 已成交待止盈的多头仓位
|
||||||
*/
|
*/
|
||||||
private final List<GridPosition> openPositions = new CopyOnWriteArrayList<>();
|
private final List<GridPosition> openPositions = new CopyOnWriteArrayList<>();
|
||||||
/**
|
|
||||||
* 正在平仓中的仓位,防止重复触发
|
|
||||||
*/
|
|
||||||
private final Map<String, Boolean> closingPositions = new ConcurrentHashMap<>();
|
|
||||||
/**
|
/**
|
||||||
* 已处理过的买单成交 orderId,避免 fill / orders 双推重复开仓补单
|
* 已处理过的买单成交 orderId,避免 fill / orders 双推重复开仓补单
|
||||||
*/
|
*/
|
||||||
@ -123,19 +123,35 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class GridSellOrder {
|
||||||
|
private final String clientOrderId;
|
||||||
|
private volatile String exchangeOrderId;
|
||||||
|
private final String clientPositionId;
|
||||||
|
private final BigDecimal sellPrice;
|
||||||
|
private final BigDecimal quantity;
|
||||||
|
|
||||||
|
GridSellOrder(String clientOrderId, String clientPositionId,
|
||||||
|
BigDecimal sellPrice, BigDecimal quantity) {
|
||||||
|
this.clientOrderId = clientOrderId;
|
||||||
|
this.clientPositionId = clientPositionId;
|
||||||
|
this.sellPrice = sellPrice;
|
||||||
|
this.quantity = quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class GridPosition {
|
private static class GridPosition {
|
||||||
private final String clientPositionId;
|
private final String clientPositionId;
|
||||||
private final BigDecimal entryPrice;
|
private final BigDecimal entryPrice;
|
||||||
private final BigDecimal quantity;
|
private final BigDecimal quantity;
|
||||||
|
/** 开仓时锁定的止盈价,不随 gridGap 变化 */
|
||||||
|
private final BigDecimal takeProfitPrice;
|
||||||
|
|
||||||
GridPosition(String clientPositionId, BigDecimal entryPrice, BigDecimal quantity) {
|
GridPosition(String clientPositionId, BigDecimal entryPrice, BigDecimal quantity,
|
||||||
|
BigDecimal takeProfitPrice) {
|
||||||
this.clientPositionId = clientPositionId;
|
this.clientPositionId = clientPositionId;
|
||||||
this.entryPrice = entryPrice;
|
this.entryPrice = entryPrice;
|
||||||
this.quantity = quantity;
|
this.quantity = quantity;
|
||||||
}
|
this.takeProfitPrice = takeProfitPrice;
|
||||||
|
|
||||||
BigDecimal takeProfitPrice(BigDecimal gridGap) {
|
|
||||||
return entryPrice.add(gridGap);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,6 +303,7 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
refreshGridGap(true);
|
refreshGridGap(true);
|
||||||
syncOpenOrdersFromExchange();
|
syncOpenOrdersFromExchange();
|
||||||
pruneExcessPendingBuys();
|
pruneExcessPendingBuys();
|
||||||
|
replenishMissingSellOrders();
|
||||||
placeInitialBuyGrids();
|
placeInitialBuyGrids();
|
||||||
|
|
||||||
createTask(this::syncAccount, 5, 600);
|
createTask(this::syncAccount, 5, 600);
|
||||||
@ -307,7 +324,7 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
try {
|
try {
|
||||||
checkPriceBounds();
|
checkPriceBounds();
|
||||||
refreshGridGap(false);
|
refreshGridGap(false);
|
||||||
processTakeProfit();
|
replenishMissingSellOrders();
|
||||||
rebaseStalePendingBuy();
|
rebaseStalePendingBuy();
|
||||||
replenishBuyGrids();
|
replenishBuyGrids();
|
||||||
updateDashboard();
|
updateDashboard();
|
||||||
@ -382,7 +399,7 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
if (oldGap != null && oldGap.compareTo(gap) != 0) {
|
if (oldGap != null && oldGap.compareTo(gap) != 0) {
|
||||||
logger.info("网格间距调整:{} -> {}(ATR={})", oldGap, gap, atr);
|
logger.info("网格间距调整:{} -> {}(ATR={})", oldGap, gap, atr);
|
||||||
submitAlarm("网格间距调整",
|
submitAlarm("网格间距调整",
|
||||||
String.format("间距 %s -> %s,ATR=%s,已有挂单保持不变,止盈按新间距计算",
|
String.format("间距 %s -> %s,ATR=%s,已有挂单及卖单价格保持不变",
|
||||||
formatPrice(oldGap), formatPrice(gap), formatPrice(atr)),
|
formatPrice(oldGap), formatPrice(gap), formatPrice(atr)),
|
||||||
AlarmLevel.INFO);
|
AlarmLevel.INFO);
|
||||||
}
|
}
|
||||||
@ -589,53 +606,60 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 止盈(市价平仓) ========================
|
// ======================== 限价卖单止盈 ========================
|
||||||
|
|
||||||
private void processTakeProfit() {
|
/**
|
||||||
if (currentGridGap == null || currentPrice == null) {
|
* 为缺少卖单的持仓补挂限价止盈单(启动恢复或卖单被撤时)。
|
||||||
|
*/
|
||||||
|
private void replenishMissingSellOrders() {
|
||||||
|
synchronized (tradeLock) {
|
||||||
|
for (GridPosition position : openPositions) {
|
||||||
|
if (!hasSellOrderFor(position.clientPositionId)) {
|
||||||
|
placeLimitSellOrder(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeLimitSellOrder(GridPosition position) {
|
||||||
|
if (hasSellOrderFor(position.clientPositionId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BigDecimal sellPrice = position.takeProfitPrice;
|
||||||
|
if (sellPrice.compareTo(upperBound) > 0) {
|
||||||
|
logger.warn("止盈价 {} 超过上限 {},跳过挂卖单", sellPrice, upperBound);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (GridPosition position : openPositions) {
|
String clientOrderId = buildClientOrderId("SELL", position.entryPrice);
|
||||||
BigDecimal tpPrice = position.takeProfitPrice(currentGridGap);
|
|
||||||
if (currentPrice.compareTo(tpPrice) < 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (closingPositions.putIfAbsent(position.clientPositionId, Boolean.TRUE) != null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
marketClosePosition(position, tpPrice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void marketClosePosition(GridPosition position, BigDecimal tpPrice) {
|
|
||||||
synchronized (tradeLock) {
|
|
||||||
try {
|
try {
|
||||||
JSONObject orderInfo = buildOrderRequest("sell", "close", "market", null,
|
JSONObject orderInfo = buildOrderRequest("sell", "close", "limit", sellPrice,
|
||||||
position.quantity, true);
|
position.quantity, true);
|
||||||
orderInfo.put("clientOid", buildClientOrderId("SELL", position.entryPrice));
|
orderInfo.put("clientOid", clientOrderId);
|
||||||
|
|
||||||
JSONObject result = bitGetClient.placeOrder(orderInfo);
|
JSONObject result = bitGetClient.placeOrder(orderInfo);
|
||||||
String exchangeOrderId = result.getString("orderId");
|
String exchangeOrderId = result.getString("orderId");
|
||||||
|
|
||||||
logger.info("市价止盈:入场={},止盈价={},当前价={},订单号={}",
|
GridSellOrder sellOrder = new GridSellOrder(clientOrderId, position.clientPositionId,
|
||||||
position.entryPrice, tpPrice, currentPrice, exchangeOrderId);
|
sellPrice, position.quantity);
|
||||||
|
sellOrder.exchangeOrderId = exchangeOrderId;
|
||||||
|
pendingSellOrders.put(position.clientPositionId, sellOrder);
|
||||||
|
|
||||||
submitAlarm("网格止盈",
|
logger.info("挂卖单成功:入场={},止盈价={},数量={},订单号={}",
|
||||||
String.format("入场 %s,止盈目标 %s,当前价 %s,市价平仓已提交",
|
position.entryPrice, sellPrice, position.quantity, exchangeOrderId);
|
||||||
formatPrice(position.entryPrice), formatPrice(tpPrice), formatPrice(currentPrice)),
|
submitOrderSync(exchangeOrderId, clientOrderId, sellPrice, position.quantity,
|
||||||
AlarmLevel.INFO);
|
|
||||||
|
|
||||||
submitOrderSync(exchangeOrderId, position.clientPositionId, currentPrice, position.quantity,
|
|
||||||
OrderSide.SELL, OrderStatus.NEW);
|
OrderSide.SELL, OrderStatus.NEW);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
closingPositions.remove(position.clientPositionId);
|
logger.error("挂卖单失败,入场价={},止盈价={}", position.entryPrice, sellPrice, e);
|
||||||
logger.error("市价止盈失败,入场价={}", position.entryPrice, e);
|
submitAlarm("挂卖单失败",
|
||||||
submitAlarm("止盈失败",
|
String.format("入场 %s,止盈 %s:%s",
|
||||||
String.format("入场 %s:%s", formatPrice(position.entryPrice), e.getMessage()),
|
formatPrice(position.entryPrice), formatPrice(sellPrice), e.getMessage()),
|
||||||
AlarmLevel.ERROR);
|
AlarmLevel.ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasSellOrderFor(String clientPositionId) {
|
||||||
|
return pendingSellOrders.containsKey(clientPositionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== WebSocket 事件处理 ========================
|
// ======================== WebSocket 事件处理 ========================
|
||||||
@ -671,7 +695,7 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
if (isOpenBuy(side, tradeSide)) {
|
if (isOpenBuy(side, tradeSide)) {
|
||||||
onBuyFilled(price, volume, orderId, clientOid, price);
|
onBuyFilled(price, volume, orderId, clientOid, price);
|
||||||
} else if (isCloseSell(side, tradeSide)) {
|
} else if (isCloseSell(side, tradeSide)) {
|
||||||
onSellFilled(price, volume, orderId);
|
onSellFilled(price, volume, orderId, clientOid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -695,6 +719,8 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
if ("cancelled".equals(status) || "canceled".equals(status)) {
|
if ("cancelled".equals(status) || "canceled".equals(status)) {
|
||||||
if ("buy".equals(side)) {
|
if ("buy".equals(side)) {
|
||||||
removePendingBuy(orderId, clientOid, orderPrice);
|
removePendingBuy(orderId, clientOid, orderPrice);
|
||||||
|
} else if ("sell".equals(side)) {
|
||||||
|
removePendingSell(orderId, clientOid, orderPrice);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -715,7 +741,7 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
if (isOpenBuy(side, tradeSide)) {
|
if (isOpenBuy(side, tradeSide)) {
|
||||||
onBuyFilled(fillPrice, volume, orderId, clientOid, orderPrice);
|
onBuyFilled(fillPrice, volume, orderId, clientOid, orderPrice);
|
||||||
} else if (isCloseSell(side, tradeSide)) {
|
} else if (isCloseSell(side, tradeSide)) {
|
||||||
onSellFilled(fillPrice, volume, orderId);
|
onSellFilled(fillPrice, volume, orderId, clientOid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -741,17 +767,26 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized (tradeLock) {
|
||||||
BigDecimal gridPrice = pending.buyPrice;
|
BigDecimal gridPrice = pending.buyPrice;
|
||||||
String clientPositionId = "POS_" + gridPrice.toPlainString();
|
String clientPositionId = "POS_" + gridPrice.toPlainString();
|
||||||
GridPosition position = new GridPosition(clientPositionId, gridPrice, volume);
|
if (currentGridGap == null) {
|
||||||
|
if (orderId != null) {
|
||||||
|
processedBuyFillOrderIds.remove(orderId);
|
||||||
|
}
|
||||||
|
logger.warn("网格间距未就绪,无法处理买单成交,orderId={}", orderId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal tpPrice = normalizePrice(gridPrice.add(currentGridGap));
|
||||||
|
GridPosition position = new GridPosition(clientPositionId, gridPrice, volume, tpPrice);
|
||||||
openPositions.add(position);
|
openPositions.add(position);
|
||||||
|
|
||||||
BigDecimal tpPrice = currentGridGap != null ? position.takeProfitPrice(currentGridGap) : null;
|
logger.info("买单成交:网格价={},成交价={},数量={},止盈价={},orderId={}",
|
||||||
logger.info("买单成交:网格价={},成交价={},数量={},止盈目标={},orderId={}",
|
|
||||||
gridPrice, fillPrice, volume, tpPrice, orderId);
|
gridPrice, fillPrice, volume, tpPrice, orderId);
|
||||||
|
|
||||||
submitAlarm("网格买入成交",
|
submitAlarm("网格买入成交",
|
||||||
String.format("买入价 %s,数量 %s,止盈目标 %s(间距 %s)",
|
String.format("买入价 %s,数量 %s,挂卖单止盈 %s(间距 %s)",
|
||||||
formatPrice(gridPrice), volume.toPlainString(),
|
formatPrice(gridPrice), volume.toPlainString(),
|
||||||
formatPrice(tpPrice), formatPrice(currentGridGap)),
|
formatPrice(tpPrice), formatPrice(currentGridGap)),
|
||||||
AlarmLevel.INFO);
|
AlarmLevel.INFO);
|
||||||
@ -759,42 +794,43 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
submitOrderSync(orderId, clientPositionId, gridPrice, volume, OrderSide.BUY, OrderStatus.FILLED);
|
submitOrderSync(orderId, clientPositionId, gridPrice, volume, OrderSide.BUY, OrderStatus.FILLED);
|
||||||
submitPositionSync(clientPositionId, gridPrice, volume, true);
|
submitPositionSync(clientPositionId, gridPrice, volume, true);
|
||||||
|
|
||||||
// 成交后再挂下方一格,保证永远只有一笔待成交买单
|
placeLimitSellOrder(position);
|
||||||
placeNextBuyBelow(gridPrice);
|
placeNextBuyBelow(gridPrice);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void onSellFilled(BigDecimal price, BigDecimal volume, String orderId) {
|
private void onSellFilled(BigDecimal price, BigDecimal volume, String orderId, String clientOid) {
|
||||||
if (orderId != null && !processedSellFillOrderIds.add(orderId)) {
|
if (orderId != null && !processedSellFillOrderIds.add(orderId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized (tradeLock) {
|
||||||
|
GridSellOrder pendingSell = removePendingSell(orderId, clientOid, null);
|
||||||
GridPosition matched = null;
|
GridPosition matched = null;
|
||||||
for (String closingId : closingPositions.keySet()) {
|
if (pendingSell != null) {
|
||||||
for (GridPosition position : openPositions) {
|
for (GridPosition position : openPositions) {
|
||||||
if (position.clientPositionId.equals(closingId)) {
|
if (position.clientPositionId.equals(pendingSell.clientPositionId)) {
|
||||||
matched = position;
|
matched = position;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matched != null) {
|
}
|
||||||
break;
|
if (matched == null && clientOid != null) {
|
||||||
}
|
BigDecimal entryPrice = parseEntryPriceFromClientOid(clientOid);
|
||||||
}
|
if (entryPrice != null) {
|
||||||
if (matched == null && volume != null) {
|
String posId = "POS_" + entryPrice.toPlainString();
|
||||||
for (GridPosition position : openPositions) {
|
for (GridPosition position : openPositions) {
|
||||||
if (position.quantity.compareTo(volume) == 0) {
|
if (position.clientPositionId.equals(posId)) {
|
||||||
matched = position;
|
matched = position;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matched == null && !openPositions.isEmpty()) {
|
|
||||||
matched = openPositions.get(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matched != null) {
|
if (matched != null) {
|
||||||
openPositions.remove(matched);
|
openPositions.remove(matched);
|
||||||
closingPositions.remove(matched.clientPositionId);
|
pendingSellOrders.remove(matched.clientPositionId);
|
||||||
submitPositionSync(matched.clientPositionId, price, matched.quantity, false);
|
submitPositionSync(matched.clientPositionId, price, matched.quantity, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -806,9 +842,9 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
submitOrderSync(orderId, matched != null ? matched.clientPositionId : "CLOSE", price, volume,
|
submitOrderSync(orderId, matched != null ? matched.clientPositionId : "CLOSE", price, volume,
|
||||||
OrderSide.SELL, OrderStatus.FILLED);
|
OrderSide.SELL, OrderStatus.FILLED);
|
||||||
|
|
||||||
// 成交后补充下一格买单
|
|
||||||
replenishBuyGrids();
|
replenishBuyGrids();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveSymbol(JSONObject payload) {
|
private String resolveSymbol(JSONObject payload) {
|
||||||
String symbol = payload.getString("symbol");
|
String symbol = payload.getString("symbol");
|
||||||
@ -881,6 +917,68 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GridSellOrder removePendingSell(String orderId, String clientOid, BigDecimal price) {
|
||||||
|
if (orderId != null) {
|
||||||
|
for (Iterator<Map.Entry<String, GridSellOrder>> it = pendingSellOrders.entrySet().iterator();
|
||||||
|
it.hasNext(); ) {
|
||||||
|
Map.Entry<String, GridSellOrder> entry = it.next();
|
||||||
|
GridSellOrder order = entry.getValue();
|
||||||
|
if (orderId.equals(order.exchangeOrderId)) {
|
||||||
|
it.remove();
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (clientOid != null) {
|
||||||
|
for (Iterator<Map.Entry<String, GridSellOrder>> it = pendingSellOrders.entrySet().iterator();
|
||||||
|
it.hasNext(); ) {
|
||||||
|
Map.Entry<String, GridSellOrder> entry = it.next();
|
||||||
|
GridSellOrder order = entry.getValue();
|
||||||
|
if (clientOid.equals(order.clientOrderId)) {
|
||||||
|
it.remove();
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (price != null) {
|
||||||
|
BigDecimal normalized = normalizePrice(price);
|
||||||
|
for (Iterator<Map.Entry<String, GridSellOrder>> it = pendingSellOrders.entrySet().iterator();
|
||||||
|
it.hasNext(); ) {
|
||||||
|
Map.Entry<String, GridSellOrder> entry = it.next();
|
||||||
|
GridSellOrder order = entry.getValue();
|
||||||
|
if (normalized.compareTo(order.sellPrice) == 0) {
|
||||||
|
it.remove();
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 clientOid(SELL_{symbol}_{entryPrice}_{ts})解析入场价。
|
||||||
|
*/
|
||||||
|
private BigDecimal parseEntryPriceFromClientOid(String clientOid) {
|
||||||
|
if (clientOid == null || !clientOid.startsWith("SELL_")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String plainSymbol = getPlainSymbol();
|
||||||
|
String prefix = "SELL_" + plainSymbol + "_";
|
||||||
|
if (!clientOid.startsWith(prefix)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String remainder = clientOid.substring(prefix.length());
|
||||||
|
int lastUnderscore = remainder.lastIndexOf('_');
|
||||||
|
if (lastUnderscore <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return normalizePrice(new BigDecimal(remainder.substring(0, lastUnderscore)));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ======================== 交易所同步 ========================
|
// ======================== 交易所同步 ========================
|
||||||
|
|
||||||
private void syncOpenOrdersFromExchange() {
|
private void syncOpenOrdersFromExchange() {
|
||||||
@ -889,29 +987,63 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
if (openOrders == null) {
|
if (openOrders == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
int buyCount = 0;
|
||||||
|
int sellCount = 0;
|
||||||
for (int i = 0; i < openOrders.size(); i++) {
|
for (int i = 0; i < openOrders.size(); i++) {
|
||||||
JSONObject order = openOrders.getJSONObject(i);
|
JSONObject order = openOrders.getJSONObject(i);
|
||||||
if (!"buy".equals(order.getString("side"))) {
|
String side = order.getString("side");
|
||||||
continue;
|
String tradeSide = order.getString("tradeSide");
|
||||||
}
|
|
||||||
if (!"open".equals(order.getString("tradeSide"))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
BigDecimal price = normalizePrice(order.getBigDecimal("price"));
|
BigDecimal price = normalizePrice(order.getBigDecimal("price"));
|
||||||
String clientOid = order.getString("clientOid");
|
String clientOid = order.getString("clientOid");
|
||||||
|
String exchangeOrderId = order.getString("orderId");
|
||||||
|
BigDecimal quantity = firstPositive(
|
||||||
|
order.getBigDecimal("size"),
|
||||||
|
order.getBigDecimal("baseVolume"));
|
||||||
|
|
||||||
|
if ("buy".equals(side) && "open".equals(tradeSide)) {
|
||||||
if (clientOid == null) {
|
if (clientOid == null) {
|
||||||
clientOid = "SYNC_" + price.toPlainString();
|
clientOid = "SYNC_BUY_" + price.toPlainString();
|
||||||
}
|
}
|
||||||
GridBuyOrder gridBuyOrder = new GridBuyOrder(clientOid, price);
|
GridBuyOrder gridBuyOrder = new GridBuyOrder(clientOid, price);
|
||||||
gridBuyOrder.exchangeOrderId = order.getString("orderId");
|
gridBuyOrder.exchangeOrderId = exchangeOrderId;
|
||||||
pendingBuyOrders.put(priceKey(price), gridBuyOrder);
|
pendingBuyOrders.put(priceKey(price), gridBuyOrder);
|
||||||
|
buyCount++;
|
||||||
|
} else if ("sell".equals(side) && "close".equals(tradeSide)) {
|
||||||
|
if (clientOid == null) {
|
||||||
|
clientOid = "SYNC_SELL_" + price.toPlainString();
|
||||||
}
|
}
|
||||||
logger.info("同步挂单 {} 笔", pendingBuyOrders.size());
|
BigDecimal entryPrice = parseEntryPriceFromClientOid(clientOid);
|
||||||
|
String clientPositionId = entryPrice != null
|
||||||
|
? "POS_" + entryPrice.toPlainString()
|
||||||
|
: "SYNC_POS_" + price.toPlainString();
|
||||||
|
if (quantity == null) {
|
||||||
|
quantity = baseQuantity;
|
||||||
|
}
|
||||||
|
GridSellOrder sellOrder = new GridSellOrder(clientOid, clientPositionId, price, quantity);
|
||||||
|
sellOrder.exchangeOrderId = exchangeOrderId;
|
||||||
|
pendingSellOrders.put(clientPositionId, sellOrder);
|
||||||
|
sellCount++;
|
||||||
|
|
||||||
|
if (entryPrice != null && !hasOpenPosition(clientPositionId)) {
|
||||||
|
openPositions.add(new GridPosition(clientPositionId, entryPrice, quantity, price));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info("同步挂单:买单 {} 笔,卖单 {} 笔", buyCount, sellCount);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("同步挂单失败:{}", e.getMessage());
|
logger.warn("同步挂单失败:{}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasOpenPosition(String clientPositionId) {
|
||||||
|
for (GridPosition position : openPositions) {
|
||||||
|
if (position.clientPositionId.equals(clientPositionId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 若交易所上残留多笔买单,只保留最靠近现价(价格最高)的一笔,撤销其余。
|
* 若交易所上残留多笔买单,只保留最靠近现价(价格最高)的一笔,撤销其余。
|
||||||
*/
|
*/
|
||||||
@ -1039,6 +1171,7 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
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()).append(" / 1 笔</div>");
|
sb.append("<div>待成交买单:").append(pendingBuyOrders.size()).append(" / 1 笔</div>");
|
||||||
|
sb.append("<div>待成交卖单:").append(pendingSellOrders.size()).append(" 笔</div>");
|
||||||
sb.append("<div>持仓待止盈:").append(openPositions.size())
|
sb.append("<div>持仓待止盈:").append(openPositions.size())
|
||||||
.append(" / ").append(gridCount).append(" 层</div>");
|
.append(" / ").append(gridCount).append(" 层</div>");
|
||||||
sb.append("<div>上限暂停:").append(upperBoundPaused ? "是" : "否").append("</div>");
|
sb.append("<div>上限暂停:").append(upperBoundPaused ? "是" : "否").append("</div>");
|
||||||
@ -1069,6 +1202,17 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
rows.add(row);
|
rows.add(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<GridSellOrder> pendingSells = new ArrayList<>(pendingSellOrders.values());
|
||||||
|
pendingSells.sort(Comparator.comparing(o -> o.sellPrice, Comparator.reverseOrder()));
|
||||||
|
for (GridSellOrder order : pendingSells) {
|
||||||
|
JSONObject row = new JSONObject();
|
||||||
|
row.put("type", "待成交卖单");
|
||||||
|
row.put("price", formatPrice(order.sellPrice));
|
||||||
|
row.put("quantity", order.quantity.toPlainString());
|
||||||
|
row.put("takeProfit", formatPrice(order.sellPrice));
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
|
||||||
List<GridPosition> positions = new ArrayList<>(openPositions);
|
List<GridPosition> positions = new ArrayList<>(openPositions);
|
||||||
positions.sort(Comparator.comparing(p -> p.entryPrice, Comparator.reverseOrder()));
|
positions.sort(Comparator.comparing(p -> p.entryPrice, Comparator.reverseOrder()));
|
||||||
for (GridPosition position : positions) {
|
for (GridPosition position : positions) {
|
||||||
@ -1076,7 +1220,7 @@ public class AutoGridStrategy extends BaseStrategy {
|
|||||||
row.put("type", "持仓");
|
row.put("type", "持仓");
|
||||||
row.put("price", formatPrice(position.entryPrice));
|
row.put("price", formatPrice(position.entryPrice));
|
||||||
row.put("quantity", position.quantity.toPlainString());
|
row.put("quantity", position.quantity.toPlainString());
|
||||||
row.put("takeProfit", formatPrice(position.takeProfitPrice(currentGridGap)));
|
row.put("takeProfit", formatPrice(position.takeProfitPrice));
|
||||||
rows.add(row);
|
rows.add(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user