Compare commits
4 Commits
868feeb34d
...
5953c9b9f2
| Author | SHA1 | Date | |
|---|---|---|---|
| 5953c9b9f2 | |||
| f8ee51c0ed | |||
| 0b9cb55788 | |||
| 585b37a1cc |
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -8,7 +8,7 @@
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="temurin-17" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -65,6 +65,19 @@ public class Stock extends SimpleEntity {
|
||||
@ToString.Exclude
|
||||
private Set<FinanceIndicator> indicators;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Stock stock = (Stock) o;
|
||||
return code.equals(stock.code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return code.hashCode();
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum Market implements SimpleEnum {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,7 @@ public class QuartzService {
|
||||
var tasks = new ArrayList<QuartzTask>();
|
||||
for (var key : scheduler.getJobKeys(GroupMatcher.anyGroup())) {
|
||||
var detail = scheduler.getJobDetail(key);
|
||||
var trigger = (CronTrigger) scheduler.getTriggersOfJob(key).get(0);
|
||||
var trigger = (CronTrigger) scheduler.getTriggersOfJob(key).getFirst();
|
||||
tasks.add(new QuartzTask(
|
||||
detail.getKey().getName(),
|
||||
detail.getJobDataMap().getLong("template_id"),
|
||||
|
||||
@@ -41,7 +41,7 @@ public class CheckDailyNode extends TaskNodeComponent {
|
||||
.data()
|
||||
.items()
|
||||
.stream()
|
||||
.map(item -> LocalDate.parse(item.get(0), TuShareService.TRADE_FORMAT))
|
||||
.map(item -> LocalDate.parse(item.getFirst(), TuShareService.TRADE_FORMAT))
|
||||
.filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate))
|
||||
.toList();
|
||||
var total = stocks.size();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,8 @@ public class UpdateDailyNode extends NodeComponent {
|
||||
for (String exchange : List.of("SSE", "SZSE", "BSE")) {
|
||||
var response = tuShareService.tradeDateList(exchange);
|
||||
for (List<String> item : response.data().items()) {
|
||||
if (ObjectUtil.isNotEmpty(item) && StrUtil.isNotBlank(item.get(0))) {
|
||||
tradeDates.add(LocalDate.parse(item.get(0), TuShareService.TRADE_FORMAT));
|
||||
if (ObjectUtil.isNotEmpty(item) && StrUtil.isNotBlank(item.getFirst())) {
|
||||
tradeDates.add(LocalDate.parse(item.getFirst(), TuShareService.TRADE_FORMAT));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public class UpdateDailyNode 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)))
|
||||
// .filter(date -> date.isAfter(LocalDate.of(2024, 12, 31)))
|
||||
.forEach(tradeDate -> {
|
||||
var factorResponse = tuShareService.factorList(tradeDate);
|
||||
var factorMap = new HashMap<String, Double>();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.lanyuanxiaoyao.leopard.server;
|
||||
|
||||
import com.lanyuanxiaoyao.leopard.server.helper.MdHelper;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250917
|
||||
*/
|
||||
public class MdTest {
|
||||
public static void main(String[] args) {
|
||||
System.out.println(
|
||||
MdHelper.of()
|
||||
.bigTitle("Markdown Helper")
|
||||
.title("Markdown Helper")
|
||||
.subTitle("Markdown Helper")
|
||||
.code("java")
|
||||
.text("System.out.println()")
|
||||
.endCode()
|
||||
.divider()
|
||||
.table()
|
||||
.data(new Object[]{"name", "value"}, new Object[][]{new Object[]{"1", "2"}, new Object[]{"3", "4"}})
|
||||
.endTable()
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.lanyuanxiaoyao.leopard.strategy;
|
||||
|
||||
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.Stock;
|
||||
@@ -32,7 +33,7 @@ public class StrategyApplication {
|
||||
@Transactional(rollbackOn = Throwable.class)
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void test() {
|
||||
var stocks = stockRepository.findAllByIndicatorsSizeGreaterThanEqual(6);
|
||||
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) {
|
||||
@@ -41,9 +42,9 @@ public class StrategyApplication {
|
||||
.sorted((a, b) -> b.getYear() - a.getYear())
|
||||
.limit(5)
|
||||
.toList();
|
||||
var latestIndicator = recentIndicators.get(0);
|
||||
var latestIndicator = recentIndicators.getFirst();
|
||||
|
||||
int roeScore = 0;
|
||||
var roeScore = 0;
|
||||
if (recentIndicators.stream().noneMatch(indicator -> indicator.getReturnOnEquity() == null || indicator.getReturnOnEquity() < 0)) {
|
||||
var averageRoe = recentIndicators.stream()
|
||||
.map(FinanceIndicator::getReturnOnEquity)
|
||||
@@ -67,11 +68,10 @@ public class StrategyApplication {
|
||||
}
|
||||
scores.put(stock.getCode(), scores.get(stock.getCode()) + roeScore);
|
||||
|
||||
int roaScore = 0;
|
||||
var 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);
|
||||
@@ -85,11 +85,10 @@ public class StrategyApplication {
|
||||
}
|
||||
scores.put(stock.getCode(), scores.get(stock.getCode()) + roaScore);
|
||||
|
||||
int netProfitScore = 0;
|
||||
var 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);
|
||||
@@ -101,8 +100,14 @@ public class StrategyApplication {
|
||||
}
|
||||
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)) {
|
||||
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);
|
||||
@@ -113,7 +118,7 @@ public class StrategyApplication {
|
||||
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 (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) {
|
||||
@@ -131,6 +136,73 @@ public class StrategyApplication {
|
||||
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);
|
||||
}
|
||||
scores.entrySet()
|
||||
.stream()
|
||||
|
||||
@@ -14,6 +14,7 @@ import TaskScheduleList from './pages/task/TaskScheduleList.tsx'
|
||||
import TaskScheduleSave from './pages/task/TaskScheduleSave.tsx'
|
||||
import StockCollectionList from './pages/stock/StockCollectionList.tsx'
|
||||
import TaskDetail from './pages/task/TaskDetail.tsx'
|
||||
import StockCollectionDetail from './pages/stock/StockCollectionDetail.tsx'
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
{
|
||||
@@ -46,6 +47,10 @@ const routes: RouteObject[] = [
|
||||
path: 'list',
|
||||
Component: StockCollectionList,
|
||||
},
|
||||
{
|
||||
path: 'detail/:id',
|
||||
Component: StockCollectionDetail,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
30
leopard-web/src/pages/stock/StockCollectionDetail.tsx
Normal file
30
leopard-web/src/pages/stock/StockCollectionDetail.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from "react"
|
||||
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, stockListColumns} from '../../util/amis.tsx'
|
||||
import {useNavigate, useParams} from 'react-router'
|
||||
|
||||
function StockCollectionDetail() {
|
||||
const navigate = useNavigate()
|
||||
const {id} = useParams()
|
||||
return (
|
||||
<div className="stock-collection-detail">
|
||||
{amisRender(
|
||||
{
|
||||
type: 'page',
|
||||
title: '股票集详情',
|
||||
initApi: `get:${commonInfo.baseUrl}/stock_collection/detail/${id}`,
|
||||
body: [
|
||||
{
|
||||
type: 'crud',
|
||||
source: '${stocks}',
|
||||
...crudCommonOptions(),
|
||||
...paginationTemplate(15, undefined, ['filter-toggler']),
|
||||
columns: stockListColumns(navigate),
|
||||
}
|
||||
]
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(StockCollectionDetail)
|
||||
@@ -1,8 +1,71 @@
|
||||
import React from "react"
|
||||
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate} from '../../util/amis.tsx'
|
||||
import {useNavigate} from 'react-router'
|
||||
|
||||
function StockCollectionList() {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<div className="stock-collection-list"></div>
|
||||
<div className="stock-collection-list">
|
||||
{amisRender(
|
||||
{
|
||||
type: 'page',
|
||||
title: '股票列表',
|
||||
body: [
|
||||
{
|
||||
type: 'crud',
|
||||
api: {
|
||||
method: 'get',
|
||||
url: `${commonInfo.baseUrl}/stock_collection/list`,
|
||||
},
|
||||
...crudCommonOptions(),
|
||||
...paginationTemplate(15, undefined, ['filter-toggler']),
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
label: '名称',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: '描述',
|
||||
},
|
||||
{
|
||||
name: 'count',
|
||||
label: '股票数量',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
type: 'operation',
|
||||
label: '操作',
|
||||
width: 100,
|
||||
buttons: [
|
||||
{
|
||||
type: 'action',
|
||||
label: '详情',
|
||||
level: 'link',
|
||||
onEvent: {
|
||||
click: {
|
||||
actions: [
|
||||
{
|
||||
actionType: 'custom',
|
||||
// @ts-ignore
|
||||
script: (context, action, event) => {
|
||||
navigate(`/stock/collection/detail/${context.props.data['id']}`)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@ import {
|
||||
amisRender,
|
||||
commonInfo,
|
||||
crudCommonOptions,
|
||||
date,
|
||||
paginationTemplate,
|
||||
remoteMappings,
|
||||
remoteOptions,
|
||||
stockListColumns,
|
||||
} from '../../util/amis.tsx'
|
||||
import {useNavigate} from 'react-router'
|
||||
|
||||
@@ -97,65 +96,7 @@ function StockList() {
|
||||
},
|
||||
],
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
name: 'code',
|
||||
label: '编号',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: '简称',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
name: 'fullname',
|
||||
label: '全名',
|
||||
},
|
||||
{
|
||||
name: 'market',
|
||||
label: '市场',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
...remoteMappings('stock_market', 'market'),
|
||||
},
|
||||
{
|
||||
name: 'industry',
|
||||
label: '行业',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
label: '上市日期',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
...date('listedDate'),
|
||||
},
|
||||
{
|
||||
type: 'operation',
|
||||
label: '操作',
|
||||
width: 100,
|
||||
buttons: [
|
||||
{
|
||||
type: 'action',
|
||||
label: '详情',
|
||||
level: 'link',
|
||||
onEvent: {
|
||||
click: {
|
||||
actions: [
|
||||
{
|
||||
actionType: 'custom',
|
||||
// @ts-ignore
|
||||
script: (context, action, event) => {
|
||||
navigate(`/stock/detail/${context.props.data['id']}`)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
columns: stockListColumns(navigate),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'amis/sdk/iconfont.css'
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||
import axios from 'axios'
|
||||
import {isEqual} from 'es-toolkit'
|
||||
import type {NavigateFunction} from 'react-router'
|
||||
|
||||
export const commonInfo = {
|
||||
debug: isEqual(import.meta.env.MODE, 'development'),
|
||||
@@ -334,3 +335,65 @@ export function remoteMappings(name: string, field: string) {
|
||||
source: `get:${commonInfo.baseUrl}/constants/mappings/${name}/${field}`,
|
||||
}
|
||||
}
|
||||
|
||||
export function stockListColumns(navigate: NavigateFunction) {
|
||||
return [
|
||||
{
|
||||
name: 'code',
|
||||
label: '编号',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: '简称',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
name: 'fullname',
|
||||
label: '全名',
|
||||
},
|
||||
{
|
||||
name: 'market',
|
||||
label: '市场',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
...remoteMappings('stock_market', 'market'),
|
||||
},
|
||||
{
|
||||
name: 'industry',
|
||||
label: '行业',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
label: '上市日期',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
...date('listedDate'),
|
||||
},
|
||||
{
|
||||
type: 'operation',
|
||||
label: '操作',
|
||||
width: 100,
|
||||
buttons: [
|
||||
{
|
||||
type: 'action',
|
||||
label: '详情',
|
||||
level: 'link',
|
||||
onEvent: {
|
||||
click: {
|
||||
actions: [
|
||||
{
|
||||
actionType: 'custom',
|
||||
// @ts-ignore
|
||||
script: (context, action, event) => {
|
||||
navigate(`/stock/detail/${context.props.data['id']}`)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
17
pom.xml
17
pom.xml
@@ -16,9 +16,9 @@
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<spring-boot.version>3.5.0</spring-boot.version>
|
||||
@@ -81,6 +81,17 @@
|
||||
<version>${hibernate.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.ralfkonrad.quantlib_for_maven</groupId>
|
||||
<artifactId>quantlib</artifactId>
|
||||
<version>1.39.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ta4j</groupId>
|
||||
<artifactId>ta4j-core</artifactId>
|
||||
<version>0.17</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
|
||||
Reference in New Issue
Block a user