feat: 完成金字塔选股和评估
This commit is contained in:
@@ -209,8 +209,8 @@ Content-Type: application/json
|
|||||||
"api_name": "fina_indicator",
|
"api_name": "fina_indicator",
|
||||||
"token": "{{api_key}}",
|
"token": "{{api_key}}",
|
||||||
"params": {
|
"params": {
|
||||||
"ts_code": "600132.SH",
|
"ts_code": "001400.SZ",
|
||||||
"period": "20191231"
|
"period": "20231231"
|
||||||
},
|
},
|
||||||
"fields": [
|
"fields": [
|
||||||
"ts_code",
|
"ts_code",
|
||||||
@@ -238,3 +238,25 @@ Content-Type: application/json
|
|||||||
"resume_date"
|
"resume_date"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
### Get Index
|
||||||
|
POST {{api_url}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"api_name": "index_basic",
|
||||||
|
"token": "{{api_key}}",
|
||||||
|
"params": {
|
||||||
|
"market": "CSI"
|
||||||
|
},
|
||||||
|
"fields": [
|
||||||
|
"ts_code",
|
||||||
|
"name",
|
||||||
|
"fullname",
|
||||||
|
"market",
|
||||||
|
"publisher",
|
||||||
|
"index_type",
|
||||||
|
"category",
|
||||||
|
"desc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
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.QStock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
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.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金字塔选股
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250917
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@LiteflowComponent("pyramid_stock_selector")
|
||||||
|
public class PyramidStockSelector extends NodeComponent {
|
||||||
|
private final StockRepository stockRepository;
|
||||||
|
|
||||||
|
public PyramidStockSelector(StockRepository stockRepository) {
|
||||||
|
this.stockRepository = stockRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Throwable.class)
|
||||||
|
@Override
|
||||||
|
public void process() {
|
||||||
|
var assessment = getContextBean(StockAssessmentNode.StockAssessment.class);
|
||||||
|
|
||||||
|
// 选择至少有最近5年财报的股票
|
||||||
|
// 有点奇怪,001400.SZ有近5年的财报但资料显示是2025年才上市的
|
||||||
|
var stocks = stockRepository.findAll(QStock.stock.listedDate.before(LocalDate.of(assessment.year(), 1, 1)));
|
||||||
|
log.info("Year: {} Stock: {}", assessment.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() < assessment.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);
|
||||||
|
}
|
||||||
|
var first50 = scores.entrySet()
|
||||||
|
.stream()
|
||||||
|
.sorted((e1, e2) -> e2.getValue() - e1.getValue())
|
||||||
|
.limit(50)
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
assessment.stocks().addAll(first50);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.strategy;
|
||||||
|
|
||||||
|
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.QStock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
|
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
||||||
|
import com.yomahub.liteflow.core.NodeComponent;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
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.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 股票评估
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250919
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@LiteflowComponent("assess_stock")
|
||||||
|
public class StockAssessmentNode extends NodeComponent {
|
||||||
|
private static final Map<String, Double> INDUSTRY_TOP = new HashMap<>();
|
||||||
|
private final StockRepository stockRepository;
|
||||||
|
private final DailyRepository dailyRepository;
|
||||||
|
|
||||||
|
public StockAssessmentNode(StockRepository stockRepository, DailyRepository dailyRepository) {
|
||||||
|
this.stockRepository = stockRepository;
|
||||||
|
this.dailyRepository = dailyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public void process() {
|
||||||
|
var assessment = getContextBean(StockAssessment.class);
|
||||||
|
if (ObjectUtil.isNotNull(assessment) && ObjectUtil.isNotEmpty(assessment.stocks())) {
|
||||||
|
var dailyMap = dailyRepository.findAll(
|
||||||
|
QDaily.daily.tradeDate.year().eq(assessment.year())
|
||||||
|
.and(QDaily.daily.stock.in(assessment.stocks()))
|
||||||
|
)
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(Daily::getStock));
|
||||||
|
for (Stock stock : assessment.stocks()) {
|
||||||
|
if (!dailyMap.containsKey(stock) || ObjectUtil.isEmpty(dailyMap.get(stock))) {
|
||||||
|
log.warn("Cannot find daily data in {} for {}", assessment.year(), stock.getCode());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var dailies = dailyMap.get(stock)
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
|
.toList();
|
||||||
|
var change = getChange(dailies);
|
||||||
|
var std = getStd(dailies);
|
||||||
|
var industryTop = getTopOfIndustry(stock.getIndustry(), assessment.year());
|
||||||
|
assessment.results().add(new StockAssessment.Result(stock, change, std, industryTop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getTopOfIndustry(String industry, int year) {
|
||||||
|
log.info("Calculate industry: {} for {}", industry, year);
|
||||||
|
if (INDUSTRY_TOP.containsKey(industry)) {
|
||||||
|
return INDUSTRY_TOP.get(industry);
|
||||||
|
}
|
||||||
|
var top = stockRepository.findAll(QStock.stock.industry.eq(industry))
|
||||||
|
.parallelStream()
|
||||||
|
.filter(stock -> stock.getListedDate().getYear() <= year)
|
||||||
|
.map(stock -> {
|
||||||
|
List<Daily> dailies = dailyRepository.findAll(
|
||||||
|
QDaily.daily.tradeDate.year().eq(year)
|
||||||
|
.and(QDaily.daily.stock.eq(stock))
|
||||||
|
);
|
||||||
|
if (ObjectUtil.isEmpty(dailies)) {
|
||||||
|
log.warn("Cannot find daily data in {} for {} {}", year, stock.getCode(), stock.getName());
|
||||||
|
}
|
||||||
|
return dailies;
|
||||||
|
})
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.map(this::getChange)
|
||||||
|
.mapToDouble(change -> change)
|
||||||
|
.max()
|
||||||
|
.orElse(0.0);
|
||||||
|
INDUSTRY_TOP.put(industry, top);
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StockAssessment(int year, Set<Stock> stocks, Set<StockAssessment.Result> results) {
|
||||||
|
public StockAssessment(int year) {
|
||||||
|
this(year, new HashSet<>(), new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +1,37 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.strategy;
|
package com.lanyuanxiaoyao.leopard.strategy;
|
||||||
|
|
||||||
import cn.hutool.core.map.MapUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
|
import cn.hutool.core.util.NumberUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Daily_;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
|
import com.yomahub.liteflow.core.FlowExecutor;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.commonmark.Extension;
|
|
||||||
import org.commonmark.ext.gfm.tables.TablesExtension;
|
|
||||||
import org.commonmark.parser.Parser;
|
|
||||||
import org.commonmark.renderer.html.HtmlRenderer;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
import org.ta4j.core.BaseBar;
|
|
||||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
|
||||||
import org.ta4j.core.indicators.SMAIndicator;
|
|
||||||
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
|
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
|
||||||
@EnableJpaAuditing
|
@EnableJpaAuditing
|
||||||
public class StrategyApplication {
|
public class StrategyApplication {
|
||||||
private static final List<Extension> extensions = List.of(TablesExtension.create());
|
|
||||||
private static final Parser parser = Parser.builder().extensions(extensions).build();
|
|
||||||
private static final HtmlRenderer render = HtmlRenderer.builder().extensions(extensions).build();
|
|
||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
@Resource
|
@Resource
|
||||||
private StockRepository stockRepository;
|
private StockRepository stockRepository;
|
||||||
@Resource
|
@Resource
|
||||||
private DailyRepository dailyRepository;
|
private DailyRepository dailyRepository;
|
||||||
|
@Resource
|
||||||
|
private FlowExecutor flowExecutor;
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(StrategyApplication.class, args);
|
SpringApplication.run(StrategyApplication.class, args);
|
||||||
@@ -320,46 +307,22 @@ public class StrategyApplication {
|
|||||||
.sorted((t1, t2) -> Double.compare(Double.parseDouble(t2.get("increase")), Double.parseDouble(t1.get("increase"))))
|
.sorted((t1, t2) -> Double.compare(Double.parseDouble(t2.get("increase")), Double.parseDouble(t1.get("increase"))))
|
||||||
.toList();
|
.toList();
|
||||||
render(Map.of("items", data));*/
|
render(Map.of("items", data));*/
|
||||||
var dailies = dailyRepository.findAll(
|
|
||||||
QDaily.daily.stock.code.eq("000002.SZ")
|
|
||||||
.and(QDaily.daily.tradeDate.year().goe(2023)),
|
|
||||||
Sort.by(Daily_.TRADE_DATE)
|
|
||||||
);
|
|
||||||
var barSeries = new BaseBarSeriesBuilder().build();
|
|
||||||
dailies.forEach(daily -> barSeries.addBar(new BaseBar(
|
|
||||||
Duration.ofDays(1),
|
|
||||||
daily.getTradeDate().atStartOfDay().atZone(ZoneId.systemDefault()),
|
|
||||||
daily.getHfqOpen(),
|
|
||||||
daily.getHfqHigh(),
|
|
||||||
daily.getHfqLow(),
|
|
||||||
daily.getHfqClose(),
|
|
||||||
daily.getVolume()
|
|
||||||
)));
|
|
||||||
var sma10 = new SMAIndicator(new ClosePriceIndicator(barSeries), 10);
|
|
||||||
var sma60 = new SMAIndicator(new ClosePriceIndicator(barSeries), 60);
|
|
||||||
var sma120 = new SMAIndicator(new ClosePriceIndicator(barSeries), 120);
|
|
||||||
|
|
||||||
var xList = new ArrayList<String>();
|
var lines = new ArrayList<String>();
|
||||||
var yList = new ArrayList<List<Double>>();
|
for (int year = 2024; year < 2025; year++) {
|
||||||
var sma10List = new ArrayList<Double>();
|
var assessment = new StockAssessmentNode.StockAssessment(year);
|
||||||
var sma60List = new ArrayList<Double>();
|
var response = flowExecutor.execute2RespWithEL("THEN(pyramid_stock_selector,assess_stock)", null, IdUtil.nanoId(), assessment);
|
||||||
var sma120List = new ArrayList<Double>();
|
assessment = response.getContextBean(StockAssessmentNode.StockAssessment.class);
|
||||||
for (int index = 0; index < dailies.size(); index++) {
|
int up = assessment.results()
|
||||||
var daily = dailies.get(index);
|
.stream()
|
||||||
xList.add(daily.getTradeDate().toString());
|
.filter(result -> result.change() > 0)
|
||||||
yList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh()));
|
.mapToInt(result -> 1)
|
||||||
sma10List.add(sma10.getValue(index).doubleValue());
|
.sum();
|
||||||
sma60List.add(sma60.getValue(index).doubleValue());
|
lines.add(NumberUtil.roundStr(up * 100.0 / assessment.results().size(), 2));
|
||||||
sma120List.add(sma120.getValue(index).doubleValue());
|
assessment.results().forEach(result -> log.info("{} {} {} {} {}", result.stock().getCode(), result.stock().getName(), result.change(), result.std(), result.industryTop()));
|
||||||
|
}
|
||||||
|
for (int index = 0, year = 2010; index < lines.size(); index++, year++) {
|
||||||
|
log.info("胜率: {} {}", year, lines.get(index));
|
||||||
}
|
}
|
||||||
render(
|
|
||||||
MapUtil.<String, Object>builder()
|
|
||||||
.put("xList", xList)
|
|
||||||
.put("yList", yList)
|
|
||||||
.put("sma10", sma10List)
|
|
||||||
.put("sma60", sma60List)
|
|
||||||
.put("sma120", sma120List)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="com.lanyuanxiaoyao.leopard" level="INFO"/>
|
<logger name="com.lanyuanxiaoyao.leopard" level="INFO"/>
|
||||||
<logger name="org.hibernate.SQL" level="DEBUG"/>
|
<!--<logger name="org.hibernate.SQL" level="DEBUG"/>-->
|
||||||
|
|
||||||
<root level="ERROR">
|
<root level="ERROR">
|
||||||
<appender-ref ref="Console"/>
|
<appender-ref ref="Console"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user