1
zj
5 days ago 60d1f642052ad8c7bd8a11f02f965b122bebf9a4
trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java
@@ -63,7 +63,10 @@
import java.math.RoundingMode;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
@@ -74,7 +77,6 @@
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -88,6 +90,12 @@
@Transactional
@Slf4j
public class ContractOrderService extends ServiceImpl<ContractOrderMapper, ContractOrder> {
    /**
     * 资金费结算间隔(分钟):从当地日界 0:00 起每 N 分钟为一个结算点。
     * 240 即每 4 小时一档(0、4、8、12、16、20 点)。
     */
    private static final int FUNDING_SETTLEMENT_INTERVAL_MINUTES = 240;
    private Logger logger = LogManager.getLogger(ContractOrderService.class);
@@ -163,6 +171,10 @@
    private ContractApplyOrderService contractApplyOrderService;
    @Autowired
    private SysparaService sysparaService;
    @Autowired
    @Lazy
    private ContractOrderCalculationService contractOrderCalculationService;
    public IPage<ContractOrderDTO> listRecordCur(Page page, ContractOrderQuery query) {
        if (query.getStartTime() != null) {
@@ -375,15 +387,30 @@
            return null;
        }
        /**
         * 收益
         */
        contractOrderCalculationService.refreshMarkPriceProfit(order);
        BigDecimal volume = order.getVolume();
        BigDecimal fundingFee = this.calculateFundingFee(order, volume, new Date());
        BigDecimal profit = settle(order, order.getVolume()).subtract(fundingFee);
        order.setAmountClose(order.getAmountClose().subtract(fundingFee));
        order.setFundingFee(defaultZero(order.getFundingFee()).add(fundingFee));
        if (volume.compareTo(BigDecimal.ZERO) <= 0) {
            return null;
        }
        ContractOrderProfit cache = getCacheProfit(order.getUuid());
        BigDecimal markPrice = order.getTradeAvgPrice();
        if (cache != null && cache.getCloseAvgPrice() != null
                && cache.getCloseAvgPrice().compareTo(BigDecimal.ZERO) > 0) {
            markPrice = cache.getCloseAvgPrice();
        }
        BigDecimal fundingPnlChunk = calculateAccruedFundingPnl(order, markPrice, new Date());
        String symbol = order.getSymbol();
        Item item = itemService.findBySymbol(symbol);
        BigDecimal closeFee = calculateCloseTradingFee(item, markPrice, volume);
        BigDecimal profit = settle(order, volume).subtract(closeFee);
        order.setFee(defaultZero(order.getFee()).add(closeFee));
        order.setAmountClose(order.getAmountClose().subtract(closeFee));
        order.setFundingFee(defaultZero(order.getFundingFee()).add(fundingPnlChunk));
//        Item item = itemService.findBySymbol(symbol);
//        profit = exchangeRateService.getUsdtByType(profit, item.getType());
        if (ContractOrder.ORDER_FOLLOW == order.getFollow()) { // 跟单订单
@@ -696,37 +723,90 @@
        return profit;
    }
    private BigDecimal calculateFundingFee(ContractOrder order, BigDecimal closeVolume, Date closeTime) {
        if (order == null || closeVolume == null || closeTime == null) {
    /**
     * 累计资金费带来的盈亏(正数表示用户获利):多仓为 -合约价值×费率×次数,空仓为 +合约价值×费率×次数。
     * 费率来自 syspara funding_fee;结算锚点为系统默认时区、从当天 0:00 起每 {@link #FUNDING_SETTLEMENT_INTERVAL_MINUTES} 分钟一档。
     */
    public BigDecimal calculateAccruedFundingPnl(ContractOrder order, BigDecimal markPrice, Date asOf) {
        if (order == null || markPrice == null || asOf == null || order.getCreateTime() == null) {
            return BigDecimal.ZERO;
        }
        if (closeVolume.compareTo(BigDecimal.ZERO) <= 0
                || order.getVolumeOpen() == null
                || order.getVolumeOpen().compareTo(BigDecimal.ZERO) <= 0
                || order.getCreateTime() == null) {
        BigDecimal vol = defaultZero(order.getVolume());
        if (vol.compareTo(BigDecimal.ZERO) <= 0 || markPrice.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        long holdHours = Duration.between(order.getCreateTime().toInstant(), closeTime.toInstant()).toHours();
        long settlementPeriods = holdHours / 4;
        if (settlementPeriods <= 0) {
        long periods = countFundingSettlementPoints(
                order.getCreateTime().toInstant(),
                asOf.toInstant(),
                ZoneId.systemDefault(),
                FUNDING_SETTLEMENT_INTERVAL_MINUTES);
        if (periods <= 0) {
            return BigDecimal.ZERO;
        }
        BigDecimal notional = markPrice.multiply(vol);
        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);
    }
        BigDecimal fundingRateTotal = BigDecimal.ZERO;
        for (int i = 0; i < settlementPeriods; i++) {
            double periodRate = ThreadLocalRandom.current().nextDouble(-0.001D, 0.0010000001D);
            fundingRateTotal = fundingRateTotal.add(BigDecimal.valueOf(periodRate));
        }
        BigDecimal currentVolume = defaultZero(order.getVolume());
        if (currentVolume.compareTo(BigDecimal.ZERO) <= 0) {
    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;
        }
    }
        BigDecimal closeRatio = closeVolume.divide(currentVolume, 10, RoundingMode.HALF_UP);
        BigDecimal borrowedBase = getCurrentBorrowedAmount(order).multiply(closeRatio);
        return borrowedBase.multiply(fundingRateTotal).setScale(8, RoundingMode.HALF_UP);
    /**
     * 平仓手续费:手续费率×平仓价×数量(与开仓计费规则一致:U 本位用 unitPercentage,否则 unitFee×价×量)。
     */
    public BigDecimal calculateCloseTradingFee(Item item, BigDecimal closePrice, BigDecimal closeVolume) {
        if (item == null || closePrice == null || closeVolume == null
                || closeVolume.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        Syspara syspara = sysparaService.find("u_standard_contract");
        if (ObjectUtils.isNotEmpty(syspara) && "1".equals(syspara.getSvalue())) {
            return closePrice.multiply(closeVolume).multiply(BigDecimal.valueOf(item.getUnitPercentage()))
                    .setScale(8, RoundingMode.HALF_UP);
        }
        return BigDecimal.valueOf(item.getUnitFee()).multiply(closePrice).multiply(closeVolume)
                .setScale(8, RoundingMode.HALF_UP);
    }
    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) {
@@ -932,20 +1012,34 @@
             */
            return applyOrder;
        }
        BigDecimal preVolume = order.getVolume();
        contractOrderCalculationService.refreshMarkPriceProfit(order);
        BigDecimal volume;
        if (applyOrder.getVolume().compareTo(order.getVolume()) > 0) {
            volume = order.getVolume();
        } else {
            volume = applyOrder.getVolume();
        }
        /**
         * 平仓退回的金额
         */
        BigDecimal fundingFee = this.calculateFundingFee(order, volume, new Date());
        BigDecimal profit = this.settle(order, volume);
        BigDecimal netProfit = profit.subtract(fundingFee);
        order.setAmountClose(order.getAmountClose().subtract(fundingFee));
        order.setFundingFee(defaultZero(order.getFundingFee()).add(fundingFee));
        BigDecimal markPrice = BigDecimal.valueOf(realtime.getClose());
        ContractOrderProfit cache = getCacheProfit(order.getUuid());
        if (cache != null && cache.getCloseAvgPrice() != null
                && cache.getCloseAvgPrice().compareTo(BigDecimal.ZERO) > 0) {
            markPrice = cache.getCloseAvgPrice();
        }
        BigDecimal fundingAccruedTotal = calculateAccruedFundingPnl(order, markPrice, new Date());
        BigDecimal fundingPnlChunk = fundingAccruedTotal.multiply(volume.divide(preVolume, 10, RoundingMode.HALF_UP))
                .setScale(8, RoundingMode.HALF_UP);
        Item item = itemService.findBySymbol(order.getSymbol());
        BigDecimal closeFee = calculateCloseTradingFee(item, markPrice, volume);
        BigDecimal profit = this.settle(order, volume).subtract(closeFee);
        BigDecimal netProfit = profit;
        order.setFee(defaultZero(order.getFee()).add(closeFee));
        order.setAmountClose(order.getAmountClose().subtract(closeFee));
        order.setFundingFee(defaultZero(order.getFundingFee()).add(fundingPnlChunk));
        update(order);
        Wallet wallet = this.walletService.findByUserId(order.getPartyId());
        BigDecimal amount_before = wallet.getMoney();
@@ -961,7 +1055,7 @@
        applyOrder.setVolume(applyOrder.getVolume().subtract(volume));
        applyOrder.setFundingFee(defaultZero(applyOrder.getFundingFee()).add(fundingFee));
        applyOrder.setFundingFee(defaultZero(applyOrder.getFundingFee()).add(fundingPnlChunk));
        if (applyOrder.getVolume().compareTo(BigDecimal.ZERO) <= 0) {
            applyOrder.setState(ContractApplyOrder.STATE_CREATED);
        }