1
0

Compare commits

...

91 Commits

Author SHA1 Message Date
6179137e0d fix: 不知道啥东西 2026-01-09 09:32:33 +08:00
958bc459b2 feat: 优化回测的行情展示 2025-12-02 17:18:24 +08:00
7eec44f21c feat: 优化行情展示代码 2025-12-02 17:18:24 +08:00
5f42d36436 feat: 增加简约行情展示 2025-12-02 17:18:24 +08:00
9a31b8cae4 feat: 简化交易计算 2025-11-11 23:33:13 +08:00
482eb70465 feat: 增加图表选项 2025-11-10 15:25:27 +08:00
7703f88d7f feat: 简化交易计算 2025-11-07 23:08:56 +08:00
db8a094c8f feat: 实现基本的回测图表绘制 2025-11-03 23:28:04 +08:00
f4fef2bd95 feat: 尝试多图生成 2025-10-31 00:56:06 +08:00
53a6d33fd5 feat: 建立批处理也能输出图表的套路 2025-10-30 17:48:30 +08:00
83574e1229 feat: 增加简单回测 2025-10-16 23:19:56 +08:00
e2c5729f87 feat: 搭建回测流程 2025-10-16 17:58:15 +08:00
47f8b30a02 feat: 增加日线的均线显示 2025-10-15 23:14:50 +08:00
452d1c681d feat: 增加月线周线蜡烛图 2025-10-15 15:14:38 +08:00
42b402d4ef fix: 修复缓存调用错误 2025-10-15 15:14:10 +08:00
8c4e3baacb feat: 增加年线和周线的计算 2025-10-14 23:26:35 +08:00
a075adf4b6 refactor: 移除周线类 2025-10-14 20:48:14 +08:00
963c2a9878 refactor: 移除年线数据
需要用的时候现算就是了,避免维护更多的原始数据
2025-10-14 18:08:47 +08:00
e387fc839f feat: 增加周线表 2025-10-11 18:41:32 +08:00
b9a02194e2 feat: 增加接口缓存 2025-10-11 16:24:28 +08:00
b0c2530e63 feat: 股票集增加创建和更新时间 2025-10-11 15:04:40 +08:00
49a03adf21 feat: 增加额外得分数据保存 2025-10-11 15:04:16 +08:00
5390a879e7 feat: 增加股票现价显示,日线改为后复权数据 2025-10-11 15:03:02 +08:00
69020852b9 feat: 使用dialog代替股票详情跳转 2025-10-10 23:13:29 +08:00
846c6fe819 feat: 股票集中显示股票对应的得分 2025-10-10 18:04:47 +08:00
d3f337e2c4 feat: 调整结果展示 2025-09-26 18:13:12 +08:00
d4fec4c426 refactor: 优化年线情况计算 2025-09-26 17:25:29 +08:00
0dd421ca43 refactor: 通用逻辑移到core中方便strategy模块一起使用 2025-09-26 09:17:11 +08:00
3991effa88 feat: 增加金字塔选股任务 2025-09-25 17:46:52 +08:00
02508a5426 fix: 模版排序不稳定 2025-09-25 16:49:39 +08:00
edd18061eb fix: 加入事务方式数据冲突 2025-09-25 15:39:45 +08:00
3d428d9d0a fix: 项目详情中的进度显示错误 2025-09-25 15:39:12 +08:00
b4e2c81d36 perf: 优化查询语句 2025-09-25 15:38:40 +08:00
1edd74e35d feat: 增加静态选项默认 2025-09-25 15:38:10 +08:00
7d3b3758f3 perf: 不显示SQL语句节省空间 2025-09-25 00:10:08 +08:00
d6aab42892 feat: 更新日线数据增加进度 2025-09-25 00:08:43 +08:00
5cd4875cf9 feat: 任务没完成时显示最新耗时 2025-09-25 00:08:14 +08:00
f046427480 feat: 忽略项目运行的日志文件 2025-09-24 22:34:49 +08:00
01690bbcd6 refactor: 重构任务执行 2025-09-24 22:34:16 +08:00
8011a4f2cb fix: 修复year字段在不同数据库的表现 2025-09-24 21:57:07 +08:00
a9b2561be1 feat: 增加年线行情更新 2025-09-22 23:11:31 +08:00
dd81ca1150 feat: 完成金字塔选股和评估 2025-09-19 16:48:02 +08:00
01bd5ed178 feat(strategy): 添加股票数据可视化功能
- 在 StrategyApplication 中实现了一个简单的股票数据可视化功能
- 使用 ECharts 和 Amis 渲染股票数据的蜡烛图和均线
- 新增了 TestMarkdown 类,用于测试 Markdown 渲染功能
- 在 application.yml 中添加了 LiteFlow 相关配置
- 更新了 pom.xml,添加了 LiteFlow、CommonMark 等依赖
2025-09-18 18:18:53 +08:00
a4db463dbd refactor(server): 优化财务指标更新逻辑
- 在更新财务指标时,增加了对股票上市日期的判断
- 只处理上市日期早于当前年份的股票,避免更新未上市公司的数据- 提高了数据处理的准确性和效率
2025-09-18 09:31:11 +08:00
68aa6ff33f refactor(server): 更新 Java 版本并优化停止脚本
- 将 Java 版本从17.0.16+8 升级到 21.0.8+9
- 优化停止脚本以更准确地获取应用进程 ID
- 添加使用 jps 命令获取进程 ID 的备选方案
2025-09-18 09:30:47 +08:00
19dd19a9f8 feat: 增加一点测试 2025-09-17 23:32:05 +08:00
5953c9b9f2 feat: 增加股票集展示 2025-09-17 18:24:05 +08:00
f8ee51c0ed feat: 增加一个简单的markdown编写 2025-09-17 17:03:51 +08:00
0b9cb55788 feat: 补全金字塔选股 2025-09-17 17:02:40 +08:00
585b37a1cc feat: 升级JDK到21 2025-09-17 17:02:20 +08:00
868feeb34d feat: 增加金字塔选股策略 2025-09-17 00:51:11 +08:00
d2b3305ca6 fix: 修复价格显示错误 2025-09-16 18:40:44 +08:00
10a0e14024 fix: 修复指标显示错位 2025-09-16 18:21:49 +08:00
a9621a10ac perf: 优化查询效率 2025-09-16 18:21:33 +08:00
b5688bd3ab feat: 查询优化 2025-09-16 18:20:59 +08:00
17c96e96fc feat: 增加日线数据显示 2025-09-16 07:49:51 +08:00
596e3caa59 perf: 优化财报显示 2025-09-15 22:11:09 +08:00
f28360e6ec perf: 升级依赖 2025-09-15 22:06:15 +08:00
7d0062eae0 perf: 优化UI展示 2025-09-15 22:06:04 +08:00
697a58a0e4 feat: 增加财务指标显示 2025-09-15 18:42:36 +08:00
b569d62a25 fix: 修复大数字显示 2025-09-15 15:20:05 +08:00
d3538ddce0 fix: 修复流动和长期负债的取值 2025-09-15 13:59:01 +08:00
12b622956a perf: 替换licia为es-toolkit 2025-09-15 13:56:49 +08:00
aee6673c64 perf: 优化任务异常的内容 2025-09-15 13:56:25 +08:00
9f781ce794 perf: 优化财务数据的采集和显示 2025-09-15 13:56:07 +08:00
4cc7d2344f feat: 增加新的财务指标采集模式 2025-09-14 23:17:04 +08:00
7fa524b8d5 feat: 一些细小的调整 2025-09-13 00:55:44 +08:00
acab6978d4 perf: 优化财务数字格式化函数 2025-09-12 17:57:04 +08:00
5b08e9cc8a perf: 优化数字格式 2025-09-12 17:54:18 +08:00
cfc71f83a1 feat: 增加财务报表近5年图表展示 2025-09-12 17:46:56 +08:00
338554c523 perf: 股票详情页显示财报 2025-09-12 16:18:39 +08:00
5fa2a4e8e7 perf: 优化数值处理 2025-09-12 16:18:04 +08:00
eba80bd9cc feat: 增加日线数据更新进度 2025-09-12 16:16:16 +08:00
bf123a2747 feat: 增加任务详情页面 2025-09-12 16:15:53 +08:00
9a021ddd9d perf: 优化任务列表展示 2025-09-12 16:15:05 +08:00
23dba3eb06 fix: 修复删除按钮高亮颜色 2025-09-12 10:17:01 +08:00
edf320205d perf: 直接使用仓库类简化代码 2025-09-12 00:20:58 +08:00
fe5f303241 fix: 修复财务报表的接口字段 2025-09-12 00:20:10 +08:00
6ed33e43f1 feat: 增加财务信息采集 2025-09-11 20:09:46 +08:00
b57cfb3422 fix: 修复el表达式无法修改 2025-09-11 14:30:33 +08:00
4fd8320b32 feat: 优化股票列表显示 2025-09-11 12:09:39 +08:00
c3099a970e feat: EL表达式简写,默认套任务监控模板 2025-09-11 12:08:39 +08:00
8a0c1ae3be feat: 增加DDL语句生成小工具 2025-09-11 11:31:12 +08:00
efd7f5e9f2 feat: liteflow任务保存到数据库 2025-09-11 00:13:09 +08:00
ced4cde3e3 fix: 避免日志无限增长 2025-09-10 23:39:19 +08:00
5669fd3aa7 feat: 本地调试使用测试表 2025-09-10 23:38:53 +08:00
74eac6f825 feat: 增加财务报表表 2025-09-10 23:38:32 +08:00
33fc47df87 feat: 尝试使用bun代替pnpm 2025-09-09 09:45:37 +08:00
f6603ec10b feat: 增加财务数据列表 2025-09-09 00:14:53 +08:00
388456ff24 feat: 增加股票集相关服务 2025-09-08 23:12:21 +08:00
1ad5b10e20 feat: 增加进度显示 2025-09-08 21:53:19 +08:00
99 changed files with 8827 additions and 9846 deletions

1
.gitignore vendored
View File

@@ -252,3 +252,4 @@ gradle-app.setting
!.yarn/cache
nohup.out
!leopard*/bin
archive

View File

@@ -1,5 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="AUTODETECT_INDENTS" value="false" />
<JSCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
@@ -27,6 +28,9 @@
<ScalaCodeStyleSettings>
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
</ScalaCodeStyleSettings>
<SqlCodeStyleSettings version="7">
<option name="KEYWORD_CASE" value="1" />
</SqlCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
@@ -73,6 +77,15 @@
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SQL">
<option name="KEEP_LINE_BREAKS" value="false" />
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
@@ -91,5 +104,10 @@
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
<codeStyleSettings language="yaml">
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="LINE_COMMENT_ADD_SPACE_ON_REFORMAT" value="true" />
</codeStyleSettings>
</code_scheme>
</component>

54
.idea/compiler.xml generated
View File

@@ -17,13 +17,11 @@
<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-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$/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$/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/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" />
@@ -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/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$/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/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-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" />
@@ -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$/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$/org/jetbrains/annotations/26.0.2/annotations-26.0.2.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" />
<entry name="$MAVEN_REPOSITORY$/jakarta/persistence/jakarta.persistence-api/3.2.0/jakarta.persistence-api-3.2.0.jar" />
</processorPath>
<module name="leopard-core" />
</profile>

4
.idea/dataSources.xml generated
View File

@@ -39,11 +39,11 @@
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="leopard@81.71.3.24" uuid="4b5dd0f8-26c8-49e2-b794-d464461c121a">
<data-source source="LOCAL" name="leopard_dev@81.71.3.24" uuid="63824900-a456-4883-8de4-8f436cd00c71">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://81.71.3.24:6785/leopard</jdbc-url>
<jdbc-url>jdbc:postgresql://81.71.3.24:6785/leopard_dev</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />

View File

@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourcePerFileMappings">
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/4b5dd0f8-26c8-49e2-b794-d464461c121a/console.sql" value="4b5dd0f8-26c8-49e2-b794-d464461c121a" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/a8b9cd0a-335e-42ae-991a-f2733200afbf/console.sql" value="a8b9cd0a-335e-42ae-991a-f2733200afbf" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/63824900-a456-4883-8de4-8f436cd00c71/console.sql" value="63824900-a456-4883-8de4-8f436cd00c71" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f7d817dc-8c9c-479f-b469-583df17cb013/console.sql" value="f7d817dc-8c9c-479f-b469-583df17cb013" />
</component>
</project>

View 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
View File

@@ -8,7 +8,7 @@
</list>
</option>
</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" />
</component>
</project>

View File

@@ -20,10 +20,44 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</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>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-ant</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -1,5 +1,6 @@
package com.lanyuanxiaoyao.leopard.core.entity;
import cn.hutool.core.util.ObjectUtil;
import com.lanyuanxiaoyao.leopard.core.Constants;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import jakarta.persistence.Column;
@@ -29,6 +30,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@Table(name = Constants.DATABASE_PREFIX + "daily")
public class Daily extends SimpleEntity {
@Column(nullable = false)
@Comment("交易日")
private LocalDate tradeDate;
@Comment("开盘价")
private Double open;
@@ -55,4 +57,20 @@ public class Daily extends SimpleEntity {
@JoinColumn(nullable = false)
@ToString.Exclude
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);
}
}

View File

@@ -0,0 +1,152 @@
package com.lanyuanxiaoyao.leopard.core.entity;
import com.lanyuanxiaoyao.leopard.core.Constants;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@Setter
@Getter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Table(name = Constants.DATABASE_PREFIX + "finance_indicator")
public class FinanceIndicator extends SimpleEntity {
@ManyToOne
private Stock stock;
@Column(name = "`year`", nullable = false)
@Comment("年报年度")
private Integer year;
@Comment("总股本")
private Double totalShareCapital;
@Comment("资本公积金")
private Double capitalSurplus;
@Comment("盈余公积金")
private Double surplusReserve;
@Comment("未分配利润")
private Double undistributedProfit;
@Comment("现金及现金等价物")
private Double cashAndCashEquivalents;
@Comment("现金及现金等价物占总资产比率")
private Double cashAndCashEquivalentsToTotalAssetsRatio;
@Comment("应收账款")
private Double accountsReceivable;
@Comment("应收账款占总资产比率")
private Double accountsReceivableToTotalAssetsRatio;
@Comment("应付账款")
private Double accountsPayable;
@Comment("应付账款占总资产比率")
private Double accountsPayableToTotalAssetsRatio;
@Comment("存货")
private Double inventory;
@Comment("存货占总资产比率")
private Double inventoryToTotalAssetsRatio;
@Comment("商誉")
private Double goodwill;
@Comment("商誉占总资产比率")
private Double goodwillToTotalAssetsRatio;
@Comment("流动资产")
private Double currentAssets;
@Comment("流动资产占总资产比率")
private Double currentAssetsToTotalAssetsRatio;
@Comment("固定资产")
private Double fixedAssets;
@Comment("固定资产占总资产比率")
private Double fixedAssetsToTotalAssetsRatio;
@Comment("流动负债")
private Double currentLiabilities;
@Comment("流动负债占总资产比率")
private Double currentLiabilitiesToTotalAssetsRatio;
@Comment("流动负债占总负债比率")
private Double currentLiabilitiesToTotalLiabilitiesRatio;
@Comment("长期负债")
private Double longTermLiabilities;
@Comment("长期负债占总资产比率")
private Double longTermLiabilitiesToTotalAssetsRatio;
@Comment("长期负债占总负债比率")
private Double longTermLiabilitiesToTotalLiabilitiesRatio;
@Comment("总负债")
private Double totalLiabilities;
@Comment("负债占总资产比率")
private Double liabilitiesToTotalAssetsRatio;
@Comment("股东权益")
private Double shareholdersEquity;
@Comment("股东权益占总资产比率")
private Double shareholdersEquityToTotalAssetsRatio;
@Comment("总资产")
private Double totalAssets;
@Comment("营业收入")
private Double operatingRevenue;
@Comment("营业成本")
private Double operatingCost;
@Comment("营业利润")
private Double operatingProfit;
@Comment("营业支出")
private Double operatingExpenses;
@Comment("净利润")
private Double netProfit;
@Comment("经营活动现金流净额")
private Double netCashFlowFromOperatingActivities;
@Comment("营业活动现金流量")
private Double cashFlowFromOperatingActivities;
@Comment("投资活动现金流量")
private Double cashFlowFromInvestingActivities;
@Comment("筹资活动现金流量")
private Double cashFlowFromFinancingActivities;
@Comment("流动比率")
private Double currentRatio;
@Comment("速动比率")
private Double quickRatio;
@Comment("长期资金占固定资产比率")
private Double longTermFundsToFixedAssetsRatio;
@Comment("应收账款周转率")
private Double accountsReceivableTurnover;
@Comment("应收账款周转天数(平均收现天数)")
private Double daysAccountsReceivableTurnover;
@Comment("存货周转率")
private Double inventoryTurnover;
@Comment("存货周转天数(平均销货天数)")
private Double daysInventoryTurnover;
@Comment("固定资产周转率")
private Double fixedAssetsTurnover;
@Comment("固定资产周转天数")
private Double daysFixedAssetsTurnover;
@Comment("总资产周转率")
private Double totalAssetsTurnover;
@Comment("总资产周转天数")
private Double daysTotalAssetsTurnover;
@Comment("ROE")
private Double returnOnEquity;
@Comment("ROA")
private Double returnOnAssets;
@Comment("营业毛利率")
private Double operatingGrossProfitMargin;
@Comment("营业利益率")
private Double operatingProfitMargin;
@Comment("经营安全边际率")
private Double operatingSafetyMarginRatio;
@Comment("净利率")
private Double netProfitMargin;
@Comment("每股盈余")
private Double earningsPerShare;
@Comment("现金流量比率")
private Double cashFlowRatio;
@Comment("现金流量允当比率")
private Double cashFlowAdequacyRatio;
@Comment("现金再投资比率")
private Double cashReinvestmentRatio;
}

