Compare commits

...

10 Commits
master ... dev

Author SHA1 Message Date
liruiqiang f99bec6808 主方法 5 days ago
liruiqiang 1a6192e625 实现方法 5 days ago
liruiqiang 59bf1daffe Merge remote-tracking branch 'origin/dev' into dev 5 days ago
majun 47ffb8deef 11.24 11:08 6 days ago
liruiqiang 829913e5ec WR指标 1 week ago
liruiqiang 48aa7dafbc EXPMA指标 1 week ago
liruiqiang 14cbc45601 MA指标与CCI指标 1 week 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. 19
      pom.xml
  2. 125
      src/main/java/com/deepchart/Main.java
  3. 27
      src/main/java/com/deepchart/StockApiDemo.java
  4. 68
      src/main/java/com/deepchart/controller/IndexController.java
  5. 19
      src/main/java/com/deepchart/entity/KDJData.java
  6. 22
      src/main/java/com/deepchart/entity/MACDData.java
  7. 18
      src/main/java/com/deepchart/entity/StockDailyData.java
  8. 9
      src/main/java/com/deepchart/entity/StockInfo.java
  9. 23
      src/main/java/com/deepchart/service/IndexService.java
  10. 54
      src/main/java/com/deepchart/service/impl/IndexServiceImpl.java
  11. 118
      src/main/java/com/deepchart/utils/CCIUtil.java
  12. 156
      src/main/java/com/deepchart/utils/EXPMAUtil.java
  13. 102
      src/main/java/com/deepchart/utils/KDJUtil.java
  14. 108
      src/main/java/com/deepchart/utils/KDUtil.java
  15. 227
      src/main/java/com/deepchart/utils/MACDUtil.java
  16. 102
      src/main/java/com/deepchart/utils/MAUtil.java
  17. 91
      src/main/java/com/deepchart/utils/RSIUtil.java
  18. 24
      src/main/java/com/deepchart/utils/Result.java
  19. 110
      src/main/java/com/deepchart/utils/StockDataUtil.java
  20. 231
      src/main/java/com/deepchart/utils/WRUtil.java

19
pom.xml

@ -33,23 +33,18 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.22.0</version> <!-- 或更高版本,如2.13.0等 -->
</dependency>
</dependencies>
<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>

125
src/main/java/com/deepchart/Main.java

