1
0

Compare commits

...

6 Commits

15 changed files with 126 additions and 56 deletions

View File

@@ -6,6 +6,7 @@ import com.querydsl.core.types.Predicate;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
@@ -14,10 +15,7 @@ import org.springframework.stereotype.Repository;
@Repository @Repository
public interface DailyRepository extends SimpleRepository<Daily> { public interface DailyRepository extends SimpleRepository<Daily> {
@Query("select distinct daily.tradeDate from Daily daily") @Query("select distinct daily.tradeDate from Daily daily")
List<LocalDate> findDistinctTradeDate(); Set<LocalDate> findDistinctTradeDate();
@Query("select distinct daily.tradeDate from Daily daily where daily.stock.id = ?1")
List<LocalDate> findDistinctTradeDateByStockId(Long stockId);
@Query("select max(daily.tradeDate) from Daily daily") @Query("select max(daily.tradeDate) from Daily daily")
LocalDate findMaxTradeDate(); LocalDate findMaxTradeDate();

View File

@@ -4,8 +4,7 @@ import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository; import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.Set;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@@ -17,16 +16,12 @@ import org.springframework.stereotype.Repository;
@Repository @Repository
public interface StockRepository extends SimpleRepository<Stock> { public interface StockRepository extends SimpleRepository<Stock> {
@Query("select distinct stock.industry from Stock stock where stock.industry is not null") @Query("select distinct stock.industry from Stock stock where stock.industry is not null")
List<String> findDistinctIndustries(); Set<String> findDistinctIndustries();
@Query("select distinct stock.code from Stock stock") @Query("select distinct stock.code from Stock stock")
List<String> findDistinctCodes(); Set<String> findDistinctCodes();
@Modifying
@Transactional(rollbackOn = Throwable.class) @Transactional(rollbackOn = Throwable.class)
@Modifying
void deleteAllByCodeIn(Collection<String> code); void deleteAllByCodeIn(Collection<String> code);
@EntityGraph(attributePaths = {"indicators"})
@Query("from Stock stock where size(stock.indicators) >= ?1")
List<Stock> findAllByIndicatorsSizeGreaterThanEqual(int count);
} }

View File

@@ -14,7 +14,11 @@ import org.springframework.transaction.annotation.Transactional;
@Repository @Repository
public interface TaskRepository extends SimpleRepository<Task> { public interface TaskRepository extends SimpleRepository<Task> {
@Modifying @Modifying
@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") @Query("""
update Task task set
task.status = com.lanyuanxiaoyao.leopard.core.entity.Task.Status.FAILURE,
task.finishedTime = current timestamp
where task.status = com.lanyuanxiaoyao.leopard.core.entity.Task.Status.RUNNING""")
void updateAllRunningTaskToFailure(); void updateAllRunningTaskToFailure();
@Transactional(rollbackFor = Throwable.class) @Transactional(rollbackFor = Throwable.class)

View File

@@ -12,6 +12,7 @@ import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/** /**
* 金字塔选股 * 金字塔选股
@@ -28,6 +29,7 @@ public class PyramidStockSelector implements StockSelector<PyramidStockSelector.
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
} }
@Transactional(readOnly = true)
@Override @Override
public Set<Candidate> select(Request request) { public Set<Candidate> select(Request request) {
// 选择至少有最近5年财报的股票 // 选择至少有最近5年财报的股票

View File

@@ -6,6 +6,7 @@ import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.server.service.TaskService; import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse; import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -100,18 +101,21 @@ public class CommonOptionsController {
case "stock_market" -> GlobalResponse.responseSuccess( case "stock_market" -> GlobalResponse.responseSuccess(
Arrays.stream(Stock.Market.values()) Arrays.stream(Stock.Market.values())
.map(market -> new Option(market.getChineseName(), market.name())) .map(market -> new Option(market.getChineseName(), market.name()))
.sorted(Comparator.comparing(Option::label))
.toList() .toList()
); );
case "stock_industry" -> GlobalResponse.responseSuccess( case "stock_industry" -> GlobalResponse.responseSuccess(
stockRepository.findDistinctIndustries() stockRepository.findDistinctIndustries()
.stream() .stream()
.map(industry -> new Option(industry, industry)) .map(industry -> new Option(industry, industry))
.sorted(Comparator.comparing(Option::label))
.toList() .toList()
); );
case "task_template_id" -> GlobalResponse.responseSuccess( case "task_template_id" -> GlobalResponse.responseSuccess(
taskService.getTemplates() taskService.getTemplates()
.stream() .stream()
.map(template -> new Option(template.name(), template.id())) .map(template -> new Option(template.name(), template.id()))
.sorted(Comparator.comparing(Option::label))
.toList() .toList()
); );
default -> GlobalResponse.responseSuccess(List.of()); default -> GlobalResponse.responseSuccess(List.of());

View File

@@ -9,6 +9,7 @@ import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport; import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -48,7 +49,10 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
@GetMapping("template/list") @GetMapping("template/list")
public GlobalResponse<Map<String, Object>> templateList() { public GlobalResponse<Map<String, Object>> templateList() {
var templates = taskService.getTemplates(); var templates = taskService.getTemplates()
.stream()
.sorted(Comparator.comparing(TaskService.TaskTemplate::name))
.toList();
return GlobalResponse.responseCrudData(templates, templates.size()); return GlobalResponse.responseCrudData(templates, templates.size());
} }

View File

@@ -25,13 +25,11 @@ import org.springframework.stereotype.Service;
@Slf4j @Slf4j
@Service @Service
public class StockService extends SimpleServiceSupport<Stock> { public class StockService extends SimpleServiceSupport<Stock> {
private final StockRepository stockRepository;
private final FinanceIndicatorRepository financeIndicatorRepository; private final FinanceIndicatorRepository financeIndicatorRepository;
private final DailyRepository dailyRepository; private final DailyRepository dailyRepository;
public StockService(StockRepository repository, FinanceIndicatorRepository financeIndicatorRepository, DailyRepository dailyRepository) { public StockService(StockRepository repository, FinanceIndicatorRepository financeIndicatorRepository, DailyRepository dailyRepository) {
super(repository); super(repository);
this.stockRepository = repository;
this.financeIndicatorRepository = financeIndicatorRepository; this.financeIndicatorRepository = financeIndicatorRepository;
this.dailyRepository = dailyRepository; this.dailyRepository = dailyRepository;
} }

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Task; import com.lanyuanxiaoyao.leopard.core.entity.Task;
import com.lanyuanxiaoyao.leopard.core.repository.TaskRepository; import com.lanyuanxiaoyao.leopard.core.repository.TaskRepository;
import com.lanyuanxiaoyao.leopard.server.service.task.PyramidSelect;
import com.lanyuanxiaoyao.leopard.server.service.task.TaskRunner; import com.lanyuanxiaoyao.leopard.server.service.task.TaskRunner;
import com.lanyuanxiaoyao.leopard.server.service.task.UpdateDailyTask; import com.lanyuanxiaoyao.leopard.server.service.task.UpdateDailyTask;
import com.lanyuanxiaoyao.leopard.server.service.task.UpdateFinanceIndicatorTask; import com.lanyuanxiaoyao.leopard.server.service.task.UpdateFinanceIndicatorTask;
@@ -40,7 +41,8 @@ public class TaskService extends SimpleServiceSupport<Task> {
new TaskTemplate("更新股票信息", "更新股票信息", UpdateStockTask.class), new TaskTemplate("更新股票信息", "更新股票信息", UpdateStockTask.class),
new TaskTemplate("更新年线指标", "更新年线指标", UpdateYearlyTask.class), new TaskTemplate("更新年线指标", "更新年线指标", UpdateYearlyTask.class),
new TaskTemplate("更新日线数据", "更新日线数据", UpdateDailyTask.class), new TaskTemplate("更新日线数据", "更新日线数据", UpdateDailyTask.class),
new TaskTemplate("更新财务指标", "更新财务指标", UpdateFinanceIndicatorTask.class) new TaskTemplate("更新财务指标", "更新财务指标", UpdateFinanceIndicatorTask.class),
new TaskTemplate("金字塔选股", "金字塔选股", PyramidSelect.class)
).collect(Collectors.toSet()); ).collect(Collectors.toSet());
private final Map<String, TaskTemplate> templateMap = templates.stream() private final Map<String, TaskTemplate> templateMap = templates.stream()
.collect(Collectors.toMap(TaskTemplate::id, template -> template)); .collect(Collectors.toMap(TaskTemplate::id, template -> template));

View File

@@ -0,0 +1,45 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository;
import com.lanyuanxiaoyao.leopard.core.service.selector.PyramidStockSelector;
import com.lanyuanxiaoyao.leopard.core.service.selector.StockSelector;
import java.time.LocalDate;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 金字塔选股
*
* @author lanyuanxiaoyao
* @version 20250925
*/
@Slf4j
@Component
public class PyramidSelect extends TaskRunner {
private final StockCollectionRepository stockCollectionRepository;
private final PyramidStockSelector pyramidStockSelector;
protected PyramidSelect(ApplicationContext context, StockCollectionRepository stockCollectionRepository, PyramidStockSelector pyramidStockSelector) {
super(context);
this.stockCollectionRepository = stockCollectionRepository;
this.pyramidStockSelector = pyramidStockSelector;
}
@Transactional(rollbackFor = Throwable.class)
@Override
public String process(Map<String, Object> params, StepUpdater updater) throws Exception {
var candidates = pyramidStockSelector.select(new PyramidStockSelector.Request(LocalDate.now().getYear(), 50));
var collection = new StockCollection();
collection.setName("金字塔选股");
collection.setDescription("金字塔选股");
collection.setStocks(candidates.stream().map(StockSelector.Candidate::stock).collect(Collectors.toSet()));
stockCollectionRepository.save(collection);
return null;
}
}

View File

@@ -37,7 +37,11 @@ public abstract class TaskRunner {
taskRepository.saveAndFlush(task); taskRepository.saveAndFlush(task);
try { try {
var result = process(params, step -> taskRepository.updateStepById(task.getId(), step)); var result = process(params, step -> {
synchronized (task) {
taskRepository.updateStepById(task.getId(), step);
}
});
task.setStatus(Task.Status.SUCCESS); task.setStatus(Task.Status.SUCCESS);
task.setStep(1.0); task.setStep(1.0);

View File

@@ -10,15 +10,17 @@ import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper; import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper;
import com.lanyuanxiaoyao.leopard.server.service.TuShareService; import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate;
/** /**
* 更新日线数据 * 更新日线数据
@@ -32,16 +34,18 @@ public class UpdateDailyTask extends TaskRunner {
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final DailyRepository dailyRepository; private final DailyRepository dailyRepository;
private final TransactionTemplate transactionTemplate;
private final TuShareService tuShareService; private final TuShareService tuShareService;
protected UpdateDailyTask(ApplicationContext context, StockRepository stockRepository, DailyRepository dailyRepository, TuShareService tuShareService) { protected UpdateDailyTask(ApplicationContext context, StockRepository stockRepository, DailyRepository dailyRepository, TransactionTemplate transactionTemplate, TuShareService tuShareService) {
super(context); super(context);
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
this.dailyRepository = dailyRepository; this.dailyRepository = dailyRepository;
this.transactionTemplate = transactionTemplate;
this.tuShareService = tuShareService; this.tuShareService = tuShareService;
} }
@Transactional(rollbackFor = Throwable.class)
@Override @Override
public String process(Map<String, Object> params, StepUpdater updater) throws Exception { public String process(Map<String, Object> params, StepUpdater updater) throws Exception {
var tradeDates = new HashSet<LocalDate>(); var tradeDates = new HashSet<LocalDate>();
@@ -60,39 +64,44 @@ public class UpdateDailyTask extends TaskRunner {
.filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate)) .filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate))
.filter(date -> !existsTradeDates.contains(date)) .filter(date -> !existsTradeDates.contains(date))
.toList(); .toList();
for (int index = 0, total = targetTradeDates.size(); index < total; index++) { var total = targetTradeDates.size();
var tradeDate = targetTradeDates.get(index); var finished = new AtomicInteger(0);
var factorResponse = tuShareService.factorList(tradeDate); targetTradeDates.parallelStream()
var factorMap = new HashMap<String, Double>(); .forEach(tradeDate -> {
for (List<String> item : factorResponse.data().items()) { var factorResponse = tuShareService.factorList(tradeDate);
factorMap.put(item.get(0), Double.valueOf(item.get(2))); var factorMap = new HashMap<String, Double>();
} for (List<String> item : factorResponse.data().items()) {
factorMap.put(item.get(0), Double.valueOf(item.get(2)));
var response = tuShareService.dailyList(tradeDate);
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);
} }
} transactionTemplate.execute(status -> {
updater.update(index * 1.0 / total); var response = tuShareService.dailyList(tradeDate);
} var dailies = new ArrayList<Daily>();
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);
dailies.add(daily);
}
}
dailyRepository.saveAll(dailies);
return null;
});
updater.update(finished.incrementAndGet() * 1.0 / total);
});
return null; return null;
} }
} }

View File

@@ -17,6 +17,7 @@ import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/** /**
* 更新财务指标数据 * 更新财务指标数据
@@ -39,6 +40,7 @@ public class UpdateFinanceIndicatorTask extends TaskRunner {
this.tuShareService = tuShareService; this.tuShareService = tuShareService;
} }
@Transactional(rollbackFor = Throwable.class)
@Override @Override
public String process(Map<String, Object> params, StepUpdater updater) throws JsonProcessingException { public String process(Map<String, Object> params, StepUpdater updater) throws JsonProcessingException {
var stocks = stockRepository.findAll(); var stocks = stockRepository.findAll();

View File

@@ -0,0 +1,3 @@
spring:
datasource:
url: jdbc:postgresql://127.0.0.1:6785/leopard_dev

View File

@@ -23,7 +23,7 @@ spring:
job-store-type: jdbc job-store-type: jdbc
jdbc: jdbc:
platform: postgres platform: postgres
# initialize-schema: always initialize-schema: always
properties: properties:
org: org:
quartz: quartz:

View File

@@ -28,7 +28,7 @@ function TaskDetail() {
label: '进度', label: '进度',
content: { content: {
type: 'tpl', type: 'tpl',
tpl: "${step}%", tpl: "${ROUND(step * 100, 2)}%",
}, },
span: 2, span: 2,
}, },