diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/helper/TaHelper.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/helper/TaHelper.java new file mode 100644 index 0000000..bcad1dd --- /dev/null +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/helper/TaHelper.java @@ -0,0 +1,37 @@ +package com.lanyuanxiaoyao.leopard.core.helper; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import org.ta4j.core.Bar; +import org.ta4j.core.BaseBar; +import org.ta4j.core.BaseBarSeries; +import org.ta4j.core.indicators.SMAIndicator; +import org.ta4j.core.indicators.helpers.ClosePriceIndicator; + +public class TaHelper { + public static List sma(List data, int period, Function closeFunction) { + var series = new BaseBarSeries(); + for (int i = 0; i < data.size(); i++) { + var price = closeFunction.apply(data.get(i)); + Bar bar = new BaseBar( + Duration.ofDays(1), + ZonedDateTime.now().plusDays(i), + price, + price, + price, + price, + 0 + ); + series.addBar(bar); + } + var sma = new SMAIndicator(new ClosePriceIndicator(series), period); + var result = new ArrayList(series.getBarCount()); + for (int i = 0; i < series.getBarCount(); i++) { + result.add(sma.getValue(i).doubleValue()); + } + return result; + } +} \ No newline at end of file diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/DailyRepository.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/DailyRepository.java index d61a52f..0ead1e8 100644 --- a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/DailyRepository.java +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/repository/DailyRepository.java @@ -33,4 +33,8 @@ public interface DailyRepository extends SimpleRepository { @EntityGraph(attributePaths = {"stock"}) @Override List findAll(Predicate predicate, OrderSpecifier... orders); + + @EntityGraph(attributePaths = {"stock"}) + @Query("from Daily daily where daily.stock.id = ?1 and daily.tradeDate <= current date order by daily.tradeDate desc limit ?2") + List findRecent(Long stockId, int days); } diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/StockService.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/StockService.java index c560b59..1684d7c 100644 --- a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/StockService.java +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/service/StockService.java @@ -55,18 +55,16 @@ public class StockService extends SimpleServiceSupport { return financeIndicatorRepository.findAll( QFinanceIndicator.financeIndicator.stock.id.eq(stockId) .and(QFinanceIndicator.financeIndicator.year.between(current.minusYears(years).getYear(), current.getYear())), - QDaily.daily.tradeDate.asc() + QFinanceIndicator.financeIndicator.year.asc() ); } @Cacheable(value = "findDailyRecent", cacheManager = "long-cache", sync = true) public List findDailyRecent(Long stockId, int days) { - var current = LocalDate.now(); - return dailyRepository.findAll( - QDaily.daily.stock.id.eq(stockId) - .and(QDaily.daily.tradeDate.between(current.minusDays(days), current)), - QDaily.daily.tradeDate.asc() - ); + return dailyRepository.findRecent(stockId, days) + .stream() + .sorted(Comparator.comparing(Daily::getTradeDate)) + .toList(); } @Cacheable(value = "findDailyLatest", cacheManager = "long-cache", sync = true) diff --git a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/StockController.java b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/StockController.java index 4af47bd..2e8d73f 100644 --- a/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/StockController.java +++ b/leopard-server/src/main/java/com/lanyuanxiaoyao/leopard/server/controller/StockController.java @@ -4,6 +4,7 @@ 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.helper.NumberHelper; +import com.lanyuanxiaoyao.leopard.core.helper.TaHelper; import com.lanyuanxiaoyao.leopard.core.service.StockService; import com.lanyuanxiaoyao.leopard.server.entity.StockDetailVo; import com.lanyuanxiaoyao.service.template.controller.GlobalResponse; @@ -123,15 +124,20 @@ public class StockController extends SimpleControllerSupport> dailyCharts(@PathVariable("id") Long id) { - var data = stockService.findDailyRecent(id, 100); + var data = stockService.findDailyRecent(id, 100 + 60); + log.info("Size: {}", data.size()); var xList = new ArrayList(); var yList = new ArrayList>(); - for (var daily : data) { + for (var daily : data.subList(60, data.size() - 1)) { xList.add(daily.getTradeDate().toString()); yList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh())); } return GlobalResponse.responseMapData(Map.of( - "xList", xList, "yList", yList + "xList", xList, + "yList", yList, + "sma10", TaHelper.sma(data, 10, Daily::getHfqClose).subList(60, data.size() - 1), + "sma30", TaHelper.sma(data, 30, Daily::getHfqClose).subList(60, data.size() - 1), + "sma60", TaHelper.sma(data, 60, Daily::getHfqClose).subList(60, data.size() - 1) )); } diff --git a/leopard-web/src/util/amis.tsx b/leopard-web/src/util/amis.tsx index f67a33c..8e1a099 100644 --- a/leopard-web/src/util/amis.tsx +++ b/leopard-web/src/util/amis.tsx @@ -563,6 +563,7 @@ const financePropertyLabel = (idField: string, label: string, type: FinanceType, const candleChart = (title: string, subtitle: string, api: Api): Schema => { return { + className: 'mt-2', type: 'chart', height: 500, api: api, @@ -638,30 +639,47 @@ const candleChart = (title: string, subtitle: string, api: Api): Schema => { show: false, }, }, - yAxis: { - scale: true, - axisLine: { - lineStyle: { - color: '#e0e0e0', + 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, }, }, - axisLabel: { - color: '#666', - fontWeight: 'bold', - formatter: function (value: number) { - return value.toFixed(2) + { + scale: true, + axisLine: { + show: false }, - }, - splitLine: { - lineStyle: { - type: 'dashed', - color: '#f0f0f0', + axisTick: { + show: false }, + axisLabel: { + show: false + }, + splitLine: { + show: false + } }, - axisTick: { - show: false, - }, - }, + ], dataZoom: [ { type: 'inside', @@ -680,6 +698,7 @@ const candleChart = (title: string, subtitle: string, api: Api): Schema => { { type: 'candlestick', data: '${yList || []}', + yAxisIndex: 0, itemStyle: { color: '#eb5454', color0: '#4aaa93', @@ -687,7 +706,36 @@ const candleChart = (title: string, subtitle: string, api: Api): Schema => { borderColor0: '#4aaa93', borderWidth: 1, }, - + }, + { + type: 'line', + yAxisIndex: 0, + data: '${sma10 || []}', + smooth: true, + symbol: 'none', + lineStyle: { + color: 'rgba(25,147,51,0.5)', + }, + }, + { + type: 'line', + yAxisIndex: 0, + data: '${sma30 || []}', + smooth: true, + symbol: 'none', + lineStyle: { + color: 'rgba(10,94,131,0.5)', + }, + }, + { + type: 'line', + yAxisIndex: 0, + data: '${sma60 || []}', + smooth: true, + symbol: 'none', + lineStyle: { + color: 'rgba(231,15,130,0.5)', + }, }, ], },