@ -0,0 +1,125 @@
package com.deepchart;
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.tools.FunctionDefinition;
import com.alibaba.dashscope.tools.ToolCallBase;
import com.alibaba.dashscope.tools.ToolCallFunction;
import com.alibaba.dashscope.tools.ToolFunction;
import com.alibaba.dashscope.utils.JsonUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
public class Main {
// 若使用新加坡地域的模型请释放下列注释
// static {Constants.baseHttpApiUrl="https://dashscope-intl.aliyuncs.com/api/v1";}
/**
* 第一步定义工具的本地实现
* @param arguments 模型传入的包含工具所需参数的JSON字符串
* @return 工具执行后的结果字符串
*/
public static String getCurrentWeather(String arguments) {
try {
// 模型提供的参数是JSON格式的需要我们手动解析
ObjectMapper objectMapper = new ObjectMapper();
JsonNode argsNode = objectMapper.readTree(arguments);
String location = argsNode.get("location").asText();
// 用随机结果来模拟真实的API调用或业务逻辑
List<String> weatherConditions = Arrays.asList("晴天", "多云", "雨天");
String randomWeather = weatherConditions.get(new Random().nextInt(weatherConditions.size()));
return location + "今天是" + randomWeather + "。";
} catch (Exception e) {
// 异常处理确保程序健壮性
return "无法解析地点参数。";
}
}
public static void main(String[] args) {
try {
// 第二步向模型描述注册我们的工具
String weatherParamsSchema =
"{\"type\":\"object\",\"properties\":{\"location\":{\"type\":\"string\",\"description\":\"城市或县区,比如北京市、杭州市、余杭区等。\"}},\"required\":[\"location\"]}";
FunctionDefinition weatherFunction = FunctionDefinition.builder()
.name("get_current_weather") // 工具的唯一标识名必须与本地实现对应
.description("当你想查询指定城市的天气时非常有用。") // 清晰的描述能帮助模型更好地决定何时使用该工具
.parameters(JsonUtils.parseString(weatherParamsSchema).getAsJsonObject())
.build();
Generation gen = new Generation();
String userInput = "北京天气咋样";
List<Message> messages = new ArrayList<>();
messages.add(Message.builder().role(Role.USER.getValue()).content(userInput).build());
// 第四步首次调用模型将用户的请求和我们定义好的工具列表一同发送给模型
GenerationParam param = GenerationParam.builder()
.model("qwen-plus") // 指定需要调用的模型
.apiKey("sk-5bf09a590daf408cb1126c2c6684e982") // 从环境变量中获取API Key
.messages(messages) // 传入当前的对话历史
.tools(Arrays.asList(ToolFunction.builder().function(weatherFunction).build())) // 传入可用的工具列表
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.build();
GenerationResult result = gen.call(param);
Message assistantOutput = result.getOutput().getChoices().get(0).getMessage();
messages.add(assistantOutput); // 将模型的首次回复也加入到对话历史中
// 第五步检查模型的回复判断它是否请求调用工具
if (assistantOutput.getToolCalls() == null || assistantOutput.getToolCalls().isEmpty()) {
// 情况A模型没有调用工具而是直接给出了回答
System.out.println("无需调用天气查询工具,直接回复:" + assistantOutput.getContent());
} else {
// 情况B模型决定调用工具
// 使用 while 循环可以处理模型连续调用多次工具的场景
while (assistantOutput.getToolCalls() != null && !assistantOutput.getToolCalls().isEmpty()) {
ToolCallBase toolCall = assistantOutput.getToolCalls().get(0);
// 从模型的回复中解析出工具调用的具体信息要调用的函数名参数
ToolCallFunction functionCall = (ToolCallFunction) toolCall;
String funcName = functionCall.getFunction().getName();
String arguments = functionCall.getFunction().getArguments();
System.out.println("正在调用工具 [" + funcName + "],参数:" + arguments);
// 根据工具名在本地执行对应的Java方法
String toolResult = getCurrentWeather(arguments);
// 构造一个 role "tool" 的消息其中包含工具的执行结果
Message toolMessage = Message.builder()
.role("tool")
.toolCallId(toolCall.getId())
.content(toolResult)
.build();
System.out.println("工具返回:" + toolMessage.getContent());
messages.add(toolMessage); // 将工具的返回结果也加入到对话历史中
// 第六步再次调用模型
param.setMessages(messages);
result = gen.call(param);
assistantOutput = result.getOutput().getChoices().get(0).getMessage();
messages.add(assistantOutput);
}
// 第七步打印模型经过总结后生成的最终回复
System.out.println("助手最终回复:" + assistantOutput.getContent());
}
} catch (NoApiKeyException | InputRequiredException e) {
System.err.println("错误: " + e.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
}

27
src/main/java/com/deepchart/StockApiDemo.java

@ -0,0 +1,27 @@
package com.deepchart;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class StockApiDemo {
public static void main(String[] args) throws Exception {
String url = "https://www.stockapi.com.cn/v1/quota/macd2";
// 构建参数
String params = "code=" + URLEncoder.encode("002131", StandardCharsets.UTF_8) +
"&cycle=9&startDate=2025-10-10&endDate=2025-10-15&longCycle=26&shortCycle=12&calculationCycle=100";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url + "?" + params))
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(data -> System.out.println("日线行情数据:" + data))
.join();
}
}

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

@ -1,10 +1,78 @@
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("/kdj")
public Result kdj(@RequestBody StockInfo stock) {
List<StockDailyData> list = indexService.getStockData(stock);
String result = indexService.kdj(list);
return Result.success("success", result);
}
@PostMapping("/kd")
public Result kd(@RequestBody StockInfo stock) {
List<StockDailyData> list = indexService.getStockData(stock);
String result = indexService.kd(list);
return Result.success("success", result);
}
@PostMapping("/rsi")
public Result rsi(@RequestBody StockInfo stock) {
List<StockDailyData> list = indexService.getStockData(stock);
String result = indexService.rsi(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);
}
}

19
src/main/java/com/deepchart/entity/KDJData.java

