Compare commits

...

6 Commits

Author SHA1 Message Date
liruiqiang 829913e5ec WR指标 2 weeks ago
liruiqiang 48aa7dafbc EXPMA指标 2 weeks ago
liruiqiang 14cbc45601 MA指标与CCI指标 2 weeks ago
liruiqiang c350de9937 MA工具版本1 2 weeks ago
majun bf36f084b4 11.18 22:22 2 weeks ago
majun 423959272a 11.18 16:50 2 weeks ago
  1. 12
      pom.xml
  2. 46
      src/main/java/com/deepchart/controller/IndexController.java
  3. 22
      src/main/java/com/deepchart/entity/MACDData.java
  4. 17
      src/main/java/com/deepchart/entity/StockDailyData.java
  5. 9
      src/main/java/com/deepchart/entity/StockInfo.java
  6. 16
      src/main/java/com/deepchart/service/IndexService.java
  7. 39
      src/main/java/com/deepchart/service/impl/IndexServiceImpl.java
  8. 118
      src/main/java/com/deepchart/utils/CCIUtil.java
  9. 156
      src/main/java/com/deepchart/utils/EXPMAUtil.java
  10. 227
      src/main/java/com/deepchart/utils/MACDUtil.java
  11. 102
      src/main/java/com/deepchart/utils/MAUtil.java
  12. 24
      src/main/java/com/deepchart/utils/Result.java
  13. 100
      src/main/java/com/deepchart/utils/StockDataUtil.java
  14. 231
      src/main/java/com/deepchart/utils/WRUtil.java

12
pom.xml

@ -38,18 +38,6 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>

46
src/main/java/com/deepchart/controller/IndexController.java

@ -1,10 +1,56 @@
package com.deepchart.controller;
import com.deepchart.entity.StockDailyData;
import com.deepchart.entity.StockInfo;
import com.deepchart.service.IndexService;
import com.deepchart.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping(value = "/api/index", produces = "application/json")
public class IndexController {
@Autowired
private IndexService indexService;
@PostMapping("/macd")
public Result macd(@RequestBody StockInfo stock) {
List<StockDailyData> list = indexService.getStockData(stock);
String result = indexService.macd(list);
return Result.success("success", result);
}
@PostMapping("/ma")
public Result ma(@RequestBody StockInfo stock) {
List<StockDailyData> list = indexService.getStockData(stock);
String result = indexService.ma(list);
return Result.success("success", result);
}
@PostMapping("/cci")
public Result cci(@RequestBody StockInfo stock) {
List<StockDailyData> list = indexService.getStockData(stock);
String result = indexService.cci(list);
return Result.success("success", result);
}
@PostMapping("/expma")
public Result expma(@RequestBody StockInfo stock) {
List<StockDailyData> list = indexService.getStockData(stock);
String result = indexService.expma(list);
return Result.success("success", result);
}
@PostMapping("/wr")
public Result wr(@RequestBody StockInfo stock) {
List<StockDailyData> list = indexService.getStockData(stock);
String result = indexService.wr(list);
return Result.success("success", result);
}
}

22
src/main/java/com/deepchart/entity/MACDData.java

@ -0,0 +1,22 @@
package com.deepchart.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.time.LocalDate;
/**
* MACD指标计算结果实体类
*/
@Data
@AllArgsConstructor
public class MACDData {
private LocalDate date; // 交易日日期
private double dif; // 差离值
private double dea; // 信号线
private double macdBar; // MACD柱状图
// 判断MACD柱颜色/绿
public boolean isRedBar() { return macdBar > 0; }
public boolean isGreenBar() { return macdBar < 0; }
}

17
src/main/java/com/deepchart/entity/StockDailyData.java

@ -0,0 +1,17 @@
package com.deepchart.entity;
import lombok.Data;
import java.time.LocalDate;
/**
* 股票单日行情基本数据实体类
*/
@Data
public class StockDailyData {
private LocalDate date; // 交易日日期
private double openPrice; // 开盘价
private double closePrice; // 收盘价
private double lowPrice; // 最低价
private double highPrice; // 最高价
}

9
src/main/java/com/deepchart/entity/StockInfo.java

@ -0,0 +1,9 @@
package com.deepchart.entity;
import lombok.Data;
@Data
public class StockInfo {
private String market;
private String code;
}

