package com.market.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.market.entity.Kline;
import com.market.entity.RobotConfig;
import com.market.enums.RobotStatus;
import com.market.enums.RobotTrend;
import com.market.mapper.RobotConfigMapper;
import com.market.model.RobotConfigDTO;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.market.service.PriceRobotService;
import com.market.service.SpotKlineRecordService;
import com.market.service.SpotSymbolPrecisionService;
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.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 行情机器人服务实现
 * <p>
 * 负责价格生成算法、Caffeine 配置缓存、DB 读写及 DTO 转换。
 *
 * @author jean
 * @date 2026-05-11
 */
@DS("kucoin")
@Service
public class PriceRobotServiceImpl implements PriceRobotService {

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

    // ------------------------------------------------------------------ 上涨概率默认值

    /** 上涨趋势默认上涨概率（65% 涨，35% 跌） */
    private static final BigDecimal UP_PROB_UP = new BigDecimal("0.65");

    /** 下跌趋势默认上涨概率（35% 涨，65% 跌） */
    private static final BigDecimal UP_PROB_DOWN = new BigDecimal("0.35");

    /** 横盘趋势默认上涨概率 */
    private static final BigDecimal UP_PROB_SIDEWAYS = new BigDecimal("0.50");

    // ------------------------------------------------------------------ 缓存

    /**
     * 配置缓存：symbol → RobotConfig
     * <p>
     * 写后 1 分钟过期，未命中时自动回源 DB。
     */
    private final Cache<String, RobotConfig> configCache = Caffeine.newBuilder()
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .build();

    /**
     * 当前价格内存映射：symbol → 最新生成价格
     * <p>
     * 仅在内存中维护，不持久化。
     */
    private final Map<String, BigDecimal> currentPriceMap = new ConcurrentHashMap<>();

    /**
     * 桥锚点价映射：symbol → 本次桥的基准价（targetPrice 或横盘中心价）
     * <p>
     * 每次新桥开始时写入，桥结束后不变（下次桥仍以此为锚点）。
     */
    private final Map<String, BigDecimal> bridgeAnchorMap = new ConcurrentHashMap<>();

    /**
     * 桥顶点价映射：symbol → 本次桥偏离段的目标顶点价
     * <p>
     * 上升桥：顶点 = anchor × (1 + ratio)；下降桥：顶点 = anchor × (1 - ratio)。
     */
    private final Map<String, BigDecimal> bridgePeakMap = new ConcurrentHashMap<>();

    /**
     * 桥方向映射：symbol → true=本次桥先向上偏离，false=本次桥先向下偏离
     */
    private final Map<String, Boolean> bridgeDirectionMap = new ConcurrentHashMap<>();

    /**
     * 桥阶段映射：symbol → true=回归段（已到顶点，正在回归锚点），false=偏离段（正在走向顶点）
     */
    private final Map<String, Boolean> bridgeReturningMap = new ConcurrentHashMap<>();

    /**
     * 分钟K线开盘价映射：symbol → [开盘价bits, 分钟开始时间戳ms]
     * <p>
     * 每分钟重置，用于分钟涨跌幅限制计算。
     * index 0 = openPrice 的 doubleToLongBits，index 1 = 分钟开始时间戳。
     */
    private final Map<String, long[]> minuteOpenMap = new ConcurrentHashMap<>();

    // ------------------------------------------------------------------ 依赖

    @Autowired
    private RobotConfigMapper robotConfigMapper;

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @Autowired
    private SpotSymbolPrecisionService symbolPrecisionService;

    @Autowired
    private SpotKlineRecordService klineRecordService;

    /** 机器人价格 Kafka topic，消费方负责推送 kline / trade / ticker */
    @Value("${spot.kafka.topic.robot-price:SPOT_ROBOT_PRICE}")
    private String robotPriceTopic;

    private final ObjectMapper objectMapper = new ObjectMapper();

    // ------------------------------------------------------------------ getConfig / invalidateConfig

    /**
     * 获取机器人配置，优先走 Caffeine 缓存，未命中回源 DB 并写入缓存
     *
     * @param symbol 交易对（小写）
     * @return 机器人配置，不存在返回 null
     */
    @Override
    public RobotConfig getConfig(String symbol) {
        // 优先命中缓存
        RobotConfig cached = configCache.getIfPresent(symbol);
        if (cached != null) {
            return cached;
        }
        // 缓存未命中：查询 DB，结果不为 null 时写入缓存
        RobotConfig config = queryConfigBySymbol(symbol);
        if (config != null) {
            configCache.put(symbol, config);
        }
        return config;
    }

    /**
     * 令指定 symbol 的配置缓存立即失效
     *
     * @param symbol 交易对（小写）
     */
    @Override
    public void invalidateConfig(String symbol) {
        configCache.invalidate(symbol);
    }

    // ------------------------------------------------------------------ getCurrentPrice

    /**
     * 获取当前价格（仅内存缓存）
     *
     * @param symbol 交易对（小写）
     * @return 当前价格，未生成过则返回 null
     */
    @Override
    public BigDecimal getCurrentPrice(String symbol) {
        return currentPriceMap.get(symbol);
    }

    // ------------------------------------------------------------------ tick

    /**
     * 执行一次价格生成并推送
     * <p>
     * 流程：确定基准价 → 按状态生成新价 → 趋势约束检查 → 截断精度 → 推送 → 更新内存
     *
     * @param symbol 交易对（小写）
     * @param config 当前机器人配置
     */
    @Override
    public void tick(String symbol, RobotConfig config) {
        // 1. 取当前基准价（优先内存，无历史则用起始价）
        BigDecimal basePrice = resolveCurrentPrice(symbol, config);

        // 2. 识别当前状态与趋势
        RobotStatus status = RobotStatus.fromCode(config.getStatus());
        RobotTrend trend = RobotTrend.fromCode(config.getTrend());

        // 3. 按状态生成新价格
        // outRate[0]=幅度, outRate[1]=方向(1=涨 0=跌)
        BigDecimal[] outRate = new BigDecimal[2];
        BigDecimal newPrice;
        if (status == RobotStatus.BRIDGING) {
            // 成桥阶段：向桥目标价方向运行，到达后翻转方向循环
            newPrice = generateBridgingPrice(symbol, basePrice, config, trend, outRate);
        } else {
            // RUNNING 阶段（含 SIDEWAYS 趋势）：按 upProbability 随机涨跌
            newPrice = generateTrendPrice(basePrice, config, trend, outRate);

            // 4. 检查是否到达目标价，触发切换盘整
            newPrice = applyTrendConstraints(symbol, newPrice, config, trend);
        }

        // 5. 分钟涨跌幅限制：成桥阶段自身已在震荡，跳过限制；RUNNING 阶段超出边界则弹回
        if (status != RobotStatus.BRIDGING) {
            newPrice = applyMinuteChangeLimit(symbol, newPrice, config);
        }

        // 5.1 K线价格上下界限制：以当前1分钟线开盘价为基准，硬截断生成价格
        newPrice = applyKlinePriceLimit(symbol, newPrice, config);

        // 6. 截断价格精度
        newPrice = truncateByPrecision(symbol, newPrice, config);

        // 6. 发送到 MQ，由消费方负责更新 kline / 推送 trade 和 24hrMiniTicker
        try {
            // 在配置的最大比例内随机本次 high/low 限制值，默认上限 0.5%
            BigDecimal maxHigh = config.getKlineMaxHighRatio() != null
                    ? config.getKlineMaxHighRatio() : new BigDecimal("0.005");
            BigDecimal maxLow  = config.getKlineMaxLowRatio()  != null
                    ? config.getKlineMaxLowRatio()  : new BigDecimal("0.005");
            String highRatio = BigDecimal.valueOf(ThreadLocalRandom.current().nextDouble(maxHigh.doubleValue()))
                    .setScale(6, RoundingMode.HALF_UP).toPlainString();
            String lowRatio  = BigDecimal.valueOf(ThreadLocalRandom.current().nextDouble(maxLow.doubleValue()))
                    .setScale(6, RoundingMode.HALF_UP).toPlainString();

            Map<String, String> msg = new java.util.HashMap<>();
            msg.put("symbol", symbol);
            msg.put("price", newPrice.toPlainString());
            msg.put("klineHighRatio", highRatio);
            msg.put("klineLowRatio",  lowRatio);
            kafkaTemplate.send(robotPriceTopic, objectMapper.writeValueAsString(msg));
        } catch (Exception e) {
            log.error("行情机器人价格发送 MQ 失败: symbol={}, price={}", symbol, newPrice, e);
        }

        // 7. 更新内存当前价格
        currentPriceMap.put(symbol, newPrice);
    }

    // ------------------------------------------------------------------ CRUD

    /**
     * 查询所有状态不为 STOPPED 的机器人配置
     *
     * @return 活跃配置列表
     */
    @Override
    @Transactional(readOnly = true)
    public List<RobotConfig> listAllActive() {
        LambdaQueryWrapper<RobotConfig> qw = new LambdaQueryWrapper<>();
        qw.ne(RobotConfig::getStatus, RobotStatus.STOPPED.getCode());
        return robotConfigMapper.selectList(qw);
    }

    /**
     * 查询所有机器人配置（含已停止）
     *
     * @return 全量配置列表
     */
    @Override
    @Transactional(readOnly = true)
    public List<RobotConfig> listAll() {
        return robotConfigMapper.selectList(null);
    }

    /**
     * 新增或更新机器人配置（以 symbol 为唯一键）
     *
     * @param dto 配置 DTO
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void save(RobotConfigDTO dto) {
        // 1. 查询是否已存在
        LambdaQueryWrapper<RobotConfig> qw = new LambdaQueryWrapper<>();
        qw.eq(RobotConfig::getSymbol, dto.getSymbol());
        RobotConfig existing = robotConfigMapper.selectOne(qw);

        if (existing == null) {
            // 2a. 新增记录
            RobotConfig config = buildRobotConfig(dto);
            robotConfigMapper.insert(config);
        } else {
            // 2b. 更新已有记录
            fillRobotConfigFields(existing, dto);
            robotConfigMapper.updateById(existing);
        }

        // 3. 清除缓存，线程下次 tick 时自动加载新配置
        invalidateConfig(dto.getSymbol());
    }

    /**
     * 启动机器人：将状态改为 RUNNING
     *
     * @param symbol 交易对（小写）
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void start(String symbol) {
        LambdaUpdateWrapper<RobotConfig> uw = new LambdaUpdateWrapper<>();
        uw.eq(RobotConfig::getSymbol, symbol);
        uw.set(RobotConfig::getStatus, RobotStatus.RUNNING.getCode());
        robotConfigMapper.update(null, uw);
        invalidateConfig(symbol);
    }

    /**
     * 停止机器人：将状态改为 STOPPED
     *
     * @param symbol 交易对（小写）
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void stop(String symbol) {
        LambdaUpdateWrapper<RobotConfig> uw = new LambdaUpdateWrapper<>();
        uw.eq(RobotConfig::getSymbol, symbol);
        uw.set(RobotConfig::getStatus, RobotStatus.STOPPED.getCode());
        robotConfigMapper.update(null, uw);
        invalidateConfig(symbol);
    }

    // ------------------------------------------------------------------ DTO 转换

    /**
     * 将 RobotConfig 实体转换为 DTO（含运行时字段）
     *
     * @param config       实体
     * @param threadStatus 线程状态字符串
     * @return DTO
     */
    @Override
    public RobotConfigDTO toDTO(RobotConfig config, String threadStatus) {
        RobotConfigDTO dto = buildResponseDTO(config);
        dto.setThreadStatus(threadStatus);

        // 填充当前实时价格
        BigDecimal currentPrice = currentPriceMap.get(config.getSymbol());
        dto.setCurrentPrice(currentPrice != null ? currentPrice.toPlainString() : null);

        return dto;
    }

    /**
     * 批量转换为 DTO 列表
     *
     * @param configs       实体列表
     * @param statusFetcher 根据 symbol 获取线程状态的函数
     * @return DTO 列表
     */
    @Override
    public List<RobotConfigDTO> toDTOList(List<RobotConfig> configs, Function<String, String> statusFetcher) {
        return configs.stream()
                .map(c -> toDTO(c, statusFetcher.apply(c.getSymbol())))
                .collect(Collectors.toList());
    }

    // ------------------------------------------------------------------ 私有方法：价格生成

    /**
     * 获取基准价格
     * <p>
     * 优先级：① 内存缓存（currentPriceMap）→ ② DB 最新1分钟线收盘价 → ③ 配置起始价
     * <p>
     * 不读 Redis：Redis 最新价可能被撮合引擎覆盖，与机器人生成序列不连续，会导致价格跳变。
     *
     * @param symbol 交易对
     * @param config 机器人配置
     * @return 基准价格
     */
    private BigDecimal resolveCurrentPrice(String symbol, RobotConfig config) {
        // ① 内存缓存（最优先，保证价格连续）
        BigDecimal price = currentPriceMap.get(symbol);
        if (price != null) {
            return price;
        }

        // ② DB 最新1分钟线收盘价（仅首次启动时兜底）
        try {
            Kline kline = klineRecordService.queryLatestKline(symbol, "1MIN");
            if (kline != null && kline.getClose() != null) {
                log.info("行情机器人基准价取自 DB 1m kline: symbol={}, close={}", symbol, kline.getClose());
                return kline.getClose();
            }
        } catch (Exception e) {
            log.warn("行情机器人读取 DB 1m kline 失败: symbol={}", symbol, e);
        }

        // ③ 配置起始价兜底
        log.info("行情机器人基准价取自配置起始价: symbol={}, openPrice={}", symbol, config.getOpenPrice());
        return config.getOpenPrice();
    }

    /**
     * 趋势阶段价格生成：按 upProbability 随机决定方向，random 幅度 [minChangeRate, maxChangeRate]
     *
     * @param basePrice 基准价格
     * @param config    机器人配置
     * @param trend     趋势方向
     * @return 新价格（未截断精度）
     */
    private BigDecimal generateTrendPrice(BigDecimal basePrice, RobotConfig config, RobotTrend trend,
                                           BigDecimal[] outRate) {
        // 确定上涨概率
        BigDecimal upProb = resolveUpProbability(config, trend);
        boolean goUp = ThreadLocalRandom.current().nextDouble() < upProb.doubleValue();

        // 随机变动幅度
        BigDecimal rate = randomRate(config.getMinChangeRate(), config.getMaxChangeRate());
        outRate[0] = rate;
        outRate[1] = goUp ? BigDecimal.ONE : BigDecimal.ZERO;

        if (goUp) {
            return basePrice.multiply(BigDecimal.ONE.add(rate));
        } else {
            return basePrice.multiply(BigDecimal.ONE.subtract(rate));
        }
    }