@ -0,0 +1,19 @@
package com.deepchart.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.time.LocalDate;
/**
* KDJ指标计算结果实体类
*/
@Data
@AllArgsConstructor
public class KDJData {
private LocalDate date; // 交易日日期
private double rsv; // RSV
private double k; // K
private double d; // D
private double j; // J
}

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; }
}

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

@ -0,0 +1,18 @@
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; // 最高价
private Long volume; // 成交量
}

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;
}

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

@ -1,4 +1,27 @@
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 kdj(List<StockDailyData> list);
String kd(List<StockDailyData> list);
String rsi(List<StockDailyData> list);
String ma(List<StockDailyData> list);
String cci(List<StockDailyData> list);
String expma(List<StockDailyData> list);
String wr(List<StockDailyData> list);
}

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

@ -1,8 +1,62 @@
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);
}
@Override
public String kdj(List<StockDailyData> list) {
return KDJUtil.calculateAndAnalyze(list);
}
@Override
public String kd(List<StockDailyData> list) {
return KDUtil.generateReport(list);
}
@Override
public String rsi(List<StockDailyData> list) {
return RSIUtil.generateReport(list, 14);
}
}

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;
}
}

102
src/main/java/com/deepchart/utils/KDJUtil.java

@ -0,0 +1,102 @@
package com.deepchart.utils;
import com.deepchart.entity.KDJData;
import com.deepchart.entity.StockDailyData;
import java.util.*;
/**
* KDJ指标分析工具类
*/
public class KDJUtil {
private static final int N = 9; // 默认周期
private static final int M1 = 3; // 平滑周期 K
private static final int M2 = 3; // 平滑周期 D
public static String calculateAndAnalyze(List<StockDailyData> dataList) {
if (dataList == null || dataList.size() < N) {
return "数据不足,无法计算KDJ指标。";
}
List<KDJData> kdjList = new ArrayList<>();
Queue<Double> lowQueue = new LinkedList<>();
Queue<Double> highQueue = new LinkedList<>();
for (int i = 0; i < dataList.size(); i++) {
StockDailyData today = dataList.get(i);
lowQueue.offer(today.getLowPrice());
highQueue.offer(today.getHighPrice());
// 维持窗口大小为 N
if (lowQueue.size() > N) {
lowQueue.poll();
highQueue.poll();
}
// 不足N天跳过
if (i < N - 1) continue;
double lowestLow = Collections.min(lowQueue);
double highestHigh = Collections.max(highQueue);
double rsv = ((today.getClosePrice() - lowestLow) / (highestHigh - lowestLow)) * 100;
double k, d, j;
if (kdjList.isEmpty()) {
k = rsv;
d = k;
} else {
KDJData prev = kdjList.get(kdjList.size() - 1);
k = (2.0 / 3) * prev.getK() + (1.0 / 3) * rsv;
d = (2.0 / 3) * prev.getD() + (1.0 / 3) * k;
}
j = 3 * k - 2 * d;
kdjList.add(new KDJData(today.getDate(), rsv, k, d, j));
}
return generateReport(kdjList);
}
private static String generateReport(List<KDJData> kdjList) {
StringBuilder report = new StringBuilder("📈【KDJ技术分析报告】\n");
if (kdjList.isEmpty()) {
return report.append("❌ 数据不完整,无法提供有效分析。\n").toString();
}
KDJData latest = kdjList.get(kdjList.size() - 1);
// 超买/超卖判断
if (latest.getK() > 80 || latest.getD() > 80 || latest.getJ() > 100) {
report.append("🔴 当前处于超买区域,请注意回调风险!\n");
} else if (latest.getK() < 20 || latest.getD() < 20 || latest.getJ() < 0) {
report.append("🟢 当前处于超卖区域,存在反弹机会!\n");
} else {
report.append("🟡 当前KDJ指标运行平稳,无明显买卖信号。\n");
}
// 交叉信号检测简单实现
if (kdjList.size() >= 2) {
KDJData prev = kdjList.get(kdjList.size() - 2);
if (prev.getK() <= prev.getD() && latest.getK() > latest.getD()) {
report.append("✨ 发生金叉,建议关注买入时机(尤其在低位)。\n");
}
if (prev.getK() >= prev.getD() && latest.getK() < latest.getD()) {
report.append("🔻 发生死叉,建议控制仓位或考虑卖出(尤其在高位)。\n");
}
}
// J 线强化提示
if (latest.getJ() > 100) {
report.append("⚠️ J值超过100,进入极端超买状态,短期可能剧烈回调。\n");
} else if (latest.getJ() < 0) {
report.append("⚠️ J值低于0,进入极端超卖状态,短期可能强势反弹。\n");
}
return report.toString();
}
}

