| | |
| | | accumulatedShortLossCount = 0; |
| | | shortPriceQueue.clear(); |
| | | longPriceQueue.clear(); |
| | | totalShortPriceQueue.clear(); |
| | | totalLongPriceQueue.clear(); |
| | | currentLongOrderIds.clear(); |
| | | currentShortOrderIds.clear(); |
| | | // 每次重启重新获取当前本金 |
| | |
| | | longTakeProfitTraderIdParam(longTpElem, null, false); |
| | | log.info("[Gate] 多仓止盈触发 gridId:{}, orderId:{}", longTpElem.getId(), orderId); |
| | | cancelFarthestLongStopLoss(); |
| | | checkLastTakeProfitAndRestart(); |
| | | // checkLastTakeProfitAndRestart(); |
| | | return; |
| | | } |
| | | // [Gate-需求1] 空仓止盈触发:清空止盈状态 + 取消最远空仓止损 + 检查是否最后一个止盈 |
| | |
| | | shortTakeProfitTraderIdParam(shortTpElem, null, false); |
| | | log.info("[Gate] 空仓止盈触发 gridId:{}, orderId:{}", shortTpElem.getId(), orderId); |
| | | cancelFarthestShortStopLoss(); |
| | | checkLastTakeProfitAndRestart(); |
| | | // checkLastTakeProfitAndRestart(); |
| | | return; |
| | | } |
| | | |
| | |
| | | if (shortGridElement.isHasShortOrder() && StrUtil.isNotEmpty(tradeId) && !tradeId.equals("0") ){ |
| | | int filledQty = Integer.parseInt(shortGridElement.getShortTraderParam().getQuantity()); |
| | | shortEntryTraderIdParam(shortGridElement, null, false); |
| | | // [Gate-需求2] 加仓后先撤空仓所有止盈+止损,再重挂 |
| | | // [Gate-需求2] 加仓后先撤空仓所有止盈+止损,再查交易所持仓后重挂 |
| | | cancelAllShortTakeProfitsAndStopLosses(); |
| | | extendShortStopLoss(filledQty,shortGridElement.getId()); |
| | | // REST 查询可能因交易所延迟返回旧值,与 WS 本地缓存取最大值兜底 |
| | | int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue()); |
| | | extendShortStopLoss(posSize, shortGridElement.getId()); |
| | | accumulatedShortLossCount = 0; // 加仓订单成交,重置止损累计 |
| | | log.info("[Gate] 空单成交 gridId:{}", filledQty); |
| | | log.info("[Gate] 空单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize); |
| | | |
| | | // 空仓持仓超过baseQuantity时,从gridId-2开始向外追挂止盈 |
| | | // 空仓持仓超过baseQuantity时,先找多仓第一个止损位置,从该位置向下挂止盈(间隔=1) |
| | | BigDecimal shortBaseQty = new BigDecimal(config.getBaseQuantity()); |
| | | BigDecimal shortGridQty = new BigDecimal(config.getQuantity()); |
| | | if (shortPositionSize.compareTo(shortBaseQty) > 0) { |
| | | BigDecimal shortExcess = shortPositionSize.subtract(shortBaseQty); |
| | | if (BigDecimal.valueOf(posSize).compareTo(shortBaseQty) > 0) { |
| | | BigDecimal shortExcess = BigDecimal.valueOf(posSize).subtract(shortBaseQty); |
| | | int shortExcessCount = shortExcess.divide(shortGridQty, 0, RoundingMode.DOWN).intValue(); |
| | | |
| | | // 找多仓第一个(最近的)止损位置 |
| | | int firstLongSlId = 0; |
| | | for (GridElement e : config.getGridElements()) { |
| | | if (e.getLongStopLossOrderId() != null) { |
| | | if (firstLongSlId == 0 || e.getId() > firstLongSlId) { |
| | | firstLongSlId = e.getId(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | for (int i = 0; i < shortExcessCount; i++) { |
| | | int tpGridId = shortGridElement.getId() - 2 - i; |
| | | int tpGridId; |
| | | if (firstLongSlId != 0) { |
| | | tpGridId = firstLongSlId - i; // 从多仓第一个止损位置开始,向下挂,间隔=1 |
| | | } else { |
| | | tpGridId = shortGridElement.getId() - 2 * (i + 1); // 无多仓止损时回退原逻辑 |
| | | } |
| | | GridElement tpElem = GridElement.findById(tpGridId); |
| | | if (tpElem == null || tpElem.getShortTakeProfitOrderId() != null) { |
| | | continue; |
| | |
| | | |
| | | int filledQty = Integer.parseInt(longGridElement.getLongTraderParam().getQuantity()); |
| | | longEntryTraderIdParam(longGridElement, null, false); |
| | | // [Gate-需求2] 加仓后先撤多仓所有止盈+止损,再重挂 |
| | | // [Gate-需求2] 加仓后先撤多仓所有止盈+止损,再查交易所持仓后重挂 |
| | | cancelAllLongTakeProfitsAndStopLosses(); |
| | | extendLongStopLoss(filledQty,longGridElement.getId()); |
| | | // REST 查询可能因交易所延迟返回旧值,与 WS 本地缓存取最大值兜底 |
| | | int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue()); |
| | | extendLongStopLoss(posSize, longGridElement.getId()); |
| | | accumulatedLongLossCount = 0; // 加仓订单成交,重置止损累计 |
| | | log.info("[Gate] 多单成交 gridId:{}", filledQty); |
| | | log.info("[Gate] 多单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize); |
| | | |
| | | // 多仓持仓超过baseQuantity时,从gridId+2开始向外追挂止盈 |
| | | // 多仓持仓超过baseQuantity时,先找空仓第一个止损位置,从该位置向上挂止盈(间隔=1) |
| | | BigDecimal longBaseQty = new BigDecimal(config.getBaseQuantity()); |
| | | BigDecimal longGridQty = new BigDecimal(config.getQuantity()); |
| | | if (longPositionSize.compareTo(longBaseQty) > 0) { |
| | | BigDecimal longExcess = longPositionSize.subtract(longBaseQty); |
| | | if (BigDecimal.valueOf(posSize).compareTo(longBaseQty) > 0) { |
| | | BigDecimal longExcess = BigDecimal.valueOf(posSize).subtract(longBaseQty); |
| | | int longExcessCount = longExcess.divide(longGridQty, 0, RoundingMode.DOWN).intValue(); |
| | | |
| | | // 找空仓第一个(最近的)止损位置 |
| | | int firstShortSlId = 0; |
| | | for (GridElement e : config.getGridElements()) { |
| | | if (e.getShortStopLossOrderId() != null) { |
| | | if (firstShortSlId == 0 || e.getId() < firstShortSlId) { |
| | | firstShortSlId = e.getId(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | for (int i = 0; i < longExcessCount; i++) { |
| | | int tpGridId = longGridElement.getId() + 2 + i; |
| | | int tpGridId; |
| | | if (firstShortSlId != 0) { |
| | | tpGridId = firstShortSlId + i; // 从空仓第一个止损位置开始,向上挂,间隔=1 |
| | | } else { |
| | | tpGridId = longGridElement.getId() + 2 * (i + 1); // 无空仓止损时回退原逻辑 |
| | | } |
| | | GridElement tpElem = GridElement.findById(tpGridId); |
| | | if (tpElem == null || tpElem.getLongTakeProfitOrderId() != null) { |
| | | continue; |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 查询交易所当前持仓张数(绝对值)。加仓/开仓后重挂止盈止损时调用, |
| | | * 绕过本地 WS 推送缓存避免时序竞态,直接拿到交易所权威数据。 |
| | | * |
| | | * @param mode 持仓模式(DUAL_LONG / DUAL_SHORT) |
| | | * @return 持仓张数(绝对值),查询失败返回 0 |
| | | */ |
| | | private int queryPositionSize(Position.ModeEnum mode) { |
| | | Position p = queryPosition(mode); |
| | | if (p != null) { |
| | | return new BigDecimal(p.getSize()).abs().intValue(); |
| | | } |
| | | return 0; |
| | | } |
| | | |
| | | /** |
| | | * 查询交易所当前持仓均价,绕过本地 WS 推送缓存避免时序竞态。 |
| | | * |
| | | * @param mode 持仓模式(DUAL_LONG / DUAL_SHORT) |
| | | * @return 持仓均价,无持仓或查询失败返回 BigDecimal.ZERO |
| | | */ |
| | | private BigDecimal queryEntryPrice(Position.ModeEnum mode) { |
| | | Position p = queryPosition(mode); |
| | | if (p != null && p.getEntryPrice() != null) { |
| | | return new BigDecimal(p.getEntryPrice()); |
| | | } |
| | | return BigDecimal.ZERO; |
| | | } |
| | | |
| | | /** |
| | | * 查询指定模式的持仓对象。 |
| | | */ |
| | | private Position queryPosition(Position.ModeEnum mode) { |
| | | try { |
| | | List<Position> positions = futuresApi.listPositions(SETTLE).execute(); |
| | | if (positions != null) { |
| | | for (Position p : positions) { |
| | | if (mode == p.getMode() && config.getContract().equals(p.getContract())) { |
| | | return p; |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("[Gate] 查询{}持仓失败", mode, e); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | // ---- 网格队列处理 ---- |
| | | |
| | |
| | | */ |
| | | private void generateShortQueue() { |
| | | shortPriceQueue.clear(); |
| | | totalShortPriceQueue.clear(); |
| | | totalLongPriceQueue.clear(); |
| | | int prec = config.getPriceScale(); |
| | | BigDecimal step = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(prec, RoundingMode.HALF_UP); |
| | | config.setStep(step); |
| | |
| | | elements.add(GridElement.builder() |
| | | .id(0) |
| | | .gridPrice(price) |
| | | .upId(shortSize > 0 ? 1 : null) |
| | | .downId(longSize > 0 ? -1 : null) |
| | | .upId(longSize > 0 ? 1 : null) |
| | | .downId(shortSize > 0 ? -1 : null) |
| | | .longTraderParam(longParam) |
| | | .shortTraderParam(shortParam) |
| | | .build()); |
| | |
| | | if (!newEntryGrid.isHasLongOrder()) { |
| | | BigDecimal triggerPrice = newEntryGrid.getGridPrice(); |
| | | |
| | | // 累计止损张数 + 当前止损量作为追单size,不再依赖positionSize(避免WS竞态) |
| | | accumulatedLongLossCount += Integer.parseInt(config.getQuantity()); |
| | | String size = String.valueOf(accumulatedLongLossCount + Integer.parseInt(config.getQuantity())); |
| | | log.info("[Gate] 多仓止损触发 gridId:{}, 在gridId:{}挂{}基础张多单", |
| | | gridId, newEntryGridId, size); |
| | | |
| | | newEntryGrid.getLongTraderParam().setQuantity(size); |
| | | placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_1, size); |
| | | // 止损触发后持仓在减少,取REST和WS缓存中较小值更准确 |
| | | int posSize = Math.min(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue()); |
| | | int maxPos = config.getMaxPositionSize(); |
| | | int targetAmount = Integer.parseInt(config.getQuantity()) * 2; // quantity + 本次止损量 |
| | | int addSize; |
| | | if (maxPos > 0) { |
| | | int remainingRoom = maxPos - posSize; |
| | | if (remainingRoom <= 0) { |
| | | log.warn("[Gate] 多仓止损触发 gridId:{}, 当前持仓{}/{}已达上限,跳过追单", |
| | | gridId, posSize, maxPos); |
| | | addSize = 0; |
| | | } else { |
| | | addSize = Math.min(remainingRoom, targetAmount); |
| | | } |
| | | } else { |
| | | addSize = targetAmount; |
| | | } |
| | | if (addSize > 0) { |
| | | String size = String.valueOf(addSize); |
| | | log.info("[Gate] 多仓止损触发 gridId:{}, 在gridId:{}补{}张多单(当前{}/上限{})", |
| | | gridId, newEntryGridId, size, posSize, maxPos > 0 ? maxPos : "无"); |
| | | newEntryGrid.getLongTraderParam().setQuantity(size); |
| | | placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_1, size); |
| | | } |
| | | }else{ |
| | | log.warn("[Gate] 多仓止损触发 gridId:{}, 目标gridId:{}已有挂单,跳过重复下单", gridId, newEntryGridId); |
| | | } |
| | |
| | | if (!newEntryGrid.isHasShortOrder()) { |
| | | BigDecimal triggerPrice = newEntryGrid.getGridPrice(); |
| | | |
| | | // 累计止损张数 + 当前止损量作为追单size,不再依赖positionSize(避免WS竞态) |
| | | accumulatedShortLossCount += Integer.parseInt(config.getQuantity()); |
| | | String size = String.valueOf(accumulatedShortLossCount + Integer.parseInt(config.getQuantity())); |
| | | log.info("[Gate] 空仓止损触发 gridId:{}, 在gridId:{}挂{}基础张空单", |
| | | gridId, newEntryGridId, size); |
| | | newEntryGrid.getShortTraderParam().setQuantity(size); |
| | | placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_2, negate(size)); |
| | | // 止损触发后持仓在减少,取REST和WS缓存中较小值更准确 |
| | | int posSize = Math.min(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue()); |
| | | int maxPos = config.getMaxPositionSize(); |
| | | int targetAmount = Integer.parseInt(config.getQuantity()) * 2; // quantity + 本次止损量 |
| | | int addSize; |
| | | if (maxPos > 0) { |
| | | int remainingRoom = maxPos - posSize; |
| | | if (remainingRoom <= 0) { |
| | | log.warn("[Gate] 空仓止损触发 gridId:{}, 当前持仓{}/{}已达上限,跳过追单", |
| | | gridId, posSize, maxPos); |
| | | addSize = 0; |
| | | } else { |
| | | addSize = Math.min(remainingRoom, targetAmount); |
| | | } |
| | | } else { |
| | | addSize = targetAmount; |
| | | } |
| | | if (addSize > 0) { |
| | | String size = String.valueOf(addSize); |
| | | log.info("[Gate] 空仓止损触发 gridId:{}, 在gridId:{}补{}张空单(当前{}/上限{})", |
| | | gridId, newEntryGridId, size, posSize, maxPos > 0 ? maxPos : "无"); |
| | | newEntryGrid.getShortTraderParam().setQuantity(size); |
| | | placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_2, negate(size)); |
| | | } |
| | | }else{ |
| | | log.warn("[Gate] 空仓止损触发 gridId:{}, 目标gridId:{}已有挂单,跳过重复下单", gridId, newEntryGridId); |
| | | } |
| | |
| | | if (span <= 0) { |
| | | return; |
| | | } |
| | | |
| | | // 检查是否还有剩余止盈单,只有多空止盈全部清空才继续 |
| | | if (GridElement.getLongTakeProfitCount() > 0 || GridElement.getShortTakeProfitCount() > 0) { |
| | | log.info("[Gate] 尚有未触发止盈单, 暂不检查跨度重启 longTpCount:{}, shortTpCount:{}", |
| | | GridElement.getLongTakeProfitCount(), GridElement.getShortTakeProfitCount()); |
| | | return; |
| | | } |
| | | |
| | | BigDecimal step = config.getStep(); |
| | | if (step == null || step.compareTo(BigDecimal.ZERO) == 0) { |
| | | return; |
| | |
| | | return; |
| | | } |
| | | |
| | | // 查交易所获取最新持仓均价和持仓量,不用本地缓存避免 WS 时序竞态 |
| | | Position longPos = queryPosition(Position.ModeEnum.DUAL_LONG); |
| | | Position shortPos = queryPosition(Position.ModeEnum.DUAL_SHORT); |
| | | boolean hasLong = longPos != null && Math.abs(Integer.parseInt(longPos.getSize())) > 0; |
| | | boolean hasShort = shortPos != null && Math.abs(Integer.parseInt(shortPos.getSize())) > 0; |
| | | BigDecimal longAvgPrice = (longPos != null && longPos.getEntryPrice() != null) |
| | | ? new BigDecimal(longPos.getEntryPrice()) : BigDecimal.ZERO; |
| | | BigDecimal shortAvgPrice = (shortPos != null && shortPos.getEntryPrice() != null) |
| | | ? new BigDecimal(shortPos.getEntryPrice()) : BigDecimal.ZERO; |
| | | |
| | | boolean shouldRestart = false; |
| | | String reason = ""; |
| | | |
| | | if (longActive && shortActive) { |
| | | // 多空双边持仓:多均价 − 空均价 > span × step |
| | | BigDecimal gap = longEntryPrice.subtract(shortEntryPrice); |
| | | if (gap.compareTo(threshold) > 0) { |
| | | if (hasLong && hasShort) { |
| | | // 多空双边持仓:|多均价 − 空均价| > span × step |
| | | BigDecimal gap = shortAvgPrice.subtract(longAvgPrice); |
| | | if (gap.compareTo(threshold) >= 0) { |
| | | shouldRestart = true; |
| | | reason = StrUtil.format("双边跨度 多均价:{} − 空均价:{} = {} > {} (span:{}×step:{})", |
| | | longEntryPrice, shortEntryPrice, gap, threshold, span, step); |
| | | reason = StrUtil.format("双边跨度 |多均价:{} − 空均价:{}| = {} >= {} (span:{}×step:{})", |
| | | longAvgPrice, shortAvgPrice, gap, threshold, span, step); |
| | | } |
| | | } else if (longActive) { |
| | | } else if (hasLong) { |
| | | // 仅持多仓:当前价 − 多均价 > span × step |
| | | BigDecimal gap = currentPrice.subtract(longEntryPrice); |
| | | if (gap.compareTo(threshold) > 0) { |
| | | BigDecimal gap = currentPrice.subtract(longAvgPrice); |
| | | if (gap.compareTo(threshold) >= 0) { |
| | | shouldRestart = true; |
| | | reason = StrUtil.format("多仓跨度 当前价:{} − 多均价:{} = {} > {} (span:{}×step:{})", |
| | | currentPrice, longEntryPrice, gap, threshold, span, step); |
| | | currentPrice, longAvgPrice, gap, threshold, span, step); |
| | | } |
| | | } else if (shortActive) { |
| | | } else if (hasShort) { |
| | | // 仅持空仓:空均价 − 当前价 > span × step |
| | | BigDecimal gap = shortEntryPrice.subtract(currentPrice); |
| | | if (gap.compareTo(threshold) > 0) { |
| | | BigDecimal gap = shortAvgPrice.subtract(currentPrice); |
| | | if (gap.compareTo(threshold) >= 0) { |
| | | shouldRestart = true; |
| | | reason = StrUtil.format("空仓跨度 空均价:{} − 当前价:{} = {} > {} (span:{}×step:{})", |
| | | shortEntryPrice, currentPrice, gap, threshold, span, step); |
| | | shortAvgPrice, currentPrice, gap, threshold, span, step); |
| | | } |
| | | } |
| | | |