新版仿ok交易所-后端
1
zj
2026-05-21 98a5e458479e7f0034fda19e12cf7c04675f88c8
1
12 files modified
1 files added
993 ■■■■■ changed files
trading-order-admin/src/main/java/com/yami/trading/admin/controller/data/AdminMarketQuotationsManageController.java 16 ●●●● patch | view | raw | blame | history
trading-order-admin/src/main/java/com/yami/trading/admin/dto/AdminMarketQuotationsUpdateDto.java 30 ●●●●● patch | view | raw | blame | history
trading-order-admin/src/main/java/com/yami/trading/admin/facade/MarketQuotationsFacade.java 84 ●●●● patch | view | raw | blame | history
trading-order-admin/src/main/java/com/yami/trading/admin/task/RealtimeWebsocketServer.java 5 ●●●●● patch | view | raw | blame | history
trading-order-admin/src/main/java/com/yami/trading/api/controller/KlineController.java 77 ●●●●● patch | view | raw | blame | history
trading-order-bean/src/main/java/com/yami/trading/bean/contract/domain/ContractOrderProfit.java 34 ●●●●● patch | view | raw | blame | history
trading-order-common/src/main/java/com/yami/trading/common/constants/ContractRedisKeys.java 5 ●●●●● patch | view | raw | blame | history
trading-order-huobi/src/main/java/com.yami.trading.huobi/data/internal/AdjustmentValueServiceImpl.java 14 ●●●●● patch | view | raw | blame | history
trading-order-huobi/src/main/java/com.yami.trading.huobi/data/internal/DataDBServiceImpl.java 69 ●●●●● patch | view | raw | blame | history
trading-order-huobi/src/main/java/com.yami.trading.huobi/data/job/AbstractGetDataJob.java 46 ●●●● patch | view | raw | blame | history
trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderCalculationService.java 10 ●●●●● patch | view | raw | blame | history
trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderCalculationServiceImpl.java 335 ●●●● patch | view | raw | blame | history
trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java 268 ●●●● patch | view | raw | blame | history
trading-order-admin/src/main/java/com/yami/trading/admin/controller/data/AdminMarketQuotationsManageController.java
@@ -30,6 +30,7 @@
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -92,10 +93,17 @@
    @ApiOperation(value = "调整值")
    @PostMapping("adjust.action")
    public Result<String> adjust(@RequestBody AdminMarketQuotationsUpdateDto adminMarketQuotationsUpdateDto) {
//        if(!itemService.isOpen(adminMarketQuotationsUpdateDto.getSymbol())){
//            throw new YamiShopBindException("当前休市状态");
//        }
        marketQuotationsFacade.adjust(adminMarketQuotationsUpdateDto.getSymbol(), adminMarketQuotationsUpdateDto.getSecond(), adminMarketQuotationsUpdateDto.getValue());
        List<String> supportCoins = Arrays.asList("btcusdt", "ethusdt", "dotusdt");
        if (supportCoins.contains(adminMarketQuotationsUpdateDto.getSymbol())) {
            // 包含在列表中
            return Result.failed("该币种不支持调整!");
        }
        marketQuotationsFacade.adjust(
                adminMarketQuotationsUpdateDto.getSymbol(),
                adminMarketQuotationsUpdateDto.getSecond(),
                adminMarketQuotationsUpdateDto.getValue(),
                adminMarketQuotationsUpdateDto.getType());
        return Result.succeed("操作成功");
    }
}
trading-order-admin/src/main/java/com/yami/trading/admin/dto/AdminMarketQuotationsUpdateDto.java
@@ -1,13 +1,14 @@
package com.yami.trading.admin.dto;
import com.fasterxml.jackson.annotation.JsonSetter;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.math.BigDecimal;
@ApiModel
@@ -16,9 +17,32 @@
    @NotEmpty
    @ApiModelProperty("币对")
    private String symbol;
    @ApiModelProperty("延迟秒")
    @Setter(AccessLevel.NONE)
    @ApiModelProperty("延迟秒,0 或空表示立即生效;支持数字或字符串")
    private Double second;
    @JsonSetter("second")
    public void setSecondFlexible(Object second) {
        this.second = parseFlexibleDouble(second);
    }
    private static Double parseFlexibleDouble(Object v) {
        if (v == null) {
            return null;
        }
        if (v instanceof Number) {
            return ((Number) v).doubleValue();
        }
        String s = String.valueOf(v).trim();
        if (s.isEmpty()) {
            return null;
        }
        return Double.parseDouble(s);
    }
    @NotNull(message = "调整值必填")
    @ApiModelProperty("调整值")
    private BigDecimal value;
    @ApiModelProperty("0=加1个pips 1=减1个pips 2=直接作为调整增量(默认)")
    private String type = "2";
}
trading-order-admin/src/main/java/com/yami/trading/admin/facade/MarketQuotationsFacade.java
@@ -184,24 +184,88 @@
    }
    public void adjust(String symbol, Double second, BigDecimal value) {
    /**
     * 提交调整(与 getValue.action 预计算使用同一套 type 规则)。
     */
    public void adjust(String symbol, Double second, BigDecimal value, String type) {
        AdjustmentValueCache.getDelayValue().remove(symbol);
        AdjustmentValueCache.getPreAllocatedAdjustments().remove(symbol);
        AdjustmentValueCache.getCurrentAdjustmentIndex().remove(symbol);
        AdjustmentValueCache.getFrequency().remove(symbol);
        BigDecimal currentValue = this.adjustmentValueService.getCurrentValue(symbol);
        if (currentValue == null) {
            Realtime realtime = this.dataService.realtime(symbol).get(0);
            currentValue = realtime.getClose();
        String adjustType = StringUtils.isBlank(type) ? "2" : type;
        BigDecimal effectiveDelta = resolveEffectiveAdjustDelta(symbol, adjustType, value);
        if (effectiveDelta == null || effectiveDelta.compareTo(BigDecimal.ZERO) == 0) {
            log.warn("行情调整无效 symbol={} value={} type={}", symbol, value, adjustType);
            return;
        }
        String log = MessageFormat.format("ip:" + IPHelper.getIpAddr() + ",管理员调整行情,币种:{0},原值:{1},调整值:{2},调整时间:{3}",
                symbol, currentValue.toPlainString(), value.toPlainString(), second);
        this.adjustmentValueService.adjust(symbol, value, second);
        saveLog(log);
        ThreadUtils.sleep(1000);
        BigDecimal beforeAdjust = adjustmentValueService.getCurrentValue(symbol);
        double secondVal = second == null ? 0D : second;
        String logContent = MessageFormat.format(
                "ip:" + IPHelper.getIpAddr() + ",管理员调整行情,币种:{0},调整前累计:{1},本次增量:{2},type:{3},延迟秒:{4}",
                symbol,
                beforeAdjust == null ? "0" : beforeAdjust.toPlainString(),
                effectiveDelta.toPlainString(),
                adjustType,
                secondVal);
        adjustmentValueService.adjust(symbol, effectiveDelta, secondVal);
        // 立即生效时同步内存行情,避免只改缓存累计值但页面仍显示旧价
        if (secondVal <= 0) {
            syncRealtimeCacheAfterAdjust(symbol, effectiveDelta);
        } else {
            log.info("延迟调整已提交 symbol={} 目标增量={} 时长约{}秒,价格将分步变化(非瞬间到位)",
                    symbol, effectiveDelta.toPlainString(), secondVal);
        }
        saveLog(logContent);
        ThreadUtils.sleep(500);
    }
    /**
     * 与 calculateValue 一致:type 0/1 在输入值基础上加减 pips,type 2 直接使用输入值作为本次增量。
     */
    private BigDecimal resolveEffectiveAdjustDelta(String symbol, String type, BigDecimal inputValue) {
        if (inputValue == null) {
            return null;
        }
        Item item = itemService.findBySymbol(symbol);
        if (item == null) {
            return inputValue;
        }
        double pips = item.getPips() != null ? item.getPips().doubleValue() : 0D;
        double v = inputValue.doubleValue();
        if ("0".equalsIgnoreCase(type)) {
            return BigDecimal.valueOf(Arith.add(v, pips));
        }
        if ("1".equalsIgnoreCase(type)) {
            return BigDecimal.valueOf(Arith.sub(v, pips));
        }
        return inputValue;
    }
    /** 将本次增量反映到 DataCache,与 WebSocket/采集任务展示逻辑一致 */
    private void syncRealtimeCacheAfterAdjust(String symbol, BigDecimal effectiveDelta) {
        Realtime realtime = DataCache.getRealtime(symbol);
        if (realtime == null) {
            List<Realtime> list = dataService.realtime(symbol);
            if (CollectionUtil.isEmpty(list)) {
                return;
            }
            realtime = list.get(0);
        }
        Integer decimal = itemService.getDecimal(symbol);
        realtime.setClose(realtime.getClose().add(effectiveDelta).setScale(decimal, RoundingMode.HALF_UP));
        if (realtime.getAsk() != null) {
            realtime.setAsk(realtime.getAsk().add(effectiveDelta).setScale(decimal, RoundingMode.HALF_UP));
        }
        if (realtime.getBid() != null) {
            realtime.setBid(realtime.getBid().add(effectiveDelta).setScale(decimal, RoundingMode.HALF_UP));
        }
        DataCache.putRealtime(symbol, realtime);
    }
    public void saveLog(String content) {
trading-order-admin/src/main/java/com/yami/trading/admin/task/RealtimeWebsocketServer.java
@@ -96,7 +96,8 @@
            if (itemService.isSuspended(symbol)) {
                return;
            }
            Double currentValue = AdjustmentValueCache.getCurrentValue().get(symbol).doubleValue();
            BigDecimal adjustment = AdjustmentValueCache.getCurrentValue().get(symbol);
            double currentValue = adjustment != null ? adjustment.doubleValue() : 0D;
            double close = event.getTicker().getClose().doubleValue();
            double vol = event.getTicker().getVol().doubleValue();
            double amount = event.getTicker().getAmount().doubleValue();
@@ -111,7 +112,7 @@
            realtime.setVolume(new BigDecimal(vol));
            realtime.setAmount(new BigDecimal(amount));
            if (currentValue != null && currentValue != 0) {
            if (currentValue != 0) {
                realtime.setClose(new BigDecimal(Arith.add(close, currentValue)));
                //realtime.setVolume(new BigDecimal(Arith.add(vol, Arith.mul(Arith.div(currentValue, close), vol))));
                //realtime.setAmount(new BigDecimal(Arith.add(amount, Arith.mul(Arith.div(currentValue, close), amount))));
trading-order-admin/src/main/java/com/yami/trading/api/controller/KlineController.java
@@ -69,24 +69,6 @@
            }
            // 数据处理
            List<Kline> data = this.dataService.kline(symbol, line);
            Item item = itemService.findBySymbol(symbol);
            if (item != null) {
                if (item.getType().equals(Item.cryptos) && (item.getCurrencyType() != null && item.getCurrencyType() == 1)) {
                    /*QueryWrapper<Ico> iQuery  = new QueryWrapper<>();
                    iQuery.eq("symbol", symbol);
                    iQuery.eq("symbol_data", item.getSymbolData());
                    Ico ico = icoService.getOne(iQuery);
                    if (ico != null) {
                        long now = ico.getMarketDate().getTime();*/
                        long now = item.getCreateTimeTs() * 1000;
                        data = data.stream().filter(kline -> kline != null
                                        && kline.getTs() != null
                                        && kline.getTs() > now)
                                .collect(Collectors.toList());
                    //}
                }
            }
            if ("1day".equals(line) || "5day".equals(line) || "1mon".equals(line) || "1week".equals(line) || "quarter".equalsIgnoreCase(line)
                    || "year".equalsIgnoreCase(line)) {
                for (Kline datum : data) {
@@ -103,29 +85,13 @@
                            DateUtils.timeStamp2Date(String.valueOf(datum.getTs()), "MM-dd HH:mm"));
                }
            }
            /*BigDecimal currentValue = AdjustmentValueCache.getCurrentValue().get(symbol);
            if (currentValue != null) {
                data.forEach(kline -> {
                    *//*logger.info("==currentValue==close:{}, low:{}, high:{}, open:{}, currentValue:{}",
                            kline.getClose(), kline.getLow(), kline.getHigh(), kline.getOpen(), currentValue);*//*
                    if (!kline.isAdjusted()){
                        if (kline.getClose().compareTo(kline.getLow()) >= 0 && kline.getClose().compareTo(kline.getHigh()) <= 0) {
                            kline.setClose(kline.getClose().add(currentValue));
                        }
                        kline.setOpen(kline.getOpen().add(currentValue));
                        kline.setLow(kline.getLow().add(currentValue));
                        kline.setHigh(kline.getHigh().add(currentValue));
                        kline.setAdjusted(true);
                    }
                });
            }*/
            return Result.succeed(this.build(data, line, symbol));
        } catch (Exception e) {
            logger.error("getKline error", e);
            throw new YamiShopBindException("k线图获取失败");
        }
    }
    private List<Map<String, Object>> build(List<Kline> data, String line, String symbol) {
        Collections.sort(data);
@@ -181,23 +147,42 @@
            map.put("low", low.setScale(decimal, RoundingMode.HALF_UP));
            map.put("volume", kline.getVolume());
            //if (line.equalsIgnoreCase(Kline.PERIOD_15MIN) || line.equalsIgnoreCase(Kline.PERIOD_30MIN) || line.equalsIgnoreCase(Kline.PERIOD_60MIN)) {
                if (i == data.size() - 1) {
                    //获取当前价格
                    Realtime realtime = DataCache.getLatestRealTime(symbol);
                    if (realtime != null) {
                        map.put("close", realtime.getClose().setScale(decimal, RoundingMode.HALF_UP));
            if (i == data.size() - 1 && isRollingIntradayLine(line)) {
                Realtime realtime = DataCache.getLatestRealTime(symbol);
                BigDecimal latestClose = close;
                if (realtime != null && realtime.getClose() != null) {
                    latestClose = realtime.getClose();
                    map.put("close", latestClose.setScale(decimal, RoundingMode.HALF_UP));
                }
                BeforeClose beforeClose = dataDBService.getBeforeClose(kline.getSymbol(), line, ts, realtime);
                BigDecimal periodHigh = high;
                BigDecimal periodLow = low;
                if (beforeClose != null) {
                    if (beforeClose.getMaxClose() != null && beforeClose.getMaxClose().compareTo(BigDecimal.ZERO) > 0) {
                        periodHigh = beforeClose.getMaxClose();
                    }
                    BeforeClose beforeClose = dataDBService.getBeforeClose(kline.getSymbol(), line, ts, realtime);
                    if (beforeClose != null) {
                        map.put("high", beforeClose.getMaxClose().setScale(decimal, RoundingMode.HALF_UP));
                        map.put("low", beforeClose.getMinClose().setScale(decimal, RoundingMode.HALF_UP));
                    if (beforeClose.getMinClose() != null && beforeClose.getMinClose().compareTo(BigDecimal.ZERO) > 0) {
                        periodLow = beforeClose.getMinClose();
                    }
                }
            //}
                periodHigh = periodHigh.max(latestClose);
                periodLow = periodLow.min(latestClose);
                map.put("high", periodHigh.setScale(decimal, RoundingMode.HALF_UP));
                map.put("low", periodLow.setScale(decimal, RoundingMode.HALF_UP));
            }
            list.add(map);
        }
        return list;
    }
    /**
     * 5/15/30/60 分钟未收盘 K 线需用当前周期内的最高/最低价刷新
     */
    private boolean isRollingIntradayLine(String line) {
        return Kline.PERIOD_5MIN.equalsIgnoreCase(line)
                || Kline.PERIOD_15MIN.equalsIgnoreCase(line)
                || Kline.PERIOD_30MIN.equalsIgnoreCase(line)
                || Kline.PERIOD_60MIN.equalsIgnoreCase(line);
    }
}
trading-order-bean/src/main/java/com/yami/trading/bean/contract/domain/ContractOrderProfit.java
New file
@@ -0,0 +1,34 @@
package com.yami.trading.bean.contract.domain;
import com.yami.trading.common.domain.BaseEntity;
import lombok.Data;
import java.math.BigDecimal;
/**
 * 永续合约持仓盈亏缓存
 */
@Data
public class ContractOrderProfit extends BaseEntity {
    /**
     * 收益
     */
    private BigDecimal profit;
    /**
     * 强平价格
     */
    private String forceClosePrice;
    /**
     * 平仓均价
     */
    private BigDecimal closeAvgPrice;
    public BigDecimal getProfit() {
        if (profit == null) {
            return BigDecimal.ZERO;
        }
        return profit;
    }
}
trading-order-common/src/main/java/com/yami/trading/common/constants/ContractRedisKeys.java
@@ -6,6 +6,11 @@
     * 永续合约,orderNo做key
     */
    public final static String CONTRACT_ORDERNO = "CONTRACT_ORDERNO_";
    /**
     * 永续利润
     */
    public final static String CONTRACT_PROFIT_V1 = "CONTRACT_PROFIT_V1";
    
    /**
     * 永续合约,查询订单map,partyid做key
trading-order-huobi/src/main/java/com.yami.trading.huobi/data/internal/AdjustmentValueServiceImpl.java
@@ -50,15 +50,17 @@
            } else {
                AdjustmentValueCache.getCurrentValue().put(symbol, currentValue.add(value));
            }
            // 马上扣除价格,避免因为数据没有拉取导致加不正确
            realtime.setClose(realtime.getClose().add(value));
            /*
             * 持久化缓存
             * 持久化到品种表(重启后 InitHandle 会加载到 AdjustmentValueCache)
             */
            Item item = this.itemService.findBySymbol(symbol);
            if (item.getAdjustmentValue().compareTo(AdjustmentValueCache.getCurrentValue().get(symbol)) != 0) {
                item.setAdjustmentValue(AdjustmentValueCache.getCurrentValue().get(symbol));
                itemService.saveOrUpdate(item);
            BigDecimal cachedAdjust = AdjustmentValueCache.getCurrentValue().get(symbol);
            if (item != null && cachedAdjust != null) {
                BigDecimal itemAdjust = item.getAdjustmentValue();
                if (itemAdjust == null || itemAdjust.compareTo(cachedAdjust) != 0) {
                    item.setAdjustmentValue(cachedAdjust);
                    itemService.saveOrUpdate(item);
                }
            }
        } else {
trading-order-huobi/src/main/java/com.yami.trading.huobi/data/internal/DataDBServiceImpl.java
@@ -146,46 +146,63 @@
        BeforeClose beforeClose = (BeforeClose) redisTemplate.opsForValue().get(RedisKeys.REAL_TIME_BEFORE_CLOSE + symbol + line);
        //超出时间重新计算
        if (beforeClose == null || ts > beforeClose.getTs()) {
            // 直接获取当前时间的毫秒级时间戳(系统默认时区,但值是全球统一的)
            long currentTimeStamp = System.currentTimeMillis();
            // 如果需要严格基于东京时区的当前时间戳(结果和上面一致,因为时间戳是UTC绝对时间)
            //long currentTokyoTimeStamp = Instant.now().atZone(ZoneId.of("America/New_York")).toInstant().toEpochMilli();
            RequestDataHelper.set("symbol", symbol);
            QueryWrapper<Realtime> queryWrapper = new QueryWrapper<Realtime>()
                    .eq("symbol", symbol) // 直接写数据库字段名(需和表字段一致)
                    .eq("symbol", symbol)
                    .ge("ts", ts)
                    .le("ts", currentTimeStamp)
                    .select("MAX(CAST(close AS DECIMAL(10,4))) as maxClose",
                            "MIN(CAST(close AS DECIMAL(10,4))) as minClose");
            // 4. 执行聚合查询,用selectMap接收结果(键值对:maxClose/minClose -> 对应值)
                    .select("MAX(close) as maxClose", "MIN(close) as minClose");
            Map<String, Object> resultMap = realtimeService.getMap(queryWrapper);
            RequestDataHelper.clear();
            beforeClose = new BeforeClose();
            if (resultMap == null || resultMap.isEmpty()) {
                return beforeClose;
            }
            beforeClose.setMaxClose(convertToBigDecimal(resultMap.get("maxClose")));
            beforeClose.setMinClose(convertToBigDecimal(resultMap.get("minClose")));
            BigDecimal maxClose = extractAggregateValue(resultMap, "maxClose");
            BigDecimal minClose = extractAggregateValue(resultMap, "minClose");
            mergeRealtimeIntoBeforeClose(beforeClose, maxClose, minClose, realtime);
            beforeClose.setTs(ts);
            redisTemplate.opsForValue().set(RedisKeys.REAL_TIME_BEFORE_CLOSE + symbol + line, beforeClose);
        } else if (realtime != null) {
            mergeRealtimeIntoBeforeClose(beforeClose, beforeClose.getMaxClose(), beforeClose.getMinClose(), realtime);
            redisTemplate.opsForValue().set(RedisKeys.REAL_TIME_BEFORE_CLOSE + symbol + line, beforeClose);
        }
        if (realtime != null) {
            if (realtime.getClose().compareTo(beforeClose.getMaxClose()) > 0) {
                beforeClose.setMaxClose(realtime.getClose());
                redisTemplate.opsForValue().set(RedisKeys.REAL_TIME_BEFORE_CLOSE + symbol + line, beforeClose);
            }
            if (realtime.getClose().compareTo(beforeClose.getMinClose()) < 0) {
                beforeClose.setMinClose(realtime.getClose());
                redisTemplate.opsForValue().set(RedisKeys.REAL_TIME_BEFORE_CLOSE + symbol + line, beforeClose);
            }
        }
        System.out.println("realtime.getClose():" + realtime.getClose() + "==" + beforeClose);
        return beforeClose;
    }
    private void mergeRealtimeIntoBeforeClose(BeforeClose beforeClose, BigDecimal maxClose, BigDecimal minClose, Realtime realtime) {
        BigDecimal max = maxClose;
        BigDecimal min = minClose;
        if (realtime != null && realtime.getClose() != null) {
            BigDecimal latest = realtime.getClose();
            if (max == null || max.compareTo(BigDecimal.ZERO) <= 0) {
                max = latest;
            } else {
                max = max.max(latest);
            }
            if (min == null || min.compareTo(BigDecimal.ZERO) <= 0) {
                min = latest;
            } else {
                min = min.min(latest);
            }
        }
        beforeClose.setMaxClose(max == null ? BigDecimal.ZERO : max);
        beforeClose.setMinClose(min == null ? BigDecimal.ZERO : min);
    }
    private BigDecimal extractAggregateValue(Map<String, Object> resultMap, String key) {
        if (resultMap == null || resultMap.isEmpty()) {
            return null;
        }
        if (resultMap.containsKey(key)) {
            return convertToBigDecimal(resultMap.get(key));
        }
        for (Map.Entry<String, Object> entry : resultMap.entrySet()) {
            if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(key)) {
                return convertToBigDecimal(entry.getValue());
            }
        }
        return null;
    }
    // 辅助方法:统一转换为BigDecimal,避免类型错误
    private BigDecimal convertToBigDecimal(Object value) {
        if (value == null) {
trading-order-huobi/src/main/java/com.yami.trading.huobi/data/job/AbstractGetDataJob.java
@@ -94,20 +94,21 @@
                        List<BigDecimal> adjustments = AdjustmentValueCache.getPreAllocatedAdjustments().get(symbol);
                        Integer currentIndex = AdjustmentValueCache.getCurrentAdjustmentIndex().get(symbol);
                        // 首次执行:生成含正负值的调整序列
                        // 首次执行:生成调整序列(小幅调整用等额分步,确保合计等于目标值)
                        if (adjustments == null || currentIndex == null) {
                            //分几段执行
                            int nums = Math.max(10, frequency / 10);
                            List<BigDecimal> result = RandomNumbersGenerator.generateNumbers(delayValue.getValue(), nums, decimal + 4);
                            for (int i = 0; i < result.size(); i++) {
                                if (adjustments == null) {
                                    adjustments = new ArrayList<>();
                            if (delayValue.getValue().abs().compareTo(new BigDecimal("2")) <= 0) {
                                adjustments = buildEqualAdjustments(delayValue.getValue(), frequency, decimal);
                            } else {
                                int nums = Math.max(10, frequency / 10);
                                List<BigDecimal> result = RandomNumbersGenerator.generateNumbers(delayValue.getValue(), nums, decimal + 4);
                                adjustments = new ArrayList<>();
                                for (int i = 0; i < result.size(); i++) {
                                    int count = frequency / nums;
                                    if (i == result.size() - 1) {
                                        count += frequency % nums;
                                    }
                                    adjustments.addAll(generateRandomAdjustments(result.get(i), count, decimal));
                                }
                                int count = frequency / nums;
                                if (i == result.size() - 1) {
                                    count += frequency % nums;
                                }
                                adjustments.addAll(generateRandomAdjustments(result.get(i), count, decimal));
                            }
                            currentIndex = 0;
                            AdjustmentValueCache.getPreAllocatedAdjustments().put(symbol, adjustments);
@@ -208,6 +209,7 @@
        if (low != null) {
            realtime.setLow(BigDecimal.valueOf(low));
        }
        DataCache.putRealtime(symbol, realtime);
        this.dataDBService.saveAsyn(realtime);
    }
@@ -218,6 +220,26 @@
        AdjustmentValueCache.getFrequency().remove(symbol);
    }
    /**
     * 等额分步,保证各步相加严格等于 totalValue(适用于管理员小幅调价,如 -0.1)。
     */
    private List<BigDecimal> buildEqualAdjustments(BigDecimal totalValue, int count, int decimal) {
        List<BigDecimal> adjustments = new ArrayList<>(Math.max(1, count));
        if (count <= 1) {
            adjustments.add(totalValue.setScale(decimal, RoundingMode.HALF_UP));
            return adjustments;
        }
        BigDecimal per = totalValue.divide(BigDecimal.valueOf(count), decimal + 4, RoundingMode.HALF_UP);
        BigDecimal sum = BigDecimal.ZERO;
        for (int i = 0; i < count - 1; i++) {
            BigDecimal step = per.setScale(decimal, RoundingMode.HALF_UP);
            adjustments.add(step);
            sum = sum.add(step);
        }
        adjustments.add(totalValue.subtract(sum).setScale(decimal, RoundingMode.HALF_UP));
        return adjustments;
    }
    private List<BigDecimal> generateRandomAdjustments(BigDecimal totalValue, int count, int decimal) {
        List<BigDecimal> adjustments = new ArrayList<>(count);
        BigDecimal sum = BigDecimal.ZERO; // 整体累积和
trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderCalculationService.java
@@ -19,4 +19,14 @@
    public void setOrder_close_line_type(int order_close_line_type);
    /**
     * 按与 settle 相同的规则计算强平价格(全仓/逐仓由 order_close_line_type 决定)
     */
    String calculateForceClosePriceForOrder(ContractOrder order);
    /**
     * 按最新行情刷新持仓未实现盈亏(价差盈亏 + 资金费)
     */
    void refreshMarkPriceProfit(ContractOrder order);
}
trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderCalculationServiceImpl.java
@@ -1,15 +1,12 @@
package com.yami.trading.service.contract;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yami.trading.bean.contract.domain.ContractOrder;
import com.yami.trading.bean.contract.domain.ContractOrderProfit;
import com.yami.trading.bean.data.domain.Realtime;
import com.yami.trading.bean.item.domain.Item;
import com.yami.trading.bean.model.Wallet;
import com.yami.trading.common.util.ThreadUtils;
import com.yami.trading.service.StrongLevelCalculationService;
import com.yami.trading.service.WalletService;
import com.yami.trading.service.data.DataService;
import com.yami.trading.service.impl.StrongLevelCalculationServiceImpl;
import com.yami.trading.service.item.ItemService;
import com.yami.trading.service.syspara.SysparaService;
import lombok.extern.slf4j.Slf4j;
@@ -20,8 +17,8 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@@ -45,9 +42,6 @@
    @Autowired
    private WalletService walletService;
    @Autowired
    private StrongLevelCalculationService strongLevelCalculationService;
    private SysparaService sysparaService;
    public void saveCalculation(String order_no, List<ContractOrder> partyContractOrders) {
@@ -67,6 +61,7 @@
            Realtime realtime = list.get(0);
            BigDecimal close = realtime.getClose();
            settle(order, "watch", close, partyContractOrders);
            BigDecimal add = order.getTradeAvgPrice().add(order.getPips());
            BigDecimal subtract = order.getTradeAvgPrice().subtract(order.getPips());
@@ -101,101 +96,14 @@
    }
    /**
     * 盈亏计算   收益=(平仓均价-开仓均价)*面值*张数
     *
     * USDT保证金合约做多:收益=(平仓均价-开仓均价)*面值*张数
     * USDT保证金合约做空:收益=(开仓均价-平仓均价)*面值*张数
     * 以BTC为例,BTC一张合约面值为0.01BTC,在价格19000的时候,开了10张多单。当价格涨到20000的时候,小明的收益=(20000-19000)*0.01*10=100USDT
     * 币本位合约:
     * 做多:收益=面值*张数/开仓价-面值*张数/平仓价
     * 做空:收益=面值*张数/平仓价-面值*张数/开仓价
     * 以BTC为例,BTC一张合约面值为100美元,小明在价格20000的时候,开了5张空单。当价格下跌到19000的时候,小明的收益=100*5/19000-100*5/20000=0.
     * 盈亏计算
     *
     * @param profit_loss  profit 盈 loss亏
     * @param currentPrice 当前点位
     */
    public void settle(ContractOrder order, String profit_loss, BigDecimal currentPrice, List<ContractOrder> partyContractOrders) {
        Item item = itemService.findBySymbol(order.getSymbol());
        if(null != order.getProfitLossRatio() && order.getProfitLossRatio() > 0){//根据后台设置的盈亏比来
            order.setProfit(order.getDepositOpen().multiply(new BigDecimal((order.getProfitLossRatio()/100))).setScale(2, RoundingMode.DOWN));
        }else{
            /*
             * 根据偏 差点数和手数算出盈亏金额
             */
            /**
             * 偏差点位
             */
            BigDecimal point = currentPrice.subtract(order.getTradeAvgPrice());
            BigDecimal amount = point.multiply(new BigDecimal("0.01")).multiply(order.getVolumeOpen()).setScale(4, BigDecimal.ROUND_DOWN);
            if (ContractOrder.DIRECTION_BUY.equals(order.getDirection())) {
                order.setProfit(amount);
            } else{
                order.setProfit(amount.negate());
            }
        }
        double faceValue = 0.01; // 合约面值(固定面值不能调整)
        double maintenanceMarginRate = 0.004; // 维持保证金率(固定不变)
        /**
         * 全仓收益加入保证金计算
         */
        BigDecimal earnings;
        if (order.getLocationType() == 1) {
            earnings = BigDecimal.ZERO;
            // 统计非当前订单的其他收益
            /*List<ContractOrder> list = contractOrderService.list(new LambdaQueryWrapper<>(ContractOrder.class)
                    .eq(ContractOrder::getState, ContractOrder.STATE_SUBMITTED)
                    .eq(ContractOrder::getPartyId, order.getPartyId())
                    .ne(ContractOrder::getOrderNo, order.getOrderNo())
            );
            // 提前计算 currentPrice 与 order.getTradeAvgPrice() 的差值,避免重复计算
            BigDecimal priceDifference = currentPrice.subtract(order.getTradeAvgPrice());
            // 计算所有订单的收益
            for (ContractOrder contractOrder : list) {
                BigDecimal profit = priceDifference
                        .multiply(new BigDecimal("0.01"))
                        .multiply(contractOrder.getVolumeOpen())
                        .setScale(4, RoundingMode.DOWN);
                earnings = earnings.add(profit);  // 累加收益
            }*/
            // 获取当前账户余额并加到收益中
            //Map<String, Object> moneyAll = walletService.getMoneyAll(order.getPartyId());
            Wallet wallet = walletService.saveWalletByPartyId(order.getPartyId());
            earnings = earnings.add(wallet.getMoney());
            earnings = earnings.add(order.getDepositOpen());
        } else {
            // 如果不符合条件,直接使用 order.getDepositOpen() 作为收益
            earnings = order.getDepositOpen().add(order.getAddDepositOpen());
        }
        if(ContractOrder.DIRECTION_BUY.equals(order.getDirection())){
            double forceClosePrice = strongLevelCalculationService.calculateLiquidationPrice(earnings.doubleValue(),
                    faceValue, order.getVolumeOpen().doubleValue(), order.getTradeAvgPrice().doubleValue()
                    , maintenanceMarginRate, item.getUnitFee().doubleValue());
            order.setForceClosePrice(BigDecimal.valueOf(forceClosePrice).toString());
        }else{
            double forceClosePrice = strongLevelCalculationService.calculateEmptyLiquidationPrice(earnings.doubleValue(),
                    faceValue, order.getVolumeOpen().doubleValue(), order.getTradeAvgPrice().doubleValue()
                    , maintenanceMarginRate, item.getUnitFee().doubleValue());
            order.setForceClosePrice(BigDecimal.valueOf(forceClosePrice).toString());
        }
        /**
         * 多次平仓价格不对,后续修
         */
        order.setCloseAvgPrice(currentPrice);
        this.contractOrderService.updateByIdBuffer(order);
        applyMarkPriceToOrder(order, currentPrice);
        /**
         * 止盈价
@@ -206,7 +114,7 @@
             * 买涨
             */
            if (currentPrice.compareTo(profitStop) >= 0) {
                this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(),null);
                this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(), null);
                return;
            }
        } else if (profitStop != null && profitStop.compareTo(BigDecimal.ZERO) > 0
@@ -215,7 +123,7 @@
             * 买跌
             */
            if (currentPrice.compareTo(profitStop) <= 0) {
                this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(),null);
                this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(), null);
                return;
            }
        }
@@ -230,7 +138,7 @@
             * 买涨
             */
            if (currentPrice.compareTo(loss_stop) <= 0) {
                this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(),null);
                this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(), null);
                return;
            }
@@ -240,88 +148,157 @@
             */
            if (currentPrice.compareTo(loss_stop) >= 0) {
                this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(),null);
                this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(), null);
                return;
            }
        }
        /**
         * 强平计算
         */
        //重新计算强平
        BigDecimal forceClosePrice = new BigDecimal(order.getForceClosePrice());
        //达到强平价
        if ((ContractOrder.DIRECTION_BUY.equals(order.getDirection()) && currentPrice.compareTo(new BigDecimal(order.getForceClosePrice())) <= 0)
                || (ContractOrder.DIRECTION_SELL.equals(order.getDirection()) && currentPrice.compareTo(new BigDecimal(order.getForceClosePrice())) >= 0)) {
            BigDecimal point = forceClosePrice.subtract(order.getTradeAvgPrice());
            BigDecimal amount = point.multiply(new BigDecimal("0.01")).multiply(order.getVolumeOpen()).setScale(4, BigDecimal.ROUND_DOWN);
            if (ContractOrder.DIRECTION_BUY.equals(order.getDirection())) {
                order.setProfit(amount);
            } else{
                order.setProfit(amount.negate());
            }
            //强平利润固定-100%
            order.setProfit(order.getDepositOpen().add(order.getAddDepositOpen()).negate());
            //全仓强平利润+账户余额
            if (order.getLocationType() == 1) {
                Wallet wallet = this.walletService.findByUserId(order.getPartyId());
                order.setProfit(order.getProfit().subtract(wallet.getMoney()));
            }
        }
        ContractOrderProfit cp = contractOrderService.getCacheProfit(order.getUuid());
        BigDecimal profit1 = cp != null ? cp.getProfit() : defaultZero(order.getProfit());
        order.setForceClosePrice(calculateForceClosePriceForOrder(order));
        this.contractOrderService.updateByIdBuffer(order);
        //判断是全仓还是逐仓
        if (order.getLocationType() == 1) {
//            /**
//             * 收益
//             */
//            BigDecimal profit = BigDecimal.ZERO;
//
//            List<ContractOrder> list = partyContractOrders;
//            for (int i = 0; i < list.size(); i++) {
//                ContractOrder close_line = list.get(i);
//                profit = profit.add(close_line.getProfit()).add(close_line.getDeposit());
//            }
//            Wallet wallet = this.walletService.findByUserId(order.getPartyId().toString());
            //this.contractOrderService.updateByIdBuffer(order);
//            if (profit.add(wallet.getMoney()).compareTo(BigDecimal.ZERO) <= 0) {
            //判断买涨还是买跌"buy":买(多) "sell":卖(空)
            if(order.getDirection().equals("buy")){
                if (currentPrice.compareTo(new BigDecimal(order.getForceClosePrice())) <= 0) {//达到强平价
                    /**
                     * 触发全仓强平
                     */
                    log.info("------------------currentPrice-------------:"+currentPrice);
                    log.info("------------------order.getForceClosePrice()-------------"+order.getForceClosePrice());
                    log.info("------------------开多强平-------------");
                    this.contractOrderService.allClose(order.getPartyId());
        if (order_close_line_type == 1) {
            Wallet wallet = this.walletService.findByUserId(order.getPartyId().toString());
            List<ContractOrder> list = contractOrderService.findSubmitted(order.getPartyId(), null, null, null, null, null);
            BigDecimal totalEquity = defaultZero(wallet.getMoney());
            for (ContractOrder contractOrder : list) {
                if (ContractOrder.STATE_SUBMITTED.equals(contractOrder.getState())) {
                    contractOrderService.wrapProfit(contractOrder);
                }
            }else{
                if (currentPrice.compareTo(new BigDecimal(order.getForceClosePrice()))>= 0) {//达到强平价
                    /**
                     * 触发全仓强平
                     */
                    log.info("------------------currentPrice-------------:"+currentPrice);
                    log.info("------------------order.getForceClosePrice()-------------"+order.getForceClosePrice());
                    log.info("------------------开空强平-------------");
                    this.contractOrderService.allClose(order.getPartyId());
                totalEquity = totalEquity.add(defaultZero(contractOrder.getProfit()).add(defaultZero(contractOrder.getDeposit())));
            }
                }
            if (totalEquity.compareTo(BigDecimal.ZERO) <= 0) {
                /**
                 * 触发全仓强平
                 */
                this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(), "强平");
            }
        } else {
            if(order.getDirection().equals("buy")){
                if (currentPrice.compareTo(new BigDecimal(order.getForceClosePrice())) <= 0) {//达到强平价
                    this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(),"强平");
                }
            }else{
                if (currentPrice.compareTo(new BigDecimal(order.getForceClosePrice())) >= 0) {//达到强平价
                    this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(),"强平");
                }
            if (profit1.compareTo(BigDecimal.ZERO) >= 0 || profit1.abs().compareTo(new BigDecimal("0.000001")) < 0) {
                return;
            }
            BigDecimal divide = order.getDeposit().divide(profit1.abs(), 10, RoundingMode.HALF_UP);
            if (divide.compareTo(order_close_line.divide(new BigDecimal(100), 10, RoundingMode.HALF_UP)) <= 0) {
                /**
                 * 低于系统默认平仓线,进行强平
                 */
                this.contractOrderService.saveClose(order.getPartyId().toString(), order.getOrderNo(), "强平");
            }
        }
    }
    @Override
    public String calculateForceClosePriceForOrder(ContractOrder order) {
        if (order == null || defaultZero(order.getVolume()).compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO.toPlainString();
        }
        BigDecimal forceClose;
        if (order_close_line_type == 1) {
            Wallet wallet = this.walletService.findByUserId(order.getPartyId().toString());
            forceClose = calculateType1ForceClosePrice(order, wallet);
        } else {
            forceClose = calculateType2ForceClosePrice(order);
        }
        if (forceClose.compareTo(BigDecimal.ZERO) < 0) {
            forceClose = BigDecimal.ZERO;
        }
        Integer decimal = itemService.getDecimal(order.getSymbol());
        return forceClose.setScale(decimal, RoundingMode.HALF_UP).toPlainString();
    }
    private BigDecimal defaultZero(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    /**
     * 合约张数对应标的数量系数(每张面值,与 countSheets 一致,默认 0.01)
     */
    private BigDecimal getContractFaceValue(ContractOrder order) {
        Item item = itemService.findBySymbol(order.getSymbol());
        if (item != null && item.getFaceValue() > 0) {
            return BigDecimal.valueOf(item.getFaceValue());
        }
        return new BigDecimal("0.01");
    }
    /**
     * 每张张数对应的价格敏感度:盈亏/强平 = 价差 × volume × faceValue
     */
    private BigDecimal getVolumePriceFactor(ContractOrder order) {
        return defaultZero(order.getVolume()).multiply(getContractFaceValue(order));
    }
    private BigDecimal calculateType1ForceClosePrice(ContractOrder order, Wallet wallet) {
        BigDecimal tradeAvgPrice = defaultZero(order.getTradeAvgPrice());
        BigDecimal volumeFactor = getVolumePriceFactor(order);
        if (volumeFactor.compareTo(BigDecimal.ZERO) <= 0 || tradeAvgPrice.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        List<ContractOrder> list = contractOrderService.findSubmitted(order.getPartyId(), null, null, null, null, null);
        BigDecimal otherEquity = BigDecimal.ZERO;
        for (ContractOrder contractOrder : list) {
            if (ContractOrder.STATE_SUBMITTED.equals(contractOrder.getState())) {
                contractOrderService.wrapProfit(contractOrder);
            }
            if (order.getUuid().equals(contractOrder.getUuid())) {
                continue;
            }
            otherEquity = otherEquity.add(defaultZero(contractOrder.getProfit()).add(defaultZero(contractOrder.getDeposit())));
        }
        BigDecimal baseEquity = defaultZero(wallet.getMoney())
                .add(otherEquity)
                .add(defaultZero(order.getDeposit()));
        BigDecimal priceOffset = baseEquity.divide(volumeFactor, 10, RoundingMode.HALF_UP);
        if (ContractOrder.DIRECTION_BUY.equalsIgnoreCase(order.getDirection())) {
            return tradeAvgPrice.subtract(priceOffset);
        }
        return tradeAvgPrice.add(priceOffset);
    }
    private BigDecimal calculateType2ForceClosePrice(ContractOrder order) {
        BigDecimal tradeAvgPrice = defaultZero(order.getTradeAvgPrice());
        BigDecimal volumeFactor = getVolumePriceFactor(order);
        if (volumeFactor.compareTo(BigDecimal.ZERO) <= 0 || tradeAvgPrice.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        BigDecimal thresholdRatio = order_close_line.divide(new BigDecimal(100), 10, RoundingMode.HALF_UP);
        if (thresholdRatio.compareTo(BigDecimal.ZERO) <= 0) {
            return tradeAvgPrice;
        }
        BigDecimal availableDeposit = defaultZero(order.getDeposit());
        if (availableDeposit.compareTo(BigDecimal.ZERO) <= 0) {
            return tradeAvgPrice;
        }
        BigDecimal requiredLoss = availableDeposit.divide(thresholdRatio, 10, RoundingMode.HALF_UP);
        BigDecimal priceOffset = requiredLoss.divide(volumeFactor, 10, RoundingMode.HALF_UP);
        if (ContractOrder.DIRECTION_BUY.equalsIgnoreCase(order.getDirection())) {
            return tradeAvgPrice.subtract(priceOffset);
        }
        return tradeAvgPrice.add(priceOffset);
    }
    private void applyMarkPriceToOrder(ContractOrder order, BigDecimal currentPrice) {
        BigDecimal tradeAvgPrice = defaultZero(order.getTradeAvgPrice());
        if (tradeAvgPrice.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        BigDecimal volumeFactor = getVolumePriceFactor(order);
        if (volumeFactor.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        // 与平仓结算一致:盈亏 = (现价 - 开仓价) × 张数 × 面值
        BigDecimal point = currentPrice.subtract(tradeAvgPrice);
        BigDecimal priceProfit = point.multiply(volumeFactor).setScale(6, RoundingMode.DOWN);
        if (!ContractOrder.DIRECTION_BUY.equals(order.getDirection())) {
            priceProfit = priceProfit.negate();
        }
        BigDecimal fundingPnl = contractOrderService.calculateAccruedFundingPnl(order, currentPrice, new Date());
        order.setProfit(priceProfit.add(fundingPnl));
        order.setCloseAvgPrice(currentPrice);
        this.contractOrderService.updateByIdBuffer(order);
    }
    public void setDataService(DataService dataService) {
@@ -348,10 +325,18 @@
        this.order_close_line_type = order_close_line_type;
    }
    public static void main(String[] args) {
        StrongLevelCalculationService strongLevelCalculationService = new StrongLevelCalculationServiceImpl();
        double forceClosePrice = strongLevelCalculationService.calculateLiquidationPrice(100,
                0.01, 67704.80, 1.477
                , 0.004, 0.0005);
    @Override
    public void refreshMarkPriceProfit(ContractOrder order) {
        if (order == null || !ContractOrder.STATE_SUBMITTED.equals(order.getState())) {
            return;
        }
        if (defaultZero(order.getVolume()).compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        List<Realtime> list = this.dataService.realtime(order.getSymbol());
        if (list.isEmpty()) {
            return;
        }
        applyMarkPriceToOrder(order, list.get(0).getClose());
    }
}
trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java
@@ -1,5 +1,6 @@
package com.yami.trading.service.contract;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
@@ -9,6 +10,8 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yami.trading.bean.contract.domain.ContractApplyOrder;
import com.yami.trading.bean.contract.domain.ContractOrderProfit;
import com.yami.trading.bean.syspara.domain.Syspara;
import com.yami.trading.bean.contract.dto.ContractApplyOrderDTO;
import com.yami.trading.bean.contract.dto.ContractOrderDTO;
import com.yami.trading.bean.contract.query.ContractApplyOrderQuery;
@@ -32,7 +35,9 @@
import com.yami.trading.service.user.UserService;
import com.yami.trading.service.WalletService;
import com.yami.trading.service.item.ItemService;
import com.yami.trading.service.syspara.SysparaService;
import com.yami.trading.util.ConverterUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.jetbrains.annotations.NotNull;
@@ -52,6 +57,10 @@
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
@@ -62,9 +71,11 @@
 * @author lucas
 * @version 2023-03-29
 */
@Slf4j
@Service
@Transactional
public class ContractOrderService extends ServiceImpl<ContractOrderMapper, ContractOrder> {
    private static final int FUNDING_SETTLEMENT_INTERVAL_MINUTES = 240;
    private final ConcurrentMap<String, ContractOrder> map = new ConcurrentHashMap<>();
    private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
@@ -100,10 +111,17 @@
    private ContractApplyOrderService contractApplyOrderService;
    @Autowired
    @Lazy
    private ContractOrderCalculationService contractOrderCalculationService;
    @Autowired
    private StrongLevelCalculationService strongLevelCalculationService;
    @Autowired
    MoneyLogService moneyLogService;
    @Autowired
    private SysparaService sysparaService;
    public IPage<ContractOrderDTO> listRecord(Page page, ContractOrderQuery query) {
        return baseMapper.listRecord(page, query);
@@ -273,6 +291,8 @@
                return null;
            }
            contractOrderCalculationService.refreshMarkPriceProfit(order);
            /**
             * 收益
             */
@@ -425,7 +445,145 @@
     */
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void updateByIdBuffer(ContractOrder entity) {
        updateProfit(entity);
        map.put(entity.getUuid(), entity);
    }
    public void wrapProfit(ContractOrder contractOrder) {
        if (ContractOrder.STATE_SUBMITTED.equalsIgnoreCase(contractOrder.getState())) {
            ContractOrderProfit cacheProfit = getCacheProfit(contractOrder.getUuid());
            if (cacheProfit != null) {
                contractOrder.setProfit(cacheProfit.getProfit());
                contractOrder.setCloseAvgPrice(cacheProfit.getCloseAvgPrice());
                contractOrder.setForceClosePrice(cacheProfit.getForceClosePrice());
            } else {
                contractOrder.setProfit(BigDecimal.ZERO);
            }
        }
    }
    public ContractOrderProfit getCacheProfit(String uuid) {
        return RedisUtil.get(ContractRedisKeys.CONTRACT_PROFIT_V1 + uuid);
    }
    public void updateProfit(ContractOrder order) {
        if (ContractOrder.STATE_SUBMITTED.equals(order.getState())) {
            RedisUtil.set(ContractRedisKeys.CONTRACT_PROFIT_V1 + order.getUuid(), BeanUtil.copyProperties(order, ContractOrderProfit.class));
            Map<String, ContractOrder> submittedMap =
                    RedisUtil.get(ContractRedisKeys.CONTRACT_SUBMITTED_ORDER_PARTY_ID + order.getPartyId());
            if (null == submittedMap) {
                submittedMap = new ConcurrentHashMap<>();
            }
            ContractOrder orderOld = submittedMap.get(order.getOrderNo());
            if (orderOld == null) {
                return;
            }
            orderOld.setCloseAvgPrice(order.getCloseAvgPrice());
            orderOld.setProfit(order.getProfit());
            orderOld.setForceClosePrice(order.getForceClosePrice());
            submittedMap.put(order.getOrderNo(), orderOld);
            RedisUtil.set(ContractRedisKeys.CONTRACT_SUBMITTED_ORDER_PARTY_ID + order.getPartyId(), submittedMap);
            Map<String, BigDecimal> contractAssetsOrder = this.walletService.getMoneyContractByOrder(order);
            Map<String, BigDecimal> contractAssetsOrderOld = this.walletService.getMoneyContractByOrder(orderOld);
            BigDecimal contractAssets = RedisUtil.get(ContractRedisKeys.CONTRACT_ASSETS_PARTY_ID + order.getPartyId().toString());
            if (contractAssets == null) {
                contractAssets = BigDecimal.ZERO;
            }
            BigDecimal contractAssetsDeposit = RedisUtil.get(ContractRedisKeys.CONTRACT_ASSETS_DEPOSIT_PARTY_ID + order.getPartyId().toString());
            if (contractAssetsDeposit == null) {
                contractAssetsDeposit = BigDecimal.ZERO;
            }
            BigDecimal contractAssetsProfit = RedisUtil.get(ContractRedisKeys.CONTRACT_ASSETS_PROFIT_PARTY_ID + order.getPartyId().toString());
            if (contractAssetsProfit == null) {
                contractAssetsProfit = BigDecimal.ZERO;
            }
            RedisUtil.set(ContractRedisKeys.CONTRACT_ASSETS_PARTY_ID + order.getPartyId().toString(),
                    contractAssets.add(contractAssetsOrder.get("money_contract")).subtract(contractAssetsOrderOld.get("money_contract")));
            RedisUtil.set(ContractRedisKeys.CONTRACT_ASSETS_DEPOSIT_PARTY_ID + order.getPartyId().toString(),
                    contractAssetsDeposit.add(contractAssetsOrder.get("money_contract_deposit")).subtract(contractAssetsOrderOld.get("money_contract_deposit")));
            RedisUtil.set(ContractRedisKeys.CONTRACT_ASSETS_PROFIT_PARTY_ID + order.getPartyId().toString(),
                    contractAssetsProfit.add(contractAssetsOrder.get("money_contract_profit")).subtract(contractAssetsOrderOld.get("money_contract_profit")));
        }
    }
    /**
     * 累计资金费带来的盈亏(正数表示用户获利):多仓为 -合约价值×费率×次数,空仓为 +合约价值×费率×次数。
     */
    public BigDecimal calculateAccruedFundingPnl(ContractOrder order, BigDecimal markPrice, Date asOf) {
        if (order == null || markPrice == null || asOf == null || order.getCreateTime() == null) {
            return BigDecimal.ZERO;
        }
        BigDecimal vol = defaultZero(order.getVolume());
        if (vol.compareTo(BigDecimal.ZERO) <= 0 || markPrice.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        long periods = countFundingSettlementPoints(
                order.getCreateTime().toInstant(),
                asOf.toInstant(),
                ZoneId.systemDefault(),
                FUNDING_SETTLEMENT_INTERVAL_MINUTES);
        if (periods <= 0) {
            return BigDecimal.ZERO;
        }
        Item item = itemService.findBySymbol(order.getSymbol());
        BigDecimal faceValue = item != null && item.getFaceValue() > 0
                ? BigDecimal.valueOf(item.getFaceValue()) : new BigDecimal("0.01");
        BigDecimal notional = markPrice.multiply(vol).multiply(faceValue);
        BigDecimal rate = getContractFundingRate();
        BigDecimal signed = ContractOrder.DIRECTION_BUY.equalsIgnoreCase(order.getDirection())
                ? rate.negate() : rate;
        return notional.multiply(signed).multiply(BigDecimal.valueOf(periods)).setScale(8, RoundingMode.HALF_UP);
    }
    public BigDecimal getContractFundingRate() {
        try {
            Syspara p = sysparaService.find("funding_fee");
            if (p == null || StringUtils.isEmptyString(p.getSvalue())) {
                return BigDecimal.ZERO;
            }
            return new BigDecimal(p.getSvalue().trim());
        } catch (Exception e) {
            log.warn("parse funding_fee syspara failed", e);
            return BigDecimal.ZERO;
        }
    }
    private static ZonedDateTime nextFundingSettlementStrictlyAfter(ZonedDateTime t, int intervalMinutes) {
        int step = intervalMinutes <= 0 ? 240 : intervalMinutes;
        ZonedDateTime tTrunc = t.withSecond(0).withNano(0);
        ZonedDateTime dayStart = tTrunc.toLocalDate().atStartOfDay(tTrunc.getZone());
        long minutesFromDayStart = ChronoUnit.MINUTES.between(dayStart, tTrunc);
        long slotIndex = minutesFromDayStart / step;
        ZonedDateTime slotStart = dayStart.plusMinutes(slotIndex * step);
        ZonedDateTime next = slotStart;
        while (!next.isAfter(t)) {
            next = next.plusMinutes(step);
        }
        return next;
    }
    private static long countFundingSettlementPoints(Instant open, Instant end, ZoneId zone, int intervalMinutes) {
        if (open == null || end == null || !end.isAfter(open)) {
            return 0;
        }
        int step = intervalMinutes <= 0 ? 240 : intervalMinutes;
        ZonedDateTime zEnd = end.atZone(zone);
        ZonedDateTime cursor = nextFundingSettlementStrictlyAfter(open.atZone(zone), step);
        long cnt = 0;
        while (!cursor.isAfter(zEnd)) {
            cnt++;
            cursor = cursor.plusMinutes(step);
        }
        return cnt;
    }
    private BigDecimal defaultZero(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    public void update(ContractOrder order) {
@@ -515,49 +673,58 @@
    /**
     * 价差盈亏(与持仓定时计算一致):(平仓价 - 开仓价) × 平仓张数 × 面值,空仓取反
     */
    private BigDecimal calculatePricePnl(ContractOrder order, BigDecimal closePrice, BigDecimal sheets) {
        BigDecimal tradeAvgPrice = order.getTradeAvgPrice() == null ? BigDecimal.ZERO : order.getTradeAvgPrice();
        if (tradeAvgPrice.compareTo(BigDecimal.ZERO) <= 0 || sheets == null || sheets.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        Item item = itemService.findBySymbol(order.getSymbol());
        BigDecimal faceValue = item != null && item.getFaceValue() > 0
                ? BigDecimal.valueOf(item.getFaceValue()) : new BigDecimal("0.01");
        BigDecimal point = closePrice.subtract(tradeAvgPrice);
        BigDecimal pnl = point.multiply(faceValue).multiply(sheets).setScale(4, RoundingMode.DOWN);
        if (!ContractOrder.DIRECTION_BUY.equals(order.getDirection())) {
            pnl = pnl.negate();
        }
        return pnl;
    }
    /**
     * 收益结算,平仓时计算
     *
     * @param closevolume 平仓的张数
     * @param volume 平仓的张数
     */
    public BigDecimal settle(ContractOrder order, BigDecimal volume) {
        /**
         * 偏差点位
         */
        List<Realtime> list = this.dataService.realtime(order.getSymbol());
        if (list.size() == 0) {
            order.getProfit();
        ContractOrderProfit cacheProfit = getCacheProfit(order.getUuid());
        BigDecimal currentVolume = order.getVolume() == null ? BigDecimal.ZERO : order.getVolume();
        if (currentVolume.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        Realtime realtime = list.get(0);
        BigDecimal close = realtime.getClose();
        BigDecimal point = close.subtract(order.getTradeAvgPrice());
        BigDecimal profit = point.multiply(new BigDecimal("0.01")).multiply(order.getVolumeOpen()).setScale(4, BigDecimal.ROUND_DOWN);;
        if (order.getForceClosePrice() != null && !order.getForceClosePrice().isEmpty()) {  //达到强平价
            if ((ContractOrder.DIRECTION_BUY.equals(order.getDirection()) && close.compareTo(new BigDecimal(order.getForceClosePrice())) <= 0)
                || (ContractOrder.DIRECTION_SELL.equals(order.getDirection()) && close.compareTo(new BigDecimal(order.getForceClosePrice())) >= 0)) {
                point = new BigDecimal(order.getForceClosePrice()).subtract(order.getTradeAvgPrice());
                profit = point.multiply(new BigDecimal("0.01")).multiply(order.getVolumeOpen()).setScale(4, BigDecimal.ROUND_DOWN);;
                System.out.println("------------------point-------------:"+point);
                System.out.println("------------------profit------------:"+profit);
                profit = order.getDepositOpen().add(order.getAddDepositOpen()).negate();
                System.out.println("------------------扣除保证金------------:"+profit);
                if (order.getLocationType() == 1) {
                    Wallet wallet = this.walletService.findByUserId(order.getPartyId());
                    System.out.println("------------------扣除余额------------:"+wallet.getMoney());
                    profit = profit.subtract(wallet.getMoney());
                }
        BigDecimal closeRatio = volume.divide(currentVolume, 10, RoundingMode.HALF_UP);
        BigDecimal originProfit = BigDecimal.ZERO;
        if (cacheProfit != null) {
            originProfit = cacheProfit.getProfit();
            if (cacheProfit.getCloseAvgPrice() != null) {
                order.setCloseAvgPrice(cacheProfit.getCloseAvgPrice());
            }
        } else {
            List<Realtime> list = this.dataService.realtime(order.getSymbol());
            if (!list.isEmpty()) {
                originProfit = calculatePricePnl(order, list.get(0).getClose(), currentVolume);
            }
        }
        if(null != order.getProfitLossRatio() && order.getProfitLossRatio() > 0){
            profit = order.getDepositOpen().multiply(new BigDecimal((order.getProfitLossRatio()/100))).setScale(2, RoundingMode.DOWN);
        BigDecimal profit = originProfit.multiply(closeRatio).setScale(4, RoundingMode.DOWN);
        if (null != order.getProfitLossRatio() && order.getProfitLossRatio() > 0) {
            profit = order.getDepositOpen().multiply(new BigDecimal((order.getProfitLossRatio() / 100))).setScale(2, RoundingMode.DOWN);
        }
        if (ContractOrder.DIRECTION_BUY.equals(order.getDirection())) {
            order.setProfit(profit);
        } else{
            order.setProfit(profit.negate());
        }
        order.setProfit(profit);
        BigDecimal rentalProfit = order.getDepositOpen().add(order.getProfit()).add(order.getAddDepositOpen());
        System.out.println("------------------rentalProfit------------:"+rentalProfit);
        BigDecimal rate = volume.divide(order.getVolumeOpen(), 2, RoundingMode.HALF_UP);
        order.setAmountClose(order.getAmountClose().add(profit));
        order.setVolume(order.getVolume().subtract(volume));
@@ -566,7 +733,6 @@
            order.setState(ContractOrder.STATE_CREATED);
            order.setCloseTime(DateUtil.currentSeconds());
            order.setCloseTimeTs(DateUtil.currentSeconds());
        }
        return rentalProfit;
    }
@@ -615,7 +781,7 @@
                );
            }
            //计算强平价格
            getStrongPrice(f,item);
            getStrongPrice(f);
            update(f);
            refreshOrder(applyOrder, f);
        }else{
@@ -634,9 +800,10 @@
                    fee = fee.setScale(4, RoundingMode.DOWN);  // 保留两位小数
                    order.setFee(fee);
                }
                double number = strongLevelCalculationService.countSheets(order.getDepositOpen().doubleValue(), order.getLeverRate().intValue(), 0.01, applyOrder.getPrice().doubleValue());
                double number = strongLevelCalculationService.countSheets(order.getDepositOpen().doubleValue(), order.getLeverRate().intValue(), 0.01, realtime.getClose().doubleValue());
                order.setVolume(new BigDecimal(number));
                order.setVolumeOpen(new BigDecimal(number));
                order.setTradeAvgPrice(realtime.getClose());
                walletService.updateMoney(order.getSymbol(), order.getPartyId(), BigDecimal.ZERO.subtract(order.getDeposit()), BigDecimal.ZERO
                        , Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_OPEN, "委托单,订单号[" + order.getOrderNo() + "]"
@@ -646,7 +813,7 @@
                );
            }
            //计算强平价格
            getStrongPrice(order,item);
            getStrongPrice(order);
            save(order);
            refreshOrder(applyOrder, order);
        }
@@ -719,31 +886,8 @@
        }
    }
    private void getStrongPrice(ContractOrder order, Item item) {
        BigDecimal earnings = BigDecimal.ZERO;
        if(order.getLocationType() == 1){//全仓
            // 获取当前账户余额并加到收益中
            //Map<String, Object> moneyAll = walletService.getMoneyAll(order.getPartyId());
            //earnings = order.getDepositOpen().add(new BigDecimal(moneyAll.get("money_wallet").toString()));
            Wallet wallet = walletService.saveWalletByPartyId(order.getPartyId());
            earnings = order.getDepositOpen().add(earnings.add(wallet.getMoney()));
        }else{
            earnings = order.getDepositOpen().add(order.getAddDepositOpen());
        }
        double faceValue = 0.01; // 合约面值(固定面值不能调整)
        double maintenanceMarginRate = 0.004; // 维持保证金率(固定不变)
        //"buy":买(多) "sell":卖(空)
        if(order.getDirection().equals("buy")){
            double forceClosePrice = strongLevelCalculationService.calculateLiquidationPrice(earnings.doubleValue(),
                    faceValue, order.getVolumeOpen().doubleValue(), order.getTradeAvgPrice().doubleValue()
                    , maintenanceMarginRate, item.getUnitFee().doubleValue());
            order.setForceClosePrice(BigDecimal.valueOf(forceClosePrice).toString());
        }else{
            double forceClosePrice = strongLevelCalculationService.calculateEmptyLiquidationPrice(earnings.doubleValue(),
                    faceValue, order.getVolumeOpen().doubleValue(), order.getTradeAvgPrice().doubleValue()
                    , maintenanceMarginRate, item.getUnitFee().doubleValue());
            order.setForceClosePrice(BigDecimal.valueOf(forceClosePrice).toString());
        }
    private void getStrongPrice(ContractOrder order) {
        order.setForceClosePrice(contractOrderCalculationService.calculateForceClosePriceForOrder(order));
    }
    public ContractApplyOrder saveClose(ContractApplyOrder applyOrder, Realtime realtime, String order_no) {