refactor: 重构任务执行
This commit is contained in:
@@ -25,6 +25,15 @@
|
||||
<artifactId>hutool-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.ralfkonrad.quantlib_for_maven</groupId>
|
||||
<artifactId>quantlib</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ta4j</groupId>
|
||||
<artifactId>ta4j-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-ant</artifactId>
|
||||
|
||||
@@ -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("任务结束时间")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<Task> {
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -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<TaskTemplate> {
|
||||
}
|
||||
@@ -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<Result> assess(Set<Stock> 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<Daily> dailies) {
|
||||
return (dailies.getLast().getHfqClose() - dailies.getFirst().getHfqClose()) / dailies.getFirst().getHfqClose();
|
||||
}
|
||||
|
||||
private double getStd(List<Daily> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IndustryYearlyKey, Double> topChange(int year) {
|
||||
return topChange(year, null, null);
|
||||
}
|
||||
|
||||
public Map<IndustryYearlyKey, Double> topChange(int year, Set<String> includeIndustries) {
|
||||
return topChange(year, includeIndustries, null);
|
||||
}
|
||||
|
||||
public Map<IndustryYearlyKey, Double> topChange(int year, Set<String> includeIndustries, Set<Stock> includeStocks) {
|
||||
return topChange(year, year, includeIndustries, includeStocks);
|
||||
}
|
||||
|
||||
public Map<IndustryYearlyKey, Double> topChange(int startYear, int endYear) {
|
||||
return topChange(startYear, endYear, null, null);
|
||||
}
|
||||
|
||||
public Map<IndustryYearlyKey, Double> topChange(int startYear, int endYear, Set<String> includeIndustries) {
|
||||
return topChange(startYear, endYear, includeIndustries, null);
|
||||
}
|
||||
|
||||
public Map<IndustryYearlyKey, Double> topChange(int startYear, int endYear, Set<String> includeIndustries, Set<Stock> includeStocks) {
|
||||
return stockRepository.findDistinctIndustries()
|
||||
.parallelStream()
|
||||
.filter(industry -> includeIndustries == null || includeIndustries.contains(industry))
|
||||
.flatMap(industry -> {
|
||||
var keys = new ArrayList<IndustryYearlyKey>();
|
||||
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) {
|
||||
}
|
||||
}
|
||||
@@ -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<PyramidStockSelector.Request> {
|
||||
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<Candidate> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<T> {
|
||||
Set<Candidate> select(T request);
|
||||
|
||||
record Candidate(Stock stock, double score, Map<String, Object> extra) {
|
||||
public Candidate(Stock stock, double score) {
|
||||
this(stock, score, Map.of());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,15 +41,6 @@
|
||||
<artifactId>spring-boot-starter-quartz</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-rule-sql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-core</artifactId>
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -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<Task, Void, TaskCont
|
||||
return GlobalResponse.responseSuccess();
|
||||
}
|
||||
|
||||
@GetMapping("template/list")
|
||||
public GlobalResponse<Map<String, Object>> templateList() {
|
||||
var templates = taskService.getTemplates();
|
||||
return GlobalResponse.responseCrudData(templates, templates.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Function<Void, Task> saveItemMapper() {
|
||||
throw new UnsupportedOperationException();
|
||||
@@ -108,7 +115,7 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
|
||||
LocalDateTime finishedTime,
|
||||
Long cost,
|
||||
String costText,
|
||||
Integer step
|
||||
Double step
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -123,11 +130,11 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
|
||||
LocalDateTime finishedTime,
|
||||
Long cost,
|
||||
String costText,
|
||||
Integer step
|
||||
Double step
|
||||
) {
|
||||
}
|
||||
|
||||
public record ExecuteRequest(Long templateId, Map<String, Object> params) {
|
||||
public record ExecuteRequest(String templateId, Map<String, Object> params) {
|
||||
}
|
||||
|
||||
public record TaskCost(Long cost, String costText) {
|
||||
|
||||
@@ -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<TaskTemplate, TaskTemplateController.Item, TaskTemplateController.Item, TaskTemplateController.Item> {
|
||||
@Value("${spring.application.name}")
|
||||
private String application;
|
||||
|
||||
public TaskTemplateController(TaskTemplateService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Function<Item, TaskTemplate> 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<TaskTemplate, Item> listItemMapper() {
|
||||
return this::convert;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Function<TaskTemplate, Item> detailItemMapper() {
|
||||
return this::convert;
|
||||
}
|
||||
|
||||
public record Item(
|
||||
Long id,
|
||||
String name,
|
||||
String description,
|
||||
String expression
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object>) 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,
|
||||
|
||||
@@ -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<Task> {
|
||||
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<TaskTemplate> 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<String, TaskTemplate> 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<Task> {
|
||||
taskRepository.updateAllRunningTaskToFailure();
|
||||
}
|
||||
|
||||
public void execute(Long templateId, Map<String, Object> 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<String, Object> params) {
|
||||
execute(templateId, params, true);
|
||||
}
|
||||
|
||||
public void execute(String templateId, Map<String, Object> 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<? extends TaskRunner> runnerClass
|
||||
) {
|
||||
public TaskTemplate(String name, String description, Class<? extends TaskRunner> runnerClass) {
|
||||
this(IdUtil.fastUUID(), name, description, runnerClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TaskTemplate> {
|
||||
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<TaskTemplate> taskTemplates) {
|
||||
taskTemplates.forEach(template -> validateExpression(template.getExpression()));
|
||||
super.save(taskTemplates);
|
||||
LiteflowMetaOperator.reloadAllChain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Iterable<Long> ids) {
|
||||
super.remove(ids);
|
||||
LiteflowMetaOperator.reloadAllChain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Long id) {
|
||||
super.remove(id);
|
||||
LiteflowMetaOperator.reloadAllChain();
|
||||
}
|
||||
}
|
||||
@@ -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<MissedTradeReport>();
|
||||
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<LocalDate> missedTradeDates
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.<String>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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> params, StepUpdater updater) throws Exception;
|
||||
|
||||
public void run(TaskService.TaskTemplate template, Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> params, StepUpdater updater) throws Exception {
|
||||
var tradeDates = new HashSet<LocalDate>();
|
||||
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<String, Double>();
|
||||
@@ -62,36 +67,29 @@ public class UpdateDaily extends NodeComponent {
|
||||
}
|
||||
|
||||
var response = tuShareService.dailyList(tradeDate);
|
||||
transactionTemplate.execute(status -> {
|
||||
try {
|
||||
for (List<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<String, Double> 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<Daily> dailies) {
|
||||
return (dailies.getLast().getHfqClose() - dailies.getFirst().getHfqClose()) / dailies.getFirst().getHfqClose();
|
||||
}
|
||||
|
||||
private double getStd(List<Daily> 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<Daily> 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<Stock> stocks, Set<StockAssessment.Result> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>();
|
||||
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));
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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: '耗时',
|
||||
|
||||
@@ -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 (
|
||||
<div className="task-template-list">
|
||||
{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: '确认执行模板<span class="text-lg font-bold mx-2">${name}</span>?',
|
||||
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: '确认删除模板<span class="text-lg font-bold mx-2">${name}</span>?',
|
||||
confirmTitle: '删除',
|
||||
confirmTitle: '执行',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
<div className="task-template-save">
|
||||
{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)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TaskTemplateSave)
|
||||
Reference in New Issue
Block a user