feat(okx): 新增OKX账户与交易相关功能模块
- 添加账户WebSocket处理类,支持订阅账户频道并处理推送数据
- 实现账户余额和持仓信息的解析与Redis存储逻辑
- 新增API消息DTO及VO类,用于接收和响应账户相关信息
- 创建操作服务接口及实现类,处理交易策略相关逻辑
- 添加币种枚举类,定义交易相关常量
- 新增数据工具类,提供数字处理与日期格式化功能
- 配置文件中增加OKEX基础URL及环境相关参数设置
3 files modified
93 files added
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxNewPrice.celue.CaoZuoService; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.*; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.CoinEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.OrderParamEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.SSLConfig; |
| | | import com.xcong.excoin.modules.okxNewPrice.wangge.WangGeService; |
| | | import com.xcong.excoin.utils.RedisUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.java_websocket.client.WebSocketClient; |
| | | import org.java_websocket.handshake.ServerHandshake; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import javax.annotation.PostConstruct; |
| | | import javax.annotation.PreDestroy; |
| | | import java.math.BigDecimal; |
| | | import java.net.URI; |
| | | import java.net.URISyntaxException; |
| | | import java.util.concurrent.*; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | /** |
| | | * OKX 新价格 WebSocket 客户端类,用于连接 OKX 的 WebSocket 接口, |
| | | * 实时获取并处理标记价格(mark price)数据,并将价格信息存储到 Redis 中。 |
| | | * 同时支持心跳检测、自动重连以及异常恢复机制。 |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | @Component |
| | | @ConditionalOnProperty(prefix = "app", name = "quant", havingValue = "true") |
| | | public class OkxQuantWebSocketClient { |
| | | @Autowired |
| | | private WangGeService wangGeService; |
| | | @Autowired |
| | | private CaoZuoService caoZuoService; |
| | | @Autowired |
| | | private RedisUtils redisUtils; |
| | | |
| | | private WebSocketClient webSocketClient; |
| | | private ScheduledExecutorService heartbeatExecutor; |
| | | private volatile ScheduledFuture<?> pongTimeoutFuture; |
| | | private final AtomicReference<Long> lastMessageTime = new AtomicReference<>(System.currentTimeMillis()); |
| | | |
| | | private static final String WS_URL_MONIPAN = "wss://wspap.okx.com:8443/ws/v5/private"; |
| | | private static final String WS_URL_SHIPAN = "wss://ws.okx.com:8443/ws/v5/private"; |
| | | private static final boolean INTERNET = false; |
| | | |
| | | /** |
| | | * 订阅频道指令 |
| | | */ |
| | | private static final String SUBSCRIBE = "subscribe"; |
| | | private static final String UNSUBSCRIBE = "unsubscribe"; |
| | | |
| | | // 心跳超时时间(秒),小于30秒 |
| | | private static final int HEARTBEAT_TIMEOUT = 10; |
| | | |
| | | // 共享线程池用于重连等异步任务 |
| | | private final ExecutorService sharedExecutor = Executors.newCachedThreadPool(r -> { |
| | | Thread t = new Thread(r, "okx-ws-account-order-worker"); |
| | | t.setDaemon(true); |
| | | return t; |
| | | }); |
| | | |
| | | /** |
| | | * 初始化方法,在 Spring Bean 构造完成后执行。 |
| | | * 负责建立 WebSocket 连接并启动心跳检测任务。 |
| | | */ |
| | | @PostConstruct |
| | | public void init() { |
| | | connect(); |
| | | startHeartbeat(); |
| | | } |
| | | |
| | | /** |
| | | * 销毁方法,在 Spring Bean 销毁前执行。 |
| | | * 关闭 WebSocket 连接、停止心跳定时器及相关的线程资源。 |
| | | */ |
| | | @PreDestroy |
| | | public void destroy() { |
| | | if (webSocketClient != null && webSocketClient.isOpen()) { |
| | | subscribeAccountChannel(UNSUBSCRIBE); |
| | | subscribePositionChannel(UNSUBSCRIBE); |
| | | subscribeOrderInfoChannel(UNSUBSCRIBE); |
| | | webSocketClient.close(); |
| | | } |
| | | shutdownExecutorGracefully(heartbeatExecutor); |
| | | if (pongTimeoutFuture != null) { |
| | | pongTimeoutFuture.cancel(true); |
| | | } |
| | | shutdownExecutorGracefully(sharedExecutor); |
| | | } |
| | | |
| | | private void shutdownExecutorGracefully(ExecutorService executor) { |
| | | if (executor == null || executor.isTerminated()) { |
| | | return; |
| | | } |
| | | try { |
| | | executor.shutdown(); |
| | | if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { |
| | | executor.shutdownNow(); |
| | | } |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | executor.shutdownNow(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 建立与 OKX WebSocket 服务器的连接。 |
| | | * 设置回调函数以监听连接打开、接收消息、关闭和错误事件。 |
| | | */ |
| | | private void connect() { |
| | | try { |
| | | InstrumentsWs.handleEvent(redisUtils); |
| | | wangGeService.initWangGe(); |
| | | SSLConfig.configureSSL(); |
| | | System.setProperty("https.protocols", "TLSv1.2,TLSv1.3"); |
| | | String WS_URL = WS_URL_MONIPAN; |
| | | if (INTERNET){ |
| | | WS_URL = WS_URL_SHIPAN; |
| | | } |
| | | URI uri = new URI(WS_URL); |
| | | webSocketClient = new WebSocketClient(uri) { |
| | | @Override |
| | | public void onOpen(ServerHandshake handshake) { |
| | | log.info("OKX account-order WebSocket连接成功"); |
| | | resetHeartbeatTimer(); |
| | | websocketLogin(); |
| | | } |
| | | |
| | | @Override |
| | | public void onMessage(String message) { |
| | | lastMessageTime.set(System.currentTimeMillis()); |
| | | handleWebSocketMessage(message); |
| | | resetHeartbeatTimer(); |
| | | } |
| | | |
| | | @Override |
| | | public void onClose(int code, String reason, boolean remote) { |
| | | log.warn("OKX account-order WebSocket连接关闭: code={}, reason={}", code, reason); |
| | | cancelPongTimeout(); |
| | | |
| | | if (sharedExecutor != null && !sharedExecutor.isShutdown() && !sharedExecutor.isTerminated()) { |
| | | sharedExecutor.execute(() -> { |
| | | try { |
| | | reconnectWithBackoff(); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | log.error("重连线程被中断", e); |
| | | } catch (Exception e) { |
| | | log.error("重连失败", e); |
| | | } |
| | | }); |
| | | } else { |
| | | log.warn("共享线程池已关闭,无法执行重连任务"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void onError(Exception ex) { |
| | | log.error("OKX account-order WebSocket发生错误", ex); |
| | | } |
| | | }; |
| | | |
| | | webSocketClient.connect(); |
| | | } catch (URISyntaxException e) { |
| | | log.error("WebSocket URI格式错误", e); |
| | | } |
| | | } |
| | | |
| | | private void websocketLogin() { |
| | | LoginWs.websocketLogin(webSocketClient); |
| | | } |
| | | |
| | | private void subscribeBalanceAndPositionChannel(String option) { |
| | | BalanceAndPositionWs.subscribeBalanceAndPositionChannel(webSocketClient, option); |
| | | } |
| | | |
| | | private void subscribeOrderInfoChannel(String option) { |
| | | OrderInfoWs.subscribeOrderInfoChannel(webSocketClient, option); |
| | | } |
| | | |
| | | private void subscribeAccountChannel(String option) { |
| | | AccountWs.subscribeAccountChannel(webSocketClient, option); |
| | | } |
| | | |
| | | private void subscribePositionChannel(String option) { |
| | | PositionsWs.subscribePositionChannel(webSocketClient, option); |
| | | } |
| | | |
| | | /** |
| | | * 处理从 WebSocket 收到的消息。 |
| | | * 包括订阅确认、错误响应、心跳响应以及实际的数据推送。 |
| | | * |
| | | * @param message 来自 WebSocket 的原始字符串消息 |
| | | */ |
| | | private void handleWebSocketMessage(String message) { |
| | | try { |
| | | if ("pong".equals(message)) { |
| | | log.info("收到心跳响应"); |
| | | cancelPongTimeout(); |
| | | return; |
| | | } |
| | | JSONObject response = JSON.parseObject(message); |
| | | String event = response.getString("event"); |
| | | |
| | | if ("login".equals(event)) { |
| | | String code = response.getString("code"); |
| | | if ("0".equals(code)) { |
| | | String connId = response.getString("connId"); |
| | | log.info("WebSocket登录成功, connId: {}", connId); |
| | | subscribeAccountChannel(SUBSCRIBE); |
| | | subscribeOrderInfoChannel(SUBSCRIBE); |
| | | subscribePositionChannel(SUBSCRIBE); |
| | | } else { |
| | | log.error("WebSocket登录失败, code: {}, msg: {}", code, response.getString("msg")); |
| | | } |
| | | } else if ("subscribe".equals(event)) { |
| | | log.info("订阅成功: {}", response.getJSONObject("arg")); |
| | | } else if ("error".equals(event)) { |
| | | log.error("订阅错误: code={}, msg={}", |
| | | response.getString("code"), response.getString("msg")); |
| | | } else if ("channel-conn-count".equals(event)) { |
| | | log.info("连接限制更新: channel={}, connCount={}", |
| | | response.getString("channel"), response.getString("connCount")); |
| | | } else { |
| | | processPushData(response); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("处理WebSocket消息失败: {}", message, e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 解析并处理价格推送数据。 |
| | | * 将最新的标记价格存入 Redis 并触发后续业务逻辑比较处理。 |
| | | * |
| | | * @param response 包含价格数据的 JSON 对象 |
| | | */ |
| | | private void processPushData(JSONObject response) { |
| | | JSONObject arg = response.getJSONObject("arg"); |
| | | if (arg == null) { |
| | | log.warn("无效的推送数据,缺少 'arg' 字段"); |
| | | return; |
| | | } |
| | | |
| | | String channel = arg.getString("channel"); |
| | | if (channel == null) { |
| | | log.warn("无效的推送数据,缺少 'channel' 字段"); |
| | | return; |
| | | } |
| | | |
| | | if (OrderInfoWs.ORDERINFOWS_CHANNEL.equals(channel)) { |
| | | OrderInfoWs.handleEvent(response, redisUtils); |
| | | }else if (AccountWs.ACCOUNTWS_CHANNEL.equals(channel)) { |
| | | AccountWs.handleEvent(response, redisUtils); |
| | | } else if (PositionsWs.POSITIONSWS_CHANNEL.equals(channel)) { |
| | | PositionsWs.handleEvent(response, redisUtils); |
| | | |
| | | String avgPxKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":avgPx"; |
| | | String avgPx = (String) redisUtils.get(avgPxKey); |
| | | if (StrUtil.isBlank(avgPx)) { |
| | | log.error("未获取到持仓均价"); |
| | | TradeOrderWs.orderEvent(webSocketClient, redisUtils, OrderParamEnums.INIT.getValue()); |
| | | return; |
| | | } |
| | | |
| | | String uplKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":upl"; |
| | | String totalOrderUsdtKey = AccountWs.ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":totalOrderUsdt"; |
| | | |
| | | String upl = (String) redisUtils.get(uplKey); |
| | | String totalOrderUsdt = (String) redisUtils.get(totalOrderUsdtKey); |
| | | BigDecimal multiply = new BigDecimal(upl).multiply(new BigDecimal("-1")); |
| | | |
| | | if (new BigDecimal(totalOrderUsdt).compareTo(multiply) < 0) { |
| | | log.error("持仓盈亏超过下单总保证金,止损冷静一天......"); |
| | | TradeOrderWs.orderEvent(webSocketClient, redisUtils, OrderParamEnums.OUT.getValue()); |
| | | return; |
| | | } |
| | | |
| | | String side = caoZuoService.caoZuo(); |
| | | if (StrUtil.isNotBlank(avgPx)) { |
| | | TradeOrderWs.orderEvent(webSocketClient, redisUtils, side); |
| | | } |
| | | } else if (BalanceAndPositionWs.CHANNEL_NAME.equals(channel)) { |
| | | BalanceAndPositionWs.handleEvent(response); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 启动心跳检测任务。 |
| | | * 使用 ScheduledExecutorService 定期检查是否需要发送 ping 请求来维持连接。 |
| | | */ |
| | | private void startHeartbeat() { |
| | | if (heartbeatExecutor != null && !heartbeatExecutor.isTerminated()) { |
| | | heartbeatExecutor.shutdownNow(); |
| | | } |
| | | |
| | | heartbeatExecutor = Executors.newSingleThreadScheduledExecutor(r -> { |
| | | Thread t = new Thread(r, "okx-account-order-heartbeat"); |
| | | t.setDaemon(true); |
| | | return t; |
| | | }); |
| | | |
| | | heartbeatExecutor.scheduleWithFixedDelay(this::checkHeartbeatTimeout, 25, 25, TimeUnit.SECONDS); |
| | | } |
| | | |
| | | /** |
| | | * 重置心跳计时器。 |
| | | * 当收到新消息或发送 ping 后取消当前超时任务并重新安排下一次超时检查。 |
| | | */ |
| | | private void resetHeartbeatTimer() { |
| | | cancelPongTimeout(); |
| | | |
| | | if (heartbeatExecutor != null) { |
| | | pongTimeoutFuture = heartbeatExecutor.schedule(this::checkHeartbeatTimeout, |
| | | HEARTBEAT_TIMEOUT, TimeUnit.SECONDS); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 检查心跳超时情况。 |
| | | * 若长时间未收到任何消息则主动发送 ping 请求保持连接活跃。 |
| | | */ |
| | | private void checkHeartbeatTimeout() { |
| | | long currentTime = System.currentTimeMillis(); |
| | | long lastTime = lastMessageTime.get(); |
| | | |
| | | if (currentTime - lastTime >= HEARTBEAT_TIMEOUT * 1000L) { |
| | | sendPing(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 发送 ping 请求至 WebSocket 服务端。 |
| | | * 用于维持长连接有效性。 |
| | | */ |
| | | private void sendPing() { |
| | | try { |
| | | if (webSocketClient != null && webSocketClient.isOpen()) { |
| | | webSocketClient.send("ping"); |
| | | log.debug("发送ping请求"); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("发送ping失败", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 取消当前的心跳超时任务。 |
| | | * 在收到 pong 或其他有效消息时调用此方法避免不必要的断开重连。 |
| | | */ |
| | | private void cancelPongTimeout() { |
| | | if (pongTimeoutFuture != null && !pongTimeoutFuture.isDone()) { |
| | | pongTimeoutFuture.cancel(true); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 执行 WebSocket 重连操作。 |
| | | * 在连接意外中断后尝试重新建立连接。 |
| | | */ |
| | | private void reconnectWithBackoff() throws InterruptedException { |
| | | int attempt = 0; |
| | | int maxAttempts = 5; |
| | | long delayMs = 1000; |
| | | |
| | | while (attempt < maxAttempts) { |
| | | try { |
| | | Thread.sleep(delayMs); |
| | | connect(); |
| | | return; |
| | | } catch (Exception e) { |
| | | log.warn("第{}次重连失败", attempt + 1, e); |
| | | delayMs *= 2; |
| | | attempt++; |
| | | } |
| | | } |
| | | |
| | | log.error("超过最大重试次数({})仍未连接成功", maxAttempts); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice; |
| | | |
| | | public class OkxWebSocketClientMain { |
| | | public static void main(String[] args) throws InterruptedException { |
| | | OkxQuantWebSocketClient clientV2 = new OkxQuantWebSocketClient(); |
| | | // 手动调用初始化方法 |
| | | clientV2.init(); |
| | | |
| | | // 运行一段时间以观察结果 |
| | | Thread.sleep(1200000000L); // 运行一小时 |
| | | |
| | | // 关闭连接 |
| | | clientV2.destroy(); |
| | | |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.celue; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | public interface CaoZuoService { |
| | | |
| | | String caoZuo(); |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.celue; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.InstrumentsWs; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.PositionsWs; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.CoinEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.OrderParamEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.wangge.WangGeQueue; |
| | | import com.xcong.excoin.modules.okxNewPrice.wangge.WangGeService; |
| | | import com.xcong.excoin.rabbit.pricequeue.AscBigDecimal; |
| | | import com.xcong.excoin.rabbit.pricequeue.DescBigDecimal; |
| | | import com.xcong.excoin.utils.RedisUtils; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.concurrent.PriorityBlockingQueue; |
| | | |
| | | /** |
| | | * 操作服务实现类,用于处理与交易相关的逻辑操作。 |
| | | * 包括根据市场行情判断是否进行加仓或减仓,并维护相关价格队列。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class CaoZuoServiceImpl implements CaoZuoService { |
| | | |
| | | private final RedisUtils redisUtils; |
| | | private final WangGeService wangGeService; |
| | | |
| | | /** |
| | | * 执行主要的操作逻辑,包括读取合约状态、获取市场价格信息, |
| | | * 并根据当前持仓均价和标记价格决定是否执行买卖操作。 |
| | | * |
| | | * @return 返回操作类型字符串(如买入BUY、卖出SELL等) |
| | | */ |
| | | @Override |
| | | public String caoZuo() { |
| | | // 构造Redis键名 |
| | | final String coinCode = CoinEnums.HE_YUE.getCode(); |
| | | final String instrumentsKey = InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + coinCode + ":state"; |
| | | final String positionsMarkPxKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + coinCode + ":markPx"; |
| | | final String positionsAvgPxKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + coinCode + ":avgPx"; |
| | | final String positionsOrderPriceKey = PositionsWs.POSITIONSWS_CHANNEL + ":" + coinCode + ":orderPrice"; |
| | | |
| | | // 获取合约状态 |
| | | String state = (String) redisUtils.get(instrumentsKey); |
| | | if (state == null || !OrderParamEnums.STATE_1.getValue().equals(state)) { |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | if (OrderParamEnums.STATE_4.getValue().equals(state)) { |
| | | return OrderParamEnums.ORDERING.getValue(); |
| | | } |
| | | |
| | | log.info(OrderParamEnums.getNameByValue(state)); |
| | | |
| | | // 获取标记价格和平均持仓价格 |
| | | Object markPxObj = redisUtils.get(positionsMarkPxKey); |
| | | Object avgPxObj = redisUtils.get(positionsAvgPxKey); |
| | | |
| | | if (markPxObj == null || avgPxObj == null) { |
| | | return OrderParamEnums.INIT.getValue(); |
| | | } |
| | | |
| | | try { |
| | | BigDecimal markPx = new BigDecimal((String) markPxObj); |
| | | BigDecimal avgPx = new BigDecimal((String) avgPxObj); |
| | | |
| | | log.info("开仓价格: {}, 当前价格:{},匹配队列中......", avgPx, markPx); |
| | | |
| | | // 初始化网格队列 |
| | | PriorityBlockingQueue<AscBigDecimal> queueAsc = WangGeQueue.getQueueAsc(); |
| | | PriorityBlockingQueue<DescBigDecimal> queueKaiCang = wangGeService.initKaiCang(avgPx, queueAsc); |
| | | PriorityBlockingQueue<AscBigDecimal> queuePingCang = wangGeService.initPingCang(avgPx, queueAsc); |
| | | |
| | | // 处理订单价格在队列中的情况 |
| | | String orderPrice = (String) redisUtils.get(positionsOrderPriceKey); |
| | | handleOrderPriceInQueues(orderPrice, queueKaiCang, queuePingCang); |
| | | |
| | | String side = OrderParamEnums.HOLDING.getValue(); |
| | | |
| | | // 判断是加仓还是减仓 |
| | | if (avgPx.compareTo(markPx) > 0) { |
| | | DescBigDecimal kaiCang = queueKaiCang.peek(); |
| | | if (kaiCang != null && kaiCang.getValue().compareTo(markPx) >= 0) { |
| | | log.info("开始加仓...开仓队列价格大于当前价格{}>{}", kaiCang.getValue(), markPx); |
| | | side = OrderParamEnums.BUY.getValue(); |
| | | redisUtils.set(positionsOrderPriceKey, String.valueOf(kaiCang.getValue()), 0); |
| | | } else { |
| | | log.info("未触发加仓......,等待"); |
| | | } |
| | | } else if (avgPx.compareTo(markPx) < 0) { |
| | | AscBigDecimal pingCang = queuePingCang.peek(); |
| | | if (pingCang != null && pingCang.getValue().compareTo(markPx) <= 0) { |
| | | log.info("开始减仓...平仓队列价格小于当前价格{}<={}", pingCang.getValue(), markPx); |
| | | side = OrderParamEnums.SELL.getValue(); |
| | | redisUtils.set(positionsOrderPriceKey, String.valueOf(pingCang.getValue()), 0); |
| | | } else { |
| | | log.info("未触发减仓......,等待"); |
| | | } |
| | | } else { |
| | | log.info("价格波动较小......,等待"); |
| | | } |
| | | |
| | | return side; |
| | | } catch (NumberFormatException e) { |
| | | log.error("解析价格失败,请检查Redis中的值是否合法", e); |
| | | return OrderParamEnums.HOLDING.getValue(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据订单价格更新开仓和平仓队列的内容。 |
| | | * 若该价格不在对应队列中则加入;若已存在,则从队列中移除。 |
| | | * |
| | | * @param orderPrice 订单价格 |
| | | * @param queueKaiCang 开仓价格优先队列(降序) |
| | | * @param queuePingCang 平仓价格优先队列(升序) |
| | | */ |
| | | private void handleOrderPriceInQueues(String orderPrice, |
| | | PriorityBlockingQueue<DescBigDecimal> queueKaiCang, |
| | | PriorityBlockingQueue<AscBigDecimal> queuePingCang) { |
| | | if (orderPrice == null) { |
| | | return; |
| | | } |
| | | |
| | | BigDecimal priceDecimal; |
| | | try { |
| | | priceDecimal = new BigDecimal(orderPrice); |
| | | } catch (NumberFormatException ex) { |
| | | log.warn("无效的价格格式: {}", orderPrice); |
| | | return; |
| | | } |
| | | |
| | | boolean kaiCangExists = queueKaiCang.stream().anyMatch(item -> item.getValue().equals(priceDecimal)); |
| | | if (!kaiCangExists) { |
| | | queueKaiCang.add(new DescBigDecimal(orderPrice)); |
| | | } else { |
| | | queueKaiCang.removeIf(item -> item.getValue().equals(priceDecimal)); |
| | | } |
| | | |
| | | boolean pingCangExists = queuePingCang.stream().anyMatch(item -> item.getValue().equals(priceDecimal)); |
| | | if (!pingCangExists) { |
| | | queuePingCang.add(new AscBigDecimal(orderPrice)); |
| | | } else { |
| | | queuePingCang.removeIf(item -> item.getValue().equals(priceDecimal)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 计算盈亏金额。 |
| | | * |
| | | * @param faceValue 面值 |
| | | * @param position 持仓数量 |
| | | * @param contractMultiplier 合约乘数 |
| | | * @param markPrice 标记价格 |
| | | * @param openPrice 开仓价格 |
| | | * @param isLong 是否为多头仓位 |
| | | * @param minTickSz 最小变动单位精度 |
| | | * @return 盈亏金额,保留指定精度的小数位 |
| | | */ |
| | | public BigDecimal profit(BigDecimal faceValue, BigDecimal position, BigDecimal contractMultiplier, |
| | | BigDecimal markPrice, BigDecimal openPrice, boolean isLong, int minTickSz) { |
| | | BigDecimal profit = BigDecimal.ZERO; |
| | | if (isLong) { |
| | | profit = markPrice.subtract(openPrice) |
| | | .multiply(faceValue) |
| | | .multiply(contractMultiplier) |
| | | .multiply(position); |
| | | } else { |
| | | profit = openPrice.subtract(markPrice) |
| | | .multiply(faceValue) |
| | | .multiply(contractMultiplier) |
| | | .multiply(position); |
| | | } |
| | | return profit.setScale(minTickSz, BigDecimal.ROUND_DOWN); |
| | | } |
| | | } |
| | | |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.jiaoyi; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeRequestBuy; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeRequestSell; |
| | | |
| | | public interface IMQService { |
| | | |
| | | |
| | | |
| | | /** |
| | | * 消费买入消息 |
| | | * @param returnVo |
| | | */ |
| | | void operationBuyMsg(TradeRequestBuy returnVo); |
| | | |
| | | |
| | | /** |
| | | * 消费卖出消息 |
| | | * @param returnVo |
| | | */ |
| | | void operationSellMsg(TradeRequestSell returnVo); |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.jiaoyi; |
| | | |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import cn.hutool.json.JSONUtil; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.MallUtils; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.QuantApiMessage; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.enumerates.TradeTypeEnum; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeEventEnum; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeEventRunner; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeRequestBuy; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeRequestSell; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.VerifyAccountFactory; |
| | | import com.xcong.excoin.modules.okxNewPrice.zhanghu.IApiMessageService; |
| | | import com.xcong.excoin.modules.okxNewPrice.zhanghu.ZhangHuEnum; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.LinkedHashMap; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class IMQServiceImpl implements IMQService{ |
| | | |
| | | private final IApiMessageService apiMessageService; |
| | | |
| | | private final VerifyAccountFactory verifyAccountFactory; |
| | | @Override |
| | | public void operationBuyMsg(TradeRequestBuy returnVo) { |
| | | |
| | | log.info("买入入参:{}", JSON.toJSONString(returnVo)); |
| | | /** |
| | | * 1、获取用户的策略信息 |
| | | * 2、quant_operate_recode 记录表新增一条记录,finish_status 为1.未完成 |
| | | * 3、发起OKX接口调用 |
| | | */ |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage(ZhangHuEnum.JIAOYISUO.getValue()); |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | return; |
| | | } |
| | | |
| | | OKXAccount okxAccount = (OKXAccount) verifyAccountFactory.getAccountMap() |
| | | .get(quantApiMessage.getExchange()) |
| | | .initAccount(quantApiMessage); |
| | | if(ObjectUtil.isEmpty(okxAccount)){ |
| | | return; |
| | | } |
| | | |
| | | LinkedHashMap<String, Object> requestParam = new LinkedHashMap<>(); |
| | | requestParam.put("instId", returnVo.getInstId()+"-SWAP"); |
| | | /** |
| | | * 订单方向 |
| | | * buy:买, sell:卖 |
| | | */ |
| | | requestParam.put("side", TradeTypeEnum.BUY.getValue()); |
| | | /** |
| | | * 订单类型 |
| | | * market:市价单 |
| | | * limit:限价单 |
| | | * post_only:只做maker单 |
| | | * fok:全部成交或立即取消 |
| | | * ioc:立即成交并取消剩余 |
| | | * optimal_limit_ioc:市价委托立即成交并取消剩余(仅适用交割、永续) |
| | | * mmp:做市商保护(仅适用于组合保证金账户模式下的期权订单) |
| | | * mmp_and_post_only:做市商保护且只做maker单(仅适用于组合保证金账户模式下的期权订单) |
| | | */ |
| | | String type = (BigDecimal.ZERO.compareTo(new BigDecimal(returnVo.getLimitPrice())) == 0) ? TradeTypeEnum.MARKET.getValue() : TradeTypeEnum.LIMIT.getValue(); |
| | | requestParam.put("ordType", type); |
| | | if (TradeTypeEnum.LIMIT.getValue().equals(type)) { |
| | | requestParam.put("px", returnVo.getLimitPrice()); |
| | | requestParam.put("ordType", TradeTypeEnum.LIMIT.getValue()); |
| | | } |
| | | /** |
| | | * 持仓方向 |
| | | * 在开平仓模式下必填,且仅可选择 long 或 short。 仅适用交割、永续。 |
| | | */ |
| | | String positionSide = (TradeTypeEnum.LONG.getCode() == returnVo.getPositionSide()) ? TradeTypeEnum.LONG.getValue() : TradeTypeEnum.SHORT.getValue(); |
| | | requestParam.put("posSide", positionSide); |
| | | /** |
| | | * 交易模式 |
| | | * 保证金模式:isolated:逐仓 ;cross:全仓 |
| | | * 非保证金模式:cash:非保证金 |
| | | * spot_isolated:现货逐仓(仅适用于现货带单) ,现货带单时,tdMode 的值需要指定为spot_isolated |
| | | */ |
| | | String tdMode = (TradeTypeEnum.ISOLATED.getCode() == returnVo.getTdMode()) ? TradeTypeEnum.ISOLATED.getValue() : TradeTypeEnum.CROSS.getValue(); |
| | | requestParam.put("tdMode", tdMode); |
| | | /** |
| | | * 委托数量 |
| | | */ |
| | | requestParam.put("sz", returnVo.getTradeCnt()); |
| | | /** |
| | | * 客户自定义订单ID |
| | | * 字母(区分大小写)与数字的组合,可以是纯字母、纯数字且长度要在1-32位之间。 |
| | | */ |
| | | String clOrdId = MallUtils.getOrderNum(); |
| | | requestParam.put("clOrdId", clOrdId); |
| | | |
| | | String placeOrderRspOkxRestResponse = TradeEventRunner.runTradeEvent(TradeEventEnum.TRADE_BUY.getEventPoint(),requestParam,okxAccount); |
| | | log.info("下买单结果成功:{}", placeOrderRspOkxRestResponse); |
| | | log.info("消费成功:{}", JSONUtil.parseObj(returnVo)); |
| | | } |
| | | |
| | | @Override |
| | | public void operationSellMsg(TradeRequestSell returnVo) { |
| | | /** |
| | | * 1、获取用户的策略信息 |
| | | * 2、quant_operate_recode 记录表新增一条记录,finish_status 为1.未完成 |
| | | * 3、发起OKX接口调用 |
| | | */ |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage(ZhangHuEnum.JIAOYISUO.getValue()); |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | return; |
| | | } |
| | | |
| | | OKXAccount okxAccount = (OKXAccount) verifyAccountFactory.getAccountMap() |
| | | .get(quantApiMessage.getExchange()) |
| | | .initAccount(quantApiMessage); |
| | | if(ObjectUtil.isEmpty(okxAccount)){ |
| | | return; |
| | | } |
| | | LinkedHashMap<String, Object> requestParam = new LinkedHashMap<>(); |
| | | |
| | | requestParam.put("instId", returnVo.getInstId()+"-SWAP"); |
| | | /** |
| | | * 订单方向 |
| | | * buy:买, sell:卖 |
| | | */ |
| | | requestParam.put("side", TradeTypeEnum.SELL.getValue()); |
| | | /** |
| | | * 订单类型 |
| | | * market:市价单 |
| | | * limit:限价单 |
| | | * post_only:只做maker单 |
| | | * fok:全部成交或立即取消 |
| | | * ioc:立即成交并取消剩余 |
| | | * optimal_limit_ioc:市价委托立即成交并取消剩余(仅适用交割、永续) |
| | | * mmp:做市商保护(仅适用于组合保证金账户模式下的期权订单) |
| | | * mmp_and_post_only:做市商保护且只做maker单(仅适用于组合保证金账户模式下的期权订单) |
| | | */ |
| | | String type = (BigDecimal.ZERO.compareTo(new BigDecimal(returnVo.getLimitPrice())) == 0) ? TradeTypeEnum.MARKET.getValue() : TradeTypeEnum.LIMIT.getValue(); |
| | | requestParam.put("ordType", type); |
| | | if (TradeTypeEnum.LIMIT.getValue().equals(type)) { |
| | | requestParam.put("px", returnVo.getLimitPrice()); |
| | | requestParam.put("ordType", TradeTypeEnum.LIMIT.getValue()); |
| | | } |
| | | /** |
| | | * 持仓方向 |
| | | * 在开平仓模式下必填,且仅可选择 long 或 short。 仅适用交割、永续。 |
| | | */ |
| | | String positionSide = (TradeTypeEnum.LONG.getCode() == returnVo.getPositionSide()) ? TradeTypeEnum.LONG.getValue() : TradeTypeEnum.SHORT.getValue(); |
| | | requestParam.put("posSide", positionSide); |
| | | /** |
| | | * 交易模式 |
| | | * 保证金模式:isolated:逐仓 ;cross:全仓 |
| | | * 非保证金模式:cash:非保证金 |
| | | * spot_isolated:现货逐仓(仅适用于现货带单) ,现货带单时,tdMode 的值需要指定为spot_isolated |
| | | */ |
| | | String tdMode = (TradeTypeEnum.ISOLATED.getCode() == returnVo.getTdMode()) ? TradeTypeEnum.ISOLATED.getValue() : TradeTypeEnum.CROSS.getValue(); |
| | | requestParam.put("tdMode", tdMode); |
| | | /** |
| | | * 委托数量 |
| | | */ |
| | | requestParam.put("sz", returnVo.getTradeCnt()); |
| | | /** |
| | | * 客户自定义订单ID |
| | | * 字母(区分大小写)与数字的组合,可以是纯字母、纯数字且长度要在1-32位之间。 |
| | | */ |
| | | String clOrdId = MallUtils.getOrderNum(); |
| | | requestParam.put("clOrdId", clOrdId); |
| | | |
| | | |
| | | String placeOrderRspOkxRestResponse = TradeEventRunner.runTradeEvent(TradeEventEnum.TRADE_SELL.getEventPoint(), requestParam, okxAccount); |
| | | |
| | | String code = JSON.parseObject(placeOrderRspOkxRestResponse).get("code").toString(); |
| | | if("1".equals(code)){ |
| | | String data = JSON.parseObject(placeOrderRspOkxRestResponse).get("data").toString(); |
| | | |
| | | JSONArray jsonArray = JSON.parseArray(data); |
| | | JSONObject jsonObject = jsonArray.getJSONObject(0); |
| | | |
| | | String sCode = jsonObject.getString("sCode"); |
| | | if("51169".equals(sCode)){ |
| | | log.warn("平仓下单返回结果-失败:{}", data); |
| | | } |
| | | } |
| | | log.info("平仓下单返回结果:{}", placeOrderRspOkxRestResponse); |
| | | log.info("消费成功:{}", JSONUtil.parseObj(returnVo)); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxWs; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.CoinEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.OrderParamEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.MallUtils; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.WsParamBuild; |
| | | import com.xcong.excoin.utils.RedisUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.java_websocket.client.WebSocketClient; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | |
| | | /** |
| | | * 账户 WebSocket 处理类,用于订阅 OKX 的账户频道并处理账户信息推送。 |
| | | * 包含账户资金状态判断、计算下单保证金及保存到 Redis 的逻辑。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class AccountWs { |
| | | |
| | | /** |
| | | * 账户频道名称常量 |
| | | */ |
| | | public static final String ACCOUNTWS_CHANNEL = "account"; |
| | | |
| | | private static final String CCY_KEY = "ccy"; |
| | | private static final String AVAIL_BAL_KEY = "availBal"; |
| | | private static final String CASH_BAL_KEY = "cashBal"; |
| | | private static final String EQ_KEY = "eq"; |
| | | private static final String DETAILS_KEY = "details"; |
| | | private static final String DATA_KEY = "data"; |
| | | |
| | | // 缓存常用 BigDecimal 常量 |
| | | private static final BigDecimal KANG_CANG_THRESHOLD = new BigDecimal(OrderParamEnums.KANG_CANG.getValue()); |
| | | private static final BigDecimal ZHI_SUN_THRESHOLD = new BigDecimal(OrderParamEnums.ZHI_SUN.getValue()); |
| | | private static final BigDecimal TOTAL_ORDER_USDT_FACTOR = new BigDecimal(OrderParamEnums.TOTAL_ORDER_USDT.getValue()); |
| | | private static final BigDecimal EVERY_TIME_USDT_FACTOR = new BigDecimal(OrderParamEnums.EVERY_TIME_USDT.getValue()); |
| | | |
| | | /** |
| | | * 订阅账户频道 |
| | | * |
| | | * @param webSocketClient WebSocket 客户端实例 |
| | | * @param option 请求选项(如 unsubscribe 或 subscribe) |
| | | */ |
| | | public static void subscribeAccountChannel(WebSocketClient webSocketClient, String option) { |
| | | try { |
| | | JSONArray argsArray = new JSONArray(); |
| | | JSONObject args = new JSONObject(); |
| | | args.put("channel", ACCOUNTWS_CHANNEL); |
| | | args.put(CCY_KEY, CoinEnums.USDT.getCode()); |
| | | argsArray.add(args); |
| | | |
| | | String connId = MallUtils.getOrderNum(ACCOUNTWS_CHANNEL); |
| | | JSONObject jsonObject = WsParamBuild.buildJsonObject(connId, option, argsArray); |
| | | webSocketClient.send(jsonObject.toJSONString()); |
| | | log.info("发送账户频道:{}", option); |
| | | } catch (Exception e) { |
| | | log.error("订阅账户频道构建失败", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 处理账户频道推送的数据 |
| | | * |
| | | * @param response 推送的 JSON 数据对象 |
| | | * @param redisUtils Redis 工具类实例,用于存储账户相关信息 |
| | | */ |
| | | public static void handleEvent(JSONObject response, RedisUtils redisUtils) { |
| | | try { |
| | | JSONArray dataArray = response.getJSONArray(DATA_KEY); |
| | | if (dataArray == null || dataArray.isEmpty()) { |
| | | log.warn("账户频道数据为空"); |
| | | return; |
| | | } |
| | | |
| | | for (int i = 0; i < dataArray.size(); i++) { |
| | | try { |
| | | JSONObject accountData = dataArray.getJSONObject(i); |
| | | JSONArray detailsArray = accountData.getJSONArray(DETAILS_KEY); |
| | | if (detailsArray == null || detailsArray.isEmpty()) { |
| | | continue; |
| | | } |
| | | |
| | | for (int j = 0; j < detailsArray.size(); j++) { |
| | | JSONObject detail = detailsArray.getJSONObject(j); |
| | | |
| | | String ccy = detail.getString(CCY_KEY); |
| | | String availBalStr = detail.getString(AVAIL_BAL_KEY); |
| | | String cashBalStr = detail.getString(CASH_BAL_KEY); |
| | | String eq = detail.getString(EQ_KEY); |
| | | |
| | | if (StrUtil.isBlank(ccy) || StrUtil.isBlank(availBalStr) || StrUtil.isBlank(cashBalStr)) { |
| | | log.warn("账户详情缺失必要字段,跳过处理"); |
| | | continue; |
| | | } |
| | | |
| | | BigDecimal availBal = parseBigDecimalSafe(availBalStr); |
| | | BigDecimal cashBal = parseBigDecimalSafe(cashBalStr); |
| | | |
| | | if (availBal == null || cashBal == null || cashBal.compareTo(BigDecimal.ZERO) == 0) { |
| | | log.warn("无效的账户余额数据,跳过处理"); |
| | | continue; |
| | | } |
| | | |
| | | // 可用余额 / 现金余额 比例判断是否允许开仓 |
| | | BigDecimal divide = availBal.divide(cashBal, 4, RoundingMode.DOWN); |
| | | |
| | | String state; |
| | | if (divide.compareTo(KANG_CANG_THRESHOLD) > 0) { |
| | | log.info(OrderParamEnums.STATE_1.getName()); |
| | | state = OrderParamEnums.STATE_1.getValue(); |
| | | } else if (divide.compareTo(ZHI_SUN_THRESHOLD) > 0) { |
| | | log.warn(OrderParamEnums.STATE_2.getName()); |
| | | state = OrderParamEnums.STATE_2.getValue(); |
| | | } else { |
| | | log.error(OrderParamEnums.STATE_3.getName()); |
| | | state = OrderParamEnums.STATE_3.getValue(); |
| | | } |
| | | |
| | | // 根据可用余额计算下单总保证金与每次下单金额 |
| | | BigDecimal totalOrderUsdt = availBal.multiply(TOTAL_ORDER_USDT_FACTOR).setScale(4, RoundingMode.DOWN); |
| | | BigDecimal everyTimeUsdt = totalOrderUsdt.divide(EVERY_TIME_USDT_FACTOR, 4, RoundingMode.DOWN); |
| | | |
| | | log.info( |
| | | "账户详情-币种: {}, 可用余额: {}, 现金余额: {}, 余额: {}, 下单总保证金: {}, 每次下单保证金: {}, 是否允许开仓: {}", |
| | | ccy, availBalStr, cashBalStr, eq, totalOrderUsdt, everyTimeUsdt, state |
| | | ); |
| | | |
| | | saveToRedis(redisUtils, availBalStr, cashBalStr, totalOrderUsdt.toString(), everyTimeUsdt.toString(), state); |
| | | } |
| | | } catch (Exception innerEx) { |
| | | log.warn("处理账户频道数据失败", innerEx); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("处理账户频道推送数据失败", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 安全地将字符串解析为 BigDecimal 类型 |
| | | * |
| | | * @param value 字符串数值 |
| | | * @return 解析后的 BigDecimal 对象,若解析失败则返回 null |
| | | */ |
| | | private static BigDecimal parseBigDecimalSafe(String value) { |
| | | try { |
| | | return new BigDecimal(value); |
| | | } catch (NumberFormatException e) { |
| | | log.warn("无法转换为 BigDecimal: {}", value); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 将账户相关数据保存至 Redis 中 |
| | | * |
| | | * @param redisUtils Redis 工具类实例 |
| | | * @param availBal 可用余额 |
| | | * @param cashBal 现金余额 |
| | | * @param totalOrderUsdt 总下单保证金 |
| | | * @param everyTimeUsdt 每次下单保证金 |
| | | * @param state 当前账户状态(是否可开仓) |
| | | */ |
| | | private static void saveToRedis(RedisUtils redisUtils, String availBal, String cashBal, |
| | | String totalOrderUsdt, String everyTimeUsdt, String state) { |
| | | try { |
| | | boolean setResult = |
| | | redisUtils.set(ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":availBal", availBal, 0) |
| | | && redisUtils.set(ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":cashBal", cashBal, 0) |
| | | && redisUtils.set(ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":totalOrderUsdt", totalOrderUsdt, 0) |
| | | && redisUtils.set(ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":everyTimeUsdt", everyTimeUsdt, 0) |
| | | && redisUtils.set(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state", state, 0); |
| | | |
| | | if (!setResult) { |
| | | log.warn("Redis set operation failed for key: account:{}", CoinEnums.USDT.getCode()); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("Redis操作异常,key: account:{}, error: {}", CoinEnums.USDT.getCode(), e.getMessage(), e); |
| | | } |
| | | } |
| | | } |
| | | |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxWs; |
| | | |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.MallUtils; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.WsParamBuild; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.java_websocket.client.WebSocketClient; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class BalanceAndPositionWs { |
| | | |
| | | public static final String CHANNEL_NAME = "balance_and_position"; |
| | | private static final String LOG_PREFIX = "账户余额和持仓频道"; |
| | | |
| | | public static void subscribeBalanceAndPositionChannel(WebSocketClient webSocketClient, String option) { |
| | | try { |
| | | JSONArray argsArray = new JSONArray(); |
| | | JSONObject args = new JSONObject(); |
| | | args.put("channel", CHANNEL_NAME); |
| | | argsArray.add(args); |
| | | |
| | | String connId = MallUtils.getOrderNum("bap"); |
| | | JSONObject jsonObject = WsParamBuild.buildJsonObject(connId, option, argsArray); |
| | | webSocketClient.send(jsonObject.toJSONString()); |
| | | log.info("账户余额和持仓频道订阅成功: {}", LOG_PREFIX, option); |
| | | } catch (Exception e) { |
| | | log.error("账户余额和持仓频道订阅构建失败", LOG_PREFIX, e); |
| | | } |
| | | } |
| | | |
| | | public static void handleEvent(JSONObject response) { |
| | | try { |
| | | JSONArray dataArray = response.getJSONArray("data"); |
| | | if (dataArray == null || dataArray.isEmpty()) { |
| | | log.warn("{}数据为空", LOG_PREFIX); |
| | | return; |
| | | } |
| | | |
| | | JSONObject firstData = dataArray.getJSONObject(0); |
| | | processBalData(firstData); |
| | | processPosData(firstData); |
| | | } catch (Exception e) { |
| | | log.error("{}推送数据处理失败", LOG_PREFIX, e); |
| | | } |
| | | } |
| | | |
| | | private static void processBalData(JSONObject dataObject) { |
| | | JSONArray balDataArray = dataObject.getJSONArray("balData"); |
| | | if (balDataArray == null || balDataArray.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | for (int i = 0; i < balDataArray.size(); i++) { |
| | | JSONObject balData = balDataArray.getJSONObject(i); |
| | | if (!balData.containsKey("ccy") || !balData.containsKey("cashBal") || !balData.containsKey("uTime")) { |
| | | continue; |
| | | } |
| | | |
| | | String ccy = balData.getString("ccy"); |
| | | String cashBal = balData.getString("cashBal"); |
| | | String uTime = balData.getString("uTime"); |
| | | |
| | | log.info("币种: {}, 余额: {}, 更新时间: {}", ccy, cashBal, uTime); |
| | | } |
| | | } |
| | | |
| | | private static void processPosData(JSONObject dataObject) { |
| | | JSONArray posDataArray = dataObject.getJSONArray("posData"); |
| | | if (posDataArray == null || posDataArray.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | for (int i = 0; i < posDataArray.size(); i++) { |
| | | JSONObject posData = posDataArray.getJSONObject(i); |
| | | if (!posData.containsKey("posId") || !posData.containsKey("instId") |
| | | || !posData.containsKey("instType") || !posData.containsKey("posSide") |
| | | || !posData.containsKey("pos") || !posData.containsKey("avgPx") |
| | | || !posData.containsKey("ccy")) { |
| | | continue; |
| | | } |
| | | |
| | | String posId = posData.getString("posId"); |
| | | String instId = posData.getString("instId"); |
| | | String instType = posData.getString("instType"); |
| | | String posSide = posData.getString("posSide"); |
| | | String pos = posData.getString("pos"); |
| | | String avgPx = posData.getString("avgPx"); |
| | | String ccy = posData.getString("ccy"); |
| | | |
| | | log.info("持仓ID: {}, 产品ID: {}, 类型: {}, 方向: {}, 数量: {}, 平均价: {}, 币种: {}", |
| | | posId, instId, instType, posSide, pos, avgPx, ccy); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxWs; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.CoinEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.OrderParamEnums; |
| | | import com.xcong.excoin.utils.RedisUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class InstrumentsWs { |
| | | |
| | | public static final String INSTRUMENTSWS_CHANNEL = "instruments"; |
| | | |
| | | public static void handleEvent(RedisUtils redisUtils) { |
| | | // 将账户数据保存到Redis中,设置过期时间为30分钟 |
| | | try { |
| | | boolean setResult = |
| | | redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":instId", CoinEnums.HE_YUE.getCode(), 0) |
| | | && redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":ctVal", "0.01", 0) |
| | | && redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":tickSz", "2", 0) |
| | | && redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":minSz", "2", 0) |
| | | && redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":instIdCode", CoinEnums.HE_YUE.getCode(), 0) |
| | | && redisUtils.set(INSTRUMENTSWS_CHANNEL+":" + CoinEnums.HE_YUE.getCode()+":state", OrderParamEnums.STATE_0.getValue(), 0) |
| | | ; |
| | | if (!setResult) { |
| | | log.warn("Redis set operation failed for key: account:{}", CoinEnums.HE_YUE.getCode()); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("Redis操作异常,key: account:{}, error: {}", CoinEnums.HE_YUE.getCode(), e.getMessage(), e); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxWs; |
| | | |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.ExchangeInfoEnum; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.SignUtils; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.WsParamBuild; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.java_websocket.client.WebSocketClient; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class LoginWs { |
| | | public static void websocketLogin(WebSocketClient webSocketClient) { |
| | | try { |
| | | |
| | | JSONArray argsArray = new JSONArray(); |
| | | JSONObject loginArgs = new JSONObject(); |
| | | // 获取登录凭证信息(需要从配置或Redis中获取) |
| | | String apiKey = ExchangeInfoEnum.OKX_UAT.getApiKey(); |
| | | String passphrase = ExchangeInfoEnum.OKX_UAT.getPassphrase(); |
| | | String timestamp = String.valueOf(System.currentTimeMillis() /1000); |
| | | String sign = SignUtils.signWebsocket(timestamp, ExchangeInfoEnum.OKX_UAT.getSecretKey()); |
| | | |
| | | loginArgs.put("apiKey", apiKey); |
| | | loginArgs.put("passphrase", passphrase); |
| | | loginArgs.put("timestamp", timestamp); |
| | | loginArgs.put("sign", sign); |
| | | argsArray.add(loginArgs); |
| | | |
| | | String option = "login"; |
| | | JSONObject login = WsParamBuild.buildJsonObject(null,option, argsArray); |
| | | webSocketClient.send(login.toJSONString()); |
| | | log.info("发送登录:{}",option); |
| | | } catch (Exception e) { |
| | | log.error("WebSocket登录请求构建失败", e); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxWs; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.CoinEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.OrderParamEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.MallUtils; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.WsParamBuild; |
| | | import com.xcong.excoin.utils.RedisUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.java_websocket.client.WebSocketClient; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class OrderInfoWs { |
| | | |
| | | public static final String ORDERINFOWS_CHANNEL = "orders"; |
| | | |
| | | public static void subscribeOrderInfoChannel(WebSocketClient webSocketClient, String option) { |
| | | try { |
| | | JSONArray argsArray = new JSONArray(); |
| | | JSONObject args = new JSONObject(); |
| | | args.put("channel", ORDERINFOWS_CHANNEL); |
| | | args.put("instType", CoinEnums.INSTTYPE_SWAP.getCode()); |
| | | args.put("instId", CoinEnums.HE_YUE.getCode()); |
| | | argsArray.add(args); |
| | | |
| | | String connId = MallUtils.getOrderNum(ORDERINFOWS_CHANNEL); |
| | | JSONObject jsonObject = WsParamBuild.buildJsonObject(connId, option, argsArray); |
| | | webSocketClient.send(jsonObject.toJSONString()); |
| | | log.info("发送订单频道频道:{}", option); |
| | | } catch (Exception e) { |
| | | log.error("订阅订单频道构建失败", e); |
| | | } |
| | | } |
| | | |
| | | |
| | | private static final String DATA_KEY = "data"; |
| | | private static final String INSTID_KEY = "instId"; |
| | | private static final String ORDID_KEY = "ordId"; |
| | | private static final String CLORDID_KEY = "clOrdId"; |
| | | private static final String SIDE_KEY = "side"; |
| | | private static final String TDMODE_KEY = "tdMode"; |
| | | private static final String ACCFILLSZ_KEY = "accFillSz"; |
| | | private static final String AVGPX_KEY = "avgPx"; |
| | | private static final String STATE_KEY = "state"; |
| | | public static void handleEvent(JSONObject response, RedisUtils redisUtils) { |
| | | try { |
| | | JSONArray dataArray = response.getJSONArray(DATA_KEY); |
| | | if (dataArray == null || dataArray.isEmpty()) { |
| | | log.warn("订单频道数据为空"); |
| | | return; |
| | | } |
| | | |
| | | for (int i = 0; i < dataArray.size(); i++) { |
| | | JSONObject detail = dataArray.getJSONObject(i); |
| | | |
| | | String instId = detail.getString(INSTID_KEY); |
| | | String ordId = detail.getString(ORDID_KEY); |
| | | String clOrdId = detail.getString(CLORDID_KEY); |
| | | String side = detail.getString(SIDE_KEY); |
| | | String tdMode = detail.getString(TDMODE_KEY); |
| | | String accFillSz = detail.getString(ACCFILLSZ_KEY); |
| | | String avgPx = detail.getString(AVGPX_KEY); |
| | | String state = detail.getString(STATE_KEY); |
| | | |
| | | log.info( |
| | | "订单详情-币种: {}, 系统编号: {}, 自定义编号: {}, 订单方向: {}, 交易模式: {}," + |
| | | " 累计成交数量: {}, 成交均价: {}, 订单状态: {}", |
| | | instId, ordId, clOrdId, side, tdMode, |
| | | accFillSz, avgPx,state |
| | | ); |
| | | |
| | | String clOrdIdStr = (String) redisUtils.get(TradeOrderWs.ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":clOrdId"); |
| | | String stateStr = (String) redisUtils.get(TradeOrderWs.ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state"); |
| | | if ( |
| | | clOrdIdStr != null |
| | | && clOrdId.equals(clOrdIdStr) |
| | | && stateStr != null |
| | | && state.equals(stateStr) |
| | | ){ |
| | | redisUtils.set(TradeOrderWs.ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state", OrderParamEnums.STATE_1.getValue(), 0); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("处理订单频道推送数据失败", e); |
| | | } |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxWs; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.CoinEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.OrderParamEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.MallUtils; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.WsParamBuild; |
| | | import com.xcong.excoin.utils.RedisUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.java_websocket.client.WebSocketClient; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.Optional; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class PositionsWs { |
| | | |
| | | public static final String POSITIONSWS_CHANNEL = "positions"; |
| | | private static final String REDIS_KEY_PREFIX = POSITIONSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode(); |
| | | |
| | | public static void subscribePositionChannel(WebSocketClient webSocketClient, String option) { |
| | | try { |
| | | JSONArray argsArray = new JSONArray(); |
| | | JSONObject args = new JSONObject(); |
| | | args.put("channel", POSITIONSWS_CHANNEL); |
| | | args.put("instType", CoinEnums.INSTTYPE_SWAP.getCode()); |
| | | args.put("instId", CoinEnums.HE_YUE.getCode()); |
| | | argsArray.add(args); |
| | | |
| | | String connId = MallUtils.getOrderNum(POSITIONSWS_CHANNEL); |
| | | JSONObject jsonObject = WsParamBuild.buildJsonObject(connId, option, argsArray); |
| | | webSocketClient.send(jsonObject.toJSONString()); |
| | | log.info("发送持仓频道频道:{}", option); |
| | | } catch (Exception e) { |
| | | log.error("订阅持仓频道频道构建失败", e); |
| | | } |
| | | } |
| | | |
| | | public static void handleEvent(JSONObject response, RedisUtils redisUtils) { |
| | | try { |
| | | JSONArray dataArray = response.getJSONArray("data"); |
| | | if (dataArray == null || dataArray.isEmpty()) { |
| | | log.info("账户持仓频道数据为空,已当前价买入,并且初始化网格"); |
| | | return; |
| | | } |
| | | |
| | | for (int i = 0; i < dataArray.size(); i++) { |
| | | JSONObject posData = dataArray.getJSONObject(i); |
| | | String instId = posData.getString("instId"); |
| | | |
| | | if (!CoinEnums.HE_YUE.getCode().equals(instId)) { |
| | | continue; |
| | | } |
| | | String mgnMode = posData.getString("mgnMode"); |
| | | String posSide = posData.getString("posSide"); |
| | | String pos = posData.getString("pos"); |
| | | String avgPx = posData.getString("avgPx"); |
| | | String upl = posData.getString("upl"); |
| | | String uplRatio = posData.getString("uplRatio"); |
| | | String lever = posData.getString("lever"); |
| | | String liqPx = posData.getString("liqPx"); |
| | | String markPx = posData.getString("markPx"); |
| | | String imr = posData.getString("imr"); |
| | | String mgnRatio = posData.getString("mgnRatio"); |
| | | String mmr = posData.getString("mmr"); |
| | | String notionalUsd = posData.getString("notionalUsd"); |
| | | String ccy = posData.getString("ccy"); |
| | | String last = posData.getString("last"); |
| | | String idxPx = posData.getString("idxPx"); |
| | | String bePx = posData.getString("bePx"); |
| | | String realizedPnl = posData.getString("realizedPnl"); |
| | | String settledPnl = posData.getString("settledPnl"); |
| | | log.info( |
| | | "账户持仓频道-产品类型: {}, 保证金模式: {}, 持仓方向: {}, 持仓数量: {}, 开仓平均价: {}, " |
| | | + "未实现收益: {}, 未实现收益率: {}, 杠杆倍数: {}, 预估强平价: {}, 初始保证金: {}, " |
| | | + "维持保证金率: {}, 维持保证金: {}, 以美金价值为单位的持仓数量: {}, 占用保证金的币种: {}, " |
| | | + "最新成交价: {}, 最新指数价格: {}, 盈亏平衡价: {}, 已实现收益: {}, 累计已结算收益: {}" |
| | | + "最新标记价格: {}", |
| | | instId, mgnMode, posSide, pos, avgPx, |
| | | upl, uplRatio, lever, liqPx, imr, |
| | | mgnRatio, mmr, notionalUsd, ccy, |
| | | last, idxPx, bePx, realizedPnl, settledPnl, |
| | | markPx |
| | | ); |
| | | |
| | | processPositionData(posData, redisUtils); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("处理持仓频道推送数据失败", e); |
| | | } |
| | | } |
| | | |
| | | private static void processPositionData(JSONObject posData, RedisUtils redisUtils) { |
| | | try { |
| | | String avgPx = safeGetString(posData, "avgPx"); |
| | | String pos = safeGetString(posData, "pos"); |
| | | String upl = safeGetString(posData, "upl"); |
| | | String imr = safeGetString(posData, "imr"); |
| | | String mgnRatio = safeGetString(posData, "mgnRatio"); |
| | | String markPx = safeGetString(posData, "markPx"); |
| | | String bePx = safeGetString(posData, "bePx"); |
| | | |
| | | boolean setResult = saveToRedis(redisUtils, avgPx, pos, upl, imr, mgnRatio, markPx, bePx); |
| | | |
| | | if (setResult) { |
| | | calculateAndSaveBuyCount(redisUtils); |
| | | } else { |
| | | log.warn("Redis操作失败"); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("Redis操作异常", e); |
| | | } |
| | | } |
| | | |
| | | private static boolean saveToRedis(RedisUtils redisUtils, |
| | | String avgPx, String pos, String upl, |
| | | String imr, String mgnRatio, String markPx, String bePx) { |
| | | return redisUtils.set(REDIS_KEY_PREFIX + ":avgPx", avgPx, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":pos", pos, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":upl", upl, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":imr", imr, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":mgnRatio", mgnRatio, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":markPx", markPx, 0) |
| | | && redisUtils.set(REDIS_KEY_PREFIX + ":bePx", bePx, 0); |
| | | } |
| | | |
| | | private static void calculateAndSaveBuyCount(RedisUtils redisUtils) { |
| | | try { |
| | | String ctValStr = (String) redisUtils.get(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":ctVal"); |
| | | String markPxStr = (String) redisUtils.get(REDIS_KEY_PREFIX + ":markPx"); |
| | | String minSzStr = (String) redisUtils.get(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":minSz"); |
| | | String everyTimeUsdt = (String) redisUtils.get(AccountWs.ACCOUNTWS_CHANNEL + ":" + CoinEnums.USDT.getCode() + ":everyTimeUsdt"); |
| | | |
| | | BigDecimal margin = parseBigDecimal(everyTimeUsdt, "0"); |
| | | BigDecimal leverage = parseBigDecimal(OrderParamEnums.LEVERAGE.getValue(), "1"); |
| | | BigDecimal faceValue = parseBigDecimal(ctValStr, "0"); |
| | | BigDecimal markPrice = parseBigDecimal(markPxStr, "0"); // 默认值需谨慎对待 |
| | | int minLotSz = parseInt(minSzStr, 0); |
| | | |
| | | BigDecimal buyCnt = buyCnt(margin, leverage, faceValue, markPrice, minLotSz); |
| | | redisUtils.set(REDIS_KEY_PREFIX + ":buyCnt", buyCnt.toString(), 0); |
| | | } catch (NumberFormatException | ArithmeticException e) { |
| | | log.error("计算购买数量时发生数字转换错误", e); |
| | | } |
| | | } |
| | | |
| | | private static String safeGetString(JSONObject obj, String key) { |
| | | return Optional.ofNullable(obj.getString(key)).orElse(""); |
| | | } |
| | | |
| | | private static BigDecimal parseBigDecimal(String value, String defaultValue) { |
| | | return new BigDecimal(Optional.ofNullable(value).filter(s -> !s.isEmpty()).orElse(defaultValue)); |
| | | } |
| | | |
| | | private static int parseInt(String value, int defaultValue) { |
| | | try { |
| | | return Integer.parseInt(value); |
| | | } catch (NumberFormatException e) { |
| | | return defaultValue; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 计算购买合约的数量 |
| | | * |
| | | * USDT 币本位合约 |
| | | * 公式:张数 = 保证金 / (面值 * 标记价格 / 杠杆倍数) |
| | | * |
| | | * @param margin 用户的保证金金额 |
| | | * @param leverage 杠杆倍数 |
| | | * @param faceValue 合约面值 |
| | | * @param markPrice 标记价格 |
| | | * @param minLotSz 最小下单精度 |
| | | * @return 返回用户可以购买的合约数量 |
| | | */ |
| | | public static BigDecimal buyCnt(BigDecimal margin, BigDecimal leverage, BigDecimal faceValue, BigDecimal markPrice, int minLotSz) { |
| | | if (margin.compareTo(BigDecimal.ZERO) <= 0 || |
| | | leverage.compareTo(BigDecimal.ZERO) <= 0 || |
| | | faceValue.compareTo(BigDecimal.ZERO) <= 0 || |
| | | markPrice.compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new IllegalArgumentException("所有参数必须大于零"); |
| | | } |
| | | |
| | | BigDecimal divisor = markPrice.divide(leverage, 10, BigDecimal.ROUND_DOWN); |
| | | BigDecimal denominator = faceValue.multiply(divisor); |
| | | return margin.divide(denominator, minLotSz, BigDecimal.ROUND_DOWN); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxWs; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.CoinEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxWs.enums.OrderParamEnums; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.MallUtils; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.WsParamBuild; |
| | | import com.xcong.excoin.utils.RedisUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.java_websocket.client.WebSocketClient; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | public class TradeOrderWs { |
| | | |
| | | public static final String ORDERWS_CHANNEL = "order"; |
| | | |
| | | public static void orderEvent(WebSocketClient webSocketClient, RedisUtils redisUtils, String side) { |
| | | |
| | | String buyCnt = null; |
| | | |
| | | if (OrderParamEnums.ORDERING.getValue().equals(side)) { |
| | | return; |
| | | } else if (OrderParamEnums.HOLDING.getValue().equals(side)) { |
| | | return; |
| | | } else if (OrderParamEnums.INIT.getValue().equals(side)) { |
| | | side = OrderParamEnums.BUY.getValue(); |
| | | buyCnt = getRedisValue(redisUtils, InstrumentsWs.INSTRUMENTSWS_CHANNEL, ":ctVal"); |
| | | } else if (OrderParamEnums.OUT.getValue().equals(side)) { |
| | | side = OrderParamEnums.SELL.getValue(); |
| | | buyCnt = getRedisValue(redisUtils, PositionsWs.POSITIONSWS_CHANNEL, ":pos"); |
| | | } else { |
| | | String buyCntNormal = getRedisValue(redisUtils, PositionsWs.POSITIONSWS_CHANNEL, ":buyCnt"); |
| | | if (StrUtil.isNotBlank(buyCntNormal)) { |
| | | buyCnt = buyCntNormal; |
| | | } |
| | | } |
| | | |
| | | // 校验必要参数 |
| | | if (StrUtil.isBlank(side)) { |
| | | log.warn("下单参数 side 为空,取消发送"); |
| | | return; |
| | | } |
| | | |
| | | if (StrUtil.isBlank(buyCnt)) { |
| | | log.warn("下单数量 buyCnt 为空,取消发送"); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | String clOrdId = MallUtils.getOrderNum(side); |
| | | JSONArray argsArray = new JSONArray(); |
| | | JSONObject args = new JSONObject(); |
| | | args.put("instId", CoinEnums.HE_YUE.getCode()); |
| | | args.put("tdMode", CoinEnums.CROSS.getCode()); |
| | | args.put("clOrdId", clOrdId); |
| | | args.put("side", side); |
| | | args.put("posSide", CoinEnums.POSSIDE_LONG.getCode()); |
| | | args.put("ordType", CoinEnums.ORDTYPE_MARKET.getCode()); |
| | | args.put("sz", buyCnt); |
| | | argsArray.add(args); |
| | | |
| | | String connId = MallUtils.getOrderNum(ORDERWS_CHANNEL); |
| | | JSONObject jsonObject = WsParamBuild.buildJsonObject(connId, ORDERWS_CHANNEL, argsArray); |
| | | webSocketClient.send(jsonObject.toJSONString()); |
| | | log.info("发送下单频道:{},数量:{}", side, buyCnt); |
| | | boolean setResult = |
| | | redisUtils.set(ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":clOrdId", clOrdId, 0) |
| | | && redisUtils.set(ORDERWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state", CoinEnums.ORDER_FILLED.getCode(), 0) |
| | | && redisUtils.set(InstrumentsWs.INSTRUMENTSWS_CHANNEL + ":" + CoinEnums.HE_YUE.getCode() + ":state", OrderParamEnums.STATE_4.getValue(), 0); |
| | | if (!setResult) { |
| | | log.warn("Redis set operation failed for key: order:{}", CoinEnums.HE_YUE.getCode()); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("下单构建失败", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 统一封装 Redis Key 构建逻辑 |
| | | * |
| | | * @param redisUtils Redis 工具类实例 |
| | | * @param prefix 渠道前缀 |
| | | * @param suffix 字段后缀 |
| | | * @return Redis 中存储的值 |
| | | */ |
| | | private static String getRedisValue(RedisUtils redisUtils, String prefix, String suffix) { |
| | | String key = prefix + ":" + CoinEnums.HE_YUE.getCode() + suffix; |
| | | Object valueObj = redisUtils.get(key); |
| | | return valueObj == null ? null : String.valueOf(valueObj); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxWs.enums; |
| | | |
| | | import lombok.Getter; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | @Getter |
| | | public enum CoinEnums { |
| | | |
| | | |
| | | ORDER_FILLED("filled", |
| | | "filled"), |
| | | ORDER_LIVE("live", |
| | | "live"), |
| | | |
| | | |
| | | INSTTYPE_SWAP("SWAP", |
| | | "SWAP"), |
| | | |
| | | |
| | | ORDTYPE_MARKET("market", |
| | | "market"), |
| | | |
| | | |
| | | POSSIDE_SHORT("short", |
| | | "short"), |
| | | |
| | | |
| | | POSSIDE_LONG("long", |
| | | "long"), |
| | | |
| | | |
| | | SIDE_SELL("sell", |
| | | "sell"), |
| | | |
| | | SIDE_BUY("buy", |
| | | "buy"), |
| | | |
| | | CROSS("cross", |
| | | "cross"), |
| | | |
| | | |
| | | USDT("USDT", |
| | | "USDT"), |
| | | |
| | | |
| | | HE_YUE("BTC-USDT-SWAP", |
| | | "BTC-USDT-SWAP"); |
| | | |
| | | private String name; |
| | | |
| | | private String code; |
| | | |
| | | /** |
| | | * 构造方法 |
| | | * |
| | | * @param name |
| | | * @param code |
| | | */ |
| | | CoinEnums(String name, String code) { |
| | | this.name = name; |
| | | this.code = code; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxWs.enums; |
| | | |
| | | import com.xcong.excoin.common.enumerates.SymbolEnum; |
| | | import lombok.Getter; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | |
| | | @Getter |
| | | public enum OrderParamEnums { |
| | | ORDERING("操作下单中", "ORDERING"), |
| | | OUT("止损", "out"), |
| | | INIT("初始化", "init"), |
| | | HOLDING("持仓", "holding"), |
| | | BUY("买", "buy"), |
| | | SELL("卖", "sell"), |
| | | |
| | | |
| | | STATE_4("操作下单中", "4"), |
| | | STATE_3("止损......冷静一下", "3"), |
| | | STATE_2("抗压......", "2"), |
| | | STATE_1("允许开仓", "1"), |
| | | STATE_0("不允许开仓", "0"), |
| | | |
| | | ZHI_SUN("止损......", "0.5"), |
| | | |
| | | KANG_CANG("抗压......", "0.7"), |
| | | |
| | | LEVERAGE("杠杆倍数", "100"), |
| | | |
| | | EVERY_TIME_USDT("总下单次数", "20"), |
| | | //下单的总保障金为账户总金额cashBal * TOTAL_ORDER_USDT用来做保证金 |
| | | TOTAL_ORDER_USDT("下单的总金额比例", "0.5"), |
| | | ; |
| | | |
| | | private String name; |
| | | |
| | | private String value; |
| | | |
| | | private OrderParamEnums(String name, String value) { |
| | | this.name = name; |
| | | this.value = value; |
| | | } |
| | | |
| | | public static String getNameByValue(String value) { |
| | | String name = ""; |
| | | for (OrderParamEnums orderParamEnum : values()) { |
| | | if (value.equals(orderParamEnum.getValue())){ |
| | | name = orderParamEnum.getName(); |
| | | break; |
| | | } |
| | | } |
| | | return name; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | /** |
| | | * 时间工具类 |
| | | * |
| | | * @author MrBird |
| | | */ |
| | | public class DataUtil { |
| | | |
| | | //输入两个整数a、b,a/b取模,输出模,如果有余数,输出模+1 |
| | | public static int mod(int a, int b) { |
| | | int mod = a / b; |
| | | if (mod != 0) { |
| | | return mod + 1; |
| | | } |
| | | return mod; |
| | | } |
| | | |
| | | public static void main(String[] args) { |
| | | System.out.println(getDecimalDigits8("3422.66666666666666")); |
| | | } |
| | | |
| | | //输入一个字符串类的小数,输出小数位数 |
| | | public static int getDecimalDigits(String num) { |
| | | if( num.indexOf(".") == -1){ |
| | | return Integer.valueOf(num); |
| | | } else { |
| | | return String.valueOf(num).split("\\.")[1].length(); //split() 方法用于把一个字符串分割成字符串数组。 |
| | | } |
| | | } |
| | | |
| | | //输入一个BigDecimal类的小数,输出小数位数, |
| | | public static int getDecimalDigitsNew(BigDecimal num) { |
| | | //除去小数点后多余的0 |
| | | num = num.stripTrailingZeros(); |
| | | if (num.scale() == 0) { |
| | | return 0; |
| | | } else { |
| | | return num.scale(); //scale() 方法返回小数点后的位数。 |
| | | } |
| | | } |
| | | |
| | | //输入一个包含有小数的字符串,输出原字符串,如果小数位数超过8位,则保留8位小数 |
| | | public static String getDecimalDigits8(String num) { |
| | | if (getDecimalDigits(num) > 8) { |
| | | return String.format("%.8f", Double.valueOf(num)); |
| | | } else { |
| | | return num; |
| | | } |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | import java.util.Random; |
| | | |
| | | /** |
| | | * @author wzy |
| | | * @date 2021-09-22 |
| | | **/ |
| | | public class MallUtils { |
| | | |
| | | public static String getRandomNum(int length) { |
| | | String str = "0123456789"; |
| | | Random random = new Random(); |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (int i = 0; i < length; ++i) { |
| | | int number = random.nextInt(str.length()); |
| | | sb.append(str.charAt(number)); |
| | | } |
| | | |
| | | return sb.toString(); |
| | | } |
| | | |
| | | public static String getOrderNum(String prefix) { |
| | | SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss"); |
| | | String dd=df.format(new Date()); |
| | | if (StrUtil.isNotBlank(prefix)) { |
| | | return prefix+dd+getRandomNum(5); |
| | | } |
| | | return dd+getRandomNum(5); |
| | | } |
| | | |
| | | public static String getOrderNum() { |
| | | return getOrderNum(null); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public abstract class Account { |
| | | // 交易所的基URL |
| | | public String baseUrl; |
| | | // 处理HTTP请求的处理器 |
| | | public RequestHandler requestHandler; |
| | | // 表示是否显示限制使用情况 |
| | | public boolean isSimluate; |
| | | |
| | | public Account(){} |
| | | |
| | | public Account(String baseUrl, String apiKey, String secretKey, String passPhrase,boolean isSimluate) { |
| | | this.baseUrl = baseUrl; |
| | | this.requestHandler = new RequestHandler(apiKey, secretKey,passPhrase); |
| | | this.isSimluate = isSimluate; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto; |
| | | |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * 交易所信息绑定 |
| | | */ |
| | | @Data |
| | | public class QuantApiMessage{ |
| | | private Long memberId;//用户ID |
| | | private String exchange;//交易所名称 |
| | | private String aSecretkey;//A秘钥(access_key) |
| | | private String bSecretkey;//s秘钥(secret_key) |
| | | private String passPhrass;//passphrass |
| | | private String accountType;//账户类型 true:正式 false:测试 |
| | | private int state;//是否成功连通账户 0-失败 1-成功 |
| | | private int isTrade;//是否可交易 1.是 2-否 |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Data |
| | | public class SubmitOrderReqDto { |
| | | /** |
| | | * 是 当前价 |
| | | */ |
| | | private String price; |
| | | /** |
| | | * 是 产品ID,如 BTC-USDT |
| | | */ |
| | | private String instId; |
| | | /** |
| | | * 是 交易模式 |
| | | * 保证金模式:isolated:逐仓 ;cross:全仓 |
| | | * 非保证金模式:cash:非保证金 |
| | | * spot_isolated:现货逐仓(仅适用于现货带单) ,现货带单时,tdMode 的值需要指定为spot_isolated |
| | | */ |
| | | private String tdMode; |
| | | /** |
| | | * 否 保证金币种,仅适用于现货和合约模式下的全仓杠杆订单 |
| | | */ |
| | | private String ccy; |
| | | /** |
| | | * 否 客户自定义订单ID |
| | | * 字母(区分大小写)与数字的组合,可以是纯字母、纯数字且长度要在1-32位之间。 |
| | | */ |
| | | private String clOrdId; |
| | | /** |
| | | * 否 订单标签 |
| | | * 字母(区分大小写)与数字的组合,可以是纯字母、纯数字,且长度在1-16位之间。 |
| | | */ |
| | | private String tag; |
| | | /** |
| | | * 是 订单方向 |
| | | * buy:买, sell:卖 |
| | | */ |
| | | private String side; |
| | | /** |
| | | * 可选 持仓方向 |
| | | * 在开平仓模式下必填,且仅可选择 long 或 short。 仅适用交割、永续。 |
| | | */ |
| | | private String posSide; |
| | | /** |
| | | * 是 订单类型 |
| | | * market:市价单 |
| | | * limit:限价单 |
| | | * post_only:只做maker单 |
| | | * fok:全部成交或立即取消 |
| | | * ioc:立即成交并取消剩余 |
| | | * optimal_limit_ioc:市价委托立即成交并取消剩余(仅适用交割、永续) |
| | | * mmp:做市商保护(仅适用于组合保证金账户模式下的期权订单) |
| | | * mmp_and_post_only:做市商保护且只做maker单(仅适用于组合保证金账户模式下的期权订单) |
| | | */ |
| | | private String ordType; |
| | | /** |
| | | * 是 委托数量 |
| | | */ |
| | | private String sz; |
| | | /** |
| | | * 可选 委托价格,仅适用于limit、post_only、fok、ioc、mmp、mmp_and_post_only类型的订单 |
| | | * 期权下单时,px/pxUsd/pxVol 只能填一个 |
| | | */ |
| | | private String px; |
| | | /** |
| | | * 可选 以USD价格进行期权下单 |
| | | * 仅适用于期权 |
| | | * 期权下单时 px/pxUsd/pxVol 必填一个,且只能填一个 |
| | | */ |
| | | private String pxUsd; |
| | | /** |
| | | * 可选 以隐含波动率进行期权下单,例如 1 代表 100% |
| | | * 仅适用于期权 |
| | | * 期权下单时 px/pxUsd/pxVol 必填一个,且只能填一个 |
| | | */ |
| | | private String pxVol; |
| | | /** |
| | | * 否 是否只减仓,true 或 false,默认false |
| | | * 仅适用于币币杠杆,以及买卖模式下的交割/永续 |
| | | * 仅适用于现货和合约模式和跨币种保证金模式 |
| | | */ |
| | | private Boolean reduceOnly; |
| | | /** |
| | | * 否 市价单委托数量sz的单位,仅适用于币币市价订单 |
| | | * base_ccy: 交易货币 ;quote_ccy:计价货币 |
| | | * 买单默认quote_ccy, 卖单默认base_ccy |
| | | */ |
| | | private String tgtCcy; |
| | | /** |
| | | * 否 是否禁止币币市价改单,true 或 false,默认false |
| | | * 为true时,余额不足时,系统不会改单,下单会失败,仅适用于币币市价单 |
| | | */ |
| | | private Boolean banAmend; |
| | | /** |
| | | * 否 自成交保护模式 |
| | | * 默认为 cancel maker |
| | | * cancel_maker,cancel_taker, cancel_both |
| | | * Cancel both不支持FOK |
| | | */ |
| | | private String stpMode; |
| | | /** |
| | | * 否 下单附带止盈止损信息 |
| | | */ |
| | | private List<Object> attachAlgoOrds; |
| | | /** |
| | | * 否 下单附带止盈止损时,客户自定义的策略订单ID |
| | | * 字母(区分大小写)与数字的组合,可以是纯字母、纯数字且长度要在1-32位之间。 |
| | | * 订单完全成交,下止盈止损委托单时,该值会传给algoClOrdId |
| | | */ |
| | | private String attachAlgoClOrdId; |
| | | /** |
| | | * 可选 止盈触发价 |
| | | * 对于条件止盈单,如果填写此参数,必须填写 止盈委托价 |
| | | */ |
| | | private String tpTriggerPx; |
| | | /** |
| | | * 可选 止盈委托价 |
| | | * 对于条件止盈单,如果填写此参数,必须填写 止盈触发价 |
| | | * 对于限价止盈单,需填写此参数,不需要填写止盈触发价 |
| | | * 委托价格为-1时,执行市价止盈 |
| | | */ |
| | | private String tpOrdPx; |
| | | /** |
| | | * 否 止盈订单类型 |
| | | * condition: 条件单 |
| | | * limit: 限价单 |
| | | * 默认为condition |
| | | */ |
| | | private String tpOrdKind; |
| | | /** |
| | | * 可选 止损触发价,如果填写此参数,必须填写 止损委托价 |
| | | */ |
| | | private String slTriggerPx; |
| | | /** |
| | | * String 可选 止损委托价,如果填写此参数,必须填写 止损触发价 |
| | | * 委托价格为-1时,执行市价止损 |
| | | */ |
| | | private String slOrdPx; |
| | | /** |
| | | * 否 止盈触发价类型 |
| | | * last:最新价格 |
| | | * index:指数价格 |
| | | * mark:标记价格 |
| | | * 默认为last |
| | | */ |
| | | private String tpTriggerPxType; |
| | | /** |
| | | * 否 止损触发价类型 |
| | | * last:最新价格 |
| | | * index:指数价格 |
| | | * mark:标记价格 |
| | | * 默认为last |
| | | */ |
| | | private String slTriggerPxType; |
| | | /** |
| | | * 否 是否启用开仓价止损,仅适用于分批止盈的止损订单,第一笔止盈触发时,止损触发价格是否移动到开仓均价止损 |
| | | * 0:不开启,默认值 |
| | | * 1:开启,且止损触发价不能为空 |
| | | */ |
| | | private String amendPxOnTriggerType; |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * @author wzy |
| | | * @date 2021-09-16 |
| | | **/ |
| | | @Data |
| | | @ApiModel(value = "TradeOrderDto", description = "交易订单参数接收类") |
| | | public class TradeOrderDto { |
| | | private String instrumentId; |
| | | private String side; |
| | | private String type; |
| | | private String size; |
| | | private String price; |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config; |
| | | |
| | | import lombok.Getter; |
| | | |
| | | /** |
| | | * 交易信息枚举类 |
| | | * 用于存储不同交易账户的密钥信息,包括实盘账户和模拟账户 |
| | | */ |
| | | @Getter |
| | | public enum ExchangeInfoEnum { |
| | | |
| | | /** |
| | | * 模拟盘账户信息 |
| | | * 存储了模拟盘交易所需的API密钥、秘钥和通过码 |
| | | */ |
| | | OKX_UAT("7a023eb2-06c0-4255-9969-b86ea1cef0d7", |
| | | "D0106A4D63BD22BEAB9CBA8F41219661", |
| | | "Aa12345678@", |
| | | false); |
| | | |
| | | // /** |
| | | // * 模拟盘账户信息 |
| | | // * 存储了模拟盘交易所需的API密钥、秘钥和通过码 |
| | | // */ |
| | | // OKX_UAT("0769b50c-2c36-4310-8bd9-cad6bc6c9d8f", |
| | | // "7AF4A574BC44907CE76BBFF91F53852D", |
| | | // "Aa123456@", |
| | | // false); |
| | | |
| | | // API公钥,用于识别用户身份 |
| | | private String apiKey; |
| | | |
| | | // API秘钥,用于签名和验证请求 |
| | | private String secretKey; |
| | | |
| | | // API通过码,用于额外的身份验证 |
| | | private String passphrase; |
| | | |
| | | // 账户类型,true表示实盘账户,false表示模拟账户 |
| | | private boolean accountType; |
| | | |
| | | /** |
| | | * 构造方法 |
| | | * |
| | | * @param apiKey API公钥,用于识别用户身份 |
| | | * @param secretKey API秘钥,用于签名和验证请求 |
| | | * @param passphrase API通过码,用于额外的身份验证 |
| | | * @param accountType 账户类型,true表示实盘账户,false表示模拟账户 |
| | | */ |
| | | ExchangeInfoEnum(String apiKey, String secretKey, String passphrase, boolean accountType) { |
| | | this.apiKey = apiKey; |
| | | this.secretKey = secretKey; |
| | | this.passphrase = passphrase; |
| | | this.accountType = accountType; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.SubmitOrderReqDto; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | |
| | | public interface ExchangeLoginEventService { |
| | | /** |
| | | * 获取交易产品基础信息 |
| | | * 获取所有可交易产品的信息列表。 |
| | | * <br><br> |
| | | * GET /api/v5/public/instruments /api/v5/account/instruments |
| | | * <br> |
| | | * |
| | | * @param parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instType -- String 是 产品类型 SPOT:币币 MARGIN:币币杠杆 SWAP:永续合约 FUTURES:交割合约 OPTION:期权 <br> |
| | | * uly -- String 可选 标的指数,仅适用于交割/永续/期权,期权必填 <br> |
| | | * instFamily -- String 否 交易品种,仅适用于交割/永续/期权 <br> |
| | | * instId -- String 否 产品ID <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments</a> |
| | | */ |
| | | String exchangeInfo(LinkedHashMap<String, Object> parameters); |
| | | |
| | | String lineHistory(LinkedHashMap<String, Object> parameters); |
| | | /** |
| | | * 查看账户余额 |
| | | * 获取交易账户中资金余额信息。 |
| | | * <br><br> |
| | | * GET /api/v5/account/balance |
| | | * <br> |
| | | * @param |
| | | * parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * ccy -- String 否 币种,如 BTC 支持多币种查询(不超过20个),币种之间半角逗号分隔 <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-balance"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-account-get-balance</a> |
| | | */ |
| | | String balance(LinkedHashMap<String, Object> parameters); |
| | | /** |
| | | * 查看持仓信息 |
| | | * 获取该账户下拥有实际持仓的信息。账户为单向持仓模式会显示净持仓(net),账户为双向持仓模式下会分别返回多头(long)或空头(short)的仓位。按照仓位创建时间倒序排列。 |
| | | * <br><br> |
| | | * GET /api/v5/account/positions |
| | | * <br> |
| | | * @param |
| | | * parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instType -- String 否 产品类型 |
| | | * MARGIN:币币杠杆 |
| | | * SWAP:永续合约 |
| | | * FUTURES:交割合约 |
| | | * OPTION:期权 |
| | | * instType和instId同时传入的时候会校验instId与instType是否一致。<br> |
| | | * instId -- String 否 交易产品ID,如:BTC-USD-190927-5000-C |
| | | * 支持多个instId查询(不超过10个),半角逗号分隔<br> |
| | | * posId -- String 否 持仓ID |
| | | * 支持多个posId查询(不超过20个),半角逗号分割<br> |
| | | * @return String <br> |
| | | * note: 如果该 instId 拥有过仓位且当前持仓量为0,传 instId 时,会返回仓位信息;不传 instId 时,仓位信息不返回。 |
| | | * 逐仓交易设置中,如果设置为自主划转模式,逐仓转入保证金后,会生成一个持仓量为0的仓位 <br> |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions</a> |
| | | */ |
| | | String positions(LinkedHashMap<String, Object> parameters); |
| | | /** |
| | | * 查看历史持仓信息 |
| | | * 获取最近3个月有更新的仓位信息,按照仓位更新时间倒序排列。 |
| | | * <br><br> |
| | | * GET /api/v5/account/positions-history |
| | | * <br> |
| | | * @param |
| | | * parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instType -- String 否 产品类型 |
| | | * MARGIN:币币杠杆 |
| | | * SWAP:永续合约 |
| | | * FUTURES:交割合约 |
| | | * OPTION:期权 <br> |
| | | * instId -- String 否 交易产品ID,如:BTC-USD-SWAP <br> |
| | | * mgnMode -- String 否 保证金模式 |
| | | * cross:全仓,isolated:逐仓 |
| | | * type -- String 否 平仓类型 |
| | | * 1:部分平仓;2:完全平仓;3:强平;4:强减; 5:ADL自动减仓; |
| | | * 状态叠加时,以最新的平仓类型为准状态为准。 <br> |
| | | * posId -- String 否 持仓ID <br> |
| | | * after -- String 否 查询仓位更新 (uTime) 之前的内容,值为时间戳,Unix 时间戳为毫秒数格式,如 1597026383085 <br> |
| | | * before -- String 否 查询仓位更新 (uTime) 之后的内容,值为时间戳,Unix 时间戳为毫秒数格式,如 1597026383085 <br> |
| | | * limit -- String 否 分页返回结果的数量,最大为100,默认100条 <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions-history"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions-history</a> |
| | | */ |
| | | String positionsHistory(LinkedHashMap<String, Object> parameters); |
| | | /** |
| | | * 撤单 |
| | | * 撤销之前下的未完成订单。 |
| | | * |
| | | * <br><br> |
| | | * GET /api/v5/trade/cancel-order |
| | | * <br> |
| | | * |
| | | * @param originOrderId 用户自定义ID |
| | | * LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instId -- String 是 产品ID,如 BTC-USD-190927 <br> |
| | | * ordId -- String 可选 订单ID, ordId和clOrdId必须传一个,若传两个,以ordId为主 <br> |
| | | * clOrdId -- String 可选 用户自定义ID <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-trade-cancel-order"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-trade-cancel-order</a> |
| | | */ |
| | | boolean cancelOrder(String originOrderId,String instId); |
| | | /** |
| | | * 下单 |
| | | * 只有当您的账户有足够的资金才能下单。 |
| | | * |
| | | * <br><br> |
| | | * GET /api/v5/trade/order |
| | | * <br> |
| | | * |
| | | * @param submitOrderReq |
| | | * LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instId -- String 是 产品ID,如 BTC-USD-190927-5000-C <br> |
| | | * tdMode -- String 是 交易模式 |
| | | * 保证金模式:isolated:逐仓 ;cross:全仓 |
| | | * 非保证金模式:cash:非保证金 <br> |
| | | * ccy -- String 否 保证金币种,仅适用于单币种保证金模式下的全仓杠杆订单 <br> |
| | | * clOrdId -- String 否 客户自定义订单ID |
| | | * 字母(区分大小写)与数字的组合,可以是纯字母、纯数字且长度要在1-32位之间。<br> |
| | | * tag -- String 否 订单标签 |
| | | * 字母(区分大小写)与数字的组合,可以是纯字母、纯数字,且长度在1-16位之间。 <br> |
| | | * side -- String 是 订单方向 |
| | | * buy:买, sell:卖 <br> |
| | | * posSide -- String 可选 持仓方向 |
| | | * 在双向持仓模式下必填,且仅可选择 long 或 short。 仅适用交割、永续。 <br> |
| | | * ordType -- String 是 订单类型 |
| | | * market:市价单 |
| | | * limit:限价单 |
| | | * post_only:只做maker单 |
| | | * fok:全部成交或立即取消 |
| | | * ioc:立即成交并取消剩余 |
| | | * optimal_limit_ioc:市价委托立即成交并取消剩余(仅适用交割、永续) <br> |
| | | * sz -- String 是 委托数量 <br> |
| | | * px -- String 可选 委托价格,仅适用于limit、post_only、fok、ioc类型的订单 <br> |
| | | * reduceOnly -- Boolean 否 是否只减仓,true 或 false,默认false |
| | | * 仅适用于币币杠杆,以及买卖模式下的交割/永续 |
| | | * 仅适用于单币种保证金模式和跨币种保证金模式 <br> |
| | | * tgtCcy -- String 否 市价单委托数量sz的单位,仅适用于币币市价订单 |
| | | * base_ccy: 交易货币 ;quote_ccy:计价货币 |
| | | * 买单默认quote_ccy, 卖单默认base_ccy <br> |
| | | * banAmend -- Boolean 否 是否禁止币币市价改单,true 或 false,默认false |
| | | * 为true时,余额不足时,系统不会改单,下单会失败,仅适用于币币市价单 <br> |
| | | * tpTriggerPx -- String 否 止盈触发价,如果填写此参数,必须填写 止盈委托价 <br> |
| | | * tpOrdPx -- String 否 止盈委托价,如果填写此参数,必须填写 止盈触发价 |
| | | * 委托价格为-1时,执行市价止盈 <br> |
| | | * slTriggerPx -- String 否 止损触发价,如果填写此参数,必须填写 止损委托价 <br> |
| | | * slOrdPx -- String 否 止损委托价,如果填写此参数,必须填写 止损触发价 |
| | | * 委托价格为-1时,执行市价止损 <br> |
| | | * tpTriggerPxType -- String 否 止盈触发价类型 |
| | | * last:最新价格 |
| | | * index:指数价格 |
| | | * mark:标记价格 |
| | | * 默认为last <br> |
| | | * slTriggerPxType -- String 否 止损触发价类型 |
| | | * last:最新价格 |
| | | * index:指数价格 |
| | | * mark:标记价格 |
| | | * 默认为last <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-trade-place-order"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-trade-place-order</a> |
| | | */ |
| | | String submitOrder(SubmitOrderReqDto submitOrderReq); |
| | | |
| | | /** |
| | | * 获取单个币种价格信息 |
| | | * @param parameters |
| | | * @return |
| | | */ |
| | | String tickerMess(LinkedHashMap<String, Object> parameters); |
| | | |
| | | /** |
| | | * 获取单个订单信息 |
| | | * @param parameters |
| | | * @return |
| | | */ |
| | | public String getOrderMessage(LinkedHashMap<String, Object> parameters); |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config; |
| | | |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsException; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.impl.ExchangeLoginEventServiceImpl; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 交易所登录服务 singleton模式 |
| | | * 负责根据交易所类型提供对应的交易所登录事件服务 |
| | | */ |
| | | public class ExchangeLoginService { |
| | | // 存储交易所类型与登录服务实例的映射 |
| | | private final static Map<String, ExchangeLoginEventService> eventMap = new HashMap<>(); |
| | | |
| | | // 静态代码块,用于初始化eventMap |
| | | static { |
| | | for (ExchangeInfoEnum infoEnum : ExchangeInfoEnum.values()) { |
| | | eventMap.put(infoEnum.name(), new ExchangeLoginEventServiceImpl( |
| | | infoEnum.getApiKey(), |
| | | infoEnum.getSecretKey(), |
| | | infoEnum.getPassphrase(), |
| | | infoEnum.isAccountType())); |
| | | } |
| | | } |
| | | |
| | | // 私有构造方法,防止外部实例化 |
| | | private ExchangeLoginService() { |
| | | } |
| | | |
| | | // Singleton实例 |
| | | public final static ExchangeLoginService INSTANCE = new ExchangeLoginService(); |
| | | |
| | | /** |
| | | * 根据交易所类型获取对应的交易所登录事件服务 |
| | | * @param exchangeType 交易所类型 |
| | | * @return 对应的交易所登录事件服务实例 |
| | | * @throws FebsException 如果提供的交易所类型无效,则抛出异常 |
| | | */ |
| | | public static ExchangeLoginEventService getInstance(String exchangeType) { |
| | | ExchangeLoginEventService exchange = eventMap.get(exchangeType); |
| | | if (exchange == null) { |
| | | throw new FebsException("参数错误"); |
| | | } |
| | | |
| | | return exchange; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config; |
| | | |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * OKX交易所账户配置类 |
| | | * 用于存储和管理OKX交易所的账户信息和请求处理 |
| | | */ |
| | | @Data |
| | | public class OKXAccount extends Account{ |
| | | |
| | | // 用于在OKX交易所进行身份验证的密码短语 |
| | | String passPhrase; |
| | | // 表示是否显示限制使用情况 |
| | | public boolean showLimitUsage; |
| | | |
| | | /** |
| | | * OKXAccount的构造方法 |
| | | * |
| | | * @param baseUrl OKX交易所的基URL |
| | | * @param apiKey API的密钥 |
| | | * @param secretKey 私钥 |
| | | * @param passPhrase 用于身份验证的密码短语 |
| | | * @param isSimluate 表示是否为模拟交易 |
| | | */ |
| | | public OKXAccount(String baseUrl, String apiKey, String secretKey, String passPhrase,boolean isSimluate) { |
| | | super(baseUrl, apiKey, secretKey,passPhrase, isSimluate); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsException; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.HttpMethod; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.RequestType; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.DateUtils; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.RequestBuilder; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.SignUtils; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.UrlBuilder; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import okhttp3.Request; |
| | | |
| | | import java.util.Date; |
| | | import java.util.LinkedHashMap; |
| | | |
| | | @Slf4j |
| | | public class RequestHandler { |
| | | private final String apiKey; |
| | | private final String secretKey; |
| | | private final String passphrase; |
| | | |
| | | public RequestHandler(String apiKey) { |
| | | this.apiKey = apiKey; |
| | | this.secretKey = null; |
| | | this.passphrase = null; |
| | | } |
| | | |
| | | public RequestHandler(String apiKey, String secretKey, String passphrase) { |
| | | this.apiKey = apiKey; |
| | | this.secretKey = secretKey; |
| | | this.passphrase = passphrase; |
| | | } |
| | | |
| | | public static void main(String[] args) { |
| | | LinkedHashMap<String, Object> balanceParameters = new LinkedHashMap<>(); |
| | | String queryString = UrlBuilder.joinQueryParameters(new StringBuilder("/api/v5/account/balance"), balanceParameters).toString(); |
| | | String balanceParameters1 = UrlBuilder.buildFullUrl("/api/v5/account/balance","" , balanceParameters, null); |
| | | System.out.println(queryString); |
| | | System.out.println(balanceParameters1); |
| | | } |
| | | |
| | | /** |
| | | * Build request based on request type and send the requests to server. |
| | | * |
| | | * @param baseUrl |
| | | * @param urlPath |
| | | * @param parameters |
| | | * @param httpMethod |
| | | * @param requestType |
| | | * @return String - response from server |
| | | */ |
| | | private String sendApiRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters, |
| | | HttpMethod httpMethod, RequestType requestType, boolean isSimluate) { |
| | | String fullUrl = UrlBuilder.buildFullUrl(baseUrl, urlPath, parameters, null); |
| | | log.debug("{} {}", httpMethod, fullUrl); |
| | | //System.out.println("sendApiRequest:fullUrl"+fullUrl); |
| | | Request request; |
| | | switch (requestType) { |
| | | case PUBLIC: |
| | | request = RequestBuilder.buildPublicRequest(fullUrl, httpMethod, isSimluate).build(); |
| | | break; |
| | | case WITH_API_KEY: |
| | | case SIGNED: |
| | | // 获取签名 |
| | | String timestamp = DateUtils.format(DateUtils.FORMAT_UTC_ISO8601, new Date(), 0); |
| | | String queryString = UrlBuilder.joinQueryParameters(new StringBuilder(urlPath), parameters).toString(); |
| | | // String timestamp = System.currentTimeMillis()+""; |
| | | // System.out.println("timestamp:"+timestamp); |
| | | // System.out.println("timestamp:"+timestamp); |
| | | // System.out.println("secretKey:"+secretKey); |
| | | // System.out.println("httpMethod.toString():"+httpMethod.toString()); |
| | | // System.out.println("queryString:"+queryString); |
| | | // System.out.println("passphrase:"+passphrase); |
| | | // 组装body |
| | | String body = ""; |
| | | if (HttpMethod.POST.equals(httpMethod)) { |
| | | body = JSON.toJSONString(parameters); |
| | | queryString = UrlBuilder.joinQueryParameters(new StringBuilder(urlPath), null).toString(); |
| | | fullUrl = UrlBuilder.buildFullUrl(baseUrl, urlPath, null, null); |
| | | } |
| | | if (HttpMethod.GET.equals(httpMethod)) { |
| | | queryString = UrlBuilder.buildFullUrl(urlPath,"" , parameters, null); |
| | | // queryString = UrlBuilder.buildFullUrl(null, urlPath, parameters, null); |
| | | } |
| | | String sign = SignUtils.signRest(secretKey, |
| | | timestamp, |
| | | httpMethod.toString(), |
| | | queryString, body); |
| | | |
| | | |
| | | request = RequestBuilder.buildApiKeyRequest(fullUrl, body, passphrase, sign, timestamp, httpMethod, apiKey,isSimluate); |
| | | |
| | | |
| | | break; |
| | | default: |
| | | throw new FebsException("[RequestHandler] Invalid request type: " + requestType); |
| | | } |
| | | return ResponseHandler.handleResponse(request, isSimluate); |
| | | } |
| | | |
| | | public String sendPublicRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters, |
| | | HttpMethod httpMethod, boolean isSimluate) { |
| | | return sendApiRequest(baseUrl, urlPath, parameters, httpMethod, RequestType.PUBLIC, isSimluate); |
| | | } |
| | | |
| | | public String sendWithApiKeyRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters, |
| | | HttpMethod httpMethod, boolean isSimluate) { |
| | | if (null == apiKey || apiKey.isEmpty()) { |
| | | throw new FebsException("[RequestHandler] API key cannot be null or empty!"); |
| | | } |
| | | return sendApiRequest(baseUrl, urlPath, parameters, httpMethod, RequestType.WITH_API_KEY, isSimluate); |
| | | } |
| | | |
| | | public String sendSignedRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters, |
| | | HttpMethod httpMethod, boolean isSimluate) { |
| | | if (null == secretKey || secretKey.isEmpty() || null == apiKey || apiKey.isEmpty()) { |
| | | throw new FebsException("[RequestHandler] Secret key/API key cannot be null or empty!"); |
| | | } |
| | | //parameters.put("timestamp", UrlBuilder.buildTimestamp()); |
| | | //String queryString = UrlBuilder.joinQueryParameters(parameters); |
| | | //String signature = SignatureGenerator.getSignature(queryString, secretKey); |
| | | return sendApiRequest(baseUrl, urlPath, parameters, httpMethod, RequestType.SIGNED, isSimluate); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config; |
| | | |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsException; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.JSONParser; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.OkHttpUtils; |
| | | import okhttp3.Request; |
| | | import okhttp3.Response; |
| | | import okhttp3.ResponseBody; |
| | | import org.json.JSONException; |
| | | |
| | | import java.io.IOException; |
| | | |
| | | public final class ResponseHandler { |
| | | |
| | | private static final int HTTP_STATUS_CODE_400 = 400; |
| | | private static final int HTTP_STATUS_CODE_499 = 499; |
| | | private static final int HTTP_STATUS_CODE_500 = 500; |
| | | |
| | | private ResponseHandler() { |
| | | } |
| | | |
| | | public static String handleResponse(Request request, boolean showLimitUsage) { |
| | | try (Response response = OkHttpUtils.okHttpClient.newCall(request).execute()) {//OkHttpUtils.builder().okHttpClient |
| | | String responseAsString = getResponseBodyAsString(response.body()); |
| | | |
| | | if (response.code() >= HTTP_STATUS_CODE_400 && response.code() <= HTTP_STATUS_CODE_499) { |
| | | throw handleErrorResponse(responseAsString, response.code()); |
| | | } else if (response.code() >= HTTP_STATUS_CODE_500) { |
| | | System.out.println("handleResponse:"+response.code()); |
| | | throw new FebsException("responseAsString-"+responseAsString+";handleResponse-"+response.code()); |
| | | } |
| | | return responseAsString; |
| | | // if (showLimitUsage) { |
| | | // return getlimitUsage(response, responseAsString); |
| | | // } else { |
| | | // return responseAsString; |
| | | // } |
| | | } catch (IOException | IllegalStateException e) { |
| | | e.printStackTrace(); |
| | | throw new FebsException("[ResponseHandler] OKHTTP Error: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | |
| | | private static FebsException handleErrorResponse(String responseBody, int responseCode) { |
| | | try { |
| | | String errorMsg = JSONParser.getJSONStringValue(responseBody, "msg"); |
| | | int errorCode = JSONParser.getJSONIntValue(responseBody, "code"); |
| | | return new FebsException("responseBody-"+responseBody+";errorMsg-"+errorMsg+";responseCode-"+responseCode+";errorCode-"+errorCode); |
| | | } catch (JSONException e) { |
| | | throw new FebsException("responseBody-"+responseBody+";responseCode-"+responseCode); |
| | | } |
| | | } |
| | | |
| | | private static String getResponseBodyAsString(ResponseBody body) throws IOException { |
| | | if (null != body) { |
| | | return body.string(); |
| | | } else { |
| | | return ""; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums; |
| | | |
| | | public final class DefaultUrls { |
| | | public static final String USDM_UAT_URL = "https://www.okx.com"; |
| | | public static final String USDM_UAT_WSS_URL = "wss://wspap.okx.com:8443"; |
| | | //public static final String USDM_UAT_WSS_URL = "wss://ws.okx.com:8443"; |
| | | //USD-M Futures |
| | | public static final String USDM_PROD_URL = "https://aws.okx.com"; |
| | | public static final String USDM_PROD_WS_URL = "wss://ws.okx.com:8443"; |
| | | //比特币买入数量 |
| | | public static final String BTC_BUYNUMBER = "0.001"; |
| | | //以太坊买入数量 |
| | | public static final String ETH_BUYNUMBER = "0.01"; |
| | | //狗狗币买入数量 |
| | | public static final String DOGE_BUYNUMBER = "100"; |
| | | /** |
| | | * 全部卖出 |
| | | */ |
| | | public static final String OPERATION_ALLSOLD = "allsold"; |
| | | |
| | | /** |
| | | * 卖出 |
| | | */ |
| | | public static final String OPERATION_SOLD = "sell"; |
| | | |
| | | /** |
| | | * 买入 |
| | | */ |
| | | public static final String OPERATION_BUY = "buy"; |
| | | |
| | | /** |
| | | * 不买入 |
| | | */ |
| | | public static final String OPERATION_NOBUY = "nobuy"; |
| | | |
| | | private DefaultUrls() { |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums; |
| | | |
| | | public enum HttpMethod { |
| | | POST, |
| | | GET, |
| | | PUT, |
| | | DELETE, |
| | | INVALID |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums; |
| | | |
| | | public enum RequestType { |
| | | PUBLIC, |
| | | WITH_API_KEY, |
| | | SIGNED |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.impl; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.SubmitOrderReqDto; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.ExchangeInfoEnum; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.ExchangeLoginEventService; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.ExchangeLoginService; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.DefaultUrls; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.HttpMethod; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.OKXContants; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.*; |
| | | |
| | | @Slf4j |
| | | public class ExchangeLoginEventServiceImpl implements ExchangeLoginEventService { |
| | | |
| | | |
| | | private OKXAccount OKXAccount; |
| | | private String apiKey; |
| | | private String secretKey; |
| | | private String passphrase; |
| | | private boolean accountType; |
| | | |
| | | public ExchangeLoginEventServiceImpl(String apiKey, String secretKey, String passphrase, boolean accountType) { |
| | | this.apiKey = apiKey; |
| | | this.secretKey = secretKey; |
| | | this.passphrase = passphrase; |
| | | this.accountType = accountType; |
| | | OKXAccount = new OKXAccount( |
| | | accountType ? DefaultUrls.USDM_PROD_URL : DefaultUrls.USDM_UAT_URL, |
| | | apiKey, |
| | | secretKey, |
| | | passphrase, |
| | | !accountType); |
| | | } |
| | | |
| | | @Override |
| | | public String exchangeInfo(LinkedHashMap<String, Object> parameters) { |
| | | return OKXAccount.requestHandler.sendPublicRequest(OKXAccount.baseUrl, OKXContants.INSTRUMENTS,parameters, HttpMethod.GET, OKXAccount.isSimluate()); |
| | | } |
| | | |
| | | @Override |
| | | public String lineHistory(LinkedHashMap<String, Object> parameters) { |
| | | return OKXAccount.requestHandler.sendPublicRequest(OKXAccount.baseUrl, OKXContants.K_LINE_HISTORY,parameters, HttpMethod.GET, OKXAccount.isSimluate()); |
| | | } |
| | | |
| | | @Override |
| | | public String balance(LinkedHashMap<String, Object> parameters) { |
| | | return OKXAccount.requestHandler.sendSignedRequest(OKXAccount.baseUrl, OKXContants.BALANCE, parameters, HttpMethod.GET, OKXAccount.isSimluate()); |
| | | } |
| | | |
| | | @Override |
| | | public String positions(LinkedHashMap<String, Object> parameters) { |
| | | return OKXAccount.requestHandler.sendSignedRequest(OKXAccount.baseUrl, OKXContants.POSITIONS, parameters, HttpMethod.GET, OKXAccount.isSimluate()); |
| | | } |
| | | |
| | | @Override |
| | | public String positionsHistory(LinkedHashMap<String, Object> parameters) { |
| | | return OKXAccount.requestHandler.sendSignedRequest(OKXAccount.baseUrl, OKXContants.POSITIONS_HISTORY, parameters, HttpMethod.GET, OKXAccount.isSimluate()); |
| | | } |
| | | |
| | | @Override |
| | | public boolean cancelOrder(String originOrderId,String instId) { |
| | | LinkedHashMap<String, Object> parameters = new LinkedHashMap<>(); |
| | | parameters.put("instId", instId); |
| | | parameters.put("clOrdId", originOrderId); |
| | | |
| | | String s = OKXAccount.requestHandler.sendSignedRequest(OKXAccount.baseUrl, OKXContants.CANCEL_ORDER, parameters, HttpMethod.POST, OKXAccount.isSimluate()); |
| | | log.info("[{}] 收到撤单请求,返回", s); |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public String submitOrder(SubmitOrderReqDto submitOrderReq) { |
| | | log.info("收到下单请求,参数:[{}]", submitOrderReq); |
| | | LinkedHashMap<String, Object> parameters = new LinkedHashMap<>(); |
| | | |
| | | String side = submitOrderReq.getSide(); |
| | | String type = submitOrderReq.getOrdType(); |
| | | String positionSides = submitOrderReq.getPosSide(); |
| | | // 开仓 |
| | | //开多:买多BUY、LONG |
| | | //开空:卖空SELL、SHORT |
| | | if ("buy".equals(side)) { |
| | | if ("limit".equals(type)) { |
| | | parameters.put("px", submitOrderReq.getPx()); |
| | | parameters.put("ordType", "limit"); |
| | | } |
| | | // 持仓方向 |
| | | parameters.put("posSide", positionSides); |
| | | //placeOrderReq.setPosSide(positionSide); |
| | | // slTriggerPx 止损触发价,如果填写此参数,必须填写 止损委托价 |
| | | // slOrdPx 止损委托价,如果填写此参数,必须填写 止损触发价 |
| | | |
| | | if (new BigDecimal(submitOrderReq.getSlTriggerPx()).compareTo(BigDecimal.ZERO) > 0) { |
| | | Map<String, Object> attachAlgoOrder = new HashMap<>(); |
| | | // 如果是开多 止损价小于传来的价格 |
| | | |
| | | List<Map<String, Object>> attachAlgoOrds = new ArrayList<>(); |
| | | // 如果是开空 止损价小于传来的价格 |
| | | attachAlgoOrder.put("slTriggerPx", submitOrderReq.getSlTriggerPx()); |
| | | attachAlgoOrder.put("slOrdPx", submitOrderReq.getSlOrdPx()); |
| | | attachAlgoOrds.add(attachAlgoOrder); |
| | | parameters.put("attachAlgoOrds", attachAlgoOrds); |
| | | } else { |
| | | BigDecimal price = new BigDecimal(submitOrderReq.getPrice()); |
| | | BigDecimal stopPrice = BigDecimal.ZERO; |
| | | if ("buy".equalsIgnoreCase(side)) { |
| | | stopPrice = price.multiply(new BigDecimal("0.99")).setScale(2,BigDecimal.ROUND_DOWN); |
| | | } else { |
| | | stopPrice = price.multiply(new BigDecimal("1.01")).setScale(2,BigDecimal.ROUND_DOWN); |
| | | } |
| | | Map<String, Object> attachAlgoOrder = new HashMap<>(); |
| | | // 如果是开多 止损价小于传来的价格 |
| | | List<Map<String, Object>> attachAlgoOrds = new ArrayList<>(); |
| | | // 如果是开空 止损价小于传来的价格 |
| | | attachAlgoOrder.put("slTriggerPx", stopPrice); |
| | | attachAlgoOrder.put("slOrdPx", stopPrice); |
| | | attachAlgoOrds.add(attachAlgoOrder); |
| | | parameters.put("attachAlgoOrds", attachAlgoOrds); |
| | | } |
| | | } else { |
| | | // 平仓 |
| | | //平空:卖空BUY、SHORT |
| | | //平多:卖多SELL、LONG |
| | | //side = (CoreEnum.DirectionEnum.D_Buy.getNumber() == direction.getNumber()) ? "BUY" : "SELL"; |
| | | // 平仓方向 |
| | | //positionSide = (CoreEnum.DirectionEnum.D_Buy.getNumber() == direction.getNumber()) ? "SHORT" : "LONG"; |
| | | if ("limit".equals(type)) { |
| | | //placeOrderReq.setPx(new BigDecimal(submitOrderReq.price())); |
| | | parameters.put("px", submitOrderReq.getPrice()); |
| | | //parameters.put("timeInForce", timeInForce); |
| | | } |
| | | //placeOrderReq.setPosSide(positionSide); |
| | | parameters.put("posSide", positionSides); |
| | | } |
| | | |
| | | //订单种类,市价单不传价格 |
| | | parameters.put("instId", submitOrderReq.getInstId()); |
| | | parameters.put("side", side); |
| | | //placeOrderReq.setSide(side); |
| | | parameters.put("ordType", type); |
| | | //placeOrderReq.setSz(new BigDecimal(quantity)); |
| | | parameters.put("sz", submitOrderReq.getSz()); |
| | | //placeOrderReq.setClOrdId(submitOrderReq.originOrderId()); |
| | | parameters.put("clOrdId", submitOrderReq.getClOrdId()); |
| | | parameters.put("tdMode", submitOrderReq.getTdMode()); |
| | | log.info("下单参数:[{}]",JSON.toJSONString(parameters)); |
| | | String placeOrderRspOkxRestResponse = OKXAccount.requestHandler.sendSignedRequest(OKXAccount.baseUrl, OKXContants.ORDER, parameters, HttpMethod.POST, OKXAccount.isSimluate()); |
| | | log.info("收到下单返回,响应:[{}]", JSON.parseObject(placeOrderRspOkxRestResponse).get("data")); |
| | | return submitOrderReq.getClOrdId(); |
| | | } |
| | | |
| | | @Override |
| | | public String tickerMess(LinkedHashMap<String, Object> parameters) { |
| | | return OKXAccount.requestHandler.sendSignedRequest(OKXAccount.baseUrl, OKXContants.TICKER, parameters, HttpMethod.GET, OKXAccount.isSimluate()); |
| | | } |
| | | |
| | | @Override |
| | | public String getOrderMessage(LinkedHashMap<String, Object> parameters) { |
| | | return OKXAccount.requestHandler.sendSignedRequest(OKXAccount.baseUrl, OKXContants.TICKER, parameters, HttpMethod.GET, OKXAccount.isSimluate()); |
| | | } |
| | | |
| | | public static void main(String[] args) { |
| | | LinkedHashMap<String, Object> balanceParameters = new LinkedHashMap<>(); |
| | | String balance = ExchangeLoginService.getInstance(ExchangeInfoEnum.OKX_UAT.name()).balance(balanceParameters); |
| | | JSONObject balanceJson = JSON.parseObject(balance); |
| | | JSONObject data = balanceJson.getJSONArray("data").getJSONObject(0); |
| | | JSONArray details = data.getJSONArray("details"); |
| | | System.out.println(balanceJson); |
| | | System.out.println(data); |
| | | System.out.println(details); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils; |
| | | |
| | | import java.text.ParseException; |
| | | import java.text.SimpleDateFormat; |
| | | import java.time.ZoneId; |
| | | import java.util.Date; |
| | | import java.util.TimeZone; |
| | | |
| | | public class DateUtils { |
| | | |
| | | public static final String FORMAT_Y = "yyyy"; |
| | | public static final String FORMAT_D_1 = "yyyy/MM/dd"; |
| | | public static final String FORMAT_D_2 = "yyyy-MM-dd"; |
| | | public static final String FORMAT_D_3 = "yyyyMMdd"; |
| | | public static final String FORMAT_D_4 = "yyyy.MM.dd"; |
| | | public static final String FORMAT_D = "dd"; |
| | | public static final String FORMAT_DT_1 = "yyyy/MM/dd HH:mm:ss"; |
| | | public static final String FORMAT_DT_2 = "yyyy-MM-dd HH:mm:ss"; |
| | | public static final String FORMAT_DT_3 = "yyyyMMdd HH:mm:ss"; |
| | | public static final String FORMAT_DT_4 = "yyyy-MM-dd HH:mm"; |
| | | public static final String FORMAT_DT_5 = "yyyy.MM.dd HH:mm:ss"; |
| | | public static final String FORMAT_DT_6 = "yyyyMMddHHmmss"; |
| | | public static final String FORMAT_DT_7 = "yyyyMMddHH"; |
| | | public static final String FORMAT_M_1 = "yyyy/MM"; |
| | | public static final String FORMAT_M_2 = "yyyy-MM"; |
| | | public static final String FORMAT_M_3 = "yyyyMM"; |
| | | public static final String FORMAT_M = "MM"; |
| | | public static final String FORMAT_MD_1 = "MM/dd"; |
| | | public static final String FORMAT_MD_2 = "MM-dd"; |
| | | public static final String FORMAT_MD_3 = "MMdd"; |
| | | public static final String FORMAT_T_1 = "HH:mm:ss"; |
| | | public static final String FORMAT_T_2 = "HH:mm"; |
| | | public static final String FORMAT_TH = "HH"; |
| | | public static final String FORMAT_TM = "mm"; |
| | | public static final String FORMAT_TS = "ss"; |
| | | public static final String FORMAT_UTC_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'"; |
| | | |
| | | public static String format(String format, Date date) { |
| | | SimpleDateFormat sdf = new SimpleDateFormat(format); |
| | | return sdf.format(date); |
| | | } |
| | | |
| | | /** |
| | | * @param format format |
| | | * @param date date |
| | | * @param timeZone 时区数字 -8, 0, 8 等 |
| | | * @return date string |
| | | */ |
| | | public static String format(String format, Date date, int timeZone) { |
| | | timeZone = timeZone % 13; |
| | | SimpleDateFormat sdf = new SimpleDateFormat(format); |
| | | ZoneId zoneId = ZoneId.of("GMT" + (timeZone >= 0 ? "+" : "") + timeZone); |
| | | TimeZone tz = TimeZone.getTimeZone(zoneId); |
| | | sdf.setTimeZone(tz); |
| | | return sdf.format(date); |
| | | } |
| | | |
| | | public static Date parse(String dateString, String format) { |
| | | SimpleDateFormat sdf = new SimpleDateFormat(format); |
| | | |
| | | try { |
| | | return sdf.parse(dateString); |
| | | } catch (ParseException var4) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | public static Date parse(String dateString, String format, int timeZone) { |
| | | SimpleDateFormat sdf = new SimpleDateFormat(format); |
| | | ZoneId zoneId = ZoneId.of("GMT" + (timeZone >= 0 ? "+" : "") + timeZone); |
| | | TimeZone tz = TimeZone.getTimeZone(zoneId); |
| | | sdf.setTimeZone(tz); |
| | | try { |
| | | return sdf.parse(dateString); |
| | | } catch (ParseException var4) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils; |
| | | |
| | | import org.json.JSONArray; |
| | | import org.json.JSONException; |
| | | import org.json.JSONObject; |
| | | |
| | | import java.util.ArrayList; |
| | | |
| | | public final class JSONParser { |
| | | |
| | | private JSONParser() { |
| | | } |
| | | |
| | | public static String getJSONStringValue(String json, String key) { |
| | | try { |
| | | JSONObject obj = new JSONObject(json); |
| | | return obj.getString(key); |
| | | } catch (JSONException e) { |
| | | throw new JSONException(String.format("[JSONParser] Failed to get \"%s\" from JSON object", key)); |
| | | } |
| | | } |
| | | |
| | | public static int getJSONIntValue(String json, String key) { |
| | | try { |
| | | JSONObject obj = new JSONObject(json); |
| | | return obj.getInt(key); |
| | | } catch (JSONException e) { |
| | | throw new JSONException(String.format("[JSONParser] Failed to get \"%s\" from JSON object", key)); |
| | | } |
| | | } |
| | | |
| | | public static String getJSONArray(ArrayList<?> symbols, String key) { |
| | | try { |
| | | JSONArray arr = new JSONArray(symbols); |
| | | return arr.toString(); |
| | | } catch (JSONException e) { |
| | | throw new JSONException(String.format("[JSONParser] Failed to convert \"%s\" to JSON array", key)); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils; |
| | | |
| | | public class OKXContants { |
| | | /** |
| | | * 获取交易产品基础信息 |
| | | * 获取所有可交易产品的信息列表。 |
| | | * <br><br> |
| | | * GET /api/v5/public/instruments |
| | | * <br> |
| | | * |
| | | * @param parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instType -- String 是 产品类型 SPOT:币币 MARGIN:币币杠杆 SWAP:永续合约 FUTURES:交割合约 OPTION:期权 <br> |
| | | * uly -- String 可选 标的指数,仅适用于交割/永续/期权,期权必填 <br> |
| | | * instFamily -- String 否 交易品种,仅适用于交割/永续/期权 <br> |
| | | * instId -- String 否 产品ID <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments</a> |
| | | */ |
| | | public static final String INSTRUMENTS = "/api/v5/public/instruments"; |
| | | public static final String K_LINE_HISTORY_MARK_PRICE = "/api/v5/market/history-mark-price-candles"; |
| | | public static final String K_LINE_HISTORY = "/api/v5/market/history-candles"; |
| | | /** |
| | | * 查看账户余额 |
| | | * 获取交易账户中资金余额信息。 |
| | | * <br><br> |
| | | * GET /api/v5/account/balance |
| | | * <br> |
| | | * @param |
| | | * parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * ccy -- String 否 币种,如 BTC 支持多币种查询(不超过20个),币种之间半角逗号分隔 <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-balance"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-account-get-balance</a> |
| | | */ |
| | | public static final String BALANCE = "/api/v5/account/balance"; |
| | | /** |
| | | * /api/v5/asset/balances |
| | | * 获取资金账户余额 |
| | | * 获取资金账户所有资产列表,查询各币种的余额、冻结和可用等信息。 |
| | | * 币种,如 BTC |
| | | * 支持多币种查询(不超过20个),币种之间半角逗号分隔 |
| | | */ |
| | | public static final String ACCOUNT_BALANCE = "/api/v5/asset/balances"; |
| | | /** |
| | | * 查看持仓信息 |
| | | * 获取该账户下拥有实际持仓的信息。账户为单向持仓模式会显示净持仓(net),账户为双向持仓模式下会分别返回多头(long)或空头(short)的仓位。按照仓位创建时间倒序排列。 |
| | | * <br><br> |
| | | * GET /api/v5/account/positions |
| | | * <br> |
| | | * @param |
| | | * parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instType -- String 否 产品类型 |
| | | * MARGIN:币币杠杆 |
| | | * SWAP:永续合约 |
| | | * FUTURES:交割合约 |
| | | * OPTION:期权 |
| | | * instType和instId同时传入的时候会校验instId与instType是否一致。<br> |
| | | * instId -- String 否 交易产品ID,如:BTC-USD-190927-5000-C |
| | | * 支持多个instId查询(不超过10个),半角逗号分隔<br> |
| | | * posId -- String 否 持仓ID |
| | | * 支持多个posId查询(不超过20个),半角逗号分割<br> |
| | | * @return String <br> |
| | | * note: 如果该 instId 拥有过仓位且当前持仓量为0,传 instId 时,会返回仓位信息;不传 instId 时,仓位信息不返回。 |
| | | * 逐仓交易设置中,如果设置为自主划转模式,逐仓转入保证金后,会生成一个持仓量为0的仓位 <br> |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions</a> |
| | | */ |
| | | public static final String POSITIONS = "/api/v5/account/positions"; |
| | | /** |
| | | * 查看历史持仓信息 |
| | | * 获取最近3个月有更新的仓位信息,按照仓位更新时间倒序排列。 |
| | | * <br><br> |
| | | * GET /api/v5/account/positions-history |
| | | * <br> |
| | | * @param |
| | | * parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instType -- String 否 产品类型 |
| | | * MARGIN:币币杠杆 |
| | | * SWAP:永续合约 |
| | | * FUTURES:交割合约 |
| | | * OPTION:期权 <br> |
| | | * instId -- String 否 交易产品ID,如:BTC-USD-SWAP <br> |
| | | * mgnMode -- String 否 保证金模式 |
| | | * cross:全仓,isolated:逐仓 |
| | | * type -- String 否 平仓类型 |
| | | * 1:部分平仓;2:完全平仓;3:强平;4:强减; 5:ADL自动减仓; |
| | | * 状态叠加时,以最新的平仓类型为准状态为准。 <br> |
| | | * posId -- String 否 持仓ID <br> |
| | | * after -- String 否 查询仓位更新 (uTime) 之前的内容,值为时间戳,Unix 时间戳为毫秒数格式,如 1597026383085 <br> |
| | | * before -- String 否 查询仓位更新 (uTime) 之后的内容,值为时间戳,Unix 时间戳为毫秒数格式,如 1597026383085 <br> |
| | | * limit -- String 否 分页返回结果的数量,最大为100,默认100条 <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions-history"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions-history</a> |
| | | */ |
| | | public static final String POSITIONS_HISTORY = "/api/v5/account/positions-history"; |
| | | /** |
| | | * 撤单 |
| | | * 撤销之前下的未完成订单。 |
| | | * |
| | | * <br><br> |
| | | * GET /api/v5/trade/cancel-order |
| | | * <br> |
| | | * |
| | | * @param parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instId -- String 是 产品ID,如 BTC-USD-190927 <br> |
| | | * ordId -- String 可选 订单ID, ordId和clOrdId必须传一个,若传两个,以ordId为主 <br> |
| | | * clOrdId -- String 可选 用户自定义ID <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-trade-cancel-order"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-trade-cancel-order</a> |
| | | */ |
| | | public static final String CANCEL_ORDER = "/api/v5/trade/cancel-order"; |
| | | /** |
| | | * 下单 |
| | | * 只有当您的账户有足够的资金才能下单。 |
| | | * |
| | | * <br><br> |
| | | * GET /api/v5/trade/order |
| | | * <br> |
| | | * |
| | | * @param parameters LinkedHashedMap of String,Object pair |
| | | * where String is the name of the parameter and Object is the value of the parameter |
| | | * <br><br> |
| | | * instId -- String 是 产品ID,如 BTC-USD-190927-5000-C <br> |
| | | * tdMode -- String 是 交易模式 |
| | | * 保证金模式:isolated:逐仓 ;cross:全仓 |
| | | * 非保证金模式:cash:非保证金 <br> |
| | | * ccy -- String 否 保证金币种,仅适用于单币种保证金模式下的全仓杠杆订单 <br> |
| | | * clOrdId -- String 否 客户自定义订单ID |
| | | * 字母(区分大小写)与数字的组合,可以是纯字母、纯数字且长度要在1-32位之间。<br> |
| | | * tag -- String 否 订单标签 |
| | | * 字母(区分大小写)与数字的组合,可以是纯字母、纯数字,且长度在1-16位之间。 <br> |
| | | * side -- String 是 订单方向 |
| | | * buy:买, sell:卖 <br> |
| | | * posSide -- String 可选 持仓方向 |
| | | * 在双向持仓模式下必填,且仅可选择 long 或 short。 仅适用交割、永续。 <br> |
| | | * ordType -- String 是 订单类型 |
| | | * market:市价单 |
| | | * limit:限价单 |
| | | * post_only:只做maker单 |
| | | * fok:全部成交或立即取消 |
| | | * ioc:立即成交并取消剩余 |
| | | * optimal_limit_ioc:市价委托立即成交并取消剩余(仅适用交割、永续) <br> |
| | | * sz -- String 是 委托数量 <br> |
| | | * px -- String 可选 委托价格,仅适用于limit、post_only、fok、ioc类型的订单 <br> |
| | | * reduceOnly -- Boolean 否 是否只减仓,true 或 false,默认false |
| | | * 仅适用于币币杠杆,以及买卖模式下的交割/永续 |
| | | * 仅适用于单币种保证金模式和跨币种保证金模式 <br> |
| | | * tgtCcy -- String 否 市价单委托数量sz的单位,仅适用于币币市价订单 |
| | | * base_ccy: 交易货币 ;quote_ccy:计价货币 |
| | | * 买单默认quote_ccy, 卖单默认base_ccy <br> |
| | | * banAmend -- Boolean 否 是否禁止币币市价改单,true 或 false,默认false |
| | | * 为true时,余额不足时,系统不会改单,下单会失败,仅适用于币币市价单 <br> |
| | | * tpTriggerPx -- String 否 止盈触发价,如果填写此参数,必须填写 止盈委托价 <br> |
| | | * tpOrdPx -- String 否 止盈委托价,如果填写此参数,必须填写 止盈触发价 |
| | | * 委托价格为-1时,执行市价止盈 <br> |
| | | * slTriggerPx -- String 否 止损触发价,如果填写此参数,必须填写 止损委托价 <br> |
| | | * slOrdPx -- String 否 止损委托价,如果填写此参数,必须填写 止损触发价 |
| | | * 委托价格为-1时,执行市价止损 <br> |
| | | * tpTriggerPxType -- String 否 止盈触发价类型 |
| | | * last:最新价格 |
| | | * index:指数价格 |
| | | * mark:标记价格 |
| | | * 默认为last <br> |
| | | * slTriggerPxType -- String 否 止损触发价类型 |
| | | * last:最新价格 |
| | | * index:指数价格 |
| | | * mark:标记价格 |
| | | * 默认为last <br> |
| | | * @return String |
| | | * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-trade-place-order"> |
| | | * https://www.okx.com/docs-v5/zh/#rest-api-trade-place-order</a> |
| | | */ |
| | | public static final String ORDER = "/api/v5/trade/order"; |
| | | |
| | | /** |
| | | * 获取币种价格信息 |
| | | */ |
| | | public static final String TICKER = "/api/v5/market/ticker"; |
| | | |
| | | /** |
| | | * 获取杠杆倍数 |
| | | */ |
| | | public static final String LEVERAGE = "/api/v5/account/leverage-info"; |
| | | |
| | | /** |
| | | * 设置杠杆倍数 |
| | | */ |
| | | public static final String SETLEVERAGE = "/api/v5/account/set-leverage"; |
| | | |
| | | |
| | | /** |
| | | * 获取支持大数据的币种列表 |
| | | */ |
| | | public static final String TRADEDATA = "/api/v5/rubik/stat/trading-data/support-coin"; |
| | | |
| | | /** |
| | | * 获取合约主动买入/卖出情况 |
| | | */ |
| | | public static final String BUYSELLSITUATION = "/api/v5/rubik/stat/taker-volume-contract"; |
| | | |
| | | /** |
| | | * 获取合约多空持仓人数比 |
| | | */ |
| | | public static final String POSITIONRATIO = "/api/v5/rubik/stat/contracts/long-short-account-ratio-contract"; |
| | | |
| | | /** |
| | | * 获取合约持仓量及交易量 |
| | | */ |
| | | public static final String POSITIONVOLUME = "/api/v5/rubik/stat/contracts/open-interest-volume"; |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils; |
| | | |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import okhttp3.*; |
| | | import org.json.JSONObject; |
| | | |
| | | import javax.net.ssl.SSLContext; |
| | | import javax.net.ssl.SSLSocketFactory; |
| | | import javax.net.ssl.TrustManager; |
| | | import javax.net.ssl.X509TrustManager; |
| | | import java.io.IOException; |
| | | import java.net.URLEncoder; |
| | | import java.security.SecureRandom; |
| | | import java.security.cert.X509Certificate; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import java.util.concurrent.Semaphore; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | | * OkHttp请求工具封装 |
| | | * 参考:https://blog.csdn.net/DwZ735660836/article/details/119976068 |
| | | */ |
| | | @Slf4j |
| | | public class OkHttpUtils { |
| | | public static volatile OkHttpClient okHttpClient = null; |
| | | private static volatile Semaphore semaphore = null; |
| | | private Map<String, String> headerMap; |
| | | private Map<String, String> paramMap; |
| | | private String url; |
| | | private Request.Builder request; |
| | | // 开发环境用的 ShadowsocksR-dotnet4.0 免费版本 正式环境得使用外网服务器 |
| | | // 安易代理 http://127.0.0.1:10809/ http://127.0.0.1:10808/ |
| | | |
| | | /** |
| | | * 初始化okHttpClient,并且允许https访问 |
| | | */ |
| | | private OkHttpUtils() { |
| | | if (okHttpClient == null) { |
| | | synchronized (OkHttpUtils.class) { |
| | | if (okHttpClient == null) { |
| | | TrustManager[] trustManagers = buildTrustManagers(); |
| | | okHttpClient = new OkHttpClient.Builder() |
| | | .connectTimeout(30, TimeUnit.SECONDS) |
| | | .writeTimeout(20, TimeUnit.SECONDS) |
| | | .readTimeout(60, TimeUnit.SECONDS) |
| | | .sslSocketFactory(createSSLSocketFactory(trustManagers), (X509TrustManager) trustManagers[0]) |
| | | //.hostnameVerifier((hostName, session) -> true) |
| | | //配置自定义连接池参数 |
| | | .connectionPool(new ConnectionPool(5, 60, TimeUnit.SECONDS)) |
| | | .retryOnConnectionFailure(true) |
| | | .build(); |
| | | addHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"); |
| | | addHeader("Connection", "close"); |
| | | addHeader("Accept-Encoding", "identity"); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 用于异步请求时,控制访问线程数,返回结果 |
| | | * |
| | | * @return |
| | | */ |
| | | private static Semaphore getSemaphoreInstance() { |
| | | //只能1个线程同时访问 |
| | | synchronized (OkHttpUtils.class) { |
| | | if (semaphore == null) { |
| | | semaphore = new Semaphore(0); |
| | | } |
| | | } |
| | | return semaphore; |
| | | } |
| | | |
| | | /** |
| | | * 创建OkHttpUtils |
| | | * |
| | | * @return |
| | | */ |
| | | public static OkHttpUtils builder() { |
| | | return new OkHttpUtils(); |
| | | } |
| | | |
| | | /** |
| | | * 添加url |
| | | * |
| | | * @param url |
| | | * @return |
| | | */ |
| | | public OkHttpUtils url(String url) { |
| | | this.url = url; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 添加参数 |
| | | * |
| | | * @param key 参数名 |
| | | * @param value 参数值 |
| | | * @return |
| | | */ |
| | | public OkHttpUtils addParam(String key, String value) { |
| | | if (paramMap == null) { |
| | | paramMap = new LinkedHashMap<>(16); |
| | | } |
| | | paramMap.put(key, value); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 添加请求头 |
| | | * |
| | | * @param key 参数名 |
| | | * @param value 参数值 |
| | | * @return |
| | | */ |
| | | public OkHttpUtils addHeader(String key, String value) { |
| | | if (headerMap == null) { |
| | | headerMap = new LinkedHashMap<>(16); |
| | | } |
| | | headerMap.put(key, value); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 初始化get方法 |
| | | * |
| | | * @return |
| | | */ |
| | | public OkHttpUtils get() { |
| | | request = new Request.Builder().get(); |
| | | StringBuilder urlBuilder = new StringBuilder(url); |
| | | if (paramMap != null) { |
| | | urlBuilder.append("?"); |
| | | try { |
| | | for (Map.Entry<String, String> entry : paramMap.entrySet()) { |
| | | urlBuilder.append(URLEncoder.encode(entry.getKey(), "utf-8")). |
| | | append("="). |
| | | append(URLEncoder.encode(entry.getValue(), "utf-8")). |
| | | append("&"); |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | urlBuilder.deleteCharAt(urlBuilder.length() - 1); |
| | | } |
| | | request.url(urlBuilder.toString()); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 初始化post方法 |
| | | * |
| | | * @param isJsonPost true等于json的方式提交数据,类似postman里post方法的raw |
| | | * false等于普通的表单提交 |
| | | * @return |
| | | */ |
| | | public OkHttpUtils post(boolean isJsonPost) { |
| | | RequestBody requestBody; |
| | | if (isJsonPost) { |
| | | String json = ""; |
| | | if (paramMap != null) { |
| | | json = JSONObject.valueToString(paramMap); |
| | | } |
| | | requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json); |
| | | } else { |
| | | FormBody.Builder formBody = new FormBody.Builder(); |
| | | if (paramMap != null) { |
| | | paramMap.forEach(formBody::add); |
| | | } |
| | | requestBody = formBody.build(); |
| | | } |
| | | request = new Request.Builder().post(requestBody).url(url); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 同步请求 |
| | | * |
| | | * @return |
| | | */ |
| | | public Request.Builder sync() { |
| | | return setHeader(request); |
| | | } |
| | | |
| | | /** |
| | | * 同步请求 |
| | | * |
| | | * @return |
| | | */ |
| | | public String syncStr() { |
| | | setHeader(request); |
| | | try { |
| | | Response response = okHttpClient.newCall(request.build()).execute(); |
| | | assert response.body() != null; |
| | | return response.body().string(); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | return "请求失败:" + e.getMessage(); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 异步请求,有返回值 |
| | | */ |
| | | public String async() { |
| | | StringBuilder buffer = new StringBuilder(""); |
| | | setHeader(request); |
| | | okHttpClient.newCall(request.build()).enqueue(new Callback() { |
| | | @Override |
| | | public void onFailure(Call call, IOException e) { |
| | | buffer.append("请求出错:").append(e.getMessage()); |
| | | } |
| | | |
| | | @Override |
| | | public void onResponse(Call call, Response response) throws IOException { |
| | | assert response.body() != null; |
| | | buffer.append(response.body().string()); |
| | | getSemaphoreInstance().release(); |
| | | } |
| | | }); |
| | | try { |
| | | getSemaphoreInstance().acquire(); |
| | | } catch (InterruptedException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | return buffer.toString(); |
| | | } |
| | | |
| | | /** |
| | | * 异步请求,带有接口回调 |
| | | * |
| | | * @param callBack |
| | | */ |
| | | public void async(ICallBack callBack) { |
| | | setHeader(request); |
| | | okHttpClient.newCall(request.build()).enqueue(new Callback() { |
| | | @Override |
| | | public void onFailure(Call call, IOException e) { |
| | | callBack.onFailure(call, e.getMessage()); |
| | | } |
| | | |
| | | @Override |
| | | public void onResponse(Call call, Response response) throws IOException { |
| | | assert response.body() != null; |
| | | callBack.onSuccessful(call, response.body().string()); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 为request添加请求头 |
| | | * |
| | | * @param request |
| | | */ |
| | | private Request.Builder setHeader(Request.Builder request) { |
| | | if (headerMap != null) { |
| | | try { |
| | | for (Map.Entry<String, String> entry : headerMap.entrySet()) { |
| | | request.addHeader(entry.getKey(), entry.getValue()); |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | return request; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 生成安全套接字工厂,用于https请求的证书跳过 |
| | | * |
| | | * @return |
| | | */ |
| | | private static SSLSocketFactory createSSLSocketFactory(TrustManager[] trustAllCerts) { |
| | | SSLSocketFactory ssfFactory = null; |
| | | try { |
| | | SSLContext sc = SSLContext.getInstance("SSL"); |
| | | sc.init(null, trustAllCerts, new SecureRandom()); |
| | | ssfFactory = sc.getSocketFactory(); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | return ssfFactory; |
| | | } |
| | | |
| | | private static TrustManager[] buildTrustManagers() { |
| | | return new TrustManager[]{ |
| | | new X509TrustManager() { |
| | | @Override |
| | | public void checkClientTrusted(X509Certificate[] chain, String authType) { |
| | | } |
| | | |
| | | @Override |
| | | public void checkServerTrusted(X509Certificate[] chain, String authType) { |
| | | } |
| | | |
| | | @Override |
| | | public X509Certificate[] getAcceptedIssuers() { |
| | | return new X509Certificate[]{}; |
| | | } |
| | | } |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * 自定义一个接口回调 |
| | | */ |
| | | public interface ICallBack { |
| | | |
| | | void onSuccessful(Call call, String data); |
| | | |
| | | void onFailure(Call call, String errorMsg); |
| | | |
| | | } |
| | | |
| | | public static void main(String[] args) { |
| | | String url = "https://api2.binance.com/api/v3/ticker/24hr?symbol=BNBUSDT&type=MINI"; |
| | | String result = OkHttpUtils.builder() |
| | | .url(url) |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .get() |
| | | .syncStr(); |
| | | System.out.println(result); |
| | | } |
| | | } |
| | | |
| | | |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsException; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | |
| | | public final class ParameterChecker { |
| | | |
| | | private ParameterChecker() { |
| | | } |
| | | |
| | | public static void checkParameter(LinkedHashMap<String, Object> parameters, String parameter, Class t) { |
| | | checkRequiredParameter(parameters, parameter); |
| | | checkParameterType(parameters.get(parameter), t, parameter); |
| | | } |
| | | |
| | | public static void checkOrParameters(LinkedHashMap<String, Object> parameters, String parameter, String parameter2) { |
| | | if (!parameters.containsKey(parameter) && (!parameters.containsKey(parameter2))) { |
| | | throw new FebsException(String.format("Either \"%s\" or \"%s\" is required!", parameter, parameter2)); |
| | | } |
| | | } |
| | | |
| | | public static void checkRequiredParameter(LinkedHashMap<String, Object> parameters, String parameter) { |
| | | if (!parameters.containsKey(parameter)) { |
| | | throw new FebsException(String.format("\"%s\" is a mandatory parameter!", parameter)); |
| | | } |
| | | } |
| | | |
| | | public static void checkParameterType(Object parameter, Class t, String name) { |
| | | if (!t.isInstance(parameter)) { |
| | | throw new FebsException(String.format("\"%s\" must be of %s type.", name, t)); |
| | | } else if (t == String.class && parameter.toString().trim().equals("")) { |
| | | throw new FebsException(String.format("\"%s\" must not be empty.", name)); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils; |
| | | |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsException; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.HttpMethod; |
| | | import okhttp3.MediaType; |
| | | import okhttp3.Request; |
| | | import okhttp3.RequestBody; |
| | | |
| | | public final class RequestBuilder { |
| | | private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8"); |
| | | |
| | | private RequestBuilder() { |
| | | } |
| | | |
| | | public static Request.Builder buildPublicRequest(String fullUrl, HttpMethod httpMethod, boolean issimulated) { |
| | | try { |
| | | final Request.Builder result; |
| | | switch (httpMethod) { |
| | | case POST: |
| | | OkHttpUtils builder = OkHttpUtils.builder(); |
| | | if(issimulated){ |
| | | builder.addHeader("x-simulated-trading","1"); |
| | | } |
| | | result = builder |
| | | .url(fullUrl) |
| | | .post(true) |
| | | .sync(); |
| | | break; |
| | | case GET: |
| | | OkHttpUtils builder1 = OkHttpUtils.builder(); |
| | | if(issimulated){ |
| | | builder1.addHeader("x-simulated-trading","1"); |
| | | } |
| | | result = builder1 |
| | | .url(fullUrl) |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .get() |
| | | .sync(); |
| | | break; |
| | | case PUT: |
| | | OkHttpUtils builder2 = OkHttpUtils.builder(); |
| | | if(issimulated){ |
| | | builder2.addHeader("x-simulated-trading","1"); |
| | | } |
| | | result = builder2 |
| | | .url(fullUrl) |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .post(false) |
| | | .sync(); |
| | | break; |
| | | case DELETE: |
| | | OkHttpUtils builder3 = OkHttpUtils.builder(); |
| | | if(issimulated){ |
| | | builder3.addHeader("x-simulated-trading","1"); |
| | | } |
| | | result = builder3 |
| | | .url(fullUrl) |
| | | .post(false) |
| | | .sync(); |
| | | break; |
| | | default: |
| | | throw new FebsException("Invalid HTTP method: " + httpMethod); |
| | | } |
| | | return result; |
| | | } catch (IllegalArgumentException e) { |
| | | throw new FebsException("Invalid URL: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | public static Request buildApiKeyRequest(String fullUrl,String body,String passphrase,String sign,String timeStamp, HttpMethod httpMethod, String apiKey,boolean issimulate) { |
| | | try { |
| | | final Request request; |
| | | switch (httpMethod) { |
| | | case POST: |
| | | Request.Builder builder = new Request.Builder(); |
| | | if(issimulate){ |
| | | builder.addHeader("x-simulated-trading","1"); |
| | | } |
| | | request = builder |
| | | .url(fullUrl) |
| | | .post(RequestBody.create(JSON_TYPE, body)) |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-SIGN", sign) |
| | | .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) |
| | | .addHeader("OK-ACCESS-PASSPHRASE", passphrase) |
| | | .build(); |
| | | break; |
| | | case GET: |
| | | Request.Builder builder1 = new Request.Builder(); |
| | | if(issimulate){ |
| | | builder1.addHeader("x-simulated-trading","1"); |
| | | } |
| | | request = builder1 |
| | | .url(fullUrl) |
| | | .get() |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-SIGN", sign) |
| | | .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) |
| | | .addHeader("OK-ACCESS-PASSPHRASE", passphrase) |
| | | .build(); |
| | | break; |
| | | case PUT: |
| | | Request.Builder builder2 = new Request.Builder(); |
| | | if(issimulate){ |
| | | builder2.addHeader("x-simulated-trading","1"); |
| | | } |
| | | request = builder2 |
| | | .url(fullUrl) |
| | | .put(RequestBody.create(JSON_TYPE, "")) |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-SIGN", sign) |
| | | .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) |
| | | .addHeader("OK-ACCESS-PASSPHRASE", passphrase) |
| | | .build(); |
| | | break; |
| | | case DELETE: |
| | | Request.Builder builder3 = new Request.Builder(); |
| | | if(issimulate){ |
| | | builder3.addHeader("x-simulated-trading","1"); |
| | | } |
| | | request = builder3 |
| | | .url(fullUrl) |
| | | .delete() |
| | | .addHeader("Content-Type", "application/x-www-form-urlencoded") |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-KEY", apiKey) |
| | | .addHeader("OK-ACCESS-SIGN", sign) |
| | | .addHeader("OK-ACCESS-TIMESTAMP", timeStamp) |
| | | .addHeader("OK-ACCESS-PASSPHRASE", passphrase) |
| | | .build(); |
| | | break; |
| | | default: |
| | | throw new FebsException("Invalid HTTP method: " + httpMethod); |
| | | } |
| | | return request; |
| | | } catch (IllegalArgumentException e) { |
| | | throw new FebsException("Invalid URL: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | public static Request buildWebsocketRequest(String fullUrl) { |
| | | return new Request.Builder().url(fullUrl).build(); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils; |
| | | |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import javax.crypto.Mac; |
| | | import javax.crypto.spec.SecretKeySpec; |
| | | import java.util.Base64; |
| | | |
| | | |
| | | @Slf4j |
| | | public class SignUtils { |
| | | |
| | | public static String signRest(String secretKey, String timestamp, String method, String path, String body) { |
| | | String str = String.format("%s%s%s%s", |
| | | timestamp, // timestamp |
| | | method, // method GET/POST |
| | | path, // requestPath |
| | | body // body |
| | | ); |
| | | try { |
| | | return Base64.getEncoder().encodeToString(hmacSHA256(secretKey.getBytes(), str.getBytes())); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * HmacSHA256算法,返回的结果始终是32位 |
| | | * |
| | | * @param key 加密的键,可以是任何数据 |
| | | * @param content 待加密的内容 |
| | | * @return 加密后的内容 |
| | | * @throws Exception ex |
| | | */ |
| | | public static byte[] hmacSHA256(byte[] key, byte[] content) throws Exception { |
| | | Mac hmacSha256 = Mac.getInstance("HmacSHA256"); |
| | | hmacSha256.init(new SecretKeySpec(key, 0, key.length, "HmacSHA256")); |
| | | return hmacSha256.doFinal(content); |
| | | } |
| | | |
| | | public static String signWebsocket(String timestamp, String secretKey) { |
| | | String str = String.format("%s%s%s", |
| | | timestamp, |
| | | "GET", |
| | | "/users/self/verify"); |
| | | try { |
| | | return Base64.getEncoder().encodeToString(hmacSHA256(secretKey.getBytes(), str.getBytes())); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils; |
| | | |
| | | import java.io.UnsupportedEncodingException; |
| | | import java.net.URLEncoder; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.text.DecimalFormat; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | |
| | | |
| | | public final class UrlBuilder { |
| | | private static final int MAX_DECIMAL_DIGITS = 30; |
| | | private static DecimalFormat df; |
| | | |
| | | |
| | | private UrlBuilder() { |
| | | } |
| | | |
| | | public static String buildFullUrl(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters, String signature) { |
| | | if (parameters != null && !parameters.isEmpty()) { |
| | | StringBuilder sb = new StringBuilder(baseUrl); |
| | | sb.append(urlPath).append('?'); |
| | | joinQueryParameters(sb, parameters); |
| | | if (null != signature) { |
| | | sb.append("&signature=").append(signature); |
| | | } |
| | | return sb.toString(); |
| | | } else { |
| | | return baseUrl + urlPath; |
| | | } |
| | | } |
| | | |
| | | public static String buildStreamUrl(String baseUrl, ArrayList<String> streams) { |
| | | StringBuilder sb = new StringBuilder(baseUrl); |
| | | sb.append("?streams="); |
| | | return joinStreamUrls(sb, streams); |
| | | } |
| | | |
| | | //concatenate query parameters |
| | | public static String joinQueryParameters(LinkedHashMap<String, Object> parameters) { |
| | | return joinQueryParameters(new StringBuilder(), parameters).toString(); |
| | | } |
| | | |
| | | public static StringBuilder joinQueryParameters(StringBuilder urlPath, LinkedHashMap<String, Object> parameters) { |
| | | if (parameters == null || parameters.isEmpty()) { |
| | | return urlPath; |
| | | } |
| | | |
| | | boolean isFirst = true; |
| | | for (Map.Entry<String, Object> mapElement : parameters.entrySet()) { |
| | | |
| | | if (mapElement.getValue() instanceof Double) { |
| | | parameters.replace(mapElement.getKey(), getFormatter().format(mapElement.getValue())); |
| | | } else if (mapElement.getValue() instanceof ArrayList) { |
| | | if (((ArrayList<?>) mapElement.getValue()).isEmpty()) { |
| | | continue; |
| | | } |
| | | String key = mapElement.getKey(); |
| | | joinArrayListParameters(key, urlPath, (ArrayList<?>) mapElement.getValue(), isFirst); |
| | | isFirst = false; |
| | | continue; |
| | | } |
| | | |
| | | if (isFirst) { |
| | | isFirst = false; |
| | | } else { |
| | | urlPath.append('&'); |
| | | } |
| | | |
| | | urlPath.append(mapElement.getKey()) |
| | | .append('=') |
| | | .append(urlEncode(mapElement.getValue().toString())); |
| | | } |
| | | return urlPath; |
| | | } |
| | | |
| | | private static void joinArrayListParameters(String key, StringBuilder urlPath, ArrayList<?> values, boolean isFirst) { |
| | | for (Object value: values) { |
| | | if (isFirst) { |
| | | isFirst = false; |
| | | } else { |
| | | urlPath.append('&'); |
| | | } |
| | | |
| | | urlPath.append(key) |
| | | .append('=') |
| | | .append(urlEncode(value.toString())); |
| | | } |
| | | } |
| | | |
| | | private static String joinStreamUrls(StringBuilder urlPath, ArrayList<String> streams) { |
| | | boolean isFirst = true; |
| | | for (String stream: streams) { |
| | | if (isFirst) { |
| | | isFirst = false; |
| | | } else { |
| | | urlPath.append('/'); |
| | | } |
| | | urlPath.append(stream); |
| | | } |
| | | return urlPath.toString(); |
| | | } |
| | | |
| | | |
| | | public static String urlEncode(String s) { |
| | | try { |
| | | return URLEncoder.encode(s, StandardCharsets.UTF_8.name()); |
| | | } catch (UnsupportedEncodingException e) { |
| | | // UTF-8 being unsuppored is unlikely |
| | | // Replace with a unchecked exception to tidy up exception handling |
| | | throw new RuntimeException(StandardCharsets.UTF_8.name() + " is unsupported", e); |
| | | } |
| | | } |
| | | |
| | | private static DecimalFormat getFormatter() { |
| | | if (null == df) { |
| | | df = new DecimalFormat(); |
| | | df.setMaximumFractionDigits(MAX_DECIMAL_DIGITS); |
| | | df.setGroupingUsed(false); |
| | | } |
| | | return df; |
| | | } |
| | | |
| | | public static String buildTimestamp() { |
| | | return String.valueOf(System.currentTimeMillis()); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.vo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public class BalanceVo { |
| | | //币种 如BTC |
| | | private String ccy; |
| | | // 可用余额 |
| | | private String availBal; |
| | | // 币种余额 |
| | | private String cashBal; |
| | | // 可用保证金 |
| | | private String availEq; |
| | | // 未实现盈亏总额 |
| | | private String unrealizedProfit; |
| | | // 维持保证金 |
| | | private String maintMargin; |
| | | //合约率 |
| | | private String mgnRatio; |
| | | //占用保证金 |
| | | private String frozenBal; |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.vo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public class InstrumentsVo { |
| | | /** |
| | | * 产品id, 如 BTC-USDT |
| | | */ |
| | | private String instId; |
| | | /** |
| | | * 产品状态 |
| | | * live:交易中 |
| | | * suspend:暂停中 |
| | | * preopen:预上线,如:交割和期权的新合约在 live 之前,会有 preopen 状态 |
| | | * test:测试中(测试产品,不可交易) |
| | | */ |
| | | private String state; |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.config.vo; |
| | | |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import lombok.Data; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Data |
| | | public class PositionsVo { |
| | | private List<JSONObject> positionList; |
| | | private Map<String, JSONObject> newPositionMap; |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.enumerates; |
| | | |
| | | import lombok.Getter; |
| | | |
| | | @Getter |
| | | public enum TradeTypeEnum { |
| | | /** |
| | | * 1 - 开仓 2 - 平仓 |
| | | */ |
| | | OPEN_ORDER(1,"open","开仓"), |
| | | CLOSE_ORDER(2,"close","平仓"), |
| | | /** |
| | | * 交易模式 |
| | | * 保证金模式:isolated:逐仓 ;cross:全仓 |
| | | * 非保证金模式:cash:非保证金 |
| | | * spot_isolated:现货逐仓(仅适用于现货带单) ,现货带单时,tdMode 的值需要指定为spot_isolated |
| | | */ |
| | | ISOLATED(1,"isolated","逐仓"), |
| | | CROSS(2,"cross","全仓"), |
| | | /** |
| | | * 持仓方向 |
| | | * long:开平仓模式开多,pos为正 |
| | | * short:开平仓模式开空,pos为正 |
| | | * net:买卖模式(交割/永续/期权:pos为正代表开多,pos为负代表开空。币币杠杆时,pos均为正,posCcy为交易货币时,代表开多;posCcy为计价货币时,代表开空。) |
| | | */ |
| | | LONG(1,"long","持仓方向-long"), |
| | | SHORT(2,"short","持仓方向-short"), |
| | | /** |
| | | * 订单类型 |
| | | * market:市价单 |
| | | * limit:限价单 |
| | | * post_only:只做maker单 |
| | | * fok:全部成交或立即取消 |
| | | * ioc:立即成交并取消剩余 |
| | | * optimal_limit_ioc:市价委托立即成交并取消剩余(仅适用交割、永续) |
| | | * mmp:做市商保护(仅适用于组合保证金账户模式下的期权订单) |
| | | * mmp_and_post_only:做市商保护且只做maker单(仅适用于组合保证金账户模式下的期权订单) |
| | | */ |
| | | MARKET(1,"market","市价单"), |
| | | LIMIT(2,"limit","限价单"), |
| | | /** |
| | | * 订单方向 |
| | | * buy:买, sell:卖 |
| | | */ |
| | | BUY(1,"buy","买"), |
| | | SELL(2,"sell","卖") |
| | | ; |
| | | |
| | | private int code; |
| | | private String value; |
| | | private String description; |
| | | |
| | | TradeTypeEnum(int code, String value, String description) { |
| | | this.code = code; |
| | | this.value = value; |
| | | this.description = description; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.order; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsResponse; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.QuantApiMessage; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.TradeOrderDto; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeRequestBuy; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeRequestSell; |
| | | |
| | | //订单交易接口 欧易调用的接口是 撮合交易/交易/ POST 下单 |
| | | public interface ITradeOrderService { |
| | | FebsResponse QuantExchangeReturnVo(TradeOrderDto tradeOrderDto, QuantApiMessage quantApiMessage); |
| | | |
| | | /** |
| | | * 消费买入消息 |
| | | * @param returnVo |
| | | */ |
| | | void operationBuyMsg(TradeRequestBuy returnVo); |
| | | |
| | | /** |
| | | * 消费卖出消息 |
| | | * @param returnVo |
| | | */ |
| | | void operationSellMsg(TradeRequestSell returnVo); |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.order; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 交易订单工厂类 |
| | | * 该类用于根据不同的交易平台(如OKX、BINANCE)获取相应的交易订单服务实例 |
| | | */ |
| | | @Component |
| | | @Data |
| | | public class TradeOrderFactory { |
| | | /** |
| | | * OKX交易平台的交易订单服务实例 |
| | | */ |
| | | @Qualifier("oKXTradeOrderServiceImpl") |
| | | private final ITradeOrderService oKXTradeOrderServiceImpl; |
| | | |
| | | /** |
| | | * 存储不同交易平台对应的交易订单服务实例的映射 |
| | | */ |
| | | private Map<String, ITradeOrderService> accountMap = new HashMap<>(); |
| | | |
| | | /** |
| | | * 构造方法,初始化交易平台与交易订单服务实例的映射 |
| | | * |
| | | * @param oKXTradeOrderServiceImpl OKX交易平台的交易订单服务实例 |
| | | */ |
| | | public TradeOrderFactory(ITradeOrderService oKXTradeOrderServiceImpl) { |
| | | this.oKXTradeOrderServiceImpl = oKXTradeOrderServiceImpl; |
| | | accountMap.put("OKX", oKXTradeOrderServiceImpl); |
| | | } |
| | | |
| | | /** |
| | | * 根据平台关键字获取交易订单服务实例 |
| | | * |
| | | * @param key 平台关键字,如"OKX"、"BINANCE" |
| | | * @return 对应平台的交易订单服务实例 |
| | | */ |
| | | public ITradeOrderService get(String key) { |
| | | return accountMap.get(key); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.order.impl; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsResponse; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.QuantApiMessage; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.TradeOrderDto; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.order.ITradeOrderService; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.order.vo.QuantExchangeReturnVo; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeRequestBuy; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeRequestSell; |
| | | import com.xcong.excoin.modules.okxNewPrice.jiaoyi.IMQService; |
| | | import lombok.SneakyThrows; |
| | | import org.json.JSONObject; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.crypto.Mac; |
| | | import javax.crypto.spec.SecretKeySpec; |
| | | import java.io.BufferedReader; |
| | | import java.io.InputStreamReader; |
| | | import java.io.OutputStream; |
| | | import java.net.HttpURLConnection; |
| | | import java.net.URL; |
| | | import java.util.Base64; |
| | | |
| | | @Service("oKXTradeOrderServiceImpl") |
| | | public class OKXTradeOrderServiceImpl implements ITradeOrderService { |
| | | @Value("${spring.OKEX.baseurl}") |
| | | private String baseurl; |
| | | |
| | | @Resource |
| | | private IMQService imqService; |
| | | |
| | | @SneakyThrows |
| | | @Override |
| | | public FebsResponse QuantExchangeReturnVo(TradeOrderDto tradeOrderDto, QuantApiMessage quantApiMessage) { |
| | | try { |
| | | // 构建订单的JSON对象 |
| | | JSONObject jsonBody = new JSONObject(); |
| | | jsonBody.put("instId", "BTC-USDT"); |
| | | jsonBody.put("tdMode", "cash"); |
| | | jsonBody.put("side", "buy"); |
| | | jsonBody.put("ordType", "limit"); |
| | | jsonBody.put("sz", "0.01"); |
| | | jsonBody.put("px", "30000"); |
| | | |
| | | // 发起下单请求 |
| | | String result = postRequest("/api/v5/trade/order", jsonBody.toString(),quantApiMessage); |
| | | System.out.println("Result: " + result); |
| | | |
| | | //解析返回数据 |
| | | JSONObject jsonResponse = new JSONObject(result); |
| | | if (jsonResponse.has("code") && "0".equals(jsonResponse.getString("code"))) { |
| | | // 订单提交成功 |
| | | JSONObject jSONObject = jsonResponse.getJSONObject("data"); |
| | | String clOrdId = jSONObject.getString("clOrdId"); |
| | | String ordId = jSONObject.getString("ordId"); |
| | | QuantExchangeReturnVo quantExchangeReturnVo = new QuantExchangeReturnVo(); |
| | | quantExchangeReturnVo.setCode("0"); |
| | | quantExchangeReturnVo.setOrdId(ordId); |
| | | quantExchangeReturnVo.setClOrdId(clOrdId); |
| | | return new FebsResponse().success().data(quantExchangeReturnVo); |
| | | } else { |
| | | String code = jsonResponse.getString("code"); |
| | | String msg = jsonResponse.getString("msg"); |
| | | QuantExchangeReturnVo quantExchangeReturnVo = new QuantExchangeReturnVo(); |
| | | quantExchangeReturnVo.setCode(code); |
| | | quantExchangeReturnVo.setMessage(msg); |
| | | return new FebsResponse().fail().data(quantExchangeReturnVo); |
| | | } |
| | | |
| | | } catch (Exception e) { |
| | | return new FebsResponse().fail().message("下单失败"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void operationBuyMsg(TradeRequestBuy returnVo) { |
| | | |
| | | imqService.operationBuyMsg(returnVo); |
| | | } |
| | | |
| | | @Override |
| | | public void operationSellMsg(TradeRequestSell returnVo) { |
| | | |
| | | imqService.operationSellMsg(returnVo); |
| | | } |
| | | |
| | | private String postRequest(String endpoint, String body, QuantApiMessage quantApiMessage) throws Exception { |
| | | URL url = new URL(baseurl + endpoint); |
| | | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); |
| | | connection.setDoOutput(true); |
| | | connection.setRequestMethod("POST"); |
| | | connection.setRequestProperty("Content-Type", "application/json"); |
| | | connection.setRequestProperty("OK-ACCESS-KEY", quantApiMessage.getASecretkey()); |
| | | connection.setRequestProperty("OK-ACCESS-SIGN", generateSignature(body,quantApiMessage)); |
| | | connection.setRequestProperty("OK-ACCESS-TIMESTAMP", getTimestamp()); |
| | | connection.setRequestProperty("OK-ACCESS-PASSPHRASE", quantApiMessage.getPassPhrass()); |
| | | |
| | | try (OutputStream os = connection.getOutputStream()) { |
| | | os.write(body.getBytes()); |
| | | os.flush(); |
| | | } |
| | | |
| | | if (connection.getResponseCode() != 200) { |
| | | throw new RuntimeException("Failed : HTTP Error code : " + connection.getResponseCode()); |
| | | } |
| | | |
| | | BufferedReader br = new BufferedReader(new InputStreamReader((connection.getInputStream()))); |
| | | StringBuilder output = new StringBuilder(); |
| | | String line; |
| | | while ((line = br.readLine()) != null) { |
| | | output.append(line); |
| | | } |
| | | connection.disconnect(); |
| | | return output.toString(); |
| | | } |
| | | |
| | | private static String generateSignature(String body, QuantApiMessage quantApiMessage) throws Exception { |
| | | String preHash = getTimestamp() + "POST" + "/api/v5/order" + body; |
| | | SecretKeySpec secretKey = new SecretKeySpec(quantApiMessage.getBSecretkey().getBytes(), "HmacSHA256"); |
| | | Mac mac = Mac.getInstance("HmacSHA256"); |
| | | mac.init(secretKey); |
| | | return Base64.getEncoder().encodeToString(mac.doFinal(preHash.getBytes())); |
| | | } |
| | | |
| | | private static String getTimestamp() { |
| | | return String.valueOf(System.currentTimeMillis() / 1000); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.order.vo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * 订单返回信息 |
| | | */ |
| | | @Data |
| | | public class QuantExchangeReturnVo{ |
| | | |
| | | private String code;//交易状态 0代表成功 |
| | | private String message;//错误信息 |
| | | private String ordId;//交易所订单号 |
| | | private String clOrdId;//客户订单id |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.query; |
| | | |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsResponse; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.QuantApiMessage; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.query.dto.QuantOperateRecode; |
| | | |
| | | //订单查询接口 欧易调用的接口是 撮合交易/交易/ GET 获取订单信息 |
| | | public interface IQueryOrderService { |
| | | FebsResponse QueryOrder(QuantApiMessage quantApiMessage, String instId, String clOrdId); |
| | | |
| | | //检查订单在交易所交易情况 exChangeState返回状态说明 1.filled代表交易成功 2.canceled代表交易失败或者取消 3.live代表交易中 |
| | | FebsResponse checkOrder(QuantOperateRecode operateRecode); |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.query; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 查询委托工厂类 |
| | | * 该类用于根据不同的交易平台(如OKX、BINANCE)获取相应的查询委托服务实例 |
| | | */ |
| | | @Component |
| | | @Data |
| | | public class QueryOrderFactory { |
| | | // OKX的查询委托服务实现 |
| | | @Qualifier("oKXQueryOrderServiceImpl") |
| | | private final IQueryOrderService oKXQueryOrderServiceImpl; |
| | | |
| | | // 存储不同交易平台查询委托服务的映射 |
| | | private Map<String, IQueryOrderService> accountMap = new HashMap<>(); |
| | | |
| | | /** |
| | | * 构造方法,初始化查询委托服务映射 |
| | | * @param oKXQueryOrderServiceImpl OKX的查询委托服务实现 |
| | | */ |
| | | public QueryOrderFactory(IQueryOrderService oKXQueryOrderServiceImpl) { |
| | | this.oKXQueryOrderServiceImpl = oKXQueryOrderServiceImpl; |
| | | accountMap.put("OKX", oKXQueryOrderServiceImpl); |
| | | } |
| | | |
| | | /** |
| | | * 根据平台名称获取查询委托服务实例 |
| | | * @param key 平台名称,如"OKX"、"BINANCE" |
| | | * @return 对应平台的查询委托服务实例 |
| | | */ |
| | | public IQueryOrderService get(String key) { |
| | | return accountMap.get(key); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.query.dto; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | @Data |
| | | public class QuantOperateRecode{ |
| | | |
| | | private String tradeNumber;//交易编号 |
| | | private String operationCode;//操作编号 |
| | | private String sellTradeNumber;//卖出的订单号 |
| | | private Long currencyId;//操作币种ID |
| | | private String exchange;//交易所名称 |
| | | private Long apiMessageId;//交易所ID |
| | | private Long memberId;//用户ID |
| | | private String coinPair;//货币对 |
| | | private Integer coinType;//计价货币 1-USDTU本位 2-USDT币本位 |
| | | private Integer coinDirect;//方向 1-买多 2-买空 |
| | | private BigDecimal coinLevel;//杠杆倍数 |
| | | private BigDecimal pageNum;//张数 |
| | | private BigDecimal price;//均价 |
| | | private BigDecimal amount;//总价 |
| | | private BigDecimal profit;//盈亏 |
| | | private BigDecimal fee;//费用与返佣 |
| | | private BigDecimal singleOrder;//单序 |
| | | private BigDecimal quantity;//数量 |
| | | private Integer directStatus;//策略 1-顺势 2-逆势 |
| | | private Integer type;//方式 1.网络 2.其他 |
| | | private Integer status;//所属状态 1-开仓 2-平仓 |
| | | private Integer finishStatus;//所属状态 1-未完成 2-已完成 3-撤单 4-已卖出(针对买单由此状态)5.-交易失败 |
| | | private String ordId;//交易所订单ID |
| | | private BigDecimal sellPrice;//计划平仓价格 |
| | | private BigDecimal incomePrice;//平仓传入价格 |
| | | private BigDecimal rangeRatio;//比率区域 |
| | | private Integer lockStatus;//卖出状态 0-未锁定 1-已锁定 |
| | | private String buyRediskey;//买单的rediskey |
| | | private Integer tickSz;//下单精度 |
| | | |
| | | @TableField(exist = false) |
| | | private String startTime;//开始时间 |
| | | @TableField(exist = false) |
| | | private String endTime;//结束时间 |
| | | @TableField(exist = false) |
| | | private Integer excludeFinishStatus;//查询时排除状态 |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.query.impl; |
| | | |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsException; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsResponse; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.QuantApiMessage; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.HttpMethod; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.OKXContants; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.vo.BalanceVo; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.query.IQueryOrderService; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.query.dto.QuantOperateRecode; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.query.vo.QuantCheckOrderVo; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.query.vo.QuantOperateRecodeVo; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.VerifyAccountFactory; |
| | | import com.xcong.excoin.modules.okxNewPrice.zhanghu.IApiMessageService; |
| | | import com.xcong.excoin.modules.okxNewPrice.zhanghu.ZhangHuEnum; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.SneakyThrows; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.json.JSONObject; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.crypto.Mac; |
| | | import javax.crypto.spec.SecretKeySpec; |
| | | import java.io.BufferedReader; |
| | | import java.io.InputStreamReader; |
| | | import java.math.BigDecimal; |
| | | import java.net.HttpURLConnection; |
| | | import java.net.URL; |
| | | import java.util.ArrayList; |
| | | import java.util.Base64; |
| | | import java.util.LinkedHashMap; |
| | | |
| | | @Service("oKXQueryOrderServiceImpl") |
| | | @RequiredArgsConstructor |
| | | @Slf4j |
| | | public class OKXQueryOrderServiceImpl implements IQueryOrderService { |
| | | @Value("spring.OKEX.baseurl") |
| | | private String baseurl; |
| | | |
| | | private final IApiMessageService apiMessageService; |
| | | |
| | | private final VerifyAccountFactory verifyAccountFactory; |
| | | |
| | | private static String hmacSha256(String data, String key) throws Exception { |
| | | Mac mac = Mac.getInstance("HmacSHA256"); |
| | | SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "HmacSHA256"); |
| | | mac.init(secretKeySpec); |
| | | return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes())); |
| | | } |
| | | |
| | | @SneakyThrows |
| | | @Override |
| | | public FebsResponse QueryOrder(QuantApiMessage quantApiMessage, String instId, String clOrdId) { |
| | | String url = baseurl + "/api/v5/trade/order?instId=" + instId + "&clOrdId=" + clOrdId; // 构建请求URL |
| | | try { |
| | | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); |
| | | connection.setRequestMethod("GET"); |
| | | connection.setRequestProperty("Content-Type", "application/json"); |
| | | connection.setRequestProperty("OK-ACCESS-KEY", quantApiMessage.getASecretkey()); |
| | | connection.setRequestProperty("OK-ACCESS-SIGN", hmacSha256(quantApiMessage.getBSecretkey(), clOrdId)); // 根据需要生成签名 |
| | | connection.setRequestProperty("OK-ACCESS-TIMESTAMP", String.valueOf(System.currentTimeMillis() / 1000)); |
| | | connection.setRequestProperty("OK-ACCESS-PASSPHRASE", "your_passphrase"); // 替换为您的Passphrase |
| | | |
| | | int responseCode = connection.getResponseCode(); |
| | | |
| | | if (responseCode == HttpURLConnection.HTTP_OK) { |
| | | BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); |
| | | String inputLine; |
| | | StringBuilder response = new StringBuilder(); |
| | | |
| | | while ((inputLine = in.readLine()) != null) { |
| | | response.append(inputLine); |
| | | } |
| | | in.close(); |
| | | |
| | | JSONObject jsonResponse = new JSONObject(response.toString()); |
| | | QuantCheckOrderVo quantCheckOrderVo = new QuantCheckOrderVo(); |
| | | if (jsonResponse.has("clOrdId") && StringUtils.isNotBlank(jsonResponse.getString("clOrdId"))) { |
| | | quantCheckOrderVo.setTradeNumber(jsonResponse.getString("clOrdId")); |
| | | } |
| | | if (jsonResponse.has("ordId") && StringUtils.isNotBlank(jsonResponse.getString("ordId"))) { |
| | | quantCheckOrderVo.setOrdId(jsonResponse.getString("ordId")); |
| | | } |
| | | if (jsonResponse.has("pnl") && StringUtils.isNotBlank(jsonResponse.getString("pnl"))) { |
| | | quantCheckOrderVo.setProfit(new BigDecimal(jsonResponse.getString("pnl"))); |
| | | } |
| | | if (jsonResponse.has("sz") && StringUtils.isNotBlank(jsonResponse.getString("sz"))) { |
| | | quantCheckOrderVo.setPageNum(new Integer(jsonResponse.getString("sz"))); |
| | | } |
| | | if (jsonResponse.has("fillPx") && StringUtils.isNotBlank(jsonResponse.getString("fillPx"))) { |
| | | quantCheckOrderVo.setPrice(new BigDecimal(jsonResponse.getString("fillPx"))); |
| | | BigDecimal total = new BigDecimal(new Integer(jsonResponse.getString("sz"))).multiply(new BigDecimal(jsonResponse.getString("fillPx"))); |
| | | quantCheckOrderVo.setAmount(total); |
| | | } |
| | | return new FebsResponse().success().data(quantCheckOrderVo); |
| | | } else { |
| | | return new FebsResponse().fail().message("交易所订单查询异常"); |
| | | } |
| | | } catch (Exception e) { |
| | | return new FebsResponse().fail().message("订单查询异常"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public FebsResponse checkOrder(QuantOperateRecode operateRecode) { |
| | | log.info("查询OKX订单信息:{}",operateRecode); |
| | | |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage(ZhangHuEnum.JIAOYISUO.getValue()); |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | throw new FebsException("参数错误"); |
| | | } |
| | | |
| | | OKXAccount okxAccount = (OKXAccount) verifyAccountFactory.getAccountMap().get(quantApiMessage.getExchange()) |
| | | .initAccount(quantApiMessage); |
| | | ArrayList<BalanceVo> balanceVos = new ArrayList<>(); |
| | | |
| | | LinkedHashMap<String, Object> orderParameters = new LinkedHashMap<>(); |
| | | orderParameters.put("clOrdId", operateRecode.getTradeNumber()); |
| | | //将字符串BTC-USDTU-空 截取为 BTC-USDT |
| | | String coinPair = operateRecode.getCoinPair().substring(0, operateRecode.getCoinPair().indexOf("-USDT")+5); |
| | | orderParameters.put("instId", coinPair+"-SWAP"); |
| | | String orderStr = okxAccount.requestHandler.sendSignedRequest(okxAccount.baseUrl, OKXContants.ORDER, orderParameters, HttpMethod.GET, okxAccount.isSimluate()); |
| | | log.info("查询OKX订单返回信息:{}",orderStr); |
| | | QuantOperateRecodeVo quantOperateRecodeVo = new QuantOperateRecodeVo(); |
| | | com.alibaba.fastjson.JSONObject orderStrJson = JSON.parseObject(orderStr); |
| | | String code = orderStrJson.getString("code"); |
| | | if("0".equals(code)){ |
| | | com.alibaba.fastjson.JSONObject data = orderStrJson.getJSONArray("data").getJSONObject(0); |
| | | quantOperateRecodeVo.setQuantity(ObjectUtil.isEmpty(data.getString("accFillSz"))?new BigDecimal("0"): new BigDecimal(data.getString("accFillSz"))); |
| | | quantOperateRecodeVo.setPrice(ObjectUtil.isEmpty(data.getString("avgPx"))?new BigDecimal("0"): new BigDecimal(data.getString("avgPx"))); |
| | | quantOperateRecodeVo.setProfit(ObjectUtil.isEmpty(data.getString("pnl"))?new BigDecimal("0"): new BigDecimal(data.getString("pnl"))); |
| | | quantOperateRecodeVo.setFee(ObjectUtil.isEmpty(data.getString("fee"))?new BigDecimal("0"): new BigDecimal(data.getString("fee"))); |
| | | quantOperateRecodeVo.setExchangeNumber(data.getString("ordId")); |
| | | quantOperateRecodeVo.setCoinLevel(ObjectUtil.isEmpty(data.getString("lever"))?new BigDecimal("0"): new BigDecimal(data.getString("lever"))); |
| | | quantOperateRecodeVo.setOrdType(data.getString("ordType")); |
| | | quantOperateRecodeVo.setExChangeState(data.getString("state")); |
| | | } else if("51603".equals(code)){ |
| | | quantOperateRecodeVo.setQuantity(new BigDecimal("0")); |
| | | quantOperateRecodeVo.setPrice(new BigDecimal("0")); |
| | | quantOperateRecodeVo.setProfit(new BigDecimal("0")); |
| | | quantOperateRecodeVo.setCoinLevel(new BigDecimal("0")); |
| | | quantOperateRecodeVo.setExchangeNumber(""); |
| | | quantOperateRecodeVo.setOrdType(""); |
| | | quantOperateRecodeVo.setExChangeState("canceled"); |
| | | } else { |
| | | return null; |
| | | } |
| | | |
| | | return new FebsResponse().success().data(quantOperateRecodeVo); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.query.vo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | /** |
| | | * 订单返回信息 |
| | | */ |
| | | @Data |
| | | public class QuantCheckOrderVo{ |
| | | |
| | | private String tradeNumber;//交易编号 |
| | | private Integer pageNum;//张数 |
| | | private BigDecimal price;//均价 |
| | | private BigDecimal amount;//总价 |
| | | private BigDecimal profit;//盈亏 |
| | | private String ordId;//交易所订单ID |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.query.vo; |
| | | |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | /** |
| | | * 合约操作记录 |
| | | */ |
| | | @Data |
| | | public class QuantOperateRecodeVo{ |
| | | |
| | | private String tradeNumber;//交易编号 |
| | | private Long apiMessageId;//交易所ID |
| | | private String exchange;//交易所名称 |
| | | private String exchangeNumber;//交易所交易编号 |
| | | private String operationCode;//操作编号 |
| | | private Long currencyId;//操作币种ID |
| | | private Long memberId;//用户ID |
| | | private String coinPair;//货币对 |
| | | private Integer coinDirect;//方向 1-买多 2-买空 |
| | | private BigDecimal coinLevel;//杠杆倍数 |
| | | private BigDecimal pageNum;//张数 |
| | | private BigDecimal quantity;//数量 |
| | | private BigDecimal price;//均价 |
| | | private BigDecimal amount;//成交金额 |
| | | private BigDecimal profit;//盈亏 |
| | | private String sellTradeNumber;//卖出的订单号 |
| | | private BigDecimal fee;//费用与返佣 |
| | | private BigDecimal singleOrder;//单序 |
| | | private BigDecimal sellPrice;//计划平仓价格 |
| | | private Integer coinType;//计价货币 1-USDTU本位 2-USDT币本位 |
| | | private Integer directStatus;//策略 1-顺势 2-逆势 |
| | | private Integer type;//方式 1.网络 2.其他 |
| | | private Integer status;//所属状态 1-开仓 2-平仓 |
| | | private Integer tradeStatus;//交易状态 1-未完成 2-成功 3-失败 |
| | | private Integer finishStatus;//所属状态 1-未完成 2-已完成 3-撤单 4-已卖出(针对买单由此状态)5.-交易失败 |
| | | private String exChangeState;//交易所状态 |
| | | private Integer lockStatus;//卖出状态 0-未锁定 1-已锁定 |
| | | private Integer tickSz;//下单精度 1-0.1 2-0.01 3-0.001 4-0.0001 5-0.00001 |
| | | private String ordType;//订单类型 订单类型 market:市价单 limit:限价单 post_only:只做maker单 fok:全部成交或立即取消 ioc:立即成交并取消剩余 optimal_limit_ioc:市价委托立即成交并取消剩余(仅适用交割、永续) |
| | | //mmp:做市商保护(仅适用于组合保证金账户模式下的期权订单) mmp_and_post_only:做市商保护且只做maker单(仅适用于组合保证金账户模式下的期权订单) op_fok:期权简选(全部成交或立即取消) |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.trade; |
| | | |
| | | import lombok.Getter; |
| | | |
| | | @Getter |
| | | public enum TradeEventEnum { |
| | | |
| | | |
| | | TRADE_CLOSE_POSITION("CLOSE_POSITION", |
| | | "编码:{},用户:{}, 触发价:{},实际成交价格:{},执行全平操作"), |
| | | |
| | | TRADE_SELL("SELL", |
| | | "编码:{},用户:{}, 触发价:{},实际成交价格:{},执行平仓操作"), |
| | | |
| | | TRADE_BUY("BUY", |
| | | "编码:{},用户:{}, 触发价:{},实际成交价格:{},执行开仓操作"); |
| | | |
| | | private String eventPoint; |
| | | |
| | | private String eventDec; |
| | | |
| | | |
| | | TradeEventEnum(String eventPoint, String eventDec) { |
| | | this.eventPoint = eventPoint; |
| | | this.eventDec = eventDec; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.trade; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsException; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.impl.TradeServiceBuy; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.impl.TradeServiceClosePosition; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.impl.TradeServiceSell; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | /** |
| | | * 交易事件运行类,用于根据不同的事件点选择相应的交易策略并执行 |
| | | */ |
| | | public class TradeEventRunner { |
| | | |
| | | // 使用线程安全的ConcurrentHashMap存储交易事件和对应的策略服务 |
| | | private static final Map<String, TradeService> TRADE_EVENT_MAP = new ConcurrentHashMap<>(); |
| | | |
| | | static { |
| | | // 初始化策略 |
| | | List<TradeService> tradeServices = Arrays.asList( |
| | | new TradeServiceBuy(), // 买入策略 |
| | | new TradeServiceSell(), // 卖出策略 |
| | | new TradeServiceClosePosition() // 平仓策略 |
| | | ); |
| | | |
| | | // 将策略放入ConcurrentHashMap中,确保线程安全 |
| | | for (TradeService service : tradeServices) { |
| | | TRADE_EVENT_MAP.put(service.tradeEvent(), service); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据事件点选择并执行相应的交易策略 |
| | | * |
| | | * @param eventPoint 事件点,用于匹配相应的交易策略 |
| | | * @param tradeRequest 包含交易请求信息的LinkedHashMap |
| | | * @param okxAccount OKX账户信息,用于执行交易 |
| | | * @return 交易执行的结果 |
| | | * @throws FebsException 如果找不到对应的交易策略,则抛出异常 |
| | | */ |
| | | public static String runTradeEvent(String eventPoint, LinkedHashMap<String, Object> tradeRequest, OKXAccount okxAccount) { |
| | | TradeService tradeService = TRADE_EVENT_MAP.get(eventPoint); |
| | | if (tradeService == null) { |
| | | throw new FebsException("交易EVENT异常"); |
| | | } |
| | | |
| | | return tradeService.doTrade(tradeRequest,okxAccount); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.trade; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public class TradeRequest { |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.trade; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.math.BigDecimal; |
| | | |
| | | @Data |
| | | public class TradeRequestBuy implements Serializable { |
| | | /** |
| | | * 实现了 Serializable 接口,rabbitmq可以直接发送它,但需要确保 SimpleMessageConverter 能够处理它(对象简单,不是复杂的对象) |
| | | */ |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | private Long strategiesId;//策略ID ReturnBuyVo.strategyId |
| | | |
| | | private String tradeCode;//自定义订单编号 |
| | | |
| | | private Double tradeCnt;//交易数量 ReturnBuyVo.quantity |
| | | /** |
| | | * 限定价格 |
| | | * 限定价格为0的时候,表示市价操作当前订单 |
| | | */ |
| | | private String limitPrice = "0"; |
| | | /** |
| | | * 产品ID,如 BTC-USDT |
| | | */ |
| | | private String instId; //ReturnBuyVo.instId |
| | | /** |
| | | * 持仓方向 |
| | | * 在开平仓模式下必填,且仅可选择 long 或 short。 仅适用交割、永续。 |
| | | * 1-long 2-short |
| | | */ |
| | | private Integer positionSide = 1; |
| | | /** |
| | | * 1 - isolated:逐仓 ;2 - cross:全仓 |
| | | * 交易模式 |
| | | * 保证金模式:isolated:逐仓 ;cross:全仓 |
| | | * 非保证金模式:cash:非保证金 |
| | | * spot_isolated:现货逐仓(仅适用于现货带单) ,现货带单时,tdMode 的值需要指定为spot_isolated |
| | | */ |
| | | private Integer tdMode = 2; |
| | | |
| | | private BigDecimal priceRange; //买入的价格段位 |
| | | |
| | | private Long operationCurrencyId; //操作货币id |
| | | |
| | | private String redisKey; //redisKey 用于标识是否已经处理过 |
| | | |
| | | private String exchange; //交易所 |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.trade; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.vo.SellOrderVo; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | |
| | | @Data |
| | | public class TradeRequestSell implements Serializable { |
| | | /** |
| | | * 实现了 Serializable 接口,rabbitmq可以直接发送它,但需要确保 SimpleMessageConverter 能够处理它(对象简单,不是复杂的对象) |
| | | */ |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | private Long strategiesId;//策略ID ReturnVo.strategyId |
| | | |
| | | private String tradeCode;//自定义订单编号 |
| | | |
| | | private BigDecimal rangeRatio;//区域比率 |
| | | |
| | | private BigDecimal incomePrice;//平仓传入价格 |
| | | |
| | | private Double tradeCnt;//交易数量 ReturnVo.quantity |
| | | /** |
| | | * 限定价格 |
| | | * 限定价格为0的时候,表示市价操作当前订单 |
| | | */ |
| | | private String limitPrice = "0"; |
| | | /** |
| | | * 产品ID,如 BTC-USDT |
| | | */ |
| | | private String instId; //ReturnVo.instId |
| | | /** |
| | | * 持仓方向 |
| | | * 在开平仓模式下必填,且仅可选择 long 或 short。 仅适用交割、永续。 |
| | | * 1-long 2-short |
| | | */ |
| | | private Integer positionSide = 1; |
| | | /** |
| | | * 1 - isolated:逐仓 ;2 - cross:全仓 |
| | | * 交易模式 |
| | | * 保证金模式:isolated:逐仓 ;cross:全仓 |
| | | * 非保证金模式:cash:非保证金 |
| | | * spot_isolated:现货逐仓(仅适用于现货带单) ,现货带单时,tdMode 的值需要指定为spot_isolated |
| | | */ |
| | | private Integer tdMode = 2; |
| | | |
| | | private List<SellOrderVo> sellOrderList; //卖出订单列表,当operations为buy时,该字段为空 |
| | | |
| | | private Long operationCurrencyId; //操作货币id |
| | | |
| | | private String redisKey; //redisKey 用于标识是否已经处理过 |
| | | |
| | | private String exchange; //交易所 |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.trade; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | |
| | | public interface TradeService { |
| | | /** |
| | | * 采用策略 |
| | | * @return |
| | | */ |
| | | String tradeEvent(); |
| | | |
| | | /** |
| | | * 执行 |
| | | * @param tradeDto |
| | | * @param okxAccount |
| | | */ |
| | | String doTrade(LinkedHashMap<String, Object> tradeDto, OKXAccount okxAccount); |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.trade.impl; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.HttpMethod; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.OKXContants; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.ParameterChecker; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeEventEnum; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeService; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | |
| | | @Slf4j |
| | | @RequiredArgsConstructor |
| | | public class TradeServiceBuy implements TradeService { |
| | | |
| | | @Override |
| | | public String tradeEvent() { |
| | | return TradeEventEnum.TRADE_BUY.getEventPoint(); |
| | | } |
| | | |
| | | @Override |
| | | public String doTrade(LinkedHashMap<String, Object> tradeDto, OKXAccount okxAccount) { |
| | | ParameterChecker.checkParameter(tradeDto, "instId", String.class); |
| | | ParameterChecker.checkParameter(tradeDto, "tdMode", String.class); |
| | | ParameterChecker.checkParameter(tradeDto, "side", String.class); |
| | | ParameterChecker.checkParameter(tradeDto, "ordType", String.class); |
| | | ParameterChecker.checkParameter(tradeDto, "sz", Double.class); |
| | | String placeOrderRspOkxRestResponse = okxAccount.requestHandler.sendSignedRequest(okxAccount.baseUrl, OKXContants.ORDER, tradeDto, HttpMethod.POST, okxAccount.isSimluate()); |
| | | log.info("开仓响应:{}",JSON.parseObject(placeOrderRspOkxRestResponse).get("data")); |
| | | /** |
| | | * todo 下单之后的日志处理 |
| | | */ |
| | | return placeOrderRspOkxRestResponse; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.trade.impl; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeEventEnum; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeService; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | |
| | | @Slf4j |
| | | @RequiredArgsConstructor |
| | | public class TradeServiceClosePosition implements TradeService { |
| | | @Override |
| | | public String tradeEvent() { |
| | | return TradeEventEnum.TRADE_CLOSE_POSITION.getEventPoint(); |
| | | } |
| | | |
| | | @Override |
| | | public String doTrade(LinkedHashMap<String, Object> tradeDto, OKXAccount okxAccount) { |
| | | System.out.println(this.tradeEvent()); |
| | | return null; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.trade.impl; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.HttpMethod; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.OKXContants; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.ParameterChecker; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeEventEnum; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.trade.TradeService; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | |
| | | @Slf4j |
| | | @RequiredArgsConstructor |
| | | public class TradeServiceSell implements TradeService { |
| | | @Override |
| | | public String tradeEvent() { |
| | | return TradeEventEnum.TRADE_SELL.getEventPoint(); |
| | | } |
| | | |
| | | @Override |
| | | public String doTrade(LinkedHashMap<String, Object> tradeDto, OKXAccount okxAccount) { |
| | | ParameterChecker.checkParameter(tradeDto, "instId", String.class); |
| | | ParameterChecker.checkParameter(tradeDto, "tdMode", String.class); |
| | | ParameterChecker.checkParameter(tradeDto, "side", String.class); |
| | | ParameterChecker.checkParameter(tradeDto, "ordType", String.class); |
| | | ParameterChecker.checkParameter(tradeDto, "sz", Double.class); |
| | | String placeOrderRspOkxRestResponse = okxAccount.requestHandler.sendSignedRequest(okxAccount.baseUrl, OKXContants.ORDER, tradeDto, HttpMethod.POST, okxAccount.isSimluate()); |
| | | log.info("平仓响应:{}", JSON.parseObject(placeOrderRspOkxRestResponse).get("data")); |
| | | /** |
| | | * todo 下单之后的日志处理 |
| | | */ |
| | | |
| | | return placeOrderRspOkxRestResponse; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.trade.vo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.math.BigDecimal; |
| | | |
| | | /** |
| | | * @author wzy |
| | | * @date 2021-09-16 |
| | | **/ |
| | | @Data |
| | | public class SellOrderVo implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | private String ordId; //交易所订单号 |
| | | |
| | | private String tradeNumber; //本地订单号 |
| | | |
| | | private BigDecimal quantity;//数量 |
| | | |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsResponse; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Account; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.QuantApiMessage; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto.*; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.vo.ApiAccountHoldVo; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.vo.ApiPositionsInfoVo; |
| | | |
| | | //检查账户信息是否合格 欧易调用的接口是交易账户/REST API/查看账户余额 |
| | | public interface IVerifyAccountService { |
| | | |
| | | /** |
| | | * 初始化api信息 |
| | | * @return |
| | | */ |
| | | OKXAccount initAccount(QuantApiMessage quantApiMessage); |
| | | /** |
| | | * 初始化api信息 |
| | | * @param apiKey |
| | | * @param secretKey |
| | | * @param accountType |
| | | * @return |
| | | */ |
| | | Account initAccountV2(String apiKey, String secretKey, String passPhrass, boolean accountType); |
| | | |
| | | FebsResponse verifyAccount(ApiMessageDto apiMessageDto); |
| | | |
| | | /** |
| | | * 获取产品信息 |
| | | * @param apiValidApiMessageDto |
| | | */ |
| | | FebsResponse getProductMess(ApiValidApiMessageDto apiValidApiMessageDto); |
| | | |
| | | /** |
| | | * 获取当前价格 |
| | | * @param operateCurrencyDto |
| | | */ |
| | | FebsResponse getCurrenPrice(OperateCurrencyDto operateCurrencyDto); |
| | | |
| | | /** |
| | | * 获取杠杆列表 |
| | | * @param operateCurrencyLeverDto |
| | | */ |
| | | FebsResponse getlever(OperateCurrencyLeverDto operateCurrencyLeverDto); |
| | | |
| | | /** |
| | | * 设置币种杠杆 |
| | | * @param operateCurrencyLeverDto |
| | | */ |
| | | FebsResponse setLever(OperateCurrencyLeverDto operateCurrencyLeverDto); |
| | | |
| | | /** |
| | | * 获取持仓信息 |
| | | * @param quantApiMessage |
| | | */ |
| | | FebsResponse testApiLink(QuantApiMessage quantApiMessage); |
| | | |
| | | /** |
| | | * 获取账户总权益 |
| | | * @param apiAccountBalanceDto |
| | | */ |
| | | void getAccountBalance(ApiAccountBalanceDto apiAccountBalanceDto); |
| | | |
| | | /** |
| | | * 获取持仓概况 |
| | | * @param apiAccountBalanceInfoDto |
| | | */ |
| | | ApiAccountHoldVo getAccountBalanceInfo(ApiAccountBalanceInfoDto apiAccountBalanceInfoDto); |
| | | |
| | | ApiPositionsInfoVo getAccountPositionsInfo(ApiPositionsInfoDto apiPositionsInfoDto); |
| | | |
| | | /** |
| | | * 获取交易大数据 |
| | | * @return |
| | | */ |
| | | FebsResponse getTradeData(ApiValidApiMessageDto apiValidApiMessageDto); |
| | | |
| | | /** |
| | | * 获取买入卖出情况 |
| | | * @return |
| | | */ |
| | | FebsResponse getBuySellSituation(ApiValidApiMessageDto apiValidApiMessageDto); |
| | | |
| | | /** |
| | | * 获取持仓人数比 |
| | | * @return |
| | | */ |
| | | FebsResponse getPositionRatio(ApiValidApiMessageDto apiValidApiMessageDto); |
| | | |
| | | /** |
| | | * 获取合约持仓量及交易量 |
| | | * @return |
| | | */ |
| | | FebsResponse getPositionTradingvolume(ApiValidApiMessageDto apiValidApiMessageDto); |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 账号验证工厂类 |
| | | * 该类用于管理不同交易所的账号验证服务通过传入的键获取相应的验证服务 |
| | | */ |
| | | @Component |
| | | @Data |
| | | public class VerifyAccountFactory { |
| | | |
| | | /** |
| | | * OKX交易所的账号验证服务 |
| | | */ |
| | | @Qualifier("oKXVerifyAccount") |
| | | private final IVerifyAccountService oKXVerifyAccount; |
| | | |
| | | /** |
| | | * 存储不同交易所的账号验证服务的映射 |
| | | */ |
| | | private Map<String, IVerifyAccountService> accountMap = new HashMap<>(); |
| | | |
| | | /** |
| | | * 构造方法,初始化账号验证工厂 |
| | | * @param oKXVerifyAccount OKX交易所的账号验证服务 |
| | | */ |
| | | public VerifyAccountFactory(IVerifyAccountService oKXVerifyAccount) { |
| | | this.oKXVerifyAccount = oKXVerifyAccount; |
| | | accountMap.put("OKX", oKXVerifyAccount); |
| | | } |
| | | |
| | | /** |
| | | * 根据传入的键获取相应的账号验证服务 |
| | | * @param key 交易所的键,如"OKX"或"BINANCE" |
| | | * @return 对应的账号验证服务 |
| | | */ |
| | | public IVerifyAccountService get(String key) { |
| | | return accountMap.get(key); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import javax.validation.constraints.NotBlank; |
| | | |
| | | @Data |
| | | @ApiModel(value = "ApiAccountBalDto", description = "参数接收类") |
| | | public class ApiAccountBalDto { |
| | | |
| | | @NotBlank(message = "请选择时间范围") |
| | | @ApiModelProperty(value = "开始时间", example = "") |
| | | private String startTime;//开始时间 |
| | | |
| | | @NotBlank(message = "请选择时间范围") |
| | | @ApiModelProperty(value = "结束时间", example = "") |
| | | private String endTime;//结束时间 |
| | | |
| | | @ApiModelProperty(value = "交易所类型", example = "1") |
| | | private String exchange; |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import javax.validation.constraints.NotNull; |
| | | |
| | | @Data |
| | | @ApiModel(value = "ApiAccountBalanceDto", description = "参数接收类") |
| | | public class ApiAccountBalanceDto { |
| | | |
| | | @ApiModelProperty(value = "apiMessageId", example = "11") |
| | | private Long id; |
| | | |
| | | @NotNull(message = "账户类型不能为空") |
| | | @ApiModelProperty(value = "账户类型:实盘账户-true, 模拟账户-false", example = "false") |
| | | private boolean accountType; |
| | | |
| | | @ApiModelProperty(value = "币种名称", example = "BTC") |
| | | private String coinName; |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | @ApiModel(value = "ApiAccountBalanceInfoDto", description = "参数接收类") |
| | | public class ApiAccountBalanceInfoDto { |
| | | |
| | | @ApiModelProperty(value = "apiMessageId", example = "11") |
| | | private Long id; |
| | | |
| | | @ApiModelProperty(value = "币种名称", example = "BTC") |
| | | private String coinName; |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | @ApiModel(value = "ApiAccountBalanceInfoDto", description = "参数接收类") |
| | | public class ApiAccountProfitDailyDto { |
| | | |
| | | @ApiModelProperty(value = "交易所类型", example = "1") |
| | | private String exchange; |
| | | |
| | | @ApiModelProperty(value = "开始时间", example = "") |
| | | private String startTime;//开始时间 |
| | | |
| | | @ApiModelProperty(hidden = true) |
| | | private String endTime;//结束时间 |
| | | |
| | | @ApiModelProperty(hidden = true) |
| | | private Long memberId; |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto; |
| | | |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import javax.validation.constraints.NotBlank; |
| | | import javax.validation.constraints.NotNull; |
| | | |
| | | /** |
| | | * @author lyq |
| | | * @date 2024-09-16 |
| | | **/ |
| | | @Data |
| | | public class ApiMessageDto { |
| | | |
| | | @ApiModelProperty(value = "id") |
| | | private Long id; |
| | | |
| | | @ApiModelProperty(value = "会员id", example = "11") |
| | | private Long memberId; |
| | | |
| | | @NotBlank(message = "交易所不能为空") |
| | | @ApiModelProperty(value = "交易所", example = "jys") |
| | | private String exchange; |
| | | |
| | | @NotBlank(message = "a秘钥不能为空") |
| | | @ApiModelProperty(value = "a秘钥", example = "123456") |
| | | private String aSecretkey; |
| | | |
| | | @NotBlank(message = "b秘钥不能为空") |
| | | @ApiModelProperty(value = "b秘钥", example = "123456") |
| | | private String bSecretkey; |
| | | |
| | | @NotBlank(message = "passPhrass不能为空") |
| | | @ApiModelProperty(value = "passPhrass", example = "123456") |
| | | private String passPhrass; |
| | | |
| | | @NotNull(message = "账户类型不能为空") |
| | | @ApiModelProperty(value = "账户类型:实盘账户-true, 模拟账户-false", example = "false") |
| | | private String accountType; |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | @ApiModel(value = "ApiAccountBalanceInfoDto", description = "参数接收类") |
| | | public class ApiPositionsInfoDto { |
| | | |
| | | @ApiModelProperty(value = "apiMessageId", example = "11") |
| | | private Long id; |
| | | |
| | | @ApiModelProperty(value = "币种名称", example = "BTC") |
| | | private String coinName; |
| | | |
| | | @ApiModelProperty(value = "交易所名称", example = "OKX") |
| | | private String exchange; |
| | | |
| | | @ApiModelProperty(value = "持仓ID", example = "") |
| | | private String posId; |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import javax.validation.constraints.NotNull; |
| | | |
| | | @Data |
| | | @ApiModel(value = "ApiValidApiMessageDto", description = "API信息参数接收类") |
| | | public class ApiValidApiMessageDto { |
| | | |
| | | @NotNull(message = "apiMessageId") |
| | | @ApiModelProperty(value = "apiMessageId", example = "11") |
| | | private Long id; |
| | | |
| | | @NotNull(message = "账户类型不能为空") |
| | | @ApiModelProperty(value = "账户类型:实盘账户-true, 模拟账户-false", example = "false") |
| | | private boolean accountType; |
| | | |
| | | @ApiModelProperty(value = "币种名称", example = "BTC") |
| | | private String coinName; |
| | | |
| | | @ApiModelProperty(value = "交易所", example = "OKX") |
| | | private String exchange; |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import javax.validation.constraints.NotBlank; |
| | | |
| | | /** |
| | | * @author wzy |
| | | * @date 2024-08-10 |
| | | **/ |
| | | @Data |
| | | @ApiModel(value = "OperateCurrencyDto", description = "登录接口参数接收类") |
| | | public class OperateCurrencyDto { |
| | | |
| | | @ApiModelProperty(value = "id") |
| | | private Long id; |
| | | |
| | | @ApiModelProperty(value = "交易所ID", example = "") |
| | | private Long apiMessageId;//交易所ID |
| | | |
| | | @ApiModelProperty(value = "交易所名称", example = "") |
| | | private String exchange;//交易所名称 |
| | | |
| | | // @NotBlank(message = "币种标识不能为空") |
| | | // @ApiModelProperty(value = "币种标识", example = "DOGE") |
| | | // private String coinSymbol;//货币对(交易货币(币种标识)) |
| | | |
| | | @NotBlank(message = "币种标识不能为空") |
| | | @ApiModelProperty(value = "币种标识", example = "DOGE") |
| | | private String coinName;//货币货币名称(交易货币(币种标识)) |
| | | |
| | | @NotBlank(message = "计价货币不能为空") |
| | | @ApiModelProperty(value = "计价货币", example = "USDTU") |
| | | private Integer coinType;//计价货币 1-USDTU本位 2-USDT币本位 |
| | | |
| | | @NotBlank(message = "方向不能为空") |
| | | @ApiModelProperty(value = "方向", example = "1") |
| | | private Integer coinDirect;//方向 1-多 2-空 |
| | | |
| | | @ApiModelProperty(value = "策略方向", example = "1") |
| | | private Integer directStatus;//策略方向 1-顺势 2-逆势 |
| | | |
| | | @ApiModelProperty(value = "货币对", example = "-SWAP") |
| | | private String coinSymbol;//货币对(交易货币(币种标识)) |
| | | |
| | | @ApiModelProperty(value = "用户id", example = "1") |
| | | private Long MemberId;//用户ID |
| | | |
| | | @ApiModelProperty(value = "账户类型BTC-USD:实盘账户-true, 模拟账户-false", example = "false") |
| | | private boolean accountType; |
| | | |
| | | @ApiModelProperty(value = "是否开启监控 0-否 1-是", example = "0") |
| | | private Integer monitorStatus;//是否开启监控 0-未开启 1-已开启 |
| | | |
| | | @ApiModelProperty(value = "杠杆倍率", example = "20") |
| | | private String lever;//杠杆倍率 |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * @author wzy |
| | | * @date 2024-08-10 |
| | | **/ |
| | | @Data |
| | | @ApiModel(value = "OperateCurrencyLeverDto", description = "倍率查询接收类") |
| | | public class OperateCurrencyLeverDto { |
| | | |
| | | @ApiModelProperty(value = "apiMessageId", example = "1") |
| | | private Long apiMessageId;//交易所ID |
| | | |
| | | @ApiModelProperty(value = "产品ID", example = "BTC-USDT") |
| | | private String instId;//交易所名称 |
| | | |
| | | @ApiModelProperty(value = "杠杆倍率", example = "20") |
| | | private String lever;//杠杆倍率 |
| | | |
| | | @ApiModelProperty(value = "交易所名称", example = "OKX") |
| | | private String exchange;//交易所名称 |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | @Data |
| | | @ApiModel(value = "TradeBigdataDto", description = "交易大数据数据接收类") |
| | | public class TradeBigdataDto implements Serializable { |
| | | private static final long serialVersionUID = 1L; |
| | | @ApiModelProperty(value = "数据产生时间", example = "2024-09-18 14:38:01") |
| | | private String ts;//数据产生时间 |
| | | |
| | | @ApiModelProperty(value = "卖出量", example = "1000") |
| | | private String sellVol;//卖出量 |
| | | |
| | | @ApiModelProperty(value = "买入量", example = "1000") |
| | | private String buyVol;//买入量 |
| | | |
| | | @ApiModelProperty(value = "多空人数比", example = "0.8") |
| | | private String longShortAcctRatio;//多空人数比 |
| | | |
| | | @ApiModelProperty(value = "持仓总量", example = "1000") |
| | | private String oi;//持仓总量 |
| | | |
| | | @ApiModelProperty(value = "交易总量", example = "1000") |
| | | private String vol;//交易总量 |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.enums; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | public class PublicStatusEnum { |
| | | |
| | | /** |
| | | * API验证返回状态 |
| | | */ |
| | | public static final int API_MESSAGE_STATUS_SUCCESS = 1; |
| | | public static final int API_MESSAGE_STATUS_FAIL = 0; |
| | | |
| | | /** |
| | | * 货币对监控状态 |
| | | */ |
| | | //货币对监控状态开启 |
| | | public static final int CURRENCY_MONITOR_OPEN = 1; |
| | | |
| | | //货币对监控状态开启 |
| | | public static final int CURRENCY_ONLYSELL_OPEN = 1; |
| | | //货币对监控状态关闭 |
| | | |
| | | public static final int CURRENCY_MONITOR_CLOSE = 0; |
| | | //是否开启过监听 |
| | | public static final int CURRENCY_MONITORED_OPEN = 1; |
| | | //订单未完成 |
| | | public static final int ORDER_FINISH_STATUS_NO = 1; |
| | | //订单已完成 |
| | | public static final int ORDER_FINISH_STATUS_YES = 2; |
| | | //订单取消 |
| | | public static final int ORDER_FINISH_STATUS_CANEL = 3; |
| | | //订单已卖出(针对买单) |
| | | public static final int ORDER_FINISH_STATUS_SELL = 4; |
| | | //订单下单失败 |
| | | public static final int ORDER_FINISH_STATUS_FAIL = 5; |
| | | |
| | | //其他方式卖出 |
| | | public static final int ORDER_FINISH_STATUS_OTHERSELL = 6; |
| | | |
| | | //买单 |
| | | public static final int ORDER_STATUS_BUY = 1; |
| | | |
| | | public static final BigDecimal ORDER_DOWN_RATIO= new BigDecimal("0.01"); |
| | | //买单 |
| | | |
| | | public static final int ORDER_STATUS_SELL = 2; |
| | | |
| | | // public static final boolean AccountType = false; |
| | | |
| | | public static final int ORDER_FINISH_STATUS = 2; |
| | | |
| | | //订单卖出未锁定 |
| | | public static final int ORDER_SELL_LOCK_STATUS_NO = 0; |
| | | //订单卖出已锁定 |
| | | public static final int ORDER_SELL_LOCK_STATUS_YES = 1; |
| | | |
| | | //定义超过开仓均价时需向下调整卖价的比率 |
| | | public static final BigDecimal HIGH_SELL_RATIO= new BigDecimal("1.1"); |
| | | |
| | | //定义需要调整后的比率 |
| | | public static final BigDecimal HIGH_ADJUST_RATIO= new BigDecimal("1.03"); |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.impl; |
| | | |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsException; |
| | | import com.xcong.excoin.modules.okxNewPrice.utils.FebsResponse; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.DataUtil; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.QuantApiMessage; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.OKXAccount; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.DefaultUrls; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.enums.HttpMethod; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.utils.OKXContants; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.IVerifyAccountService; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.dto.*; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.enums.PublicStatusEnum; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.vo.ApiAccountHoldVo; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.vo.ApiPositionsInfoVo; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.vo.ProductMessVo; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.verify.vo.SinglemarketVo; |
| | | import com.xcong.excoin.modules.okxNewPrice.zhanghu.IApiMessageService; |
| | | import com.xcong.excoin.modules.okxNewPrice.zhanghu.ZhangHuEnum; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.SneakyThrows; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.io.BufferedReader; |
| | | import java.io.InputStreamReader; |
| | | import java.math.BigDecimal; |
| | | import java.net.HttpURLConnection; |
| | | import java.net.URL; |
| | | import java.util.*; |
| | | |
| | | @Slf4j |
| | | @Service("oKXVerifyAccount") |
| | | @RequiredArgsConstructor |
| | | public class OKXVerifyAccountServiceImpl implements IVerifyAccountService { |
| | | |
| | | private final IApiMessageService apiMessageService; |
| | | @Value("${spring.OKEX.baseurl}") |
| | | private String baseurl; |
| | | |
| | | @Override |
| | | public OKXAccount initAccount(QuantApiMessage quantApiMessage) { |
| | | return new OKXAccount( |
| | | quantApiMessage.getAccountType().equalsIgnoreCase("true") ? DefaultUrls.USDM_PROD_URL : DefaultUrls.USDM_UAT_URL, |
| | | quantApiMessage.getASecretkey(), |
| | | quantApiMessage.getBSecretkey(), |
| | | quantApiMessage.getPassPhrass(), |
| | | !quantApiMessage.getAccountType().equalsIgnoreCase("true")); |
| | | |
| | | } |
| | | |
| | | @Override |
| | | public OKXAccount initAccountV2(String apiKey, String secretKey, String passPhrass, boolean accountType) { |
| | | return new OKXAccount( |
| | | accountType ? DefaultUrls.USDM_PROD_URL : DefaultUrls.USDM_UAT_URL, |
| | | apiKey, |
| | | secretKey, |
| | | passPhrass, |
| | | !accountType); |
| | | } |
| | | |
| | | @SneakyThrows |
| | | @Override |
| | | public FebsResponse verifyAccount(ApiMessageDto apiMessageDto) { |
| | | if(validateAccount(apiMessageDto)){ |
| | | return new FebsResponse().success(); |
| | | } |
| | | return new FebsResponse().fail(); |
| | | } |
| | | |
| | | @Override |
| | | public FebsResponse getProductMess(ApiValidApiMessageDto apiValidApiMessageDto) { |
| | | Long id = apiValidApiMessageDto.getId(); |
| | | |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage(ZhangHuEnum.JIAOYISUO.getValue()); |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | throw new FebsException("参数错误"); |
| | | } |
| | | |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | LinkedHashMap<String, Object> balanceParameters = new LinkedHashMap<>(); |
| | | balanceParameters.put("instType", "SWAP"); |
| | | if(StringUtils.isNotBlank(apiValidApiMessageDto.getCoinName())){ |
| | | balanceParameters.put("instId", apiValidApiMessageDto.getCoinName()+"-USD-SWAP"); |
| | | balanceParameters.put("instFamily", apiValidApiMessageDto.getCoinName()+"-USD"); |
| | | } |
| | | |
| | | String productStr = okxAccount.requestHandler.sendSignedRequest(okxAccount.baseUrl, OKXContants.INSTRUMENTS, balanceParameters, HttpMethod.GET, okxAccount.isSimluate()); |
| | | log.info("productStr:{}", productStr); |
| | | JSONObject balanceJson = JSON.parseObject(productStr); |
| | | JSONObject data = balanceJson.getJSONArray("data").getJSONObject(0); |
| | | |
| | | ProductMessVo productMessVo = new ProductMessVo(); |
| | | productMessVo.setLotSz(data.getString("lotSz")); |
| | | productMessVo.setMinSz(data.getString("minSz")); |
| | | productMessVo.setTickSz(data.getString("tickSz")); |
| | | productMessVo.setCtVal(data.getString("ctVal")); |
| | | |
| | | return new FebsResponse().success().data(productMessVo); |
| | | } |
| | | |
| | | @Override |
| | | public FebsResponse getCurrenPrice(OperateCurrencyDto operateCurrencyDto) { |
| | | |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage(ZhangHuEnum.JIAOYISUO.getValue()); |
| | | if(null == quantApiMessage){ |
| | | return new FebsResponse().fail().message("请先配置交易所"); |
| | | } |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | |
| | | LinkedHashMap<String, Object> balanceParameters = new LinkedHashMap<>(); |
| | | if(StringUtils.isNotBlank(operateCurrencyDto.getCoinSymbol())){ |
| | | balanceParameters.put("instId", operateCurrencyDto.getCoinSymbol()+"-SWAP"); |
| | | } |
| | | String ticker = okxAccount.requestHandler.sendSignedRequest(okxAccount.baseUrl, OKXContants.TICKER, balanceParameters, HttpMethod.GET, okxAccount.isSimluate()); |
| | | |
| | | log.info("ticker================:{}", ticker); |
| | | |
| | | JSONObject jsonObject = JSON.parseObject(ticker); |
| | | JSONObject data = jsonObject.getJSONArray("data").getJSONObject(0); |
| | | |
| | | SinglemarketVo singlemarketVo = new SinglemarketVo(); |
| | | singlemarketVo.setInstId(data.getString("instId")); |
| | | singlemarketVo.setLast(data.getString("last")); |
| | | singlemarketVo.setInstType(data.getString("instType")); |
| | | |
| | | return new FebsResponse().success().data(singlemarketVo); |
| | | } |
| | | |
| | | @Override |
| | | public FebsResponse getlever(OperateCurrencyLeverDto operateCurrencyLeverDto) { |
| | | |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage(ZhangHuEnum.JIAOYISUO.getValue()); |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | throw new FebsException("参数错误"); |
| | | } |
| | | |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | LinkedHashMap<String, Object> balanceParameters = new LinkedHashMap<>(); |
| | | balanceParameters.put("instId", operateCurrencyLeverDto.getInstId()); |
| | | balanceParameters.put("mgnMode", "cross"); |
| | | String leverStr = okxAccount.requestHandler.sendSignedRequest(okxAccount.baseUrl, OKXContants.LEVERAGE, balanceParameters, HttpMethod.GET, okxAccount.isSimluate()); |
| | | log.info("leverStr:{}", leverStr); |
| | | JSONObject leverStrJson = JSON.parseObject(leverStr); |
| | | |
| | | Set<String> leverage = new HashSet<>(); |
| | | |
| | | JSONArray details = leverStrJson.getJSONArray("data"); |
| | | int size = details.size(); |
| | | for (int i = 0; i < size; i++) { |
| | | leverage.add(details.getJSONObject(i).getString("lever")); |
| | | } |
| | | |
| | | |
| | | log.info("leverage:{}", leverage); |
| | | |
| | | return new FebsResponse().success().data(leverage); |
| | | } |
| | | |
| | | @Override |
| | | public FebsResponse setLever(OperateCurrencyLeverDto operateCurrencyLeverDto) { |
| | | |
| | | log.info("OKX_SET_LEVEL"); |
| | | log.info("operateCurrencyLeverDto:{}", operateCurrencyLeverDto); |
| | | |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage(ZhangHuEnum.JIAOYISUO.getValue()); |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | throw new FebsException("参数错误"); |
| | | } |
| | | |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | LinkedHashMap<String, Object> balanceParameters = new LinkedHashMap<>(); |
| | | balanceParameters.put("instId", operateCurrencyLeverDto.getInstId()+"-SWAP"); |
| | | balanceParameters.put("mgnMode", "cross"); |
| | | balanceParameters.put("lever", operateCurrencyLeverDto.getLever()); |
| | | String setleverStr = okxAccount.requestHandler.sendSignedRequest(okxAccount.baseUrl, OKXContants.SETLEVERAGE, balanceParameters, HttpMethod.POST, okxAccount.isSimluate()); |
| | | log.info("setleverStr:{}", setleverStr); |
| | | JSONObject leverStrJson = JSON.parseObject(setleverStr); |
| | | |
| | | String code = leverStrJson.getString("code"); |
| | | if("0".equals(code)||"59000".equals(code)){ |
| | | if("59000".equals(code)){ |
| | | return new FebsResponse().success().message("59000"); |
| | | } |
| | | return new FebsResponse().success().message("设置杠杆成功"); |
| | | } else { |
| | | return new FebsResponse().fail().message("设置杠杆失败"); |
| | | } |
| | | |
| | | } |
| | | |
| | | /** |
| | | * 测试API链接 |
| | | * |
| | | * @param quantApiMessage 包含API链接信息的DTO对象 |
| | | * @return 返回一个FebsResponse对象,包含验证结果和消息 |
| | | */ |
| | | @Override |
| | | public FebsResponse testApiLink(QuantApiMessage quantApiMessage) { |
| | | log.info("apiMessageDto:{}",quantApiMessage); |
| | | try{ |
| | | // 初始化账户对象 |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | |
| | | // 创建用于存储余额查询参数的容器 |
| | | LinkedHashMap<String, Object> balanceParameters = new LinkedHashMap<>(); |
| | | log.info("okxAccount:{}",okxAccount); |
| | | |
| | | // 发送签名请求以获取账户余额 |
| | | String balance = okxAccount.requestHandler.sendSignedRequest(okxAccount.baseUrl, OKXContants.BALANCE, balanceParameters, HttpMethod.GET, okxAccount.isSimluate()); |
| | | log.info("balance:{}",balance); |
| | | |
| | | // 解析余额响应JSON |
| | | JSONObject balanceJson = JSON.parseObject(balance); |
| | | |
| | | // 检查余额响应代码,如果为0则表示成功 |
| | | if("0".equals(balanceJson.getString("code").toString())){ |
| | | // 更新QuantApiMessage状态为成功 |
| | | HashMap<String, Integer> objectObjectHashMap = new HashMap<>(); |
| | | objectObjectHashMap.put("state", PublicStatusEnum.API_MESSAGE_STATUS_SUCCESS); |
| | | return new FebsResponse().success().data(objectObjectHashMap).message("账号验证成功"); |
| | | } |
| | | |
| | | // 返回验证失败的消息 |
| | | return new FebsResponse().fail().message("账号验证失败"); |
| | | } catch (Exception e){ |
| | | // 异常情况下返回验证失败的消息 |
| | | return new FebsResponse().fail().message("账号验证失败"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取账户余额的方法 |
| | | * |
| | | * @param apiAccountBalanceDto 包含账户余额查询信息的数据传输对象 |
| | | * @throws FebsException 当API消息信息未设置时抛出异常 |
| | | */ |
| | | @Override |
| | | public void getAccountBalance(ApiAccountBalanceDto apiAccountBalanceDto) { |
| | | // 提取API消息ID、账户类型和币种名称 |
| | | String coinName = apiAccountBalanceDto.getCoinName(); |
| | | |
| | | // 根据ID查询API消息信息 |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage(ZhangHuEnum.JIAOYISUO.getValue()); |
| | | // 检查API消息信息是否存在 |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | throw new FebsException("API_MESSAGE信息未设置"); |
| | | } |
| | | |
| | | // 初始化账户对象 |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | |
| | | // 构建查询余额的参数 |
| | | LinkedHashMap<String, Object> balanceParameters = new LinkedHashMap<>(); |
| | | balanceParameters.put("ccy", coinName); |
| | | // 发送签名请求获取余额信息 |
| | | String balance = okxAccount.requestHandler.sendSignedRequest( |
| | | okxAccount.baseUrl, |
| | | OKXContants.BALANCE, |
| | | balanceParameters, |
| | | HttpMethod.GET, |
| | | okxAccount.isSimluate()); |
| | | // 记录余额信息日志 |
| | | log.info("ACCOUNT_BALANCE:{}", balance); |
| | | JSONObject balanceJson = JSON.parseObject(balance); |
| | | JSONObject data = balanceJson.getJSONArray("data").getJSONObject(0); |
| | | JSONArray details = data.getJSONArray("details"); |
| | | for (int i = 0; i < details.size(); i++) { |
| | | JSONObject jsonObject = details.getJSONObject(i); |
| | | // 总权益 |
| | | String bal = jsonObject.getString("eq"); |
| | | // String upl = jsonObject.getString("upl"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public ApiAccountHoldVo getAccountBalanceInfo(ApiAccountBalanceInfoDto apiAccountBalanceInfoDto) { |
| | | ApiAccountHoldVo apiAccountHoldVo = new ApiAccountHoldVo(); |
| | | |
| | | // 提取API消息ID、账户类型和币种名称 |
| | | Long apiMessageId = apiAccountBalanceInfoDto.getId(); |
| | | String coinName = apiAccountBalanceInfoDto.getCoinName(); |
| | | |
| | | // 根据ID查询API消息信息 |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage( |
| | | ZhangHuEnum.JIAOYISUO.getValue() |
| | | ); |
| | | // 检查API消息信息是否存在 |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | throw new FebsException("API_MESSAGE信息未设置"); |
| | | } |
| | | |
| | | // 初始化账户对象 |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | |
| | | // 构建查询余额的参数 |
| | | LinkedHashMap<String, Object> balanceParameters = new LinkedHashMap<>(); |
| | | balanceParameters.put("ccy", coinName); |
| | | // 发送签名请求获取余额信息 |
| | | String balance = okxAccount.requestHandler.sendSignedRequest( |
| | | okxAccount.baseUrl, |
| | | OKXContants.BALANCE, |
| | | balanceParameters, |
| | | HttpMethod.GET, |
| | | okxAccount.isSimluate()); |
| | | // 记录余额信息日志 |
| | | log.info("ACCOUNT_BALANCE:{}", balance); |
| | | JSONObject balanceJson = JSON.parseObject(balance); |
| | | JSONObject data = balanceJson.getJSONArray("data").getJSONObject(0); |
| | | JSONArray details = data.getJSONArray("details"); |
| | | for (int i = 0; i < details.size(); i++) { |
| | | JSONObject jsonObject = details.getJSONObject(i); |
| | | log.info("OKX-HOLD:",jsonObject); |
| | | // 总权益 |
| | | String bal = jsonObject.getString("eq");//币种总权益 |
| | | String cashBal = jsonObject.getString("cashBal");//币种余额 |
| | | String availEq = jsonObject.getString("availEq");//可用保证金 |
| | | String availBal = jsonObject.getString("availBal");//可用余额 |
| | | String frozenBal = jsonObject.getString("frozenBal");//币种占用金额 |
| | | String imr = jsonObject.getString("imr");//币种维度全仓占用保证金,适用于现货和合约模式且有全仓仓位时 |
| | | String mgnRatio = jsonObject.getString("mgnRatio");//币种全仓保证金率,衡量账户内某项资产风险的指标 |
| | | String mmr = jsonObject.getString("mmr");//币种维度全仓维持保证金 |
| | | String upl = jsonObject.getString("upl");//未实现盈亏 |
| | | apiAccountHoldVo.setTotalBalance(new BigDecimal(bal)); |
| | | apiAccountHoldVo.setTotalPercent(new BigDecimal(upl)); |
| | | apiAccountHoldVo.setAvailableBalance(new BigDecimal(availBal)); |
| | | // apiAccountHoldVo.setHoldBalance(new BigDecimal(mmr)); |
| | | apiAccountHoldVo.setHoldBalance(new BigDecimal(mmr)); |
| | | apiAccountHoldVo.setUseBalance(new BigDecimal(mmr)); |
| | | apiAccountHoldVo.setCashBal(new BigDecimal(cashBal)); |
| | | apiAccountHoldVo.setFrozenBal(new BigDecimal(frozenBal)); |
| | | apiAccountHoldVo.setAvailEq(new BigDecimal(availEq)); |
| | | apiAccountHoldVo.setMgnRatio(mgnRatio); |
| | | } |
| | | return apiAccountHoldVo; |
| | | } |
| | | |
| | | @Override |
| | | public ApiPositionsInfoVo getAccountPositionsInfo(ApiPositionsInfoDto apiPositionsInfoDto) { |
| | | |
| | | log.info("getAccountPositionsInfo:{}",apiPositionsInfoDto); |
| | | ApiPositionsInfoVo apiPositionsInfoVo = new ApiPositionsInfoVo(); |
| | | // 提取API消息ID、账户类型和币种名称 |
| | | Long apiMessageId = apiPositionsInfoDto.getId(); |
| | | String coinName = apiPositionsInfoDto.getCoinName(); |
| | | |
| | | // 根据ID查询API消息信息 |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage( |
| | | ZhangHuEnum.JIAOYISUO.getValue() |
| | | ); |
| | | // 检查API消息信息是否存在 |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | throw new FebsException("API_MESSAGE信息未设置"); |
| | | } |
| | | |
| | | // 初始化账户对象 |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | |
| | | // 构建查询余额的参数 |
| | | LinkedHashMap<String, Object> positionsParameters = new LinkedHashMap<>(); |
| | | positionsParameters.put("instType", "SWAP"); |
| | | positionsParameters.put("instId", coinName+"-USDT-SWAP"); |
| | | try{ |
| | | // 发送签名请求获取余额信息 |
| | | String positions = okxAccount.requestHandler.sendSignedRequest( |
| | | okxAccount.baseUrl, |
| | | OKXContants.POSITIONS, |
| | | positionsParameters, |
| | | HttpMethod.GET, |
| | | okxAccount.isSimluate()); |
| | | // 记录余额信息日志 |
| | | log.info("POSITIONS:{}", positions); |
| | | JSONObject balanceJson = JSON.parseObject(positions); |
| | | String code = balanceJson.getString("code"); |
| | | if(!"0".equals(code)){ |
| | | return null; |
| | | } |
| | | JSONObject data = balanceJson.getJSONArray("data").getJSONObject(0); |
| | | String avgPx = data.getString("avgPx"); |
| | | String upl = data.getString("upl"); |
| | | String markPx = data.getString("markPx"); |
| | | String bePx = data.getString("bePx"); |
| | | String pos = data.getString("pos"); |
| | | apiPositionsInfoVo.setUpl(new BigDecimal(ObjectUtil.isNotEmpty(upl)? DataUtil.getDecimalDigits8(upl):"0")); |
| | | apiPositionsInfoVo.setAvgPx(new BigDecimal(ObjectUtil.isNotEmpty(avgPx)?DataUtil.getDecimalDigits8(avgPx):"0")); |
| | | apiPositionsInfoVo.setMarkPx(new BigDecimal(ObjectUtil.isNotEmpty(markPx)?DataUtil.getDecimalDigits8(markPx):"0")); |
| | | apiPositionsInfoVo.setBePx(new BigDecimal(ObjectUtil.isNotEmpty(bePx)?DataUtil.getDecimalDigits8(bePx):"0")); |
| | | apiPositionsInfoVo.setPos(new BigDecimal(ObjectUtil.isNotEmpty(pos)?DataUtil.getDecimalDigits8(pos):"0")); |
| | | apiPositionsInfoVo.setCoinName(coinName); |
| | | return apiPositionsInfoVo; |
| | | }catch (Exception e){ |
| | | return null; |
| | | } |
| | | |
| | | } |
| | | |
| | | @Override |
| | | public FebsResponse getTradeData(ApiValidApiMessageDto apiValidApiMessageDto) { |
| | | |
| | | log.info("getTradeData:{}", apiValidApiMessageDto); |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage( |
| | | ZhangHuEnum.JIAOYISUO.getValue() |
| | | ); |
| | | // 检查API消息信息是否存在 |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | throw new FebsException("API_MESSAGE信息未设置"); |
| | | } |
| | | |
| | | // 初始化账户对象 |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | |
| | | // 构建查询余额的参数 |
| | | LinkedHashMap<String, Object> positionsParameters = new LinkedHashMap<>(); |
| | | try{ |
| | | // 发送签名请求获取余额信息 |
| | | String tradedata = okxAccount.requestHandler.sendSignedRequest( |
| | | okxAccount.baseUrl, |
| | | OKXContants.TRADEDATA, |
| | | positionsParameters, |
| | | HttpMethod.GET, |
| | | okxAccount.isSimluate()); |
| | | // 记录余额信息日志 |
| | | log.info("tradedata:{}", tradedata); |
| | | JSONObject balanceJson = JSON.parseObject(tradedata); |
| | | String code = balanceJson.getString("code"); |
| | | if(!"0".equals(code)){ |
| | | return null; |
| | | } |
| | | JSONObject dataObject = balanceJson.getJSONObject("data"); |
| | | JSONArray contractArray = dataObject.getJSONArray("contract"); |
| | | List<String> contractList = new ArrayList<>(); |
| | | if (contractArray != null) { |
| | | for (int i = 0; i < contractArray.size(); i++) { |
| | | contractList.add(contractArray.getString(i)); |
| | | } |
| | | } |
| | | return new FebsResponse().data(contractList); |
| | | }catch (Exception e){ |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public FebsResponse getBuySellSituation(ApiValidApiMessageDto apiValidApiMessageDto) { |
| | | log.info("getBuySellSituation:{}", apiValidApiMessageDto); |
| | | |
| | | String coinName = apiValidApiMessageDto.getCoinName();// String coinName = "BTC"; |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage( |
| | | ZhangHuEnum.JIAOYISUO.getValue() |
| | | ); |
| | | // 检查API消息信息是否存在 |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | throw new FebsException("API_MESSAGE信息未设置"); |
| | | } |
| | | |
| | | // 初始化账户对象 |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | |
| | | // 构建查询余额的参数 |
| | | LinkedHashMap<String, Object> positionsParameters = new LinkedHashMap<>(); |
| | | positionsParameters.put("period", "4H"); |
| | | positionsParameters.put("instId", coinName+"-USDT-SWAP"); |
| | | positionsParameters.put("limit", "1"); |
| | | try{ |
| | | // 发送签名请求获取余额信息 |
| | | String buySellSituation = okxAccount.requestHandler.sendSignedRequest( |
| | | okxAccount.baseUrl, |
| | | OKXContants.BUYSELLSITUATION, |
| | | positionsParameters, |
| | | HttpMethod.GET, |
| | | okxAccount.isSimluate()); |
| | | // 记录余额信息日志 |
| | | log.info("buySellSituation:{}", buySellSituation); |
| | | JSONObject balanceJson = JSON.parseObject(buySellSituation); |
| | | String code = balanceJson.getString("code"); |
| | | if(!"0".equals(code)){ |
| | | return null; |
| | | } |
| | | JSONArray dataArray = balanceJson.getJSONArray("data"); |
| | | TradeBigdataDto tradeBigdataDto = new TradeBigdataDto(); |
| | | if (dataArray != null) { |
| | | JSONArray innerArray = dataArray.getJSONArray(0); |
| | | String sellVol = innerArray.getString(1); |
| | | String buyVol = innerArray.getString(2); |
| | | String ts = innerArray.getString(0); |
| | | tradeBigdataDto.setBuyVol(buyVol); |
| | | tradeBigdataDto.setSellVol(sellVol); |
| | | tradeBigdataDto.setTs(ts); |
| | | } |
| | | return new FebsResponse().data(tradeBigdataDto); |
| | | }catch (Exception e){ |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public FebsResponse getPositionRatio(ApiValidApiMessageDto apiValidApiMessageDto) { |
| | | log.info("getPositionRatio:{}", apiValidApiMessageDto); |
| | | |
| | | String coinName = apiValidApiMessageDto.getCoinName();// String coinName = "BTC"; |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage( |
| | | ZhangHuEnum.JIAOYISUO.getValue() |
| | | ); |
| | | // 检查API消息信息是否存在 |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | throw new FebsException("API_MESSAGE信息未设置"); |
| | | } |
| | | |
| | | // 初始化账户对象 |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | |
| | | // 构建查询余额的参数 |
| | | LinkedHashMap<String, Object> positionsParameters = new LinkedHashMap<>(); |
| | | positionsParameters.put("period", "4H"); |
| | | positionsParameters.put("instId", coinName+"-USDT-SWAP"); |
| | | positionsParameters.put("limit", "1"); |
| | | try{ |
| | | // 发送签名请求获取余额信息 |
| | | String positionRatio = okxAccount.requestHandler.sendSignedRequest( |
| | | okxAccount.baseUrl, |
| | | OKXContants.POSITIONRATIO, |
| | | positionsParameters, |
| | | HttpMethod.GET, |
| | | okxAccount.isSimluate()); |
| | | // 记录余额信息日志 |
| | | log.info("positionRatio:{}", positionRatio); |
| | | JSONObject balanceJson = JSON.parseObject(positionRatio); |
| | | String code = balanceJson.getString("code"); |
| | | if(!"0".equals(code)){ |
| | | return null; |
| | | } |
| | | JSONArray dataArray = balanceJson.getJSONArray("data"); |
| | | TradeBigdataDto tradeBigdataDto = new TradeBigdataDto(); |
| | | if (dataArray != null) { |
| | | JSONArray innerArray = dataArray.getJSONArray(0); |
| | | String longShortAcctRatio = innerArray.getString(1); |
| | | String ts = innerArray.getString(0); |
| | | tradeBigdataDto.setLongShortAcctRatio(longShortAcctRatio); |
| | | tradeBigdataDto.setTs(ts); |
| | | } |
| | | return new FebsResponse().data(tradeBigdataDto); |
| | | }catch (Exception e){ |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public FebsResponse getPositionTradingvolume(ApiValidApiMessageDto apiValidApiMessageDto) { |
| | | log.info("getPositionTradingvolume:{}", apiValidApiMessageDto); |
| | | |
| | | String coinName = apiValidApiMessageDto.getCoinName();// String coinName = "BTC"; |
| | | QuantApiMessage quantApiMessage = apiMessageService.getApiMessage( |
| | | ZhangHuEnum.JIAOYISUO.getValue() |
| | | ); |
| | | // 检查API消息信息是否存在 |
| | | if(ObjectUtil.isEmpty(quantApiMessage)){ |
| | | throw new FebsException("API_MESSAGE信息未设置"); |
| | | } |
| | | |
| | | // 初始化账户对象 |
| | | OKXAccount okxAccount = this.initAccount(quantApiMessage); |
| | | |
| | | // 构建查询余额的参数 |
| | | LinkedHashMap<String, Object> positionsParameters = new LinkedHashMap<>(); |
| | | positionsParameters.put("period", "1D"); |
| | | positionsParameters.put("ccy", coinName); |
| | | positionsParameters.put("limit", "1"); |
| | | try{ |
| | | // 发送签名请求获取余额信息 |
| | | String positionTradingvolume = okxAccount.requestHandler.sendSignedRequest( |
| | | okxAccount.baseUrl, |
| | | OKXContants.POSITIONVOLUME, |
| | | positionsParameters, |
| | | HttpMethod.GET, |
| | | okxAccount.isSimluate()); |
| | | // 记录余额信息日志 |
| | | log.info("positionTradingvolume:{}", positionTradingvolume); |
| | | JSONObject balanceJson = JSON.parseObject(positionTradingvolume); |
| | | String code = balanceJson.getString("code"); |
| | | if(!"0".equals(code)){ |
| | | return null; |
| | | } |
| | | JSONArray dataArray = balanceJson.getJSONArray("data"); |
| | | TradeBigdataDto tradeBigdataDto = new TradeBigdataDto(); |
| | | if (dataArray != null) { |
| | | JSONArray innerArray = dataArray.getJSONArray(0); |
| | | String oi = innerArray.getString(1); |
| | | String vol = innerArray.getString(2); |
| | | String ts = innerArray.getString(0); |
| | | tradeBigdataDto.setOi(oi); |
| | | tradeBigdataDto.setVol(vol); |
| | | tradeBigdataDto.setTs(ts); |
| | | } |
| | | return new FebsResponse().data(tradeBigdataDto); |
| | | }catch (Exception e){ |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | //此方法用于验证账号 |
| | | private boolean validateAccount(ApiMessageDto apiMessageDto) throws Exception { |
| | | boolean flag = false; |
| | | // try { |
| | | //设置请求 |
| | | URL url = new URL(baseurl+"/api/v5/account/balance"); |
| | | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); |
| | | |
| | | //设置请求方法 |
| | | connection.setRequestMethod("GET"); |
| | | |
| | | //设置必需的头信息 |
| | | long now = System.currentTimeMillis() / 1000; |
| | | String timestamp = String.valueOf(now); |
| | | String preHash = timestamp + "GET" + "/api/v5/account/balance"; //URL路径 |
| | | String signature = generateSignature(preHash, apiMessageDto.getBSecretkey()); |
| | | |
| | | connection.setRequestProperty("OK-ACCESS-KEY", apiMessageDto.getASecretkey()); |
| | | connection.setRequestProperty("OK-ACCESS-SIGN", signature); |
| | | connection.setRequestProperty("OK-ACCESS-TIMESTAMP", timestamp); |
| | | connection.setRequestProperty("OK-ACCESS-PASSPHRASE", apiMessageDto.getPassPhrass()); |
| | | |
| | | connection.setRequestProperty("Content-Type", "application/json"); |
| | | |
| | | //执行请求 |
| | | int responseCode = connection.getResponseCode(); |
| | | BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); |
| | | String inputLine; |
| | | StringBuffer response = new StringBuffer(); |
| | | |
| | | while ((inputLine = in.readLine()) != null) { |
| | | response.append(inputLine); |
| | | } |
| | | in.close(); |
| | | |
| | | //打印响应 |
| | | if(responseCode == 200){ |
| | | flag = true; |
| | | } else { |
| | | flag = false; |
| | | } |
| | | // } catch (Exception e) { |
| | | // //在此打印异常信息 |
| | | // log.error("Exception: " + e.getMessage()); |
| | | // flag = false; |
| | | // } |
| | | return flag; |
| | | } |
| | | |
| | | //此方法用于生成签名 |
| | | private static String generateSignature(String preHash, String secretKey) throws Exception{ |
| | | javax.crypto.Mac sha256_HMAC = javax.crypto.Mac.getInstance("HmacSHA256"); |
| | | javax.crypto.spec.SecretKeySpec secret_key = new javax.crypto.spec.SecretKeySpec(secretKey.getBytes(), "HmacSHA256"); |
| | | sha256_HMAC.init(secret_key); |
| | | return Base64.getEncoder().encodeToString(sha256_HMAC.doFinal(preHash.getBytes())); |
| | | } |
| | | |
| | | public static void main(String[] args) throws Exception { |
| | | String buySellSituation = "{\"code\":\"0\",\"data\":[[\"1718164800000\",\"52000.2999999999975\",\"60671.0000000000019\"]],\"msg\":\"\"}"; |
| | | log.info("buySellSituation:{}", buySellSituation); |
| | | JSONObject balanceJson = JSON.parseObject(buySellSituation); |
| | | JSONArray dataArray = balanceJson.getJSONArray("data"); |
| | | TradeBigdataDto tradeBigdataDto = new TradeBigdataDto(); |
| | | if (dataArray != null) { |
| | | JSONArray innerArray = dataArray.getJSONArray(0); |
| | | String sellVol = innerArray.getString(1); |
| | | String buyVol = innerArray.getString(2); |
| | | String ts = innerArray.getString(0); |
| | | tradeBigdataDto.setBuyVol(buyVol); |
| | | tradeBigdataDto.setSellVol(sellVol); |
| | | tradeBigdataDto.setTs(ts); |
| | | } |
| | | System.out.println(tradeBigdataDto.toString()); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.vo; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | @Data |
| | | @ApiModel(value = "ApiAccountHoldVo", description = "列表") |
| | | public class ApiAccountHoldVo { |
| | | |
| | | @ApiModelProperty(value = "总金额") |
| | | private BigDecimal totalBalance = BigDecimal.ZERO; |
| | | @ApiModelProperty(value = "盈亏比") |
| | | private BigDecimal totalPercent = BigDecimal.ZERO; |
| | | @ApiModelProperty(value = "可用金额") |
| | | private BigDecimal availableBalance = BigDecimal.ZERO; |
| | | @ApiModelProperty(value = "持仓金额") |
| | | private BigDecimal holdBalance = BigDecimal.ZERO; |
| | | @ApiModelProperty(value = "占用保证金") |
| | | private BigDecimal useBalance = BigDecimal.ZERO; |
| | | |
| | | @ApiModelProperty(value = "币种余额") |
| | | private BigDecimal cashBal = BigDecimal.ZERO; |
| | | |
| | | @ApiModelProperty(value = "可用保证金") |
| | | private BigDecimal availEq = BigDecimal.ZERO; |
| | | |
| | | @ApiModelProperty(value = "币种占用金额") |
| | | private BigDecimal frozenBal = BigDecimal.ZERO; |
| | | |
| | | @ApiModelProperty(value = "币种全仓保证金率") |
| | | private String mgnRatio; |
| | | |
| | | @ApiModelProperty(value = "当前所需起始保证金") |
| | | private BigDecimal initialMargin; |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.vo; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.math.BigDecimal; |
| | | |
| | | @Data |
| | | @ApiModel(value = "ApiPositionsInfoVo", description = "持仓信息") |
| | | public class ApiPositionsInfoVo implements Serializable { |
| | | |
| | | @ApiModelProperty(value = "开仓均价") |
| | | private BigDecimal avgPx = BigDecimal.ZERO; |
| | | @ApiModelProperty(value = "未实现盈亏") |
| | | private BigDecimal upl = BigDecimal.ZERO; |
| | | @ApiModelProperty(value = "盈亏平衡价") |
| | | private BigDecimal bePx = BigDecimal.ZERO; |
| | | @ApiModelProperty(value = "标记价格") |
| | | private BigDecimal markPx = BigDecimal.ZERO; |
| | | @ApiModelProperty(value = "持仓数量") |
| | | private BigDecimal pos = BigDecimal.ZERO; |
| | | @ApiModelProperty(value = "币种") |
| | | private String coinName; |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.vo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public class ProductMessVo { |
| | | // 币种 |
| | | private String symbol; |
| | | |
| | | // 下单价格精度 |
| | | private String tickSz; |
| | | |
| | | // 下单数量精度 |
| | | private String lotSz; |
| | | |
| | | // 最小下单数量 |
| | | private String minSz; |
| | | |
| | | // 合约面值 |
| | | private String ctVal; |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.okxpi.verify.vo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public class SinglemarketVo { |
| | | // 产品类型 |
| | | private String instType; |
| | | |
| | | // 产品ID |
| | | private String instId; |
| | | |
| | | // 最新成交价 |
| | | private String last; |
| | | |
| | | // 最新成交的数量,0 代表没有成交量 |
| | | private String lastSz; |
| | | |
| | | // 卖一价 |
| | | private String askPx; |
| | | |
| | | // 卖一价对应的数量 |
| | | private String askSz; |
| | | |
| | | // 买一价 |
| | | private String bidPx; |
| | | |
| | | // 买一价对应的数量 |
| | | private String bidSz; |
| | | |
| | | // 24小时开盘价 |
| | | private String open24h; |
| | | |
| | | // 24小时最高价 |
| | | private String high24h; |
| | | |
| | | // 24小时最低价 |
| | | private String low24h; |
| | | |
| | | // 24小时成交量,以币为单位 |
| | | // 如果是衍生品合约,数值为交易货币的数量。 |
| | | // 如果是币币/币币杠杆,数值为计价货币的数量。 |
| | | private String volCcy24h; |
| | | |
| | | // 24小时成交量,以张为单位 |
| | | // 如果是衍生品合约,数值为合约的张数。 |
| | | // 如果是币币/币币杠杆,数值为交易货币的数量。 |
| | | private String vol24h; |
| | | |
| | | // UTC+0 时开盘价 |
| | | private String sodUtc0; |
| | | |
| | | // UTC+8 时开盘价 |
| | | private String sodUtc8; |
| | | |
| | | // ticker数据产生时间,Unix时间戳的毫秒数格式,如 1597026383085 |
| | | private String ts; |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.utils; |
| | | |
| | | /** |
| | | * FEBS系统内部异常 |
| | | * |
| | | * @author MrBird |
| | | */ |
| | | public class FebsException extends RuntimeException { |
| | | |
| | | private static final long serialVersionUID = -994962710559017255L; |
| | | |
| | | public FebsException(String message) { |
| | | super(message); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.utils; |
| | | |
| | | import org.springframework.http.HttpStatus; |
| | | |
| | | import java.util.HashMap; |
| | | |
| | | /** |
| | | * @author MrBird |
| | | */ |
| | | public class FebsResponse extends HashMap<String, Object> { |
| | | |
| | | private static final long serialVersionUID = -8713837118340960775L; |
| | | |
| | | public FebsResponse code(HttpStatus status) { |
| | | this.put("code", status.value()); |
| | | return this; |
| | | } |
| | | |
| | | public FebsResponse message(String message) { |
| | | this.put("message", message); |
| | | return this; |
| | | } |
| | | |
| | | public FebsResponse data(Object data) { |
| | | this.put("data", data); |
| | | return this; |
| | | } |
| | | |
| | | public FebsResponse success() { |
| | | this.code(HttpStatus.OK); |
| | | return this; |
| | | } |
| | | |
| | | public FebsResponse fail() { |
| | | this.code(HttpStatus.INTERNAL_SERVER_ERROR); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public FebsResponse put(String key, Object value) { |
| | | super.put(key, value); |
| | | return this; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.utils; |
| | | |
| | | import javax.net.ssl.HttpsURLConnection; |
| | | import javax.net.ssl.SSLContext; |
| | | import java.security.SecureRandom; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | public class SSLConfig { |
| | | public static void configureSSL() { |
| | | try { |
| | | // 配置SSL上下文 |
| | | SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); |
| | | sslContext.init(null, null, new SecureRandom()); |
| | | |
| | | // 设置默认SSL套接字工厂 |
| | | HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.utils; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | public class WsParamBuild { |
| | | |
| | | public static JSONObject buildJsonObject(String connId, String option, JSONArray argsArray) { |
| | | JSONObject jsonObject = new JSONObject(); |
| | | if (StrUtil.isNotEmpty(connId)){ |
| | | jsonObject.put("id", connId); |
| | | } |
| | | jsonObject.put("op", option); |
| | | jsonObject.put("args", argsArray); |
| | | return jsonObject; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.wangge; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | * 网格数据枚举 数据 |
| | | * todo 后期考虑优化为可配置项 |
| | | */ |
| | | |
| | | public enum WangGeEnum { |
| | | |
| | | XIAOSHU_WEISHU("网格价格小数位数", "2"), |
| | | JIAGE_SHANGXIAN("网格上限", "94000"), |
| | | JIAGE_XIAXIAN("网格下限", "90000"), |
| | | JIAN_JU("网格间距", "100") |
| | | ; |
| | | |
| | | private String name; |
| | | |
| | | private String value; |
| | | |
| | | public String getName() { |
| | | return name; |
| | | } |
| | | |
| | | public void setName(String name) { |
| | | this.name = name; |
| | | } |
| | | |
| | | public String getValue() { |
| | | return value; |
| | | } |
| | | |
| | | public void setValue(String value) { |
| | | this.value = value; |
| | | } |
| | | |
| | | private WangGeEnum(String name, String value) { |
| | | this.name = name; |
| | | this.value = value; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.wangge; |
| | | |
| | | import com.xcong.excoin.rabbit.pricequeue.AscBigDecimal; |
| | | import com.xcong.excoin.rabbit.pricequeue.DescBigDecimal; |
| | | |
| | | import java.util.concurrent.PriorityBlockingQueue; |
| | | |
| | | /** |
| | | * 网格交易队列管理类 |
| | | * |
| | | * 用于管理系统中各种网格交易相关的优先级阻塞队列, |
| | | * 包括完整的网格队列、平仓队列和开仓队列。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | public class WangGeQueue { |
| | | |
| | | //------------------------------------------------------------------------------------------------------------------ |
| | | //------------------------------------------------------------------------------------------------------------------ |
| | | // todo 系统启动后,初始化网格队列 |
| | | /** |
| | | * 完整的网格 头元素最小 |
| | | */ |
| | | public static PriorityBlockingQueue<AscBigDecimal> QUEUE_ASC = null; |
| | | |
| | | |
| | | //------------------------------------------------------------------------------------------------------------------ |
| | | //------------------------------------------------------------------------------------------------------------------ |
| | | // todo 当用户下了第一单后,根据开仓价格初始化网格平仓队列和开仓队列 |
| | | /** |
| | | * 网格平仓队列 头元素最小 |
| | | */ |
| | | public static PriorityBlockingQueue<AscBigDecimal> QUEUE_PINGCANG_ASC = null; |
| | | |
| | | /** |
| | | * 网格开仓队列 头元素最大 |
| | | */ |
| | | public static PriorityBlockingQueue<DescBigDecimal> QUEUE_KAICANG_DESC = null; |
| | | |
| | | /** |
| | | * 获取完整的网格队列(升序) |
| | | * 如果队列未初始化则创建新的优先级阻塞队列 |
| | | * |
| | | * @return 返回升序排列的PriorityBlockingQueue队列,队列头部元素最小 |
| | | */ |
| | | public static PriorityBlockingQueue<AscBigDecimal> getQueueAsc() { |
| | | if (QUEUE_ASC == null) { |
| | | QUEUE_ASC = new PriorityBlockingQueue<AscBigDecimal>(); |
| | | } |
| | | return QUEUE_ASC; |
| | | } |
| | | |
| | | /** |
| | | * 获取网格平仓队列(升序) |
| | | * 如果队列未初始化则创建新的优先级阻塞队列 |
| | | * |
| | | * @return 返回升序排列的PriorityBlockingQueue队列,队列头部元素最小 |
| | | */ |
| | | public static PriorityBlockingQueue<AscBigDecimal> getPingCang() { |
| | | if (QUEUE_PINGCANG_ASC == null) { |
| | | QUEUE_PINGCANG_ASC = new PriorityBlockingQueue<AscBigDecimal>(); |
| | | } |
| | | return QUEUE_PINGCANG_ASC; |
| | | } |
| | | |
| | | /** |
| | | * 获取网格开仓队列(降序) |
| | | * 如果队列未初始化则创建新的优先级阻塞队列 |
| | | * |
| | | * @return 返回降序排列的PriorityBlockingQueue队列,队列头部元素最大 |
| | | */ |
| | | public static PriorityBlockingQueue<DescBigDecimal> getKaiCang() { |
| | | if (QUEUE_KAICANG_DESC == null) { |
| | | QUEUE_KAICANG_DESC = new PriorityBlockingQueue<DescBigDecimal>(); |
| | | } |
| | | return QUEUE_KAICANG_DESC; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.wangge; |
| | | |
| | | import com.xcong.excoin.rabbit.pricequeue.AscBigDecimal; |
| | | import com.xcong.excoin.rabbit.pricequeue.DescBigDecimal; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.concurrent.PriorityBlockingQueue; |
| | | |
| | | /** |
| | | * 网格交易服务接口 |
| | | * 定义了网格交易的核心操作方法,包括初始化网格、开仓和平仓等操作 |
| | | * @author Administrator |
| | | */ |
| | | public interface WangGeService { |
| | | |
| | | /** |
| | | * 初始化网格交易 |
| | | * 创建并初始化用于网格交易的价格队列,按照价格升序排列 |
| | | * @return 初始化结果信息,返回按价格升序排列的阻塞队列 |
| | | */ |
| | | PriorityBlockingQueue<AscBigDecimal> initWangGe(); |
| | | |
| | | /** |
| | | * 初始化开仓操作 |
| | | * 根据指定价格初始化开仓队列,将开仓价格点加入到价格队列中 |
| | | * @param jiaGe 开仓价格 |
| | | * @param queueAsc 价格队列,用于存储按升序排列的价格点 |
| | | */ |
| | | PriorityBlockingQueue<DescBigDecimal> initKaiCang(BigDecimal jiaGe, PriorityBlockingQueue<AscBigDecimal> queueAsc); |
| | | |
| | | /** |
| | | * 初始化平仓操作 |
| | | * 根据指定价格初始化平仓队列,将平仓价格点加入到价格队列中 |
| | | * @param jiaGe 开仓价格 |
| | | * @param queueAsc 价格队列,用于存储按升序排列的价格点 |
| | | */ |
| | | PriorityBlockingQueue<AscBigDecimal> initPingCang(BigDecimal jiaGe, PriorityBlockingQueue<AscBigDecimal> queueAsc); |
| | | |
| | | |
| | | } |
| | | |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.wangge; |
| | | |
| | | import com.xcong.excoin.rabbit.pricequeue.AscBigDecimal; |
| | | import com.xcong.excoin.rabbit.pricequeue.DescBigDecimal; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.concurrent.PriorityBlockingQueue; |
| | | |
| | | /** |
| | | * 网格交易服务实现类,用于初始化价格网格、开仓和平仓操作。 |
| | | * |
| | | * @author Administrator |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class WangGeServiceImpl implements WangGeService { |
| | | |
| | | /** |
| | | * 初始化价格网格队列。根据配置的价格上限、下限和间隔生成一系列价格点, |
| | | * 并将这些价格点存入升序优先阻塞队列中。 |
| | | * |
| | | * @return 返回初始化完成的升序价格队列;若初始化失败则返回null |
| | | */ |
| | | @Override |
| | | public PriorityBlockingQueue<AscBigDecimal> initWangGe() { |
| | | log.info("网格初始化中"); |
| | | PriorityBlockingQueue<AscBigDecimal> queueAsc = WangGeQueue.getQueueAsc(); |
| | | queueAsc.clear(); |
| | | |
| | | String shangxianValue = WangGeEnum.JIAGE_SHANGXIAN.getValue(); |
| | | String xiaxianValue = WangGeEnum.JIAGE_XIAXIAN.getValue(); |
| | | String jianjuValue = WangGeEnum.JIAN_JU.getValue(); |
| | | String weishuValueStr = WangGeEnum.XIAOSHU_WEISHU.getValue(); |
| | | |
| | | try { |
| | | BigDecimal shangxian = new BigDecimal(shangxianValue); |
| | | BigDecimal xiaxian = new BigDecimal(xiaxianValue); |
| | | BigDecimal jianju = new BigDecimal(jianjuValue); |
| | | |
| | | if (jianju.compareTo(BigDecimal.ZERO) == 0) { |
| | | log.error("价格间隔不能为0"); |
| | | return null; |
| | | } |
| | | |
| | | int weishu = Integer.parseInt(weishuValueStr); |
| | | BigDecimal diff = shangxian.subtract(xiaxian); |
| | | int count = diff.divide(jianju, 0, RoundingMode.DOWN).intValue(); |
| | | |
| | | BigDecimal currentStep = BigDecimal.ZERO; |
| | | for (int i = 0; i <= count; i++) { |
| | | BigDecimal stepMultiplier = currentStep.multiply(jianju); |
| | | BigDecimal wangGeJiaGe = xiaxian.add(stepMultiplier).setScale(weishu, RoundingMode.DOWN); |
| | | AscBigDecimal ascBigDecimal = new AscBigDecimal(wangGeJiaGe.toString()); |
| | | queueAsc.add(ascBigDecimal); |
| | | currentStep = currentStep.add(BigDecimal.ONE); |
| | | } |
| | | |
| | | if (queueAsc.isEmpty()) { |
| | | log.info("网格初始化失败"); |
| | | return null; |
| | | } |
| | | |
| | | log.info("网格初始化成功"); |
| | | return queueAsc; |
| | | } catch (NumberFormatException e) { |
| | | log.error("解析价格参数失败", e); |
| | | return null; |
| | | } catch (Exception e) { |
| | | log.error("初始化网格发生未知异常", e); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据当前价格初始化开仓队列。遍历已有的升序价格队列, |
| | | * 将小于当前价格的所有价格点加入降序的开仓队列中。 |
| | | * |
| | | * @param jiaGe 当前价格 |
| | | * @param queueAsc 已初始化的价格升序队列 |
| | | */ |
| | | @Override |
| | | public PriorityBlockingQueue<DescBigDecimal> initKaiCang(BigDecimal jiaGe, PriorityBlockingQueue<AscBigDecimal> queueAsc) { |
| | | PriorityBlockingQueue<DescBigDecimal> queueKaiCang = WangGeQueue.getKaiCang(); |
| | | queueKaiCang.clear(); |
| | | |
| | | AscBigDecimal now = new AscBigDecimal(jiaGe.toString()); |
| | | |
| | | for (AscBigDecimal ascBigDecimal : queueAsc) { |
| | | if (ascBigDecimal.compareTo(now) < 0) { |
| | | DescBigDecimal kaiCangJia = new DescBigDecimal(ascBigDecimal.getValue().toString()); |
| | | queueKaiCang.add(kaiCangJia); |
| | | } |
| | | } |
| | | |
| | | return queueKaiCang; |
| | | } |
| | | |
| | | /** |
| | | * 根据当前价格初始化平仓队列。遍历已有的升序价格队列, |
| | | * 将大于当前价格的所有价格点加入升序的平仓队列中。 |
| | | * |
| | | * @param jiaGe 当前价格 |
| | | * @param queueAsc 已初始化的价格升序队列 |
| | | */ |
| | | @Override |
| | | public PriorityBlockingQueue<AscBigDecimal> initPingCang(BigDecimal jiaGe, PriorityBlockingQueue<AscBigDecimal> queueAsc) { |
| | | PriorityBlockingQueue<AscBigDecimal> queuePingCang = WangGeQueue.getPingCang(); |
| | | queuePingCang.clear(); |
| | | |
| | | AscBigDecimal now = new AscBigDecimal(jiaGe.toString()); |
| | | |
| | | for (AscBigDecimal ascBigDecimal : queueAsc) { |
| | | if (ascBigDecimal.compareTo(now) > 0) { |
| | | queuePingCang.add(ascBigDecimal); |
| | | } |
| | | } |
| | | |
| | | return queuePingCang; |
| | | } |
| | | |
| | | /** |
| | | * 主方法,用于测试网格初始化及开仓/平仓逻辑。 |
| | | * 示例使用固定价格"0.355"进行模拟调用。 |
| | | * |
| | | * @param args 启动参数(未使用) |
| | | */ |
| | | public static void main(String[] args) { |
| | | for (int i = 0; i < 10; i++) { |
| | | WangGeServiceImpl wangGeService = new WangGeServiceImpl(); |
| | | PriorityBlockingQueue<AscBigDecimal> queueAsc = wangGeService.initWangGe(); |
| | | if (queueAsc != null) { |
| | | wangGeService.initKaiCang(new BigDecimal("91000"), queueAsc); |
| | | wangGeService.initPingCang(new BigDecimal("91000"), queueAsc); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.zhanghu; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.QuantApiMessage; |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.ExchangeInfoEnum; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | |
| | | @Slf4j |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class ApiMessageServiceImpl implements IApiMessageService { |
| | | |
| | | @Override |
| | | public QuantApiMessage getApiMessage(String okx) { |
| | | |
| | | QuantApiMessage quantApiMessage = new QuantApiMessage(); |
| | | quantApiMessage.setExchange(okx); |
| | | quantApiMessage.setMemberId(1L); |
| | | |
| | | |
| | | quantApiMessage.setAccountType(ExchangeInfoEnum.OKX_UAT.isAccountType()? "true":"false"); |
| | | quantApiMessage.setState(1); |
| | | quantApiMessage.setIsTrade(1); |
| | | quantApiMessage.setASecretkey(ExchangeInfoEnum.OKX_UAT.getApiKey()); |
| | | quantApiMessage.setBSecretkey(ExchangeInfoEnum.OKX_UAT.getSecretKey()); |
| | | quantApiMessage.setPassPhrass(ExchangeInfoEnum.OKX_UAT.getPassphrase()); |
| | | return quantApiMessage; |
| | | } |
| | | |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.zhanghu; |
| | | |
| | | import com.xcong.excoin.modules.okxNewPrice.okxpi.config.Dto.QuantApiMessage; |
| | | |
| | | |
| | | public interface IApiMessageService{ |
| | | |
| | | QuantApiMessage getApiMessage(String okx); |
| | | |
| | | } |
| New file |
| | |
| | | package com.xcong.excoin.modules.okxNewPrice.zhanghu; |
| | | |
| | | /** |
| | | * @author Administrator |
| | | */ |
| | | |
| | | public enum ZhangHuEnum { |
| | | JIAOYISUO("交易所名字", "OKX"), |
| | | zhanghu_zongjine("账户总资金", "1000"), |
| | | baozhengjine("账户下单总保证金", "300"), |
| | | meicixiadanjine("每次下单金额", "30"), |
| | | ; |
| | | |
| | | private String name; |
| | | |
| | | private String value; |
| | | |
| | | public String getName() { |
| | | return name; |
| | | } |
| | | |
| | | public void setName(String name) { |
| | | this.name = name; |
| | | } |
| | | |
| | | public String getValue() { |
| | | return value; |
| | | } |
| | | |
| | | public void setValue(String value) { |
| | | this.value = value; |
| | | } |
| | | |
| | | private ZhangHuEnum(String name, String value) { |
| | | this.name = name; |
| | | this.value = value; |
| | | } |
| | | } |
| | |
| | | |
| | | spring: |
| | | profiles: |
| | | active: app |
| | | active: prd |
| | | datasource: |
| | | url: jdbc:mysql://rm-bp151tw8er79ig9kb5o.mysql.rds.aliyuncs.com:3306/db_biue?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2b8 |
| | | username: ctcoin_data |
| | | password: ctcoin_123 |
| | | url: jdbc:mysql://47.76.217.51:3306/db_base?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2b8 |
| | | username: root |
| | | password: =[;.-pl,1234!@#$!QAZ |
| | | driver-class-name: com.mysql.jdbc.Driver |
| | | type: com.alibaba.druid.pool.DruidDataSource |
| | | druid: |
| | |
| | | basename: i18n/messages |
| | | ## redis配置 |
| | | redis: |
| | | ## Redis数据库索引(默认为0) |
| | | database: 2 |
| | | ## Redis服务器地址 |
| | | host: 47.114.114.219 |
| | | ## Redis服务器连接端口 |
| | | # Redis数据库索引(默认为 0) |
| | | database: 13 |
| | | # Redis服务器地址 |
| | | host: 47.76.217.51 |
| | | # Redis服务器连接端口 |
| | | port: 6379 |
| | | ## Redis服务器连接密码(默认为空) |
| | | password: biyi123 |
| | | jedis: |
| | | # Redis 密码 |
| | | password: lianghua1!qaz2@WSX |
| | | lettuce: |
| | | pool: |
| | | ## 连接池最大连接数(使用负值表示没有限制) |
| | | #spring.redis.pool.max-active=8 |
| | | max-active: 300 |
| | | ## 连接池最大阻塞等待时间(使用负值表示没有限制) |
| | | #spring.redis.pool.max-wait=-1 |
| | | max-wait: -1 |
| | | ## 连接池中的最大空闲连接 |
| | | #spring.redis.pool.max-idle=8 |
| | | max-idle: 100 |
| | | ## 连接池中的最小空闲连接 |
| | | #spring.redis.pool.min-idle=0 |
| | | # 连接池中的最小空闲连接 |
| | | min-idle: 8 |
| | | ## 连接超时时间(毫秒) |
| | | timeout: 30000 |
| | | # 连接池中的最大空闲连接 |
| | | max-idle: 500 |
| | | # 连接池最大连接数(使用负值表示没有限制) |
| | | max-active: 2000 |
| | | # 连接池最大阻塞等待时间(使用负值表示没有限制) |
| | | max-wait: 10000 |
| | | # 连接超时时间(毫秒) |
| | | timeout: 500000 |
| | | |
| | | rabbitmq: |
| | | host: 120.55.86.146 |
| | | host: 47.76.217.51 |
| | | port: 5672 |
| | | username: biyict |
| | | password: biyict123 |
| | | username: lianghua20210816 |
| | | password: lianghua20210816 |
| | | publisher-confirm-type: correlated |
| | | |
| | | |
| | |
| | | app: |
| | | debug: false |
| | | redis_expire: 3000 |
| | | # k线更新任务控制 |
| | | kline-update-job: false |
| | | #最新价任务控制 |
| | | newest-price-update-job: false |
| | | #日线 该任务不能与最新价处于同一个服务器 |
| | | day-line: false |
| | | #其他任务控制 |
| | | other-job: false |
| | | loop-job: false |
| | | rabbit-consumer: false |
| | | block-job: false |
| | | websocket: false |
| | | quant: true |
| | | |
| | | aliyun: |
| | | oss: |
| | |
| | | loop-job: false |
| | | rabbit-consumer: false |
| | | block-job: false |
| | | websocket: false |
| | | quant: true |
| | | |
| | | aliyun: |
| | | oss: |
| | |
| | | context-path: / |
| | | |
| | | spring: |
| | | OKEX: |
| | | baseurl: https://www.okex.com |
| | | profiles: |
| | | active: test |
| | | datasource: |
| New file |
| | |
| | | package com.xcong.excoin.modules.newPrice; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.boot.test.context.SpringBootTest; |
| | | |
| | | @SpringBootTest |
| | | public class OkxWebSocketClientTest { |
| | | |
| | | @Test |
| | | public void testOkxWebSocketConnection() throws InterruptedException { |
| | | // 给足够的时间观察WebSocket连接和数据接收 |
| | | Thread.sleep(60000); // 运行1分钟 |
| | | } |
| | | } |