    /**
     * 趋势约束：检测价格是否到达目标价，到达则切换为成桥状态并钳制价格
     * <p>
     * 横盘（SIDEWAYS）趋势不受目标价约束，直接返回。
     *
     * @param symbol   交易对
     * @param newPrice 当前生成的新价格
     * @param config   机器人配置
     * @param trend    趋势方向
     * @return 约束后的价格
     */
    private BigDecimal applyTrendConstraints(String symbol, BigDecimal newPrice,
                                              RobotConfig config, RobotTrend trend) {
        // 横盘：首次进入时以 targetPrice（无则用当前价）为锚点，随机方向进入成桥循环
        if (trend == RobotTrend.SIDEWAYS) {
            BigDecimal anchor = config.getTargetPrice() != null ? config.getTargetPrice() : newPrice;
            boolean initGoUp = nextBridgeDirection(trend);
            startNextBridge(symbol, anchor, initGoUp, config);
            doTransitionToBridging(symbol, config);
            return newPrice;
        }

        BigDecimal targetPrice = config.getTargetPrice();
        if (targetPrice == null) {
            return newPrice;
        }

        boolean reachedTarget = false;
        // 进入目标价千分之五范围即触发成桥
        BigDecimal threshold = targetPrice.multiply(new BigDecimal("0.005"));
        if (trend == RobotTrend.UP && newPrice.compareTo(targetPrice.subtract(threshold)) >= 0) {
            // 上涨至目标价 0.5% 范围内：钳制到目标价，切换成桥
            newPrice = targetPrice;
            reachedTarget = true;
        } else if (trend == RobotTrend.DOWN && newPrice.compareTo(targetPrice.add(threshold)) <= 0) {
            // 下跌至目标价 0.5% 范围内：钳制到目标价，切换成桥
            newPrice = targetPrice;
            reachedTarget = true;
        }

        if (reachedTarget) {
            // 以目标价为锚点开启第一次桥，同步更新 trend=SIDEWAYS、status=BRIDGING
            boolean firstBridgeGoUp = nextBridgeDirection(trend);
            startNextBridge(symbol, targetPrice, firstBridgeGoUp, config);
            doTransitionToBridging(symbol, config);
        }

        return newPrice;
    }

    /**
     * 切换到成桥状态：将 status 改为 BRIDGING(4)、trend 改为 SIDEWAYS(3)，同步写库并使缓存失效
     *
     * @param symbol 交易对
     * @param config 当前内存配置对象（同步更新 trend 字段，避免本 tick 残留旧趋势值）
     */
    private void doTransitionToBridging(String symbol, RobotConfig config) {
        // 同步更新内存配置，防止当前 tick 后续逻辑仍读到旧 trend
        config.setStatus(RobotStatus.BRIDGING.getCode());
        config.setTrend(RobotTrend.SIDEWAYS.getCode());

        // 写库：status=4, trend=3
        LambdaUpdateWrapper<RobotConfig> uw = new LambdaUpdateWrapper<>();
        uw.eq(RobotConfig::getSymbol, symbol);
        uw.set(RobotConfig::getStatus, RobotStatus.BRIDGING.getCode());
        uw.set(RobotConfig::getTrend, RobotTrend.SIDEWAYS.getCode());
        robotConfigMapper.update(null, uw);
        invalidateConfig(symbol);
        log.info("行情机器人切换成桥状态: symbol={}, status=BRIDGING, trend=SIDEWAYS", symbol);
    }

    /** 成桥完成判定阈值：距锚点价在千分之5以内即视为回归完成 */
    private static final BigDecimal BRIDGE_DONE_THRESHOLD = new BigDecimal("0.005");

    /**
     * 决定本次桥的偏离方向
     * <p>
     * UP(1)：固定向下偏离（先跌后涨回）<br>
     * DOWN(2)：固定向上偏离（先涨后跌回）<br>
     * SIDEWAYS(3)：每次随机
     *
     * @param trend 趋势方向
     * @return true=本次桥先向上偏离，false=本次桥先向下偏离
     */
    private boolean nextBridgeDirection(RobotTrend trend) {
        if (trend == RobotTrend.UP) {
            return false;
        } else if (trend == RobotTrend.DOWN) {
            return true;
        } else {
            return ThreadLocalRandom.current().nextBoolean();
        }
    }

