1
0

feat: 增加日线的均线显示

This commit is contained in:
2025-10-15 23:14:50 +08:00
parent 452d1c681d
commit 47f8b30a02
5 changed files with 123 additions and 30 deletions

View File

@@ -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 <T> List<Double> sma(List<T> data, int period, Function<T, Double> 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<Double>(series.getBarCount());
for (int i = 0; i < series.getBarCount(); i++) {
result.add(sma.getValue(i).doubleValue());
}
return result;
}
}

View File

@@ -33,4 +33,8 @@ public interface DailyRepository extends SimpleRepository<Daily> {
@EntityGraph(attributePaths = {"stock"}) @EntityGraph(attributePaths = {"stock"})
@Override @Override
List<Daily> findAll(Predicate predicate, OrderSpecifier<?>... orders); List<Daily> 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<Daily> findRecent(Long stockId, int days);
} }

View File

@@ -55,18 +55,16 @@ public class StockService extends SimpleServiceSupport<Stock> {
return financeIndicatorRepository.findAll( return financeIndicatorRepository.findAll(
QFinanceIndicator.financeIndicator.stock.id.eq(stockId) QFinanceIndicator.financeIndicator.stock.id.eq(stockId)
.and(QFinanceIndicator.financeIndicator.year.between(current.minusYears(years).getYear(), current.getYear())), .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) @Cacheable(value = "findDailyRecent", cacheManager = "long-cache", sync = true)
public List<Daily> findDailyRecent(Long stockId, int days) { public List<Daily> findDailyRecent(Long stockId, int days) {
var current = LocalDate.now(); return dailyRepository.findRecent(stockId, days)
return dailyRepository.findAll( .stream()
QDaily.daily.stock.id.eq(stockId) .sorted(Comparator.comparing(Daily::getTradeDate))
.and(QDaily.daily.tradeDate.between(current.minusDays(days), current)), .toList();
QDaily.daily.tradeDate.asc()
);
} }
@Cacheable(value = "findDailyLatest", cacheManager = "long-cache", sync = true) @Cacheable(value = "findDailyLatest", cacheManager = "long-cache", sync = true)

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Daily; 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.helper.TaHelper;
import com.lanyuanxiaoyao.leopard.core.service.StockService; import com.lanyuanxiaoyao.leopard.core.service.StockService;
import com.lanyuanxiaoyao.leopard.server.entity.StockDetailVo; import com.lanyuanxiaoyao.leopard.server.entity.StockDetailVo;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse; import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
@@ -123,15 +124,20 @@ public class StockController extends SimpleControllerSupport<Stock, Void, StockD
@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 + 60);
log.info("Size: {}", data.size());
var xList = new ArrayList<String>(); var xList = new ArrayList<String>();
var yList = new ArrayList<List<Double>>(); var yList = new ArrayList<List<Double>>();
for (var daily : data) { for (var daily : data.subList(60, data.size() - 1)) {
xList.add(daily.getTradeDate().toString()); xList.add(daily.getTradeDate().toString());
yList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh())); 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,
"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)
)); ));
} }

View File

@@ -563,6 +563,7 @@ const financePropertyLabel = (idField: string, label: string, type: FinanceType,
const candleChart = (title: string, subtitle: string, api: Api): Schema => { const candleChart = (title: string, subtitle: string, api: Api): Schema => {
return { return {
className: 'mt-2',
type: 'chart', type: 'chart',
height: 500, height: 500,
api: api, api: api,
@@ -638,30 +639,47 @@ const candleChart = (title: string, subtitle: string, api: Api): Schema => {
show: false, show: false,
}, },
}, },
yAxis: { yAxis: [
scale: true, {
axisLine: { scale: true,
lineStyle: { axisLine: {
color: '#e0e0e0', 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', scale: true,
fontWeight: 'bold', axisLine: {
formatter: function (value: number) { show: false
return value.toFixed(2)
}, },
}, axisTick: {
splitLine: { show: false
lineStyle: {
type: 'dashed',
color: '#f0f0f0',
}, },
axisLabel: {
show: false
},
splitLine: {
show: false
}
}, },
axisTick: { ],
show: false,
},
},
dataZoom: [ dataZoom: [
{ {
type: 'inside', type: 'inside',
@@ -680,6 +698,7 @@ const candleChart = (title: string, subtitle: string, api: Api): Schema => {
{ {
type: 'candlestick', type: 'candlestick',
data: '${yList || []}', data: '${yList || []}',
yAxisIndex: 0,
itemStyle: { itemStyle: {
color: '#eb5454', color: '#eb5454',
color0: '#4aaa93', color0: '#4aaa93',
@@ -687,7 +706,36 @@ const candleChart = (title: string, subtitle: string, api: Api): Schema => {
borderColor0: '#4aaa93', borderColor0: '#4aaa93',
borderWidth: 1, 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)',
},
}, },
], ],
}, },