Normalize market contract synchronization

This commit is contained in:
Lee 2026-07-27 23:24:27 +08:00
parent b5e34b6d18
commit 10c434013b
12 changed files with 1102 additions and 254 deletions

View File

@ -7,6 +7,8 @@ import com.yangwale.backtestify.common.BaseEntity;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.math.BigDecimal;
/** /**
* 合约字典表 * 合约字典表
*/ */
@ -30,6 +32,9 @@ public class InstrumentDictionary extends BaseEntity {
/** 价格放大倍数 */ /** 价格放大倍数 */
private Integer priceScale; private Integer priceScale;
/** 最小变动价位 */
private BigDecimal priceTick;
/** 是否当前主力合约0-否1-是 */ /** 是否当前主力合约0-否1-是 */
private Integer isMain; private Integer isMain;
} }

View File

@ -1,6 +1,7 @@
package com.yangwale.backtestify.entity; package com.yangwale.backtestify.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter; import lombok.Getter;
@ -22,6 +23,7 @@ public class MarketDataSyncLog {
private String syncType; private String syncType;
@TableField("f_period")
private String period; private String period;
private String contractCode; private String contractCode;

View File

@ -3,7 +3,32 @@ package com.yangwale.backtestify.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yangwale.backtestify.entity.InstrumentDictionary; import com.yangwale.backtestify.entity.InstrumentDictionary;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper @Mapper
public interface InstrumentDictionaryMapper extends BaseMapper<InstrumentDictionary> { public interface InstrumentDictionaryMapper extends BaseMapper<InstrumentDictionary> {
@Select("""
SELECT *
FROM t_instrument_dictionary
WHERE exchange_id = #{exchangeId}
AND LOWER(contract_code) = LOWER(#{contractCode})
ORDER BY id
""")
List<InstrumentDictionary> 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<InstrumentDictionary> selectActiveByContractCodeIgnoreCase(
@Param("contractCode") String contractCode);
} }

View File

@ -24,6 +24,14 @@ public interface KLineMapper {
@Param("startTimestamp") long startTimestamp, @Param("startTimestamp") long startTimestamp,
@Param("endTimestamp") long endTimestamp); @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("""
SELECT instrument_id, k_time, open, high, low, close, SELECT instrument_id, k_time, open, high, low, close,
volume, turnover, open_interest volume, turnover, open_interest

View File

@ -1,7 +1,7 @@
package com.yangwale.backtestify.service.market.client; package com.yangwale.backtestify.service.market.client;
import com.alibaba.fastjson2.JSON; 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.BusinessException;
import com.yangwale.backtestify.common.ErrorCode; import com.yangwale.backtestify.common.ErrorCode;
import com.yangwale.backtestify.config.MarketDataProperties; import com.yangwale.backtestify.config.MarketDataProperties;
@ -33,11 +33,10 @@ public class CnQuotationClient {
String body = get(urlBuilder("goods/list") String body = get(urlBuilder("goods/list")
.addQueryParameter("pageSize", "-1") .addQueryParameter("pageSize", "-1")
.build()); .build());
CnQuotationModels.ResultModel<CnQuotationModels.GoodsPage> result = JSON.parseObject(body, JSONObject data = parseData(body);
new TypeReference<CnQuotationModels.ResultModel<CnQuotationModels.GoodsPage>>() { return data == null || data.getJSONArray("list") == null
}); ? List.of()
CnQuotationModels.GoodsPage data = unwrap(result); : data.getJSONArray("list").toJavaList(CnQuotationModels.GoodsItem.class);
return data == null || data.list() == null ? List.of() : data.list();
} }
public List<CnQuotationModels.KChartItem> getKChart(String excode, String code, String period) { public List<CnQuotationModels.KChartItem> getKChart(String excode, String code, String period) {
@ -47,11 +46,15 @@ public class CnQuotationClient {
.addQueryParameter("code", code) .addQueryParameter("code", code)
.addQueryParameter("type", String.valueOf(type)) .addQueryParameter("type", String.valueOf(type))
.build()); .build());
CnQuotationModels.ResultModel<CnQuotationModels.KChartResult> result = JSON.parseObject(body, return parseKChartItems(body);
new TypeReference<CnQuotationModels.ResultModel<CnQuotationModels.KChartResult>>() { }
});
CnQuotationModels.KChartResult data = unwrap(result); public CnQuotationModels.ContractDetail getContractDetail(String contractCode) {
return data == null || data.chats() == null ? List.of() : data.chats(); 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, public List<CnQuotationModels.KChartItem> getKChartByDate(String excode, String code, String period,
@ -64,11 +67,7 @@ public class CnQuotationClient {
.addQueryParameter("date", String.valueOf(date)) .addQueryParameter("date", String.valueOf(date))
.addQueryParameter("direction", direction) .addQueryParameter("direction", direction)
.build()); .build());
CnQuotationModels.ResultModel<CnQuotationModels.KChartResult> result = JSON.parseObject(body, return parseKChartItems(body);
new TypeReference<CnQuotationModels.ResultModel<CnQuotationModels.KChartResult>>() {
});
CnQuotationModels.KChartResult data = unwrap(result);
return data == null || data.chats() == null ? List.of() : data.chats();
} }
public Integer typeOf(String period) { public Integer typeOf(String period) {
@ -86,15 +85,25 @@ public class CnQuotationClient {
}; };
} }
private <T> T unwrap(CnQuotationModels.ResultModel<T> result) { private List<CnQuotationModels.KChartItem> 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) { if (result == null) {
throw new BusinessException(ErrorCode.MARKET_DATA_UNAVAILABLE, "行情接口返回为空"); 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, 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) { private HttpUrl.Builder urlBuilder(String path) {

View File

@ -1,5 +1,6 @@
package com.yangwale.backtestify.service.market.client; package com.yangwale.backtestify.service.market.client;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -19,8 +20,13 @@ public class CnQuotationModels {
String productId, String productId,
String goodsName, String goodsName,
String mainContractCode, String mainContractCode,
Integer isPrincipal, Integer isPrincipal) {
Integer decimalPrecision) { }
public record ContractDetail(String excode,
String contractCode,
String productId,
BigDecimal priceTick) {
} }
public record KChartResult(List<KChartItem> chats) { public record KChartResult(List<KChartItem> chats) {

View File

@ -33,16 +33,8 @@ public final class PriceScaleConverter {
if (value == null || value.isBlank() || "-".equals(value.trim())) { if (value == null || value.isBlank() || "-".equals(value.trim())) {
return 0L; return 0L;
} }
return new BigDecimal(value.trim()).setScale(0, RoundingMode.HALF_UP).longValue(); long parsed = new BigDecimal(value.trim()).setScale(0, RoundingMode.HALF_UP).longValue();
} return Math.max(parsed, 0L);
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;
} }
private static int scaleDigits(int priceScale) { private static int scaleDigits(int priceScale) {

View File

@ -19,11 +19,13 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -36,6 +38,8 @@ import java.util.Objects;
@ConditionalOnProperty(prefix = "market-data", name = "provider", havingValue = "mysql") @ConditionalOnProperty(prefix = "market-data", name = "provider", havingValue = "mysql")
public class MarketDataSyncService { public class MarketDataSyncService {
private static final int DEFAULT_PRICE_SCALE = 1000;
private final MarketDataProperties properties; private final MarketDataProperties properties;
private final CnQuotationClient cnQuotationClient; private final CnQuotationClient cnQuotationClient;
private final InstrumentDictionaryMapper instrumentDictionaryMapper; private final InstrumentDictionaryMapper instrumentDictionaryMapper;
@ -53,18 +57,40 @@ public class MarketDataSyncService {
syncIncrementalForMainContracts(); syncIncrementalForMainContracts();
} }
@Transactional(rollbackFor = Exception.class)
public void syncIncrementalForMainContracts() { public void syncIncrementalForMainContracts() {
refreshMainContracts(); refreshMainContracts();
ZoneId zoneId = properties.getSync().zoneId(); ZoneId zoneId = properties.getSync().zoneId();
LocalDateTime now = LocalDateTime.now(zoneId); LocalDateTime now = LocalDateTime.now(zoneId);
LocalDate syncDate = now.toLocalDate().minusDays(1); LocalDate syncDate = now.toLocalDate();
long startTimestamp = syncDate.atStartOfDay(zoneId).toEpochSecond(); long fallbackStartTimestamp = syncDate.minusDays(1).atStartOfDay(zoneId).toEpochSecond();
long endTimestamp = now.toLocalDate() long endTimestamp = now.atZone(zoneId).toEpochSecond();
.atTime(properties.getSync().getIncrementalWindowEndHour(), 0) syncMainContracts(syncDate, fallbackStartTimestamp, endTimestamp, null, false);
.atZone(zoneId) }
.toEpochSecond();
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<InstrumentDictionary> 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<InstrumentDictionary> instruments = instrumentDictionaryMapper.selectList( List<InstrumentDictionary> instruments = instrumentDictionaryMapper.selectList(
new LambdaQueryWrapper<InstrumentDictionary>() new LambdaQueryWrapper<InstrumentDictionary>()
.eq(InstrumentDictionary::getIsDeleted, 0) .eq(InstrumentDictionary::getIsDeleted, 0)
@ -72,7 +98,8 @@ public class MarketDataSyncService {
for (InstrumentDictionary instrument : instruments) { for (InstrumentDictionary instrument : instruments) {
for (String period : properties.getSync().getPeriods()) { 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; int count = 0;
try { try {
List<CnQuotationModels.GoodsItem> goodsItems = cnQuotationClient.listMainContracts(); List<CnQuotationModels.GoodsItem> goodsItems = cnQuotationClient.listMainContracts();
instrumentDictionaryMapper.update(null, new LambdaUpdateWrapper<InstrumentDictionary>() List<ResolvedMainContract> resolvedContracts = new ArrayList<>();
.set(InstrumentDictionary::getIsMain, 0)
.eq(InstrumentDictionary::getIsDeleted, 0));
for (CnQuotationModels.GoodsItem item : goodsItems) { for (CnQuotationModels.GoodsItem item : goodsItems) {
if (item.mainContractCode() == null || item.mainContractCode().isBlank()) { if (item.mainContractCode() == null || item.mainContractCode().isBlank()) {
continue; 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<InstrumentDictionary>()
.set(InstrumentDictionary::getIsMain, 0)
.eq(InstrumentDictionary::getIsDeleted, 0));
for (ResolvedMainContract resolved : resolvedContracts) {
upsertInstrument(resolved.item(), resolved.priceTick());
count++; count++;
} }
saveLog("CONTRACT", null, null, LocalDate.now(), "SUCCESS", count, null, start); 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, 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(); LocalDateTime start = LocalDateTime.now();
int count = 0; int count = 0;
String syncType = rebuildIndicators ? "KLINE_REPAIR" : "KLINE";
try { try {
List<CnQuotationModels.KChartItem> items = cnQuotationClient.getKChartByDate( String tableName = tableResolver.resolve(period);
instrument.getExchangeId(), instrument.getContractCode(), period, startTimestamp, "after"); Long latestTimestamp = kLineMapper.selectLatestTimestamp(tableName, instrument.getId());
List<KLineRecord> records = items.stream() long cursor = forcedCursor != null
.filter(item -> item.u() != null && item.u() >= startTimestamp && item.u() <= endTimestamp) ? forcedCursor
.map(item -> toRecord(instrument, item)) : latestTimestamp == null ? fallbackStartTimestamp : latestTimestamp;
.filter(Objects::nonNull) while (cursor < endTimestamp) {
.toList(); long pageCursor = cursor;
if (!records.isEmpty()) { List<CnQuotationModels.KChartItem> items = cnQuotationClient.getKChartByDate(
count = kLineMapper.upsertBatch(tableResolver.resolve(period), records); instrument.getExchangeId(), instrument.getContractCode(), period, pageCursor, "after");
List<KLineRecord> 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()); 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) { } catch (Exception e) {
log.warn("同步行情失败: {} {}", instrument.getContractCode(), period, 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(); String contractCode = item.mainContractCode();
int priceScale = PriceScaleConverter.scaleFromPrecision(item.decimalPrecision()); List<InstrumentDictionary> matches =
InstrumentDictionary existing = instrumentDictionaryMapper.selectOne( instrumentDictionaryMapper.selectByExchangeAndContractCodeIgnoreCase(
new LambdaQueryWrapper<InstrumentDictionary>() item.excode(), contractCode);
.eq(InstrumentDictionary::getContractCode, contractCode) InstrumentDictionary existing = matches.isEmpty()
.last("LIMIT 1")); ? null
: requireSingleContract(matches, item.excode() + "/" + contractCode);
if (existing == null) { if (existing == null) {
InstrumentDictionary instrument = new InstrumentDictionary(); InstrumentDictionary instrument = new InstrumentDictionary();
instrument.setExchangeId(item.excode()); instrument.setExchangeId(item.excode());
instrument.setSymbol(firstNonBlank(item.productId(), item.goodsCode(), productPrefix(contractCode))); instrument.setSymbol(firstNonBlank(item.productId(), item.goodsCode(), productPrefix(contractCode)));
instrument.setContractCode(contractCode); instrument.setContractCode(contractCode);
instrument.setPriceScale(priceScale); instrument.setPriceScale(DEFAULT_PRICE_SCALE);
instrument.setPriceTick(priceTick);
instrument.setIsMain(1); instrument.setIsMain(1);
instrumentDictionaryMapper.insert(instrument); instrumentDictionaryMapper.insert(instrument);
} else { } else {
existing.setExchangeId(item.excode()); existing.setExchangeId(item.excode());
existing.setSymbol(firstNonBlank(item.productId(), item.goodsCode(), productPrefix(contractCode))); existing.setSymbol(firstNonBlank(item.productId(), item.goodsCode(), productPrefix(contractCode)));
existing.setPriceScale(priceScale); existing.setContractCode(contractCode);
existing.setPriceTick(priceTick);
existing.setIsMain(1); existing.setIsMain(1);
existing.setIsDeleted(0); existing.setIsDeleted(0);
instrumentDictionaryMapper.updateById(existing); instrumentDictionaryMapper.updateById(existing);
} }
} }
private InstrumentDictionary requireSingleContract(List<InstrumentDictionary> 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) { private KLineRecord toRecord(InstrumentDictionary instrument, CnQuotationModels.KChartItem item) {
try { try {
return KLineRecord.builder() return KLineRecord.builder()
@ -197,4 +267,7 @@ public class MarketDataSyncService {
} }
return contractCode.replaceAll("\\d+$", ""); return contractCode.replaceAll("\\d+$", "");
} }
private record ResolvedMainContract(CnQuotationModels.GoodsItem item, BigDecimal priceTick) {
}
} }

View File

@ -8,198 +8,610 @@ CREATE DATABASE IF NOT EXISTS backtestify
USE backtestify; USE backtestify;
-- --------------------------------------------------- -- ----------------------------
-- 合约字典表(不分区) -- Table structure for bt_strategy_config
-- --------------------------------------------------- -- ----------------------------
DROP TABLE IF EXISTS t_instrument_dictionary; DROP TABLE IF EXISTS `bt_strategy_config`;
CREATE TABLE t_instrument_dictionary ( CREATE TABLE `bt_strategy_config` (
id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '合约自增ID', `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
exchange_id VARCHAR(16) DEFAULT NULL COMMENT '交易所代码 (如 SHFE)', `user_id` bigint NOT NULL COMMENT '用户ID',
symbol VARCHAR(10) NOT NULL COMMENT '期货品种 (如 rb)', `contract_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '合约代码',
contract_code VARCHAR(20) NOT NULL COMMENT '具体合约代码 (如 rb2610)', `contract_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '合约名称',
price_scale INT UNSIGNED NOT NULL DEFAULT 100 COMMENT '价格放大倍数 (100表示保留2位小数)', `direction` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '交易方向: LONG/SHORT',
is_main TINYINT NOT NULL DEFAULT 0 COMMENT '是否当前主力合约: 0-否, 1-是', `kline_period` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'K线周期: 1m/3m/5m/15m/30m/1h/4h/1d/1w',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `indicators` json NOT NULL COMMENT '技术指标列表, 如[\"MACD\",\"KDJ\"]',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `open_volume` int NOT NULL DEFAULT 1 COMMENT '开仓数量',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除: 0-未删除, 1-已删除', `volume_unit` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'LOT' COMMENT '数量单位: LOT/POSITION',
PRIMARY KEY (id), `stop_loss_value` decimal(18, 4) NULL DEFAULT NULL COMMENT '止损值',
UNIQUE KEY uk_contract (contract_code), `stop_loss_unit` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '止损单位: TICK/PERCENT',
KEY idx_symbol_main (symbol, is_main), `take_profit_value` decimal(18, 4) NULL DEFAULT NULL COMMENT '止盈值',
KEY idx_exchange_contract (exchange_id, contract_code) `take_profit_unit` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '止盈单位: TICK/PERCENT',
) ENGINE=InnoDB DEFAULT CHARSET=ascii COMMENT='期货合约字典表'; `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 -- Table structure for bt_strategy_result
-- --------------------------------------------------- -- ----------------------------
DROP TABLE IF EXISTS t_kline_1m; DROP TABLE IF EXISTS `bt_strategy_result`;
CREATE TABLE t_kline_1m ( CREATE TABLE `bt_strategy_result` (
instrument_id SMALLINT UNSIGNED NOT NULL COMMENT '合约字典ID', `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
timestamp INT UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级每根K线起始点)', `strategy_id` bigint NOT NULL COMMENT '关联策略ID',
open INT NOT NULL COMMENT '开盘价 (实际价格 * price_scale)', `initial_capital` decimal(18, 2) NOT NULL COMMENT '初始资金',
high INT NOT NULL COMMENT '最高价 (实际价格 * price_scale)', `final_capital` decimal(18, 2) NOT NULL COMMENT '期末总资产',
low INT NOT NULL COMMENT '最低价 (实际价格 * price_scale)', `max_equity` decimal(18, 2) NOT NULL COMMENT '回测期间最高净值',
close INT NOT NULL COMMENT '收盘价 (实际价格 * price_scale)', `min_equity` decimal(18, 2) NOT NULL COMMENT '回测期间最低净值',
volume INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交量 (手)', `total_yield` decimal(10, 4) NOT NULL COMMENT '总收益率(%)',
turnover BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '成交额 (元)', `profit_amount` decimal(18, 2) NOT NULL COMMENT '收益金额',
open_interest INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '持仓量', `annualized_yield` decimal(10, 4) NOT NULL COMMENT '年化收益率(%)',
PRIMARY KEY (instrument_id, timestamp) `trade_count` int NOT NULL DEFAULT 0 COMMENT '交易次数',
) ENGINE=InnoDB DEFAULT CHARSET=ascii COMMENT='1分钟K线数据表' `max_drawdown` decimal(10, 4) NOT NULL COMMENT '最大回撤(%)',
PARTITION BY RANGE (timestamp) ( `sharpe_ratio` decimal(10, 4) NOT NULL COMMENT '夏普比率',
PARTITION p2020 VALUES LESS THAN (1609459200), `win_rate` decimal(10, 4) NOT NULL COMMENT '胜率(%)',
PARTITION p2021 VALUES LESS THAN (1640995200), `start_date` date NOT NULL COMMENT '回测开始日期',
PARTITION p2022 VALUES LESS THAN (1672531200), `end_date` date NOT NULL COMMENT '回测结束日期',
PARTITION p2023 VALUES LESS THAN (1704067200), `daily_equity_curve` json NULL COMMENT '每日净值曲线 [{date,equity,yield}]',
PARTITION p2024 VALUES LESS THAN (1735689600), `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PARTITION p2025 VALUES LESS THAN (1767225600), PRIMARY KEY (`id`) USING BTREE,
PARTITION p2026 VALUES LESS THAN (1798761600), UNIQUE INDEX `uk_strategy_id`(`strategy_id` ASC) USING BTREE
PARTITION p2027 VALUES LESS THAN (1830297600), ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '回测结果表' ROW_FORMAT = Dynamic;
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
);
DROP TABLE IF EXISTS t_kline_5m; -- ----------------------------
CREATE TABLE t_kline_5m LIKE t_kline_1m; -- Table structure for bt_trade_detail
ALTER TABLE t_kline_5m COMMENT='5分钟K线数据表'; -- ----------------------------
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; -- Table structure for bt_user_signal
ALTER TABLE t_kline_15m COMMENT='15分钟K线数据表'; -- ----------------------------
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; -- Table structure for t_indicator_15m
ALTER TABLE t_kline_30m COMMENT='30分钟K线数据表'; -- ----------------------------
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; -- Table structure for t_indicator_1d
ALTER TABLE t_kline_1h COMMENT='1小时K线数据表'; -- ----------------------------
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; -- Table structure for t_indicator_1h
ALTER TABLE t_kline_4h COMMENT='4小时K线数据表'; -- ----------------------------
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; -- Table structure for t_indicator_1m
ALTER TABLE t_kline_1d COMMENT='日K线数据表'; -- ----------------------------
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; -- Table structure for t_indicator_1mo
ALTER TABLE t_kline_1w COMMENT='周K线数据表'; -- ----------------------------
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;
-- --------------------------------------------------- -- ----------------------------
-- 行情同步日志表 -- Table structure for t_indicator_1w
-- --------------------------------------------------- -- ----------------------------
DROP TABLE IF EXISTS t_market_data_sync_log; DROP TABLE IF EXISTS `t_indicator_1w`;
CREATE TABLE t_market_data_sync_log ( CREATE TABLE `t_indicator_1w` (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID',
sync_type VARCHAR(32) NOT NULL COMMENT '同步类型: CONTRACT/KLINE', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级对应K线起始点)',
period VARCHAR(10) DEFAULT NULL COMMENT 'K线周期', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5',
contract_code VARCHAR(20) DEFAULT NULL COMMENT '合约代码', `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10',
sync_date DATE DEFAULT NULL COMMENT '同步日期', `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20',
status VARCHAR(16) NOT NULL COMMENT '状态: SUCCESS/FAILED', `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60',
success_count INT NOT NULL DEFAULT 0 COMMENT '成功条数', `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)',
error_message TEXT DEFAULT NULL COMMENT '错误信息', `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨',
start_time DATETIME NOT NULL COMMENT '开始时间', `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨',
end_time DATETIME DEFAULT NULL COMMENT '结束时间', `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12',
PRIMARY KEY (id), `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20',
KEY idx_sync_date (sync_date), `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)',
KEY idx_contract_period (contract_code, period), `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA',
KEY idx_status (status) `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR',
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='行情同步日志表'; `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;
-- --------------------------------------------------- -- ----------------------------
-- 策略配置表 -- Table structure for t_indicator_30m
-- --------------------------------------------------- -- ----------------------------
DROP TABLE IF EXISTS bt_strategy_config; DROP TABLE IF EXISTS `t_indicator_30m`;
CREATE TABLE bt_strategy_config ( CREATE TABLE `t_indicator_30m` (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID',
user_id BIGINT NOT NULL COMMENT '用户ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级对应K线起始点)',
contract_code VARCHAR(32) NOT NULL COMMENT '合约代码', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5',
contract_name VARCHAR(64) NOT NULL COMMENT '合约名称', `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10',
direction VARCHAR(10) NOT NULL COMMENT '交易方向: LONG/SHORT', `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20',
kline_period VARCHAR(10) NOT NULL COMMENT 'K线周期: 1m/3m/5m/15m/30m/1h/4h/1d/1w', `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60',
indicators JSON NOT NULL COMMENT '技术指标列表, 如["MACD","KDJ"]', `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)',
open_volume INT NOT NULL DEFAULT 1 COMMENT '开仓数量', `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨',
volume_unit VARCHAR(10) NOT NULL DEFAULT 'LOT' COMMENT '数量单位: LOT/POSITION', `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨',
stop_loss_value DECIMAL(18,4) DEFAULT NULL COMMENT '止损值', `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6',
stop_loss_unit VARCHAR(10) DEFAULT NULL COMMENT '止损单位: TICK/PERCENT', `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12',
take_profit_value DECIMAL(18,4) DEFAULT NULL COMMENT '止盈值', `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20',
take_profit_unit VARCHAR(10) DEFAULT NULL COMMENT '止盈单位: TICK/PERCENT', `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)',
backtest_period VARCHAR(10) NOT NULL COMMENT '回测区间: 1m/3m/6m/1y', `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0-已保存, 1-已启用信号', `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `rsi6` decimal(10, 4) NULL DEFAULT NULL COMMENT '短期 RSI6',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `rsi12` decimal(10, 4) NULL DEFAULT NULL COMMENT '长期 RSI12',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除: 0-未删除, 1-已删除', `kdj_k` decimal(10, 4) NULL DEFAULT NULL COMMENT 'K值',
PRIMARY KEY (id), `kdj_d` decimal(10, 4) NULL DEFAULT NULL COMMENT 'D值',
INDEX idx_user_id (user_id), `kdj_j` decimal(10, 4) NULL DEFAULT NULL COMMENT 'J值',
INDEX idx_contract_code (contract_code), `create_at` int UNSIGNED NULL DEFAULT NULL COMMENT '记录创建时间',
INDEX idx_create_time (create_time) PRIMARY KEY (`instrument_id`, `k_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='策略配置表'; ) ENGINE = InnoDB CHARACTER SET = ascii COLLATE = ascii_general_ci COMMENT = '30分钟指标表' ROW_FORMAT = DYNAMIC;
-- --------------------------------------------------- -- ----------------------------
-- 回测结果表 -- Table structure for t_indicator_3m
-- --------------------------------------------------- -- ----------------------------
DROP TABLE IF EXISTS bt_strategy_result; DROP TABLE IF EXISTS `t_indicator_3m`;
CREATE TABLE bt_strategy_result ( CREATE TABLE `t_indicator_3m` (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID',
strategy_id BIGINT NOT NULL COMMENT '关联策略ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级对应K线起始点)',
initial_capital DECIMAL(18,2) NOT NULL COMMENT '初始资金', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5',
final_capital DECIMAL(18,2) NOT NULL COMMENT '期末总资产', `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10',
max_equity DECIMAL(18,2) NOT NULL COMMENT '回测期间最高净值', `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20',
min_equity DECIMAL(18,2) NOT NULL COMMENT '回测期间最低净值', `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60',
total_yield DECIMAL(10,4) NOT NULL COMMENT '总收益率(%)', `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)',
profit_amount DECIMAL(18,2) NOT NULL COMMENT '收益金额', `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨',
annualized_yield DECIMAL(10,4) NOT NULL COMMENT '年化收益率(%)', `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨',
trade_count INT NOT NULL DEFAULT 0 COMMENT '交易次数', `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6',
max_drawdown DECIMAL(10,4) NOT NULL COMMENT '最大回撤(%)', `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12',
sharpe_ratio DECIMAL(10,4) NOT NULL COMMENT '夏普比率', `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20',
win_rate DECIMAL(10,4) NOT NULL COMMENT '胜率(%)', `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)',
start_date DATE NOT NULL COMMENT '回测开始日期', `macd_dea` decimal(18, 6) NULL DEFAULT NULL COMMENT '慢线 DEA',
end_date DATE NOT NULL COMMENT '回测结束日期', `macd_bar` decimal(18, 6) NULL DEFAULT NULL COMMENT '柱状图 BAR',
daily_equity_curve JSON DEFAULT NULL COMMENT '每日净值曲线 [{date,equity,yield}]', `rsi6` decimal(10, 4) NULL DEFAULT NULL COMMENT '短期 RSI6',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `rsi12` decimal(10, 4) NULL DEFAULT NULL COMMENT '长期 RSI12',
PRIMARY KEY (id), `kdj_k` decimal(10, 4) NULL DEFAULT NULL COMMENT 'K值',
UNIQUE INDEX uk_strategy_id (strategy_id) `kdj_d` decimal(10, 4) NULL DEFAULT NULL COMMENT 'D值',
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='回测结果表'; `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;
-- --------------------------------------------------- -- ----------------------------
-- 交易明细表 -- Table structure for t_indicator_4h
-- --------------------------------------------------- -- ----------------------------
DROP TABLE IF EXISTS bt_trade_detail; DROP TABLE IF EXISTS `t_indicator_4h`;
CREATE TABLE bt_trade_detail ( CREATE TABLE `t_indicator_4h` (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID',
strategy_id BIGINT NOT NULL COMMENT '关联策略ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级对应K线起始点)',
action VARCHAR(20) NOT NULL COMMENT '操作: BUY_OPEN/SELL_CLOSE/SELL_OPEN/BUY_CLOSE', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5',
price DECIMAL(18,4) NOT NULL COMMENT '成交价', `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10',
volume INT NOT NULL COMMENT '成交数量', `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20',
turnover DECIMAL(18,2) NOT NULL COMMENT '成交金额', `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60',
trade_time DATETIME NOT NULL COMMENT '成交时间', `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)',
kline_time DATETIME NOT NULL COMMENT '对应K线时间', `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨',
signal_type VARCHAR(5) NOT NULL COMMENT '信号类型: B/S', `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6',
PRIMARY KEY (id), `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12',
INDEX idx_strategy_id (strategy_id), `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20',
INDEX idx_trade_time (trade_time) `macd_dif` decimal(18, 6) NULL DEFAULT NULL COMMENT '快线 DIF (EMA12 - EMA26)',
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='交易明细表'; `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;
-- --------------------------------------------------- -- ----------------------------
-- 用户信号标记表 -- Table structure for t_indicator_5m
-- --------------------------------------------------- -- ----------------------------
DROP TABLE IF EXISTS bt_user_signal; DROP TABLE IF EXISTS `t_indicator_5m`;
CREATE TABLE bt_user_signal ( CREATE TABLE `t_indicator_5m` (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `instrument_id` smallint UNSIGNED NOT NULL COMMENT '合约字典ID',
user_id BIGINT NOT NULL COMMENT '用户ID', `k_time` int UNSIGNED NOT NULL COMMENT 'Unix时间戳 (秒级对应K线起始点)',
strategy_id BIGINT NOT NULL COMMENT '策略ID', `ma5` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA5',
contract_code VARCHAR(32) NOT NULL COMMENT '合约代码', `ma10` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA10',
kline_period VARCHAR(10) NOT NULL COMMENT 'K线周期', `ma20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA20',
is_active TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用: 0-停用, 1-启用', `ma60` decimal(18, 6) NULL DEFAULT NULL COMMENT 'MA60',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `boll_mb` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线中轨 (MA20)',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `boll_up` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线上轨',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除: 0-未删除, 1-已删除', `boll_dn` decimal(18, 6) NULL DEFAULT NULL COMMENT '布林线下轨',
PRIMARY KEY (id), `ema6` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA6',
INDEX idx_user_contract_period (user_id, contract_code, kline_period), `ema12` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA12',
INDEX idx_strategy_id (strategy_id) `ema20` decimal(18, 6) NULL DEFAULT NULL COMMENT 'EMA20',
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户信号标记表'; `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;

View File

@ -27,4 +27,10 @@ class PriceScaleConverterTest {
void rejectsBlankPrice() { void rejectsBlankPrice() {
assertThrows(IllegalArgumentException.class, () -> PriceScaleConverter.toScaled("", 100)); assertThrows(IllegalArgumentException.class, () -> PriceScaleConverter.toScaled("", 100));
} }
@Test
void normalizesNegativeUnsignedMarketQuantityToZero() {
assertEquals(0L, PriceScaleConverter.toLong("-1"));
assertEquals(12L, PriceScaleConverter.toLong("12"));
}
} }

View File

@ -1,6 +1,8 @@
package com.yangwale.backtestify.service.market.sync; package com.yangwale.backtestify.service.market.sync;
import com.baomidou.mybatisplus.core.conditions.Wrapper; 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.config.MarketDataProperties;
import com.yangwale.backtestify.entity.InstrumentDictionary; import com.yangwale.backtestify.entity.InstrumentDictionary;
import com.yangwale.backtestify.entity.KLineRecord; 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.indicator.IndicatorCalculationService;
import com.yangwale.backtestify.service.market.repository.KLineTableResolver; import com.yangwale.backtestify.service.market.repository.KLineTableResolver;
import org.junit.jupiter.api.Test; 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 java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; 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.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
class MarketDataSyncServiceTest { class MarketDataSyncServiceTest {
@Test @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<InstrumentDictionary> 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<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
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<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
void catchesUpFromLatestDatabaseTimestampAcrossMultiplePages() {
MarketDataProperties properties = new MarketDataProperties(); MarketDataProperties properties = new MarketDataProperties();
properties.getSync().setPeriods(List.of("1d")); properties.getSync().setPeriods(List.of("1d"));
InstrumentDictionaryMapper instrumentMapper = mock(InstrumentDictionaryMapper.class); InstrumentDictionaryMapper instrumentMapper = mock(InstrumentDictionaryMapper.class);
@ -40,11 +152,14 @@ class MarketDataSyncServiceTest {
instrument.setPriceScale(100); instrument.setPriceScale(100);
instrument.setIsMain(1); instrument.setIsMain(1);
when(instrumentMapper.selectList(any(Wrapper.class))).thenReturn(List.of(instrument)); 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.<List<KLineRecord>>getArgument(1).size());
FakeQuotationClient quotationClient = new FakeQuotationClient(properties);
MarketDataSyncService service = new MarketDataSyncService( MarketDataSyncService service = new MarketDataSyncService(
properties, properties,
new FakeQuotationClient(properties), quotationClient,
instrumentMapper, instrumentMapper,
kLineMapper, kLineMapper,
syncLogMapper, syncLogMapper,
@ -58,13 +173,130 @@ class MarketDataSyncServiceTest {
service.syncIncrementalForMainContracts(); service.syncIncrementalForMainContracts();
verify(kLineMapper).upsertBatch(eq("t_kline_1d"), anyList()); ArgumentCaptor<List<KLineRecord>> 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(indicatorService).updateIncremental("1d", 7);
verify(syncLogMapper, org.mockito.Mockito.atLeastOnce()).insert(any(MarketDataSyncLog.class)); 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<List<KLineRecord>> 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 static final class FakeQuotationClient extends CnQuotationClient {
private final List<Long> requestedCursors = new ArrayList<>();
private FakeQuotationClient(MarketDataProperties properties) { private FakeQuotationClient(MarketDataProperties properties) {
super(properties); super(properties);
} }
@ -77,9 +309,86 @@ class MarketDataSyncServiceTest {
@Override @Override
public List<CnQuotationModels.KChartItem> getKChartByDate( public List<CnQuotationModels.KChartItem> getKChartByDate(
String excode, String code, String period, long date, String direction) { 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", 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<CnQuotationModels.KChartItem> 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<CnQuotationModels.KChartItem> 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<CnQuotationModels.GoodsItem> 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<String> requestedCodes = new ArrayList<>();
private CapturingRepairQuotationClient(MarketDataProperties properties) {
super(properties);
}
@Override
public List<CnQuotationModels.KChartItem> getKChartByDate(
String excode, String code, String period, long date, String direction) {
requestedCodes.add(code);
return List.of();
} }
} }
} }

View File

@ -4,7 +4,8 @@ CREATE TABLE IF NOT EXISTS t_instrument_dictionary (
exchange_id VARCHAR(16), exchange_id VARCHAR(16),
symbol VARCHAR(10) NOT NULL, symbol VARCHAR(10) NOT NULL,
contract_code VARCHAR(20) 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, is_main TINYINT NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,