    /**
     * 开启新一次桥：随机桥幅度，计算顶点价，写入缓存，进入偏离段
     * <p>
     * 上升桥（goUp=true）：顶点 = anchor × (1 + ratio)<br>
     * 下降桥（goUp=false）：顶点 = anchor × (1 - ratio)
     *
     * @param symbol 交易对
     * @param anchor 本次桥锚点价（targetPrice 或横盘中心价）
     * @param goUp   本次桥偏离方向：true=先涨，false=先跌
     * @param config 机器人配置
     */
    private void startNextBridge(String symbol, BigDecimal anchor, boolean goUp, RobotConfig config) {
        // 未配置或两者均为 0 时使用默认 5%~10%
        BigDecimal minRatio = config.getBridgeMinRatio() != null
                ? config.getBridgeMinRatio() : new BigDecimal("0.05");
        BigDecimal maxRatio = config.getBridgeMaxRatio() != null
                ? config.getBridgeMaxRatio() : new BigDecimal("0.10");
        if (minRatio.compareTo(BigDecimal.ZERO) == 0 && maxRatio.compareTo(BigDecimal.ZERO) == 0) {
            minRatio = new BigDecimal("0.05");
            maxRatio = new BigDecimal("0.10");
        }

        // 随机本次桥幅度，计算顶点价
        BigDecimal bridgeRatio = randomRate(minRatio, maxRatio);
        BigDecimal peak = goUp
                ? anchor.multiply(BigDecimal.ONE.add(bridgeRatio))
                : anchor.multiply(BigDecimal.ONE.subtract(bridgeRatio));

        // 写入缓存：锚点、顶点、方向、阶段（偏离段）
        bridgeAnchorMap.put(symbol, anchor);
        bridgePeakMap.put(symbol, peak);
        bridgeDirectionMap.put(symbol, goUp);
        bridgeReturningMap.put(symbol, false);

        log.info("行情机器人开始新桥: symbol={}, anchor={}, goUp={}, ratio={}, peak={}",
                symbol, anchor, goUp, bridgeRatio, peak);
    }

    /**
     * 成桥阶段价格生成
     * <p>
     * 每次桥分两段：<br>
     * 1. 偏离段：从锚点向顶点方向运行，到达顶点后切换为回归段<br>
     * 2. 回归段：从顶点向锚点方向运行，到达锚点附近后本次桥结束，开启新桥<br>
     * <br>
     * UP 趋势：固定先向下偏离，回归后再向下，循环<br>
     * DOWN 趋势：固定先向上偏离，回归后再向上，循环<br>
     * SIDEWAYS：每次桥偏离方向随机
     *
     * @param symbol    交易对
     * @param basePrice 基准价格
     * @param config    机器人配置
     * @param trend     趋势方向
     * @return 新价格（未截断精度）
     */
    private BigDecimal generateBridgingPrice(String symbol, BigDecimal basePrice,
                                              RobotConfig config, RobotTrend trend,
                                              BigDecimal[] outRate) {
        BigDecimal anchor    = bridgeAnchorMap.get(symbol);
        BigDecimal peak      = bridgePeakMap.get(symbol);
        Boolean    goUp      = bridgeDirectionMap.get(symbol);
        Boolean    returning = bridgeReturningMap.get(symbol);

        // 重启后内存丢失，以当前价格为锚点重新初始化
        if (anchor == null || peak == null || goUp == null || returning == null) {
            BigDecimal initAnchor = config.getTargetPrice() != null ? config.getTargetPrice() : basePrice;
            boolean initGoUp = nextBridgeDirection(trend);
            startNextBridge(symbol, initAnchor, initGoUp, config);
            anchor    = bridgeAnchorMap.get(symbol);
            peak      = bridgePeakMap.get(symbol);
            goUp      = initGoUp;
            returning = false;
        }

        // 使用盘整幅度步长（更小，K线更平滑）
        BigDecimal rate = randomRate(config.getHoverMinChangeRate(), config.getHoverMaxChangeRate());
        outRate[0] = rate;
        BigDecimal newPrice;

        // 偏离段：涨桥 55% 涨，跌桥 55% 跌；回归段方向相反：涨桥 45% 涨，跌桥 45% 跌
        double upProb = returning ? (goUp ? 0.45 : 0.55) : (goUp ? 0.55 : 0.45);
        boolean actualUp = ThreadLocalRandom.current().nextDouble() < upProb;
        outRate[1] = actualUp ? BigDecimal.ONE : BigDecimal.ZERO;
        newPrice = actualUp
                ? basePrice.multiply(BigDecimal.ONE.add(rate))
                : basePrice.multiply(BigDecimal.ONE.subtract(rate));

        if (!returning) {
            // ---- 偏离段：判断是否到达顶点（超过或距离 ≤ 阈值）----
            BigDecimal gapToPeak = peak.subtract(newPrice).abs()
                    .divide(peak.abs().max(BigDecimal.ONE), 8, RoundingMode.HALF_UP);
            boolean reachedPeak = goUp
                    ? (newPrice.compareTo(peak) >= 0 || gapToPeak.compareTo(BRIDGE_DONE_THRESHOLD) <= 0)
                    : (newPrice.compareTo(peak) <= 0 || gapToPeak.compareTo(BRIDGE_DONE_THRESHOLD) <= 0);

            if (reachedPeak) {
                // 钳制到顶点，切换为回归段；到达最高/最低点时重置保底/保顶保护
                newPrice = peak;
                bridgeReturningMap.put(symbol, true);
                log.info("行情机器人到达顶点，开始回归: symbol={}, peak={}, anchor={}", symbol, peak, anchor);
            }
        } else {
            // ---- 回归段：判断是否回归到锚点附近（超过或距离 ≤ 阈值）----
            BigDecimal gapToAnchor = anchor.subtract(newPrice).abs()
                    .divide(anchor.abs().max(BigDecimal.ONE), 8, RoundingMode.HALF_UP);
            boolean reachedAnchor = goUp
                    ? (newPrice.compareTo(anchor) <= 0 || gapToAnchor.compareTo(BRIDGE_DONE_THRESHOLD) <= 0)
                    : (newPrice.compareTo(anchor) >= 0 || gapToAnchor.compareTo(BRIDGE_DONE_THRESHOLD) <= 0);

            if (reachedAnchor) {
                // 钳制到锚点，本次桥结束，开启新桥
                newPrice = anchor;
                boolean nextGoUp = nextBridgeDirection(trend);
                startNextBridge(symbol, anchor, nextGoUp, config);
                log.info("行情机器人本次桥完成，开启新桥: symbol={}, anchor={}, nextGoUp={}", symbol, anchor, nextGoUp);
            }
        }

        // 保底/保顶：偏离段因概率抖动跌破/涨破锚点时，强制跳回锚点±5%
        // 涨桥：价格不得低于锚点，否则从锚点跳涨5%；跌桥：价格不得高于锚点，否则从锚点跳跌5%
        if (goUp && newPrice.compareTo(anchor) < 0) {
            newPrice = anchor.multiply(new BigDecimal("1.05"));
            log.info("行情机器人涨桥触发保底反弹: symbol={}, anchor={}, newPrice={}", symbol, anchor, newPrice);
        } else if (!goUp && newPrice.compareTo(anchor) > 0) {
            newPrice = anchor.multiply(new BigDecimal("0.95"));
            log.info("行情机器人跌桥触发保顶回落: symbol={}, anchor={}, newPrice={}", symbol, anchor, newPrice);
        }

        return newPrice;
    }

