feat(okxApi): 添加仓位模式配置和REST客户端功能
- 在OkxConfig中添加posMode字段及相关getter和builder方法
- 添加getRestBaseUrl方法用于获取REST API基础地址
- 在OkxEnums中添加条件订单类型和计划平仓订单类型常量
- 创建OkxRestClient类实现 setPositionMode 和 setLeverage 功能
- 重构OkxGridTradeService中的网格队列生成逻辑,使用固定步长计算
- 提取replenishOwnQueue和transferBetweenQueues方法优化队列补货逻辑
- 更新止盈单逻辑使用条件订单类型并通过algo接口发送
- 在OkxWebSocketClientManager中添加setupAccount方法配置账户参数
- 优化OkxWsUtil中的时间格式化和随机数生成性能
7 files modified
2 files added
| | |
| | | private final String contract; |
| | | private final String leverage; |
| | | private final String marginMode; |
| | | private final String posMode; |
| | | private final BigDecimal gridRate; |
| | | private final BigDecimal overallTp; |
| | | private final BigDecimal maxLoss; |
| | |
| | | this.contract = builder.contract; |
| | | this.leverage = builder.leverage; |
| | | this.marginMode = builder.marginMode; |
| | | this.posMode = builder.posMode; |
| | | this.gridRate = builder.gridRate; |
| | | this.overallTp = builder.overallTp; |
| | | this.maxLoss = builder.maxLoss; |
| | |
| | | : "wss://wspap.okx.com:8443/ws/v5/private"; |
| | | } |
| | | |
| | | // ==================== REST 地址 ==================== |
| | | |
| | | public String getRestBaseUrl() { |
| | | return isProduction |
| | | ? "https://www.okx.com" |
| | | : "https://www.okx.cab"; |
| | | } |
| | | |
| | | // ==================== 认证信息 ==================== |
| | | |
| | | public String getApiKey() { return apiKey; } |
| | |
| | | // ==================== 持仓配置 ==================== |
| | | |
| | | public String getMarginMode() { return marginMode; } |
| | | public String getPosMode() { return posMode; } |
| | | |
| | | // ==================== 策略参数 ==================== |
| | | |
| | |
| | | private String contract = "BTC-USDT-SWAP"; |
| | | private String leverage = "100"; |
| | | private String marginMode = "cross"; |
| | | private String posMode = "long_short_mode"; |
| | | private BigDecimal gridRate = new BigDecimal("0.0035"); |
| | | private BigDecimal overallTp = new BigDecimal("5"); |
| | | private BigDecimal maxLoss = new BigDecimal("15"); |
| | |
| | | public Builder contract(String contract) { this.contract = contract; return this; } |
| | | public Builder leverage(String leverage) { this.leverage = leverage; return this; } |
| | | public Builder marginMode(String marginMode) { this.marginMode = marginMode; return this; } |
| | | public Builder posMode(String posMode) { this.posMode = posMode; return this; } |
| | | public Builder gridRate(BigDecimal gridRate) { this.gridRate = gridRate; return this; } |
| | | public Builder overallTp(BigDecimal overallTp) { this.overallTp = overallTp; return this; } |
| | | public Builder maxLoss(BigDecimal maxLoss) { this.maxLoss = maxLoss; return this; } |
| | |
| | | import java.math.RoundingMode; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Comparator; |
| | | import java.util.List; |
| | | |
| | | @Slf4j |
| | |
| | | WAITING_KLINE, OPENING, ACTIVE, STOPPED |
| | | } |
| | | |
| | | private static final String ORDER_TYPE_CLOSE_LONG = "plan-close-long-position"; |
| | | private static final String ORDER_TYPE_CLOSE_SHORT = "plan-close-short-position"; |
| | | |
| | | private final String accountName; |
| | | private final OkxConfig config; |
| | | private final OkxTradeExecutor executor; |
| | | |
| | |
| | | |
| | | public OkxGridTradeService(OkxConfig config, String accountName) { |
| | | this.config = config; |
| | | this.accountName = accountName; |
| | | this.executor = new OkxTradeExecutor(config.getContract(), config.getMarginMode(), accountName); |
| | | } |
| | | |
| | |
| | | log.warn("[{}] 多仓队列为空,无法设止盈", config.getContract()); |
| | | } else { |
| | | BigDecimal tpPrice = longPriceQueue.get(0); |
| | | executor.placeTakeProfit(tpPrice, ORDER_TYPE_CLOSE_LONG, config.getQuantity()); |
| | | executor.placeTakeProfit(tpPrice, OkxEnums.ORDER_TYPE_CLOSE_LONG, config.getQuantity()); |
| | | log.info("[{}] 多单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity()); |
| | | } |
| | | } else { |
| | |
| | | log.warn("[{}] 空仓队列为空,无法设止盈", config.getContract()); |
| | | } else { |
| | | BigDecimal tpPrice = shortPriceQueue.get(0); |
| | | executor.placeTakeProfit(tpPrice, ORDER_TYPE_CLOSE_SHORT, config.getQuantity()); |
| | | executor.placeTakeProfit(tpPrice, OkxEnums.ORDER_TYPE_CLOSE_SHORT, config.getQuantity()); |
| | | log.info("[{}] 空单止盈已设, entry:{}, tp:{}, size:{}", config.getContract(), entryPrice, tpPrice, config.getQuantity()); |
| | | } |
| | | } else { |
| | |
| | | |
| | | private void generateShortQueue() { |
| | | shortPriceQueue.clear(); |
| | | BigDecimal step = config.getGridRate(); |
| | | for (int i = 1; i <= config.getGridQueueSize(); i++) { |
| | | shortPriceQueue.add(shortBaseEntryPrice.multiply(BigDecimal.ONE.subtract(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP)); |
| | | BigDecimal fixedStep = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal prev = shortBaseEntryPrice; |
| | | for (int i = 0; i < config.getGridQueueSize(); i++) { |
| | | prev = prev.subtract(fixedStep).setScale(1, RoundingMode.HALF_UP); |
| | | shortPriceQueue.add(prev); |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | log.info("[{}] 空队列:{}", config.getContract(), shortPriceQueue); |
| | | log.info("[{}] 空队列:{} 步长:{}", config.getContract(), shortPriceQueue, fixedStep); |
| | | } |
| | | |
| | | private void generateLongQueue() { |
| | | longPriceQueue.clear(); |
| | | BigDecimal step = config.getGridRate(); |
| | | for (int i = 1; i <= config.getGridQueueSize(); i++) { |
| | | longPriceQueue.add(longBaseEntryPrice.multiply(BigDecimal.ONE.add(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP)); |
| | | BigDecimal fixedStep = longBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal prev = longBaseEntryPrice; |
| | | for (int i = 0; i < config.getGridQueueSize(); i++) { |
| | | prev = prev.add(fixedStep).setScale(1, RoundingMode.HALF_UP); |
| | | longPriceQueue.add(prev); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | log.info("[{}] 多队列:{}", config.getContract(), longPriceQueue); |
| | | Collections.sort(longPriceQueue); |
| | | log.info("[{}] 多队列:{} 步长:{}", config.getContract(), longPriceQueue, fixedStep); |
| | | } |
| | | |
| | | /** |
| | |
| | | * <h3>执行流程</h3> |
| | | * <ol> |
| | | * <li>匹配队列元素 → 为空则直接返回</li> |
| | | * <li>空仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 − gridRate) 循环递减)</li> |
| | | * <li>多仓队列:以多仓队列首元素(最小价)为种子,生成 matched.size() 个递减元素加入</li> |
| | | * <li>空仓队列:移除 matched 元素,尾部以固定步长(shortBasePrice × gridRate)递减补充新元素</li> |
| | | * <li>多仓队列:以多仓队列首元素(最小价)为种子,以多仓固定步长递减加入新元素</li> |
| | | * <li>保证金检查 → 安全则开空一次</li> |
| | | * <li>额外反向开多:若多仓均价 > 空仓均价 且 当前价夹在中间且远离多仓均价</li> |
| | | * </ol> |
| | |
| | | } |
| | | log.info("[{}] 空仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), currentPrice); |
| | | |
| | | synchronized (shortPriceQueue) { |
| | | shortPriceQueue.removeAll(matched); |
| | | BigDecimal min = shortPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : shortPriceQueue.get(shortPriceQueue.size() - 1); |
| | | BigDecimal step = config.getGridRate(); |
| | | for (int i = 0; i < matched.size(); i++) { |
| | | min = min.multiply(BigDecimal.ONE.subtract(step)).setScale(1, RoundingMode.HALF_UP); |
| | | shortPriceQueue.add(min); |
| | | log.info("[{}] 空队列增加:{}", config.getContract(), min); |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | log.info("[{}] 现空队列:{}", config.getContract(), shortPriceQueue); |
| | | } |
| | | BigDecimal shortFixedStep = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal longFixedStep = longBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | replenishOwnQueue(shortPriceQueue, matched, shortFixedStep, true, "空"); |
| | | |
| | | synchronized (longPriceQueue) { |
| | | BigDecimal first = longPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : longPriceQueue.get(0); |
| | | BigDecimal step = config.getGridRate(); |
| | | for (int i = 1; i <= matched.size(); i++) { |
| | | BigDecimal elem = first.multiply(BigDecimal.ONE.subtract(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP); |
| | | if (longEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && currentPrice.subtract(longEntryPrice).abs().compareTo(longEntryPrice.multiply(step)) < 0) { |
| | | log.info("[{}] 多队列跳过(price≈longEntry):{}", config.getContract(), elem); |
| | | continue; |
| | | } |
| | | longPriceQueue.add(elem); |
| | | log.info("[{}] 多队列增加:{}", config.getContract(), elem); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | while (longPriceQueue.size() > config.getGridQueueSize()) { |
| | | longPriceQueue.remove(longPriceQueue.size() - 1); |
| | | } |
| | | log.info("[{}] 现多队列:{}", config.getContract(), longPriceQueue); |
| | | } |
| | | transferBetweenQueues(longPriceQueue, matched, matched.get(matched.size() - 1), |
| | | longFixedStep, false, longEntryPrice, BigDecimal::compareTo, "多", currentPrice); |
| | | |
| | | if (!isMarginSafe()) { |
| | | log.warn("[{}] 保证金超限,跳过空单开仓", config.getContract()); |
| | |
| | | && longEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && longEntryPrice.compareTo(shortEntryPrice) > 0 |
| | | && currentPrice.compareTo(shortEntryPrice) > 0 |
| | | && currentPrice.compareTo(longEntryPrice.multiply(BigDecimal.ONE.subtract(config.getGridRate()))) < 0) { |
| | | && currentPrice.compareTo(longEntryPrice.subtract(longFixedStep)) < 0) { |
| | | executor.openLong(config.getQuantity(), null, null); |
| | | log.info("[{}] 触发价在多/空持仓价之间且多>空且远离多仓均价, 额外开多一次, 当前价:{}", config.getContract(), currentPrice); |
| | | } |
| | |
| | | * <h3>执行流程</h3> |
| | | * <ol> |
| | | * <li>匹配队列元素 → 为空则直接返回</li> |
| | | * <li>多仓队列:移除 matched 元素,尾部补充新元素(尾价 × (1 + gridRate) 循环递增)</li> |
| | | * <li>空仓队列:以空仓队列首元素(最高价)为种子,生成 matched.size() 个递增元素加入</li> |
| | | * <li>多仓队列:移除 matched 元素,尾部以固定步长(longBasePrice × gridRate)递增补充新元素</li> |
| | | * <li>空仓队列:以空仓队列首元素(最高价)为种子,以空仓固定步长递增加入新元素</li> |
| | | * <li>保证金检查 → 安全则开多一次</li> |
| | | * <li>额外反向开空:若多仓均价 > 空仓均价 且 当前价夹在中间且远离空仓均价</li> |
| | | * </ol> |
| | |
| | | } |
| | | log.info("[{}] 多仓队列触发, 匹配{}个元素, 当前价:{}", config.getContract(), matched.size(), currentPrice); |
| | | |
| | | synchronized (longPriceQueue) { |
| | | longPriceQueue.removeAll(matched); |
| | | BigDecimal max = longPriceQueue.isEmpty() ? matched.get(matched.size() - 1) : longPriceQueue.get(longPriceQueue.size() - 1); |
| | | BigDecimal step = config.getGridRate(); |
| | | for (int i = 0; i < matched.size(); i++) { |
| | | max = max.multiply(BigDecimal.ONE.add(step)).setScale(1, RoundingMode.HALF_UP); |
| | | longPriceQueue.add(max); |
| | | log.info("[{}] 多队列增加:{}", config.getContract(), max); |
| | | } |
| | | longPriceQueue.sort(BigDecimal::compareTo); |
| | | log.info("[{}] 现多队列:{}", config.getContract(), longPriceQueue); |
| | | } |
| | | BigDecimal longFixedStep = longBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | BigDecimal shortFixedStep = shortBaseEntryPrice.multiply(config.getGridRate()).setScale(1, RoundingMode.HALF_UP); |
| | | replenishOwnQueue(longPriceQueue, matched, longFixedStep, false, "多"); |
| | | |
| | | synchronized (shortPriceQueue) { |
| | | BigDecimal first = shortPriceQueue.isEmpty() ? matched.get(0) : shortPriceQueue.get(0); |
| | | BigDecimal step = config.getGridRate(); |
| | | for (int i = 1; i <= matched.size(); i++) { |
| | | BigDecimal elem = first.multiply(BigDecimal.ONE.add(step.multiply(BigDecimal.valueOf(i)))).setScale(1, RoundingMode.HALF_UP); |
| | | if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && currentPrice.subtract(shortEntryPrice).abs().compareTo(shortEntryPrice.multiply(step)) < 0) { |
| | | log.info("[{}] 空队列跳过(price≈shortEntry):{}", config.getContract(), elem); |
| | | continue; |
| | | } |
| | | shortPriceQueue.add(elem); |
| | | log.info("[{}] 空队列增加:{}", config.getContract(), elem); |
| | | } |
| | | shortPriceQueue.sort((a, b) -> b.compareTo(a)); |
| | | while (shortPriceQueue.size() > config.getGridQueueSize()) { |
| | | shortPriceQueue.remove(shortPriceQueue.size() - 1); |
| | | } |
| | | log.info("[{}] 现空队列:{}", config.getContract(), shortPriceQueue); |
| | | } |
| | | transferBetweenQueues(shortPriceQueue, matched, matched.get(0), |
| | | shortFixedStep, true, shortEntryPrice, (a, b) -> b.compareTo(a), "空", currentPrice); |
| | | |
| | | if (!isMarginSafe()) { |
| | | log.warn("[{}] 保证金超限,跳过多单开仓", config.getContract()); |
| | |
| | | if (shortEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && longEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && longEntryPrice.compareTo(shortEntryPrice) > 0 |
| | | && currentPrice.compareTo(shortEntryPrice.multiply(BigDecimal.ONE.add(config.getGridRate()))) > 0 |
| | | && currentPrice.compareTo(shortEntryPrice.add(shortFixedStep)) > 0 |
| | | && currentPrice.compareTo(longEntryPrice) < 0) { |
| | | executor.openShort(config.getQuantity(), null, null); |
| | | log.info("[{}] 触发价在多/空持仓价之间且多>空且远离空仓均价, 额外开空一次, 当前价:{}", config.getContract(), currentPrice); |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 保证金安全阀检查。 |
| | | * |
| | | * <p>当前版本通过 wsHandler 推送的账户/持仓数据间接判断保证金状态。 |
| | | * 后续可通过 OKX REST API 实时查询保证金占用比例,超 marginRatioLimit 时拒绝开仓。 |
| | | * |
| | | * @return true=安全可开仓 / false=保证金超限跳过开仓 |
| | | */ |
| | | private void replenishOwnQueue(List<BigDecimal> queue, List<BigDecimal> matched, BigDecimal fixedStep, |
| | | boolean isShort, String label) { |
| | | synchronized (queue) { |
| | | queue.removeAll(matched); |
| | | BigDecimal tail = queue.isEmpty() ? matched.get(matched.size() - 1) : queue.get(queue.size() - 1); |
| | | Comparator<BigDecimal> comparator = isShort ? (a, b) -> b.compareTo(a) : BigDecimal::compareTo; |
| | | for (int i = 0; i < matched.size(); i++) { |
| | | tail = isShort |
| | | ? tail.subtract(fixedStep).setScale(1, RoundingMode.HALF_UP) |
| | | : tail.add(fixedStep).setScale(1, RoundingMode.HALF_UP); |
| | | queue.add(tail); |
| | | log.info("[{}] {}队列增加:{}", config.getContract(), label, tail); |
| | | } |
| | | queue.sort(comparator); |
| | | log.info("[{}] 现{}队列:{}", config.getContract(), label, queue); |
| | | } |
| | | } |
| | | |
| | | private void transferBetweenQueues(List<BigDecimal> targetQueue, List<BigDecimal> matched, |
| | | BigDecimal firstFallback, BigDecimal fixedStep, boolean isShort, |
| | | BigDecimal filterEntryPrice, Comparator<BigDecimal> comparator, |
| | | String label, BigDecimal currentPrice) { |
| | | synchronized (targetQueue) { |
| | | BigDecimal first = targetQueue.isEmpty() ? firstFallback : targetQueue.get(0); |
| | | for (int i = 1; i <= matched.size(); i++) { |
| | | BigDecimal offset = fixedStep.multiply(BigDecimal.valueOf(i)); |
| | | BigDecimal elem = isShort |
| | | ? first.add(offset).setScale(1, RoundingMode.HALF_UP) |
| | | : first.subtract(offset).setScale(1, RoundingMode.HALF_UP); |
| | | if (filterEntryPrice.compareTo(BigDecimal.ZERO) > 0 |
| | | && currentPrice.subtract(filterEntryPrice).abs().compareTo(filterEntryPrice.multiply(config.getGridRate())) < 0) { |
| | | log.info("[{}] {}队列跳过(price≈entry):{}", config.getContract(), label, elem); |
| | | continue; |
| | | } |
| | | targetQueue.add(elem); |
| | | log.info("[{}] {}队列增加:{}", config.getContract(), label, elem); |
| | | } |
| | | targetQueue.sort(comparator); |
| | | while (targetQueue.size() > config.getGridQueueSize()) { |
| | | targetQueue.remove(targetQueue.size() - 1); |
| | | } |
| | | log.info("[{}] 现{}队列:{}", config.getContract(), label, targetQueue); |
| | | } |
| | | } |
| | | |
| | | private boolean isMarginSafe() { |
| | | return true; |
| | | } |
| | |
| | | public BigDecimal getCumulativePnl() { return cumulativePnl; } |
| | | public BigDecimal getUnrealizedPnl() { return unrealizedPnl; } |
| | | public StrategyState getState() { return state; } |
| | | public String getAccountName() { return config.getContract(); } |
| | | public String getAccountName() { return accountName; } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxApi; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.io.BufferedReader; |
| | | import java.io.InputStreamReader; |
| | | import java.io.OutputStream; |
| | | import java.net.HttpURLConnection; |
| | | import java.net.URL; |
| | | import java.nio.charset.StandardCharsets; |
| | | |
| | | @Slf4j |
| | | public class OkxRestClient { |
| | | |
| | | private final String baseUrl; |
| | | private final String apiKey; |
| | | private final String secretKey; |
| | | private final String passphrase; |
| | | private final boolean isSimulate; |
| | | |
| | | public OkxRestClient(String baseUrl, String apiKey, String secretKey, String passphrase, boolean isSimulate) { |
| | | this.baseUrl = baseUrl; |
| | | this.apiKey = apiKey; |
| | | this.secretKey = secretKey; |
| | | this.passphrase = passphrase; |
| | | this.isSimulate = isSimulate; |
| | | } |
| | | |
| | | public boolean setPositionMode(String posMode) { |
| | | JSONObject params = new JSONObject(); |
| | | params.put("posMode", posMode); |
| | | JSONObject result = post("/api/v5/account/set-position-mode", params); |
| | | return isSuccess(result, "设置持仓方式"); |
| | | } |
| | | |
| | | public boolean setLeverage(String instId, String lever, String mgnMode) { |
| | | JSONObject params = new JSONObject(); |
| | | params.put("instId", instId); |
| | | params.put("lever", lever); |
| | | params.put("mgnMode", mgnMode); |
| | | JSONObject result = post("/api/v5/account/set-leverage", params); |
| | | return isSuccess(result, "设置杠杆"); |
| | | } |
| | | |
| | | private JSONObject post(String path, JSONObject body) { |
| | | HttpURLConnection conn = null; |
| | | try { |
| | | String bodyStr = body.toJSONString(); |
| | | String timestamp = OkxWsUtil.getIso8601Timestamp(); |
| | | String sign = OkxWsUtil.signRest(timestamp, "POST", path, bodyStr, secretKey); |
| | | |
| | | URL url = new URL(baseUrl + path); |
| | | conn = (HttpURLConnection) url.openConnection(); |
| | | conn.setRequestMethod("POST"); |
| | | conn.setDoOutput(true); |
| | | conn.setConnectTimeout(15000); |
| | | conn.setReadTimeout(15000); |
| | | conn.setRequestProperty("Content-Type", "application/json"); |
| | | conn.setRequestProperty("OK-ACCESS-KEY", apiKey); |
| | | conn.setRequestProperty("OK-ACCESS-SIGN", sign); |
| | | conn.setRequestProperty("OK-ACCESS-TIMESTAMP", timestamp); |
| | | conn.setRequestProperty("OK-ACCESS-PASSPHRASE", passphrase); |
| | | if (isSimulate) { |
| | | conn.setRequestProperty("x-simulated-trading", "1"); |
| | | } |
| | | |
| | | try (OutputStream os = conn.getOutputStream()) { |
| | | os.write(bodyStr.getBytes(StandardCharsets.UTF_8)); |
| | | os.flush(); |
| | | } |
| | | |
| | | int code = conn.getResponseCode(); |
| | | String response = readResponse(conn); |
| | | log.info("[REST] POST {} → HTTP {} body:{}", path, code, response); |
| | | |
| | | return JSON.parseObject(response); |
| | | } catch (Exception e) { |
| | | log.error("[REST] POST {} 失败: {}", path, e.getMessage()); |
| | | return null; |
| | | } finally { |
| | | if (conn != null) { |
| | | conn.disconnect(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private String readResponse(HttpURLConnection conn) throws Exception { |
| | | StringBuilder sb = new StringBuilder(); |
| | | String line; |
| | | if (conn.getResponseCode() >= 200 && conn.getResponseCode() < 300) { |
| | | try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { |
| | | while ((line = br.readLine()) != null) { |
| | | sb.append(line); |
| | | } |
| | | } |
| | | } else { |
| | | try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8))) { |
| | | while ((line = br.readLine()) != null) { |
| | | sb.append(line); |
| | | } |
| | | } |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | private boolean isSuccess(JSONObject result, String label) { |
| | | if (result == null) { |
| | | log.error("[REST] {}失败: 无响应", label); |
| | | return false; |
| | | } |
| | | String code = result.getString("code"); |
| | | if ("0".equals(code)) { |
| | | log.info("[REST] {}成功", label); |
| | | return true; |
| | | } |
| | | if ("59000".equals(code)) { |
| | | log.info("[REST] {}已设置(59000:配置未变更)", label); |
| | | return true; |
| | | } |
| | | log.error("[REST] {}失败, code:{}, msg:{}", label, code, result.getString("msg")); |
| | | return false; |
| | | } |
| | | } |
| | |
| | | import org.java_websocket.client.WebSocketClient; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.concurrent.ExecutorService; |
| | | import java.util.concurrent.LinkedBlockingQueue; |
| | |
| | | */ |
| | | @Slf4j |
| | | public class OkxTradeExecutor { |
| | | |
| | | private static final String ORDER_TYPE_CLOSE_LONG = "plan-close-long-position"; |
| | | private static final String ORDER_TYPE_CLOSE_SHORT = "plan-close-short-position"; |
| | | |
| | | private final String contract; |
| | | private final String marginMode; |
| | |
| | | executor.execute(() -> { |
| | | String posSide; |
| | | String side; |
| | | if (ORDER_TYPE_CLOSE_LONG.equals(orderType)) { |
| | | if (OkxEnums.ORDER_TYPE_CLOSE_LONG.equals(orderType)) { |
| | | posSide = OkxEnums.POSSIDE_LONG; |
| | | side = OkxEnums.SIDE_SELL; |
| | | } else { |
| | | } else if (OkxEnums.ORDER_TYPE_CLOSE_SHORT.equals(orderType)) { |
| | | posSide = OkxEnums.POSSIDE_SHORT; |
| | | side = OkxEnums.SIDE_BUY; |
| | | } else { |
| | | log.error("[TradeExec] 未知止盈类型: {}", orderType); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | TradeRequestParam param = buildParam(side, posSide, size, OkxEnums.ORDTYPE_LIMIT); |
| | | param.setMarkPx(triggerPrice.toString()); |
| | | if (wsClient == null || !wsClient.isOpen()) { |
| | | log.warn("[TradeExec] WS未连接,跳过止盈单"); |
| | | return; |
| | | } |
| | | if (BigDecimal.ZERO.compareTo(new BigDecimal(size)) >= 0) { |
| | | log.warn("[TradeExec] 止盈数量<=0,跳过"); |
| | | return; |
| | | } |
| | | |
| | | List<TradeRequestParam> params = new ArrayList<>(); |
| | | params.add(param); |
| | | sendBatchOrders(params); |
| | | JSONArray argsArray = new JSONArray(); |
| | | JSONObject args = new JSONObject(); |
| | | args.put("instId", contract); |
| | | args.put("tdMode", marginMode); |
| | | args.put("side", side); |
| | | args.put("posSide", posSide); |
| | | args.put("ordType", OkxEnums.ORDTYPE_CONDITIONAL); |
| | | args.put("sz", size); |
| | | args.put("tpTriggerPx", triggerPrice.toString()); |
| | | args.put("tpOrdPx", "-1"); |
| | | argsArray.add(args); |
| | | |
| | | String connId = OkxWsUtil.getOrderNum("algo"); |
| | | JSONObject msg = OkxWsUtil.buildJsonObject(connId, "order-algo", argsArray); |
| | | wsClient.send(msg.toJSONString()); |
| | | log.info("[TradeExec] 止盈单已发送, 触发价:{}, 类型:{}, size:{}", triggerPrice, orderType, size); |
| | | } catch (Exception e) { |
| | | log.error("[TradeExec] 止盈单发送失败, 触发价:{}, size:{}, 立即市价止盈", triggerPrice, size, e); |
| | |
| | | return param; |
| | | } |
| | | |
| | | private JSONObject buildOrderArgs(TradeRequestParam param) { |
| | | JSONObject args = new JSONObject(); |
| | | args.put("instId", param.getInstId()); |
| | | args.put("tdMode", param.getTdMode()); |
| | | args.put("clOrdId", param.getClOrdId()); |
| | | args.put("side", param.getSide()); |
| | | args.put("posSide", param.getPosSide()); |
| | | args.put("ordType", param.getOrdType()); |
| | | args.put("sz", param.getSz()); |
| | | return args; |
| | | } |
| | | |
| | | private void sendOrder(TradeRequestParam param) { |
| | | if (wsClient == null || !wsClient.isOpen()) { |
| | | log.warn("[TradeExec] WS未连接,跳过下单"); |
| | |
| | | } |
| | | |
| | | JSONArray argsArray = new JSONArray(); |
| | | JSONObject args = new JSONObject(); |
| | | args.put("instId", param.getInstId()); |
| | | args.put("tdMode", param.getTdMode()); |
| | | args.put("clOrdId", param.getClOrdId()); |
| | | args.put("side", param.getSide()); |
| | | args.put("posSide", param.getPosSide()); |
| | | args.put("ordType", param.getOrdType()); |
| | | args.put("sz", param.getSz()); |
| | | argsArray.add(args); |
| | | argsArray.add(buildOrderArgs(param)); |
| | | |
| | | String connId = OkxWsUtil.getOrderNum("order"); |
| | | JSONObject msg = OkxWsUtil.buildJsonObject(connId, "order", argsArray); |
| | |
| | | |
| | | JSONArray argsArray = new JSONArray(); |
| | | for (TradeRequestParam p : params) { |
| | | JSONObject args = new JSONObject(); |
| | | args.put("instId", p.getInstId()); |
| | | args.put("tdMode", p.getTdMode()); |
| | | args.put("clOrdId", p.getClOrdId()); |
| | | args.put("side", p.getSide()); |
| | | args.put("posSide", p.getPosSide()); |
| | | args.put("ordType", p.getOrdType()); |
| | | args.put("sz", p.getSz()); |
| | | args.put("px", p.getMarkPx()); |
| | | argsArray.add(args); |
| | | argsArray.add(buildOrderArgs(p)); |
| | | } |
| | | |
| | | String connId = OkxWsUtil.getOrderNum(null); |
| | |
| | | .contract("BTC-USDT-SWAP") |
| | | .leverage("100") |
| | | .marginMode("cross") |
| | | .posMode("long_short_mode") |
| | | .gridRate(new BigDecimal("0.0015")) |
| | | .overallTp(new BigDecimal("5")) |
| | | .maxLoss(new BigDecimal("15")) |
| | |
| | | .unrealizedPnlPriceMode(OkxConfig.PnLPriceMode.LAST_PRICE) |
| | | .isProduction(false) |
| | | .build(); |
| | | |
| | | setupAccount(); |
| | | |
| | | String accountName = "OKX_API"; |
| | | gridTradeService = new OkxGridTradeService(config, accountName); |
| | |
| | | log.info("[管理器] 销毁完成"); |
| | | } |
| | | |
| | | private void setupAccount() { |
| | | log.info("[管理器] 开始配置账户..."); |
| | | OkxRestClient restClient = new OkxRestClient( |
| | | config.getRestBaseUrl(), |
| | | config.getApiKey(), |
| | | config.getSecretKey(), |
| | | config.getPassphrase(), |
| | | !config.isProduction()); |
| | | |
| | | boolean posModeOk = restClient.setPositionMode(config.getPosMode()); |
| | | if (!posModeOk) { |
| | | log.error("[管理器] 设置持仓方式失败,策略可能无法正常运作"); |
| | | } |
| | | |
| | | boolean leverOk = restClient.setLeverage( |
| | | config.getContract(), config.getLeverage(), config.getMarginMode()); |
| | | if (!leverOk) { |
| | | log.error("[管理器] 设置杠杆倍数失败,策略可能无法正常运作"); |
| | | } |
| | | |
| | | log.info("[管理器] 账户配置完成, posMode:{}, leverage:{}, marginMode:{}", |
| | | config.getPosMode(), config.getLeverage(), config.getMarginMode()); |
| | | } |
| | | |
| | | public OkxKlineWebSocketClient getKlineWebSocketClient() { return wsKlineClient; } |
| | | public OkxKlineWebSocketClient getPrivateWebSocketClient() { return wsPrivateClient; } |
| | | public OkxGridTradeService getGridTradeService() { return gridTradeService; } |
| | |
| | | // ==================== HMAC-SHA256 签名 ==================== |
| | | |
| | | public static String signWebsocket(String timestamp, String secretKey) { |
| | | return sign(timestamp, "GET", "/users/self/verify", "", secretKey); |
| | | } |
| | | |
| | | public static String signRest(String timestamp, String method, String path, String body, String secretKey) { |
| | | return sign(timestamp, method.toUpperCase(), path, body, secretKey); |
| | | } |
| | | |
| | | private static String sign(String timestamp, String method, String path, String body, String secretKey) { |
| | | try { |
| | | String message = String.format("%s%s%s", timestamp, "GET", "/users/self/verify"); |
| | | String message = String.format("%s%s%s%s", timestamp, method, path, body); |
| | | Mac mac = Mac.getInstance("HmacSHA256"); |
| | | SecretKeySpec spec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"); |
| | | mac.init(spec); |
| | |
| | | |
| | | // ==================== 订单ID ==================== |
| | | |
| | | private static final ThreadLocal<SimpleDateFormat> ORDER_ID_DF = |
| | | ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMddHHmmss")); |
| | | |
| | | private static final ThreadLocal<Random> RANDOM = |
| | | ThreadLocal.withInitial(Random::new); |
| | | |
| | | public static String getOrderNum(String prefix) { |
| | | SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss"); |
| | | String dd = df.format(new Date()); |
| | | String dd = ORDER_ID_DF.get().format(new Date()); |
| | | if (prefix != null && !prefix.isEmpty()) { |
| | | return prefix + dd + getRandomNum(5); |
| | | } |
| | |
| | | |
| | | private static String getRandomNum(int length) { |
| | | String str = "0123456789"; |
| | | Random random = new Random(); |
| | | Random random = RANDOM.get(); |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (int i = 0; i < length; ++i) { |
| | | sb.append(str.charAt(random.nextInt(str.length()))); |
| | |
| | | |
| | | // ==================== 日期格式化 ==================== |
| | | |
| | | private static final ThreadLocal<SimpleDateFormat> DATE_TIME_DF = |
| | | ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); |
| | | |
| | | private static final ThreadLocal<SimpleDateFormat> ISO8601_DF = |
| | | ThreadLocal.withInitial(() -> { |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); |
| | | sdf.setTimeZone(java.util.TimeZone.getTimeZone("UTC")); |
| | | return sdf; |
| | | }); |
| | | |
| | | public static String getIso8601Timestamp() { |
| | | return ISO8601_DF.get().format(new Date()); |
| | | } |
| | | |
| | | public static String timestampToDateTime(long timestamp) { |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| | | return sdf.format(new Date(timestamp)); |
| | | return DATE_TIME_DF.get().format(new Date(timestamp)); |
| | | } |
| | | } |
| | |
| | | public static final String SIDE_SELL = "sell"; |
| | | public static final String ORDTYPE_MARKET = "market"; |
| | | public static final String ORDTYPE_LIMIT = "limit"; |
| | | public static final String ORDTYPE_CONDITIONAL = "conditional"; |
| | | public static final String INSTTYPE_SWAP = "SWAP"; |
| | | public static final String MARGIN_CROSS = "cross"; |
| | | |
| | | public static final String ORDER_TYPE_CLOSE_LONG = "plan-close-long-position"; |
| | | public static final String ORDER_TYPE_CLOSE_SHORT = "plan-close-short-position"; |
| | | |
| | | public static final String CHANNEL_CANDLE = "candle1m"; |
| | | public static final String CHANNEL_POSITIONS = "positions"; |
| | | public static final String CHANNEL_ACCOUNT = "account"; |
| | |
| | | } |
| | | String state = detail.getString("state"); |
| | | String accFillSz = detail.getString("accFillSz"); |
| | | String fillPnl = detail.getString("fillPnl"); |
| | | String pnl = detail.getString("pnl"); |
| | | String posSide = detail.getString("posSide"); |
| | | String avgPx = detail.getString("avgPx"); |
| | | String clOrdId = detail.getString("clOrdId"); |
| | | |
| | | log.info("[{}] 订单, 方向:{}, 状态:{}, 成交量:{}, 均价:{}, 盈亏:{}, 编号:{}", |
| | | CHANNEL_NAME, posSide, state, accFillSz, avgPx, fillPnl, clOrdId); |
| | | CHANNEL_NAME, posSide, state, accFillSz, avgPx, pnl, clOrdId); |
| | | |
| | | if ("filled".equals(state) && accFillSz != null && new BigDecimal(accFillSz).compareTo(BigDecimal.ZERO) > 0) { |
| | | if (gridTradeService != null) { |
| | | BigDecimal pnl = fillPnl != null ? new BigDecimal(fillPnl) : BigDecimal.ZERO; |
| | | gridTradeService.onOrderFilled(posSide, new BigDecimal(accFillSz), pnl); |
| | | BigDecimal pnlVal = pnl != null ? new BigDecimal(pnl) : BigDecimal.ZERO; |
| | | gridTradeService.onOrderFilled(posSide, new BigDecimal(accFillSz), pnlVal); |
| | | } |
| | | } |
| | | } |