1
0

feat: 增加股票集展示

This commit is contained in:
2025-09-17 18:24:05 +08:00
parent f8ee51c0ed
commit 5953c9b9f2
12 changed files with 448 additions and 167 deletions

View File

@@ -59,6 +59,15 @@
<artifactId>hutool-http</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>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>

View File

@@ -1,13 +1,14 @@
package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
import com.lanyuanxiaoyao.leopard.server.entity.StockDetailVo;
import com.lanyuanxiaoyao.leopard.server.service.StockCollectionService;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -52,6 +53,9 @@ public class StockCollectionController extends SimpleControllerSupport<StockColl
collection.getDescription(),
collection.getStocks().size(),
collection.getStocks()
.stream()
.map(StockDetailVo::of)
.collect(Collectors.toSet())
);
}
@@ -76,7 +80,7 @@ public class StockCollectionController extends SimpleControllerSupport<StockColl
String name,
String description,
Integer count,
Set<Stock> stocks
Set<StockDetailVo> stocks
) {
}
}

View File

@@ -2,6 +2,7 @@ package com.lanyuanxiaoyao.leopard.server.controller;
import cn.hutool.core.bean.BeanUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.server.entity.StockDetailVo;
import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
@@ -24,7 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("stock")
public class StockController extends SimpleControllerSupport<Stock, Void, StockController.DetailItem, StockController.DetailItem> {
public class StockController extends SimpleControllerSupport<Stock, Void, StockDetailVo, StockDetailVo> {
private final StockService stockService;
public StockController(StockService service, StockService stockService) {
@@ -126,37 +127,14 @@ public class StockController extends SimpleControllerSupport<Stock, Void, StockC
throw new UnsupportedOperationException();
}
private DetailItem covert(Stock stock) {
return new DetailItem(
stock.getId(),
stock.getCode(),
stock.getName(),
stock.getFullname(),
stock.getMarket(),
stock.getIndustry(),
stock.getListedDate()
);
@Override
protected Function<Stock, StockDetailVo> listItemMapper() {
return StockDetailVo::of;
}
@Override
protected Function<Stock, DetailItem> listItemMapper() {
return this::covert;
}
@Override
protected Function<Stock, DetailItem> detailItemMapper() {
return this::covert;
}
public record DetailItem(
Long id,
String code,
String name,
String fullname,
Stock.Market market,
String industry,
LocalDate listedDate
) {
protected Function<Stock, StockDetailVo> detailItemMapper() {
return StockDetailVo::of;
}
public record FinanceItem(

View File

@@ -0,0 +1,30 @@
package com.lanyuanxiaoyao.leopard.server.entity;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import java.time.LocalDate;
/**
* @author lanyuanxiaoyao
* @version 20250917
*/
public record StockDetailVo(
Long id,
String code,
String name,
String fullname,
Stock.Market market,
String industry,
LocalDate listedDate
) {
public static StockDetailVo of(Stock stock) {
return new StockDetailVo(
stock.getId(),
stock.getCode(),
stock.getName(),
stock.getFullname(),
stock.getMarket(),
stock.getIndustry(),
stock.getListedDate()
);
}
}

View File

@@ -0,0 +1,221 @@
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);
}
}