From b5e34b6d186a485386e4092e58db00a3da78b68e Mon Sep 17 00:00:00 2001
From: Lee <1633292@qq.com>
Date: Fri, 24 Jul 2026 12:56:20 +0800
Subject: [PATCH] Add persisted technical indicator calculation
---
.../backtestify/entity/KLineIndicator.java | 49 ++++
.../backtestify/entity/KLineRecord.java | 2 +-
.../backtestify/mapper/IndicatorMapper.java | 77 +++++
.../backtestify/mapper/KLineMapper.java | 46 ++-
.../market/MysqlMarketDataProvider.java | 2 +-
.../IndicatorCalculationService.java | 21 ++
.../market/indicator/IndicatorCalculator.java | 19 ++
.../TechnicalIndicatorCalculator.java | 275 ++++++++++++++++++
.../impl/IndicatorCalculationServiceImpl.java | 92 ++++++
.../market/repository/KLineTableResolver.java | 9 +-
.../market/sync/MarketDataSyncService.java | 5 +-
.../IndicatorCalculationServiceImplTest.java | 125 ++++++++
.../IndicatorPersistenceIntegrationTest.java | 82 ++++++
.../TechnicalIndicatorCalculatorTest.java | 159 ++++++++++
.../repository/KLineTableResolverTest.java | 10 +-
.../sync/MarketDataSyncServiceTest.java | 85 ++++++
src/test/resources/schema-test.sql | 64 +++-
17 files changed, 1095 insertions(+), 27 deletions(-)
create mode 100644 src/main/java/com/yangwale/backtestify/entity/KLineIndicator.java
create mode 100644 src/main/java/com/yangwale/backtestify/mapper/IndicatorMapper.java
create mode 100644 src/main/java/com/yangwale/backtestify/service/market/indicator/IndicatorCalculationService.java
create mode 100644 src/main/java/com/yangwale/backtestify/service/market/indicator/IndicatorCalculator.java
create mode 100644 src/main/java/com/yangwale/backtestify/service/market/indicator/TechnicalIndicatorCalculator.java
create mode 100644 src/main/java/com/yangwale/backtestify/service/market/indicator/impl/IndicatorCalculationServiceImpl.java
create mode 100644 src/test/java/com/yangwale/backtestify/service/market/indicator/IndicatorCalculationServiceImplTest.java
create mode 100644 src/test/java/com/yangwale/backtestify/service/market/indicator/IndicatorPersistenceIntegrationTest.java
create mode 100644 src/test/java/com/yangwale/backtestify/service/market/indicator/TechnicalIndicatorCalculatorTest.java
create mode 100644 src/test/java/com/yangwale/backtestify/service/market/sync/MarketDataSyncServiceTest.java
diff --git a/src/main/java/com/yangwale/backtestify/entity/KLineIndicator.java b/src/main/java/com/yangwale/backtestify/entity/KLineIndicator.java
new file mode 100644
index 0000000..041160f
--- /dev/null
+++ b/src/main/java/com/yangwale/backtestify/entity/KLineIndicator.java
@@ -0,0 +1,49 @@
+package com.yangwale.backtestify.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/**
+ * K线指标记录,映射各周期同结构的 t_indicator_* 表。
+ */
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class KLineIndicator {
+
+ private Integer instrumentId;
+ private Long kTime;
+
+ private BigDecimal ma5;
+ private BigDecimal ma10;
+ private BigDecimal ma20;
+ private BigDecimal ma60;
+
+ private BigDecimal bollMb;
+ private BigDecimal bollUp;
+ private BigDecimal bollDn;
+
+ private BigDecimal ema6;
+ private BigDecimal ema12;
+ private BigDecimal ema20;
+
+ private BigDecimal macdDif;
+ private BigDecimal macdDea;
+ private BigDecimal macdBar;
+
+ private BigDecimal rsi6;
+ private BigDecimal rsi12;
+
+ private BigDecimal kdjK;
+ private BigDecimal kdjD;
+ private BigDecimal kdjJ;
+
+ private Long createAt;
+}
diff --git a/src/main/java/com/yangwale/backtestify/entity/KLineRecord.java b/src/main/java/com/yangwale/backtestify/entity/KLineRecord.java
index 52a81e3..88212ee 100644
--- a/src/main/java/com/yangwale/backtestify/entity/KLineRecord.java
+++ b/src/main/java/com/yangwale/backtestify/entity/KLineRecord.java
@@ -15,7 +15,7 @@ public class KLineRecord {
private Integer instrumentId;
/** Unix时间戳,秒级 */
- private Long timestamp;
+ private Long kTime;
private Integer open;
diff --git a/src/main/java/com/yangwale/backtestify/mapper/IndicatorMapper.java b/src/main/java/com/yangwale/backtestify/mapper/IndicatorMapper.java
new file mode 100644
index 0000000..96383fa
--- /dev/null
+++ b/src/main/java/com/yangwale/backtestify/mapper/IndicatorMapper.java
@@ -0,0 +1,77 @@
+package com.yangwale.backtestify.mapper;
+
+import com.yangwale.backtestify.entity.KLineIndicator;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface IndicatorMapper {
+
+ @Select("""
+ SELECT instrument_id, k_time,
+ ma5, ma10, ma20, ma60,
+ boll_mb, boll_up, boll_dn,
+ ema6, ema12, ema20,
+ macd_dif, macd_dea, macd_bar,
+ rsi6, rsi12,
+ kdj_k, kdj_d, kdj_j,
+ create_at
+ FROM ${tableName}
+ WHERE instrument_id = #{instrumentId}
+ ORDER BY k_time DESC
+ LIMIT 1
+ """)
+ KLineIndicator selectLatest(@Param("tableName") String tableName,
+ @Param("instrumentId") Integer instrumentId);
+
+ @Insert("""
+
+ """)
+ int upsertBatch(@Param("tableName") String tableName,
+ @Param("records") List records);
+}
diff --git a/src/main/java/com/yangwale/backtestify/mapper/KLineMapper.java b/src/main/java/com/yangwale/backtestify/mapper/KLineMapper.java
index 71fd18f..cc941cd 100644
--- a/src/main/java/com/yangwale/backtestify/mapper/KLineMapper.java
+++ b/src/main/java/com/yangwale/backtestify/mapper/KLineMapper.java
@@ -12,25 +12,61 @@ import java.util.List;
public interface KLineMapper {
@Select("""
- SELECT instrument_id, timestamp, open, high, low, close,
+ SELECT instrument_id, k_time, open, high, low, close,
volume, turnover, open_interest
FROM ${tableName}
WHERE instrument_id = #{instrumentId}
- AND timestamp BETWEEN #{startTimestamp} AND #{endTimestamp}
- ORDER BY timestamp ASC
+ AND k_time BETWEEN #{startTimestamp} AND #{endTimestamp}
+ ORDER BY k_time ASC
""")
List selectRange(@Param("tableName") String tableName,
@Param("instrumentId") Integer instrumentId,
@Param("startTimestamp") long startTimestamp,
@Param("endTimestamp") long endTimestamp);
+ @Select("""
+ SELECT instrument_id, k_time, open, high, low, close,
+ volume, turnover, open_interest
+ FROM ${tableName}
+ WHERE instrument_id = #{instrumentId}
+ ORDER BY k_time ASC
+ """)
+ List selectAll(@Param("tableName") String tableName,
+ @Param("instrumentId") Integer instrumentId);
+
+ @Select("""
+ SELECT instrument_id, k_time, open, high, low, close,
+ volume, turnover, open_interest
+ FROM ${tableName}
+ WHERE instrument_id = #{instrumentId}
+ AND k_time > #{afterTimestamp}
+ ORDER BY k_time ASC
+ """)
+ List selectAfter(@Param("tableName") String tableName,
+ @Param("instrumentId") Integer instrumentId,
+ @Param("afterTimestamp") long afterTimestamp);
+
+ @Select("""
+ SELECT instrument_id, k_time, open, high, low, close,
+ volume, turnover, open_interest
+ FROM ${tableName}
+ WHERE instrument_id = #{instrumentId}
+ AND k_time < #{beforeTimestamp}
+ ORDER BY k_time DESC
+ LIMIT #{limit}
+ """)
+ List selectBefore(@Param("tableName") String tableName,
+ @Param("instrumentId") Integer instrumentId,
+ @Param("beforeTimestamp") long beforeTimestamp,
+ @Param("limit") int limit);
+
@Insert("""