diff --git a/src/main/java/com/yangwale/backtestify/entity/InstrumentDictionary.java b/src/main/java/com/yangwale/backtestify/entity/InstrumentDictionary.java index cd87abe..061fd63 100644 --- a/src/main/java/com/yangwale/backtestify/entity/InstrumentDictionary.java +++ b/src/main/java/com/yangwale/backtestify/entity/InstrumentDictionary.java @@ -7,6 +7,8 @@ import com.yangwale.backtestify.common.BaseEntity; import lombok.Getter; import lombok.Setter; +import java.math.BigDecimal; + /** * 合约字典表 */ @@ -30,6 +32,9 @@ public class InstrumentDictionary extends BaseEntity { /** 价格放大倍数 */ private Integer priceScale; + /** 最小变动价位 */ + private BigDecimal priceTick; + /** 是否当前主力合约:0-否,1-是 */ private Integer isMain; } diff --git a/src/main/java/com/yangwale/backtestify/entity/MarketDataSyncLog.java b/src/main/java/com/yangwale/backtestify/entity/MarketDataSyncLog.java index 64439dd..d24b006 100644 --- a/src/main/java/com/yangwale/backtestify/entity/MarketDataSyncLog.java +++ b/src/main/java/com/yangwale/backtestify/entity/MarketDataSyncLog.java @@ -1,6 +1,7 @@ package com.yangwale.backtestify.entity; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Getter; @@ -22,6 +23,7 @@ public class MarketDataSyncLog { private String syncType; + @TableField("f_period") private String period; private String contractCode; diff --git a/src/main/java/com/yangwale/backtestify/mapper/InstrumentDictionaryMapper.java b/src/main/java/com/yangwale/backtestify/mapper/InstrumentDictionaryMapper.java index a80b76d..37117b7 100644 --- a/src/main/java/com/yangwale/backtestify/mapper/InstrumentDictionaryMapper.java +++ b/src/main/java/com/yangwale/backtestify/mapper/InstrumentDictionaryMapper.java @@ -3,7 +3,32 @@ package com.yangwale.backtestify.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.yangwale.backtestify.entity.InstrumentDictionary; 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 InstrumentDictionaryMapper extends BaseMapper { + + @Select(""" + SELECT * + FROM t_instrument_dictionary + WHERE exchange_id = #{exchangeId} + AND LOWER(contract_code) = LOWER(#{contractCode}) + ORDER BY id + """) + List selectByExchangeAndContractCodeIgnoreCase( + @Param("exchangeId") String exchangeId, + @Param("contractCode") String contractCode); + + @Select(""" + SELECT * + FROM t_instrument_dictionary + WHERE LOWER(contract_code) = LOWER(#{contractCode}) + AND is_deleted = 0 + ORDER BY id + """) + List selectActiveByContractCodeIgnoreCase( + @Param("contractCode") String contractCode); } diff --git a/src/main/java/com/yangwale/backtestify/mapper/KLineMapper.java b/src/main/java/com/yangwale/backtestify/mapper/KLineMapper.java index cc941cd..20fde6b 100644 --- a/src/main/java/com/yangwale/backtestify/mapper/KLineMapper.java +++ b/src/main/java/com/yangwale/backtestify/mapper/KLineMapper.java @@ -24,6 +24,14 @@ public interface KLineMapper { @Param("startTimestamp") long startTimestamp, @Param("endTimestamp") long endTimestamp); + @Select(""" + SELECT MAX(k_time) + FROM ${tableName} + WHERE instrument_id = #{instrumentId} + """) + Long selectLatestTimestamp(@Param("tableName") String tableName, + @Param("instrumentId") Integer instrumentId); + @Select(""" SELECT instrument_id, k_time, open, high, low, close, volume, turnover, open_interest 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 0ba78af..04ff3b4 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 @@ -1,7 +1,7 @@ package com.yangwale.backtestify.service.market.client; import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.TypeReference; +import com.alibaba.fastjson2.JSONObject; import com.yangwale.backtestify.common.BusinessException; import com.yangwale.backtestify.common.ErrorCode; import com.yangwale.backtestify.config.MarketDataProperties; @@ -33,11 +33,10 @@ public class CnQuotationClient { String body = get(urlBuilder("goods/list") .addQueryParameter("pageSize", "-1") .build()); - CnQuotationModels.ResultModel result = JSON.parseObject(body, - new TypeReference>() { - }); - CnQuotationModels.GoodsPage data = unwrap(result); - return data == null || data.list() == null ? List.of() : data.list(); + JSONObject data = parseData(body); + return data == null || data.getJSONArray("list") == null + ? List.of() + : data.getJSONArray("list").toJavaList(CnQuotationModels.GoodsItem.class); } public List getKChart(String excode, String code, String period) { @@ -47,11 +46,15 @@ public class CnQuotationClient { .addQueryParameter("code", code) .addQueryParameter("type", String.valueOf(type)) .build()); - CnQuotationModels.ResultModel result = JSON.parseObject(body, - new TypeReference>() { - }); - CnQuotationModels.KChartResult data = unwrap(result); - return data == null || data.chats() == null ? List.of() : data.chats(); + 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, @@ -64,11 +67,7 @@ public class CnQuotationClient { .addQueryParameter("date", String.valueOf(date)) .addQueryParameter("direction", direction) .build()); - CnQuotationModels.ResultModel result = JSON.parseObject(body, - new TypeReference>() { - }); - CnQuotationModels.KChartResult data = unwrap(result); - return data == null || data.chats() == null ? List.of() : data.chats(); + return parseKChartItems(body); } public Integer typeOf(String period) { @@ -86,15 +85,25 @@ public class CnQuotationClient { }; } - private T unwrap(CnQuotationModels.ResultModel result) { + private List parseKChartItems(String body) { + JSONObject data = parseData(body); + return data == null || data.getJSONArray("chats") == null + ? List.of() + : data.getJSONArray("chats").toJavaList(CnQuotationModels.KChartItem.class); + } + + private JSONObject parseData(String body) { + JSONObject result = JSON.parseObject(body); if (result == null) { throw new BusinessException(ErrorCode.MARKET_DATA_UNAVAILABLE, "行情接口返回为空"); } - if (!Boolean.TRUE.equals(result.success())) { + if (!Boolean.TRUE.equals(result.getBoolean("success"))) { throw new BusinessException(ErrorCode.MARKET_DATA_UNAVAILABLE, - result.errorInfo() != null ? result.errorInfo() : "行情接口调用失败: " + result.errorCode()); + result.getString("errorInfo") != null + ? result.getString("errorInfo") + : "行情接口调用失败: " + result.getString("errorCode")); } - return result.data(); + return result.getJSONObject("data"); } private HttpUrl.Builder urlBuilder(String path) { 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 1137819..98d0f7b 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,5 +1,6 @@ package com.yangwale.backtestify.service.market.client; +import java.math.BigDecimal; import java.util.List; import java.util.Map; @@ -19,8 +20,13 @@ public class CnQuotationModels { String productId, String goodsName, String mainContractCode, - Integer isPrincipal, - Integer decimalPrecision) { + 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/convert/PriceScaleConverter.java b/src/main/java/com/yangwale/backtestify/service/market/convert/PriceScaleConverter.java index c0b19d1..54002df 100644 --- a/src/main/java/com/yangwale/backtestify/service/market/convert/PriceScaleConverter.java +++ b/src/main/java/com/yangwale/backtestify/service/market/convert/PriceScaleConverter.java @@ -33,16 +33,8 @@ public final class PriceScaleConverter { if (value == null || value.isBlank() || "-".equals(value.trim())) { return 0L; } - return new BigDecimal(value.trim()).setScale(0, RoundingMode.HALF_UP).longValue(); - } - - public static int scaleFromPrecision(Integer decimalPrecision) { - int precision = decimalPrecision == null ? 0 : Math.max(decimalPrecision, 0); - int scale = 1; - for (int i = 0; i < precision; i++) { - scale *= 10; - } - return scale; + long parsed = new BigDecimal(value.trim()).setScale(0, RoundingMode.HALF_UP).longValue(); + return Math.max(parsed, 0L); } private static int scaleDigits(int priceScale) { 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 4412f67..f8752c9 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,11 +19,13 @@ 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.annotation.Transactional; +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; @@ -36,6 +38,8 @@ import java.util.Objects; @ConditionalOnProperty(prefix = "market-data", name = "provider", havingValue = "mysql") public class MarketDataSyncService { + private static final int DEFAULT_PRICE_SCALE = 1000; + private final MarketDataProperties properties; private final CnQuotationClient cnQuotationClient; private final InstrumentDictionaryMapper instrumentDictionaryMapper; @@ -53,18 +57,40 @@ public class MarketDataSyncService { syncIncrementalForMainContracts(); } - @Transactional(rollbackFor = Exception.class) public void syncIncrementalForMainContracts() { refreshMainContracts(); ZoneId zoneId = properties.getSync().zoneId(); LocalDateTime now = LocalDateTime.now(zoneId); - LocalDate syncDate = now.toLocalDate().minusDays(1); - long startTimestamp = syncDate.atStartOfDay(zoneId).toEpochSecond(); - long endTimestamp = now.toLocalDate() - .atTime(properties.getSync().getIncrementalWindowEndHour(), 0) - .atZone(zoneId) - .toEpochSecond(); + LocalDate syncDate = now.toLocalDate(); + long fallbackStartTimestamp = syncDate.minusDays(1).atStartOfDay(zoneId).toEpochSecond(); + long endTimestamp = now.atZone(zoneId).toEpochSecond(); + syncMainContracts(syncDate, fallbackStartTimestamp, endTimestamp, null, false); + } + public void repairMainContractsFrom(long startTimestamp) { + refreshMainContracts(); + ZoneId zoneId = properties.getSync().zoneId(); + LocalDateTime now = LocalDateTime.now(zoneId); + long forcedCursor = Math.max(0, startTimestamp - 1); + syncMainContracts(now.toLocalDate(), forcedCursor, now.atZone(zoneId).toEpochSecond(), + forcedCursor, true); + } + + public void repairContractFrom(String contractCode, long startTimestamp) { + List matches = + instrumentDictionaryMapper.selectActiveByContractCodeIgnoreCase(contractCode); + InstrumentDictionary instrument = requireSingleContract(matches, contractCode); + ZoneId zoneId = properties.getSync().zoneId(); + LocalDateTime now = LocalDateTime.now(zoneId); + long forcedCursor = Math.max(0, startTimestamp - 1); + for (String period : properties.getSync().getPeriods()) { + syncOnePeriod(instrument, period, now.toLocalDate(), forcedCursor, + now.atZone(zoneId).toEpochSecond(), forcedCursor, true); + } + } + + private void syncMainContracts(LocalDate syncDate, long fallbackStartTimestamp, long endTimestamp, + Long forcedCursor, boolean rebuildIndicators) { List instruments = instrumentDictionaryMapper.selectList( new LambdaQueryWrapper() .eq(InstrumentDictionary::getIsDeleted, 0) @@ -72,7 +98,8 @@ public class MarketDataSyncService { for (InstrumentDictionary instrument : instruments) { for (String period : properties.getSync().getPeriods()) { - syncOnePeriod(instrument, period, syncDate, startTimestamp, endTimestamp); + syncOnePeriod(instrument, period, syncDate, fallbackStartTimestamp, endTimestamp, + forcedCursor, rebuildIndicators); } } } @@ -82,14 +109,23 @@ public class MarketDataSyncService { int count = 0; try { List goodsItems = cnQuotationClient.listMainContracts(); - instrumentDictionaryMapper.update(null, new LambdaUpdateWrapper() - .set(InstrumentDictionary::getIsMain, 0) - .eq(InstrumentDictionary::getIsDeleted, 0)); + List resolvedContracts = new ArrayList<>(); for (CnQuotationModels.GoodsItem item : goodsItems) { if (item.mainContractCode() == null || item.mainContractCode().isBlank()) { continue; } - 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++; } saveLog("CONTRACT", null, null, LocalDate.now(), "SUCCESS", count, null, start); @@ -100,53 +136,87 @@ public class MarketDataSyncService { } private void syncOnePeriod(InstrumentDictionary instrument, String period, LocalDate syncDate, - long startTimestamp, long endTimestamp) { + long fallbackStartTimestamp, long endTimestamp, + Long forcedCursor, boolean rebuildIndicators) { LocalDateTime start = LocalDateTime.now(); int count = 0; + String syncType = rebuildIndicators ? "KLINE_REPAIR" : "KLINE"; try { - List items = cnQuotationClient.getKChartByDate( - instrument.getExchangeId(), instrument.getContractCode(), period, startTimestamp, "after"); - List records = items.stream() - .filter(item -> item.u() != null && item.u() >= startTimestamp && item.u() <= endTimestamp) - .map(item -> toRecord(instrument, item)) - .filter(Objects::nonNull) - .toList(); - if (!records.isEmpty()) { - count = kLineMapper.upsertBatch(tableResolver.resolve(period), records); + String tableName = tableResolver.resolve(period); + Long latestTimestamp = kLineMapper.selectLatestTimestamp(tableName, instrument.getId()); + long cursor = forcedCursor != null + ? forcedCursor + : latestTimestamp == null ? fallbackStartTimestamp : latestTimestamp; + while (cursor < endTimestamp) { + long pageCursor = cursor; + List items = cnQuotationClient.getKChartByDate( + instrument.getExchangeId(), instrument.getContractCode(), period, pageCursor, "after"); + List records = items.stream() + .filter(item -> item.u() != null && item.u() > pageCursor && item.u() <= endTimestamp) + .sorted(Comparator.comparing(CnQuotationModels.KChartItem::u)) + .map(item -> toRecord(instrument, item)) + .filter(Objects::nonNull) + .toList(); + if (records.isEmpty()) { + break; + } + count += kLineMapper.upsertBatch(tableName, records); + long nextCursor = records.getLast().getKTime(); + if (nextCursor <= cursor) { + break; + } + cursor = nextCursor; + } + if (rebuildIndicators) { + indicatorCalculationService.rebuild(period, instrument.getId()); + } else { indicatorCalculationService.updateIncremental(period, instrument.getId()); } - saveLog("KLINE", period, instrument.getContractCode(), syncDate, "SUCCESS", count, null, start); + saveLog(syncType, period, instrument.getContractCode(), syncDate, "SUCCESS", count, null, start); } catch (Exception e) { log.warn("同步行情失败: {} {}", instrument.getContractCode(), period, e); - saveLog("KLINE", period, instrument.getContractCode(), syncDate, "FAILED", count, e.getMessage(), start); + saveLog(syncType, period, instrument.getContractCode(), syncDate, "FAILED", count, e.getMessage(), start); } } - private void upsertInstrument(CnQuotationModels.GoodsItem item) { + private void upsertInstrument(CnQuotationModels.GoodsItem item, BigDecimal priceTick) { String contractCode = item.mainContractCode(); - int priceScale = PriceScaleConverter.scaleFromPrecision(item.decimalPrecision()); - InstrumentDictionary existing = instrumentDictionaryMapper.selectOne( - new LambdaQueryWrapper() - .eq(InstrumentDictionary::getContractCode, contractCode) - .last("LIMIT 1")); + List matches = + instrumentDictionaryMapper.selectByExchangeAndContractCodeIgnoreCase( + item.excode(), contractCode); + InstrumentDictionary existing = matches.isEmpty() + ? null + : requireSingleContract(matches, item.excode() + "/" + contractCode); if (existing == null) { InstrumentDictionary instrument = new InstrumentDictionary(); instrument.setExchangeId(item.excode()); instrument.setSymbol(firstNonBlank(item.productId(), item.goodsCode(), productPrefix(contractCode))); instrument.setContractCode(contractCode); - instrument.setPriceScale(priceScale); + 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.setPriceScale(priceScale); + existing.setContractCode(contractCode); + existing.setPriceTick(priceTick); existing.setIsMain(1); existing.setIsDeleted(0); instrumentDictionaryMapper.updateById(existing); } } + private InstrumentDictionary requireSingleContract(List matches, String contractCode) { + if (matches == null || matches.isEmpty()) { + throw new IllegalArgumentException("合约不存在: " + contractCode); + } + if (matches.size() > 1) { + throw new IllegalStateException("存在仅大小写不同的重复合约: " + contractCode); + } + return matches.getFirst(); + } + private KLineRecord toRecord(InstrumentDictionary instrument, CnQuotationModels.KChartItem item) { try { return KLineRecord.builder() @@ -197,4 +267,7 @@ 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 b2f8a3a..76c3128 100644 --- a/src/main/resources/db/init.sql +++ b/src/main/resources/db/init.sql @@ -8,198 +8,610 @@ CREATE DATABASE IF NOT EXISTS backtestify USE backtestify; --- --------------------------------------------------- --- 合约字典表(不分区) --- --------------------------------------------------- -DROP TABLE IF EXISTS t_instrument_dictionary; -CREATE TABLE t_instrument_dictionary ( - id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '合约自增ID', - exchange_id VARCHAR(16) DEFAULT NULL COMMENT '交易所代码 (如 SHFE)', - symbol VARCHAR(10) NOT NULL COMMENT '期货品种 (如 rb)', - contract_code VARCHAR(20) NOT NULL COMMENT '具体合约代码 (如 rb2610)', - price_scale INT UNSIGNED NOT NULL DEFAULT 100 COMMENT '价格放大倍数 (100表示保留2位小数)', - 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), - UNIQUE KEY uk_contract (contract_code), - KEY idx_symbol_main (symbol, is_main), - KEY idx_exchange_contract (exchange_id, contract_code) -) ENGINE=InnoDB DEFAULT CHARSET=ascii COMMENT='期货合约字典表'; +-- ---------------------------- +-- Table structure for bt_strategy_config +-- ---------------------------- +DROP TABLE IF EXISTS `bt_strategy_config`; +CREATE TABLE `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 '合约代码', + `contract_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '合约名称', + `direction` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '交易方向: LONG/SHORT', + `kline_period` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'K线周期: 1m/3m/5m/15m/30m/1h/4h/1d/1w', + `indicators` json NOT NULL COMMENT '技术指标列表, 如[\"MACD\",\"KDJ\"]', + `open_volume` int NOT NULL DEFAULT 1 COMMENT '开仓数量', + `volume_unit` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'LOT' COMMENT '数量单位: LOT/POSITION', + `stop_loss_value` decimal(18, 4) NULL DEFAULT NULL COMMENT '止损值', + `stop_loss_unit` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '止损单位: TICK/PERCENT', + `take_profit_value` decimal(18, 4) NULL DEFAULT NULL COMMENT '止盈值', + `take_profit_unit` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '止盈单位: TICK/PERCENT', + `backtest_period` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '回测区间: 1m/3m/6m/1y', + `status` 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, + INDEX `idx_user_id`(`user_id` ASC) USING BTREE, + INDEX `idx_contract_code`(`contract_code` ASC) USING BTREE, + INDEX `idx_create_time`(`create_time` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '策略配置表' ROW_FORMAT = Dynamic; --- --------------------------------------------------- --- K线数据表(每种周期一张表,按年分区;真实行情不包含3m) --- --------------------------------------------------- -DROP TABLE IF EXISTS t_kline_1m; -CREATE TABLE t_kline_1m ( - instrument_id SMALLINT UNSIGNED NOT NULL COMMENT '合约字典ID', - timestamp INT UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级,每根K线起始点)', - open INT NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', - high INT NOT NULL COMMENT '最高价 (实际价格 * price_scale)', - low INT NOT NULL COMMENT '最低价 (实际价格 * price_scale)', - close INT NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', - volume INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', - turnover BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', - open_interest INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', - PRIMARY KEY (instrument_id, timestamp) -) ENGINE=InnoDB DEFAULT CHARSET=ascii COMMENT='1分钟K线数据表' -PARTITION BY RANGE (timestamp) ( - PARTITION p2020 VALUES LESS THAN (1609459200), - PARTITION p2021 VALUES LESS THAN (1640995200), - PARTITION p2022 VALUES LESS THAN (1672531200), - PARTITION p2023 VALUES LESS THAN (1704067200), - PARTITION p2024 VALUES LESS THAN (1735689600), - PARTITION p2025 VALUES LESS THAN (1767225600), - PARTITION p2026 VALUES LESS THAN (1798761600), - PARTITION p2027 VALUES LESS THAN (1830297600), - PARTITION p2028 VALUES LESS THAN (1861920000), - PARTITION p2029 VALUES LESS THAN (1893456000), - PARTITION p2030 VALUES LESS THAN (1924992000), - PARTITION p2031 VALUES LESS THAN (1956528000), - PARTITION pmax VALUES LESS THAN MAXVALUE -); +-- ---------------------------- +-- Table structure for bt_strategy_result +-- ---------------------------- +DROP TABLE IF EXISTS `bt_strategy_result`; +CREATE TABLE `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 '初始资金', + `final_capital` decimal(18, 2) NOT NULL COMMENT '期末总资产', + `max_equity` decimal(18, 2) NOT NULL COMMENT '回测期间最高净值', + `min_equity` decimal(18, 2) NOT NULL COMMENT '回测期间最低净值', + `total_yield` decimal(10, 4) NOT NULL COMMENT '总收益率(%)', + `profit_amount` decimal(18, 2) NOT NULL COMMENT '收益金额', + `annualized_yield` decimal(10, 4) NOT NULL COMMENT '年化收益率(%)', + `trade_count` int NOT NULL DEFAULT 0 COMMENT '交易次数', + `max_drawdown` decimal(10, 4) NOT NULL COMMENT '最大回撤(%)', + `sharpe_ratio` decimal(10, 4) NOT NULL COMMENT '夏普比率', + `win_rate` decimal(10, 4) NOT NULL COMMENT '胜率(%)', + `start_date` date NOT NULL COMMENT '回测开始日期', + `end_date` date NOT NULL COMMENT '回测结束日期', + `daily_equity_curve` json NULL COMMENT '每日净值曲线 [{date,equity,yield}]', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_strategy_id`(`strategy_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '回测结果表' ROW_FORMAT = Dynamic; -DROP TABLE IF EXISTS t_kline_5m; -CREATE TABLE t_kline_5m LIKE t_kline_1m; -ALTER TABLE t_kline_5m COMMENT='5分钟K线数据表'; +-- ---------------------------- +-- Table structure for bt_trade_detail +-- ---------------------------- +DROP TABLE IF EXISTS `bt_trade_detail`; +CREATE TABLE `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', + `price` decimal(18, 4) NOT NULL COMMENT '成交价', + `volume` int NOT NULL COMMENT '成交数量', + `turnover` decimal(18, 2) NOT NULL COMMENT '成交金额', + `trade_time` datetime NOT NULL COMMENT '成交时间', + `kline_time` datetime NOT NULL COMMENT '对应K线时间', + `signal_type` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '信号类型: B/S', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_strategy_id`(`strategy_id` ASC) USING BTREE, + INDEX `idx_trade_time`(`trade_time` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '交易明细表' ROW_FORMAT = Dynamic; -DROP TABLE IF EXISTS t_kline_15m; -CREATE TABLE t_kline_15m LIKE t_kline_1m; -ALTER TABLE t_kline_15m COMMENT='15分钟K线数据表'; +-- ---------------------------- +-- Table structure for bt_user_signal +-- ---------------------------- +DROP TABLE IF EXISTS `bt_user_signal`; +CREATE TABLE `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', + `contract_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '合约代码', + `kline_period` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'K线周期', + `is_active` tinyint NOT NULL DEFAULT 1 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, + INDEX `idx_user_contract_period`(`user_id` ASC, `contract_code` ASC, `kline_period` ASC) USING BTREE, + INDEX `idx_strategy_id`(`strategy_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信号标记表' ROW_FORMAT = Dynamic; -DROP TABLE IF EXISTS t_kline_30m; -CREATE TABLE t_kline_30m LIKE t_kline_1m; -ALTER TABLE t_kline_30m COMMENT='30分钟K线数据表'; +-- ---------------------------- +-- Table structure for t_indicator_15m +-- ---------------------------- +DROP TABLE IF EXISTS `t_indicator_15m`; +CREATE TABLE `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', + `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10', + `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20', + `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60', + `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)', + `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨', + `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨', + `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6', + `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12', + `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20', + `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)', + `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA', + `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR', + `rsi6` decimal(10, 4) NULL DEFAULT NULL COMMENT '短期 RSI6', + `rsi12` decimal(10, 4) NULL DEFAULT NULL COMMENT '长期 RSI12', + `kdj_k` decimal(10, 4) NULL DEFAULT NULL COMMENT 'K值', + `kdj_d` decimal(10, 4) NULL DEFAULT NULL COMMENT 'D值', + `kdj_j` decimal(10, 4) NULL DEFAULT NULL COMMENT 'J值', + `create_at` int UNSIGNED NULL DEFAULT NULL COMMENT '记录创建时间', + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '15分钟指标表' ROW_FORMAT = DYNAMIC; -DROP TABLE IF EXISTS t_kline_1h; -CREATE TABLE t_kline_1h LIKE t_kline_1m; -ALTER TABLE t_kline_1h COMMENT='1小时K线数据表'; +-- ---------------------------- +-- Table structure for t_indicator_1d +-- ---------------------------- +DROP TABLE IF EXISTS `t_indicator_1d`; +CREATE TABLE `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', + `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10', + `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20', + `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60', + `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)', + `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨', + `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨', + `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6', + `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12', + `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20', + `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)', + `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA', + `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR', + `rsi6` decimal(10, 4) NULL DEFAULT NULL COMMENT '短期 RSI6', + `rsi12` decimal(10, 4) NULL DEFAULT NULL COMMENT '长期 RSI12', + `kdj_k` decimal(10, 4) NULL DEFAULT NULL COMMENT 'K值', + `kdj_d` decimal(10, 4) NULL DEFAULT NULL COMMENT 'D值', + `kdj_j` decimal(10, 4) NULL DEFAULT NULL COMMENT 'J值', + `create_at` int UNSIGNED NULL DEFAULT NULL COMMENT '记录创建时间', + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '日指标表' ROW_FORMAT = DYNAMIC; -DROP TABLE IF EXISTS t_kline_4h; -CREATE TABLE t_kline_4h LIKE t_kline_1m; -ALTER TABLE t_kline_4h COMMENT='4小时K线数据表'; +-- ---------------------------- +-- Table structure for t_indicator_1h +-- ---------------------------- +DROP TABLE IF EXISTS `t_indicator_1h`; +CREATE TABLE `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', + `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10', + `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20', + `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60', + `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)', + `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨', + `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨', + `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6', + `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12', + `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20', + `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)', + `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA', + `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR', + `rsi6` decimal(10, 4) NULL DEFAULT NULL COMMENT '短期 RSI6', + `rsi12` decimal(10, 4) NULL DEFAULT NULL COMMENT '长期 RSI12', + `kdj_k` decimal(10, 4) NULL DEFAULT NULL COMMENT 'K值', + `kdj_d` decimal(10, 4) NULL DEFAULT NULL COMMENT 'D值', + `kdj_j` decimal(10, 4) NULL DEFAULT NULL COMMENT 'J值', + `create_at` int UNSIGNED NULL DEFAULT NULL COMMENT '记录创建时间', + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '1小时指标数据表' ROW_FORMAT = DYNAMIC; -DROP TABLE IF EXISTS t_kline_1d; -CREATE TABLE t_kline_1d LIKE t_kline_1m; -ALTER TABLE t_kline_1d COMMENT='日K线数据表'; +-- ---------------------------- +-- Table structure for t_indicator_1m +-- ---------------------------- +DROP TABLE IF EXISTS `t_indicator_1m`; +CREATE TABLE `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, + `ma10` decimal(18, 6) NULL DEFAULT NULL, + `ma20` decimal(18, 6) NULL DEFAULT NULL, + `ma60` decimal(18, 6) NULL DEFAULT NULL, + `boll_mb` decimal(18, 6) NULL DEFAULT NULL, + `boll_up` decimal(18, 6) NULL DEFAULT NULL, + `boll_dn` decimal(18, 6) NULL DEFAULT NULL, + `ema6` decimal(18, 6) NULL DEFAULT NULL, + `ema12` decimal(18, 6) NULL DEFAULT NULL, + `ema20` decimal(18, 6) NULL DEFAULT NULL, + `macd_dif` decimal(18, 6) NULL DEFAULT NULL, + `macd_dea` decimal(18, 6) NULL DEFAULT NULL, + `macd_bar` decimal(18, 6) NULL DEFAULT NULL, + `rsi6` decimal(10, 4) NULL DEFAULT NULL, + `rsi12` decimal(10, 4) NULL DEFAULT NULL, + `kdj_k` decimal(10, 4) NULL DEFAULT NULL, + `kdj_d` decimal(10, 4) NULL DEFAULT NULL, + `kdj_j` decimal(10, 4) NULL DEFAULT NULL, + `create_at` int UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '1分钟指标数据表(按季度分区)' ROW_FORMAT = DYNAMIC; -DROP TABLE IF EXISTS t_kline_1w; -CREATE TABLE t_kline_1w LIKE t_kline_1m; -ALTER TABLE t_kline_1w COMMENT='周K线数据表'; +-- ---------------------------- +-- Table structure for t_indicator_1mo +-- ---------------------------- +DROP TABLE IF EXISTS `t_indicator_1mo`; +CREATE TABLE `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', + `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10', + `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20', + `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60', + `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)', + `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨', + `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨', + `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6', + `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12', + `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20', + `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)', + `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA', + `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR', + `rsi6` decimal(10, 4) NULL DEFAULT NULL COMMENT '短期 RSI6', + `rsi12` decimal(10, 4) NULL DEFAULT NULL COMMENT '长期 RSI12', + `kdj_k` decimal(10, 4) NULL DEFAULT NULL COMMENT 'K值', + `kdj_d` decimal(10, 4) NULL DEFAULT NULL COMMENT 'D值', + `kdj_j` decimal(10, 4) NULL DEFAULT NULL COMMENT 'J值', + `create_at` int UNSIGNED NULL DEFAULT NULL COMMENT '记录创建时间', + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '月指标表' ROW_FORMAT = DYNAMIC; --- --------------------------------------------------- --- 行情同步日志表 --- --------------------------------------------------- -DROP TABLE IF EXISTS t_market_data_sync_log; -CREATE TABLE t_market_data_sync_log ( - id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', - sync_type VARCHAR(32) NOT NULL COMMENT '同步类型: CONTRACT/KLINE', - period VARCHAR(10) DEFAULT NULL COMMENT 'K线周期', - contract_code VARCHAR(20) DEFAULT NULL COMMENT '合约代码', - sync_date DATE DEFAULT NULL COMMENT '同步日期', - status VARCHAR(16) NOT NULL COMMENT '状态: SUCCESS/FAILED', - success_count INT NOT NULL DEFAULT 0 COMMENT '成功条数', - error_message TEXT DEFAULT NULL COMMENT '错误信息', - start_time DATETIME NOT NULL COMMENT '开始时间', - end_time DATETIME DEFAULT NULL COMMENT '结束时间', - create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (id), - KEY idx_sync_date (sync_date), - KEY idx_contract_period (contract_code, period), - KEY idx_status (status) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='行情同步日志表'; +-- ---------------------------- +-- Table structure for t_indicator_1w +-- ---------------------------- +DROP TABLE IF EXISTS `t_indicator_1w`; +CREATE TABLE `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', + `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10', + `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20', + `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60', + `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)', + `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨', + `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨', + `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6', + `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12', + `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20', + `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)', + `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA', + `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR', + `rsi6` decimal(10, 4) NULL DEFAULT NULL COMMENT '短期 RSI6', + `rsi12` decimal(10, 4) NULL DEFAULT NULL COMMENT '长期 RSI12', + `kdj_k` decimal(10, 4) NULL DEFAULT NULL COMMENT 'K值', + `kdj_d` decimal(10, 4) NULL DEFAULT NULL COMMENT 'D值', + `kdj_j` decimal(10, 4) NULL DEFAULT NULL COMMENT 'J值', + `create_at` int UNSIGNED NULL DEFAULT NULL COMMENT '记录创建时间', + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '周指标表' ROW_FORMAT = DYNAMIC; --- --------------------------------------------------- --- 策略配置表 --- --------------------------------------------------- -DROP TABLE IF EXISTS bt_strategy_config; -CREATE TABLE bt_strategy_config ( - id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', - user_id BIGINT NOT NULL COMMENT '用户ID', - contract_code VARCHAR(32) NOT NULL COMMENT '合约代码', - contract_name VARCHAR(64) NOT NULL COMMENT '合约名称', - direction VARCHAR(10) NOT NULL COMMENT '交易方向: LONG/SHORT', - kline_period VARCHAR(10) NOT NULL COMMENT 'K线周期: 1m/3m/5m/15m/30m/1h/4h/1d/1w', - indicators JSON NOT NULL COMMENT '技术指标列表, 如["MACD","KDJ"]', - open_volume INT NOT NULL DEFAULT 1 COMMENT '开仓数量', - volume_unit VARCHAR(10) NOT NULL DEFAULT 'LOT' COMMENT '数量单位: LOT/POSITION', - stop_loss_value DECIMAL(18,4) DEFAULT NULL COMMENT '止损值', - stop_loss_unit VARCHAR(10) DEFAULT NULL COMMENT '止损单位: TICK/PERCENT', - take_profit_value DECIMAL(18,4) DEFAULT NULL COMMENT '止盈值', - take_profit_unit VARCHAR(10) DEFAULT NULL COMMENT '止盈单位: TICK/PERCENT', - backtest_period VARCHAR(10) NOT NULL COMMENT '回测区间: 1m/3m/6m/1y', - status 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), - INDEX idx_user_id (user_id), - INDEX idx_contract_code (contract_code), - INDEX idx_create_time (create_time) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='策略配置表'; +-- ---------------------------- +-- Table structure for t_indicator_30m +-- ---------------------------- +DROP TABLE IF EXISTS `t_indicator_30m`; +CREATE TABLE `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', + `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10', + `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20', + `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60', + `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)', + `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨', + `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨', + `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6', + `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12', + `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20', + `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)', + `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA', + `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR', + `rsi6` decimal(10, 4) NULL DEFAULT NULL COMMENT '短期 RSI6', + `rsi12` decimal(10, 4) NULL DEFAULT NULL COMMENT '长期 RSI12', + `kdj_k` decimal(10, 4) NULL DEFAULT NULL COMMENT 'K值', + `kdj_d` decimal(10, 4) NULL DEFAULT NULL COMMENT 'D值', + `kdj_j` decimal(10, 4) NULL DEFAULT NULL COMMENT 'J值', + `create_at` int UNSIGNED NULL DEFAULT NULL COMMENT '记录创建时间', + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '30分钟指标表' ROW_FORMAT = DYNAMIC; --- --------------------------------------------------- --- 回测结果表 --- --------------------------------------------------- -DROP TABLE IF EXISTS bt_strategy_result; -CREATE TABLE 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 '初始资金', - final_capital DECIMAL(18,2) NOT NULL COMMENT '期末总资产', - max_equity DECIMAL(18,2) NOT NULL COMMENT '回测期间最高净值', - min_equity DECIMAL(18,2) NOT NULL COMMENT '回测期间最低净值', - total_yield DECIMAL(10,4) NOT NULL COMMENT '总收益率(%)', - profit_amount DECIMAL(18,2) NOT NULL COMMENT '收益金额', - annualized_yield DECIMAL(10,4) NOT NULL COMMENT '年化收益率(%)', - trade_count INT NOT NULL DEFAULT 0 COMMENT '交易次数', - max_drawdown DECIMAL(10,4) NOT NULL COMMENT '最大回撤(%)', - sharpe_ratio DECIMAL(10,4) NOT NULL COMMENT '夏普比率', - win_rate DECIMAL(10,4) NOT NULL COMMENT '胜率(%)', - start_date DATE NOT NULL COMMENT '回测开始日期', - end_date DATE NOT NULL COMMENT '回测结束日期', - daily_equity_curve JSON DEFAULT NULL COMMENT '每日净值曲线 [{date,equity,yield}]', - create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (id), - UNIQUE INDEX uk_strategy_id (strategy_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='回测结果表'; +-- ---------------------------- +-- Table structure for t_indicator_3m +-- ---------------------------- +DROP TABLE IF EXISTS `t_indicator_3m`; +CREATE TABLE `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', + `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10', + `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20', + `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60', + `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)', + `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨', + `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨', + `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6', + `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12', + `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20', + `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)', + `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA', + `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR', + `rsi6` decimal(10, 4) NULL DEFAULT NULL COMMENT '短期 RSI6', + `rsi12` decimal(10, 4) NULL DEFAULT NULL COMMENT '长期 RSI12', + `kdj_k` decimal(10, 4) NULL DEFAULT NULL COMMENT 'K值', + `kdj_d` decimal(10, 4) NULL DEFAULT NULL COMMENT 'D值', + `kdj_j` decimal(10, 4) NULL DEFAULT NULL COMMENT 'J值', + `create_at` int UNSIGNED NULL DEFAULT NULL COMMENT '记录创建时间', + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '3分钟指标表' ROW_FORMAT = DYNAMIC; --- --------------------------------------------------- --- 交易明细表 --- --------------------------------------------------- -DROP TABLE IF EXISTS bt_trade_detail; -CREATE TABLE bt_trade_detail ( - id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', - strategy_id BIGINT NOT NULL COMMENT '关联策略ID', - action VARCHAR(20) NOT NULL COMMENT '操作: BUY_OPEN/SELL_CLOSE/SELL_OPEN/BUY_CLOSE', - price DECIMAL(18,4) NOT NULL COMMENT '成交价', - volume INT NOT NULL COMMENT '成交数量', - turnover DECIMAL(18,2) NOT NULL COMMENT '成交金额', - trade_time DATETIME NOT NULL COMMENT '成交时间', - kline_time DATETIME NOT NULL COMMENT '对应K线时间', - signal_type VARCHAR(5) NOT NULL COMMENT '信号类型: B/S', - create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (id), - INDEX idx_strategy_id (strategy_id), - INDEX idx_trade_time (trade_time) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='交易明细表'; +-- ---------------------------- +-- Table structure for t_indicator_4h +-- ---------------------------- +DROP TABLE IF EXISTS `t_indicator_4h`; +CREATE TABLE `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', + `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10', + `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20', + `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60', + `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)', + `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨', + `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨', + `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6', + `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12', + `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20', + `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)', + `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA', + `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR', + `rsi6` decimal(10, 4) NULL DEFAULT NULL COMMENT '短期 RSI6', + `rsi12` decimal(10, 4) NULL DEFAULT NULL COMMENT '长期 RSI12', + `kdj_k` decimal(10, 4) NULL DEFAULT NULL COMMENT 'K值', + `kdj_d` decimal(10, 4) NULL DEFAULT NULL COMMENT 'D值', + `kdj_j` decimal(10, 4) NULL DEFAULT NULL COMMENT 'J值', + `create_at` int UNSIGNED NULL DEFAULT NULL COMMENT '记录创建时间', + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '4小时指标表' ROW_FORMAT = DYNAMIC; --- --------------------------------------------------- --- 用户信号标记表 --- --------------------------------------------------- -DROP TABLE IF EXISTS bt_user_signal; -CREATE TABLE 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', - contract_code VARCHAR(32) NOT NULL COMMENT '合约代码', - kline_period VARCHAR(10) NOT NULL COMMENT 'K线周期', - is_active TINYINT NOT NULL DEFAULT 1 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), - INDEX idx_user_contract_period (user_id, contract_code, kline_period), - INDEX idx_strategy_id (strategy_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户信号标记表'; +-- ---------------------------- +-- Table structure for t_indicator_5m +-- ---------------------------- +DROP TABLE IF EXISTS `t_indicator_5m`; +CREATE TABLE `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', + `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10', + `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20', + `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60', + `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)', + `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨', + `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨', + `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6', + `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12', + `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20', + `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)', + `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA', + `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR', + `rsi6` decimal(10, 4) NULL DEFAULT NULL COMMENT '短期 RSI6', + `rsi12` decimal(10, 4) NULL DEFAULT NULL COMMENT '长期 RSI12', + `kdj_k` decimal(10, 4) NULL DEFAULT NULL COMMENT 'K值', + `kdj_d` decimal(10, 4) NULL DEFAULT NULL COMMENT 'D值', + `kdj_j` decimal(10, 4) NULL DEFAULT NULL COMMENT 'J值', + `create_at` int UNSIGNED NULL DEFAULT NULL COMMENT '记录创建时间', + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '5分钟指标表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for t_instrument_dictionary +-- ---------------------------- +DROP TABLE IF EXISTS `t_instrument_dictionary`; +CREATE TABLE `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, + `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', + `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, + 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; + +-- ---------------------------- +-- Table structure for t_kline_15m +-- ---------------------------- +DROP TABLE IF EXISTS `t_kline_15m`; +CREATE TABLE `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)', + `high` int NOT NULL COMMENT '最高价 (实际价格 * price_scale)', + `low` int NOT NULL COMMENT '最低价 (实际价格 * price_scale)', + `close` int NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', + `volume` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', + `turnover` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', + `open_interest` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', + `create_at` int UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '15分钟K线数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_kline_1d +-- ---------------------------- +DROP TABLE IF EXISTS `t_kline_1d`; +CREATE TABLE `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)', + `high` int NOT NULL COMMENT '最高价 (实际价格 * price_scale)', + `low` int NOT NULL COMMENT '最低价 (实际价格 * price_scale)', + `close` int NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', + `settle` int NULL DEFAULT NULL, + `volume` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', + `turnover` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', + `open_interest` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', + `create_at` int UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '日K线数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_kline_1h +-- ---------------------------- +DROP TABLE IF EXISTS `t_kline_1h`; +CREATE TABLE `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)', + `high` int NOT NULL COMMENT '最高价 (实际价格 * price_scale)', + `low` int NOT NULL COMMENT '最低价 (实际价格 * price_scale)', + `close` int NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', + `volume` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', + `turnover` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', + `open_interest` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', + `create_at` int UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '1小时K线数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_kline_1m +-- ---------------------------- +DROP TABLE IF EXISTS `t_kline_1m`; +CREATE TABLE `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)', + `high` int NOT NULL COMMENT '最高价 (实际价格 * price_scale)', + `low` int NOT NULL COMMENT '最低价 (实际价格 * price_scale)', + `close` int NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', + `volume` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', + `turnover` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', + `open_interest` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', + `create_at` int UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '1分钟K线数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_kline_1mo +-- ---------------------------- +DROP TABLE IF EXISTS `t_kline_1mo`; +CREATE TABLE `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)', + `high` int NOT NULL COMMENT '最高价 (实际价格 * price_scale)', + `low` int NOT NULL COMMENT '最低价 (实际价格 * price_scale)', + `close` int NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', + `volume` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', + `turnover` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', + `open_interest` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', + `create_at` int UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '月K线数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_kline_1w +-- ---------------------------- +DROP TABLE IF EXISTS `t_kline_1w`; +CREATE TABLE `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)', + `high` int NOT NULL COMMENT '最高价 (实际价格 * price_scale)', + `low` int NOT NULL COMMENT '最低价 (实际价格 * price_scale)', + `close` int NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', + `volume` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', + `turnover` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', + `open_interest` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', + `create_at` int UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '周K线数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_kline_30m +-- ---------------------------- +DROP TABLE IF EXISTS `t_kline_30m`; +CREATE TABLE `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)', + `high` int NOT NULL COMMENT '最高价 (实际价格 * price_scale)', + `low` int NOT NULL COMMENT '最低价 (实际价格 * price_scale)', + `close` int NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', + `volume` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', + `turnover` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', + `open_interest` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', + `create_at` int UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '30分钟K线数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_kline_3m +-- ---------------------------- +DROP TABLE IF EXISTS `t_kline_3m`; +CREATE TABLE `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)', + `high` int NOT NULL COMMENT '最高价 (实际价格 * price_scale)', + `low` int NOT NULL COMMENT '最低价 (实际价格 * price_scale)', + `close` int NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', + `volume` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', + `turnover` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', + `open_interest` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', + `create_at` int UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '3分钟K线数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_kline_4h +-- ---------------------------- +DROP TABLE IF EXISTS `t_kline_4h`; +CREATE TABLE `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)', + `high` int NOT NULL COMMENT '最高价 (实际价格 * price_scale)', + `low` int NOT NULL COMMENT '最低价 (实际价格 * price_scale)', + `close` int NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', + `volume` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', + `turnover` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', + `open_interest` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', + `create_at` int UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '4小时K线数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_kline_5m +-- ---------------------------- +DROP TABLE IF EXISTS `t_kline_5m`; +CREATE TABLE `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)', + `high` int NOT NULL COMMENT '最高价 (实际价格 * price_scale)', + `low` int NOT NULL COMMENT '最低价 (实际价格 * price_scale)', + `close` int NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', + `volume` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', + `turnover` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', + `open_interest` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', + `create_at` int UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '5分钟K线数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for t_market_data_sync_log +-- ---------------------------- +DROP TABLE IF EXISTS `t_market_data_sync_log`; +CREATE TABLE `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线周期', + `contract_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '合约代码', + `sync_date` date NULL DEFAULT NULL COMMENT '同步日期', + `status` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '状态: SUCCESS/FAILED', + `success_count` int NOT NULL DEFAULT 0 COMMENT '成功条数', + `error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '错误信息', + `start_time` datetime NOT NULL COMMENT '开始时间', + `end_time` datetime NULL DEFAULT NULL COMMENT '结束时间', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_sync_date`(`sync_date` ASC) USING BTREE, + INDEX `idx_contract_period`(`contract_code` ASC, `f_period` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '行情同步日志表' ROW_FORMAT = Dynamic; diff --git a/src/test/java/com/yangwale/backtestify/service/market/convert/PriceScaleConverterTest.java b/src/test/java/com/yangwale/backtestify/service/market/convert/PriceScaleConverterTest.java index a8ee8d3..c0fb6b0 100644 --- a/src/test/java/com/yangwale/backtestify/service/market/convert/PriceScaleConverterTest.java +++ b/src/test/java/com/yangwale/backtestify/service/market/convert/PriceScaleConverterTest.java @@ -27,4 +27,10 @@ class PriceScaleConverterTest { void rejectsBlankPrice() { assertThrows(IllegalArgumentException.class, () -> PriceScaleConverter.toScaled("", 100)); } + + @Test + void normalizesNegativeUnsignedMarketQuantityToZero() { + assertEquals(0L, PriceScaleConverter.toLong("-1")); + assertEquals(12L, PriceScaleConverter.toLong("12")); + } } 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 c79329c..503a244 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,6 +1,8 @@ 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.metadata.TableInfoHelper; import com.yangwale.backtestify.config.MarketDataProperties; import com.yangwale.backtestify.entity.InstrumentDictionary; import com.yangwale.backtestify.entity.KLineRecord; @@ -13,20 +15,130 @@ 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.mockito.ArgumentCaptor; +import org.springframework.test.util.ReflectionTestUtils; +import java.math.BigDecimal; +import java.util.ArrayList; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class MarketDataSyncServiceTest { @Test - void calculatesIncrementalIndicatorsAfterKLinesArePersisted() { + void refreshAdoptsApiContractCodeCasingWithoutCreatingDuplicate() { + TableInfoHelper.initTableInfo( + new MapperBuilderAssistant(new MybatisConfiguration(), ""), + InstrumentDictionary.class); + MarketDataProperties properties = new MarketDataProperties(); + InstrumentDictionaryMapper instrumentMapper = mock(InstrumentDictionaryMapper.class); + KLineMapper kLineMapper = mock(KLineMapper.class); + MarketDataSyncLogMapper syncLogMapper = mock(MarketDataSyncLogMapper.class); + IndicatorCalculationService indicatorService = mock(IndicatorCalculationService.class); + InstrumentDictionary existing = new InstrumentDictionary(); + existing.setId(7); + existing.setExchangeId("CZCE"); + existing.setContractCode("ap610"); + existing.setPriceScale(1000); + when(instrumentMapper.selectByExchangeAndContractCodeIgnoreCase("CZCE", "AP610")) + .thenReturn(List.of(existing)); + + MarketDataSyncService service = service(properties, + new CanonicalQuotationClient(properties), instrumentMapper, kLineMapper, + syncLogMapper, indicatorService); + + service.refreshMainContracts(); + + ArgumentCaptor updated = ArgumentCaptor.forClass(InstrumentDictionary.class); + verify(instrumentMapper).updateById(updated.capture()); + assertEquals("AP610", updated.getValue().getContractCode()); + verify(instrumentMapper, never()).insert(any(InstrumentDictionary.class)); + } + + @Test + void repairFindsContractIgnoringCallerCasingAndUsesStoredCanonicalCode() { + MarketDataProperties properties = new MarketDataProperties(); + properties.getSync().setPeriods(List.of("1d")); + InstrumentDictionaryMapper instrumentMapper = mock(InstrumentDictionaryMapper.class); + KLineMapper kLineMapper = mock(KLineMapper.class); + MarketDataSyncLogMapper syncLogMapper = mock(MarketDataSyncLogMapper.class); + IndicatorCalculationService indicatorService = mock(IndicatorCalculationService.class); + InstrumentDictionary instrument = new InstrumentDictionary(); + instrument.setId(7); + instrument.setExchangeId("CZCE"); + instrument.setContractCode("AP610"); + instrument.setPriceScale(1000); + when(instrumentMapper.selectActiveByContractCodeIgnoreCase("ap610")) + .thenReturn(List.of(instrument)); + CapturingRepairQuotationClient quotationClient = new CapturingRepairQuotationClient(properties); + + MarketDataSyncService service = service(properties, quotationClient, + instrumentMapper, kLineMapper, syncLogMapper, indicatorService); + + service.repairContractFrom("ap610", 100L); + + assertEquals(List.of("AP610"), quotationClient.requestedCodes); + verify(indicatorService).rebuild("1d", 7); + } + + @Test + void refreshPreservesExistingPriceScale() { + MarketDataProperties properties = new MarketDataProperties(); + InstrumentDictionaryMapper instrumentMapper = mock(InstrumentDictionaryMapper.class); + KLineMapper kLineMapper = mock(KLineMapper.class); + MarketDataSyncLogMapper syncLogMapper = mock(MarketDataSyncLogMapper.class); + IndicatorCalculationService indicatorService = mock(IndicatorCalculationService.class); + InstrumentDictionary existing = new InstrumentDictionary(); + existing.setId(7); + existing.setContractCode("rb2610"); + existing.setPriceScale(500); + when(instrumentMapper.selectByExchangeAndContractCodeIgnoreCase("SHFE", "rb2610")) + .thenReturn(List.of(existing)); + + MarketDataSyncService service = service(properties, + new EmptyQuotationClient(properties), instrumentMapper, kLineMapper, + syncLogMapper, indicatorService); + + ReflectionTestUtils.invokeMethod(service, "upsertInstrument", goodsItem(), new BigDecimal("0.5")); + + 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 + void refreshUsesDefaultScaleForNewContract() { + MarketDataProperties properties = new MarketDataProperties(); + InstrumentDictionaryMapper instrumentMapper = mock(InstrumentDictionaryMapper.class); + KLineMapper kLineMapper = mock(KLineMapper.class); + MarketDataSyncLogMapper syncLogMapper = mock(MarketDataSyncLogMapper.class); + IndicatorCalculationService indicatorService = mock(IndicatorCalculationService.class); + + MarketDataSyncService service = service(properties, + new EmptyQuotationClient(properties), instrumentMapper, kLineMapper, + syncLogMapper, indicatorService); + + ReflectionTestUtils.invokeMethod(service, "upsertInstrument", goodsItem(), new BigDecimal("0.5")); + + 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 + void catchesUpFromLatestDatabaseTimestampAcrossMultiplePages() { MarketDataProperties properties = new MarketDataProperties(); properties.getSync().setPeriods(List.of("1d")); InstrumentDictionaryMapper instrumentMapper = mock(InstrumentDictionaryMapper.class); @@ -40,11 +152,14 @@ class MarketDataSyncServiceTest { instrument.setPriceScale(100); instrument.setIsMain(1); when(instrumentMapper.selectList(any(Wrapper.class))).thenReturn(List.of(instrument)); - when(kLineMapper.upsertBatch(eq("t_kline_1d"), anyList())).thenReturn(1); + when(kLineMapper.selectLatestTimestamp("t_kline_1d", 7)).thenReturn(100L); + when(kLineMapper.upsertBatch(eq("t_kline_1d"), anyList())) + .thenAnswer(invocation -> invocation.>getArgument(1).size()); + FakeQuotationClient quotationClient = new FakeQuotationClient(properties); MarketDataSyncService service = new MarketDataSyncService( properties, - new FakeQuotationClient(properties), + quotationClient, instrumentMapper, kLineMapper, syncLogMapper, @@ -58,13 +173,130 @@ class MarketDataSyncServiceTest { service.syncIncrementalForMainContracts(); - verify(kLineMapper).upsertBatch(eq("t_kline_1d"), anyList()); + ArgumentCaptor> batches = ArgumentCaptor.forClass(List.class); + verify(kLineMapper, times(2)).upsertBatch(eq("t_kline_1d"), batches.capture()); + assertEquals(List.of(101L, 102L), batches.getAllValues().get(0).stream() + .map(KLineRecord::getKTime).toList()); + assertEquals(List.of(103L), batches.getAllValues().get(1).stream() + .map(KLineRecord::getKTime).toList()); + assertEquals(List.of(100L, 102L, 103L), quotationClient.requestedCursors); verify(indicatorService).updateIncremental("1d", 7); verify(syncLogMapper, org.mockito.Mockito.atLeastOnce()).insert(any(MarketDataSyncLog.class)); } + @Test + void initializesIndicatorsEvenWhenMarketApiHasNoNewBars() { + MarketDataProperties properties = new MarketDataProperties(); + properties.getSync().setPeriods(List.of("1d")); + InstrumentDictionaryMapper instrumentMapper = mock(InstrumentDictionaryMapper.class); + KLineMapper kLineMapper = mock(KLineMapper.class); + MarketDataSyncLogMapper syncLogMapper = mock(MarketDataSyncLogMapper.class); + IndicatorCalculationService indicatorService = mock(IndicatorCalculationService.class); + CnQuotationClient quotationClient = new EmptyQuotationClient(properties); + InstrumentDictionary instrument = new InstrumentDictionary(); + instrument.setId(7); + instrument.setExchangeId("SHFE"); + instrument.setContractCode("rb2610"); + instrument.setPriceScale(100); + instrument.setIsMain(1); + when(instrumentMapper.selectList(any(Wrapper.class))).thenReturn(List.of(instrument)); + when(kLineMapper.selectLatestTimestamp("t_kline_1d", 7)).thenReturn(100L); + MarketDataSyncService service = new MarketDataSyncService( + properties, quotationClient, instrumentMapper, kLineMapper, syncLogMapper, + new KLineTableResolver(), indicatorService) { + @Override + public void refreshMainContracts() { + // Contract refresh is outside this test's synchronization seam. + } + }; + + service.syncIncrementalForMainContracts(); + + verify(kLineMapper, never()).upsertBatch(eq("t_kline_1d"), anyList()); + verify(indicatorService).updateIncremental("1d", 7); + } + + @Test + void repairRewritesExistingTimestampUsingContractScaleAndRebuildsIndicators() { + MarketDataProperties properties = new MarketDataProperties(); + properties.getSync().setPeriods(List.of("1d")); + InstrumentDictionaryMapper instrumentMapper = mock(InstrumentDictionaryMapper.class); + KLineMapper kLineMapper = mock(KLineMapper.class); + MarketDataSyncLogMapper syncLogMapper = mock(MarketDataSyncLogMapper.class); + IndicatorCalculationService indicatorService = mock(IndicatorCalculationService.class); + InstrumentDictionary instrument = new InstrumentDictionary(); + instrument.setId(7); + instrument.setExchangeId("SHFE"); + instrument.setContractCode("rb2610"); + instrument.setPriceScale(1000); + instrument.setIsMain(1); + when(instrumentMapper.selectList(any(Wrapper.class))).thenReturn(List.of(instrument)); + + MarketDataSyncService service = new MarketDataSyncService( + properties, new RepairQuotationClient(properties), instrumentMapper, + kLineMapper, syncLogMapper, new KLineTableResolver(), indicatorService) { + @Override + public void refreshMainContracts() { + // Contract refresh is outside this test's synchronization seam. + } + }; + + service.repairMainContractsFrom(100L); + + ArgumentCaptor> records = ArgumentCaptor.forClass(List.class); + verify(kLineMapper).upsertBatch(eq("t_kline_1d"), records.capture()); + assertEquals(100L, records.getValue().getFirst().getKTime()); + assertEquals(100500, records.getValue().getFirst().getClose()); + verify(indicatorService).rebuild("1d", 7); + verify(indicatorService, never()).updateIncremental("1d", 7); + } + + @Test + void repairsOnlyRequestedContractFromForcedCursor() { + MarketDataProperties properties = new MarketDataProperties(); + properties.getSync().setPeriods(List.of("1d")); + InstrumentDictionaryMapper instrumentMapper = mock(InstrumentDictionaryMapper.class); + KLineMapper kLineMapper = mock(KLineMapper.class); + MarketDataSyncLogMapper syncLogMapper = mock(MarketDataSyncLogMapper.class); + IndicatorCalculationService indicatorService = mock(IndicatorCalculationService.class); + InstrumentDictionary instrument = new InstrumentDictionary(); + instrument.setId(7); + instrument.setExchangeId("SHFE"); + instrument.setContractCode("rb2610"); + instrument.setPriceScale(1000); + when(instrumentMapper.selectActiveByContractCodeIgnoreCase("rb2610")) + .thenReturn(List.of(instrument)); + + MarketDataSyncService service = service(properties, new RepairQuotationClient(properties), + instrumentMapper, kLineMapper, syncLogMapper, indicatorService); + + service.repairContractFrom("rb2610", 100L); + + verify(kLineMapper).upsertBatch(eq("t_kline_1d"), anyList()); + verify(indicatorService).rebuild("1d", 7); + verify(instrumentMapper, never()).selectList(any(Wrapper.class)); + } + + private MarketDataSyncService service(MarketDataProperties properties, + CnQuotationClient quotationClient, + InstrumentDictionaryMapper instrumentMapper, + KLineMapper kLineMapper, + MarketDataSyncLogMapper syncLogMapper, + IndicatorCalculationService indicatorService) { + return new MarketDataSyncService( + properties, quotationClient, instrumentMapper, kLineMapper, syncLogMapper, + new KLineTableResolver(), indicatorService); + } + + private CnQuotationModels.GoodsItem goodsItem() { + return new CnQuotationModels.GoodsItem( + "SHFE", "rb", "rb", "螺纹钢", "rb2610", 1); + } + private static final class FakeQuotationClient extends CnQuotationClient { + private final List requestedCursors = new ArrayList<>(); + private FakeQuotationClient(MarketDataProperties properties) { super(properties); } @@ -77,9 +309,86 @@ class MarketDataSyncServiceTest { @Override public List getKChartByDate( String excode, String code, String period, long date, String direction) { - return List.of(new CnQuotationModels.KChartItem( + requestedCursors.add(date); + if (date == 100L) { + return List.of(item(101L), item(102L)); + } + if (date == 102L) { + return List.of(item(103L)); + } + return List.of(); + } + + private CnQuotationModels.KChartItem item(long timestamp) { + return new CnQuotationModels.KChartItem( null, "100.00", "101.00", "99.00", "100.50", - "10", "1000", date + 1, "20", null)); + "10", "1000", timestamp, "20", null); + } + } + + private static class EmptyQuotationClient extends CnQuotationClient { + + protected EmptyQuotationClient(MarketDataProperties properties) { + super(properties); + } + + @Override + public List getKChartByDate( + String excode, String code, String period, long date, String direction) { + return List.of(); + } + } + + private static final class RepairQuotationClient extends EmptyQuotationClient { + + private RepairQuotationClient(MarketDataProperties properties) { + super(properties); + } + + @Override + public List getKChartByDate( + String excode, String code, String period, long date, String direction) { + if (date == 99L) { + return List.of(new CnQuotationModels.KChartItem( + null, "100.00", "101.00", "99.00", "100.50", + "10", "1000", 100L, "20", null)); + } + return List.of(); + } + } + + private static final class CanonicalQuotationClient extends EmptyQuotationClient { + + private CanonicalQuotationClient(MarketDataProperties properties) { + super(properties); + } + + @Override + public List listMainContracts() { + 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 { + + private final List requestedCodes = new ArrayList<>(); + + private CapturingRepairQuotationClient(MarketDataProperties properties) { + super(properties); + } + + @Override + public List getKChartByDate( + String excode, String code, String period, long date, String direction) { + requestedCodes.add(code); + return List.of(); } } } diff --git a/src/test/resources/schema-test.sql b/src/test/resources/schema-test.sql index 3f7f6a2..e3d98ee 100644 --- a/src/test/resources/schema-test.sql +++ b/src/test/resources/schema-test.sql @@ -4,7 +4,8 @@ CREATE TABLE IF NOT EXISTS t_instrument_dictionary ( exchange_id VARCHAR(16), symbol VARCHAR(10) NOT NULL, contract_code VARCHAR(20) NOT NULL, - price_scale INT NOT NULL DEFAULT 100, + price_scale INT NOT NULL DEFAULT 1000, + price_tick DECIMAL(18, 6) NOT NULL DEFAULT 1, is_main TINYINT NOT NULL DEFAULT 0, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,