108
src/main/java/com/deepchart/utils/KDUtil.java

@ -0,0 +1,108 @@
package com.deepchart.utils;
import com.deepchart.entity.KDJData;
import com.deepchart.entity.StockDailyData;
import java.util.*;
/**
* KD指标分析工具类
*/
public class KDUtil {
private static final int N = 9; // 默认周期长度
private static final double SMOOTH_K = 1.0 / 3; // K 平滑系数
private static final double SMOOTH_D = 1.0 / 3; // D 平滑系数
private static List<KDJData> calculateKD(List<StockDailyData> dataList) {
if (dataList == null || dataList.size() < N) {
throw new IllegalArgumentException("数据不足,至少需要" + N + "条记录");
}
List<KDJData> kdList = new ArrayList<>();
Deque<Double> highs = new ArrayDeque<>(N);
Deque<Double> lows = new ArrayDeque<>(N);
Double prevK = null;
Double prevD = null;
for (int i = 0; i < dataList.size(); i++) {
StockDailyData data = dataList.get(i);
highs.offerLast(data.getHighPrice());
lows.offerLast(data.getLowPrice());
if (highs.size() > N) {
highs.pollFirst();
lows.pollFirst();
}
if (i >= N - 1) {
double hhv = Collections.max(highs);
double llv = Collections.min(lows);
double rsv = ((data.getClosePrice() - llv) / (hhv - llv)) * 100;
double kValue = (prevK == null ? rsv : prevK * (1 - SMOOTH_K) + rsv * SMOOTH_K);
double dValue = (prevD == null ? kValue : prevD * (1 - SMOOTH_D) + kValue * SMOOTH_D);
kdList.add(new KDJData(data.getDate(), rsv, kValue, dValue, 0));
prevK = kValue;
prevD = dValue;
} else {
kdList.add(new KDJData(data.getDate(), 0, 0, 0, 0)); // 占位符
}
}
return kdList;
}
public static String generateReport(List<StockDailyData> stockDataList) {
List<KDJData> kdList = calculateKD(stockDataList);
StringBuilder report = new StringBuilder();
report.append("📈 股票KD指标分析报告\n");
report.append("=======================\n");
if (kdList.isEmpty()) {
report.append("❌ 数据不足,无法生成分析。\n");
return report.toString();
}
KDJData latest = kdList.get(kdList.size() - 1);
KDJData previous = kdList.size() >= 2 ? kdList.get(kdList.size() - 2) : null;
double k = latest.getK();
double d = latest.getD();
// 判断超买或超卖
if (k > 80 && d > 80) {
report.append("🔴 当前处于【超买】状态,注意回调风险。\n");
} else if (k < 20 && d < 20) {
report.append("🟢 当前处于【超卖】状态,关注反弹机会。\n");
} else {
report.append("🟡 当前处于中性区域,暂无明显买卖信号。\n");
}
// 判断金叉/死叉
if (previous != null) {
boolean isGoldenCross = previous.getK() <= previous.getD() && k > d;
boolean isDeathCross = previous.getK() >= previous.getD() && k < d;
if (isGoldenCross && d < 30) {
report.append("✨ 出现金叉(K线上穿D线),特别是在低位,是潜在买入信号。\n");
} else if (isDeathCross && d > 70) {
report.append("⚠️ 出现死叉(K线下穿D线),尤其是在高位,建议谨慎卖出。\n");
}
}
// 多空趋势判断
if (k > 50 && d > 50 && k > d) {
report.append("⬆️ 当前为多头趋势,可考虑持股观望。\n");
} else if (k < 50 && d < 50 && k < d) {
report.append("⬇️ 当前为空头趋势,宜控制仓位。\n");
} else {
report.append("🔄 当前为震荡行情,短线操作更适宜。\n");
}
return report.toString();
}
}

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

91
src/main/java/com/deepchart/utils/RSIUtil.java

