Compare commits
5 Commits
69020852b9
...
e387fc839f
| Author | SHA1 | Date | |
|---|---|---|---|
| e387fc839f | |||
| b9a02194e2 | |||
| b0c2530e63 | |||
| 49a03adf21 | |||
| 5390a879e7 |
@@ -2,10 +2,13 @@ package com.lanyuanxiaoyao.leopard.core.entity;
|
|||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.Constants;
|
import com.lanyuanxiaoyao.leopard.core.Constants;
|
||||||
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
|
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
|
||||||
|
import jakarta.persistence.ElementCollection;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.EntityListeners;
|
import jakarta.persistence.EntityListeners;
|
||||||
|
import jakarta.persistence.JoinTable;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import java.util.Map;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
@@ -28,5 +31,8 @@ public class StockScore extends SimpleEntity {
|
|||||||
private Stock stock;
|
private Stock stock;
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
private StockCollection collection;
|
private StockCollection collection;
|
||||||
|
@ElementCollection
|
||||||
|
@JoinTable(name = Constants.DATABASE_PREFIX + "stock_score_extra")
|
||||||
|
private Map<String, String> extra;
|
||||||
private Double score;
|
private Double score;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.experimental.FieldNameConstants;
|
||||||
|
import org.hibernate.annotations.Comment;
|
||||||
|
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 + "weekly")
|
||||||
|
public class Weekly extends SimpleEntity {
|
||||||
|
@Column(name = "`year`", nullable = false)
|
||||||
|
@Comment("年份")
|
||||||
|
private Integer year;
|
||||||
|
@Column(name = "`week`", nullable = false)
|
||||||
|
@Comment("周数")
|
||||||
|
private Integer week;
|
||||||
|
@Comment("开盘价")
|
||||||
|
private Double open;
|
||||||
|
@Comment("最高价")
|
||||||
|
private Double high;
|
||||||
|
@Comment("最低价")
|
||||||
|
private Double low;
|
||||||
|
@Comment("收盘价")
|
||||||
|
private Double close;
|
||||||
|
@Comment("涨跌额")
|
||||||
|
private Double priceChangeAmount;
|
||||||
|
@Comment("涨跌幅")
|
||||||
|
private Double priceFluctuationRange;
|
||||||
|
@Comment("成交量")
|
||||||
|
private Double volume;
|
||||||
|
@Comment("成交额")
|
||||||
|
private Double turnover;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(nullable = false)
|
||||||
|
@ToString.Exclude
|
||||||
|
private Stock stock;
|
||||||
|
}
|
||||||
@@ -42,6 +42,20 @@ public class NumberHelper {
|
|||||||
return NumberUtil.decimalFormat("0.00%", value);
|
return NumberUtil.decimalFormat("0.00%", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String formatPriceDouble(Double value) {
|
||||||
|
if (ObjectUtil.isNull(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return NumberUtil.decimalFormat("0.00", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatPriceDouble(Integer value) {
|
||||||
|
if (ObjectUtil.isNull(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return NumberUtil.decimalFormat("0.00", value);
|
||||||
|
}
|
||||||
|
|
||||||
public static Double parseDouble(String value) {
|
public static Double parseDouble(String value) {
|
||||||
if (StrUtil.isBlank(value)) {
|
if (StrUtil.isBlank(value)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ public interface DailyRepository extends SimpleRepository<Daily> {
|
|||||||
@Query("select min(daily.tradeDate) from Daily daily")
|
@Query("select min(daily.tradeDate) from Daily daily")
|
||||||
LocalDate findMinTradeDate();
|
LocalDate findMinTradeDate();
|
||||||
|
|
||||||
|
@Query("from Daily daily where daily.stock.id = ?1 order by daily.tradeDate desc limit 1")
|
||||||
|
Optional<Daily> findLatest(Long stockId);
|
||||||
|
|
||||||
@EntityGraph(attributePaths = {"stock"})
|
@EntityGraph(attributePaths = {"stock"})
|
||||||
@Override
|
@Override
|
||||||
Optional<Daily> findOne(Predicate predicate);
|
Optional<Daily> findOne(Predicate predicate);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import java.time.LocalDate;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
this.dailyRepository = dailyRepository;
|
this.dailyRepository = dailyRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "long-cache", sync = true)
|
||||||
public Optional<FinanceIndicator> findFinanceIndicator(Long stockId, Integer year) {
|
public Optional<FinanceIndicator> findFinanceIndicator(Long stockId, Integer year) {
|
||||||
return financeIndicatorRepository.findOne(
|
return financeIndicatorRepository.findOne(
|
||||||
QFinanceIndicator.financeIndicator.year.eq(year)
|
QFinanceIndicator.financeIndicator.year.eq(year)
|
||||||
@@ -41,6 +43,7 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "long-cache", sync = true)
|
||||||
public List<FinanceIndicator> findFinanceIndicatorRecent(Long stockId, int years) {
|
public List<FinanceIndicator> findFinanceIndicatorRecent(Long stockId, int years) {
|
||||||
var current = LocalDate.now();
|
var current = LocalDate.now();
|
||||||
return financeIndicatorRepository.findAll(
|
return financeIndicatorRepository.findAll(
|
||||||
@@ -50,6 +53,7 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "long-cache", sync = true)
|
||||||
public List<Daily> findDailyRecent(Long stockId, int days) {
|
public List<Daily> findDailyRecent(Long stockId, int days) {
|
||||||
var current = LocalDate.now();
|
var current = LocalDate.now();
|
||||||
return dailyRepository.findAll(
|
return dailyRepository.findAll(
|
||||||
@@ -58,4 +62,9 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
Sort.by(Sort.Direction.ASC, Daily_.TRADE_DATE)
|
Sort.by(Sort.Direction.ASC, Daily_.TRADE_DATE)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "long-cache", sync = true)
|
||||||
|
public Optional<Daily> findDailyLatest(Long stockId) {
|
||||||
|
return dailyRepository.findLatest(stockId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ public class TaskService extends SimpleServiceSupport<Task> {
|
|||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final Set<TaskTemplate> templates = Stream.of(
|
private final Set<TaskTemplate> templates = Stream.of(
|
||||||
new TaskTemplate("更新股票信息", "更新股票信息", UpdateStockTask.class),
|
new TaskTemplate("b29f76a5-b07d-4182-85f8-2641c2a975c1", "更新股票信息", "更新股票信息", UpdateStockTask.class),
|
||||||
new TaskTemplate("更新年线指标", "更新年线指标", UpdateYearlyTask.class),
|
new TaskTemplate("e42dde60-5584-4c27-b3f7-72e4a4ff662d", "更新年线数据", "更新年线数据", UpdateYearlyTask.class),
|
||||||
new TaskTemplate("更新日线数据", "更新日线数据", UpdateDailyTask.class),
|
new TaskTemplate("b9df25ce-aa55-4f73-8265-d8a724614177", "更新日线数据", "更新日线数据", UpdateDailyTask.class),
|
||||||
new TaskTemplate("更新财务指标", "更新财务指标", UpdateFinanceIndicatorTask.class),
|
new TaskTemplate("8ab30478-c81f-4bbf-94dd-7e05fa537b50", "更新财务指标", "更新财务指标", UpdateFinanceIndicatorTask.class),
|
||||||
new TaskTemplate("金字塔选股", "金字塔选股", PyramidSelect.class)
|
new TaskTemplate("a6a7b569-a171-481b-9184-716925571639", "金字塔选股", "金字塔选股", PyramidSelect.class)
|
||||||
).collect(Collectors.toSet());
|
).collect(Collectors.toSet());
|
||||||
private final Map<String, TaskTemplate> templateMap = templates.stream()
|
private final Map<String, TaskTemplate> templateMap = templates.stream()
|
||||||
.collect(Collectors.toMap(TaskTemplate::id, template -> template));
|
.collect(Collectors.toMap(TaskTemplate::id, template -> template));
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import cn.hutool.core.util.ArrayUtil;
|
|||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
|
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.QStock;
|
import com.lanyuanxiaoyao.leopard.core.entity.QStock;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
import com.lanyuanxiaoyao.leopard.core.helper.NumberHelper;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -34,185 +36,202 @@ public class PyramidStockSelector implements StockSelector<PyramidStockSelector.
|
|||||||
public Set<Candidate> select(Request request) {
|
public Set<Candidate> select(Request request) {
|
||||||
// 选择至少有最近5年财报的股票
|
// 选择至少有最近5年财报的股票
|
||||||
// 有点奇怪,001400.SZ有近5年的财报但资料显示是2025年才上市的
|
// 有点奇怪,001400.SZ有近5年的财报但资料显示是2025年才上市的
|
||||||
var stocks = stockRepository.findAll(QStock.stock.listedDate.before(LocalDate.of(request.year(), 1, 1)));
|
return stockRepository.findAll(QStock.stock.listedDate.before(LocalDate.of(request.year(), 1, 1)))
|
||||||
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()
|
.stream()
|
||||||
.sorted((e1, e2) -> e2.getValue() - e1.getValue())
|
.map(stock -> {
|
||||||
.limit(request.limit())
|
var extra = new HashMap<String, String>();
|
||||||
.map(entry -> new Candidate(entry.getKey(), entry.getValue()))
|
var score = 0;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
extra.put("平均ROE", NumberHelper.formatPriceDouble(averageRoe));
|
||||||
|
}
|
||||||
|
extra.put("平均ROE得分", NumberHelper.formatPriceDouble(roeScore));
|
||||||
|
score += 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;
|
||||||
|
}
|
||||||
|
extra.put("平均ROA", NumberHelper.formatPriceDouble(averageRoa));
|
||||||
|
}
|
||||||
|
extra.put("平均ROA得分", NumberHelper.formatPriceDouble(roaScore));
|
||||||
|
score += 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;
|
||||||
|
}
|
||||||
|
extra.put("平均净利润", NumberHelper.formatPriceDouble(averageNetProfit));
|
||||||
|
}
|
||||||
|
extra.put("平均净利润得分", NumberHelper.formatPriceDouble(netProfitScore));
|
||||||
|
score += 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;
|
||||||
|
}
|
||||||
|
extra.put("现金流得分", NumberHelper.formatPriceDouble(cashScore));
|
||||||
|
score += cashScore;
|
||||||
|
|
||||||
|
if (ObjectUtil.isNotNull(latestIndicator.getDaysAccountsReceivableTurnover()) && latestIndicator.getDaysAccountsReceivableTurnover() <= 30) {
|
||||||
|
extra.put("应收账款周转天数得分", "20");
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
|
if (ObjectUtil.isNotNull(latestIndicator.getDaysInventoryTurnover()) && latestIndicator.getDaysInventoryTurnover() <= 30) {
|
||||||
|
extra.put("存货周转天数得分", "20");
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
|
if (ArrayUtil.isAllNotNull(latestIndicator.getDaysAccountsReceivableTurnover(), latestIndicator.getDaysInventoryTurnover())) {
|
||||||
|
if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 40) {
|
||||||
|
score += 20;
|
||||||
|
} else if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 60) {
|
||||||
|
score += 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) {
|
||||||
|
extra.put("毛利率标准差得分", "50");
|
||||||
|
score += 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;
|
||||||
|
}
|
||||||
|
extra.put("安全边际比率", NumberHelper.formatPriceDouble(latestIndicator.getOperatingSafetyMarginRatio()));
|
||||||
|
}
|
||||||
|
extra.put("安全边际比率得分", NumberHelper.formatPriceDouble(operatingSafeMarginScore));
|
||||||
|
score += 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extra.put("近五年净利润得分", NumberHelper.formatPriceDouble(netProfitAscendingScore));
|
||||||
|
score += 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extra.put("近五年现金流得分", NumberHelper.formatPriceDouble(cashAscendingScore));
|
||||||
|
score += cashAscendingScore;
|
||||||
|
|
||||||
|
return new Candidate(stock, score, extra);
|
||||||
|
})
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.sorted(Comparator.comparingDouble(Candidate::score).reversed())
|
||||||
|
.limit(request.limit)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import java.util.Set;
|
|||||||
public interface StockSelector<T> {
|
public interface StockSelector<T> {
|
||||||
Set<Candidate> select(T request);
|
Set<Candidate> select(T request);
|
||||||
|
|
||||||
record Candidate(Stock stock, double score, Map<String, Object> extra) {
|
record Candidate(Stock stock, double score, Map<String, String> extra) {
|
||||||
public Candidate(Stock stock, double score) {
|
public Candidate(Stock stock, double score) {
|
||||||
this(stock, score, Map.of());
|
this(stock, score, Map.of());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public class PyramidSelect extends TaskRunner {
|
|||||||
var score = new StockScore();
|
var score = new StockScore();
|
||||||
score.setStock(candidate.stock());
|
score.setStock(candidate.stock());
|
||||||
score.setScore(candidate.score());
|
score.setScore(candidate.score());
|
||||||
|
score.setExtra(candidate.extra());
|
||||||
score.setCollection(collection);
|
score.setCollection(collection);
|
||||||
return score;
|
return score;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -40,31 +40,39 @@ public class UpdateYearlyTask extends TaskRunner {
|
|||||||
var endYear = dailyRepository.findMaxTradeDate().getYear();
|
var endYear = dailyRepository.findMaxTradeDate().getYear();
|
||||||
var stocks = stockRepository.findAll();
|
var stocks = stockRepository.findAll();
|
||||||
for (int year = startYear, index = 0; year <= endYear; year++, index++) {
|
for (int year = startYear, index = 0; year <= endYear; year++, index++) {
|
||||||
for (var stock : stocks) {
|
int currentYear = year;
|
||||||
log.info("Processing {} {}", stock.getCode(), year);
|
stocks.parallelStream()
|
||||||
if (stock.getListedDate().getYear() > year) {
|
.forEach(stock -> {
|
||||||
continue;
|
if (stock.getListedDate().getYear() > currentYear) {
|
||||||
}
|
return;
|
||||||
var dailies = dailyRepository.findAll(
|
}
|
||||||
QDaily.daily.tradeDate.year().eq(year)
|
var yearlyOptional = yearlyRepository.findOne(
|
||||||
.and(QDaily.daily.stock.eq(stock))
|
QYearly.yearly.stock.eq(stock)
|
||||||
);
|
.and(QYearly.yearly.year.eq(currentYear))
|
||||||
var yearly = yearlyRepository.findOne(
|
);
|
||||||
QYearly.yearly.stock.eq(stock)
|
if (yearlyOptional.isPresent() && currentYear != endYear) {
|
||||||
.and(QYearly.yearly.year.eq(year))
|
return;
|
||||||
).orElseGet(Yearly::new);
|
}
|
||||||
yearly.setStock(stock);
|
var dailies = dailyRepository.findAll(
|
||||||
yearly.setYear(year);
|
QDaily.daily.tradeDate.year().eq(currentYear)
|
||||||
yearly.setClose(dailies.getLast().getHfqClose());
|
.and(QDaily.daily.stock.eq(stock))
|
||||||
yearly.setOpen(dailies.getFirst().getHfqOpen());
|
);
|
||||||
yearly.setHigh(dailies.stream().map(Daily::getHfqHigh).max(Double::compareTo).orElse(0.0));
|
if (dailies.isEmpty()) {
|
||||||
yearly.setLow(dailies.stream().map(Daily::getHfqLow).min(Double::compareTo).orElse(0.0));
|
return;
|
||||||
yearly.setVolume(dailies.stream().mapToDouble(Daily::getVolume).sum());
|
}
|
||||||
yearly.setTurnover(dailies.stream().mapToDouble(Daily::getTurnover).sum());
|
var yearly = yearlyOptional.orElseGet(Yearly::new);
|
||||||
yearly.setPriceChangeAmount(yearly.getClose() - yearly.getOpen());
|
yearly.setStock(stock);
|
||||||
yearly.setPriceFluctuationRange((yearly.getClose() - yearly.getOpen()) / yearly.getOpen());
|
yearly.setYear(currentYear);
|
||||||
yearlyRepository.save(yearly);
|
yearly.setClose(dailies.getLast().getHfqClose());
|
||||||
}
|
yearly.setOpen(dailies.getFirst().getHfqOpen());
|
||||||
|
yearly.setHigh(dailies.stream().map(Daily::getHfqHigh).max(Double::compareTo).orElse(0.0));
|
||||||
|
yearly.setLow(dailies.stream().map(Daily::getHfqLow).min(Double::compareTo).orElse(0.0));
|
||||||
|
yearly.setVolume(dailies.stream().mapToDouble(Daily::getVolume).sum());
|
||||||
|
yearly.setTurnover(dailies.stream().mapToDouble(Daily::getTurnover).sum());
|
||||||
|
yearly.setPriceChangeAmount(yearly.getClose() - yearly.getOpen());
|
||||||
|
yearly.setPriceFluctuationRange((yearly.getClose() - yearly.getOpen()) / yearly.getOpen());
|
||||||
|
yearlyRepository.save(yearly);
|
||||||
|
});
|
||||||
updater.update((year - startYear) * 1.0 / (endYear - startYear + 1));
|
updater.update((year - startYear) * 1.0 / (endYear - startYear + 1));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -31,6 +31,14 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-jetty</artifactId>
|
<artifactId>spring-boot-starter-jetty</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-cache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.server.configuration;
|
||||||
|
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存提供
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @date 2023-04-23
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableCaching
|
||||||
|
public class CacheProvider {
|
||||||
|
@Primary
|
||||||
|
@Bean("short-cache")
|
||||||
|
public CacheManager normalCache() {
|
||||||
|
CaffeineCacheManager manager = new CaffeineCacheManager();
|
||||||
|
manager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES));
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("long-cache")
|
||||||
|
public CacheManager longCache() {
|
||||||
|
CaffeineCacheManager manager = new CaffeineCacheManager();
|
||||||
|
manager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.HOURS));
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,10 @@ package com.lanyuanxiaoyao.leopard.server.controller;
|
|||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
|
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
|
||||||
import com.lanyuanxiaoyao.leopard.core.service.StockCollectionService;
|
import com.lanyuanxiaoyao.leopard.core.service.StockCollectionService;
|
||||||
import com.lanyuanxiaoyao.leopard.core.service.StockService;
|
|
||||||
import com.lanyuanxiaoyao.leopard.server.entity.StockScoreVo;
|
import com.lanyuanxiaoyao.leopard.server.entity.StockScoreVo;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -15,11 +15,8 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("stock_collection")
|
@RequestMapping("stock_collection")
|
||||||
public class StockCollectionController extends SimpleControllerSupport<StockCollection, Void, StockCollectionController.ListItem, StockCollectionController.DetailItem> {
|
public class StockCollectionController extends SimpleControllerSupport<StockCollection, Void, StockCollectionController.ListItem, StockCollectionController.DetailItem> {
|
||||||
private final StockService stockService;
|
public StockCollectionController(StockCollectionService service) {
|
||||||
|
|
||||||
public StockCollectionController(StockCollectionService service, StockService stockService) {
|
|
||||||
super(service);
|
super(service);
|
||||||
this.stockService = stockService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -38,7 +35,9 @@ public class StockCollectionController extends SimpleControllerSupport<StockColl
|
|||||||
collection.getId(),
|
collection.getId(),
|
||||||
collection.getName(),
|
collection.getName(),
|
||||||
collection.getDescription(),
|
collection.getDescription(),
|
||||||
collection.getScores().size()
|
collection.getScores().size(),
|
||||||
|
collection.getCreatedTime(),
|
||||||
|
collection.getModifiedTime()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +52,9 @@ public class StockCollectionController extends SimpleControllerSupport<StockColl
|
|||||||
.stream()
|
.stream()
|
||||||
.map(StockScoreVo::of)
|
.map(StockScoreVo::of)
|
||||||
.sorted(Comparator.comparing(StockScoreVo::score).reversed())
|
.sorted(Comparator.comparing(StockScoreVo::score).reversed())
|
||||||
.toList()
|
.toList(),
|
||||||
|
collection.getCreatedTime(),
|
||||||
|
collection.getModifiedTime()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +62,9 @@ public class StockCollectionController extends SimpleControllerSupport<StockColl
|
|||||||
Long id,
|
Long id,
|
||||||
String name,
|
String name,
|
||||||
String description,
|
String description,
|
||||||
Integer count
|
Integer count,
|
||||||
|
LocalDateTime createdTime,
|
||||||
|
LocalDateTime modifiedTime
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +73,9 @@ public class StockCollectionController extends SimpleControllerSupport<StockColl
|
|||||||
String name,
|
String name,
|
||||||
String description,
|
String description,
|
||||||
Integer count,
|
Integer count,
|
||||||
List<StockScoreVo> scores
|
List<StockScoreVo> scores,
|
||||||
|
LocalDateTime createdTime,
|
||||||
|
LocalDateTime modifiedTime
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.controller;
|
package com.lanyuanxiaoyao.leopard.server.controller;
|
||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
import com.lanyuanxiaoyao.leopard.core.helper.NumberHelper;
|
import com.lanyuanxiaoyao.leopard.core.helper.NumberHelper;
|
||||||
import com.lanyuanxiaoyao.leopard.core.service.StockService;
|
import com.lanyuanxiaoyao.leopard.core.service.StockService;
|
||||||
@@ -108,6 +109,18 @@ public class StockController extends SimpleControllerSupport<Stock, Void, StockD
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("daily/current/{id}")
|
||||||
|
public GlobalResponse<Map<String, Object>> dailyCurrent(@PathVariable("id") Long id) {
|
||||||
|
var daily = stockService.findDailyLatest(id);
|
||||||
|
return GlobalResponse.responseMapData(Map.of(
|
||||||
|
"date", daily.map(Daily::getTradeDate).map(LocalDate::toString).orElse("/"),
|
||||||
|
"open", daily.map(Daily::getOpen).map(NumberHelper::formatPriceDouble).orElse(NumberHelper.FINANCE_NULL_DOUBLE),
|
||||||
|
"close", daily.map(Daily::getClose).map(NumberHelper::formatPriceDouble).orElse(NumberHelper.FINANCE_NULL_DOUBLE),
|
||||||
|
"low", daily.map(Daily::getLow).map(NumberHelper::formatPriceDouble).orElse(NumberHelper.FINANCE_NULL_DOUBLE),
|
||||||
|
"high", daily.map(Daily::getHigh).map(NumberHelper::formatPriceDouble).orElse(NumberHelper.FINANCE_NULL_DOUBLE)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("daily/{id}")
|
@GetMapping("daily/{id}")
|
||||||
public GlobalResponse<Map<String, Object>> dailyCharts(@PathVariable("id") Long id) {
|
public GlobalResponse<Map<String, Object>> dailyCharts(@PathVariable("id") Long id) {
|
||||||
var data = stockService.findDailyRecent(id, 100);
|
var data = stockService.findDailyRecent(id, 100);
|
||||||
@@ -115,7 +128,7 @@ public class StockController extends SimpleControllerSupport<Stock, Void, StockD
|
|||||||
var yList = new ArrayList<List<Double>>();
|
var yList = new ArrayList<List<Double>>();
|
||||||
for (var daily : data) {
|
for (var daily : data) {
|
||||||
xList.add(daily.getTradeDate().toString());
|
xList.add(daily.getTradeDate().toString());
|
||||||
yList.add(List.of(daily.getOpen(), daily.getClose(), daily.getLow(), daily.getHigh()));
|
yList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh()));
|
||||||
}
|
}
|
||||||
return GlobalResponse.responseMapData(Map.of(
|
return GlobalResponse.responseMapData(Map.of(
|
||||||
"xList", xList, "yList", yList
|
"xList", xList, "yList", yList
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.lanyuanxiaoyao.leopard.server.entity;
|
|||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.StockScore;
|
import com.lanyuanxiaoyao.leopard.core.entity.StockScore;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author lanyuanxiaoyao
|
* @author lanyuanxiaoyao
|
||||||
@@ -16,7 +18,8 @@ public record StockScoreVo(
|
|||||||
Stock.Market market,
|
Stock.Market market,
|
||||||
String industry,
|
String industry,
|
||||||
LocalDate listedDate,
|
LocalDate listedDate,
|
||||||
Double score
|
Double score,
|
||||||
|
String extra
|
||||||
) {
|
) {
|
||||||
public static StockScoreVo of(StockScore score) {
|
public static StockScoreVo of(StockScore score) {
|
||||||
return new StockScoreVo(
|
return new StockScoreVo(
|
||||||
@@ -27,7 +30,13 @@ public record StockScoreVo(
|
|||||||
score.getStock().getMarket(),
|
score.getStock().getMarket(),
|
||||||
score.getStock().getIndustry(),
|
score.getStock().getIndustry(),
|
||||||
score.getStock().getListedDate(),
|
score.getStock().getListedDate(),
|
||||||
score.getScore()
|
score.getScore(),
|
||||||
|
score.getExtra()
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.sorted(Map.Entry.comparingByKey())
|
||||||
|
.map(entry -> entry.getKey() + ": " + entry.getValue())
|
||||||
|
.collect(Collectors.joining("<br>"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import Overview from './pages/Overview.tsx'
|
|||||||
import Root from './pages/Root.tsx'
|
import Root from './pages/Root.tsx'
|
||||||
import Test from './pages/Test.tsx'
|
import Test from './pages/Test.tsx'
|
||||||
import StockList from './pages/stock/StockList.tsx'
|
import StockList from './pages/stock/StockList.tsx'
|
||||||
import StockDetail from './pages/stock/StockDetail.tsx'
|
|
||||||
import TaskList from './pages/task/TaskList.tsx'
|
import TaskList from './pages/task/TaskList.tsx'
|
||||||
import TaskTemplateList from './pages/task/TaskTemplateList.tsx'
|
import TaskTemplateList from './pages/task/TaskTemplateList.tsx'
|
||||||
import TaskScheduleList from './pages/task/TaskScheduleList.tsx'
|
import TaskScheduleList from './pages/task/TaskScheduleList.tsx'
|
||||||
@@ -35,10 +34,6 @@ const routes: RouteObject[] = [
|
|||||||
path: 'list',
|
path: 'list',
|
||||||
Component: StockList,
|
Component: StockList,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'detail/:id',
|
|
||||||
Component: StockDetail,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "collection",
|
path: "collection",
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -21,10 +21,21 @@ function StockCollectionDetail() {
|
|||||||
undefined,
|
undefined,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
className: 'white-space-pre',
|
||||||
name: 'score',
|
name: 'score',
|
||||||
label: '得分',
|
label: '得分',
|
||||||
width: 50,
|
width: 50,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
type: 'tpl',
|
||||||
|
tpl: '${score}',
|
||||||
|
popOver: {
|
||||||
|
trigger: 'click',
|
||||||
|
showIcon: false,
|
||||||
|
body: {
|
||||||
|
type: 'tpl',
|
||||||
|
tpl: '${extra|raw}',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate} from '../../util/amis.tsx'
|
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, time} from '../../util/amis.tsx'
|
||||||
import {useNavigate} from 'react-router'
|
import {useNavigate} from 'react-router'
|
||||||
|
|
||||||
function StockCollectionList() {
|
function StockCollectionList() {
|
||||||
@@ -14,8 +14,16 @@ function StockCollectionList() {
|
|||||||
{
|
{
|
||||||
type: 'crud',
|
type: 'crud',
|
||||||
api: {
|
api: {
|
||||||
method: 'get',
|
method: 'post',
|
||||||
url: `${commonInfo.baseUrl}/stock_collection/list`,
|
url: `${commonInfo.baseUrl}/stock_collection/list`,
|
||||||
|
data: {
|
||||||
|
sort: [
|
||||||
|
{
|
||||||
|
column: 'createdTime',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...crudCommonOptions(),
|
...crudCommonOptions(),
|
||||||
...paginationTemplate(15, undefined, ['filter-toggler']),
|
...paginationTemplate(15, undefined, ['filter-toggler']),
|
||||||
@@ -35,6 +43,20 @@ function StockCollectionList() {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'createdTime',
|
||||||
|
label: '创建时间',
|
||||||
|
width: 150,
|
||||||
|
align: 'center',
|
||||||
|
...time('createdTime'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'modifiedTime',
|
||||||
|
label: '更新时间',
|
||||||
|
width: 150,
|
||||||
|
align: 'center',
|
||||||
|
...time('modifiedTime'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'operation',
|
type: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
@@ -58,6 +80,16 @@ function StockCollectionList() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
className: 'text-danger btn-deleted',
|
||||||
|
type: 'action',
|
||||||
|
label: '删除',
|
||||||
|
level: 'link',
|
||||||
|
actionType: 'ajax',
|
||||||
|
api: `get:${commonInfo.baseUrl}/stock_collection/remove/\${id}`,
|
||||||
|
confirmText: '确认删除股票集<span class="text-lg font-bold mx-2">${name}</span>?',
|
||||||
|
confirmTitle: '删除',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,550 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {useParams} from 'react-router'
|
|
||||||
import {amisRender, commonInfo, readOnlyDialogOptions, remoteMappings} from '../../util/amis.tsx'
|
|
||||||
import type {Schema} from 'amis'
|
|
||||||
import {isNil} from 'es-toolkit'
|
|
||||||
import {toNumber} from 'es-toolkit/compat'
|
|
||||||
|
|
||||||
const formatFinanceNumber = (value: number): string => {
|
|
||||||
if (isNil(value)) {
|
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNegative = value < 0
|
|
||||||
const absoluteValue = Math.abs(value)
|
|
||||||
|
|
||||||
let formatted: string
|
|
||||||
if (absoluteValue >= 100000000) {
|
|
||||||
formatted = (absoluteValue / 100000000).toFixed(2) + '亿'
|
|
||||||
} else if (absoluteValue >= 10000) {
|
|
||||||
formatted = (absoluteValue / 10000).toFixed(2) + '万'
|
|
||||||
} else {
|
|
||||||
formatted = absoluteValue.toLocaleString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return isNegative ? `-${formatted}` : formatted
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDaysNumber = (value: number): string => {
|
|
||||||
if (isNil(value)) {
|
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
return `${value.toFixed(0)}天`
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatPercentageNumber = (value: number): string => {
|
|
||||||
if (isNil(value)) {
|
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
return `${(value * 100).toFixed(2)}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FinanceType = 'PERCENTAGE' | 'FINANCE' | 'DAYS'
|
|
||||||
|
|
||||||
const financePropertyLabel = (id: string | undefined, label: string, type: FinanceType, field: string): Schema => {
|
|
||||||
if (!id) {
|
|
||||||
return {
|
|
||||||
type: 'tpl',
|
|
||||||
tpl: label,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let formatter: (value: number) => string
|
|
||||||
switch (type) {
|
|
||||||
case 'PERCENTAGE':
|
|
||||||
formatter = formatPercentageNumber
|
|
||||||
break
|
|
||||||
case 'FINANCE':
|
|
||||||
formatter = formatFinanceNumber
|
|
||||||
break
|
|
||||||
case 'DAYS':
|
|
||||||
formatter = formatDaysNumber
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
formatter = (v: number) => v.toFixed(2)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'wrapper',
|
|
||||||
size: 'none',
|
|
||||||
body: [
|
|
||||||
{
|
|
||||||
className: 'text-current font-bold',
|
|
||||||
type: 'action',
|
|
||||||
label: label,
|
|
||||||
level: 'link',
|
|
||||||
tooltip: '这是什么?',
|
|
||||||
tooltipPlacement: 'top',
|
|
||||||
actionType: 'dialog',
|
|
||||||
dialog: {
|
|
||||||
title: '',
|
|
||||||
size: 'lg',
|
|
||||||
...readOnlyDialogOptions(),
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
type: 'action',
|
|
||||||
label: '新页面打开',
|
|
||||||
icon: 'fa fa-solid fa-arrow-up-right-from-square',
|
|
||||||
actionType: 'url',
|
|
||||||
url: `https://zh.wikipedia.org/wiki/${label}`,
|
|
||||||
blank: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
body: {
|
|
||||||
type: 'iframe',
|
|
||||||
src: `https://zh.wikipedia.org/wiki/${label}`,
|
|
||||||
height: 800,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
className: 'text-secondary',
|
|
||||||
type: 'action',
|
|
||||||
label: '',
|
|
||||||
icon: 'fa fa-eye',
|
|
||||||
level: 'link',
|
|
||||||
size: 'xs',
|
|
||||||
tooltip: '查看五年趋势',
|
|
||||||
tooltipPlacement: 'top',
|
|
||||||
actionType: 'dialog',
|
|
||||||
dialog: {
|
|
||||||
title: `${label}五年趋势`,
|
|
||||||
size: 'lg',
|
|
||||||
bodyClassName: 'p-0',
|
|
||||||
...readOnlyDialogOptions(),
|
|
||||||
body: {
|
|
||||||
type: 'chart',
|
|
||||||
api: `get:${commonInfo.baseUrl}/stock/finance/${id}/${field}`,
|
|
||||||
height: 500,
|
|
||||||
config: {
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
||||||
borderColor: '#ccc',
|
|
||||||
borderWidth: 1,
|
|
||||||
textStyle: {
|
|
||||||
color: '#333',
|
|
||||||
},
|
|
||||||
padding: [10, 15],
|
|
||||||
formatter: (params: any) => {
|
|
||||||
const item = params[0]
|
|
||||||
return `${item.name}<br/>${item.marker}${formatter(item.value)}`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '5%',
|
|
||||||
right: '5%',
|
|
||||||
top: '10%',
|
|
||||||
bottom: '15%',
|
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: '${xList || []}',
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: '#e0e0e0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: '#666',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
show: true,
|
|
||||||
splitLine: {
|
|
||||||
lineStyle: {
|
|
||||||
type: 'dashed',
|
|
||||||
color: '#f0f0f0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: '#999',
|
|
||||||
fontSize: 12,
|
|
||||||
formatter: (value: number) => {
|
|
||||||
return formatter(value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
data: '${yList || []}',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
showSymbol: true,
|
|
||||||
symbolSize: 6,
|
|
||||||
lineStyle: {
|
|
||||||
width: 3,
|
|
||||||
color: '#4096ff',
|
|
||||||
shadowColor: 'rgba(64, 150, 255, 0.3)',
|
|
||||||
shadowBlur: 5,
|
|
||||||
shadowOffsetY: 2,
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
color: '#4096ff',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: '#fff',
|
|
||||||
},
|
|
||||||
areaStyle: {
|
|
||||||
color: {
|
|
||||||
type: 'linear',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
x2: 0,
|
|
||||||
y2: 1,
|
|
||||||
colorStops: [{
|
|
||||||
offset: 0, color: 'rgba(64, 150, 255, 0.2)',
|
|
||||||
}, {
|
|
||||||
offset: 1, color: 'rgba(64, 150, 255, 0.01)',
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
position: 'top',
|
|
||||||
color: '#333',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 12,
|
|
||||||
formatter: (params: any) => {
|
|
||||||
return formatter(params.value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function StockDetail() {
|
|
||||||
const {id} = useParams()
|
|
||||||
return (
|
|
||||||
<div className="stock-detail">
|
|
||||||
{amisRender(
|
|
||||||
{
|
|
||||||
type: 'page',
|
|
||||||
title: '股票详情(${code} ${name})',
|
|
||||||
initApi: `get:${commonInfo.baseUrl}/stock/detail/${id}`,
|
|
||||||
body: [
|
|
||||||
{
|
|
||||||
type: 'property',
|
|
||||||
items: [
|
|
||||||
{label: '编码', content: '${code}'},
|
|
||||||
{label: '名称', content: '${name}'},
|
|
||||||
{label: '全名', content: '${fullname}'},
|
|
||||||
{
|
|
||||||
label: '市场',
|
|
||||||
content: {
|
|
||||||
...remoteMappings('stock_market', 'market'),
|
|
||||||
value: '${market}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{label: '行业', content: '${industry}'},
|
|
||||||
{label: '上市日期', content: '${listedDate}'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{type: 'divider'},
|
|
||||||
{
|
|
||||||
type: 'service',
|
|
||||||
api: `get:${commonInfo.baseUrl}/stock/finance/${id}`,
|
|
||||||
body: [
|
|
||||||
'资产负债表',
|
|
||||||
{
|
|
||||||
className: 'my-2',
|
|
||||||
type: 'property',
|
|
||||||
column: 4,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '总资产', 'FINANCE', 'totalAssets'),
|
|
||||||
content: '${balanceSheet.totalAssets}',
|
|
||||||
span: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '总负债', 'FINANCE', 'totalLiabilities'),
|
|
||||||
content: '${balanceSheet.totalLiabilities}',
|
|
||||||
span: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '流动资产', 'FINANCE', 'currentAssets'),
|
|
||||||
content: '${balanceSheet.currentAssets}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '流动资产占比', 'PERCENTAGE', 'currentAssetsToTotalAssetsRatio'),
|
|
||||||
content: '${balanceSheet.currentAssetsRatio}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '流动负债', 'FINANCE', 'currentLiabilities'),
|
|
||||||
content: '${balanceSheet.currentLiabilities}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '流动负债占比', 'PERCENTAGE', 'currentLiabilitiesToTotalAssetsRatio'),
|
|
||||||
content: '${balanceSheet.currentLiabilitiesRatio}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '非流动资产', 'FINANCE', 'fixedAssets'),
|
|
||||||
content: '${balanceSheet.fixedAssets}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '非流动资产占比', 'PERCENTAGE', 'fixedAssetsToTotalAssetsRatio'),
|
|
||||||
content: '${balanceSheet.fixedAssetsRatio}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '非流动负债', 'FINANCE', 'longTermLiabilities'),
|
|
||||||
content: '${balanceSheet.longTermLiabilities}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '非流动负债占比', 'PERCENTAGE', 'longTermLiabilitiesToTotalAssetsRatio'),
|
|
||||||
content: '${balanceSheet.longTermLiabilitiesRatio}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'利润表',
|
|
||||||
{
|
|
||||||
className: 'my-2',
|
|
||||||
type: 'property',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '营业收入', 'FINANCE', 'operatingRevenue'),
|
|
||||||
content: '${income.operatingRevenue}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '营业成本', 'FINANCE', 'operatingCost'),
|
|
||||||
content: '${income.operatingCost}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '营业利润', 'FINANCE', 'operatingProfit'),
|
|
||||||
content: '${income.operatingProfit}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'现金流量表',
|
|
||||||
{
|
|
||||||
className: 'my-2',
|
|
||||||
type: 'property',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '净利润', 'FINANCE', 'netProfit'),
|
|
||||||
content: '${cashFlow.netProfit}',
|
|
||||||
span: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '营业活动现金流量', 'FINANCE', 'cashFlowFromOperatingActivities'),
|
|
||||||
content: '${cashFlow.cashFlowFromOperatingActivities}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '投资活动现金流量', 'FINANCE', 'cashFlowFromInvestingActivities'),
|
|
||||||
content: '${cashFlow.cashFlowFromInvestingActivities}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '筹资活动现金流量', 'FINANCE', 'cashFlowFromFinancingActivities'),
|
|
||||||
content: '${cashFlow.cashFlowFromFinancingActivities}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'财务指标',
|
|
||||||
{
|
|
||||||
className: 'my-2',
|
|
||||||
type: 'property',
|
|
||||||
column: 4,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '流动比率', 'FINANCE', 'currentRatio'),
|
|
||||||
content: '${indicate.currentRatio}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '速动比率', 'FINANCE', 'quickRatio'),
|
|
||||||
content: '${indicate.quickRatio}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, 'ROE', 'FINANCE', 'returnOnEquity'),
|
|
||||||
content: '${indicate.roe}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, 'ROA', 'FINANCE', 'returnOnAssets'),
|
|
||||||
content: '${indicate.roa}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '应收账款周转率', 'FINANCE', 'accountsReceivableTurnover'),
|
|
||||||
content: '${indicate.accountsReceivableTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '应收账款周转天数', 'DAYS', 'daysAccountsReceivableTurnover'),
|
|
||||||
content: '${indicate.daysAccountsReceivableTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '存货周转率', 'FINANCE', 'inventoryTurnover'),
|
|
||||||
content: '${indicate.inventoryTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '存货周转天数', 'DAYS', 'daysInventoryTurnover'),
|
|
||||||
content: '${indicate.daysInventoryTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '固定资产周转率', 'FINANCE', 'fixedAssetsTurnover'),
|
|
||||||
content: '${indicate.fixedAssetsTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '固定资产周转天数', 'DAYS', 'daysFixedAssetsTurnover'),
|
|
||||||
content: '${indicate.daysFixedAssetsTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '总资产周转率', 'FINANCE', 'totalAssetsTurnover'),
|
|
||||||
content: '${indicate.totalAssetsTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '总资产周转天数', 'DAYS', 'daysTotalAssetsTurnover'),
|
|
||||||
content: '${indicate.daysTotalAssetsTurnover}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{type: 'divider'},
|
|
||||||
"100日线数据",
|
|
||||||
{
|
|
||||||
type: 'chart',
|
|
||||||
height: 500,
|
|
||||||
api: `get:${commonInfo.baseUrl}/stock/daily/${id}`,
|
|
||||||
config: {
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
animation: true,
|
|
||||||
animationDuration: 1000,
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: {
|
|
||||||
type: 'cross',
|
|
||||||
},
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
||||||
borderColor: '#333',
|
|
||||||
borderWidth: 1,
|
|
||||||
textStyle: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
padding: 12,
|
|
||||||
formatter: function (params: any) {
|
|
||||||
const param = params[0]
|
|
||||||
const open = toNumber(param.data[1]).toFixed(2)
|
|
||||||
const close = toNumber(param.data[2]).toFixed(2)
|
|
||||||
const lowest = toNumber(param.data[3]).toFixed(2)
|
|
||||||
const highest = toNumber(param.data[4]).toFixed(2)
|
|
||||||
|
|
||||||
return `<div class="text-center font-bold mb-2">${param.name}</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<span>开盘:</span>
|
|
||||||
<span class="font-bold ml-4">${open}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<span>收盘:</span>
|
|
||||||
<span class="font-bold ml-4">${close}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<span>最低:</span>
|
|
||||||
<span class="font-bold ml-4">${lowest}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<span>最高:</span>
|
|
||||||
<span class="font-bold ml-4">${highest}</span>
|
|
||||||
</div>`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '10%',
|
|
||||||
right: '10%',
|
|
||||||
top: '10%',
|
|
||||||
bottom: '15%',
|
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
data: '${xList || []}',
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: '#e0e0e0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: '#666',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
scale: true,
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: '#e0e0e0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: '#666',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
formatter: function (value: number) {
|
|
||||||
return value.toFixed(2)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
lineStyle: {
|
|
||||||
type: 'dashed',
|
|
||||||
color: '#f0f0f0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataZoom: [
|
|
||||||
{
|
|
||||||
type: 'inside',
|
|
||||||
start: 0,
|
|
||||||
end: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: true,
|
|
||||||
type: 'slider',
|
|
||||||
top: '90%',
|
|
||||||
start: 0,
|
|
||||||
end: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'candlestick',
|
|
||||||
data: '${yList || []}',
|
|
||||||
itemStyle: {
|
|
||||||
color: '#eb5454',
|
|
||||||
color0: '#4aaa93',
|
|
||||||
borderColor: '#eb5454',
|
|
||||||
borderColor0: '#4aaa93',
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"12月线数据",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(StockDetail)
|
|
||||||
@@ -571,7 +571,7 @@ export function stockListColumns(idField: string = 'id', extraColumns: Array<Col
|
|||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: '简称',
|
label: '简称',
|
||||||
width: 150,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'fullname',
|
name: 'fullname',
|
||||||
@@ -782,37 +782,61 @@ export function stockListColumns(idField: string = 'id', extraColumns: Array<Col
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{type: 'divider'},
|
],
|
||||||
"100日线数据",
|
},
|
||||||
|
{type: 'divider'},
|
||||||
|
{
|
||||||
|
type: 'service',
|
||||||
|
api: `get:${commonInfo.baseUrl}/stock/daily/current/\${${idField}}`,
|
||||||
|
body: [
|
||||||
|
"现价 (${date})",
|
||||||
{
|
{
|
||||||
type: 'chart',
|
className: 'my-2',
|
||||||
height: 500,
|
type: 'property',
|
||||||
api: `get:${commonInfo.baseUrl}/stock/daily/\${${idField}}`,
|
column: 4,
|
||||||
config: {
|
items: [
|
||||||
backgroundColor: '#fff',
|
{label: '开盘价', content: '${open}'},
|
||||||
animation: true,
|
{label: '收盘价', content: '${close}'},
|
||||||
animationDuration: 1000,
|
{label: '最高价', content: '${high}'},
|
||||||
tooltip: {
|
{label: '最低价', content: '${low}'},
|
||||||
trigger: 'axis',
|
],
|
||||||
axisPointer: {
|
},
|
||||||
type: 'cross',
|
],
|
||||||
},
|
},
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
{
|
||||||
borderColor: '#333',
|
type: 'chart',
|
||||||
borderWidth: 1,
|
title: '100日线数据',
|
||||||
textStyle: {
|
height: 500,
|
||||||
color: '#fff',
|
api: `get:${commonInfo.baseUrl}/stock/daily/\${${idField}}`,
|
||||||
fontSize: 12,
|
config: {
|
||||||
},
|
title: {
|
||||||
padding: 12,
|
text: '100日线数据',
|
||||||
formatter: function (params: any) {
|
subtext: '后复权数据',
|
||||||
const param = params[0]
|
},
|
||||||
const open = toNumber(param.data[1]).toFixed(2)
|
backgroundColor: '#fff',
|
||||||
const close = toNumber(param.data[2]).toFixed(2)
|
animation: true,
|
||||||
const lowest = toNumber(param.data[3]).toFixed(2)
|
animationDuration: 1000,
|
||||||
const highest = toNumber(param.data[4]).toFixed(2)
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
},
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
borderColor: '#333',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
padding: 12,
|
||||||
|
formatter: function (params: any) {
|
||||||
|
const param = params[0]
|
||||||
|
const open = toNumber(param.data[1]).toFixed(2)
|
||||||
|
const close = toNumber(param.data[2]).toFixed(2)
|
||||||
|
const lowest = toNumber(param.data[3]).toFixed(2)
|
||||||
|
const highest = toNumber(param.data[4]).toFixed(2)
|
||||||
|
|
||||||
return `<div class="text-center font-bold mb-2">${param.name}</div>
|
return `<div class="text-center font-bold mb-2">${param.name}</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<span>开盘:</span>
|
<span>开盘:</span>
|
||||||
<span class="font-bold ml-4">${open}</span>
|
<span class="font-bold ml-4">${open}</span>
|
||||||
@@ -829,89 +853,86 @@ export function stockListColumns(idField: string = 'id', extraColumns: Array<Col
|
|||||||
<span>最高:</span>
|
<span>最高:</span>
|
||||||
<span class="font-bold ml-4">${highest}</span>
|
<span class="font-bold ml-4">${highest}</span>
|
||||||
</div>`
|
</div>`
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '10%',
|
|
||||||
right: '10%',
|
|
||||||
top: '10%',
|
|
||||||
bottom: '15%',
|
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
data: '${xList || []}',
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: '#e0e0e0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: '#666',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
scale: true,
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: '#e0e0e0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: '#666',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
formatter: function (value: number) {
|
|
||||||
return value.toFixed(2)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
lineStyle: {
|
|
||||||
type: 'dashed',
|
|
||||||
color: '#f0f0f0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataZoom: [
|
|
||||||
{
|
|
||||||
type: 'inside',
|
|
||||||
start: 0,
|
|
||||||
end: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: true,
|
|
||||||
type: 'slider',
|
|
||||||
top: '90%',
|
|
||||||
start: 0,
|
|
||||||
end: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'candlestick',
|
|
||||||
data: '${yList || []}',
|
|
||||||
itemStyle: {
|
|
||||||
color: '#eb5454',
|
|
||||||
color0: '#4aaa93',
|
|
||||||
borderColor: '#eb5454',
|
|
||||||
borderColor0: '#4aaa93',
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"12月线数据",
|
grid: {
|
||||||
],
|
left: '2%',
|
||||||
|
right: '2%',
|
||||||
|
top: '15%',
|
||||||
|
bottom: '15%',
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
data: '${xList || []}',
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
scale: true,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
formatter: function (value: number) {
|
||||||
|
return value.toFixed(2)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
type: 'dashed',
|
||||||
|
color: '#f0f0f0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'inside',
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
type: 'slider',
|
||||||
|
top: '90%',
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'candlestick',
|
||||||
|
data: '${yList || []}',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#eb5454',
|
||||||
|
color0: '#4aaa93',
|
||||||
|
borderColor: '#eb5454',
|
||||||
|
borderColor0: '#4aaa93',
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user