| | |
| | | private final OkxConfig config; |
| | | private final OkxTradeExecutor executor; |
| | | private final OKXAccount okxAccount; |
| | | private final StopLossManager stopLossManager; |
| | | |
| | | private volatile StrategyState state = StrategyState.WAITING_KLINE; |
| | | |
| | |
| | | /** 基底空头是否已开 */ |
| | | private volatile boolean baseShortOpened = false; |
| | | |
| | | // ---- WS 订阅就绪标志 ---- |
| | | /** candle1m (Business WS) 订阅已确认 */ |
| | | private volatile boolean candle1mSubscribed = false; |
| | | /** positions (Private WS) 订阅已确认 */ |
| | | private volatile boolean positionsSubscribed = false; |
| | | /** orders (Private WS) 订阅已确认 */ |
| | | private volatile boolean ordersSubscribed = false; |
| | | /** 等待所有订阅就绪期间缓存的最新 K 线价格 */ |
| | | private volatile BigDecimal pendingKlinePrice = null; |
| | | |
| | | private volatile boolean shortActive = false; |
| | | private volatile boolean longActive = false; |
| | | |
| | |
| | | this.config = config; |
| | | this.okxAccount = okxAccount; |
| | | this.executor = new OkxTradeExecutor(okxAccount, config.getInstId(), config.getTdMode()); |
| | | this.stopLossManager = new StopLossManager(config, executor); |
| | | } |
| | | |
| | | // ---- 初始化 ---- |
| | |
| | | */ |
| | | public void init() { |
| | | try { |
| | | // 1. 查询账户获取初始本金 |
| | | // 1. 查询账户获取初始本金(仅取 USDT 合约账户余额) |
| | | String balanceResp = executor.getBalance(); |
| | | if (balanceResp != null) { |
| | | JSONObject json = JSON.parseObject(balanceResp); |
| | | if ("0".equals(json.getString("code"))) { |
| | | JSONArray data = json.getJSONArray("data"); |
| | | if (data != null && !data.isEmpty()) { |
| | | JSONObject detail = data.getJSONObject(0); |
| | | String totalEq = detail.getString("totalEq"); |
| | | if (totalEq != null) { |
| | | this.initialPrincipal = new BigDecimal(totalEq); |
| | | log.info("[OKX] 初始本金: {} USDT", initialPrincipal); |
| | | JSONObject accountData = data.getJSONObject(0); |
| | | JSONArray details = accountData.getJSONArray("details"); |
| | | if (details != null) { |
| | | for (int i = 0; i < details.size(); i++) { |
| | | JSONObject detail = details.getJSONObject(i); |
| | | if ("USDT".equals(detail.getString("ccy"))) { |
| | | String eq = detail.getString("eq"); |
| | | if (eq != null) { |
| | | this.initialPrincipal = new BigDecimal(eq); |
| | | log.info("[OKX] 初始本金(USDT合约): {} USDT", initialPrincipal); |
| | | } |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | baseShortOpened = false; |
| | | longActive = false; |
| | | shortActive = false; |
| | | candle1mSubscribed = false; |
| | | positionsSubscribed = false; |
| | | ordersSubscribed = false; |
| | | pendingKlinePrice = null; |
| | | shortPriceQueue.clear(); |
| | | longPriceQueue.clear(); |
| | | currentLongOrderIds.clear(); |
| | |
| | | if ("0".equals(json.getString("code"))) { |
| | | JSONArray data = json.getJSONArray("data"); |
| | | if (data != null && !data.isEmpty()) { |
| | | JSONObject detail = data.getJSONObject(0); |
| | | String totalEq = detail.getString("totalEq"); |
| | | if (totalEq != null) { |
| | | this.initialPrincipal = new BigDecimal(totalEq); |
| | | JSONObject accountData = data.getJSONObject(0); |
| | | JSONArray details = accountData.getJSONArray("details"); |
| | | if (details != null) { |
| | | for (int i = 0; i < details.size(); i++) { |
| | | JSONObject detail = details.getJSONObject(i); |
| | | if ("USDT".equals(detail.getString("ccy"))) { |
| | | String eq = detail.getString("eq"); |
| | | if (eq != null) { |
| | | this.initialPrincipal = new BigDecimal(eq); |
| | | } |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | updateUnrealizedPnl(); |
| | | |
| | | if (state == StrategyState.STOPPED) { |
| | | executor.cancelAllAlgoOrders(); |
| | | closeExistingPositions(); |
| | | // stopGrid() 已做清理,仅打印日志不重复操作 |
| | | BigDecimal totalPnl = cumulativePnl.add(unrealizedPnl); |
| | | log.info("[OKX] 已实现:{}, 未实现:{}, 合计:{}", cumulativePnl, unrealizedPnl, totalPnl); |
| | | startGrid(); |
| | | return; |
| | | } |
| | | |
| | | if (state == StrategyState.WAITING_KLINE) { |
| | | // 等待所有 WS 订阅就绪后再开仓 |
| | | if (!allSubscriptionsReady()) { |
| | | pendingKlinePrice = closePrice; |
| | | log.info("[OKX] 等待所有 WS 订阅就绪(candle1m={}, positions={}, orders={}), 当前价: {}", |
| | | candle1mSubscribed, positionsSubscribed, ordersSubscribed, closePrice); |
| | | return; |
| | | } |
| | | state = StrategyState.OPENING; |
| | | log.info("[OKX] 首根K线到达,开基底仓位 多空各{}张...", config.getBaseQuantity()); |
| | | executor.openLong(config.getBaseQuantity(), (orderId) -> { |
| | | OkxTraderParam baseLongTp = OkxTraderParam.builder().entryOrderId(orderId).build(); |
| | | config.setBaseLongTraderParam(baseLongTp); |
| | | }, null); |
| | | executor.openShort(config.getBaseQuantity(), (orderId) -> { |
| | | OkxTraderParam baseShortTp = OkxTraderParam.builder().entryOrderId(orderId).build(); |
| | | config.setBaseShortTraderParam(baseShortTp); |
| | | }, null); |
| | | log.info("[OKX] 首根K线到达,所有订阅就绪,开基底仓位 多空各{}张...", config.getBaseQuantity()); |
| | | openBasePositions(); |
| | | return; |
| | | } |
| | | |
| | |
| | | return; |
| | | } |
| | | checkProfitAndReset(); |
| | | } |
| | | |
| | | /** |
| | | * WS 订阅成功确认回调,由各 {@link OkxGridChannelHandler#onSubscribed()} 触发。 |
| | | * 当所有订阅就绪且已有缓存的 K 线价格时,自动触发开仓。 |
| | | */ |
| | | public void onSubscriptionConfirmed(String channel) { |
| | | if ("candle1m".equals(channel)) candle1mSubscribed = true; |
| | | else if ("positions".equals(channel)) positionsSubscribed = true; |
| | | else if ("orders".equals(channel)) ordersSubscribed = true; |
| | | |
| | | log.info("[OKX] 订阅就绪: {}, 全部就绪: {}", channel, allSubscriptionsReady()); |
| | | |
| | | // 所有订阅就绪 + 有缓存 K 线价格 + 仍处于等待状态 → 触发开仓 |
| | | if (allSubscriptionsReady() && pendingKlinePrice != null && state == StrategyState.WAITING_KLINE) { |
| | | BigDecimal price = pendingKlinePrice; |
| | | pendingKlinePrice = null; |
| | | log.info("[OKX] 所有 WS 订阅就绪,触发开仓, 价格: {}", price); |
| | | state = StrategyState.OPENING; |
| | | log.info("[OKX] 首根K线到达,所有订阅就绪,开基底仓位 多空各{}张...", config.getBaseQuantity()); |
| | | openBasePositions(); |
| | | } |
| | | } |
| | | |
| | | /** @return true 表示 candle1m + positions + orders 三个频道均已订阅成功 */ |
| | | private boolean allSubscriptionsReady() { |
| | | return candle1mSubscribed && positionsSubscribed && ordersSubscribed; |
| | | } |
| | | |
| | | /** 市价双开基底仓位(多 + 空),对齐 Gate 版本逻辑 */ |
| | | private void openBasePositions() { |
| | | executor.openLong(config.getBaseQuantity(), (orderId) -> { |
| | | OkxTraderParam baseLongTp = OkxTraderParam.builder().entryOrderId(orderId).build(); |
| | | config.setBaseLongTraderParam(baseLongTp); |
| | | tryGenerateQueues(); |
| | | }, null); |
| | | executor.openShort(config.getBaseQuantity(), (orderId) -> { |
| | | OkxTraderParam baseShortTp = OkxTraderParam.builder().entryOrderId(orderId).build(); |
| | | config.setBaseShortTraderParam(baseShortTp); |
| | | tryGenerateQueues(); |
| | | }, null); |
| | | } |
| | | |
| | | // ---- 仓位推送回调 ---- |
| | |
| | | baseLongOpened = true; |
| | | log.info("[OKX] 基底多成交价: {}", longBaseEntryPrice); |
| | | tryGenerateQueues(); |
| | | } else { |
| | | longPositionSize = posSize; |
| | | } |
| | | } else { |
| | | longPositionSize = posSize; |
| | | tryGenerateQueues(); // 后续 WS 推送触发重试,兜底此前 NPE 失败的情况 |
| | | } |
| | | } else { |
| | | if (longActive && state == StrategyState.ACTIVE) { |
| | | log.info("[OKX] 多仓持仓归零,重置策略"); |
| | | handlePositionZeroAndReset("多仓"); |
| | | } |
| | | longActive = false; |
| | |
| | | baseShortOpened = true; |
| | | log.info("[OKX] 基底空成交价: {}", shortBaseEntryPrice); |
| | | tryGenerateQueues(); |
| | | } else { |
| | | shortPositionSize = posSize; |
| | | } |
| | | } else { |
| | | shortPositionSize = posSize; |
| | | tryGenerateQueues(); // 后续 WS 推送触发重试,兜底此前 NPE 失败的情况 |
| | | } |
| | | } else { |
| | | if (shortActive && state == StrategyState.ACTIVE) { |
| | | log.info("[OKX] 空仓持仓归零,重置策略"); |
| | | handlePositionZeroAndReset("空仓"); |
| | | } |
| | | shortActive = false; |
| | |
| | | // ---- 订单/条件单推送回调 ---- |
| | | |
| | | public void onOrderUpdate(String algoId, String state, String ordType) { |
| | | if (!"effective".equals(state) && !"canceled".equals(state)) { |
| | | if (!"filled".equals(state) && !"canceled".equals(state)) { |
| | | return; |
| | | } |
| | | |
| | | // 匹配止损单 |
| | | // 匹配止损单 → 委托 StopLossManager |
| | | OkxGridElement byLongStopLoss = OkxGridElement.findByLongStopLossOrderId(algoId); |
| | | if (byLongStopLoss != null) { |
| | | handleLongStopLossTriggered(byLongStopLoss); |
| | | stopLossManager.handleLongStopLossTriggered(byLongStopLoss, longEntryPrice); |
| | | return; |
| | | } |
| | | OkxGridElement byShortStopLoss = OkxGridElement.findByShortStopLossOrderId(algoId); |
| | | if (byShortStopLoss != null) { |
| | | handleShortStopLossTriggered(byShortStopLoss); |
| | | stopLossManager.handleShortStopLossTriggered(byShortStopLoss, shortEntryPrice); |
| | | return; |
| | | } |
| | | |
| | | // 匹配挂单 —— 条件单成交后:清空挂单状态 + 追挂止损 + 挂止盈单 |
| | | // 匹配挂单 —— 条件单成交后:清空挂单状态 + 追挂止损 |
| | | OkxGridElement shortGridElement = OkxGridElement.findByShortOrderId(algoId); |
| | | if (shortGridElement != null && shortGridElement.isHasShortOrder()) { |
| | | int filledQty = Integer.parseInt(shortGridElement.getShortTraderParam().getQuantity()); |
| | | shortEntryTraderIdParam(shortGridElement, null, false); |
| | | extendShortStopLoss(filledQty); |
| | | stopLossManager.clearShortEntryState(shortGridElement); |
| | | stopLossManager.extendShortStopLoss(filledQty); |
| | | log.info("[OKX] 空单成交 gridId:{}, qty:{}, 追挂止损", shortGridElement.getId(), filledQty); |
| | | return; |
| | | } |
| | | OkxGridElement longGridElement = OkxGridElement.findByLongOrderId(algoId); |
| | | if (longGridElement != null && longGridElement.isHasLongOrder()) { |
| | | int filledQty = Integer.parseInt(longGridElement.getLongTraderParam().getQuantity()); |
| | | longEntryTraderIdParam(longGridElement, null, false); |
| | | extendLongStopLoss(filledQty); |
| | | stopLossManager.clearLongEntryState(longGridElement); |
| | | stopLossManager.extendLongStopLoss(filledQty); |
| | | log.info("[OKX] 多单成交 gridId:{}, qty:{}, 追挂止损", longGridElement.getId(), filledQty); |
| | | return; |
| | | } |
| | |
| | | // ---- 网格队列处理 ---- |
| | | |
| | | private void tryGenerateQueues() { |
| | | // 防止重复执行:一旦已进入 ACTIVE 状态不再重复初始化 |
| | | if (state == StrategyState.ACTIVE) { |
| | | return; |
| | | } |
| | | if (baseLongOpened && baseShortOpened) { |
| | | generateShortQueue(); |
| | | generateLongQueue(); |
| | | updateGridElements(); |
| | | // 防御异步竞态:openLong/openShort 的回调可能还未设置 trader param |
| | | OkxTraderParam baseLongTp = config.getBaseLongTraderParam(); |
| | | OkxTraderParam baseShortTp = config.getBaseShortTraderParam(); |
| | | if (baseLongTp == null || baseShortTp == null) { |
| | | log.warn("[OKX] tryGenerateQueues 等待异步回调: longTp={}, shortTp={}", |
| | | baseLongTp != null, baseShortTp != null); |
| | | return; |
| | | } |
| | | |
| | | // 委托 GridQueueBuilder 构建价格队列 + GridElements |
| | | List<BigDecimal> tmpShort = GridQueueBuilder.buildShortQueue(config, shortBaseEntryPrice); |
| | | List<BigDecimal> tmpLong = GridQueueBuilder.buildLongQueue(config, shortBaseEntryPrice); |
| | | synchronized (shortPriceQueue) { |
| | | shortPriceQueue.clear(); |
| | | shortPriceQueue.addAll(tmpShort); |
| | | } |
| | | synchronized (longPriceQueue) { |
| | | longPriceQueue.clear(); |
| | | longPriceQueue.addAll(tmpLong); |
| | | } |
| | | GridQueueBuilder.buildGridElements(config, shortPriceQueue, longPriceQueue, shortBaseEntryPrice); |
| | | |
| | | // 标记基座挂单 |
| | | OkxGridElement baseGridElement = OkxGridElement.findById(0); |
| | | OkxTraderParam baseLongTp = config.getBaseLongTraderParam(); |
| | | baseGridElement.setLongOrderId(baseLongTp.getEntryOrderId()); |
| | | baseGridElement.setHasLongOrder(true); |
| | | OkxTraderParam baseShortTp = config.getBaseShortTraderParam(); |
| | | baseGridElement.setShortOrderId(baseShortTp.getEntryOrderId()); |
| | | baseGridElement.setHasShortOrder(true); |
| | | |
| | | // 挂多仓止损 (id=-2 到 -11) |
| | | for (int id = -2; id >= -11; id--) { |
| | | OkxGridElement elem = OkxGridElement.findById(id); |
| | | if (elem == null) continue; |
| | | BigDecimal triggerPrice = elem.getGridPrice(); |
| | | int finalId = id; |
| | | executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", "1", |
| | | profitId -> { |
| | | elem.setLongStopLossOrderId(profitId); |
| | | OkxGridElement.refreshIndices(); |
| | | log.info("[OKX] 多仓止损已挂, gridId:{}, 触发价:{}, stopLossId:{}", finalId, triggerPrice, profitId); |
| | | }); |
| | | } |
| | | |
| | | // 挂空仓止损 (id=2 到 11) |
| | | for (int id = 2; id <= 11; id++) { |
| | | OkxGridElement elem = OkxGridElement.findById(id); |
| | | if (elem == null) continue; |
| | | BigDecimal triggerPrice = elem.getGridPrice(); |
| | | int finalId = id; |
| | | executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", "1", |
| | | profitId -> { |
| | | elem.setShortStopLossOrderId(profitId); |
| | | OkxGridElement.refreshIndices(); |
| | | log.info("[OKX] 空仓止损已挂, gridId:{}, 触发价:{}, stopLossId:{}", finalId, triggerPrice, profitId); |
| | | }); |
| | | } |
| | | |
| | | log.info("[OKX] 止损单已全部挂完, 空仓止损: 2~11, 多仓止损: -2~-11"); |
| | | // 委托 StopLossManager 挂止损单 |
| | | stopLossManager.setupBaseStopLosses(); |
| | | state = StrategyState.ACTIVE; |
| | | } |
| | | } |
| | | |
| | | private void generateShortQueue() { |
| | | shortPriceQueue.clear(); |
| | | int prec = config.getPriceScale(); |
| | | BigDecimal step = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(prec, RoundingMode.HALF_UP); |
| | | config.setStep(step); |
| | | BigDecimal elem = shortBaseEntryPrice.subtract(step).setScale(prec, RoundingMode.HALF_UP); |
| | | for (int i = 0; i < config.getGridQueueSize(); i++) { |
| | | shortPriceQueue.add(elem); |
| | | elem = elem.subtract(step).setScale(prec, RoundingMode.HALF_UP); |
| | | if (elem.compareTo(BigDecimal.ZERO) <= 0) break; |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | log.info("[OKX] 空队列:{}", shortPriceQueue); |
| | | } |
| | | |
| | | private void generateLongQueue() { |
| | | longPriceQueue.clear(); |
| | | int prec = config.getPriceScale(); |
| | | BigDecimal step = config.getStep(); |
| | | BigDecimal elem = shortBaseEntryPrice.add(step).setScale(prec, RoundingMode.HALF_UP); |
| | | for (int i = 0; i < config.getGridQueueSize(); i++) { |
| | | longPriceQueue.add(elem); |
| | | elem = elem.add(step).setScale(prec, RoundingMode.HALF_UP); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | log.info("[OKX] 多队列:{}", longPriceQueue); |
| | | } |
| | | |
| | | private void updateGridElements() { |
| | | List<OkxGridElement> elements = new ArrayList<>(); |
| | | int shortSize = shortPriceQueue.size(); |
| | | int longSize = longPriceQueue.size(); |
| | | int prec = config.getPriceScale(); |
| | | BigDecimal step = config.getStep(); |
| | | String qty = config.getQuantity(); |
| | | |
| | | // 空仓队列: id=-1, -2, ... |
| | | for (int i = 0; i < shortSize; i++) { |
| | | int id = -(i + 1); |
| | | Integer upId = (i == 0) ? 0 : id + 1; |
| | | Integer downId = (i == shortSize - 1) ? null : id - 1; |
| | | BigDecimal price = shortPriceQueue.get(i); |
| | | OkxTraderParam longParam = OkxTraderParam.builder() |
| | | .direction(OkxTraderParam.Direction.LONG) |
| | | .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); |
| | | OkxTraderParam shortParam = OkxTraderParam.builder() |
| | | .direction(OkxTraderParam.Direction.SHORT) |
| | | .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); |
| | | elements.add(OkxGridElement.builder().id(id).gridPrice(price).upId(upId).downId(downId) |
| | | .longTraderParam(longParam).shortTraderParam(shortParam).build()); |
| | | } |
| | | |
| | | // 位置 0: 基底价格 |
| | | { |
| | | BigDecimal price = shortBaseEntryPrice; |
| | | OkxTraderParam longParam = OkxTraderParam.builder() |
| | | .direction(OkxTraderParam.Direction.LONG) |
| | | .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); |
| | | OkxTraderParam shortParam = OkxTraderParam.builder() |
| | | .direction(OkxTraderParam.Direction.SHORT) |
| | | .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); |
| | | elements.add(OkxGridElement.builder().id(0).gridPrice(price) |
| | | .upId(shortSize > 0 ? 1 : null).downId(longSize > 0 ? -1 : null) |
| | | .longTraderParam(longParam).shortTraderParam(shortParam).build()); |
| | | } |
| | | |
| | | // 多仓队列: id=1, 2, ... |
| | | for (int i = 0; i < longSize; i++) { |
| | | int id = i + 1; |
| | | Integer downId = (i == 0) ? 0 : id - 1; |
| | | Integer upId = (i == longSize - 1) ? null : id + 1; |
| | | BigDecimal price = longPriceQueue.get(i); |
| | | OkxTraderParam longParam = OkxTraderParam.builder() |
| | | .direction(OkxTraderParam.Direction.LONG) |
| | | .entryPrice(price).takeProfitPrice(price.add(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); |
| | | OkxTraderParam shortParam = OkxTraderParam.builder() |
| | | .direction(OkxTraderParam.Direction.SHORT) |
| | | .entryPrice(price).takeProfitPrice(price.subtract(step).setScale(prec, RoundingMode.HALF_UP)).quantity(qty).build(); |
| | | elements.add(OkxGridElement.builder().id(id).gridPrice(price).upId(upId).downId(downId) |
| | | .longTraderParam(longParam).shortTraderParam(shortParam).build()); |
| | | } |
| | | |
| | | config.setGridElements(elements); |
| | | log.info("[OKX] 网格元素列表已构建, 共{}个元素", elements.size()); |
| | | } |
| | | |
| | | // ---- 止损触发处理 ---- |
| | | |
| | | /** |
| | | * 多仓止损触发处理(Gate 模式逐步缩进)。 |
| | | * 止损触发后向基底方向缩进 1 格挂条件多单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。 |
| | | * 若 N>2,先取消上一步的旧挂单。 |
| | | */ |
| | | private void handleLongStopLossTriggered(OkxGridElement gridElement) { |
| | | int gridId = gridElement.getId(); |
| | | int N = Math.abs(gridId); |
| | | gridElement.setLongStopLossOrderId(null); |
| | | log.info("[OKX] 多仓止损触发 gridId:{}, 逐步缩进", gridId); |
| | | |
| | | int newEntryGridId = -(N - 1); |
| | | OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId); |
| | | if (newEntryGrid == null) { |
| | | OkxGridElement.refreshIndices(); |
| | | log.warn("[OKX] 多仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId); |
| | | return; |
| | | } |
| | | |
| | | if (N > 2) { |
| | | int cancelGridId = -(N - 2); |
| | | OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId); |
| | | if (cancelGrid != null && cancelGrid.isHasLongOrder()) { |
| | | executor.cancelAlgoOrder(cancelGrid.getLongOrderId(), oid -> { |
| | | longEntryTraderIdParam(cancelGrid, null, false); |
| | | log.info("[OKX] 多仓止损触发, 取消gridId:{}的多单", cancelGridId); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | BigDecimal triggerPrice = newEntryGrid.getGridPrice(); |
| | | BigDecimal priceDiff = longEntryPrice.subtract(triggerPrice).abs(); |
| | | int entryQty = priceDiff.divide(config.getStep(), 0, RoundingMode.DOWN).intValue(); |
| | | entryQty = Math.max(1, entryQty); |
| | | String size = String.valueOf(entryQty); |
| | | log.info("[OKX] 多仓止损触发 gridId:{}, 在gridId:{}挂{}张多单", gridId, newEntryGridId, entryQty); |
| | | newEntryGrid.getLongTraderParam().setQuantity(size); |
| | | placeEntryOrderWithPreFlag(newEntryGrid, true, triggerPrice, size); |
| | | } |
| | | |
| | | /** |
| | | * 空仓止损触发处理(Gate 模式逐步缩进)。 |
| | | * 止损触发后向基底方向缩进 1 格挂条件空单,数量 = |触发价 - 当前持仓均价| / 网格步长,取整。 |
| | | * 若 N>2,先取消上一步的旧挂单。 |
| | | */ |
| | | private void handleShortStopLossTriggered(OkxGridElement gridElement) { |
| | | int gridId = gridElement.getId(); |
| | | int N = gridId; |
| | | gridElement.setShortStopLossOrderId(null); |
| | | log.info("[OKX] 空仓止损触发 gridId:{}, 逐步缩进", gridId); |
| | | |
| | | int newEntryGridId = N - 1; |
| | | OkxGridElement newEntryGrid = OkxGridElement.findById(newEntryGridId); |
| | | if (newEntryGrid == null) { |
| | | OkxGridElement.refreshIndices(); |
| | | log.warn("[OKX] 空仓止损触发 gridId:{} 找不到入单网格({})", gridId, newEntryGridId); |
| | | return; |
| | | } |
| | | |
| | | if (N > 2) { |
| | | int cancelGridId = N - 2; |
| | | OkxGridElement cancelGrid = OkxGridElement.findById(cancelGridId); |
| | | if (cancelGrid != null && cancelGrid.isHasShortOrder()) { |
| | | executor.cancelAlgoOrder(cancelGrid.getShortOrderId(), oid -> { |
| | | shortEntryTraderIdParam(cancelGrid, null, false); |
| | | log.info("[OKX] 空仓止损触发, 取消gridId:{}的空单", cancelGridId); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | BigDecimal triggerPrice = newEntryGrid.getGridPrice(); |
| | | BigDecimal priceDiff = shortEntryPrice.subtract(triggerPrice).abs(); |
| | | int entryQty = priceDiff.divide(config.getStep(), 0, RoundingMode.DOWN).intValue(); |
| | | entryQty = Math.max(1, entryQty); |
| | | String size = String.valueOf(entryQty); |
| | | log.info("[OKX] 空仓止损触发 gridId:{}, 在gridId:{}挂{}张空单", gridId, newEntryGridId, entryQty); |
| | | newEntryGrid.getShortTraderParam().setQuantity(size); |
| | | placeEntryOrderWithPreFlag(newEntryGrid, false, triggerPrice, size); |
| | | } |
| | | |
| | | private void extendLongStopLoss(int filledQty) { |
| | | int furthestSlId = 0; |
| | | for (OkxGridElement e : config.getGridElements()) { |
| | | if (e.getLongStopLossOrderId() != null && e.getId() < furthestSlId) { |
| | | furthestSlId = e.getId(); |
| | | } |
| | | } |
| | | if (furthestSlId == 0) furthestSlId = -11; |
| | | log.info("[OKX] 多仓追挂止损, 当前最远止损gridId:{}, 追加{}张", furthestSlId, filledQty); |
| | | for (int i = 0; i < filledQty; i++) { |
| | | int newSlId = furthestSlId - i - 1; |
| | | OkxGridElement elem = OkxGridElement.findById(newSlId); |
| | | if (elem == null) continue; |
| | | BigDecimal triggerPrice = elem.getGridPrice(); |
| | | int finalSlId = newSlId; |
| | | executor.placeTakeProfit(triggerPrice.toString(), "sell", "long", "1", |
| | | profitId -> { |
| | | elem.setLongStopLossOrderId(profitId); |
| | | OkxGridElement.refreshIndices(); |
| | | log.info("[OKX] 多仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | private void extendShortStopLoss(int filledQty) { |
| | | int furthestSlId = 0; |
| | | for (OkxGridElement e : config.getGridElements()) { |
| | | if (e.getShortStopLossOrderId() != null && e.getId() > furthestSlId) { |
| | | furthestSlId = e.getId(); |
| | | } |
| | | } |
| | | if (furthestSlId == 0) furthestSlId = 11; |
| | | log.info("[OKX] 空仓追挂止损, 当前最远止损gridId:{}, 追加{}张", furthestSlId, filledQty); |
| | | for (int i = 0; i < filledQty; i++) { |
| | | int newSlId = furthestSlId + i + 1; |
| | | OkxGridElement elem = OkxGridElement.findById(newSlId); |
| | | if (elem == null) continue; |
| | | BigDecimal triggerPrice = elem.getGridPrice(); |
| | | int finalSlId = newSlId; |
| | | executor.placeTakeProfit(triggerPrice.toString(), "buy", "short", "1", |
| | | profitId -> { |
| | | elem.setShortStopLossOrderId(profitId); |
| | | OkxGridElement.refreshIndices(); |
| | | log.info("[OKX] 空仓止损追加, gridId:{}, 触发价:{}, stopLossId:{}", finalSlId, triggerPrice, profitId); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | // ---- 辅助方法 ---- |
| | | |
| | | private void longTakeProfitTraderIdParam(OkxGridElement baseElement, String profitId, boolean flag) { |
| | | OkxTraderParam tp = baseElement.getLongTraderParam(); |
| | | tp.setTakeProfitOrderId(profitId); |
| | | tp.setTakeProfitPlaced(flag); |
| | | baseElement.setLongTakeProfitOrderId(profitId); |
| | | OkxGridElement.refreshIndices(); |
| | | } |
| | | |
| | | private void shortTakeProfitTraderIdParam(OkxGridElement baseElement, String profitId, boolean flag) { |
| | | OkxTraderParam tp = baseElement.getShortTraderParam(); |
| | | tp.setTakeProfitOrderId(profitId); |
| | | tp.setTakeProfitPlaced(flag); |
| | | baseElement.setShortTakeProfitOrderId(profitId); |
| | | OkxGridElement.refreshIndices(); |
| | | } |
| | | |
| | | private void longEntryTraderIdParam(OkxGridElement baseElement, String entryId, boolean flag) { |
| | | OkxTraderParam tp = baseElement.getLongTraderParam(); |
| | | tp.setEntryOrderId(entryId); |
| | | tp.setEntryOrderPlaced(flag); |
| | | baseElement.setHasLongOrder(flag); |
| | | baseElement.setLongOrderId(entryId); |
| | | OkxGridElement.refreshIndices(); |
| | | } |
| | | |
| | | private void shortEntryTraderIdParam(OkxGridElement baseElement, String entryId, boolean flag) { |
| | | OkxTraderParam tp = baseElement.getShortTraderParam(); |
| | | tp.setEntryOrderId(entryId); |
| | | tp.setEntryOrderPlaced(flag); |
| | | baseElement.setHasShortOrder(flag); |
| | | baseElement.setShortOrderId(entryId); |
| | | OkxGridElement.refreshIndices(); |
| | | } |
| | | |
| | | private void placeEntryOrderWithPreFlag(OkxGridElement gridElement, boolean isLong, |
| | | BigDecimal triggerPrice, String size) { |
| | | if (isLong) { |
| | | gridElement.setHasLongOrder(true); |
| | | } else { |
| | | gridElement.setHasShortOrder(true); |
| | | } |
| | | String side = isLong ? "buy" : "sell"; |
| | | String posSide = isLong ? "long" : "short"; |
| | | executor.placeConditionalEntryOrder(triggerPrice.toString(), side, posSide, size, |
| | | orderId -> { |
| | | if (isLong) { |
| | | longEntryTraderIdParam(gridElement, orderId, true); |
| | | } else { |
| | | shortEntryTraderIdParam(gridElement, orderId, true); |
| | | } |
| | | }, |
| | | () -> { |
| | | if (isLong) { |
| | | gridElement.setHasLongOrder(false); |
| | | gridElement.setLongOrderId(null); |
| | | } else { |
| | | gridElement.setHasShortOrder(false); |
| | | gridElement.setShortOrderId(null); |
| | | } |
| | | OkxGridElement.refreshIndices(); |
| | | log.warn("[OKX] 条件单创建失败,回滚标志位 gridId:{}, isLong:{}", gridElement.getId(), isLong); |
| | | } |
| | | ); |
| | | } |
| | | // ---- 盈亏计算 ---- |
| | | |
| | | private void updateUnrealizedPnl() { |
| | | if (lastKlinePrice == null || lastKlinePrice.compareTo(BigDecimal.ZERO) == 0) return; |
| | |
| | | state = StrategyState.STOPPED; |
| | | closeExistingPositions(); |
| | | executor.cancelAllAlgoOrders(); |
| | | startGrid(); |
| | | // 提交到 executor 末尾:单线程FIFO保证前面所有平仓/取消任务完成后才重置 |
| | | executor.submitTask(() -> { |
| | | try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } |
| | | startGrid(); |
| | | }); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("[OKX] 盈亏检查失败", e); |
| | |
| | | } |
| | | |
| | | private void handlePositionZeroAndReset(String direction) { |
| | | log.info("[OKX] {}持仓归零,重置策略", direction); |
| | | state = StrategyState.STOPPED; |
| | | executor.cancelAllAlgoOrders(); |
| | | closeExistingPositions(); |
| | | startGrid(); |
| | | // 提交到 executor 末尾:FIFO保证平仓完成后再重置 |
| | | executor.submitTask(() -> { |
| | | try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } |
| | | startGrid(); |
| | | }); |
| | | } |
| | | |
| | | // ---- getters ---- |