Compare commits
merge into: majun:master
majun:dev
majun:liruiqiang
majun:majun
majun:master
pull from: majun:majun
majun:dev
majun:liruiqiang
majun:majun
majun:master
3 Commits
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
47ffb8deef |
11.24 11:08
完成四个股票基本指标的插件 |
5 days ago |
|
|
bf36f084b4 |
11.18 22:22
完成MACD指标的插件 |
1 week ago |
|
|
423959272a |
11.18 16:50
完成连续交易日股票基本数据的工具类 |
2 weeks ago |
16 changed files with 976 additions and 12 deletions
-
19pom.xml
-
125src/main/java/com/deepchart/Main.java
-
27src/main/java/com/deepchart/StockApiDemo.java
-
39src/main/java/com/deepchart/controller/IndexController.java
-
19src/main/java/com/deepchart/entity/KDJData.java
-
22src/main/java/com/deepchart/entity/MACDData.java
-
18src/main/java/com/deepchart/entity/StockDailyData.java
-
9src/main/java/com/deepchart/entity/StockInfo.java
-
14src/main/java/com/deepchart/service/IndexService.java
-
34src/main/java/com/deepchart/service/impl/IndexServiceImpl.java
-
102src/main/java/com/deepchart/utils/KDJUtil.java
-
108src/main/java/com/deepchart/utils/KDUtil.java
-
227src/main/java/com/deepchart/utils/MACDUtil.java
-
91src/main/java/com/deepchart/utils/RSIUtil.java
-
24src/main/java/com/deepchart/utils/Result.java
-
110src/main/java/com/deepchart/utils/StockDataUtil.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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -1,10 +1,49 @@ |
|||
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); |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; // 成交量 |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
package com.deepchart.entity; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class StockInfo { |
|||
private String market; |
|||
private String code; |
|||
} |
|||
@ -1,4 +1,18 @@ |
|||
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); |
|||
} |
|||
@ -1,8 +1,42 @@ |
|||
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 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); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
|
|||
// 计算DEA(DIF的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"); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue