diff --git a/src/main/java/com/yangwale/backtestify/entity/InstrumentDictionary.java b/src/main/java/com/yangwale/backtestify/entity/InstrumentDictionary.java index 061fd63..fd06c77 100644 --- a/src/main/java/com/yangwale/backtestify/entity/InstrumentDictionary.java +++ b/src/main/java/com/yangwale/backtestify/entity/InstrumentDictionary.java @@ -32,7 +32,7 @@ public class InstrumentDictionary extends BaseEntity { /** 价格放大倍数 */ private Integer priceScale; - /** 最小变动价位 */ + /** 合约最小变动价位(不参与行情价格缩放) */ private BigDecimal priceTick; /** 是否当前主力合约:0-否,1-是 */ diff --git a/src/main/java/com/yangwale/backtestify/mapper/InstrumentDictionaryMapper.java b/src/main/java/com/yangwale/backtestify/mapper/InstrumentDictionaryMapper.java index 37117b7..8ff0d9d 100644 --- a/src/main/java/com/yangwale/backtestify/mapper/InstrumentDictionaryMapper.java +++ b/src/main/java/com/yangwale/backtestify/mapper/InstrumentDictionaryMapper.java @@ -27,8 +27,9 @@ public interface InstrumentDictionaryMapper extends BaseMapper selectActiveByContractCodeIgnoreCase( + List selectMainByContractCodeIgnoreCase( @Param("contractCode") String contractCode); } diff --git a/src/main/java/com/yangwale/backtestify/service/market/client/CnQuotationClient.java b/src/main/java/com/yangwale/backtestify/service/market/client/CnQuotationClient.java index 04ff3b4..4450cd6 100644 --- a/src/main/java/com/yangwale/backtestify/service/market/client/CnQuotationClient.java +++ b/src/main/java/com/yangwale/backtestify/service/market/client/CnQuotationClient.java @@ -49,14 +49,6 @@ public class CnQuotationClient { return parseKChartItems(body); } - public CnQuotationModels.ContractDetail getContractDetail(String contractCode) { - String body = get(urlBuilder("contract/detail") - .addQueryParameter("contractCode", contractCode) - .build()); - JSONObject data = parseData(body); - return data == null ? null : data.toJavaObject(CnQuotationModels.ContractDetail.class); - } - public List getKChartByDate(String excode, String code, String period, long date, String direction) { Integer type = typeOf(period); diff --git a/src/main/java/com/yangwale/backtestify/service/market/client/CnQuotationModels.java b/src/main/java/com/yangwale/backtestify/service/market/client/CnQuotationModels.java index 98d0f7b..9980357 100644 --- a/src/main/java/com/yangwale/backtestify/service/market/client/CnQuotationModels.java +++ b/src/main/java/com/yangwale/backtestify/service/market/client/CnQuotationModels.java @@ -1,6 +1,5 @@ package com.yangwale.backtestify.service.market.client; -import java.math.BigDecimal; import java.util.List; import java.util.Map; @@ -23,12 +22,6 @@ public class CnQuotationModels { Integer isPrincipal) { } - public record ContractDetail(String excode, - String contractCode, - String productId, - BigDecimal priceTick) { - } - public record KChartResult(List chats) { } diff --git a/src/main/java/com/yangwale/backtestify/service/market/sync/MarketDataSyncService.java b/src/main/java/com/yangwale/backtestify/service/market/sync/MarketDataSyncService.java index f8752c9..9c957fb 100644 --- a/src/main/java/com/yangwale/backtestify/service/market/sync/MarketDataSyncService.java +++ b/src/main/java/com/yangwale/backtestify/service/market/sync/MarketDataSyncService.java @@ -19,12 +19,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionTemplate; -import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; @@ -47,6 +46,7 @@ public class MarketDataSyncService { private final MarketDataSyncLogMapper syncLogMapper; private final KLineTableResolver tableResolver; private final IndicatorCalculationService indicatorCalculationService; + private final TransactionTemplate transactionTemplate; @Scheduled(cron = "${market-data.sync.cron:0 0 6 * * ?}", zone = "${market-data.sync.zone:Asia/Shanghai}") public void syncYesterdayMainContracts() { @@ -78,7 +78,7 @@ public class MarketDataSyncService { public void repairContractFrom(String contractCode, long startTimestamp) { List matches = - instrumentDictionaryMapper.selectActiveByContractCodeIgnoreCase(contractCode); + instrumentDictionaryMapper.selectMainByContractCodeIgnoreCase(contractCode); InstrumentDictionary instrument = requireSingleContract(matches, contractCode); ZoneId zoneId = properties.getSync().zoneId(); LocalDateTime now = LocalDateTime.now(zoneId); @@ -109,25 +109,18 @@ public class MarketDataSyncService { int count = 0; try { List goodsItems = cnQuotationClient.listMainContracts(); - List resolvedContracts = new ArrayList<>(); - for (CnQuotationModels.GoodsItem item : goodsItems) { - if (item.mainContractCode() == null || item.mainContractCode().isBlank()) { - continue; + List mainContracts = goodsItems.stream() + .filter(item -> item.mainContractCode() != null && !item.mainContractCode().isBlank()) + .toList(); + transactionTemplate.executeWithoutResult(status -> { + instrumentDictionaryMapper.update(null, new LambdaUpdateWrapper() + .set(InstrumentDictionary::getIsMain, 0) + .eq(InstrumentDictionary::getIsDeleted, 0)); + for (CnQuotationModels.GoodsItem item : mainContracts) { + upsertInstrument(item); } - CnQuotationModels.ContractDetail detail = - cnQuotationClient.getContractDetail(item.mainContractCode()); - if (detail == null || detail.priceTick() == null || detail.priceTick().signum() <= 0) { - throw new IllegalStateException("行情接口未返回有效最小变动价位: " + item.mainContractCode()); - } - resolvedContracts.add(new ResolvedMainContract(item, detail.priceTick())); - } - instrumentDictionaryMapper.update(null, new LambdaUpdateWrapper() - .set(InstrumentDictionary::getIsMain, 0) - .eq(InstrumentDictionary::getIsDeleted, 0)); - for (ResolvedMainContract resolved : resolvedContracts) { - upsertInstrument(resolved.item(), resolved.priceTick()); - count++; - } + }); + count = mainContracts.size(); saveLog("CONTRACT", null, null, LocalDate.now(), "SUCCESS", count, null, start); } catch (Exception e) { saveLog("CONTRACT", null, null, LocalDate.now(), "FAILED", count, e.getMessage(), start); @@ -179,7 +172,7 @@ public class MarketDataSyncService { } } - private void upsertInstrument(CnQuotationModels.GoodsItem item, BigDecimal priceTick) { + private void upsertInstrument(CnQuotationModels.GoodsItem item) { String contractCode = item.mainContractCode(); List matches = instrumentDictionaryMapper.selectByExchangeAndContractCodeIgnoreCase( @@ -193,14 +186,12 @@ public class MarketDataSyncService { instrument.setSymbol(firstNonBlank(item.productId(), item.goodsCode(), productPrefix(contractCode))); instrument.setContractCode(contractCode); instrument.setPriceScale(DEFAULT_PRICE_SCALE); - instrument.setPriceTick(priceTick); instrument.setIsMain(1); instrumentDictionaryMapper.insert(instrument); } else { existing.setExchangeId(item.excode()); existing.setSymbol(firstNonBlank(item.productId(), item.goodsCode(), productPrefix(contractCode))); existing.setContractCode(contractCode); - existing.setPriceTick(priceTick); existing.setIsMain(1); existing.setIsDeleted(0); instrumentDictionaryMapper.updateById(existing); @@ -267,7 +258,4 @@ public class MarketDataSyncService { } return contractCode.replaceAll("\\d+$", ""); } - - private record ResolvedMainContract(CnQuotationModels.GoodsItem item, BigDecimal priceTick) { - } } diff --git a/src/main/resources/db/init.sql b/src/main/resources/db/init.sql index 76c3128..d91dc82 100644 --- a/src/main/resources/db/init.sql +++ b/src/main/resources/db/init.sql @@ -11,8 +11,7 @@ USE backtestify; -- ---------------------------- -- Table structure for bt_strategy_config -- ---------------------------- -DROP TABLE IF EXISTS `bt_strategy_config`; -CREATE TABLE `bt_strategy_config` ( +CREATE TABLE IF NOT EXISTS `bt_strategy_config` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `user_id` bigint NOT NULL COMMENT '用户ID', `contract_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '合约代码', @@ -40,8 +39,7 @@ CREATE TABLE `bt_strategy_config` ( -- ---------------------------- -- Table structure for bt_strategy_result -- ---------------------------- -DROP TABLE IF EXISTS `bt_strategy_result`; -CREATE TABLE `bt_strategy_result` ( +CREATE TABLE IF NOT EXISTS `bt_strategy_result` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `strategy_id` bigint NOT NULL COMMENT '关联策略ID', `initial_capital` decimal(18, 2) NOT NULL COMMENT '初始资金', @@ -66,8 +64,7 @@ CREATE TABLE `bt_strategy_result` ( -- ---------------------------- -- Table structure for bt_trade_detail -- ---------------------------- -DROP TABLE IF EXISTS `bt_trade_detail`; -CREATE TABLE `bt_trade_detail` ( +CREATE TABLE IF NOT EXISTS `bt_trade_detail` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `strategy_id` bigint NOT NULL COMMENT '关联策略ID', `action` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作: BUY_OPEN/SELL_CLOSE/SELL_OPEN/BUY_CLOSE', @@ -86,8 +83,7 @@ CREATE TABLE `bt_trade_detail` ( -- ---------------------------- -- Table structure for bt_user_signal -- ---------------------------- -DROP TABLE IF EXISTS `bt_user_signal`; -CREATE TABLE `bt_user_signal` ( +CREATE TABLE IF NOT EXISTS `bt_user_signal` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `user_id` bigint NOT NULL COMMENT '用户ID', `strategy_id` bigint NOT NULL COMMENT '策略ID', @@ -105,8 +101,7 @@ CREATE TABLE `bt_user_signal` ( -- ---------------------------- -- Table structure for t_indicator_15m -- ---------------------------- -DROP TABLE IF EXISTS `t_indicator_15m`; -CREATE TABLE `t_indicator_15m` ( +CREATE TABLE IF NOT EXISTS `t_indicator_15m` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,对应K线起始点)', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5', @@ -134,8 +129,7 @@ CREATE TABLE `t_indicator_15m` ( -- ---------------------------- -- Table structure for t_indicator_1d -- ---------------------------- -DROP TABLE IF EXISTS `t_indicator_1d`; -CREATE TABLE `t_indicator_1d` ( +CREATE TABLE IF NOT EXISTS `t_indicator_1d` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,对应K线起始点)', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5', @@ -163,8 +157,7 @@ CREATE TABLE `t_indicator_1d` ( -- ---------------------------- -- Table structure for t_indicator_1h -- ---------------------------- -DROP TABLE IF EXISTS `t_indicator_1h`; -CREATE TABLE `t_indicator_1h` ( +CREATE TABLE IF NOT EXISTS `t_indicator_1h` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,对应K线起始点)', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5', @@ -192,8 +185,7 @@ CREATE TABLE `t_indicator_1h` ( -- ---------------------------- -- Table structure for t_indicator_1m -- ---------------------------- -DROP TABLE IF EXISTS `t_indicator_1m`; -CREATE TABLE `t_indicator_1m` ( +CREATE TABLE IF NOT EXISTS `t_indicator_1m` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级)', `ma5` decimal(18, 6) NULL DEFAULT NULL, @@ -221,8 +213,7 @@ CREATE TABLE `t_indicator_1m` ( -- ---------------------------- -- Table structure for t_indicator_1mo -- ---------------------------- -DROP TABLE IF EXISTS `t_indicator_1mo`; -CREATE TABLE `t_indicator_1mo` ( +CREATE TABLE IF NOT EXISTS `t_indicator_1mo` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,对应K线起始点)', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5', @@ -250,8 +241,7 @@ CREATE TABLE `t_indicator_1mo` ( -- ---------------------------- -- Table structure for t_indicator_1w -- ---------------------------- -DROP TABLE IF EXISTS `t_indicator_1w`; -CREATE TABLE `t_indicator_1w` ( +CREATE TABLE IF NOT EXISTS `t_indicator_1w` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,对应K线起始点)', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5', @@ -279,8 +269,7 @@ CREATE TABLE `t_indicator_1w` ( -- ---------------------------- -- Table structure for t_indicator_30m -- ---------------------------- -DROP TABLE IF EXISTS `t_indicator_30m`; -CREATE TABLE `t_indicator_30m` ( +CREATE TABLE IF NOT EXISTS `t_indicator_30m` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,对应K线起始点)', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5', @@ -308,8 +297,7 @@ CREATE TABLE `t_indicator_30m` ( -- ---------------------------- -- Table structure for t_indicator_3m -- ---------------------------- -DROP TABLE IF EXISTS `t_indicator_3m`; -CREATE TABLE `t_indicator_3m` ( +CREATE TABLE IF NOT EXISTS `t_indicator_3m` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,对应K线起始点)', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5', @@ -337,8 +325,7 @@ CREATE TABLE `t_indicator_3m` ( -- ---------------------------- -- Table structure for t_indicator_4h -- ---------------------------- -DROP TABLE IF EXISTS `t_indicator_4h`; -CREATE TABLE `t_indicator_4h` ( +CREATE TABLE IF NOT EXISTS `t_indicator_4h` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,对应K线起始点)', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5', @@ -366,8 +353,7 @@ CREATE TABLE `t_indicator_4h` ( -- ---------------------------- -- Table structure for t_indicator_5m -- ---------------------------- -DROP TABLE IF EXISTS `t_indicator_5m`; -CREATE TABLE `t_indicator_5m` ( +CREATE TABLE IF NOT EXISTS `t_indicator_5m` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,对应K线起始点)', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5', @@ -395,20 +381,20 @@ CREATE TABLE `t_indicator_5m` ( -- ---------------------------- -- Table structure for t_instrument_dictionary -- ---------------------------- -DROP TABLE IF EXISTS `t_instrument_dictionary`; -CREATE TABLE `t_instrument_dictionary` ( +CREATE TABLE IF NOT EXISTS `t_instrument_dictionary` ( `id` smallint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '合约自增ID', `exchange_id` varchar(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, `symbol` varchar(10) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, `contract_code` varchar(20) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `contract_code_normalized` varchar(20) CHARACTER SET ascii COLLATE ascii_general_ci GENERATED ALWAYS AS (lower(`contract_code`)) STORED, `price_scale` int UNSIGNED NOT NULL DEFAULT 1000 COMMENT '价格放大倍数(暂统一使用1000)', - `price_tick` decimal(18, 6) NOT NULL COMMENT '最小变动价位;历史源tb_quotations_futures_contract.price_tick,增量源contract/detail.priceTick', + `price_tick` decimal(18, 6) NOT NULL DEFAULT 1 COMMENT '合约最小变动价位,不参与行情价格缩放', `is_main` tinyint NOT NULL DEFAULT 0 COMMENT '是否当前主力合约: 0-否, 1-是', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除: 0-未删除, 1-已删除', PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `uk_exchange_contract`(`exchange_id` ASC, `contract_code` ASC) USING BTREE, + UNIQUE INDEX `uk_exchange_contract`(`exchange_id` ASC, `contract_code_normalized` ASC) USING BTREE, INDEX `idx_symbol_main`(`symbol` ASC, `is_main` ASC) USING BTREE, INDEX `idx_contract_code`(`contract_code` ASC) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2048 CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '期货合约字典表' ROW_FORMAT = Dynamic; @@ -416,8 +402,7 @@ CREATE TABLE `t_instrument_dictionary` ( -- ---------------------------- -- Table structure for t_kline_15m -- ---------------------------- -DROP TABLE IF EXISTS `t_kline_15m`; -CREATE TABLE `t_kline_15m` ( +CREATE TABLE IF NOT EXISTS `t_kline_15m` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,每根K线起始点)', `open` int NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', @@ -434,8 +419,7 @@ CREATE TABLE `t_kline_15m` ( -- ---------------------------- -- Table structure for t_kline_1d -- ---------------------------- -DROP TABLE IF EXISTS `t_kline_1d`; -CREATE TABLE `t_kline_1d` ( +CREATE TABLE IF NOT EXISTS `t_kline_1d` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,每根K线起始点)', `open` int NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', @@ -453,8 +437,7 @@ CREATE TABLE `t_kline_1d` ( -- ---------------------------- -- Table structure for t_kline_1h -- ---------------------------- -DROP TABLE IF EXISTS `t_kline_1h`; -CREATE TABLE `t_kline_1h` ( +CREATE TABLE IF NOT EXISTS `t_kline_1h` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,每根K线起始点)', `open` int NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', @@ -471,8 +454,7 @@ CREATE TABLE `t_kline_1h` ( -- ---------------------------- -- Table structure for t_kline_1m -- ---------------------------- -DROP TABLE IF EXISTS `t_kline_1m`; -CREATE TABLE `t_kline_1m` ( +CREATE TABLE IF NOT EXISTS `t_kline_1m` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,每根K线起始点)', `open` int NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', @@ -489,8 +471,7 @@ CREATE TABLE `t_kline_1m` ( -- ---------------------------- -- Table structure for t_kline_1mo -- ---------------------------- -DROP TABLE IF EXISTS `t_kline_1mo`; -CREATE TABLE `t_kline_1mo` ( +CREATE TABLE IF NOT EXISTS `t_kline_1mo` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,每根K线起始点)', `open` int NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', @@ -507,8 +488,7 @@ CREATE TABLE `t_kline_1mo` ( -- ---------------------------- -- Table structure for t_kline_1w -- ---------------------------- -DROP TABLE IF EXISTS `t_kline_1w`; -CREATE TABLE `t_kline_1w` ( +CREATE TABLE IF NOT EXISTS `t_kline_1w` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,每根K线起始点)', `open` int NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', @@ -525,8 +505,7 @@ CREATE TABLE `t_kline_1w` ( -- ---------------------------- -- Table structure for t_kline_30m -- ---------------------------- -DROP TABLE IF EXISTS `t_kline_30m`; -CREATE TABLE `t_kline_30m` ( +CREATE TABLE IF NOT EXISTS `t_kline_30m` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,每根K线起始点)', `open` int NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', @@ -543,8 +522,7 @@ CREATE TABLE `t_kline_30m` ( -- ---------------------------- -- Table structure for t_kline_3m -- ---------------------------- -DROP TABLE IF EXISTS `t_kline_3m`; -CREATE TABLE `t_kline_3m` ( +CREATE TABLE IF NOT EXISTS `t_kline_3m` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,每根K线起始点)', `open` int NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', @@ -561,8 +539,7 @@ CREATE TABLE `t_kline_3m` ( -- ---------------------------- -- Table structure for t_kline_4h -- ---------------------------- -DROP TABLE IF EXISTS `t_kline_4h`; -CREATE TABLE `t_kline_4h` ( +CREATE TABLE IF NOT EXISTS `t_kline_4h` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,每根K线起始点)', `open` int NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', @@ -579,8 +556,7 @@ CREATE TABLE `t_kline_4h` ( -- ---------------------------- -- Table structure for t_kline_5m -- ---------------------------- -DROP TABLE IF EXISTS `t_kline_5m`; -CREATE TABLE `t_kline_5m` ( +CREATE TABLE IF NOT EXISTS `t_kline_5m` ( `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,每根K线起始点)', `open` int NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', @@ -597,8 +573,7 @@ CREATE TABLE `t_kline_5m` ( -- ---------------------------- -- Table structure for t_market_data_sync_log -- ---------------------------- -DROP TABLE IF EXISTS `t_market_data_sync_log`; -CREATE TABLE `t_market_data_sync_log` ( +CREATE TABLE IF NOT EXISTS `t_market_data_sync_log` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `sync_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '同步类型: CONTRACT/KLINE', `f_period` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'K线周期', diff --git a/src/test/java/com/yangwale/backtestify/service/market/sync/MarketDataSyncServiceTest.java b/src/test/java/com/yangwale/backtestify/service/market/sync/MarketDataSyncServiceTest.java index 503a244..a26a92d 100644 --- a/src/test/java/com/yangwale/backtestify/service/market/sync/MarketDataSyncServiceTest.java +++ b/src/test/java/com/yangwale/backtestify/service/market/sync/MarketDataSyncServiceTest.java @@ -1,7 +1,7 @@ package com.yangwale.backtestify.service.market.sync; -import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.yangwale.backtestify.config.MarketDataProperties; import com.yangwale.backtestify.entity.InstrumentDictionary; @@ -14,16 +14,21 @@ import com.yangwale.backtestify.service.market.client.CnQuotationClient; import com.yangwale.backtestify.service.market.client.CnQuotationModels; import com.yangwale.backtestify.service.market.indicator.IndicatorCalculationService; import com.yangwale.backtestify.service.market.repository.KLineTableResolver; -import org.junit.jupiter.api.Test; import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.SimpleTransactionStatus; +import org.springframework.transaction.support.TransactionTemplate; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; @@ -78,7 +83,8 @@ class MarketDataSyncServiceTest { instrument.setExchangeId("CZCE"); instrument.setContractCode("AP610"); instrument.setPriceScale(1000); - when(instrumentMapper.selectActiveByContractCodeIgnoreCase("ap610")) + instrument.setIsMain(1); + when(instrumentMapper.selectMainByContractCodeIgnoreCase("ap610")) .thenReturn(List.of(instrument)); CapturingRepairQuotationClient quotationClient = new CapturingRepairQuotationClient(properties); @@ -91,6 +97,21 @@ class MarketDataSyncServiceTest { verify(indicatorService).rebuild("1d", 7); } + @Test + void repairRejectsContractThatIsNotCurrentMain() { + MarketDataProperties properties = new MarketDataProperties(); + InstrumentDictionaryMapper instrumentMapper = mock(InstrumentDictionaryMapper.class); + when(instrumentMapper.selectMainByContractCodeIgnoreCase("AP610")) + .thenReturn(List.of()); + + MarketDataSyncService service = service(properties, + new EmptyQuotationClient(properties), instrumentMapper, mock(KLineMapper.class), + mock(MarketDataSyncLogMapper.class), mock(IndicatorCalculationService.class)); + + assertThrows(IllegalArgumentException.class, + () -> service.repairContractFrom("AP610", 100L)); + } + @Test void refreshPreservesExistingPriceScale() { MarketDataProperties properties = new MarketDataProperties(); @@ -109,12 +130,11 @@ class MarketDataSyncServiceTest { new EmptyQuotationClient(properties), instrumentMapper, kLineMapper, syncLogMapper, indicatorService); - ReflectionTestUtils.invokeMethod(service, "upsertInstrument", goodsItem(), new BigDecimal("0.5")); + ReflectionTestUtils.invokeMethod(service, "upsertInstrument", goodsItem()); ArgumentCaptor updated = ArgumentCaptor.forClass(InstrumentDictionary.class); verify(instrumentMapper).updateById(updated.capture()); assertEquals(500, updated.getValue().getPriceScale()); - assertEquals(new BigDecimal("0.5"), updated.getValue().getPriceTick()); } @Test @@ -129,12 +149,11 @@ class MarketDataSyncServiceTest { new EmptyQuotationClient(properties), instrumentMapper, kLineMapper, syncLogMapper, indicatorService); - ReflectionTestUtils.invokeMethod(service, "upsertInstrument", goodsItem(), new BigDecimal("0.5")); + ReflectionTestUtils.invokeMethod(service, "upsertInstrument", goodsItem()); ArgumentCaptor inserted = ArgumentCaptor.forClass(InstrumentDictionary.class); verify(instrumentMapper).insert(inserted.capture()); assertEquals(1000, inserted.getValue().getPriceScale()); - assertEquals(new BigDecimal("0.5"), inserted.getValue().getPriceTick()); } @Test @@ -164,7 +183,8 @@ class MarketDataSyncServiceTest { kLineMapper, syncLogMapper, new KLineTableResolver(), - indicatorService) { + indicatorService, + transactionTemplate()) { @Override public void refreshMainContracts() { // Contract refresh is outside this test's synchronization seam. @@ -203,7 +223,7 @@ class MarketDataSyncServiceTest { when(kLineMapper.selectLatestTimestamp("t_kline_1d", 7)).thenReturn(100L); MarketDataSyncService service = new MarketDataSyncService( properties, quotationClient, instrumentMapper, kLineMapper, syncLogMapper, - new KLineTableResolver(), indicatorService) { + new KLineTableResolver(), indicatorService, transactionTemplate()) { @Override public void refreshMainContracts() { // Contract refresh is outside this test's synchronization seam. @@ -234,7 +254,8 @@ class MarketDataSyncServiceTest { MarketDataSyncService service = new MarketDataSyncService( properties, new RepairQuotationClient(properties), instrumentMapper, - kLineMapper, syncLogMapper, new KLineTableResolver(), indicatorService) { + kLineMapper, syncLogMapper, new KLineTableResolver(), indicatorService, + transactionTemplate()) { @Override public void refreshMainContracts() { // Contract refresh is outside this test's synchronization seam. @@ -264,7 +285,8 @@ class MarketDataSyncServiceTest { instrument.setExchangeId("SHFE"); instrument.setContractCode("rb2610"); instrument.setPriceScale(1000); - when(instrumentMapper.selectActiveByContractCodeIgnoreCase("rb2610")) + instrument.setIsMain(1); + when(instrumentMapper.selectMainByContractCodeIgnoreCase("rb2610")) .thenReturn(List.of(instrument)); MarketDataSyncService service = service(properties, new RepairQuotationClient(properties), @@ -285,7 +307,24 @@ class MarketDataSyncServiceTest { IndicatorCalculationService indicatorService) { return new MarketDataSyncService( properties, quotationClient, instrumentMapper, kLineMapper, syncLogMapper, - new KLineTableResolver(), indicatorService); + new KLineTableResolver(), indicatorService, transactionTemplate()); + } + + private TransactionTemplate transactionTemplate() { + return new TransactionTemplate(new PlatformTransactionManager() { + @Override + public TransactionStatus getTransaction(TransactionDefinition definition) { + return new SimpleTransactionStatus(); + } + + @Override + public void commit(TransactionStatus status) { + } + + @Override + public void rollback(TransactionStatus status) { + } + }); } private CnQuotationModels.GoodsItem goodsItem() { @@ -368,12 +407,6 @@ class MarketDataSyncServiceTest { return List.of(new CnQuotationModels.GoodsItem( "CZCE", "AP", "AP", "苹果", "AP610", 1)); } - - @Override - public CnQuotationModels.ContractDetail getContractDetail(String contractCode) { - return new CnQuotationModels.ContractDetail( - "CZCE", contractCode, "AP", new BigDecimal("1")); - } } private static final class CapturingRepairQuotationClient extends EmptyQuotationClient { diff --git a/src/test/resources/schema-test.sql b/src/test/resources/schema-test.sql index e3d98ee..804b628 100644 --- a/src/test/resources/schema-test.sql +++ b/src/test/resources/schema-test.sql @@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS t_instrument_dictionary ( create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_deleted TINYINT NOT NULL DEFAULT 0, - UNIQUE (contract_code) + UNIQUE (exchange_id, contract_code) ); CREATE TABLE IF NOT EXISTS t_kline_1m (