From 14cbc45601f022b9293e1f84c8ff7bf7f5ddd55a Mon Sep 17 00:00:00 2001 From: liruiqiang <3151805288@qq.com> Date: Wed, 19 Nov 2025 13:59:09 +0800 Subject: [PATCH] =?UTF-8?q?MA=E6=8C=87=E6=A0=87=E4=B8=8ECCI=E6=8C=87?= =?UTF-8?q?=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/deepchart/controller/IndexController.java | 14 ++ .../java/com/deepchart/service/IndexService.java | 4 + .../deepchart/service/impl/IndexServiceImpl.java | 12 ++ src/main/java/com/deepchart/utils/CCIUtil.java | 118 ++++++++++++++++ src/main/java/com/deepchart/utils/MATools.java | 157 --------------------- src/main/java/com/deepchart/utils/MAUtil.java | 102 +++++++++++++ 6 files changed, 250 insertions(+), 157 deletions(-) create mode 100644 src/main/java/com/deepchart/utils/CCIUtil.java delete mode 100644 src/main/java/com/deepchart/utils/MATools.java create mode 100644 src/main/java/com/deepchart/utils/MAUtil.java diff --git a/src/main/java/com/deepchart/controller/IndexController.java b/src/main/java/com/deepchart/controller/IndexController.java index 502422e..463bb42 100644 --- a/src/main/java/com/deepchart/controller/IndexController.java +++ b/src/main/java/com/deepchart/controller/IndexController.java @@ -25,4 +25,18 @@ public class IndexController { String result = indexService.macd(list); return Result.success("success", result); } + + @PostMapping("/ma") + public Result ma(@RequestBody StockInfo stock) { + List list = indexService.getStockData(stock); + String result = indexService.ma(list); + return Result.success("success", result); + } + + @PostMapping("/cci") + public Result cci(@RequestBody StockInfo stock) { + List list = indexService.getStockData(stock); + String result = indexService.cci(list); + return Result.success("success", result); + } } diff --git a/src/main/java/com/deepchart/service/IndexService.java b/src/main/java/com/deepchart/service/IndexService.java index 5a84fd2..7e92945 100644 --- a/src/main/java/com/deepchart/service/IndexService.java +++ b/src/main/java/com/deepchart/service/IndexService.java @@ -9,4 +9,8 @@ public interface IndexService { List getStockData(StockInfo stock); String macd(List list); + + String ma(List list); + + String cci(List list); } diff --git a/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java b/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java index 28da9f5..a2c495f 100644 --- a/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java +++ b/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java @@ -3,7 +3,9 @@ package com.deepchart.service.impl; import com.deepchart.entity.StockDailyData; import com.deepchart.entity.StockInfo; import com.deepchart.service.IndexService; +import com.deepchart.utils.CCIUtil; import com.deepchart.utils.MACDUtil; +import com.deepchart.utils.MAUtil; import com.deepchart.utils.StockDataUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -25,4 +27,14 @@ public class IndexServiceImpl implements IndexService { public String macd(List list) { return MACDUtil.analyzeStock(list); } + + @Override + public String ma(List list) { + return MAUtil.analyzeMA(list); + } + + @Override + public String cci(List list) { + return CCIUtil.analyzeCCI(list); + } } diff --git a/src/main/java/com/deepchart/utils/CCIUtil.java b/src/main/java/com/deepchart/utils/CCIUtil.java new file mode 100644 index 0000000..533ed22 --- /dev/null +++ b/src/main/java/com/deepchart/utils/CCIUtil.java @@ -0,0 +1,118 @@ +package com.deepchart.utils; + +import com.deepchart.entity.StockDailyData; +import java.util.ArrayList; +import java.util.List; + +public class CCIUtil { + + private static final int CCI_PERIOD = 14; // CCI默认周期 + + public static String analyzeCCI(List stockDataList) { + if (stockDataList == null || stockDataList.size() < CCI_PERIOD) { + return "CCI分析失败:数据不足,至少需要" + CCI_PERIOD + "个交易日"; + } + + // 1. 计算CCI指标值 + double currentCCI = calculateCCI(stockDataList); + double prevCCI = calculateCCI(stockDataList.subList(0, stockDataList.size() - 1)); + + // 2. 计算20日均线(用于判断价格位置) + double twentyDayMA = calculateMA(stockDataList, 20); + double currentClose = stockDataList.get(stockDataList.size() - 1).getClosePrice(); + String pricePosition = (currentClose > twentyDayMA) ? "20日均线之上" : "20日均线之下"; + + // 3. 判断信号类型 + String signalType = "无信号"; + String signalDescription = "CCI值未触发标准信号"; + + if (prevCCI <= -100 && currentCCI > -100) { + signalType = "超卖反弹买入信号"; + signalDescription = "CCI从超卖区上穿-100线,进入常态区间"; + } else if (prevCCI >= 100 && currentCCI < 100) { + signalType = "超买回调卖出信号"; + signalDescription = "CCI从超买区下穿+100线,进入常态区间"; + } else if (currentCCI > 100) { + signalType = "强势上涨信号"; + signalDescription = "CCI持续在+100上方,处于强势上涨趋势"; + } else if (currentCCI < -100) { + signalType = "强势下跌信号"; + signalDescription = "CCI持续在-100下方,处于强势下跌趋势"; + } + + // 4. 生成分析报告 + return generateReport(currentCCI, signalType, signalDescription, pricePosition, currentClose, twentyDayMA); + } + + private static double calculateCCI(List stockDataList) { + int n = stockDataList.size(); + List typicalPrices = new ArrayList<>(); + + // 计算典型价格 (H+L+C)/3 + for (StockDailyData data : stockDataList) { + double typicalPrice = (data.getHighPrice() + data.getLowPrice() + data.getClosePrice()) / 3; + typicalPrices.add(typicalPrice); + } + + // 计算N期平均典型价格 + double avgTypicalPrice = calculateMAFromDoubleList(typicalPrices, CCI_PERIOD); + + // 计算平均偏差 + double sumAbsDev = 0.0; + for (int i = n - CCI_PERIOD; i < n; i++) { + sumAbsDev += Math.abs(typicalPrices.get(i) - avgTypicalPrice); + } + double avgDev = sumAbsDev / CCI_PERIOD; + + // 计算CCI + return (typicalPrices.get(n - 1) - avgTypicalPrice) / (0.015 * avgDev); + } + + private static double calculateMA(List stockDataList, int period) { + if (stockDataList.size() < period) { + throw new IllegalArgumentException("数据不足,无法计算" + period + "日均线"); + } + double sum = 0.0; + for (int i = stockDataList.size() - period; i < stockDataList.size(); i++) { + sum += stockDataList.get(i).getClosePrice(); + } + return sum / period; + } + + private static double calculateMAFromDoubleList(List prices, int period) { + if (prices.size() < period) { + throw new IllegalArgumentException("数据不足,无法计算" + period + "日均线"); + } + double sum = 0.0; + for (int i = prices.size() - period; i < prices.size(); i++) { + sum += prices.get(i); + } + return sum / period; + } + + private static String generateReport(double cciValue, String signalType, String signalDescription, + String pricePosition, double currentClose, double twentyDayMA) { + return String.format( + "CCI分析报告(14日周期)\n" + + "当前CCI值: %.1f | 信号类型: %s | 信号描述: %s\n" + + "价格位置: %s | 当前价格: %.2f | 20日均线: %.2f\n" + + "核心逻辑: %s", + cciValue, signalType, signalDescription, + pricePosition, currentClose, twentyDayMA, + getSignalLogic(signalType, cciValue) + ); + } + + private static String getSignalLogic(String signalType, double cciValue) { + if (signalType.contains("买入信号")) { + return "CCI上穿-100线 + 价格站上20日均线 + 市场情绪从超卖转为常态"; + } else if (signalType.contains("卖出信号")) { + return "CCI下穿+100线 + 价格回调至常态区间 + 市场情绪从超买转为常态"; + } else if (signalType.contains("强势上涨")) { + return "CCI持续在+100以上,趋势强劲,应持有多头仓位"; + } else if (signalType.contains("强势下跌")) { + return "CCI持续在-100以下,趋势疲软,应持币观望"; + } + return "CCI在常态区间(-100~100),无明确信号"; + } +} \ No newline at end of file diff --git a/src/main/java/com/deepchart/utils/MATools.java b/src/main/java/com/deepchart/utils/MATools.java deleted file mode 100644 index bea7e6f..0000000 --- a/src/main/java/com/deepchart/utils/MATools.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.deepchart.utils; - -import com.deepchart.entity.StockDailyData; - -import java.time.LocalDate; -import java.util.*; - -public class MATools { - - public static class MAAnalysisResult { - private String trend; // 趋势方向:上升 / 下降 / 震荡 - private String crossSignal; // 金叉 / 死叉 / 无信号 - private String maArrangement; // 多头排列 / 空头排列 / 均线粘合 - private double shortMA; // 短期均线值(如5日) - private double midMA; // 中期均线值(如20日) - private Double longMA; // 长期均线值(如30日,可能为 null) - - // Getters and Setters - public String getTrend() { return trend; } - public void setTrend(String trend) { this.trend = trend; } - - public String getCrossSignal() { return crossSignal; } - public void setCrossSignal(String crossSignal) { this.crossSignal = crossSignal; } - - public String getMaArrangement() { return maArrangement; } - public void setMaArrangement(String maArrangement) { this.maArrangement = maArrangement; } - - public double getShortMA() { return shortMA; } - public void setShortMA(double shortMA) { this.shortMA = shortMA; } - - public double getMidMA() { return midMA; } - public void setMidMA(double midMA) { this.midMA = midMA; } - - public Double getLongMA() { return longMA; } - public void setLongMA(Double longMA) { this.longMA = longMA; } - - @Override - public String toString() { - return "MAAnalysisResult{" + - "trend='" + trend + '\'' + - ", crossSignal='" + crossSignal + '\'' + - ", maArrangement='" + maArrangement + '\'' + - ", shortMA=" + shortMA + - ", midMA=" + midMA + - ", longMA=" + longMA + - '}'; - } - } - - /** - * 对股票日线数据进行MA指标分析 - * @param stockDataList 至少包含20个交易日的StockDailyData列表(按时间升序排列) - * @return MA分析结果 - */ - public static MAAnalysisResult analyze(List stockDataList) { - if (stockDataList == null || stockDataList.size() < 20) { - throw new IllegalArgumentException("数据不足,至少需要20个交易日的收盘价"); - } - - // 提取收盘价(确保按时间顺序,最新在最后) - List closePrices = new ArrayList<>(); - for (StockDailyData data : stockDataList) { - closePrices.add(data.getClosePrice()); - } - - int n = closePrices.size(); - - // 定义周期 - int shortPeriod = 5; - int midPeriod = 20; - int longPeriod = 30; // 因为只有40天数据,60日无法计算,改用30日 - - // 计算MA(取最近一个值) - double shortMA = calculateMA(closePrices, shortPeriod); - double midMA = calculateMA(closePrices, midPeriod); - Double longMA = n >= longPeriod ? calculateMA(closePrices, longPeriod) : null; - - MAAnalysisResult result = new MAAnalysisResult(); - result.setShortMA(shortMA); - result.setMidMA(midMA); - result.setLongMA(longMA); - - // 1. 趋势判断(基于短中长期均线相对位置) - String trend = "震荡趋势"; - if (shortMA > midMA && (longMA == null || midMA > longMA)) { - trend = "上升趋势"; - } else if (shortMA < midMA && (longMA == null || midMA < longMA)) { - trend = "下降趋势"; - } - result.setTrend(trend); - - // 2. 金叉/死叉判断(5日 vs 20日) - String crossSignal = "无信号"; - // 需要前一日数据才能判断交叉,这里简化:仅用当前值判断是否刚发生交叉(实际应比较前后两日) - // 更严谨的做法是保留前一天的MA值,但此处假设调用方只关心当前状态下的潜在信号 - // 我们采用“当前短线上穿中线”作为金叉近似判断(需注意这是简化版) - if (shortMA > midMA) { - // 检查昨日是否 short <= mid(需要倒数第2个MA值) - if (n >= Math.max(shortPeriod, midPeriod) + 1) { - double prevShortMA = calculateMA(closePrices.subList(0, n - 1), shortPeriod); - double prevMidMA = calculateMA(closePrices.subList(0, n - 1), midPeriod); - if (prevShortMA <= prevMidMA) { - crossSignal = "金叉信号"; - } - } - } else if (shortMA < midMA) { - if (n >= Math.max(shortPeriod, midPeriod) + 1) { - double prevShortMA = calculateMA(closePrices.subList(0, n - 1), shortPeriod); - double prevMidMA = calculateMA(closePrices.subList(0, n - 1), midPeriod); - if (prevShortMA >= prevMidMA) { - crossSignal = "死叉信号"; - } - } - } - result.setCrossSignal(crossSignal); - - // 3. 均线排列 - String arrangement = "均线粘合"; - if (longMA != null) { - if (shortMA > midMA && midMA > longMA) { - arrangement = "多头排列"; - } else if (shortMA < midMA && midMA < longMA) { - arrangement = "空头排列"; - } - } else { - // 无长期均线时,仅用短中判断 - if (Math.abs(shortMA - midMA) / midMA < 0.01) { // 差距小于1% - arrangement = "均线粘合"; - } else if (shortMA > midMA) { - arrangement = "短期强于中期"; - } else { - arrangement = "短期弱于中期"; - } - } - result.setMaArrangement(arrangement); - - return result; - } - - /** - * 计算最近一期的简单移动平均值(SMA) - * @param prices 收盘价列表(时间升序,最新在末尾) - * @param period 周期 - * @return 最新一期的MA值 - */ - private static double calculateMA(List prices, int period) { - if (prices.size() < period) { - throw new IllegalArgumentException("价格数据长度不足,无法计算" + period + "日均线"); - } - double sum = 0.0; - int n = prices.size(); - for (int i = n - period; i < n; i++) { - sum += prices.get(i); - } - return sum / period; - } -} \ No newline at end of file diff --git a/src/main/java/com/deepchart/utils/MAUtil.java b/src/main/java/com/deepchart/utils/MAUtil.java new file mode 100644 index 0000000..a65eb04 --- /dev/null +++ b/src/main/java/com/deepchart/utils/MAUtil.java @@ -0,0 +1,102 @@ +package com.deepchart.utils; + +import com.deepchart.entity.StockDailyData; + +import java.util.*; + +public class MAUtil { + + // 周期定义:短期5日、中期20日、长期40日 + private static final int SHORT_PERIOD = 5; + private static final int MID_PERIOD = 20; + private static final int LONG_PERIOD = 40; + + /** + * 对股票日线数据进行MA指标分析,返回JSON格式的中文字符串结果 + * @param stockDataList 至少包含40个交易日的StockDailyData列表(按时间升序排列) + * @return JSON格式的分析结果字符串(全中文内容) + */ + public static String analyzeMA(List stockDataList) { + if (stockDataList == null || stockDataList.size() < LONG_PERIOD) { + return buildJsonResult("错误", "无信号", "数据不足,至少需要40个交易日", 0.0, 0.0, null); + } + + List closePrices = new ArrayList<>(); + for (StockDailyData data : stockDataList) { + closePrices.add(data.getClosePrice()); + } + + int n = closePrices.size(); // 应为40 + + double shortMA = calculateMA(closePrices, SHORT_PERIOD); + double midMA = calculateMA(closePrices, MID_PERIOD); + Double longMA = calculateMA(closePrices, LONG_PERIOD); + + // 1. 趋势判断 + String trend = "震荡趋势"; + if (shortMA > midMA && midMA > longMA) { + trend = "上升趋势"; + } else if (shortMA < midMA && midMA < longMA) { + trend = "下降趋势"; + } + + // 2. 金叉/死叉判断(5日上穿20日为金叉,下穿为死叉) + String crossSignal = "无交叉信号"; + if (n >= Math.max(SHORT_PERIOD, MID_PERIOD) + 1) { + double prevShortMA = calculateMA(closePrices.subList(0, n - 1), SHORT_PERIOD); + double prevMidMA = calculateMA(closePrices.subList(0, n - 1), MID_PERIOD); + + if (prevShortMA <= prevMidMA && shortMA > midMA) { + crossSignal = "金叉信号"; + } else if (prevShortMA >= prevMidMA && shortMA < midMA) { + crossSignal = "死叉信号"; + } + } + + // 3. 均线排列状态 + String arrangement = "均线粘合"; + if (Math.abs(shortMA - midMA) / midMA < 0.005 && Math.abs(midMA - longMA) / longMA < 0.005) { + arrangement = "均线粘合"; + } else if (shortMA > midMA && midMA > longMA) { + arrangement = "多头排列"; + } else if (shortMA < midMA && midMA < longMA) { + arrangement = "空头排列"; + } + + return buildJsonResult(trend, crossSignal, arrangement, shortMA, midMA, longMA); + } + + private static double calculateMA(List prices, int period) { + if (prices.size() < period) { + throw new IllegalArgumentException("价格数据长度不足,无法计算" + period + "日均线"); + } + double sum = 0.0; + int n = prices.size(); + for (int i = n - period; i < n; i++) { + sum += prices.get(i); + } + return sum / period; + } + + // 手动构建轻量级 JSON 字符串(全中文值) + private static String buildJsonResult(String trend, String crossSignal, String arrangement, + double shortMA, double midMA, Double longMA) { + StringBuilder sb = new StringBuilder(); + sb.append("趋势:").append(escapeJson(trend)).append(","); + sb.append("交叉信号:").append(escapeJson(crossSignal)).append(","); + sb.append("均线排列:").append(escapeJson(arrangement)).append(","); + sb.append("五日均线:").append(String.format("%.4f", shortMA)).append(","); + sb.append("二十日均线:").append(String.format("%.4f", midMA)).append(","); + if (longMA != null) { + sb.append("四十日均线:").append(String.format("%.4f", longMA)); + } else { + sb.append("四十日均线:无"); + } + return sb.toString(); + } + + private static String escapeJson(String str) { + if (str == null) return ""; + return str.replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); + } +} \ No newline at end of file