6 changed files with 250 additions and 157 deletions
-
14src/main/java/com/deepchart/controller/IndexController.java
-
4src/main/java/com/deepchart/service/IndexService.java
-
12src/main/java/com/deepchart/service/impl/IndexServiceImpl.java
-
118src/main/java/com/deepchart/utils/CCIUtil.java
-
157src/main/java/com/deepchart/utils/MATools.java
-
102src/main/java/com/deepchart/utils/MAUtil.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<StockDailyData> 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<StockDailyData> stockDataList) { |
|||
int n = stockDataList.size(); |
|||
List<Double> 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<StockDailyData> 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<Double> 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),无明确信号"; |
|||
} |
|||
} |
|||
@ -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<StockDailyData> stockDataList) { |
|||
if (stockDataList == null || stockDataList.size() < 20) { |
|||
throw new IllegalArgumentException("数据不足,至少需要20个交易日的收盘价"); |
|||
} |
|||
|
|||
// 提取收盘价(确保按时间顺序,最新在最后) |
|||
List<Double> 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<Double> 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; |
|||
} |
|||
} |
|||
@ -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<StockDailyData> stockDataList) { |
|||
if (stockDataList == null || stockDataList.size() < LONG_PERIOD) { |
|||
return buildJsonResult("错误", "无信号", "数据不足,至少需要40个交易日", 0.0, 0.0, null); |
|||
} |
|||
|
|||
List<Double> 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<Double> 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"); |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue