diff --git a/leopard-core/pom.xml b/leopard-core/pom.xml index 3f153f6..0dfe9d4 100644 --- a/leopard-core/pom.xml +++ b/leopard-core/pom.xml @@ -25,6 +25,15 @@ hutool-core + + io.github.ralfkonrad.quantlib_for_maven + quantlib + + + org.ta4j + ta4j-core + + org.hibernate.orm hibernate-ant diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/Task.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/Task.java index 27b3202..1b7c01d 100644 --- a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/Task.java +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/Task.java @@ -61,7 +61,7 @@ public class Task extends SimpleEntity { private Status status = Status.RUNNING; @Column(nullable = false) @Comment("任务进度") - private Integer step = 0; + private Double step = 0.0; @Comment("任务开始时间") private LocalDateTime launchedTime; @Comment("任务结束时间") diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/TaskTemplate.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/TaskTemplate.java deleted file mode 100644 index 5a40d70..0000000 --- a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/TaskTemplate.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.lanyuanxiaoyao.leopard.core.entity; - -import com.lanyuanxiaoyao.leopard.core.Constants; -import com.lanyuanxiaoyao.service.template.entity.SimpleEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import lombok.experimental.FieldNameConstants; -import org.hibernate.annotations.DynamicInsert; -import org.hibernate.annotations.DynamicUpdate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -@Setter -@Getter -@ToString(callSuper = true) -@FieldNameConstants -@Entity -@DynamicUpdate -@DynamicInsert -@EntityListeners(AuditingEntityListener.class) -@Table(name = Constants.DATABASE_PREFIX + "task_template") -public class TaskTemplate extends SimpleEntity { - @Column(nullable = false) - private String name; - @Column(nullable = false, length = 500) - private String description; - @Column(nullable = false) - private String application; - @Column(nullable = false) - private String chain; - @Column(nullable = false) - private String expression; - @Column(nullable = false) - private String expressionEl; -} \ No newline at end of file diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/TaskRepository.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/TaskRepository.java index 152f2cb..efb8416 100644 --- a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/TaskRepository.java +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/TaskRepository.java @@ -5,6 +5,7 @@ import com.lanyuanxiaoyao.service.template.repository.SimpleRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; /** * @author lanyuanxiaoyao @@ -16,7 +17,8 @@ public interface TaskRepository extends SimpleRepository { @Query("update Task task set task.status = com.lanyuanxiaoyao.leopard.core.entity.Task.Status.FAILURE where task.status = com.lanyuanxiaoyao.leopard.core.entity.Task.Status.RUNNING") void updateAllRunningTaskToFailure(); + @Transactional(rollbackFor = Throwable.class) @Modifying - @Query("update Task task set task.step = ?1 where task.id = ?2") - void updateStepById(Integer step, Long id); + @Query("update Task task set task.step = ?2 where task.id = ?1") + void updateStepById(Long id, Double step); } diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/TaskTemplateRepository.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/TaskTemplateRepository.java deleted file mode 100644 index 9b84f40..0000000 --- a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/TaskTemplateRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.lanyuanxiaoyao.leopard.core.repository; - -import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate; -import com.lanyuanxiaoyao.service.template.repository.SimpleRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface TaskTemplateRepository extends SimpleRepository { -} diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/AssessmentService.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/AssessmentService.java new file mode 100644 index 0000000..eebcb40 --- /dev/null +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/AssessmentService.java @@ -0,0 +1,94 @@ +package com.lanyuanxiaoyao.leopard.core.service; + +import cn.hutool.core.util.ObjectUtil; +import com.lanyuanxiaoyao.leopard.core.entity.Daily; +import com.lanyuanxiaoyao.leopard.core.entity.QDaily; +import com.lanyuanxiaoyao.leopard.core.entity.Stock; +import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; +import org.springframework.stereotype.Service; + +/** + * 股票评估 + * + * @author lanyuanxiaoyao + * @version 20250924 + */ +@Slf4j +@Service +public class AssessmentService { + private final IndustryService industryService; + private final DailyRepository dailyRepository; + + public AssessmentService(IndustryService industryService, DailyRepository dailyRepository) { + this.industryService = industryService; + this.dailyRepository = dailyRepository; + } + + public Set assess(Set stocks, int year) { + if (ObjectUtil.isNotEmpty(stocks)) { + var industries = stocks + .stream() + .map(Stock::getIndustry) + .collect(Collectors.toSet()); + var topChange = industryService.topChange(year, industries, stocks); + var dailyMap = dailyRepository.findAll( + QDaily.daily.tradeDate.year().eq(year) + .and(QDaily.daily.stock.in(stocks)) + ) + .stream() + .collect(Collectors.groupingBy(Daily::getStock)); + return stocks + .stream() + .filter(stock -> { + if (!dailyMap.containsKey(stock) || ObjectUtil.isEmpty(dailyMap.get(stock))) { + log.warn("Cannot find daily data in {} for {}", year, stock.getCode()); + return false; + } + return true; + }) + .map(stock -> { + var dailies = dailyMap.get(stock) + .stream() + .sorted(Comparator.comparing(Daily::getTradeDate)) + .toList(); + var change = getChange(dailies); + var std = getStd(dailies); + var industryTop = topChange.getOrDefault(new IndustryService.IndustryYearlyKey(stock.getIndustry(), year), 0.0); + return new Result(stock, change, std, industryTop); + }) + .collect(Collectors.toSet()); + } + return Set.of(); + } + + private double getChange(List dailies) { + return (dailies.getLast().getHfqClose() - dailies.getFirst().getHfqClose()) / dailies.getFirst().getHfqClose(); + } + + private double getStd(List dailies) { + var statistics = new DescriptiveStatistics(); + dailies.forEach(daily -> statistics.addValue(daily.getHfqClose())); + return statistics.getStandardDeviation(); + } + + public record Result(Stock stock, double change, double std, double industryTop) { + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + + Result result = (Result) o; + return stock.equals(result.stock); + } + + @Override + public int hashCode() { + return stock.hashCode(); + } + } +} diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/IndustryService.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/IndustryService.java new file mode 100644 index 0000000..93f6ca7 --- /dev/null +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/IndustryService.java @@ -0,0 +1,93 @@ +package com.lanyuanxiaoyao.leopard.core.service; + +import com.lanyuanxiaoyao.leopard.core.entity.Daily; +import com.lanyuanxiaoyao.leopard.core.entity.QDaily; +import com.lanyuanxiaoyao.leopard.core.entity.Stock; +import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; +import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 计算行业相关指标 + * + * @author lanyuanxiaoyao + * @version 20250924 + */ +@Slf4j +@Service +public class IndustryService { + private final StockRepository stockRepository; + private final DailyRepository dailyRepository; + + public IndustryService(StockRepository stockRepository, DailyRepository dailyRepository) { + this.stockRepository = stockRepository; + this.dailyRepository = dailyRepository; + } + + public Map topChange(int year) { + return topChange(year, null, null); + } + + public Map topChange(int year, Set includeIndustries) { + return topChange(year, includeIndustries, null); + } + + public Map topChange(int year, Set includeIndustries, Set includeStocks) { + return topChange(year, year, includeIndustries, includeStocks); + } + + public Map topChange(int startYear, int endYear) { + return topChange(startYear, endYear, null, null); + } + + public Map topChange(int startYear, int endYear, Set includeIndustries) { + return topChange(startYear, endYear, includeIndustries, null); + } + + public Map topChange(int startYear, int endYear, Set includeIndustries, Set includeStocks) { + return stockRepository.findDistinctIndustries() + .parallelStream() + .filter(industry -> includeIndustries == null || includeIndustries.contains(industry)) + .flatMap(industry -> { + var keys = new ArrayList(); + for (int year = startYear; year <= endYear; year++) { + keys.add(new IndustryYearlyKey(industry, year)); + } + return keys.stream(); + }) + .map(key -> { + log.info("计算行业 {} {} 年度涨跌幅", key.industry(), key.year()); + var maxChange = dailyRepository + .findAll( + QDaily.daily.stock.industry.eq(key.industry()) + .and(QDaily.daily.stock.in(includeStocks)) + .and(QDaily.daily.tradeDate.year().eq(key.year())), + QDaily.daily.tradeDate.asc() + ) + .stream() + .collect(Collectors.groupingBy(Daily::getStock)) + .values() + .stream() + .mapToDouble(dailies -> { + var dailiesSorted = dailies + .stream() + .sorted(Comparator.comparing(Daily::getTradeDate)) + .toList(); + return (dailiesSorted.getLast().getHfqClose() - dailiesSorted.getFirst().getHfqClose()) / dailiesSorted.getFirst().getHfqClose(); + }) + .max() + .orElse(0.0); + return Map.entry(key, maxChange); + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + public record IndustryYearlyKey(String industry, int year) { + } +} diff --git a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/PyramidStockSelector.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/selector/PyramidStockSelector.java similarity index 90% rename from leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/PyramidStockSelector.java rename to leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/selector/PyramidStockSelector.java index 3a38ac0..bc02734 100644 --- a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/PyramidStockSelector.java +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/selector/PyramidStockSelector.java @@ -1,4 +1,4 @@ -package com.lanyuanxiaoyao.leopard.strategy; +package com.lanyuanxiaoyao.leopard.core.service.selector; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; @@ -6,44 +6,39 @@ import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator; import com.lanyuanxiaoyao.leopard.core.entity.QStock; import com.lanyuanxiaoyao.leopard.core.entity.Stock; import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; -import com.yomahub.liteflow.annotation.LiteflowComponent; -import com.yomahub.liteflow.core.NodeComponent; import java.time.LocalDate; -import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.stereotype.Service; /** * 金字塔选股 * * @author lanyuanxiaoyao - * @version 20250917 + * @version 20250924 */ @Slf4j -@LiteflowComponent("pyramid_stock_selector") -public class PyramidStockSelector extends NodeComponent { +@Service +public class PyramidStockSelector implements StockSelector { private final StockRepository stockRepository; public PyramidStockSelector(StockRepository stockRepository) { this.stockRepository = stockRepository; } - @Transactional(rollbackFor = Throwable.class) @Override - public void process() { - var assessment = getContextBean(StockAssessmentNode.StockAssessment.class); - + public Set select(Request request) { // 选择至少有最近5年财报的股票 // 有点奇怪,001400.SZ有近5年的财报但资料显示是2025年才上市的 - var stocks = stockRepository.findAll(QStock.stock.listedDate.before(LocalDate.of(assessment.year(), 1, 1))); - log.info("Year: {} Stock: {}", assessment.year(), stocks.size()); + var stocks = stockRepository.findAll(QStock.stock.listedDate.before(LocalDate.of(request.year(), 1, 1))); + log.info("Year: {} Stock: {}", request.year(), stocks.size()); var scores = stocks.stream().collect(Collectors.toMap(stock -> stock, code -> 0)); for (Stock stock : stocks) { var recentIndicators = stock.getIndicators() .stream() - .filter(indicator -> indicator.getYear() < assessment.year()) + .filter(indicator -> indicator.getYear() < request.year()) .sorted((a, b) -> b.getYear() - a.getYear()) .limit(5) .toList(); @@ -212,13 +207,17 @@ public class PyramidStockSelector extends NodeComponent { } scores.put(stock, scores.get(stock) + cashAscendingScore); } - var first50 = scores.entrySet() + return scores.entrySet() .stream() .sorted((e1, e2) -> e2.getValue() - e1.getValue()) - .limit(50) - .map(Map.Entry::getKey) + .limit(request.limit()) + .map(entry -> new Candidate(entry.getKey(), entry.getValue())) .collect(Collectors.toSet()); + } - assessment.stocks().addAll(first50); + public record Request(int year, int limit) { + public Request(int year) { + this(year, 50); + } } } diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/selector/StockSelector.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/selector/StockSelector.java new file mode 100644 index 0000000..a5e200c --- /dev/null +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/selector/StockSelector.java @@ -0,0 +1,21 @@ +package com.lanyuanxiaoyao.leopard.core.service.selector; + +import com.lanyuanxiaoyao.leopard.core.entity.Stock; +import java.util.Map; +import java.util.Set; + +/** + * 选股器 + * + * @author lanyuanxiaoyao + * @version 20250924 + */ +public interface StockSelector { + Set select(T request); + + record Candidate(Stock stock, double score, Map extra) { + public Candidate(Stock stock, double score) { + this(stock, score, Map.of()); + } + } +} diff --git a/leopard-server/pom.xml b/leopard-server/pom.xml index ee1b9e2..2379018 100644 --- a/leopard-server/pom.xml +++ b/leopard-server/pom.xml @@ -41,15 +41,6 @@ spring-boot-starter-quartz - - com.yomahub - liteflow-spring-boot-starter - - - com.yomahub - liteflow-rule-sql - - cn.hutool hutool-core 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 df7a657..e49251f 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,8 +1,5 @@ package com.lanyuanxiaoyao.leopard.server; -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; @@ -22,15 +19,7 @@ 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)"); - // executor.execute2RespWithEL("THEN(check_daily)"); } } diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/CommonOptionsController.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/CommonOptionsController.java index 20c7a36..f3ba91e 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/CommonOptionsController.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/CommonOptionsController.java @@ -3,7 +3,7 @@ package com.lanyuanxiaoyao.leopard.server.controller; import com.lanyuanxiaoyao.leopard.core.entity.Stock; import com.lanyuanxiaoyao.leopard.core.entity.Task; import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; -import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService; +import com.lanyuanxiaoyao.leopard.server.service.TaskService; import com.lanyuanxiaoyao.service.template.controller.GlobalResponse; import java.util.Arrays; import java.util.List; @@ -87,11 +87,11 @@ public class CommonOptionsController { ); private final StockRepository stockRepository; - private final TaskTemplateService taskTemplateService; + private final TaskService taskService; - public CommonOptionsController(StockRepository stockRepository, TaskTemplateService taskTemplateService) { + public CommonOptionsController(StockRepository stockRepository, TaskService taskService) { this.stockRepository = stockRepository; - this.taskTemplateService = taskTemplateService; + this.taskService = taskService; } @GetMapping("/options/{name}") @@ -109,9 +109,9 @@ public class CommonOptionsController { .toList() ); case "task_template_id" -> GlobalResponse.responseSuccess( - taskTemplateService.list() + taskService.getTemplates() .stream() - .map(template -> new Option(template.getName(), template.getId())) + .map(template -> new Option(template.name(), template.id())) .toList() ); default -> GlobalResponse.responseSuccess(List.of()); diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/QuartzController.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/QuartzController.java index b8c01b6..8c6cca1 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/QuartzController.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/QuartzController.java @@ -1,7 +1,7 @@ package com.lanyuanxiaoyao.leopard.server.controller; import com.lanyuanxiaoyao.leopard.server.service.QuartzService; -import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService; +import com.lanyuanxiaoyao.leopard.server.service.TaskService; import com.lanyuanxiaoyao.service.template.controller.GlobalResponse; import java.time.LocalDateTime; import java.util.List; @@ -20,11 +20,11 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping("task_schedule") public class QuartzController { private final QuartzService quartzService; - private final TaskTemplateService taskTemplateService; + private final TaskService taskService; - public QuartzController(QuartzService quartzService, TaskTemplateService taskTemplateService) { + public QuartzController(QuartzService quartzService, TaskService taskService) { this.quartzService = quartzService; - this.taskTemplateService = taskTemplateService; + this.taskService = taskService; } @PostMapping("save") @@ -38,11 +38,11 @@ public class QuartzController { var list = quartzService.list() .stream() .map(task -> { - var template = taskTemplateService.detail(task.templateId()); + var template = taskService.getTemplate(task.templateId()); return new ListItem( task.key(), - template.getName(), - template.getDescription(), + template.name(), + template.description(), task.cron(), task.status(), task.previousFireTime(), @@ -72,7 +72,7 @@ public class QuartzController { } public record SaveItem( - Long templateId, + String templateId, String cron ) { } diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/TaskController.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/TaskController.java index bb2f4db..0ac0a06 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/TaskController.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/TaskController.java @@ -12,6 +12,7 @@ import java.time.LocalDateTime; import java.util.Map; import java.util.function.Function; import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -45,6 +46,12 @@ public class TaskController extends SimpleControllerSupport> templateList() { + var templates = taskService.getTemplates(); + return GlobalResponse.responseCrudData(templates, templates.size()); + } + @Override protected Function saveItemMapper() { throw new UnsupportedOperationException(); @@ -108,7 +115,7 @@ public class TaskController extends SimpleControllerSupport params) { + public record ExecuteRequest(String templateId, Map params) { } public record TaskCost(Long cost, String costText) { diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/TaskTemplateController.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/TaskTemplateController.java deleted file mode 100644 index a14bcf2..0000000 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/TaskTemplateController.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.lanyuanxiaoyao.leopard.server.controller; - -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate; -import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService; -import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport; -import java.util.function.Function; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Slf4j -@RestController -@RequestMapping("task_template") -public class TaskTemplateController extends SimpleControllerSupport { - @Value("${spring.application.name}") - private String application; - - public TaskTemplateController(TaskTemplateService service) { - super(service); - } - - @Override - protected Function saveItemMapper() { - return item -> { - var template = new TaskTemplate(); - template.setId(item.id()); - template.setName(item.name()); - template.setDescription(item.description()); - template.setApplication(application); - template.setChain(IdUtil.simpleUUID()); - template.setExpression(item.expression()); - template.setExpressionEl(StrUtil.format("CATCH(THEN(task_start, ({}), task_end)).DO(task_error)", item.expression())); - return template; - }; - } - - private Item convert(TaskTemplate template) { - return new Item( - template.getId(), - template.getName(), - template.getDescription(), - template.getExpression() - ); - } - - @Override - protected Function listItemMapper() { - return this::convert; - } - - @Override - protected Function detailItemMapper() { - return this::convert; - } - - public record Item( - Long id, - String name, - String description, - String expression - ) { - } -} diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/QuartzService.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/QuartzService.java index 4e18d80..873cd4c 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/QuartzService.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/QuartzService.java @@ -2,8 +2,7 @@ package com.lanyuanxiaoyao.leopard.server.service; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; -import com.lanyuanxiaoyao.leopard.server.service.task.TaskMonitorNodes; -import com.yomahub.liteflow.core.FlowExecutor; +import cn.hutool.core.util.StrUtil; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; @@ -39,7 +38,7 @@ public class QuartzService { var trigger = (CronTrigger) scheduler.getTriggersOfJob(key).getFirst(); tasks.add(new QuartzTask( detail.getKey().getName(), - detail.getJobDataMap().getLong("template_id"), + detail.getJobDataMap().getString("template_id"), trigger.getCronExpression(), scheduler.getTriggerState(trigger.getKey()), ObjectUtil.isNull(trigger.getPreviousFireTime()) ? null : LocalDateTime.ofInstant(trigger.getPreviousFireTime().toInstant(), ZoneId.systemDefault()), @@ -49,7 +48,7 @@ public class QuartzService { return tasks; } - public void save(Long templateId, String cron) throws SchedulerException { + public void save(String templateId, String cron) throws SchedulerException { var detail = JobBuilder.newJob(TaskExecutionJob.class) .withIdentity("task_execution_" + IdUtil.fastUUID()) .usingJobData("template_id", templateId) @@ -81,30 +80,27 @@ public class QuartzService { @Slf4j public static class TaskExecutionJob extends QuartzJobBean { - private final TaskTemplateService taskTemplateService; - private final FlowExecutor flowExecutor; + private final TaskService taskService; - public TaskExecutionJob(TaskTemplateService taskTemplateService, FlowExecutor flowExecutor) { - this.taskTemplateService = taskTemplateService; - this.flowExecutor = flowExecutor; + public TaskExecutionJob(TaskService taskService) { + this.taskService = taskService; } + @SuppressWarnings("unchecked") @Override protected void executeInternal(JobExecutionContext context) { var dataMap = context.getMergedJobDataMap(); - var templateId = dataMap.getLong("template_id"); - if (ObjectUtil.isNotNull(templateId)) { - var template = taskTemplateService.detail(templateId); + var templateId = dataMap.getString("template_id"); + if (StrUtil.isNotBlank(templateId)) { var params = (Map) dataMap.getOrDefault("params", Map.of()); - var monitorContext = new TaskMonitorNodes.TaskMonitorContext(template); - flowExecutor.execute2Resp(template.getChain(), params, monitorContext); + taskService.execute(templateId, params, true); } } } public record QuartzTask( String key, - Long templateId, + String templateId, String cron, Trigger.TriggerState status, LocalDateTime previousFireTime, diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/TaskService.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/TaskService.java index 8b557fb..dfbe638 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/TaskService.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/TaskService.java @@ -1,14 +1,26 @@ package com.lanyuanxiaoyao.leopard.server.service; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; import com.lanyuanxiaoyao.leopard.core.entity.Task; import com.lanyuanxiaoyao.leopard.core.repository.TaskRepository; -import com.lanyuanxiaoyao.leopard.server.service.task.TaskMonitorNodes; +import com.lanyuanxiaoyao.leopard.server.service.task.TaskRunner; +import com.lanyuanxiaoyao.leopard.server.service.task.UpdateDailyTask; +import com.lanyuanxiaoyao.leopard.server.service.task.UpdateFinanceIndicatorTask; +import com.lanyuanxiaoyao.leopard.server.service.task.UpdateStockTask; +import com.lanyuanxiaoyao.leopard.server.service.task.UpdateYearlyTask; import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport; -import com.yomahub.liteflow.core.FlowExecutor; import jakarta.transaction.Transactional; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationContext; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; @@ -19,15 +31,24 @@ import org.springframework.stereotype.Service; @Slf4j @Service public class TaskService extends SimpleServiceSupport { + private final ExecutorService executor = Executors.newFixedThreadPool(50); private final TaskRepository taskRepository; - private final TaskTemplateService taskTemplateService; - private final FlowExecutor flowExecutor; + private final ApplicationContext context; - public TaskService(TaskRepository repository, TaskTemplateService taskTemplateService, @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") FlowExecutor flowExecutor) { + @Getter + private final Set templates = Stream.of( + new TaskTemplate("更新股票信息", "更新股票信息", UpdateStockTask.class), + new TaskTemplate("更新年线指标", "更新年线指标", UpdateYearlyTask.class), + new TaskTemplate("更新日线数据", "更新日线数据", UpdateDailyTask.class), + new TaskTemplate("更新财务指标", "更新财务指标", UpdateFinanceIndicatorTask.class) + ).collect(Collectors.toSet()); + private final Map templateMap = templates.stream() + .collect(Collectors.toMap(TaskTemplate::id, template -> template)); + + public TaskService(TaskRepository repository, ApplicationContext context) { super(repository); this.taskRepository = repository; - this.taskTemplateService = taskTemplateService; - this.flowExecutor = flowExecutor; + this.context = context; } @Transactional(rollbackOn = Throwable.class) @@ -37,14 +58,35 @@ public class TaskService extends SimpleServiceSupport { taskRepository.updateAllRunningTaskToFailure(); } - public void execute(Long templateId, Map params) { - var template = taskTemplateService.detail(templateId); - var context = new TaskMonitorNodes.TaskMonitorContext(template); - flowExecutor.execute2Future(template.getChain(), params, context); + public TaskTemplate getTemplate(String templateId) { + return templateMap.get(templateId); } - @Transactional(rollbackOn = Throwable.class) - public void updateStepById(Integer step, Long id) { - taskRepository.updateStepById(step, id); + public void execute(String templateId, Map params) { + execute(templateId, params, true); + } + + public void execute(String templateId, Map params, boolean async) { + var template = templateMap.get(templateId); + if (ObjectUtil.isNull(template)) { + throw new RuntimeException("任务模板不存在"); + } + var instance = context.getBean(template.runnerClass()); + if (async) { + executor.submit(() -> instance.run(template, params)); + } else { + instance.run(template, params); + } + } + + public record TaskTemplate( + String id, + String name, + String description, + Class runnerClass + ) { + public TaskTemplate(String name, String description, Class runnerClass) { + this(IdUtil.fastUUID(), name, description, runnerClass); + } } } diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/TaskTemplateService.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/TaskTemplateService.java deleted file mode 100644 index 0c890a0..0000000 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/TaskTemplateService.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.lanyuanxiaoyao.leopard.server.service; - -import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate; -import com.lanyuanxiaoyao.leopard.core.repository.TaskTemplateRepository; -import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport; -import com.yomahub.liteflow.builder.el.LiteFlowChainELBuilder; -import com.yomahub.liteflow.meta.LiteflowMetaOperator; -import org.springframework.stereotype.Service; - -@Service -public class TaskTemplateService extends SimpleServiceSupport { - public TaskTemplateService(TaskTemplateRepository repository) { - super(repository); - } - - private void validateExpression(String expression) { - var response = LiteFlowChainELBuilder.validateWithEx(expression); - if (!response.isSuccess()) { - throw new RuntimeException(response.getCause()); - } - } - - @Override - public Long save(TaskTemplate entity) { - validateExpression(entity.getExpression()); - Long id = super.save(entity); - LiteflowMetaOperator.reloadAllChain(); - return id; - } - - @Override - public void save(Iterable taskTemplates) { - taskTemplates.forEach(template -> validateExpression(template.getExpression())); - super.save(taskTemplates); - LiteflowMetaOperator.reloadAllChain(); - } - - @Override - public void remove(Iterable ids) { - super.remove(ids); - LiteflowMetaOperator.reloadAllChain(); - } - - @Override - public void remove(Long id) { - super.remove(id); - LiteflowMetaOperator.reloadAllChain(); - } -} diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/CheckDaily.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/CheckDaily.java deleted file mode 100644 index 48d5b4f..0000000 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/CheckDaily.java +++ /dev/null @@ -1,89 +0,0 @@ -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.core.entity.Stock; -import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; -import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; -import com.lanyuanxiaoyao.leopard.server.service.TaskService; -import com.lanyuanxiaoyao.leopard.server.service.TuShareService; -import com.yomahub.liteflow.annotation.LiteflowComponent; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@LiteflowComponent("check_daily") -public class CheckDaily extends TaskNodeComponent { - private final StockRepository stockRepository; - private final DailyRepository dailyRepository; - - private final TuShareService tuShareService; - - public CheckDaily(TaskService taskService, StockRepository stockRepository, DailyRepository dailyRepository, TuShareService tuShareService) { - super(taskService); - this.stockRepository = stockRepository; - this.dailyRepository = dailyRepository; - this.tuShareService = tuShareService; - } - - @Override - public void process() { - var reports = new ArrayList(); - var stocks = stockRepository.findAll(); - var exchanges = stocks.stream().map(Stock::getMarket).distinct().toList(); - for (Stock.Market exchange : exchanges) { - var nowDate = LocalDate.now(); - var allTradeDates = tuShareService.tradeDateList(exchange.name()) - .data() - .items() - .stream() - .map(item -> LocalDate.parse(item.getFirst(), TuShareService.TRADE_FORMAT)) - .filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate)) - .toList(); - var total = stocks.size(); - var progress = 0; - for (Stock stock : stocks) { - log.info("正在处理:{} {}", stock.getCode(), stock.getName()); - if (exchange.equals(stock.getMarket())) { - var existsTradeDates = dailyRepository.findDistinctTradeDateByStockId(stock.getId()); - var missedTradeDates = allTradeDates.stream() - .filter(date -> date.isAfter(stock.getListedDate()) || date.isEqual(stock.getListedDate())) - .filter(date -> !existsTradeDates.contains(date)) - .filter(date -> { - ThreadUtil.safeSleep(100); - var response = tuShareService.dailyList(date, stock.getCode()); - return !response.data().items().isEmpty(); - }) - .toList(); - if (ObjectUtil.isNotEmpty(missedTradeDates)) { - reports.add(new MissedTradeReport( - stock.getCode(), - stock.getName(), - missedTradeDates - )); - } - } - setStep(++progress * 100 / total); - } - } - if (ObjectUtil.isNotEmpty(reports)) { - var context = getContextBean(TaskMonitorNodes.TaskMonitorContext.class); - context.setTaskResult( - reports.stream() - .map(report -> StrUtil.format("{}({})缺少如下交易日数据:{}", report.name(), report.code(), report.missedTradeDates().stream().map(LocalDate::toString).collect(Collectors.joining(", ")))) - .collect(Collectors.joining("\n")) - ); - } - } - - public record MissedTradeReport( - String code, - String name, - List missedTradeDates - ) { - } -} diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/PyramidStockSelector.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/PyramidStockSelector.java deleted file mode 100644 index 1a578a2..0000000 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/PyramidStockSelector.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.lanyuanxiaoyao.leopard.server.service.task; - -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; -import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator; -import com.lanyuanxiaoyao.leopard.core.entity.Stock; -import com.lanyuanxiaoyao.leopard.core.entity.StockCollection; -import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository; -import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; -import com.yomahub.liteflow.annotation.LiteflowComponent; -import com.yomahub.liteflow.core.NodeComponent; -import java.time.LocalDate; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; - -/** - * 金字塔选股 - * - * @author lanyuanxiaoyao - * @version 20250917 - */ -@Slf4j -@LiteflowComponent("pyramid_stock_selector") -public class PyramidStockSelector extends NodeComponent { - private final StockRepository stockRepository; - private final StockCollectionRepository stockCollectionRepository; - - public PyramidStockSelector(StockRepository stockRepository, StockCollectionRepository stockCollectionRepository) { - this.stockRepository = stockRepository; - this.stockCollectionRepository = stockCollectionRepository; - } - - @Override - public void process() { - // 选择至少有最近5年财报的股票 - var stocks = stockRepository.findAllByIndicatorsSizeGreaterThanEqual(5); - var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock)); - var scores = stocks.stream().map(Stock::getCode).collect(Collectors.toMap(code -> code, code -> 0)); - for (Stock stock : stocks) { - var recentIndicators = stock.getIndicators() - .stream() - .sorted((a, b) -> b.getYear() - a.getYear()) - .limit(5) - .toList(); - var latestIndicator = recentIndicators.getFirst(); - - var roeScore = 0; - if (recentIndicators.stream().noneMatch(indicator -> indicator.getReturnOnEquity() == null || indicator.getReturnOnEquity() < 0)) { - var averageRoe = recentIndicators.stream() - .map(FinanceIndicator::getReturnOnEquity) - .map(item -> ObjectUtil.defaultIfNull(item, 0.0)) - .mapToDouble(Double::doubleValue) - .average() - .orElse(0.0); - if (averageRoe >= 35) { - roeScore = 550; - } else if (averageRoe >= 30) { - roeScore = 500; - } else if (averageRoe >= 25) { - roeScore = 450; - } else if (averageRoe >= 20) { - roeScore = 400; - } else if (averageRoe >= 15) { - roeScore = 350; - } else if (averageRoe >= 10) { - roeScore = 300; - } - } - scores.put(stock.getCode(), scores.get(stock.getCode()) + roeScore); - - var roaScore = 0; - if (recentIndicators.stream().noneMatch(indicator -> indicator.getReturnOnAssets() == null)) { - var averageRoa = recentIndicators.stream() - .map(FinanceIndicator::getReturnOnAssets) - .mapToDouble(Double::doubleValue) - .average() - .orElse(0.0); - if (averageRoa >= 15) { - roaScore = 100; - } else if (averageRoa >= 11) { - roaScore = 80; - } else if (averageRoa >= 7) { - roaScore = 50; - } - } - scores.put(stock.getCode(), scores.get(stock.getCode()) + roaScore); - - var netProfitScore = 0; - if (recentIndicators.stream().noneMatch(indicator -> indicator.getNetProfit() == null)) { - var averageNetProfit = recentIndicators.stream() - .map(FinanceIndicator::getNetProfit) - .mapToDouble(Double::doubleValue) - .average() - .orElse(0.0); - if (averageNetProfit >= 10000.0 * 10000000) { - netProfitScore = 150; - } else if (averageNetProfit >= 1000.0 * 10000000) { - netProfitScore = 100; - } - } - scores.put(stock.getCode(), scores.get(stock.getCode()) + netProfitScore); - - var cashScore = 0; - if ( - ArrayUtil.isAllNotNull(latestIndicator.getTotalAssetsTurnover(), latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio()) - && ( - latestIndicator.getTotalAssetsTurnover() > 0.8 && latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio() >= 0.1 - || latestIndicator.getTotalAssetsTurnover() <= 0.8 && latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio() >= 0.2 - ) - ) { - cashScore = 50; - } - scores.put(stock.getCode(), scores.get(stock.getCode()) + cashScore); - - if (ObjectUtil.isNotNull(latestIndicator.getDaysAccountsReceivableTurnover()) && latestIndicator.getDaysAccountsReceivableTurnover() <= 30) { - scores.put(stock.getCode(), scores.get(stock.getCode()) + 20); - } - if (ObjectUtil.isNotNull(latestIndicator.getDaysInventoryTurnover()) && latestIndicator.getDaysInventoryTurnover() <= 30) { - scores.put(stock.getCode(), scores.get(stock.getCode()) + 20); - } - if (ArrayUtil.isAllNotNull(latestIndicator.getDaysAccountsReceivableTurnover(), latestIndicator.getDaysInventoryTurnover())) { - if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 40) { - scores.put(stock.getCode(), scores.get(stock.getCode()) + 20); - } else if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 60) { - scores.put(stock.getCode(), scores.get(stock.getCode()) + 10); - } - } - - if (recentIndicators.stream().noneMatch(indicator -> indicator.getOperatingGrossProfitMargin() == null)) { - var stat = new DescriptiveStatistics(); - recentIndicators.stream() - .map(FinanceIndicator::getOperatingGrossProfitMargin) - .mapToDouble(Double::doubleValue) - .forEach(stat::addValue); - if (stat.getStandardDeviation() <= 0.3) { - scores.put(stock.getCode(), scores.get(stock.getCode()) + 50); - } - } - - var operatingSafeMarginScore = 0; - if (ObjectUtil.isNotNull(latestIndicator.getOperatingSafetyMarginRatio())) { - if (latestIndicator.getOperatingSafetyMarginRatio() >= 70) { - operatingSafeMarginScore = 50; - } else if (latestIndicator.getOperatingSafetyMarginRatio() >= 50) { - operatingSafeMarginScore = 30; - } else if (latestIndicator.getOperatingSafetyMarginRatio() >= 30) { - operatingSafeMarginScore = 10; - } - } - scores.put(stock.getCode(), scores.get(stock.getCode()) + operatingSafeMarginScore); - - var netProfitAscendingScore = 0; - if (recentIndicators.stream().noneMatch(indicator -> indicator.getNetProfit() == null)) { - if (recentIndicators.get(0).getNetProfit() > recentIndicators.get(1).getNetProfit()) { - netProfitAscendingScore += 30; - } else { - netProfitAscendingScore -= 30; - } - if (recentIndicators.get(1).getNetProfit() > recentIndicators.get(2).getNetProfit()) { - netProfitAscendingScore += 25; - } else { - netProfitAscendingScore -= 25; - } - - if (recentIndicators.get(2).getNetProfit() > recentIndicators.get(3).getNetProfit()) { - netProfitAscendingScore += 20; - } else { - netProfitAscendingScore -= 20; - } - - if (recentIndicators.get(3).getNetProfit() > recentIndicators.get(4).getNetProfit()) { - netProfitAscendingScore += 15; - } else { - netProfitAscendingScore -= 15; - } - } - scores.put(stock.getCode(), scores.get(stock.getCode()) + netProfitAscendingScore); - - var cashAscendingScore = 0; - if (recentIndicators.stream().noneMatch(indicator -> indicator.getCashAndCashEquivalents() == null)) { - if (recentIndicators.get(0).getCashAndCashEquivalents() > recentIndicators.get(1).getCashAndCashEquivalents()) { - cashAscendingScore += 30; - } else { - cashAscendingScore -= 30; - } - - if (recentIndicators.get(1).getCashAndCashEquivalents() > recentIndicators.get(2).getCashAndCashEquivalents()) { - cashAscendingScore += 25; - } else { - cashAscendingScore -= 25; - } - - if (recentIndicators.get(2).getCashAndCashEquivalents() > recentIndicators.get(3).getCashAndCashEquivalents()) { - cashAscendingScore += 20; - } else { - cashAscendingScore -= 20; - } - - if (recentIndicators.get(3).getCashAndCashEquivalents() > recentIndicators.get(4).getCashAndCashEquivalents()) { - cashAscendingScore += 15; - } else { - cashAscendingScore -= 15; - } - } - scores.put(stock.getCode(), scores.get(stock.getCode()) + cashAscendingScore); - } - var first50 = scores.entrySet() - .stream() - .sorted((e1, e2) -> e2.getValue() - e1.getValue()) - .limit(50) - .map(entry -> stocksMap.get(entry.getKey())) - .collect(Collectors.toSet()); - var collection = new StockCollection(); - collection.setName(StrUtil.format("金字塔选股 ({})", LocalDate.now())); - collection.setDescription(""); - collection.setStocks(first50); - stockCollectionRepository.save(collection); - } -} 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 deleted file mode 100644 index 434e03d..0000000 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/TaskMonitorNodes.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.lanyuanxiaoyao.leopard.server.service.task; - -import cn.hutool.core.exceptions.ExceptionUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; -import com.lanyuanxiaoyao.leopard.core.entity.Task; -import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate; -import com.lanyuanxiaoyao.leopard.server.service.TaskService; -import com.yomahub.liteflow.annotation.LiteflowComponent; -import com.yomahub.liteflow.annotation.LiteflowFact; -import com.yomahub.liteflow.annotation.LiteflowMethod; -import com.yomahub.liteflow.core.NodeComponent; -import com.yomahub.liteflow.enums.LiteFlowMethodEnum; -import com.yomahub.liteflow.enums.NodeTypeEnum; -import java.time.LocalDateTime; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@LiteflowComponent -public class TaskMonitorNodes { - private final TaskService taskService; - - public TaskMonitorNodes(TaskService taskService) { - this.taskService = taskService; - } - - @LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "task_start", nodeName = "任务开始", nodeType = NodeTypeEnum.COMMON) - public void taskStart(NodeComponent node) { - try { - var context = node.getContextBean(TaskMonitorContext.class); - var task = new Task(); - task.setName(context.getTemplate().getName()); - task.setDescription(context.getTemplate().getDescription()); - task.setStatus(Task.Status.RUNNING); - task.setLaunchedTime(LocalDateTime.now()); - var taskId = taskService.save(task); - context.setTaskId(taskId); - } catch (Exception exception) { - log.warn("Not in task", exception); - } - } - - @LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "task_end", nodeName = "任务结束", nodeType = NodeTypeEnum.COMMON) - public void taskEnd(NodeComponent node, @LiteflowFact("taskId") Long taskId) { - if (ObjectUtil.isNotNull(taskId)) { - var task = taskService.detail(taskId); - task.setStatus(Task.Status.SUCCESS); - task.setStep(100); - task.setFinishedTime(LocalDateTime.now()); - var result = node.getContextValue("taskResult"); - if (StrUtil.isNotBlank(result)) { - task.setResult(result); - } - taskService.save(task); - } - } - - @LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "task_error", nodeName = "任务错误", nodeType = NodeTypeEnum.COMMON) - public void taskError(NodeComponent node, @LiteflowFact("taskId") Long taskId) { - if (ObjectUtil.isNotNull(taskId)) { - var task = taskService.detail(taskId); - task.setStatus(Task.Status.FAILURE); - task.setFinishedTime(LocalDateTime.now()); - var exception = node.getSlot().getException(); - if (ObjectUtil.isNotNull(exception)) { - task.setError(ExceptionUtil.stacktraceToString(exception)); - } - taskService.save(task); - } - } - - @Data - public static final class TaskMonitorContext { - private TaskTemplate template; - private Long taskId; - private String taskResult; - - public TaskMonitorContext(TaskTemplate template) { - this.template = template; - } - } -} diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/TaskNodeComponent.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/TaskNodeComponent.java deleted file mode 100644 index 5c0839e..0000000 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/TaskNodeComponent.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.lanyuanxiaoyao.leopard.server.service.task; - -import com.lanyuanxiaoyao.leopard.server.service.TaskService; -import com.yomahub.liteflow.core.NodeComponent; -import com.yomahub.liteflow.exception.NoSuchContextBeanException; - -public abstract class TaskNodeComponent extends NodeComponent { - private final TaskService taskService; - - protected TaskNodeComponent(TaskService taskService) { - this.taskService = taskService; - } - - protected void setStep(int step) { - if (step < 0 || step > 100) { - throw new IllegalArgumentException("step must be between 0 and 100"); - } - try { - var context = getContextBean(TaskMonitorNodes.TaskMonitorContext.class); - taskService.updateStepById(step, context.getTaskId()); - } catch (NoSuchContextBeanException ignored) { - } - } -} diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/TaskRunner.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/TaskRunner.java new file mode 100644 index 0000000..62a75d1 --- /dev/null +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/TaskRunner.java @@ -0,0 +1,63 @@ +package com.lanyuanxiaoyao.leopard.server.service.task; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.lanyuanxiaoyao.leopard.core.entity.Task; +import com.lanyuanxiaoyao.leopard.core.repository.TaskRepository; +import com.lanyuanxiaoyao.leopard.server.service.TaskService; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; + +/** + * 任务运行 + * + * @author lanyuanxiaoyao + * @version 20250924 + */ +@Slf4j +public abstract class TaskRunner { + private final ApplicationContext context; + + protected TaskRunner(ApplicationContext context) { + this.context = context; + } + + public abstract String process(Map params, StepUpdater updater) throws Exception; + + public void run(TaskService.TaskTemplate template, Map params) { + var taskRepository = context.getBean(TaskRepository.class); + var task = new Task(); + task.setName(template.name()); + task.setDescription(template.description()); + task.setStatus(Task.Status.RUNNING); + task.setLaunchedTime(LocalDateTime.now()); + taskRepository.saveAndFlush(task); + + try { + var result = process(params, step -> taskRepository.updateStepById(task.getId(), step)); + + task.setStatus(Task.Status.SUCCESS); + task.setStep(1.0); + task.setFinishedTime(LocalDateTime.now()); + if (StrUtil.isNotBlank(result)) { + task.setResult(result); + } + taskRepository.saveAndFlush(task); + } catch (Throwable throwable) { + log.error("任务执行失败", throwable); + task.setStatus(Task.Status.FAILURE); + task.setFinishedTime(LocalDateTime.now()); + if (ObjectUtil.isNotNull(throwable)) { + task.setError(ExceptionUtil.stacktraceToString(throwable)); + } + taskRepository.saveAndFlush(task); + } + } + + public interface StepUpdater { + void update(double step); + } +} diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateDaily.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateDailyTask.java similarity index 52% rename from leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateDaily.java rename to leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateDailyTask.java index 7698944..35965b6 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateDaily.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateDailyTask.java @@ -9,35 +9,41 @@ import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper; 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.Map; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; -import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +/** + * 更新日线数据 + * + * @author lanyuanxiaoyao + * @version 20250924 + */ @Slf4j -@LiteflowComponent("update_daily") -public class UpdateDaily extends NodeComponent { +@Component +public class UpdateDailyTask extends TaskRunner { private final StockRepository stockRepository; private final DailyRepository dailyRepository; private final TuShareService tuShareService; - private final TransactionTemplate transactionTemplate; - - public UpdateDaily(StockRepository stockRepository, DailyRepository dailyRepository, TuShareService tuShareService, TransactionTemplate transactionTemplate) { + protected UpdateDailyTask(ApplicationContext context, StockRepository stockRepository, DailyRepository dailyRepository, TuShareService tuShareService) { + super(context); this.stockRepository = stockRepository; this.dailyRepository = dailyRepository; this.tuShareService = tuShareService; - this.transactionTemplate = transactionTemplate; } + @Transactional(rollbackFor = Throwable.class) @Override - public void process() { + public String process(Map params, StepUpdater updater) throws Exception { var tradeDates = new HashSet(); for (String exchange : List.of("SSE", "SZSE", "BSE")) { var response = tuShareService.tradeDateList(exchange); @@ -53,7 +59,6 @@ public class UpdateDaily extends NodeComponent { tradeDates.parallelStream() .filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate)) .filter(date -> !existsTradeDates.contains(date)) - // .filter(date -> date.isAfter(LocalDate.of(2024, 12, 31))) .forEach(tradeDate -> { var factorResponse = tuShareService.factorList(tradeDate); var factorMap = new HashMap(); @@ -62,36 +67,29 @@ public class UpdateDaily extends NodeComponent { } 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(tradeDate); - daily.setOpen(NumberHelper.parseDouble(item.get(2))); - daily.setHigh(NumberUtil.parseDouble(item.get(3))); - daily.setLow(NumberUtil.parseDouble(item.get(4))); - daily.setClose(NumberUtil.parseDouble(item.get(5))); - daily.setPreviousClose(NumberUtil.parseDouble(item.get(6))); - daily.setPriceChangeAmount(NumberUtil.parseDouble(item.get(7))); - daily.setPriceFluctuationRange(NumberUtil.parseDouble(item.get(8))); - daily.setVolume(NumberUtil.parseDouble(item.get(9))); - daily.setTurnover(NumberUtil.parseDouble(item.get(10))); - daily.setFactor(factor); - daily.setStock(stock); - dailyRepository.save(daily); - } - } - return true; - } catch (Exception exception) { - log.error("Error", exception); - status.setRollbackOnly(); - return false; + 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(tradeDate); + daily.setOpen(NumberHelper.parseDouble(item.get(2))); + daily.setHigh(NumberUtil.parseDouble(item.get(3))); + daily.setLow(NumberUtil.parseDouble(item.get(4))); + daily.setClose(NumberUtil.parseDouble(item.get(5))); + daily.setPreviousClose(NumberUtil.parseDouble(item.get(6))); + daily.setPriceChangeAmount(NumberUtil.parseDouble(item.get(7))); + daily.setPriceFluctuationRange(NumberUtil.parseDouble(item.get(8))); + daily.setVolume(NumberUtil.parseDouble(item.get(9))); + daily.setTurnover(NumberUtil.parseDouble(item.get(10))); + daily.setFactor(factor); + daily.setStock(stock); + dailyRepository.save(daily); } - }); + } }); + + return null; } } diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateFinanceIndicator.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateFinanceIndicatorTask.java similarity index 93% rename from leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateFinanceIndicator.java rename to leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateFinanceIndicatorTask.java index 0f92ef7..9670763 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateFinanceIndicator.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateFinanceIndicatorTask.java @@ -2,38 +2,45 @@ package com.lanyuanxiaoyao.leopard.server.service.task; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; +import com.fasterxml.jackson.core.JsonProcessingException; import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator; import com.lanyuanxiaoyao.leopard.core.entity.QFinanceIndicator; import com.lanyuanxiaoyao.leopard.core.entity.Stock; import com.lanyuanxiaoyao.leopard.core.repository.FinanceIndicatorRepository; import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper; -import com.lanyuanxiaoyao.leopard.server.service.TaskService; import com.lanyuanxiaoyao.leopard.server.service.TuShareService; -import com.yomahub.liteflow.annotation.LiteflowComponent; import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +/** + * 更新财务指标数据 + * + * @author lanyuanxiaoyao + * @version 20250924 + */ @Slf4j -@LiteflowComponent("update_finance") -public class UpdateFinanceIndicator extends TaskNodeComponent { - private final FinanceIndicatorRepository financeIndicatorRepository; +@Component +public class UpdateFinanceIndicatorTask extends TaskRunner { private final StockRepository stockRepository; + private final FinanceIndicatorRepository financeIndicatorRepository; private final TuShareService tuShareService; - protected UpdateFinanceIndicator(TaskService taskService, FinanceIndicatorRepository financeIndicatorRepository, StockRepository stockRepository, TuShareService tuShareService) { - super(taskService); - this.financeIndicatorRepository = financeIndicatorRepository; + protected UpdateFinanceIndicatorTask(ApplicationContext context, StockRepository stockRepository, FinanceIndicatorRepository financeIndicatorRepository, TuShareService tuShareService) { + super(context); this.stockRepository = stockRepository; + this.financeIndicatorRepository = financeIndicatorRepository; this.tuShareService = tuShareService; } @Override - public void process() throws Exception { + public String process(Map params, StepUpdater updater) throws JsonProcessingException { var stocks = stockRepository.findAll(); var currentYear = LocalDate.now().getYear(); for (int year = 1990; year < currentYear; year++) { @@ -209,7 +216,8 @@ public class UpdateFinanceIndicator extends TaskNodeComponent { financeIndicatorRepository.save(indicator); } - setStep((year - 1990) * 100 / (currentYear - 1990)); + updater.update((year - 1990) * 1.0 / (currentYear - 1990)); } + return null; } } diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStock.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStockTask.java similarity index 73% rename from leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStock.java rename to leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStockTask.java index 0897072..2db3359 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStock.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateStockTask.java @@ -4,26 +4,34 @@ import cn.hutool.core.util.EnumUtil; import com.lanyuanxiaoyao.leopard.core.entity.Stock; import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; 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.time.LocalDate; +import java.util.Map; import java.util.stream.Collectors; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; -@LiteflowComponent("update_stock") -public class UpdateStock extends NodeComponent { +/** + * 更新股票信息 + * + * @author lanyuanxiaoyao + * @version 20250924 + */ +@Component +public class UpdateStockTask extends TaskRunner { private final StockRepository stockRepository; private final TuShareService tuShareService; - public UpdateStock(StockRepository stockRepository, TuShareService tuShareService) { + public UpdateStockTask(ApplicationContext context, StockRepository stockRepository, TuShareService tuShareService) { + super(context); this.stockRepository = stockRepository; this.tuShareService = tuShareService; } - @Transactional(rollbackOn = Throwable.class) + @Transactional(rollbackFor = Throwable.class) @Override - public void process() { + public String process(Map params, StepUpdater updater) { var existsStockMap = stockRepository.findAll().stream().collect(Collectors.toMap(Stock::getCode, stock -> stock)); var stocks = tuShareService.stockList() .data() @@ -51,5 +59,6 @@ public class UpdateStock extends NodeComponent { var deleteCodes = existsCodes.stream().filter(code -> !currentCodes.contains(code)).toList(); stockRepository.deleteAllByCodeIn(deleteCodes); stockRepository.saveAll(stocks); + return null; } } diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateYearly.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateYearly.java deleted file mode 100644 index d394cca..0000000 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateYearly.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.lanyuanxiaoyao.leopard.server.service.task; - -import com.lanyuanxiaoyao.leopard.core.entity.Daily; -import com.lanyuanxiaoyao.leopard.core.entity.QDaily; -import com.lanyuanxiaoyao.leopard.core.entity.QYearly; -import com.lanyuanxiaoyao.leopard.core.entity.Yearly; -import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; -import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; -import com.lanyuanxiaoyao.leopard.core.repository.YearlyRepository; -import com.lanyuanxiaoyao.leopard.server.service.TaskService; -import com.yomahub.liteflow.annotation.LiteflowComponent; -import lombok.extern.slf4j.Slf4j; -import org.springframework.transaction.support.TransactionTemplate; - -@Slf4j -@LiteflowComponent("update_yearly") -public class UpdateYearly extends TaskNodeComponent { - private final StockRepository stockRepository; - private final DailyRepository dailyRepository; - private final YearlyRepository yearlyRepository; - - private final TransactionTemplate transactionTemplate; - - protected UpdateYearly(TaskService taskService, StockRepository stockRepository, DailyRepository dailyRepository, YearlyRepository yearlyRepository, TransactionTemplate transactionTemplate) { - super(taskService); - this.stockRepository = stockRepository; - this.dailyRepository = dailyRepository; - this.yearlyRepository = yearlyRepository; - this.transactionTemplate = transactionTemplate; - } - - @Override - public void process() { - var startYear = dailyRepository.findMinTradeDate().getYear(); - var endYear = dailyRepository.findMaxTradeDate().getYear(); - var stocks = stockRepository.findAll(); - for (int year = startYear, index = 0; year <= endYear; year++, index++) { - var currentYear = year; - transactionTemplate.execute(status -> { - try { - for (var stock : stocks) { - log.info("Processing {} {}", stock.getCode(), currentYear); - if (stock.getListedDate().getYear() > currentYear) { - continue; - } - var dailies = dailyRepository.findAll( - QDaily.daily.tradeDate.year().eq(currentYear) - .and(QDaily.daily.stock.eq(stock)) - ); - var yearly = yearlyRepository.findOne( - QYearly.yearly.stock.eq(stock) - .and(QYearly.yearly.year.eq(currentYear)) - ).orElseGet(Yearly::new); - yearly.setStock(stock); - yearly.setYear(currentYear); - yearly.setClose(dailies.getLast().getHfqClose()); - yearly.setOpen(dailies.getFirst().getHfqOpen()); - yearly.setHigh(dailies.stream().map(Daily::getHfqHigh).max(Double::compareTo).orElse(0.0)); - yearly.setLow(dailies.stream().map(Daily::getHfqLow).min(Double::compareTo).orElse(0.0)); - yearly.setVolume(dailies.stream().mapToDouble(Daily::getVolume).sum()); - yearly.setTurnover(dailies.stream().mapToDouble(Daily::getTurnover).sum()); - yearly.setPriceChangeAmount(yearly.getClose() - yearly.getOpen()); - yearly.setPriceFluctuationRange((yearly.getClose() - yearly.getOpen()) / yearly.getOpen()); - yearlyRepository.save(yearly); - } - return true; - } catch (Exception exception) { - log.error("Error for %s".formatted(currentYear), exception); - status.setRollbackOnly(); - return false; - } - }); - setStep((currentYear - startYear) * 100 / (endYear - startYear + 1)); - } - } -} diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateYearlyTask.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateYearlyTask.java new file mode 100644 index 0000000..04155c8 --- /dev/null +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/service/task/UpdateYearlyTask.java @@ -0,0 +1,72 @@ +package com.lanyuanxiaoyao.leopard.server.service.task; + +import com.lanyuanxiaoyao.leopard.core.entity.Daily; +import com.lanyuanxiaoyao.leopard.core.entity.QDaily; +import com.lanyuanxiaoyao.leopard.core.entity.QYearly; +import com.lanyuanxiaoyao.leopard.core.entity.Yearly; +import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; +import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; +import com.lanyuanxiaoyao.leopard.core.repository.YearlyRepository; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * 更新年线指标 + * + * @author lanyuanxiaoyao + * @version 20250924 + */ +@Slf4j +@Component +public class UpdateYearlyTask extends TaskRunner { + private final StockRepository stockRepository; + private final DailyRepository dailyRepository; + private final YearlyRepository yearlyRepository; + + protected UpdateYearlyTask(ApplicationContext context, StockRepository stockRepository, DailyRepository dailyRepository, YearlyRepository yearlyRepository) { + super(context); + this.stockRepository = stockRepository; + this.dailyRepository = dailyRepository; + this.yearlyRepository = yearlyRepository; + } + + @Transactional(rollbackFor = Throwable.class) + @Override + public String process(Map params, StepUpdater updater) { + var startYear = dailyRepository.findMinTradeDate().getYear(); + var endYear = dailyRepository.findMaxTradeDate().getYear(); + var stocks = stockRepository.findAll(); + for (int year = startYear, index = 0; year <= endYear; year++, index++) { + for (var stock : stocks) { + log.info("Processing {} {}", stock.getCode(), year); + if (stock.getListedDate().getYear() > year) { + continue; + } + var dailies = dailyRepository.findAll( + QDaily.daily.tradeDate.year().eq(year) + .and(QDaily.daily.stock.eq(stock)) + ); + var yearly = yearlyRepository.findOne( + QYearly.yearly.stock.eq(stock) + .and(QYearly.yearly.year.eq(year)) + ).orElseGet(Yearly::new); + yearly.setStock(stock); + yearly.setYear(year); + yearly.setClose(dailies.getLast().getHfqClose()); + yearly.setOpen(dailies.getFirst().getHfqOpen()); + yearly.setHigh(dailies.stream().map(Daily::getHfqHigh).max(Double::compareTo).orElse(0.0)); + yearly.setLow(dailies.stream().map(Daily::getHfqLow).min(Double::compareTo).orElse(0.0)); + yearly.setVolume(dailies.stream().mapToDouble(Daily::getVolume).sum()); + yearly.setTurnover(dailies.stream().mapToDouble(Daily::getTurnover).sum()); + yearly.setPriceChangeAmount(yearly.getClose() - yearly.getOpen()); + yearly.setPriceFluctuationRange((yearly.getClose() - yearly.getOpen()) / yearly.getOpen()); + yearlyRepository.save(yearly); + } + updater.update((year - startYear) * 1.0 / (endYear - startYear + 1)); + } + return null; + } +} diff --git a/leopard-server/src/main/resources/application.yml b/leopard-server/src/main/resources/application.yml index 3a7fb88..7c58193 100644 --- a/leopard-server/src/main/resources/application.yml +++ b/leopard-server/src/main/resources/application.yml @@ -31,13 +31,3 @@ spring: driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate fenix: print-banner: false -liteflow: - print-banner: false - check-node-exists: false - rule-source-ext-data-map: - applicationName: ${spring.application.name} - sqlLogEnabled: true - chainTableName: leopard_task_template - chainApplicationNameField: application - chainNameField: chain - elDataField: expression_el diff --git a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StockAssessmentNode.java b/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StockAssessmentNode.java deleted file mode 100644 index 5e6eded..0000000 --- a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StockAssessmentNode.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.lanyuanxiaoyao.leopard.strategy; - -import cn.hutool.core.util.ObjectUtil; -import com.lanyuanxiaoyao.leopard.core.entity.Daily; -import com.lanyuanxiaoyao.leopard.core.entity.QDaily; -import com.lanyuanxiaoyao.leopard.core.entity.QStock; -import com.lanyuanxiaoyao.leopard.core.entity.Stock; -import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; -import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; -import com.yomahub.liteflow.annotation.LiteflowComponent; -import com.yomahub.liteflow.core.NodeComponent; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; -import org.springframework.transaction.annotation.Transactional; - -/** - * 股票评估 - * - * @author lanyuanxiaoyao - * @version 20250919 - */ -@Slf4j -@LiteflowComponent("assess_stock") -public class StockAssessmentNode extends NodeComponent { - private static final Map INDUSTRY_TOP = new HashMap<>(); - private final StockRepository stockRepository; - private final DailyRepository dailyRepository; - - public StockAssessmentNode(StockRepository stockRepository, DailyRepository dailyRepository) { - this.stockRepository = stockRepository; - this.dailyRepository = dailyRepository; - } - - @Transactional(readOnly = true) - @Override - public void process() { - var assessment = getContextBean(StockAssessment.class); - if (ObjectUtil.isNotNull(assessment) && ObjectUtil.isNotEmpty(assessment.stocks())) { - var dailyMap = dailyRepository.findAll( - QDaily.daily.tradeDate.year().eq(assessment.year()) - .and(QDaily.daily.stock.in(assessment.stocks())) - ) - .stream() - .collect(Collectors.groupingBy(Daily::getStock)); - for (Stock stock : assessment.stocks()) { - if (!dailyMap.containsKey(stock) || ObjectUtil.isEmpty(dailyMap.get(stock))) { - log.warn("Cannot find daily data in {} for {}", assessment.year(), stock.getCode()); - continue; - } - var dailies = dailyMap.get(stock) - .stream() - .sorted(Comparator.comparing(Daily::getTradeDate)) - .toList(); - var change = getChange(dailies); - var std = getStd(dailies); - var industryTop = getTopOfIndustry(stock.getIndustry(), assessment.year()); - assessment.results().add(new StockAssessment.Result(stock, change, std, industryTop)); - } - } - } - - private double getChange(List dailies) { - return (dailies.getLast().getHfqClose() - dailies.getFirst().getHfqClose()) / dailies.getFirst().getHfqClose(); - } - - private double getStd(List dailies) { - var statistics = new DescriptiveStatistics(); - dailies.forEach(daily -> statistics.addValue(daily.getHfqClose())); - return statistics.getStandardDeviation(); - } - - private double getTopOfIndustry(String industry, int year) { - log.info("Calculate industry: {} for {}", industry, year); - if (INDUSTRY_TOP.containsKey(industry)) { - return INDUSTRY_TOP.get(industry); - } - var top = stockRepository.findAll(QStock.stock.industry.eq(industry)) - .parallelStream() - .filter(stock -> stock.getListedDate().getYear() <= year) - .map(stock -> { - List dailies = dailyRepository.findAll( - QDaily.daily.tradeDate.year().eq(year) - .and(QDaily.daily.stock.eq(stock)) - ); - if (ObjectUtil.isEmpty(dailies)) { - log.warn("Cannot find daily data in {} for {} {}", year, stock.getCode(), stock.getName()); - } - return dailies; - }) - .filter(ObjectUtil::isNotEmpty) - .map(this::getChange) - .mapToDouble(change -> change) - .max() - .orElse(0.0); - INDUSTRY_TOP.put(industry, top); - return top; - } - - public record StockAssessment(int year, Set stocks, Set results) { - public StockAssessment(int year) { - this(year, new HashSet<>(), new HashSet<>()); - } - - public record Result(Stock stock, double change, double std, double industryTop) { - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - - Result result = (Result) o; - return stock.equals(result.stock); - } - - @Override - public int hashCode() { - return stock.hashCode(); - } - } - } -} diff --git a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java b/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java index 5dd2116..0326652 100644 --- a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java +++ b/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java @@ -1,25 +1,25 @@ package com.lanyuanxiaoyao.leopard.strategy; -import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.databind.ObjectMapper; -import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; -import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; -import com.yomahub.liteflow.core.FlowExecutor; +import com.lanyuanxiaoyao.leopard.core.service.AssessmentService; +import com.lanyuanxiaoyao.leopard.core.service.selector.PyramidStockSelector; +import com.lanyuanxiaoyao.leopard.core.service.selector.StockSelector; import jakarta.annotation.Resource; -import jakarta.transaction.Transactional; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Map; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.transaction.annotation.Transactional; @Slf4j @SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard") @@ -27,11 +27,9 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; public class StrategyApplication { private static final ObjectMapper mapper = new ObjectMapper(); @Resource - private StockRepository stockRepository; + private PyramidStockSelector pyramidStockSelector; @Resource - private DailyRepository dailyRepository; - @Resource - private FlowExecutor flowExecutor; + private AssessmentService assessmentService; public static void main(String[] args) { SpringApplication.run(StrategyApplication.class, args); @@ -268,9 +266,9 @@ public class StrategyApplication { ); } - @Transactional(rollbackOn = Throwable.class) + @Transactional(readOnly = true) @EventListener(ApplicationReadyEvent.class) - public void test() throws IOException { + public void test() { /*var dailies = dailyRepository.findAll( QDaily.daily.tradeDate.year().eq(2025), Sort.by(Daily_.TRADE_DATE) @@ -310,16 +308,20 @@ public class StrategyApplication { var lines = new ArrayList(); for (int year = 2024; year < 2025; year++) { - var assessment = new StockAssessmentNode.StockAssessment(year); - var response = flowExecutor.execute2RespWithEL("THEN(pyramid_stock_selector,assess_stock)", null, IdUtil.nanoId(), assessment); - assessment = response.getContextBean(StockAssessmentNode.StockAssessment.class); - int up = assessment.results() - .stream() + var candidates = pyramidStockSelector.select(new PyramidStockSelector.Request(year)); + for (StockSelector.Candidate candidate : candidates) { + log.info("{} {}", candidate.stock().getName(), candidate.score()); + } + var stocks = candidates.stream() + .map(StockSelector.Candidate::stock) + .collect(Collectors.toSet()); + var results = assessmentService.assess(stocks, year); + int up = results.stream() .filter(result -> result.change() > 0) .mapToInt(result -> 1) .sum(); - lines.add(NumberUtil.roundStr(up * 100.0 / assessment.results().size(), 2)); - assessment.results().forEach(result -> log.info("{} {} {} {} {}", result.stock().getCode(), result.stock().getName(), result.change(), result.std(), result.industryTop())); + lines.add(NumberUtil.roundStr(up * 100.0 / results.size(), 2)); + results.forEach(result -> log.info("{} {} {} {} {}", result.stock().getCode(), result.stock().getName(), result.change(), result.std(), result.industryTop())); } for (int index = 0, year = 2010; index < lines.size(); index++, year++) { log.info("胜率: {} {}", year, lines.get(index)); diff --git a/leopard-web/src/index.tsx b/leopard-web/src/index.tsx index 90c7a0a..1e85008 100644 --- a/leopard-web/src/index.tsx +++ b/leopard-web/src/index.tsx @@ -9,7 +9,6 @@ import StockList from './pages/stock/StockList.tsx' import StockDetail from './pages/stock/StockDetail.tsx' import TaskList from './pages/task/TaskList.tsx' import TaskTemplateList from './pages/task/TaskTemplateList.tsx' -import TaskTemplateSave from './pages/task/TaskTemplateSave.tsx' import TaskScheduleList from './pages/task/TaskScheduleList.tsx' import TaskScheduleSave from './pages/task/TaskScheduleSave.tsx' import StockCollectionList from './pages/stock/StockCollectionList.tsx' @@ -73,10 +72,6 @@ const routes: RouteObject[] = [ path: 'list', Component: TaskTemplateList, }, - { - path: 'save/:id', - Component: TaskTemplateSave, - }, ], }, { diff --git a/leopard-web/src/pages/task/TaskList.tsx b/leopard-web/src/pages/task/TaskList.tsx index 8e603d8..3df3cb4 100644 --- a/leopard-web/src/pages/task/TaskList.tsx +++ b/leopard-web/src/pages/task/TaskList.tsx @@ -1,5 +1,14 @@ import React from 'react' -import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, remoteMappings, time} from '../../util/amis.tsx' +import { + amisRender, + commonInfo, + crudCommonOptions, + horizontalFormOptions, + paginationTemplate, + remoteMappings, + remoteOptions, + time, +} from '../../util/amis.tsx' import {useNavigate} from 'react-router' function TaskList() { @@ -25,7 +34,41 @@ function TaskList() { }, interval: 30000, ...crudCommonOptions(), - ...paginationTemplate(15), + ...paginationTemplate( + 15, + undefined, + [ + { + type: 'action', + label: '', + icon: 'fa fa-plus', + actionType: 'dialog', + dialog: { + title: '创建任务', + body: { + type: 'form', + api: { + method: 'post', + url: `${commonInfo.baseUrl}/task/execute`, + data: { + templateId: '${templateId|default:undefined}', + }, + }, + ...horizontalFormOptions(), + body: [ + { + name: 'templateId', + label: '名称', + required: true, + selectFirst: true, + ...remoteOptions('select', 'task_template_id'), + }, + ], + }, + }, + }, + ], + ), columns: [ { name: 'name', @@ -44,10 +87,10 @@ function TaskList() { ...remoteMappings('task_status', 'status'), }, { - name: 'step', label: '进度', type: 'progress', width: 200, + value: '${ROUND(step * 100, 0)}', }, { label: '耗时', diff --git a/leopard-web/src/pages/task/TaskTemplateList.tsx b/leopard-web/src/pages/task/TaskTemplateList.tsx index df971f0..c6191ba 100644 --- a/leopard-web/src/pages/task/TaskTemplateList.tsx +++ b/leopard-web/src/pages/task/TaskTemplateList.tsx @@ -1,9 +1,7 @@ import React from 'react' import {amisRender, commonInfo, crudCommonOptions, paginationTemplate} from '../../util/amis.tsx' -import {useNavigate} from 'react-router' function TaskTemplateList() { - const navigate = useNavigate() return (
{amisRender( @@ -13,43 +11,10 @@ function TaskTemplateList() { body: [ { type: 'crud', - api: { - method: 'post', - url: `${commonInfo.baseUrl}/task_template/list`, - data: { - page: { - index: '${page}', - size: '${perPage}', - }, - }, - }, + api: `get:${commonInfo.baseUrl}/task/template/list`, ...crudCommonOptions(), - ...paginationTemplate( - 15, - undefined, - [ - { - type: 'action', - label: '', - icon: 'fa fa-plus', - tooltip: '添加模板', - tooltipPlacement: 'top', - onEvent: { - click: { - actions: [ - { - actionType: 'custom', - // @ts-ignore - script: (context, action, event) => { - navigate('/task/template/save/-1') - }, - }, - ], - }, - }, - }, - ], - ), + ...paginationTemplate(15), + loadOnce: true, columns: [ { name: 'name', @@ -63,7 +28,7 @@ function TaskTemplateList() { { type: 'operation', label: '操作', - width: 150, + width: 100, buttons: [ { type: 'action', @@ -78,35 +43,7 @@ function TaskTemplateList() { }, }, confirmText: '确认执行模板${name}?', - confirmTitle: '删除', - }, - { - type: 'action', - label: '详情', - level: 'link', - onEvent: { - click: { - actions: [ - { - actionType: 'custom', - // @ts-ignore - script: (context, action, event) => { - navigate(`/task/template/save/${context.props.data['id']}`) - }, - }, - ], - }, - }, - }, - { - className: 'text-danger btn-deleted', - type: 'action', - label: '删除', - level: 'link', - actionType: 'ajax', - api: `get:${commonInfo.baseUrl}/task_template/remove/\${id}`, - confirmText: '确认删除模板${name}?', - confirmTitle: '删除', + confirmTitle: '执行', }, ], }, diff --git a/leopard-web/src/pages/task/TaskTemplateSave.tsx b/leopard-web/src/pages/task/TaskTemplateSave.tsx deleted file mode 100644 index ae7ff80..0000000 --- a/leopard-web/src/pages/task/TaskTemplateSave.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react' -import {amisRender, commonInfo} from '../../util/amis.tsx' -import {useNavigate, useParams} from 'react-router' - -function TaskTemplateSave() { - const navigate = useNavigate() - const {id} = useParams() - return ( -
- {amisRender( - { - type: 'page', - title: '任务模板添加', - body: [ - { - debug: commonInfo.debug, - type: 'form', - api: `post:${commonInfo.baseUrl}/task_template/save`, - initApi: `get:${commonInfo.baseUrl}/task_template/detail/${id}`, - initFetchOn: `${id} !== -1`, - wrapWithPanel: false, - mode: 'horizontal', - labelAlign: 'left', - onEvent: { - submitSucc: { - actions: [ - { - actionType: 'custom', - // @ts-ignore - script: (context, action, event) => { - navigate(-1) - }, - }, - ], - }, - }, - body: [ - { - type: 'hidden', - name: 'id', - }, - { - type: 'input-text', - name: 'name', - label: '名称', - required: true, - clearable: true, - }, - { - type: 'textarea', - name: 'description', - label: '描述', - required: true, - clearable: true, - }, - { - type: 'input-text', - name: 'expression', - label: 'EL表达式', - required: true, - clearable: true, - }, - { - type: 'button-toolbar', - buttons: [ - { - type: 'action', - label: '提交', - actionType: 'submit', - level: 'primary', - }, - { - type: 'action', - label: '重置', - actionType: 'reset', - }, - { - type: 'action', - label: '返回', - onEvent: { - click: { - actions: [ - { - actionType: 'custom', - // @ts-ignore - script: (context, action, event) => { - navigate(-1) - }, - }, - ], - }, - }, - }, - ], - }, - ], - }, - ], - }, - )} -
- ) -} - -export default React.memo(TaskTemplateSave) \ No newline at end of file