Compare commits
15 Commits
8c4e3baacb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6179137e0d | |||
| 958bc459b2 | |||
| 7eec44f21c | |||
| 5f42d36436 | |||
| 9a31b8cae4 | |||
| 482eb70465 | |||
| 7703f88d7f | |||
| db8a094c8f | |||
| f4fef2bd95 | |||
| 53a6d33fd5 | |||
| 83574e1229 | |||
| e2c5729f87 | |||
| 47f8b30a02 | |||
| 452d1c681d | |||
| 42b402d4ef |
54
.idea/compiler.xml
generated
54
.idea/compiler.xml
generated
@@ -17,13 +17,11 @@
|
|||||||
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hibernate/orm/hibernate-jpamodelgen/6.6.8.Final/hibernate-jpamodelgen-6.6.8.Final.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/hibernate/orm/hibernate-jpamodelgen/6.6.8.Final/hibernate-jpamodelgen-6.6.8.Final.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hibernate/orm/hibernate-core/6.6.8.Final/hibernate-core-6.6.8.Final.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/hibernate/orm/hibernate-core/6.6.8.Final/hibernate-core-6.6.8.Final.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/persistence/jakarta.persistence-api/3.2.0/jakarta.persistence-api-3.2.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar" />
|
<entry name="$MAVEN_REPOSITORY$/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jboss/logging/jboss-logging/3.5.0.Final/jboss-logging-3.5.0.Final.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar" />
|
||||||
|
<entry name="$MAVEN_REPOSITORY$/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hibernate/common/hibernate-commons-annotations/7.0.3.Final/hibernate-commons-annotations-7.0.3.Final.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/hibernate/common/hibernate-commons-annotations/7.0.3.Final/hibernate-commons-annotations-7.0.3.Final.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/jandex/3.2.0/jandex-3.2.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/io/smallrye/jandex/3.2.0/jandex-3.2.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy/1.15.11/byte-buddy-1.15.11.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/xml/bind/jakarta.xml.bind-api/4.0.0/jakarta.xml.bind-api-4.0.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/jakarta/xml/bind/jakarta.xml.bind-api/4.0.0/jakarta.xml.bind-api-4.0.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/activation/jakarta.activation-api/2.1.0/jakarta.activation-api-2.1.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/jakarta/activation/jakarta.activation-api/2.1.0/jakarta.activation-api-2.1.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/glassfish/jaxb/jaxb-runtime/4.0.2/jaxb-runtime-4.0.2.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/glassfish/jaxb/jaxb-runtime/4.0.2/jaxb-runtime-4.0.2.jar" />
|
||||||
@@ -31,10 +29,11 @@
|
|||||||
<entry name="$MAVEN_REPOSITORY$/org/eclipse/angus/angus-activation/2.0.0/angus-activation-2.0.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/eclipse/angus/angus-activation/2.0.0/angus-activation-2.0.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/glassfish/jaxb/txw2/4.0.2/txw2-4.0.2.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/glassfish/jaxb/txw2/4.0.2/txw2-4.0.2.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/sun/istack/istack-commons-runtime/4.1.1/istack-commons-runtime-4.1.1.jar" />
|
<entry name="$MAVEN_REPOSITORY$/com/sun/istack/istack-commons-runtime/4.1.1/istack-commons-runtime-4.1.1.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/antlr/antlr4-runtime/4.13.0/antlr4-runtime-4.13.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/validation/jakarta.validation-api/3.0.2/jakarta.validation-api-3.0.2.jar" />
|
<entry name="$MAVEN_REPOSITORY$/jakarta/validation/jakarta.validation-api/3.0.2/jakarta.validation-api-3.0.2.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar" />
|
<entry name="$MAVEN_REPOSITORY$/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar" />
|
||||||
|
<entry name="$MAVEN_REPOSITORY$/org/antlr/antlr4-runtime/4.13.0/antlr4-runtime-4.13.0.jar" />
|
||||||
|
<entry name="$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy/1.15.11/byte-buddy-1.15.11.jar" />
|
||||||
|
<entry name="$MAVEN_REPOSITORY$/org/jboss/logging/jboss-logging/3.5.0.Final/jboss-logging-3.5.0.Final.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-apt/7.0/querydsl-apt-7.0-jpa.jar" />
|
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-apt/7.0/querydsl-apt-7.0-jpa.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-codegen/7.0/querydsl-codegen-7.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-codegen/7.0/querydsl-codegen-7.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-core/7.0/querydsl-core-7.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-core/7.0/querydsl-core-7.0.jar" />
|
||||||
@@ -43,48 +42,7 @@
|
|||||||
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-codegen-utils/7.0/querydsl-codegen-utils-7.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-codegen-utils/7.0/querydsl-codegen-utils-7.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/eclipse/jdt/ecj/3.40.0/ecj-3.40.0.jar" />
|
<entry name="$MAVEN_REPOSITORY$/org/eclipse/jdt/ecj/3.40.0/ecj-3.40.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/github/classgraph/classgraph/4.8.179/classgraph-4.8.179.jar" />
|
<entry name="$MAVEN_REPOSITORY$/io/github/classgraph/classgraph/4.8.179/classgraph-4.8.179.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/26.0.2/annotations-26.0.2.jar" />
|
<entry name="$MAVEN_REPOSITORY$/jakarta/persistence/jakarta.persistence-api/3.2.0/jakarta.persistence-api-3.2.0.jar" />
|
||||||
<entry name="$MAVEN_REPOSITORY$/dev/morphia/morphia/morphia-core/2.5.0/morphia-core-2.5.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/config/smallrye-config/3.10.1/smallrye-config-3.10.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/config/smallrye-config-core/3.10.1/smallrye-config-core-3.10.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/eclipse/microprofile/config/microprofile-config-api/3.1/microprofile-config-api-3.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-annotation/2.8.0/smallrye-common-annotation-2.8.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-expression/2.8.0/smallrye-common-expression-2.8.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-function/2.8.0/smallrye-common-function-2.8.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-constraint/2.8.0/smallrye-common-constraint-2.8.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-classloader/2.8.0/smallrye-common-classloader-2.8.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/ow2/asm/asm/9.8/asm-9.8.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/smallrye/config/smallrye-config-common/3.10.1/smallrye-config-common-3.10.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/mongodb/mongodb-driver-sync/5.4.0/mongodb-driver-sync-5.4.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/mongodb/bson/5.4.0/bson-5.4.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/mongodb/mongodb-driver-core/5.4.0/mongodb-driver-core-5.4.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/mongodb/bson-record-codec/5.4.0/bson-record-codec-5.4.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/mongodb/mongodb-driver-legacy/5.4.0/mongodb-driver-legacy-5.4.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/github/spotbugs/spotbugs-annotations/4.8.6/spotbugs-annotations-4.8.6.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/semver4j/semver4j/5.6.0/semver4j-5.6.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/jsoup/jsoup/1.18.3/jsoup-1.18.3.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hibernate/orm/hibernate-envers/7.0.0.Beta1/hibernate-envers-7.0.0.Beta1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hibernate/models/hibernate-models/0.8.6/hibernate-models-0.8.6.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-core/7.0/querydsl-core-7.0-tests.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/joda/joda-money/2.0.2/joda-money-2.0.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter/5.13.1/junit-jupiter-5.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-api/5.13.1/junit-jupiter-api-5.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-commons/1.13.1/junit-platform-commons-1.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-params/5.13.1/junit-jupiter-params-5.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-engine/5.13.1/junit-jupiter-engine-5.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-engine/1.13.1/junit-platform-engine-1.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/assertj/assertj-core/3.27.3/assertj-core-3.27.3.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/junit/vintage/junit-vintage-engine/5.13.1/junit-vintage-engine-5.13.1.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/junit/junit/4.13.2/junit-4.13.2.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/easymock/easymock/5.6.0/easymock-5.6.0.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/objenesis/objenesis/3.4/objenesis-3.4.jar" />
|
|
||||||
<entry name="$MAVEN_REPOSITORY$/org/javassist/javassist/3.30.2-GA/javassist-3.30.2-GA.jar" />
|
|
||||||
</processorPath>
|
</processorPath>
|
||||||
<module name="leopard-core" />
|
<module name="leopard-core" />
|
||||||
</profile>
|
</profile>
|
||||||
|
|||||||
@@ -33,6 +33,12 @@
|
|||||||
<artifactId>hutool-http</artifactId>
|
<artifactId>hutool-http</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.icepear.echarts</groupId>
|
||||||
|
<artifactId>echarts-java</artifactId>
|
||||||
|
<version>1.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.ralfkonrad.quantlib_for_maven</groupId>
|
<groupId>io.github.ralfkonrad.quantlib_for_maven</groupId>
|
||||||
<artifactId>quantlib</artifactId>
|
<artifactId>quantlib</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.entity.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
public record DailyDouble(LocalDate date, Double value) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.entity.dto;
|
||||||
|
|
||||||
|
public record YearAndMonth(int year, int month) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.entity.dto;
|
||||||
|
|
||||||
|
public record YearAndWeek(int year, int week) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Double maxFromDaily(List<Daily> dailies, Function<Daily, Double> function) {
|
||||||
|
return dailies.stream()
|
||||||
|
.map(function)
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.max()
|
||||||
|
.orElse(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Double minFromDaily(List<Daily> dailies, Function<Daily, Double> function) {
|
||||||
|
return dailies.stream()
|
||||||
|
.map(function)
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.min()
|
||||||
|
.orElse(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Double sumFromDaily(List<Daily> dailies, Function<Daily, Double> function) {
|
||||||
|
return dailies.stream()
|
||||||
|
.map(function)
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.core.service;
|
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.Daily;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
|
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
|
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.Stock;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.dto.Monthly;
|
import com.lanyuanxiaoyao.leopard.core.entity.dto.Monthly;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.dto.Weekly;
|
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.entity.dto.Yearly;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.FinanceIndicatorRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.FinanceIndicatorRepository;
|
||||||
@@ -19,12 +20,15 @@ import java.time.temporal.WeekFields;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.cache.annotation.Cacheable;
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.stereotype.Service;
|
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
|
* @author lanyuanxiaoyao
|
||||||
* @version 20250828
|
* @version 20250828
|
||||||
@@ -41,7 +45,7 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
this.dailyRepository = dailyRepository;
|
this.dailyRepository = dailyRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cacheable(value = "long-cache", sync = true)
|
@Cacheable(value = "findFinanceIndicator", cacheManager = "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)
|
||||||
@@ -49,32 +53,30 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cacheable(value = "long-cache", sync = true)
|
@Cacheable(value = "findFinanceIndicatorRecent", cacheManager = "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(
|
||||||
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 = "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 = "long-cache", sync = true)
|
@Cacheable(value = "findDailyLatest", cacheManager = "long-cache", sync = true)
|
||||||
public Optional<Daily> findDailyLatest(Long stockId) {
|
public Optional<Daily> findDailyLatest(Long stockId) {
|
||||||
return dailyRepository.findLatest(stockId);
|
return dailyRepository.findLatest(stockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cacheable(value = "long-cache", sync = true)
|
@Cacheable(value = "findYearlyRecent", cacheManager = "long-cache", sync = true)
|
||||||
public List<Yearly> findYearlyRecent(Long stockId, int years) {
|
public List<Yearly> findYearlyRecent(Long stockId, int years) {
|
||||||
var current = LocalDate.now().withMonth(1).withDayOfMonth(1);
|
var current = LocalDate.now().withMonth(1).withDayOfMonth(1);
|
||||||
var start = current.minusYears(years).getYear();
|
var start = current.minusYears(years).getYear();
|
||||||
@@ -112,7 +114,7 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cacheable(value = "long-cache", sync = true)
|
@Cacheable(value = "findMonthlyRecent", cacheManager = "long-cache", sync = true)
|
||||||
public List<Monthly> findMonthlyRecent(Long stockId, int months) {
|
public List<Monthly> findMonthlyRecent(Long stockId, int months) {
|
||||||
var end = LocalDate.now().withDayOfMonth(1);
|
var end = LocalDate.now().withDayOfMonth(1);
|
||||||
var start = end.minusMonths(months);
|
var start = end.minusMonths(months);
|
||||||
@@ -145,9 +147,9 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
var open = dailies.getFirst().getHfqOpen();
|
var open = dailies.getFirst().getHfqOpen();
|
||||||
var close = dailies.getLast().getHfqClose();
|
var close = dailies.getLast().getHfqClose();
|
||||||
return new Monthly(
|
return new Monthly(
|
||||||
LocalDate.of(yearAndMonth.year, yearAndMonth.month, 1),
|
LocalDate.of(yearAndMonth.year(), yearAndMonth.month(), 1),
|
||||||
yearAndMonth.year,
|
yearAndMonth.year(),
|
||||||
yearAndMonth.month,
|
yearAndMonth.month(),
|
||||||
open,
|
open,
|
||||||
maxFromDaily(dailies, Daily::getHfqHigh),
|
maxFromDaily(dailies, Daily::getHfqHigh),
|
||||||
minFromDaily(dailies, Daily::getHfqLow),
|
minFromDaily(dailies, Daily::getHfqLow),
|
||||||
@@ -162,7 +164,7 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cacheable(value = "long-cache", sync = true)
|
@Cacheable(value = "findWeeklyRecent", cacheManager = "long-cache", sync = true)
|
||||||
public List<Weekly> findWeeklyRecent(Long stockId, int weeks) {
|
public List<Weekly> findWeeklyRecent(Long stockId, int weeks) {
|
||||||
var end = LocalDate.now().with(ChronoField.DAY_OF_WEEK, 1);
|
var end = LocalDate.now().with(ChronoField.DAY_OF_WEEK, 1);
|
||||||
var start = end.minusWeeks(weeks);
|
var start = end.minusWeeks(weeks);
|
||||||
@@ -195,9 +197,9 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
var open = dailies.getFirst().getHfqOpen();
|
var open = dailies.getFirst().getHfqOpen();
|
||||||
var close = dailies.getLast().getHfqClose();
|
var close = dailies.getLast().getHfqClose();
|
||||||
return new Weekly(
|
return new Weekly(
|
||||||
LocalDate.of(yearAndWeek.year, 1, 1).with(WeekFields.ISO.weekOfYear(), yearAndWeek.week),
|
LocalDate.of(yearAndWeek.year(), 1, 1).with(WeekFields.ISO.weekOfYear(), yearAndWeek.week()),
|
||||||
yearAndWeek.year,
|
yearAndWeek.year(),
|
||||||
yearAndWeek.week,
|
yearAndWeek.week(),
|
||||||
open,
|
open,
|
||||||
maxFromDaily(dailies, Daily::getHfqHigh),
|
maxFromDaily(dailies, Daily::getHfqHigh),
|
||||||
minFromDaily(dailies, Daily::getHfqLow),
|
minFromDaily(dailies, Daily::getHfqLow),
|
||||||
@@ -211,36 +213,4 @@ public class StockService extends SimpleServiceSupport<Stock> {
|
|||||||
.sorted(Comparator.comparingInt(weekly -> weekly.year() * 100 + weekly.week()))
|
.sorted(Comparator.comparingInt(weekly -> weekly.year() * 100 + weekly.week()))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Double maxFromDaily(List<Daily> dailies, Function<Daily, Double> function) {
|
|
||||||
return dailies.stream()
|
|
||||||
.map(function)
|
|
||||||
.filter(ObjectUtil::isNotNull)
|
|
||||||
.mapToDouble(Double::doubleValue)
|
|
||||||
.max()
|
|
||||||
.orElse(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Double minFromDaily(List<Daily> dailies, Function<Daily, Double> function) {
|
|
||||||
return dailies.stream()
|
|
||||||
.map(function)
|
|
||||||
.filter(ObjectUtil::isNotNull)
|
|
||||||
.mapToDouble(Double::doubleValue)
|
|
||||||
.min()
|
|
||||||
.orElse(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Double sumFromDaily(List<Daily> dailies, Function<Daily, Double> 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) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.strategy;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交易策略
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20251016
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class TradeEngine {
|
||||||
|
private final DailyRepository dailyRepository;
|
||||||
|
|
||||||
|
public TradeEngine(DailyRepository dailyRepository) {
|
||||||
|
this.dailyRepository = dailyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Asset backtest(Stock stock, TradeStrategy strategy, LocalDate startDate, LocalDate endDate) {
|
||||||
|
var dailies = dailyRepository.findAll(
|
||||||
|
QDaily.daily.stock.eq(stock)
|
||||||
|
.and(QDaily.daily.tradeDate.before(endDate)),
|
||||||
|
QDaily.daily.tradeDate.asc()
|
||||||
|
);
|
||||||
|
var validTradeDates = dailies.stream()
|
||||||
|
.map(Daily::getTradeDate)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
var asset = new Asset();
|
||||||
|
for (var now = startDate; now.isBefore(endDate) || now.isEqual(endDate); now = now.plusDays(1)) {
|
||||||
|
if (!validTradeDates.contains(now)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final var currentDate = now;
|
||||||
|
var trade = strategy.trade(
|
||||||
|
now,
|
||||||
|
asset,
|
||||||
|
dailies.stream()
|
||||||
|
.filter(daily -> daily.getTradeDate().isBefore(currentDate))
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
var daily = dailies.stream()
|
||||||
|
.filter(d -> ObjectUtil.equals(d.getTradeDate(), currentDate))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
asset.addTrade(now, trade, daily.getHfqClose());
|
||||||
|
}
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface TradeStrategy {
|
||||||
|
int trade(LocalDate now, Asset asset, List<Daily> dailies);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static final class Asset {
|
||||||
|
private List<Trade> trades = new ArrayList<>();
|
||||||
|
|
||||||
|
public void addTrade(LocalDate date, int volume, double price) {
|
||||||
|
trades.add(new Trade(date, volume, price));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVolume() {
|
||||||
|
return getVolume(trade -> true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVolume(LocalDate date) {
|
||||||
|
return getVolume(trade -> trade.date().isBefore(date) || trade.date().isEqual(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getVolume(Predicate<Trade> predicate) {
|
||||||
|
return trades.stream()
|
||||||
|
.filter(predicate)
|
||||||
|
.mapToInt(Trade::volume)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCash() {
|
||||||
|
return getCash(trade -> true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCash(LocalDate date) {
|
||||||
|
return getCash(trade -> trade.date().isBefore(date) || trade.date().isEqual(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getCash(Predicate<Trade> predicate) {
|
||||||
|
return trades.stream()
|
||||||
|
.filter(predicate)
|
||||||
|
.mapToDouble(trade -> -1 * trade.volume() * trade.price())
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPrice() {
|
||||||
|
return getPrice(trade -> true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPrice(LocalDate date) {
|
||||||
|
return getPrice(trade -> trade.date().isBefore(date) || trade.date().isEqual(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPrice(Predicate<Trade> predicate) {
|
||||||
|
int volume = getVolume(predicate);
|
||||||
|
return volume == 0 ? 0 : getCash(predicate) / volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Trade(
|
||||||
|
LocalDate date,
|
||||||
|
int volume,
|
||||||
|
double price
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,13 +124,48 @@ 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(
|
||||||
|
"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)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("DuplicatedCode")
|
||||||
|
@GetMapping("weekly/{id}")
|
||||||
|
public GlobalResponse<Map<String, Object>> weeklyCharts(@PathVariable("id") Long id) {
|
||||||
|
var data = stockService.findWeeklyRecent(id, 50);
|
||||||
|
var xList = new ArrayList<String>();
|
||||||
|
var yList = new ArrayList<List<Double>>();
|
||||||
|
for (var weekly : data) {
|
||||||
|
xList.add(weekly.tradeDate().toString());
|
||||||
|
yList.add(List.of(weekly.open(), weekly.close(), weekly.low(), weekly.high()));
|
||||||
|
}
|
||||||
|
return GlobalResponse.responseMapData(Map.of(
|
||||||
|
"xList", xList, "yList", yList
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("DuplicatedCode")
|
||||||
|
@GetMapping("monthly/{id}")
|
||||||
|
public GlobalResponse<Map<String, Object>> monthlyCharts(@PathVariable("id") Long id) {
|
||||||
|
var data = stockService.findMonthlyRecent(id, 24);
|
||||||
|
var xList = new ArrayList<String>();
|
||||||
|
var yList = new ArrayList<List<Double>>();
|
||||||
|
for (var monthly : data) {
|
||||||
|
xList.add(monthly.tradeDate().toString());
|
||||||
|
yList.add(List.of(monthly.open(), monthly.close(), monthly.low(), monthly.high()));
|
||||||
|
}
|
||||||
return GlobalResponse.responseMapData(Map.of(
|
return GlobalResponse.responseMapData(Map.of(
|
||||||
"xList", xList, "yList", yList
|
"xList", xList, "yList", yList
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="com.zaxxer.hikari" level="ERROR"/>
|
<logger name="com.zaxxer.hikari" level="ERROR"/>
|
||||||
<!--<logger name="org.hibernate.SQL" level="DEBUG"/>-->
|
<logger name="org.hibernate.SQL" level="DEBUG"/>
|
||||||
|
|
||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
<appender-ref ref="Console"/>
|
<appender-ref ref="Console"/>
|
||||||
|
|||||||
@@ -34,6 +34,15 @@
|
|||||||
<artifactId>commonmark-ext-gfm-tables</artifactId>
|
<artifactId>commonmark-ext-gfm-tables</artifactId>
|
||||||
<version>0.26.0</version>
|
<version>0.26.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.thymeleaf</groupId>
|
||||||
|
<artifactId>thymeleaf</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-extra</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.strategy;
|
package com.lanyuanxiaoyao.leopard.strategy;
|
||||||
|
|
||||||
|
import cn.hutool.core.lang.Dict;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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.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.DailyRepository;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
import com.lanyuanxiaoyao.leopard.core.service.AssessmentService;
|
import com.lanyuanxiaoyao.leopard.core.strategy.TradeEngine;
|
||||||
import com.lanyuanxiaoyao.leopard.core.service.StockService;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.service.TuShareService;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.service.selector.PyramidStockSelector;
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Map;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
@@ -22,266 +36,257 @@ import org.springframework.context.event.EventListener;
|
|||||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
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
|
@Slf4j
|
||||||
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
|
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
|
||||||
@EnableJpaAuditing
|
@EnableJpaAuditing
|
||||||
public class StrategyApplication {
|
public class StrategyApplication {
|
||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
private static final TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private PyramidStockSelector pyramidStockSelector;
|
private TradeEngine tradeEngine;
|
||||||
@Resource
|
|
||||||
private AssessmentService assessmentService;
|
|
||||||
@Resource
|
|
||||||
private TuShareService tuShareService;
|
|
||||||
@Resource
|
|
||||||
private DailyRepository dailyRepository;
|
|
||||||
@Resource
|
@Resource
|
||||||
private StockRepository stockRepository;
|
private StockRepository stockRepository;
|
||||||
@Resource
|
@Resource
|
||||||
private StockService stockService;
|
private DailyRepository dailyRepository;
|
||||||
|
@Resource
|
||||||
|
private StockCollectionRepository stockCollectionRepository;
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(StrategyApplication.class, args);
|
SpringApplication.run(StrategyApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void render(Map<String, Object> data) throws IOException {
|
@Transactional(readOnly = true)
|
||||||
Files.writeString(
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
Path.of("result.html"),
|
public void backtest() throws IOException {
|
||||||
StrUtil.format(
|
var startDate = LocalDate.of(2024, 12, 1);
|
||||||
// language=HTML
|
var endDate = LocalDate.of(2024, 12, 31);
|
||||||
"""
|
var charts = new ArrayList<Dict>();
|
||||||
<html lang='zh'>
|
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"*/)
|
||||||
<head>
|
.parallelStream()
|
||||||
<meta charset='utf-8'/>
|
.forEach(code -> {
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
var stock = stockRepository.findOne(QStock.stock.code.eq(code)).orElseThrow();
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
|
var asset = tradeEngine.backtest(
|
||||||
<title>Strategy</title>
|
stockRepository.findById(stock.getId()).orElseThrow(),
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/antd.min.css"/>
|
(now, currentAsset, dailies) -> {
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/helper.min.css"/>
|
var stockDailies = dailies
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/iconfont.min.css"/>
|
.stream()
|
||||||
<style>
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
html, body, #root {
|
.toList();
|
||||||
position: relative;
|
var yesterday = stockDailies.getLast();
|
||||||
width: 100%;
|
if (yesterday.getHfqClose() > yesterday.getHfqOpen()) {
|
||||||
height: 100%;
|
return 100;
|
||||||
margin: 0;
|
} else if (yesterday.getHfqClose() < yesterday.getHfqOpen()) {
|
||||||
padding: 0;
|
var hold = currentAsset.getVolume();
|
||||||
|
if (hold > 0) {
|
||||||
|
return -1 * hold;
|
||||||
}
|
}
|
||||||
</style>
|
}
|
||||||
</head>
|
return 0;
|
||||||
<body>
|
|
||||||
<div id='root'/>
|
|
||||||
</body>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/sdk.min.js"></script>
|
|
||||||
<script type='text/javascript'>
|
|
||||||
(function () {
|
|
||||||
const amis = amisRequire('amis/embed')
|
|
||||||
const amisJson = {
|
|
||||||
type: 'page',
|
|
||||||
title: 'Strategy',
|
|
||||||
data: {},
|
|
||||||
body: [
|
|
||||||
/*{
|
|
||||||
type: 'table2',
|
|
||||||
source: '${items}',
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'date',
|
|
||||||
label: 'Date',
|
|
||||||
width: 100,
|
|
||||||
},
|
},
|
||||||
{
|
startDate,
|
||||||
name: 'code',
|
endDate
|
||||||
label: 'Code',
|
);
|
||||||
width: 100,
|
var sources = dailyRepository.findAll(
|
||||||
},
|
QDaily.daily.stock.code.eq(code)
|
||||||
{
|
.and(QDaily.daily.tradeDate.after(startDate.minusDays(150)))
|
||||||
name: 'name',
|
.and(QDaily.daily.tradeDate.before(endDate)),
|
||||||
label: 'Name',
|
QDaily.daily.tradeDate.asc()
|
||||||
width: 100,
|
);
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sma5',
|
|
||||||
label: 'SMA-5',
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sma10',
|
|
||||||
label: 'SMA-10',
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},*/
|
|
||||||
{
|
|
||||||
type: 'chart',
|
|
||||||
height: 800,
|
|
||||||
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) {
|
|
||||||
const param = params[0]
|
|
||||||
const open = param.data[1].toFixed(2)
|
|
||||||
const close = param.data[2].toFixed(2)
|
|
||||||
const lowest = param.data[3].toFixed(2)
|
|
||||||
const highest = param.data[4].toFixed(2)
|
|
||||||
|
|
||||||
return `<div class="text-center font-bold mb-2">${param.name}</div>
|
var dailies = sources.stream()
|
||||||
<div class="text-center">
|
.filter(daily -> daily.getTradeDate().isAfter(startDate) && daily.getTradeDate().isBefore(endDate))
|
||||||
<span>开盘:</span>
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
<span class="font-bold ml-4">${open}</span>
|
.toList();
|
||||||
</div>
|
var oclhList = new HashMap<>();
|
||||||
<div class="text-center">
|
var dailyCloseMapping = new HashMap<String, Double>();
|
||||||
<span>收盘:</span>
|
for (var daily : dailies) {
|
||||||
<span class="font-bold ml-4">${close}</span>
|
oclhList.put(
|
||||||
</div>
|
daily.getTradeDate().toString(),
|
||||||
<div class="text-center">
|
List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh())
|
||||||
<span>最低:</span>
|
);
|
||||||
<span class="font-bold ml-4">${lowest}</span>
|
dailyCloseMapping.put(daily.getTradeDate().toString(), daily.getHfqClose());
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<span>最高:</span>
|
|
||||||
<span class="font-bold ml-4">${highest}</span>
|
|
||||||
</div>`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '2%',
|
|
||||||
right: '2%',
|
|
||||||
top: '5%',
|
|
||||||
bottom: '12%',
|
|
||||||
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) {
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'line',
|
|
||||||
data: '${sma10 || []}',
|
|
||||||
smooth: true,
|
|
||||||
lineStyle: {
|
|
||||||
color: '#dcb38a',
|
|
||||||
},
|
|
||||||
symbol: 'none',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'line',
|
|
||||||
data: '${sma60 || []}',
|
|
||||||
smooth: true,
|
|
||||||
lineStyle: {
|
|
||||||
color: '#6ce3c6',
|
|
||||||
},
|
|
||||||
symbol: 'none',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'line',
|
|
||||||
data: '${sma120 || []}',
|
|
||||||
smooth: true,
|
|
||||||
lineStyle: {
|
|
||||||
color: '#6cd5e3',
|
|
||||||
},
|
|
||||||
symbol: 'none',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
amis.embed('#root', amisJson, {}, {theme: 'antd'})
|
charts.add(
|
||||||
})()
|
Dict.create()
|
||||||
</script>
|
.set("title", code)
|
||||||
</html>
|
.set("type", "candle")
|
||||||
""",
|
.set(
|
||||||
mapper.writeValueAsString(data)
|
"data",
|
||||||
|
Dict.create()
|
||||||
|
.set(
|
||||||
|
"日线",
|
||||||
|
Dict.create()
|
||||||
|
.set("type", "candle")
|
||||||
|
.set("oclh", oclhList)
|
||||||
|
.set(
|
||||||
|
"points",
|
||||||
|
asset.getTrades()
|
||||||
|
.stream()
|
||||||
|
.filter(trade -> trade.volume() != 0)
|
||||||
|
.map(trade -> Dict.create()
|
||||||
|
.set("value", trade.volume())
|
||||||
|
.set("itemStyle", Dict.create()
|
||||||
|
.set("color", trade.volume() > 0 ? "#e5b8b5" : "#b5e2e5")
|
||||||
|
)
|
||||||
|
.set("coord", List.of(trade.date().toString(), dailyCloseMapping.getOrDefault(trade.date().toString(), 0.0)))
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.set(
|
||||||
|
"资金",
|
||||||
|
Dict.create()
|
||||||
|
.set("type", "line")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
/*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)
|
@Transactional(readOnly = true)
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
public void test() {
|
public void test() throws IOException {
|
||||||
var stock = stockRepository.findOne(QStock.stock.code.eq("000001.SZ")).orElseThrow();
|
var dailyRange = 150;
|
||||||
var weeklies = stockService.findWeeklyRecent(stock.getId(), 2);
|
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<String>();
|
||||||
|
var dailyYList = new ArrayList<List<Double>>();
|
||||||
|
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<Double>();
|
||||||
|
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());
|
||||||
|
|
||||||
|
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<String>();
|
||||||
|
var weekYList = new ArrayList<List<Double>>();
|
||||||
for (var weekly : weeklies) {
|
for (var weekly : weeklies) {
|
||||||
log.info("{}", weekly);
|
weekXList.add(weekly.tradeDate().toString());
|
||||||
}
|
weekYList.add(List.of(weekly.open(), weekly.close(), weekly.low(), weekly.high()));
|
||||||
|
}
|
||||||
|
|
||||||
|
charts.set(
|
||||||
|
StrUtil.format("周线 {}", code),
|
||||||
|
Dict.create()
|
||||||
|
.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<String>();
|
||||||
|
var monthYList = new ArrayList<List<Double>>();
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
var template = engine.getTemplate("report.html");
|
||||||
|
Files.writeString(Path.of("report.html"), template.render(
|
||||||
|
Dict.create().set("charts", charts)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="com.lanyuanxiaoyao.leopard" level="INFO"/>
|
<logger name="com.lanyuanxiaoyao.leopard" level="INFO"/>
|
||||||
<!--<logger name="org.hibernate.SQL" level="DEBUG"/>-->
|
<logger name="org.hibernate.SQL" level="DEBUG"/>
|
||||||
|
|
||||||
<root level="ERROR">
|
<root level="ERROR">
|
||||||
<appender-ref ref="Console"/>
|
<appender-ref ref="Console"/>
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
<html lang='zh'>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'/>
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
|
||||||
|
<meta content='width=device-width, initial-scale=1.0' name='viewport'/>
|
||||||
|
<title>Strategy</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/antd.min.css" rel="stylesheet"/>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/helper.min.css" rel="stylesheet"/>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/iconfont.min.css" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
html, body, #root {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='root'></div>
|
||||||
|
</body>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/sdk.min.js"></script>
|
||||||
|
<script th:inline="javascript" type='text/javascript'>
|
||||||
|
// 全局配置(颜色、尺寸、间距等),集中管理,便于统一调整
|
||||||
|
const CONFIG = {
|
||||||
|
colors: {up: '#000000FF', down: '#00000045'},
|
||||||
|
grid: {left: '2%', right: '2%', top: 40, bottom: 110},
|
||||||
|
zoom: {bottom: 16, height: 50},
|
||||||
|
linewidth: {stem: 1.5, openTick: 1.2, closeTick: 1.6, closeLine: 1.5},
|
||||||
|
tick: {min: 4, max: 10, scale: 0.4},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用 tooltip 格式化(按索引回读原始 O/H/L/C)
|
||||||
|
function makeTooltipFormatter(dataMap, dates) {
|
||||||
|
return function (params) {
|
||||||
|
let p = Array.isArray(params) ? params[0] : params
|
||||||
|
let idx = p.dataIndex
|
||||||
|
let d = dates[idx]
|
||||||
|
let ohlc = dataMap[d] || []
|
||||||
|
let o = ohlc[0], c = ohlc[1], l = ohlc[2], h = ohlc[3]
|
||||||
|
let chg = (c - o)
|
||||||
|
let chgPct = o ? (chg / o * 100) : 0
|
||||||
|
let sign = chg >= 0 ? '+' : ''
|
||||||
|
return [
|
||||||
|
d,
|
||||||
|
'O: ' + o,
|
||||||
|
'C: ' + c,
|
||||||
|
'H: ' + h,
|
||||||
|
'L: ' + l,
|
||||||
|
'Chg: ' + sign + chg.toFixed(2) + ' (' + sign + chgPct.toFixed(2) + '%)',
|
||||||
|
].join('<br/>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用基础配置构建(legend/tooltip/grid/xAxis/yAxis/dataZoom)
|
||||||
|
function buildBaseOption(dates, series, formatter) {
|
||||||
|
return {
|
||||||
|
animation: false,
|
||||||
|
legend: {show: false},
|
||||||
|
tooltip: {trigger: 'axis', axisPointer: {type: 'cross'}, formatter},
|
||||||
|
grid: CONFIG.grid,
|
||||||
|
xAxis: {type: 'category', data: dates, boundaryGap: true, axisLine: {onZero: false}},
|
||||||
|
yAxis: {scale: true},
|
||||||
|
dataZoom: [
|
||||||
|
{type: 'inside', xAxisIndex: 0, start: 0, end: 100},
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
type: 'slider',
|
||||||
|
xAxisIndex: 0,
|
||||||
|
bottom: CONFIG.zoom.bottom,
|
||||||
|
height: CONFIG.zoom.height,
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range Band + Close Line(高低区间带 + 收盘线):趋势与波动范围直观
|
||||||
|
function buildRangeCloseOption(dataMap) {
|
||||||
|
const dates = Object.keys(dataMap).sort()
|
||||||
|
const lowArr = dates.map(d => dataMap[d][2])
|
||||||
|
const highArr = dates.map(d => dataMap[d][3])
|
||||||
|
const closeArr = dates.map(d => dataMap[d][1])
|
||||||
|
const rangeArr = highArr.map((h, i) => h - lowArr[i])
|
||||||
|
|
||||||
|
const series = [
|
||||||
|
{
|
||||||
|
name: 'Low',
|
||||||
|
type: 'line',
|
||||||
|
data: lowArr,
|
||||||
|
stack: 'range',
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {width: 0},
|
||||||
|
emphasis: {disabled: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Range',
|
||||||
|
type: 'line',
|
||||||
|
data: rangeArr,
|
||||||
|
stack: 'range',
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {width: 0},
|
||||||
|
areaStyle: {color: CONFIG.colors.down, opacity: 0.6},
|
||||||
|
z: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Close',
|
||||||
|
type: 'line',
|
||||||
|
data: closeArr,
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {color: CONFIG.colors.up, width: CONFIG.linewidth.closeLine},
|
||||||
|
z: 3,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return buildBaseOption(dates, series, makeTooltipFormatter(dataMap, dates))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal OHLC(竖线 + 左/右短横):信息等价于K线但形态简洁
|
||||||
|
function buildOHLCMinimalOption(dataMap) {
|
||||||
|
const dates = Object.keys(dataMap).sort()
|
||||||
|
const ohlcData = dates.map((d, i) => [i, ...dataMap[d]])
|
||||||
|
|
||||||
|
// 自定义渲染:竖线=高低区间;左短横=开盘;右短横=收盘;颜色=涨跌
|
||||||
|
function renderOHLCMinimal(params, api) {
|
||||||
|
let idx = api.value(0)
|
||||||
|
let open = api.value(1)
|
||||||
|
let close = api.value(2)
|
||||||
|
let low = api.value(3)
|
||||||
|
let high = api.value(4)
|
||||||
|
let up = close >= open
|
||||||
|
|
||||||
|
let x = api.coord([idx, 0])[0]
|
||||||
|
let highPoint = api.coord([idx, high])
|
||||||
|
let lowPoint = api.coord([idx, low])
|
||||||
|
let openPoint = api.coord([idx, open])
|
||||||
|
let closePoint = api.coord([idx, close])
|
||||||
|
|
||||||
|
let band = api.size([1, 0])[0]
|
||||||
|
let tick = Math.max(CONFIG.tick.min, Math.min(CONFIG.tick.max, band * CONFIG.tick.scale))
|
||||||
|
let color = up ? CONFIG.colors.up : CONFIG.colors.down
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'group',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
shape: {x1: x, y1: highPoint[1], x2: x, y2: lowPoint[1]},
|
||||||
|
style: {stroke: color, lineWidth: CONFIG.linewidth.stem, opacity: 0.9},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
shape: {x1: x - tick, y1: openPoint[1], x2: x, y2: openPoint[1]},
|
||||||
|
style: {stroke: color, lineWidth: CONFIG.linewidth.openTick, opacity: 0.95},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
shape: {x1: x, y1: closePoint[1], x2: x + tick, y2: closePoint[1]},
|
||||||
|
style: {stroke: color, lineWidth: CONFIG.linewidth.closeTick, opacity: 0.95},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const series = [
|
||||||
|
{
|
||||||
|
name: 'ohlc',
|
||||||
|
type: 'custom',
|
||||||
|
renderItem: renderOHLCMinimal,
|
||||||
|
encode: {x: 0, y: [1, 2, 3, 4]},
|
||||||
|
data: ohlcData,
|
||||||
|
z: 10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return buildBaseOption(dates, series, makeTooltipFormatter(dataMap, dates))
|
||||||
|
}
|
||||||
|
|
||||||
|
function candleChart(title, data) {
|
||||||
|
return {
|
||||||
|
type: 'service',
|
||||||
|
data: data,
|
||||||
|
body: {
|
||||||
|
className: 'mt-2',
|
||||||
|
type: 'chart',
|
||||||
|
height: 800,
|
||||||
|
config: {
|
||||||
|
title: {
|
||||||
|
text: title,
|
||||||
|
},
|
||||||
|
...buildRangeCloseOption(data['oclh']),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineChart(title, data) {
|
||||||
|
return {
|
||||||
|
type: 'service',
|
||||||
|
data: data,
|
||||||
|
body: {
|
||||||
|
className: 'mt-2',
|
||||||
|
type: 'chart',
|
||||||
|
height: 800,
|
||||||
|
config: {
|
||||||
|
title: {
|
||||||
|
text: title,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
position: 'left',
|
||||||
|
scale: true,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
formatter: function (value) {
|
||||||
|
return value.toFixed(2)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
type: 'dashed',
|
||||||
|
color: '#f0f0f0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: 'right',
|
||||||
|
scale: true,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
formatter: function (value) {
|
||||||
|
return value.toFixed(2)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
type: 'dashed',
|
||||||
|
color: '#f0f0f0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: true,
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'inside',
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
type: 'slider',
|
||||||
|
top: '90%',
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
yAxisIndex: 0,
|
||||||
|
data: '${yList || []}',
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(0,111,255,0.5)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = /*[[${charts}]]*/ [];
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const amis = amisRequire('amis/embed')
|
||||||
|
const amisJson = {
|
||||||
|
type: 'page',
|
||||||
|
title: 'Strategy',
|
||||||
|
body: {
|
||||||
|
type: 'tabs',
|
||||||
|
tabsMode: 'vertical',
|
||||||
|
tabs: data.map(item => {
|
||||||
|
let body = {}
|
||||||
|
if (item?.type === 'candle') {
|
||||||
|
body = Object.keys(item?.data ?? {})
|
||||||
|
.map(key => candleChart(key, item.data[key]))
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: item?.title,
|
||||||
|
body: Object.keys(item?.data ?? {})
|
||||||
|
.map(key => {
|
||||||
|
let value = item.data[key]
|
||||||
|
let type = value['type']
|
||||||
|
if (type) {
|
||||||
|
if (type === 'candle')
|
||||||
|
return candleChart(key, item.data[key])
|
||||||
|
else if (type === 'line')
|
||||||
|
return lineChart(key, item.data[key])
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(item => item),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
amis.embed('#root', amisJson, {}, {theme: 'antd'})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
228
leopard-strategy/src/main/resources/templates/report.html
Normal file
228
leopard-strategy/src/main/resources/templates/report.html
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<html lang='zh'>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'/>
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
|
||||||
|
<meta content='width=device-width, initial-scale=1.0' name='viewport'/>
|
||||||
|
<title>Strategy</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/antd.min.css" rel="stylesheet"/>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/helper.min.css" rel="stylesheet"/>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/iconfont.min.css" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
html, body, #root {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='root'></div>
|
||||||
|
</body>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/amis/6.13.0/sdk.min.js"></script>
|
||||||
|
<script th:inline="javascript" type='text/javascript'>
|
||||||
|
// Market data as KV: { 'YYYY-MM-DD': [open, close, low, high], ... }
|
||||||
|
const data = {
|
||||||
|
'2025-01-01': [100, 105, 98, 108],
|
||||||
|
'2025-01-02': [105, 102, 100, 107],
|
||||||
|
'2025-01-03': [102, 110, 101, 112],
|
||||||
|
'2025-01-04': [110, 108, 106, 113],
|
||||||
|
'2025-01-05': [108, 115, 107, 116],
|
||||||
|
'2025-01-06': [115, 117, 114, 120],
|
||||||
|
'2025-01-07': [117, 112, 111, 119],
|
||||||
|
'2025-01-08': [112, 118, 110, 121],
|
||||||
|
'2025-01-09': [118, 121, 117, 123],
|
||||||
|
'2025-01-10': [121, 119, 118, 122],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局配置(颜色、尺寸、间距等),集中管理,便于统一调整
|
||||||
|
const CONFIG = {
|
||||||
|
colors: {up: '#000000FF', down: '#00000045'},
|
||||||
|
grid: {left: '2%', right: '2%', top: 40, bottom: 110},
|
||||||
|
zoom: {bottom: 16, height: 50},
|
||||||
|
linewidth: {stem: 1.5, openTick: 1.2, closeTick: 1.6, closeLine: 1.5},
|
||||||
|
tick: {min: 4, max: 10, scale: 0.4},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用 tooltip 格式化(按索引回读原始 O/H/L/C)
|
||||||
|
function makeTooltipFormatter(dataMap, dates) {
|
||||||
|
return function (params) {
|
||||||
|
let p = Array.isArray(params) ? params[0] : params
|
||||||
|
let idx = p.dataIndex
|
||||||
|
let d = dates[idx]
|
||||||
|
let ohlc = dataMap[d] || []
|
||||||
|
let o = ohlc[0], c = ohlc[1], l = ohlc[2], h = ohlc[3]
|
||||||
|
let chg = (c - o)
|
||||||
|
let chgPct = o ? (chg / o * 100) : 0
|
||||||
|
let sign = chg >= 0 ? '+' : ''
|
||||||
|
return [
|
||||||
|
d,
|
||||||
|
'O: ' + o,
|
||||||
|
'C: ' + c,
|
||||||
|
'H: ' + h,
|
||||||
|
'L: ' + l,
|
||||||
|
'Chg: ' + sign + chg.toFixed(2) + ' (' + sign + chgPct.toFixed(2) + '%)',
|
||||||
|
].join('<br/>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用基础配置构建(legend/tooltip/grid/xAxis/yAxis/dataZoom)
|
||||||
|
function buildBaseOption(dates, series, formatter) {
|
||||||
|
return {
|
||||||
|
animation: false,
|
||||||
|
legend: {show: false},
|
||||||
|
tooltip: {trigger: 'axis', axisPointer: {type: 'cross'}, formatter},
|
||||||
|
grid: CONFIG.grid,
|
||||||
|
xAxis: {type: 'category', data: dates, boundaryGap: true, axisLine: {onZero: false}},
|
||||||
|
yAxis: {scale: true},
|
||||||
|
dataZoom: [
|
||||||
|
{type: 'inside', xAxisIndex: 0, start: 0, end: 100},
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
type: 'slider',
|
||||||
|
xAxisIndex: 0,
|
||||||
|
bottom: CONFIG.zoom.bottom,
|
||||||
|
height: CONFIG.zoom.height,
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range Band + Close Line(高低区间带 + 收盘线):趋势与波动范围直观
|
||||||
|
function buildRangeCloseOption(dataMap) {
|
||||||
|
const dates = Object.keys(dataMap).sort()
|
||||||
|
const lowArr = dates.map(d => dataMap[d][2])
|
||||||
|
const highArr = dates.map(d => dataMap[d][3])
|
||||||
|
const closeArr = dates.map(d => dataMap[d][1])
|
||||||
|
const rangeArr = highArr.map((h, i) => h - lowArr[i])
|
||||||
|
|
||||||
|
const series = [
|
||||||
|
{
|
||||||
|
name: 'Low',
|
||||||
|
type: 'line',
|
||||||
|
data: lowArr,
|
||||||
|
stack: 'range',
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {width: 0},
|
||||||
|
emphasis: {disabled: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Range',
|
||||||
|
type: 'line',
|
||||||
|
data: rangeArr,
|
||||||
|
stack: 'range',
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {width: 0},
|
||||||
|
areaStyle: {color: CONFIG.colors.down, opacity: 0.6},
|
||||||
|
z: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Close',
|
||||||
|
type: 'line',
|
||||||
|
data: closeArr,
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {color: CONFIG.colors.up, width: CONFIG.linewidth.closeLine},
|
||||||
|
z: 3,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return buildBaseOption(dates, series, makeTooltipFormatter(dataMap, dates))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal OHLC(竖线 + 左/右短横):信息等价于K线但形态简洁
|
||||||
|
function buildOHLCMinimalOption(dataMap) {
|
||||||
|
const dates = Object.keys(dataMap).sort()
|
||||||
|
const ohlcData = dates.map((d, i) => [i, ...dataMap[d]])
|
||||||
|
|
||||||
|
// 自定义渲染:竖线=高低区间;左短横=开盘;右短横=收盘;颜色=涨跌
|
||||||
|
function renderOHLCMinimal(params, api) {
|
||||||
|
let idx = api.value(0)
|
||||||
|
let open = api.value(1)
|
||||||
|
let close = api.value(2)
|
||||||
|
let low = api.value(3)
|
||||||
|
let high = api.value(4)
|
||||||
|
let up = close >= open
|
||||||
|
|
||||||
|
let x = api.coord([idx, 0])[0]
|
||||||
|
let highPoint = api.coord([idx, high])
|
||||||
|
let lowPoint = api.coord([idx, low])
|
||||||
|
let openPoint = api.coord([idx, open])
|
||||||
|
let closePoint = api.coord([idx, close])
|
||||||
|
|
||||||
|
let band = api.size([1, 0])[0]
|
||||||
|
let tick = Math.max(CONFIG.tick.min, Math.min(CONFIG.tick.max, band * CONFIG.tick.scale))
|
||||||
|
let color = up ? CONFIG.colors.up : CONFIG.colors.down
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'group',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
shape: {x1: x, y1: highPoint[1], x2: x, y2: lowPoint[1]},
|
||||||
|
style: {stroke: color, lineWidth: CONFIG.linewidth.stem, opacity: 0.9},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
shape: {x1: x - tick, y1: openPoint[1], x2: x, y2: openPoint[1]},
|
||||||
|
style: {stroke: color, lineWidth: CONFIG.linewidth.openTick, opacity: 0.95},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
shape: {x1: x, y1: closePoint[1], x2: x + tick, y2: closePoint[1]},
|
||||||
|
style: {stroke: color, lineWidth: CONFIG.linewidth.closeTick, opacity: 0.95},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const series = [
|
||||||
|
{
|
||||||
|
name: 'ohlc',
|
||||||
|
type: 'custom',
|
||||||
|
renderItem: renderOHLCMinimal,
|
||||||
|
encode: {x: 0, y: [1, 2, 3, 4]},
|
||||||
|
data: ohlcData,
|
||||||
|
z: 10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return buildBaseOption(dates, series, makeTooltipFormatter(dataMap, dates))
|
||||||
|
}
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const amis = amisRequire('amis/embed')
|
||||||
|
const amisJson = {
|
||||||
|
type: 'page',
|
||||||
|
title: 'Strategy',
|
||||||
|
body: {
|
||||||
|
type: 'tabs',
|
||||||
|
tabsMode: 'vertical',
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
title: 'Charts',
|
||||||
|
// 在一个 Tab 中展示两张图,便于同屏对比
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: 'chart',
|
||||||
|
height: 500,
|
||||||
|
config: buildRangeCloseOption(data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'chart',
|
||||||
|
height: 500,
|
||||||
|
config: buildOHLCMinimalOption(data),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// `dates` 已直接在图表配置中使用,这里无需通过 amis data 传递
|
||||||
|
amis.embed('#root', amisJson, {}, {theme: 'antd'})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {AlertComponent, attachmentAdpator, makeTranslator, render, type Schema, ToastComponent} from 'amis'
|
import {AlertComponent, type Api, attachmentAdpator, makeTranslator, render, type Schema, ToastComponent} from 'amis'
|
||||||
import 'amis/lib/themes/antd.css'
|
import 'amis/lib/themes/antd.css'
|
||||||
import 'amis/lib/helper.css'
|
import 'amis/lib/helper.css'
|
||||||
import 'amis/sdk/iconfont.css'
|
import 'amis/sdk/iconfont.css'
|
||||||
@@ -561,6 +561,187 @@ 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,
|
||||||
|
config: {
|
||||||
|
title: {
|
||||||
|
text: title,
|
||||||
|
subtext: subtitle,
|
||||||
|
},
|
||||||
|
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: '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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: true,
|
||||||
|
axisLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'inside',
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
type: 'slider',
|
||||||
|
top: '90%',
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'candlestick',
|
||||||
|
data: '${yList || []}',
|
||||||
|
yAxisIndex: 0,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#eb5454',
|
||||||
|
color0: '#4aaa93',
|
||||||
|
borderColor: '#eb5454',
|
||||||
|
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)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function stockListColumns(idField: string = 'id', extraColumns: Array<ColumnSchema> = []) {
|
export function stockListColumns(idField: string = 'id', extraColumns: Array<ColumnSchema> = []) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -803,137 +984,21 @@ export function stockListColumns(idField: string = 'id', extraColumns: Array<Col
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
candleChart(
|
||||||
type: 'chart',
|
'100日线数据',
|
||||||
title: '100日线数据',
|
'后复权数据',
|
||||||
height: 500,
|
`get:${commonInfo.baseUrl}/stock/daily/\${${idField}}`,
|
||||||
api: `get:${commonInfo.baseUrl}/stock/daily/\${${idField}}`,
|
),
|
||||||
config: {
|
candleChart(
|
||||||
title: {
|
'50周线数据',
|
||||||
text: '100日线数据',
|
'后复权数据',
|
||||||
subtext: '后复权数据',
|
`get:${commonInfo.baseUrl}/stock/weekly/\${${idField}}`,
|
||||||
},
|
),
|
||||||
backgroundColor: '#fff',
|
candleChart(
|
||||||
animation: true,
|
'24月线数据',
|
||||||
animationDuration: 1000,
|
'后复权数据',
|
||||||
tooltip: {
|
`get:${commonInfo.baseUrl}/stock/monthly/\${${idField}}`,
|
||||||
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: '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,
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
5
pom.xml
5
pom.xml
@@ -63,6 +63,11 @@
|
|||||||
<artifactId>hutool-http</artifactId>
|
<artifactId>hutool-http</artifactId>
|
||||||
<version>${hutool.version}</version>
|
<version>${hutool.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-extra</artifactId>
|
||||||
|
<version>${hutool.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.yomahub</groupId>
|
<groupId>com.yomahub</groupId>
|
||||||
|
|||||||
Reference in New Issue
Block a user