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 | 取消所有条件单(策略停止时) |
止盈单创建失败时立即调用 {@link #marketClose(String, String)} 市价平仓兜底。
*
* @param triggerPrice 触发价格
* @param orderType 平仓类型:"close_long" 平多 / "close_short" 平空
* @param size 平仓张数(正数,如 "15")
* @param onSuccess 成功回调,接收 algoId(可为 null)
*/
public void placeTakeProfit(BigDecimal triggerPrice,
String orderType,
String size,
Consumer 止损单创建失败时立即调用 {@link #marketClose(String, String)} 市价平仓兜底。
*
* @param triggerPrice 触发价格
* @param orderType 平仓类型:"close_long" 平多 / "close_short" 平空
* @param size 平仓张数(正数,如 "15")
* @param onSuccess 成功回调,接收 algoId(可为 null)
*/
public void placeStopLoss(BigDecimal triggerPrice,
String orderType,
String size,
Consumer 通过 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());
}
}
*
*
* @param size 平仓张数(正数)
* @param posSide 持仓方向(long / short)
*/
private void marketClose(String size, String posSide) {
String side = "long".equals(posSide) ? "sell" : "buy";
marketClose(size, side, posSide);
}
/**
* 对外暴露的市价平仓接口(含回调),供策略层直接调用。
*
* @param size 平仓张数(正数)
* @param posSide 持仓方向(long=平多 / short=平空)
* @param onSuccess 成功回调(可为 null)
* @param onFailure 失败回调(可为 null)
*/
public void marketClosePosition(String size, String posSide,
Runnable onSuccess, Runnable onFailure) {
executor.execute(() -> {
String side = "long".equals(posSide) ? "sell" : "buy";
try {
JSONObject body = new JSONObject();
body.put("instId", contract);
body.put("tdMode", "cross");
body.put("side", side);
body.put("posSide", posSide);
body.put("ordType", "market");
body.put("sz", size);
JSONObject resp = okPost("/api/v5/trade/order", body.toJSONString());
String code = resp.getString("code");
if (!"0".equals(code)) {
log.error("[TradeExec-OKX] 市价平仓失败, side:{}, posSide:{}, sz:{}, code:{}, msg:{}",
side, posSide, size, code, resp.getString("msg"));
if (onFailure != null) {
onFailure.run();
}
return;
}
log.info("[TradeExec-OKX] 市价平仓成功, side:{}, posSide:{}, sz:{}", side, posSide, size);
if (onSuccess != null) {
onSuccess.run();
}
} catch (Exception e) {
log.error("[TradeExec-OKX] 市价平仓失败, side:{}, posSide:{}, sz:{}", side, posSide, size, e);
if (onFailure != null) {
onFailure.run();
}
}
});
}
/**
* 指定方向的市价平仓。
*
* @param sz 平仓张数
* @param side 交易方向(sell=平多 / buy=平空)
* @param posSide 持仓方向(long / short)
*/
private void marketClose(String sz, String side, String posSide) {
try {
JSONObject body = new JSONObject();
body.put("instId", contract);
body.put("tdMode", "cross");
body.put("side", side);
body.put("posSide", posSide);
body.put("ordType", "market");
body.put("sz", sz);
JSONObject resp = okPost("/api/v5/trade/order", body.toJSONString());
String code = resp.getString("code");
if (!"0".equals(code)) {
log.warn("[TradeExec-OKX] 市价止盈失败, side:{}, posSide:{}, sz:{}, code:{}, msg:{}",
side, posSide, sz, code, resp.getString("msg"));
return;
}
JSONArray data = resp.getJSONArray("data");
String ordId = (data != null && !data.isEmpty())
? data.getJSONObject(0).getString("ordId") : null;
log.info("[TradeExec-OKX] 市价止盈成功, side:{}, posSide:{}, sz:{}, ordId:{}",
side, posSide, sz, ordId);
} catch (Exception e) {
log.error("[TradeExec-OKX] 市价止盈也失败, sz:{}", sz, e);
}
}
// ==================== 条件开仓单 ====================
/**
* 异步创建条件开仓单(价格触发后市价开仓)。
*
* 与止盈止损的区别
*
*
*
* @param triggerPrice 触发价格
* @param isLong true=开多(side=buy)/ false=开空(side=sell)
* @param size 开仓张数(正数,如 "1")
* @param onSuccess 成功回调,接收 algoId(可为 null)
* @param onFailure 失败回调(可为 null)
*/
public void placeConditionalEntryOrder(BigDecimal triggerPrice,
boolean isLong,
String size,
Consumer
*
*
* @param timestamp OKX 格式时间戳(ISO 8601)
* @param method HTTP 方法(GET/POST)
* @param path API 路径(如 /api/v5/trade/order)
* @param body 请求体(GET 请求传 "")
* @return Base64 编码的签名字符串
* @throws Exception 签名计算异常
*/
private String sign(String timestamp, String method, String path, String body) throws Exception {
String signString = timestamp + method + path + body;
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(config.getApiSecret().getBytes(), "HmacSHA256");
sha256Hmac.init(secretKey);
byte[] signedBytes = sha256Hmac.doFinal(signString.getBytes());
return Base64.getEncoder().encodeToString(signedBytes);
}
/**
* 获取 OKX 格式的 ISO 8601 时间戳。
*
*