From fccf059416ecec44bbba68a6ffeb1e6945ef005c Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Sat, 6 Sep 2025 20:32:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=85=A5=E5=BA=93?= =?UTF-8?q?=E5=8E=9F=E5=A7=8B=E6=97=A5=E7=BA=BF=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/data_source_mapping.xml | 6 ++ .../server/LeopardServerApplication.java | 10 ++ .../leopard/server/entity/Daily.java | 58 ++++++++++ .../server/repository/DailyRepository.java | 14 +++ .../leopard/server/service/DailyService.java | 22 ++++ .../server/service/TuShareService.java | 71 +++++++++--- .../server/service/task/TaskMonitorNodes.java | 6 +- .../server/service/task/UpdateDailyNode.java | 101 ++++++++++++++++++ ...ormationNode.java => UpdateStockNode.java} | 21 ++-- leopard-server/src/main/resources/flow.xml | 5 +- .../src/test/resources/tushare.http | 61 +++++++++++ 11 files changed, 351 insertions(+), 24 deletions(-) create mode 100644 .idea/data_source_mapping.xml create mode 100644 leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/entity/Daily.java create mode 100644 leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/repository/DailyRepository.java create mode 100644 leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/DailyService.java create mode 100644 leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateDailyNode.java rename leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/{UpdateStockInformationNode.java => UpdateStockNode.java} (77%) diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml new file mode 100644 index 0000000..cd6d0d2 --- /dev/null +++ b/.idea/data_source_mapping.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/LeopardServerApplication.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/LeopardServerApplication.java index 76794da..f493c2f 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/LeopardServerApplication.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/LeopardServerApplication.java @@ -1,6 +1,9 @@ package com.lanyuanxiaoyao.leopard.server; import com.blinkfox.fenix.EnableFenix; +import com.lanyuanxiaoyao.leopard.server.service.TuShareService; +import com.yomahub.liteflow.core.FlowExecutor; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; @@ -21,7 +24,14 @@ public class LeopardServerApplication implements ApplicationRunner { SpringApplication.run(LeopardServerApplication.class, args); } + @Resource + private FlowExecutor executor; + @Resource + private TuShareService tuShareService; + @Override public void run(ApplicationArguments args) { + executor.execute2RespWithEL("THEN(update_daily)"); + // executor.execute2RespWithEL("THEN(update_stock)"); } } diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/entity/Daily.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/entity/Daily.java new file mode 100644 index 0000000..07c7ed3 --- /dev/null +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/entity/Daily.java @@ -0,0 +1,58 @@ +package com.lanyuanxiaoyao.leopard.server.entity; + +import com.lanyuanxiaoyao.leopard.server.Constants; +import com.lanyuanxiaoyao.service.template.entity.SimpleEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDate; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.FieldNameConstants; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.SoftDelete; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Setter +@Getter +@ToString(callSuper = true) +@FieldNameConstants +@Entity +@SoftDelete +@DynamicUpdate +@DynamicInsert +@EntityListeners(AuditingEntityListener.class) +@Table(name = Constants.DATABASE_PREFIX + "daily") +public class Daily extends SimpleEntity { + @Column(nullable = false) + private LocalDate tradeDate; + @Comment("开盘价") + private Double open; + @Comment("最高价") + private Double high; + @Comment("最低价") + private Double low; + @Comment("收盘价") + private Double close; + @Comment("昨收价") + private Double previousClose; + @Comment("涨跌额") + private Double priceChangeAmount; + @Comment("涨跌幅") + private Double priceFluctuationRange; + @Comment("成交量") + private Double volume; + @Comment("成交额") + private Double turnover; + @Comment("除权因子") + private Double factor; + + @ManyToOne + @ToString.Exclude + private Stock stock; +} diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/repository/DailyRepository.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/repository/DailyRepository.java new file mode 100644 index 0000000..75ce150 --- /dev/null +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/repository/DailyRepository.java @@ -0,0 +1,14 @@ +package com.lanyuanxiaoyao.leopard.server.repository; + +import com.lanyuanxiaoyao.leopard.server.entity.Daily; +import com.lanyuanxiaoyao.service.template.repository.SimpleRepository; +import java.time.LocalDate; +import java.util.List; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface DailyRepository extends SimpleRepository { + @Query("select distinct daily.tradeDate from Daily daily") + List findDistinctTradeDate(); +} diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/DailyService.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/DailyService.java new file mode 100644 index 0000000..161824d --- /dev/null +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/DailyService.java @@ -0,0 +1,22 @@ +package com.lanyuanxiaoyao.leopard.server.service; + +import com.lanyuanxiaoyao.leopard.server.entity.Daily; +import com.lanyuanxiaoyao.leopard.server.repository.DailyRepository; +import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport; +import java.time.LocalDate; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class DailyService extends SimpleServiceSupport { + private final DailyRepository dailyRepository; + + public DailyService(DailyRepository repository) { + super(repository); + this.dailyRepository = repository; + } + + public List findDistinctTradeDate() { + return dailyRepository.findDistinctTradeDate(); + } +} diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/TuShareService.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/TuShareService.java index 424b019..0e69afa 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/TuShareService.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/TuShareService.java @@ -3,6 +3,8 @@ package com.lanyuanxiaoyao.leopard.server.service; import cn.hutool.http.HttpUtil; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import lombok.SneakyThrows; @@ -21,6 +23,7 @@ import org.springframework.stereotype.Service; public class TuShareService { private static final String API_URL = "https://api.tushare.pro"; private static final String API_TOKEN = "64ebff4fa679167600b905ee45dd88e76f3963c0ff39157f3f085f0e"; + public static final DateTimeFormatter TRADE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd"); private final ObjectMapper mapper; @@ -31,19 +34,61 @@ public class TuShareService { @SneakyThrows private String buildRequest(String apiName, Map params, List fields) { return mapper.writeValueAsString(Map.of( - "api_name", apiName, - "token", API_TOKEN, - "params", params, - "fields", fields + "api_name", apiName, + "token", API_TOKEN, + "params", params, + "fields", fields )); } @SneakyThrows public TuShareResponse stockList() { var response = HttpUtil.post(API_URL, buildRequest( - "stock_basic", - Map.of("list_status", "D,P,L"), - List.of("ts_code", "name", "fullname", "exchange", "industry", "list_status") + "stock_basic", + Map.of("list_status", "L"), + List.of("ts_code", "name", "fullname", "exchange", "industry") + )); + var tuShareResponse = mapper.readValue(response, TuShareResponse.class); + if (tuShareResponse.code != 0) { + throw new RuntimeException(tuShareResponse.message); + } + return tuShareResponse; + } + + @SneakyThrows + public TuShareResponse dailyList(LocalDate tradeDate) { + var response = HttpUtil.post(API_URL, buildRequest( + "daily", + Map.of("trade_date", tradeDate.format(TRADE_FORMAT)), + List.of("ts_code", "trade_date", "open", "high", "low", "close", "pre_close", "change", "pct_chg", "vol", "amount") + )); + var tuShareResponse = mapper.readValue(response, TuShareResponse.class); + if (tuShareResponse.code != 0) { + throw new RuntimeException(tuShareResponse.message); + } + return tuShareResponse; + } + + @SneakyThrows + public TuShareResponse tradeDateList(String exchange) { + var response = HttpUtil.post(API_URL, buildRequest( + "trade_cal", + Map.of("exchange", exchange, "is_open", 1), + List.of("cal_date") + )); + var tuShareResponse = mapper.readValue(response, TuShareResponse.class); + if (tuShareResponse.code != 0) { + throw new RuntimeException(tuShareResponse.message); + } + return tuShareResponse; + } + + @SneakyThrows + public TuShareResponse factorList(LocalDate tradeDate) { + var response = HttpUtil.post(API_URL, buildRequest( + "adj_factor", + Map.of("trade_date", tradeDate.format(TRADE_FORMAT)), + List.of("ts_code", "trade_date", "adj_factor") )); var tuShareResponse = mapper.readValue(response, TuShareResponse.class); if (tuShareResponse.code != 0) { @@ -53,14 +98,14 @@ public class TuShareService { } public record TuShareResponse( - Integer code, - @JsonProperty("msg") - String message, - Data data + Integer code, + @JsonProperty("msg") + String message, + Data data ) { public record Data( - List fields, - List> items + List fields, + List> items ) { } } diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/TaskMonitorNodes.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/TaskMonitorNodes.java index 9c6f77d..809c266 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/TaskMonitorNodes.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/TaskMonitorNodes.java @@ -26,8 +26,8 @@ public class TaskMonitorNodes { @LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "task_start", nodeName = "任务开始", nodeType = NodeTypeEnum.COMMON) public void taskStart(NodeComponent node) { - var context = node.getContextBean(TaskMonitorContext.class); - if (ObjectUtil.isNotNull(context)) { + try { + var context = node.getContextBean(TaskMonitorContext.class); var task = new Task(); task.setName(context.getTemplate().getName()); task.setDescription(context.getTemplate().getDescription()); @@ -35,6 +35,8 @@ public class TaskMonitorNodes { task.setLaunchedTime(LocalDateTime.now()); var taskId = taskService.save(task); context.setTaskId(taskId); + } catch (Exception exception) { + log.warn("Not in task", exception); } } diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateDailyNode.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateDailyNode.java new file mode 100644 index 0000000..d90b9f1 --- /dev/null +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateDailyNode.java @@ -0,0 +1,101 @@ +package com.lanyuanxiaoyao.leopard.server.service.task; + +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.lanyuanxiaoyao.leopard.server.entity.Daily; +import com.lanyuanxiaoyao.leopard.server.entity.Stock; +import com.lanyuanxiaoyao.leopard.server.service.DailyService; +import com.lanyuanxiaoyao.leopard.server.service.StockService; +import com.lanyuanxiaoyao.leopard.server.service.TuShareService; +import com.yomahub.liteflow.annotation.LiteflowComponent; +import com.yomahub.liteflow.core.NodeComponent; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.support.TransactionTemplate; + +@Slf4j +@LiteflowComponent("update_daily") +public class UpdateDailyNode extends NodeComponent { + private final StockService stockService; + private final DailyService dailyService; + private final TuShareService tuShareService; + private final TransactionTemplate transactionTemplate; + + public UpdateDailyNode(StockService stockService, DailyService dailyService, TuShareService tuShareService, TransactionTemplate transactionTemplate) { + this.stockService = stockService; + this.dailyService = dailyService; + this.tuShareService = tuShareService; + this.transactionTemplate = transactionTemplate; + } + + // @Transactional(rollbackOn = Throwable.class) + @Override + public void process() { + var tradeDates = new HashSet(); + for (String exchange : List.of("SSE", "SZSE", "BSE")) { + var response = tuShareService.tradeDateList(exchange); + for (List item : response.data().items()) { + if (ObjectUtil.isNotEmpty(item) && StrUtil.isNotBlank(item.get(0))) { + tradeDates.add(LocalDate.parse(item.get(0), TuShareService.TRADE_FORMAT)); + } + } + } + var existsTradeDates = dailyService.findDistinctTradeDate(); + var nowDate = LocalDate.now(); + var targetDates = tradeDates.stream() + .filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate)) + .filter(date -> !existsTradeDates.contains(date)) + .sorted() + .toList(); + log.info("Target: {}", targetDates); + var stocks = stockService.list(); + var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock)); + for (LocalDate tradeDate : targetDates) { + log.info("Trade date: {}", tradeDate); + var factorResponse = tuShareService.factorList(tradeDate); + var factorMap = new HashMap(); + for (List item : factorResponse.data().items()) { + factorMap.put(item.get(0), Double.valueOf(item.get(2))); + } + log.info("Factor: {}", factorMap); + + var response = tuShareService.dailyList(tradeDate); + transactionTemplate.execute(status -> { + try { + for (List item : response.data().items()) { + var code = item.get(0); + if (stocksMap.containsKey(code)) { + var stock = stocksMap.get(code); + var factor = factorMap.get(code); + var daily = new Daily(); + daily.setTradeDate(LocalDate.parse(item.get(1), TuShareService.TRADE_FORMAT)); + daily.setOpen(Double.valueOf(item.get(2))); + daily.setHigh(Double.valueOf(item.get(3))); + daily.setLow(Double.valueOf(item.get(4))); + daily.setClose(Double.valueOf(item.get(5))); + daily.setPreviousClose(Double.valueOf(item.get(6))); + daily.setPriceChangeAmount(Double.valueOf(item.get(7))); + daily.setPriceFluctuationRange(Double.valueOf(item.get(8))); + daily.setVolume(Double.valueOf(item.get(9))); + daily.setTurnover(Double.valueOf(item.get(10))); + daily.setFactor(factor); + daily.setStock(stock); + log.info("Daily: {}", daily); + dailyService.save(daily); + } + } + return true; + } catch (Exception exception) { + status.setRollbackOnly(); + return false; + } + }); + ThreadUtil.safeSleep(1000); + } + } +} diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStockInformationNode.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStockNode.java similarity index 77% rename from leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStockInformationNode.java rename to leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStockNode.java index 1741046..33aacdf 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStockInformationNode.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStockNode.java @@ -1,30 +1,31 @@ package com.lanyuanxiaoyao.leopard.server.service.task; import cn.hutool.core.util.EnumUtil; -import cn.hutool.core.util.StrUtil; import com.lanyuanxiaoyao.leopard.server.entity.Stock; import com.lanyuanxiaoyao.leopard.server.service.StockService; import com.lanyuanxiaoyao.leopard.server.service.TuShareService; import com.yomahub.liteflow.annotation.LiteflowComponent; import com.yomahub.liteflow.core.NodeComponent; import jakarta.transaction.Transactional; +import java.util.HashSet; import java.util.stream.Collectors; -@LiteflowComponent("update_stock_information") -public class UpdateStockInformationNode extends NodeComponent { +@LiteflowComponent("update_stock") +public class UpdateStockNode extends NodeComponent { private final StockService stockService; private final TuShareService tuShareService; - public UpdateStockInformationNode(StockService stockService, TuShareService tuShareService) { + public UpdateStockNode(StockService stockService, TuShareService tuShareService) { this.stockService = stockService; this.tuShareService = tuShareService; } @Transactional(rollbackOn = Throwable.class) @Override - public void process() throws Exception { + public void process() { var stocks = stockService.list(); var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock)); + var targetCodes = new HashSet(); tuShareService.stockList() .data() .items() @@ -34,14 +35,12 @@ public class UpdateStockInformationNode extends NodeComponent { var fullname = item.get(2); var market = EnumUtil.fromString(Stock.Market.class, item.get(3)); var industry = item.get(4); - var listed = StrUtil.equals("L", item.get(5)); if (stocksMap.containsKey(code)) { var stock = stocksMap.get(code); stock.setName(name); stock.setFullname(fullname); stock.setMarket(market); stock.setIndustry(industry); - stock.setListed(listed); } else { var stock = new Stock(); stock.setCode(code); @@ -49,10 +48,16 @@ public class UpdateStockInformationNode extends NodeComponent { stock.setFullname(fullname); stock.setMarket(market); stock.setIndustry(industry); - stock.setListed(listed); stocks.add(stock); } + targetCodes.add(code); }); + var deleteStocks = stocks.stream() + .filter(stock -> !targetCodes.contains(stock.getCode())) + .map(Stock::getId) + .toList(); + stockService.remove(deleteStocks); + stockService.save(stocks); } } diff --git a/leopard-server/src/main/resources/flow.xml b/leopard-server/src/main/resources/flow.xml index a7d4480..a4a28c8 100644 --- a/leopard-server/src/main/resources/flow.xml +++ b/leopard-server/src/main/resources/flow.xml @@ -2,6 +2,9 @@ - CATCH(THEN(task_start, update_stock_information, task_end)).DO(task_error) + CATCH(THEN(task_start, update_stock, task_end)).DO(task_error) + + + CATCH(THEN(task_start, update_daily, task_end)).DO(task_error) diff --git a/leopard-server/src/test/resources/tushare.http b/leopard-server/src/test/resources/tushare.http index 8f8f8ca..ae4b472 100644 --- a/leopard-server/src/test/resources/tushare.http +++ b/leopard-server/src/test/resources/tushare.http @@ -45,3 +45,64 @@ Content-Type: application/json "total_revenue_to_parent" ] } + +### Get daily list +POST {{api_url}} +Content-Type: application/json + +{ + "api_name": "daily", + "token": "{{api_key}}", + "params": { + "trade_date": "19901219" + }, + "fields": [ + "ts_code", + "trade_date", + "open", + "high", + "low", + "close", + "pre_close", + "change", + "pct_chg", + "vol", + "amount" + ] +} + +### Get trade date +POST {{api_url}} +Content-Type: application/json + +{ + "api_name": "trade_cal", + "token": "{{api_key}}", + "params": { + "exchange": "BSE", + "is_open": 1 + }, + "fields": [ + "exchange", + "cal_date", + "is_open", + "pretrade_date" + ] +} + +### Get Factor +POST {{api_url}} +Content-Type: application/json + +{ + "api_name": "adj_factor", + "token": "{{api_key}}", + "params": { + "trade_date": "20241231" + }, + "fields": [ + "ts_code", + "trade_date", + "adj_factor" + ] +}