16
src/main/java/com/deepchart/service/IndexService.java

@ -1,4 +1,20 @@
package com.deepchart.service;
import com.deepchart.entity.StockDailyData;
import com.deepchart.entity.StockInfo;
import java.util.List;
public interface IndexService {
List<StockDailyData> getStockData(StockInfo stock);
String macd(List<StockDailyData> list);
String ma(List<StockDailyData> list);
String cci(List<StockDailyData> list);
String expma(List<StockDailyData> list);
String wr(List<StockDailyData> list);
}

39
src/main/java/com/deepchart/service/impl/IndexServiceImpl.java

@ -1,8 +1,47 @@
package com.deepchart.service.impl;
import com.deepchart.entity.StockDailyData;
import com.deepchart.entity.StockInfo;
import com.deepchart.service.IndexService;
import com.deepchart.utils.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class IndexServiceImpl implements IndexService {
@Autowired
private StockDataUtil stockDataUtil;
@Override
public List<StockDailyData> getStockData(StockInfo stock) {
return stockDataUtil.getStockData(stock);
}
@Override
public String macd(List<StockDailyData> list) {
return MACDUtil.analyzeStock(list);
}
@Override
public String ma(List<StockDailyData> list) {
return MAUtil.analyzeMA(list);
}
@Override
public String cci(List<StockDailyData> list) {
return CCIUtil.analyzeCCI(list);
}
@Override
public String expma(List<StockDailyData> list) {
return EXPMAUtil.analyzeEXPMA(list,12,40);
}
@Override
public String wr(List<StockDailyData> list) {
return WRUtil.analyzeWR(list);
}
}

118
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<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),无明确信号";
}
}

156
src/main/java/com/deepchart/utils/EXPMAUtil.java

