| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
| 2025-12-29 | Administrator | ![]() |
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxKlineWebSocketClient.java
@@ -1,6 +1,7 @@ package com.xcong.excoin.modules.okxNewPrice; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.json.JSONException; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; @@ -56,7 +57,8 @@ private final AtomicBoolean isConnecting = new AtomicBoolean(false); private final AtomicBoolean isInitialized = new AtomicBoolean(false); private static final String CHANNEL = "candle5m"; // private static final String CHANNEL = "mark-price"; private static final String CHANNEL = "candle1m"; // private static final String CHANNEL = "candle15m"; // 心跳超时时间(秒),小于30秒 @@ -121,7 +123,7 @@ private static final String WS_URL_MONIPAN = "wss://wspap.okx.com:8443/ws/v5/business"; private static final String WS_URL_SHIPAN = "wss://ws.okx.com:8443/ws/v5/business"; private static final boolean isAccountType = true; private static final boolean isAccountType = false; /** * 建立与 OKX WebSocket 服务器的连接。 @@ -337,195 +339,34 @@ MacdMaStrategy strategy = new MacdMaStrategy(); // 生成100个15分钟价格数据点 List<Kline> kline15MinuteData = getKlineDataByInstIdAndBar(instId, "15m"); List<BigDecimal> historicalPrices = kline15MinuteData.stream() List<Kline> kline1MinuteData = getKlineDataByInstIdAndBar(instId, "1m"); List<BigDecimal> historicalPrices1M = kline1MinuteData.stream() .map(Kline::getC) .collect(Collectors.toList()); log.info("生成100个15分钟价格数据点成功!"); log.info("生成100个1分钟价格数据点成功!"); // 使用策略分析最新价格数据 MacdMaStrategy.TradingOrder tradingOrder = strategy.generateTradingOrder(historicalPrices); if (tradingOrder == null){ MacdMaStrategy.TradingOrder tradingOrderOpen1M = strategy.generateTradingOrder(historicalPrices1M,MacdMaStrategy.OperationType.open.name()); if (tradingOrderOpen1M == null ){ return; } Collection<OkxQuantWebSocketClient> allClients = clientManager.getAllClients(); //如果为空,则直接返回 if (allClients.isEmpty()) { return; } // 获取所有OkxQuantWebSocketClient实例 for (OkxQuantWebSocketClient client : clientManager.getAllClients()) { String accountName = client.getAccountName(); if (accountName != null) { // 根据信号执行交易操作 TradeRequestParam tradeRequestParam = new TradeRequestParam(); String posSide = tradingOrder.getPosSide(); tradeRequestParam.setPosSide(posSide); String currentPrice = String.valueOf(closePx); tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, currentPrice, posSide); String side = tradingOrder.getSide(); tradeRequestParam.setSide(side); String clOrdId = WsParamBuild.getOrderNum(side); tradeRequestParam.setClOrdId(clOrdId); String sz = null; if (posSide == CoinEnums.POSSIDE_LONG.getCode() && side == CoinEnums.SIDE_BUY.getCode()){ sz = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.BUY_CNT_INIT.name()); tradeRequestParam.setSz(sz); TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam); BigDecimal pos = PositionsWs.getAccountMap(PositionsWs.initAccountName(accountName, CoinEnums.POSSIDE_SHORT.getCode())).get("pos"); if (BigDecimal.ZERO.compareTo( pos) >= 0) { TradeRequestParam tradeRequestParamOld = caoZuoService.caoZuoZhiSunEvent(accountName, String.valueOf(closePx), CoinEnums.POSSIDE_SHORT.getCode()); TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParamOld); } }else if (posSide == CoinEnums.POSSIDE_SHORT.getCode() && side == CoinEnums.SIDE_SELL.getCode()){ sz = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.BUY_CNT_INIT.name()); tradeRequestParam.setSz(sz); TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam); BigDecimal pos = PositionsWs.getAccountMap(PositionsWs.initAccountName(accountName, CoinEnums.POSSIDE_LONG.getCode())).get("pos"); if (BigDecimal.ZERO.compareTo( pos) >= 0) { TradeRequestParam tradeRequestParamOld = caoZuoService.caoZuoZhiSunEvent(accountName, String.valueOf(closePx), CoinEnums.POSSIDE_LONG.getCode()); TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParamOld); } } } } } } } catch (Exception e) { log.error("处理 K线频道推送数据失败", e); } } /** * 解析并处理价格推送数据。 * 将最新的标记价格存入 Redis 并触发后续业务逻辑比较处理。 * 当价格变化时,调用CaoZuoService的caoZuo方法,触发所有账号的量化操作 * * @param response 包含价格数据的 JSON 对象 */ private void processPushData(JSONObject response) { try { /** * { * "arg": { * "channel": "candle1D", * "instId": "BTC-USDT" * }, * "data": [ * [ * "1629993600000", * "42500", * "48199.9", * "41006.1", * "41006.1", * "3587.41204591", * "166741046.22583129", * "166741046.22583129", * "0" * ] * ] * } */ JSONObject arg = response.getJSONObject("arg"); if (arg == null) { log.warn("{}: 无效的推送数据,缺少 'arg' 字段", response); return; } String channel = arg.getString("channel"); if (channel == null) { log.warn("{}: 无效的推送数据,缺少 'channel' 字段", response); return; } String instId = arg.getString("instId"); if (instId == null) { log.warn("{}: 无效的推送数据,缺少 'instId' 字段", response); return; } if (CHANNEL.equals(channel) && CoinEnums.HE_YUE.getCode().equals(instId)) { JSONArray dataArray = response.getJSONArray("data"); if (dataArray == null || dataArray.isEmpty()) { log.warn("K线频道数据为空"); return; } JSONArray data = dataArray.getJSONArray(0); BigDecimal openPx = new BigDecimal(data.getString(1)); BigDecimal highPx = new BigDecimal(data.getString(2)); BigDecimal lowPx = new BigDecimal(data.getString(3)); BigDecimal closePx = new BigDecimal(data.getString(4)); BigDecimal vol = new BigDecimal(data.getString(5)); /** * K线状态 * 0:K线未完结 * 1:K线已完结 */ String confirm = data.getString(8); if ("1".equals(confirm)){ //调用策略 // 创建交易策略 TradingStrategy tradingStrategy = new TradingStrategy(); // 生成100个15分钟价格数据点 List<Kline> kline15MinuteData = getKlineDataByInstIdAndBar(instId, "15m"); //stream流获取kline15MinuteData中的o数据的集合 List<BigDecimal> prices = kline15MinuteData.stream() List<BigDecimal> historicalPrices15M = kline15MinuteData.stream() .map(Kline::getC) .collect(Collectors.toList()); // 使用策略分析最新价格数据 MacdMaStrategy.TradingOrder tradingOrderOpen15M = strategy.generateTradingOrder(historicalPrices15M,MacdMaStrategy.OperationType.open.name()); if (tradingOrderOpen15M == null ){ return; } // 生成对应的高、低、收盘价数据 List<BigDecimal> high = kline15MinuteData.stream() .map(Kline::getH) .collect(Collectors.toList()); List<BigDecimal> low = kline15MinuteData.stream() .map(Kline::getL) .collect(Collectors.toList()); List<BigDecimal> close = prices; if (!tradingOrderOpen1M.getPosSide().equals(tradingOrderOpen15M.getPosSide())){ return; } // 生成成交量数据 List<BigDecimal> volume = kline15MinuteData.stream() .map(Kline::getVol) .collect(Collectors.toList()); log.info("1分钟和15分钟K线方向一致,开始执行交易操作!"); // 获取最新价格 BigDecimal currentPrice = closePx; // 生成多周期价格数据(5分钟、1小时、4小时) List<Kline> kline5MinuteData = getKlineDataByInstIdAndBar(instId, "5m"); List<BigDecimal> fiveMinPrices = kline5MinuteData.stream() .map(Kline::getC) .collect(Collectors.toList()); List<Kline> kline60MinuteData = getKlineDataByInstIdAndBar(instId, "1H"); List<BigDecimal> oneHourPrices = kline60MinuteData.stream() .map(Kline::getC) .collect(Collectors.toList()); List<Kline> kline240MinuteData = getKlineDataByInstIdAndBar(instId, "4H"); List<BigDecimal> fourHourPrices = kline240MinuteData.stream() .map(Kline::getC) .collect(Collectors.toList()); // 其他参数 BigDecimal fundingRate = new BigDecimal("0.001"); // 正常资金费率 boolean hasLargeTransfer = false; // 无大额转账 boolean hasUpcomingEvent = false; // 无即将到来的重大事件 // 确定市场方向 TradingStrategy.Direction direction = tradingStrategy.getDirection(prices, high, low, close, currentPrice); log.info("市场方向(15分钟): {}", direction); // if (direction == TradingStrategy.Direction.RANGING){ // return; // } /** * 获取当前网格信息 * 根据当前网格的持仓方向获取反方向是否存在持仓 * 如果持有,直接止损 */ Collection<OkxQuantWebSocketClient> allClients = clientManager.getAllClients(); //如果为空,则直接返回 if (allClients.isEmpty()) { @@ -535,70 +376,23 @@ for (OkxQuantWebSocketClient client : clientManager.getAllClients()) { String accountName = client.getAccountName(); if (accountName != null) { TradingStrategy.SignalType signal = TradingStrategy.SignalType.NONE; TradeRequestParam tradeRequestParam = new TradeRequestParam(); // 检查当前持仓状态 boolean hasLongPosition = false; // 示例:无当前做多持仓 boolean hasShortPosition = false; // 示例:无当前做空持仓 //先判断账户是否有持多仓 String positionLongAccountName = PositionsWs.initAccountName(accountName, CoinEnums.POSSIDE_LONG.getCode()); BigDecimal imrLong = PositionsWs.getAccountMap(positionLongAccountName).get("imr"); if (imrLong != null && imrLong.compareTo(BigDecimal.ZERO) > 0){ log.info("账户{}有持多仓", accountName); hasLongPosition = true; if (ObjectUtil.isNotEmpty(tradingOrderOpen1M)){ // 根据信号执行交易操作 TradeRequestParam tradeRequestParam = new TradeRequestParam(); } //先判断账户是否有持空仓 String positionShortAccountName = PositionsWs.initAccountName(accountName, CoinEnums.POSSIDE_SHORT.getCode()); BigDecimal imrShort = PositionsWs.getAccountMap(positionShortAccountName).get("imr"); if (imrShort != null && imrShort.compareTo(BigDecimal.ZERO) > 0){ log.info("账户{}有持空仓", accountName); hasShortPosition = true; } signal = tradingStrategy.generateSignal(prices, high, low, close, volume, currentPrice, hasLongPosition, hasShortPosition, fiveMinPrices, oneHourPrices, fourHourPrices, fundingRate, hasLargeTransfer, hasUpcomingEvent); log.info("账户{}交易信号: " + signal, accountName); if (TradingStrategy.SignalType.NONE == signal) { continue; }else if (TradingStrategy.SignalType.BUY == signal){ tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, String.valueOf(currentPrice), CoinEnums.POSSIDE_LONG.getCode()); tradeRequestParam.setSide(CoinEnums.SIDE_BUY.getCode()); String clOrdId = WsParamBuild.getOrderNum(CoinEnums.SIDE_BUY.getCode()); String posSide = tradingOrderOpen1M.getPosSide(); tradeRequestParam.setPosSide(posSide); String currentPrice = String.valueOf(closePx); tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, currentPrice, posSide); String side = tradingOrderOpen1M.getSide(); tradeRequestParam.setSide(side); String clOrdId = WsParamBuild.getOrderNum(side); tradeRequestParam.setClOrdId(clOrdId); String sz = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.BUY_CNT_INIT.name()); tradeRequestParam.setSz(sz); TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam); }else if (TradingStrategy.SignalType.SELL == signal){ tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, String.valueOf(currentPrice), CoinEnums.POSSIDE_SHORT.getCode()); tradeRequestParam.setSide(CoinEnums.SIDE_SELL.getCode()); String clOrdId = WsParamBuild.getOrderNum(CoinEnums.SIDE_SELL.getCode()); tradeRequestParam.setClOrdId(clOrdId); String sz = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.BUY_CNT_INIT.name()); tradeRequestParam.setSz(sz); TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam); }else if (TradingStrategy.SignalType.CLOSE_BUY == signal){ tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, String.valueOf(currentPrice), CoinEnums.POSSIDE_LONG.getCode()); tradeRequestParam.setSide(CoinEnums.SIDE_SELL.getCode()); String clOrdId = WsParamBuild.getOrderNum(CoinEnums.SIDE_SELL.getCode()); tradeRequestParam.setClOrdId(clOrdId); BigDecimal pos = PositionsWs.getAccountMap(PositionsWs.initAccountName(accountName, CoinEnums.POSSIDE_LONG.getCode())).get("pos"); if (BigDecimal.ZERO.compareTo( pos) >= 0) { tradeRequestParam.setTradeType(OrderParamEnums.TRADE_NO.getValue()); } tradeRequestParam.setSz(String.valueOf( pos)); TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam); }else if (TradingStrategy.SignalType.CLOSE_SELL == signal){ tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, String.valueOf(currentPrice), CoinEnums.POSSIDE_SHORT.getCode()); tradeRequestParam.setSide(CoinEnums.SIDE_BUY.getCode()); String clOrdId = WsParamBuild.getOrderNum(CoinEnums.SIDE_BUY.getCode()); tradeRequestParam.setClOrdId(clOrdId); BigDecimal pos = PositionsWs.getAccountMap(PositionsWs.initAccountName(accountName, CoinEnums.POSSIDE_SHORT.getCode())).get("pos"); if (BigDecimal.ZERO.compareTo( pos) >= 0) { tradeRequestParam.setTradeType(OrderParamEnums.TRADE_NO.getValue()); } tradeRequestParam.setSz(String.valueOf( pos)); TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam); } } @@ -607,26 +401,6 @@ } } catch (Exception e) { log.error("处理 K线频道推送数据失败", e); } } /** * 触发所有账号的量化操作 * @param markPx 当前标记价格 */ private void triggerQuantOperations(String markPx) { try { // 1. 判断当前价格属于哪个网格 WangGeListEnum gridByPriceNew = WangGeListEnum.getGridByPrice(new BigDecimal(markPx)); if (gridByPriceNew == null) { log.error("当前 K线频道{}不在任何网格范围内,无法触发量化操作", markPx); return; } } catch (Exception e) { log.error("触发量化操作失败", e); } } src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxQuantWebSocketClient.java
@@ -4,20 +4,18 @@ 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.ExchangeInfoEnum; import com.xcong.excoin.modules.okxNewPrice.okxWs.param.TradeRequestParam; 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.*; @@ -34,6 +32,7 @@ public class OkxQuantWebSocketClient { private final RedisUtils redisUtils; private final ExchangeInfoEnum account; private final CaoZuoService caoZuoService; private WebSocketClient webSocketClient; private ScheduledExecutorService heartbeatExecutor; @@ -61,8 +60,10 @@ } public OkxQuantWebSocketClient(ExchangeInfoEnum account, CaoZuoService caoZuoService, RedisUtils redisUtils) { this.account = account; this.caoZuoService = caoZuoService; this.redisUtils = redisUtils; } @@ -382,9 +383,8 @@ // 注意:当前实现中,OrderInfoWs等类使用静态Map存储数据 // 这会导致多账号之间的数据冲突。需要进一步修改这些类的设计,让数据存储与特定账号关联 if (OrderInfoWs.ORDERINFOWS_CHANNEL.equals(channel)) { OrderInfoWs.handleEvent(response, redisUtils, account.name()); // TradeRequestParam tradeRequestParam = OrderInfoWs.handleEvent(response, redisUtils, account.name()); // TradeOrderWs.orderZhiYingEvent(webSocketClient, tradeRequestParam); TradeRequestParam tradeRequestParam = OrderInfoWs.handleEvent(response, redisUtils, account.name()); TradeOrderWs.orderEvent(webSocketClient, tradeRequestParam); }else if (AccountWs.ACCOUNTWS_CHANNEL.equals(channel)) { AccountWs.handleEvent(response, account.name()); } else if (PositionsWs.POSITIONSWS_CHANNEL.equals(channel)) { src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxWebSocketClientManager.java
@@ -58,7 +58,7 @@ // 为每个账号创建一个WebSocket客户端实例 for (ExchangeInfoEnum account : accounts) { try { OkxQuantWebSocketClient client = new OkxQuantWebSocketClient(account, redisUtils); OkxQuantWebSocketClient client = new OkxQuantWebSocketClient(account, caoZuoService, redisUtils); quantClientMap.put(account.name(), client); client.init(); log.info("已初始化账号 {} 的WebSocket客户端", account.name()); src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/macdAndMatrategy/MACD_MA_Strategy_Documentation
File was deleted src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/macdAndMatrategy/MacdMaStrategy.java
@@ -10,8 +10,6 @@ import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; /** @@ -24,11 +22,20 @@ public class MacdMaStrategy { /** 持仓状态枚举 */ public enum OperationType { /** 开仓平仓 */ open, close } /** 持仓状态枚举 */ public enum PositionType { /** 多头持仓 */ LONG, LONG_BUY, LONG_SELL, /** 空头持仓 */ SHORT, SHORT_SELL, SHORT_BUY, /** 空仓 */ NONE } @@ -40,10 +47,6 @@ private int volatilityPeriod; // 波动率计算周期 private BigDecimal stopLossRatio; // 止损比例 private BigDecimal takeProfitRatio; // 止盈比例 // 持仓信息 private BigDecimal entryPrice; // 开仓价格 private long entryTime; // 开仓时间戳 /** * 默认构造函数,使用标准MACD参数 @@ -73,9 +76,6 @@ this.stopLossRatio = stopLossRatio; this.takeProfitRatio = takeProfitRatio; // 初始化持仓状态为空仓 this.entryPrice = BigDecimal.ZERO; this.entryTime = 0; } /** @@ -84,7 +84,7 @@ * @param closePrices 收盘价序列 * @return 生成的交易信号(LONG、SHORT或NONE) */ public PositionType analyze(List<BigDecimal> closePrices) { public PositionType analyzeOpen(List<BigDecimal> closePrices) { // 数据检查:确保有足够的数据点进行计算 if (closePrices == null || closePrices.size() < 34) { return PositionType.NONE; // 数据不足,无法生成信号 @@ -109,17 +109,49 @@ // 多头开仓条件检查 if (isLongEntryCondition(macdResult, closePrices, volatility.getValue())) { // 执行开多 this.entryPrice = latestPrice; this.entryTime = System.currentTimeMillis(); return PositionType.LONG; log.info( "多头开仓信号,价格:{}", latestPrice); return PositionType.LONG_BUY; } // 空头开仓条件检查 if (isShortEntryCondition(macdResult, closePrices, volatility.getValue())) { // 执行开空 this.entryPrice = latestPrice; this.entryTime = System.currentTimeMillis(); return PositionType.SHORT; log.info( "空头开仓信号,价格:{}", latestPrice); return PositionType.SHORT_SELL; } // 无信号 return PositionType.NONE; } /** * 分析最新价格数据并生成交易信号 * * @param closePrices 收盘价序列 * @return 生成的交易信号(LONG、SHORT或NONE) */ public PositionType analyzeClose(List<BigDecimal> closePrices) { // 数据检查:确保有足够的数据点进行计算 if (closePrices == null || closePrices.size() < 34) { return PositionType.NONE; // 数据不足,无法生成信号 } // 1. 计算MACD指标 MACDResult macdResult = MACDCalculator.calculateMACD( closePrices, shortPeriod, longPeriod, signalPeriod); // 最新收盘价 BigDecimal latestPrice = closePrices.get(closePrices.size() - 1); if (isLongExitCondition(macdResult, latestPrice)) { // 执行平多 log.info( "多头平仓信号,价格:{}", latestPrice); return PositionType.LONG_SELL; } if (isShortExitCondition(macdResult, latestPrice)) { // 执行平空 log.info( "空头平仓信号,价格:{}", latestPrice); return PositionType.SHORT_BUY; } // 无信号 @@ -159,16 +191,31 @@ * @param historicalPrices 历史价格序列 * @return 交易指令(包含side和posSide),如果没有交易信号则返回null */ public TradingOrder generateTradingOrder(List<BigDecimal> historicalPrices) { PositionType signal = analyze(historicalPrices); public TradingOrder generateTradingOrder(List<BigDecimal> historicalPrices,String operation) { PositionType signal = null; if ( operation == OperationType.open.name()){ signal = analyzeOpen(historicalPrices); }else if ( operation == OperationType.close.name()){ signal = analyzeClose(historicalPrices); } // 根据信号和当前持仓状态生成交易指令 if (signal == PositionType.LONG) { if (signal == PositionType.LONG_BUY) { // 开多:买入开多(side 填写 buy; posSide 填写 long ) return new TradingOrder("buy", "long"); } else if (signal == PositionType.SHORT) { } else if (signal == PositionType.LONG_SELL) { // 开空:卖出开空(side 填写 sell; posSide 填写 short ) return new TradingOrder("sell", "long"); } else if (signal == PositionType.SHORT_SELL) { // 开空:卖出开空(side 填写 sell; posSide 填写 short ) return new TradingOrder("sell", "short"); } else if (signal == PositionType.SHORT_BUY) { // 开空:卖出开空(side 填写 sell; posSide 填写 short ) return new TradingOrder("buy", "short"); } // 没有交易信号 return null; @@ -183,108 +230,108 @@ * @return 是否满足多头开仓条件 */ private boolean isLongEntryCondition(MACDResult macdResult, List<BigDecimal> closePrices, BigDecimal volatility) { // 1. EMA金叉检查(简化为只检查当前短期EMA > 当前长期EMA) List<PriceData> macdData = macdResult.getMacdData(); if (macdData.size() < 1) { return false; // 2. MACD金叉且柱状线扩张检查 boolean isMacdFavorable = isMacdGoldenCrossAndExpanding(macdResult); // 3. MACD柱状线必须为正 boolean macdPositive = isMacdPositive(macdResult); // 4. 波动率过滤(必须在合理范围内) boolean volatilityFilter = isVolatilityInRange(volatility); if (macdPositive && volatilityFilter && isMacdFavorable) { log.info("多头信号形成, MACD有利状态: {}, 柱状线为正: {}, 波动率过滤: {}", isMacdFavorable, macdPositive, volatilityFilter); } boolean emaGoldenCross = isMacdGoldenCrossAndExpanding(macdResult); PriceData latest = macdData.get(macdData.size() - 1); // 2. MACD柱状线为正 boolean macdPositive = latest.getMacdHist().compareTo(BigDecimal.ZERO) > 0; // 3. 简化的波动率检查 boolean volatilityFilter = volatility.compareTo(BigDecimal.ZERO) > 0; if ( emaGoldenCross && macdPositive && volatilityFilter){ log.info( "EMA金叉: {}", emaGoldenCross); log.info( "MACD柱状线为正: {}" ,macdPositive); log.info( "波动率过滤: {}" ,volatilityFilter); } // 只需要EMA短期在长期上方、MACD柱状线为正且波动率大于0 return emaGoldenCross && macdPositive && volatilityFilter; // 所有条件必须同时满足 return macdPositive && volatilityFilter && isMacdFavorable; } /** * 空头开仓条件检查 * * * @param macdResult MACD计算结果 * @param closePrices 收盘价序列 * @param volatility 当前波动率 * @return 是否满足空头开仓条件 */ private boolean isShortEntryCondition(MACDResult macdResult, List<BigDecimal> closePrices, BigDecimal volatility) { List<PriceData> macdData = macdResult.getMacdData(); if (macdData.size() < 1) { return false; } // 2. MACD柱状线收缩+死叉检查 boolean macdDeathCross = isMacdDeathCrossAndContracting(macdResult); // 2. MACD柱状线为负 PriceData latest = macdData.get(macdData.size() - 1); boolean macdPositive = latest.getMacdHist().compareTo(BigDecimal.ZERO) < 0; // 4. 波动率过滤检查(0.5% ~ 5%) private boolean isShortEntryCondition(MACDResult macdResult, List<BigDecimal> closePrices, BigDecimal volatility) { // 2. MACD死叉且柱状线收缩检查 boolean isMacdFavorable = isMacdDeathCrossAndContracting(macdResult); // 3. MACD柱状线必须为负 boolean macdNegative = isMacdNegative(macdResult); // 4. 波动率过滤(必须在合理范围内) boolean volatilityFilter = isVolatilityInRange(volatility); if ( macdDeathCross&& volatilityFilter){ log.info( "MACD柱状线收缩+死叉: {}", macdDeathCross); log.info( "MACD柱状线为负: {}" ,macdPositive); log.info( "波动率过滤: {}", volatilityFilter); if (macdNegative && volatilityFilter && isMacdFavorable) { log.info("空头信号形成, MACD有利状态: {}, 柱状线为负: {}, 波动率过滤: {}", isMacdFavorable, macdNegative, volatilityFilter); } // 所有条件必须同时满足 return macdDeathCross && volatilityFilter && macdPositive; return macdNegative && volatilityFilter && isMacdFavorable; } /** * EMA金叉检查 * * 多头平仓条件检查 * * @param macdResult MACD计算结果 * @return 是否形成EMA金叉 * @param currentPrice 当前价格 * @return 是否满足多头平仓条件 */ private boolean isEmaGoldenCross(MACDResult macdResult) { private boolean isLongExitCondition(MACDResult macdResult, BigDecimal currentPrice) { // 1. MACD柱状线由正转负(动量转变) List<PriceData> macdData = macdResult.getMacdData(); if (macdData.size() < 2) { return false; if (macdData.size() >= 2) { PriceData latest = macdData.get(macdData.size() - 1); PriceData previous = macdData.get(macdData.size() - 2); boolean momentumShift = previous.getMacdHist().compareTo(BigDecimal.ZERO) >= 0 && latest.getMacdHist().abs().compareTo(previous.getMacdHist().abs()) < 0; if (momentumShift) { return true; } } PriceData latest = macdData.get(macdData.size() - 1); PriceData previous = macdData.get(macdData.size() - 2); // 当前短期EMA > 当前长期EMA,并且前一期短期EMA <= 前一期长期EMA return latest.getEmaShort().compareTo(latest.getEmaLong()) > 0 && previous.getEmaShort().compareTo(previous.getEmaLong()) <= 0; return false; } /** * EMA死叉检查 * * 空头平仓条件检查 * * @param macdResult MACD计算结果 * @return 是否形成EMA死叉 * @param currentPrice 当前价格 * @return 是否满足空头平仓条件 */ private boolean isEmaDeathCross(MACDResult macdResult) { private boolean isShortExitCondition(MACDResult macdResult, BigDecimal currentPrice) { // 1. MACD柱状线由负转正(动量转变) List<PriceData> macdData = macdResult.getMacdData(); if (macdData.size() < 2) { return false; if (macdData.size() >= 2) { PriceData latest = macdData.get(macdData.size() - 1); PriceData previous = macdData.get(macdData.size() - 2); boolean momentumShift = previous.getMacdHist().compareTo(BigDecimal.ZERO) <= 0 && latest.getMacdHist().abs().compareTo(previous.getMacdHist().abs()) < 0; if (momentumShift) { return true; } } PriceData latest = macdData.get(macdData.size() - 1); PriceData previous = macdData.get(macdData.size() - 2); // 当前短期EMA < 当前长期EMA,并且前一期短期EMA >= 前一期长期EMA return latest.getEmaShort().compareTo(latest.getEmaLong()) < 0 && previous.getEmaShort().compareTo(previous.getEmaLong()) >= 0; return false; } /** * MACD金叉且柱状线扩张检查 * * <p> * 条件: * 1. DIF线从下往上穿过DEA线(金叉) * 2. MACD柱状线绝对值增大且为正值(动量增强) * * @param macdResult MACD计算结果 * @return 是否形成MACD金叉且柱状线扩张 * @return 是否形成MACD金叉或柱状线扩张 */ private boolean isMacdGoldenCrossAndExpanding(MACDResult macdResult) { List<PriceData> macdData = macdResult.getMacdData(); @@ -294,26 +341,30 @@ PriceData latest = macdData.get(macdData.size() - 1); PriceData previous = macdData.get(macdData.size() - 2); // 1. MACD金叉检查(DIF上穿DEA) boolean goldenCross = previous.getDif().compareTo(previous.getDea()) <= 0 && latest.getDif().compareTo(latest.getDea()) > 0; // 2. MACD柱状线扩张检查 boolean histogramExpanding = previous.getMacdHist().compareTo(latest.getMacdHist()) < 0 && latest.getMacdHist().compareTo(BigDecimal.ZERO) > 0; return goldenCross && histogramExpanding; PriceData prevPrev = macdData.get(macdData.size() - 3); // 金叉判断:DIF从下往上穿过DEA boolean isGoldenCross = prevPrev.getDif().compareTo(prevPrev.getDea()) <= 0 && previous.getDif().compareTo(previous.getDea()) > 0; // 柱状线扩张判断:连续正值且绝对值增大 boolean isExpanding = latest.getMacdHist().compareTo(BigDecimal.ZERO) > 0 && previous.getMacdHist().compareTo(BigDecimal.ZERO) > 0 && previous.getMacdHist().abs().compareTo(latest.getMacdHist().abs()) < 0; // 金叉或柱状线扩张任一满足即可 return isGoldenCross && isExpanding; } /** * MACD死叉且柱状线收缩检查 * * MACD柱状线扩张检查 * <p> * 条件:当前MACD柱状线绝对值大于前一根 * * @param macdResult MACD计算结果 * @return 是否形成MACD死叉且柱状线收缩 * @return MACD柱状线是否扩张 */ private boolean isMacdDeathCrossAndContracting(MACDResult macdResult) { private boolean isExpanding(MACDResult macdResult) { List<PriceData> macdData = macdResult.getMacdData(); if (macdData.size() < 2) { return false; @@ -321,18 +372,91 @@ PriceData latest = macdData.get(macdData.size() - 1); PriceData previous = macdData.get(macdData.size() - 2); // MACD柱状线扩张:当前绝对值大于前一根 return latest.getMacdHist().abs().compareTo(previous.getMacdHist().abs()) > 0; } // 1. MACD死叉检查(DIF下穿DEA) boolean deathCross = previous.getDif().compareTo(previous.getDea()) >= 0 && latest.getDif().compareTo(latest.getDea()) < 0; /** * MACD死叉且柱状线收缩检查 * <p> * 条件: * 1. DIF线从上往下穿过DEA线(死叉) * 2. MACD柱状线绝对值减小且为负值(动量减弱) * * @param macdResult MACD计算结果 * @return 是否形成MACD死叉或柱状线收缩 */ private boolean isMacdDeathCrossAndContracting(MACDResult macdResult) { List<PriceData> macdData = macdResult.getMacdData(); if (macdData.size() < 3) { return false; } // 2. MACD柱状线收缩检查(绝对值减小) boolean histogramContracting = previous.getMacdHist().abs().compareTo( latest.getMacdHist().abs()) < 0 && latest.getMacdHist().compareTo(BigDecimal.ZERO) < 0; PriceData latest = macdData.get(macdData.size() - 1); PriceData previous = macdData.get(macdData.size() - 2); PriceData prevPrev = macdData.get(macdData.size() - 3); // 死叉判断:DIF从上往下穿过DEA boolean isDeathCross = prevPrev.getDif().compareTo(prevPrev.getDea()) >= 0 && previous.getDif().compareTo(previous.getDea()) < 0; // 柱状线收缩判断:连续负值且绝对值减小 boolean isContracting = latest.getMacdHist().compareTo(BigDecimal.ZERO) < 0 && previous.getMacdHist().compareTo(BigDecimal.ZERO) < 0 && previous.getMacdHist().abs().compareTo(latest.getMacdHist().abs()) > 0; // 死叉或柱状线收缩任一满足即可 return isDeathCross && isContracting; } /** * MACD柱状线收缩检查 * <p> * 条件:前一根MACD柱状线绝对值大于当前 * * @param macdResult MACD计算结果 * @return MACD柱状线是否收缩 */ private boolean isContracting(MACDResult macdResult) { List<PriceData> macdData = macdResult.getMacdData(); if (macdData.size() < 2) { return false; } return deathCross && histogramContracting; PriceData latest = macdData.get(macdData.size() - 1); PriceData previous = macdData.get(macdData.size() - 2); // MACD柱状线收缩:前一根绝对值大于当前 return previous.getMacdHist().abs().compareTo(latest.getMacdHist().abs()) > 0; } /** * 检查MACD柱状线是否为正值 * * @param macdResult MACD计算结果 * @return MACD柱状线是否为正值 */ private boolean isMacdPositive(MACDResult macdResult) { List<PriceData> macdData = macdResult.getMacdData(); if (macdData.isEmpty()) { return false; } return macdData.get(macdData.size() - 1).getMacdHist().compareTo(BigDecimal.ZERO) > 0; } /** * 检查MACD柱状线是否为负值 * * @param macdResult MACD计算结果 * @return MACD柱状线是否为负值 */ private boolean isMacdNegative(MACDResult macdResult) { List<PriceData> macdData = macdResult.getMacdData(); if (macdData.isEmpty()) { return false; } return macdData.get(macdData.size() - 1).getMacdHist().compareTo(BigDecimal.ZERO) < 0; } /** @@ -349,22 +473,4 @@ volatility.compareTo(maxVolatility) <= 0; } /** * 获取开仓价格 * * @return 开仓价格 */ public BigDecimal getEntryPrice() { return entryPrice; } /** * 获取开仓时间戳 * * @return 开仓时间戳 */ public long getEntryTime() { return entryTime; } } src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/macdAndMatrategy/MacdMaStrategyTest.java
File was deleted src/main/java/com/xcong/excoin/modules/okxNewPrice/okxWs/OrderInfoWs.java
@@ -116,6 +116,29 @@ log.info("{}: 订单详情已完成: {}, 自定义编号: {}", accountName, CoinEnums.HE_YUE.getCode(), clOrdId); TradeRequestParam tradeRequestParam = new TradeRequestParam(); tradeRequestParam.setAccountName(accountName); BigDecimal zhiYingPx = getZhiYingPx( accountName, posSide, fillFee, WsMapBuild.parseBigDecimalSafe(InstrumentsWs.getAccountMap(accountName).get(CoinEnums.CTVAL.name())), WsMapBuild.parseBigDecimalSafe(accFillSz), WsMapBuild.parseBigDecimalSafe(InstrumentsWs.getAccountMap(accountName).get(CoinEnums.CONTRACTMULTIPLIER.name())), WsMapBuild.parseBigDecimalSafe(avgPx), WsMapBuild.parseBigDecimalSafe(CoinEnums.LEVERAGE.getCode()) ); tradeRequestParam.setMarkPx(String.valueOf(zhiYingPx)); tradeRequestParam.setInstId(CoinEnums.HE_YUE.getCode()); tradeRequestParam.setTdMode(CoinEnums.CROSS.getCode()); tradeRequestParam.setPosSide(posSide); tradeRequestParam.setOrdType(CoinEnums.ORDTYPE_LIMIT.getCode()); tradeRequestParam.setTradeType(OrderParamEnums.TRADE_YES.getValue()); tradeRequestParam.setSide(CoinEnums.POSSIDE_LONG.getCode().equals(posSide) ? CoinEnums.SIDE_SELL.getCode() : CoinEnums.SIDE_BUY.getCode()); tradeRequestParam.setClOrdId(WsParamBuild.getOrderNum(tradeRequestParam.getSide())); tradeRequestParam.setSz(accFillSz); return tradeRequestParam; } return null; } src/main/java/com/xcong/excoin/modules/okxNewPrice/okxWs/PositionsWs.java
@@ -63,7 +63,7 @@ public static void handleEvent(JSONObject response, String accountName) { log.info("开始执行PositionsWs......"); // log.info("开始执行PositionsWs......"); try { JSONArray dataArray = response.getJSONArray("data"); if (dataArray == null || dataArray.isEmpty()) { @@ -75,7 +75,7 @@ JSONObject posData = dataArray.getJSONObject(i); String instId = posData.getString("instId"); if (CoinEnums.HE_YUE.getCode().equals(instId)) { log.info("查询到账户{}持仓数据",CoinEnums.HE_YUE.getCode()); // log.info("查询到账户{}持仓数据",CoinEnums.HE_YUE.getCode()); String mgnMode = posData.getString("mgnMode"); String posSide = posData.getString("posSide"); String pos = posData.getString("pos"); @@ -97,18 +97,18 @@ String settledPnl = posData.getString("settledPnl"); String fee = posData.getString("fee"); String fundingFee = posData.getString("fundingFee"); log.info( "{}: 账户持仓频道-产品类型: {}, 保证金模式: {}, 持仓方向: {}, 持仓数量: {}, 开仓平均价: {}, " + "未实现收益: {}, 未实现收益率: {}, 杠杆倍数: {}, 预估强平价: {}, 初始保证金: {}, " + "维持保证金率: {}, 维持保证金: {}, 以美金价值为单位的持仓数量: {}, 占用保证金的币种: {}, " + "最新成交价: {}, 最新指数价格: {}, 盈亏平衡价: {}, 已实现收益: {}, 累计已结算收益: {}" + "最新标记价格: {},累计手续费: {},累计持仓费: {},", initAccountName(accountName, posSide), instId, mgnMode, posSide, pos, avgPx, upl, uplRatio, lever, liqPx, imr, mgnRatio, mmr, notionalUsd, ccy, last, idxPx, bePx, realizedPnl, settledPnl, markPx,fee,fundingFee ); // log.info( // "{}: 账户持仓频道-产品类型: {}, 保证金模式: {}, 持仓方向: {}, 持仓数量: {}, 开仓平均价: {}, " // + "未实现收益: {}, 未实现收益率: {}, 杠杆倍数: {}, 预估强平价: {}, 初始保证金: {}, " // + "维持保证金率: {}, 维持保证金: {}, 以美金价值为单位的持仓数量: {}, 占用保证金的币种: {}, " // + "最新成交价: {}, 最新指数价格: {}, 盈亏平衡价: {}, 已实现收益: {}, 累计已结算收益: {}" // + "最新标记价格: {},累计手续费: {},累计持仓费: {},", // initAccountName(accountName, posSide), instId, mgnMode, posSide, pos, avgPx, // upl, uplRatio, lever, liqPx, imr, // mgnRatio, mmr, notionalUsd, ccy, // last, idxPx, bePx, realizedPnl, settledPnl, // markPx,fee,fundingFee // ); initParam(posData, accountName,posSide); } } src/main/java/com/xcong/excoin/modules/okxNewPrice/okxWs/TradeOrderWs.java
@@ -74,6 +74,10 @@ log.warn("下单参数缺失,取消发送"); return; } if (BigDecimal.ZERO.compareTo(new BigDecimal(sz)) >= 0) { log.warn("下单数量{}不允许下单,取消发送", sz); return; } /** * 检验账户和仓位是否准备就绪 @@ -82,18 +86,23 @@ * 平多:卖出平多(side 填写 sell;posSide 填写 long ) * 平空:买入平空(side 填写 buy; posSide 填写 short ) 需要检验仓位通道是否准备就绪 */ //买入开多、卖出开空则验证仓位通道是否准备就绪 String positionAccountName = PositionsWs.initAccountName(accountName, posSide); BigDecimal positionsReadyState = PositionsWs.getAccountMap(positionAccountName).get(CoinEnums.READY_STATE.name()) == null ? BigDecimal.ZERO : PositionsWs.getAccountMap(positionAccountName).get(CoinEnums.READY_STATE.name()); if (WsMapBuild.parseBigDecimalSafe(CoinEnums.READY_STATE_YES.getCode()).compareTo(positionsReadyState) != 0) { log.info("仓位{}通道未就绪,取消发送",positionAccountName); return; } String accountReadyState = AccountWs.getAccountMap(accountName).get(CoinEnums.READY_STATE.name()); if (!CoinEnums.READY_STATE_YES.getCode().equals(accountReadyState)) { log.info("账户通道未就绪,取消发送"); return; boolean b = posSide.equals(CoinEnums.POSSIDE_LONG.getCode()) && side.equals(CoinEnums.SIDE_BUY.getCode()); boolean c = posSide.equals(CoinEnums.POSSIDE_SHORT.getCode()) && side.equals(CoinEnums.SIDE_SELL.getCode()); if ( b || c ){ BigDecimal positionsReadyState = PositionsWs.getAccountMap(positionAccountName).get(CoinEnums.READY_STATE.name()) == null ? BigDecimal.ZERO : PositionsWs.getAccountMap(positionAccountName).get(CoinEnums.READY_STATE.name()); if (WsMapBuild.parseBigDecimalSafe(CoinEnums.READY_STATE_YES.getCode()).compareTo(positionsReadyState) != 0) { log.info("仓位{}通道未就绪,取消发送",positionAccountName); return; } String accountReadyState = AccountWs.getAccountMap(accountName).get(CoinEnums.READY_STATE.name()); if (!CoinEnums.READY_STATE_YES.getCode().equals(accountReadyState)) { log.info("账户通道未就绪,取消发送"); return; } } try { @@ -126,7 +135,7 @@ } } public static void orderZhiYingEvent(WebSocketClient webSocketClient, TradeRequestParam tradeRequestParam) { public static void orderZhiYingZhiSunEventNoState(WebSocketClient webSocketClient, TradeRequestParam tradeRequestParam) { log.info("开始执行TradeOrderWs......");