    /**
     * 确定上涨概率：
     * trend=UP/DOWN 时，up_probability 不为空且大于0则取配置值，否则取默认值；
     * 其他 trend 固定返回 0.50。
     *
     * @param config 机器人配置
     * @param trend  趋势方向
     * @return 上涨概率
     */
    private BigDecimal resolveUpProbability(RobotConfig config, RobotTrend trend) {
        if (trend == RobotTrend.UP) {
            if (config.getUpProbability() != null && config.getUpProbability().compareTo(BigDecimal.ZERO) > 0) {
                return config.getUpProbability();
            }
            return UP_PROB_UP;
        }
        if (trend == RobotTrend.DOWN) {
            if (config.getUpProbability() != null && config.getUpProbability().compareTo(BigDecimal.ZERO) > 0) {
                return config.getUpProbability();
            }
            return UP_PROB_DOWN;
        }
        return UP_PROB_SIDEWAYS;
    }

    /**
     * 在 [min, max] 区间内随机生成变动幅度
     *
     * @param min 最小幅度
     * @param max 最大幅度
     * @return 随机幅度
     */
    private BigDecimal randomRate(BigDecimal min, BigDecimal max) {
        if (min.compareTo(max) >= 0) {
            return min;
        }
        double r = ThreadLocalRandom.current().nextDouble(min.doubleValue(), max.doubleValue());
        return BigDecimal.valueOf(r);
    }

    /**
     * 分钟涨跌幅限制：检查当前价格是否超出本分钟开盘价的涨跌幅上限
     * <p>
     * 超出上边界（涨幅 > limit）：从上边界反向生成一个随机步长（桥幅度区间），价格向下弹回<br>
     * 超出下边界（跌幅 > limit）：从下边界反向生成一个随机步长，价格向上弹回<br>
     * 未配置 minuteChangeLimit 时直接返回原价格。
     *
     * @param symbol   交易对
     * @param newPrice 当前生成的价格
     * @param config   机器人配置
     * @return 限制后的价格
     */
    private BigDecimal applyMinuteChangeLimit(String symbol, BigDecimal newPrice, RobotConfig config) {
        BigDecimal limit = config.getMinuteChangeLimit();
        if (limit == null || limit.compareTo(BigDecimal.ZERO) <= 0) {
            return newPrice;
        }

        // 获取当前分钟开盘价，新的一分钟自动初始化
        BigDecimal minuteOpen = resolveMinuteOpen(symbol, newPrice);
        if (minuteOpen == null) {
            return newPrice;
        }

        BigDecimal upperBound = minuteOpen.multiply(BigDecimal.ONE.add(limit));
        BigDecimal lowerBound = minuteOpen.multiply(BigDecimal.ONE.subtract(limit));

        // 反弹步长：使用盘整幅度（小步长），避免弹回时价格大幅跳动
        BigDecimal bounceRate = randomRate(config.getHoverMinChangeRate(), config.getHoverMaxChangeRate());

        if (newPrice.compareTo(upperBound) > 0) {
            // 超出上边界：从上边界向下弹回
            return upperBound.multiply(BigDecimal.ONE.subtract(bounceRate));
        } else if (newPrice.compareTo(lowerBound) < 0) {
            // 超出下边界：从下边界向上弹回
            return lowerBound.multiply(BigDecimal.ONE.add(bounceRate));
        }

        return newPrice;
    }

