package com.market.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.market.constant.SpotRedisKey;
import com.market.entity.Kline;
import com.market.model.binance.SpotKlineMessage;
import com.market.model.binance.SpotTickerMessage;
import com.market.model.binance.SpotTradeMessage;
import com.market.utils.SpotConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 现货行情数据处理服务（Binance → 直接推送，Kafka 通知撮合）
 * <p>
 * 优化点：
 * <ul>
 *   <li>Binance WS 数据直接推送前端，消除 MQ 中转延迟</li>
 *   <li>Kline 实时状态使用 Caffeine 内存缓存</li>
 *   <li>Caffeine 节流：kline/ticker 1000ms，trade 500ms</li>
 *   <li>Kafka 发送 SPOT_NEW_PRICE 通知撮合模块最新价格</li>
 * </ul>
 *
 * @author system
 * @date 2026-05-07
 */
@Service
public class SpotDataProcessService {

    private static final Logger log = LoggerFactory.getLogger(SpotDataProcessService.class);

    private static final Random RANDOM            = new Random();

    /** 上次发送到 Kafka 的价格缓存：key=symbol → 上次价格字符串，用于去重 */
    private static final Cache<String, String> LAST_NOTIFY_PRICE = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(5000)
            .build();

    /** 节流缓存：key="type:symbol" → 上次推送时间戳 */
    private static final Cache<String, Long> THROTTLE_CACHE = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.SECONDS)
            .maximumSize(50000)
            .build();

    /** K 线上一周期缓存：key="symbol:period" → 上一条 Kline */
    private static final Cache<String, SpotKlineMessage.Kline> LAST_KLINE_CACHE = Caffeine.newBuilder()
            .expireAfterAccess(5, TimeUnit.MINUTES)
            .maximumSize(10000)
            .build();

    /** K 线上次落库时间：key="symbol:period" → ms */
    private static final Cache<String, Long> KLINE_LAST_SAVE_TIME = Caffeine.newBuilder()
            .expireAfterAccess(1, TimeUnit.HOURS)
            .maximumSize(10000)
            .build();

    /**
     * 机器人 24h 前 1m K 线缓存：key="symbol:minuteStart24hAgo" → Optional&lt;Kline&gt;
     * <p>
     * key 以 24h 前对应的整分钟时间戳为粒度，每分钟自然变化；
     * {@code Optional.empty()} 表示 DB 中确认无数据（防止同一分钟内重复查库）。
     */
    private static final Cache<String, Optional<Kline>> ROBOT_OPEN_24H_CACHE = Caffeine.newBuilder()
            .expireAfterWrite(2, TimeUnit.MINUTES)
            .maximumSize(1000)
            .build();

    /**
     * 24h 合成 K 线历史聚合缓存：key=symbol → Object[]
     * <p>
     * Object[0] = Long hourStart（当前小时开始时间戳，用于判断是否需要刷新）<br>
     * Object[1] = BigDecimal open（23 根历史 1h 线中最老一根的 open，无历史则 null）<br>
     * Object[2] = BigDecimal maxHigh（23 根历史 1h 线的最高价，无历史则 null）<br>
     * Object[3] = BigDecimal minLow（23 根历史 1h 线的最低价，无历史则 null）<br>
     * Object[4] = BigDecimal sumVol（23 根历史 1h 线的成交量之和）<br>
     * Object[5] = BigDecimal sumAmount（23 根历史 1h 线的成交额之和）
     */
    private static final Map<String, Object[]> KLINE_24H_HIST_CACHE = new java.util.concurrent.ConcurrentHashMap<>();

    @Autowired
    private SpotMarketPushService marketPushService;

    @Autowired
    private SpotSymbolPrecisionService symbolPrecisionService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @Autowired
    private SpotKlineRecordService klineRecordService;

    @Value("${spot.kafka.topic.new-price:SPOT_NEW_PRICE}")
    private String spotNewPriceTopic;

    private final ObjectMapper objectMapper = new ObjectMapper();

    // ------------------------------------------------------------------ Kline

    /**
     * 处理 K 线消息
     *
     * @param message 币安 K 线消息
     */
    public void processKlineMessage(SpotKlineMessage message) {
        try {
            SpotKlineMessage.Kline kline = message.getKline();
            if (kline == null) {
                return;
            }
            String symbol = kline.getSymbol() != null ? kline.getSymbol() : message.getSymbol();
            if (symbol == null) {
                return;
            }
            String code     = symbol.toLowerCase();
            String interval = kline.getInterval();
            String period   = SpotConstant.KLINE_PERIOD_MAP.get(interval);
            if (period == null) {
                return;
            }

            // 1. 判断是否换周期，处理落库
            String cacheKey = code + ":" + period;
            SpotKlineMessage.Kline last = LAST_KLINE_CACHE.getIfPresent(cacheKey);
            boolean newPeriod = last == null || !kline.getStartTime().equals(last.getStartTime());

            if (newPeriod) {
                // 新周期：关闭旧 K 线（收盘价 = 新周期开盘价），插入新 K 线
                if (last != null) {
                    last.setClose(kline.getOpen());
                    klineRecordService.saveKlineData(code, last);
                }
                klineRecordService.saveKlineData(code, kline);
                KLINE_LAST_SAVE_TIME.put(cacheKey, System.currentTimeMillis());
            } else {
                // 同周期：先更新内存，再 5s 节流直接 UPDATE（无 SELECT）
                LAST_KLINE_CACHE.put(cacheKey, kline);
                long now = System.currentTimeMillis();
                Long lastSave = KLINE_LAST_SAVE_TIME.getIfPresent(cacheKey);
                if (lastSave == null || now - lastSave >= SpotConstant.KLINE_DB_SAVE_INTERVAL_MS) {
                    klineRecordService.updateKlineDirect(code, kline);
                    KLINE_LAST_SAVE_TIME.put(cacheKey, now);
                }
            }
            if (newPeriod) {
                LAST_KLINE_CACHE.put(cacheKey, kline);
            }

            // 2. 构建 tick 并推送（参照合约不做节流，每条 kline 消息直接推送）
            String flutterPeriod = SpotConstant.KLINE_TO_PERIOD_FLUTTER.get(interval);
            if (flutterPeriod == null) {
                return;
            }
            Map<String, Object> tick = buildKlineTick(kline);
            marketPushService.push(SpotConstant.CHANNEL_PREFIX_MARKET + code + SpotConstant.CHANNEL_TYPE_KLINE + flutterPeriod, tick);

            // 3. 1m K 线触发生成盘口深度（参照合约，随每条 kline 推送）
            if (SpotConstant.INTERVAL_1M.equals(interval)) {
                pushDepth(code, new BigDecimal(kline.getClose()));
            }

        } catch (Exception e) {
            log.error("处理现货 Kline 消息失败", e);
        }
    }

    // ------------------------------------------------------------------ Trade

    /**
     * 处理逐笔成交消息
     *
     * @param message 币安 Trade 消息
     */
    public void processTradeMessage(SpotTradeMessage message) {
        try {
            String code = message.getSymbol() != null ? message.getSymbol().toLowerCase() : null;
            if (code == null) {
                return;
            }

            // 1. 维护成交方向和金额
            String side = Boolean.TRUE.equals(message.getIsBuyerMaker()) ? SpotConstant.SIDE_SELL : SpotConstant.SIDE_BUY;
            BigDecimal amount = new BigDecimal(message.getPrice())
                    .multiply(new BigDecimal(message.getQuantity()));
            Map<String, Object> tradeData = buildTradeData(message, side, amount);

            // 2. 更新最新价格并通知撮合（Kafka 内部有价格去重）
            redisTemplate.opsForHash().put(SpotRedisKey.LATEST_PRICE, code, message.getPrice());
            notifyNewPrice(code, message.getPrice());

            // 3. 推送成交到前端
            marketPushService.push(SpotConstant.CHANNEL_PREFIX_MARKET + code + SpotConstant.CHANNEL_TYPE_TRADE_TICKER, tradeData);

            // 4. 用成交价驱动 kline 实时更新（Binance spot kline 流每 2s 才推一次，
            //    trade 流每笔成交即触发，节流后约 500ms，可模拟接近合约的更新频率）
            pushKlineFromTrade(code, message.getPrice());

        } catch (Exception e) {
            log.error("处理现货 Trade 消息失败", e);
        }
    }

    /**
     * 用成交价更新内存中所有周期的 kline 收盘价并推送给前端
     * <p>
     * Binance spot kline 流 2s 推一次；trade 流节流后约 500ms，
     * 用它驱动 kline 推送可让前端刷新频率与合约持平。
     *
     * @param code  交易对（小写）
     * @param price 最新成交价字符串
     */
    private void pushKlineFromTrade(String code, String price) {
        for (String interval : SpotConstant.KLINE_PERIODS) {
            try {
                String period = SpotConstant.KLINE_PERIOD_MAP.get(interval);
                if (period == null) {
                    continue;
                }
                String cacheKey = code + ":" + period;
                SpotKlineMessage.Kline cached = LAST_KLINE_CACHE.getIfPresent(cacheKey);
                if (cached == null) {
                    continue;
                }
                // 用最新成交价更新收盘价（内存副本，不持久化，5s 节流由 processKlineMessage 负责）
                cached.setClose(price);
                Map<String, Object> tick = buildKlineTick(cached);
                String flutterPeriod = SpotConstant.KLINE_TO_PERIOD_FLUTTER.get(interval);
                if (flutterPeriod == null) {
                    continue;
                }
                marketPushService.push(SpotConstant.CHANNEL_PREFIX_MARKET + code + SpotConstant.CHANNEL_TYPE_KLINE + flutterPeriod, tick);
            } catch (Exception e) {
                log.warn("trade 驱动 kline 推送失败: code={}, interval={}", code, interval, e);
            }
        }
    }

    // ------------------------------------------------------------------ Ticker

    /**
     * 处理 24h Ticker 消息
     *
     * @param message 币安 Ticker 消息
     */
    public void processTickerMessage(SpotTickerMessage message) {
        try {
            String code = message.getSymbol() != null ? message.getSymbol().toLowerCase() : null;
            if (code == null) {
                return;
            }
            // 3. 推送 ticker 到前端
            Map<String, Object> tickerData = buildTickerData(message);
            marketPushService.push(SpotConstant.CHANNEL_PREFIX_MARKET + code + SpotConstant.CHANNEL_TYPE_TICKER, tickerData);

        } catch (Exception e) {
            log.error("处理现货 Ticker 消息失败", e);
        }
    }

    // ------------------------------------------------------------------ private builders

    /** 构建 K 线 tick 数据 */
    private Map<String, Object> buildKlineTick(SpotKlineMessage.Kline kline) {
        BigDecimal close  = new BigDecimal(kline.getClose());
        BigDecimal open   = new BigDecimal(kline.getOpen());
        BigDecimal change = close.subtract(open);
        BigDecimal ratio  = open.compareTo(BigDecimal.ZERO) != 0
                ? change.divide(open, SpotConstant.DECIMAL_SCALE, RoundingMode.DOWN)
                .multiply(BigDecimal.valueOf(SpotConstant.PERCENTAGE_MULTIPLIER))
                : BigDecimal.ZERO;

        Map<String, Object> tick = new LinkedHashMap<>();
        tick.put(SpotConstant.DATA_FIELD_TIME,   kline.getStartTime());
        tick.put(SpotConstant.DATA_FIELD_OPEN,   kline.getOpen());
        tick.put(SpotConstant.DATA_FIELD_CLOSE,  kline.getClose());
        tick.put(SpotConstant.DATA_FIELD_HIGH,   kline.getHigh());
        tick.put(SpotConstant.DATA_FIELD_LOW,    kline.getLow());
        tick.put(SpotConstant.DATA_FIELD_VOL,    kline.getVolume());
        tick.put(SpotConstant.DATA_FIELD_AMOUNT, kline.getQuoteAssetVolume());
        tick.put("change", change.toPlainString());
        tick.put("ratio",  ratio.toPlainString());
        tick.put(SpotConstant.DATA_FIELD_ID,     kline.getStartTime() / SpotConstant.MILLISECONDS_TO_SECONDS);
        return tick;
    }

    /** 构建成交 tick 数据 */
    private Map<String, Object> buildTradeData(SpotTradeMessage message, String side, BigDecimal amount) {
        Map<String, Object> tradeData = new LinkedHashMap<>();
        tradeData.put(SpotConstant.DATA_FIELD_ID,     message.getTradeId());
        tradeData.put(SpotConstant.DATA_FIELD_TIME,   message.getTradeTime());
        tradeData.put("price",  message.getPrice());
        tradeData.put(SpotConstant.DATA_FIELD_VOL,    message.getQuantity());
        tradeData.put(SpotConstant.DATA_FIELD_AMOUNT, amount.toPlainString());
        tradeData.put("side",   side);
        return tradeData;
    }

    /** 构建推送给前端的 ticker 数据（基于 24hrMiniTicker，涨跌幅由 open/close 计算） */
    private Map<String, Object> buildTickerData(SpotTickerMessage message) {
        BigDecimal close = message.getCurrentPrice() != null ? new BigDecimal(message.getCurrentPrice()) : BigDecimal.ZERO;
        BigDecimal open  = message.getOpenPrice()    != null ? new BigDecimal(message.getOpenPrice())    : BigDecimal.ZERO;
        BigDecimal rose  = open.compareTo(BigDecimal.ZERO) != 0
                ? close.subtract(open).divide(open, SpotConstant.DECIMAL_SCALE, RoundingMode.DOWN)
                        .multiply(BigDecimal.valueOf(SpotConstant.PERCENTAGE_MULTIPLIER))
                : BigDecimal.ZERO;

        Map<String, Object> d = new LinkedHashMap<>();
        d.put(SpotConstant.DATA_FIELD_OPEN,   message.getOpenPrice());
        d.put(SpotConstant.DATA_FIELD_CLOSE,  message.getCurrentPrice());
        d.put(SpotConstant.DATA_FIELD_HIGH,   message.getHighPrice());
        d.put(SpotConstant.DATA_FIELD_LOW,    message.getLowPrice());
        d.put(SpotConstant.DATA_FIELD_ROSE,   rose.toPlainString());
        d.put(SpotConstant.DATA_FIELD_VOL,    message.getVolume());
        d.put(SpotConstant.DATA_FIELD_AMOUNT, message.getQuoteVolume());
        d.put("es_price", message.getCurrentPrice());
        return d;
    }

    // ------------------------------------------------------------------ 手动注入价格

    /**
     * 手动注入价格：遍历所有已订阅周期，将内存缓存和数据库中最新一根 K 线的收盘价更新为指定价格，
     * 并同步更新 Redis 最新成交价（ExchangeNew2:latestPrice Hash）。
     *
     * @param symbol 交易对（小写，如 btcusdt）
     * @param price  注入价格
     */
    public void applyManualPrice(String symbol, BigDecimal price) {
        // 1. 遍历所有周期，更新内存缓存和数据库
        String priceStr = price.toPlainString();
        for (String interval : SpotConstant.KLINE_PERIODS) {
            updateSpotPeriodClose(symbol, interval, priceStr);
        }

        // 2. 更新 Redis 最新成交价
        try {
            redisTemplate.opsForHash().put(SpotRedisKey.LATEST_PRICE, symbol, priceStr);
        } catch (Exception e) {
            log.warn("手动现货价格写 Redis 失败: symbol={}", symbol, e);
        }

        // 3. 通知现货撮合引擎最新价（Kafka SPOT_NEW_PRICE）
        notifyNewPrice(symbol, priceStr);
    }

    /** 更新单个周期的内存缓存和数据库收盘价 */
    private void updateSpotPeriodClose(String symbol, String interval, String priceStr) {
        String period = SpotConstant.KLINE_PERIOD_MAP.get(interval);
        if (period == null) {
            return;
        }
        String cacheKey = symbol + ":" + period;
        SpotKlineMessage.Kline cached = LAST_KLINE_CACHE.getIfPresent(cacheKey);
        if (cached == null) {
            return;
        }
        // 更新内存
        cached.setClose(priceStr);
        LAST_KLINE_CACHE.put(cacheKey, cached);
        // 更新数据库
        try {
            klineRecordService.saveKlineData(symbol, cached);
        } catch (Exception e) {
            log.warn("手动现货价格写库失败: symbol={}, interval={}", symbol, interval, e);
        }
    }

    // ------------------------------------------------------------------ 机器人价格处理

    /**
     * 机器人线程启动前预热各周期 kline 缓存
     * <p>
     * close 价格优先级：① Redis 最新成交价；② DB kline 的 open 价（无 Redis 数据时兜底）。
     * 预热后，新周期开盘价可以直接取上一根线的 close，保证 K 线数据连续。
     * 若缓存中已有该 symbol 的某周期数据则跳过（避免覆盖实时数据）。
     *
     * @param symbol 交易对（小写）
     */
    public void preloadRobotKlineCache(String symbol) {
        // 优先从 Redis 获取最新成交价作为 close 基准
        String redisPrice = null;
        try {
            Object val = redisTemplate.opsForHash().get(SpotRedisKey.LATEST_PRICE, symbol);
            if (val != null) {
                redisPrice = val.toString();
            }
        } catch (Exception e) {
            log.warn("机器人缓存预热读取 Redis 最新价失败: symbol={}", symbol, e);
        }
        log.info("机器人 kline 缓存预热: symbol={}, redisPrice={}", symbol, redisPrice != null ? redisPrice : "未找到，使用 open_price 兜底");

        for (String interval : SpotConstant.KLINE_PERIODS) {
            String period = SpotConstant.KLINE_PERIOD_MAP.get(interval);
            if (period == null) {
                continue;
            }
            String cacheKey = symbol + ":" + period;
            // 缓存已有数据则跳过
            if (LAST_KLINE_CACHE.getIfPresent(cacheKey) != null) {
                continue;
            }
            try {
                Kline kline = klineRecordService.queryLatestKline(symbol, period);
                if (kline == null) {
                    continue;
                }
                // close = Redis最新价 > DB kline.open（兜底）
                String closeStr = redisPrice != null ? redisPrice
                        : (kline.getOpen() != null ? kline.getOpen().toPlainString() : "0");
                LAST_KLINE_CACHE.put(cacheKey, buildSpotKlineFromEntity(kline, symbol, interval, closeStr));
                log.debug("机器人缓存预热: symbol={}, interval={}, close={}", symbol, interval, closeStr);
            } catch (Exception e) {
                log.warn("机器人缓存预热失败: symbol={}, interval={}", symbol, interval, e);
            }
        }
        log.info("机器人 kline 缓存预热完成: symbol={}", symbol);
    }

    /**
     * 将数据库 Kline 实体转换为 SpotKlineMessage.Kline（用于缓存预热）
     * <p>
     * close 由外部传入（Redis最新价或 open 兜底），high/low 在原始区间与 close 之间取极值。
     *
     * @param kline    数据库 K 线实体
     * @param symbol   交易对（小写）
     * @param interval 币安周期字符串
     * @param closeStr close 价格字符串
     * @return SpotKlineMessage.Kline 对象
     */
    private SpotKlineMessage.Kline buildSpotKlineFromEntity(Kline kline, String symbol,
                                                             String interval, String closeStr) {
        BigDecimal closeVal  = new BigDecimal(closeStr);
        BigDecimal dbHigh    = kline.getHigh() != null ? kline.getHigh() : closeVal;
        BigDecimal dbLow     = kline.getLow()  != null ? kline.getLow()  : closeVal;
        String high = dbHigh.compareTo(closeVal) >= 0 ? dbHigh.toPlainString() : closeStr;
        String low  = dbLow.compareTo(closeVal)  <= 0 ? dbLow.toPlainString()  : closeStr;

        SpotKlineMessage.Kline sk = new SpotKlineMessage.Kline();
        sk.setSymbol(symbol);
        sk.setInterval(interval);
        sk.setStartTime(kline.getBeginTime());
        sk.setEndTime(kline.getEndTime());
        sk.setOpen(kline.getOpen() != null ? kline.getOpen().toPlainString() : "0");
        sk.setClose(closeStr);
        sk.setHigh(high);
        sk.setLow(low);
        sk.setVolume(kline.getVolume() != null ? kline.getVolume().toPlainString() : "0");
        sk.setQuoteAssetVolume(kline.getTurnover() != null ? kline.getTurnover().toPlainString() : "0");
        sk.setIsClosed(true);
        return sk;
    }

    /**
     * 机器人自动生成价格处理入口
     * <p>
     * 初始化或更新各周期 kline 缓存并推送，同时推送 trade 和 24hrMiniTicker，
     * 最后更新 Redis 最新价并通知撮合引擎。
     *
     * @param symbol         交易对（小写）
     * @param price          机器人生成的最新价格
     * @param klineHighRatio 本次K线 high 上限比例（high ≤ close × (1 + ratio)），NULL 时不限制
     * @param klineLowRatio  本次K线 low 下限比例（low ≥ close × (1 - ratio)），NULL 时不限制
     */
    public void applyRobotPrice(String symbol, BigDecimal price,
                                BigDecimal klineHighRatio, BigDecimal klineLowRatio) {
        String priceStr = price.toPlainString();
        long now = System.currentTimeMillis();

        // 预先生成本次成交量/成交额（所有周期共享同一笔模拟成交）
        BigDecimal tradeAmount = BigDecimal.valueOf(200 + ThreadLocalRandom.current().nextInt(1801));
        BigDecimal tradeVol    = price.compareTo(BigDecimal.ZERO) != 0
                ? tradeAmount.divide(price, price.scale() + 4, RoundingMode.DOWN)
                : BigDecimal.ZERO;

        // 1. 更新/初始化各周期 kline（含 volume/turnover 累加），并推送 kline 到前端
        for (String interval : SpotConstant.KLINE_PERIODS) {
            try {
                updateOrInitRobotKline(symbol, interval, priceStr, tradeVol, tradeAmount,
                        now, klineHighRatio, klineLowRatio);
            } catch (Exception e) {
                log.warn("机器人 kline 更新失败: symbol={}, interval={}", symbol, interval, e);
            }
        }

        // 2. 更新 Redis 最新成交价
        try {
            redisTemplate.opsForHash().put(SpotRedisKey.LATEST_PRICE, symbol, priceStr);
        } catch (Exception e) {
            log.warn("机器人价格写 Redis 失败: symbol={}", symbol, e);
        }

        // 3. 推送模拟成交记录（trade，复用入口处生成的 vol/amount）
        try {
            pushRobotTrade(symbol, priceStr, tradeVol, tradeAmount, now);
        } catch (Exception e) {
            log.warn("机器人 trade 推送失败: symbol={}", symbol, e);
        }

        // 4. 推送 24hrMiniTicker（24h 无数据时开盘价返回 "0"，涨跌幅显示 0）
        try {
            pushRobotTicker(symbol, priceStr, now);
        } catch (Exception e) {
            log.warn("机器人 ticker 推送失败: symbol={}", symbol, e);
        }

        // 5. 通知撮合引擎最新价
        notifyNewPrice(symbol, priceStr);
    }

    /**
     * 更新或初始化指定周期的 kline 缓存，并推送到前端
     *
     * @param symbol         交易对（小写）
     * @param interval       币安周期字符串（如 "1m"）
     * @param priceStr       最新价格字符串
     * @param tradeVol       本次模拟成交量
     * @param tradeAmount    本次模拟成交额（USDT）
     * @param now            当前时间戳（毫秒）
     * @param klineHighRatio high 上限比例，NULL 时不限制
     * @param klineLowRatio  low 下限比例，NULL 时不限制
     */
    private void updateOrInitRobotKline(String symbol, String interval, String priceStr,
                                         BigDecimal tradeVol, BigDecimal tradeAmount, long now,
                                         BigDecimal klineHighRatio, BigDecimal klineLowRatio) {
        String period = SpotConstant.KLINE_PERIOD_MAP.get(interval);
        if (period == null) {
            return;
        }

        String cacheKey = symbol + ":" + period;
        SpotKlineMessage.Kline cached = LAST_KLINE_CACHE.getIfPresent(cacheKey);
        long periodStart = computePeriodStart(interval, now);

        if (cached == null || cached.getStartTime() < periodStart) {
            // 无缓存或新周期：关闭旧 kline（如有），初始化新 kline
            String openPriceStr = (cached != null) ? cached.getClose() : priceStr;
            if (cached != null) {
                klineRecordService.saveKlineData(symbol, cached);
            }
            cached = buildSyntheticKline(symbol, interval, openPriceStr, priceStr,
                    tradeVol, tradeAmount, periodStart, klineHighRatio, klineLowRatio);
            LAST_KLINE_CACHE.put(cacheKey, cached);
            klineRecordService.saveKlineData(symbol, cached);
            KLINE_LAST_SAVE_TIME.put(cacheKey, now);
        } else {
            // 同周期：更新 close / high / low，累加 volume / turnover
            BigDecimal newPrice = new BigDecimal(priceStr);
            BigDecimal openPrice = new BigDecimal(cached.getOpen());
            cached.setClose(priceStr);

            // high 候选：取 max(当前价, 已有high)，历史最高只单调递增，不因价格回落而压低
            BigDecimal candidateHigh = newPrice.compareTo(new BigDecimal(cached.getHigh())) > 0
                    ? newPrice : new BigDecimal(cached.getHigh());
            // 保证 high >= max(open, close)
            BigDecimal bodyHigh = openPrice.compareTo(newPrice) >= 0 ? openPrice : newPrice;
            if (candidateHigh.compareTo(bodyHigh) < 0) {
                candidateHigh = bodyHigh;
            }
            cached.setHigh(candidateHigh.toPlainString());

            // low 候选：取 min(当前价, 已有low)，历史最低只单调递减，不因价格回升而抬高
            BigDecimal candidateLow = newPrice.compareTo(new BigDecimal(cached.getLow())) < 0
                    ? newPrice : new BigDecimal(cached.getLow());
            // 保证 low <= min(open, close)
            BigDecimal bodyLow = openPrice.compareTo(newPrice) <= 0 ? openPrice : newPrice;
            if (candidateLow.compareTo(bodyLow) > 0) {
                candidateLow = bodyLow;
            }
            cached.setLow(candidateLow.toPlainString());

            // 累加成交量和成交额
            BigDecimal newVol    = new BigDecimal(cached.getVolume()).add(tradeVol);
            BigDecimal newAmount = new BigDecimal(cached.getQuoteAssetVolume()).add(tradeAmount);
            cached.setVolume(newVol.toPlainString());
            cached.setQuoteAssetVolume(newAmount.toPlainString());
            LAST_KLINE_CACHE.put(cacheKey, cached);
            // 5s 节流写库
            Long lastSave = KLINE_LAST_SAVE_TIME.getIfPresent(cacheKey);
            if (lastSave == null || now - lastSave >= SpotConstant.KLINE_DB_SAVE_INTERVAL_MS) {
                klineRecordService.updateKlineDirect(symbol, cached);
                KLINE_LAST_SAVE_TIME.put(cacheKey, now);
            }
        }

        // 推送 kline 到前端
        String flutterPeriod = SpotConstant.KLINE_TO_PERIOD_FLUTTER.get(interval);
        if (flutterPeriod != null) {
            Map<String, Object> tick = buildKlineTick(cached);
            marketPushService.push(
                    SpotConstant.CHANNEL_PREFIX_MARKET + symbol + SpotConstant.CHANNEL_TYPE_KLINE + flutterPeriod,
                    tick);
        }

        // 1m kline 同步触发深度推送
        if (SpotConstant.INTERVAL_1M.equals(interval)) {
            pushDepth(symbol, new BigDecimal(priceStr));
        }
    }

    /**
     * 构建合成 kline
     * <p>
     * open = 上一根线 close，close = 当前价，
     * high/low 在 open/close 极值基础上加随机影线，但不超过 klineHighRatio/klineLowRatio 限制。
     *
     * @param symbol         交易对（小写）
     * @param interval       币安周期字符串
     * @param openPriceStr   开盘价字符串
     * @param closePriceStr  收盘价字符串
     * @param tradeVol       本次成交量
     * @param tradeAmount    本次成交额（USDT）
     * @param startTime      周期开始时间戳（毫秒）
     * @param klineHighRatio high 上限比例，NULL 时不限制
     * @param klineLowRatio  low 下限比例，NULL 时不限制
     * @return 合成 kline 对象
     */
    private SpotKlineMessage.Kline buildSyntheticKline(String symbol, String interval,
                                                        String openPriceStr, String closePriceStr,
                                                        BigDecimal tradeVol, BigDecimal tradeAmount,
                                                        long startTime,
                                                        BigDecimal klineHighRatio, BigDecimal klineLowRatio) {
        long durationMs = getPeriodDurationMs(interval);
        BigDecimal open  = new BigDecimal(openPriceStr);
        BigDecimal close = new BigDecimal(closePriceStr);

        // 取 open/close 极值作为影线基准
        BigDecimal bodyHigh = open.compareTo(close) >= 0 ? open : close;
        BigDecimal bodyLow  = open.compareTo(close) <= 0 ? open : close;

        // 实体高度占价格的比例，实体越大影线越小
        BigDecimal bodyRange = bodyHigh.subtract(bodyLow)
                .divide(bodyLow, 8, RoundingMode.HALF_UP);
        BigDecimal maxWick = new BigDecimal("0.005").subtract(bodyRange);
        if (maxWick.compareTo(BigDecimal.ZERO) <= 0) {
            maxWick = BigDecimal.ZERO;
        }

        BigDecimal highVal;
        BigDecimal lowVal;
        if (maxWick.compareTo(BigDecimal.ZERO) > 0) {
            double maxWickD = maxWick.doubleValue();
            double wickHighRate = 0.0002 + ThreadLocalRandom.current().nextDouble(maxWickD);
            double wickLowRate  = 0.0002 + ThreadLocalRandom.current().nextDouble(maxWickD);
            highVal = bodyHigh.multiply(BigDecimal.ONE.add(BigDecimal.valueOf(wickHighRate)))
                    .setScale(bodyHigh.scale(), RoundingMode.HALF_UP);
            lowVal  = bodyLow.multiply(BigDecimal.ONE.subtract(BigDecimal.valueOf(wickLowRate)))
                    .setScale(bodyLow.scale(), RoundingMode.HALF_UP);
        } else {
            highVal = bodyHigh;
            lowVal  = bodyLow;
        }

        // 用 klineHighRatio/klineLowRatio 限制 high/low 上下界
        if (klineHighRatio != null) {
            BigDecimal highLimit = close.multiply(BigDecimal.ONE.add(klineHighRatio));
            if (highVal.compareTo(highLimit) > 0) {
                highVal = highLimit;
            }
        }
        if (klineLowRatio != null) {
            BigDecimal lowLimit = close.multiply(BigDecimal.ONE.subtract(klineLowRatio));
            if (lowVal.compareTo(lowLimit) < 0) {
                lowVal = lowLimit;
            }
        }

        // 确保 high >= max(open,close)，low <= min(open,close)
        if (highVal.compareTo(bodyHigh) < 0) { highVal = bodyHigh; }
        if (lowVal.compareTo(bodyLow)   > 0) { lowVal  = bodyLow;  }

        SpotKlineMessage.Kline kline = new SpotKlineMessage.Kline();
        kline.setSymbol(symbol);
        kline.setInterval(interval);
        kline.setStartTime(startTime);
        kline.setEndTime(startTime + durationMs - 1L);
        kline.setOpen(openPriceStr);
        kline.setClose(closePriceStr);
        kline.setHigh(highVal.toPlainString());
        kline.setLow(lowVal.toPlainString());
        kline.setVolume(tradeVol.toPlainString());
        kline.setQuoteAssetVolume(tradeAmount.toPlainString());
        kline.setIsClosed(false);
        return kline;
    }

    /**
     * 推送模拟成交记录到前端（trade channel）
     * <p>
     * vol/amount 由调用方统一生成并传入，与 kline 累加数据保持一致。
     *
     * @param symbol      交易对（小写）
     * @param priceStr    最新价格字符串
     * @param tradeVol    本次成交量
     * @param tradeAmount 本次成交额（USDT）
     * @param now         当前时间戳（毫秒）
     */
    private void pushRobotTrade(String symbol, String priceStr,
                                 BigDecimal tradeVol, BigDecimal tradeAmount, long now) {
        Map<String, Object> tradeData = new LinkedHashMap<>();
        tradeData.put(SpotConstant.DATA_FIELD_ID,     now);
        tradeData.put(SpotConstant.DATA_FIELD_TIME,   now);
        tradeData.put("price",                         priceStr);
        tradeData.put(SpotConstant.DATA_FIELD_VOL,    tradeVol.toPlainString());
        tradeData.put(SpotConstant.DATA_FIELD_AMOUNT, tradeAmount.toPlainString());
        tradeData.put("side", ThreadLocalRandom.current().nextBoolean()
                ? SpotConstant.SIDE_BUY : SpotConstant.SIDE_SELL);
        marketPushService.push(
                SpotConstant.CHANNEL_PREFIX_MARKET + symbol + SpotConstant.CHANNEL_TYPE_TRADE_TICKER,
                tradeData);
    }

    /**
     * 推送 24hrMiniTicker 到前端（ticker channel）
     * <p>
     * <b>数据来源：</b>
     * <ul>
     *   <li>{@code kline24h}（24h 前整分钟 1m kline）：提供 open；为 null 时所有 24h 字段置 "0"</li>
     *   <li>当前 1m kline 缓存（{@code LAST_KLINE_CACHE}）：提供 high / low / vol / amount</li>
     *   <li>当前价格：close 和 es_price</li>
     * </ul>
     *
     * @param symbol   交易对（小写）
     * @param priceStr 最新价格字符串
     * @param now      当前时间戳（毫秒）
     */
    private void pushRobotTicker(String symbol, String priceStr, long now) {
        // 1. 取 24h 前整分钟对应的 kline
        long open24hMs = computePeriodStart("1m", now - 86_400_000L);
        Kline kline24h = load24hKline(symbol, open24hMs);

        // 2. 取 24h 合成 K 线（23 根历史 1h + 当前 1h 内存线拼接）
        SpotKlineMessage.Kline kline1d = load24hSyntheticKline(symbol, now);
        String high   = kline1d != null && kline1d.getHigh()             != null ? kline1d.getHigh()             : "0";
        String low    = kline1d != null && kline1d.getLow()              != null ? kline1d.getLow()              : "0";
        String vol    = kline1d != null && kline1d.getVolume()           != null ? kline1d.getVolume()           : "0";
        String amount = kline1d != null && kline1d.getQuoteAssetVolume() != null ? kline1d.getQuoteAssetVolume() : "0";

        // open24h：优先取 24h 前精确 1m 线的 open；无数据则用合成 K 线的 open（最老 1h 线 open）兜底
        boolean has24h = kline24h != null && kline24h.getOpen() != null;
        String open24h;
        if (has24h) {
            open24h = kline24h.getOpen().toPlainString();
        } else if (kline1d != null && kline1d.getOpen() != null) {
            open24h = kline1d.getOpen();
            has24h  = true;
        } else {
            open24h = "0";
        }

        // 3. 计算涨跌幅（open=0 时 rose=0，防止除零）
        BigDecimal close = new BigDecimal(priceStr);
        BigDecimal open  = new BigDecimal(open24h);
        BigDecimal rose  = has24h && open.compareTo(BigDecimal.ZERO) != 0
                ? close.subtract(open).divide(open, SpotConstant.DECIMAL_SCALE, RoundingMode.DOWN)
                        .multiply(BigDecimal.valueOf(SpotConstant.PERCENTAGE_MULTIPLIER))
                : BigDecimal.ZERO;

        // 4. 推送
        Map<String, Object> d = new LinkedHashMap<>();
        d.put(SpotConstant.DATA_FIELD_OPEN,   open24h);
        d.put(SpotConstant.DATA_FIELD_CLOSE,  priceStr);
        d.put(SpotConstant.DATA_FIELD_HIGH,   high);
        d.put(SpotConstant.DATA_FIELD_LOW,    low);
        d.put(SpotConstant.DATA_FIELD_ROSE,   rose.toPlainString());
        d.put(SpotConstant.DATA_FIELD_VOL,    vol);
        d.put(SpotConstant.DATA_FIELD_AMOUNT, amount);
        d.put("es_price",                      priceStr);
        marketPushService.push(
                SpotConstant.CHANNEL_PREFIX_MARKET + symbol + SpotConstant.CHANNEL_TYPE_TICKER,
                d);
    }

    /**
     * 获取 24 小时前整分钟时间点的 1m K 线
     * <p>
     * 先查 {@link #ROBOT_OPEN_24H_CACHE}（key={@code "symbol:open24hMs"}）：
     * 命中 → 直接返回（{@code Optional.empty()} 表示 DB 已确认无数据）；
     * 未命中 → 查 DB，将结果包装为 {@code Optional} 写入缓存后返回。
     * <p>
     * key 每分钟自然变化，同一分钟内多次 tick 只查一次 DB。
     *
     * @param symbol    交易对（小写）
     * @param open24hMs 24h 前整分钟时间戳（毫秒）
     * @return 对应的 {@link Kline} 实体，无数据时返回 null
     */
    private Kline load24hKline(String symbol, long open24hMs) {
        String cacheKey = symbol + ":" + open24hMs;

        // 查缓存（Optional.empty() = 已确认无数据）
        Optional<Kline> cached = ROBOT_OPEN_24H_CACHE.getIfPresent(cacheKey);
        if (cached != null) {
            return cached.orElse(null);
        }

        // 缓存未命中，查 DB
        Kline kline = klineRecordService.queryKlineByBeginTime(symbol, "1MIN", open24hMs);

        // 无论有无数据均写缓存，防止同一分钟内重复查库
        ROBOT_OPEN_24H_CACHE.put(cacheKey, Optional.ofNullable(kline));
        return kline;
    }

    /**
     * 加载 24h 合成 K 线：23 根历史 1h 线（DB）+ 当前 1h 线（内存/DB）拼接
     * <p>
     * 历史聚合结果按小时缓存，整点后首次调用时刷新；当前 1h 线每 tick 实时合入。
     * <ul>
     *   <li>open   = 最老一根 1h 线的 open（无历史则取当前 1h 线 open）</li>
     *   <li>close  = 当前 1h 线的 close（即最新价）</li>
     *   <li>high   = 24 根线 high 的最大值</li>
     *   <li>low    = 24 根线 low 的最小值</li>
     *   <li>vol    = 24 根线成交量之和</li>
     *   <li>amount = 24 根线成交额之和</li>
     * </ul>
     *
     * @param symbol 交易对（小写）
     * @param now    当前时间戳（毫秒）
     * @return 合成 K 线，无任何数据时返回 null
     */
    private SpotKlineMessage.Kline load24hSyntheticKline(String symbol, long now) {
        long hourStart = (now / 3_600_000L) * 3_600_000L;
        String h1Period = SpotConstant.KLINE_PERIOD_MAP.get("1h");

        // 1. 检查历史聚合缓存是否仍在当前小时内，失效则重新从 DB 聚合 23 根历史 1h 线
        Object[] hist = KLINE_24H_HIST_CACHE.get(symbol);
        if (hist == null || (long) hist[0] != hourStart) {
            List<Kline> rows = klineRecordService.queryKlinesBefore(symbol, h1Period, hourStart, 23);
            BigDecimal histOpen      = null;
            BigDecimal histMaxHigh   = null;
            BigDecimal histMinLow    = null;
            BigDecimal histSumVol    = BigDecimal.ZERO;
            BigDecimal histSumAmount = BigDecimal.ZERO;
            if (!rows.isEmpty()) {
                // 降序结果：最后一条是最老的，open 取最老那根
                histOpen = rows.get(rows.size() - 1).getOpen();
                for (Kline row : rows) {
                    if (row.getHigh()     != null && (histMaxHigh == null || row.getHigh().compareTo(histMaxHigh) > 0)) {
                        histMaxHigh = row.getHigh();
                    }
                    if (row.getLow()      != null && (histMinLow  == null || row.getLow().compareTo(histMinLow)   < 0)) {
                        histMinLow = row.getLow();
                    }
                    if (row.getVolume()   != null) { histSumVol    = histSumVol.add(row.getVolume()); }
                    if (row.getTurnover() != null) { histSumAmount = histSumAmount.add(row.getTurnover()); }
                }
            }
            hist = new Object[]{hourStart, histOpen, histMaxHigh, histMinLow, histSumVol, histSumAmount};
            KLINE_24H_HIST_CACHE.put(symbol, hist);
        }

        // 2. 取内存中当前 1h 线；内存缓存可能因 5min 无访问而过期，回退查 DB 当前小时线
        SpotKlineMessage.Kline cur = LAST_KLINE_CACHE.getIfPresent(symbol + ":" + h1Period);
        if (cur == null) {
            Kline dbCur = klineRecordService.queryKlineByBeginTime(symbol, h1Period, hourStart);
            if (dbCur != null) {
                String closeStr = dbCur.getClose() != null
                        ? dbCur.getClose().toPlainString()
                        : dbCur.getOpen().toPlainString();
                cur = buildSpotKlineFromEntity(dbCur, symbol, "1h", closeStr);
            }
        }

        // 3. 从缓存中取历史聚合值
        BigDecimal histOpen      = (BigDecimal) hist[1];
        BigDecimal histMaxHigh   = (BigDecimal) hist[2];
        BigDecimal histMinLow    = (BigDecimal) hist[3];
        BigDecimal histSumVol    = (BigDecimal) hist[4];
        BigDecimal histSumAmount = (BigDecimal) hist[5];

        // open：优先历史最老 open，无历史则取当前 1h open
        String openStr = histOpen != null ? histOpen.toPlainString()
                : (cur != null && cur.getOpen() != null ? cur.getOpen() : null);
        if (openStr == null) {
            return null;
        }

        // 4. 合并当前 1h 线的 high / low / vol / amount
        BigDecimal curHigh   = cur != null && cur.getHigh()             != null ? new BigDecimal(cur.getHigh())             : null;
        BigDecimal curLow    = cur != null && cur.getLow()              != null ? new BigDecimal(cur.getLow())              : null;
        BigDecimal curVol    = cur != null && cur.getVolume()           != null ? new BigDecimal(cur.getVolume())           : BigDecimal.ZERO;
        BigDecimal curAmount = cur != null && cur.getQuoteAssetVolume() != null ? new BigDecimal(cur.getQuoteAssetVolume()) : BigDecimal.ZERO;

        // high = max(历史最高, 当前1h最高)
        BigDecimal high = histMaxHigh;
        if (curHigh != null && (high == null || curHigh.compareTo(high) > 0)) { high = curHigh; }

        // low = min(历史最低, 当前1h最低)
        BigDecimal low = histMinLow;
        if (curLow != null && (low == null || curLow.compareTo(low) < 0)) { low = curLow; }

        BigDecimal totalVol    = histSumVol.add(curVol);
        BigDecimal totalAmount = histSumAmount.add(curAmount);

        // 5. 组装合成 K 线对象
        SpotKlineMessage.Kline synthetic = new SpotKlineMessage.Kline();
        synthetic.setOpen(openStr);
        synthetic.setClose(cur != null ? cur.getClose() : openStr);
        synthetic.setHigh(high  != null ? high.toPlainString()  : openStr);
        synthetic.setLow(low    != null ? low.toPlainString()   : openStr);
        synthetic.setVolume(totalVol.toPlainString());
        synthetic.setQuoteAssetVolume(totalAmount.toPlainString());
        return synthetic;
    }

    /**
     * 计算指定周期在给定时间点的开始时间戳（毫秒，UTC）
     *
     * @param interval 币安周期字符串（如 "1m"、"4h"、"1d" 等）
     * @param nowMs    当前时间戳（毫秒）
     * @return 周期开始时间戳
     */
    private long computePeriodStart(String interval, long nowMs) {
        switch (interval) {
            case "1m":  return nowMs - (nowMs % 60_000L);
            case "5m":  return nowMs - (nowMs % 300_000L);
            case "15m": return nowMs - (nowMs % 900_000L);
            case "30m": return nowMs - (nowMs % 1_800_000L);
            case "1h":  return nowMs - (nowMs % 3_600_000L);
            case "4h":  return nowMs - (nowMs % 14_400_000L);
            case "1d":  return nowMs - (nowMs % 86_400_000L);
            case "1w": {
                // 1970-01-01 是周四，以周一为周起始，向前偏移 3 天对齐
                long adjusted = nowMs + 3L * 86_400_000L;
                return adjusted - (adjusted % (7L * 86_400_000L)) - 3L * 86_400_000L;
            }
            case "1M": {
                // 截断到 UTC 月初 00:00:00.000
                Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
                cal.setTimeInMillis(nowMs);
                cal.set(Calendar.DAY_OF_MONTH, 1);
                cal.set(Calendar.HOUR_OF_DAY,  0);
                cal.set(Calendar.MINUTE,        0);
                cal.set(Calendar.SECOND,        0);
                cal.set(Calendar.MILLISECOND,   0);
                return cal.getTimeInMillis();
            }
            default: return nowMs - (nowMs % 60_000L);
        }
    }

    /**
     * 获取指定周期的时长（毫秒）；月周期使用 30 天近似值
     *
     * @param interval 币安周期字符串
     * @return 时长（毫秒）
     */
    private long getPeriodDurationMs(String interval) {
        switch (interval) {
            case "1m":  return 60_000L;
            case "5m":  return 300_000L;
            case "15m": return 900_000L;
            case "30m": return 1_800_000L;
            case "1h":  return 3_600_000L;
            case "4h":  return 14_400_000L;
            case "1d":  return 86_400_000L;
            case "1w":  return 7L * 86_400_000L;
            case "1M":  return 30L * 86_400_000L;
            default:    return 60_000L;
        }
    }

    // ------------------------------------------------------------------ 深度生成

    /**
     * 根据最新收盘价生成模拟三档盘口深度并推送
     * <p>
     * depth2Pre：随机步长，最精细档（如 100.0010, 100.0013...）<br>
     * depth1Pre：固定步长，从 depth2Pre 外侧顺序延伸（如 100.01, 100.02...）<br>
     * depth0Pre：固定步长，从 depth1Pre 外侧顺序延伸（如 101, 102...）
     *
     * @param symbol 交易对（小写）
     * @param close  最新收盘价
     */
    private void pushDepth(String symbol, BigDecimal close) {
        try {
            // 1. 获取精度配置，不存在时按收盘价小数位降级
            Integer depth0Pre = symbolPrecisionService.getPrecision(symbol, 0);
            Integer depth1Pre = symbolPrecisionService.getPrecision(symbol, 1);
            Integer depth2Pre = symbolPrecisionService.getPrecision(symbol, 2);
            if (depth0Pre == null || depth1Pre == null || depth2Pre == null) {
                depth2Pre = close.scale();
                depth1Pre = Math.max(0, depth2Pre - 2);
                depth0Pre = Math.max(0, depth2Pre - 4);
            }

            // 2. depth2Pre：随机步长生成最精细档（买盘降序、卖盘升序，共 DEPTH_LEVELS 条）
            BigDecimal baseStep = BigDecimal.ONE.scaleByPowerOfTen(-depth2Pre);
            List<List<String>> rawBids = new ArrayList<>();
            List<List<String>> rawAsks = new ArrayList<>();
            BigDecimal bidOffset = BigDecimal.ZERO;
            BigDecimal askOffset = BigDecimal.ZERO;

            for (int i = 1; i <= SpotConstant.DEPTH_LEVELS; i++) {
                bidOffset = bidOffset.add(baseStep.multiply(BigDecimal.valueOf(SpotConstant.RANDOM_STEP_BASE + RANDOM.nextInt(SpotConstant.RANDOM_STEP_BOUND))));
                askOffset = askOffset.add(baseStep.multiply(BigDecimal.valueOf(SpotConstant.RANDOM_STEP_BASE + RANDOM.nextInt(SpotConstant.RANDOM_STEP_BOUND))));

                BigDecimal bidPrice = close.subtract(bidOffset).setScale(depth2Pre, RoundingMode.DOWN);
                BigDecimal askPrice = close.add(askOffset).setScale(depth2Pre, RoundingMode.HALF_UP);

                BigDecimal bidQty = BigDecimal.valueOf(SpotConstant.RANDOM_QTY_BASE + RANDOM.nextInt(SpotConstant.RANDOM_QTY_BOUND))
                        .divide(bidPrice, depth2Pre + SpotConstant.DEPTH_SCALE_EXTRA, RoundingMode.DOWN);
                BigDecimal askQty = BigDecimal.valueOf(SpotConstant.RANDOM_QTY_BASE + RANDOM.nextInt(SpotConstant.RANDOM_QTY_BOUND))
                        .divide(askPrice, depth2Pre + SpotConstant.DEPTH_SCALE_EXTRA, RoundingMode.DOWN);

                rawBids.add(Arrays.asList(bidPrice.toPlainString(), bidQty.toPlainString()));
                rawAsks.add(Arrays.asList(askPrice.toPlainString(), askQty.toPlainString()));
            }

            // 3. depth1Pre：从 depth2Pre 最外侧价格继续，按固定步长顺序延伸
            BigDecimal lowestRawBid  = new BigDecimal(rawBids.get(rawBids.size() - 1).get(0));
            BigDecimal highestRawAsk = new BigDecimal(rawAsks.get(rawAsks.size() - 1).get(0));
            List<List<String>> step1Bids = generateSequentialBids(lowestRawBid,  depth1Pre, SpotConstant.DEPTH_LEVELS);
            List<List<String>> step1Asks = generateSequentialAsks(highestRawAsk, depth1Pre, SpotConstant.DEPTH_LEVELS);

            // 4. depth0Pre：从 depth1Pre 最外侧价格继续，按固定步长顺序延伸
            BigDecimal lowestStep1Bid  = new BigDecimal(step1Bids.get(step1Bids.size() - 1).get(0));
            BigDecimal highestStep1Ask = new BigDecimal(step1Asks.get(step1Asks.size() - 1).get(0));
            List<List<String>> step0Bids = generateSequentialBids(lowestStep1Bid,  depth0Pre, SpotConstant.DEPTH_LEVELS);
            List<List<String>> step0Asks = generateSequentialAsks(highestStep1Ask, depth0Pre, SpotConstant.DEPTH_LEVELS);

            // 5. 合并三档，买盘降序（从高到低），卖盘降序（从高到低）
            List<List<String>> mergedBids = Stream.of(rawBids, step1Bids, step0Bids)
                    .flatMap(Collection::stream)
                    .sorted((a, b) -> new BigDecimal(b.get(0)).compareTo(new BigDecimal(a.get(0))))
                    .collect(Collectors.toList());
            List<List<String>> mergedAsks = Stream.of(rawAsks, step1Asks, step0Asks)
                    .flatMap(Collection::stream)
                    .sorted((a, b) -> new BigDecimal(a.get(0)).compareTo(new BigDecimal(b.get(0))))
                    .collect(Collectors.toList());

            // 6. 推送
            Map<String, Object> tick = new HashMap<>();
            tick.put(SpotConstant.DATA_FIELD_ASKS, mergedAsks);
            tick.put(SpotConstant.DATA_FIELD_BUYS, mergedBids);
            marketPushService.push(SpotConstant.CHANNEL_PREFIX_MARKET + symbol + SpotConstant.CHANNEL_TYPE_DEPTH, tick);

        } catch (Exception e) {
            log.error("生成现货深度数据失败: symbol={}", symbol, e);
        }
    }

    /**
     * 从 startPrice 向下顺序生成 levels 条买盘档位，步长 = 10^(-precision)
     *
     * @param startPrice 起始价格（不含，从此价格向下走第一步）
     * @param precision  价格精度（小数位数）
     * @param levels     档位数量
     * @return 买盘档位列表，价格从高到低
     */
    private List<List<String>> generateSequentialBids(BigDecimal startPrice, int precision, int levels) {
        BigDecimal step = BigDecimal.ONE.scaleByPowerOfTen(-precision);
        List<List<String>> result = new ArrayList<>(levels);
        BigDecimal price = startPrice;
        for (int i = 0; i < levels; i++) {
            // 向下步进一格，并按精度截断
            price = price.subtract(step).setScale(precision, RoundingMode.DOWN);
            if(price.compareTo(BigDecimal.ZERO) <= 0){
                continue;
            }
            BigDecimal qty = BigDecimal.valueOf(SpotConstant.RANDOM_QTY_BASE + RANDOM.nextInt(SpotConstant.RANDOM_QTY_BOUND))
                    .divide(price, precision + SpotConstant.DEPTH_SCALE_EXTRA, RoundingMode.DOWN);
            result.add(Arrays.asList(price.toPlainString(), qty.toPlainString()));
        }
        return result;
    }

    /**
     * 从 startPrice 向上顺序生成 levels 条卖盘档位，步长 = 10^(-precision)
     *
     * @param startPrice 起始价格（不含，从此价格向上走第一步）
     * @param precision  价格精度（小数位数）
     * @param levels     档位数量
     * @return 卖盘档位列表，价格从低到高
     */
    private List<List<String>> generateSequentialAsks(BigDecimal startPrice, int precision, int levels) {
        BigDecimal step = BigDecimal.ONE.scaleByPowerOfTen(-precision);
        List<List<String>> result = new ArrayList<>(levels);
        BigDecimal price = startPrice;
        for (int i = 0; i < levels; i++) {
            // 向上步进一格，并按精度保留
            price = price.add(step).setScale(precision, RoundingMode.HALF_UP);
            BigDecimal qty = BigDecimal.valueOf(SpotConstant.RANDOM_QTY_BASE + RANDOM.nextInt(SpotConstant.RANDOM_QTY_BOUND))
                    .divide(price, precision + SpotConstant.DEPTH_SCALE_EXTRA, RoundingMode.DOWN);
            result.add(Arrays.asList(price.toPlainString(), qty.toPlainString()));
        }
        return result;
    }

    // ------------------------------------------------------------------ 撮合通知

    /** 通过 Kafka 通知撮合模块最新价格 */
    private void notifyNewPrice(String code, String price) {
        // 价格与上次相同则跳过，避免重复发送
        String lastPrice = LAST_NOTIFY_PRICE.getIfPresent(code);
        if (price.equals(lastPrice)) {
            return;
        }
        try {
            Map<String, Object> spotData = new HashMap<>();
            spotData.put("symbol", code.toUpperCase());
            spotData.put("price", price);
            String json = objectMapper.writeValueAsString(spotData);
            kafkaTemplate.send(spotNewPriceTopic, json);
            LAST_NOTIFY_PRICE.put(code, price);
        } catch (Exception e) {
            log.warn("发送现货最新价格到 Kafka 失败: symbol={}, price={}", code, price, e);
        }
    }

    // ------------------------------------------------------------------ 节流

}
