trading-order-admin/src/main/java/com/yami/trading/admin/controller/data/AdminMarketQuotationsManageController.java
@@ -99,7 +99,11 @@ // 包含在列表中 return Result.failed("该币种不支持调整!"); } marketQuotationsFacade.adjust(adminMarketQuotationsUpdateDto.getSymbol(), adminMarketQuotationsUpdateDto.getSecond(), adminMarketQuotationsUpdateDto.getValue()); 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) {