    /**
     * K线价格上下界限制：以当前1分钟线开盘价为基准，超出边界则回弹
     * <p>
     * 上界 = minuteOpen × (1 + klineMaxHighRatio)，下界 = minuteOpen × (1 - klineMaxLowRatio)。
     * 超出上界时从上界向下弹回一个随机步长，低于下界时从下界向上弹回，步长取自 hoverChangeRate。
     * 未配置对应比例时不限制该方向。
     *
     * @param symbol   交易对
     * @param newPrice 当前生成的价格
     * @param config   机器人配置
     * @return 限制后的价格
     */
    private BigDecimal applyKlinePriceLimit(String symbol, BigDecimal newPrice, RobotConfig config) {
        BigDecimal highRatio = config.getKlineMaxHighRatio();
        BigDecimal lowRatio  = config.getKlineMaxLowRatio();
        if ((highRatio == null || highRatio.compareTo(BigDecimal.ZERO) <= 0)
                && (lowRatio == null || lowRatio.compareTo(BigDecimal.ZERO) <= 0)) {
            return newPrice;
        }

        // 获取当前分钟开盘价，新的一分钟自动初始化
        BigDecimal minuteOpen = resolveMinuteOpen(symbol, newPrice);
        if (minuteOpen == null) {
            return newPrice;
        }

        // 反弹步长：从盘整幅度随机取一个小步长，避免价格在边界处大幅跳动
        BigDecimal bounceRate = randomRate(config.getHoverMinChangeRate(), config.getHoverMaxChangeRate());

        // 上界回弹：超出上边界则从上边界向下弹回
        if (highRatio != null && highRatio.compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal upperBound = minuteOpen.multiply(BigDecimal.ONE.add(highRatio));
            if (newPrice.compareTo(upperBound) > 0) {
                return upperBound.multiply(BigDecimal.ONE.subtract(bounceRate));
            }
        }

        // 下界回弹：低于下边界则从下边界向上弹回
        if (lowRatio != null && lowRatio.compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal lowerBound = minuteOpen.multiply(BigDecimal.ONE.subtract(lowRatio));
            if (newPrice.compareTo(lowerBound) < 0) {
                return lowerBound.multiply(BigDecimal.ONE.add(bounceRate));
            }
        }

        return newPrice;
    }

    /**
     * 获取当前分钟开盘价，新的一分钟或首次调用时以 currentPrice 初始化
     * <p>
     * 初始化场景（新一分钟）返回 null，调用方直接放行当前价格。
     *
     * @param symbol       交易对
     * @param currentPrice 本次生成的价格（用于初始化开盘价）
     * @return 当前分钟开盘价，初始化场景返回 null
     */
    private BigDecimal resolveMinuteOpen(String symbol, BigDecimal currentPrice) {
        long nowMs       = System.currentTimeMillis();
        long minuteStart = (nowMs / 60_000L) * 60_000L;

        long[] entry = minuteOpenMap.get(symbol);
        // 新的一分钟或首次：以当前价作为开盘价，返回 null 表示本 tick 无需限制
        if (entry == null || entry[1] < minuteStart) {
            minuteOpenMap.put(symbol, new long[]{
                    Double.doubleToLongBits(currentPrice.doubleValue()), minuteStart
            });
            return null;
        }

        return BigDecimal.valueOf(Double.longBitsToDouble(entry[0]));
    }

    /**
     * 按精度截断价格，精度取 SpotSymbolPrecisionService 的 depth2Pre，不存在则回退到 openPrice 的小数位数
     *
     * @param symbol 交易对
     * @param price  原始价格
     * @param config 机器人配置
     * @return 截断后的价格
     */
    private BigDecimal truncateByPrecision(String symbol, BigDecimal price, RobotConfig config) {
        Integer scale = symbolPrecisionService.getPrecision(symbol, 2);
        if (scale == null) {
            // 回退：使用 openPrice 的小数位数
            scale = config.getOpenPrice().scale();
        }
        return price.setScale(scale, RoundingMode.DOWN);
    }

