Administrator
2025-12-29 24bd0399d73a2d48e50233326fca7d9526f06259
refactor(trading): 重构MACD策略交易信号生成逻辑

- 添加OperationType枚举用于区分开仓和平仓操作
- 分离analyze方法为analyzeOpen和analyzeClose两个独立方法
- 移除类中的持仓状态字段(entryPrice和entryTime)
- 修改generateTradingOrder方法支持操作类型参数
- 调整MACD交叉条件判断逻辑,金叉和柱状线扩张需同时满足
- 调整死叉和柱状线收缩条件判断逻辑,两者需同时满足
- 删除测试类MacdMaStrategyTest
- 在WebSocket客户端中分别处理开仓和平仓信号
- 优化仓位通道就绪状态验证逻辑,仅在开仓时检查
1 files deleted
3 files modified
395 ■■■■ changed files
src/main/java/com/xcong/excoin/modules/okxNewPrice/OkxKlineWebSocketClient.java 59 ●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/macdAndMatrategy/MacdMaStrategy.java 94 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/macdAndMatrategy/MacdMaStrategyTest.java 217 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/okxNewPrice/okxWs/TradeOrderWs.java 25 ●●●●● patch | view | raw | blame | history
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;
@@ -344,8 +345,9 @@
                            .collect(Collectors.toList());
                    log.info("生成100个15分钟价格数据点成功!");
                    // 使用策略分析最新价格数据
                    MacdMaStrategy.TradingOrder tradingOrder = strategy.generateTradingOrder(historicalPrices);
                    if (tradingOrder == null){
                    MacdMaStrategy.TradingOrder tradingOrderOpen = strategy.generateTradingOrder(historicalPrices,MacdMaStrategy.OperationType.open.name());
                    MacdMaStrategy.TradingOrder tradingOrderClose = strategy.generateTradingOrder(historicalPrices,MacdMaStrategy.OperationType.close.name());
                    if (tradingOrderOpen == null && tradingOrderClose == null){
                        return;
                    }
                    Collection<OkxQuantWebSocketClient> allClients = clientManager.getAllClients();
@@ -357,38 +359,37 @@
                    for (OkxQuantWebSocketClient client : clientManager.getAllClients()) {
                        String accountName = client.getAccountName();
                        if (accountName != null) {
                            // 根据信号执行交易操作
                            TradeRequestParam tradeRequestParam = new TradeRequestParam();
                            if (ObjectUtil.isNotEmpty(tradingOrderOpen)){
                                // 根据信号执行交易操作
                                TradeRequestParam tradeRequestParam = new TradeRequestParam();
                            String posSide = tradingOrder.getPosSide();
                            tradeRequestParam.setPosSide(posSide);
                            String currentPrice = String.valueOf(closePx);
                            tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, currentPrice, posSide);
                                String posSide = tradingOrderOpen.getPosSide();
                                tradeRequestParam.setPosSide(posSide);
                                String currentPrice = String.valueOf(closePx);
                                tradeRequestParam = caoZuoService.caoZuoStrategy(accountName, currentPrice, posSide);
                            String side = tradingOrder.getSide();
                            tradeRequestParam.setSide(side);
                                String side = tradingOrderOpen.getSide();
                                tradeRequestParam.setSide(side);
                            String clOrdId = WsParamBuild.getOrderNum(side);
                            tradeRequestParam.setClOrdId(clOrdId);
                                String clOrdId = WsParamBuild.getOrderNum(side);
                                tradeRequestParam.setClOrdId(clOrdId);
                            String sz = null;
                            if (
                                    (posSide == CoinEnums.POSSIDE_LONG.getCode() && side == CoinEnums.SIDE_BUY.getCode())
                                            ||
                                            (posSide == CoinEnums.POSSIDE_SHORT.getCode() && side == CoinEnums.SIDE_SELL.getCode())
                            ){
                                sz = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.BUY_CNT_INIT.name());
                            }else if (
                                    (posSide == CoinEnums.POSSIDE_LONG.getCode() && side == CoinEnums.SIDE_SELL.getCode())
                                            ||
                                            (posSide == CoinEnums.POSSIDE_SHORT.getCode() && side == CoinEnums.SIDE_BUY.getCode())
                            ){
                                BigDecimal pos = PositionsWs.getAccountMap(PositionsWs.initAccountName(accountName, posSide)).get("pos");
                                sz = String.valueOf(pos);
                                String sz = InstrumentsWs.getAccountMap(accountName).get(CoinEnums.BUY_CNT_INIT.name());
                                tradeRequestParam.setSz(sz);
                                TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam);
                            }
                            tradeRequestParam.setSz(sz);
                            TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam);
                            if (ObjectUtil.isNotEmpty(tradingOrderClose)){
                                // 根据信号执行交易操作
                                TradeRequestParam tradeRequestParam = new TradeRequestParam();
                                String posSide = tradingOrderClose.getPosSide();
                                tradeRequestParam.setPosSide(posSide);
                                String currentPrice = String.valueOf(closePx);
                                tradeRequestParam = caoZuoService.caoZuoZhiSunEvent(accountName, currentPrice, posSide);
                                TradeOrderWs.orderEvent(client.getWebSocketClient(), tradeRequestParam);
                            }
                        }
                    }
                }
src/main/java/com/xcong/excoin/modules/okxNewPrice/indicator/macdAndMatrategy/MacdMaStrategy.java
@@ -22,6 +22,13 @@
public class MacdMaStrategy {
    /** 持仓状态枚举 */
    public enum OperationType {
        /** 开仓平仓 */
        open,
        close
    }
    /** 持仓状态枚举 */
    public enum PositionType {
        /** 多头持仓 */
        LONG_BUY,
@@ -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,24 +109,49 @@
        // 多头开仓条件检查
        if (isLongEntryCondition(macdResult, closePrices, volatility.getValue())) {
            // 执行开多
            this.entryPrice = latestPrice;
            this.entryTime = System.currentTimeMillis();
            log.info( "多头开仓信号,价格:{}", latestPrice);
            return PositionType.LONG_BUY;
        }
        // 空头开仓条件检查
        if (isShortEntryCondition(macdResult, closePrices, volatility.getValue())) {
            // 执行开空
            this.entryPrice = latestPrice;
            this.entryTime = System.currentTimeMillis();
             log.info( "空头开仓信号,价格:{}", latestPrice);
            return PositionType.SHORT_SELL;
        }
         if (isLongExitCondition(macdResult, latestPrice)) {
              // 执行平多
             return PositionType.LONG_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; // 数据不足,无法生成信号
        }
         if (isShortExitCondition(macdResult, latestPrice)) {
              return PositionType.SHORT_BUY;
        // 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;
        }
        // 无信号
@@ -166,9 +191,18 @@
     * @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()){
            analyzeClose(historicalPrices);
        }
        // 根据信号和当前持仓状态生成交易指令
        if (signal == PositionType.LONG_BUY) {
            // 开多:买入开多(side 填写 buy; posSide 填写 long )
@@ -319,7 +353,7 @@
                            previous.getMacdHist().abs().compareTo(latest.getMacdHist().abs()) < 0;
        
        // 金叉或柱状线扩张任一满足即可
        return isGoldenCross || isExpanding;
        return isGoldenCross && isExpanding;
    }
    
    /**
@@ -373,7 +407,7 @@
                              previous.getMacdHist().abs().compareTo(latest.getMacdHist().abs()) > 0;
        
        // 死叉或柱状线收缩任一满足即可
        return isDeathCross || isContracting;
        return isDeathCross && isContracting;
    }
    
    /**
@@ -439,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/TradeOrderWs.java
@@ -86,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 {