diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/dto/DailyDouble.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/dto/DailyDouble.java new file mode 100644 index 0000000..869c82f --- /dev/null +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/dto/DailyDouble.java @@ -0,0 +1,6 @@ +package com.lanyuanxiaoyao.leopard.core.entity.dto; + +import java.time.LocalDate; + +public record DailyDouble(LocalDate date, Double value) { +} diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/dto/YearAndMonth.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/dto/YearAndMonth.java new file mode 100644 index 0000000..2096e0d --- /dev/null +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/dto/YearAndMonth.java @@ -0,0 +1,4 @@ +package com.lanyuanxiaoyao.leopard.core.entity.dto; + +public record YearAndMonth(int year, int month) { +} diff --git a/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/dto/YearAndWeek.java b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/dto/YearAndWeek.java new file mode 100644 index 0000000..6b7284a --- /dev/null +++ b/leopard-core/src/main/java/com/lanyuanxiaoyao/leopard/core/entity/dto/YearAndWeek.java @@ -0,0 +1,4 @@ +package com.lanyuanxiaoyao.leopard.core.entity.dto; + +public record YearAndWeek(int year, int week) { +} 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 index bcad1dd..8b22ef7 100644 --- 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 @@ -1,5 +1,7 @@ package com.lanyuanxiaoyao.leopard.core.helper; +import cn.hutool.core.util.ObjectUtil; +import com.lanyuanxiaoyao.leopard.core.entity.Daily; import java.time.Duration; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -34,4 +36,30 @@ public class TaHelper { } return result; } + + public static Double maxFromDaily(List dailies, Function function) { + return dailies.stream() + .map(function) + .filter(ObjectUtil::isNotNull) + .mapToDouble(Double::doubleValue) + .max() + .orElse(0); + } + + public static Double minFromDaily(List dailies, Function function) { + return dailies.stream() + .map(function) + .filter(ObjectUtil::isNotNull) + .mapToDouble(Double::doubleValue) + .min() + .orElse(0); + } + + public static Double sumFromDaily(List dailies, Function function) { + return dailies.stream() + .map(function) + .filter(ObjectUtil::isNotNull) + .mapToDouble(Double::doubleValue) + .sum(); + } } \ No newline at end of file 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 1684d7c..d48526f 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 @@ -1,6 +1,5 @@ package com.lanyuanxiaoyao.leopard.core.service; -import cn.hutool.core.util.ObjectUtil; import com.lanyuanxiaoyao.leopard.core.entity.Daily; import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator; import com.lanyuanxiaoyao.leopard.core.entity.QDaily; @@ -8,6 +7,8 @@ import com.lanyuanxiaoyao.leopard.core.entity.QFinanceIndicator; import com.lanyuanxiaoyao.leopard.core.entity.Stock; import com.lanyuanxiaoyao.leopard.core.entity.dto.Monthly; import com.lanyuanxiaoyao.leopard.core.entity.dto.Weekly; +import com.lanyuanxiaoyao.leopard.core.entity.dto.YearAndMonth; +import com.lanyuanxiaoyao.leopard.core.entity.dto.YearAndWeek; import com.lanyuanxiaoyao.leopard.core.entity.dto.Yearly; import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; import com.lanyuanxiaoyao.leopard.core.repository.FinanceIndicatorRepository; @@ -19,12 +20,15 @@ import java.time.temporal.WeekFields; import java.util.Comparator; import java.util.List; import java.util.Optional; -import java.util.function.Function; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.maxFromDaily; +import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.minFromDaily; +import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.sumFromDaily; + /** * @author lanyuanxiaoyao * @version 20250828 @@ -143,9 +147,9 @@ public class StockService extends SimpleServiceSupport { var open = dailies.getFirst().getHfqOpen(); var close = dailies.getLast().getHfqClose(); return new Monthly( - LocalDate.of(yearAndMonth.year, yearAndMonth.month, 1), - yearAndMonth.year, - yearAndMonth.month, + LocalDate.of(yearAndMonth.year(), yearAndMonth.month(), 1), + yearAndMonth.year(), + yearAndMonth.month(), open, maxFromDaily(dailies, Daily::getHfqHigh), minFromDaily(dailies, Daily::getHfqLow), @@ -193,9 +197,9 @@ public class StockService extends SimpleServiceSupport { var open = dailies.getFirst().getHfqOpen(); var close = dailies.getLast().getHfqClose(); return new Weekly( - LocalDate.of(yearAndWeek.year, 1, 1).with(WeekFields.ISO.weekOfYear(), yearAndWeek.week), - yearAndWeek.year, - yearAndWeek.week, + LocalDate.of(yearAndWeek.year(), 1, 1).with(WeekFields.ISO.weekOfYear(), yearAndWeek.week()), + yearAndWeek.year(), + yearAndWeek.week(), open, maxFromDaily(dailies, Daily::getHfqHigh), minFromDaily(dailies, Daily::getHfqLow), @@ -209,36 +213,4 @@ public class StockService extends SimpleServiceSupport { .sorted(Comparator.comparingInt(weekly -> weekly.year() * 100 + weekly.week())) .toList(); } - - private Double maxFromDaily(List dailies, Function function) { - return dailies.stream() - .map(function) - .filter(ObjectUtil::isNotNull) - .mapToDouble(Double::doubleValue) - .max() - .orElse(0); - } - - private Double minFromDaily(List dailies, Function function) { - return dailies.stream() - .map(function) - .filter(ObjectUtil::isNotNull) - .mapToDouble(Double::doubleValue) - .min() - .orElse(0); - } - - private Double sumFromDaily(List dailies, Function function) { - return dailies.stream() - .map(function) - .filter(ObjectUtil::isNotNull) - .mapToDouble(Double::doubleValue) - .sum(); - } - - private record YearAndMonth(int year, int month) { - } - - private record YearAndWeek(int year, int week) { - } } diff --git a/leopard-server/src/main/resources/logback-spring.xml b/leopard-server/src/main/resources/logback-spring.xml index a857bbe..0f70cf1 100644 --- a/leopard-server/src/main/resources/logback-spring.xml +++ b/leopard-server/src/main/resources/logback-spring.xml @@ -28,7 +28,7 @@ - + diff --git a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java b/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java index 4037233..11ae7ae 100644 --- a/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java +++ b/leopard-strategy/src/main/java/com/lanyuanxiaoyao/leopard/strategy/StrategyApplication.java @@ -1,21 +1,34 @@ package com.lanyuanxiaoyao.leopard.strategy; import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.extra.template.TemplateConfig; import cn.hutool.extra.template.TemplateEngine; import cn.hutool.extra.template.TemplateUtil; 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.dto.Monthly; +import com.lanyuanxiaoyao.leopard.core.entity.dto.Weekly; +import com.lanyuanxiaoyao.leopard.core.entity.dto.YearAndMonth; +import com.lanyuanxiaoyao.leopard.core.entity.dto.YearAndWeek; import com.lanyuanxiaoyao.leopard.core.helper.TaHelper; import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository; import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository; +import com.lanyuanxiaoyao.leopard.core.repository.StockRepository; +import com.lanyuanxiaoyao.leopard.core.strategy.TradeEngine; import jakarta.annotation.Resource; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDate; +import java.time.temporal.WeekFields; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -24,12 +37,20 @@ import org.springframework.context.event.EventListener; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.transaction.annotation.Transactional; +import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.maxFromDaily; +import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.minFromDaily; +import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.sumFromDaily; + @Slf4j @SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard") @EnableJpaAuditing public class StrategyApplication { private static final TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + @Resource + private TradeEngine tradeEngine; + @Resource + private StockRepository stockRepository; @Resource private DailyRepository dailyRepository; @Resource @@ -41,38 +62,234 @@ public class StrategyApplication { @Transactional(readOnly = true) @EventListener(ApplicationReadyEvent.class) - public void test() throws IOException { - var charts = Dict.create(); - List.of("000048.SZ", "000333.SZ", "000568.SZ", "000596.SZ", "000651.SZ", "000848.SZ", "000858.SZ", "000933.SZ", "002027.SZ", "002032.SZ", "002142.SZ", "002192.SZ", "002415.SZ", "002432.SZ", "002475.SZ", "002517.SZ", "002555.SZ", "002648.SZ", "002756.SZ", "002847.SZ", "600036.SH", "600096.SH", "600132.SH", "600188.SH", "600309.SH", "600426.SH", "600436.SH", "600519.SH", "600546.SH", "600563.SH", "600702.SH", "600779.SH", "600803.SH", "600809.SH", "600961.SH", "601001.SH", "601100.SH", "601138.SH", "601225.SH", "601899.SH", "601919.SH", "603195.SH", "603198.SH", "603288.SH", "603369.SH", "603444.SH", "603565.SH", "603568.SH", "603605.SH", "603688.SH") + public void backtest() throws IOException { + var startDate = LocalDate.of(2024, 12, 1); + var endDate = LocalDate.of(2024, 12, 31); + var charts = new ArrayList(); + List.of("000048.SZ", "000333.SZ", "000568.SZ", "000596.SZ"/*, "000651.SZ", "000848.SZ", "000858.SZ", "000933.SZ", "002027.SZ", "002032.SZ", "002142.SZ", "002192.SZ", "002415.SZ", "002432.SZ", "002475.SZ", "002517.SZ", "002555.SZ", "002648.SZ", "002756.SZ", "002847.SZ", "600036.SH", "600096.SH", "600132.SH", "600188.SH", "600309.SH", "600426.SH", "600436.SH", "600519.SH", "600546.SH", "600563.SH", "600702.SH", "600779.SH", "600803.SH", "600809.SH", "600961.SH", "601001.SH", "601100.SH", "601138.SH", "601225.SH", "601899.SH", "601919.SH", "603195.SH", "603198.SH", "603288.SH", "603369.SH", "603444.SH", "603565.SH", "603568.SH", "603605.SH", "603688.SH"*/) .parallelStream() .forEach(code -> { - var dailies = dailyRepository.findAll( + var stock = stockRepository.findOne(QStock.stock.code.eq(code)).orElseThrow(); + var asset = tradeEngine.backtest( + List.of(stock.getId()), + (now, currentAsset, dailies) -> { + return dailies.entrySet() + .stream() + .map(entry -> { + var stockId = entry.getKey(); + var stockDailies = entry.getValue() + .stream() + .sorted(Comparator.comparing(Daily::getTradeDate)) + .toList(); + var yesterday = stockDailies.getLast(); + if (yesterday.getHfqClose() > yesterday.getHfqOpen()) { + log.info("{} Buy for price {} {}", now, yesterday.getHfqOpen(), yesterday.getHfqClose()); + return new TradeEngine.Trade(now, stockId, 100); + } else if (yesterday.getHfqClose() < yesterday.getHfqOpen()) { + var hold = currentAsset.getStocks().getOrDefault(stockId, 0); + if (hold > 0) { + log.info("{} Sell for price {} {}", now, yesterday.getHfqOpen(), yesterday.getHfqClose()); + return new TradeEngine.Trade(now, stockId, -1 * hold); + } + } else { + log.info("{} Hold for price {} {}", now, yesterday.getHfqOpen(), yesterday.getHfqClose()); + } + return null; + }) + .filter(ObjectUtil::isNotNull) + .toList(); + }, + startDate, + endDate + ); + var sources = dailyRepository.findAll( QDaily.daily.stock.code.eq(code) - .and(QDaily.daily.tradeDate.after(LocalDate.of(2025, 1, 1))), + .and(QDaily.daily.tradeDate.after(startDate.minusDays(150))) + .and(QDaily.daily.tradeDate.before(endDate)), QDaily.daily.tradeDate.asc() ); - var sma30 = TaHelper.sma(dailies, 30, Daily::getHfqClose); - var slopes = new ArrayList(); + var dailies = sources.stream() + .filter(daily -> daily.getTradeDate().isAfter(startDate) && daily.getTradeDate().isBefore(endDate)) + .sorted(Comparator.comparing(Daily::getTradeDate)) + .toList(); + var dailyXList = new ArrayList(); + var dailyYList = new ArrayList>(); + var dailyCloseMapping = new HashMap(); + for (var daily : dailies) { + dailyXList.add(daily.getTradeDate().toString()); + dailyYList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh())); + dailyCloseMapping.put(daily.getTradeDate().toString(), daily.getHfqClose()); + } + charts.add(Dict.create() + .set("title", code) + .set( + "data", + Dict.create() + .set( + "日线", + Dict.create() + .set("xList", dailyXList) + .set("yList", dailyYList) + .set( + "points", + asset.getHistories() + .stream() + .filter(history -> ObjectUtil.isNotEmpty(history.trades())) + .filter(history -> history.trades().containsKey(stock.getId())) + .filter(history -> ObjectUtil.isNotEmpty(history.trades().get(stock.getId()))) + .map(history -> { + var trade = history.trades().get(stock.getId()).getFirst(); + return Dict.create() + .set("value", trade.volume()) + .set("itemStyle", Dict.create() + .set("color", trade.volume() > 0 ? "#e5b8b5" : "#b5e2e5") + ) + .set("coord", List.of(history.date().toString(), dailyCloseMapping.getOrDefault(history.date().toString(), 0.0))); + } + ) + .toList() + ) + ) + ) + ); + /*log.info("Final Cash: {}", asset.getCash()); + for (var history : asset.getHistories()) { + log.info("Date: {} Cash: {} Trade: {}", history.date(), history.cash(), history.trades().values()); + }*/ + }); + var template = engine.getTemplate("backtest_report.html"); + Files.writeString(Path.of("backtest_report.html"), template.render( + Dict.create().set("charts", charts) + )); + } + + @Transactional(readOnly = true) + @EventListener(ApplicationReadyEvent.class) + public void test() throws IOException { + var dailyRange = 150; + var weekRange = 24; + var monthRange = 12; + var charts = Dict.create(); + List.of("000048.SZ", "000333.SZ", "000568.SZ", "000596.SZ", "000651.SZ", "000848.SZ"/*, "000858.SZ", "000933.SZ", "002027.SZ", "002032.SZ", "002142.SZ", "002192.SZ", "002415.SZ", "002432.SZ", "002475.SZ", "002517.SZ", "002555.SZ", "002648.SZ", "002756.SZ", "002847.SZ", "600036.SH", "600096.SH", "600132.SH", "600188.SH", "600309.SH", "600426.SH", "600436.SH", "600519.SH", "600546.SH", "600563.SH", "600702.SH", "600779.SH", "600803.SH", "600809.SH", "600961.SH", "601001.SH", "601100.SH", "601138.SH", "601225.SH", "601899.SH", "601919.SH", "603195.SH", "603198.SH", "603288.SH", "603369.SH", "603444.SH", "603565.SH", "603568.SH", "603605.SH", "603688.SH"*/) + .parallelStream() + .forEach(code -> { + var sources = dailyRepository.findAll( + QDaily.daily.stock.code.eq(code) + .and(QDaily.daily.tradeDate.after(LocalDate.now().minusMonths(12))), + QDaily.daily.tradeDate.asc() + ); + + var dailies = sources.stream() + .filter(daily -> daily.getTradeDate().isAfter(LocalDate.now().minusDays(dailyRange))) + .sorted(Comparator.comparing(Daily::getTradeDate)) + .toList(); + var dailyXList = new ArrayList(); + var dailyYList = new ArrayList>(); + for (var daily : dailies) { + dailyXList.add(daily.getTradeDate().toString()); + dailyYList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh())); + } + + // 30日均线和均线斜率 + var sma30 = TaHelper.sma(sources, 30, Daily::getHfqClose).subList(sources.size() - dailyRange, sources.size()); + /*var slopes = new ArrayList(); slopes.add(0.0); for (int i = 1; i < sma30.size(); i++) { slopes.add(((sma30.get(i) - sma30.get(i - 1)) * 1000.0) / sma30.get(i - 1)); - } + }*/ + var sma60 = TaHelper.sma(sources, 60, Daily::getHfqClose).subList(sources.size() - dailyRange, sources.size()); - var xList = new ArrayList(); - var yList = new ArrayList>(); - for (var daily : dailies) { - xList.add(daily.getTradeDate().toString()); - yList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh())); + charts.set( + StrUtil.format("日线 {}", code), + Dict.create() + .set("xList", dailyXList) + .set("yList", dailyYList) + .set("sma30", sma30) + .set("sma60", sma60) + // .set("sma30Slopes", slopes) + ); + + var weeklies = sources.stream() + .filter(daily -> daily.getTradeDate().isAfter(LocalDate.now().minusWeeks(weekRange))) + .collect(Collectors.groupingBy(daily -> new YearAndWeek(daily.getTradeDate().getYear(), daily.getTradeDate().get(WeekFields.ISO.weekOfYear())))) + .entrySet() + .stream() + .map(entry -> { + var yearAndWeek = entry.getKey(); + var subDailies = entry.getValue(); + var open = subDailies.getFirst().getHfqOpen(); + var close = subDailies.getLast().getHfqClose(); + return new Weekly( + LocalDate.of(yearAndWeek.year(), 1, 1).with(WeekFields.ISO.weekOfYear(), yearAndWeek.week()), + yearAndWeek.year(), + yearAndWeek.week(), + open, + maxFromDaily(subDailies, Daily::getHfqHigh), + minFromDaily(subDailies, Daily::getHfqLow), + close, + close - open, + (close - open) / open * 100, + sumFromDaily(subDailies, Daily::getVolume), + sumFromDaily(subDailies, Daily::getTurnover) + ); + }) + .sorted(Comparator.comparingInt(weekly -> weekly.year() * 100 + weekly.week())) + .toList(); + + var weekXList = new ArrayList(); + var weekYList = new ArrayList>(); + for (var weekly : weeklies) { + weekXList.add(weekly.tradeDate().toString()); + weekYList.add(List.of(weekly.open(), weekly.close(), weekly.low(), weekly.high())); } charts.set( - code, + StrUtil.format("周线 {}", code), Dict.create() - .set("xList", xList) - .set("yList", yList) - .set("sma30", sma30) - .set("sma30Slopes", slopes) + .set("xList", weekXList) + .set("yList", weekYList) + ); + + var monthlies = sources.stream() + .filter(daily -> daily.getTradeDate().isAfter(LocalDate.now().minusMonths(monthRange))) + .collect(Collectors.groupingBy(daily -> new YearAndMonth(daily.getTradeDate().getYear(), daily.getTradeDate().getMonthValue()))) + .entrySet() + .stream() + .map(entry -> { + var yearAndMonth = entry.getKey(); + var subDailies = entry.getValue(); + var open = subDailies.getFirst().getHfqOpen(); + var close = subDailies.getLast().getHfqClose(); + return new Monthly( + LocalDate.of(yearAndMonth.year(), yearAndMonth.month(), 1), + yearAndMonth.year(), + yearAndMonth.month(), + open, + maxFromDaily(subDailies, Daily::getHfqHigh), + minFromDaily(subDailies, Daily::getHfqLow), + close, + close - open, + (close - open) / open * 100, + sumFromDaily(subDailies, Daily::getVolume), + sumFromDaily(subDailies, Daily::getTurnover) + ); + }) + .sorted(Comparator.comparingInt(monthly -> monthly.year() * 100 + monthly.month())) + .toList(); + + var monthXList = new ArrayList(); + var monthYList = new ArrayList>(); + for (var month : monthlies) { + monthXList.add(month.tradeDate().toString()); + monthYList.add(List.of(month.open(), month.close(), month.low(), month.high())); + } + + charts.set( + StrUtil.format("月线 {}", code), + Dict.create() + .set("xList", monthXList) + .set("yList", monthYList) ); }); diff --git a/leopard-strategy/src/main/resources/logback-spring.xml b/leopard-strategy/src/main/resources/logback-spring.xml index 9a1d6e0..8587748 100644 --- a/leopard-strategy/src/main/resources/logback-spring.xml +++ b/leopard-strategy/src/main/resources/logback-spring.xml @@ -15,7 +15,7 @@ - + diff --git a/leopard-strategy/src/main/resources/templates/backtest_report.html b/leopard-strategy/src/main/resources/templates/backtest_report.html new file mode 100644 index 0000000..ba360d1 --- /dev/null +++ b/leopard-strategy/src/main/resources/templates/backtest_report.html @@ -0,0 +1,259 @@ + + + + + + Strategy + + + + + + +
+ + + + diff --git a/leopard-strategy/src/main/resources/templates/report.html b/leopard-strategy/src/main/resources/templates/report.html index e8f560f..16caf9f 100644 --- a/leopard-strategy/src/main/resources/templates/report.html +++ b/leopard-strategy/src/main/resources/templates/report.html @@ -205,6 +205,16 @@ color: 'rgba(0,111,255,0.5)', }, }, + { + type: 'line', + yAxisIndex: 0, + data: '${sma60 || []}', + smooth: true, + symbol: 'none', + lineStyle: { + color: 'rgba(115,0,255,0.5)', + }, + }, { type: 'line', yAxisIndex: 1,