Normalize market contract synchronization
This commit is contained in:
parent
b5e34b6d18
commit
10c434013b
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
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(
|
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()
|
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))
|
.map(item -> toRecord(instrument, item))
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.toList();
|
.toList();
|
||||||
if (!records.isEmpty()) {
|
if (records.isEmpty()) {
|
||||||
count = kLineMapper.upsertBatch(tableResolver.resolve(period), records);
|
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) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
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(
|
return List.of(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", 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user