    // ------------------------------------------------------------------ 私有方法：DB 查询

    /**
     * 从数据库查询指定 symbol 的机器人配置（Caffeine loader 使用）
     *
     * @param symbol 交易对（小写）
     * @return 机器人配置，不存在返回 null
     */
    private RobotConfig queryConfigBySymbol(String symbol) {
        LambdaQueryWrapper<RobotConfig> qw = new LambdaQueryWrapper<>();
        qw.eq(RobotConfig::getSymbol, symbol);
        return robotConfigMapper.selectOne(qw);
    }

    // ------------------------------------------------------------------ 私有方法：对象构建

    /**
     * 从 DTO 构建新的 RobotConfig 实体
     *
     * @param dto 配置 DTO
     * @return 新实体
     */
    private RobotConfig buildRobotConfig(RobotConfigDTO dto) {
        RobotConfig config = new RobotConfig();
        fillRobotConfigFields(config, dto);
        return config;
    }

    /**
     * 将 DTO 的配置字段填充到实体（新增和更新共用）
     *
     * @param config 目标实体
     * @param dto    来源 DTO
     */
    private void fillRobotConfigFields(RobotConfig config, RobotConfigDTO dto) {
        config.setSymbol(dto.getSymbol());
        if (dto.getTrend() != null) {
            config.setTrend(dto.getTrend());
        }
        if (dto.getOpenPrice() != null) {
            config.setOpenPrice(dto.getOpenPrice());
        }
        // targetPrice 允许为 null（表示无目标，持续趋势运行）
        config.setTargetPrice(dto.getTargetPrice());
        if (dto.getHoverRange() != null) {
            config.setHoverRange(dto.getHoverRange());
        }
        if (dto.getMinChangeRate() != null) {
            config.setMinChangeRate(dto.getMinChangeRate());
        }
        if (dto.getMaxChangeRate() != null) {
            config.setMaxChangeRate(dto.getMaxChangeRate());
        }
        if (dto.getHoverMinChangeRate() != null) {
            config.setHoverMinChangeRate(dto.getHoverMinChangeRate());
        }
        if (dto.getHoverMaxChangeRate() != null) {
            config.setHoverMaxChangeRate(dto.getHoverMaxChangeRate());
        }
        // upProbability 允许为 null（由 trend 自动推导）
        config.setUpProbability(dto.getUpProbability());
        // bridgeMinRatio/bridgeMaxRatio 允许为 null（默认 5%~10%）
        config.setBridgeMinRatio(dto.getBridgeMinRatio());
        config.setBridgeMaxRatio(dto.getBridgeMaxRatio());
        // klineMaxHighRatio/klineMaxLowRatio 允许为 null（默认 0.5%）
        config.setKlineMaxHighRatio(dto.getKlineMaxHighRatio());
        config.setKlineMaxLowRatio(dto.getKlineMaxLowRatio());
        // minuteChangeLimit 允许为 null（不限制）
        config.setMinuteChangeLimit(dto.getMinuteChangeLimit());
        if (dto.getIntervalMs() != null) {
            config.setIntervalMs(dto.getIntervalMs());
        }
        if (dto.getStatus() != null) {
            config.setStatus(dto.getStatus());
        }
        config.setRemark(dto.getRemark());
    }

    /**
     * 从实体构建响应 DTO（不含运行时字段，由调用方补充）
     *
     * @param config 实体
     * @return DTO（threadStatus 和 currentPrice 为 null）
     */
    private RobotConfigDTO buildResponseDTO(RobotConfig config) {
        RobotConfigDTO dto = new RobotConfigDTO();
        dto.setId(config.getId());
        dto.setSymbol(config.getSymbol());
        dto.setTrend(config.getTrend());
        dto.setOpenPrice(config.getOpenPrice());
        dto.setTargetPrice(config.getTargetPrice());
        dto.setHoverRange(config.getHoverRange());
        dto.setMinChangeRate(config.getMinChangeRate());
        dto.setMaxChangeRate(config.getMaxChangeRate());
        dto.setHoverMinChangeRate(config.getHoverMinChangeRate());
        dto.setHoverMaxChangeRate(config.getHoverMaxChangeRate());
        dto.setUpProbability(config.getUpProbability());
        dto.setBridgeMinRatio(config.getBridgeMinRatio());
        dto.setBridgeMaxRatio(config.getBridgeMaxRatio());
        dto.setKlineMaxHighRatio(config.getKlineMaxHighRatio());
        dto.setKlineMaxLowRatio(config.getKlineMaxLowRatio());
        dto.setMinuteChangeLimit(config.getMinuteChangeLimit());
        dto.setIntervalMs(config.getIntervalMs());
        dto.setStatus(config.getStatus());
        dto.setRemark(config.getRemark());
        return dto;
    }
}
