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) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.lanyuanxiaoyao.leopard.core.service.selector;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
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 java.time.LocalDate;
|
||||
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 PyramidStockSelector implements StockSelector<PyramidStockSelector.Request> {
|
||||
private final StockRepository stockRepository;
|
||||
|
||||
public PyramidStockSelector(StockRepository stockRepository) {
|
||||
this.stockRepository = stockRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Candidate> select(Request request) {
|
||||
// 选择至少有最近5年财报的股票
|
||||
// 有点奇怪,001400.SZ有近5年的财报但资料显示是2025年才上市的
|
||||
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() < request.year())
|
||||
.sorted((a, b) -> b.getYear() - a.getYear())
|
||||
.limit(5)
|
||||
.toList();
|
||||
if (recentIndicators.size() < 5) {
|
||||
continue;
|
||||
}
|
||||
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, scores.get(stock) + 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, scores.get(stock) + 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, scores.get(stock) + 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, scores.get(stock) + cashScore);
|
||||
|
||||
if (ObjectUtil.isNotNull(latestIndicator.getDaysAccountsReceivableTurnover()) && latestIndicator.getDaysAccountsReceivableTurnover() <= 30) {
|
||||
scores.put(stock, scores.get(stock) + 20);
|
||||
}
|
||||
if (ObjectUtil.isNotNull(latestIndicator.getDaysInventoryTurnover()) && latestIndicator.getDaysInventoryTurnover() <= 30) {
|
||||
scores.put(stock, scores.get(stock) + 20);
|
||||
}
|
||||
if (ArrayUtil.isAllNotNull(latestIndicator.getDaysAccountsReceivableTurnover(), latestIndicator.getDaysInventoryTurnover())) {
|
||||
if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 40) {
|
||||
scores.put(stock, scores.get(stock) + 20);
|
||||
} else if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 60) {
|
||||
scores.put(stock, scores.get(stock) + 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, scores.get(stock) + 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, scores.get(stock) + 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, scores.get(stock) + 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, scores.get(stock) + cashAscendingScore);
|
||||
}
|
||||
return scores.entrySet()
|
||||
.stream()
|
||||
.sorted((e1, e2) -> e2.getValue() - e1.getValue())
|
||||
.limit(request.limit())
|
||||
.map(entry -> new Candidate(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user