@ -0,0 +1,156 @@
package com.deepchart.utils;
import com.deepchart.entity.StockDailyData;
import java.time.LocalDate;
import java.util.*;
public class EXPMAUtil {
public static String analyzeEXPMA(List<StockDailyData> stockData, int shortPeriod, int longPeriod) {
if (stockData == null || stockData.size() < Math.max(shortPeriod, longPeriod)) {
return "数据不足,无法进行 EXPMA 分析。请至少提供 " + Math.max(shortPeriod, longPeriod) + " 天数据。";
}
// 1. 计算 EXPMA
List<Double> expmaShort = calculateSingleExpma(stockData, shortPeriod);
List<Double> expmaLong = calculateSingleExpma(stockData, longPeriod);
int n = stockData.size();
LocalDate latestDate = stockData.get(n - 1).getDate();
double latestPrice = stockData.get(n - 1).getClosePrice();
double currentShort = expmaShort.get(n - 1);
double currentLong = expmaLong.get(n - 1);
StringBuilder analysis = new StringBuilder();
analysis.append("📊 EXPMA 趋势分析(截至 ").append(latestDate).append(")\n");
analysis.append(" • 短期EXPMA(").append(shortPeriod).append("): ").append(String.format("%.2f", currentShort)).append("\n");
analysis.append(" • 长期EXPMA(").append(longPeriod).append("): ").append(String.format("%.2f", currentLong)).append("\n");
analysis.append(" • 当前收盘价: ").append(String.format("%.2f", latestPrice)).append("\n\n");
// 2. 判断多头/空头排列
boolean isBullish = currentShort > currentLong;
boolean shortUp = n >= 2 && expmaShort.get(n - 1) > expmaShort.get(n - 2);
boolean longUp = n >= 2 && expmaLong.get(n - 1) > expmaLong.get(n - 2);
if (isBullish && shortUp && longUp) {
analysis.append("📈 趋势状态:【多头排列】\n");
analysis.append(" → 市场处于上升趋势,短期均线在长期均线上方且双双上行。\n");
if (latestPrice >= currentShort) {
analysis.append(" → 股价站稳短期EXPMA之上,强势格局。\n");
} else {
analysis.append(" → 股价回踩短期EXPMA,若不破可视为加仓机会。\n");
}
} else if (!isBullish && !shortUp && !longUp) {
analysis.append("📉 趋势状态:【空头排列】\n");
analysis.append(" → 市场处于下降趋势,短期均线在长期均线下方且双双下行。\n");
if (latestPrice <= currentShort) {
analysis.append(" → 股价受压于短期EXPMA,弱势明显,建议观望。\n");
} else {
analysis.append(" → 股价反弹至短期EXPMA附近,可能是做空或减仓时机。\n");
}
} else {
analysis.append("🔄 趋势状态:【震荡整理】\n");
analysis.append(" → 均线缠绕或方向不一致,市场缺乏明确趋势。\n");
}
// 3. 检测最近是否发生金叉/死叉过去5日内
boolean goldenCross = false;
boolean deathCross = false;
int crossDayIndex = -1;
String crossType = "";
// 从后往前找最近一次交叉最多看最近10天
int lookback = Math.min(10, n - 1);
for (int i = n - 1; i >= lookback; i--) {
double shortToday = expmaShort.get(i);
double longToday = expmaLong.get(i);
double shortYesterday = expmaShort.get(i - 1);
double longYesterday = expmaLong.get(i - 1);
// 金叉昨日 short <= long今日 short > long
if (shortYesterday <= longYesterday && shortToday > longToday) {
goldenCross = true;
crossDayIndex = i;
crossType = "黄金交叉";
break;
}
// 死叉昨日 short >= long今日 short < long
if (shortYesterday >= longYesterday && shortToday < longToday) {
deathCross = true;
crossDayIndex = i;
crossType = "死亡交叉";
break;
}
}
if (goldenCross || deathCross) {
LocalDate crossDate = stockData.get(crossDayIndex).getDate();
analysis.append("\n🔔 近期信号:")
.append(crossType)
.append("(").append(crossDate).append(")\n");
if (goldenCross) {
analysis.append(" → 短期动能转强,可视为潜在买入信号。\n");
if (currentLong > expmaLong.get(Math.max(0, crossDayIndex - 5))) {
analysis.append(" → 且长期EXPMA已走平或向上,信号可靠性较高。\n");
}
} else {
analysis.append(" → 短期动能转弱,可视为潜在卖出信号。\n");
}
}
// 4. 支撑/阻力提示
if (isBullish) {
double support = currentShort;
if (Math.abs(latestPrice - support) / support < 0.01) { // 价格接近短期EXPMA1%
analysis.append("\n🛡️ 支撑观察:当前股价接近短期EXPMA(")
.append(String.format("%.2f", support))
.append("),若在此企稳,可能延续升势。\n");
}
} else if (!isBullish) {
double resistance = currentShort;
if (Math.abs(latestPrice - resistance) / resistance < 0.01) {
analysis.append("\n⚠️ 阻力观察:当前股价接近短期EXPMA(")
.append(String.format("%.2f", resistance))
.append("),若遇阻回落,可能延续跌势。\n");
}
}
// 5. 总结建议
analysis.append("\n📌 操作建议:\n");
if (isBullish && shortUp) {
analysis.append(" • 多头趋势中,持股待涨;回调至短期EXPMA附近可考虑低吸。\n");
} else if (!isBullish && !shortUp) {
analysis.append(" • 空头趋势中,避免抄底;反弹至短期EXPMA附近可考虑离场。\n");
} else {
analysis.append(" • 市场方向不明,建议结合成交量或震荡指标(如KDJ)辅助决策。\n");
}
analysis.append("\n❗ 注意:EXPMA为趋势跟踪指标,在横盘震荡市中易产生假信号,请勿单独依赖。");
return analysis.toString();
}
// 辅助方法计算单周期 EXPMA
private static List<Double> calculateSingleExpma(List<StockDailyData> data, int period) {
List<Double> expma = new ArrayList<>(data.size());
double alpha = 2.0 / (period + 1);
double value = 0.0;
for (int i = 0; i < data.size(); i++) {
double price = data.get(i).getClosePrice();
if (i == 0) {
// 初始值用 SMA(period)
double sum = 0;
int count = Math.min(period, data.size());
for (int j = 0; j < count; j++) {
sum += data.get(j).getClosePrice();
}
value = sum / count;
} else {
value = alpha * price + (1 - alpha) * value;
}
expma.add(value);
}
return expma;
}
}

227
src/main/java/com/deepchart/utils/MACDUtil.java

