refactor: 重构任务执行
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user