Compare commits
51 Commits
d2b3305ca6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6179137e0d | |||
| 958bc459b2 | |||
| 7eec44f21c | |||
| 5f42d36436 | |||
| 9a31b8cae4 | |||
| 482eb70465 | |||
| 7703f88d7f | |||
| db8a094c8f | |||
| f4fef2bd95 | |||
| 53a6d33fd5 | |||
| 83574e1229 | |||
| e2c5729f87 | |||
| 47f8b30a02 | |||
| 452d1c681d | |||
| 42b402d4ef | |||
| 8c4e3baacb | |||
| a075adf4b6 | |||
| 963c2a9878 | |||
| e387fc839f | |||
| b9a02194e2 | |||
| b0c2530e63 | |||
| 49a03adf21 | |||
| 5390a879e7 | |||
| 69020852b9 | |||
| 846c6fe819 | |||
| d3f337e2c4 | |||
| d4fec4c426 | |||
| 0dd421ca43 | |||
| 3991effa88 | |||
| 02508a5426 | |||
| edd18061eb | |||
| 3d428d9d0a | |||
| b4e2c81d36 | |||
| 1edd74e35d | |||
| 7d3b3758f3 | |||
| d6aab42892 | |||
| 5cd4875cf9 | |||
| f046427480 | |||
| 01690bbcd6 | |||
| 8011a4f2cb | |||
| a9b2561be1 | |||
| dd81ca1150 | |||
| 01bd5ed178 | |||
| a4db463dbd | |||
| 68aa6ff33f | |||
| 19dd19a9f8 | |||
| 5953c9b9f2 | |||
| f8ee51c0ed | |||
| 0b9cb55788 | |||
| 585b37a1cc | |||
| 868feeb34d |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -252,3 +252,4 @@ gradle-app.setting
|
|||||||
!.yarn/cache
|
!.yarn/cache
|
||||||
nohup.out
|
nohup.out
|
||||||
!leopard*/bin
|
!leopard*/bin
|
||||||
|
archive
|
||||||
|
|||||||
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>
|
||||||
|
|||||||
15
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
15
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="NullableProblems" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="REPORT_NULLABLE_METHOD_OVERRIDES_NOTNULL" value="true" />
|
||||||
|
<option name="REPORT_NOT_ANNOTATED_METHOD_OVERRIDES_NOTNULL" value="true" />
|
||||||
|
<option name="REPORT_NOTNULL_PARAMETER_OVERRIDES_NULLABLE" value="true" />
|
||||||
|
<option name="REPORT_NOT_ANNOTATED_PARAMETER_OVERRIDES_NOTNULL" value="true" />
|
||||||
|
<option name="REPORT_NOT_ANNOTATED_GETTER" value="true" />
|
||||||
|
<option name="REPORT_NOT_ANNOTATED_SETTER_PARAMETER" value="true" />
|
||||||
|
<option name="REPORT_ANNOTATION_NOT_PROPAGATED_TO_OVERRIDERS" value="true" />
|
||||||
|
<option name="REPORT_NULLS_PASSED_TO_NON_ANNOTATED_METHOD" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -8,7 +8,7 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="temurin-17" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -20,10 +20,33 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-json</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-core</artifactId>
|
<artifactId>hutool-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-http</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.icepear.echarts</groupId>
|
||||||
|
<artifactId>echarts-java</artifactId>
|
||||||
|
<version>1.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.ralfkonrad.quantlib_for_maven</groupId>
|
||||||
|
<artifactId>quantlib</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ta4j</groupId>
|
||||||
|
<artifactId>ta4j-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.hibernate.orm</groupId>
|
<groupId>org.hibernate.orm</groupId>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.core.entity;
|
package com.lanyuanxiaoyao.leopard.core.entity;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.lanyuanxiaoyao.leopard.core.Constants;
|
import com.lanyuanxiaoyao.leopard.core.Constants;
|
||||||
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
|
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
@@ -29,6 +30,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
|||||||
@Table(name = Constants.DATABASE_PREFIX + "daily")
|
@Table(name = Constants.DATABASE_PREFIX + "daily")
|
||||||
public class Daily extends SimpleEntity {
|
public class Daily extends SimpleEntity {
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
@Comment("交易日")
|
||||||
private LocalDate tradeDate;
|
private LocalDate tradeDate;
|
||||||
@Comment("开盘价")
|
@Comment("开盘价")
|
||||||
private Double open;
|
private Double open;
|
||||||
@@ -55,4 +57,20 @@ public class Daily extends SimpleEntity {
|
|||||||
@JoinColumn(nullable = false)
|
@JoinColumn(nullable = false)
|
||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
private Stock stock;
|
private Stock stock;
|
||||||
|
|
||||||
|
public Double getHfqOpen() {
|
||||||
|
return open * ObjectUtil.defaultIfNull(factor, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getHfqClose() {
|
||||||
|
return close * ObjectUtil.defaultIfNull(factor, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getHfqHigh() {
|
||||||
|
return high * ObjectUtil.defaultIfNull(factor, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getHfqLow() {
|
||||||
|
return low * ObjectUtil.defaultIfNull(factor, 1.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.lanyuanxiaoyao.leopard.core.entity;
|
|||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.Constants;
|
import com.lanyuanxiaoyao.leopard.core.Constants;
|
||||||
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
|
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.EntityListeners;
|
import jakarta.persistence.EntityListeners;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
@@ -27,6 +28,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
|||||||
public class FinanceIndicator extends SimpleEntity {
|
public class FinanceIndicator extends SimpleEntity {
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
private Stock stock;
|
private Stock stock;
|
||||||
|
@Column(name = "`year`", nullable = false)
|
||||||
@Comment("年报年度")
|
@Comment("年报年度")
|
||||||
private Integer year;
|
private Integer year;
|
||||||
@Comment("总股本")
|
@Comment("总股本")
|
||||||
|
|||||||
@@ -65,6 +65,19 @@ public class Stock extends SimpleEntity {
|
|||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
private Set<FinanceIndicator> indicators;
|
private Set<FinanceIndicator> indicators;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
Stock stock = (Stock) o;
|
||||||
|
return code.equals(stock.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return code.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public enum Market implements SimpleEnum {
|
public enum Market implements SimpleEnum {
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ package com.lanyuanxiaoyao.leopard.core.entity;
|
|||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.Constants;
|
import com.lanyuanxiaoyao.leopard.core.Constants;
|
||||||
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
|
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
|
||||||
|
import jakarta.persistence.CascadeType;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.EntityListeners;
|
import jakarta.persistence.EntityListeners;
|
||||||
import jakarta.persistence.ManyToMany;
|
import jakarta.persistence.OneToMany;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -31,7 +32,7 @@ public class StockCollection extends SimpleEntity {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@ManyToMany
|
@OneToMany(cascade = CascadeType.ALL)
|
||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
private Set<Stock> stocks;
|
private Set<StockScore> scores;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ package com.lanyuanxiaoyao.leopard.core.entity;
|
|||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.Constants;
|
import com.lanyuanxiaoyao.leopard.core.Constants;
|
||||||
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
|
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.ElementCollection;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.EntityListeners;
|
import jakarta.persistence.EntityListeners;
|
||||||
|
import jakarta.persistence.JoinTable;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import java.util.Map;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
@@ -22,18 +25,14 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
|||||||
@DynamicUpdate
|
@DynamicUpdate
|
||||||
@DynamicInsert
|
@DynamicInsert
|
||||||
@EntityListeners(AuditingEntityListener.class)
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
@Table(name = Constants.DATABASE_PREFIX + "task_template")
|
@Table(name = Constants.DATABASE_PREFIX + "stock_score")
|
||||||
public class TaskTemplate extends SimpleEntity {
|
public class StockScore extends SimpleEntity {
|
||||||
@Column(nullable = false)
|
@ManyToOne
|
||||||
private String name;
|
private Stock stock;
|
||||||
@Column(nullable = false, length = 500)
|
@ManyToOne
|
||||||
private String description;
|
private StockCollection collection;
|
||||||
@Column(nullable = false)
|
@ElementCollection
|
||||||
private String application;
|
@JoinTable(name = Constants.DATABASE_PREFIX + "stock_score_extra")
|
||||||
@Column(nullable = false)
|
private Map<String, String> extra;
|
||||||
private String chain;
|
private Double score;
|
||||||
@Column(nullable = false)
|
}
|
||||||
private String expression;
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String expressionEl;
|
|
||||||
}
|
|
||||||
@@ -61,7 +61,7 @@ public class Task extends SimpleEntity {
|
|||||||
private Status status = Status.RUNNING;
|
private Status status = Status.RUNNING;
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
@Comment("任务进度")
|
@Comment("任务进度")
|
||||||
private Integer step = 0;
|
private Double step = 0.0;
|
||||||
@Comment("任务开始时间")
|
@Comment("任务开始时间")
|
||||||
private LocalDateTime launchedTime;
|
private LocalDateTime launchedTime;
|
||||||
@Comment("任务结束时间")
|
@Comment("任务结束时间")
|
||||||
|
|||||||
@@ -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,18 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.entity.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
public record Monthly(
|
||||||
|
LocalDate tradeDate,
|
||||||
|
int year,
|
||||||
|
int month,
|
||||||
|
Double open,
|
||||||
|
Double high,
|
||||||
|
Double low,
|
||||||
|
Double close,
|
||||||
|
Double priceChangeAmount,
|
||||||
|
Double priceFluctuationRange,
|
||||||
|
Double volume,
|
||||||
|
Double turnover
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.entity.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
public record Weekly(
|
||||||
|
LocalDate tradeDate,
|
||||||
|
int year,
|
||||||
|
int week,
|
||||||
|
Double open,
|
||||||
|
Double high,
|
||||||
|
Double low,
|
||||||
|
Double close,
|
||||||
|
Double priceChangeAmount,
|
||||||
|
Double priceFluctuationRange,
|
||||||
|
Double volume,
|
||||||
|
Double turnover
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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,17 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.entity.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
public record Yearly(
|
||||||
|
LocalDate tradeDate,
|
||||||
|
int year,
|
||||||
|
Double open,
|
||||||
|
Double high,
|
||||||
|
Double low,
|
||||||
|
Double close,
|
||||||
|
Double priceChangeAmount,
|
||||||
|
Double priceFluctuationRange,
|
||||||
|
Double volume,
|
||||||
|
Double turnover
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.helper;
|
package com.lanyuanxiaoyao.leopard.core.helper;
|
||||||
|
|
||||||
import cn.hutool.core.util.NumberUtil;
|
import cn.hutool.core.util.NumberUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
@@ -42,6 +42,20 @@ public class NumberHelper {
|
|||||||
return NumberUtil.decimalFormat("0.00%", value);
|
return NumberUtil.decimalFormat("0.00%", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String formatPriceDouble(Double value) {
|
||||||
|
if (ObjectUtil.isNull(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return NumberUtil.decimalFormat("0.00", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatPriceDouble(Integer value) {
|
||||||
|
if (ObjectUtil.isNull(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return NumberUtil.decimalFormat("0.00", value);
|
||||||
|
}
|
||||||
|
|
||||||
public static Double parseDouble(String value) {
|
public static Double parseDouble(String value) {
|
||||||
if (StrUtil.isBlank(value)) {
|
if (StrUtil.isBlank(value)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,12 @@ package com.lanyuanxiaoyao.leopard.core.repository;
|
|||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
||||||
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
|
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
|
||||||
|
import com.querydsl.core.types.OrderSpecifier;
|
||||||
import com.querydsl.core.types.Predicate;
|
import com.querydsl.core.types.Predicate;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.springframework.data.domain.Sort;
|
import java.util.Set;
|
||||||
import org.springframework.data.jpa.repository.EntityGraph;
|
import org.springframework.data.jpa.repository.EntityGraph;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
@@ -14,10 +15,16 @@ import org.springframework.stereotype.Repository;
|
|||||||
@Repository
|
@Repository
|
||||||
public interface DailyRepository extends SimpleRepository<Daily> {
|
public interface DailyRepository extends SimpleRepository<Daily> {
|
||||||
@Query("select distinct daily.tradeDate from Daily daily")
|
@Query("select distinct daily.tradeDate from Daily daily")
|
||||||
List<LocalDate> findDistinctTradeDate();
|
Set<LocalDate> findDistinctTradeDate();
|
||||||
|
|
||||||
@Query("select distinct daily.tradeDate from Daily daily where daily.stock.id = ?1")
|
@Query("select max(daily.tradeDate) from Daily daily")
|
||||||
List<LocalDate> findDistinctTradeDateByStockId(Long stockId);
|
LocalDate findMaxTradeDate();
|
||||||
|
|
||||||
|
@Query("select min(daily.tradeDate) from Daily daily")
|
||||||
|
LocalDate findMinTradeDate();
|
||||||
|
|
||||||
|
@Query("from Daily daily where daily.stock.id = ?1 order by daily.tradeDate desc limit 1")
|
||||||
|
Optional<Daily> findLatest(Long stockId);
|
||||||
|
|
||||||
@EntityGraph(attributePaths = {"stock"})
|
@EntityGraph(attributePaths = {"stock"})
|
||||||
@Override
|
@Override
|
||||||
@@ -25,5 +32,9 @@ public interface DailyRepository extends SimpleRepository<Daily> {
|
|||||||
|
|
||||||
@EntityGraph(attributePaths = {"stock"})
|
@EntityGraph(attributePaths = {"stock"})
|
||||||
@Override
|
@Override
|
||||||
List<Daily> findAll(Predicate predicate, Sort sort);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
|||||||
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
|
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.Set;
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
@@ -16,12 +16,12 @@ import org.springframework.stereotype.Repository;
|
|||||||
@Repository
|
@Repository
|
||||||
public interface StockRepository extends SimpleRepository<Stock> {
|
public interface StockRepository extends SimpleRepository<Stock> {
|
||||||
@Query("select distinct stock.industry from Stock stock where stock.industry is not null")
|
@Query("select distinct stock.industry from Stock stock where stock.industry is not null")
|
||||||
List<String> findDistinctIndustries();
|
Set<String> findDistinctIndustries();
|
||||||
|
|
||||||
@Query("select distinct stock.code from Stock stock")
|
@Query("select distinct stock.code from Stock stock")
|
||||||
List<String> findDistinctCodes();
|
Set<String> findDistinctCodes();
|
||||||
|
|
||||||
@Modifying
|
|
||||||
@Transactional(rollbackOn = Throwable.class)
|
@Transactional(rollbackOn = Throwable.class)
|
||||||
|
@Modifying
|
||||||
void deleteAllByCodeIn(Collection<String> code);
|
void deleteAllByCodeIn(Collection<String> code);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
|
|||||||
import org.springframework.data.jpa.repository.Modifying;
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author lanyuanxiaoyao
|
* @author lanyuanxiaoyao
|
||||||
@@ -13,10 +14,15 @@ import org.springframework.stereotype.Repository;
|
|||||||
@Repository
|
@Repository
|
||||||
public interface TaskRepository extends SimpleRepository<Task> {
|
public interface TaskRepository extends SimpleRepository<Task> {
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query("update Task task set task.status = com.lanyuanxiaoyao.leopard.core.entity.Task.Status.FAILURE where task.status = com.lanyuanxiaoyao.leopard.core.entity.Task.Status.RUNNING")
|
@Query("""
|
||||||
|
update Task task set
|
||||||
|
task.status = com.lanyuanxiaoyao.leopard.core.entity.Task.Status.FAILURE,
|
||||||
|
task.finishedTime = current timestamp
|
||||||
|
where task.status = com.lanyuanxiaoyao.leopard.core.entity.Task.Status.RUNNING""")
|
||||||
void updateAllRunningTaskToFailure();
|
void updateAllRunningTaskToFailure();
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Throwable.class)
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query("update Task task set task.step = ?1 where task.id = ?2")
|
@Query("update Task task set task.step = ?2 where task.id = ?1")
|
||||||
void updateStepById(Integer step, Long id);
|
void updateStepById(Long id, Double step);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.core.repository;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate;
|
|
||||||
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface TaskTemplateRepository extends SimpleRepository<TaskTemplate> {
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
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.QDaily;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 股票评估
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250924
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class AssessmentService {
|
||||||
|
private final IndustryService industryService;
|
||||||
|
private final DailyRepository dailyRepository;
|
||||||
|
|
||||||
|
public AssessmentService(IndustryService industryService, DailyRepository dailyRepository) {
|
||||||
|
this.industryService = industryService;
|
||||||
|
this.dailyRepository = dailyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Result> assess(Set<Stock> stocks, int year) {
|
||||||
|
if (ObjectUtil.isNotEmpty(stocks)) {
|
||||||
|
var topChange = industryService.topChange(year, stocks);
|
||||||
|
var dailyMap = dailyRepository.findAll(
|
||||||
|
QDaily.daily.tradeDate.year().eq(year)
|
||||||
|
.and(QDaily.daily.stock.in(stocks))
|
||||||
|
)
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(Daily::getStock));
|
||||||
|
return stocks
|
||||||
|
.stream()
|
||||||
|
.filter(stock -> {
|
||||||
|
if (!dailyMap.containsKey(stock) || ObjectUtil.isEmpty(dailyMap.get(stock))) {
|
||||||
|
log.warn("Cannot find daily data in {} for {}", year, stock.getCode());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(stock -> {
|
||||||
|
var dailies = dailyMap.get(stock)
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
|
.toList();
|
||||||
|
var change = getChange(dailies);
|
||||||
|
var std = getStd(dailies);
|
||||||
|
var industryTop = topChange.getOrDefault(new IndustryService.IndustryYearlyKey(stock.getIndustry(), year), 0.0);
|
||||||
|
return new Result(stock, change, std, industryTop);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getChange(List<Daily> dailies) {
|
||||||
|
return (dailies.getLast().getHfqClose() - dailies.getFirst().getHfqClose()) / dailies.getFirst().getHfqClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getStd(List<Daily> dailies) {
|
||||||
|
var statistics = new DescriptiveStatistics();
|
||||||
|
dailies.forEach(daily -> statistics.addValue(daily.getHfqClose()));
|
||||||
|
return statistics.getStandardDeviation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Result(Stock stock, double change, double std, double industryTop) {
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
Result result = (Result) o;
|
||||||
|
return stock.equals(result.stock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return stock.hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
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.QDaily;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算行业相关指标
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250924
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class IndustryService {
|
||||||
|
private final StockRepository stockRepository;
|
||||||
|
private final DailyRepository dailyRepository;
|
||||||
|
|
||||||
|
public IndustryService(StockRepository stockRepository, DailyRepository dailyRepository) {
|
||||||
|
this.stockRepository = stockRepository;
|
||||||
|
this.dailyRepository = dailyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<IndustryYearlyKey, Double> topChange(int year) {
|
||||||
|
return topChange(year, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<IndustryYearlyKey, Double> topChange(int year, Set<Stock> includeStocks) {
|
||||||
|
return topChange(year, year, includeStocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<IndustryYearlyKey, Double> topChange(int startYear, int endYear) {
|
||||||
|
return topChange(startYear, endYear, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<IndustryYearlyKey, Double> topChange(int startYear, int endYear, Set<Stock> includeStocks) {
|
||||||
|
var includeIndustries = ObjectUtil.isNull(includeStocks)
|
||||||
|
? null
|
||||||
|
: includeStocks.stream().map(Stock::getIndustry).collect(Collectors.toSet());
|
||||||
|
return stockRepository.findDistinctIndustries()
|
||||||
|
.parallelStream()
|
||||||
|
.filter(o -> ObjectUtil.isNull(includeIndustries) || includeIndustries.contains(o))
|
||||||
|
.flatMap(industry -> {
|
||||||
|
var keys = new ArrayList<IndustryYearlyKey>();
|
||||||
|
for (int year = startYear; year <= endYear; year++) {
|
||||||
|
keys.add(new IndustryYearlyKey(industry, year));
|
||||||
|
}
|
||||||
|
return keys.stream();
|
||||||
|
})
|
||||||
|
.map(key -> {
|
||||||
|
var maxChange = dailyRepository
|
||||||
|
.findAll(
|
||||||
|
QDaily.daily.stock.industry.eq(key.industry())
|
||||||
|
.and(QDaily.daily.stock.in(includeStocks))
|
||||||
|
.and(QDaily.daily.tradeDate.year().eq(key.year())),
|
||||||
|
QDaily.daily.tradeDate.asc()
|
||||||
|
)
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(Daily::getStock))
|
||||||
|
.values()
|
||||||
|
.stream()
|
||||||
|
.mapToDouble(dailies -> {
|
||||||
|
var dailiesSorted = dailies
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
|
.toList();
|
||||||
|
return (dailiesSorted.getLast().getHfqClose() - dailiesSorted.getFirst().getHfqClose()) / dailiesSorted.getFirst().getHfqClose();
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.orElse(0.0);
|
||||||
|
return Map.entry(key, maxChange);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record IndustryYearlyKey(String industry, int year) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record IndustryYearlyData(
|
||||||
|
String industry,
|
||||||
|
int year,
|
||||||
|
double maxChange,
|
||||||
|
double minChange,
|
||||||
|
double avgChange,
|
||||||
|
double medianChange
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.service;
|
package com.lanyuanxiaoyao.leopard.core.service;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
|
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository;
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.service;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
|
||||||
|
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;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
|
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.temporal.ChronoField;
|
||||||
|
import java.time.temporal.WeekFields;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class StockService extends SimpleServiceSupport<Stock> {
|
||||||
|
private final FinanceIndicatorRepository financeIndicatorRepository;
|
||||||
|
private final DailyRepository dailyRepository;
|
||||||
|
|
||||||
|
public StockService(StockRepository repository, FinanceIndicatorRepository financeIndicatorRepository, DailyRepository dailyRepository) {
|
||||||
|
super(repository);
|
||||||
|
this.financeIndicatorRepository = financeIndicatorRepository;
|
||||||
|
this.dailyRepository = dailyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "findFinanceIndicator", cacheManager = "long-cache", sync = true)
|
||||||
|
public Optional<FinanceIndicator> findFinanceIndicator(Long stockId, Integer year) {
|
||||||
|
return financeIndicatorRepository.findOne(
|
||||||
|
QFinanceIndicator.financeIndicator.year.eq(year)
|
||||||
|
.and(QFinanceIndicator.financeIndicator.stock.id.eq(stockId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "findFinanceIndicatorRecent", cacheManager = "long-cache", sync = true)
|
||||||
|
public List<FinanceIndicator> findFinanceIndicatorRecent(Long stockId, int years) {
|
||||||
|
var current = LocalDate.now();
|
||||||
|
return financeIndicatorRepository.findAll(
|
||||||
|
QFinanceIndicator.financeIndicator.stock.id.eq(stockId)
|
||||||
|
.and(QFinanceIndicator.financeIndicator.year.between(current.minusYears(years).getYear(), current.getYear())),
|
||||||
|
QFinanceIndicator.financeIndicator.year.asc()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "findDailyRecent", cacheManager = "long-cache", sync = true)
|
||||||
|
public List<Daily> findDailyRecent(Long stockId, int days) {
|
||||||
|
return dailyRepository.findRecent(stockId, days)
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "findDailyLatest", cacheManager = "long-cache", sync = true)
|
||||||
|
public Optional<Daily> findDailyLatest(Long stockId) {
|
||||||
|
return dailyRepository.findLatest(stockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "findYearlyRecent", cacheManager = "long-cache", sync = true)
|
||||||
|
public List<Yearly> findYearlyRecent(Long stockId, int years) {
|
||||||
|
var current = LocalDate.now().withMonth(1).withDayOfMonth(1);
|
||||||
|
var start = current.minusYears(years).getYear();
|
||||||
|
var end = current.getYear();
|
||||||
|
return dailyRepository
|
||||||
|
.findAll(
|
||||||
|
QDaily.daily.stock.id.eq(stockId)
|
||||||
|
.and(QDaily.daily.tradeDate.year().gt(start))
|
||||||
|
.and(QDaily.daily.tradeDate.year().loe(end)),
|
||||||
|
QDaily.daily.tradeDate.asc()
|
||||||
|
)
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(daily -> daily.getTradeDate().getYear()))
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(entry -> {
|
||||||
|
var year = entry.getKey();
|
||||||
|
var dailies = entry.getValue();
|
||||||
|
var open = dailies.getFirst().getHfqOpen();
|
||||||
|
var close = dailies.getLast().getHfqClose();
|
||||||
|
return new Yearly(
|
||||||
|
LocalDate.of(year, 1, 1),
|
||||||
|
year,
|
||||||
|
open,
|
||||||
|
maxFromDaily(dailies, Daily::getHfqHigh),
|
||||||
|
minFromDaily(dailies, Daily::getHfqLow),
|
||||||
|
close,
|
||||||
|
close - open,
|
||||||
|
(close - open) / open * 100,
|
||||||
|
sumFromDaily(dailies, Daily::getVolume),
|
||||||
|
sumFromDaily(dailies, Daily::getTurnover)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sorted(Comparator.comparingInt(Yearly::year))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "findMonthlyRecent", cacheManager = "long-cache", sync = true)
|
||||||
|
public List<Monthly> findMonthlyRecent(Long stockId, int months) {
|
||||||
|
var end = LocalDate.now().withDayOfMonth(1);
|
||||||
|
var start = end.minusMonths(months);
|
||||||
|
return dailyRepository
|
||||||
|
.findAll(
|
||||||
|
QDaily.daily.stock.id.eq(stockId)
|
||||||
|
.and(
|
||||||
|
QDaily.daily.tradeDate.year().gt(start.getYear())
|
||||||
|
.or(
|
||||||
|
QDaily.daily.tradeDate.year().eq(start.getYear())
|
||||||
|
.and(QDaily.daily.tradeDate.month().gt(start.getMonthValue()))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.and(
|
||||||
|
QDaily.daily.tradeDate.year().lt(end.getYear())
|
||||||
|
.or(
|
||||||
|
QDaily.daily.tradeDate.year().eq(end.getYear())
|
||||||
|
.and(QDaily.daily.tradeDate.month().loe(end.getMonthValue()))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
QDaily.daily.tradeDate.asc()
|
||||||
|
)
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(daily -> new YearAndMonth(daily.getTradeDate().getYear(), daily.getTradeDate().getMonthValue())))
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(entry -> {
|
||||||
|
var yearAndMonth = entry.getKey();
|
||||||
|
var dailies = entry.getValue();
|
||||||
|
var open = dailies.getFirst().getHfqOpen();
|
||||||
|
var close = dailies.getLast().getHfqClose();
|
||||||
|
return new Monthly(
|
||||||
|
LocalDate.of(yearAndMonth.year(), yearAndMonth.month(), 1),
|
||||||
|
yearAndMonth.year(),
|
||||||
|
yearAndMonth.month(),
|
||||||
|
open,
|
||||||
|
maxFromDaily(dailies, Daily::getHfqHigh),
|
||||||
|
minFromDaily(dailies, Daily::getHfqLow),
|
||||||
|
close,
|
||||||
|
close - open,
|
||||||
|
(close - open) / open * 100,
|
||||||
|
sumFromDaily(dailies, Daily::getVolume),
|
||||||
|
sumFromDaily(dailies, Daily::getTurnover)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sorted(Comparator.comparingInt(monthly -> monthly.year() * 100 + monthly.month()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "findWeeklyRecent", cacheManager = "long-cache", sync = true)
|
||||||
|
public List<Weekly> findWeeklyRecent(Long stockId, int weeks) {
|
||||||
|
var end = LocalDate.now().with(ChronoField.DAY_OF_WEEK, 1);
|
||||||
|
var start = end.minusWeeks(weeks);
|
||||||
|
return dailyRepository
|
||||||
|
.findAll(
|
||||||
|
QDaily.daily.stock.id.eq(stockId)
|
||||||
|
.and(
|
||||||
|
QDaily.daily.tradeDate.year().gt(start.getYear())
|
||||||
|
.or(
|
||||||
|
QDaily.daily.tradeDate.year().eq(start.getYear())
|
||||||
|
.and(QDaily.daily.tradeDate.week().gt(start.get(WeekFields.ISO.weekOfYear())))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.and(
|
||||||
|
QDaily.daily.tradeDate.year().lt(end.getYear())
|
||||||
|
.or(
|
||||||
|
QDaily.daily.tradeDate.year().eq(end.getYear())
|
||||||
|
.and(QDaily.daily.tradeDate.month().loe(end.get(WeekFields.ISO.weekOfYear())))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
QDaily.daily.tradeDate.asc()
|
||||||
|
)
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(daily -> new YearAndWeek(daily.getTradeDate().getYear(), daily.getTradeDate().get(WeekFields.ISO.weekOfYear()))))
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(entry -> {
|
||||||
|
var yearAndWeek = entry.getKey();
|
||||||
|
var dailies = entry.getValue();
|
||||||
|
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(),
|
||||||
|
open,
|
||||||
|
maxFromDaily(dailies, Daily::getHfqHigh),
|
||||||
|
minFromDaily(dailies, Daily::getHfqLow),
|
||||||
|
close,
|
||||||
|
close - open,
|
||||||
|
(close - open) / open * 100,
|
||||||
|
sumFromDaily(dailies, Daily::getVolume),
|
||||||
|
sumFromDaily(dailies, Daily::getTurnover)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sorted(Comparator.comparingInt(weekly -> weekly.year() * 100 + weekly.week()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Task;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.TaskRepository;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.task.PyramidSelect;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.task.TaskRunner;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.task.UpdateDailyTask;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.task.UpdateFinanceIndicatorTask;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.task.UpdateStockTask;
|
||||||
|
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250829
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class TaskService extends SimpleServiceSupport<Task> {
|
||||||
|
private final ExecutorService executor = Executors.newFixedThreadPool(50);
|
||||||
|
private final ApplicationContext context;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Set<TaskTemplate> templates = Stream.of(
|
||||||
|
new TaskTemplate("b29f76a5-b07d-4182-85f8-2641c2a975c1", "更新股票信息", "更新股票信息", UpdateStockTask.class),
|
||||||
|
new TaskTemplate("b9df25ce-aa55-4f73-8265-d8a724614177", "更新日线数据", "更新日线数据", UpdateDailyTask.class),
|
||||||
|
new TaskTemplate("8ab30478-c81f-4bbf-94dd-7e05fa537b50", "更新财务指标", "更新财务指标", UpdateFinanceIndicatorTask.class),
|
||||||
|
new TaskTemplate("a6a7b569-a171-481b-9184-716925571639", "金字塔选股", "金字塔选股", PyramidSelect.class)
|
||||||
|
).collect(Collectors.toSet());
|
||||||
|
private final Map<String, TaskTemplate> templateMap = templates.stream()
|
||||||
|
.collect(Collectors.toMap(TaskTemplate::id, template -> template));
|
||||||
|
|
||||||
|
public TaskService(TaskRepository repository, ApplicationContext context) {
|
||||||
|
super(repository);
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TaskTemplate getTemplate(String templateId) {
|
||||||
|
return templateMap.get(templateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execute(String templateId, Map<String, Object> params) {
|
||||||
|
execute(templateId, params, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execute(String templateId, Map<String, Object> params, boolean async) {
|
||||||
|
var template = templateMap.get(templateId);
|
||||||
|
if (ObjectUtil.isNull(template)) {
|
||||||
|
throw new RuntimeException("任务模板不存在");
|
||||||
|
}
|
||||||
|
var instance = context.getBean(template.runnerClass());
|
||||||
|
if (async) {
|
||||||
|
executor.submit(() -> instance.run(template, params));
|
||||||
|
} else {
|
||||||
|
instance.run(template, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record TaskTemplate(
|
||||||
|
String id,
|
||||||
|
String name,
|
||||||
|
String description,
|
||||||
|
Class<? extends TaskRunner> runnerClass
|
||||||
|
) {
|
||||||
|
public TaskTemplate(String name, String description, Class<? extends TaskRunner> runnerClass) {
|
||||||
|
this(IdUtil.fastUUID(), name, description, runnerClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.service;
|
package com.lanyuanxiaoyao.leopard.core.service;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.http.HttpUtil;
|
import cn.hutool.http.HttpUtil;
|
||||||
@@ -97,9 +97,19 @@ public class TuShareService {
|
|||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public TuShareResponse factorList(LocalDate tradeDate) {
|
public TuShareResponse factorList(LocalDate tradeDate) {
|
||||||
|
return factorList(tradeDate, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public TuShareResponse factorList(LocalDate tradeDate, String stockCode) {
|
||||||
|
var paramsMap = new HashMap<String, Object>();
|
||||||
|
paramsMap.put("trade_date", tradeDate.format(TRADE_FORMAT));
|
||||||
|
if (StrUtil.isNotBlank(stockCode)) {
|
||||||
|
paramsMap.put("ts_code", stockCode);
|
||||||
|
}
|
||||||
var response = HttpUtil.post(API_URL, buildRequest(
|
var response = HttpUtil.post(API_URL, buildRequest(
|
||||||
"adj_factor",
|
"adj_factor",
|
||||||
Map.of("trade_date", tradeDate.format(TRADE_FORMAT)),
|
paramsMap,
|
||||||
List.of("ts_code", "trade_date", "adj_factor")
|
List.of("ts_code", "trade_date", "adj_factor")
|
||||||
));
|
));
|
||||||
var tuShareResponse = mapper.readValue(response, TuShareResponse.class);
|
var tuShareResponse = mapper.readValue(response, TuShareResponse.class);
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.service.selector;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.QStock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.helper.NumberHelper;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金字塔选股
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250924
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class PyramidStockSelector implements StockSelector<PyramidStockSelector.Request> {
|
||||||
|
private final StockRepository stockRepository;
|
||||||
|
|
||||||
|
public PyramidStockSelector(StockRepository stockRepository) {
|
||||||
|
this.stockRepository = stockRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Override
|
||||||
|
public Set<Candidate> select(Request request) {
|
||||||
|
// 选择至少有最近5年财报的股票
|
||||||
|
// 有点奇怪,001400.SZ有近5年的财报但资料显示是2025年才上市的
|
||||||
|
return stockRepository.findAll(QStock.stock.listedDate.before(LocalDate.of(request.year(), 1, 1)))
|
||||||
|
.stream()
|
||||||
|
.map(stock -> {
|
||||||
|
var extra = new HashMap<String, String>();
|
||||||
|
var score = 0;
|
||||||
|
|
||||||
|
var recentIndicators = stock.getIndicators()
|
||||||
|
.stream()
|
||||||
|
.filter(indicator -> indicator.getYear() < request.year())
|
||||||
|
.sorted((a, b) -> b.getYear() - a.getYear())
|
||||||
|
.limit(5)
|
||||||
|
.toList();
|
||||||
|
if (recentIndicators.size() < 5) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var latestIndicator = recentIndicators.getFirst();
|
||||||
|
|
||||||
|
var roeScore = 0;
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getReturnOnEquity() == null || indicator.getReturnOnEquity() < 0)) {
|
||||||
|
var averageRoe = recentIndicators.stream()
|
||||||
|
.map(FinanceIndicator::getReturnOnEquity)
|
||||||
|
.map(item -> ObjectUtil.defaultIfNull(item, 0.0))
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.average()
|
||||||
|
.orElse(0.0);
|
||||||
|
if (averageRoe >= 35) {
|
||||||
|
roeScore = 550;
|
||||||
|
} else if (averageRoe >= 30) {
|
||||||
|
roeScore = 500;
|
||||||
|
} else if (averageRoe >= 25) {
|
||||||
|
roeScore = 450;
|
||||||
|
} else if (averageRoe >= 20) {
|
||||||
|
roeScore = 400;
|
||||||
|
} else if (averageRoe >= 15) {
|
||||||
|
roeScore = 350;
|
||||||
|
} else if (averageRoe >= 10) {
|
||||||
|
roeScore = 300;
|
||||||
|
}
|
||||||
|
extra.put("平均ROE", NumberHelper.formatPriceDouble(averageRoe));
|
||||||
|
}
|
||||||
|
extra.put("平均ROE得分", NumberHelper.formatPriceDouble(roeScore));
|
||||||
|
score += roeScore;
|
||||||
|
|
||||||
|
var roaScore = 0;
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getReturnOnAssets() == null)) {
|
||||||
|
var averageRoa = recentIndicators.stream()
|
||||||
|
.map(FinanceIndicator::getReturnOnAssets)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.average()
|
||||||
|
.orElse(0.0);
|
||||||
|
if (averageRoa >= 15) {
|
||||||
|
roaScore = 100;
|
||||||
|
} else if (averageRoa >= 11) {
|
||||||
|
roaScore = 80;
|
||||||
|
} else if (averageRoa >= 7) {
|
||||||
|
roaScore = 50;
|
||||||
|
}
|
||||||
|
extra.put("平均ROA", NumberHelper.formatPriceDouble(averageRoa));
|
||||||
|
}
|
||||||
|
extra.put("平均ROA得分", NumberHelper.formatPriceDouble(roaScore));
|
||||||
|
score += roaScore;
|
||||||
|
|
||||||
|
var netProfitScore = 0;
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getNetProfit() == null)) {
|
||||||
|
var averageNetProfit = recentIndicators.stream()
|
||||||
|
.map(FinanceIndicator::getNetProfit)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.average()
|
||||||
|
.orElse(0.0);
|
||||||
|
if (averageNetProfit >= 10000.0 * 10000000) {
|
||||||
|
netProfitScore = 150;
|
||||||
|
} else if (averageNetProfit >= 1000.0 * 10000000) {
|
||||||
|
netProfitScore = 100;
|
||||||
|
}
|
||||||
|
extra.put("平均净利润", NumberHelper.formatPriceDouble(averageNetProfit));
|
||||||
|
}
|
||||||
|
extra.put("平均净利润得分", NumberHelper.formatPriceDouble(netProfitScore));
|
||||||
|
score += netProfitScore;
|
||||||
|
|
||||||
|
var cashScore = 0;
|
||||||
|
if (
|
||||||
|
ArrayUtil.isAllNotNull(latestIndicator.getTotalAssetsTurnover(), latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio())
|
||||||
|
&& (
|
||||||
|
latestIndicator.getTotalAssetsTurnover() > 0.8 && latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio() >= 0.1
|
||||||
|
|| latestIndicator.getTotalAssetsTurnover() <= 0.8 && latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio() >= 0.2
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
cashScore = 50;
|
||||||
|
}
|
||||||
|
extra.put("现金流得分", NumberHelper.formatPriceDouble(cashScore));
|
||||||
|
score += cashScore;
|
||||||
|
|
||||||
|
if (ObjectUtil.isNotNull(latestIndicator.getDaysAccountsReceivableTurnover()) && latestIndicator.getDaysAccountsReceivableTurnover() <= 30) {
|
||||||
|
extra.put("应收账款周转天数得分", "20");
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
|
if (ObjectUtil.isNotNull(latestIndicator.getDaysInventoryTurnover()) && latestIndicator.getDaysInventoryTurnover() <= 30) {
|
||||||
|
extra.put("存货周转天数得分", "20");
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
|
if (ArrayUtil.isAllNotNull(latestIndicator.getDaysAccountsReceivableTurnover(), latestIndicator.getDaysInventoryTurnover())) {
|
||||||
|
if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 40) {
|
||||||
|
score += 20;
|
||||||
|
} else if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 60) {
|
||||||
|
score += 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getOperatingGrossProfitMargin() == null)) {
|
||||||
|
var stat = new DescriptiveStatistics();
|
||||||
|
recentIndicators.stream()
|
||||||
|
.map(FinanceIndicator::getOperatingGrossProfitMargin)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.forEach(stat::addValue);
|
||||||
|
if (stat.getStandardDeviation() <= 0.3) {
|
||||||
|
extra.put("毛利率标准差得分", "50");
|
||||||
|
score += 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var operatingSafeMarginScore = 0;
|
||||||
|
if (ObjectUtil.isNotNull(latestIndicator.getOperatingSafetyMarginRatio())) {
|
||||||
|
if (latestIndicator.getOperatingSafetyMarginRatio() >= 70) {
|
||||||
|
operatingSafeMarginScore = 50;
|
||||||
|
} else if (latestIndicator.getOperatingSafetyMarginRatio() >= 50) {
|
||||||
|
operatingSafeMarginScore = 30;
|
||||||
|
} else if (latestIndicator.getOperatingSafetyMarginRatio() >= 30) {
|
||||||
|
operatingSafeMarginScore = 10;
|
||||||
|
}
|
||||||
|
extra.put("安全边际比率", NumberHelper.formatPriceDouble(latestIndicator.getOperatingSafetyMarginRatio()));
|
||||||
|
}
|
||||||
|
extra.put("安全边际比率得分", NumberHelper.formatPriceDouble(operatingSafeMarginScore));
|
||||||
|
score += operatingSafeMarginScore;
|
||||||
|
|
||||||
|
var netProfitAscendingScore = 0;
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getNetProfit() == null)) {
|
||||||
|
if (recentIndicators.get(0).getNetProfit() > recentIndicators.get(1).getNetProfit()) {
|
||||||
|
netProfitAscendingScore += 30;
|
||||||
|
} else {
|
||||||
|
netProfitAscendingScore -= 30;
|
||||||
|
}
|
||||||
|
if (recentIndicators.get(1).getNetProfit() > recentIndicators.get(2).getNetProfit()) {
|
||||||
|
netProfitAscendingScore += 25;
|
||||||
|
} else {
|
||||||
|
netProfitAscendingScore -= 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.get(2).getNetProfit() > recentIndicators.get(3).getNetProfit()) {
|
||||||
|
netProfitAscendingScore += 20;
|
||||||
|
} else {
|
||||||
|
netProfitAscendingScore -= 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.get(3).getNetProfit() > recentIndicators.get(4).getNetProfit()) {
|
||||||
|
netProfitAscendingScore += 15;
|
||||||
|
} else {
|
||||||
|
netProfitAscendingScore -= 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extra.put("近五年净利润得分", NumberHelper.formatPriceDouble(netProfitAscendingScore));
|
||||||
|
score += netProfitAscendingScore;
|
||||||
|
|
||||||
|
var cashAscendingScore = 0;
|
||||||
|
if (recentIndicators.stream().noneMatch(indicator -> indicator.getCashAndCashEquivalents() == null)) {
|
||||||
|
if (recentIndicators.get(0).getCashAndCashEquivalents() > recentIndicators.get(1).getCashAndCashEquivalents()) {
|
||||||
|
cashAscendingScore += 30;
|
||||||
|
} else {
|
||||||
|
cashAscendingScore -= 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.get(1).getCashAndCashEquivalents() > recentIndicators.get(2).getCashAndCashEquivalents()) {
|
||||||
|
cashAscendingScore += 25;
|
||||||
|
} else {
|
||||||
|
cashAscendingScore -= 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.get(2).getCashAndCashEquivalents() > recentIndicators.get(3).getCashAndCashEquivalents()) {
|
||||||
|
cashAscendingScore += 20;
|
||||||
|
} else {
|
||||||
|
cashAscendingScore -= 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIndicators.get(3).getCashAndCashEquivalents() > recentIndicators.get(4).getCashAndCashEquivalents()) {
|
||||||
|
cashAscendingScore += 15;
|
||||||
|
} else {
|
||||||
|
cashAscendingScore -= 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extra.put("近五年现金流得分", NumberHelper.formatPriceDouble(cashAscendingScore));
|
||||||
|
score += cashAscendingScore;
|
||||||
|
|
||||||
|
return new Candidate(stock, score, extra);
|
||||||
|
})
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.sorted(Comparator.comparingDouble(Candidate::score).reversed())
|
||||||
|
.limit(request.limit)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Request(int year, int limit) {
|
||||||
|
public Request(int year) {
|
||||||
|
this(year, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.service.selector;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选股器
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250924
|
||||||
|
*/
|
||||||
|
public interface StockSelector<T> {
|
||||||
|
Set<Candidate> select(T request);
|
||||||
|
|
||||||
|
record Candidate(Stock stock, double score, Map<String, String> extra) {
|
||||||
|
public Candidate(Stock stock, double score) {
|
||||||
|
this(stock, score, Map.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.task;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.StockScore;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.service.selector.PyramidStockSelector;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金字塔选股
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250925
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class PyramidSelect extends TaskRunner {
|
||||||
|
private final StockCollectionRepository stockCollectionRepository;
|
||||||
|
|
||||||
|
private final PyramidStockSelector pyramidStockSelector;
|
||||||
|
|
||||||
|
protected PyramidSelect(ApplicationContext context, StockCollectionRepository stockCollectionRepository, PyramidStockSelector pyramidStockSelector) {
|
||||||
|
super(context);
|
||||||
|
this.stockCollectionRepository = stockCollectionRepository;
|
||||||
|
this.pyramidStockSelector = pyramidStockSelector;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Throwable.class)
|
||||||
|
@Override
|
||||||
|
public String process(Map<String, Object> params, StepUpdater updater) {
|
||||||
|
var candidates = pyramidStockSelector.select(new PyramidStockSelector.Request(LocalDate.now().getYear(), 50));
|
||||||
|
|
||||||
|
var collection = new StockCollection();
|
||||||
|
collection.setName("金字塔选股");
|
||||||
|
collection.setDescription("金字塔选股");
|
||||||
|
collection.setScores(
|
||||||
|
candidates.stream()
|
||||||
|
.map(candidate -> {
|
||||||
|
var score = new StockScore();
|
||||||
|
score.setStock(candidate.stock());
|
||||||
|
score.setScore(candidate.score());
|
||||||
|
score.setExtra(candidate.extra());
|
||||||
|
score.setCollection(collection);
|
||||||
|
return score;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toSet())
|
||||||
|
);
|
||||||
|
stockCollectionRepository.save(collection);
|
||||||
|
|
||||||
|
return """
|
||||||
|
| Code | Name | Score |
|
||||||
|
| ---- | ---- | ----- |
|
||||||
|
%s
|
||||||
|
""".formatted(candidates.stream()
|
||||||
|
.map(candidate -> "| %s | %s | %.2f |".formatted(candidate.stock().getCode(), candidate.stock().getName(), candidate.score()))
|
||||||
|
.collect(Collectors.joining("\n")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.task;
|
||||||
|
|
||||||
|
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Task;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.TaskRepository;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.service.TaskService;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务运行
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250924
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public abstract class TaskRunner {
|
||||||
|
private final ApplicationContext context;
|
||||||
|
|
||||||
|
protected TaskRunner(ApplicationContext context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract String process(Map<String, Object> params, StepUpdater updater) throws Exception;
|
||||||
|
|
||||||
|
public void run(TaskService.TaskTemplate template, Map<String, Object> params) {
|
||||||
|
var taskRepository = context.getBean(TaskRepository.class);
|
||||||
|
var task = new Task();
|
||||||
|
task.setName(template.name());
|
||||||
|
task.setDescription(template.description());
|
||||||
|
task.setStatus(Task.Status.RUNNING);
|
||||||
|
task.setLaunchedTime(LocalDateTime.now());
|
||||||
|
taskRepository.saveAndFlush(task);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var result = process(params, step -> {
|
||||||
|
synchronized (task) {
|
||||||
|
taskRepository.updateStepById(task.getId(), step);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
task.setStatus(Task.Status.SUCCESS);
|
||||||
|
task.setStep(1.0);
|
||||||
|
task.setFinishedTime(LocalDateTime.now());
|
||||||
|
if (StrUtil.isNotBlank(result)) {
|
||||||
|
task.setResult(result);
|
||||||
|
}
|
||||||
|
taskRepository.saveAndFlush(task);
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
log.error("任务执行失败", throwable);
|
||||||
|
task.setStatus(Task.Status.FAILURE);
|
||||||
|
task.setFinishedTime(LocalDateTime.now());
|
||||||
|
if (ObjectUtil.isNotNull(throwable)) {
|
||||||
|
task.setError(ExceptionUtil.stacktraceToString(throwable));
|
||||||
|
}
|
||||||
|
taskRepository.saveAndFlush(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface StepUpdater {
|
||||||
|
void update(double step);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.core.task;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.NumberUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.helper.NumberHelper;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.service.TuShareService;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新日线数据
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250924
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class UpdateDailyTask extends TaskRunner {
|
||||||
|
private final StockRepository stockRepository;
|
||||||
|
private final DailyRepository dailyRepository;
|
||||||
|
|
||||||
|
private final TransactionTemplate transactionTemplate;
|
||||||
|
|
||||||
|
private final TuShareService tuShareService;
|
||||||
|
|
||||||
|
protected UpdateDailyTask(ApplicationContext context, StockRepository stockRepository, DailyRepository dailyRepository, TransactionTemplate transactionTemplate, TuShareService tuShareService) {
|
||||||
|
super(context);
|
||||||
|
this.stockRepository = stockRepository;
|
||||||
|
this.dailyRepository = dailyRepository;
|
||||||
|
this.transactionTemplate = transactionTemplate;
|
||||||
|
this.tuShareService = tuShareService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String process(Map<String, Object> params, StepUpdater updater) throws Exception {
|
||||||
|
var tradeDates = new HashSet<LocalDate>();
|
||||||
|
for (String exchange : List.of("SSE", "SZSE", "BSE")) {
|
||||||
|
var response = tuShareService.tradeDateList(exchange);
|
||||||
|
for (List<String> item : response.data().items()) {
|
||||||
|
if (ObjectUtil.isNotEmpty(item) && StrUtil.isNotBlank(item.getFirst())) {
|
||||||
|
tradeDates.add(LocalDate.parse(item.getFirst(), TuShareService.TRADE_FORMAT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var existsTradeDates = dailyRepository.findDistinctTradeDate();
|
||||||
|
var nowDate = LocalDate.now();
|
||||||
|
var stocksMap = stockRepository.findAll().stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
|
||||||
|
var targetTradeDates = tradeDates.stream()
|
||||||
|
.filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate))
|
||||||
|
.filter(date -> !existsTradeDates.contains(date))
|
||||||
|
.toList();
|
||||||
|
var total = targetTradeDates.size();
|
||||||
|
var finished = new AtomicInteger(0);
|
||||||
|
targetTradeDates.parallelStream()
|
||||||
|
.forEach(tradeDate -> {
|
||||||
|
var factorResponse = tuShareService.factorList(tradeDate);
|
||||||
|
var factorMap = new HashMap<String, Double>();
|
||||||
|
for (List<String> item : factorResponse.data().items()) {
|
||||||
|
factorMap.put(item.get(0), Double.valueOf(item.get(2)));
|
||||||
|
}
|
||||||
|
transactionTemplate.execute(status -> {
|
||||||
|
var response = tuShareService.dailyList(tradeDate);
|
||||||
|
var dailies = new ArrayList<Daily>();
|
||||||
|
for (List<String> item : response.data().items()) {
|
||||||
|
var code = item.get(0);
|
||||||
|
if (stocksMap.containsKey(code)) {
|
||||||
|
var stock = stocksMap.get(code);
|
||||||
|
var factor = factorMap.get(code);
|
||||||
|
var daily = new Daily();
|
||||||
|
daily.setTradeDate(tradeDate);
|
||||||
|
daily.setOpen(NumberHelper.parseDouble(item.get(2)));
|
||||||
|
daily.setHigh(NumberUtil.parseDouble(item.get(3)));
|
||||||
|
daily.setLow(NumberUtil.parseDouble(item.get(4)));
|
||||||
|
daily.setClose(NumberUtil.parseDouble(item.get(5)));
|
||||||
|
daily.setPreviousClose(NumberUtil.parseDouble(item.get(6)));
|
||||||
|
daily.setPriceChangeAmount(NumberUtil.parseDouble(item.get(7)));
|
||||||
|
daily.setPriceFluctuationRange(NumberUtil.parseDouble(item.get(8)));
|
||||||
|
daily.setVolume(NumberUtil.parseDouble(item.get(9)));
|
||||||
|
daily.setTurnover(NumberUtil.parseDouble(item.get(10)));
|
||||||
|
daily.setFactor(factor);
|
||||||
|
daily.setStock(stock);
|
||||||
|
dailies.add(daily);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dailyRepository.saveAll(dailies);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
updater.update(finished.incrementAndGet() * 1.0 / total);
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +1,48 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.service.task;
|
package com.lanyuanxiaoyao.leopard.core.task;
|
||||||
|
|
||||||
import cn.hutool.core.util.ArrayUtil;
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
|
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.QFinanceIndicator;
|
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.helper.NumberHelper;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.FinanceIndicatorRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.FinanceIndicatorRepository;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper;
|
import com.lanyuanxiaoyao.leopard.core.service.TuShareService;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
|
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
|
|
||||||
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新财务指标数据
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250924
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@LiteflowComponent("update_finance")
|
@Component
|
||||||
public class UpdateFinanceIndicatorNode extends TaskNodeComponent {
|
public class UpdateFinanceIndicatorTask extends TaskRunner {
|
||||||
private final FinanceIndicatorRepository financeIndicatorRepository;
|
|
||||||
private final StockRepository stockRepository;
|
private final StockRepository stockRepository;
|
||||||
|
private final FinanceIndicatorRepository financeIndicatorRepository;
|
||||||
|
|
||||||
private final TuShareService tuShareService;
|
private final TuShareService tuShareService;
|
||||||
|
|
||||||
protected UpdateFinanceIndicatorNode(TaskService taskService, FinanceIndicatorRepository financeIndicatorRepository, StockRepository stockRepository, TuShareService tuShareService) {
|
protected UpdateFinanceIndicatorTask(ApplicationContext context, StockRepository stockRepository, FinanceIndicatorRepository financeIndicatorRepository, TuShareService tuShareService) {
|
||||||
super(taskService);
|
super(context);
|
||||||
this.financeIndicatorRepository = financeIndicatorRepository;
|
|
||||||
this.stockRepository = stockRepository;
|
this.stockRepository = stockRepository;
|
||||||
|
this.financeIndicatorRepository = financeIndicatorRepository;
|
||||||
this.tuShareService = tuShareService;
|
this.tuShareService = tuShareService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Throwable.class)
|
||||||
@Override
|
@Override
|
||||||
public void process() throws Exception {
|
public String process(Map<String, Object> params, StepUpdater updater) throws JsonProcessingException {
|
||||||
var stocks = stockRepository.findAll();
|
var stocks = stockRepository.findAll();
|
||||||
var currentYear = LocalDate.now().getYear();
|
var currentYear = LocalDate.now().getYear();
|
||||||
for (int year = 1990; year < currentYear; year++) {
|
for (int year = 1990; year < currentYear; year++) {
|
||||||
@@ -138,7 +147,7 @@ public class UpdateFinanceIndicatorNode extends TaskNodeComponent {
|
|||||||
var income = incomesMap.get(stock.getCode());
|
var income = incomesMap.get(stock.getCode());
|
||||||
var cashFlow = cashFlowsMap.get(stock.getCode());
|
var cashFlow = cashFlowsMap.get(stock.getCode());
|
||||||
var finaIndicator = finaIndicatorsMap.get(stock.getCode());
|
var finaIndicator = finaIndicatorsMap.get(stock.getCode());
|
||||||
if (ArrayUtil.<Object>isAllNull(balance, income, cashFlow, finaIndicator)) {
|
if (stock.getListedDate().getYear() > year || ArrayUtil.<Object>isAllNull(balance, income, cashFlow, finaIndicator)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var indicator = financeIndicatorsMap.getOrDefault(stock.getCode(), new FinanceIndicator());
|
var indicator = financeIndicatorsMap.getOrDefault(stock.getCode(), new FinanceIndicator());
|
||||||
@@ -209,7 +218,8 @@ public class UpdateFinanceIndicatorNode extends TaskNodeComponent {
|
|||||||
financeIndicatorRepository.save(indicator);
|
financeIndicatorRepository.save(indicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStep((year - 1990) * 100 / (currentYear - 1990));
|
updater.update((year - 1990) * 1.0 / (currentYear - 1990));
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,37 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.service.task;
|
package com.lanyuanxiaoyao.leopard.core.task;
|
||||||
|
|
||||||
import cn.hutool.core.util.EnumUtil;
|
import cn.hutool.core.util.EnumUtil;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
|
import com.lanyuanxiaoyao.leopard.core.service.TuShareService;
|
||||||
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
|
||||||
import com.yomahub.liteflow.core.NodeComponent;
|
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@LiteflowComponent("update_stock")
|
/**
|
||||||
public class UpdateStockNode extends NodeComponent {
|
* 更新股票信息
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250924
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class UpdateStockTask extends TaskRunner {
|
||||||
private final StockRepository stockRepository;
|
private final StockRepository stockRepository;
|
||||||
|
|
||||||
private final TuShareService tuShareService;
|
private final TuShareService tuShareService;
|
||||||
|
|
||||||
public UpdateStockNode(StockRepository stockRepository, TuShareService tuShareService) {
|
public UpdateStockTask(ApplicationContext context, StockRepository stockRepository, TuShareService tuShareService) {
|
||||||
|
super(context);
|
||||||
this.stockRepository = stockRepository;
|
this.stockRepository = stockRepository;
|
||||||
this.tuShareService = tuShareService;
|
this.tuShareService = tuShareService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(rollbackOn = Throwable.class)
|
@Transactional(rollbackFor = Throwable.class)
|
||||||
@Override
|
@Override
|
||||||
public void process() {
|
public String process(Map<String, Object> params, StepUpdater updater) {
|
||||||
var existsStockMap = stockRepository.findAll().stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
|
var existsStockMap = stockRepository.findAll().stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
|
||||||
var stocks = tuShareService.stockList()
|
var stocks = tuShareService.stockList()
|
||||||
.data()
|
.data()
|
||||||
@@ -51,5 +59,6 @@ public class UpdateStockNode extends NodeComponent {
|
|||||||
var deleteCodes = existsCodes.stream().filter(code -> !currentCodes.contains(code)).toList();
|
var deleteCodes = existsCodes.stream().filter(code -> !currentCodes.contains(code)).toList();
|
||||||
stockRepository.deleteAllByCodeIn(deleteCodes);
|
stockRepository.deleteAllByCodeIn(deleteCodes);
|
||||||
stockRepository.saveAll(stocks);
|
stockRepository.saveAll(stocks);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.core;
|
package com.lanyuanxiaoyao.leopard.core;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.util.DDLGenerator;
|
import com.lanyuanxiaoyao.service.template.Helper;
|
||||||
import org.hibernate.dialect.PostgreSQLDialect;
|
import org.hibernate.dialect.PostgreSQLDialect;
|
||||||
import org.postgresql.Driver;
|
import org.postgresql.Driver;
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import org.postgresql.Driver;
|
|||||||
*/
|
*/
|
||||||
public class GenerateDDL {
|
public class GenerateDDL {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
DDLGenerator.generateDDL(
|
Helper.generateDDL(
|
||||||
"com.lanyuanxiaoyao.leopard.core.entity",
|
"com.lanyuanxiaoyao.leopard.core.entity",
|
||||||
"/Users/lanyuanxiaoyao/Project/IdeaProjects/leopard/leopard-core/target",
|
"/Users/lanyuanxiaoyao/Project/IdeaProjects/leopard/leopard-core/target",
|
||||||
PostgreSQLDialect.class,
|
PostgreSQLDialect.class,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
nohup /home/ubuntu/jdk-17.0.16+8/bin/java -jar /home/ubuntu/app/leopard-server-1.0.0.jar --spring.profiles.active=build --spring.web.resources.static-locations=file:/home/ubuntu/app/dist --logging.parent=/home/ubuntu/app > /dev/null 2>&1 &
|
nohup /home/ubuntu/jdk-21.0.8+9/bin/java -jar /home/ubuntu/app/leopard-server-1.0.0.jar --spring.profiles.active=build --spring.web.resources.static-locations=file:/home/ubuntu/app/dist --logging.parent=/home/ubuntu/app > /dev/null 2>&1 &
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
original_command="$0"
|
original_command="$0"
|
||||||
application_name="/home/ubuntu/app/leopard-server-1.0.0.jar"
|
application_name="leopard-server-1.0.0.jar"
|
||||||
|
|
||||||
function get_pid() {
|
function get_pid() {
|
||||||
echo $(ps ef | grep "$application_name" | grep -v grep | awk '{ print $1 }')
|
ID=$(ps -ef | grep "$application_name" | grep -v grep | grep -v $original_command | awk '{ print $2 }')
|
||||||
|
if [[ -z "$ID" ]]; then
|
||||||
|
ID=$(ps aux | grep "$application_name" | grep -v grep | grep -v $original_command | awk '{print $2}')
|
||||||
|
if [[ -z "$ID" ]]; then
|
||||||
|
ID=$(/home/ubuntu/jdk-21.0.8+9/bin/jps -lmvV | grep "$application_name" | awk '{print $1}')
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo $ID
|
||||||
}
|
}
|
||||||
|
|
||||||
pid=$(get_pid)
|
pid=$(get_pid)
|
||||||
@@ -31,3 +38,4 @@ while (true); do
|
|||||||
fi
|
fi
|
||||||
sleep 1s
|
sleep 1s
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,14 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-jetty</artifactId>
|
<artifactId>spring-boot-starter-jetty</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-cache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
@@ -41,24 +49,6 @@
|
|||||||
<artifactId>spring-boot-starter-quartz</artifactId>
|
<artifactId>spring-boot-starter-quartz</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yomahub</groupId>
|
|
||||||
<artifactId>liteflow-spring-boot-starter</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yomahub</groupId>
|
|
||||||
<artifactId>liteflow-rule-sql</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.hutool</groupId>
|
|
||||||
<artifactId>hutool-core</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.hutool</groupId>
|
|
||||||
<artifactId>hutool-http</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.mysql</groupId>
|
<groupId>com.mysql</groupId>
|
||||||
<artifactId>mysql-connector-j</artifactId>
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
@@ -69,6 +59,11 @@
|
|||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server;
|
package com.lanyuanxiaoyao.leopard.server;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
|
import com.lanyuanxiaoyao.leopard.core.repository.TaskRepository;
|
||||||
import com.yomahub.liteflow.core.FlowExecutor;
|
import jakarta.transaction.Transactional;
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.ApplicationArguments;
|
import org.springframework.boot.ApplicationArguments;
|
||||||
import org.springframework.boot.ApplicationRunner;
|
import org.springframework.boot.ApplicationRunner;
|
||||||
@@ -18,19 +17,20 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
|||||||
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
|
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
|
||||||
@EnableJpaAuditing
|
@EnableJpaAuditing
|
||||||
public class LeopardServerApplication implements ApplicationRunner {
|
public class LeopardServerApplication implements ApplicationRunner {
|
||||||
|
private final TaskRepository taskRepository;
|
||||||
|
|
||||||
|
public LeopardServerApplication(TaskRepository taskRepository) {
|
||||||
|
this.taskRepository = taskRepository;
|
||||||
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(LeopardServerApplication.class, args);
|
SpringApplication.run(LeopardServerApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resource
|
@Transactional(rollbackOn = Throwable.class)
|
||||||
private FlowExecutor executor;
|
|
||||||
@Resource
|
|
||||||
private TuShareService tuShareService;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(ApplicationArguments args) {
|
public void run(ApplicationArguments args) {
|
||||||
// executor.execute2RespWithEL("THEN(update_daily)");
|
log.warn("更新所有未完成的任务状态为失败");
|
||||||
// executor.execute2RespWithEL("THEN(update_stock)");
|
taskRepository.updateAllRunningTaskToFailure();
|
||||||
// executor.execute2RespWithEL("THEN(check_daily)");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.server.configuration;
|
||||||
|
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存提供
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @date 2023-04-23
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableCaching
|
||||||
|
public class CacheProvider {
|
||||||
|
@Primary
|
||||||
|
@Bean("short-cache")
|
||||||
|
public CacheManager normalCache() {
|
||||||
|
CaffeineCacheManager manager = new CaffeineCacheManager();
|
||||||
|
manager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES));
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("long-cache")
|
||||||
|
public CacheManager longCache() {
|
||||||
|
CaffeineCacheManager manager = new CaffeineCacheManager();
|
||||||
|
manager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.HOURS));
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,10 @@ package com.lanyuanxiaoyao.leopard.server.controller;
|
|||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Task;
|
import com.lanyuanxiaoyao.leopard.core.entity.Task;
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService;
|
import com.lanyuanxiaoyao.leopard.core.service.TaskService;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -87,11 +88,11 @@ public class CommonOptionsController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private final StockRepository stockRepository;
|
private final StockRepository stockRepository;
|
||||||
private final TaskTemplateService taskTemplateService;
|
private final TaskService taskService;
|
||||||
|
|
||||||
public CommonOptionsController(StockRepository stockRepository, TaskTemplateService taskTemplateService) {
|
public CommonOptionsController(StockRepository stockRepository, TaskService taskService) {
|
||||||
this.stockRepository = stockRepository;
|
this.stockRepository = stockRepository;
|
||||||
this.taskTemplateService = taskTemplateService;
|
this.taskService = taskService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/options/{name}")
|
@GetMapping("/options/{name}")
|
||||||
@@ -100,18 +101,21 @@ public class CommonOptionsController {
|
|||||||
case "stock_market" -> GlobalResponse.responseSuccess(
|
case "stock_market" -> GlobalResponse.responseSuccess(
|
||||||
Arrays.stream(Stock.Market.values())
|
Arrays.stream(Stock.Market.values())
|
||||||
.map(market -> new Option(market.getChineseName(), market.name()))
|
.map(market -> new Option(market.getChineseName(), market.name()))
|
||||||
|
.sorted(Comparator.comparing(Option::label))
|
||||||
.toList()
|
.toList()
|
||||||
);
|
);
|
||||||
case "stock_industry" -> GlobalResponse.responseSuccess(
|
case "stock_industry" -> GlobalResponse.responseSuccess(
|
||||||
stockRepository.findDistinctIndustries()
|
stockRepository.findDistinctIndustries()
|
||||||
.stream()
|
.stream()
|
||||||
.map(industry -> new Option(industry, industry))
|
.map(industry -> new Option(industry, industry))
|
||||||
|
.sorted(Comparator.comparing(Option::label))
|
||||||
.toList()
|
.toList()
|
||||||
);
|
);
|
||||||
case "task_template_id" -> GlobalResponse.responseSuccess(
|
case "task_template_id" -> GlobalResponse.responseSuccess(
|
||||||
taskTemplateService.list()
|
taskService.getTemplates()
|
||||||
.stream()
|
.stream()
|
||||||
.map(template -> new Option(template.getName(), template.getId()))
|
.map(template -> new Option(template.name(), template.id()))
|
||||||
|
.sorted(Comparator.comparing(Option::label))
|
||||||
.toList()
|
.toList()
|
||||||
);
|
);
|
||||||
default -> GlobalResponse.responseSuccess(List.of());
|
default -> GlobalResponse.responseSuccess(List.of());
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.controller;
|
package com.lanyuanxiaoyao.leopard.server.controller;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.service.TaskService;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.QuartzService;
|
import com.lanyuanxiaoyao.leopard.server.service.QuartzService;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService;
|
|
||||||
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -20,11 +20,11 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@RequestMapping("task_schedule")
|
@RequestMapping("task_schedule")
|
||||||
public class QuartzController {
|
public class QuartzController {
|
||||||
private final QuartzService quartzService;
|
private final QuartzService quartzService;
|
||||||
private final TaskTemplateService taskTemplateService;
|
private final TaskService taskService;
|
||||||
|
|
||||||
public QuartzController(QuartzService quartzService, TaskTemplateService taskTemplateService) {
|
public QuartzController(QuartzService quartzService, TaskService taskService) {
|
||||||
this.quartzService = quartzService;
|
this.quartzService = quartzService;
|
||||||
this.taskTemplateService = taskTemplateService;
|
this.taskService = taskService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("save")
|
@PostMapping("save")
|
||||||
@@ -38,11 +38,11 @@ public class QuartzController {
|
|||||||
var list = quartzService.list()
|
var list = quartzService.list()
|
||||||
.stream()
|
.stream()
|
||||||
.map(task -> {
|
.map(task -> {
|
||||||
var template = taskTemplateService.detail(task.templateId());
|
var template = taskService.getTemplate(task.templateId());
|
||||||
return new ListItem(
|
return new ListItem(
|
||||||
task.key(),
|
task.key(),
|
||||||
template.getName(),
|
template.name(),
|
||||||
template.getDescription(),
|
template.description(),
|
||||||
task.cron(),
|
task.cron(),
|
||||||
task.status(),
|
task.status(),
|
||||||
task.previousFireTime(),
|
task.previousFireTime(),
|
||||||
@@ -72,7 +72,7 @@ public class QuartzController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public record SaveItem(
|
public record SaveItem(
|
||||||
Long templateId,
|
String templateId,
|
||||||
String cron
|
String cron
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,32 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.controller;
|
package com.lanyuanxiaoyao.leopard.server.controller;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
|
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.StockCollectionService;
|
import com.lanyuanxiaoyao.leopard.core.service.StockCollectionService;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.StockService;
|
import com.lanyuanxiaoyao.leopard.server.entity.StockScoreVo;
|
||||||
|
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
||||||
import java.util.HashSet;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Set;
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("stock_collection")
|
@RequestMapping("stock_collection")
|
||||||
public class StockCollectionController extends SimpleControllerSupport<StockCollection, StockCollectionController.SaveItem, StockCollectionController.ListItem, StockCollectionController.DetailItem> {
|
public class StockCollectionController extends SimpleControllerSupport<StockCollection, Void, StockCollectionController.ListItem, StockCollectionController.DetailItem> {
|
||||||
private final StockService stockService;
|
public StockCollectionController(StockCollectionService service) {
|
||||||
|
|
||||||
public StockCollectionController(StockCollectionService service, StockService stockService) {
|
|
||||||
super(service);
|
super(service);
|
||||||
this.stockService = stockService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Function<SaveItem, StockCollection> saveItemMapper() {
|
public GlobalResponse<Long> save(Void unused) throws Exception {
|
||||||
return item -> {
|
throw new UnsupportedOperationException();
|
||||||
var collection = new StockCollection();
|
}
|
||||||
collection.setId(item.id());
|
|
||||||
collection.setName(item.name());
|
@Override
|
||||||
collection.setDescription(item.description());
|
protected Function<Void, StockCollection> saveItemMapper() {
|
||||||
var stocks = stockService.list(item.stockIds());
|
throw new UnsupportedOperationException();
|
||||||
collection.setStocks(new HashSet<>(stocks));
|
|
||||||
return collection;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -40,7 +35,9 @@ public class StockCollectionController extends SimpleControllerSupport<StockColl
|
|||||||
collection.getId(),
|
collection.getId(),
|
||||||
collection.getName(),
|
collection.getName(),
|
||||||
collection.getDescription(),
|
collection.getDescription(),
|
||||||
collection.getStocks().size()
|
collection.getScores().size(),
|
||||||
|
collection.getCreatedTime(),
|
||||||
|
collection.getModifiedTime()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,24 +47,24 @@ public class StockCollectionController extends SimpleControllerSupport<StockColl
|
|||||||
collection.getId(),
|
collection.getId(),
|
||||||
collection.getName(),
|
collection.getName(),
|
||||||
collection.getDescription(),
|
collection.getDescription(),
|
||||||
collection.getStocks().size(),
|
collection.getScores().size(),
|
||||||
collection.getStocks()
|
collection.getScores()
|
||||||
|
.stream()
|
||||||
|
.map(StockScoreVo::of)
|
||||||
|
.sorted(Comparator.comparing(StockScoreVo::score).reversed())
|
||||||
|
.toList(),
|
||||||
|
collection.getCreatedTime(),
|
||||||
|
collection.getModifiedTime()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record SaveItem(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
String description,
|
|
||||||
Set<Long> stockIds
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ListItem(
|
public record ListItem(
|
||||||
Long id,
|
Long id,
|
||||||
String name,
|
String name,
|
||||||
String description,
|
String description,
|
||||||
Integer count
|
Integer count,
|
||||||
|
LocalDateTime createdTime,
|
||||||
|
LocalDateTime modifiedTime
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +73,9 @@ public class StockCollectionController extends SimpleControllerSupport<StockColl
|
|||||||
String name,
|
String name,
|
||||||
String description,
|
String description,
|
||||||
Integer count,
|
Integer count,
|
||||||
Set<Stock> stocks
|
List<StockScoreVo> scores,
|
||||||
|
LocalDateTime createdTime,
|
||||||
|
LocalDateTime modifiedTime
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.controller;
|
package com.lanyuanxiaoyao.leopard.server.controller;
|
||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper;
|
import com.lanyuanxiaoyao.leopard.core.helper.NumberHelper;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.StockService;
|
import com.lanyuanxiaoyao.leopard.core.helper.TaHelper;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.service.StockService;
|
||||||
|
import com.lanyuanxiaoyao.leopard.server.entity.StockDetailVo;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -24,7 +27,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("stock")
|
@RequestMapping("stock")
|
||||||
public class StockController extends SimpleControllerSupport<Stock, Void, StockController.DetailItem, StockController.DetailItem> {
|
public class StockController extends SimpleControllerSupport<Stock, Void, StockDetailVo, StockDetailVo> {
|
||||||
private final StockService stockService;
|
private final StockService stockService;
|
||||||
|
|
||||||
public StockController(StockService service, StockService stockService) {
|
public StockController(StockService service, StockService stockService) {
|
||||||
@@ -107,14 +110,61 @@ public class StockController extends SimpleControllerSupport<Stock, Void, StockC
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("daily/current/{id}")
|
||||||
|
public GlobalResponse<Map<String, Object>> dailyCurrent(@PathVariable("id") Long id) {
|
||||||
|
var daily = stockService.findDailyLatest(id);
|
||||||
|
return GlobalResponse.responseMapData(Map.of(
|
||||||
|
"date", daily.map(Daily::getTradeDate).map(LocalDate::toString).orElse("/"),
|
||||||
|
"open", daily.map(Daily::getOpen).map(NumberHelper::formatPriceDouble).orElse(NumberHelper.FINANCE_NULL_DOUBLE),
|
||||||
|
"close", daily.map(Daily::getClose).map(NumberHelper::formatPriceDouble).orElse(NumberHelper.FINANCE_NULL_DOUBLE),
|
||||||
|
"low", daily.map(Daily::getLow).map(NumberHelper::formatPriceDouble).orElse(NumberHelper.FINANCE_NULL_DOUBLE),
|
||||||
|
"high", daily.map(Daily::getHigh).map(NumberHelper::formatPriceDouble).orElse(NumberHelper.FINANCE_NULL_DOUBLE)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("daily/{id}")
|
@GetMapping("daily/{id}")
|
||||||
public GlobalResponse<Map<String, Object>> dailyCharts(@PathVariable("id") Long id) {
|
public GlobalResponse<Map<String, Object>> dailyCharts(@PathVariable("id") Long id) {
|
||||||
var data = stockService.findDailyRecent(id, 100);
|
var data = stockService.findDailyRecent(id, 100 + 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.getOpen(), daily.getClose(), daily.getLow(), daily.getHigh()));
|
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
|
||||||
@@ -126,37 +176,14 @@ public class StockController extends SimpleControllerSupport<Stock, Void, StockC
|
|||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private DetailItem covert(Stock stock) {
|
@Override
|
||||||
return new DetailItem(
|
protected Function<Stock, StockDetailVo> listItemMapper() {
|
||||||
stock.getId(),
|
return StockDetailVo::of;
|
||||||
stock.getCode(),
|
|
||||||
stock.getName(),
|
|
||||||
stock.getFullname(),
|
|
||||||
stock.getMarket(),
|
|
||||||
stock.getIndustry(),
|
|
||||||
stock.getListedDate()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Function<Stock, DetailItem> listItemMapper() {
|
protected Function<Stock, StockDetailVo> detailItemMapper() {
|
||||||
return this::covert;
|
return StockDetailVo::of;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Stock, DetailItem> detailItemMapper() {
|
|
||||||
return this::covert;
|
|
||||||
}
|
|
||||||
|
|
||||||
public record DetailItem(
|
|
||||||
Long id,
|
|
||||||
String code,
|
|
||||||
String name,
|
|
||||||
String fullname,
|
|
||||||
Stock.Market market,
|
|
||||||
String industry,
|
|
||||||
LocalDate listedDate
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record FinanceItem(
|
public record FinanceItem(
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ import cn.hutool.core.date.BetweenFormatter;
|
|||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Task;
|
import com.lanyuanxiaoyao.leopard.core.entity.Task;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
|
import com.lanyuanxiaoyao.leopard.core.service.TaskService;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -45,14 +47,25 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
|
|||||||
return GlobalResponse.responseSuccess();
|
return GlobalResponse.responseSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("template/list")
|
||||||
|
public GlobalResponse<Map<String, Object>> templateList() {
|
||||||
|
var templates = taskService.getTemplates()
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(TaskService.TaskTemplate::name))
|
||||||
|
.toList();
|
||||||
|
return GlobalResponse.responseCrudData(templates, templates.size());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Function<Void, Task> saveItemMapper() {
|
protected Function<Void, Task> saveItemMapper() {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private TaskCost calculateCost(LocalDateTime start, LocalDateTime finish) {
|
private TaskCost calculateCost(LocalDateTime start, LocalDateTime finish) {
|
||||||
if (ObjectUtil.isNull(start) || ObjectUtil.isNull(finish)) {
|
if (ObjectUtil.isNull(start)) {
|
||||||
return new TaskCost(null, null);
|
return new TaskCost(null, null);
|
||||||
|
} else if (ObjectUtil.isNull(finish)) {
|
||||||
|
finish = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
var duration = Duration.between(start, finish).toMillis();
|
var duration = Duration.between(start, finish).toMillis();
|
||||||
return new TaskCost(
|
return new TaskCost(
|
||||||
@@ -108,7 +121,7 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
|
|||||||
LocalDateTime finishedTime,
|
LocalDateTime finishedTime,
|
||||||
Long cost,
|
Long cost,
|
||||||
String costText,
|
String costText,
|
||||||
Integer step
|
Double step
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,11 +136,11 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
|
|||||||
LocalDateTime finishedTime,
|
LocalDateTime finishedTime,
|
||||||
Long cost,
|
Long cost,
|
||||||
String costText,
|
String costText,
|
||||||
Integer step
|
Double step
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ExecuteRequest(Long templateId, Map<String, Object> params) {
|
public record ExecuteRequest(String templateId, Map<String, Object> params) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record TaskCost(Long cost, String costText) {
|
public record TaskCost(Long cost, String costText) {
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.controller;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.IdUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate;
|
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService;
|
|
||||||
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("task_template")
|
|
||||||
public class TaskTemplateController extends SimpleControllerSupport<TaskTemplate, TaskTemplateController.Item, TaskTemplateController.Item, TaskTemplateController.Item> {
|
|
||||||
@Value("${spring.application.name}")
|
|
||||||
private String application;
|
|
||||||
|
|
||||||
public TaskTemplateController(TaskTemplateService service) {
|
|
||||||
super(service);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Item, TaskTemplate> saveItemMapper() {
|
|
||||||
return item -> {
|
|
||||||
var template = new TaskTemplate();
|
|
||||||
template.setId(item.id());
|
|
||||||
template.setName(item.name());
|
|
||||||
template.setDescription(item.description());
|
|
||||||
template.setApplication(application);
|
|
||||||
template.setChain(IdUtil.simpleUUID());
|
|
||||||
template.setExpression(item.expression());
|
|
||||||
template.setExpressionEl(StrUtil.format("CATCH(THEN(task_start, ({}), task_end)).DO(task_error)", item.expression()));
|
|
||||||
return template;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Item convert(TaskTemplate template) {
|
|
||||||
return new Item(
|
|
||||||
template.getId(),
|
|
||||||
template.getName(),
|
|
||||||
template.getDescription(),
|
|
||||||
template.getExpression()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<TaskTemplate, Item> listItemMapper() {
|
|
||||||
return this::convert;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<TaskTemplate, Item> detailItemMapper() {
|
|
||||||
return this::convert;
|
|
||||||
}
|
|
||||||
|
|
||||||
public record Item(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
String description,
|
|
||||||
String expression
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.server.entity;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250917
|
||||||
|
*/
|
||||||
|
public record StockDetailVo(
|
||||||
|
Long id,
|
||||||
|
String code,
|
||||||
|
String name,
|
||||||
|
String fullname,
|
||||||
|
Stock.Market market,
|
||||||
|
String industry,
|
||||||
|
LocalDate listedDate
|
||||||
|
) {
|
||||||
|
public static StockDetailVo of(Stock stock) {
|
||||||
|
return new StockDetailVo(
|
||||||
|
stock.getId(),
|
||||||
|
stock.getCode(),
|
||||||
|
stock.getName(),
|
||||||
|
stock.getFullname(),
|
||||||
|
stock.getMarket(),
|
||||||
|
stock.getIndustry(),
|
||||||
|
stock.getListedDate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.server.entity;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
||||||
|
import com.lanyuanxiaoyao.leopard.core.entity.StockScore;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250917
|
||||||
|
*/
|
||||||
|
public record StockScoreVo(
|
||||||
|
Long id,
|
||||||
|
String code,
|
||||||
|
String name,
|
||||||
|
String fullname,
|
||||||
|
Stock.Market market,
|
||||||
|
String industry,
|
||||||
|
LocalDate listedDate,
|
||||||
|
Double score,
|
||||||
|
String extra
|
||||||
|
) {
|
||||||
|
public static StockScoreVo of(StockScore score) {
|
||||||
|
return new StockScoreVo(
|
||||||
|
score.getStock().getId(),
|
||||||
|
score.getStock().getCode(),
|
||||||
|
score.getStock().getName(),
|
||||||
|
score.getStock().getFullname(),
|
||||||
|
score.getStock().getMarket(),
|
||||||
|
score.getStock().getIndustry(),
|
||||||
|
score.getStock().getListedDate(),
|
||||||
|
score.getScore(),
|
||||||
|
score.getExtra()
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.sorted(Map.Entry.comparingByKey())
|
||||||
|
.map(entry -> entry.getKey() + ": " + entry.getValue())
|
||||||
|
.collect(Collectors.joining("<br>"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,8 @@ package com.lanyuanxiaoyao.leopard.server.service;
|
|||||||
|
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.task.TaskMonitorNodes;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.yomahub.liteflow.core.FlowExecutor;
|
import com.lanyuanxiaoyao.leopard.core.service.TaskService;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -36,10 +36,10 @@ public class QuartzService {
|
|||||||
var tasks = new ArrayList<QuartzTask>();
|
var tasks = new ArrayList<QuartzTask>();
|
||||||
for (var key : scheduler.getJobKeys(GroupMatcher.anyGroup())) {
|
for (var key : scheduler.getJobKeys(GroupMatcher.anyGroup())) {
|
||||||
var detail = scheduler.getJobDetail(key);
|
var detail = scheduler.getJobDetail(key);
|
||||||
var trigger = (CronTrigger) scheduler.getTriggersOfJob(key).get(0);
|
var trigger = (CronTrigger) scheduler.getTriggersOfJob(key).getFirst();
|
||||||
tasks.add(new QuartzTask(
|
tasks.add(new QuartzTask(
|
||||||
detail.getKey().getName(),
|
detail.getKey().getName(),
|
||||||
detail.getJobDataMap().getLong("template_id"),
|
detail.getJobDataMap().getString("template_id"),
|
||||||
trigger.getCronExpression(),
|
trigger.getCronExpression(),
|
||||||
scheduler.getTriggerState(trigger.getKey()),
|
scheduler.getTriggerState(trigger.getKey()),
|
||||||
ObjectUtil.isNull(trigger.getPreviousFireTime()) ? null : LocalDateTime.ofInstant(trigger.getPreviousFireTime().toInstant(), ZoneId.systemDefault()),
|
ObjectUtil.isNull(trigger.getPreviousFireTime()) ? null : LocalDateTime.ofInstant(trigger.getPreviousFireTime().toInstant(), ZoneId.systemDefault()),
|
||||||
@@ -49,7 +49,7 @@ public class QuartzService {
|
|||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save(Long templateId, String cron) throws SchedulerException {
|
public void save(String templateId, String cron) throws SchedulerException {
|
||||||
var detail = JobBuilder.newJob(TaskExecutionJob.class)
|
var detail = JobBuilder.newJob(TaskExecutionJob.class)
|
||||||
.withIdentity("task_execution_" + IdUtil.fastUUID())
|
.withIdentity("task_execution_" + IdUtil.fastUUID())
|
||||||
.usingJobData("template_id", templateId)
|
.usingJobData("template_id", templateId)
|
||||||
@@ -81,30 +81,27 @@ public class QuartzService {
|
|||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public static class TaskExecutionJob extends QuartzJobBean {
|
public static class TaskExecutionJob extends QuartzJobBean {
|
||||||
private final TaskTemplateService taskTemplateService;
|
private final TaskService taskService;
|
||||||
private final FlowExecutor flowExecutor;
|
|
||||||
|
|
||||||
public TaskExecutionJob(TaskTemplateService taskTemplateService, FlowExecutor flowExecutor) {
|
public TaskExecutionJob(TaskService taskService) {
|
||||||
this.taskTemplateService = taskTemplateService;
|
this.taskService = taskService;
|
||||||
this.flowExecutor = flowExecutor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
@Override
|
@Override
|
||||||
protected void executeInternal(JobExecutionContext context) {
|
protected void executeInternal(JobExecutionContext context) {
|
||||||
var dataMap = context.getMergedJobDataMap();
|
var dataMap = context.getMergedJobDataMap();
|
||||||
var templateId = dataMap.getLong("template_id");
|
var templateId = dataMap.getString("template_id");
|
||||||
if (ObjectUtil.isNotNull(templateId)) {
|
if (StrUtil.isNotBlank(templateId)) {
|
||||||
var template = taskTemplateService.detail(templateId);
|
|
||||||
var params = (Map<String, Object>) dataMap.getOrDefault("params", Map.of());
|
var params = (Map<String, Object>) dataMap.getOrDefault("params", Map.of());
|
||||||
var monitorContext = new TaskMonitorNodes.TaskMonitorContext(template);
|
taskService.execute(templateId, params, true);
|
||||||
flowExecutor.execute2Resp(template.getChain(), params, monitorContext);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record QuartzTask(
|
public record QuartzTask(
|
||||||
String key,
|
String key,
|
||||||
Long templateId,
|
String templateId,
|
||||||
String cron,
|
String cron,
|
||||||
Trigger.TriggerState status,
|
Trigger.TriggerState status,
|
||||||
LocalDateTime previousFireTime,
|
LocalDateTime previousFireTime,
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.service;
|
|
||||||
|
|
||||||
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.QFinanceIndicator;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.FinanceIndicatorRepository;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
|
||||||
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author lanyuanxiaoyao
|
|
||||||
* @version 20250828
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class StockService extends SimpleServiceSupport<Stock> {
|
|
||||||
private final StockRepository stockRepository;
|
|
||||||
private final FinanceIndicatorRepository financeIndicatorRepository;
|
|
||||||
private final DailyRepository dailyRepository;
|
|
||||||
|
|
||||||
public StockService(StockRepository repository, FinanceIndicatorRepository financeIndicatorRepository, DailyRepository dailyRepository) {
|
|
||||||
super(repository);
|
|
||||||
this.stockRepository = repository;
|
|
||||||
this.financeIndicatorRepository = financeIndicatorRepository;
|
|
||||||
this.dailyRepository = dailyRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<FinanceIndicator> findFinanceIndicator(Long stockId, Integer year) {
|
|
||||||
return financeIndicatorRepository.findOne(
|
|
||||||
QFinanceIndicator.financeIndicator.year.eq(year)
|
|
||||||
.and(QFinanceIndicator.financeIndicator.stock.id.eq(stockId))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<FinanceIndicator> findFinanceIndicatorRecent(Long stockId, int years) {
|
|
||||||
var current = LocalDate.now();
|
|
||||||
return financeIndicatorRepository.findAll(
|
|
||||||
QFinanceIndicator.financeIndicator.stock.id.eq(stockId)
|
|
||||||
.and(QFinanceIndicator.financeIndicator.year.between(current.minusYears(years).getYear(), current.getYear())),
|
|
||||||
Sort.by(Sort.Direction.ASC, FinanceIndicator_.YEAR)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Daily> findDailyRecent(Long stockId, int days) {
|
|
||||||
var current = LocalDate.now();
|
|
||||||
return dailyRepository.findAll(
|
|
||||||
QDaily.daily.stock.id.eq(stockId)
|
|
||||||
.and(QDaily.daily.tradeDate.between(current.minusDays(days), current)),
|
|
||||||
Sort.by(Sort.Direction.ASC, Daily_.TRADE_DATE)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.service;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Task;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.TaskRepository;
|
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.task.TaskMonitorNodes;
|
|
||||||
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
|
|
||||||
import com.yomahub.liteflow.core.FlowExecutor;
|
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author lanyuanxiaoyao
|
|
||||||
* @version 20250829
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class TaskService extends SimpleServiceSupport<Task> {
|
|
||||||
private final TaskRepository taskRepository;
|
|
||||||
private final TaskTemplateService taskTemplateService;
|
|
||||||
private final FlowExecutor flowExecutor;
|
|
||||||
|
|
||||||
public TaskService(TaskRepository repository, TaskTemplateService taskTemplateService, @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") FlowExecutor flowExecutor) {
|
|
||||||
super(repository);
|
|
||||||
this.taskRepository = repository;
|
|
||||||
this.taskTemplateService = taskTemplateService;
|
|
||||||
this.flowExecutor = flowExecutor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(rollbackOn = Throwable.class)
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void onApplicationReady() {
|
|
||||||
log.warn("更新所有未完成的任务状态为失败");
|
|
||||||
taskRepository.updateAllRunningTaskToFailure();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void execute(Long templateId, Map<String, Object> params) {
|
|
||||||
var template = taskTemplateService.detail(templateId);
|
|
||||||
var context = new TaskMonitorNodes.TaskMonitorContext(template);
|
|
||||||
flowExecutor.execute2Future(template.getChain(), params, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(rollbackOn = Throwable.class)
|
|
||||||
public void updateStepById(Integer step, Long id) {
|
|
||||||
taskRepository.updateStepById(step, id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.service;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.TaskTemplateRepository;
|
|
||||||
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
|
|
||||||
import com.yomahub.liteflow.builder.el.LiteFlowChainELBuilder;
|
|
||||||
import com.yomahub.liteflow.meta.LiteflowMetaOperator;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class TaskTemplateService extends SimpleServiceSupport<TaskTemplate> {
|
|
||||||
public TaskTemplateService(TaskTemplateRepository repository) {
|
|
||||||
super(repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateExpression(String expression) {
|
|
||||||
var response = LiteFlowChainELBuilder.validateWithEx(expression);
|
|
||||||
if (!response.isSuccess()) {
|
|
||||||
throw new RuntimeException(response.getCause());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long save(TaskTemplate entity) {
|
|
||||||
validateExpression(entity.getExpression());
|
|
||||||
Long id = super.save(entity);
|
|
||||||
LiteflowMetaOperator.reloadAllChain();
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void save(Iterable<TaskTemplate> taskTemplates) {
|
|
||||||
taskTemplates.forEach(template -> validateExpression(template.getExpression()));
|
|
||||||
super.save(taskTemplates);
|
|
||||||
LiteflowMetaOperator.reloadAllChain();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void remove(Iterable<Long> ids) {
|
|
||||||
super.remove(ids);
|
|
||||||
LiteflowMetaOperator.reloadAllChain();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void remove(Long id) {
|
|
||||||
super.remove(id);
|
|
||||||
LiteflowMetaOperator.reloadAllChain();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.service.task;
|
|
||||||
|
|
||||||
import cn.hutool.core.thread.ThreadUtil;
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
|
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
|
|
||||||
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@LiteflowComponent("check_daily")
|
|
||||||
public class CheckDailyNode extends TaskNodeComponent {
|
|
||||||
private final StockRepository stockRepository;
|
|
||||||
private final DailyRepository dailyRepository;
|
|
||||||
|
|
||||||
private final TuShareService tuShareService;
|
|
||||||
|
|
||||||
public CheckDailyNode(TaskService taskService, StockRepository stockRepository, DailyRepository dailyRepository, TuShareService tuShareService) {
|
|
||||||
super(taskService);
|
|
||||||
this.stockRepository = stockRepository;
|
|
||||||
this.dailyRepository = dailyRepository;
|
|
||||||
this.tuShareService = tuShareService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void process() {
|
|
||||||
var reports = new ArrayList<MissedTradeReport>();
|
|
||||||
var stocks = stockRepository.findAll();
|
|
||||||
var exchanges = stocks.stream().map(Stock::getMarket).distinct().toList();
|
|
||||||
for (Stock.Market exchange : exchanges) {
|
|
||||||
var nowDate = LocalDate.now();
|
|
||||||
var allTradeDates = tuShareService.tradeDateList(exchange.name())
|
|
||||||
.data()
|
|
||||||
.items()
|
|
||||||
.stream()
|
|
||||||
.map(item -> LocalDate.parse(item.get(0), TuShareService.TRADE_FORMAT))
|
|
||||||
.filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate))
|
|
||||||
.toList();
|
|
||||||
var total = stocks.size();
|
|
||||||
var progress = 0;
|
|
||||||
for (Stock stock : stocks) {
|
|
||||||
log.info("正在处理:{} {}", stock.getCode(), stock.getName());
|
|
||||||
if (exchange.equals(stock.getMarket())) {
|
|
||||||
var existsTradeDates = dailyRepository.findDistinctTradeDateByStockId(stock.getId());
|
|
||||||
var missedTradeDates = allTradeDates.stream()
|
|
||||||
.filter(date -> date.isAfter(stock.getListedDate()) || date.isEqual(stock.getListedDate()))
|
|
||||||
.filter(date -> !existsTradeDates.contains(date))
|
|
||||||
.filter(date -> {
|
|
||||||
ThreadUtil.safeSleep(100);
|
|
||||||
var response = tuShareService.dailyList(date, stock.getCode());
|
|
||||||
return !response.data().items().isEmpty();
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
if (ObjectUtil.isNotEmpty(missedTradeDates)) {
|
|
||||||
reports.add(new MissedTradeReport(
|
|
||||||
stock.getCode(),
|
|
||||||
stock.getName(),
|
|
||||||
missedTradeDates
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setStep(++progress * 100 / total);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ObjectUtil.isNotEmpty(reports)) {
|
|
||||||
var context = getContextBean(TaskMonitorNodes.TaskMonitorContext.class);
|
|
||||||
context.setTaskResult(
|
|
||||||
reports.stream()
|
|
||||||
.map(report -> StrUtil.format("{}({})缺少如下交易日数据:{}", report.name(), report.code(), report.missedTradeDates().stream().map(LocalDate::toString).collect(Collectors.joining(", "))))
|
|
||||||
.collect(Collectors.joining("\n"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record MissedTradeReport(
|
|
||||||
String code,
|
|
||||||
String name,
|
|
||||||
List<LocalDate> missedTradeDates
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.service.task;
|
|
||||||
|
|
||||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Task;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate;
|
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
|
|
||||||
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
|
||||||
import com.yomahub.liteflow.annotation.LiteflowFact;
|
|
||||||
import com.yomahub.liteflow.annotation.LiteflowMethod;
|
|
||||||
import com.yomahub.liteflow.core.NodeComponent;
|
|
||||||
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
|
|
||||||
import com.yomahub.liteflow.enums.NodeTypeEnum;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@LiteflowComponent
|
|
||||||
public class TaskMonitorNodes {
|
|
||||||
private final TaskService taskService;
|
|
||||||
|
|
||||||
public TaskMonitorNodes(TaskService taskService) {
|
|
||||||
this.taskService = taskService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "task_start", nodeName = "任务开始", nodeType = NodeTypeEnum.COMMON)
|
|
||||||
public void taskStart(NodeComponent node) {
|
|
||||||
try {
|
|
||||||
var context = node.getContextBean(TaskMonitorContext.class);
|
|
||||||
var task = new Task();
|
|
||||||
task.setName(context.getTemplate().getName());
|
|
||||||
task.setDescription(context.getTemplate().getDescription());
|
|
||||||
task.setStatus(Task.Status.RUNNING);
|
|
||||||
task.setLaunchedTime(LocalDateTime.now());
|
|
||||||
var taskId = taskService.save(task);
|
|
||||||
context.setTaskId(taskId);
|
|
||||||
} catch (Exception exception) {
|
|
||||||
log.warn("Not in task", exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "task_end", nodeName = "任务结束", nodeType = NodeTypeEnum.COMMON)
|
|
||||||
public void taskEnd(NodeComponent node, @LiteflowFact("taskId") Long taskId) {
|
|
||||||
if (ObjectUtil.isNotNull(taskId)) {
|
|
||||||
var task = taskService.detail(taskId);
|
|
||||||
task.setStatus(Task.Status.SUCCESS);
|
|
||||||
task.setStep(100);
|
|
||||||
task.setFinishedTime(LocalDateTime.now());
|
|
||||||
var result = node.<String>getContextValue("taskResult");
|
|
||||||
if (StrUtil.isNotBlank(result)) {
|
|
||||||
task.setResult(result);
|
|
||||||
}
|
|
||||||
taskService.save(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "task_error", nodeName = "任务错误", nodeType = NodeTypeEnum.COMMON)
|
|
||||||
public void taskError(NodeComponent node, @LiteflowFact("taskId") Long taskId) {
|
|
||||||
if (ObjectUtil.isNotNull(taskId)) {
|
|
||||||
var task = taskService.detail(taskId);
|
|
||||||
task.setStatus(Task.Status.FAILURE);
|
|
||||||
task.setFinishedTime(LocalDateTime.now());
|
|
||||||
var exception = node.getSlot().getException();
|
|
||||||
if (ObjectUtil.isNotNull(exception)) {
|
|
||||||
task.setError(ExceptionUtil.stacktraceToString(exception));
|
|
||||||
}
|
|
||||||
taskService.save(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static final class TaskMonitorContext {
|
|
||||||
private TaskTemplate template;
|
|
||||||
private Long taskId;
|
|
||||||
private String taskResult;
|
|
||||||
|
|
||||||
public TaskMonitorContext(TaskTemplate template) {
|
|
||||||
this.template = template;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.service.task;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
|
|
||||||
import com.yomahub.liteflow.core.NodeComponent;
|
|
||||||
import com.yomahub.liteflow.exception.NoSuchContextBeanException;
|
|
||||||
|
|
||||||
public abstract class TaskNodeComponent extends NodeComponent {
|
|
||||||
private final TaskService taskService;
|
|
||||||
|
|
||||||
protected TaskNodeComponent(TaskService taskService) {
|
|
||||||
this.taskService = taskService;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setStep(int step) {
|
|
||||||
if (step < 0 || step > 100) {
|
|
||||||
throw new IllegalArgumentException("step must be between 0 and 100");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
var context = getContextBean(TaskMonitorNodes.TaskMonitorContext.class);
|
|
||||||
taskService.updateStepById(step, context.getTaskId());
|
|
||||||
} catch (NoSuchContextBeanException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.leopard.server.service.task;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.NumberUtil;
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
|
|
||||||
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
|
|
||||||
import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper;
|
|
||||||
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
|
|
||||||
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
|
||||||
import com.yomahub.liteflow.core.NodeComponent;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@LiteflowComponent("update_daily")
|
|
||||||
public class UpdateDailyNode extends NodeComponent {
|
|
||||||
private final StockRepository stockRepository;
|
|
||||||
private final DailyRepository dailyRepository;
|
|
||||||
|
|
||||||
private final TuShareService tuShareService;
|
|
||||||
|
|
||||||
private final TransactionTemplate transactionTemplate;
|
|
||||||
|
|
||||||
public UpdateDailyNode(StockRepository stockRepository, DailyRepository dailyRepository, TuShareService tuShareService, TransactionTemplate transactionTemplate) {
|
|
||||||
this.stockRepository = stockRepository;
|
|
||||||
this.dailyRepository = dailyRepository;
|
|
||||||
this.tuShareService = tuShareService;
|
|
||||||
this.transactionTemplate = transactionTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void process() {
|
|
||||||
var tradeDates = new HashSet<LocalDate>();
|
|
||||||
for (String exchange : List.of("SSE", "SZSE", "BSE")) {
|
|
||||||
var response = tuShareService.tradeDateList(exchange);
|
|
||||||
for (List<String> item : response.data().items()) {
|
|
||||||
if (ObjectUtil.isNotEmpty(item) && StrUtil.isNotBlank(item.get(0))) {
|
|
||||||
tradeDates.add(LocalDate.parse(item.get(0), TuShareService.TRADE_FORMAT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var existsTradeDates = dailyRepository.findDistinctTradeDate();
|
|
||||||
var nowDate = LocalDate.now();
|
|
||||||
var stocksMap = stockRepository.findAll().stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
|
|
||||||
tradeDates.parallelStream()
|
|
||||||
.filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate))
|
|
||||||
.filter(date -> !existsTradeDates.contains(date))
|
|
||||||
.filter(date -> date.isAfter(LocalDate.of(2024, 12, 31)))
|
|
||||||
.forEach(tradeDate -> {
|
|
||||||
var factorResponse = tuShareService.factorList(tradeDate);
|
|
||||||
var factorMap = new HashMap<String, Double>();
|
|
||||||
for (List<String> item : factorResponse.data().items()) {
|
|
||||||
factorMap.put(item.get(0), Double.valueOf(item.get(2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = tuShareService.dailyList(tradeDate);
|
|
||||||
transactionTemplate.execute(status -> {
|
|
||||||
try {
|
|
||||||
for (List<String> item : response.data().items()) {
|
|
||||||
var code = item.get(0);
|
|
||||||
if (stocksMap.containsKey(code)) {
|
|
||||||
var stock = stocksMap.get(code);
|
|
||||||
var factor = factorMap.get(code);
|
|
||||||
var daily = new Daily();
|
|
||||||
daily.setTradeDate(tradeDate);
|
|
||||||
daily.setOpen(NumberHelper.parseDouble(item.get(2)));
|
|
||||||
daily.setHigh(NumberUtil.parseDouble(item.get(3)));
|
|
||||||
daily.setLow(NumberUtil.parseDouble(item.get(4)));
|
|
||||||
daily.setClose(NumberUtil.parseDouble(item.get(5)));
|
|
||||||
daily.setPreviousClose(NumberUtil.parseDouble(item.get(6)));
|
|
||||||
daily.setPriceChangeAmount(NumberUtil.parseDouble(item.get(7)));
|
|
||||||
daily.setPriceFluctuationRange(NumberUtil.parseDouble(item.get(8)));
|
|
||||||
daily.setVolume(NumberUtil.parseDouble(item.get(9)));
|
|
||||||
daily.setTurnover(NumberUtil.parseDouble(item.get(10)));
|
|
||||||
daily.setFactor(factor);
|
|
||||||
daily.setStock(stock);
|
|
||||||
dailyRepository.save(daily);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (Exception exception) {
|
|
||||||
log.error("Error", exception);
|
|
||||||
status.setRollbackOnly();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
leopard-server/src/main/resources/application-dev.yml
Normal file
3
leopard-server/src/main/resources/application-dev.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://127.0.0.1:6785/leopard_dev
|
||||||
14
leopard-server/src/main/resources/application-h2.yml
Normal file
14
leopard-server/src/main/resources/application-h2.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:h2:file:./leopard;DB_CLOSE_ON_EXIT=TRUE
|
||||||
|
username: leopard
|
||||||
|
password: leopard
|
||||||
|
driver-class-name: org.h2.Driver
|
||||||
|
quartz:
|
||||||
|
jdbc:
|
||||||
|
platform: h2
|
||||||
|
properties:
|
||||||
|
org:
|
||||||
|
quartz:
|
||||||
|
jobStore:
|
||||||
|
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
|
||||||
@@ -23,7 +23,7 @@ spring:
|
|||||||
job-store-type: jdbc
|
job-store-type: jdbc
|
||||||
jdbc:
|
jdbc:
|
||||||
platform: postgres
|
platform: postgres
|
||||||
# initialize-schema: always
|
initialize-schema: always
|
||||||
properties:
|
properties:
|
||||||
org:
|
org:
|
||||||
quartz:
|
quartz:
|
||||||
@@ -31,13 +31,3 @@ spring:
|
|||||||
driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
|
driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
|
||||||
fenix:
|
fenix:
|
||||||
print-banner: false
|
print-banner: false
|
||||||
liteflow:
|
|
||||||
print-banner: false
|
|
||||||
check-node-exists: false
|
|
||||||
rule-source-ext-data-map:
|
|
||||||
applicationName: ${spring.application.name}
|
|
||||||
sqlLogEnabled: true
|
|
||||||
chainTableName: leopard_task_template
|
|
||||||
chainApplicationNameField: application
|
|
||||||
chainNameField: chain
|
|
||||||
elDataField: expression_el
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.server;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.leopard.server.helper.MdHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250917
|
||||||
|
*/
|
||||||
|
public class MdTest {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println(
|
||||||
|
MdHelper.of()
|
||||||
|
.bigTitle("Markdown Helper")
|
||||||
|
.title("Markdown Helper")
|
||||||
|
.subTitle("Markdown Helper")
|
||||||
|
.code("java")
|
||||||
|
.text("System.out.println()")
|
||||||
|
.endCode()
|
||||||
|
.divider()
|
||||||
|
.table()
|
||||||
|
.data(new Object[]{"name", "value"}, new Object[][]{new Object[]{"1", "2"}, new Object[]{"3", "4"}})
|
||||||
|
.endTable()
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,7 +109,8 @@ Content-Type: application/json
|
|||||||
"api_name": "adj_factor",
|
"api_name": "adj_factor",
|
||||||
"token": "{{api_key}}",
|
"token": "{{api_key}}",
|
||||||
"params": {
|
"params": {
|
||||||
"trade_date": "20241231"
|
"ts_code": "600018.SH",
|
||||||
|
"trade_date": "20060331"
|
||||||
},
|
},
|
||||||
"fields": [
|
"fields": [
|
||||||
"ts_code",
|
"ts_code",
|
||||||
@@ -126,16 +127,18 @@ Content-Type: application/json
|
|||||||
"api_name": "stk_factor_pro",
|
"api_name": "stk_factor_pro",
|
||||||
"token": "{{api_key}}",
|
"token": "{{api_key}}",
|
||||||
"params": {
|
"params": {
|
||||||
"ts_code": "000001.SZ",
|
"ts_code": "600018.SH",
|
||||||
"trade_date": "20250225"
|
"trade_date": "20060331"
|
||||||
},
|
},
|
||||||
"fields": [
|
"fields": [
|
||||||
"ts_code",
|
"ts_code",
|
||||||
"trade_date",
|
"trade_date",
|
||||||
|
"open",
|
||||||
|
"open_qfq",
|
||||||
|
"open_hfq",
|
||||||
"close",
|
"close",
|
||||||
"close_qfq",
|
"close_qfq",
|
||||||
"close_hfq",
|
"close_hfq"
|
||||||
"ema_hfq_5"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,8 +213,8 @@ Content-Type: application/json
|
|||||||
"api_name": "fina_indicator",
|
"api_name": "fina_indicator",
|
||||||
"token": "{{api_key}}",
|
"token": "{{api_key}}",
|
||||||
"params": {
|
"params": {
|
||||||
"ts_code": "600132.SH",
|
"ts_code": "001400.SZ",
|
||||||
"period": "20191231"
|
"period": "20231231"
|
||||||
},
|
},
|
||||||
"fields": [
|
"fields": [
|
||||||
"ts_code",
|
"ts_code",
|
||||||
@@ -239,3 +242,25 @@ Content-Type: application/json
|
|||||||
"resume_date"
|
"resume_date"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
### Get Index
|
||||||
|
POST {{api_url}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"api_name": "index_basic",
|
||||||
|
"token": "{{api_key}}",
|
||||||
|
"params": {
|
||||||
|
"market": "CSI"
|
||||||
|
},
|
||||||
|
"fields": [
|
||||||
|
"ts_code",
|
||||||
|
"name",
|
||||||
|
"fullname",
|
||||||
|
"market",
|
||||||
|
"publisher",
|
||||||
|
"index_type",
|
||||||
|
"category",
|
||||||
|
"desc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,14 +21,27 @@
|
|||||||
<artifactId>spring-boot-starter</artifactId>
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.ralfkonrad.quantlib_for_maven</groupId>
|
<groupId>com.yomahub</groupId>
|
||||||
<artifactId>quantlib</artifactId>
|
<artifactId>liteflow-spring-boot-starter</artifactId>
|
||||||
<version>1.39.0</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ta4j</groupId>
|
<groupId>org.commonmark</groupId>
|
||||||
<artifactId>ta4j-core</artifactId>
|
<artifactId>commonmark</artifactId>
|
||||||
<version>0.17</version>
|
<version>0.26.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.commonmark</groupId>
|
||||||
|
<artifactId>commonmark-ext-gfm-tables</artifactId>
|
||||||
|
<version>0.26.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.thymeleaf</groupId>
|
||||||
|
<artifactId>thymeleaf</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-extra</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -41,6 +54,11 @@
|
|||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -1,31 +1,292 @@
|
|||||||
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.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.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.strategy.TradeEngine;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.transaction.Transactional;
|
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 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;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.event.EventListener;
|
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 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 TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TradeEngine tradeEngine;
|
||||||
@Resource
|
@Resource
|
||||||
private StockRepository stockRepository;
|
private StockRepository stockRepository;
|
||||||
@Resource
|
@Resource
|
||||||
private DailyRepository dailyRepository;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(rollbackOn = Throwable.class)
|
@Transactional(readOnly = true)
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
public void test() {
|
public void backtest() throws IOException {
|
||||||
|
var startDate = LocalDate.of(2024, 12, 1);
|
||||||
|
var endDate = LocalDate.of(2024, 12, 31);
|
||||||
|
var charts = new ArrayList<Dict>();
|
||||||
|
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 stock = stockRepository.findOne(QStock.stock.code.eq(code)).orElseThrow();
|
||||||
|
var asset = tradeEngine.backtest(
|
||||||
|
stockRepository.findById(stock.getId()).orElseThrow(),
|
||||||
|
(now, currentAsset, dailies) -> {
|
||||||
|
var stockDailies = dailies
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
|
.toList();
|
||||||
|
var yesterday = stockDailies.getLast();
|
||||||
|
if (yesterday.getHfqClose() > yesterday.getHfqOpen()) {
|
||||||
|
return 100;
|
||||||
|
} else if (yesterday.getHfqClose() < yesterday.getHfqOpen()) {
|
||||||
|
var hold = currentAsset.getVolume();
|
||||||
|
if (hold > 0) {
|
||||||
|
return -1 * hold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
);
|
||||||
|
var sources = dailyRepository.findAll(
|
||||||
|
QDaily.daily.stock.code.eq(code)
|
||||||
|
.and(QDaily.daily.tradeDate.after(startDate.minusDays(150)))
|
||||||
|
.and(QDaily.daily.tradeDate.before(endDate)),
|
||||||
|
QDaily.daily.tradeDate.asc()
|
||||||
|
);
|
||||||
|
|
||||||
|
var dailies = sources.stream()
|
||||||
|
.filter(daily -> daily.getTradeDate().isAfter(startDate) && daily.getTradeDate().isBefore(endDate))
|
||||||
|
.sorted(Comparator.comparing(Daily::getTradeDate))
|
||||||
|
.toList();
|
||||||
|
var oclhList = new HashMap<>();
|
||||||
|
var dailyCloseMapping = new HashMap<String, Double>();
|
||||||
|
for (var daily : dailies) {
|
||||||
|
oclhList.put(
|
||||||
|
daily.getTradeDate().toString(),
|
||||||
|
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("type", "candle")
|
||||||
|
.set(
|
||||||
|
"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)
|
||||||
|
@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<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) {
|
||||||
|
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)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ spring:
|
|||||||
application:
|
application:
|
||||||
name: leopard-strategy
|
name: leopard-strategy
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://81.71.3.24:6785/leopard_dev
|
url: jdbc:postgresql://81.71.3.24:6785/leopard
|
||||||
username: leopard
|
username: leopard
|
||||||
password: '9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X'
|
password: '9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X'
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
@@ -12,3 +12,6 @@ spring:
|
|||||||
banner-mode: off
|
banner-mode: off
|
||||||
fenix:
|
fenix:
|
||||||
print-banner: false
|
print-banner: false
|
||||||
|
liteflow:
|
||||||
|
print-banner: false
|
||||||
|
check-node-exists: false
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.lanyuanxiaoyao.leopard.strategy;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import org.commonmark.ext.gfm.tables.TablesExtension;
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
import org.commonmark.renderer.html.HtmlRenderer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markdown Render
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250918
|
||||||
|
*/
|
||||||
|
public class TestMarkdown {
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
var extensions = List.of(TablesExtension.create());
|
||||||
|
var parser = Parser.builder()
|
||||||
|
.extensions(extensions)
|
||||||
|
.build();
|
||||||
|
var render = HtmlRenderer.builder()
|
||||||
|
.extensions(extensions)
|
||||||
|
.build();
|
||||||
|
var result = render.render(parser.parse(
|
||||||
|
// language=Markdown
|
||||||
|
"""
|
||||||
|
### Hello
|
||||||
|
|
||||||
|
```echarts
|
||||||
|
System.out.println("go");
|
||||||
|
```
|
||||||
|
|
||||||
|
> I am iron man
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|------|----|
|
||||||
|
| Tony | 12 |
|
||||||
|
"""
|
||||||
|
));
|
||||||
|
Files.writeString(Path.of("result.html"), result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,14 +6,13 @@ import Overview from './pages/Overview.tsx'
|
|||||||
import Root from './pages/Root.tsx'
|
import Root from './pages/Root.tsx'
|
||||||
import Test from './pages/Test.tsx'
|
import Test from './pages/Test.tsx'
|
||||||
import StockList from './pages/stock/StockList.tsx'
|
import StockList from './pages/stock/StockList.tsx'
|
||||||
import StockDetail from './pages/stock/StockDetail.tsx'
|
|
||||||
import TaskList from './pages/task/TaskList.tsx'
|
import TaskList from './pages/task/TaskList.tsx'
|
||||||
import TaskTemplateList from './pages/task/TaskTemplateList.tsx'
|
import TaskTemplateList from './pages/task/TaskTemplateList.tsx'
|
||||||
import TaskTemplateSave from './pages/task/TaskTemplateSave.tsx'
|
|
||||||
import TaskScheduleList from './pages/task/TaskScheduleList.tsx'
|
import TaskScheduleList from './pages/task/TaskScheduleList.tsx'
|
||||||
import TaskScheduleSave from './pages/task/TaskScheduleSave.tsx'
|
import TaskScheduleSave from './pages/task/TaskScheduleSave.tsx'
|
||||||
import StockCollectionList from './pages/stock/StockCollectionList.tsx'
|
import StockCollectionList from './pages/stock/StockCollectionList.tsx'
|
||||||
import TaskDetail from './pages/task/TaskDetail.tsx'
|
import TaskDetail from './pages/task/TaskDetail.tsx'
|
||||||
|
import StockCollectionDetail from './pages/stock/StockCollectionDetail.tsx'
|
||||||
|
|
||||||
const routes: RouteObject[] = [
|
const routes: RouteObject[] = [
|
||||||
{
|
{
|
||||||
@@ -35,10 +34,6 @@ const routes: RouteObject[] = [
|
|||||||
path: 'list',
|
path: 'list',
|
||||||
Component: StockList,
|
Component: StockList,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'detail/:id',
|
|
||||||
Component: StockDetail,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "collection",
|
path: "collection",
|
||||||
children: [
|
children: [
|
||||||
@@ -46,6 +41,10 @@ const routes: RouteObject[] = [
|
|||||||
path: 'list',
|
path: 'list',
|
||||||
Component: StockCollectionList,
|
Component: StockCollectionList,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'detail/:id',
|
||||||
|
Component: StockCollectionDetail,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -68,10 +67,6 @@ const routes: RouteObject[] = [
|
|||||||
path: 'list',
|
path: 'list',
|
||||||
Component: TaskTemplateList,
|
Component: TaskTemplateList,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'save/:id',
|
|
||||||
Component: TaskTemplateSave,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
50
leopard-web/src/pages/stock/StockCollectionDetail.tsx
Normal file
50
leopard-web/src/pages/stock/StockCollectionDetail.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React from "react"
|
||||||
|
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, stockListColumns} from '../../util/amis.tsx'
|
||||||
|
import {useParams} from 'react-router'
|
||||||
|
|
||||||
|
function StockCollectionDetail() {
|
||||||
|
const {id} = useParams()
|
||||||
|
return (
|
||||||
|
<div className="stock-collection-detail">
|
||||||
|
{amisRender(
|
||||||
|
{
|
||||||
|
type: 'page',
|
||||||
|
title: '股票集详情',
|
||||||
|
initApi: `get:${commonInfo.baseUrl}/stock_collection/detail/${id}`,
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: 'crud',
|
||||||
|
source: '${scores}',
|
||||||
|
...crudCommonOptions(),
|
||||||
|
...paginationTemplate(100, undefined, ['filter-toggler']),
|
||||||
|
columns: stockListColumns(
|
||||||
|
undefined,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
className: 'white-space-pre',
|
||||||
|
name: 'score',
|
||||||
|
label: '得分',
|
||||||
|
width: 50,
|
||||||
|
align: 'center',
|
||||||
|
type: 'tpl',
|
||||||
|
tpl: '${score}',
|
||||||
|
popOver: {
|
||||||
|
trigger: 'click',
|
||||||
|
showIcon: false,
|
||||||
|
body: {
|
||||||
|
type: 'tpl',
|
||||||
|
tpl: '${extra|raw}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(StockCollectionDetail)
|
||||||
@@ -1,8 +1,103 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, time} from '../../util/amis.tsx'
|
||||||
|
import {useNavigate} from 'react-router'
|
||||||
|
|
||||||
function StockCollectionList() {
|
function StockCollectionList() {
|
||||||
|
const navigate = useNavigate()
|
||||||
return (
|
return (
|
||||||
<div className="stock-collection-list"></div>
|
<div className="stock-collection-list">
|
||||||
|
{amisRender(
|
||||||
|
{
|
||||||
|
type: 'page',
|
||||||
|
title: '股票列表',
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: 'crud',
|
||||||
|
api: {
|
||||||
|
method: 'post',
|
||||||
|
url: `${commonInfo.baseUrl}/stock_collection/list`,
|
||||||
|
data: {
|
||||||
|
sort: [
|
||||||
|
{
|
||||||
|
column: 'createdTime',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...crudCommonOptions(),
|
||||||
|
...paginationTemplate(15, undefined, ['filter-toggler']),
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: '名称',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
label: '描述',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'count',
|
||||||
|
label: '股票数量',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'createdTime',
|
||||||
|
label: '创建时间',
|
||||||
|
width: 150,
|
||||||
|
align: 'center',
|
||||||
|
...time('createdTime'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'modifiedTime',
|
||||||
|
label: '更新时间',
|
||||||
|
width: 150,
|
||||||
|
align: 'center',
|
||||||
|
...time('modifiedTime'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'operation',
|
||||||
|
label: '操作',
|
||||||
|
width: 100,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
label: '详情',
|
||||||
|
level: 'link',
|
||||||
|
onEvent: {
|
||||||
|
click: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
actionType: 'custom',
|
||||||
|
// @ts-ignore
|
||||||
|
script: (context, action, event) => {
|
||||||
|
navigate(`/stock/collection/detail/${context.props.data['id']}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'text-danger btn-deleted',
|
||||||
|
type: 'action',
|
||||||
|
label: '删除',
|
||||||
|
level: 'link',
|
||||||
|
actionType: 'ajax',
|
||||||
|
api: `get:${commonInfo.baseUrl}/stock_collection/remove/\${id}`,
|
||||||
|
confirmText: '确认删除股票集<span class="text-lg font-bold mx-2">${name}</span>?',
|
||||||
|
confirmTitle: '删除',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,550 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {useParams} from 'react-router'
|
|
||||||
import {amisRender, commonInfo, readOnlyDialogOptions, remoteMappings} from '../../util/amis.tsx'
|
|
||||||
import type {Schema} from 'amis'
|
|
||||||
import {isNil} from 'es-toolkit'
|
|
||||||
import {toNumber} from 'es-toolkit/compat'
|
|
||||||
|
|
||||||
const formatFinanceNumber = (value: number): string => {
|
|
||||||
if (isNil(value)) {
|
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNegative = value < 0
|
|
||||||
const absoluteValue = Math.abs(value)
|
|
||||||
|
|
||||||
let formatted: string
|
|
||||||
if (absoluteValue >= 100000000) {
|
|
||||||
formatted = (absoluteValue / 100000000).toFixed(2) + '亿'
|
|
||||||
} else if (absoluteValue >= 10000) {
|
|
||||||
formatted = (absoluteValue / 10000).toFixed(2) + '万'
|
|
||||||
} else {
|
|
||||||
formatted = absoluteValue.toLocaleString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return isNegative ? `-${formatted}` : formatted
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDaysNumber = (value: number): string => {
|
|
||||||
if (isNil(value)) {
|
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
return `${value.toFixed(0)}天`
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatPercentageNumber = (value: number): string => {
|
|
||||||
if (isNil(value)) {
|
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
return `${(value * 100).toFixed(2)}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FinanceType = 'PERCENTAGE' | 'FINANCE' | 'DAYS'
|
|
||||||
|
|
||||||
const financePropertyLabel = (id: string | undefined, label: string, type: FinanceType, field: string): Schema => {
|
|
||||||
if (!id) {
|
|
||||||
return {
|
|
||||||
type: 'tpl',
|
|
||||||
tpl: label,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let formatter: (value: number) => string
|
|
||||||
switch (type) {
|
|
||||||
case 'PERCENTAGE':
|
|
||||||
formatter = formatPercentageNumber
|
|
||||||
break
|
|
||||||
case 'FINANCE':
|
|
||||||
formatter = formatFinanceNumber
|
|
||||||
break
|
|
||||||
case 'DAYS':
|
|
||||||
formatter = formatDaysNumber
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
formatter = (v: number) => v.toFixed(2)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'wrapper',
|
|
||||||
size: 'none',
|
|
||||||
body: [
|
|
||||||
{
|
|
||||||
className: 'text-current font-bold',
|
|
||||||
type: 'action',
|
|
||||||
label: label,
|
|
||||||
level: 'link',
|
|
||||||
tooltip: '这是什么?',
|
|
||||||
tooltipPlacement: 'top',
|
|
||||||
actionType: 'dialog',
|
|
||||||
dialog: {
|
|
||||||
title: '',
|
|
||||||
size: 'lg',
|
|
||||||
...readOnlyDialogOptions(),
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
type: 'action',
|
|
||||||
label: '新页面打开',
|
|
||||||
icon: 'fa fa-solid fa-arrow-up-right-from-square',
|
|
||||||
actionType: 'url',
|
|
||||||
url: `https://zh.wikipedia.org/wiki/${label}`,
|
|
||||||
blank: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
body: {
|
|
||||||
type: 'iframe',
|
|
||||||
src: `https://zh.wikipedia.org/wiki/${label}`,
|
|
||||||
height: 800,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
className: 'text-secondary',
|
|
||||||
type: 'action',
|
|
||||||
label: '',
|
|
||||||
icon: 'fa fa-eye',
|
|
||||||
level: 'link',
|
|
||||||
size: 'xs',
|
|
||||||
tooltip: '查看五年趋势',
|
|
||||||
tooltipPlacement: 'top',
|
|
||||||
actionType: 'dialog',
|
|
||||||
dialog: {
|
|
||||||
title: `${label}五年趋势`,
|
|
||||||
size: 'lg',
|
|
||||||
bodyClassName: 'p-0',
|
|
||||||
...readOnlyDialogOptions(),
|
|
||||||
body: {
|
|
||||||
type: 'chart',
|
|
||||||
api: `get:${commonInfo.baseUrl}/stock/finance/${id}/${field}`,
|
|
||||||
height: 500,
|
|
||||||
config: {
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
||||||
borderColor: '#ccc',
|
|
||||||
borderWidth: 1,
|
|
||||||
textStyle: {
|
|
||||||
color: '#333',
|
|
||||||
},
|
|
||||||
padding: [10, 15],
|
|
||||||
formatter: (params: any) => {
|
|
||||||
const item = params[0]
|
|
||||||
return `${item.name}<br/>${item.marker}${formatter(item.value)}`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '5%',
|
|
||||||
right: '5%',
|
|
||||||
top: '10%',
|
|
||||||
bottom: '15%',
|
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: '${xList || []}',
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: '#e0e0e0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: '#666',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
show: true,
|
|
||||||
splitLine: {
|
|
||||||
lineStyle: {
|
|
||||||
type: 'dashed',
|
|
||||||
color: '#f0f0f0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: '#999',
|
|
||||||
fontSize: 12,
|
|
||||||
formatter: (value: number) => {
|
|
||||||
return formatter(value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
data: '${yList || []}',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
showSymbol: true,
|
|
||||||
symbolSize: 6,
|
|
||||||
lineStyle: {
|
|
||||||
width: 3,
|
|
||||||
color: '#4096ff',
|
|
||||||
shadowColor: 'rgba(64, 150, 255, 0.3)',
|
|
||||||
shadowBlur: 5,
|
|
||||||
shadowOffsetY: 2,
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
color: '#4096ff',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: '#fff',
|
|
||||||
},
|
|
||||||
areaStyle: {
|
|
||||||
color: {
|
|
||||||
type: 'linear',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
x2: 0,
|
|
||||||
y2: 1,
|
|
||||||
colorStops: [{
|
|
||||||
offset: 0, color: 'rgba(64, 150, 255, 0.2)',
|
|
||||||
}, {
|
|
||||||
offset: 1, color: 'rgba(64, 150, 255, 0.01)',
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
position: 'top',
|
|
||||||
color: '#333',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 12,
|
|
||||||
formatter: (params: any) => {
|
|
||||||
return formatter(params.value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function StockDetail() {
|
|
||||||
const {id} = useParams()
|
|
||||||
return (
|
|
||||||
<div className="stock-detail">
|
|
||||||
{amisRender(
|
|
||||||
{
|
|
||||||
type: 'page',
|
|
||||||
title: '股票详情(${code} ${name})',
|
|
||||||
initApi: `get:${commonInfo.baseUrl}/stock/detail/${id}`,
|
|
||||||
body: [
|
|
||||||
{
|
|
||||||
type: 'property',
|
|
||||||
items: [
|
|
||||||
{label: '编码', content: '${code}'},
|
|
||||||
{label: '名称', content: '${name}'},
|
|
||||||
{label: '全名', content: '${fullname}'},
|
|
||||||
{
|
|
||||||
label: '市场',
|
|
||||||
content: {
|
|
||||||
...remoteMappings('stock_market', 'market'),
|
|
||||||
value: '${market}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{label: '行业', content: '${industry}'},
|
|
||||||
{label: '上市日期', content: '${listedDate}'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{type: 'divider'},
|
|
||||||
{
|
|
||||||
type: 'service',
|
|
||||||
api: `get:${commonInfo.baseUrl}/stock/finance/${id}`,
|
|
||||||
body: [
|
|
||||||
'资产负债表',
|
|
||||||
{
|
|
||||||
className: 'my-2',
|
|
||||||
type: 'property',
|
|
||||||
column: 4,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '总资产', 'FINANCE', 'totalAssets'),
|
|
||||||
content: '${balanceSheet.totalAssets}',
|
|
||||||
span: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '总负债', 'FINANCE', 'totalLiabilities'),
|
|
||||||
content: '${balanceSheet.totalLiabilities}',
|
|
||||||
span: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '流动资产', 'FINANCE', 'currentAssets'),
|
|
||||||
content: '${balanceSheet.currentAssets}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '流动资产占比', 'PERCENTAGE', 'currentAssetsToTotalAssetsRatio'),
|
|
||||||
content: '${balanceSheet.currentAssetsRatio}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '流动负债', 'FINANCE', 'currentLiabilities'),
|
|
||||||
content: '${balanceSheet.currentLiabilities}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '流动负债占比', 'PERCENTAGE', 'currentLiabilitiesToTotalAssetsRatio'),
|
|
||||||
content: '${balanceSheet.currentLiabilitiesRatio}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '非流动资产', 'FINANCE', 'fixedAssets'),
|
|
||||||
content: '${balanceSheet.fixedAssets}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '非流动资产占比', 'PERCENTAGE', 'fixedAssetsToTotalAssetsRatio'),
|
|
||||||
content: '${balanceSheet.fixedAssetsRatio}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '非流动负债', 'FINANCE', 'longTermLiabilities'),
|
|
||||||
content: '${balanceSheet.longTermLiabilities}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '非流动负债占比', 'PERCENTAGE', 'longTermLiabilitiesToTotalAssetsRatio'),
|
|
||||||
content: '${balanceSheet.longTermLiabilitiesRatio}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'利润表',
|
|
||||||
{
|
|
||||||
className: 'my-2',
|
|
||||||
type: 'property',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '营业收入', 'FINANCE', 'operatingRevenue'),
|
|
||||||
content: '${income.operatingRevenue}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '营业成本', 'FINANCE', 'operatingCost'),
|
|
||||||
content: '${income.operatingCost}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '营业利润', 'FINANCE', 'operatingProfit'),
|
|
||||||
content: '${income.operatingProfit}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'现金流量表',
|
|
||||||
{
|
|
||||||
className: 'my-2',
|
|
||||||
type: 'property',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '净利润', 'FINANCE', 'netProfit'),
|
|
||||||
content: '${cashFlow.netProfit}',
|
|
||||||
span: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '营业活动现金流量', 'FINANCE', 'cashFlowFromOperatingActivities'),
|
|
||||||
content: '${cashFlow.cashFlowFromOperatingActivities}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '投资活动现金流量', 'FINANCE', 'cashFlowFromInvestingActivities'),
|
|
||||||
content: '${cashFlow.cashFlowFromInvestingActivities}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '筹资活动现金流量', 'FINANCE', 'cashFlowFromFinancingActivities'),
|
|
||||||
content: '${cashFlow.cashFlowFromFinancingActivities}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'财务指标',
|
|
||||||
{
|
|
||||||
className: 'my-2',
|
|
||||||
type: 'property',
|
|
||||||
column: 4,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '流动比率', 'FINANCE', 'currentRatio'),
|
|
||||||
content: '${indicate.currentRatio}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '速动比率', 'FINANCE', 'quickRatio'),
|
|
||||||
content: '${indicate.quickRatio}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, 'ROE', 'FINANCE', 'returnOnEquity'),
|
|
||||||
content: '${indicate.roe}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, 'ROA', 'FINANCE', 'returnOnAssets'),
|
|
||||||
content: '${indicate.roa}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '应收账款周转率', 'FINANCE', 'accountsReceivableTurnover'),
|
|
||||||
content: '${indicate.accountsReceivableTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '应收账款周转天数', 'DAYS', 'daysAccountsReceivableTurnover'),
|
|
||||||
content: '${indicate.daysAccountsReceivableTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '存货周转率', 'FINANCE', 'inventoryTurnover'),
|
|
||||||
content: '${indicate.inventoryTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '存货周转天数', 'DAYS', 'daysInventoryTurnover'),
|
|
||||||
content: '${indicate.daysInventoryTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '固定资产周转率', 'FINANCE', 'fixedAssetsTurnover'),
|
|
||||||
content: '${indicate.fixedAssetsTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '固定资产周转天数', 'DAYS', 'daysFixedAssetsTurnover'),
|
|
||||||
content: '${indicate.daysFixedAssetsTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '总资产周转率', 'FINANCE', 'totalAssetsTurnover'),
|
|
||||||
content: '${indicate.totalAssetsTurnover}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: financePropertyLabel(id, '总资产周转天数', 'DAYS', 'daysTotalAssetsTurnover'),
|
|
||||||
content: '${indicate.daysTotalAssetsTurnover}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{type: 'divider'},
|
|
||||||
"100日线数据",
|
|
||||||
{
|
|
||||||
type: 'chart',
|
|
||||||
height: 500,
|
|
||||||
api: `get:${commonInfo.baseUrl}/stock/daily/${id}`,
|
|
||||||
config: {
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
animation: true,
|
|
||||||
animationDuration: 1000,
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: {
|
|
||||||
type: 'cross',
|
|
||||||
},
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
||||||
borderColor: '#333',
|
|
||||||
borderWidth: 1,
|
|
||||||
textStyle: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
padding: 12,
|
|
||||||
formatter: function (params: any) {
|
|
||||||
const param = params[0]
|
|
||||||
const open = toNumber(param.data[1]).toFixed(2)
|
|
||||||
const close = toNumber(param.data[2]).toFixed(2)
|
|
||||||
const lowest = toNumber(param.data[3]).toFixed(2)
|
|
||||||
const highest = toNumber(param.data[4]).toFixed(2)
|
|
||||||
|
|
||||||
return `<div class="text-center font-bold mb-2">${param.name}</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<span>开盘:</span>
|
|
||||||
<span class="font-bold ml-4">${open}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<span>收盘:</span>
|
|
||||||
<span class="font-bold ml-4">${close}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<span>最低:</span>
|
|
||||||
<span class="font-bold ml-4">${lowest}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<span>最高:</span>
|
|
||||||
<span class="font-bold ml-4">${highest}</span>
|
|
||||||
</div>`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '10%',
|
|
||||||
right: '10%',
|
|
||||||
top: '10%',
|
|
||||||
bottom: '15%',
|
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
data: '${xList || []}',
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: '#e0e0e0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: '#666',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
scale: true,
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: '#e0e0e0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: '#666',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
formatter: function (value: number) {
|
|
||||||
return value.toFixed(2)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
lineStyle: {
|
|
||||||
type: 'dashed',
|
|
||||||
color: '#f0f0f0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataZoom: [
|
|
||||||
{
|
|
||||||
type: 'inside',
|
|
||||||
start: 0,
|
|
||||||
end: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: true,
|
|
||||||
type: 'slider',
|
|
||||||
top: '90%',
|
|
||||||
start: 0,
|
|
||||||
end: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'candlestick',
|
|
||||||
data: '${yList || []}',
|
|
||||||
itemStyle: {
|
|
||||||
color: '#eb5454',
|
|
||||||
color0: '#4aaa93',
|
|
||||||
borderColor: '#eb5454',
|
|
||||||
borderColor0: '#4aaa93',
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"12月线数据",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(StockDetail)
|
|
||||||
@@ -3,15 +3,12 @@ import {
|
|||||||
amisRender,
|
amisRender,
|
||||||
commonInfo,
|
commonInfo,
|
||||||
crudCommonOptions,
|
crudCommonOptions,
|
||||||
date,
|
|
||||||
paginationTemplate,
|
paginationTemplate,
|
||||||
remoteMappings,
|
|
||||||
remoteOptions,
|
remoteOptions,
|
||||||
|
stockListColumns,
|
||||||
} from '../../util/amis.tsx'
|
} from '../../util/amis.tsx'
|
||||||
import {useNavigate} from 'react-router'
|
|
||||||
|
|
||||||
function StockList() {
|
function StockList() {
|
||||||
const navigate = useNavigate()
|
|
||||||
return (
|
return (
|
||||||
<div className="stock-list">
|
<div className="stock-list">
|
||||||
{amisRender(
|
{amisRender(
|
||||||
@@ -97,65 +94,7 @@ function StockList() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
columns: [
|
columns: stockListColumns(),
|
||||||
{
|
|
||||||
name: 'code',
|
|
||||||
label: '编号',
|
|
||||||
width: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: '简称',
|
|
||||||
width: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fullname',
|
|
||||||
label: '全名',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'market',
|
|
||||||
label: '市场',
|
|
||||||
width: 100,
|
|
||||||
align: 'center',
|
|
||||||
...remoteMappings('stock_market', 'market'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'industry',
|
|
||||||
label: '行业',
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '上市日期',
|
|
||||||
width: 100,
|
|
||||||
align: 'center',
|
|
||||||
...date('listedDate'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'operation',
|
|
||||||
label: '操作',
|
|
||||||
width: 100,
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
type: 'action',
|
|
||||||
label: '详情',
|
|
||||||
level: 'link',
|
|
||||||
onEvent: {
|
|
||||||
click: {
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
actionType: 'custom',
|
|
||||||
// @ts-ignore
|
|
||||||
script: (context, action, event) => {
|
|
||||||
navigate(`/stock/detail/${context.props.data['id']}`)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function TaskDetail() {
|
|||||||
label: '进度',
|
label: '进度',
|
||||||
content: {
|
content: {
|
||||||
type: 'tpl',
|
type: 'tpl',
|
||||||
tpl: "${step}%",
|
tpl: "${ROUND(step * 100, 2)}%",
|
||||||
},
|
},
|
||||||
span: 2,
|
span: 2,
|
||||||
},
|
},
|
||||||
@@ -56,9 +56,10 @@ function TaskDetail() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
visibleOn: 'result',
|
visibleOn: 'result',
|
||||||
type: 'editor',
|
type: 'markdown-enhance',
|
||||||
name: 'result',
|
name: 'result',
|
||||||
label: '结果',
|
label: '结果',
|
||||||
|
content: '${result}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, remoteMappings, time} from '../../util/amis.tsx'
|
import {
|
||||||
|
amisRender,
|
||||||
|
commonInfo,
|
||||||
|
crudCommonOptions,
|
||||||
|
horizontalFormOptions,
|
||||||
|
paginationTemplate,
|
||||||
|
remoteMappings,
|
||||||
|
remoteOptions,
|
||||||
|
time,
|
||||||
|
} from '../../util/amis.tsx'
|
||||||
import {useNavigate} from 'react-router'
|
import {useNavigate} from 'react-router'
|
||||||
|
|
||||||
function TaskList() {
|
function TaskList() {
|
||||||
@@ -25,7 +34,41 @@ function TaskList() {
|
|||||||
},
|
},
|
||||||
interval: 30000,
|
interval: 30000,
|
||||||
...crudCommonOptions(),
|
...crudCommonOptions(),
|
||||||
...paginationTemplate(15),
|
...paginationTemplate(
|
||||||
|
15,
|
||||||
|
undefined,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
label: '',
|
||||||
|
icon: 'fa fa-plus',
|
||||||
|
actionType: 'dialog',
|
||||||
|
dialog: {
|
||||||
|
title: '创建任务',
|
||||||
|
body: {
|
||||||
|
type: 'form',
|
||||||
|
api: {
|
||||||
|
method: 'post',
|
||||||
|
url: `${commonInfo.baseUrl}/task/execute`,
|
||||||
|
data: {
|
||||||
|
templateId: '${templateId|default:undefined}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...horizontalFormOptions(),
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
name: 'templateId',
|
||||||
|
label: '名称',
|
||||||
|
required: true,
|
||||||
|
selectFirst: true,
|
||||||
|
...remoteOptions('select', 'task_template_id'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@@ -44,10 +87,10 @@ function TaskList() {
|
|||||||
...remoteMappings('task_status', 'status'),
|
...remoteMappings('task_status', 'status'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'step',
|
|
||||||
label: '进度',
|
label: '进度',
|
||||||
type: 'progress',
|
type: 'progress',
|
||||||
width: 200,
|
width: 200,
|
||||||
|
value: '${ROUND(step * 100, 0)}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '耗时',
|
label: '耗时',
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate} from '../../util/amis.tsx'
|
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate} from '../../util/amis.tsx'
|
||||||
import {useNavigate} from 'react-router'
|
|
||||||
|
|
||||||
function TaskTemplateList() {
|
function TaskTemplateList() {
|
||||||
const navigate = useNavigate()
|
|
||||||
return (
|
return (
|
||||||
<div className="task-template-list">
|
<div className="task-template-list">
|
||||||
{amisRender(
|
{amisRender(
|
||||||
@@ -13,43 +11,10 @@ function TaskTemplateList() {
|
|||||||
body: [
|
body: [
|
||||||
{
|
{
|
||||||
type: 'crud',
|
type: 'crud',
|
||||||
api: {
|
api: `get:${commonInfo.baseUrl}/task/template/list`,
|
||||||
method: 'post',
|
|
||||||
url: `${commonInfo.baseUrl}/task_template/list`,
|
|
||||||
data: {
|
|
||||||
page: {
|
|
||||||
index: '${page}',
|
|
||||||
size: '${perPage}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...crudCommonOptions(),
|
...crudCommonOptions(),
|
||||||
...paginationTemplate(
|
...paginationTemplate(15),
|
||||||
15,
|
loadOnce: true,
|
||||||
undefined,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
type: 'action',
|
|
||||||
label: '',
|
|
||||||
icon: 'fa fa-plus',
|
|
||||||
tooltip: '添加模板',
|
|
||||||
tooltipPlacement: 'top',
|
|
||||||
onEvent: {
|
|
||||||
click: {
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
actionType: 'custom',
|
|
||||||
// @ts-ignore
|
|
||||||
script: (context, action, event) => {
|
|
||||||
navigate('/task/template/save/-1')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
),
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@@ -63,7 +28,7 @@ function TaskTemplateList() {
|
|||||||
{
|
{
|
||||||
type: 'operation',
|
type: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 150,
|
width: 100,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
type: 'action',
|
type: 'action',
|
||||||
@@ -78,35 +43,7 @@ function TaskTemplateList() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
confirmText: '确认执行模板<span class="text-lg font-bold mx-2">${name}</span>?',
|
confirmText: '确认执行模板<span class="text-lg font-bold mx-2">${name}</span>?',
|
||||||
confirmTitle: '删除',
|
confirmTitle: '执行',
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'action',
|
|
||||||
label: '详情',
|
|
||||||
level: 'link',
|
|
||||||
onEvent: {
|
|
||||||
click: {
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
actionType: 'custom',
|
|
||||||
// @ts-ignore
|
|
||||||
script: (context, action, event) => {
|
|
||||||
navigate(`/task/template/save/${context.props.data['id']}`)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
className: 'text-danger btn-deleted',
|
|
||||||
type: 'action',
|
|
||||||
label: '删除',
|
|
||||||
level: 'link',
|
|
||||||
actionType: 'ajax',
|
|
||||||
api: `get:${commonInfo.baseUrl}/task_template/remove/\${id}`,
|
|
||||||
confirmText: '确认删除模板<span class="text-lg font-bold mx-2">${name}</span>?',
|
|
||||||
confirmTitle: '删除',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {amisRender, commonInfo} from '../../util/amis.tsx'
|
|
||||||
import {useNavigate, useParams} from 'react-router'
|
|
||||||
|
|
||||||
function TaskTemplateSave() {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const {id} = useParams()
|
|
||||||
return (
|
|
||||||
<div className="task-template-save">
|
|
||||||
{amisRender(
|
|
||||||
{
|
|
||||||
type: 'page',
|
|
||||||
title: '任务模板添加',
|
|
||||||
body: [
|
|
||||||
{
|
|
||||||
debug: commonInfo.debug,
|
|
||||||
type: 'form',
|
|
||||||
api: `post:${commonInfo.baseUrl}/task_template/save`,
|
|
||||||
initApi: `get:${commonInfo.baseUrl}/task_template/detail/${id}`,
|
|
||||||
initFetchOn: `${id} !== -1`,
|
|
||||||
wrapWithPanel: false,
|
|
||||||
mode: 'horizontal',
|
|
||||||
labelAlign: 'left',
|
|
||||||
onEvent: {
|
|
||||||
submitSucc: {
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
actionType: 'custom',
|
|
||||||
// @ts-ignore
|
|
||||||
script: (context, action, event) => {
|
|
||||||
navigate(-1)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body: [
|
|
||||||
{
|
|
||||||
type: 'hidden',
|
|
||||||
name: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input-text',
|
|
||||||
name: 'name',
|
|
||||||
label: '名称',
|
|
||||||
required: true,
|
|
||||||
clearable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'textarea',
|
|
||||||
name: 'description',
|
|
||||||
label: '描述',
|
|
||||||
required: true,
|
|
||||||
clearable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'input-text',
|
|
||||||
name: 'expression',
|
|
||||||
label: 'EL表达式',
|
|
||||||
required: true,
|
|
||||||
clearable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'button-toolbar',
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
type: 'action',
|
|
||||||
label: '提交',
|
|
||||||
actionType: 'submit',
|
|
||||||
level: 'primary',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'action',
|
|
||||||
label: '重置',
|
|
||||||
actionType: 'reset',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'action',
|
|
||||||
label: '返回',
|
|
||||||
onEvent: {
|
|
||||||
click: {
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
actionType: 'custom',
|
|
||||||
// @ts-ignore
|
|
||||||
script: (context, action, event) => {
|
|
||||||
navigate(-1)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(TaskTemplateSave)
|
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
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'
|
||||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {isEqual} from 'es-toolkit'
|
import {isEqual, isNil} from 'es-toolkit'
|
||||||
|
// @ts-ignore
|
||||||
|
import type {ColumnSchema} from 'amis/lib/renderers/Table2'
|
||||||
|
import {toNumber} from 'es-toolkit/compat'
|
||||||
|
|
||||||
export const commonInfo = {
|
export const commonInfo = {
|
||||||
debug: isEqual(import.meta.env.MODE, 'development'),
|
debug: isEqual(import.meta.env.MODE, 'development'),
|
||||||
@@ -334,3 +337,672 @@ export function remoteMappings(name: string, field: string) {
|
|||||||
source: `get:${commonInfo.baseUrl}/constants/mappings/${name}/${field}`,
|
source: `get:${commonInfo.baseUrl}/constants/mappings/${name}/${field}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatFinanceNumber = (value: number): string => {
|
||||||
|
if (isNil(value)) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNegative = value < 0
|
||||||
|
const absoluteValue = Math.abs(value)
|
||||||
|
|
||||||
|
let formatted: string
|
||||||
|
if (absoluteValue >= 100000000) {
|
||||||
|
formatted = (absoluteValue / 100000000).toFixed(2) + '亿'
|
||||||
|
} else if (absoluteValue >= 10000) {
|
||||||
|
formatted = (absoluteValue / 10000).toFixed(2) + '万'
|
||||||
|
} else {
|
||||||
|
formatted = absoluteValue.toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return isNegative ? `-${formatted}` : formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDaysNumber = (value: number): string => {
|
||||||
|
if (isNil(value)) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
return `${value.toFixed(0)}天`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatPercentageNumber = (value: number): string => {
|
||||||
|
if (isNil(value)) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
return `${(value * 100).toFixed(2)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FinanceType = 'PERCENTAGE' | 'FINANCE' | 'DAYS'
|
||||||
|
|
||||||
|
const financePropertyLabel = (idField: string, label: string, type: FinanceType, field: string): Schema => {
|
||||||
|
let formatter: (value: number) => string
|
||||||
|
switch (type) {
|
||||||
|
case 'PERCENTAGE':
|
||||||
|
formatter = formatPercentageNumber
|
||||||
|
break
|
||||||
|
case 'FINANCE':
|
||||||
|
formatter = formatFinanceNumber
|
||||||
|
break
|
||||||
|
case 'DAYS':
|
||||||
|
formatter = formatDaysNumber
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
formatter = (v: number) => v.toFixed(2)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'wrapper',
|
||||||
|
size: 'none',
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
visibleOn: `\${!${idField}}`,
|
||||||
|
type: 'tpl',
|
||||||
|
tpl: label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
visibleOn: `\${${idField}}`,
|
||||||
|
className: 'text-current font-bold',
|
||||||
|
type: 'action',
|
||||||
|
label: label,
|
||||||
|
level: 'link',
|
||||||
|
tooltip: '这是什么?',
|
||||||
|
tooltipPlacement: 'top',
|
||||||
|
actionType: 'dialog',
|
||||||
|
dialog: {
|
||||||
|
title: '',
|
||||||
|
size: 'lg',
|
||||||
|
...readOnlyDialogOptions(),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
label: '新页面打开',
|
||||||
|
icon: 'fa fa-solid fa-arrow-up-right-from-square',
|
||||||
|
actionType: 'url',
|
||||||
|
url: `https://zh.wikipedia.org/wiki/${label}`,
|
||||||
|
blank: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
body: {
|
||||||
|
type: 'iframe',
|
||||||
|
src: `https://zh.wikipedia.org/wiki/${label}`,
|
||||||
|
height: 800,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'text-secondary',
|
||||||
|
type: 'action',
|
||||||
|
label: '',
|
||||||
|
icon: 'fa fa-eye',
|
||||||
|
level: 'link',
|
||||||
|
size: 'xs',
|
||||||
|
tooltip: '查看五年趋势',
|
||||||
|
tooltipPlacement: 'top',
|
||||||
|
actionType: 'dialog',
|
||||||
|
dialog: {
|
||||||
|
title: `${label}五年趋势`,
|
||||||
|
size: 'lg',
|
||||||
|
bodyClassName: 'p-0',
|
||||||
|
...readOnlyDialogOptions(),
|
||||||
|
body: {
|
||||||
|
type: 'chart',
|
||||||
|
api: `get:${commonInfo.baseUrl}/stock/finance/\${${idField}}/${field}`,
|
||||||
|
height: 500,
|
||||||
|
config: {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
borderColor: '#ccc',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: {
|
||||||
|
color: '#333',
|
||||||
|
},
|
||||||
|
padding: [10, 15],
|
||||||
|
formatter: (params: any) => {
|
||||||
|
const item = params[0]
|
||||||
|
return `${item.name}<br/>${item.marker}${formatter(item.value)}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '5%',
|
||||||
|
right: '5%',
|
||||||
|
top: '10%',
|
||||||
|
bottom: '15%',
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: '${xList || []}',
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
show: true,
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
type: 'dashed',
|
||||||
|
color: '#f0f0f0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#999',
|
||||||
|
fontSize: 12,
|
||||||
|
formatter: (value: number) => {
|
||||||
|
return formatter(value)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: '${yList || []}',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
showSymbol: true,
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: {
|
||||||
|
width: 3,
|
||||||
|
color: '#4096ff',
|
||||||
|
shadowColor: 'rgba(64, 150, 255, 0.3)',
|
||||||
|
shadowBlur: 5,
|
||||||
|
shadowOffsetY: 2,
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#4096ff',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#fff',
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [{
|
||||||
|
offset: 0, color: 'rgba(64, 150, 255, 0.2)',
|
||||||
|
}, {
|
||||||
|
offset: 1, color: 'rgba(64, 150, 255, 0.01)',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: 12,
|
||||||
|
formatter: (params: any) => {
|
||||||
|
return formatter(params.value)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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> = []) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
label: '编号',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: '简称',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fullname',
|
||||||
|
label: '全名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'market',
|
||||||
|
label: '市场',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
...remoteMappings('stock_market', 'market'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'industry',
|
||||||
|
label: '行业',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '上市日期',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
...date('listedDate'),
|
||||||
|
},
|
||||||
|
...extraColumns,
|
||||||
|
{
|
||||||
|
type: 'operation',
|
||||||
|
label: '操作',
|
||||||
|
width: 100,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
label: '详情',
|
||||||
|
level: 'link',
|
||||||
|
actionType: 'dialog',
|
||||||
|
dialog: {
|
||||||
|
title: '股票详情',
|
||||||
|
size: 'full',
|
||||||
|
...readOnlyDialogOptions(),
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: 'property',
|
||||||
|
items: [
|
||||||
|
{label: '编码', content: '${code}'},
|
||||||
|
{label: '名称', content: '${name}'},
|
||||||
|
{label: '全名', content: '${fullname}'},
|
||||||
|
{
|
||||||
|
label: '市场',
|
||||||
|
content: {
|
||||||
|
...remoteMappings('stock_market', 'market'),
|
||||||
|
value: '${market}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{label: '行业', content: '${industry}'},
|
||||||
|
{label: '上市日期', content: '${listedDate}'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{type: 'divider'},
|
||||||
|
{
|
||||||
|
type: 'service',
|
||||||
|
api: `get:${commonInfo.baseUrl}/stock/finance/\${${idField}}`,
|
||||||
|
body: [
|
||||||
|
'资产负债表',
|
||||||
|
{
|
||||||
|
className: 'my-2',
|
||||||
|
type: 'property',
|
||||||
|
column: 4,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '总资产', 'FINANCE', 'totalAssets'),
|
||||||
|
content: '${balanceSheet.totalAssets}',
|
||||||
|
span: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '总负债', 'FINANCE', 'totalLiabilities'),
|
||||||
|
content: '${balanceSheet.totalLiabilities}',
|
||||||
|
span: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '流动资产', 'FINANCE', 'currentAssets'),
|
||||||
|
content: '${balanceSheet.currentAssets}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '流动资产占比', 'PERCENTAGE', 'currentAssetsToTotalAssetsRatio'),
|
||||||
|
content: '${balanceSheet.currentAssetsRatio}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '流动负债', 'FINANCE', 'currentLiabilities'),
|
||||||
|
content: '${balanceSheet.currentLiabilities}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '流动负债占比', 'PERCENTAGE', 'currentLiabilitiesToTotalAssetsRatio'),
|
||||||
|
content: '${balanceSheet.currentLiabilitiesRatio}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '非流动资产', 'FINANCE', 'fixedAssets'),
|
||||||
|
content: '${balanceSheet.fixedAssets}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '非流动资产占比', 'PERCENTAGE', 'fixedAssetsToTotalAssetsRatio'),
|
||||||
|
content: '${balanceSheet.fixedAssetsRatio}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '非流动负债', 'FINANCE', 'longTermLiabilities'),
|
||||||
|
content: '${balanceSheet.longTermLiabilities}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '非流动负债占比', 'PERCENTAGE', 'longTermLiabilitiesToTotalAssetsRatio'),
|
||||||
|
content: '${balanceSheet.longTermLiabilitiesRatio}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'利润表',
|
||||||
|
{
|
||||||
|
className: 'my-2',
|
||||||
|
type: 'property',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '营业收入', 'FINANCE', 'operatingRevenue'),
|
||||||
|
content: '${income.operatingRevenue}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '营业成本', 'FINANCE', 'operatingCost'),
|
||||||
|
content: '${income.operatingCost}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '营业利润', 'FINANCE', 'operatingProfit'),
|
||||||
|
content: '${income.operatingProfit}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'现金流量表',
|
||||||
|
{
|
||||||
|
className: 'my-2',
|
||||||
|
type: 'property',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '净利润', 'FINANCE', 'netProfit'),
|
||||||
|
content: '${cashFlow.netProfit}',
|
||||||
|
span: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '营业活动现金流量', 'FINANCE', 'cashFlowFromOperatingActivities'),
|
||||||
|
content: '${cashFlow.cashFlowFromOperatingActivities}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '投资活动现金流量', 'FINANCE', 'cashFlowFromInvestingActivities'),
|
||||||
|
content: '${cashFlow.cashFlowFromInvestingActivities}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '筹资活动现金流量', 'FINANCE', 'cashFlowFromFinancingActivities'),
|
||||||
|
content: '${cashFlow.cashFlowFromFinancingActivities}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'财务指标',
|
||||||
|
{
|
||||||
|
className: 'my-2',
|
||||||
|
type: 'property',
|
||||||
|
column: 4,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '流动比率', 'FINANCE', 'currentRatio'),
|
||||||
|
content: '${indicate.currentRatio}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '速动比率', 'FINANCE', 'quickRatio'),
|
||||||
|
content: '${indicate.quickRatio}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, 'ROE', 'FINANCE', 'returnOnEquity'),
|
||||||
|
content: '${indicate.roe}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, 'ROA', 'FINANCE', 'returnOnAssets'),
|
||||||
|
content: '${indicate.roa}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '应收账款周转率', 'FINANCE', 'accountsReceivableTurnover'),
|
||||||
|
content: '${indicate.accountsReceivableTurnover}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '应收账款周转天数', 'DAYS', 'daysAccountsReceivableTurnover'),
|
||||||
|
content: '${indicate.daysAccountsReceivableTurnover}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '存货周转率', 'FINANCE', 'inventoryTurnover'),
|
||||||
|
content: '${indicate.inventoryTurnover}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '存货周转天数', 'DAYS', 'daysInventoryTurnover'),
|
||||||
|
content: '${indicate.daysInventoryTurnover}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '固定资产周转率', 'FINANCE', 'fixedAssetsTurnover'),
|
||||||
|
content: '${indicate.fixedAssetsTurnover}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '固定资产周转天数', 'DAYS', 'daysFixedAssetsTurnover'),
|
||||||
|
content: '${indicate.daysFixedAssetsTurnover}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '总资产周转率', 'FINANCE', 'totalAssetsTurnover'),
|
||||||
|
content: '${indicate.totalAssetsTurnover}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: financePropertyLabel(idField, '总资产周转天数', 'DAYS', 'daysTotalAssetsTurnover'),
|
||||||
|
content: '${indicate.daysTotalAssetsTurnover}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{type: 'divider'},
|
||||||
|
{
|
||||||
|
type: 'service',
|
||||||
|
api: `get:${commonInfo.baseUrl}/stock/daily/current/\${${idField}}`,
|
||||||
|
body: [
|
||||||
|
"现价 (${date})",
|
||||||
|
{
|
||||||
|
className: 'my-2',
|
||||||
|
type: 'property',
|
||||||
|
column: 4,
|
||||||
|
items: [
|
||||||
|
{label: '开盘价', content: '${open}'},
|
||||||
|
{label: '收盘价', content: '${close}'},
|
||||||
|
{label: '最高价', content: '${high}'},
|
||||||
|
{label: '最低价', content: '${low}'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
candleChart(
|
||||||
|
'100日线数据',
|
||||||
|
'后复权数据',
|
||||||
|
`get:${commonInfo.baseUrl}/stock/daily/\${${idField}}`,
|
||||||
|
),
|
||||||
|
candleChart(
|
||||||
|
'50周线数据',
|
||||||
|
'后复权数据',
|
||||||
|
`get:${commonInfo.baseUrl}/stock/weekly/\${${idField}}`,
|
||||||
|
),
|
||||||
|
candleChart(
|
||||||
|
'24月线数据',
|
||||||
|
'后复权数据',
|
||||||
|
`get:${commonInfo.baseUrl}/stock/monthly/\${${idField}}`,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
22
pom.xml
22
pom.xml
@@ -16,9 +16,9 @@
|
|||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>17</java.version>
|
<java.version>21</java.version>
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
||||||
<spring-boot.version>3.5.0</spring-boot.version>
|
<spring-boot.version>3.5.0</spring-boot.version>
|
||||||
@@ -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>
|
||||||
@@ -81,6 +86,17 @@
|
|||||||
<version>${hibernate.version}</version>
|
<version>${hibernate.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.ralfkonrad.quantlib_for_maven</groupId>
|
||||||
|
<artifactId>quantlib</artifactId>
|
||||||
|
<version>1.39.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ta4j</groupId>
|
||||||
|
<artifactId>ta4j-core</artifactId>
|
||||||
|
<version>0.17</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-dependencies</artifactId>
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
|||||||
Reference in New Issue
Block a user