@ -0,0 +1,227 @@
package com.deepchart.utils;
import com.deepchart.entity.MACDData;
import com.deepchart.entity.StockDailyData;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* MACD指标分析工具类
* 实现五大核心分析方法
*/
public class MACDUtil {
public static String analyzeStock(List<StockDailyData> prices) {
StringBuilder report = new StringBuilder();
List<MACDData> macdData = calculateMACD(prices);
// 1. 交叉信号分析
report.append("===== 交叉信号分析 =====\n");
analyzeCrossovers(macdData, report);
// 2. 柱状图动能分析
report.append("\n===== 柱状图动能分析 =====\n");
analyzeBarMomentum(macdData, report);
// 3. 零轴趋势判断
report.append("\n===== 零轴趋势判断 =====\n");
analyzeZeroAxisTrend(macdData, report);
// 4. 背离现象检测
report.append("\n===== 背离现象分析 =====\n");
analyzeDivergence(prices, macdData, report);
// 5. 参数调整建议
// report.append("\n===== 参数调整建议 =====\n");
// report.append(" 当前参数:EMA(12,26,9)\n");
// report.append("• 短线交易可调整为EMA(6,13,5)提升灵敏度\n");
// report.append("• 长线投资可调整为EMA(24,52,9)过滤噪音\n");
return report.toString();
}
// 核心MACD计算逻辑
private static List<MACDData> calculateMACD(List<StockDailyData> stockData) {
List<MACDData> result = new ArrayList<>();
double ema12;
double ema26;
double prevEma12 = 0.0;
double prevEma26 = 0.0;
double prevDea = 0.0;
boolean firstDay = true;
for (StockDailyData daily : stockData) {
double close = daily.getClosePrice();
LocalDate date = daily.getDate();
// 首日初始化
if (firstDay) {
firstDay = false;
MACDData data = new MACDData(date, 0.0, 0.0, 0.0);
result.add(data);
continue;
}
// 计算EMA(12)和EMA(26)
ema12 = calculateEMA(close, prevEma12, 12);
ema26 = calculateEMA(close, prevEma26, 26);
// 计算DIF
double dif = ema12 - ema26;
// 计算DEADIF的9日EMA
double dea = calculateEMA(dif, prevDea, 9);
// 计算MACD柱状图
double macdBar = 2 * (dif - dea);
// 保存当前值供下个交易日使用
prevEma12 = ema12;
prevEma26 = ema26;
prevDea = dea;
result.add(new MACDData(date, dif, dea, macdBar));
}
return result;
}
// 指数移动平均(EMA)计算公式
private static double calculateEMA(double currentPrice, double prevEMA, int period) {
return currentPrice * (2.0 / (period + 1)) + prevEMA * ((period - 1.0) / (period + 1));
}
// 金叉/死叉检测
private static void analyzeCrossovers(List<MACDData> macdData, StringBuilder report) {
int goldenCrossCount = 0;
int deathCrossCount = 0;
boolean aboveZeroGolden = false;
boolean belowZeroDeath = false;
for (int i = 1; i < macdData.size(); i++) {
MACDData prev = macdData.get(i-1);
MACDData curr = macdData.get(i);
// 金叉DIF上穿DEA
if (prev.getDif() < prev.getDea() && curr.getDif() > curr.getDea()) {
goldenCrossCount++;
if (curr.getDif() > 0) {
aboveZeroGolden = true;
report.append(String.format("• %tF:强势金叉信号(零轴上方),买入机会\n", curr.getDate()));
} else {
report.append(String.format("• %tF:弱势金叉信号(零轴下方),谨慎参与\n", curr.getDate()));
}
}
// 死叉DIF下穿DEA
else if (prev.getDif() > prev.getDea() && curr.getDif() < curr.getDea()) {
deathCrossCount++;
if (curr.getDif() < 0) {
belowZeroDeath = true;
report.append(String.format("• %tF:强势死叉信号(零轴下方),卖出警示\n", curr.getDate()));
} else {
report.append(String.format("• %tF:弱势死叉信号(零轴上方),部分止盈\n", curr.getDate()));
}
}
}
report.append("\n 交叉信号统计:\n");
report.append("• 近期出现金叉次数:").append(goldenCrossCount).append("次\n");
report.append("• 近期出现死叉次数:").append(deathCrossCount).append("次\n");
if (aboveZeroGolden) report.append("▶ 存在零轴上方的强势金叉,多头动能强劲\n");
if (belowZeroDeath) report.append("▶ 存在零轴下方的强势死叉,空头风险显著\n");
}
// 柱状图动能分析
private static void analyzeBarMomentum(List<MACDData> macdData, StringBuilder report) {
if (macdData.size() < 5) return;
// 获取最近5个交易日的柱状图数据
MACDData curr = macdData.get(macdData.size() - 1);
MACDData prev1 = macdData.get(macdData.size() - 2);
MACDData prev2 = macdData.get(macdData.size() - 3);
report.append(" 最新交易日数据:\n");
report.append(String.format("• MACD柱状图数值:%.4f (%s)\n",
curr.getMacdBar(), curr.isRedBar() ? "红柱" : "绿柱"));
// 红柱分析零轴上方
if (curr.isRedBar()) {
report.append("▶ 当前处于多头市场(红柱区域)\n");
if (curr.getMacdBar() > prev1.getMacdBar() && prev1.getMacdBar() > prev2.getMacdBar()) {
report.append("▶ 红柱连续放大(3日),上涨动能加速\n");
} else if (curr.getMacdBar() < prev1.getMacdBar()) {
report.append("▶ 红柱开始缩短,警惕上涨动能衰减\n");
}
}
// 绿柱分析零轴下方
else {
report.append("▶ 当前处于空头市场(绿柱区域)\n");
if (curr.getMacdBar() < prev1.getMacdBar() && prev1.getMacdBar() < prev2.getMacdBar()) {
report.append("▶ 绿柱连续放大(3日),下跌动能加速\n");
} else if (curr.getMacdBar() > prev1.getMacdBar()) {
report.append("▶ 绿柱开始缩短,空头力量减弱\n");
}
}
}
// 零轴趋势分析
private static void analyzeZeroAxisTrend(List<MACDData> macdData, StringBuilder report) {
int aboveZeroDays = 0;
int belowZeroDays = 0;
for (MACDData data : macdData) {
if (data.getDif() > 0) aboveZeroDays++;
else belowZeroDays++;
}
double aboveRatio = 100.0 * aboveZeroDays / macdData.size();
report.append(String.format(" 历史交易日统计(共%d天):\n", macdData.size()));
report.append(String.format("• DIF在零轴上方天数:%d天 (占比%.1f%%)\n", aboveZeroDays, aboveRatio));
report.append(String.format("• DIF在零轴下方天数:%d天 (占比%.1f%%)\n", belowZeroDays, 100 - aboveRatio));
MACDData last = macdData.get(macdData.size() - 1);
if (last.getDif() > 0 && last.getDea() > 0) {
report.append("▶ 当前趋势:多头市场(DIF与DEA均在零轴上方)\n");
} else if (last.getDif() < 0 && last.getDea() < 0) {
report.append("▶ 当前趋势:空头市场(DIF与DEA均在零轴下方)\n");
} else {
report.append("▶ 当前趋势:震荡行情(多空分歧明显)\n");
}
}
// 背离分析
private static void analyzeDivergence(List<StockDailyData> prices, List<MACDData> macdData, StringBuilder report) {
boolean hasTopDivergence = false;
boolean hasBottomDivergence = false;
// 寻找最近30个交易日内的背离
int lookback = Math.min(30, prices.size() - 1);
for (int i = prices.size() - 2; i > prices.size() - lookback; i--) {
// 顶背离检测股价新高但MACD低点
if (prices.get(i).getClosePrice() > prices.get(i+1).getClosePrice() &&
prices.get(i).getClosePrice() > prices.get(i-1).getClosePrice() &&
macdData.get(i).getDif() < macdData.get(i+1).getDif() &&
macdData.get(i).getDif() < macdData.get(i-1).getDif()) {
hasTopDivergence = true;
report.append(String.format("• %tF:检测到顶背离(股价新高但MACD走弱),警示下跌风险\n",
prices.get(i).getDate()));
}
// 底背离检测股价新低但MACD高点
else if (prices.get(i).getClosePrice() < prices.get(i+1).getClosePrice() &&
prices.get(i).getClosePrice() < prices.get(i-1).getClosePrice() &&
macdData.get(i).getDif() > macdData.get(i+1).getDif() &&
macdData.get(i).getDif() > macdData.get(i-1).getDif()) {
hasBottomDivergence = true;
report.append(String.format("• %tF:检测到底背离(股价新低但MACD走强),预示反弹机会\n",
prices.get(i).getDate()));
}
}
if (!hasTopDivergence) report.append("• 近期未检测到显著顶背离现象\n");
if (!hasBottomDivergence) report.append("• 近期未检测到显著底背离现象\n");
}
}

