|
@@ -0,0 +1,230 @@
|
|
|
|
|
+package com.datacenter.service.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
|
|
+import com.datacenter.entity.stat.StatContractPnl;
|
|
|
|
|
+import com.datacenter.mapper.StatContractPnlMapper;
|
|
|
|
|
+import com.datacenter.service.StatContractPnlService;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.core.ParameterizedTypeReference;
|
|
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
|
|
+import org.springframework.http.HttpMethod;
|
|
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.math.RoundingMode;
|
|
|
|
|
+import java.time.LocalDate;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.LocalTime;
|
|
|
|
|
+import java.time.ZoneId;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+public class StatContractPnlServiceImpl
|
|
|
|
|
+ extends ServiceImpl<StatContractPnlMapper, StatContractPnl>
|
|
|
|
|
+ implements StatContractPnlService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final int SELF = 0; // 自主合约:follow_id IS NULL
|
|
|
|
|
+ private static final int COPY = 1; // 跟单合约:follow_id IS NOT NULL
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private RestTemplate restTemplate;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${admin.host}")
|
|
|
|
|
+ private String adminHost;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void computeAndSaveBoth(LocalDate date) {
|
|
|
|
|
+ computeAndSave(date, false);
|
|
|
|
|
+ computeAndSave(date, true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public StatContractPnl computeAndSave(LocalDate date, boolean includeInternal) {
|
|
|
|
|
+ int ii = includeInternal ? 1 : 0;
|
|
|
|
|
+ log.info("[ContractPnl] 开始统计 date={} includeInternal={}", date, includeInternal);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, BigDecimal> prices = fetchPrices();
|
|
|
|
|
+ if (prices.isEmpty()) {
|
|
|
|
|
+ log.warn("[ContractPnl] 无法获取实时价格,持仓盈亏将跳过本次计算");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ LocalDateTime dayStart = LocalDateTime.of(date, LocalTime.MIDNIGHT);
|
|
|
|
|
+ LocalDateTime dayEnd = dayStart.plusDays(1);
|
|
|
|
|
+ long startMs = dayStart.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
|
|
|
|
+ long endMs = dayEnd.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
|
|
|
|
+
|
|
|
|
|
+ // ── 自主合约 ────────────────────────────────────────────────────────
|
|
|
|
|
+ BigDecimal[] selfHold = sumHoldPnl(baseMapper.listOpenPositions(SELF, ii), prices);
|
|
|
|
|
+ BigDecimal selfHoldProfit = selfHold[0];
|
|
|
|
|
+ BigDecimal selfHoldLoss = selfHold[1];
|
|
|
|
|
+ BigDecimal selfTodayCloseProfit = baseMapper.sumTodayCloseProfit(SELF, startMs, endMs, ii);
|
|
|
|
|
+ BigDecimal selfTodayCloseLoss = baseMapper.sumTodayCloseLoss(SELF, startMs, endMs, ii);
|
|
|
|
|
+ BigDecimal selfTotalCloseProfit = baseMapper.sumTotalCloseProfit(SELF, ii);
|
|
|
|
|
+ BigDecimal selfTotalCloseLoss = baseMapper.sumTotalCloseLoss(SELF, ii);
|
|
|
|
|
+
|
|
|
|
|
+ // ── 跟单合约 ────────────────────────────────────────────────────────
|
|
|
|
|
+ BigDecimal[] copyHold = sumHoldPnl(baseMapper.listOpenPositions(COPY, ii), prices);
|
|
|
|
|
+ BigDecimal copyHoldProfit = copyHold[0];
|
|
|
|
|
+ BigDecimal copyHoldLoss = copyHold[1];
|
|
|
|
|
+ BigDecimal copyTodayCloseProfit = baseMapper.sumTodayCloseProfit(COPY, startMs, endMs, ii);
|
|
|
|
|
+ BigDecimal copyTodayCloseLoss = baseMapper.sumTodayCloseLoss(COPY, startMs, endMs, ii);
|
|
|
|
|
+ BigDecimal copyTotalCloseProfit = baseMapper.sumTotalCloseProfit(COPY, ii);
|
|
|
|
|
+ BigDecimal copyTotalCloseLoss = baseMapper.sumTotalCloseLoss(COPY, ii);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("[ContractPnl] date={} ii={} " +
|
|
|
|
|
+ "self: holdP={} holdL={} todayP={} todayL={} totalP={} totalL={} | " +
|
|
|
|
|
+ "copy: holdP={} holdL={} todayP={} todayL={} totalP={} totalL={}",
|
|
|
|
|
+ date, ii,
|
|
|
|
|
+ selfHoldProfit, selfHoldLoss, selfTodayCloseProfit, selfTodayCloseLoss,
|
|
|
|
|
+ selfTotalCloseProfit, selfTotalCloseLoss,
|
|
|
|
|
+ copyHoldProfit, copyHoldLoss, copyTodayCloseProfit, copyTodayCloseLoss,
|
|
|
|
|
+ copyTotalCloseProfit, copyTotalCloseLoss);
|
|
|
|
|
+
|
|
|
|
|
+ StatContractPnl entity = getByDate(date, includeInternal);
|
|
|
|
|
+ if (entity == null) {
|
|
|
|
|
+ entity = new StatContractPnl();
|
|
|
|
|
+ entity.setStatDate(date);
|
|
|
|
|
+ entity.setIncludeInternal(ii);
|
|
|
|
|
+ }
|
|
|
|
|
+ entity.setSelfHoldProfit(selfHoldProfit);
|
|
|
|
|
+ entity.setSelfHoldLoss(selfHoldLoss);
|
|
|
|
|
+ entity.setSelfTodayCloseProfit(selfTodayCloseProfit);
|
|
|
|
|
+ entity.setSelfTodayCloseLoss(selfTodayCloseLoss);
|
|
|
|
|
+ entity.setSelfTotalCloseProfit(selfTotalCloseProfit);
|
|
|
|
|
+ entity.setSelfTotalCloseLoss(selfTotalCloseLoss);
|
|
|
|
|
+ entity.setCopyHoldProfit(copyHoldProfit);
|
|
|
|
|
+ entity.setCopyHoldLoss(copyHoldLoss);
|
|
|
|
|
+ entity.setCopyTodayCloseProfit(copyTodayCloseProfit);
|
|
|
|
|
+ entity.setCopyTodayCloseLoss(copyTodayCloseLoss);
|
|
|
|
|
+ entity.setCopyTotalCloseProfit(copyTotalCloseProfit);
|
|
|
|
|
+ entity.setCopyTotalCloseLoss(copyTotalCloseLoss);
|
|
|
|
|
+ entity.setUpdatedTime(LocalDateTime.now());
|
|
|
|
|
+
|
|
|
|
|
+ saveOrUpdate(entity);
|
|
|
|
|
+ log.info("[ContractPnl] 写入完成 id={}", entity.getId());
|
|
|
|
|
+ return entity;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void backfill(int days) {
|
|
|
|
|
+ if (days < 1) days = 1;
|
|
|
|
|
+ if (days > 60) days = 60;
|
|
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
|
|
+ for (int i = days; i >= 1; i--) {
|
|
|
|
|
+ LocalDate date = today.minusDays(i);
|
|
|
|
|
+ log.info("[ContractPnl] 回填历史统计 date={}", date);
|
|
|
|
|
+ try {
|
|
|
|
|
+ computeAndSaveBoth(date);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[ContractPnl] 回填失败 date={}", date, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("[ContractPnl] 回填完成,共 {} 天", days);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public StatContractPnl getByDate(LocalDate date, boolean includeInternal) {
|
|
|
|
|
+ return getOne(new LambdaQueryWrapper<StatContractPnl>()
|
|
|
|
|
+ .eq(StatContractPnl::getStatDate, date)
|
|
|
|
|
+ .eq(StatContractPnl::getIncludeInternal, includeInternal ? 1 : 0));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从 Admin 代理接口拉取实时价格,格式:symbol → price。
|
|
|
|
|
+ * 兼容 "ETH/USDT" 和 "ETHUSDT" 两种 key 格式(各存一份)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<String, BigDecimal> fetchPrices() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ String url = adminHost + "/admin/noauth/swap-prices";
|
|
|
|
|
+ ParameterizedTypeReference<Map<String, BigDecimal>> typeRef =
|
|
|
|
|
+ new ParameterizedTypeReference<Map<String, BigDecimal>>() {};
|
|
|
|
|
+ ResponseEntity<Map<String, BigDecimal>> resp =
|
|
|
|
|
+ restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(null), typeRef);
|
|
|
|
|
+ if (resp.getBody() != null && !resp.getBody().isEmpty()) {
|
|
|
|
|
+ Map<String, BigDecimal> result = new java.util.HashMap<>(resp.getBody());
|
|
|
|
|
+ // 同时写入无斜杠格式,方便两种 symbol 风格都能命中
|
|
|
|
|
+ resp.getBody().forEach((k, v) -> result.put(k.replace("/", ""), v));
|
|
|
|
|
+ log.info("[ContractPnl] 获取到 {} 个交易对价格", resp.getBody().size());
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("[ContractPnl] 获取实时价格失败: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ return Collections.emptyMap();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 用实时价格计算一组持仓的浮盈/浮亏合计。
|
|
|
|
|
+ * 先按 member_id 汇总每人净盈亏,再分桶:净盈→profit,净亏→loss。
|
|
|
|
|
+ * 与仓位管理页面保持一致(每人一行净值,不分正负分别累加)。
|
|
|
|
|
+ * 返回 [holdProfit, holdLoss],均为正数。
|
|
|
|
|
+ */
|
|
|
|
|
+ private BigDecimal[] sumHoldPnl(List<Map<String, Object>> positions, Map<String, BigDecimal> prices) {
|
|
|
|
|
+ log.info("[ContractPnl] sumHoldPnl 持仓数量={}", positions.size());
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 计算每条持仓的 pnl,按 member_id 累加
|
|
|
|
|
+ Map<Long, BigDecimal> memberNetMap = new java.util.LinkedHashMap<>();
|
|
|
|
|
+ for (Map<String, Object> row : positions) {
|
|
|
|
|
+ Long memberId = ((Number) row.get("member_id")).longValue();
|
|
|
|
|
+ String symbol = (String) row.get("symbol");
|
|
|
|
|
+ int direction = ((Number) row.get("direction")).intValue();
|
|
|
|
|
+ int moneyType = ((Number) row.get("money_type")).intValue();
|
|
|
|
|
+ BigDecimal openP = toBD(row.get("open_price"));
|
|
|
|
|
+ BigDecimal qty = toBD(row.get("avail_qty"));
|
|
|
|
|
+ BigDecimal share = toBD(row.get("share_number"));
|
|
|
|
|
+
|
|
|
|
|
+ BigDecimal curP = prices.getOrDefault(symbol,
|
|
|
|
|
+ prices.getOrDefault(symbol.replace("/", ""), BigDecimal.ZERO));
|
|
|
|
|
+ if (curP.compareTo(BigDecimal.ZERO) <= 0 || openP.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
|
|
+ log.info("[ContractPnl] 跳过 member={} symbol={} dir={} mt={} openP={} curP={} qty={}",
|
|
|
|
|
+ memberId, symbol, direction, moneyType, openP, curP, qty);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ BigDecimal priceDiff = direction == 0
|
|
|
|
|
+ ? curP.subtract(openP)
|
|
|
|
|
+ : openP.subtract(curP);
|
|
|
|
|
+
|
|
|
|
|
+ BigDecimal pnl;
|
|
|
|
|
+ if (moneyType == 0) {
|
|
|
|
|
+ pnl = priceDiff.multiply(qty).multiply(share)
|
|
|
|
|
+ .divide(openP, 8, RoundingMode.HALF_UP);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ pnl = priceDiff.multiply(qty).setScale(8, RoundingMode.HALF_UP);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.info("[ContractPnl] member={} symbol={} dir={} mt={} openP={} curP={} qty={} share={} pnl={}",
|
|
|
|
|
+ memberId, symbol, direction, moneyType, openP, curP, qty, share, pnl);
|
|
|
|
|
+
|
|
|
|
|
+ memberNetMap.merge(memberId, pnl, BigDecimal::add);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 按每人净值分桶(与仓位管理页一致:整人净盈亏,不拆分正负持仓)
|
|
|
|
|
+ BigDecimal profit = BigDecimal.ZERO;
|
|
|
|
|
+ BigDecimal loss = BigDecimal.ZERO;
|
|
|
|
|
+ for (Map.Entry<Long, BigDecimal> entry : memberNetMap.entrySet()) {
|
|
|
|
|
+ BigDecimal net = entry.getValue();
|
|
|
|
|
+ log.info("[ContractPnl] member={} net={}", entry.getKey(), net);
|
|
|
|
|
+ if (net.compareTo(BigDecimal.ZERO) > 0) {
|
|
|
|
|
+ profit = profit.add(net);
|
|
|
|
|
+ } else if (net.compareTo(BigDecimal.ZERO) < 0) {
|
|
|
|
|
+ loss = loss.add(net.abs());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return new BigDecimal[]{profit, loss};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private BigDecimal toBD(Object v) {
|
|
|
|
|
+ if (v == null) return BigDecimal.ZERO;
|
|
|
|
|
+ if (v instanceof BigDecimal) return (BigDecimal) v;
|
|
|
|
|
+ return new BigDecimal(v.toString());
|
|
|
|
|
+ }
|
|
|
|
|
+}
|