| | |
| | | } |
| | | |
| | | /** Gate 永续合约 taker 费率 0.05% */ |
| | | private static final BigDecimal TAKER_FEE_RATE = new BigDecimal("0.0005"); |
| | | private static final BigDecimal TAKER_FEE_RATE = new BigDecimal("0.001"); |
| | | private void checkProfitAndReset() { |
| | | if (state == StrategyState.STOPPED || state == StrategyState.WAITING_KLINE) { |
| | | return; |
| | |
| | | BigDecimal target = initialPrincipal.add(config.getExpectedProfit()); |
| | | |
| | | FuturesAccount account = futuresApi.listFuturesAccounts(SETTLE); |
| | | BigDecimal totalEquity = new BigDecimal(account.getTotal()).add(new BigDecimal(account.getUnrealisedPnl())); |
| | | // 预估平仓手续费 = 持仓量 × 合约乘数 × 入场价 × taker费率 |
| | | BigDecimal multiplier = config.getContractMultiplier(); |
| | | BigDecimal longCloseFee = BigDecimal.ZERO; |
| | | if (longPositionSize.compareTo(BigDecimal.ZERO) > 0 && longEntryPrice.compareTo(BigDecimal.ZERO) > 0) { |
| | | longCloseFee = longPositionSize.multiply(multiplier).multiply(longEntryPrice).multiply(TAKER_FEE_RATE); |
| | | } |
| | | BigDecimal shortCloseFee = BigDecimal.ZERO; |
| | | if (shortPositionSize.compareTo(BigDecimal.ZERO) > 0 && shortEntryPrice.compareTo(BigDecimal.ZERO) > 0) { |
| | | shortCloseFee = shortPositionSize.multiply(multiplier).multiply(shortEntryPrice).multiply(TAKER_FEE_RATE); |
| | | } |
| | | BigDecimal estimatedCloseFee = longCloseFee.add(shortCloseFee); |
| | | |
| | | BigDecimal totalEquity = new BigDecimal(account.getTotal()) |
| | | .add(new BigDecimal(account.getUnrealisedPnl())) |
| | | .subtract(estimatedCloseFee); |
| | | |
| | | if (totalEquity.compareTo(target) > 0) { |
| | | currentRound++; |
| | | int maxRounds = config.getRounds(); |
| | | log.info("[Gate] 盈亏达标(净权益{}>目标{}),第{}轮完成", totalEquity, target, currentRound); |
| | | log.info("[Gate] 盈亏达标(净权益{}→含手续费-{}=实际{}>目标{}),第{}轮完成", |
| | | new BigDecimal(account.getTotal()).add(new BigDecimal(account.getUnrealisedPnl())), |
| | | estimatedCloseFee, totalEquity, target, currentRound); |
| | | |
| | | if (maxRounds > 0 && currentRound >= maxRounds) { |
| | | log.info("[Gate] 已达到运行轮数上限({}),策略停止", maxRounds); |
| | |
| | | } |
| | | shortGridElement.setExtendStopLossInProgress(true); |
| | | |
| | | accumulatedShortLossCount = 0; |
| | | lastShortStopLossGridId = Integer.MAX_VALUE; |
| | | // [Gate-需求2] 加仓后先撤空仓所有止盈+止损,再查交易所持仓后重挂 |
| | | cancelAllShortTakeProfitsAndStopLosses(); |
| | | // REST 查询可能因交易所延迟返回旧值,与 WS 本地缓存取最大值兜底 |
| | | int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue()); |
| | | extendShortStopLoss(posSize, shortGridElement.getId()); |
| | | // [Gate] 止盈挂单:超出基础仓位的部分,挂在多仓第一止损位 |
| | | // placeExcessTakeProfit(posSize, false); |
| | | log.info("[Gate] 空单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize); |
| | | |
| | | |
| | | |
| | | checkShortStopLongProfit(accumulatedShortLossCount, shortGridElement.getId() + 2); |
| | | |
| | | } |
| | | } |
| | |
| | | } |
| | | longGridElement.setExtendStopLossInProgress(true); |
| | | |
| | | accumulatedLongLossCount = 0; |
| | | lastLongStopLossGridId = Integer.MAX_VALUE; |
| | | // [Gate-需求2] 加仓后先撤多仓所有止盈+止损,再查交易所持仓后重挂 |
| | | cancelAllLongTakeProfitsAndStopLosses(); |
| | | // REST 查询可能因交易所延迟返回旧值,与 WS 本地缓存取最大值兜底 |
| | | int posSize = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue()); |
| | | extendLongStopLoss(posSize, longGridElement.getId()); |
| | | // [Gate] 止盈挂单:超出基础仓位的部分,挂在空仓第一止损位 |
| | | // placeExcessTakeProfit(posSize, true); |
| | | log.info("[Gate] 多单成交 gridId:{}, 当前持仓:{}张", filledQty, posSize); |
| | | |
| | | |
| | | checkLongStopShortProfit(accumulatedLongLossCount, longGridElement.getId() -2); |
| | | |
| | | } |
| | | } |
| | |
| | | if (newEntryGrid != null) { |
| | | |
| | | String quantity = String.valueOf(Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue()) + Integer.parseInt(config.getQuantity())); |
| | | |
| | | // 向下检查是否已有多单挂在更低价格网格,有则跳过(防止价格回升后重复挂单) |
| | | boolean hasLongOrderBelow = false; |
| | | GridElement checkDownCursor = GridElement.findById(newEntryGrid.getDownId()); |
| | | while (checkDownCursor != null) { |
| | | if (checkDownCursor.isHasLongOrder()) { |
| | | hasLongOrderBelow = true; |
| | | log.info("[Gate] 多仓仓位归零 gridId:{}, 下方gridId:{}已有多单, 跳过本次挂单", |
| | | newEntryGrid.getId(), checkDownCursor.getId()); |
| | | break; |
| | | } |
| | | Integer nextDownId = checkDownCursor.getDownId(); |
| | | checkDownCursor = nextDownId != null ? GridElement.findById(nextDownId) : null; |
| | | } |
| | | |
| | | // 向上遍历取消所有遗留多单(跳过基础入场网格0) |
| | | GridElement cancelCursor = GridElement.findById(newEntryGrid.getUpId()); |
| | | while (cancelCursor != null) { |
| | |
| | | cancelCursor = nextUpId != null ? GridElement.findById(nextUpId) : null; |
| | | } |
| | | // log.info("[Gate-2] 多仓仓位归零 空仓队列触发, 匹配:{},当前价:{}", matched, currentPrice); |
| | | if (!newEntryGrid.isHasLongOrder()) { |
| | | if (!newEntryGrid.isHasLongOrder() && !hasLongOrderBelow) { |
| | | BigDecimal triggerPrice = newEntryGrid.getGridPrice(); |
| | | String size = quantity; |
| | | log.info("[Gate] 多仓仓位归零 gridId:{}, 挂{}基础张多单", |
| | |
| | | |
| | | // String quantity = String.valueOf((accumulatedShortLossCount + 1) * Integer.parseInt(config.getQuantity())); |
| | | String quantity = String.valueOf(Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue()) + Integer.parseInt(config.getQuantity())); |
| | | |
| | | // 向上检查是否已有空单挂在更高价格网格,有则跳过(防止价格回落后重复挂单) |
| | | boolean hasShortOrderAbove = false; |
| | | GridElement checkUpCursor = GridElement.findById(newEntryGrid.getUpId()); |
| | | while (checkUpCursor != null) { |
| | | if (checkUpCursor.isHasShortOrder()) { |
| | | hasShortOrderAbove = true; |
| | | log.info("[Gate] 空仓仓位归零 gridId:{}, 上方gridId:{}已有空单, 跳过本次挂单", |
| | | newEntryGrid.getId(), checkUpCursor.getId()); |
| | | break; |
| | | } |
| | | Integer nextUpId = checkUpCursor.getUpId(); |
| | | checkUpCursor = nextUpId != null ? GridElement.findById(nextUpId) : null; |
| | | } |
| | | |
| | | // 向下遍历取消所有遗留空单(跳过基础入场网格0) |
| | | GridElement cancelCursor = GridElement.findById(newEntryGrid.getDownId()); |
| | | while (cancelCursor != null) { |
| | |
| | | cancelCursor = nextDownId != null ? GridElement.findById(nextDownId) : null; |
| | | } |
| | | // log.info("[Gate-4] 空仓仓位归零 多仓队列触发, 匹配:{},当前价:{}", matched, currentPrice); |
| | | if (!newEntryGrid.isHasShortOrder()){ |
| | | if (!newEntryGrid.isHasShortOrder() && !hasShortOrderAbove){ |
| | | BigDecimal triggerPrice = newEntryGrid.getGridPrice(); |
| | | String size = quantity; |
| | | log.info("[Gate] 空仓仓位归零 gridId:{}, 挂{}基础张多单", |
| | |
| | | } |
| | | |
| | | lastLongStopLossGridId = gridId; |
| | | accumulatedLongLossCount++; |
| | | log.info("[Gate] 多仓止损触发 gridId:{}, 止损次数:{}{}, 开始追单", |
| | | gridId, accumulatedLongLossCount, sameGrid ? "(同网格)" : ""); |
| | | int newEntryGridId = gridId + 1; |
| | |
| | | |
| | | // 止损追单:同一网格可有多笔挂单,不判断 isHasLongOrder,直接挂单 |
| | | BigDecimal triggerPrice = newEntryGrid.getGridPrice(); |
| | | String size = String.valueOf(Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue()) + Integer.parseInt(config.getQuantity())); |
| | | // String size = String.valueOf(Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue()) + Integer.parseInt(config.getQuantity())); |
| | | String size = String.valueOf(config.getBaseQuantity()); |
| | | newEntryGrid.getLongTraderParam().setQuantity(size); |
| | | placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_1, size); |
| | |
| | | |
| | | } |
| | | |
| | | // ========== 止损触发操作表 ========== |
| | | /** |
| | | * value = int[2]: {对手满仓止盈%, 基础仓位阈值%} |
| | | * <ul> |
| | | * <li>[0] 对手满仓:挂对手仓位此%做止盈;0=不挂</li> |
| | | * <li>[1] 基础仓位阈值(占maxPos的%):对手/己方仓位超出此阈值部分做止盈;0=不挂</li> |
| | | * </ul> |
| | | */ |
| | | private static final Map<Integer, int[]> STOP_LOSS_RULES = new LinkedHashMap<>(); |
| | | private static final int DEFAULT_STOP_LOSS_PERCENT = 5; |
| | | |
| | | static { |
| | | // times {满仓%, 阈值%} |
| | | put(1, new int[]{0, 0}); |
| | | put(2, new int[]{0, 0}); |
| | | put(3, new int[]{30, 0}); |
| | | put(4, new int[]{0, 0}); |
| | | put(5, new int[]{0, 0}); |
| | | put(6, new int[]{40, 40}); |
| | | put(7, new int[]{0, 0}); |
| | | put(8, new int[]{0, 0}); |
| | | put(9, new int[]{50, 50}); |
| | | put(10, new int[]{0, 0}); |
| | | put(11, new int[]{0, 0}); |
| | | put(12, new int[]{55, 60}); |
| | | put(13, new int[]{0, 0}); |
| | | put(14, new int[]{0, 0}); |
| | | put(15, new int[]{60, 60}); |
| | | put(16, new int[]{0, 0}); |
| | | put(17, new int[]{0, 0}); |
| | | put(18, new int[]{65, 65}); |
| | | put(19, new int[]{0, 0}); |
| | | put(20, new int[]{0, 0}); |
| | | put(21, new int[]{70, 70}); |
| | | put(22, new int[]{0, 0}); |
| | | put(23, new int[]{0, 0}); |
| | | put(24, new int[]{75, 75}); |
| | | put(25, new int[]{0, 0}); |
| | | put(26, new int[]{0, 0}); |
| | | put(27, new int[]{80, 80}); |
| | | put(28, new int[]{0, 0}); |
| | | put(29, new int[]{0, 0}); |
| | | put(30, new int[]{85, 85}); |
| | | put(31, new int[]{0, 0}); |
| | | put(32, new int[]{0, 0}); |
| | | } |
| | | |
| | | private static void put(int times, int[] rule) { STOP_LOSS_RULES.put(times, rule); } |
| | | |
| | | private static int[] ruleOf(int times) { |
| | | int[] r = STOP_LOSS_RULES.get(times); |
| | | return r != null ? r : STOP_LOSS_RULES.get(32); |
| | | } |
| | | |
| | | /** |
| | | * 多仓止损触发后,查表处理对手盘(空仓)盈止 + 己方盈止 + 激活。 |
| | | */ |
| | | private void checkLongStopShortProfit(int times, int gridId) { |
| | | int[] r = ruleOf(times); |
| | | int maxPos = config.getMaxPositionSize(); |
| | | if (maxPos <= 0) { |
| | | return; |
| | | } |
| | | |
| | | int fullTpPct = r[0]; // 对手满仓 → 止盈% |
| | | int fullTpPctNum = maxPos * (100- fullTpPct)/ 100; |
| | | int thresholdPct = r[1]; // 基础仓位阈值% |
| | | int thresholdPosNum = maxPos * (100- thresholdPct)/ 100; |
| | | |
| | | // ---------- 对手盘(空仓)盈止 ---------- |
| | | int oppPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue()); |
| | | boolean isFullTp = oppPos > fullTpPctNum; |
| | | if (isFullTp) { |
| | | // 对手仓位超出阈值:超额整个挂一笔止盈,位置在触发格 - 1 |
| | | int excess = oppPos - fullTpPctNum; |
| | | GridElement tpElem = GridElement.findById(gridId); |
| | | if (tpElem != null) { |
| | | placeTakeProfitAtGrid(tpElem, false, excess, times); |
| | | } |
| | | } |
| | | |
| | | // ---------- 己方(多仓)盈止(≥6次) ---------- |
| | | if (times >= 6 && thresholdPct > 0) { |
| | | int selfPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue()); |
| | | int excess = selfPos - thresholdPosNum; |
| | | if (excess > 0) { |
| | | int perOrderQty = maxPos * DEFAULT_STOP_LOSS_PERCENT / 100; |
| | | if (perOrderQty <= 0) { |
| | | perOrderQty = 1; |
| | | } |
| | | int totalOrders = excess / perOrderQty; |
| | | if (totalOrders <= 0) { |
| | | totalOrders = 1; |
| | | } |
| | | int entryGridId = 0; // 多仓建仓格 |
| | | for (int i = 0; i < totalOrders; i++) { |
| | | int tpGridId = entryGridId + 2 * (i + 1); // 朝盈利方向(向上)间隔2格 |
| | | GridElement tpElem = GridElement.findById(tpGridId); |
| | | if (tpElem == null) { |
| | | break; |
| | | } |
| | | placeTakeProfitAtGrid(tpElem, true, perOrderQty, times); |
| | | } |
| | | log.info("[Gate] 止损{}→己方多仓止盈, excess:{}, 挂{}单, 每单{}张", |
| | | times, excess, totalOrders, perOrderQty); |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | /** |
| | | * 空仓止损触发后,查表处理对手盘(多仓)盈止 + 己方盈止。 |
| | | */ |
| | | private void checkShortStopLongProfit(int times, int gridId) { |
| | | int[] r = ruleOf(times); |
| | | int maxPos = config.getMaxPositionSize(); |
| | | if (maxPos <= 0) { |
| | | return; |
| | | } |
| | | |
| | | int fullTpPct = r[0]; // 对手满仓 → 止盈% |
| | | int fullTpPctNum = maxPos * (100 - fullTpPct) / 100; |
| | | int thresholdPct = r[1]; // 基础仓位阈值% |
| | | int thresholdPosNum = maxPos * (100 - thresholdPct) / 100; |
| | | |
| | | // ---------- 对手盘(多仓)盈止 ---------- |
| | | int oppPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue()); |
| | | boolean isFullTp = oppPos > fullTpPctNum; |
| | | if (isFullTp) { |
| | | // 对手仓位超出阈值:超额整个挂一笔止盈,位置在触发格 |
| | | int excess = oppPos - fullTpPctNum; |
| | | GridElement tpElem = GridElement.findById(gridId); |
| | | if (tpElem != null) { |
| | | placeTakeProfitAtGrid(tpElem, true, excess, times); |
| | | } |
| | | } |
| | | |
| | | // ---------- 己方(空仓)盈止(≥6次) ---------- |
| | | if (times >= 6 && thresholdPct > 0) { |
| | | int selfPos = Math.max(queryPositionSize(Position.ModeEnum.DUAL_SHORT), shortPositionSize.intValue()); |
| | | int excess = selfPos - thresholdPosNum; |
| | | if (excess > 0) { |
| | | int perOrderQty = maxPos * DEFAULT_STOP_LOSS_PERCENT / 100; |
| | | if (perOrderQty <= 0) { |
| | | perOrderQty = 1; |
| | | } |
| | | int totalOrders = excess / perOrderQty; |
| | | if (totalOrders <= 0) { |
| | | totalOrders = 1; |
| | | } |
| | | int entryGridId = 0; // 空仓建仓格 |
| | | for (int i = 0; i < totalOrders; i++) { |
| | | int tpGridId = entryGridId - 2 * (i + 1); // 朝盈利方向(向下)间隔2格 |
| | | GridElement tpElem = GridElement.findById(tpGridId); |
| | | if (tpElem == null) { |
| | | break; |
| | | } |
| | | placeTakeProfitAtGrid(tpElem, false, perOrderQty, times); |
| | | } |
| | | log.info("[Gate] 止损{}→己方空仓止盈, excess:{}, 挂{}单, 每单{}张", |
| | | times, excess, totalOrders, perOrderQty); |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | private void handleShortStopLossTriggered(GridElement gridElement, String orderId) { |
| | | gridElement.removeShortStopLossOrderId(orderId); |
| | | |
| | |
| | | return; |
| | | } |
| | | lastShortStopLossGridId = gridId; |
| | | accumulatedShortLossCount++; |
| | | log.info("[Gate] 空仓止损触发 gridId:{}, 止损次数:{}{}, 开始追单", |
| | | gridId, accumulatedShortLossCount, sameGrid ? "(同网格)" : ""); |
| | | int newEntryGridId = gridId - 1; |
| | |
| | | |
| | | // 止损追单:同一网格可有多笔挂单,不判断 isHasShortOrder,直接挂单 |
| | | BigDecimal triggerPrice = newEntryGrid.getGridPrice(); |
| | | String size = String.valueOf(Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue()) + Integer.parseInt(config.getQuantity())); |
| | | // String size = String.valueOf(Math.max(queryPositionSize(Position.ModeEnum.DUAL_LONG), longPositionSize.intValue()) + Integer.parseInt(config.getQuantity())); |
| | | String size = String.valueOf(config.getBaseQuantity()); |
| | | newEntryGrid.getShortTraderParam().setQuantity(size); |
| | | placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice, |
| | | FuturesPriceTrigger.RuleEnum.NUMBER_2, negate(size)); |
| | |
| | | public StrategyState getState() { return state; } |
| | | /** 注入WS客户端,用于订阅状态检查 */ |
| | | public void setWsClient(GateKlineWebSocketClient wsClient) { this.wsClient = wsClient; } |
| | | |
| | | // ========== 止损查表辅助方法 ========== |
| | | |
| | | /** 找到第一个有多仓止损单的网格(首个匹配即返回) */ |
| | | private GridElement findFirstLongStopLossGrid() { |
| | | for (GridElement e : config.getGridElements()) { |
| | | if (!e.getLongStopLossOrderIds().isEmpty()) return e; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 找到第一个有空仓止损单的网格(首个匹配即返回) */ |
| | | private GridElement findFirstShortStopLossGrid() { |
| | | for (GridElement e : config.getGridElements()) { |
| | | if (!e.getShortStopLossOrderIds().isEmpty()) return e; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 在指定网格挂一笔对手止盈单(非满仓超额止盈,挂在止损触发位的下一格)。 |
| | | */ |
| | | private void placeTakeProfitAtGrid(GridElement tpElem, boolean isLong, int qty, int times) { |
| | | BigDecimal triggerPrice = tpElem.getGridPrice(); |
| | | String orderType = isLong ? ORDER_TYPE_CLOSE_LONG : ORDER_TYPE_CLOSE_SHORT; |
| | | FuturesPriceTrigger.RuleEnum rule = isLong ? FuturesPriceTrigger.RuleEnum.NUMBER_1 |
| | | : FuturesPriceTrigger.RuleEnum.NUMBER_2; |
| | | String size = isLong ? negate(String.valueOf(qty)) : String.valueOf(qty); |
| | | int gridId = tpElem.getId(); |
| | | executor.placeTakeProfit(triggerPrice, rule, orderType, size, |
| | | profitId -> { |
| | | if (isLong) { |
| | | longTakeProfitTraderIdParam(tpElem, profitId, true); |
| | | } else { |
| | | shortTakeProfitTraderIdParam(tpElem, profitId, true); |
| | | } |
| | | log.info("[Gate] 止损{}→对手超额止盈 gridId:{}, 量:{}, tpId:{}", times, gridId, qty, profitId); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * 挂对手盘止盈单:在对向仓位第一止损位挂止盈。 |
| | | * @param isLong true=挂多仓止盈(对空仓), false=挂空仓止盈(对多仓) |
| | | */ |
| | | private void placeOpponentTakeProfit(boolean isLong, int tpQty, int times, int gridId) { |
| | | GridElement tpElem = GridElement.findById(isLong ? gridId + 1 : gridId - 1); |
| | | if (tpElem == null) { |
| | | log.warn("[Gate] 对手止盈挂单失败:未找到止损位"); |
| | | return; |
| | | } |
| | | int tpGridId = tpElem.getId(); |
| | | BigDecimal triggerPrice = tpElem.getGridPrice(); |
| | | String orderType = isLong ? ORDER_TYPE_CLOSE_LONG : ORDER_TYPE_CLOSE_SHORT; |
| | | FuturesPriceTrigger.RuleEnum rule = isLong ? FuturesPriceTrigger.RuleEnum.NUMBER_1 |
| | | : FuturesPriceTrigger.RuleEnum.NUMBER_2; |
| | | String size = isLong ? negate(String.valueOf(tpQty)) : String.valueOf(tpQty); |
| | | executor.placeTakeProfit(triggerPrice, rule, orderType, size, |
| | | profitId -> { |
| | | if (isLong) longTakeProfitTraderIdParam(tpElem, profitId, true); |
| | | else shortTakeProfitTraderIdParam(tpElem, profitId, true); |
| | | log.info("[Gate] 止损次数{}→对手{}止盈 gridId:{}, 量:{}, tpId:{}", |
| | | times, isLong ? "多仓" : "空仓", tpGridId, size, profitId); |
| | | } |
| | | ); |
| | | } |
| | | } |