View File

@@ -9,7 +9,6 @@ import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.time.LocalDate;
@@ -62,9 +61,22 @@ public class Stock extends SimpleEntity {
@ToString.Exclude
private Set<Daily> dailies;
@ManyToMany
@OneToMany(mappedBy = "stock", cascade = CascadeType.REMOVE)
@ToString.Exclude
private Set<StockCollection> collections;
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
@AllArgsConstructor

View File

@@ -2,10 +2,11 @@ package com.lanyuanxiaoyao.leopard.core.entity;
import com.lanyuanxiaoyao.leopard.core.Constants;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.Set;
import lombok.Getter;
@@ -31,7 +32,7 @@ public class StockCollection extends SimpleEntity {
@Column(nullable = false)
private String description;
@ManyToMany
@OneToMany(cascade = CascadeType.ALL)
@ToString.Exclude
private Set<Stock> stocks;
private Set<StockScore> scores;
}

View File

@@ -2,10 +2,13 @@ package com.lanyuanxiaoyao.leopard.core.entity;
import com.lanyuanxiaoyao.leopard.core.Constants;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@@ -22,12 +25,14 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Table(name = Constants.DATABASE_PREFIX + "task_template")
public class TaskTemplate extends SimpleEntity {
@Column(nullable = false)
private String name;
@Column(nullable = false, length = 500)
private String description;
@Column(nullable = false)
private String chain;
}
@Table(name = Constants.DATABASE_PREFIX + "stock_score")
public class StockScore extends SimpleEntity {
@ManyToOne
private Stock stock;
@ManyToOne
private StockCollection collection;
@ElementCollection
@JoinTable(name = Constants.DATABASE_PREFIX + "stock_score_extra")
private Map<String, String> extra;
private Double score;
}

View File

@@ -61,7 +61,7 @@ public class Task extends SimpleEntity {
private Status status = Status.RUNNING;
@Column(nullable = false)
@Comment("任务进度")
private Integer step = 0;
private Double step = 0.0;
@Comment("任务开始时间")
private LocalDateTime launchedTime;
@Comment("任务结束时间")

View File

@@ -0,0 +1,6 @@
package com.lanyuanxiaoyao.leopard.core.entity.dto;
import java.time.LocalDate;
public record DailyDouble(LocalDate date, Double value) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -0,0 +1,4 @@
package com.lanyuanxiaoyao.leopard.core.entity.dto;
public record YearAndMonth(int year, int month) {
}

View File

@@ -0,0 +1,4 @@
package com.lanyuanxiaoyao.leopard.core.entity.dto;
public record YearAndWeek(int year, int week) {
}

View File

@@ -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
) {
}

View File

@@ -0,0 +1,91 @@
package com.lanyuanxiaoyao.leopard.core.helper;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import java.util.function.Function;
/**
* @author lanyuanxiaoyao
* @version 20250912
*/
public class NumberHelper {
public static final String FINANCE_NULL_DOUBLE = "/";
public static String formatFinanceDouble(Double value) {
if (ObjectUtil.isNull(value)) {
return FINANCE_NULL_DOUBLE;
}
var result = FINANCE_NULL_DOUBLE;
var absValue = Double.valueOf(Math.abs(value));
if (absValue > 100000000) {
result = NumberUtil.decimalFormat("#.##亿", absValue / 100000000);
} else if (value > 10000) {
result = NumberUtil.decimalFormat("#.##万", absValue / 10000);
} else {
result = NumberUtil.decimalFormat("#.##", absValue);
}
return value < 0 ? "-" + result : result;
}
public static String formatDaysDouble(Double value) {
if (ObjectUtil.isNull(value)) {
return FINANCE_NULL_DOUBLE;
}
return NumberUtil.decimalFormat("#", value);
}
public static String formatPercentageDouble(Double value) {
if (ObjectUtil.isNull(value)) {
return null;
}
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) {
if (StrUtil.isBlank(value)) {
return null;
}
return Double.parseDouble(value);
}
public static Double parseDouble(String value, Function<Double, Double> ifSuccess) {
var result = parseDouble(value);
return ObjectUtil.isNull(result) ? null : ifSuccess.apply(result);
}
public static Double safePlus(Double a, Double b) {
if (ObjectUtil.isNull(a) || ObjectUtil.isNull(b)) {
return null;
}
return a + b;
}
public static Double safeMinus(Double a, Double b) {
if (ObjectUtil.isNull(a) || ObjectUtil.isNull(b)) {
return null;
}
return a - b;
}
public static Double safeDiv(Double a, Double b) {
if (ObjectUtil.isNull(a) || ObjectUtil.isNull(b) || b == 0) {
return null;
}
return NumberUtil.div(a, b, 4);
}
}

View File

@@ -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();
}
}

View File

@@ -2,16 +2,39 @@ package com.lanyuanxiaoyao.leopard.core.repository;
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Predicate;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface DailyRepository extends SimpleRepository<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")
List<LocalDate> findDistinctTradeDateByStockId(Long stockId);
@Query("select max(daily.tradeDate) from Daily daily")
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"})
@Override
Optional<Daily> findOne(Predicate predicate);
@EntityGraph(attributePaths = {"stock"})
@Override
List<Daily> findAll(Predicate predicate, OrderSpecifier<?>... orders);
@EntityGraph(attributePaths = {"stock"})
@Query("from Daily daily where daily.stock.id = ?1 and daily.tradeDate <= current date order by daily.tradeDate desc limit ?2")
List<Daily> findRecent(Long stockId, int days);
}

View File

@@ -0,0 +1,21 @@
package com.lanyuanxiaoyao.leopard.core.repository;
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import com.querydsl.core.types.Predicate;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.stereotype.Repository;
@Repository
public interface FinanceIndicatorRepository extends SimpleRepository<FinanceIndicator> {
@EntityGraph(attributePaths = {"stock"})
@Override
Optional<FinanceIndicator> findOne(Predicate predicate);
@EntityGraph(attributePaths = {"stock"})
@Override
List<FinanceIndicator> findAll(Predicate predicate, Sort sort);
}

View File

@@ -1,9 +1,9 @@
package com.lanyuanxiaoyao.leopard.core.repository;
import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate;
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TaskTemplateRepository extends SimpleRepository<TaskTemplate> {
public interface StockCollectionRepository extends SimpleRepository<StockCollection> {
}

View File

@@ -2,7 +2,10 @@ package com.lanyuanxiaoyao.leopard.core.repository;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import java.util.List;
import jakarta.transaction.Transactional;
import java.util.Collection;
import java.util.Set;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@@ -13,5 +16,12 @@ import org.springframework.stereotype.Repository;
@Repository
public interface StockRepository extends SimpleRepository<Stock> {
@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")
Set<String> findDistinctCodes();
@Transactional(rollbackOn = Throwable.class)
@Modifying
void deleteAllByCodeIn(Collection<String> code);
}

View File

@@ -5,6 +5,7 @@ import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
/**
* @author lanyuanxiaoyao
@@ -13,10 +14,15 @@ import org.springframework.stereotype.Repository;
@Repository
public interface TaskRepository extends SimpleRepository<Task> {
@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();
@Transactional(rollbackFor = Throwable.class)
@Modifying
@Query("update Task task set task.step = ?1 where task.id = ?2")
void updateStepById(Integer step, Long id);
@Query("update Task task set task.step = ?2 where task.id = ?1")
void updateStepById(Long id, Double step);
}

View File

@@ -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();
}
}
}

View File

@@ -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
) {
}
}

View File

@@ -0,0 +1,15 @@
package com.lanyuanxiaoyao.leopard.core.service;
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class StockCollectionService extends SimpleServiceSupport<StockCollection> {
public StockCollectionService(StockCollectionRepository repository) {
super(repository);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -1,11 +1,13 @@
package com.lanyuanxiaoyao.leopard.server.service;
package com.lanyuanxiaoyao.leopard.core.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -23,10 +25,9 @@ import org.springframework.stereotype.Service;
@Slf4j
@Service
public class TuShareService {
public static final DateTimeFormatter TRADE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final String API_URL = "https://api.tushare.pro";
private static final String API_TOKEN = "64ebff4fa679167600b905ee45dd88e76f3963c0ff39157f3f085f0e";
public static final DateTimeFormatter TRADE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
private final ObjectMapper mapper;
public TuShareService(Jackson2ObjectMapperBuilder builder) {
@@ -96,9 +97,19 @@ public class TuShareService {
@SneakyThrows
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(
"adj_factor",
Map.of("trade_date", tradeDate.format(TRADE_FORMAT)),
paramsMap,
List.of("ts_code", "trade_date", "adj_factor")
));
var tuShareResponse = mapper.readValue(response, TuShareResponse.class);
@@ -108,6 +119,24 @@ public class TuShareService {
return tuShareResponse;
}
public List<Map<String, String>> request(String api, Map<String, Object> params, List<String> fields) throws JsonProcessingException {
var response = HttpUtil.post(API_URL, buildRequest(api, params, fields));
var tuShareResponse = mapper.readValue(response, TuShareResponse.class);
if (tuShareResponse.code != 0) {
throw new RuntimeException(tuShareResponse.message);
}
var data = tuShareResponse.data;
var result = new ArrayList<Map<String, String>>();
for (var item : data.items) {
var map = new HashMap<String, String>();
for (int i = 0; i < data.fields.size(); i++) {
map.put(data.fields.get(i), item.get(i));
}
result.add(map);
}
return result;
}
public record TuShareResponse(
Integer code,
@JsonProperty("msg")

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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
) {
}
}
}

View File

@@ -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")));
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,225 @@
package com.lanyuanxiaoyao.leopard.core.task;
import cn.hutool.core.util.ArrayUtil;
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.QFinanceIndicator;
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.StockRepository;
import com.lanyuanxiaoyao.leopard.core.service.TuShareService;
import java.time.LocalDate;
import java.util.List;
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 20250924
*/
@Slf4j
@Component
public class UpdateFinanceIndicatorTask extends TaskRunner {
private final StockRepository stockRepository;
private final FinanceIndicatorRepository financeIndicatorRepository;
private final TuShareService tuShareService;
protected UpdateFinanceIndicatorTask(ApplicationContext context, StockRepository stockRepository, FinanceIndicatorRepository financeIndicatorRepository, TuShareService tuShareService) {
super(context);
this.stockRepository = stockRepository;
this.financeIndicatorRepository = financeIndicatorRepository;
this.tuShareService = tuShareService;
}
@Transactional(rollbackFor = Throwable.class)
@Override
public String process(Map<String, Object> params, StepUpdater updater) throws JsonProcessingException {
var stocks = stockRepository.findAll();
var currentYear = LocalDate.now().getYear();
for (int year = 1990; year < currentYear; year++) {
var balances = tuShareService.request(
"balancesheet_vip",
Map.of("period", LocalDate.of(year, 12, 31).format(TuShareService.TRADE_FORMAT)),
List.of(
"ts_code",
"total_share",
"cap_rese",
"surplus_rese",
"undistr_porfit",
"cash_reser_cb",
"accounts_receiv_bill",
"accounts_pay",
"inventories",
"goodwill",
"total_cur_assets",
"total_nca",
"total_cur_liab",
"total_ncl",
"total_liab",
"total_hldr_eqy_inc_min_int",
"total_assets"
)
);
var balancesMap = balances.stream().collect(Collectors.toMap(
map -> map.get("ts_code"),
map -> map,
(existing, replacement) -> existing
));
var incomes = tuShareService.request(
"income_vip",
Map.of("period", LocalDate.of(year, 12, 31).format(TuShareService.TRADE_FORMAT)),
List.of(
"ts_code",
"total_revenue",
"total_cogs",
"operate_profit",
"oper_exp",
"n_income"
)
);
var incomesMap = incomes.stream().collect(Collectors.toMap(
map -> map.get("ts_code"),
map -> map,
(existing, replacement) -> existing
));
var cashFlows = tuShareService.request(
"cashflow_vip",
Map.of("period", LocalDate.of(year, 12, 31).format(TuShareService.TRADE_FORMAT)),
List.of(
"ts_code",
"n_cashflow_act",
"n_cashflow_inv_act",
"n_cash_flows_fnc_act"
)
);
var cashFlowsMap = cashFlows.stream().collect(Collectors.toMap(
map -> map.get("ts_code"),
map -> map,
(existing, replacement) -> existing
));
var finaIndicators = tuShareService.request(
"fina_indicator_vip",
Map.of("period", LocalDate.of(year, 12, 31).format(TuShareService.TRADE_FORMAT)),
List.of(
"ts_code",
"ca_to_assets",
"nca_to_assets",
"currentdebt_to_debt",
"longdeb_to_debt",
"current_ratio",
"quick_ratio",
"ar_turn",
"arturn_days",
"inv_turn",
"invturn_days",
"fa_turn",
"assets_turn",
"roe_dt",
"roa",
"roa_dp",
"total_revenue_ps"
)
);
var finaIndicatorsMap = finaIndicators.stream().collect(Collectors.toMap(
map -> map.get("ts_code"),
map -> map,
(existing, replacement) -> existing
));
var financeIndicatorsMap = financeIndicatorRepository.findAll(QFinanceIndicator.financeIndicator.year.eq(year))
.stream()
.collect(Collectors.toMap(
indicator -> indicator.getStock().getCode(),
indicator -> indicator
));
for (Stock stock : stocks) {
var balance = balancesMap.get(stock.getCode());
var income = incomesMap.get(stock.getCode());
var cashFlow = cashFlowsMap.get(stock.getCode());
var finaIndicator = finaIndicatorsMap.get(stock.getCode());
if (stock.getListedDate().getYear() > year || ArrayUtil.<Object>isAllNull(balance, income, cashFlow, finaIndicator)) {
continue;
}
var indicator = financeIndicatorsMap.getOrDefault(stock.getCode(), new FinanceIndicator());
indicator.setStock(stock);
indicator.setYear(year);
if (ObjectUtil.isNotNull(balance)) {
indicator.setTotalAssets(NumberHelper.parseDouble(balance.get("total_assets")));
indicator.setTotalShareCapital(NumberHelper.parseDouble(balance.get("total_share")));
indicator.setCapitalSurplus(NumberHelper.parseDouble(balance.get("cap_rese")));
indicator.setSurplusReserve(NumberHelper.parseDouble(balance.get("surplus_rese")));
indicator.setUndistributedProfit(NumberHelper.parseDouble(balance.get("undistr_porfit")));
indicator.setCashAndCashEquivalents(NumberHelper.parseDouble(balance.get("cash_reser_cb")));
indicator.setCashAndCashEquivalentsToTotalAssetsRatio(NumberHelper.safeDiv(indicator.getCashAndCashEquivalents(), indicator.getTotalAssets()));
indicator.setAccountsReceivable(NumberHelper.parseDouble(balance.get("accounts_receiv_bill")));
indicator.setAccountsReceivableToTotalAssetsRatio(NumberHelper.safeDiv(indicator.getAccountsReceivable(), indicator.getTotalAssets()));
indicator.setAccountsPayable(NumberHelper.parseDouble(balance.get("accounts_pay")));
indicator.setAccountsPayableToTotalAssetsRatio(NumberHelper.safeDiv(indicator.getAccountsPayable(), indicator.getTotalAssets()));
indicator.setInventory(NumberHelper.parseDouble(balance.get("inventories")));
indicator.setInventoryToTotalAssetsRatio(NumberHelper.safeDiv(indicator.getInventory(), indicator.getTotalAssets()));
indicator.setGoodwill(NumberHelper.parseDouble(balance.get("goodwill")));
indicator.setGoodwillToTotalAssetsRatio(NumberHelper.safeDiv(indicator.getGoodwill(), indicator.getTotalAssets()));
indicator.setCurrentAssets(NumberHelper.parseDouble(balance.get("total_cur_assets")));
indicator.setCurrentAssetsToTotalAssetsRatio(NumberHelper.safeDiv(indicator.getCurrentAssets(), indicator.getTotalAssets()));
indicator.setFixedAssets(NumberHelper.parseDouble(balance.get("total_nca")));
indicator.setFixedAssetsToTotalAssetsRatio(NumberHelper.safeDiv(indicator.getFixedAssets(), indicator.getTotalAssets()));
indicator.setTotalLiabilities(NumberHelper.parseDouble(balance.get("total_liab")));
indicator.setCurrentLiabilities(NumberHelper.parseDouble(balance.get("total_cur_liab")));
indicator.setCurrentLiabilitiesToTotalAssetsRatio(NumberHelper.safeDiv(indicator.getCurrentLiabilities(), indicator.getTotalAssets()));
indicator.setCurrentLiabilitiesToTotalLiabilitiesRatio(NumberHelper.safeDiv(indicator.getCurrentLiabilities(), indicator.getTotalLiabilities()));
indicator.setLongTermLiabilities(NumberHelper.parseDouble(balance.get("total_ncl")));
indicator.setLongTermLiabilitiesToTotalAssetsRatio(NumberHelper.safeDiv(indicator.getLongTermLiabilities(), indicator.getTotalAssets()));
indicator.setLongTermLiabilitiesToTotalLiabilitiesRatio(NumberHelper.safeDiv(indicator.getLongTermLiabilities(), indicator.getTotalLiabilities()));
indicator.setLiabilitiesToTotalAssetsRatio(NumberHelper.safeDiv(indicator.getTotalLiabilities(), indicator.getTotalAssets()));
indicator.setShareholdersEquity(NumberHelper.parseDouble(balance.get("total_hldr_eqy_inc_min_int")));
indicator.setShareholdersEquityToTotalAssetsRatio(NumberHelper.safeDiv(indicator.getShareholdersEquity(), indicator.getTotalAssets()));
}
if (ObjectUtil.isNotNull(income)) {
indicator.setOperatingRevenue(NumberHelper.parseDouble(income.get("total_revenue")));
indicator.setOperatingCost(NumberHelper.parseDouble(income.get("total_cogs")));
indicator.setOperatingProfit(NumberHelper.parseDouble(income.get("operate_profit")));
indicator.setOperatingExpenses(NumberHelper.parseDouble(income.get("oper_exp")));
indicator.setNetProfit(NumberHelper.parseDouble(income.get("n_income")));
}
if (ObjectUtil.isNotNull(cashFlow)) {
indicator.setCashFlowFromOperatingActivities(NumberHelper.parseDouble(cashFlow.get("n_cashflow_act")));
indicator.setCashFlowFromInvestingActivities(NumberHelper.parseDouble(cashFlow.get("n_cashflow_inv_act")));
indicator.setCashFlowFromFinancingActivities(NumberHelper.parseDouble(cashFlow.get("n_cash_flows_fnc_act")));
}
if (ObjectUtil.isNotNull(finaIndicator)) {
indicator.setCurrentRatio(NumberHelper.parseDouble(finaIndicator.get("current_ratio")));
indicator.setQuickRatio(NumberHelper.parseDouble(finaIndicator.get("quick_ratio")));
indicator.setAccountsReceivableTurnover(NumberHelper.parseDouble(finaIndicator.get("ar_turn")));
indicator.setDaysAccountsReceivableTurnover(NumberHelper.parseDouble(finaIndicator.get("arturn_days")));
indicator.setInventoryTurnover(NumberHelper.parseDouble(finaIndicator.get("inv_turn")));
indicator.setDaysInventoryTurnover(NumberHelper.parseDouble(finaIndicator.get("invturn_days")));
indicator.setFixedAssetsTurnover(NumberHelper.parseDouble(finaIndicator.get("fa_turn")));
indicator.setDaysFixedAssetsTurnover(NumberHelper.safeDiv(360.0, indicator.getFixedAssetsTurnover()));
indicator.setTotalAssetsTurnover(NumberHelper.parseDouble(finaIndicator.get("assets_turn")));
indicator.setDaysTotalAssetsTurnover(NumberHelper.safeDiv(360.0, indicator.getTotalAssetsTurnover()));
indicator.setReturnOnEquity(NumberHelper.parseDouble(finaIndicator.get("roe_dt")));
indicator.setReturnOnAssets(NumberHelper.parseDouble(finaIndicator.get("roa")));
indicator.setOperatingGrossProfitMargin(NumberHelper.safeDiv(NumberHelper.safeMinus(indicator.getOperatingRevenue(), indicator.getOperatingCost()), indicator.getOperatingRevenue()));
indicator.setOperatingProfitMargin(NumberHelper.safeDiv(indicator.getOperatingProfit(), indicator.getOperatingRevenue()));
indicator.setOperatingSafetyMarginRatio(NumberHelper.safeDiv(indicator.getOperatingProfitMargin(), indicator.getOperatingGrossProfitMargin()));
indicator.setNetProfitMargin(NumberHelper.parseDouble(finaIndicator.get("roa_dp")));
indicator.setEarningsPerShare(NumberHelper.parseDouble(finaIndicator.get("total_revenue_ps")));
}
financeIndicatorRepository.save(indicator);
}
updater.update((year - 1990) * 1.0 / (currentYear - 1990));
}
return null;
}
}

View File

@@ -0,0 +1,64 @@
package com.lanyuanxiaoyao.leopard.core.task;
import cn.hutool.core.util.EnumUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.core.service.TuShareService;
import java.time.LocalDate;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 更新股票信息
*
* @author lanyuanxiaoyao
* @version 20250924
*/
@Component
public class UpdateStockTask extends TaskRunner {
private final StockRepository stockRepository;
private final TuShareService tuShareService;
public UpdateStockTask(ApplicationContext context, StockRepository stockRepository, TuShareService tuShareService) {
super(context);
this.stockRepository = stockRepository;
this.tuShareService = tuShareService;
}
@Transactional(rollbackFor = Throwable.class)
@Override
public String process(Map<String, Object> params, StepUpdater updater) {
var existsStockMap = stockRepository.findAll().stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
var stocks = tuShareService.stockList()
.data()
.items()
.stream()
.map(item -> {
var code = item.get(0);
var name = item.get(1);
var fullname = item.get(2);
var market = EnumUtil.fromString(Stock.Market.class, item.get(3));
var industry = item.get(4);
var listedDate = LocalDate.parse(item.get(5), TuShareService.TRADE_FORMAT);
var stock = existsStockMap.getOrDefault(code, new Stock());
stock.setCode(code);
stock.setName(name);
stock.setFullname(fullname);
stock.setMarket(market);
stock.setIndustry(industry);
stock.setListedDate(listedDate);
return stock;
})
.toList();
var currentCodes = stocks.stream().map(Stock::getCode).toList();
var existsCodes = stockRepository.findDistinctCodes();
var deleteCodes = existsCodes.stream().filter(code -> !currentCodes.contains(code)).toList();
stockRepository.deleteAllByCodeIn(deleteCodes);
stockRepository.saveAll(stocks);
return null;
}
}

View File

@@ -0,0 +1,25 @@
package com.lanyuanxiaoyao.leopard.core;
import com.lanyuanxiaoyao.service.template.Helper;
import org.hibernate.dialect.PostgreSQLDialect;
import org.postgresql.Driver;
/**
* 建表语句
*
* @author lanyuanxiaoyao
* @version 20250911
*/
public class GenerateDDL {
public static void main(String[] args) {
Helper.generateDDL(
"com.lanyuanxiaoyao.leopard.core.entity",
"/Users/lanyuanxiaoyao/Project/IdeaProjects/leopard/leopard-core/target",
PostgreSQLDialect.class,
"jdbc:postgresql://81.71.3.24:6785/leopard_dev",
"leopard",
"9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X",
Driver.class
);
}
}

View File

@@ -1,3 +1,3 @@
#!/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/web --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 &

View File

@@ -1,10 +1,17 @@
#!/bin/bash
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() {
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)
@@ -31,3 +38,4 @@ while (true); do
fi
sleep 1s
done

View File

@@ -31,6 +31,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</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>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
@@ -41,20 +49,6 @@
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
@@ -65,6 +59,11 @@
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>

View File

@@ -1,8 +1,7 @@
package com.lanyuanxiaoyao.leopard.server;
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
import com.yomahub.liteflow.core.FlowExecutor;
import jakarta.annotation.Resource;
import com.lanyuanxiaoyao.leopard.core.repository.TaskRepository;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
@@ -18,19 +17,20 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
@EnableJpaAuditing
public class LeopardServerApplication implements ApplicationRunner {
private final TaskRepository taskRepository;
public LeopardServerApplication(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
public static void main(String[] args) {
SpringApplication.run(LeopardServerApplication.class, args);
}
@Resource
private FlowExecutor executor;
@Resource
private TuShareService tuShareService;
@Transactional(rollbackOn = Throwable.class)
@Override
public void run(ApplicationArguments args) {
// executor.execute2RespWithEL("THEN(update_daily)");
// executor.execute2RespWithEL("THEN(update_stock)");
// executor.execute2RespWithEL("THEN(check_daily)");
log.warn("更新所有未完成的任务状态为失败");
taskRepository.updateAllRunningTaskToFailure();
}
}

View File

@@ -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;
}
}

View File

@@ -2,10 +2,11 @@ package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.entity.Task;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.core.service.TaskService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -86,12 +87,12 @@ public class CommonOptionsController {
"bg-pink-900"
);
private final StockService stockService;
private final TaskTemplateService taskTemplateService;
private final StockRepository stockRepository;
private final TaskService taskService;
public CommonOptionsController(StockService stockService, TaskTemplateService taskTemplateService) {
this.stockService = stockService;
this.taskTemplateService = taskTemplateService;
public CommonOptionsController(StockRepository stockRepository, TaskService taskService) {
this.stockRepository = stockRepository;
this.taskService = taskService;
}
@GetMapping("/options/{name}")
@@ -100,18 +101,21 @@ public class CommonOptionsController {
case "stock_market" -> GlobalResponse.responseSuccess(
Arrays.stream(Stock.Market.values())
.map(market -> new Option(market.getChineseName(), market.name()))
.sorted(Comparator.comparing(Option::label))
.toList()
);
case "stock_industry" -> GlobalResponse.responseSuccess(
stockService.findDistinctIndustries()
stockRepository.findDistinctIndustries()
.stream()
.map(industry -> new Option(industry, industry))
.sorted(Comparator.comparing(Option::label))
.toList()
);
case "task_template_id" -> GlobalResponse.responseSuccess(
taskTemplateService.list()
taskService.getTemplates()
.stream()
.map(template -> new Option(template.getName(), template.getId()))
.map(template -> new Option(template.name(), template.id()))
.sorted(Comparator.comparing(Option::label))
.toList()
);
default -> GlobalResponse.responseSuccess(List.of());

View File

@@ -1,7 +1,7 @@
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.TaskTemplateService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import java.time.LocalDateTime;
import java.util.List;
@@ -20,11 +20,11 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("task_schedule")
public class QuartzController {
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.taskTemplateService = taskTemplateService;
this.taskService = taskService;
}
@PostMapping("save")
@@ -38,11 +38,11 @@ public class QuartzController {
var list = quartzService.list()
.stream()
.map(task -> {
var template = taskTemplateService.detail(task.templateId());
var template = taskService.getTemplate(task.templateId());
return new ListItem(
task.key(),
template.getName(),
template.getDescription(),
template.name(),
template.description(),
task.cron(),
task.status(),
task.previousFireTime(),
@@ -72,7 +72,7 @@ public class QuartzController {
}
public record SaveItem(
Long templateId,
String templateId,
String cron
) {
}

View File

@@ -0,0 +1,81 @@
package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
import com.lanyuanxiaoyao.leopard.core.service.StockCollectionService;
import com.lanyuanxiaoyao.leopard.server.entity.StockScoreVo;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("stock_collection")
public class StockCollectionController extends SimpleControllerSupport<StockCollection, Void, StockCollectionController.ListItem, StockCollectionController.DetailItem> {
public StockCollectionController(StockCollectionService service) {
super(service);
}
@Override
public GlobalResponse<Long> save(Void unused) throws Exception {
throw new UnsupportedOperationException();
}
@Override
protected Function<Void, StockCollection> saveItemMapper() {
throw new UnsupportedOperationException();
}
@Override
protected Function<StockCollection, ListItem> listItemMapper() {
return collection -> new ListItem(
collection.getId(),
collection.getName(),
collection.getDescription(),
collection.getScores().size(),
collection.getCreatedTime(),
collection.getModifiedTime()
);
}
@Override
protected Function<StockCollection, DetailItem> detailItemMapper() {
return collection -> new DetailItem(
collection.getId(),
collection.getName(),
collection.getDescription(),
collection.getScores().size(),
collection.getScores()
.stream()
.map(StockScoreVo::of)
.sorted(Comparator.comparing(StockScoreVo::score).reversed())
.toList(),
collection.getCreatedTime(),
collection.getModifiedTime()
);
}
public record ListItem(
Long id,
String name,
String description,
Integer count,
LocalDateTime createdTime,
LocalDateTime modifiedTime
) {
}
public record DetailItem(
Long id,
String name,
String description,
Integer count,
List<StockScoreVo> scores,
LocalDateTime createdTime,
LocalDateTime modifiedTime
) {
}
}

View File

@@ -1,11 +1,22 @@
package com.lanyuanxiaoyao.leopard.server.controller;
import cn.hutool.core.bean.BeanUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
import com.lanyuanxiaoyao.leopard.core.helper.NumberHelper;
import com.lanyuanxiaoyao.leopard.core.helper.TaHelper;
import com.lanyuanxiaoyao.leopard.core.service.StockService;
import com.lanyuanxiaoyao.leopard.server.entity.StockDetailVo;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -13,11 +24,15 @@ import org.springframework.web.bind.annotation.RestController;
* @author lanyuanxiaoyao
* @version 20250829
*/
@Slf4j
@RestController
@RequestMapping("stock")
public class StockController extends SimpleControllerSupport<Stock, Void, StockController.DetailItem, StockController.DetailItem> {
public StockController(StockService service) {
public class StockController extends SimpleControllerSupport<Stock, Void, StockDetailVo, StockDetailVo> {
private final StockService stockService;
public StockController(StockService service, StockService stockService) {
super(service);
this.stockService = stockService;
}
@Override
@@ -25,41 +40,249 @@ public class StockController extends SimpleControllerSupport<Stock, Void, StockC
throw new UnsupportedOperationException();
}
@GetMapping("finance/{id}")
public GlobalResponse<FinanceItem> finance(@PathVariable("id") Long id) {
// 财报默认是上一年的
var year = LocalDate.now().minusYears(1).getYear();
var financeIndicator = stockService.findFinanceIndicator(id, year);
return GlobalResponse.responseSuccess(new FinanceItem(
id,
year,
financeIndicator
.map(fi -> new BalanceSheetItem(
NumberHelper.formatFinanceDouble(fi.getTotalAssets()),
NumberHelper.formatFinanceDouble(fi.getCurrentAssets()),
NumberHelper.formatPercentageDouble(fi.getCurrentAssetsToTotalAssetsRatio()),
NumberHelper.formatFinanceDouble(fi.getFixedAssets()),
NumberHelper.formatPercentageDouble(fi.getFixedAssetsToTotalAssetsRatio()),
NumberHelper.formatFinanceDouble(fi.getTotalLiabilities()),
NumberHelper.formatFinanceDouble(fi.getCurrentLiabilities()),
NumberHelper.formatPercentageDouble(fi.getCurrentLiabilitiesToTotalAssetsRatio()),
NumberHelper.formatFinanceDouble(fi.getLongTermLiabilities()),
NumberHelper.formatPercentageDouble(fi.getLongTermLiabilitiesToTotalAssetsRatio())
))
.orElse(new BalanceSheetItem()),
financeIndicator
.map(fi -> new IncomeItem(
NumberHelper.formatFinanceDouble(fi.getOperatingRevenue()),
NumberHelper.formatFinanceDouble(fi.getOperatingCost()),
NumberHelper.formatFinanceDouble(fi.getOperatingProfit())
))
.orElse(new IncomeItem()),
financeIndicator
.map(fi -> new CashFlowItem(
NumberHelper.formatFinanceDouble(fi.getNetProfit()),
NumberHelper.formatFinanceDouble(fi.getCashFlowFromOperatingActivities()),
NumberHelper.formatFinanceDouble(fi.getCashFlowFromInvestingActivities()),
NumberHelper.formatFinanceDouble(fi.getCashFlowFromFinancingActivities())
))
.orElse(new CashFlowItem()),
financeIndicator
.map(fi -> new IndicateItem(
NumberHelper.formatFinanceDouble(fi.getCurrentRatio()),
NumberHelper.formatFinanceDouble(fi.getQuickRatio()),
NumberHelper.formatFinanceDouble(fi.getReturnOnEquity()),
NumberHelper.formatFinanceDouble(fi.getReturnOnAssets()),
NumberHelper.formatFinanceDouble(fi.getAccountsReceivableTurnover()),
NumberHelper.formatDaysDouble(fi.getDaysAccountsReceivableTurnover()),
NumberHelper.formatFinanceDouble(fi.getInventoryTurnover()),
NumberHelper.formatDaysDouble(fi.getDaysInventoryTurnover()),
NumberHelper.formatFinanceDouble(fi.getFixedAssetsTurnover()),
NumberHelper.formatDaysDouble(fi.getDaysFixedAssetsTurnover()),
NumberHelper.formatFinanceDouble(fi.getTotalAssetsTurnover()),
NumberHelper.formatDaysDouble(fi.getDaysTotalAssetsTurnover())
))
.orElse(new IndicateItem())
));
}
@GetMapping("finance/{id}/{field}")
public GlobalResponse<Map<String, Object>> financeCharts(@PathVariable("id") Long id, @PathVariable("field") String field) {
var data = stockService.findFinanceIndicatorRecent(id, 5);
var xList = new ArrayList<Integer>();
var yList = new ArrayList<Object>();
for (var indicator : data) {
xList.add(indicator.getYear());
yList.add(BeanUtil.getFieldValue(indicator, field));
}
return GlobalResponse.responseMapData(Map.of(
"xList", xList, "yList", yList
));
}
@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}")
public GlobalResponse<Map<String, Object>> dailyCharts(@PathVariable("id") Long id) {
var data = stockService.findDailyRecent(id, 100 + 60);
log.info("Size: {}", data.size());
var xList = new ArrayList<String>();
var yList = new ArrayList<List<Double>>();
for (var daily : data.subList(60, data.size() - 1)) {
xList.add(daily.getTradeDate().toString());
yList.add(List.of(daily.getHfqOpen(), daily.getHfqClose(), daily.getHfqLow(), daily.getHfqHigh()));
}
return GlobalResponse.responseMapData(Map.of(
"xList", xList,
"yList", yList,
"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(
"xList", xList, "yList", yList
));
}
@Override
protected Function<Void, Stock> saveItemMapper() {
throw new UnsupportedOperationException();
}
private DetailItem covert(Stock stock) {
return new DetailItem(
stock.getId(),
stock.getCode(),
stock.getName(),
stock.getFullname(),
stock.getMarket(),
stock.getIndustry(),
stock.getListedDate()
);
@Override
protected Function<Stock, StockDetailVo> listItemMapper() {
return StockDetailVo::of;
}
@Override
protected Function<Stock, DetailItem> listItemMapper() {
return this::covert;
protected Function<Stock, StockDetailVo> detailItemMapper() {
return StockDetailVo::of;
}
@Override
protected Function<Stock, DetailItem> detailItemMapper() {
return this::covert;
}
public record DetailItem(
public record FinanceItem(
Long id,
String code,
String name,
String fullname,
Stock.Market market,
String industry,
LocalDate listedDate
Integer year,
BalanceSheetItem balanceSheet,
IncomeItem income,
CashFlowItem cashFlow,
IndicateItem indicate
) {
}
public record BalanceSheetItem(
String totalAssets,
String currentAssets,
String currentAssetsRatio,
String fixedAssets,
String fixedAssetsRatio,
String totalLiabilities,
String currentLiabilities,
String currentLiabilitiesRatio,
String longTermLiabilities,
String longTermLiabilitiesRatio
) {
public BalanceSheetItem() {
this(
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE
);
}
}
public record IncomeItem(
String operatingRevenue,
String operatingCost,
String operatingProfit
) {
public IncomeItem() {
this(
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE
);
}
}
public record CashFlowItem(
String netProfit,
String cashFlowFromOperatingActivities,
String cashFlowFromInvestingActivities,
String cashFlowFromFinancingActivities
) {
public CashFlowItem() {
this(
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE
);
}
}
public record IndicateItem(
String currentRatio,
String quickRatio,
String roe,
String roa,
String accountsReceivableTurnover,
String daysAccountsReceivableTurnover,
String inventoryTurnover,
String daysInventoryTurnover,
String fixedAssetsTurnover,
String daysFixedAssetsTurnover,
String totalAssetsTurnover,
String daysTotalAssetsTurnover
) {
public IndicateItem() {
this(
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE,
NumberHelper.FINANCE_NULL_DOUBLE
);
}
}
}

View File

@@ -4,14 +4,16 @@ import cn.hutool.core.date.BetweenFormatter;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
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.SimpleControllerSupport;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.Map;
import java.util.function.Function;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -45,14 +47,25 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
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
protected Function<Void, Task> saveItemMapper() {
throw new UnsupportedOperationException();
}
private TaskCost calculateCost(LocalDateTime start, LocalDateTime finish) {
if (ObjectUtil.isNull(start) || ObjectUtil.isNull(finish)) {
if (ObjectUtil.isNull(start)) {
return new TaskCost(null, null);
} else if (ObjectUtil.isNull(finish)) {
finish = LocalDateTime.now();
}
var duration = Duration.between(start, finish).toMillis();
return new TaskCost(
@@ -108,7 +121,7 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
LocalDateTime finishedTime,
Long cost,
String costText,
Integer step
Double step
) {
}
@@ -123,11 +136,11 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
LocalDateTime finishedTime,
Long cost,
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) {

View File

@@ -1,57 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.controller;
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.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> {
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.setChain(item.chain());
return template;
};
}
private Item convert(TaskTemplate template) {
return new Item(
template.getId(),
template.getName(),
template.getDescription(),
template.getChain()
);
}
@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 chain
) {
}
}

View File

@@ -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()
);
}
}

View File

@@ -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>"))
);
}
}

View File

@@ -1,26 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service;
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import java.time.LocalDate;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class DailyService extends SimpleServiceSupport<Daily> {
private final DailyRepository dailyRepository;
public DailyService(DailyRepository repository) {
super(repository);
this.dailyRepository = repository;
}
public List<LocalDate> findDistinctTradeDate() {
return dailyRepository.findDistinctTradeDate();
}
public List<LocalDate> findDistinctTradeDateByStockId(Long stockId) {
return dailyRepository.findDistinctTradeDateByStockId(stockId);
}
}

View File

@@ -2,8 +2,8 @@ package com.lanyuanxiaoyao.leopard.server.service;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.lanyuanxiaoyao.leopard.server.service.task.TaskMonitorNodes;
import com.yomahub.liteflow.core.FlowExecutor;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.core.service.TaskService;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
@@ -36,10 +36,10 @@ public class QuartzService {
var tasks = new ArrayList<QuartzTask>();
for (var key : scheduler.getJobKeys(GroupMatcher.anyGroup())) {
var detail = scheduler.getJobDetail(key);
var trigger = (CronTrigger) scheduler.getTriggersOfJob(key).get(0);
var trigger = (CronTrigger) scheduler.getTriggersOfJob(key).getFirst();
tasks.add(new QuartzTask(
detail.getKey().getName(),
detail.getJobDataMap().getLong("template_id"),
detail.getJobDataMap().getString("template_id"),
trigger.getCronExpression(),
scheduler.getTriggerState(trigger.getKey()),
ObjectUtil.isNull(trigger.getPreviousFireTime()) ? null : LocalDateTime.ofInstant(trigger.getPreviousFireTime().toInstant(), ZoneId.systemDefault()),
@@ -49,7 +49,7 @@ public class QuartzService {
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)
.withIdentity("task_execution_" + IdUtil.fastUUID())
.usingJobData("template_id", templateId)
@@ -81,30 +81,27 @@ public class QuartzService {
@Slf4j
public static class TaskExecutionJob extends QuartzJobBean {
private final TaskTemplateService taskTemplateService;
private final FlowExecutor flowExecutor;
private final TaskService taskService;
public TaskExecutionJob(TaskTemplateService taskTemplateService, FlowExecutor flowExecutor) {
this.taskTemplateService = taskTemplateService;
this.flowExecutor = flowExecutor;
public TaskExecutionJob(TaskService taskService) {
this.taskService = taskService;
}
@SuppressWarnings("unchecked")
@Override
protected void executeInternal(JobExecutionContext context) {
var dataMap = context.getMergedJobDataMap();
var templateId = dataMap.getLong("template_id");
if (ObjectUtil.isNotNull(templateId)) {
var template = taskTemplateService.detail(templateId);
var templateId = dataMap.getString("template_id");
if (StrUtil.isNotBlank(templateId)) {
var params = (Map<String, Object>) dataMap.getOrDefault("params", Map.of());
var monitorContext = new TaskMonitorNodes.TaskMonitorContext(template);
flowExecutor.execute2Resp(template.getChain(), params, monitorContext);
taskService.execute(templateId, params, true);
}
}
}
public record QuartzTask(
String key,
Long templateId,
String templateId,
String cron,
Trigger.TriggerState status,
LocalDateTime previousFireTime,

View File

@@ -1,27 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author lanyuanxiaoyao
* @version 20250828
*/
@Slf4j
@Service
public class StockService extends SimpleServiceSupport<Stock> {
private final StockRepository stockRepository;
public StockService(StockRepository repository) {
super(repository);
this.stockRepository = repository;
}
public List<String> findDistinctIndustries() {
return stockRepository.findDistinctIndustries();
}
}

View File

@@ -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);
}
}

View File

@@ -1,13 +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 org.springframework.stereotype.Service;
@Service
public class TaskTemplateService extends SimpleServiceSupport<TaskTemplate> {
public TaskTemplateService(TaskTemplateRepository repository) {
super(repository);
}
}

View File

@@ -1,87 +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.server.service.DailyService;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
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 StockService stockService;
private final DailyService dailyService;
private final TuShareService tuShareService;
public CheckDailyNode(TaskService taskService, StockService stockService, DailyService dailyService, TuShareService tuShareService) {
super(taskService);
this.stockService = stockService;
this.dailyService = dailyService;
this.tuShareService = tuShareService;
}
@Override
public void process() {
var reports = new ArrayList<MissedTradeReport>();
var stocks = stockService.list();
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) {
if (exchange.equals(stock.getMarket())) {
var existsTradeDates = dailyService.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
) {
}
}

View File

@@ -1,82 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
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(exception.getMessage());
}
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;
}
}
}

View File

@@ -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) {
}
}
}

View File

@@ -1,99 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
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.server.service.DailyService;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
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 StockService stockService;
private final DailyService dailyService;
private final TuShareService tuShareService;
private final TransactionTemplate transactionTemplate;
public UpdateDailyNode(StockService stockService, DailyService dailyService, TuShareService tuShareService, TransactionTemplate transactionTemplate) {
this.stockService = stockService;
this.dailyService = dailyService;
this.tuShareService = tuShareService;
this.transactionTemplate = transactionTemplate;
}
// @Transactional(rollbackOn = Throwable.class)
@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 = dailyService.findDistinctTradeDate();
var nowDate = LocalDate.now();
var stocks = stockService.list();
var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
tradeDates.stream()
.filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate))
.filter(date -> !existsTradeDates.contains(date))
.sorted()
.parallel()
.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 {
var count = 0;
for (List<String> item : response.data().items()) {
var code = item.get(0);
if (stocksMap.containsKey(code)) {
count++;
var stock = stocksMap.get(code);
var factor = factorMap.get(code);
var daily = new Daily();
daily.setTradeDate(LocalDate.parse(item.get(1), TuShareService.TRADE_FORMAT));
daily.setOpen(item.get(2) == null ? null : Double.parseDouble(item.get(2)));
daily.setHigh(item.get(3) == null ? null : Double.parseDouble(item.get(3)));
daily.setLow(item.get(4) == null ? null : Double.parseDouble(item.get(4)));
daily.setClose(item.get(5) == null ? null : Double.parseDouble(item.get(5)));
daily.setPreviousClose(item.get(6) == null ? null : Double.parseDouble(item.get(6)));
daily.setPriceChangeAmount(item.get(7) == null ? null : Double.parseDouble(item.get(7)));
daily.setPriceFluctuationRange(item.get(8) == null ? null : Double.parseDouble(item.get(8)));
daily.setVolume(item.get(9) == null ? null : Double.parseDouble(item.get(9)));
daily.setTurnover(item.get(10) == null ? null : Double.parseDouble(item.get(10)) * 1000);
daily.setFactor(factor);
daily.setStock(stock);
dailyService.save(daily);
}
}
log.info("Trade date: {} {}", tradeDate, count);
return true;
} catch (Exception exception) {
log.error("Error", exception);
status.setRollbackOnly();
return false;
}
});
});
}
}

View File

@@ -1,67 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
import cn.hutool.core.util.EnumUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import jakarta.transaction.Transactional;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.stream.Collectors;
@LiteflowComponent("update_stock")
public class UpdateStockNode extends NodeComponent {
private final StockService stockService;
private final TuShareService tuShareService;
public UpdateStockNode(StockService stockService, TuShareService tuShareService) {
this.stockService = stockService;
this.tuShareService = tuShareService;
}
@Transactional(rollbackOn = Throwable.class)
@Override
public void process() {
var stocks = stockService.list();
var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
var targetCodes = new HashSet<String>();
tuShareService.stockList()
.data()
.items()
.forEach(item -> {
var code = item.get(0);
var name = item.get(1);
var fullname = item.get(2);
var market = EnumUtil.fromString(Stock.Market.class, item.get(3));
var industry = item.get(4);
var listedDate = LocalDate.parse(item.get(5), TuShareService.TRADE_FORMAT);
if (stocksMap.containsKey(code)) {
var stock = stocksMap.get(code);
stock.setName(name);
stock.setFullname(fullname);
stock.setMarket(market);
stock.setIndustry(industry);
stock.setListedDate(listedDate);
} else {
var stock = new Stock();
stock.setCode(code);
stock.setName(name);
stock.setFullname(fullname);
stock.setMarket(market);
stock.setIndustry(industry);
stock.setListedDate(listedDate);
stocks.add(stock);
}
targetCodes.add(code);
});
var deleteStocks = stocks.stream()
.filter(stock -> !targetCodes.contains(stock.getCode()))
.map(Stock::getId)
.toList();
stockService.remove(deleteStocks);
stockService.save(stocks);
}
}

View File

@@ -0,0 +1,3 @@
spring:
datasource:
url: jdbc:postgresql://127.0.0.1:6785/leopard_dev

View 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

View File

@@ -9,12 +9,9 @@ spring:
async:
request-timeout: 3600000
datasource:
# url: jdbc:mysql://mysql.lanyuanxiaoyao.com:43780/leopard?useSSL=false
# url: jdbc:mysql://192.168.31.127:3780/leopard?useSSL=false
url: jdbc:postgresql://81.71.3.24:6785/leopard
url: jdbc:postgresql://81.71.3.24:6785/leopard_dev
username: leopard
password: '9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X'
# driver-class-name: com.mysql.cj.jdbc.Driver
driver-class-name: org.postgresql.Driver
jpa:
generate-ddl: true
@@ -25,9 +22,8 @@ spring:
startup-delay: 30s
job-store-type: jdbc
jdbc:
# platform: mysql
platform: postgres
# initialize-schema: always
initialize-schema: always
properties:
org:
quartz:
@@ -35,7 +31,3 @@ spring:
driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
fenix:
print-banner: false
liteflow:
print-banner: false
rule-source: flow.xml
check-node-exists: false

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flow PUBLIC "liteflow" "https://liteflow.cc/liteflow.dtd">
<flow>
<chain id="update_stock_information">
CATCH(THEN(task_start, update_stock, task_end)).DO(task_error)
</chain>
<chain id="update_daily_information">
CATCH(THEN(task_start, update_daily, task_end)).DO(task_error)
</chain>
<chain id="check_daily">
CATCH(THEN(task_start, check_daily, task_end)).DO(task_error)
</chain>
</flow>

View File

@@ -20,6 +20,7 @@
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOGGING_PARENT:-.}/archive/${APP_NAME:-run}-%d{yyyy-MM-dd}.gz</fileNamePattern>
<MaxHistory>7</MaxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %p [${HOSTNAME}] [%t] %logger: %m%n%wEx</pattern>
@@ -27,7 +28,7 @@
</appender>
<logger name="com.zaxxer.hikari" level="ERROR"/>
<!--<logger name="org.hibernate.SQL" level="DEBUG"/>-->
<logger name="org.hibernate.SQL" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="Console"/>

View File

@@ -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()
);
}
}

View File

