From befbf57e4112d07003bff18102f556a1e5a154de Mon Sep 17 00:00:00 2001
From: zj <1772600164@qq.com>
Date: Wed, 22 Apr 2026 10:53:37 +0800
Subject: [PATCH] 1
---
trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java | 325 ++++++++++++++++++++++++++++++++++++++---------------
1 files changed, 233 insertions(+), 92 deletions(-)
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java b/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java
index 4ccdca1..cea79b3 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java
+++ b/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java
@@ -16,10 +16,10 @@
import com.yami.trading.bean.contract.domain.ContractOrder;
import com.yami.trading.bean.contract.domain.ContractOrderProfit;
import com.yami.trading.bean.contract.dto.ContractOrderDTO;
+import com.yami.trading.bean.contract.dto.TraderOwnClosedAggDTO;
import com.yami.trading.bean.contract.query.ContractOrderQuery;
import com.yami.trading.bean.data.domain.Realtime;
import com.yami.trading.bean.item.domain.Item;
-import com.yami.trading.bean.model.FollowWallet;
import com.yami.trading.bean.model.User;
import com.yami.trading.bean.model.Wallet;
import com.yami.trading.bean.syspara.domain.Syspara;
@@ -31,12 +31,12 @@
import com.yami.trading.common.constants.ContractRedisKeys;
import com.yami.trading.common.constants.RedisKeys;
import com.yami.trading.common.constants.TipConstants;
+import com.yami.trading.common.util.Arith;
import com.yami.trading.common.util.DateUtils;
import com.yami.trading.common.util.RandomUtil;
import com.yami.trading.common.util.RedisUtil;
import com.yami.trading.common.util.StringUtils;
import com.yami.trading.dao.contract.ContractOrderMapper;
-import com.yami.trading.service.FollowWalletService;
import com.yami.trading.service.WalletService;
import com.yami.trading.service.item.ItemService;
import com.yami.trading.service.syspara.SysparaService;
@@ -63,9 +63,13 @@
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.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@@ -74,7 +78,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 +91,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);
@@ -136,10 +145,6 @@
private WalletService walletService;
@Autowired
- private FollowWalletService followWalletService;
-
-
- @Autowired
private UserService userService;
@Autowired
private TipService tipService;
@@ -163,6 +168,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) {
@@ -228,6 +237,89 @@
queryWrapper.orderByDesc("create_time");
return list(queryWrapper);
}
+
+ /**
+ * 仅查询交易员本人持仓(排除其作为跟单用户产生的跟单订单)。
+ */
+ public List<ContractOrder> findSubmittedTraderOwn(String partyId, String symbol) {
+ QueryWrapper<ContractOrder> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq(StrUtil.isNotBlank(partyId), "party_id", partyId);
+ queryWrapper.eq(StrUtil.isNotBlank(symbol), "symbol", symbol);
+ queryWrapper.eq("state", ContractOrder.STATE_SUBMITTED);
+ // 交易员本人持仓:排除明确的跟单单(follow=1),并兼容历史数据 follow 为空
+ queryWrapper.and(w -> w.ne("follow", ContractOrder.ORDER_FOLLOW).or().isNull("follow"));
+ queryWrapper.orderByDesc("create_time");
+ return list(queryWrapper);
+ }
+
+ /**
+ * 交易员本人、已平仓、非跟单单:全品种已实现盈亏合计(与 T_TRADER.profit 相比以合约表为准)。
+ */
+ public BigDecimal sumClosedProfitTraderOwn(String partyId) {
+ if (StrUtil.isBlank(partyId)) {
+ return BigDecimal.ZERO;
+ }
+ Map<String, TraderOwnClosedAggDTO> m = mapClosedTraderOwnAggByPartyIds(Collections.singletonList(partyId));
+ TraderOwnClosedAggDTO row = m.get(partyId);
+ if (row == null || row.getClosedProfitSum() == null) {
+ return BigDecimal.ZERO;
+ }
+ return row.getClosedProfitSum();
+ }
+
+ /**
+ * 交易员本人、已平仓、非跟单单:全品种开仓保证金 deposit_open 合计(与当前持仓 deposit 同一口径,用于收益率分母)。
+ */
+ public BigDecimal sumClosedDepositOpenTraderOwn(String partyId) {
+ if (StrUtil.isBlank(partyId)) {
+ return BigDecimal.ZERO;
+ }
+ Map<String, TraderOwnClosedAggDTO> m = mapClosedTraderOwnAggByPartyIds(Collections.singletonList(partyId));
+ TraderOwnClosedAggDTO row = m.get(partyId);
+ if (row == null || row.getClosedMarginSum() == null) {
+ return BigDecimal.ZERO;
+ }
+ return row.getClosedMarginSum();
+ }
+
+ public Map<String, TraderOwnClosedAggDTO> mapClosedTraderOwnAggByPartyIds(List<String> partyIds) {
+ if (partyIds == null || partyIds.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ List<String> distinct = partyIds.stream().filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
+ if (distinct.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ List<TraderOwnClosedAggDTO> rows = getBaseMapper().sumClosedTraderOwnAggByPartyIds(distinct);
+ if (rows == null || rows.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ return rows.stream()
+ .filter(r -> r != null && StrUtil.isNotBlank(r.getPartyId()))
+ .collect(Collectors.toMap(TraderOwnClosedAggDTO::getPartyId, r -> r, (a, b) -> a));
+ }
+
+ /**
+ * 累计收益/收益率:历史已实现盈亏(合约表全品种)+ 偏差;历史分母优先用已平仓保证金合计(与当前持仓保证金一致),否则回退 T_TRADER.order_amount。
+ */
+ public double historyProfitForTraderTotalYield(Trader entity) {
+ if (entity == null || StrUtil.isBlank(entity.getPartyId())) {
+ return 0D;
+ }
+ double closed = sumClosedProfitTraderOwn(entity.getPartyId()).doubleValue();
+ return Arith.add(closed, entity.getDeviationProfit());
+ }
+
+ public double historyAmountBasisForTraderTotalYield(Trader entity) {
+ if (entity == null) {
+ return 0D;
+ }
+ BigDecimal marginSum = sumClosedDepositOpenTraderOwn(entity.getPartyId());
+ if (marginSum != null && marginSum.compareTo(BigDecimal.ZERO) > 0) {
+ return Arith.add(marginSum.doubleValue(), entity.getDeviationOrderAmount());
+ }
+ return Arith.add(entity.getOrderAmount(), entity.getDeviationOrderAmount());
+ }
public List<ContractOrder> findSubmitted(String partyId, String symbol, String direction, String startTime, String endTime, String symbolType) {
@@ -375,42 +467,38 @@
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()) { // 跟单订单
-// if (profit > 0) {
- FollowWallet wallet = followWalletService.findByUserId(order.getPartyId());
- BigDecimal amount_before = wallet.getMoney();
-
-// wallet.setMoney(Arith.add(wallet.getMoney(), profit));
-
- if (wallet.getMoney().add(profit).compareTo(BigDecimal.ZERO) < 0) {
- profit = wallet.getMoney().negate();
- }
-
- followWalletService.updateMoney(order.getSymbol(), partyId, profit, BigDecimal.ZERO,
- Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_CLOSE, "平仓,平仓合约数[" + volume + "],订单号[" + order.getOrderNo() + "]");
- } else {
- // if (profit > 0) {
- Wallet wallet = walletService.findByUserId(order.getPartyId());
- BigDecimal amount_before = wallet.getMoney();
-
-// wallet.setMoney(Arith.add(wallet.getMoney(), profit));
- if (wallet.getMoney().add(profit).compareTo(BigDecimal.ZERO) < 0) {
- profit = wallet.getMoney().negate();
- }
- walletService.updateMoney(order.getSymbol(), partyId, profit, BigDecimal.ZERO,
- Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_CLOSE, "平仓,平仓合约数[" + volume + "],订单号[" + order.getOrderNo() + "]");
+ Wallet wallet = walletService.findByUserId(order.getPartyId());
+ if (wallet.getMoney().add(profit).compareTo(BigDecimal.ZERO) < 0) {
+ profit = wallet.getMoney().negate();
}
+ walletService.updateMoney(order.getSymbol(), partyId, profit, BigDecimal.ZERO,
+ Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_CLOSE, "平仓,平仓合约数[" + volume + "],订单号[" + order.getOrderNo() + "]");
// List<Realtime> list = this.dataService.realtime(order.getSymbol());
// // 平仓时候把当前价格先更新回去
// if (list.size() != 0) {
@@ -423,6 +511,10 @@
}
update(order);
+
+ if (ContractOrder.STATE_CREATED.equals(order.getState())) {
+ traderFollowUserOrderService.syncFollowUserOrderLinkAfterContractClose(order);
+ }
/**
* 交易员带单,用户跟单
@@ -649,29 +741,7 @@
BigDecimal realizedProfit = originProfit.multiply(closeRatio).setScale(8, RoundingMode.HALF_UP);
BigDecimal profit = releasedDeposit.add(realizedProfit);
- if (ContractOrder.ORDER_FOLLOW == order.getFollow()) { // 跟单还得减去利息收益
- BigDecimal orderAmount = order.getUnitAmount().multiply(order.getTradeAvgPrice()).multiply(order.getLeverRate()); //订单总金额
- TraderFollowUserOrder traderFollowUserOrder = traderFollowUserOrderService.findByPartyIdAndOrderNo(order.getPartyId(), order.getOrderNo());
- if (null != traderFollowUserOrder) {
- TraderFollowUser traderFollowUser = traderFollowUserService.findByPartyIdAndTrader_partyId(order.getPartyId(), traderFollowUserOrder.getTraderPartyId());
- if (StringUtils.isNotEmpty(traderFollowUser.getDaysSetting())) {
- TraderDaysSetting traderDaysSetting = traderDaysSettingService.selectById(traderFollowUser.getDaysSetting());
- if (null != traderDaysSetting) { // 借款利率
- int days = 0;
- try {
- days = daysBetween(order.getCreateTime(), new Date());
- } catch (ParseException e) {
-// throw new RuntimeException(e);
- log.error(e.getMessage());
- }
- if (days < 0) {
- days = 0;
- }
- }
- }
- }
-
- }
+ // 跟单利息功能已下线,此处不再做历史利率扣减
order.setAmountClose(order.getAmountClose().add(profit));
BigDecimal remainVolume = currentVolume.subtract(volume);
@@ -696,37 +766,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 +1055,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 +1098,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);
}
@@ -999,6 +1136,10 @@
return data;
}
+ public List<Map<String, Object>> buildDataFromOrders(List<ContractOrder> list) {
+ return bulidData(list);
+ }
+
public Map<String, Object> bulidOne(ContractOrder order) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("order_no", order.getOrderNo());
--
Gitblit v1.9.3