diff --git a/pom.xml b/pom.xml index 9ef511d..7e11449 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,13 @@ lombok true + + + + com.alibaba + dashscope-sdk-java + 2.22.0 + diff --git a/src/main/java/com/deepchart/Main.java b/src/main/java/com/deepchart/Main.java new file mode 100644 index 0000000..36107f0 --- /dev/null +++ b/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 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 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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/deepchart/StockApiDemo.java b/src/main/java/com/deepchart/StockApiDemo.java new file mode 100644 index 0000000..3e5f088 --- /dev/null +++ b/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(); + } +} diff --git a/src/main/java/com/deepchart/controller/IndexController.java b/src/main/java/com/deepchart/controller/IndexController.java index 502422e..72a573b 100644 --- a/src/main/java/com/deepchart/controller/IndexController.java +++ b/src/main/java/com/deepchart/controller/IndexController.java @@ -25,4 +25,25 @@ public class IndexController { String result = indexService.macd(list); return Result.success("success", result); } + + @PostMapping("/kdj") + public Result kdj(@RequestBody StockInfo stock) { + List list = indexService.getStockData(stock); + String result = indexService.kdj(list); + return Result.success("success", result); + } + + @PostMapping("/kd") + public Result kd(@RequestBody StockInfo stock) { + List list = indexService.getStockData(stock); + String result = indexService.kd(list); + return Result.success("success", result); + } + + @PostMapping("/rsi") + public Result rsi(@RequestBody StockInfo stock) { + List list = indexService.getStockData(stock); + String result = indexService.rsi(list); + return Result.success("success", result); + } } diff --git a/src/main/java/com/deepchart/entity/KDJData.java b/src/main/java/com/deepchart/entity/KDJData.java new file mode 100644 index 0000000..1ccb1da --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/src/main/java/com/deepchart/entity/StockDailyData.java b/src/main/java/com/deepchart/entity/StockDailyData.java index a555aaa..0789507 100644 --- a/src/main/java/com/deepchart/entity/StockDailyData.java +++ b/src/main/java/com/deepchart/entity/StockDailyData.java @@ -14,4 +14,5 @@ public class StockDailyData { private double closePrice; // 收盘价 private double lowPrice; // 最低价 private double highPrice; // 最高价 + private Long volume; // 成交量 } \ No newline at end of file diff --git a/src/main/java/com/deepchart/service/IndexService.java b/src/main/java/com/deepchart/service/IndexService.java index 5a84fd2..e38ab0b 100644 --- a/src/main/java/com/deepchart/service/IndexService.java +++ b/src/main/java/com/deepchart/service/IndexService.java @@ -9,4 +9,10 @@ public interface IndexService { List getStockData(StockInfo stock); String macd(List list); + + String kdj(List list); + + String kd(List list); + + String rsi(List list); } diff --git a/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java b/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java index 28da9f5..814ab8c 100644 --- a/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java +++ b/src/main/java/com/deepchart/service/impl/IndexServiceImpl.java @@ -3,8 +3,7 @@ package com.deepchart.service.impl; import com.deepchart.entity.StockDailyData; import com.deepchart.entity.StockInfo; import com.deepchart.service.IndexService; -import com.deepchart.utils.MACDUtil; -import com.deepchart.utils.StockDataUtil; +import com.deepchart.utils.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -25,4 +24,19 @@ public class IndexServiceImpl implements IndexService { public String macd(List list) { return MACDUtil.analyzeStock(list); } + + @Override + public String kdj(List list) { + return KDJUtil.calculateAndAnalyze(list); + } + + @Override + public String kd(List list) { + return KDUtil.generateReport(list); + } + + @Override + public String rsi(List list) { + return RSIUtil.generateReport(list, 14); + } } diff --git a/src/main/java/com/deepchart/utils/KDJUtil.java b/src/main/java/com/deepchart/utils/KDJUtil.java new file mode 100644 index 0000000..3456be9 --- /dev/null +++ b/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 dataList) { + if (dataList == null || dataList.size() < N) { + return "数据不足,无法计算KDJ指标。"; + } + + List kdjList = new ArrayList<>(); + Queue lowQueue = new LinkedList<>(); + Queue 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 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/deepchart/utils/KDUtil.java b/src/main/java/com/deepchart/utils/KDUtil.java new file mode 100644 index 0000000..9f7e73a --- /dev/null +++ b/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 calculateKD(List dataList) { + if (dataList == null || dataList.size() < N) { + throw new IllegalArgumentException("数据不足,至少需要" + N + "条记录"); + } + + List kdList = new ArrayList<>(); + Deque highs = new ArrayDeque<>(N); + Deque 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 stockDataList) { + List 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/deepchart/utils/RSIUtil.java b/src/main/java/com/deepchart/utils/RSIUtil.java new file mode 100644 index 0000000..fdada58 --- /dev/null +++ b/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 dataList, int period) { + if (dataList == null || dataList.size() <= period) { + throw new IllegalArgumentException("数据不足,无法计算" + period + "日RSI"); + } + + List ups = new ArrayList<>(); + List 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 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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/deepchart/utils/StockDataUtil.java b/src/main/java/com/deepchart/utils/StockDataUtil.java index ce1f77a..0e94862 100644 --- a/src/main/java/com/deepchart/utils/StockDataUtil.java +++ b/src/main/java/com/deepchart/utils/StockDataUtil.java @@ -30,14 +30,15 @@ public class StockDataUtil { public List getStockData(StockInfo stock) { List> kLine20 = List.of(); + List> JN = List.of(); try { // 1. 构建请求URL - String url = STOCK_DATA_PREFIX + "/api/superBrainData"; + String url = STOCK_DATA_PREFIX + "/api/aiGoldBullData"; // 2. 使用Map构造请求体参数 Map requestBody = new HashMap<>(); - requestBody.put("brainPrivilegeState", 1); + 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()); @@ -71,8 +72,9 @@ public class StockDataUtil { // 解析JSON响应 Map jsonResponse = objectMapper.readValue(response.toString(), Map.class); Map data = (Map) jsonResponse.get("data"); - Map brain = (Map) data.get("Brain"); - kLine20 = (List>) brain.get("KLine20"); + Map bull = (Map) data.get("AIGoldBull"); + kLine20 = (List>) bull.get("KLine20"); + JN = (List>) bull.get("JN"); } } @@ -83,6 +85,13 @@ public class StockDataUtil { e.printStackTrace(); } + Map volMap = new HashMap<>(); + for (List 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 list = new ArrayList<>(); for (List v : kLine20) { @@ -92,6 +101,7 @@ public class StockDataUtil { 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); }