package com.sample.trend.strategy; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.hengyi.xbaseweb.common.result.ApiResult; import com.sample.trend.config.CommonConstant; import com.sample.trend.strategy.util.AtrCalculator; import com.sample.trend.strategy.util.ScriptUtil; import okhttp3.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import vip.uuquant.exhangeapi.core.WebSocketConnectionStatus; import vip.uuquant.exhangeapi.core.WebSocketMessageHandler; import vip.uuquant.exhangeapi.enums.*; import vip.uuquant.exhangeapi.impl.bitget.BitGetClient; import vip.uuquant.tradesystem.runner.enums.AlarmLevel; import vip.uuquant.tradesystem.runner.enums.RobotChartType; import vip.uuquant.tradesystem.runner.strategy.BaseStrategy; import vip.uuquant.tradesystem.runner.vo.AccountVO; import vip.uuquant.tradesystem.runner.vo.SymbolVO; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; /** * 基于 ATR 的动态网格策略(只做多)。 *

* 网格间距 = atr_multiplier × ATR,并限制在 [min_gap, max_gap] 区间。 * 下方买单始终最多挂 1 笔:成交后再挂下一格,不会批量铺满网格。 * 若现价相对挂单价上行超过间距 × 1.5,撤单并按现价重新挂靠下一格,避免买单悬空。 * 采用限价挂单买入,买单成交后在「入场价 + 间距」挂限价卖单止盈。 * 网格间距变化时不撤销已有挂单,已挂卖单价格保持不变。 */ public class AutoGridStrategy extends BaseStrategy { private static final String MARGIN_MODE = "crossed"; /** * 现价高于挂单价超过「间距 × 该倍数」时,撤单重挂 */ private static final BigDecimal BUY_REBASE_MULTIPLIER = new BigDecimal("1.5"); private final Logger logger = LoggerFactory.getLogger(AutoGridStrategy.class); // 运行参数 private BigDecimal baseQuantity; private BigDecimal atrMultiplier; private String gridGrpScript; private String klineInterval; private int klinePeriod; private int gridAdjustIntervalMinutes; private BigDecimal minGap; private BigDecimal maxGap; private BigDecimal upperBound; private BigDecimal lowerBound; private boolean testMode; private int leverage; /** * 最大持仓层数;达到后不再新建买单。待成交买单始终最多 1 笔。 */ private int gridCount; // 精度 private int pricePrecision = 2; private int quantityPrecision = 4; // 交易所 private BitGetClient bitGetClient; // 网格状态 private volatile BigDecimal currentPrice; private volatile BigDecimal currentAtr; private volatile BigDecimal currentGridGap; private volatile long lastGridAdjustTime; private volatile boolean upperBoundPaused; private volatile boolean lowerBoundPaused; /** * 待成交买单:key = 网格价格字符串 */ private final Map pendingBuyOrders = new ConcurrentHashMap<>(); /** * 待成交卖单:key = clientPositionId */ private final Map pendingSellOrders = new ConcurrentHashMap<>(); /** * 已成交待止盈的多头仓位 */ private final List openPositions = new CopyOnWriteArrayList<>(); /** * 已处理过的买单成交 orderId,避免 fill / orders 双推重复开仓补单 */ private final Set processedBuyFillOrderIds = ConcurrentHashMap.newKeySet(); /** * 已处理过的卖单成交 orderId */ private final Set processedSellFillOrderIds = ConcurrentHashMap.newKeySet(); private final Object tradeLock = new Object(); // 图表 private Integer statusChart; private Integer gridTableChart; private Long robotId; // ======================== 内部模型 ======================== private static class GridBuyOrder { private final String clientOrderId; private volatile String exchangeOrderId; private final BigDecimal buyPrice; GridBuyOrder(String clientOrderId, BigDecimal buyPrice) { this.clientOrderId = clientOrderId; this.buyPrice = buyPrice; } } 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 final String clientPositionId; private final BigDecimal entryPrice; private final BigDecimal quantity; /** 开仓时锁定的止盈价,不随 gridGap 变化 */ private final BigDecimal takeProfitPrice; GridPosition(String clientPositionId, BigDecimal entryPrice, BigDecimal quantity, BigDecimal takeProfitPrice) { this.clientPositionId = clientPositionId; this.entryPrice = entryPrice; this.quantity = quantity; this.takeProfitPrice = takeProfitPrice; } } // ======================== WebSocket ======================== private final WebSocketMessageHandler webSocketMessageHandler = new WebSocketMessageHandler() { @Override public void onOpen(String connectionId, Response response) { logger.info("WebSocket 连接成功:{}", connectionId); if (connectionId.equals(bitGetClient.getPublicConnectionId())) { bitGetClient.subscribeTicker(getSymbol()); } else { // 私有频道必须等 login 成功回调后再订阅,否则会报 30004 bitGetClient.login(); } } @Override public void onMessage(String connectionId, String message) { // logger.debug("WebSocket 收到消息:{}", message); if ("pong".equals(message)) { return; } try { JSONObject json = JSONObject.parseObject(message); // 登录成功后再订阅 fill / orders if ("login".equals(json.getString("event"))) { Integer code = json.getInteger("code"); if (code != null && code == 0) { logger.info("私有 WebSocket 登录成功,开始订阅成交与订单"); bitGetClient.subscribeFill(); bitGetClient.subscribeOrders(); } else { logger.error("私有 WebSocket 登录失败:{}", message); submitAlarm("WebSocket 登录失败", message, AlarmLevel.ERROR); } return; } if ("error".equals(json.getString("event"))) { logger.error("WebSocket 返回错误:{}", message); return; } if (!json.containsKey("action")) { return; } String action = json.getString("action"); JSONObject arg = json.getJSONObject("arg"); if (arg == null) { return; } String channel = arg.getString("channel"); JSONArray data = json.getJSONArray("data"); if (data == null || data.isEmpty()) { return; } if ("ticker".equals(channel)) { handleTicker(data.getJSONObject(0)); } else if ("fill".equals(channel) && ("snapshot".equals(action) || "update".equals(action))) { for (int i = 0; i < data.size(); i++) { handleFill(data.getJSONObject(i)); } } else if ("orders".equals(channel) && ("snapshot".equals(action) || "update".equals(action))) { for (int i = 0; i < data.size(); i++) { handleOrderUpdate(data.getJSONObject(i)); } } } catch (Exception e) { logger.error("WebSocket 消息处理异常", e); } } @Override public void onClosing(String connectionId, int code, String reason) { logger.warn("WebSocket 关闭中:{} {} {}", connectionId, code, reason); } @Override public void onClosed(String connectionId, int code, String reason) { logger.warn("WebSocket 已关闭:{} {} {}", connectionId, code, reason); } @Override public void onFailure(String connectionId, Throwable throwable, Response response) { logger.error("WebSocket 异常:{}", connectionId, throwable); } @Override public void onStatusChange(String connectionId, WebSocketConnectionStatus oldStatus, WebSocketConnectionStatus newStatus) { logger.info("WebSocket 状态变化:{} {} -> {}", connectionId, oldStatus, newStatus); } }; // ======================== 生命周期 ======================== @Override public void init(JSONObject context) { super.init(context); logger.info("ATR 动态网格策略初始化"); tryParams("base_quantity", "atr_multiplier", "grid_grp_script", "kline_interval", "kline_period", "grid_adjust_interval", "min_gap", "max_gap", "upper_bound", "lower_bound", "grid_count", "test_mode"); baseQuantity = getRunParams().getBigDecimal("base_quantity"); atrMultiplier = getRunParams().getBigDecimal("atr_multiplier"); gridGrpScript = getRunParams().getString("grid_grp_script"); klineInterval = getRunParams().getString("kline_interval"); klinePeriod = getRunParams().getInteger("kline_period"); gridAdjustIntervalMinutes = getRunParams().getInteger("grid_adjust_interval"); minGap = getRunParams().getBigDecimal("min_gap"); maxGap = getRunParams().getBigDecimal("max_gap"); upperBound = getRunParams().getBigDecimal("upper_bound"); lowerBound = getRunParams().getBigDecimal("lower_bound"); testMode = getRunParams().getBooleanValue("test_mode"); leverage = getRunParams().getIntValue("leverage"); if (leverage <= 0) { leverage = 10; } gridCount = getRunParams().getInteger("grid_count"); if (gridCount <= 0) { throw new IllegalArgumentException("grid_count 必须大于 0"); } if (lowerBound.compareTo(upperBound) >= 0) { throw new IllegalArgumentException("lower_bound 必须小于 upper_bound"); } SymbolVO symbolInfo = getSymbolInfo(); if (symbolInfo != null) { if (symbolInfo.getPricePrecision() != null) { pricePrecision = symbolInfo.getPricePrecision(); } if (symbolInfo.getQuantityPrecision() != null) { quantityPrecision = symbolInfo.getQuantityPrecision(); } } robotId = getRobotConfig().getRobotId(); initBitGetClient(); initCharts(); refreshGridGap(true); syncOpenOrdersFromExchange(); pruneExcessPendingBuys(); replenishMissingSellOrders(); placeInitialBuyGrids(); createTask(this::syncAccount, 5, 600); submitAlarm("策略初始化", String.format("ATR 动态网格策略启动,交易对:%s,初始网格间距:%s", getSymbol(), formatPrice(currentGridGap)), AlarmLevel.INFO); logger.info("策略初始化完成,网格间距:{},ATR:{}", currentGridGap, currentAtr); } @Override public void run() { if (currentPrice == null) { fetchCurrentPriceFromRest(); } if (currentPrice == null || currentGridGap == null) { return; } try { checkPriceBounds(); refreshGridGap(false); replenishMissingSellOrders(); rebaseStalePendingBuy(); replenishBuyGrids(); updateDashboard(); } catch (Exception e) { logger.error("策略运行异常", e); } } @Override public void notify(JSONObject data) { logger.info("收到平台通知:{}", data); } @Override public void destroy() { logger.info("策略销毁"); try { removeChart(null); removeAllTask(); CommonConstant.webSocketManager.closeAll(); submitAlarm("策略销毁", "ATR 动态网格策略已停止", AlarmLevel.WARNING); } catch (Exception e) { logger.error("策略销毁异常", e); } } // ======================== 初始化 ======================== private void initBitGetClient() { bitGetClient = CommonConstant.exchangeFactory.createExchange(ExchangeType.BITGET); bitGetClient.changeTestMode(testMode); AccountVO account = getAccount(); bitGetClient.init(account.getApiKey(), account.getSecretKey(), account.getPassphrase(), CommonConstant.httpClient, CommonConstant.webSocketManager); try { bitGetClient.createPublicWebsocket(webSocketMessageHandler); bitGetClient.createPrivateWebsocket(webSocketMessageHandler); } catch (Exception e) { throw new RuntimeException("BitGet WebSocket 连接失败", e); } } private void initCharts() { statusChart = createChart(RobotChartType.TEXT, "网格状态", 1, 1, 24); gridTableChart = createChart(RobotChartType.TABLE, "网格明细", 2, 2, 24); } // ======================== ATR 与网格间距 ======================== private void refreshGridGap(boolean force) { long now = System.currentTimeMillis(); long intervalMs = gridAdjustIntervalMinutes * 60_000L; if (!force && lastGridAdjustTime > 0 && now - lastGridAdjustTime < intervalMs) { return; } try { BigDecimal atr = fetchAtr(); if (atr == null) { logger.warn("ATR 计算失败,数据不足"); return; } Map scriptVariables = buildGridScriptVariables(atr); BigDecimal rawGap; try { rawGap = ScriptUtil.evalBigDecimal(gridGrpScript, scriptVariables); } catch (Exception scriptException) { logger.warn("网格表达式执行失败,回退默认公式:{}", scriptException.getMessage()); rawGap = null; } if (rawGap == null) { rawGap = atr.multiply(atrMultiplier); } BigDecimal gap = rawGap.max(minGap).min(maxGap).setScale(pricePrecision, RoundingMode.HALF_UP); BigDecimal oldGap = currentGridGap; currentAtr = atr; currentGridGap = gap; lastGridAdjustTime = now; if (oldGap != null && oldGap.compareTo(gap) != 0) { logger.info("网格间距调整:{} -> {}(ATR={})", oldGap, gap, atr); submitAlarm("网格间距调整", String.format("间距 %s -> %s,ATR=%s,已有挂单及卖单价格保持不变", formatPrice(oldGap), formatPrice(gap), formatPrice(atr)), AlarmLevel.INFO); } } catch (Exception e) { logger.error("刷新网格间距失败", e); } } private Map buildGridScriptVariables(BigDecimal atr) { Map variables = new java.util.HashMap<>(); variables.put("atr", atr == null ? null : atr.doubleValue()); variables.put("atrMultiplier", atrMultiplier == null ? null : atrMultiplier.doubleValue()); variables.put("baseQuantity", baseQuantity == null ? null : baseQuantity.doubleValue()); variables.put("minGap", minGap == null ? null : minGap.doubleValue()); variables.put("maxGap", maxGap == null ? null : maxGap.doubleValue()); variables.put("upperBound", upperBound == null ? null : upperBound.doubleValue()); variables.put("lowerBound", lowerBound == null ? null : lowerBound.doubleValue()); variables.put("gridCount", gridCount); variables.put("leverage", leverage); variables.put("pricePrecision", pricePrecision); variables.put("quantityPrecision", quantityPrecision); variables.put("klinePeriod", klinePeriod); variables.put("gridAdjustIntervalMinutes", gridAdjustIntervalMinutes); variables.put("testMode", testMode); variables.put("currentPrice", currentPrice == null ? null : currentPrice.doubleValue()); return variables; } private BigDecimal fetchAtr() throws Exception { JSONObject params = new JSONObject(); params.put("symbol", getPlainSymbol()); params.put("granularity", klineInterval); params.put("limit", String.valueOf(klinePeriod + 2)); JSONArray klines = bitGetClient.getKLines(params); if (klines == null || klines.isEmpty()) { return null; } return AtrCalculator.calculate(klines, klinePeriod); } // ======================== 价格边界 ======================== private void checkPriceBounds() { if (currentPrice == null) { return; } if (!upperBoundPaused && currentPrice.compareTo(upperBound) > 0) { upperBoundPaused = true; submitAlarm("价格超上限", String.format("当前价 %s 突破上限 %s,暂停新建买单", formatPrice(currentPrice), formatPrice(upperBound)), AlarmLevel.WARNING); } else if (upperBoundPaused && currentPrice.compareTo(upperBound) <= 0) { upperBoundPaused = false; submitAlarm("恢复交易", "价格回到上限以内,恢复新建买单", AlarmLevel.INFO); } if (!lowerBoundPaused && currentPrice.compareTo(lowerBound) < 0) { lowerBoundPaused = true; submitAlarm("价格破下限", String.format("当前价 %s 跌破下限 %s,暂停新建买单", formatPrice(currentPrice), formatPrice(lowerBound)), AlarmLevel.ERROR); } else if (lowerBoundPaused && currentPrice.compareTo(lowerBound) >= 0) { lowerBoundPaused = false; submitAlarm("恢复交易", "价格回到下限以上,恢复新建买单", AlarmLevel.INFO); } } private boolean canPlaceBuyOrders() { return !upperBoundPaused && !lowerBoundPaused; } // ======================== 网格挂单 ======================== /** * 初始化仅挂下方一格买单,与运行中补单逻辑一致。 */ private void placeInitialBuyGrids() { if (currentPrice == null || currentGridGap == null) { fetchCurrentPriceFromRest(); } replenishBuyGrids(); } /** * 无待成交买单时,在现价下方挂一格;已有挂单则不补,保证始终只有 1 笔买单。 */ private void replenishBuyGrids() { if (!canPlaceBuyOrders() || currentGridGap == null || currentPrice == null) { return; } synchronized (tradeLock) { if (!pendingBuyOrders.isEmpty() || openPositions.size() >= gridCount) { return; } BigDecimal nextBuyPrice = findNextAvailableBuyPrice(calcBuyPriceBelowCurrent()); if (nextBuyPrice != null) { placeLimitBuyOrder(nextBuyPrice); } } } /** * 价格单边上行时,下方买单会悬空永远不成交。 * 当 现价 - 挂单价 > 间距 × 1.5 时,撤单并按现价重新对齐挂单(例:1532 / 间距 5 → 1530)。 */ private void rebaseStalePendingBuy() { if (!canPlaceBuyOrders() || currentPrice == null || currentGridGap == null || currentGridGap.compareTo(BigDecimal.ZERO) <= 0) { return; } synchronized (tradeLock) { if (pendingBuyOrders.isEmpty()) { return; } GridBuyOrder pending = pendingBuyOrders.values().iterator().next(); BigDecimal distance = currentPrice.subtract(pending.buyPrice); BigDecimal threshold = currentGridGap.multiply(BUY_REBASE_MULTIPLIER); if (distance.compareTo(threshold) <= 0) { return; } BigDecimal newPrice = findNextAvailableBuyPrice(calcBuyPriceBelowCurrent()); if (newPrice == null || newPrice.compareTo(pending.buyPrice) == 0) { return; } try { bitGetClient.cancelOrder(getSymbol(), pending.exchangeOrderId, pending.clientOrderId, null); pendingBuyOrders.remove(priceKey(pending.buyPrice)); logger.info("买单距现价过远,撤单重挂:旧价={},现价={},间距={},阈值={},新价={}", pending.buyPrice, currentPrice, currentGridGap, threshold, newPrice); submitAlarm("买单追价重挂", String.format("旧挂单价 %s,现价 %s,超过间距×1.5(%s),重挂至 %s", formatPrice(pending.buyPrice), formatPrice(currentPrice), formatPrice(threshold), formatPrice(newPrice)), AlarmLevel.INFO); placeLimitBuyOrder(newPrice); } catch (Exception e) { logger.error("买单追价重挂失败,旧价={}", pending.buyPrice, e); submitAlarm("买单追价重挂失败", String.format("旧价 %s:%s", formatPrice(pending.buyPrice), e.getMessage()), AlarmLevel.ERROR); } } } /** * 现价向下对齐到网格,确保严格低于现价。 */ private BigDecimal calcBuyPriceBelowCurrent() { if (currentPrice == null || currentGridGap == null || currentGridGap.compareTo(BigDecimal.ZERO) <= 0) { return null; } BigDecimal nextBuyPrice = alignPriceDown(currentPrice, currentGridGap); if (nextBuyPrice.compareTo(currentPrice) >= 0) { nextBuyPrice = nextBuyPrice.subtract(currentGridGap); } return normalizePrice(nextBuyPrice); } /** * 买单成交后,挂「成交价再下一格」的限价买单。 */ private void placeNextBuyBelow(BigDecimal filledPrice) { if (!canPlaceBuyOrders() || currentGridGap == null || filledPrice == null) { return; } synchronized (tradeLock) { if (!pendingBuyOrders.isEmpty() || openPositions.size() >= gridCount) { return; } BigDecimal nextBuyPrice = findNextAvailableBuyPrice( normalizePrice(filledPrice.subtract(currentGridGap))); if (nextBuyPrice != null) { placeLimitBuyOrder(nextBuyPrice); } } } private void placeLimitBuyOrder(BigDecimal price) { if (!pendingBuyOrders.isEmpty() || openPositions.size() >= gridCount) { return; } BigDecimal normalizedPrice = normalizePrice(price); if (normalizedPrice.compareTo(lowerBound) < 0 || normalizedPrice.compareTo(upperBound) > 0) { return; } if (hasBuyOrderAt(normalizedPrice) || hasOpenPositionAtPrice(normalizedPrice)) { return; } String clientOrderId = buildClientOrderId("BUY", normalizedPrice); try { JSONObject orderInfo = buildOrderRequest("buy", "open", "limit", normalizedPrice, baseQuantity); orderInfo.put("clientOid", clientOrderId); JSONObject result = bitGetClient.placeOrder(orderInfo); String exchangeOrderId = result.getString("orderId"); GridBuyOrder gridBuyOrder = new GridBuyOrder(clientOrderId, normalizedPrice); gridBuyOrder.exchangeOrderId = exchangeOrderId; pendingBuyOrders.put(priceKey(normalizedPrice), gridBuyOrder); logger.info("挂买单成功:价格={},订单号={}", normalizedPrice, exchangeOrderId); submitOrderSync(exchangeOrderId, clientOrderId, normalizedPrice, baseQuantity, OrderSide.BUY, OrderStatus.NEW); } catch (Exception e) { logger.error("挂买单失败,价格={}", normalizedPrice, e); submitAlarm("挂买单失败", String.format("价格 %s:%s", formatPrice(normalizedPrice), e.getMessage()), AlarmLevel.ERROR); } } // ======================== 限价卖单止盈 ======================== /** * 为缺少卖单的持仓补挂限价止盈单(启动恢复或卖单被撤时)。 */ 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; } String clientOrderId = buildClientOrderId("SELL", position.entryPrice); try { JSONObject orderInfo = buildOrderRequest("buy", "close", "limit", sellPrice, position.quantity); orderInfo.put("clientOid", clientOrderId); JSONObject result = bitGetClient.placeOrder(orderInfo); String exchangeOrderId = result.getString("orderId"); GridSellOrder sellOrder = new GridSellOrder(clientOrderId, position.clientPositionId, sellPrice, position.quantity); sellOrder.exchangeOrderId = exchangeOrderId; pendingSellOrders.put(position.clientPositionId, sellOrder); logger.info("挂卖单成功:入场={},止盈价={},数量={},订单号={}", position.entryPrice, sellPrice, position.quantity, exchangeOrderId); submitOrderSync(exchangeOrderId, clientOrderId, sellPrice, position.quantity, OrderSide.SELL, OrderStatus.NEW); } catch (Exception e) { logger.error("挂卖单失败,入场价={},止盈价={}", position.entryPrice, sellPrice, e); submitAlarm("挂卖单失败", String.format("入场 %s,止盈 %s:%s", formatPrice(position.entryPrice), formatPrice(sellPrice), e.getMessage()), AlarmLevel.ERROR); } } private boolean hasSellOrderFor(String clientPositionId) { return pendingSellOrders.containsKey(clientPositionId); } // ======================== WebSocket 事件处理 ======================== private void handleTicker(JSONObject ticker) { String symbol = resolveSymbol(ticker); if (!getPlainSymbol().equals(symbol)) { return; } // BitGet ticker:优先 lastPr,兼容 last BigDecimal lastPrice = firstPositive( ticker.getBigDecimal("lastPr"), ticker.getBigDecimal("last"), ticker.getBigDecimal("markPrice")); if (lastPrice != null) { currentPrice = lastPrice; } } private void handleFill(JSONObject fill) { String symbol = resolveSymbol(fill); if (!getPlainSymbol().equals(symbol)) { return; } String side = fill.getString("side"); String tradeSide = fill.getString("tradeSide"); BigDecimal price = fill.getBigDecimal("price"); BigDecimal volume = fill.getBigDecimal("baseVolume"); String orderId = fill.getString("orderId"); String clientOid = fill.getString("clientOid"); if (isOpenBuy(side, tradeSide)) { onBuyFilled(price, volume, orderId, clientOid, price); } else if (isCloseBuy(side, tradeSide)) { onSellFilled(price, volume, orderId, clientOid); } } /** * BitGet orders 推送用 instId,fill 推送用 symbol。 * 成交以 status=filled 为可靠信号(fill 频道可能收不到)。 */ private void handleOrderUpdate(JSONObject order) { String symbol = resolveSymbol(order); if (!getPlainSymbol().equals(symbol)) { return; } String status = order.getString("status"); String side = order.getString("side"); String tradeSide = order.getString("tradeSide"); String orderId = order.getString("orderId"); String clientOid = order.getString("clientOid"); BigDecimal orderPrice = order.getBigDecimal("price"); if ("cancelled".equals(status) || "canceled".equals(status)) { if (isOpenBuy(side, tradeSide)) { removePendingBuy(orderId, clientOid, orderPrice); } else if (isCloseBuy(side, tradeSide)) { removePendingSell(orderId, clientOid, orderPrice); } return; } if (!"filled".equals(status)) { return; } BigDecimal fillPrice = firstPositive( order.getBigDecimal("priceAvg"), order.getBigDecimal("fillPrice"), orderPrice); BigDecimal volume = firstPositive( order.getBigDecimal("accBaseVolume"), order.getBigDecimal("baseVolume"), order.getBigDecimal("size")); if (isOpenBuy(side, tradeSide)) { onBuyFilled(fillPrice, volume, orderId, clientOid, orderPrice); } else if (isCloseBuy(side, tradeSide)) { onSellFilled(fillPrice, volume, orderId, clientOid); } } private void onBuyFilled(BigDecimal fillPrice, BigDecimal volume, String orderId, String clientOid, BigDecimal orderPrice) { if (orderId != null && !processedBuyFillOrderIds.add(orderId)) { return; } if (volume == null || volume.compareTo(BigDecimal.ZERO) <= 0) { if (orderId != null) { processedBuyFillOrderIds.remove(orderId); } logger.warn("买单成交数量无效,orderId={},volume={}", orderId, volume); return; } GridBuyOrder pending = removePendingBuy(orderId, clientOid, orderPrice); if (pending == null) { if (orderId != null) { processedBuyFillOrderIds.remove(orderId); } logger.debug("忽略非本策略买单成交,orderId={},clientOid={}", orderId, clientOid); return; } synchronized (tradeLock) { BigDecimal gridPrice = pending.buyPrice; if (hasOpenPositionAtPrice(gridPrice)) { if (orderId != null) { processedBuyFillOrderIds.remove(orderId); } logger.warn("该价位已有持仓,忽略重复买单成交,gridPrice={},orderId={}", gridPrice, orderId); submitAlarm("重复开仓拦截", String.format("价位 %s 已有持仓,忽略 orderId=%s", formatPrice(gridPrice), orderId), AlarmLevel.WARNING); return; } String clientPositionId = "POS_" + gridPrice.toPlainString(); 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); logger.info("买单成交:网格价={},成交价={},数量={},止盈价={},orderId={}", gridPrice, fillPrice, volume, tpPrice, orderId); submitAlarm("网格买入成交", String.format("买入价 %s,数量 %s,挂卖单止盈 %s(间距 %s)", formatPrice(gridPrice), volume.toPlainString(), formatPrice(tpPrice), formatPrice(currentGridGap)), AlarmLevel.INFO); submitOrderSync(orderId, clientPositionId, gridPrice, volume, OrderSide.BUY, OrderStatus.FILLED); submitPositionSync(clientPositionId, gridPrice, volume, true); placeLimitSellOrder(position); placeNextBuyBelow(gridPrice); } } private void onSellFilled(BigDecimal price, BigDecimal volume, String orderId, String clientOid) { if (orderId != null && !processedSellFillOrderIds.add(orderId)) { return; } synchronized (tradeLock) { GridSellOrder pendingSell = removePendingSell(orderId, clientOid, null); GridPosition matched = null; if (pendingSell != null) { for (GridPosition position : openPositions) { if (position.clientPositionId.equals(pendingSell.clientPositionId)) { matched = position; break; } } } if (matched == null && clientOid != null) { BigDecimal entryPrice = parseEntryPriceFromClientOid(clientOid); if (entryPrice != null) { String posId = "POS_" + entryPrice.toPlainString(); for (GridPosition position : openPositions) { if (position.clientPositionId.equals(posId)) { matched = position; break; } } } } if (matched != null) { openPositions.remove(matched); pendingSellOrders.remove(matched.clientPositionId); submitPositionSync(matched.clientPositionId, price, matched.quantity, false); } logger.info("卖单成交(止盈):价格={},数量={},orderId={}", price, volume, orderId); submitAlarm("网格止盈成交", String.format("卖出价 %s,数量 %s", formatPrice(price), volume != null ? volume.toPlainString() : "-"), AlarmLevel.INFO); submitOrderSync(orderId, matched != null ? matched.clientPositionId : "CLOSE", price, volume, OrderSide.SELL, OrderStatus.FILLED); replenishBuyGrids(); } } private String resolveSymbol(JSONObject payload) { String symbol = payload.getString("symbol"); if (symbol == null || symbol.isEmpty()) { symbol = payload.getString("instId"); } return symbol; } private boolean isOpenBuy(String side, String tradeSide) { if (!"buy".equals(side)) { return false; } return tradeSide == null || "open".equals(tradeSide) || "buy_single".equals(tradeSide); } private boolean isCloseBuy(String side, String tradeSide) { return "buy".equals(side) && "close".equals(tradeSide); } private BigDecimal firstPositive(BigDecimal... values) { if (values == null) { return null; } for (BigDecimal value : values) { if (value != null && value.compareTo(BigDecimal.ZERO) > 0) { return value; } } return null; } /** * 优先按 exchangeOrderId / clientOid 移除,避免成交价与挂单价不一致导致残留。 */ private GridBuyOrder removePendingBuy(String orderId, String clientOid, BigDecimal price) { if (orderId != null) { for (Iterator> it = pendingBuyOrders.entrySet().iterator(); it.hasNext(); ) { Map.Entry entry = it.next(); GridBuyOrder order = entry.getValue(); if (orderId.equals(order.exchangeOrderId)) { it.remove(); return order; } } } if (clientOid != null) { for (Iterator> it = pendingBuyOrders.entrySet().iterator(); it.hasNext(); ) { Map.Entry entry = it.next(); GridBuyOrder order = entry.getValue(); if (clientOid.equals(order.clientOrderId)) { it.remove(); return order; } } } if (price != null) { return pendingBuyOrders.remove(priceKey(normalizePrice(price))); } return null; } private GridSellOrder removePendingSell(String orderId, String clientOid, BigDecimal price) { if (orderId != null) { for (Iterator> it = pendingSellOrders.entrySet().iterator(); it.hasNext(); ) { Map.Entry entry = it.next(); GridSellOrder order = entry.getValue(); if (orderId.equals(order.exchangeOrderId)) { it.remove(); return order; } } } if (clientOid != null) { for (Iterator> it = pendingSellOrders.entrySet().iterator(); it.hasNext(); ) { Map.Entry 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> it = pendingSellOrders.entrySet().iterator(); it.hasNext(); ) { Map.Entry 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() { try { JSONArray openOrders = bitGetClient.getOpenOrders(getSymbol()); if (openOrders == null) { return; } int buyCount = 0; int sellCount = 0; for (int i = 0; i < openOrders.size(); i++) { JSONObject order = openOrders.getJSONObject(i); String side = order.getString("side"); String tradeSide = order.getString("tradeSide"); BigDecimal price = normalizePrice(order.getBigDecimal("price")); 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) { clientOid = "SYNC_BUY_" + price.toPlainString(); } GridBuyOrder gridBuyOrder = new GridBuyOrder(clientOid, price); gridBuyOrder.exchangeOrderId = exchangeOrderId; pendingBuyOrders.put(priceKey(price), gridBuyOrder); buyCount++; } else if ("buy".equals(side) && "close".equals(tradeSide)) { if (clientOid == null) { clientOid = "SYNC_SELL_" + price.toPlainString(); } 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) { logger.warn("同步挂单失败:{}", e.getMessage()); } } private boolean hasOpenPosition(String clientPositionId) { for (GridPosition position : openPositions) { if (position.clientPositionId.equals(clientPositionId)) { return true; } } return false; } /** * 若交易所上残留多笔买单,只保留最靠近现价(价格最高)的一笔,撤销其余。 */ 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 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()); if (ticker != null) { BigDecimal last = ticker.getBigDecimal("lastPr"); if (last == null) { last = ticker.getBigDecimal("last"); } currentPrice = last; } } catch (Exception e) { logger.warn("获取 ticker 失败:{}", e.getMessage()); } } private void syncAccount() { try { JSONArray balance = bitGetClient.getBalance(); for (int i = 0; i < balance.size(); i++) { JSONObject item = balance.getJSONObject(i); if ("USDT".equals(item.getString("marginCoin"))) { updateAccountBalance( item.getBigDecimal("accountEquity"), item.getBigDecimal("available"), item.getBigDecimal("locked")); } } } catch (Exception e) { logger.error("同步账户失败", e); } } // ======================== 平台数据同步 ======================== private void submitOrderSync(String orderId, String clientOrderId, BigDecimal price, BigDecimal qty, OrderSide side, OrderStatus status) { if (robotId == null) { return; } JSONObject orderInfo = new JSONObject(); orderInfo.put("robotId", robotId); orderInfo.put("clientOrderId", clientOrderId); orderInfo.put("orderId", orderId); orderInfo.put("price", price); orderInfo.put("origQty", qty); orderInfo.put("symbol", getSymbol()); orderInfo.put("leverage", leverage); orderInfo.put("side", side); orderInfo.put("positionSide", PositionSide.LONG); orderInfo.put("orderStatus", status); submitOrder(orderInfo); } private void submitPositionSync(String clientPositionId, BigDecimal price, BigDecimal qty, boolean open) { if (robotId == null) { return; } JSONObject positionInfo = new JSONObject(); positionInfo.put("robotId", robotId); positionInfo.put("clientPositionId", clientPositionId); positionInfo.put("symbol", getSymbol()); positionInfo.put("leverage", leverage); positionInfo.put("positionSide", PositionSide.LONG); positionInfo.put("marginType", MarginType.CROSSED); positionInfo.put("openAvgPrice", price); positionInfo.put("qty", qty); positionInfo.put("qtyUnit", QtyUnitType.COIN); positionInfo.put("positionStatus", open ? PositionStatus.POSITION : PositionStatus.CLOSED); submitPosition(positionInfo); } // ======================== 仪表盘 ======================== private void updateDashboard() { if (statusChart == null) { return; } StringBuilder sb = new StringBuilder(); sb.append("

时间:").append(LocalDateTime.now()).append("
"); sb.append("
当前价:").append(formatPrice(currentPrice)).append("
"); sb.append("
ATR:").append(formatPrice(currentAtr)).append("
"); sb.append("
网格间距:").append(formatPrice(currentGridGap)).append("
"); sb.append("
待成交买单:").append(pendingBuyOrders.size()).append(" / 1 笔
"); sb.append("
待成交卖单:").append(pendingSellOrders.size()).append(" 笔
"); sb.append("
持仓待止盈:").append(openPositions.size()) .append(" / ").append(gridCount).append(" 层
"); sb.append("
上限暂停:").append(upperBoundPaused ? "是" : "否").append("
"); sb.append("
下限暂停:").append(lowerBoundPaused ? "是" : "否").append("
"); JSONObject style = new JSONObject(); style.put("fontSize", "14px"); updateText(statusChart, sb.toString(), style); updateGridTable(); } private void updateGridTable() { if (gridTableChart == null || currentGridGap == null) { return; } JSONArray rows = new JSONArray(); List pending = new ArrayList<>(pendingBuyOrders.values()); pending.sort(Comparator.comparing(o -> o.buyPrice, Comparator.reverseOrder())); for (GridBuyOrder order : pending) { JSONObject row = new JSONObject(); row.put("type", "待成交买单"); row.put("price", formatPrice(order.buyPrice)); row.put("quantity", baseQuantity.toPlainString()); row.put("takeProfit", "-"); rows.add(row); } List 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 positions = new ArrayList<>(openPositions); positions.sort(Comparator.comparing(p -> p.entryPrice, Comparator.reverseOrder())); for (GridPosition position : positions) { JSONObject row = new JSONObject(); row.put("type", "持仓"); row.put("price", formatPrice(position.entryPrice)); row.put("quantity", position.quantity.toPlainString()); row.put("takeProfit", formatPrice(position.takeProfitPrice)); rows.add(row); } JSONObject option = new JSONObject(); option.put("height", 400); JSONArray columns = new JSONArray(); columns.add(buildColumn("类型", "type", 120)); columns.add(buildColumn("价格", "price", 150)); columns.add(buildColumn("数量", "quantity", 150)); columns.add(buildColumn("止盈价", "takeProfit", 150)); option.put("column", columns); ApiResult result = updateTable(gridTableChart, rows, option); if (result.getCode() != 200) { logger.debug("更新表格失败:{}", result.getMsg()); } } private JSONObject buildColumn(String label, String prop, int width) { JSONObject col = new JSONObject(); col.put("label", label); col.put("prop", prop); col.put("width", width); return col; } // ======================== 工具方法 ======================== private JSONObject buildOrderRequest(String side, String tradeSide, String orderType, BigDecimal price, BigDecimal size) { JSONObject orderInfo = new JSONObject(); orderInfo.put("symbol", getPlainSymbol()); orderInfo.put("marginMode", MARGIN_MODE); orderInfo.put("size", normalizeQuantity(size).toPlainString()); orderInfo.put("side", side); orderInfo.put("tradeSide", tradeSide); orderInfo.put("orderType", orderType); if (price != null) { orderInfo.put("price", normalizePrice(price).toPlainString()); } return orderInfo; } private boolean hasBuyOrderAt(BigDecimal price) { return pendingBuyOrders.containsKey(priceKey(normalizePrice(price))); } private boolean hasOpenPositionAtPrice(BigDecimal price) { if (price == null) { return false; } String key = priceKey(price); for (GridPosition position : openPositions) { if (priceKey(position.entryPrice).equals(key)) { return true; } } return false; } /** * 从 startPrice 起向下逐格查找:无持仓且无待成交买单的价位。 */ private BigDecimal findNextAvailableBuyPrice(BigDecimal startPrice) { if (startPrice == null || currentGridGap == null) { return null; } BigDecimal candidate = normalizePrice(startPrice); while (candidate.compareTo(lowerBound) >= 0) { if (!hasOpenPositionAtPrice(candidate) && !hasBuyOrderAt(candidate)) { return candidate; } candidate = normalizePrice(candidate.subtract(currentGridGap)); } return null; } private BigDecimal alignPriceDown(BigDecimal price, BigDecimal gap) { if (gap == null || gap.compareTo(BigDecimal.ZERO) <= 0) { return normalizePrice(price); } BigDecimal steps = price.divide(gap, 0, RoundingMode.DOWN); return normalizePrice(steps.multiply(gap)); } private BigDecimal normalizePrice(BigDecimal price) { return price.setScale(pricePrecision, RoundingMode.HALF_UP); } private BigDecimal normalizeQuantity(BigDecimal qty) { return qty.setScale(quantityPrecision, RoundingMode.DOWN); } private String priceKey(BigDecimal price) { return normalizePrice(price).toPlainString(); } private String formatPrice(BigDecimal price) { if (price == null) { return "-"; } return normalizePrice(price).toPlainString(); } private String buildClientOrderId(String prefix, BigDecimal price) { return prefix + "_" + getPlainSymbol() + "_" + price.toPlainString() + "_" + System.currentTimeMillis(); } }