@@ -29,24 +29,39 @@ POST {{api_url}}
Content-Type: application/json
{
"api_name": "income",
"api_name": "income_vip",
"token": "{{api_key}}",
"params": {
"ts_code": "600000.SH"
"period": "20241231"
},
"fields": [
"ts_code",
"ann_date",
"fiscal_year",
"report_type",
"net_profit",
"basic_eps",
"diluted_eps",
"total_revenue",
"total_cost",
"gross_profit",
"operating_profit",
"net_profit_before_tax",
"net_profit_to_parent",
"total_revenue_to_parent"
"revenue",
"total_cogs",
"oper_cost",
"sell_exp",
"admin_exp",
"fin_exp",
"oper_exp",
"operate_profit",
"non_oper_income",
"non_oper_exp",
"total_profit",
"income_tax",
"n_income",
"n_income_attr_p",
"compr_inc_attr_p",
"compr_inc_attr_m_s",
"ebit",
"ebida",
"undist_profit",
"distable_profit",
"rd_exp",
"fin_exp_int_exp",
"continued_net_profit",
"end_net_profit"
]
}
@@ -94,7 +109,8 @@ Content-Type: application/json
"api_name": "adj_factor",
"token": "{{api_key}}",
"params": {
"trade_date": "20241231"
"ts_code": "600018.SH",
"trade_date": "20060331"
},
"fields": [
"ts_code",
@@ -111,16 +127,18 @@ Content-Type: application/json
"api_name": "stk_factor_pro",
"token": "{{api_key}}",
"params": {
"ts_code": "000001.SZ",
"trade_date": "20250225"
"ts_code": "600018.SH",
"trade_date": "20060331"
},
"fields": [
"ts_code",
"trade_date",
"open",
"open_qfq",
"open_hfq",
"close",
"close_qfq",
"close_hfq",
"ema_hfq_5"
"close_hfq"
]
}
@@ -129,17 +147,15 @@ POST {{api_url}}
Content-Type: application/json
{
"api_name": "income",
"api_name": "income_vip",
"token": "{{api_key}}",
"params": {
"ts_code": "000001.SZ",
"period": "20241231"
"ts_code": "000799.SZ",
"period": "20191231"
},
"fields": [
"ts_code",
"ann_date",
"total_revenue",
"int_income"
"revenue",
"total_revenue"
]
}
@@ -148,15 +164,27 @@ POST {{api_url}}
Content-Type: application/json
{
"api_name": "cashflow",
"api_name": "cashflow_vip",
"token": "{{api_key}}",
"params": {
"ts_code": "000002.SZ",
"period": "20231231"
},
"fields": [
"ann_date",
"n_cashflow_act"
"ts_code",
"net_profit",
"finan_exp",
"c_fr_sale_sg",
"c_inf_fr_operate_a",
"c_paid_to_for_empl",
"c_paid_for_taxes",
"n_cashflow_act",
"stot_inflows_inv_act",
"c_pay_acq_const_fiolta",
"stot_out_inv_act",
"stot_cashout_fnc_act",
"c_cash_equ_beg_period",
"c_cash_equ_end_period"
]
}
@@ -165,16 +193,15 @@ POST {{api_url}}
Content-Type: application/json
{
"api_name": "balancesheet",
"api_name": "balancesheet_vip",
"token": "{{api_key}}",
"params": {
"ts_code": "000002.SZ",
"period": "20231231"
"ts_code": "000799.SZ",
"period": "20191231"
},
"fields": [
"ann_date",
"total_cur_liab",
"oth_cur_liab"
"total_liab_hldr_eqy",
"total_assets"
]
}
@@ -186,13 +213,16 @@ Content-Type: application/json
"api_name": "fina_indicator",
"token": "{{api_key}}",
"params": {
"ts_code": "000002.SZ",
"ts_code": "001400.SZ",
"period": "20231231"
},
"fields": [
"ann_date",
"ocf_to_shortdebt",
"currentdebt_to_debt"
"ts_code",
"current_ratio",
"quick_ratio",
"invturn_days",
"arturn_days",
"ar_turn"
]
}
@@ -212,3 +242,25 @@ Content-Type: application/json
"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"
]
}

View File

@@ -21,14 +21,27 @@
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.github.ralfkonrad.quantlib_for_maven</groupId>
<artifactId>quantlib</artifactId>
<version>1.39.0</version>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.ta4j</groupId>
<artifactId>ta4j-core</artifactId>
<version>0.17</version>
<groupId>org.commonmark</groupId>
<artifactId>commonmark</artifactId>
<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>
@@ -41,6 +54,11 @@
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>

View File

@@ -1,69 +1,292 @@
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.Daily_;
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
import com.lanyuanxiaoyao.leopard.core.entity.QStock;
import com.lanyuanxiaoyao.leopard.core.entity.dto.Monthly;
import com.lanyuanxiaoyao.leopard.core.entity.dto.Weekly;
import com.lanyuanxiaoyao.leopard.core.entity.dto.YearAndMonth;
import com.lanyuanxiaoyao.leopard.core.entity.dto.YearAndWeek;
import com.lanyuanxiaoyao.leopard.core.helper.TaHelper;
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.core.strategy.TradeEngine;
import jakarta.annotation.Resource;
import jakarta.transaction.Transactional;
import java.time.Duration;
import java.time.ZoneId;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.ta4j.core.BaseBar;
import org.ta4j.core.BaseBarSeriesBuilder;
import org.ta4j.core.indicators.EMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.num.DoubleNum;
import org.springframework.transaction.annotation.Transactional;
import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.maxFromDaily;
import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.minFromDaily;
import static com.lanyuanxiaoyao.leopard.core.helper.TaHelper.sumFromDaily;
@Slf4j
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
@EnableJpaAuditing
public class StrategyApplication {
private static final TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
@Resource
private TradeEngine tradeEngine;
@Resource
private StockRepository stockRepository;
@Resource
private DailyRepository dailyRepository;
@Resource
private StockCollectionRepository stockCollectionRepository;
public static void main(String[] args) {
SpringApplication.run(StrategyApplication.class, args);
}
@Transactional(rollbackOn = Throwable.class)
@Transactional(readOnly = true)
@EventListener(ApplicationReadyEvent.class)
public void test() {
var dailies = dailyRepository.findAll(QDaily.daily.stock.code.eq("000001.SZ"), Sort.by(Daily_.TRADE_DATE));
var series = new BaseBarSeriesBuilder()
.withNumTypeOf(DoubleNum.class)
.build();
log.info("{}", dailies.size());
for (Daily daily : dailies) {
series.addBar(new BaseBar(
Duration.ofDays(1),
daily.getTradeDate().plusDays(1).atStartOfDay(ZoneId.systemDefault()),
DoubleNum.valueOf(daily.getOpen() * daily.getFactor()),
DoubleNum.valueOf(daily.getHigh() * daily.getFactor()),
DoubleNum.valueOf(daily.getLow() * daily.getFactor()),
DoubleNum.valueOf(daily.getClose() * daily.getFactor()),
DoubleNum.valueOf(daily.getVolume()),
DoubleNum.valueOf(daily.getPriceChangeAmount()),
daily.getTurnover().longValue()
));
}
var ema = new EMAIndicator(new ClosePriceIndicator(series), 5);
var emaValues = ema.stream().toList();
log.info("{}", emaValues.size());
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()
);
for (int index = 0; index < dailies.size(); index++) {
var daily = dailies.get(index);
var emaValue = emaValues.get(index);
log.info("{} {} {} {} {}", daily.getTradeDate().toString(), daily.getClose(), daily.getFactor(), daily.getClose() * daily.getFactor(), emaValue.doubleValue());
}
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)
));
}
}

View File

@@ -2,9 +2,16 @@ spring:
application:
name: leopard-strategy
datasource:
url: jdbc:postgresql://192.168.31.127:6785/leopard
url: jdbc:postgresql://81.71.3.24:6785/leopard
username: leopard
password: '9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X'
driver-class-name: org.postgresql.Driver
jpa:
generate-ddl: false
main:
banner-mode: off
fenix:
print-banner: false
liteflow:
print-banner: false
check-node-exists: false

View File

@@ -14,10 +14,10 @@
</encoder>
</appender>
<logger name="com.zaxxer.hikari" level="ERROR"/>
<logger name="com.lanyuanxiaoyao.leopard" level="INFO"/>
<logger name="org.hibernate.SQL" level="DEBUG"/>
<root level="INFO">
<root level="ERROR">
<appender-ref ref="Console"/>
</root>
</configuration>

View File

@@ -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>

View 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>

View File

@@ -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);
}
}

1931
leopard-web/bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,26 +11,26 @@
"clean": "rimraf dist"
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@ant-design/icons": "^6.0.2",
"@ant-design/pro-components": "^2.8.10",
"@ant-design/x": "^1.6.0",
"@ant-design/x": "^1.6.1",
"@echofly/fetch-event-source": "^3.0.2",
"@fortawesome/fontawesome-free": "^6.7.2",
"@lightenna/react-mermaid-diagram": "^1.0.21",
"ahooks": "^3.9.4",
"ahooks": "^3.9.5",
"amis": "^6.13.0",
"amis-core": "^6.13.0",
"antd": "^5.27.1",
"antd": "^5.27.3",
"axios": "1.11.0",
"chart.js": "^4.5.0",
"echarts-for-react": "^3.0.2",
"licia": "^1.48.0",
"mermaid": "^11.10.1",
"es-toolkit": "^1.39.10",
"mermaid": "^11.11.0",
"react": "^18.3.1",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18.3.1",
"react-markdown": "^10.1.0",
"react-router": "^7.8.2",
"react-router": "^7.9.1",
"remark-gfm": "^4.0.1",
"styled-components": "^6.1.19",
"yocto-queue": "^1.2.1",
@@ -40,11 +40,11 @@
"@types/react": "^18.3.24",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
"globals": "^16.3.0",
"globals": "^16.4.0",
"rimraf": "^6.0.1",
"sass": "^1.91.0",
"sass": "^1.92.1",
"typescript": "~5.8.3",
"vite": "^7.1.3",
"vite": "^7.1.5",
"vite-plugin-javascript-obfuscator": "^3.1.0",
"vitest": "^3.2.4"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
import 'chart.js/auto'
import {MermaidDiagram} from '@lightenna/react-mermaid-diagram'
import EChartsReact from 'echarts-for-react'
import {trim} from 'licia'
import {Chart} from 'react-chartjs-2'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import {trim} from 'es-toolkit'
type MarkdownOptions = {
content: string
@@ -14,7 +14,7 @@ function MarkdownRender(options: MarkdownOptions) {
return (
<Markdown
remarkPlugins={[
remarkGfm
remarkGfm,
]}
children={options.content}
components={{
@@ -45,7 +45,7 @@ function MarkdownRender(options: MarkdownOptions) {
</code>
)
}
}
},
}}
/>
)

View File

@@ -1,8 +1,8 @@
import {Renderer, type RendererProps} from "amis";
import {once} from "licia";
import React from "react";
import Markdown from "../Markdown.tsx";
import {Renderer, type RendererProps} from "amis"
import React from "react"
import Markdown from "../Markdown.tsx"
import './MarkdownEnhance.scss'
import {once} from 'es-toolkit'
const MarkdownEnhance: React.FC<RendererProps> = props => {
return (

View File

@@ -6,12 +6,13 @@ import Overview from './pages/Overview.tsx'
import Root from './pages/Root.tsx'
import Test from './pages/Test.tsx'
import StockList from './pages/stock/StockList.tsx'
import StockDetail from './pages/stock/StockDetail.tsx'
import TaskList from './pages/task/TaskList.tsx'
import TaskTemplateList from './pages/task/TaskTemplateList.tsx'
import TaskTemplateSave from './pages/task/TaskTemplateSave.tsx'
import TaskScheduleList from './pages/task/TaskScheduleList.tsx'
import TaskScheduleSave from './pages/task/TaskScheduleSave.tsx'
import StockCollectionList from './pages/stock/StockCollectionList.tsx'
import TaskDetail from './pages/task/TaskDetail.tsx'
import StockCollectionDetail from './pages/stock/StockCollectionDetail.tsx'
const routes: RouteObject[] = [
{
@@ -29,17 +30,22 @@ const routes: RouteObject[] = [
{
path: 'stock',
children: [
{
index: true,
element: <Navigate to="/stock/list" replace/>,
},
{
path: 'list',
Component: StockList,
},
{
path: 'detail/:id',
Component: StockDetail,
path: "collection",
children: [
{
path: 'list',
Component: StockCollectionList,
},
{
path: 'detail/:id',
Component: StockCollectionDetail,
},
],
},
],
},
@@ -50,6 +56,10 @@ const routes: RouteObject[] = [
path: 'list',
Component: TaskList,
},
{
path: 'detail/:id',
Component: TaskDetail,
},
{
path: 'template',
children: [
@@ -57,10 +67,6 @@ const routes: RouteObject[] = [
path: 'list',
Component: TaskTemplateList,
},
{
path: 'save/:id',
Component: TaskTemplateSave,
},
],
},
{

View File

@@ -1,12 +1,14 @@
import {
ClockCircleOutlined,
DeploymentUnitOutlined,
FileOutlined,
InfoCircleOutlined,
MoneyCollectOutlined,
StarOutlined,
UnorderedListOutlined,
} from '@ant-design/icons'
import {type AppItemProps, ProLayout} from '@ant-design/pro-components'
import {ConfigProvider} from 'antd'
import {dateFormat} from 'licia'
import React, {useMemo} from 'react'
import {NavLink, Outlet, useLocation} from 'react-router'
import styled from 'styled-components'
@@ -39,23 +41,37 @@ const menus = {
{
path: '/stock',
name: '股票',
icon: <MoneyCollectOutlined/>,
}, {
routes: [
{
path: '/stock/list',
name: '股票列表',
icon: <MoneyCollectOutlined/>,
},
{
path: "/stock/collection/list",
name: '股票集',
icon: <StarOutlined/>,
},
],
},
{
path: '/task',
name: '任务',
icon: <UnorderedListOutlined/>,
routes: [
{
path: '/task/list',
name: '任务列表',
icon: <UnorderedListOutlined/>,
},
{
path: '/task/template/list',
name: '任务模板',
icon: <FileOutlined/>,
},
{
path: '/task/schedule/list',
name: '定时任务',
icon: <ClockCircleOutlined/>,
},
],
},
@@ -71,7 +87,7 @@ const menus = {
const Root: React.FC = () => {
const location = useLocation()
const currentYear = useMemo(() => dateFormat(new Date(), 'yyyy'), [])
const currentYear = useMemo(() => new Date().getFullYear(), [])
return (
<ProLayoutDiv>
<ProLayout
@@ -105,7 +121,7 @@ const Root: React.FC = () => {
title="金钱豹"
route={menus}
location={{pathname: location.pathname}}
menu={{type: 'sub'}}
menu={{type: 'group'}}
menuItemRender={(item, defaultDom) =>
<NavLink to={item.path || '/'}>{defaultDom}</NavLink>
}

View 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)

View File

@@ -0,0 +1,104 @@
import React from "react"
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, time} from '../../util/amis.tsx'
import {useNavigate} from 'react-router'
function StockCollectionList() {
const navigate = useNavigate()
return (
<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>
)
}
export default React.memo(StockCollectionList)

View File

@@ -1,39 +0,0 @@
import React from 'react'
import {useParams} from 'react-router'
import {amisRender, commonInfo, remoteMappings} from '../../util/amis.tsx'
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}'},
],
},
{type: 'divider'},
],
},
)}
</div>
)
}
export default React.memo(StockDetail)

View File

@@ -3,15 +3,12 @@ import {
amisRender,
commonInfo,
crudCommonOptions,
date,
paginationTemplate,
remoteMappings,
remoteOptions,
stockListColumns,
} from '../../util/amis.tsx'
import {useNavigate} from 'react-router'
function StockList() {
const navigate = useNavigate()
return (
<div className="stock-list">
{amisRender(
@@ -97,65 +94,7 @@ function StockList() {
},
],
},
columns: [
{
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: 150,
},
{
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']}`)
},
},
],
},
},
},
],
},
],
columns: stockListColumns(),
},
],
},

View File

@@ -0,0 +1,73 @@
import React from 'react'
import {useParams} from 'react-router'
import {amisRender, commonInfo, remoteMappings, time} from '../../util/amis.tsx'
function TaskDetail() {
const {id} = useParams()
return (
<div className="task-detail">
{amisRender(
{
type: 'page',
title: '任务详情',
initApi: `get:${commonInfo.baseUrl}/task/detail/${id}`,
body: [
{
type: 'property',
items: [
{label: '名称', content: '${name}'},
{label: '描述', content: '${description}', span: 2},
{
label: '状态',
content: {
value: '${status}',
...remoteMappings('task_status', 'status'),
},
},
{
label: '进度',
content: {
type: 'tpl',
tpl: "${ROUND(step * 100, 2)}%",
},
span: 2,
},
{
label: '耗时',
content: {
type: 'tpl',
tpl: "${IF(costText, costText, '/')}",
},
},
{label: '启动时间', content: time('launchedTime')},
{label: '结束时间', content: time('finishedTime')},
],
},
{type: 'divider'},
{
type: 'form',
wrapWithPanel: false,
body: [
{
visibleOn: 'error',
type: 'editor',
name: 'error',
label: '错误信息',
},
{
visibleOn: 'result',
type: 'markdown-enhance',
name: 'result',
label: '结果',
content: '${result}',
},
],
},
],
},
)}
</div>
)
}
export default React.memo(TaskDetail)

View File

@@ -1,5 +1,14 @@
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'
function TaskList() {
@@ -23,8 +32,43 @@ function TaskList() {
},
},
},
interval: 30000,
...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: [
{
name: 'name',
@@ -38,9 +82,16 @@ function TaskList() {
{
name: 'status',
label: '状态',
align: 'center',
width: 100,
...remoteMappings('task_status', 'status'),
},
{
label: '进度',
type: 'progress',
width: 200,
value: '${ROUND(step * 100, 0)}',
},
{
label: '耗时',
type: 'tpl',
@@ -85,6 +136,16 @@ function TaskList() {
},
},
},
{
className: 'text-danger btn-deleted',
type: 'action',
label: '删除',
level: 'link',
actionType: 'ajax',
api: `get:${commonInfo.baseUrl}/task/remove/\${id}`,
confirmText: '确认删除任务记录<span class="text-lg font-bold mx-2">${name}</span>',
confirmTitle: '删除',
},
],
},
],

View File

