package com.xcong.excoin.modules.okxApi; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.Base64; import java.util.Date; import java.util.TimeZone; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** * OKX REST API 异步执行器,所有下单/撤单操作经此类提交。 * *
| 方法 | 用途 |
|---|---|
| openLong / openShort | 市价基底开仓 |
| placeConditionalEntryOrder | 挂条件开仓单(价格触发后市价开仓) |
| placeTakeProfit | 挂止盈条件单 |
| cancelConditionalOrder | 取消单个条件单 |
| cancelAllPriceTriggeredOrders | 取消所有条件单(策略停止时) |
服务器监控价格,达到触发价后自动以市价平仓。 * 使用 OKX 的 {@code order-algo} 接口,ordType=conditional。 * *
通过 posSide 指定平仓方向: *
使用 OKX 的 {@code order-algo} 接口,ordType=trigger(计划委托)。 * 服务器监控价格,达到触发价后以市价开仓。 * *
OKX 的 cancel-algos 接口要求必须传 algoId 或 algoClOrdId, * 不能仅凭 instId 批量取消。因此先查询待处理列表,再逐个取消。 */ public void cancelAllPriceTriggeredOrders() { executor.execute(() -> { try { // ordType 是 orders-algo-pending 的必填参数,需分别查询 conditional 和 trigger JSONArray cancelBody = new JSONArray(); for (String ordType : new String[]{"conditional", "trigger"}) { String queryPath = "/api/v5/trade/orders-algo-pending?instId=" + contract + "&ordType=" + ordType; try { JSONObject queryResp = okGet(queryPath); if (!"0".equals(queryResp.getString("code"))) { log.warn("[TradeExec-OKX] 查询 pending ordType={} 失败, code:{}, msg:{}", ordType, queryResp.getString("code"), queryResp.getString("msg")); continue; } JSONArray data = queryResp.getJSONArray("data"); if (data != null) { for (int i = 0; i < data.size(); i++) { JSONObject order = data.getJSONObject(i); String algoId = order.getString("algoId"); if (algoId == null) continue; JSONObject item = new JSONObject(); item.put("algoId", algoId); item.put("instId", contract); cancelBody.add(item); } } } catch (Exception e) { log.warn("[TradeExec-OKX] 查询待处理条件单失败, ordType:{}", ordType, e); } } if (cancelBody.isEmpty()) { log.info("[TradeExec-OKX] 无待处理条件单"); return; } // 批量取消 JSONObject cancelResp = okPost("/api/v5/trade/cancel-algos", cancelBody.toJSONString()); String cancelCode = cancelResp.getString("code"); if (!"0".equals(cancelCode)) { log.warn("[TradeExec-OKX] 清除条件单部分失败, code:{}, msg:{}", cancelCode, cancelResp.getString("msg")); return; } log.info("[TradeExec-OKX] 已清除{}个条件单", cancelBody.size()); } catch (Exception e) { log.error("[TradeExec-OKX] 清除条件单失败", e); } }); } // ==================== HTTP 请求帮助方法 ==================== /** * 发送 OKX 签名 POST 请求并返回解析后的 JSONObject。 * *
自动添加 OK-ACCESS-KEY、OK-ACCESS-SIGN、OK-ACCESS-TIMESTAMP、OK-ACCESS-PASSPHRASE * 四个认证头。签名算法:base64(HMAC-SHA256(timestamp + method + path + body))。 * * @param path API 路径(如 /api/v5/trade/order) * @param body 请求体 JSON 字符串 * @return 解析后的响应 JSONObject * @throws IOException 网络异常或业务错误 */ JSONObject okPost(String path, String body) throws IOException { String method = "POST"; String timestamp = getIsoTimestamp(); String sign = null; try { sign = sign(timestamp, method, path, body); } catch (Exception e) { e.printStackTrace(); } Request.Builder builder = new Request.Builder() .url(config.getRestBasePath() + path) .header("OK-ACCESS-KEY", config.getApiKey()) .header("OK-ACCESS-SIGN", sign) .header("OK-ACCESS-TIMESTAMP", timestamp) .header("OK-ACCESS-PASSPHRASE", config.getPassphrase()) .header("Content-Type", "application/json; charset=utf-8") .post(RequestBody.create(JSON_MEDIA_TYPE, body)); // 模拟盘需加 x-simulated-trading 头,与生产网共用同一 REST 地址 if (!config.isProduction()) { builder.header("x-simulated-trading", "1"); } Request request = builder.build(); try (Response response = httpClient.newCall(request).execute()) { String responseBody = response.body() != null ? response.body().string() : "{}"; if (!response.isSuccessful()) { log.error("[TradeExec-OKX] HTTP {} POST {}: {}", response.code(), path, responseBody); throw new IOException("HTTP " + response.code() + ": " + responseBody); } return JSON.parseObject(responseBody); } } /** * 发送 OKX 签名 GET 请求并返回解析后的 JSONObject。 * *
GET 请求的签名中 body 为空字符串。 * * @param path API 路径(如 /api/v5/account/positions) * @return 解析后的响应 JSONObject * @throws IOException 网络异常 */ JSONObject okGet(String path) throws IOException { String method = "GET"; String timestamp = getIsoTimestamp(); String sign = null; try { sign = sign(timestamp, method, path, ""); } catch (Exception e) { e.printStackTrace(); } Request.Builder builder = new Request.Builder() .url(config.getRestBasePath() + path) .header("OK-ACCESS-KEY", config.getApiKey()) .header("OK-ACCESS-SIGN", sign) .header("OK-ACCESS-TIMESTAMP", timestamp) .header("OK-ACCESS-PASSPHRASE", config.getPassphrase()) .get(); // 模拟盘需加 x-simulated-trading 头 if (!config.isProduction()) { builder.header("x-simulated-trading", "1"); } Request request = builder.build(); try (Response response = httpClient.newCall(request).execute()) { String responseBody = response.body() != null ? response.body().string() : "{}"; if (!response.isSuccessful()) { log.error("[TradeExec-OKX] HTTP {} GET {}: {}", response.code(), path, responseBody); throw new IOException("HTTP " + response.code() + ": " + responseBody); } return JSON.parseObject(responseBody); } } // ==================== 签名工具方法 ==================== /** * 生成 OKX API 签名。 * *
签名算法: *
格式示例:{@code 2023-01-01T00:00:00.000Z} * * @return ISO 8601 格式的 UTC 时间戳字符串 */ private String getIsoTimestamp() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); sdf.setTimeZone(TimeZone.getTimeZone("UTC")); return sdf.format(new Date()); } }