trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiAssetsController.java
@@ -5,6 +5,7 @@ import com.yami.trading.bean.item.domain.Item; import com.yami.trading.bean.model.MoneyLog; import com.yami.trading.bean.model.RealNameAuthRecord; import com.yami.trading.bean.syspara.domain.Syspara; import com.yami.trading.common.domain.Result; import com.yami.trading.common.exception.BusinessException; import com.yami.trading.common.util.Arith; @@ -21,6 +22,7 @@ import com.yami.trading.service.finance.service.FinanceOrderService; import com.yami.trading.service.finance.service.FinanceService; import com.yami.trading.service.impl.ContractAndFutureProfit; import com.yami.trading.service.syspara.SysparaService; import com.yami.trading.service.user.UserDataService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -58,6 +60,9 @@ @Autowired FinanceOrderMapper financeOrderMapper; @Autowired SysparaService sysparaService; /** * 交易栏 顶部数据统计 @@ -263,9 +268,10 @@ data = walletService.getMoneyAll(partyId); } } Syspara fundingFee= sysparaService.find("funding_fee"); RealNameAuthRecord kyc = realNameAuthRecordService.getByUserId(partyId); data.put("status", kyc == null ? 0 : kyc.getStatus()); data.put("fundingFee", fundingFee.getSvalue()); resultObject.setData(data); } catch (BusinessException e) { resultObject.setCode("1"); trading-order-service/src/main/java/com/yami/trading/service/contract/ContractApplyOrderService.java
@@ -278,7 +278,7 @@ order.setDeposit(BigDecimal.valueOf(close).multiply(order.getVolume()).divide(order.getLeverRate())); order.setFee(BigDecimal.valueOf(item.getUnitFee()).multiply(order.getDeposit()).divide(order.getLeverRate())); order.setFee(BigDecimal.valueOf(item.getUnitFee()).multiply(BigDecimal.valueOf(close).multiply(order.getVolume()))); BigDecimal fee = order.getFee(); // if (order.getLeverRate() != null) { // // 加上杠杆 trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderCalculationService.java
@@ -23,4 +23,9 @@ public void setOrder_close_line_type(int order_close_line_type); public BigDecimal calculateAllProfit(ContractOrder order); public BigDecimal calculateTodayProfit(ContractOrder order, ZoneId zoneId); /** * 按最新行情价刷新订单缓存盈亏(价差盈亏 + 累计资金费),用于平仓前与定时任务一致。 */ void refreshMarkPriceProfit(ContractOrder order); } trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderCalculationServiceImpl.java
@@ -1,17 +1,16 @@ package com.yami.trading.service.contract; 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.bean.syspara.domain.Syspara; import com.yami.trading.common.constants.ContractRedisKeys; import com.yami.trading.common.util.RedisUtil; import com.yami.trading.common.util.ThreadUtils; import com.yami.trading.service.WalletService; import com.yami.trading.service.data.DataService; import com.yami.trading.service.item.ItemService; import com.yami.trading.service.syspara.SysparaService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; import org.apache.logging.log4j.LogManager; @@ -21,13 +20,12 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.util.Date; import java.util.List; @Slf4j @@ -51,31 +49,10 @@ private DataService dataService; @Autowired private WalletService walletService; @Resource private SysparaService sysparaService; private Logger logger = LogManager.getLogger(ContractOrderCalculationServiceImpl.class); private BigDecimal defaultZero(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } private BigDecimal estimateAccruedFundingFee(ContractOrder order) { if (order == null || order.getCreateTime() == null) { return BigDecimal.ZERO; } BigDecimal borrowedAmount = defaultZero(order.getBorrowedAmount()); if (borrowedAmount.compareTo(BigDecimal.ZERO) <= 0) { return BigDecimal.ZERO; } long holdHours = Duration.between(order.getCreateTime().toInstant(), Instant.now()).toHours(); long settlementPeriods = holdHours / 4; if (settlementPeriods <= 0) { return BigDecimal.ZERO; } BigDecimal conservativeRate = new BigDecimal("0.001"); return borrowedAmount.multiply(conservativeRate) .multiply(BigDecimal.valueOf(settlementPeriods)) .setScale(8, RoundingMode.HALF_UP); } private BigDecimal calculateType1ForceClosePrice(ContractOrder order, Wallet wallet) { @@ -97,11 +74,9 @@ otherEquity = otherEquity.add(defaultZero(contractOrder.getProfit()).add(defaultZero(contractOrder.getDeposit()))); } BigDecimal accruedFundingFee = estimateAccruedFundingFee(order); BigDecimal baseEquity = defaultZero(wallet.getMoney()) .add(otherEquity) .add(defaultZero(order.getDeposit())) .subtract(accruedFundingFee); .add(defaultZero(order.getDeposit())); BigDecimal priceOffset = baseEquity.divide(volume, 10, RoundingMode.HALF_UP); if (ContractOrder.DIRECTION_BUY.equalsIgnoreCase(order.getDirection())) { return tradeAvgPrice.subtract(priceOffset); @@ -119,7 +94,7 @@ if (thresholdRatio.compareTo(BigDecimal.ZERO) <= 0) { return tradeAvgPrice; } BigDecimal availableDeposit = defaultZero(order.getDeposit()).subtract(estimateAccruedFundingFee(order)); BigDecimal availableDeposit = defaultZero(order.getDeposit()); if (availableDeposit.compareTo(BigDecimal.ZERO) <= 0) { return tradeAvgPrice; } @@ -195,11 +170,10 @@ BigDecimal point = close.subtract(order.getTradeAvgPrice()).abs().divide(order.getPips(), 10, RoundingMode.HALF_UP); // 根据偏 差点数和手数算出盈亏金额 BigDecimal amount = order.getPipsAmount().multiply(point).multiply(order.getVolume()); if (order.getDirection().equalsIgnoreCase(ContractOrder.DIRECTION_BUY)) { return amount; } else { return amount.negate(); } BigDecimal pricePnl = order.getDirection().equalsIgnoreCase(ContractOrder.DIRECTION_BUY) ? amount : amount.negate(); BigDecimal funding = contractOrderService.calculateAccruedFundingPnl(order, close, new Date()); return pricePnl.add(funding); } @@ -230,11 +204,10 @@ * 根据偏 差点数和手数算出盈亏金额 */ BigDecimal amount = order.getPipsAmount().multiply(point).multiply(order.getVolume()); if (order.getDirection().equalsIgnoreCase(ContractOrder.DIRECTION_BUY)) { return amount; } else { return amount.negate(); } BigDecimal pricePnl = order.getDirection().equalsIgnoreCase(ContractOrder.DIRECTION_BUY) ? amount : amount.negate(); BigDecimal funding = contractOrderService.calculateAccruedFundingPnl(order, close, new Date()); return pricePnl.add(funding); } @@ -262,28 +235,7 @@ pips = new BigDecimal("0.01"); } /** * 根据价格变化百分比和保证金计算盈亏金额 */ BigDecimal priceChangeRatio = currentPrice.subtract(order.getTradeAvgPrice()) .divide(order.getTradeAvgPrice(), 6, RoundingMode.DOWN); BigDecimal margin = order.getTradeAvgPrice().multiply(order.getVolume()); // 这是用户当前持仓对应的投资金额 BigDecimal profitAmount = margin.multiply(priceChangeRatio); if (ContractOrder.DIRECTION_BUY.equals(order.getDirection())) { order.setProfit(profitAmount.setScale(6,RoundingMode.DOWN)); } else{ order.setProfit(profitAmount.setScale(6,RoundingMode.DOWN).negate()); } /** * 多次平仓价格不对,后续修 */ order.setCloseAvgPrice(currentPrice); this.contractOrderService.updateByIdBuffer(order); applyMarkPriceToOrder(order, currentPrice); /** * 止盈价 @@ -332,7 +284,8 @@ return; } } BigDecimal profit1 = contractOrderService.getCacheProfit(order.getUuid()).getProfit(); ContractOrderProfit cp = contractOrderService.getCacheProfit(order.getUuid()); BigDecimal profit1 = cp != null ? cp.getProfit() : defaultZero(order.getProfit()); if (order_close_line_type == 1) { Wallet wallet = this.walletService.findByUserId(order.getPartyId().toString()); Integer decimal = itemService.getDecimal(order.getSymbol()); @@ -389,4 +342,36 @@ this.order_close_line_type = order_close_line_type; } private void applyMarkPriceToOrder(ContractOrder order, BigDecimal currentPrice) { BigDecimal priceChangeRatio = currentPrice.subtract(order.getTradeAvgPrice()) .divide(order.getTradeAvgPrice(), 6, RoundingMode.DOWN); BigDecimal margin = order.getTradeAvgPrice().multiply(order.getVolume()); BigDecimal profitAmount = margin.multiply(priceChangeRatio); BigDecimal priceProfit; if (ContractOrder.DIRECTION_BUY.equals(order.getDirection())) { priceProfit = profitAmount.setScale(6, RoundingMode.DOWN); } else { priceProfit = profitAmount.setScale(6, RoundingMode.DOWN).negate(); } BigDecimal fundingPnl = contractOrderService.calculateAccruedFundingPnl(order, currentPrice, new Date()); order.setProfit(priceProfit.add(fundingPnl)); order.setCloseAvgPrice(currentPrice); this.contractOrderService.updateByIdBuffer(order); } @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, BigDecimal.valueOf(list.get(0).getClose())); } } 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); }