diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/StockRepository.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/StockRepository.java index 4f84d57..cdbadbb 100644 --- a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/StockRepository.java +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/StockRepository.java @@ -5,6 +5,7 @@ import com.lanyuanxiaoyao.service.template.repository.SimpleRepository; import jakarta.transaction.Transactional; import java.util.Collection; import java.util.List; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @@ -24,4 +25,8 @@ public interface StockRepository extends SimpleRepository { @Modifying @Transactional(rollbackOn = Throwable.class) void deleteAllByCodeIn(Collection code); + + @EntityGraph(attributePaths = {"indicators"}) + @Query("from Stock stock where size(stock.indicators) >= ?1") + List findAllByIndicatorsSizeGreaterThanEqual(int count); } diff --git a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java b/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java index 63e4ae3..8a1f6ec 100644 --- a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java +++ b/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java @@ -1,10 +1,15 @@ package com.lanyuanxiaoyao.leopard.strategy; +import cn.hutool.core.util.ObjectUtil; +import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator; +import com.lanyuanxiaoyao.leopard.core.entity.Stock; import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; import jakarta.annotation.Resource; import jakarta.transaction.Transactional; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -27,5 +32,114 @@ public class StrategyApplication { @Transactional(rollbackOn = Throwable.class) @EventListener(ApplicationReadyEvent.class) public void test() { + var stocks = stockRepository.findAllByIndicatorsSizeGreaterThanEqual(6); + 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.get(0); + + int 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); + + int roaScore = 0; + if (recentIndicators.stream().noneMatch(indicator -> indicator.getReturnOnAssets() == null)) { + var averageRoa = recentIndicators.stream() + .map(FinanceIndicator::getReturnOnAssets) + .map(item -> ObjectUtil.defaultIfNull(item, 0.0)) + .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); + + int netProfitScore = 0; + if (recentIndicators.stream().noneMatch(indicator -> indicator.getNetProfit() == null)) { + var averageNetProfit = recentIndicators.stream() + .map(FinanceIndicator::getNetProfit) + .map(item -> ObjectUtil.defaultIfNull(item, 0.0)) + .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); + + int cashScore = 0; + if (ObjectUtil.isNotNull(latestIndicator.getTotalAssetsTurnover()) && ObjectUtil.isNotNull(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 (ObjectUtil.isNotNull(latestIndicator.getDaysAccountsReceivableTurnover()) && ObjectUtil.isNotNull(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); + } + } + } + scores.entrySet() + .stream() + .sorted((e1, e2) -> e2.getValue() - e1.getValue()) + .map(entry -> new StockAndScore(stocksMap.get(entry.getKey()), entry.getValue())) + .limit(50) + .forEach(ss -> log.info("{} {}", ss.stock.getName(), ss.score)); + } + + public record StockAndScore(Stock stock, int score) { } }