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.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;
}

View File

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

View File

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

View File

@ -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<CnQuotationModels.GoodsPage> result = JSON.parseObject(body,
new TypeReference<CnQuotationModels.ResultModel<CnQuotationModels.GoodsPage>>() {
});
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<CnQuotationModels.KChartItem> 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<CnQuotationModels.KChartResult> result = JSON.parseObject(body,
new TypeReference<CnQuotationModels.ResultModel<CnQuotationModels.KChartResult>>() {
});
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<CnQuotationModels.KChartItem> 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<CnQuotationModels.KChartResult> result = JSON.parseObject(body,
new TypeReference<CnQuotationModels.ResultModel<CnQuotationModels.KChartResult>>() {
});
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> 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) {
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) {

View File

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

View File

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

View File

@ -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<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(
new LambdaQueryWrapper<InstrumentDictionary>()
.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<CnQuotationModels.GoodsItem> goodsItems = cnQuotationClient.listMainContracts();
instrumentDictionaryMapper.update(null, new LambdaUpdateWrapper<InstrumentDictionary>()
.set(InstrumentDictionary::getIsMain, 0)
.eq(InstrumentDictionary::getIsDeleted, 0));
List<ResolvedMainContract> 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<InstrumentDictionary>()
.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 {
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<CnQuotationModels.KChartItem> items = cnQuotationClient.getKChartByDate(
instrument.getExchangeId(), instrument.getContractCode(), period, startTimestamp, "after");
instrument.getExchangeId(), instrument.getContractCode(), period, pageCursor, "after");
List<KLineRecord> records = items.stream()
.filter(item -> item.u() != null && item.u() >= startTimestamp && item.u() <= endTimestamp)
.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()) {
count = kLineMapper.upsertBatch(tableResolver.resolve(period), records);
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<InstrumentDictionary>()
.eq(InstrumentDictionary::getContractCode, contractCode)
.last("LIMIT 1"));
List<InstrumentDictionary> 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<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) {
try {
return KLineRecord.builder()
@ -197,4 +267,7 @@ public class MarketDataSyncService {
}
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;
-- ---------------------------------------------------
-- 合约字典表(不分区)
-- ---------------------------------------------------
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;

View File

@ -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"));
}
}

View File

@ -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<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();
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.<List<KLineRecord>>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<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(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 final List<Long> requestedCursors = new ArrayList<>();
private FakeQuotationClient(MarketDataProperties properties) {
super(properties);
}
@ -77,9 +309,86 @@ class MarketDataSyncServiceTest {
@Override
public List<CnQuotationModels.KChartItem> getKChartByDate(
String excode, String code, String period, long date, String direction) {
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", 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", date + 1, "20", null));
"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),
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,