@ -0,0 +1,91 @@
package com.deepchart.utils;
import com.deepchart.entity.StockDailyData;
import java.util.ArrayList;
import java.util.List;
/**
* RSI指标技术分析工具类
*/
public class RSIUtil {
public static Double calculateRSI(List<StockDailyData> dataList, int period) {
if (dataList == null || dataList.size() <= period) {
throw new IllegalArgumentException("数据不足,无法计算" + period + "日RSI");
}
List<Double> ups = new ArrayList<>();
List<Double> downs = new ArrayList<>();
for (int i = 1; i <= period; i++) {
StockDailyData today = dataList.get(dataList.size() - period + i - 1);
StockDailyData yesterday = dataList.get(dataList.size() - period + i - 2);
double change = today.getClosePrice() - yesterday.getClosePrice();
if (change > 0) {
ups.add(change);
downs.add(0.0);
} else {
ups.add(0.0);
downs.add(-change); // 取正值
}
}
double avgUp = ups.stream().mapToDouble(Double::doubleValue).average().orElse(0);
double avgDown = downs.stream().mapToDouble(Double::doubleValue).average().orElse(0);
if (avgDown == 0) {
return 100.0;
}
return 100 - (100 / (1 + avgUp / avgDown));
}
public static String generateReport(List<StockDailyData> dataList, int period) {
try {
Double rsiValue = calculateRSI(dataList, period);
StringBuilder report = new StringBuilder();
report.append("📊 【RSI技术分析报告】\n");
report.append(String.format("📈 当前 %d 日 RSI 值为:%.2f\n", period, rsiValue));
if (rsiValue >= 80) {
report.append("🔴 状态:严重超买!警惕高位回调风险。\n");
} else if (rsiValue >= 70) {
report.append("🟠 状态:轻度超买,短期有回调压力。\n");
} else if (rsiValue <= 20) {
report.append("🟢 状态:严重超卖!关注反弹机会。\n");
} else if (rsiValue <= 30) {
report.append("🟡 状态:轻度超卖,短期内可能反弹。\n");
} else {
report.append("🔵 状态:处于中性区间,震荡行情延续。\n");
}
if (rsiValue > 50) {
report.append("📈 多方力量较强,适合逢低介入。\n");
} else {
report.append("📉 空方占优,应谨慎操作或减仓观望。\n");
}
// 判断是否出现背离信号简单比较最近两日收盘价与RSI变化
if (dataList.size() >= period + 2) {
StockDailyData lastDay = dataList.get(dataList.size() - 1);
StockDailyData prevDay = dataList.get(dataList.size() - 2);
Double rsiLast = calculateRSI(dataList.subList(0, dataList.size()), period);
Double rsiPrev = calculateRSI(dataList.subList(0, dataList.size() - 1), period);
if (lastDay.getClosePrice() > prevDay.getClosePrice() && rsiLast < rsiPrev) {
report.append("⚠️ 出现顶背离迹象,请注意上涨动能减弱风险。\n");
} else if (lastDay.getClosePrice() < prevDay.getClosePrice() && rsiLast > rsiPrev) {
report.append("✅ 出现底背离迹象,下跌动能减弱,或迎来反弹。\n");
}
}
return report.toString();
} catch (Exception e) {
return "❌ 分析失败:" + e.getMessage();
}
}
}

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

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

@ -0,0 +1,110 @@
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();
List<List<Object>> JN = List.of();
try {
// 1. 构建请求URL
String url = STOCK_DATA_PREFIX + "/api/aiGoldBullData";
// 2. 使用Map构造请求体参数
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("aigoldBullPrivilegeState", 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> bull = (Map<String, Object>) data.get("AIGoldBull");
kLine20 = (List<List<Object>>) bull.get("KLine20");
JN = (List<List<Object>>) bull.get("JN");
}
}
// 7. 关闭连接
connection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
Map<String, Long> volMap = new HashMap<>();
for (List<Object> v : JN) {
Object volObj = v.get(5);
Long volume = volObj instanceof Number ? ((Number) volObj).longValue() : Long.valueOf(volObj.toString());
volMap.put((String) v.get(0), volume);
}
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));
s.setVolume(volMap.get((String) v.get(0)));
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