@@ -103,7 +103,7 @@ function TaskScheduleList() {
confirmTitle: '恢复',
},
{
className: 'text-danger',
className: 'text-danger btn-deleted',
type: 'action',
label: '删除',
level: 'link',

View File

@@ -1,9 +1,7 @@
import React from 'react'
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate} from '../../util/amis.tsx'
import {useNavigate} from 'react-router'
function TaskTemplateList() {
const navigate = useNavigate()
return (
<div className="task-template-list">
{amisRender(
@@ -13,43 +11,10 @@ function TaskTemplateList() {
body: [
{
type: 'crud',
api: {
method: 'post',
url: `${commonInfo.baseUrl}/task_template/list`,
data: {
page: {
index: '${page}',
size: '${perPage}',
},
},
},
api: `get:${commonInfo.baseUrl}/task/template/list`,
...crudCommonOptions(),
...paginationTemplate(
15,
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')
},
},
],
},
},
},
],
),
...paginationTemplate(15),
loadOnce: true,
columns: [
{
name: 'name',
@@ -63,7 +28,7 @@ function TaskTemplateList() {
{
type: 'operation',
label: '操作',
width: 150,
width: 100,
buttons: [
{
type: 'action',
@@ -78,35 +43,7 @@ function TaskTemplateList() {
},
},
confirmText: '确认执行模板<span class="text-lg font-bold mx-2">${name}</span>',
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',
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: '删除',
confirmTitle: '执行',
},
],
},

View File

@@ -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: 'chain',
label: '流程编号',
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)

View File

@@ -1,11 +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/helper.css'
import 'amis/sdk/iconfont.css'
import '@fortawesome/fontawesome-free/css/all.min.css'
import axios from 'axios'
import {isEqual} from 'licia'
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 = {
debug: isEqual(import.meta.env.MODE, 'development'),
@@ -335,3 +337,672 @@ export function remoteMappings(name: string, field: string) {
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}}`,
),
],
},
},
],
},
]
}

920
note.md Normal file
View File

@@ -0,0 +1,920 @@
利润表
| 名称 | 描述 |
|---------------------------|--------------------|
| basic_eps | 基本每股收益 |
| diluted_eps | 稀释每股收益 |
| total_revenue | 营业总收入 |
| revenue | 营业收入 |
| int_income | 利息收入 |
| prem_earned | 已赚保费 |
| comm_income | 手续费及佣金收入 |
| n_commis_income | 手续费及佣金净收入 |
| n_oth_income | 其他经营净收益 |
| n_oth_b_income | 加:其他业务净收益 |
| prem_income | 保险业务收入 |
| out_prem | 减:分出保费 |
| une_prem_reser | 提取未到期责任准备金 |
| reins_income | 其中:分保费收入 |
| n_sec_tb_income | 代理买卖证券业务净收入 |
| n_sec_uw_income | 证券承销业务净收入 |
| n_asset_mg_income | 受托客户资产管理业务净收入 |
| oth_b_income | 其他业务收入 |
| fv_value_chg_gain | 加:公允价值变动净收益 |
| invest_income | 加:投资净收益 |
| ass_invest_income | 其中:对联营企业和合营企业的投资收益 |
| forex_gain | 加:汇兑净收益 |
| total_cogs | 营业总成本 |
| oper_cost | 减:营业成本 |
| int_exp | 减:利息支出 |
| comm_exp | 减:手续费及佣金支出 |
| biz_tax_surchg | 减:营业税金及附加 |
| sell_exp | 减:销售费用 |
| admin_exp | 减:管理费用 |
| fin_exp | 减:财务费用 |
| assets_impair_loss | 减:资产减值损失 |
| prem_refund | 退保金 |
| compens_payout | 赔付总支出 |
| reser_insur_liab | 提取保险责任准备金 |
| div_payt | 保户红利支出 |
| reins_exp | 分保费用 |
| oper_exp | 营业支出 |
| compens_payout_refu | 减:摊回赔付支出 |
| insur_reser_refu | 减:摊回保险责任准备金 |
| reins_cost_refund | 减:摊回分保费用 |
| other_bus_cost | 其他业务成本 |
| operate_profit | 营业利润 |
| non_oper_income | 加:营业外收入 |
| non_oper_exp | 减:营业外支出 |
| nca_disploss | 其中:减:非流动资产处置净损失 |
| total_profit | 利润总额 |
| income_tax | 所得税费用 |
| n_income | 净利润(含少数股东损益) |
| n_income_attr_p | 净利润(不含少数股东损益) |
| minority_gain | 少数股东损益 |
| oth_compr_income | 其他综合收益 |
| t_compr_income | 综合收益总额 |
| compr_inc_attr_p | 归属于母公司(或股东)的综合收益总额 |
| compr_inc_attr_m_s | 归属于少数股东的综合收益总额 |
| ebit | 息税前利润 |
| ebida | 息税折旧摊销前利润 |
| insurance_exp | 保险业务支出 |
| undist_profit | 年初未分配利润 |
| distable_profit | 可分配利润 |
| rd_exp | 研发费用 |
| fin_exp_int_exp | 财务费用-利息费用 |
| fin_exp_int_inc | 财务费用-利息收入 |
| transfer_surplus_rese | 盈余公积转入 |
| transfer_housing_imprest | 住房周转金转入 |
| transfer_oth | 其他转入 |
| adj_lossgain | 调整以前年度损益 |
| withdra_legal_surplus | 提取法定盈余公积 |
| withdra_legal_pubfund | 提取法定公益金 |
| withdra_biz_devfund | 提取企业发展基金 |
| withdra_rese_fund | 提取储备基金 |
| withdra_oth_ersu | 提取任意盈余公积金 |
| workers_welfare | 职工奖福金 |
| distr_profit_shrder | 可供股东分配的利润 |
| prfshare_payable_dvd | 应付优先股股利 |
| comshare_payable_dvd | 应付普通股股利 |
| capit_comstock_div | 转作股本的普通股股利 |
| net_after_nr_p_correct | 扣除非经常性损益后的净利润(更正前) |
| credit_impa_loss | 信用减值损失 |
| net_expo_hedging_benefits | 净敞口套期收益 |
| oth_impair_loss_assets | 其他资产减值损失 |
| total_opcost | 营业总成本(二) |
| amodcost_fin_assets | 以摊余成本计量的金融资产终止确认收益 |
| oth_income | 其他收益 |
| asset_disp_income | 资产处置收益 |
| continued_net_profit | 持续经营净利润 |
| end_net_profit | 终止经营净利润 |
现金流量表
| 名称 | 描述 |
|----------------------------|---------------------------|
| net_profit | 净利润 |
| fin_exp | 财务费用 |
| c_f_sale_sg | 销售商品、提供劳务收到的现金 |
| recp_tax_refds | 收到的税费返还 |
| n_depos_incr_f | 客户存款和同业存放款项净增加额 |
| n_inc_loans_oth_bank | 拆入资金净增加额 |
| c_cap_incr_repur | 回购业务资金净增加额 |
| c_f_oth_oper_a | 经营活动现金流入小计 |
| c_paid_to_for_empl | 支付给职工以及为职工支付的现金 |
| c_paid_for_taxes | 支付的各项税费 |
| n_inc_ct_loan_adv | 存放中央银行和同业款项净增加额 |
| n_inc_dep_cbob | 客户贷款及垫款净增加额 |
| c_pay_dams_ond_inco | 支付原保险合同赔付款项的现金 |
| pay_handling_chg | 支付手续费及佣金的现金 |
| oth_cashout_oper_act | 支付其他与经营活动有关的现金 |
| st_cashflow_act | 经营活动产生的现金流量净额 |
| n_inc_recp_lal_inv_act | 收回投资收到的现金 |
| c_disp_without_inv_act | 处置固定资产、无形资产和其他长期资产收回的现金净额 |
| n_recp_return_invest | 取得投资收益收到的现金 |
| n_disp_dep_folla | 处置子公司及其他营业单位收到的现金净额 |
| stoc_inflows_inv_act | 投资活动现金流入小计 |
| c_paid_subs_oth_biz | 购置固定资产、无形资产和其他长期资产支付的现金 |
| c_paid_acq_cons_iota | 投资支付的现金 |
| n_inc_disp_bedge_act | 取得子公司及其他营业单位支付的现金净额 |
| stoc_cashout_inv_act | 投资活动现金流出小计 |
| c_recp_bonds | 质押贷款净增加额 |
| proc_issue_in_inc_act | 发行债券收到的现金 |
| incl_dvd_profit_paid_sc_ms | 其中:子公司支付少数股东的股利、利润 |
| oth_cashflow_fin_act | 支付其他与筹资活动有关的现金 |
| stoc_cashout_fin_act | 筹资活动现金流出小计 |
| eff_fx_flows_cash_equ | 汇率变动对现金及现金等价物的影响 |
| c_cash_equ_beg_period | 期初现金及现金等价物余额 |
| c_cash_equ_end_period | 期末现金及现金等价物余额 |
| incl_rec_inv_assets | 其中:子公司吸收少数股东投资收到的现金 |
| unconv_inv_assets | 加:资产减值准备 |
| prov_fa_coga_dpba | 固定资产折旧、油气资产折耗、生产性生物资产折旧 |
| amort_intang_exp | 无形资产摊销 |
| lt_amort_defer_exp | 长期待摊费用摊销 |
| defer_defere_exp | 处置固定资产、无形资产和其他长期资产的损失 |
| inc_acq_expo | 公允价值变动损失 |
| loss_disp_fa | 投资损失 |
| loss_sc | 递延所得税资产减少 |
| invest_inc_tax_assets | 递延所得税负债增加 |
| der_inc_tv | 存货的减少 |
| der_del_inventories | 经营性应收项目的减少 |
| inc_payable | 经营性应付项目的增加 |
| others | 其他 |
| im_net_cashflow_oper_act | 经营活动产生的现金流量净额(间接法) |
| conv_debt_into_cap | 债务转为资本 |
| conv_coponds_due_within1y | 一年内到期的可转换公司债券 |
| fa_in_lease | 融资租入固定资产 |
| im_n_inc_cash_equ | 现金及现金等价物净增加额(间接法) |
| net_dism_cash_equ | 加:期初现金及现金等价物余额 |
| credit_impa_loss | 信用减值损失 |
| use_light_asset_dep | 使用固定资产的折旧 |
| oth_end_asset | 其他资产减值准备 |
| beg_bal_cash | 减:期初现金及现金等价物余额 |
| end_bal_cash_equ | 加:期末现金及现金等价物余额 |
资产负债表
| 名称 | 描述 |
|----------------------------|--------------------------|
| total_share | 期末总股本 |
| cap_rese | 资本公积金 |
| undist_profit | 未分配利润 |
| surplus_rese | 盈余公积金 |
| special_rese | 专项储备 |
| money_cap | 货币资金 |
| trad_asset | 交易性金融资产 |
| notes_receiv | 应收票据 |
| accounts_receiv | 应收账款 |
| oth_receiv | 其他应收款 |
| prepayment | 预付款项 |
| div_receiv | 应收股利 |
| int_receiv | 应收利息 |
| inventories | 存货 |
| amor_exp | 待摊费用 |
| nca_within_1y | 一年内到期的非流动资产 |
| sett_rsrv | 结算备付金 |
| loan_to_oth_bank_fi | 拆出资金 |
| premium_receiv | 应收保费 |
| reinsur_receiv | 应收分保账款 |
| reinsur_res_receiv | 应收分保合同准备金 |
| pur_resale_fa | 买入返售金融资产 |
| oth_cur_assets | 其他流动资产 |
| total_cur_assets | 流动资产合计 |
| fa_avail_for_sale | 可供出售金融资产 |
| htm_invest | 持有至到期投资 |
| lt_eqt_invest | 长期股权投资 |
| invest_real_estate | 投资性房地产 |
| time_deposits | 定期存款 |
| oth_assets | 其他资产 |
| lt_rec | 长期应收款 |
| fix_assets | 固定资产 |
| cip | 在建工程 |
| const_materials | 工程物资 |
| fixed_assets_disp | 固定资产清理 |
| produc_bio_assets | 生产性生物资产 |
| oil_and_gas_assets | 油气资产 |
| intan_assets | 无形资产 |
| r_and_d | 研发支出 |
| goodwill | 商誉 |
| lt_amor_exp | 长期待摊费用 |
| defer_tax_assets | 递延所得税资产 |
| decr_in_disbur | 发放贷款及垫款 |
| oth_nca | 其他非流动资产 |
| total_nca | 非流动资产合计 |
| cash_reser_cb | 现金及存放中央银行款项 |
| depos_in_oth_bfi | 存放同业和其它金融机构款项 |
| prec_metals | 贵金属 |
| deriv_assets | 衍生金融资产 |
| rr_reins_une_prem | 应收分保未到期责任准备金 |
| rr_reins_outstd_cla | 应收分保未决赔款准备金 |
| rr_reins_lins_liab | 应收分保寿险责任准备金 |
| rr_reins_lthins_liab | 应收分保长期健康险责任准备金 |
| refund_depos | 存出保证金 |
| ph_pledge_loans | 保户质押贷款 |
| refund_cap_depos | 存出资本保证金 |
| indep_acct_assets | 独立账户资产 |
| client_depos | 其中:客户资金存款 |
| client_prov | 其中:客户备付金 |
| transac_seat_fee | 其中:交易席位费 |
| invest_as_receiv | 应收款项类投资 |
| total_assets | 资产总计 |
| lt_borr | 长期借款 |
| st_borr | 短期借款 |
| cb_borr | 向中央银行借款 |
| depos_ib_deposits | 吸收存款及同业存放 |
| loan_oth_bank | 拆入资金 |
| trading_fl | 交易性金融负债 |
| notes_payable | 应付票据 |
| acct_payable | 应付账款 |
| adv_receipts | 预收款项 |
| sold_for_repur_fa | 卖出回购金融资产款 |
| comm_payable | 应付手续费及佣金 |
| payroll_payable | 应付职工薪酬 |
| taxes_payable | 应交税费 |
| int_payable | 应付利息 |
| div_payable | 应付股利 |
| oth_payable | 其他应付款 |
| acc_exp | 预提费用 |
| deferred_inc | 递延收益 |
| st_bonds_payable | 应付短期债券 |
| payable_to_reinsurer | 应付分保账款 |
| rsv_insur_cont | 保险合同准备金 |
| acting_trading_sec | 代理买卖证券款 |
| acting_uw_sec | 代理承销证券款 |
| non_cur_lab_due_1y | 一年内到期的非流动负债 |
| oth_cur_lab | 其他流动负债 |
| total_cur_lab | 流动负债合计 |
| bond_payable | 应付债券 |
| lt_payable | 长期应付款 |
| specific_payables | 专项应付款 |
| estimated_lab | 预计负债 |
| defer_tax_lab | 递延所得税负债 |
| defer_inc_non_cur_lab | 递延收益-非流动负债 |
| oth_ncl | 其他非流动负债 |
| total_ncl | 非流动负债合计 |
| depos_oth_bfi | 同业和其它金融机构存放款项 |
| deriv_lab | 衍生金融负债 |
| depos | 吸收存款 |
| agency_bus_lab | 代理业务负债 |
| oth_lab | 其他负债 |
| prem_receiv_ava | 预收保费 |
| depos_received | 存入保证金 |
| pr_invest | 保户储金及投资款 |
| reser_une_prem | 未到期责任准备金 |
| reser_outstd_claims | 未决赔款准备金 |
| reser_lins_lab | 寿险责任准备金 |
| reser_lthins_lab | 长期健康险责任准备金 |
| indep_acct_lab | 独立账户负债 |
| pledge_borr | 其中:质押借款 |
| indem_payable | 应付赔付款 |
| policy_div_payable | 应付保单红利 |
| total_lab | 负债合计 |
| treasury_share | 减:库存股 |
| ordin_risk_reser | 一般风险准备 |
| forex_differ | 外币报表折算差额 |
| invest_loss_unconf | 未确认的投资损失 |
| minority_int | 少数股东权益 |
| total_hldr_eqy_exc_min_int | 股东权益合计(不含少数股东权益) |
| total_hldr_eqy_inc_min_int | 股东权益合计(含少数股东权益) |
| total_lab_hldr_eqy | 负债及股东权益总计 |
| lt_payroll_payable | 长期应付职工薪酬 |
| oth_compr_income | 其他综合收益 |
| oth_eq_tools | 其他权益工具 |
| oth_eq_tools_p_shr | 其他权益工具(优先股) |
| lending_funs | 融出资金 |
| acc_receivable | 应收款项 |
| st_fin_payable | 应付短期融资款 |
| payables | 应付款项 |
| hfs_assets | 持有待售的资产 |
| hfs_sales | 持有待售的负债 |
| cost_fin_assets | 以摊余成本计量的金融资产 |
| fair_value_fin_assets | 以公允价值计量且其变动计入其他综合收益的金融资产 |
| cip_total | 在建工程(合计)(元) |
| oth_pay_total | 其他应付款(合计)(元) |
| long_pay_total | 长期应付款(合计)(元) |
| debt_invest | 债权投资(元) |
| oth_debt_invest | 其他债权投资(元) |
| oth_eq_invest | 其他权益工具投资(元) |
| oth_illi_fin_assets | 其他非流动金融资产(元) |
| oth_eq_pbond | 其他权益工具-永续债(元) |
| receiv_financing | 应收款项类投资 |
| use_right_assets | 使用权资产 |
| lease_lab | 租赁负债 |
| contract_assets | 合同资产 |
| contract_lab | 合同负债 |
| accounts_receiv_bill | 应收票据及应收账款 |
| accounts_pay_bill | 应付票据及应付账款 |
| oth_rcv_total | 其他应收款(合计)(元) |
| fix_assets_total | 固定资产(合计)(元) |
```java
/**
* 利润表
*/
public class Income {
@Comment("原始名称basic_eps描述基本每股收益")
private Double basicEarningsPerShare;
@Comment("原始名称diluted_eps描述稀释每股收益")
private Double dilutedEarningsPerShare;
@Comment("原始名称total_revenue描述营业总收入")
private Double totalOperatingRevenue;
@Comment("原始名称revenue描述营业收入")
private Double operatingRevenue;
@Comment("原始名称int_income描述利息收入")
private Double interestIncome;
@Comment("原始名称prem_earned描述已赚保费")
private Double earnedPremium;
@Comment("原始名称comm_income描述手续费及佣金收入")
private Double commissionIncome;
@Comment("原始名称n_commis_income描述手续费及佣金净收入")
private Double netCommissionIncome;
@Comment("原始名称n_oth_income描述其他经营净收益")
private Double netOtherOperatingIncome;
@Comment("原始名称n_oth_b_income描述加:其他业务净收益")
private Double addNetOtherBusinessIncome;
@Comment("原始名称prem_income描述保险业务收入")
private Double insuranceBusinessIncome;
@Comment("原始名称out_prem描述减:分出保费")
private Double lessCededPremium;
@Comment("原始名称une_prem_reser描述提取未到期责任准备金")
private Double unearnedPremiumReserveWithdrawn;
@Comment("原始名称reins_income描述其中:分保费收入")
private Double reinsurancePremiumIncome;
@Comment("原始名称n_sec_tb_income描述代理买卖证券业务净收入")
private Double netIncomeFromSecuritiesAgencyTrading;
@Comment("原始名称n_sec_uw_income描述证券承销业务净收入")
private Double netIncomeFromSecuritiesUnderwriting;
@Comment("原始名称n_asset_mg_income描述受托客户资产管理业务净收入")
private Double netIncomeFromClientAssetManagement;
@Comment("原始名称oth_b_income描述其他业务收入")
private Double otherBusinessIncome;
@Comment("原始名称fv_value_chg_gain描述加:公允价值变动净收益")
private Double addNetFairValueChangeGain;
@Comment("原始名称invest_income描述加:投资净收益")
private Double addNetInvestmentIncome;
@Comment("原始名称ass_invest_income描述其中:对联营企业和合营企业的投资收益")
private Double investmentIncomeFromAssociatesAndJointVentures;
@Comment("原始名称forex_gain描述加:汇兑净收益")
private Double addNetForeignExchangeGain;
@Comment("原始名称total_cogs描述营业总成本")
private Double totalOperatingCost;
@Comment("原始名称oper_cost描述减:营业成本")
private Double lessOperatingCost;
@Comment("原始名称int_exp描述减:利息支出")
private Double lessInterestExpense;
@Comment("原始名称comm_exp描述减:手续费及佣金支出")
private Double lessCommissionExpense;
@Comment("原始名称biz_tax_surchg描述减:营业税金及附加")
private Double lessBusinessTaxAndSurcharges;
@Comment("原始名称sell_exp描述减:销售费用")
private Double lessSellingExpense;
@Comment("原始名称admin_exp描述减:管理费用")
private Double lessAdministrativeExpense;
@Comment("原始名称fin_exp描述减:财务费用")
private Double lessFinancialExpense;
@Comment("原始名称assets_impair_loss描述减:资产减值损失")
private Double lessAssetImpairmentLoss;
@Comment("原始名称prem_refund描述退保金")
private Double premiumRefund;
@Comment("原始名称compens_payout描述赔付总支出")
private Double totalCompensationPayout;
@Comment("原始名称reser_insur_liab描述提取保险责任准备金")
private Double insuranceLiabilityReserveWithdrawn;
@Comment("原始名称div_payt描述保户红利支出")
private Double policyholderDividendPayout;
@Comment("原始名称reins_exp描述分保费用")
private Double reinsuranceExpense;
@Comment("原始名称oper_exp描述营业支出")
private Double operatingExpense;
@Comment("原始名称compens_payout_refu描述减:摊回赔付支出")
private Double lessRecoveredCompensationPayout;
@Comment("原始名称insur_reser_refu描述减:摊回保险责任准备金")
private Double lessRecoveredInsuranceLiabilityReserve;
@Comment("原始名称reins_cost_refund描述减:摊回分保费用")
private Double lessRecoveredReinsuranceCost;
@Comment("原始名称other_bus_cost描述其他业务成本")
private Double otherBusinessCost;
@Comment("原始名称operate_profit描述营业利润")
private Double operatingProfit;
@Comment("原始名称non_oper_income描述加:营业外收入")
private Double addNonOperatingIncome;
@Comment("原始名称non_oper_exp描述减:营业外支出")
private Double lessNonOperatingExpense;
@Comment("原始名称nca_disploss描述其中:减:非流动资产处置净损失")
private Double lessNetLossOnDisposalOfNonCurrentAssets;
@Comment("原始名称total_profit描述利润总额")
private Double totalProfit;
@Comment("原始名称income_tax描述所得税费用")
private Double incomeTaxExpense;
@Comment("原始名称n_income描述净利润(含少数股东损益)")
private Double netProfitIncludingMinorityInterest;
@Comment("原始名称n_income_attr_p描述净利润(不含少数股东损益)")
private Double netProfitExcludingMinorityInterest;
@Comment("原始名称minority_gain描述少数股东损益")
private Double minorityInterestGain;
@Comment("原始名称oth_compr_income描述其他综合收益")
private Double otherComprehensiveIncome;
@Comment("原始名称t_compr_income描述综合收益总额")
private Double totalComprehensiveIncome;
@Comment("原始名称compr_inc_attr_p描述归属于母公司(或股东)的综合收益总额")
private Double comprehensiveIncomeAttributableToParent;
@Comment("原始名称compr_inc_attr_m_s描述归属于少数股东的综合收益总额")
private Double comprehensiveIncomeAttributableToMinorityShareholders;
@Comment("原始名称ebit描述息税前利润")
private Double earningsBeforeInterestAndTax;
@Comment("原始名称ebida描述息税折旧摊销前利润")
private Double earningsBeforeInterestTaxDepreciationAndAmortization;
@Comment("原始名称insurance_exp描述保险业务支出")
private Double insuranceBusinessExpense;
@Comment("原始名称undist_profit描述年初未分配利润")
private Double beginningUndistributedProfit;
@Comment("原始名称distable_profit描述可分配利润")
private Double distributableProfit;
@Comment("原始名称rd_exp描述研发费用")
private Double researchAndDevelopmentExpense;
@Comment("原始名称fin_exp_int_exp描述财务费用-利息费用")
private Double financialExpenseInterestExpense;
@Comment("原始名称fin_exp_int_inc描述财务费用-利息收入")
private Double financialExpenseInterestIncome;
@Comment("原始名称transfer_surplus_rese描述盈余公积转入")
private Double transferFromSurplusReserve;
@Comment("原始名称transfer_housing_imprest描述住房周转金转入")
private Double transferFromHousingRevolvingFund;
@Comment("原始名称transfer_oth描述其他转入")
private Double otherTransfersIn;
@Comment("原始名称adj_lossgain描述调整以前年度损益")
private Double priorYearProfitOrLossAdjustment;
@Comment("原始名称withdra_legal_surplus描述提取法定盈余公积")
private Double legalSurplusReserveWithdrawn;
@Comment("原始名称withdra_legal_pubfund描述提取法定公益金")
private Double legalPublicWelfareFundWithdrawn;
@Comment("原始名称withdra_biz_devfund描述提取企业发展基金")
private Double enterpriseDevelopmentFundWithdrawn;
@Comment("原始名称withdra_rese_fund描述提取储备基金")
private Double reserveFundWithdrawn;
@Comment("原始名称withdra_oth_ersu描述提取任意盈余公积金")
private Double discretionarySurplusReserveWithdrawn;
@Comment("原始名称workers_welfare描述职工奖福金")
private Double staffBonusAndWelfareFund;
@Comment("原始名称distr_profit_shrder描述可供股东分配的利润")
private Double profitAvailableForShareholdersDistribution;
@Comment("原始名称prfshare_payable_dvd描述应付优先股股利")
private Double preferredStockDividendPayable;
@Comment("原始名称comshare_payable_dvd描述应付普通股股利")
private Double commonStockDividendPayable;
@Comment("原始名称capit_comstock_div描述转作股本的普通股股利")
private Double commonStockDividendConvertedToCapital;
@Comment("原始名称net_after_nr_p_correct描述扣除非经常性损益后的净利润更正前")
private Double netProfitAfterNonRecurringItemsBeforeCorrection;
@Comment("原始名称credit_impa_loss描述信用减值损失")
private Double creditImpairmentLoss;
@Comment("原始名称net_expo_hedging_benefits描述净敞口套期收益")
private Double netExposureHedgingGains;
@Comment("原始名称oth_impair_loss_assets描述其他资产减值损失")
private Double otherAssetImpairmentLosses;
@Comment("原始名称total_opcost描述营业总成本")
private Double totalOperatingCost2;
@Comment("原始名称amodcost_fin_assets描述以摊余成本计量的金融资产终止确认收益")
private Double gainOnDerecognitionOfAmortizedCostFinancialAssets;
@Comment("原始名称oth_income描述其他收益")
private Double otherGains;
@Comment("原始名称asset_disp_income描述资产处置收益")
private Double assetDisposalGains;
@Comment("原始名称continued_net_profit描述持续经营净利润")
private Double netProfitFromContinuingOperations;
@Comment("原始名称end_net_profit描述终止经营净利润")
private Double netProfitFromDiscontinuedOperations;
}
```
```java
/**
* 现金流量表实体类
*/
public class CashFlow {
@Comment("原始名称net_profit描述净利润")
private Double netProfit;
@Comment("原始名称fin_exp描述财务费用")
private Double financialExpense;
@Comment("原始名称c_f_sale_sg描述销售商品、提供劳务收到的现金")
private Double cashReceivedFromSalesAndServices;
@Comment("原始名称recp_tax_refds描述收到的税费返还")
private Double taxRefundsReceived;
@Comment("原始名称n_depos_incr_f描述客户存款和同业存放款项净增加额")
private Double netIncreaseInCustomerDepositsAndInterbankPlacements;
@Comment("原始名称n_inc_loans_oth_bank描述拆入资金净增加额")
private Double netIncreaseInInterbankBorrowings;
@Comment("原始名称c_cap_incr_repur描述回购业务资金净增加额")
private Double netIncreaseInRepurchaseAgreementFunds;
@Comment("原始名称c_f_oth_oper_a描述经营活动现金流入小计")
private Double subtotalOfCashInflowsFromOperatingActivities;
@Comment("原始名称c_paid_to_for_empl描述支付给职工以及为职工支付的现金")
private Double cashPaidToAndForEmployees;
@Comment("原始名称c_paid_for_taxes描述支付的各项税费")
private Double cashPaidForVariousTaxes;
@Comment("原始名称n_inc_ct_loan_adv描述存放中央银行和同业款项净增加额")
private Double netIncreaseInDepositsWithCentralBankAndInterbank;
@Comment("原始名称n_inc_dep_cbob描述客户贷款及垫款净增加额")
private Double netIncreaseInCustomerLoansAndAdvances;
@Comment("原始名称c_pay_dams_ond_inco描述支付原保险合同赔付款项的现金")
private Double cashPaidForOriginalInsuranceClaims;
@Comment("原始名称pay_handling_chg描述支付手续费及佣金的现金")
private Double cashPaidForHandlingFeesAndCommissions;
@Comment("原始名称oth_cashout_oper_act描述支付其他与经营活动有关的现金")
private Double otherCashPaymentsRelatedToOperatingActivities;
@Comment("原始名称st_cashflow_act描述经营活动产生的现金流量净额")
private Double netCashFlowFromOperatingActivities;
@Comment("原始名称n_inc_recp_lal_inv_act描述收回投资收到的现金")
private Double cashReceivedFromInvestmentRecovery;
@Comment("原始名称c_disp_without_inv_act描述处置固定资产、无形资产和其他长期资产收回的现金净额")
private Double netCashReceivedFromDisposalOfFixedIntangibleAndOtherLongTermAssets;
@Comment("原始名称n_recp_return_invest描述取得投资收益收到的现金")
private Double cashReceivedFromInvestmentIncome;
@Comment("原始名称n_disp_dep_folla描述处置子公司及其他营业单位收到的现金净额")
private Double netCashReceivedFromDisposalOfSubsidiariesAndOtherBusinessUnits;
@Comment("原始名称stoc_inflows_inv_act描述投资活动现金流入小计")
private Double subtotalOfCashInflowsFromInvestingActivities;
@Comment("原始名称c_paid_subs_oth_biz描述购置固定资产、无形资产和其他长期资产支付的现金")
private Double cashPaidForAcquisitionOfFixedIntangibleAndOtherLongTermAssets;
@Comment("原始名称c_paid_acq_cons_iota描述投资支付的现金")
private Double cashPaidForInvestments;
@Comment("原始名称n_inc_disp_bedge_act描述取得子公司及其他营业单位支付的现金净额")
private Double netCashPaidForAcquisitionOfSubsidiariesAndOtherBusinessUnits;
@Comment("原始名称stoc_cashout_inv_act描述投资活动现金流出小计")
private Double subtotalOfCashOutflowsFromInvestingActivities;
@Comment("原始名称c_recp_bonds描述质押贷款净增加额")
private Double netIncreaseInPledgedLoans;
@Comment("原始名称proc_issue_in_inc_act描述发行债券收到的现金")
private Double cashReceivedFromBondIssuance;
@Comment("原始名称incl_dvd_profit_paid_sc_ms描述其中:子公司支付少数股东的股利、利润")
private Double dividendsAndProfitsPaidToMinorityShareholdersBySubsidiaries;
@Comment("原始名称oth_cashflow_fin_act描述支付其他与筹资活动有关的现金")
private Double otherCashPaymentsRelatedToFinancingActivities;
@Comment("原始名称stoc_cashout_fin_act描述筹资活动现金流出小计")
private Double subtotalOfCashOutflowsFromFinancingActivities;
@Comment("原始名称eff_fx_flows_cash_equ描述汇率变动对现金及现金等价物的影响")
private Double effectOfExchangeRateChangesOnCashAndCashEquivalents;
@Comment("原始名称c_cash_equ_beg_period描述期初现金及现金等价物余额")
private Double beginningBalanceOfCashAndCashEquivalents;
@Comment("原始名称c_cash_equ_end_period描述期末现金及现金等价物余额")
private Double endingBalanceOfCashAndCashEquivalents;
@Comment("原始名称incl_rec_inv_assets描述其中:子公司吸收少数股东投资收到的现金")
private Double cashReceivedBySubsidiariesFromMinorityShareholdersInvestment;
@Comment("原始名称unconv_inv_assets描述加:资产减值准备")
private Double addAssetImpairmentProvisions;
@Comment("原始名称prov_fa_coga_dpba描述固定资产折旧、油气资产折耗、生产性生物资产折旧")
private Double depreciationOfFixedAssetsDepletionOfOilAndGasAssetsDepreciationOfProductiveBiologicalAssets;
@Comment("原始名称amort_intang_exp描述无形资产摊销")
private Double amortizationOfIntangibleAssets;
@Comment("原始名称lt_amort_defer_exp描述长期待摊费用摊销")
private Double amortizationOfLongTermDeferredExpenses;
@Comment("原始名称defer_defere_exp描述处置固定资产、无形资产和其他长期资产的损失")
private Double lossOnDisposalOfFixedIntangibleAndOtherLongTermAssets;
@Comment("原始名称inc_acq_expo描述公允价值变动损失")
private Double fairValueChangeLoss;
@Comment("原始名称loss_disp_fa描述投资损失")
private Double investmentLoss;
@Comment("原始名称loss_sc描述递延所得税资产减少")
private Double decreaseInDeferredTaxAssets;
@Comment("原始名称invest_inc_tax_assets描述递延所得税负债增加")
private Double increaseInDeferredTaxLiabilities;
@Comment("原始名称der_inc_tv描述存货的减少")
private Double decreaseInInventories;
@Comment("原始名称der_del_inventories描述经营性应收项目的减少")
private Double decreaseInOperatingReceivables;
@Comment("原始名称inc_payable描述经营性应付项目的增加")
private Double increaseInOperatingPayables;
@Comment("原始名称others描述其他")
private Double others;
@Comment("原始名称im_net_cashflow_oper_act描述经营活动产生的现金流量净额(间接法)")
private Double netCashFlowFromOperatingActivitiesIndirectMethod;
@Comment("原始名称conv_debt_into_cap描述债务转为资本")
private Double conversionOfDebtToCapital;
@Comment("原始名称conv_coponds_due_within1y描述一年内到期的可转换公司债券")
private Double convertibleBondsDueWithinOneYear;
@Comment("原始名称fa_in_lease描述融资租入固定资产")
private Double fixedAssetsAcquiredUnderFinanceLeases;
@Comment("原始名称im_n_inc_cash_equ描述现金及现金等价物净增加额(间接法)")
private Double netIncreaseInCashAndCashEquivalentsIndirectMethod;
@Comment("原始名称net_dism_cash_equ描述加:期初现金及现金等价物余额")
private Double addBeginningBalanceOfCashAndCashEquivalents;
@Comment("原始名称credit_impa_loss描述信用减值损失")
private Double creditImpairmentLoss;
@Comment("原始名称use_light_asset_dep描述使用固定资产的折旧")
private Double depreciationOfUsedFixedAssets;
@Comment("原始名称oth_end_asset描述其他资产减值准备")
private Double otherAssetImpairmentProvisions;
@Comment("原始名称beg_bal_cash描述减:期初现金及现金等价物余额")
private Double lessBeginningBalanceOfCashAndCashEquivalents;
@Comment("原始名称end_bal_cash_equ描述加:期末现金及现金等价物余额")
private Double addEndingBalanceOfCashAndCashEquivalents;
}
```
```java
/**
* 资产负债表实体类
*/
public class BalanceSheet {
@Comment("原始名称total_share描述期末总股本")
private Double endingTotalShares;
@Comment("原始名称cap_rese描述资本公积金")
private Double capitalSurplus;
@Comment("原始名称undist_profit描述未分配利润")
private Double undistributedProfit;
@Comment("原始名称surplus_rese描述盈余公积金")
private Double surplusReserve;
@Comment("原始名称special_rese描述专项储备")
private Double specialReserve;
@Comment("原始名称money_cap描述货币资金")
private Double monetaryFunds;
@Comment("原始名称trad_asset描述交易性金融资产")
private Double tradingFinancialAssets;
@Comment("原始名称notes_receiv描述应收票据")
private Double notesReceivable;
@Comment("原始名称accounts_receiv描述应收账款")
private Double accountsReceivable;
@Comment("原始名称oth_receiv描述其他应收款")
private Double otherReceivables;
@Comment("原始名称prepayment描述预付款项")
private Double prepayments;
@Comment("原始名称div_receiv描述应收股利")
private Double dividendsReceivable;
@Comment("原始名称int_receiv描述应收利息")
private Double interestReceivable;
@Comment("原始名称inventories描述存货")
private Double inventories;
@Comment("原始名称amor_exp描述待摊费用")
private Double deferredExpenses;
@Comment("原始名称nca_within_1y描述一年内到期的非流动资产")
private Double nonCurrentAssetsDueWithinOneYear;
@Comment("原始名称sett_rsrv描述结算备付金")
private Double settlementReserves;
@Comment("原始名称loan_to_oth_bank_fi描述拆出资金")
private Double fundsLentToOtherBanksAndFinancialInstitutions;
@Comment("原始名称premium_receiv描述应收保费")
private Double premiumsReceivable;
@Comment("原始名称reinsur_receiv描述应收分保账款")
private Double reinsuranceReceivables;
@Comment("原始名称reinsur_res_receiv描述应收分保合同准备金")
private Double reinsuranceContractReservesReceivable;
@Comment("原始名称pur_resale_fa描述买入返售金融资产")
private Double financialAssetsPurchasedUnderResaleAgreements;
@Comment("原始名称oth_cur_assets描述其他流动资产")
private Double otherCurrentAssets;
@Comment("原始名称total_cur_assets描述流动资产合计")
private Double totalCurrentAssets;
@Comment("原始名称fa_avail_for_sale描述可供出售金融资产")
private Double availableForSaleFinancialAssets;
@Comment("原始名称htm_invest描述持有至到期投资")
private Double heldToMaturityInvestments;
@Comment("原始名称lt_eqt_invest描述长期股权投资")
private Double longTermEquityInvestments;
@Comment("原始名称invest_real_estate描述投资性房地产")
private Double investmentProperty;
@Comment("原始名称time_deposits描述定期存款")
private Double timeDeposits;
@Comment("原始名称oth_assets描述其他资产")
private Double otherAssets;
@Comment("原始名称lt_rec描述长期应收款")
private Double longTermReceivables;
@Comment("原始名称fix_assets描述固定资产")
private Double fixedAssets;
@Comment("原始名称cip描述在建工程")
private Double constructionInProgress;
@Comment("原始名称const_materials描述工程物资")
private Double constructionMaterials;
@Comment("原始名称fixed_assets_disp描述固定资产清理")
private Double fixedAssetsToBeDisposed;
@Comment("原始名称produc_bio_assets描述生产性生物资产")
private Double productiveBiologicalAssets;
@Comment("原始名称oil_and_gas_assets描述油气资产")
private Double oilAndGasAssets;
@Comment("原始名称intan_assets描述无形资产")
private Double intangibleAssets;
@Comment("原始名称r_and_d描述研发支出")
private Double researchAndDevelopmentExpenditures;
@Comment("原始名称goodwill描述商誉")
private Double goodwill;
@Comment("原始名称lt_amor_exp描述长期待摊费用")
private Double longTermDeferredExpenses;
@Comment("原始名称defer_tax_assets描述递延所得税资产")
private Double deferredTaxAssets;
@Comment("原始名称decr_in_disbur描述发放贷款及垫款")
private Double loansAndAdvancesGranted;
@Comment("原始名称oth_nca描述其他非流动资产")
private Double otherNonCurrentAssets;
@Comment("原始名称total_nca描述非流动资产合计")
private Double totalNonCurrentAssets;
@Comment("原始名称cash_reser_cb描述现金及存放中央银行款项")
private Double cashAndDepositsWithCentralBank;
@Comment("原始名称depos_in_oth_bfi描述存放同业和其它金融机构款项")
private Double depositsWithOtherBanksAndFinancialInstitutions;
@Comment("原始名称prec_metals描述贵金属")
private Double preciousMetals;
@Comment("原始名称deriv_assets描述衍生金融资产")
private Double derivativeFinancialAssets;
@Comment("原始名称rr_reins_une_prem描述应收分保未到期责任准备金")
private Double reinsuranceUnearnedPremiumReservesReceivable;
@Comment("原始名称rr_reins_outstd_cla描述应收分保未决赔款准备金")
private Double reinsuranceOutstandingClaimsReservesReceivable;
@Comment("原始名称rr_reins_lins_liab描述应收分保寿险责任准备金")
private Double reinsuranceLifeInsuranceLiabilityReservesReceivable;
@Comment("原始名称rr_reins_lthins_liab描述应收分保长期健康险责任准备金")
private Double reinsuranceLongTermHealthInsuranceLiabilityReservesReceivable;
@Comment("原始名称refund_depos描述存出保证金")
private Double depositsReceivedInAdvance;
@Comment("原始名称ph_pledge_loans描述保户质押贷款")
private Double policyholderPledgedLoans;
@Comment("原始名称refund_cap_depos描述存出资本保证金")
private Double capitalMarginDeposited;
@Comment("原始名称indep_acct_assets描述独立账户资产")
private Double independentAccountAssets;
@Comment("原始名称client_depos描述其中客户资金存款")
private Double clientFundDeposits;
@Comment("原始名称client_prov描述其中客户备付金")
private Double clientReserves;
@Comment("原始名称transac_seat_fee描述其中交易席位费")
private Double tradingSeatFees;
@Comment("原始名称invest_as_receiv描述应收款项类投资")
private Double receivableTypeInvestments;
@Comment("原始名称total_assets描述资产总计")
private Double totalAssets;
@Comment("原始名称lt_borr描述长期借款")
private Double longTermBorrowings;
@Comment("原始名称st_borr描述短期借款")
private Double shortTermBorrowings;
@Comment("原始名称cb_borr描述向中央银行借款")
private Double borrowingsFromCentralBank;
@Comment("原始名称depos_ib_deposits描述吸收存款及同业存放")
private Double depositsAbsorbedAndInterbankPlacements;
@Comment("原始名称loan_oth_bank描述拆入资金")
private Double fundsBorrowedFromOtherBanks;
@Comment("原始名称trading_fl描述交易性金融负债")
private Double tradingFinancialLiabilities;
@Comment("原始名称notes_payable描述应付票据")
private Double notesPayable;
@Comment("原始名称acct_payable描述应付账款")
private Double accountsPayable;
@Comment("原始名称adv_receipts描述预收款项")
private Double advancesReceived;
@Comment("原始名称sold_for_repur_fa描述卖出回购金融资产款")
private Double proceedsFromFinancialAssetsSoldUnderRepurchaseAgreements;
@Comment("原始名称comm_payable描述应付手续费及佣金")
private Double handlingFeesAndCommissionsPayable;
@Comment("原始名称payroll_payable描述应付职工薪酬")
private Double employeeBenefitsPayable;
@Comment("原始名称taxes_payable描述应交税费")
private Double taxesPayable;
@Comment("原始名称int_payable描述应付利息")
private Double interestPayable;
@Comment("原始名称div_payable描述应付股利")
private Double dividendsPayable;
@Comment("原始名称oth_payable描述其他应付款")
private Double otherPayables;
@Comment("原始名称acc_exp描述预提费用")
private Double accruedExpenses;
@Comment("原始名称deferred_inc描述递延收益")
private Double deferredRevenue;
@Comment("原始名称st_bonds_payable描述应付短期债券")
private Double shortTermBondsPayable;
@Comment("原始名称payable_to_reinsurer描述应付分保账款")
private Double reinsurancePayables;
@Comment("原始名称rsv_insur_cont描述保险合同准备金")
private Double insuranceContractReserves;
@Comment("原始名称acting_trading_sec描述代理买卖证券款")
private Double fundsForSecuritiesAgencyTrading;
@Comment("原始名称acting_uw_sec描述代理承销证券款")
private Double fundsForSecuritiesAgencyUnderwriting;
@Comment("原始名称non_cur_lab_due_1y描述一年内到期的非流动负债")
private Double nonCurrentLiabilitiesDueWithinOneYear;
@Comment("原始名称oth_cur_lab描述其他流动负债")
private Double otherCurrentLiabilities;
@Comment("原始名称total_cur_lab描述流动负债合计")
private Double totalCurrentLiabilities;
@Comment("原始名称bond_payable描述应付债券")
private Double bondsPayable;
@Comment("原始名称lt_payable描述长期应付款")
private Double longTermPayables;
@Comment("原始名称specific_payables描述专项应付款")
private Double specificPayables;
@Comment("原始名称estimated_lab描述预计负债")
private Double estimatedLiabilities;
@Comment("原始名称defer_tax_lab描述递延所得税负债")
private Double deferredTaxLiabilities;
@Comment("原始名称defer_inc_non_cur_lab描述递延收益-非流动负债")
private Double deferredRevenueNonCurrentLiabilities;
@Comment("原始名称oth_ncl描述其他非流动负债")
private Double otherNonCurrentLiabilities;
@Comment("原始名称total_ncl描述非流动负债合计")
private Double totalNonCurrentLiabilities;
@Comment("原始名称depos_oth_bfi描述同业和其它金融机构存放款项")
private Double depositsFromOtherBanksAndFinancialInstitutions;
@Comment("原始名称deriv_lab描述衍生金融负债")
private Double derivativeFinancialLiabilities;
@Comment("原始名称depos描述吸收存款")
private Double depositsAbsorbed;
@Comment("原始名称agency_bus_lab描述代理业务负债")
private Double agencyBusinessLiabilities;
@Comment("原始名称oth_lab描述其他负债")
private Double otherLiabilities;
@Comment("原始名称prem_receiv_ava描述预收保费")
private Double premiumsReceivedInAdvance;
@Comment("原始名称depos_received描述存入保证金")
private Double depositsReceived;
@Comment("原始名称pr_invest描述保户储金及投资款")
private Double policyholderDepositsAndInvestmentFunds;
@Comment("原始名称reser_une_prem描述未到期责任准备金")
private Double unearnedPremiumReserves;
@Comment("原始名称reser_outstd_claims描述未决赔款准备金")
private Double outstandingClaimsReserves;
@Comment("原始名称reser_lins_lab描述寿险责任准备金")
private Double lifeInsuranceLiabilityReserves;
@Comment("原始名称reser_lthins_lab描述长期健康险责任准备金")
private Double longTermHealthInsuranceLiabilityReserves;
@Comment("原始名称indep_acct_lab描述独立账户负债")
private Double independentAccountLiabilities;
@Comment("原始名称pledge_borr描述其中质押借款")
private Double pledgedBorrowings;
@Comment("原始名称indem_payable描述应付赔付款")
private Double indemnitiesPayable;
@Comment("原始名称policy_div_payable描述应付保单红利")
private Double policyDividendsPayable;
@Comment("原始名称total_lab描述负债合计")
private Double totalLiabilities;
@Comment("原始名称treasury_share描述库存股")
private Double lessTreasuryShares;
@Comment("原始名称ordin_risk_reser描述一般风险准备")
private Double generalRiskReserve;
@Comment("原始名称forex_differ描述外币报表折算差额")
private Double foreignCurrencyStatementTranslationDifference;
@Comment("原始名称invest_loss_unconf描述未确认的投资损失")
private Double unrecognizedInvestmentLosses;
@Comment("原始名称minority_int描述少数股东权益")
private Double minorityInterest;
@Comment("原始名称total_hldr_eqy_exc_min_int描述股东权益合计(不含少数股东权益)")
private Double totalShareholdersEquityExcludingMinorityInterest;
@Comment("原始名称total_hldr_eqy_inc_min_int描述股东权益合计(含少数股东权益)")
private Double totalShareholdersEquityIncludingMinorityInterest;
@Comment("原始名称total_lab_hldr_eqy描述负债及股东权益总计")
private Double totalLiabilitiesAndShareholdersEquity;
@Comment("原始名称lt_payroll_payable描述长期应付职工薪酬")
private Double longTermEmployeeBenefitsPayable;
@Comment("原始名称oth_compr_income描述其他综合收益")
private Double otherComprehensiveIncome;
@Comment("原始名称oth_eq_tools描述其他权益工具")
private Double otherEquityInstruments;
@Comment("原始名称oth_eq_tools_p_shr描述其他权益工具(优先股)")
private Double otherEquityInstrumentsPreferredShares;
@Comment("原始名称lending_funs描述融出资金")
private Double fundsLent;
@Comment("原始名称acc_receivable描述应收款项")
private Double receivables;
@Comment("原始名称st_fin_payable描述应付短期融资款")
private Double shortTermFinancingPayable;
@Comment("原始名称payables描述应付款项")
private Double payables;
@Comment("原始名称hfs_assets描述持有待售的资产")
private Double assetsHeldForSale;
@Comment("原始名称hfs_sales描述持有待售的负债")
private Double liabilitiesHeldForSale;
@Comment("原始名称cost_fin_assets描述以摊余成本计量的金融资产")
private Double financialAssetsMeasuredAtAmortizedCost;
@Comment("原始名称fair_value_fin_assets描述以公允价值计量且其变动计入其他综合收益的金融资产")
private Double financialAssetsMeasuredAtFairValueWithChangesInOtherComprehensiveIncome;
@Comment("原始名称cip_total描述在建工程(合计)(元)")
private Double constructionInProgressTotal;
@Comment("原始名称oth_pay_total描述其他应付款(合计)(元)")
private Double otherPayablesTotal;
@Comment("原始名称long_pay_total描述长期应付款(合计)(元)")
private Double longTermPayablesTotal;
@Comment("原始名称debt_invest描述债权投资(元)")
private Double debtInvestments;
@Comment("原始名称oth_debt_invest描述其他债权投资(元)")
private Double otherDebtInvestments;
@Comment("原始名称oth_eq_invest描述其他权益工具投资(元)")
private Double otherEquityInstrumentInvestments;
@Comment("原始名称oth_illi_fin_assets描述其他非流动金融资产(元)")
private Double otherNonCurrentFinancialAssets;
@Comment("原始名称oth_eq_pbond描述其他权益工具-永续债(元)")
private Double otherEquityInstrumentsPerpetualBonds;
@Comment("原始名称receiv_financing描述应收款项类投资")
private Double receivableTypeInvestments;
@Comment("原始名称use_right_assets描述使用权资产")
private Double rightOfUseAssets;
@Comment("原始名称lease_lab描述租赁负债")
private Double leaseLiabilities;
@Comment("原始名称contract_assets描述合同资产")
private Double contractAssets;
@Comment("原始名称contract_lab描述合同负债")
private Double contractLiabilities;
@Comment("原始名称accounts_receiv_bill描述应收票据及应收账款")
private Double notesAndAccountsReceivable;
@Comment("原始名称accounts_pay_bill描述应付票据及应付账款")
private Double notesAndAccountsPayable;
@Comment("原始名称oth_rcv_total描述其他应收款(合计)(元)")
private Double otherReceivablesTotal;
@Comment("原始名称fix_assets_total描述固定资产(合计)(元)")
private Double fixedAssetsTotal;
}
```

34
pom.xml
View File

@@ -16,14 +16,15 @@
</modules>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.5.0</spring-boot.version>
<spring-cloud.version>2025.0.0</spring-cloud.version>
<spring-ai.version>1.0.1</spring-ai.version>
<hibernate.version>6.6.15.Final</hibernate.version>
<eclipse-collections.version>13.0.0</eclipse-collections.version>
<hutool.version>5.8.39</hutool.version>
@@ -62,12 +63,39 @@
<artifactId>hutool-http</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
<version>${liteflow.version}</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-rule-sql</artifactId>
<version>${liteflow.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-ant</artifactId>
<version>${hibernate.version}</version>
</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>
<groupId>org.springframework.boot</groupId>