102
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<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");
}
}

24
src/main/java/com/deepchart/utils/Result.java

@ -0,0 +1,24 @@
package com.deepchart.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 请求响应返回结果工具类
* @param <T>
*/
@Data
@AllArgsConstructor
public class Result<T> {
private Integer code; //状态码
private String msg; //返回信息
private T data; //返回数据
public static <T> Result<T> success(String message, T data) {
return new Result<>(200, message, data);
}
public static <T> Result<T> error(Integer resultCode, String message) {
return new Result<>(resultCode, message, null);
}
}

100
src/main/java/com/deepchart/utils/StockDataUtil.java

@ -0,0 +1,100 @@
package com.deepchart.utils;
import com.deepchart.entity.StockDailyData;
import com.deepchart.entity.StockInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 股票行情基本数据工具类
*/
@Component
public class StockDataUtil {
private static final String STOCK_DATA_PREFIX = "https://api.homilychart.com/link";
private static final String TOKEN = "8nkj4QBV1RPIb4CzoRTnbZi0+fEeMx8pywnIlrmTxdwROKkuwWqAWu9orpkpeXVqL98DPfeonNYpHv+mucA";
private static final ObjectMapper objectMapper = new ObjectMapper();
public List<StockDailyData> getStockData(StockInfo stock) {
List<List<Object>> kLine20 = List.of();
try {
// 1. 构建请求URL
String url = STOCK_DATA_PREFIX + "/api/superBrainData";
// 2. 使用Map构造请求体参数
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("brainPrivilegeState", 1);
requestBody.put("marketList", "can,usa,hk,vi,sg,th,in,cn,gb,my");
requestBody.put("market", stock.getMarket());
requestBody.put("code", stock.getCode());
// 3. 将Map转换为JSON字符串
String jsonInputString = objectMapper.writeValueAsString(requestBody);
// 4. 创建HTTP连接
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("token", TOKEN);
connection.setDoOutput(true);
// 5. 发送请求数据
try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
// 6. 解析响应
if (connection.getResponseCode() == 200) {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
// 解析JSON响应
Map<String, Object> jsonResponse = objectMapper.readValue(response.toString(), Map.class);
Map<String, Object> data = (Map<String, Object>) jsonResponse.get("data");
Map<String, Object> brain = (Map<String, Object>) data.get("Brain");
kLine20 = (List<List<Object>>) brain.get("KLine20");
}
}
// 7. 关闭连接
connection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
List<StockDailyData> list = new ArrayList<>();
for (List<Object> v : kLine20) {
StockDailyData s = new StockDailyData();
s.setDate(LocalDate.parse((String) v.get(0), DateTimeFormatter.ofPattern("yyyy/MM/dd")));
s.setOpenPrice((double)v.get(1));
s.setClosePrice((double)v.get(2));
s.setLowPrice((double)v.get(3));
s.setHighPrice((double)v.get(4));
list.add(s);
}
return list;
}
}

