Harden main contract refresh

This commit is contained in:
Lee 2026-07-27 23:35:02 +08:00
parent 10c434013b
commit 48079cea72
8 changed files with 100 additions and 118 deletions

View File

@ -32,7 +32,7 @@ public class InstrumentDictionary extends BaseEntity {
/** 价格放大倍数 */
private Integer priceScale;
/** 最小变动价位 */
/** 合约最小变动价位(不参与行情价格缩放) */
private BigDecimal priceTick;
/** 是否当前主力合约0-否1-是 */

View File

@ -27,8 +27,9 @@ public interface InstrumentDictionaryMapper extends BaseMapper<InstrumentDiction
FROM t_instrument_dictionary
WHERE LOWER(contract_code) = LOWER(#{contractCode})
AND is_deleted = 0
AND is_main = 1
ORDER BY id
""")
List<InstrumentDictionary> selectActiveByContractCodeIgnoreCase(
List<InstrumentDictionary> selectMainByContractCodeIgnoreCase(
@Param("contractCode") String contractCode);
}

View File

@ -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<CnQuotationModels.KChartItem> getKChartByDate(String excode, String code, String period,
long date, String direction) {
Integer type = typeOf(period);

View File

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

View File

@ -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<InstrumentDictionary> 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<CnQuotationModels.GoodsItem> goodsItems = cnQuotationClient.listMainContracts();
List<ResolvedMainContract> resolvedContracts = new ArrayList<>();
for (CnQuotationModels.GoodsItem item : goodsItems) {
if (item.mainContractCode() == null || item.mainContractCode().isBlank()) {
continue;
}
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()));
}
List<CnQuotationModels.GoodsItem> mainContracts = goodsItems.stream()
.filter(item -> item.mainContractCode() != null && !item.mainContractCode().isBlank())
.toList();
transactionTemplate.executeWithoutResult(status -> {
instrumentDictionaryMapper.update(null, new LambdaUpdateWrapper<InstrumentDictionary>()
.set(InstrumentDictionary::getIsMain, 0)
.eq(InstrumentDictionary::getIsDeleted, 0));
for (ResolvedMainContract resolved : resolvedContracts) {
upsertInstrument(resolved.item(), resolved.priceTick());
count++;
for (CnQuotationModels.GoodsItem item : mainContracts) {
upsertInstrument(item);
}
});
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<InstrumentDictionary> 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) {
}
}

View File

@ -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线周期',

View File

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

View File

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