package com.yami.trading.service.etf; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Tuple; import cn.hutool.core.util.NumberUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yami.trading.bean.data.domain.Kline; import com.yami.trading.bean.data.domain.RobotModel; import com.yami.trading.bean.etf.domain.EtfKLine; import com.yami.trading.bean.etf.domain.EtfMinuteKLine; import com.yami.trading.bean.etf.domain.EtfSecKLine; import com.yami.trading.bean.etf.domain.KlineConfig; import com.yami.trading.bean.etf.dto.KlineConfigDTO; import com.yami.trading.bean.etf.mapstruct.EtfMinuteKLineWrapper; import com.yami.trading.bean.etf.mapstruct.EtfSecKLineWrapper; import com.yami.trading.bean.item.domain.Item; import com.yami.trading.bean.robot.domain.RobotOrder; import com.yami.trading.common.exception.YamiShopBindException; import com.yami.trading.common.util.Arith; import com.yami.trading.dao.data.RobotModelMapper; import com.yami.trading.dao.etf.mapper.KlineConfigMapper; import com.yami.trading.service.item.ItemService; import com.yami.trading.service.robot.RobotOrderService; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.*; /** * etfK线图配置表Service * * @author lucas * @version 2023-05-03 */ @Service @Transactional public class KlineConfigService extends ServiceImpl { @Autowired private RobotModelMapper robotModelMapper; @Autowired private ItemService itemService; @Autowired EtfSecKLineService etfSecKLineService; @Autowired EtfSecKLineWrapper etfSecKLineWrapper; @Autowired RobotOrderService robotOrderService; @Autowired EtfMinuteKLineService etfMinuteKLineService; @Autowired EtfMinuteKLineWrapper etfMinuteKLineWrapper; @Lazy @Autowired MarketService marketService; @Transactional(rollbackFor = Exception.class) public void deleteByRobotModelId(List ids) { long currentTimeMillis = System.currentTimeMillis(); for (String id : ids) { KlineConfig config = this.getBaseMapper().selectById(id); if (config != null) { if (config.getOpenTimeTs() <= currentTimeMillis && config.getCloseTimeTs() >= currentTimeMillis) { throw new YamiShopBindException("当前为开市时间,不能删除"); } removeById(id); etfSecKLineService.remove(new QueryWrapper().eq("symbol", config.getSymbol()).between("ts", config.getOpenTimeTs(), config.getCloseTimeTs())); //etfMinuteKLineService 根据config的开市时间,结束时间,symbol去删除 etfMinuteKLineService.remove(new QueryWrapper().eq("symbol", config.getSymbol()).between("ts", config.getOpenTimeTs(), config.getCloseTimeTs())); marketService.getCacheKline().put(config.getSymbol(), new LinkedHashMap<>()); } } } public EtfKLine queryKLine(KlineConfigDTO klineConfigDTO) { BigDecimal divide = BigDecimal.valueOf((klineConfigDTO.getClosePrice() - klineConfigDTO.getOpenPrice()) / klineConfigDTO.getOpenPrice() * 100); // BigDecimal divide = klineConfigDTO.getClosePrice().subtract(klineConfigDTO.getOpenPrice()).divide(klineConfigDTO.getOpenPrice(), 2, BigDecimal.ROUND_HALF_UP).multiply(BigDecimal.valueOf(100)); List robotModels = new LambdaQueryChainWrapper<>(robotModelMapper).between(RobotModel::getPercent, divide.subtract(new BigDecimal(0.5)), divide.add(new BigDecimal(0.5))).list(); if (CollectionUtil.isEmpty(robotModels)) { throw new YamiShopBindException("没有满足条件的趋势图"); } // 解析数据库满足条件涨幅的k线图数据 // 随机获取一个,获取完整一天的分钟k线图 RobotModel robotModel = robotModels.get(new Random().nextInt(robotModels.size())); List randomKLine = parseHistoryKLine(robotModel); // 获取完整一天的k线图 randomKLine.sort(Comparator.comparing(Kline::getTs)); // 生成分钟图返回 List klines = accrodingSecOrign(klineConfigDTO, robotModel, randomKLine, 60); // printResult(klines); return new EtfKLine(robotModel.getUuid(),klines); } public List generateSecKLine(KlineConfigDTO klineConfigDTO) { RobotModel robotModel = robotModelMapper.selectById(klineConfigDTO.getRobot_model_uuid()); // 假图的分钟图 List klines = parseHistoryKLine(robotModel); // 生成分钟图 List minute = accrodingSecOrign(klineConfigDTO, robotModel, klines, 60); etfMinuteKLineService.saveBatch(etfMinuteKLineWrapper.toDTO(minute),500); saveRobotOrder(klineConfigDTO, minute); // 生成的秒图 return getRecentWholwSecKline(klineConfigDTO.getSymbol(), minute); } private void saveRobotOrder(KlineConfigDTO klineConfigDTO, List minute) { Random random = new Random(); Item item = itemService.findBySymbol(klineConfigDTO.getSymbol()); List robotOrders = new ArrayList<>(minute.size()); for (Kline kline : minute) { RobotOrder robotOrder = new RobotOrder(); robotOrder.setSymbol(klineConfigDTO.getSymbol()); robotOrder.setPrice(NumberUtil.roundDown(randomDouble(kline.getHigh(), kline.getLow(),random),item.getDecimals()).doubleValue()); // 设置订单状态挂单中 robotOrder.setStatus(1); robotOrder.setOrderType(random.nextInt(2) + 1); robotOrder.setDirection(random.nextInt(2) + 1); robotOrder.setOrderQuantity(NumberUtil.roundDown(randomDouble(kline.getVolume(), kline.getVolume() * 0.8, random), item.getDecimals()).doubleValue()); robotOrder.setTurnover(0d); robotOrder.setTs(kline.getTs()); robotOrder.setUuid(UUID.randomUUID().toString().replaceAll("-", "")); robotOrder.setUid(UUID.randomUUID().toString().replaceAll("-", "")); robotOrders.add(robotOrder); } robotOrderService.saveBatch(robotOrders); } // 生成秒级的假图 private List accrodingSecOrign(KlineConfigDTO klineConfigDTO,RobotModel robotModel, List originalSecKline, int sec) { Item item = itemService.findBySymbol(klineConfigDTO.getSymbol()); double todayVolume = randomDouble(klineConfigDTO.getTurnoverHigh(), klineConfigDTO.getTurnoverLow(), new Random()); // BigDecimal todayAmount = todayVolume.divide(klineConfigDTO.getOpenPrice().add(klineConfigDTO.getClosePrice()).divide(BigDecimal.valueOf(2)), item.getDecimals(), BigDecimal.ROUND_HALF_UP); double todayAmount = Arith.div(todayVolume,(klineConfigDTO.getOpenPrice() + klineConfigDTO.getClosePrice()) / 2, item.getDecimals()); double totalVolume = originalSecKline.stream() .mapToDouble(klien -> klien.getVolume()) .sum(); Tuple startAndEnd = getStartAndEnd(klineConfigDTO.getOpenTimeTs()); klineConfigDTO.setOpenTimeTs(startAndEnd.get(0)); klineConfigDTO.setCloseTimeTs(startAndEnd.get(1)); Kline finalSecondKline; double open = klineConfigDTO.getOpenPrice(); double secHigh; double secLow; double secVolume; double secAmount; boolean isOver = false; List finalResult = new ArrayList<>(originalSecKline.size()); Random random = new Random(); for (int i = 0; i < originalSecKline.size(); i++) { Kline kline = originalSecKline.get(i); finalSecondKline = new Kline(); finalSecondKline.setTs(klineConfigDTO.getOpenTimeTs() + i * sec * 1000); finalSecondKline.setSymbol(item.getSymbol()); // 设置开仓价 finalSecondKline.setOpen(open); // 分钟级最高价 model.high / model.open * 分钟的open secHigh = NumberUtil.roundDown((kline.getHigh() / kline.getOpen()) * finalSecondKline.getOpen(), item.getDecimals()).doubleValue(); // secHigh = NumberUtil.roundDown(Arith.div(kline.getHigh(), kline.getOpen()) * finalSecondKline.getOpen(), item.getDecimals()).doubleValue(); // secHigh = kline.getHigh().divide(kline.getOpen(), 10, BigDecimal.ROUND_HALF_UP).multiply(finalSecondKline.getOpen()).setScale(item.getDecimals(), BigDecimal.ROUND_HALF_UP); finalSecondKline.setHigh(secHigh); // 分钟级最低价 secLow = NumberUtil.roundDown((kline.getLow() / kline.getOpen()) * finalSecondKline.getOpen(), item.getDecimals()).doubleValue(); // secLow = kline.getLow().divide(kline.getOpen(), 10, BigDecimal.ROUND_HALF_UP).multiply(finalSecondKline.getOpen()).setScale(item.getDecimals(), BigDecimal.ROUND_HALF_UP); finalSecondKline.setLow(secLow); // 分钟级成交量 if (kline.getVolume() == 0) { secVolume = 0; } else { secVolume = NumberUtil.roundDown((kline.getVolume() / totalVolume) * todayVolume, item.getDecimals()).doubleValue(); // secVolume = kline.getVolume().divide(BigDecimal.valueOf(totalVolume),10, BigDecimal.ROUND_HALF_UP).multiply(todayVolume).setScale(item.getDecimals(), BigDecimal.ROUND_HALF_UP); } finalSecondKline.setVolume(secVolume); if (kline.getAmount() == 0) { secAmount = 0; } else { // 分钟级成交手数 secAmount = NumberUtil.roundDown((kline.getAmount() / robotModel.getAmount()) * todayAmount, item.getDecimals()).doubleValue(); // secAmount = kline.getAmount().divide(BigDecimal.valueOf(robotModel.getAmount()),10, BigDecimal.ROUND_HALF_UP).multiply(todayAmount).setScale(item.getDecimals(), BigDecimal.ROUND_HALF_UP); } finalSecondKline.setAmount(secAmount); // 收盘价 if (finalSecondKline.getTs() > klineConfigDTO.getCloseTimeTs() || i == originalSecKline.size() - 1) { finalSecondKline.setClose(BigDecimal.valueOf(klineConfigDTO.getClosePrice()).setScale(item.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); isOver = true; } else { double close = randomDouble(secHigh, secLow, random); finalSecondKline.setClose(BigDecimal.valueOf(close).setScale(item.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); } open = finalSecondKline.getClose(); finalResult.add(finalSecondKline); if (isOver) { break; } } return finalResult; } @NotNull private List getRecentWholwSecKline(String symbol, List collect) { Item bySymbol = itemService.findBySymbol(symbol); List originalSecKline = new ArrayList<>(collect.size()); Kline originalSecondKline = null; Random random = new Random(); for (Kline kline : collect) { // 将这一分钟成交量拆分成60份 List volumes = splitData(kline.getVolume(), 60, 30); List amounts = splitData(kline.getAmount(), 60, 30); double open = originalSecondKline == null ? kline.getOpen() : originalSecondKline.getClose(); for (int i = 0; i < 60; i++) { originalSecondKline = new Kline(); originalSecondKline.setSymbol(symbol); originalSecondKline.setTs(kline.getTs() + i * 1000); originalSecondKline.setOpen(BigDecimal.valueOf(open).setScale(bySymbol.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); double close = 0; if (i == 59) { originalSecondKline.setClose(BigDecimal.valueOf(kline.getClose()).setScale(bySymbol.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); } else { close = randomDouble(kline.getOpen(), kline.getClose(), random); originalSecondKline.setClose(BigDecimal.valueOf(close).setScale(bySymbol.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); } double high = randomDouble(kline.getLow(), kline.getHigh(), random); originalSecondKline.setHigh(BigDecimal.valueOf(high).setScale(bySymbol.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); double low = randomDouble(kline.getLow(), originalSecondKline.getClose(), random); originalSecondKline.setLow(BigDecimal.valueOf(low).min(BigDecimal.valueOf(originalSecondKline.getOpen())).setScale(bySymbol.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); originalSecondKline.setVolume(BigDecimal.valueOf(volumes.get(i)).setScale(bySymbol.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); originalSecondKline.setAmount(BigDecimal.valueOf(amounts.get(i)).setScale(bySymbol.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); // 重新调整K线图数据 adjustKline(originalSecondKline); // 重新打散 while (originalSecondKline.getHigh() == originalSecondKline.getLow()) { // 收盘价在最高和最低之间 当最高价最低价相同时, close = random ( high * random(1,1.01) , low * random(1,1.01) ) // 随机最高价 为当前分钟最高价的1.005倍 // BigDecimal tempHigh = kline.getHigh().multiply(getRandomBigDecimal(1)); double tempHigh = kline.getHigh() * getRandomBigDecimal(1).doubleValue(); // 随机最低价 为当前分钟最低价的0.995倍 // double tempLow = kline.getLow().multiply(getRandomBigDecimal(-1)); double tempLow = kline.getLow() * getRandomBigDecimal(-1).doubleValue(); originalSecondKline.setClose(BigDecimal.valueOf(randomDouble(tempHigh,tempLow, random)).setScale(bySymbol.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); // 最高价在收盘价和这一分钟的最高价之间 originalSecondKline.setHigh(BigDecimal.valueOf(randomDouble(originalSecondKline.getClose(), kline.getHigh(), random)).setScale(bySymbol.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); // 最低价在收盘价和这一分钟的最低价之间 originalSecondKline.setLow(BigDecimal.valueOf(randomDouble(kline.getLow(), originalSecondKline.getClose(), random)).setScale(bySymbol.getDecimals(), BigDecimal.ROUND_HALF_UP).doubleValue()); adjustKline(originalSecondKline); } open = originalSecondKline.getClose(); originalSecKline.add(originalSecondKline); } } return originalSecKline; } public static Kline adjustKline(Kline kline) { double[] values = {kline.getHigh(), kline.getOpen(), kline.getClose(), kline.getLow()}; Arrays.sort(values); kline.setHigh(values[3]); kline.setLow(values[0]); return kline; } public static BigDecimal getRandomBigDecimal(int input) { BigDecimal lowerBound; BigDecimal upperBound; if (input == 1) { lowerBound = BigDecimal.ONE; upperBound = new BigDecimal("1.005"); } else if (input == -1) { lowerBound = new BigDecimal("0.995"); upperBound = BigDecimal.ONE; } else { throw new IllegalArgumentException("Invalid input value. Expected 1 or -1."); } BigDecimal range = upperBound.subtract(lowerBound); BigDecimal randomValue = range.multiply(BigDecimal.valueOf(Math.random())).add(lowerBound); return randomValue.setScale(3, RoundingMode.HALF_UP); } private void printResult(List originalSecKline) { // 输出到echarts List> result = new ArrayList<>(); List time = new ArrayList<>(); // 格式化时间 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss"); for (Kline kline : originalSecKline) { List tmp = new ArrayList<>(); time.add(simpleDateFormat.format(new Date(kline.getTs()))); tmp.add(BigDecimal.valueOf(kline.getOpen()).setScale(5, BigDecimal.ROUND_HALF_UP)); tmp.add(BigDecimal.valueOf(kline.getClose()).setScale(5, BigDecimal.ROUND_HALF_UP)); tmp.add(BigDecimal.valueOf(kline.getLow()).setScale(5, BigDecimal.ROUND_HALF_UP)); tmp.add(BigDecimal.valueOf(kline.getHigh()).setScale(5, BigDecimal.ROUND_HALF_UP)); // tmp.add(kline.getVolume().setScale(5, BigDecimal.ROUND_HALF_UP)); result.add(tmp); } System.err.println(JSON.toJSONString(time)); System.err.println(JSON.toJSONString(result)); } /** * 传入时间字符串获取当天第一个时间戳 */ private Long getTodayFirstTimestamp(String time) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); Date date = null; try { date = dateFormat.parse(time); } catch (ParseException e) { e.printStackTrace(); } Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); return calendar.getTimeInMillis(); } /** * 传入时间字符串获取当天最后一个时间戳 */ private Long getTodayLastTimestamp(String time) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); Date date = null; try { date = dateFormat.parse(time); } catch (ParseException e) { e.printStackTrace(); } Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.set(Calendar.HOUR_OF_DAY, 23); calendar.set(Calendar.MINUTE, 59); calendar.set(Calendar.SECOND, 59); return calendar.getTimeInMillis(); } @NotNull private List parseHistoryKLine(RobotModel robotModel) { JSONArray jsonArray = JSON.parseArray(robotModel.getKLineData()); List klines = new ArrayList<>(jsonArray.size()); for (int i = 0; i < jsonArray.size(); i++) { JSONObject item = jsonArray.getJSONObject(i); Kline kline = new Kline(); kline.setTs(item.getLong("ts")); kline.setVolume(item.getDouble("volume")); kline.setAmount(item.getDouble("amount")); kline.setOpen(item.getDouble("open")); kline.setHigh(item.getDouble("high")); kline.setLow(item.getDouble("low")); kline.setClose(item.getDouble("close")); klines.add(kline); } return klines; } public static double randomDouble(double a, double b, Random random) { double diff = b - a; double randNum = diff * random.nextDouble(); return a + randNum; } public static List splitData(double data, int n, double maxDeviationPercent) { // 计算每份的理论平均值 double avg = data / n; // 计算允许的最大偏差 double maxDeviation = avg * maxDeviationPercent / 100; // 创建一个存储每份数据的列表 List splits = new ArrayList<>(); Random random = new Random(); double remainingData = data; // 按照N-1份进行随机拆分 for (int i = 0; i < n - 1; i++) { // 生成一个随机数,表示当前份数据的比例(0到1之间) double ratio = random.nextDouble(); // 计算当前份数据的大小 double currentSplit = avg + (ratio - 0.5) * 2 * maxDeviation; // 将当前份数据加入列表 splits.add(currentSplit); // 更新剩余数据量 remainingData -= currentSplit; } // 最后一份数据即为剩余数据量 splits.add(remainingData); // 打乱列表中数据的顺序 Collections.shuffle(splits); return splits; } private static BigDecimal getRandomBigDecimal(BigDecimal min, BigDecimal max, Random random) { double range = max.doubleValue() - (min.doubleValue()); double randomValue = range * random.nextDouble(); return BigDecimal.valueOf(min.doubleValue() + randomValue).setScale(min.scale(), BigDecimal.ROUND_HALF_UP); } /** * 查询秒级K线图 * * @param symbol * @return */ public List querySecKlineBySymbol(String symbol) { Tuple startAndEnd = getStartAndEnd(); List list = etfSecKLineService.list(new QueryWrapper().eq("symbol", symbol).between("ts", startAndEnd.get(0), startAndEnd.get(1)).orderByAsc("ts")); return etfSecKLineWrapper.toEntity(list); } public Tuple getStartAndEnd() { // 获取当前时间的时间戳(毫秒级) long currentTimestamp = System.currentTimeMillis(); // 转换为ZonedDateTime对象,使用系统默认时区 ZonedDateTime currentDateTime = Instant.ofEpochMilli(currentTimestamp) .atZone(ZoneId.systemDefault()); // 获取当天的21:30时间点 ZonedDateTime targetDateTime; if (currentDateTime.getHour() >= 4 && currentDateTime.getHour() <= 23) { // 当前时间在4点到21点之间,获取当天的21:30时间点 targetDateTime = currentDateTime.withHour(23).withMinute(59).withSecond(59).withNano(0); } else { // 当前时间在0点到4点之间,获取前一天的21:30时间点 targetDateTime = currentDateTime.minusDays(1).withHour(21).withMinute(30).withSecond(0).withNano(0); } // 获取第二天凌晨4点时间点 ZonedDateTime nextDayDateTime = targetDateTime.plusDays(1).withHour(4).withMinute(0); // 转换为毫秒时间戳 long start = targetDateTime.toInstant().toEpochMilli(); long end = nextDayDateTime.toInstant().toEpochMilli(); return new Tuple(start, end); } public Tuple getStartAndEnd(long ts) { // 转换为ZonedDateTime对象,使用系统默认时区 ZonedDateTime inputDateTime = Instant.ofEpochMilli(ts) .atZone(ZoneId.systemDefault()); // 获取当天的21:30时间点 ZonedDateTime targetDateTime = inputDateTime.withHour(21).withMinute(30).withSecond(0).withNano(0); // 获取第二天的4:00时间点 ZonedDateTime nextDayDateTime = targetDateTime.plusDays(1).withHour(4).withMinute(0); // 转换为毫秒时间戳 long start = targetDateTime.toInstant().toEpochMilli(); long end = nextDayDateTime.toInstant().toEpochMilli(); return new Tuple(start, end); } /** * 查询所有秒级K线图 * * @param symbol * @return */ public List querySecKlineBySymbolAllData(String symbol) { List list = etfSecKLineService.list(new QueryWrapper().eq("symbol", symbol).orderBy(true, true, "ts")); return etfSecKLineWrapper.toEntity(list); } }