231
src/main/java/com/deepchart/utils/WRUtil.java

@ -0,0 +1,231 @@
package com.deepchart.utils;
import com.deepchart.entity.StockDailyData;
import java.time.LocalDate;
import java.util.*;
public class WRUtil {
/**
* 对40日股票数据进行WR威廉指标分析
*
* @param stockData 按时间升序排列的40天股票日线数据最早在前
* @return WR分析报告文本
*/
public static String analyzeWR(List<StockDailyData> stockData) {
if (stockData == null || stockData.size() < 14) {
return "❌ 数据不足:WR分析至少需要14天K线数据。";
}
int n = stockData.size();
// 常用双周期快线 N=6慢线 N=10
List<Double> wrFast = calculateWr(stockData, 6);
List<Double> wrSlow = calculateWr(stockData, 10);
List<Double> prices = new ArrayList<>();
for (var d : stockData) prices.add(d.getClosePrice());
LocalDate latestDate = stockData.get(n - 1).getDate();
double latestPrice = prices.get(n - 1);
double currentFast = wrFast.get(n - 1);
double currentSlow = wrSlow.get(n - 1);
StringBuilder report = new StringBuilder();
report.append("📊 威廉指标(WR)分析报告(截至 ").append(latestDate).append(")\n");
report.append(" • 当前收盘价: ").append(String.format("%.2f", latestPrice)).append("\n");
report.append(" • WR(6): ").append(String.format("%.1f", currentFast)).append("\n");
report.append(" • WR(10): ").append(String.format("%.1f", currentSlow)).append("\n\n");
// 区域判断
boolean fastInOversold = currentFast >= 80;
boolean slowInOversold = currentSlow >= 80;
boolean fastInOverbought = currentFast <= 20;
boolean slowInOverbought = currentSlow <= 20;
if (fastInOversold || slowInOversold) {
report.append("📉 市场状态:【超卖区域】\n");
report.append(" → 价格接近近期低点,短期下跌动能可能衰竭。\n");
} else if (fastInOverbought || slowInOverbought) {
report.append("📈 市场状态:【超买区域】\n");
report.append(" → 价格接近近期高点,短期上涨动能可能衰竭。\n");
} else {
report.append("⚖️ 市场状态:【常态波动区(20~80)】\n");
report.append(" → 无极端情绪信号,WR参考价值有限。\n");
}
// 双线交叉信号仅当有足够历史数据
String crossSignal = detectCross(wrFast, wrSlow, n);
if (!crossSignal.isEmpty()) {
report.append("\n").append(crossSignal);
}
// 背离检测
boolean topDiv = detectTopDivergence(prices, wrFast, n);
boolean bottomDiv = detectBottomDivergence(prices, wrFast, n);
if (topDiv) {
report.append("\n🚨 顶背离信号!\n");
report.append(" → 股价创新高,但WR强势减弱,上涨动能不足。\n");
report.append(" → 强烈建议减仓或止盈。\n");
}
if (bottomDiv) {
report.append("\n💎 底背离信号!\n");
report.append(" → 股价创新低,但WR弱势减弱,下跌动能衰竭。\n");
report.append(" → 可视为短线抄底机会。\n");
}
if (!topDiv && !bottomDiv) {
report.append("\n🔍 背离状态:无显著背离\n");
}
// 综合操作建议
report.append("\n📌 操作建议:\n");
if (bottomDiv || (fastInOversold && currentFast < wrFast.get(n - 2))) {
report.append(" • WR从超卖区回升,结合底背离,可考虑短线买入。\n");
} else if (topDiv || (fastInOverbought && currentFast > wrFast.get(n - 2))) {
report.append(" • WR从超买区回落,结合顶背离,建议逢高减仓。\n");
} else if (currentFast > 50 && currentSlow > 50) {
report.append(" • WR处于中性偏弱区域,观望为主。\n");
} else if (currentFast < 50 && currentSlow < 50) {
report.append(" • WR处于中性偏强区域,持股待涨。\n");
} else {
report.append(" • 市场方向不明,建议结合趋势指标(如EXPMA)综合判断。\n");
}
report.append("\n❗ 注意:WR在单边行情中易钝化(长期超买/超卖),震荡市中效果最佳。");
return report.toString();
}
// 计算WR(N)
private static List<Double> calculateWr(List<StockDailyData> data, int period) {
List<Double> wr = new ArrayList<>(Collections.nCopies(data.size(), 100.0));
if (data.size() < period) return wr;
for (int i = period - 1; i < data.size(); i++) {
double highest = Double.MIN_VALUE;
double lowest = Double.MAX_VALUE;
// 找过去period天含当日的最高价和最低价
for (int j = i - period + 1; j <= i; j++) {
highest = Math.max(highest, data.get(j).getHighPrice());
lowest = Math.min(lowest, data.get(j).getLowPrice());
}
double close = data.get(i).getClosePrice();
if (highest == lowest) {
wr.set(i, 0.0); // 防止除零
} else {
double value = (highest - close) / (highest - lowest) * 100.0;
wr.set(i, Math.max(0.0, Math.min(100.0, value))); // 限制在[0,100]
}
}
return wr;
}
// 检测双线交叉仅看最近3天
private static String detectCross(List<Double> fast, List<Double> slow, int n) {
if (n < 3) return "";
double f0 = fast.get(n - 1), f1 = fast.get(n - 2);
double s0 = slow.get(n - 1), s1 = slow.get(n - 2);
// 金叉快线上穿慢线且在超卖区附近
if (f1 <= s1 && f0 > s0 && f0 >= 70) {
return "✨ 买入信号:WR(6)在超卖区上穿WR(10),形成金叉,短期反弹可期。\n";
}
// 死叉快线下穿慢线且在超买区附近
if (f1 >= s1 && f0 < s0 && f0 <= 30) {
return "⚠️ 卖出信号:WR(6)在超买区下穿WR(10),形成死叉,短期回调风险加大。\n";
}
return "";
}
// 顶背离价格新高WR高点降低WR是反向指标高点=弱势
private static boolean detectTopDivergence(List<Double> prices, List<Double> wr, int n) {
if (n < 20) return false;
// 找最近一个价格高点局部最大
int recentHighIdx = findRecentPeak(prices, n, 5);
if (recentHighIdx == -1 || recentHighIdx < 10) return false;
// 找上一个价格高点
int prevHighIdx = findPreviousPeak(prices, recentHighIdx - 5, recentHighIdx - 15);
if (prevHighIdx == -1) return false;
double recentPriceHigh = prices.get(recentHighIdx);
double prevPriceHigh = prices.get(prevHighIdx);
double recentWrHigh = wr.get(recentHighIdx); // WR高值 = 弱势
double prevWrHigh = wr.get(prevHighIdx);
// 价格新高 + WR高点更低即弱势更弱 顶背离
return (recentPriceHigh > prevPriceHigh) && (recentWrHigh < prevWrHigh);
}
// 底背离价格新低WR低点抬高WR低值=强势
private static boolean detectBottomDivergence(List<Double> prices, List<Double> wr, int n) {
if (n < 20) return false;
int recentLowIdx = findRecentTrough(prices, n, 5);
if (recentLowIdx == -1 || recentLowIdx < 10) return false;
int prevLowIdx = findPreviousTrough(prices, recentLowIdx - 5, recentLowIdx - 15);
if (prevLowIdx == -1) return false;
double recentPriceLow = prices.get(recentLowIdx);
double prevPriceLow = prices.get(prevLowIdx);
double recentWrLow = wr.get(recentLowIdx); // WR低值 = 强势
double prevWrLow = wr.get(prevLowIdx);
// 价格新低 + WR低点更高即强势更强 底背离
return (recentPriceLow < prevPriceLow) && (recentWrLow > prevWrLow);
}
// 辅助找最近局部高点
private static int findRecentPeak(List<Double> prices, int n, int window) {
for (int i = n - 1; i >= n - window && i >= 2; i--) {
if (prices.get(i) >= prices.get(i - 1) && prices.get(i) >= prices.get(i + 1)) {
return i;
}
}
return -1;
}
private static int findPreviousPeak(List<Double> prices, int end, int start) {
start = Math.max(0, start);
end = Math.min(end, prices.size() - 1);
double max = -1;
int idx = -1;
for (int i = start; i <= end; i++) {
if (prices.get(i) > max) {
max = prices.get(i);
idx = i;
}
}
return idx;
}
private static int findRecentTrough(List<Double> prices, int n, int window) {
for (int i = n - 2; i >= n - window && i >= 2; i--) {
if (prices.get(i) <= prices.get(i - 1) && prices.get(i) <= prices.get(i + 1)) {
return i;
}
}
return -1;
}
private static int findPreviousTrough(List<Double> prices, int end, int start) {
start = Math.max(0, start);
end = Math.min(end, prices.size() - 1);
double min = Double.MAX_VALUE;
int idx = -1;
for (int i = start; i <= end; i++) {
if (prices.get(i) < min) {
min = prices.get(i);
idx = i;
}
}
return idx;
}
}
Loading…
Cancel
Save