Просмотр исходного кода

基础持仓分析接口迁移

wangzaijun 1 год назад
Родитель
Сommit
c7f2332434
100 измененных файлов с 6139 добавлено и 111 удалено
  1. 5 0
      pom.xml
  2. 1 1
      readme.md
  3. 0 30
      src/main/java/com/smppw/analysis/application/dto/HeadIndicatorVO.java
  4. 41 0
      src/main/java/com/smppw/analysis/application/dto/performance/BaseMultiParams.java
  5. 3 13
      src/main/java/com/smppw/analysis/application/dto/BaseParams.java
  6. 1 1
      src/main/java/com/smppw/analysis/application/dto/HeadIndicatorParams.java
  7. 1 1
      src/main/java/com/smppw/analysis/application/dto/IndicatorParams.java
  8. 33 0
      src/main/java/com/smppw/analysis/application/dto/performance/Params.java
  9. 1 1
      src/main/java/com/smppw/analysis/application/dto/TrendParams.java
  10. 38 0
      src/main/java/com/smppw/analysis/application/dto/position/AssetCategoryEnum.java
  11. 18 0
      src/main/java/com/smppw/analysis/application/dto/position/BaseAnalysisParams.java
  12. 26 0
      src/main/java/com/smppw/analysis/application/dto/position/BaseParams.java
  13. 37 0
      src/main/java/com/smppw/analysis/application/dto/position/CategoryConstraint.java
  14. 31 0
      src/main/java/com/smppw/analysis/application/dto/position/CompositionCategory.java
  15. 36 0
      src/main/java/com/smppw/analysis/application/dto/position/FundPositionBaseInfo.java
  16. 230 0
      src/main/java/com/smppw/analysis/application/dto/position/FundPositionDetail.java
  17. 53 0
      src/main/java/com/smppw/analysis/application/dto/position/HoldingType.java
  18. 29 0
      src/main/java/com/smppw/analysis/application/dto/position/MarketValueRatio.java
  19. 69 0
      src/main/java/com/smppw/analysis/application/dto/position/PositionConstants.java
  20. 26 0
      src/main/java/com/smppw/analysis/application/dto/position/PositionLiquidityEnum.java
  21. 59 0
      src/main/java/com/smppw/analysis/application/dto/position/PositionStyleEnum.java
  22. 52 0
      src/main/java/com/smppw/analysis/application/dto/position/RefMarketValueRatio.java
  23. 31 0
      src/main/java/com/smppw/analysis/application/dto/position/RiskExposureEnum.java
  24. 15 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/BondAssetAllocationParams.java
  25. 26 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/BondAssetAllocationVO.java
  26. 23 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/BondPerformanceAttributionParams.java
  27. 73 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/BondPerformanceAttributionVO.java
  28. 11 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/BondSortAllocationParam.java
  29. 58 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/BondSortAssetDTO.java
  30. 48 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/ConcentrationBondVO.java
  31. 48 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/CreditGradingBondVO.java
  32. 15 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/DurationAnalysisParams.java
  33. 46 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/DurationAnalysisVO.java
  34. 15 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/ProfitRiskParams.java
  35. 56 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/ProfitRiskVO.java
  36. 130 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/RefBondMarketValueRatio.java
  37. 195 0
      src/main/java/com/smppw/analysis/application/dto/position/bond/RefCreditMarketValueRatio.java
  38. 35 0
      src/main/java/com/smppw/analysis/application/dto/position/future/FutureDailyRet.java
  39. 26 0
      src/main/java/com/smppw/analysis/application/dto/position/future/MarginalRiskContribution.java
  40. 15 0
      src/main/java/com/smppw/analysis/application/dto/position/future/MarginalRiskContributionParams.java
  41. 24 0
      src/main/java/com/smppw/analysis/application/dto/position/future/MarginalRiskContributionVO.java
  42. 23 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/BarraSensitivityParams.java
  43. 82 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/BarraSensitivityVO.java
  44. 15 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/ChangeNumberParams.java
  45. 15 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/ChangeNumberVO.java
  46. 15 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/ConcentrationParams.java
  47. 50 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/ConcentrationVO.java
  48. 35 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/IndustryAllocationPreferenceVO.java
  49. 17 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/MajorChangeParams.java
  50. 42 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/MajorChangeVO.java
  51. 15 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/RiskExposureParams.java
  52. 26 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/RiskExposureVO.java
  53. 20 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/StockAllocationParams.java
  54. 20 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/StockAllocationVO.java
  55. 19 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/StockPerformanceAttributionParams.java
  56. 63 0
      src/main/java/com/smppw/analysis/application/dto/position/stock/StockPerformanceAttributionVO.java
  57. 19 0
      src/main/java/com/smppw/analysis/application/dto/position/synthesize/AssetAllocationParams.java
  58. 34 0
      src/main/java/com/smppw/analysis/application/dto/position/synthesize/AssetAllocationVO.java
  59. 15 0
      src/main/java/com/smppw/analysis/application/dto/position/synthesize/HolderInfoParams.java
  60. 54 0
      src/main/java/com/smppw/analysis/application/dto/position/synthesize/HolderInfoVO.java
  61. 15 0
      src/main/java/com/smppw/analysis/application/dto/position/synthesize/LeverageChangeParams.java
  62. 30 0
      src/main/java/com/smppw/analysis/application/dto/position/synthesize/LeverageChangeVO.java
  63. 19 0
      src/main/java/com/smppw/analysis/application/dto/position/synthesize/PositionInfoParams.java
  64. 25 0
      src/main/java/com/smppw/analysis/application/dto/position/synthesize/PositionInfoVO.java
  65. 42 0
      src/main/java/com/smppw/analysis/application/dto/position/synthesize/PositionListParams.java
  66. 57 0
      src/main/java/com/smppw/analysis/application/dto/position/synthesize/PositionListVO.java
  67. 304 0
      src/main/java/com/smppw/analysis/application/service/StockPositionAnalysis.java
  68. 127 0
      src/main/java/com/smppw/analysis/application/service/SynthesizePositionAnalysis.java
  69. 11 12
      src/main/java/com/smppw/analysis/application/service/CommonService.java
  70. 54 42
      src/main/java/com/smppw/analysis/application/service/PerformanceService.java
  71. 216 0
      src/main/java/com/smppw/analysis/application/service/position/AbstractAnalysisBizHandler.java
  72. 189 0
      src/main/java/com/smppw/analysis/application/service/position/AbstractBizHandler.java
  73. 73 0
      src/main/java/com/smppw/analysis/application/service/position/AbstractNonSynthesizeBizHandler.java
  74. 10 0
      src/main/java/com/smppw/analysis/application/service/position/AbstractPositionLoad.java
  75. 21 0
      src/main/java/com/smppw/analysis/application/service/position/BizHandler.java
  76. 26 0
      src/main/java/com/smppw/analysis/application/service/position/BizHandlerConstants.java
  77. 35 0
      src/main/java/com/smppw/analysis/application/service/position/BizHandlerFactor.java
  78. 32 0
      src/main/java/com/smppw/analysis/application/service/position/PositionLoad.java
  79. 59 0
      src/main/java/com/smppw/analysis/application/service/position/PositionLoadFactory.java
  80. 30 0
      src/main/java/com/smppw/analysis/application/service/position/PrivatePositionLoad.java
  81. 126 0
      src/main/java/com/smppw/analysis/application/service/position/PrivatelyFundPositionLoad.java
  82. 92 0
      src/main/java/com/smppw/analysis/application/service/position/PubliclyFundPositionLoad.java
  83. 584 0
      src/main/java/com/smppw/analysis/application/service/position/bond/BondPositionService.java
  84. 75 0
      src/main/java/com/smppw/analysis/application/service/position/bond/ConcentrationBizBondHandler.java
  85. 289 0
      src/main/java/com/smppw/analysis/application/service/position/future/MarginalRiskContributionBizHandler.java
  86. 70 0
      src/main/java/com/smppw/analysis/application/service/position/stock/BarraSensitivityComponent.java
  87. 50 0
      src/main/java/com/smppw/analysis/application/service/position/stock/ChangeNumberBizHandler.java
  88. 71 0
      src/main/java/com/smppw/analysis/application/service/position/stock/ConcentrationBizHandler.java
  89. 67 0
      src/main/java/com/smppw/analysis/application/service/position/stock/IndustryAllocationBizHandler.java
  90. 80 0
      src/main/java/com/smppw/analysis/application/service/position/stock/IndustryAllocationPreferenceComponent.java
  91. 166 0
      src/main/java/com/smppw/analysis/application/service/position/stock/NewLiquidityAllocationBizHandler.java
  92. 134 0
      src/main/java/com/smppw/analysis/application/service/position/stock/RiskExposureBizHandler.java
  93. 247 0
      src/main/java/com/smppw/analysis/application/service/position/stock/StockPerformanceAttributionBizHandler.java
  94. 102 0
      src/main/java/com/smppw/analysis/application/service/position/stock/StyleAllocationBizHandler.java
  95. 92 0
      src/main/java/com/smppw/analysis/application/service/position/synthesize/AssetAllocationBizHandler.java
  96. 44 0
      src/main/java/com/smppw/analysis/application/service/position/synthesize/LeverageChangeBizHandler.java
  97. 152 0
      src/main/java/com/smppw/analysis/application/service/position/synthesize/PositionListBizHandler.java
  98. 55 0
      src/main/java/com/smppw/analysis/application/service/position/synthesize/PositionParamsBizHandler.java
  99. 9 10
      src/main/java/com/smppw/analysis/client/FundApi.java
  100. 0 0
      src/main/java/com/smppw/analysis/client/FundPositionApi.java

+ 5 - 0
pom.xml

@@ -59,6 +59,11 @@
             <version>${hutool-version}</version>
         </dependency>
         <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-http</artifactId>
+            <version>${hutool-version}</version>
+        </dependency>
+        <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
             <version>2.8.8</version>

+ 1 - 1
readme.md

@@ -1,5 +1,5 @@
 # 数据分析-详情页通用服务 (开发前必读)
-springboot3+redis+mybatis的一个标的详情页通用服务
+springboot3+redis+mybatis的一个标的详情页通用服务,~~DDD架构没使用到位,复杂性太高了~~
 
 **所有来到这里的标的都是有效的!!!**
 

+ 0 - 30
src/main/java/com/smppw/analysis/application/dto/HeadIndicatorVO.java

@@ -1,30 +0,0 @@
-package com.smppw.analysis.application.dto;
-
-import lombok.Getter;
-import lombok.Setter;
-
-/**
- * @author wangzaijun
- * @date 2023/8/3 18:45
- * @description 头部成立以来指标
- */
-@Setter
-@Getter
-public class HeadIndicatorVO {
-    /**
-     * 成立以来年化收益率
-     */
-    private String annualReturn;
-    /**
-     * 成立以来年化波动率
-     */
-    private String annualStdDev;
-    /**
-     * 成立以来最大回撤
-     */
-    private String maxDrawdown;
-    /**
-     * 成立以来夏普
-     */
-    private String sharpeRatio;
-}

+ 41 - 0
src/main/java/com/smppw/analysis/application/dto/performance/BaseMultiParams.java

@@ -0,0 +1,41 @@
+package com.smppw.analysis.application.dto.performance;
+
+import com.smppw.common.pojo.enums.Frequency;
+import com.smppw.constants.Consts;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serial;
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/8/4 10:17
+ * @description 多标的的分析基础参数
+ */
+@Setter
+@Getter
+public abstract class BaseMultiParams implements Params {
+    @Serial
+    private static final long serialVersionUID = Consts.DEFAULT_SERIAL_VERSION_UID;
+    /**
+     * 标的id,包括基金、机构和经理
+     */
+    private List<String> refIds;
+    /**
+     * 基准
+     */
+    private String benchmarkId;
+    /**
+     * 频率
+     */
+    private Frequency frequency;
+    /**
+     * 开始日期
+     */
+    private String startDate;
+    /**
+     * 结束日期
+     */
+    private String endDate;
+}

+ 3 - 13
src/main/java/com/smppw/analysis/application/dto/BaseParams.java

@@ -1,4 +1,4 @@
-package com.smppw.analysis.application.dto;
+package com.smppw.analysis.application.dto.performance;
 
 import com.smppw.common.pojo.enums.Frequency;
 import com.smppw.common.pojo.enums.NavType;
@@ -9,7 +9,6 @@ import lombok.Getter;
 import lombok.Setter;
 
 import java.io.Serial;
-import java.io.Serializable;
 import java.util.List;
 
 /**
@@ -19,14 +18,13 @@ import java.util.List;
  */
 @Setter
 @Getter
-public abstract class BaseParams implements Serializable {
+public abstract class BaseParams implements Params {
     @Serial
     private static final long serialVersionUID = Consts.DEFAULT_SERIAL_VERSION_UID;
-
     /**
      * 标的id,包括基金、机构和经理
      */
-    private List<String> fundId;
+    private List<String> refIds;
     /**
      * 基准
      */
@@ -63,12 +61,4 @@ public abstract class BaseParams implements Serializable {
      * 结束日期
      */
     private String endDate;
-    /**
-     * 是否多标的,根据传参判断
-     *
-     * @return /
-     */
-    public boolean multiSec() {
-        return this.fundId.size() > 1;
-    }
 }

+ 1 - 1
src/main/java/com/smppw/analysis/application/dto/HeadIndicatorParams.java

@@ -1,4 +1,4 @@
-package com.smppw.analysis.application.dto;
+package com.smppw.analysis.application.dto.performance;
 
 import com.smppw.common.pojo.IStrategy;
 import com.smppw.common.pojo.enums.Frequency;

+ 1 - 1
src/main/java/com/smppw/analysis/application/dto/IndicatorParams.java

@@ -1,4 +1,4 @@
-package com.smppw.analysis.application.dto;
+package com.smppw.analysis.application.dto.performance;
 
 import lombok.Getter;
 import lombok.Setter;

+ 33 - 0
src/main/java/com/smppw/analysis/application/dto/performance/Params.java

@@ -0,0 +1,33 @@
+package com.smppw.analysis.application.dto.performance;
+
+import cn.hutool.core.collection.CollUtil;
+import com.smppw.common.pojo.enums.Frequency;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/8/4 10:13
+ * @description 基础参数
+ */
+public interface Params extends Serializable {
+    /**
+     * 单标的时传一个,多标的时多个
+     *
+     * @return /
+     */
+    List<String> getRefIds();
+
+    String getBenchmarkId();
+
+    Frequency getFrequency();
+
+    String getStartDate();
+
+    String getEndDate();
+
+    default boolean multiSec() {
+        return CollUtil.isNotEmpty(this.getRefIds()) && this.getRefIds().size() > 1;
+    }
+}

+ 1 - 1
src/main/java/com/smppw/analysis/application/dto/TrendParams.java

@@ -1,4 +1,4 @@
-package com.smppw.analysis.application.dto;
+package com.smppw.analysis.application.dto.performance;
 
 import lombok.Getter;
 import lombok.Setter;

+ 38 - 0
src/main/java/com/smppw/analysis/application/dto/position/AssetCategoryEnum.java

@@ -0,0 +1,38 @@
+package com.smppw.analysis.application.dto.position;
+
+import com.smppw.common.pojo.ValueLabelVO;
+
+import java.util.Arrays;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 13:52
+ * @description 资产大类枚举
+ */
+public enum AssetCategoryEnum {
+    STOCK("股票"),
+    BOND("债券"),
+    FUND("基金"),
+    FUTURE("期货及衍生品");
+
+    private final String name;
+
+    AssetCategoryEnum(String name) {
+        this.name = name;
+    }
+
+    public static AssetCategoryEnum getAssetCategory(String key) {
+        return Arrays.stream(AssetCategoryEnum.values()).filter(e -> key.equals(e.name())).findFirst().orElse(null);
+    }
+
+    public static ValueLabelVO buildValueLabelByAsset(AssetCategoryEnum categoryEnum) {
+        if (categoryEnum == null) {
+            return null;
+        }
+        return new ValueLabelVO(categoryEnum.name(), categoryEnum.getName());
+    }
+
+    public String getName() {
+        return name;
+    }
+}

+ 18 - 0
src/main/java/com/smppw/analysis/application/dto/position/BaseAnalysisParams.java

@@ -0,0 +1,18 @@
+package com.smppw.analysis.application.dto.position;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:46
+ * @description 基础参数
+ */
+@Setter
+@Getter
+public abstract class BaseAnalysisParams extends BaseParams {
+    /**
+     * 基准,只支持 沪深300、中证500、中证1000
+     */
+    private String benchmarkId;
+}

+ 26 - 0
src/main/java/com/smppw/analysis/application/dto/position/BaseParams.java

@@ -0,0 +1,26 @@
+package com.smppw.analysis.application.dto.position;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:46
+ * @description 基础参数
+ */
+@Setter
+@Getter
+public abstract class BaseParams {
+    /**
+     * 基金id
+     */
+    private String fundId;
+    /**
+     * 开始时间
+     */
+    private String startDate;
+    /**
+     * 结束时间
+     */
+    private String endDate;
+}

+ 37 - 0
src/main/java/com/smppw/analysis/application/dto/position/CategoryConstraint.java

@@ -0,0 +1,37 @@
+package com.smppw.analysis.application.dto.position;
+
+import com.smppw.common.pojo.ValueLabelVO;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 15:35
+ * @description 行业、风格或流动性 对应的多头、空头和多空数据对象
+ */
+@Setter
+@Getter
+public class CategoryConstraint {
+    /**
+     * 行业id或风格或流动性
+     */
+    private ValueLabelVO category;
+    /**
+     * 多头
+     */
+    private BigDecimal bull;
+    /**
+     * 空头
+     */
+    private BigDecimal bear;
+    /**
+     * 净比例
+     */
+    private BigDecimal pupil;
+    /**
+     * 基准同行业或同风格或同流动性权重之和
+     */
+    private BigDecimal benchmark;
+}

+ 31 - 0
src/main/java/com/smppw/analysis/application/dto/position/CompositionCategory.java

@@ -0,0 +1,31 @@
+package com.smppw.analysis.application.dto.position;
+
+import com.smppw.common.pojo.ValueLabelVO;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/9 17:46
+ * @description 持仓成分对应日期的行业、风格或流动性的id名称、以及权重
+ */
+@Setter
+@Getter
+@Builder
+public class CompositionCategory {
+    /**
+     * 成分流通代码
+     */
+    private String secCode;
+    /**
+     * 行业、风格或流动性唯一标识
+     */
+    private ValueLabelVO category;
+    /**
+     * 权重(占净值比)
+     */
+    private BigDecimal ratio;
+}

+ 36 - 0
src/main/java/com/smppw/analysis/application/dto/position/FundPositionBaseInfo.java

@@ -0,0 +1,36 @@
+package com.smppw.analysis.application.dto.position;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/7 16:29
+ * @description 基金在区间内的基本信息,包括估值日期、资产总值、净资产值
+ */
+@Setter
+@Getter
+public class FundPositionBaseInfo {
+    /**
+     * 基金id
+     */
+    private String fundId;
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 总资产
+     */
+    private BigDecimal totalAsset;
+    /**
+     * 基金净资产值
+     */
+    private BigDecimal assetNv;
+    /**
+     * 基金的估值增值
+     */
+    private BigDecimal increment;
+}

+ 230 - 0
src/main/java/com/smppw/analysis/application/dto/position/FundPositionDetail.java

@@ -0,0 +1,230 @@
+package com.smppw.analysis.application.dto.position;
+
+import com.smppw.common.pojo.ValueLabelVO;
+
+import java.math.BigDecimal;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/7 9:41
+ * @description 基金持仓信息,包含关键的基本数据
+ */
+public class FundPositionDetail {
+    /**
+     * 基金id
+     */
+    private String fundId;
+    /**
+     * 基金类型
+     */
+    private String fundType;
+    /**
+     * 私募基金估值表科目代码,用来识别子类或区分子类
+     */
+    private String subjectCode;
+    /**
+     * 私募基金估值表科目级别
+     */
+    private Integer level;
+    /**
+     * 二级类型
+     */
+    private Integer subType;
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 持仓成份的id和name
+     */
+    private ValueLabelVO ref;
+    /**
+     * 资产大类的名称映射关系
+     */
+    private ValueLabelVO asset;
+    /**
+     * 成本
+     */
+    private BigDecimal cost;
+    /**
+     * 市值
+     */
+    private BigDecimal marketValue;
+    /**
+     * 占净值比
+     */
+    private BigDecimal ratioNv;
+    /**
+     * 数量
+     */
+    private BigDecimal number;
+    /**
+     * 数量变化=本期数量-上期数量
+     */
+    private BigDecimal numberChange;
+    /**
+     * 估值增值
+     */
+    private BigDecimal valueChange;
+    /**
+     * 增值比例=单个标的的【估值增值】/基金整体的【估值增值】
+     */
+    private BigDecimal valueChangeRatio;
+    /**
+     * 停牌信息
+     */
+    private String suspension;
+    /**
+     * 多空方向,1-多 2-空
+     */
+    private Integer sharesNature;
+
+    /**
+     * 标的唯一编码id  SE000005GY 类似这种
+     */
+    private String secId;
+
+    public String getFundId() {
+        return fundId;
+    }
+
+    public void setFundId(String fundId) {
+        this.fundId = fundId;
+    }
+
+    public String getDate() {
+        return date;
+    }
+
+    public void setDate(String date) {
+        this.date = date;
+    }
+
+    public ValueLabelVO getRef() {
+        return ref;
+    }
+
+    public void setRef(ValueLabelVO ref) {
+        this.ref = ref;
+    }
+
+    public ValueLabelVO getAsset() {
+        return asset;
+    }
+
+    public void setAsset(ValueLabelVO asset) {
+        this.asset = asset;
+    }
+
+    public BigDecimal getCost() {
+        return cost;
+    }
+
+    public void setCost(BigDecimal cost) {
+        this.cost = cost;
+    }
+
+    public BigDecimal getMarketValue() {
+        return marketValue;
+    }
+
+    public void setMarketValue(BigDecimal marketValue) {
+        this.marketValue = marketValue;
+    }
+
+    public BigDecimal getRatioNv() {
+        return ratioNv;
+    }
+
+    public void setRatioNv(BigDecimal ratioNv) {
+        this.ratioNv = ratioNv;
+    }
+
+    public BigDecimal getNumber() {
+        return number;
+    }
+
+    public void setNumber(BigDecimal number) {
+        this.number = number;
+    }
+
+    public BigDecimal getNumberChange() {
+        return numberChange;
+    }
+
+    public void setNumberChange(BigDecimal numberChange) {
+        this.numberChange = numberChange;
+    }
+
+    public BigDecimal getValueChange() {
+        return valueChange;
+    }
+
+    public void setValueChange(BigDecimal valueChange) {
+        this.valueChange = valueChange;
+    }
+
+    public BigDecimal getValueChangeRatio() {
+        return valueChangeRatio;
+    }
+
+    public void setValueChangeRatio(BigDecimal valueChangeRatio) {
+        this.valueChangeRatio = valueChangeRatio;
+    }
+
+    public String getSuspension() {
+        return suspension;
+    }
+
+    public void setSuspension(String suspension) {
+        this.suspension = suspension;
+    }
+
+    public Integer getSharesNature() {
+        return sharesNature;
+    }
+
+    public void setSharesNature(Integer sharesNature) {
+        this.sharesNature = sharesNature;
+    }
+
+    public String getSecId() {
+        return secId;
+    }
+
+    public void setSecId(String secId) {
+        this.secId = secId;
+    }
+
+    public String getFundType() {
+        return fundType;
+    }
+
+    public void setFundType(String fundType) {
+        this.fundType = fundType;
+    }
+
+    public String getSubjectCode() {
+        return subjectCode;
+    }
+
+    public void setSubjectCode(String subjectCode) {
+        this.subjectCode = subjectCode;
+    }
+
+    public Integer getLevel() {
+        return level;
+    }
+
+    public void setLevel(Integer level) {
+        this.level = level;
+    }
+
+    public Integer getSubType() {
+        return subType;
+    }
+
+    public void setSubType(Integer subType) {
+        this.subType = subType;
+    }
+}

+ 53 - 0
src/main/java/com/smppw/analysis/application/dto/position/HoldingType.java

@@ -0,0 +1,53 @@
+package com.smppw.analysis.application.dto.position;
+
+import java.util.stream.Stream;
+
+
+public enum HoldingType {
+    Cash(2, "现金"),
+    CostCash(20, "冲抵现金"),
+    Stock(0, "股票"),
+    StockSZH(110, "深港通"),
+    StockSHH(111, "沪港通"),
+    StockHGT(112, "港股通"),
+    Bond(1, "债券"),
+    BondBack(5, "正逆回购"),
+    Derivatives(7, "其他衍生品"),
+    Future(3, "期货"),
+    IndexFuture(31, "股指期货"),//子类
+    CmdtFuture(32, "商品期货"),//子类
+    BondFuture(33, "国债期货"), //子类
+    Option(4, "期权"),
+    IndexOption(41, "股指期权"), //子类
+    CmdtOption(42, "期货期权"), //子类
+    Fund(6, "基金"),
+    FundPrivate(61, "私募基金"),//子类
+    Warrant(21, "权证"),
+    Other(99, "其他"),
+    All(-1, "全部"),//方便过滤
+    Tail(100, "尾部");
+
+    private final int id;
+    private final String name;
+
+    public int getId() {
+        return id;
+    }
+
+    HoldingType(int id, String name2) {
+        this.id = id;
+        this.name = name2;
+    }
+
+    public static HoldingType getHoldingType(int id) {
+        return Stream.of(HoldingType.values()).filter(e -> e.id == id).findFirst().orElse(null);
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public boolean isTrue(int stockType) {
+        return this.id == stockType;
+    }
+}

+ 29 - 0
src/main/java/com/smppw/analysis/application/dto/position/MarketValueRatio.java

@@ -0,0 +1,29 @@
+package com.smppw.analysis.application.dto.position;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Setter
+@Getter
+public class MarketValueRatio {
+    private String date;
+    /**
+     * 市值
+     */
+    private BigDecimal marketValue;
+    /**
+     * 占净值比
+     */
+    private BigDecimal ratio;
+
+    public MarketValueRatio() {
+    }
+
+    public MarketValueRatio(String date, BigDecimal marketValue, BigDecimal ratio) {
+        this.date = date;
+        this.marketValue = marketValue;
+        this.ratio = ratio;
+    }
+}

+ 69 - 0
src/main/java/com/smppw/analysis/application/dto/position/PositionConstants.java

@@ -0,0 +1,69 @@
+package com.smppw.analysis.application.dto.position;
+
+import cn.hutool.core.collection.ListUtil;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.constants.SecType;
+
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/19 11:26
+ * @description 基金持仓的常量类
+ */
+public final class PositionConstants {
+    public static final String ASSET_MONEY = "MONEY";
+    public static final String ASSET_MONEY_DESC = "存款及备付金";
+
+    public static final String ASSET_RETURN_SALE = "RETURN_SALE";
+    public static final String ASSET_RETURN_SALE_CODE = "2202";
+    public static final String ASSET_RETURN_SALE_DESC = "逆回购";
+
+    public static final String ASSET_SALE = "SALE";
+    public static final String ASSET_SALE_CODE = "1202";
+    public static final String ASSET_SALE_DESC = "正回购";
+
+    public static final String[] SALE_CODES = {ASSET_RETURN_SALE_CODE, ASSET_SALE_CODE};
+
+    public static final String ASSET_OTHER = "OTHER";
+    public static final String ASSET_OTHER_DESC = "其他";
+
+    /**
+     * 其他类
+     */
+    public static final ValueLabelVO OTHER_ASSET = new ValueLabelVO(ASSET_OTHER, ASSET_OTHER_DESC);
+    /**
+     * 私募基金支持的类型
+     */
+    public static final List<String> PRIVATELY_TYPES = ListUtil.of(SecType.PRIVATELY_OFFERED_FUND, SecType.PRIVATE_FUND);
+    /**
+     * 国债期货和股指期货,商品期货
+     */
+    public static final List<Integer> FUTURE_SECOND_TYPES = ListUtil.of(HoldingType.IndexFuture.getId(),
+            HoldingType.BondFuture.getId(), HoldingType.CmdtFuture.getId());
+
+    /**
+     * 股指
+     * IF - 沪深300指数
+     * IH - 上证50指数
+     * IC - 中证500指数
+     * IM - 中证1000指数
+     */
+    public static final List<String> STOCK_INDEX_BREEDS = ListUtil.of("IF", "IH", "IC", "IM");
+    /**
+     * 国债期货
+     * 票面利率3%的2年期名义中短期国债 - TS
+     * 票面利率3%的5年期名义中期国债 - TF
+     * 票面利率3%的名义超长期国债 - TL
+     * 票面利率3%的名义长期国债 - T
+     */
+    public static final String[]  BOND_BREED_ARR = {"TS", "TF", "TL", "T"};
+    /**
+     * 国债期货
+     * 票面利率3%的2年期名义中短期国债 - TS
+     * 票面利率3%的5年期名义中期国债 - TF
+     * 票面利率3%的名义超长期国债 - TL
+     * 票面利率3%的名义长期国债 - T
+     */
+    public static final List<String> BOND_BREEDS = ListUtil.of(BOND_BREED_ARR);
+}

+ 26 - 0
src/main/java/com/smppw/analysis/application/dto/position/PositionLiquidityEnum.java

@@ -0,0 +1,26 @@
+package com.smppw.analysis.application.dto.position;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/12 14:12
+ * @description 流动性枚举
+ */
+public enum PositionLiquidityEnum {
+    HIGH("高流动性"),
+    HIGHER("较高流动性"),
+    SAME_AS("一般流动性"),
+    LOWER("较低流动性"),
+    LOW("低流动性"),
+
+    OTHER("其他");
+
+    private final String desc;
+
+    PositionLiquidityEnum(String desc) {
+        this.desc = desc;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+}

+ 59 - 0
src/main/java/com/smppw/analysis/application/dto/position/PositionStyleEnum.java

@@ -0,0 +1,59 @@
+package com.smppw.analysis.application.dto.position;
+
+import cn.hutool.core.text.CharSequenceUtil;
+
+import java.util.Arrays;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/9 17:16
+ * @description 基金持仓分析 风格枚举
+ */
+public enum PositionStyleEnum {
+    MARKET_GROWTH(201, 101, "大盘成长型"),
+    MARKET_VALUE(201, 102, "大盘价值型"),
+    MARKET_BALANCE(201, 103, "大盘平衡型"),
+    MID_MARKET_GROWTH(202, 101, "中盘成长型"),
+    MID_MARKET_VALUE(202, 102, "中盘价值型"),
+    MID_MARKET_BALANCE(202, 103, "中盘平衡型"),
+    SMALL_MARKET_GROWTH(203, 101, "小盘成长型"),
+    SMALL_MARKET_VALUE(203, 102, "小盘价值型"),
+    SMALL_MARKET_BALANCE(203, 103, "小盘平衡型");
+
+    private final int market;
+    private final int balance;
+    private final String name;
+
+    PositionStyleEnum(int market, int balance, String name) {
+        this.market = market;
+        this.balance = balance;
+        this.name = name;
+    }
+
+    public static PositionStyleEnum getStyle(Integer market, Integer balance) {
+        if (market == null || balance == null) {
+            return null;
+        }
+        return Arrays.stream(PositionStyleEnum.values()).filter(e -> market.equals(e.getMarket()))
+                .filter(e -> balance.equals(e.getBalance())).findFirst().orElse(null);
+    }
+
+    public static PositionStyleEnum getStyle(String code) {
+        if (CharSequenceUtil.isEmpty(code)) {
+            return null;
+        }
+        return Arrays.stream(PositionStyleEnum.values()).filter(e -> e.name().equals(code)).findFirst().orElse(null);
+    }
+
+    public int getMarket() {
+        return market;
+    }
+
+    public int getBalance() {
+        return balance;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

+ 52 - 0
src/main/java/com/smppw/analysis/application/dto/position/RefMarketValueRatio.java

@@ -0,0 +1,52 @@
+package com.smppw.analysis.application.dto.position;
+
+import com.smppw.common.pojo.ValueLabelVO;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 11:41
+ * @description 标的 市值与占净值比 对象
+ */
+@Setter
+@Getter
+public class RefMarketValueRatio {
+    /**
+     * 标的id 和名称映射
+     */
+    private ValueLabelVO ref;
+    /**
+     * 市值
+     */
+    private BigDecimal marketValue;
+    /**
+     * 占净值比
+     */
+    private BigDecimal ratio;
+
+    /**
+     * 标的id  SE000005GX
+     * 删除这个字段,债券分析得修改掉
+     */
+    @Deprecated
+    private String secId;
+
+    public RefMarketValueRatio() {
+    }
+
+    public RefMarketValueRatio(ValueLabelVO ref, BigDecimal marketValue, BigDecimal ratio) {
+        this.ref = ref;
+        this.marketValue = marketValue;
+        this.ratio = ratio;
+    }
+
+    public RefMarketValueRatio(ValueLabelVO ref, BigDecimal marketValue, BigDecimal ratio,String secId) {
+        this.ref = ref;
+        this.marketValue = marketValue;
+        this.ratio = ratio;
+        this.secId = secId;
+    }
+}

+ 31 - 0
src/main/java/com/smppw/analysis/application/dto/position/RiskExposureEnum.java

@@ -0,0 +1,31 @@
+package com.smppw.analysis.application.dto.position;
+
+import com.smppw.common.pojo.ValueLabelVO;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/13 10:35
+ * @description 股票风险敞口 枚举
+ */
+public enum RiskExposureEnum {
+    STOCK_BULL("股票多头"),
+    FUTURE_BULL("股指多头"),
+    STOCK_BEAR("股票空头"),
+    FUTURE_BEAR("股指空头"),
+    ETF("ETF融券"),
+    // 前端展示
+    EXPOSURE("风险敞口");
+    private final String desc;
+
+    RiskExposureEnum(String desc) {
+        this.desc = desc;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    public static ValueLabelVO getValueLabel(RiskExposureEnum exposureEnum) {
+        return new ValueLabelVO(exposureEnum.name(), exposureEnum.getDesc());
+    }
+}

+ 15 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/BondAssetAllocationParams.java

@@ -0,0 +1,15 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 11:30
+ * @description 大类资产配置,后续支持穿透
+ */
+@Setter
+@Getter
+public class BondAssetAllocationParams extends BaseParams {
+}

+ 26 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/BondAssetAllocationVO.java

@@ -0,0 +1,26 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 11:30
+ * @description 债券配置
+ */
+@Setter
+@Getter
+public class BondAssetAllocationVO {
+    /**
+     * 日期
+     */
+    private String date;
+
+    /**
+     * 市值与占净值比
+     */
+    private List<RefMarketValueRatio> asset;
+}

+ 23 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/BondPerformanceAttributionParams.java

@@ -0,0 +1,23 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 16:31
+ * @description 债券业绩归因 接口请求参数
+ */
+@Setter
+@Getter
+public class BondPerformanceAttributionParams extends BaseParams {
+    /**
+     * 基准,只支持 沪深300、中证500、中证1000
+     */
+    private String benchmarkId;
+    /**
+     * 1-债券类型,2-持仓个债
+     */
+    private String type;
+}

+ 73 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/BondPerformanceAttributionVO.java

@@ -0,0 +1,73 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 16:31
+ * @description 债券业绩归因 接口请求参数
+ */
+@Setter
+@Getter
+public class BondPerformanceAttributionVO {
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 每个债券类型或个债效应、市值和占净值比
+     */
+    private List<Effect> effect;
+
+    /**
+     * @author wangzaijun
+     * @date 2023/6/6 16:53
+     * @description 每个行业的效应、市值和占净值比数据
+     */
+    @Setter
+    @Getter
+    public static class Effect extends RefMarketValueRatio {
+        /**
+         * 总效应
+         */
+        private String total;
+        /**
+         * 息票效应
+         */
+        private String coupon;
+        /**
+         * 收敛效应
+         */
+        private String convergence;
+        /**
+         * 收入效应
+         */
+        private String income;
+        /**
+         * 国债效应
+         */
+        private String debt;
+        /**
+         * 利差效应
+         */
+        private String creditSpread;
+        /**
+         * 择券效应
+         */
+        private String selectBound;
+
+        public Effect() {
+            super();
+        }
+
+        public Effect(ValueLabelVO ref, BigDecimal marketValue, BigDecimal ratio) {
+            super(ref, marketValue, ratio);
+        }
+    }
+}

+ 11 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/BondSortAllocationParam.java

@@ -0,0 +1,11 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+
+/**
+ * @author Rain
+ * @date 2023/6/12 9:53
+ * @description 债券 分类配置及明细
+ */
+public class BondSortAllocationParam extends BaseParams {
+}

+ 58 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/BondSortAssetDTO.java

@@ -0,0 +1,58 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import java.math.BigDecimal;
+
+/**
+ * @author Rain
+ * @date 2023/6/12 11:02
+ * @description
+ */
+public class BondSortAssetDTO {
+
+    private String valuationDate;
+    private String bondType;
+    private BigDecimal marketValue;
+    private BigDecimal netValueRatio;
+
+    public String getValuationDate() {
+        return valuationDate;
+    }
+
+    public void setValuationDate(String valuationDate) {
+        this.valuationDate = valuationDate;
+    }
+
+    public String getBondType() {
+        return bondType;
+    }
+
+    public void setBondType(String bondType) {
+        this.bondType = bondType;
+    }
+
+    public BigDecimal getMarketValue() {
+        return marketValue;
+    }
+
+    public void setMarketValue(BigDecimal marketValue) {
+        this.marketValue = marketValue;
+    }
+
+    public BigDecimal getNetValueRatio() {
+        return netValueRatio;
+    }
+
+    public void setNetValueRatio(BigDecimal netValueRatio) {
+        this.netValueRatio = netValueRatio;
+    }
+
+    @Override
+    public String toString() {
+        return "BondSortAssetDTO{" +
+                "valuationDate='" + valuationDate + '\'' +
+                ", bondType='" + bondType + '\'' +
+                ", marketValue=" + marketValue +
+                ", netValueRatio=" + netValueRatio +
+                '}';
+    }
+}

+ 48 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/ConcentrationBondVO.java

@@ -0,0 +1,48 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:53
+ * @description 股票集中度 接口返回结果
+ */
+public class ConcentrationBondVO {
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 基金的资产净值
+     */
+    private BigDecimal assetNv;
+    /**
+     * 每个估值日期内所有的股票市值和市值占比,按照占比降序
+     */
+    private List<RefBondMarketValueRatio> position;
+
+    public String getDate() {
+        return date;
+    }
+
+    public void setDate(String date) {
+        this.date = date;
+    }
+
+    public BigDecimal getAssetNv() {
+        return assetNv;
+    }
+
+    public void setAssetNv(BigDecimal assetNv) {
+        this.assetNv = assetNv;
+    }
+
+    public List<RefBondMarketValueRatio> getPosition() {
+        return position;
+    }
+
+    public void setPosition(List<RefBondMarketValueRatio> position) {
+        this.position = position;
+    }
+}

+ 48 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/CreditGradingBondVO.java

@@ -0,0 +1,48 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:53
+ * @description 股票集中度 接口返回结果
+ */
+public class CreditGradingBondVO {
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 基金的资产净值
+     */
+    private BigDecimal assetNv;
+    /**
+     * 每个估值日期内所有的股票市值和市值占比,按照占比降序
+     */
+    private List<RefCreditMarketValueRatio> position;
+
+    public String getDate() {
+        return date;
+    }
+
+    public void setDate(String date) {
+        this.date = date;
+    }
+
+    public BigDecimal getAssetNv() {
+        return assetNv;
+    }
+
+    public void setAssetNv(BigDecimal assetNv) {
+        this.assetNv = assetNv;
+    }
+
+    public List<RefCreditMarketValueRatio> getPosition() {
+        return position;
+    }
+
+    public void setPosition(List<RefCreditMarketValueRatio> position) {
+        this.position = position;
+    }
+}

+ 15 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/DurationAnalysisParams.java

@@ -0,0 +1,15 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 18:05
+ * @description 债券 久期分析 接口请求参数
+ */
+@Setter
+@Getter
+public class DurationAnalysisParams extends BaseParams {
+}

+ 46 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/DurationAnalysisVO.java

@@ -0,0 +1,46 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 18:06
+ * @description 债券 久期分析 接口返回结构
+ */
+@Setter
+@Getter
+public class DurationAnalysisVO {
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 持仓久期
+     */
+    private String duration;
+    /**
+     * 净资产久期
+     */
+    private String nvDuration;
+    /**
+     * 修正久期0~1.0
+     */
+    private String correctDuration;
+    /**
+     * 修正久期1.0~3.0
+     */
+    private String correctDuration1;
+    /**
+     * 修正久期3~5.0
+     */
+    private String correctDuration3;
+    /**
+     * 修正久期5.0~7.0
+     */
+    private String correctDuration5;
+    /**
+     * 修正久期7+
+     */
+    private String correctDuration7;
+}

+ 15 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/ProfitRiskParams.java

@@ -0,0 +1,15 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 18:05
+ * @description 债券 收益风险 接口请求参数
+ */
+@Setter
+@Getter
+public class ProfitRiskParams extends BaseParams {
+}

+ 56 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/ProfitRiskVO.java

@@ -0,0 +1,56 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 18:30
+ * @description 债券 收益风险 接口返回结构
+ */
+@Setter
+@Getter
+public class ProfitRiskVO {
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 每个日期每个债券的收益风险,基于此基本数据可以求出整体的数据
+     * 包括:信用利差、剩余期限、dvbp、凸性、修正久期、到期收益率=∑个券市值占净值比*个券各项指标
+     */
+    private List<ProfitRisk> profitRisk;
+
+    /**
+     * @author wangzaijun
+     * @date 2023/6/6 18:39
+     * @description 债券收益风险 实体对象
+     */
+    @Setter
+    @Getter
+    public static class ProfitRisk extends RefMarketValueRatio {
+        /**
+         * 凸性
+         */
+        private String convexity;
+        /**
+         * 修正久期
+         */
+        private String correctDuration;
+        /**
+         * 信用利差
+         */
+        private String creditSpread;
+        /**
+         * DVBP
+         */
+        private String dvbp;
+        /**
+         * 到期收益率
+         */
+        private String yieldMaturity;
+    }
+}

+ 130 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/RefBondMarketValueRatio.java

@@ -0,0 +1,130 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import com.smppw.common.pojo.ValueLabelVO;
+
+import java.math.BigDecimal;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 11:41
+ * @description 标的 市值与占净值比 对象
+ */
+public class RefBondMarketValueRatio {
+    /**
+     * 标的id 和名称映射
+     */
+    private ValueLabelVO ref;
+    /**
+     * 市值
+     */
+    private BigDecimal marketValue;
+    /**
+     * 占净值比
+     */
+    private BigDecimal ratio;
+
+    /**
+     * 标的id  SE000005GX
+     */
+    private String secId;
+
+    /**
+     * 债券类型
+     */
+    private String bondType;
+
+    /**
+     * 发行票面利率
+     */
+    private BigDecimal nominalInterestRate;
+
+    /**
+     * 债券评级
+     */
+    private String creditRating;
+
+    /**
+     * 剩余期限
+     */
+    private Double remainder;
+
+    /**
+     * 主体名称
+     */
+    private String industryName;
+
+    public String getIndustryName() {
+        return industryName;
+    }
+
+    public void setIndustryName(String industryName) {
+        this.industryName = industryName;
+    }
+
+    public Double getRemainder() {
+        return remainder;
+    }
+
+    public void setRemainder(Double remainder) {
+        this.remainder = remainder;
+    }
+
+    public RefBondMarketValueRatio() {
+    }
+
+    public ValueLabelVO getRef() {
+        return ref;
+    }
+
+    public void setRef(ValueLabelVO ref) {
+        this.ref = ref;
+    }
+
+    public BigDecimal getMarketValue() {
+        return marketValue;
+    }
+
+    public void setMarketValue(BigDecimal marketValue) {
+        this.marketValue = marketValue;
+    }
+
+    public BigDecimal getRatio() {
+        return ratio;
+    }
+
+    public void setRatio(BigDecimal ratio) {
+        this.ratio = ratio;
+    }
+
+    public String getSecId() {
+        return secId;
+    }
+
+    public void setSecId(String secId) {
+        this.secId = secId;
+    }
+
+    public String getBondType() {
+        return bondType;
+    }
+
+    public void setBondType(String bondType) {
+        this.bondType = bondType;
+    }
+
+    public BigDecimal getNominalInterestRate() {
+        return nominalInterestRate;
+    }
+
+    public void setNominalInterestRate(BigDecimal nominalInterestRate) {
+        this.nominalInterestRate = nominalInterestRate;
+    }
+
+    public String getCreditRating() {
+        return creditRating;
+    }
+
+    public void setCreditRating(String creditRating) {
+        this.creditRating = creditRating;
+    }
+}

+ 195 - 0
src/main/java/com/smppw/analysis/application/dto/position/bond/RefCreditMarketValueRatio.java

@@ -0,0 +1,195 @@
+package com.smppw.analysis.application.dto.position.bond;
+
+import com.smppw.common.pojo.ValueLabelVO;
+
+import java.math.BigDecimal;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 11:41
+ * @description 标的 市值与占净值比 对象
+ */
+public class RefCreditMarketValueRatio {
+    /**
+     * 标的id 和名称映射
+     */
+    private ValueLabelVO ref;
+    /**
+     * 市值
+     */
+    private BigDecimal marketValue;
+    /**
+     * 占净值比
+     */
+    private BigDecimal ratio;
+
+    /**
+     * 标的id  SE000005GX
+     */
+    private String secId;
+
+    /**
+     * 债券主体id
+     */
+    private String mainCode;
+
+    /**
+     * 上一次评级时间
+     */
+    private String lastCrDate;
+
+    /**
+     * 最近一次评级时间
+     */
+    private String nearestCrDate;
+
+    /**
+     * 上一次评级编码
+     */
+    private String lastCrCode;
+
+    /**
+     * 最近一次评级编码
+     */
+    private String nearestCrCode;
+
+    /**
+     * 上一次评级描述
+     */
+    private String lastCrDesc;
+
+    /**
+     * 最近一次评级描述
+     */
+    private String nearestCrDesc;
+
+    /**
+     * 评级展望
+     */
+    private String crAnticipate;
+
+    /**
+     * 主体名称
+     */
+    private String industryName;
+
+    /**
+     * 个券占净值比
+     */
+    private BigDecimal ratioPerBond;
+
+    public BigDecimal getRatioPerBond() {
+        return ratioPerBond;
+    }
+
+    public void setRatioPerBond(BigDecimal ratioPerBond) {
+        this.ratioPerBond = ratioPerBond;
+    }
+
+    public String getIndustryName() {
+        return industryName;
+    }
+
+    public void setIndustryName(String industryName) {
+        this.industryName = industryName;
+    }
+
+    public String getLastCrDesc() {
+        return lastCrDesc;
+    }
+
+    public void setLastCrDesc(String lastCrDesc) {
+        this.lastCrDesc = lastCrDesc;
+    }
+
+    public String getNearestCrCode() {
+        return nearestCrCode;
+    }
+
+    public void setNearestCrCode(String nearestCrCode) {
+        this.nearestCrCode = nearestCrCode;
+    }
+
+    public RefCreditMarketValueRatio() {
+    }
+
+    public ValueLabelVO getRef() {
+        return ref;
+    }
+
+    public void setRef(ValueLabelVO ref) {
+        this.ref = ref;
+    }
+
+    public BigDecimal getMarketValue() {
+        return marketValue;
+    }
+
+    public void setMarketValue(BigDecimal marketValue) {
+        this.marketValue = marketValue;
+    }
+
+    public BigDecimal getRatio() {
+        return ratio;
+    }
+
+    public void setRatio(BigDecimal ratio) {
+        this.ratio = ratio;
+    }
+
+    public String getSecId() {
+        return secId;
+    }
+
+    public void setSecId(String secId) {
+        this.secId = secId;
+    }
+
+    public String getMainCode() {
+        return mainCode;
+    }
+
+    public void setMainCode(String mainCode) {
+        this.mainCode = mainCode;
+    }
+
+    public String getLastCrDate() {
+        return lastCrDate;
+    }
+
+    public void setLastCrDate(String lastCrDate) {
+        this.lastCrDate = lastCrDate;
+    }
+
+    public String getNearestCrDate() {
+        return nearestCrDate;
+    }
+
+    public void setNearestCrDate(String nearestCrDate) {
+        this.nearestCrDate = nearestCrDate;
+    }
+
+    public String getLastCrCode() {
+        return lastCrCode;
+    }
+
+    public void setLastCrCode(String lastCrCode) {
+        this.lastCrCode = lastCrCode;
+    }
+
+    public String getNearestCrDesc() {
+        return nearestCrDesc;
+    }
+
+    public void setNearestCrDesc(String nearestCrDesc) {
+        this.nearestCrDesc = nearestCrDesc;
+    }
+
+    public String getCrAnticipate() {
+        return crAnticipate;
+    }
+
+    public void setCrAnticipate(String crAnticipate) {
+        this.crAnticipate = crAnticipate;
+    }
+}

+ 35 - 0
src/main/java/com/smppw/analysis/application/dto/position/future/FutureDailyRet.java

@@ -0,0 +1,35 @@
+package com.smppw.analysis.application.dto.position.future;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/7/26 15:07
+ * @description 期货日张跌幅,默认按照日期升序
+ */
+@Setter
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class FutureDailyRet implements Comparable<FutureDailyRet> {
+    /**
+     * 标的
+     */
+    private String refId;
+    /**
+     * 交易日
+     */
+    private String priceDate;
+    /**
+     * 日张跌幅
+     */
+    private Double ret;
+
+    @Override
+    public int compareTo(FutureDailyRet o) {
+        return this.getPriceDate().compareTo(o.getPriceDate());
+    }
+}

+ 26 - 0
src/main/java/com/smppw/analysis/application/dto/position/future/MarginalRiskContribution.java

@@ -0,0 +1,26 @@
+package com.smppw.analysis.application.dto.position.future;
+
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/19 11:19
+ * @description 边际风险贡献度
+ */
+@Setter
+@Getter
+public class MarginalRiskContribution extends RefMarketValueRatio {
+    /**
+     * 风险贡献度
+     */
+    private BigDecimal riskCont;
+    /**
+     * 所属大类
+     */
+    private ValueLabelVO asset;
+}

+ 15 - 0
src/main/java/com/smppw/analysis/application/dto/position/future/MarginalRiskContributionParams.java

@@ -0,0 +1,15 @@
+package com.smppw.analysis.application.dto.position.future;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/19 11:16
+ * @description 私募基金期货的边际风险贡献接口请求参数
+ */
+@Setter
+@Getter
+public class MarginalRiskContributionParams extends BaseParams {
+}

+ 24 - 0
src/main/java/com/smppw/analysis/application/dto/position/future/MarginalRiskContributionVO.java

@@ -0,0 +1,24 @@
+package com.smppw.analysis.application.dto.position.future;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/19 11:17
+ * @description 私募基金期货的边际风险贡献接口返回结构
+ */
+@Setter
+@Getter
+public class MarginalRiskContributionVO {
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 该日期下各品种边际风险
+     */
+    private List<MarginalRiskContribution> riskContList;
+}

+ 23 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/BarraSensitivityParams.java

@@ -0,0 +1,23 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 17:13
+ * @description 股票barra敏感度分析 接口请求参数
+ */
+@Setter
+@Getter
+public class BarraSensitivityParams extends BaseParams {
+    /**
+     * 基准,只支持 沪深300、中证500、中证1000
+     */
+    private String benchmarkId;
+    /**
+     * 估值日期
+     */
+    private String date;
+}

+ 82 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/BarraSensitivityVO.java

@@ -0,0 +1,82 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.common.pojo.ValueLabelVO;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 17:17
+ * @description 股票barra敏感度分析 接口返回结果
+ */
+@Setter
+@Getter
+public class BarraSensitivityVO {
+    /**
+     * 日期
+     */
+    private String date;
+    /**
+     * 产品所有因子数据
+     */
+    private RefBarra fund;
+    /**
+     * 基准所有因子数据
+     */
+    private RefBarra benchmark;
+    /**
+     * 敞口走势
+     */
+    private RefBarra exposure;
+
+    @Setter
+    @Getter
+    @Builder
+    public static class RefBarra {
+        /**
+         * 当前标的:基金产品、基准和敞口
+         */
+        private ValueLabelVO ref;
+        /**
+         * 市值
+         */
+        private String size;
+        /**
+         * 贝塔
+         */
+        private String beta;
+        /**
+         * 动量
+         */
+        private String momentum;
+        /**
+         * 残差波动率
+         */
+        private String residualVolatility;
+        /**
+         * 非线性市值
+         */
+        private String nonLinearSize;
+        /**
+         * 账面市值比
+         */
+        private String bootToPriceRatio;
+        /**
+         * 流动性
+         */
+        private String liquidity;
+        /**
+         * 盈利能力
+         */
+        private String earningsYield;
+        /**
+         * 成长
+         */
+        private String growth;
+        /**
+         * 杠杆
+         */
+        private String leverage;
+    }
+}

+ 15 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/ChangeNumberParams.java

@@ -0,0 +1,15 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:53
+ * @description 持股数量变动时序 接口请求参数
+ */
+@Setter
+@Getter
+public class ChangeNumberParams extends BaseParams {
+}

+ 15 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/ChangeNumberVO.java

@@ -0,0 +1,15 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.common.pojo.dto.NewDateValue;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:53
+ * @description 持股数量变动时序 接口返回结果
+ */
+@Setter
+@Getter
+public class ChangeNumberVO extends NewDateValue {
+}

+ 15 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/ConcentrationParams.java

@@ -0,0 +1,15 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:53
+ * @description 股票集中度 接口请求参数
+ */
+@Setter
+@Getter
+public class ConcentrationParams extends BaseParams {
+}

+ 50 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/ConcentrationVO.java

@@ -0,0 +1,50 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:53
+ * @description 股票集中度 接口返回结果
+ */
+public class ConcentrationVO {
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 基金的资产净值
+     */
+    private BigDecimal assetNv;
+    /**
+     * 每个估值日期内所有的股票市值和市值占比,按照占比降序
+     */
+    private List<RefMarketValueRatio> position;
+
+    public String getDate() {
+        return date;
+    }
+
+    public void setDate(String date) {
+        this.date = date;
+    }
+
+    public BigDecimal getAssetNv() {
+        return assetNv;
+    }
+
+    public void setAssetNv(BigDecimal assetNv) {
+        this.assetNv = assetNv;
+    }
+
+    public List<RefMarketValueRatio> getPosition() {
+        return position;
+    }
+
+    public void setPosition(List<RefMarketValueRatio> position) {
+        this.position = position;
+    }
+}

+ 35 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/IndustryAllocationPreferenceVO.java

@@ -0,0 +1,35 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.common.pojo.ValueLabelVO;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:53
+ * @description 股票-行业配置偏好时序 接口返回结果
+ */
+@Setter
+@Getter
+public class IndustryAllocationPreferenceVO {
+    /**
+     * 报告期
+     */
+    private String date;
+    /**
+     * 行业关系映射,只保存在第一个日期中
+     */
+    private List<ValueLabelVO> industryList;
+
+    /**
+     * 每个行业的偏好=基金行业-基准行业
+     */
+    private Map<String, String> industryPreference;
+    /**
+     * 平均行业偏移
+     */
+    private String avg;
+}

+ 17 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/MajorChangeParams.java

@@ -0,0 +1,17 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:53
+ * @description 持仓股票重大变动 接口请求参数
+ */
+@Setter
+@Getter
+public class MajorChangeParams extends BaseParams {
+//    private int pageSize;
+//    private int current;
+}

+ 42 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/MajorChangeVO.java

@@ -0,0 +1,42 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:53
+ * @description 持仓股票重大变动 接口返回结果
+ */
+@Setter
+@Getter
+public class MajorChangeVO {
+    /**
+     * 报告期
+     */
+    private String date;
+    /**
+     * 当期累计交易金额
+     */
+    private String accumulatedTradeSum;
+    /**
+     * 交易方向
+     */
+    private String changeType;
+    /**
+     * 占期初基金资产净值比例
+     */
+    private String ratioInNvAtBegin;
+    /**
+     * 股票代码
+     */
+    private String stockCode;
+    /**
+     * 股票名称
+     */
+    private String stockName;
+    /**
+     * 所属行业
+     */
+    private String industry;
+}

+ 15 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/RiskExposureParams.java

@@ -0,0 +1,15 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:53
+ * @description 股票风险敞口走势 接口请求参数
+ */
+@Setter
+@Getter
+public class RiskExposureParams extends BaseParams {
+}

+ 26 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/RiskExposureVO.java

@@ -0,0 +1,26 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 17:36
+ * @description 股票风险敞口走势 接口返回结构
+ */
+@Setter
+@Getter
+public class RiskExposureVO {
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 类型、市值和占比,包括风险敞口
+     * 风险敞口=股票多头+股指多头-股票空头-股指空头-ETF融券
+     */
+    private List<RefMarketValueRatio> mvr;
+}

+ 20 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/StockAllocationParams.java

@@ -0,0 +1,20 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.analysis.application.dto.position.BaseAnalysisParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 15:27
+ * @description 行业配置\风格配置或流动性 接口请求参数
+ */
+@Setter
+@Getter
+public class StockAllocationParams extends BaseAnalysisParams {
+    /**
+     * 方向约束,默认多空,其他条件不生效
+     * 1-多头,2-空头,3-多空
+     */
+    private String constraint;
+}

+ 20 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/StockAllocationVO.java

@@ -0,0 +1,20 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.analysis.application.dto.position.CategoryConstraint;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+@Setter
+@Getter
+public class StockAllocationVO {
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 基金个股每个流动性空头比例
+     */
+    private List<CategoryConstraint> industries;
+}

+ 19 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/StockPerformanceAttributionParams.java

@@ -0,0 +1,19 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.analysis.application.dto.position.BaseAnalysisParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 16:31
+ * @description 股票业绩归因 接口请求参数
+ */
+@Setter
+@Getter
+public class StockPerformanceAttributionParams extends BaseAnalysisParams {
+    /**
+     * 是否年化,默认不年化
+     */
+    private Boolean ifAnnualized;
+}

+ 63 - 0
src/main/java/com/smppw/analysis/application/dto/position/stock/StockPerformanceAttributionVO.java

@@ -0,0 +1,63 @@
+package com.smppw.analysis.application.dto.position.stock;
+
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 16:31
+ * @description 股票业绩归因 接口请求参数
+ */
+@Setter
+@Getter
+public class StockPerformanceAttributionVO {
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 每个行业的效应、市值和占净值比
+     */
+    private List<Effect> effect;
+
+    /**
+     * @author wangzaijun
+     * @date 2023/6/6 16:53
+     * @description 每个行业的效应、市值和占净值比数据
+     */
+    @Setter
+    @Getter
+    public static class Effect extends RefMarketValueRatio {
+        /**
+         * 超额收益=(1+持仓组合收益)/(1+基准指数收益)-1
+         * 仓组合收益=估值日单位净值/期初单位净值-1
+         * 超额=配置+选股+择时
+         */
+        private Double exactRet;
+        /**
+         * 配置效应
+         */
+        private Double allocation;
+        /**
+         * 选股效应
+         */
+        private Double stock;
+        /**
+         * 择时效应
+         */
+        private Double timing;
+
+        public Effect() {
+            super();
+        }
+
+        public Effect(ValueLabelVO ref, BigDecimal marketValue, BigDecimal ratio) {
+            super(ref, marketValue, ratio);
+        }
+    }
+}

+ 19 - 0
src/main/java/com/smppw/analysis/application/dto/position/synthesize/AssetAllocationParams.java

@@ -0,0 +1,19 @@
+package com.smppw.analysis.application.dto.position.synthesize;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 11:30
+ * @description 大类资产配置,后续支持穿透
+ */
+@Setter
+@Getter
+public class AssetAllocationParams extends BaseParams {
+    /**
+     * 是否穿透,最多4级
+     */
+    private Boolean penetrate;
+}

+ 34 - 0
src/main/java/com/smppw/analysis/application/dto/position/synthesize/AssetAllocationVO.java

@@ -0,0 +1,34 @@
+package com.smppw.analysis.application.dto.position.synthesize;
+
+import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 11:30
+ * @description 大类资产配置
+ */
+@Setter
+@Getter
+public class AssetAllocationVO {
+    /**
+     * 日期
+     */
+    private String date;
+
+    /**
+     * 市值与占净值比
+     */
+    private List<RefMarketValueRatio> asset;
+
+    public AssetAllocationVO() {
+    }
+
+    public AssetAllocationVO(String date, List<RefMarketValueRatio> asset) {
+        this.date = date;
+        this.asset = asset;
+    }
+}

+ 15 - 0
src/main/java/com/smppw/analysis/application/dto/position/synthesize/HolderInfoParams.java

@@ -0,0 +1,15 @@
+package com.smppw.analysis.application.dto.position.synthesize;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:26
+ * @description 公募基金持有人结构 请求参数
+ */
+@Setter
+@Getter
+public class HolderInfoParams extends BaseParams {
+}

+ 54 - 0
src/main/java/com/smppw/analysis/application/dto/position/synthesize/HolderInfoVO.java

@@ -0,0 +1,54 @@
+package com.smppw.analysis.application.dto.position.synthesize;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:26
+ * @description 公募基金持有人结构 请求参数
+ */
+@Setter
+@Getter
+public class HolderInfoVO {
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 户均持有份额
+     */
+    private String averageHoldShares;
+    /**
+     * ETF基金持有比例
+     */
+    private String etfFeederHoldRatio;
+    /**
+     * 持有人户数
+     */
+    private String holderAccountNumber;
+    /**
+     * 个人持有比例
+     */
+    private String individualHoldRatio;
+    /**
+     * 机构持有比例
+     */
+    private String institutionHoldRatio;
+    /**
+     * 内部持有比例
+     */
+    private String professionalHoldRatio;
+    /**
+     * 前十大持有人持有份额合计
+     */
+    private String top10HolderAmount;
+    /**
+     * 前十大持有人持有比例合计
+     */
+    private String top10HoldersProportion;
+    /**
+     * 总份额=机构持有份额+个人持有份额+未明确投资者持有份额+基金从业人员持有份额+ETF连接基金持有份额
+     */
+    private String total;
+}

+ 15 - 0
src/main/java/com/smppw/analysis/application/dto/position/synthesize/LeverageChangeParams.java

@@ -0,0 +1,15 @@
+package com.smppw.analysis.application.dto.position.synthesize;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 11:30
+ * @description 杠杆变化,不需要支持穿透
+ */
+@Setter
+@Getter
+public class LeverageChangeParams extends BaseParams {
+}

+ 30 - 0
src/main/java/com/smppw/analysis/application/dto/position/synthesize/LeverageChangeVO.java

@@ -0,0 +1,30 @@
+package com.smppw.analysis.application.dto.position.synthesize;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 11:30
+ * @description 杠杆变化
+ */
+@Setter
+@Getter
+public class LeverageChangeVO {
+    /**
+     * 日期
+     */
+    private String date;
+    /**
+     * 总资产
+     */
+    private String total;
+    /**
+     * 净资产值
+     */
+    private String nv;
+    /**
+     * 杠杆比=总资产/资产净值
+     */
+    private String leverage;
+}

+ 19 - 0
src/main/java/com/smppw/analysis/application/dto/position/synthesize/PositionInfoParams.java

@@ -0,0 +1,19 @@
+package com.smppw.analysis.application.dto.position.synthesize;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:10
+ * @description 持仓列表的返回参数接口的请求参数
+ */
+@Setter
+@Getter
+public class PositionInfoParams extends BaseParams {
+    /**
+     * 是否穿透,最多4级
+     */
+    private Boolean penetrate;
+}

+ 25 - 0
src/main/java/com/smppw/analysis/application/dto/position/synthesize/PositionInfoVO.java

@@ -0,0 +1,25 @@
+package com.smppw.analysis.application.dto.position.synthesize;
+
+import com.smppw.common.pojo.ValueLabelVO;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:10
+ * @description 持仓列表的返回参数接口的返回结构
+ */
+@Setter
+@Getter
+public class PositionInfoVO {
+    /**
+     * 类别
+     */
+    private List<ValueLabelVO> types;
+    /**
+     * 估值日期
+     */
+    private List<String> dates;
+}

+ 42 - 0
src/main/java/com/smppw/analysis/application/dto/position/synthesize/PositionListParams.java

@@ -0,0 +1,42 @@
+package com.smppw.analysis.application.dto.position.synthesize;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import com.smppw.analysis.application.dto.position.AssetCategoryEnum;
+import com.smppw.analysis.application.dto.position.BaseParams;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 11:30
+ * @description 持仓列表,后续需要支持穿透
+ */
+@Setter
+@Getter
+public class PositionListParams extends BaseParams {
+    /**
+     * 数量,前10、前20、前50,-1-全部,默认前10
+     */
+    private String num;
+    /**
+     * 类别
+     *
+     * @see AssetCategoryEnum
+     */
+    private String type;
+    /**
+     * 估值日期
+     */
+    private String date;
+    /**
+     * 是否穿透,最多4级
+     */
+    private Boolean penetrate;
+
+    public AssetCategoryEnum getAssetCategory() {
+        if (CharSequenceUtil.isBlankOrUndefined(this.type)) {
+            return null;
+        }
+        return AssetCategoryEnum.getAssetCategory(this.type);
+    }
+}

+ 57 - 0
src/main/java/com/smppw/analysis/application/dto/position/synthesize/PositionListVO.java

@@ -0,0 +1,57 @@
+package com.smppw.analysis.application.dto.position.synthesize;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 11:30
+ * @description 持仓列表
+ */
+@Setter
+@Getter
+public class PositionListVO {
+    private String date;
+    /**
+     * 成本
+     */
+    private String cost;
+    /**
+     * 市值
+     */
+    private String marketValue;
+    /**
+     * 名称
+     */
+    private String name;
+    /**
+     * 数量
+     */
+    private String num;
+    /**
+     * 数量变化
+     */
+    private String numChange;
+    /**
+     * 停牌信息
+     */
+    private String suspension;
+    /**
+     * 类别
+     */
+    private String type;
+    /**
+     * 估值增值
+     */
+    private String valueChange;
+    /**
+     * 增值比例
+     */
+    private String valueChangeRatio;
+    /**
+     * 权重
+     */
+    private BigDecimal weight;
+}

+ 304 - 0
src/main/java/com/smppw/analysis/application/service/StockPositionAnalysis.java

@@ -0,0 +1,304 @@
+package com.smppw.analysis.application.service;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.map.MapUtil;
+import com.smppw.analysis.application.dto.position.*;
+import com.smppw.analysis.application.dto.position.stock.*;
+import com.smppw.analysis.application.service.position.BizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerFactor;
+import com.smppw.analysis.application.service.position.stock.BarraSensitivityComponent;
+import com.smppw.analysis.application.service.position.stock.IndustryAllocationPreferenceComponent;
+import com.smppw.analysis.domain.dao.PubliclyFundPositionDao;
+import com.smppw.analysis.domain.entity.PubliclyFundStockChangeDO;
+import com.smppw.analysis.domain.entity.SwSecIndustryInfoDO;
+import com.smppw.analysis.infrastructure.persistence.BaseUnderlyingMapper;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.common.pojo.dto.NewDateValue;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static com.smppw.analysis.application.service.position.BizHandlerConstants.*;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:41
+ * @description 基金持仓分析-股票tab
+ */
+@Component
+public class StockPositionAnalysis {
+    private static final Map<Long, String> POSITION_NAME = MapUtil.newHashMap(true);
+
+    static {
+        POSITION_NAME.put(1L, "最大持仓");
+        POSITION_NAME.put(3L, "前三大持仓");
+        POSITION_NAME.put(5L, "前五大持仓");
+        POSITION_NAME.put(10L, "前十大持仓");
+        POSITION_NAME.put(-1L, "股票持仓");
+    }
+
+    private final BizHandlerFactor factor;
+    private final BaseUnderlyingMapper baseUnderlyingMapper;
+    private final BarraSensitivityComponent barraSensitivity;
+    private final PubliclyFundPositionDao fundPositionBaseService;
+    private final IndustryAllocationPreferenceComponent industryAllocationPreference;
+
+    public StockPositionAnalysis(BizHandlerFactor factor,
+                                 BaseUnderlyingMapper baseUnderlyingMapper,
+                                 BarraSensitivityComponent barraSensitivity,
+                                 PubliclyFundPositionDao fundPositionBaseService,
+                                 IndustryAllocationPreferenceComponent industryAllocationPreference) {
+        this.factor = factor;
+        this.barraSensitivity = barraSensitivity;
+        this.baseUnderlyingMapper = baseUnderlyingMapper;
+        this.fundPositionBaseService = fundPositionBaseService;
+        this.industryAllocationPreference = industryAllocationPreference;
+    }
+
+    public List<MajorChangeVO> getMajorChangeList(MajorChangeParams params) {
+        List<SwSecIndustryInfoDO> industryList = this.baseUnderlyingMapper.querySecIndustryInfo();
+        List<MajorChangeVO> resultList = ListUtil.list(false);
+        List<PubliclyFundStockChangeDO> dataList = this.fundPositionBaseService.mfStockChangeList(params.getFundId(),
+                params.getStartDate(), params.getEndDate());
+        for (PubliclyFundStockChangeDO data : dataList) {
+            String industryName = industryList.stream().filter(e -> data.getSecCode().equals(e.getSecCode())).findFirst()
+                    .map(SwSecIndustryInfoDO::getIndustryName).orElse(null);
+            MajorChangeVO vo = new MajorChangeVO();
+            vo.setDate(DateUtil.formatDate(data.getReportDate()));
+            vo.setStockCode(data.getSecCode());
+            vo.setStockName(data.getSecName());
+            vo.setChangeType(Optional.ofNullable(data.getChangeType()).map(Object::toString).orElse(null));
+            vo.setAccumulatedTradeSum(Optional.ofNullable(data.getAccumulatedTradeSum()).map(BigDecimal::toPlainString).orElse(null));
+            vo.setRatioInNvAtBegin(Optional.ofNullable(data.getRatioInNvAtBegin()).map(BigDecimal::toPlainString).orElse(null));
+            vo.setIndustry(industryName);
+            resultList.add(vo);
+        }
+        // 按报告期降序
+        resultList.sort((o1, o2) -> o2.getDate().compareTo(o1.getDate()));
+        return resultList;
+    }
+
+    public Map<String, Object> getConcentration(ConcentrationParams params) {
+        Map<String, Object> dataset = MapUtil.newHashMap();
+        BizHandler<ConcentrationParams, List<ConcentrationVO>> bizHandler = this.factor.getBizHandlerInstance(STOCK_CONCENTRATION);
+        List<ConcentrationVO> dataList = bizHandler.bizHandle(params);
+        if (CollUtil.isEmpty(dataList)) {
+            return MapUtil.empty();
+        }
+        for (Long integer : POSITION_NAME.keySet()) {
+            List<MarketValueRatio> tempList = ListUtil.list(true);
+            for (ConcentrationVO vo : dataList) {
+                MarketValueRatio mvr = new MarketValueRatio();
+                mvr.setDate(vo.getDate());
+                List<RefMarketValueRatio> resList = vo.getPosition();
+                if (integer != -1L) {
+                    // 不为-1时取前n条记录求和
+                    resList = vo.getPosition().stream().limit(integer).collect(Collectors.toList());
+                }
+                BigDecimal marketValue = resList.stream().map(RefMarketValueRatio::getMarketValue)
+                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
+                BigDecimal ratio = resList.stream().map(RefMarketValueRatio::getRatio).filter(Objects::nonNull)
+                        .reduce(BigDecimal::add).orElse(null);
+                mvr.setMarketValue(marketValue);
+                mvr.setRatio(ratio);
+                tempList.add(mvr);
+            }
+            dataset.put(integer.toString(), tempList);
+        }
+        for (ConcentrationVO vo : dataList) {
+            List<RefMarketValueRatio> collect = vo.getPosition().stream().sorted((o1, o2) -> o2.getRatio().compareTo(o1.getRatio()))
+                    .limit(10).collect(Collectors.toList());
+            vo.setPosition(collect);
+        }
+        return MapUtil.<String, Object>builder().put("dataset", dataset)
+                .put("productNameMapping", POSITION_NAME).put("table", dataList).build();
+    }
+
+    public List<ChangeNumberVO> getChangeNumber(ChangeNumberParams params) {
+        BizHandler<ChangeNumberParams, List<ChangeNumberVO>> bizHandler = this.factor.getBizHandlerInstance(CHANGE_NUMBER);
+        List<ChangeNumberVO> dataList = bizHandler.bizHandle(params);
+        if (CollUtil.isEmpty(dataList)) {
+            return ListUtil.empty();
+        }
+        return dataList;
+    }
+
+    public Map<String, Object> getIndustryAllocation(StockAllocationParams params) {
+        BizHandler<StockAllocationParams, List<StockAllocationVO>> bizHandler = this.factor.getBizHandlerInstance(INDUSTRY_ALLOCATION);
+        List<StockAllocationVO> dataList = bizHandler.bizHandle(params);
+        return this.convertDataset(dataList);
+    }
+
+    public Map<String, Object> getIndustryAllocationPreference(StockAllocationParams params) {
+        List<IndustryAllocationPreferenceVO> dataList = this.industryAllocationPreference.bizHandle(params);
+        if (CollUtil.isEmpty(dataList)) {
+            return MapUtil.empty();
+        }
+        List<ValueLabelVO> industryList = dataList.get(0).getIndustryList();
+        // 倒序,其他排最后面
+        industryList.sort((o1, o2) -> o2.getValue().compareTo(o1.getValue()));
+        List<Map<String, Object>> dataset = ListUtil.list(true);
+        for (IndustryAllocationPreferenceVO temp : dataList) {
+            Map<String, Object> data = MapUtil.newHashMap(true);
+            data.put("date", temp.getDate());
+            data.putAll(temp.getIndustryPreference());
+            data.put("avg", temp.getAvg());
+            dataset.add(data);
+        }
+        return MapUtil.<String, Object>builder().put("dataset", dataset).put("productNameMapping", industryList).build();
+    }
+
+    public Map<String, Object> getStyleAllocation(StockAllocationParams params) {
+        BizHandler<StockAllocationParams, List<StockAllocationVO>> bizHandler = this.factor.getBizHandlerInstance(STYLE_ALLOCATION);
+        List<StockAllocationVO> dataList = bizHandler.bizHandle(params);
+        return this.convertDataset(dataList);
+    }
+
+    public Map<String, Object> getLiquidityAllocation(StockAllocationParams params) {
+        BizHandler<StockAllocationParams, List<StockAllocationVO>> bizHandler = this.factor.getBizHandlerInstance(LIQUIDITY_ALLOCATION);
+        List<StockAllocationVO> dataList = bizHandler.bizHandle(params);
+        List<ValueLabelVO> categoryList = Arrays.stream(PositionLiquidityEnum.values())
+                .map(e -> new ValueLabelVO(e.name(), e.getDesc())).collect(Collectors.toList());
+        return this.convertDataset(dataList, categoryList);
+    }
+
+    public Map<String, Object> getBarraSensitivity(BarraSensitivityParams params) {
+        return this.barraSensitivity.bizHandle(params);
+    }
+
+    public Map<String, Object> getRiskExposure(RiskExposureParams params) {
+        BizHandler<RiskExposureParams, List<RiskExposureVO>> bizHandler = this.factor.getBizHandlerInstance(RISK_EXPOSURE);
+        List<RiskExposureVO> dataList = bizHandler.bizHandle(params);
+        if (CollUtil.isEmpty(dataList)) {
+            return MapUtil.empty();
+        }
+        Map<String, Object> dataset = MapUtil.newHashMap();
+        for (RiskExposureEnum value : RiskExposureEnum.values()) {
+            List<MarketValueRatio> tempList = ListUtil.list(true);
+            for (RiskExposureVO temp : dataList) {
+                RefMarketValueRatio ratio = temp.getMvr().stream().filter(e -> value.name()
+                        .equals(e.getRef().getValue())).findFirst().orElse(null);
+                if (ratio == null) {
+                    continue;
+                }
+                MarketValueRatio mvr = new MarketValueRatio();
+                mvr.setDate(temp.getDate());
+                mvr.setMarketValue(ratio.getMarketValue());
+                mvr.setRatio(ratio.getRatio());
+                tempList.add(mvr);
+            }
+            tempList.sort(Comparator.comparing(MarketValueRatio::getDate));
+            dataset.put(value.name(), tempList);
+        }
+        List<ValueLabelVO> collect = Arrays.stream(RiskExposureEnum.values()).map(e -> new ValueLabelVO(e.name(), e.getDesc()))
+                .collect(Collectors.toList());
+        return MapUtil.<String, Object>builder().put("dataset", dataset)
+                .put("productNameMapping", collect).put("table", dataList).build();
+    }
+
+    public Map<String, Object> getPerformanceAttribution(StockPerformanceAttributionParams params) {
+        boolean ifAnnualized = params.getIfAnnualized() != null && params.getIfAnnualized();
+        List<ValueLabelVO> effects = ListUtil.list(true);
+        effects.add(new ValueLabelVO("allocation", "配置效应"));
+        effects.add(new ValueLabelVO("stock", "选股效应"));
+        effects.add(new ValueLabelVO("timing", "择时效应"));
+        effects.add(new ValueLabelVO("total", "超额收益"));
+        BizHandler<StockPerformanceAttributionParams, List<StockPerformanceAttributionVO>> bizHandler =
+                this.factor.getBizHandlerInstance(STOCK_PERFORMANCE_ATTRIBUTION);
+        List<StockPerformanceAttributionVO> dataList = bizHandler.bizHandle(params);
+        if (CollUtil.isEmpty(dataList)) {
+            return MapUtil.empty();
+        }
+        Map<String, Object> dataset = MapUtil.newHashMap();
+        for (ValueLabelVO effect : effects) {
+            List<NewDateValue> tempList = ListUtil.list(false);
+            Function<StockPerformanceAttributionVO.Effect, Double> function = e -> {
+                if ("allocation".equals(effect.getValue())) {
+                    return e.getAllocation();
+                } else if ("stock".equals(effect.getValue())) {
+                    return e.getStock();
+                } else if ("timing".equals(effect.getValue())) {
+                    return e.getTiming();
+                } else if ("total".equals(effect.getValue())) {
+                    return e.getExactRet();
+                }
+                return null;
+            };
+            String prevDate = dataList.get(0).getDate();
+            for (StockPerformanceAttributionVO vo : dataList) {
+                String date = vo.getDate();
+                double eff = vo.getEffect().stream().map(function).filter(Objects::nonNull).reduce(Double::sum).orElse(0d);
+                // 需要年化时才年化
+                if (ifAnnualized) {
+                    int dayCount = com.smppw.utils.DateUtil.getDateDistance(prevDate, date);
+                    eff = Math.pow(1 + eff, 365.0d / dayCount) - 1;
+                }
+                tempList.add(new NewDateValue(date, String.valueOf(eff)));
+                prevDate = date;
+            }
+            dataset.put(effect.getValue(), tempList);
+        }
+        return MapUtil.<String, Object>builder().put("dataset", dataset)
+                .put("productNameMapping", effects).put("table", dataList).build();
+    }
+
+    /**
+     * 配置数据转换为前端需要的dataset和table数据结构
+     *
+     * @param dataList 配置结构
+     * @return /
+     */
+    private Map<String, Object> convertDataset(List<StockAllocationVO> dataList) {
+        if (CollUtil.isEmpty(dataList)) {
+            return MapUtil.empty();
+        }
+        List<ValueLabelVO> categoryList = ListUtil.list(false);
+        for (StockAllocationVO vo : dataList) {
+            List<ValueLabelVO> collect = vo.getIndustries().stream().map(CategoryConstraint::getCategory)
+                    .distinct().collect(Collectors.toList());
+            CollUtil.addAllIfNotContains(categoryList, collect);
+        }
+
+        //修正为统计原有分组列表数量,先过滤其它字段后进行对比,当出现值不一致时追加其它字段,保证其它永远在最后
+        int count = categoryList.size();
+        categoryList = categoryList.stream().filter(e -> !PositionConstants.ASSET_OTHER.equalsIgnoreCase(e.getValue())).collect(Collectors.toList());
+        if (count > categoryList.size()) {
+            categoryList.add(PositionConstants.OTHER_ASSET);
+        }
+
+        return this.convertDataset(dataList, categoryList);
+    }
+
+    /**
+     * 配置数据转换为前端需要的dataset和table数据结构
+     *
+     * @param dataList     配置结构
+     * @param categoryList 分类映射关系
+     * @return /
+     */
+    private Map<String, Object> convertDataset(List<StockAllocationVO> dataList, List<ValueLabelVO> categoryList) {
+        Map<String, Object> dataset = MapUtil.newHashMap(false);
+        for (ValueLabelVO category : categoryList) {
+            List<Map<String, Object>> tempList = ListUtil.list(true);
+            for (StockAllocationVO vo : dataList) {
+                Map<String, Object> temp = MapUtil.newHashMap();
+                temp.put("date", vo.getDate());
+                CategoryConstraint s = vo.getIndustries().stream().filter(e -> category.equals(e.getCategory()))
+                        .findFirst().orElse(null);
+                temp.put("bull", Optional.ofNullable(s).map(CategoryConstraint::getBull).orElse(BigDecimal.ZERO));
+                temp.put("bear", Optional.ofNullable(s).map(CategoryConstraint::getBear).orElse(BigDecimal.ZERO));
+                temp.put("pupil", Optional.ofNullable(s).map(CategoryConstraint::getPupil).orElse(BigDecimal.ZERO));
+                temp.put("benchmark", Optional.ofNullable(s).map(CategoryConstraint::getBenchmark).orElse(BigDecimal.ZERO));
+                tempList.add(temp);
+            }
+            dataset.put(category.getValue(), tempList);
+        }
+        return MapUtil.<String, Object>builder().put("dataset", dataset).put("productNameMapping", categoryList)
+                .put("table", dataList).build();
+    }
+}

+ 127 - 0
src/main/java/com/smppw/analysis/application/service/SynthesizePositionAnalysis.java

@@ -0,0 +1,127 @@
+package com.smppw.analysis.application.service;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.NumberUtil;
+import com.smppw.analysis.application.dto.position.AssetCategoryEnum;
+import com.smppw.analysis.application.dto.position.MarketValueRatio;
+import com.smppw.analysis.application.dto.position.PositionConstants;
+import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+import com.smppw.analysis.application.dto.position.synthesize.*;
+import com.smppw.analysis.application.service.position.BizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerFactor;
+import com.smppw.analysis.domain.dao.PubliclyFundPositionDao;
+import com.smppw.analysis.domain.entity.PubliclyFundHolderInfoDO;
+import com.smppw.common.pojo.ValueLabelVO;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static com.smppw.analysis.application.service.position.BizHandlerConstants.*;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/6 14:40
+ * @description 基金持仓分析-综合tab
+ */
+@Component
+public class SynthesizePositionAnalysis {
+    private static final Map<String, String> ASSET_TYPE_MAPPER = MapUtil.newHashMap(true);
+
+    static {
+        for (AssetCategoryEnum value : AssetCategoryEnum.values()) {
+            ASSET_TYPE_MAPPER.put(value.name(), value.getName());
+        }
+        ASSET_TYPE_MAPPER.put(PositionConstants.ASSET_SALE, PositionConstants.ASSET_SALE_DESC);
+        ASSET_TYPE_MAPPER.put(PositionConstants.ASSET_RETURN_SALE, PositionConstants.ASSET_RETURN_SALE_DESC);
+        ASSET_TYPE_MAPPER.put(PositionConstants.ASSET_MONEY, PositionConstants.ASSET_MONEY_DESC);
+        ASSET_TYPE_MAPPER.put(PositionConstants.ASSET_OTHER, PositionConstants.ASSET_OTHER_DESC);
+    }
+
+    private final BizHandlerFactor factor;
+    private final PubliclyFundPositionDao fundPositionBaseService;
+
+    public SynthesizePositionAnalysis(BizHandlerFactor factor, PubliclyFundPositionDao fundPositionBaseService) {
+        this.factor = factor;
+        this.fundPositionBaseService = fundPositionBaseService;
+    }
+
+    public Map<String, Object> getAssetAllocation(AssetAllocationParams params) {
+        BizHandler<AssetAllocationParams, List<AssetAllocationVO>> bizHandler = this.factor.getBizHandlerInstance(ASSET_ALLOCATION);
+        List<AssetAllocationVO> dataList = bizHandler.bizHandle(params);
+        if (CollUtil.isEmpty(dataList)) {
+            return MapUtil.empty();
+        }
+        List<ValueLabelVO> mapping = ListUtil.list(true);
+        Map<String, Object> dataset = MapUtil.newHashMap();
+        ASSET_TYPE_MAPPER.forEach((k, v) -> {
+            List<MarketValueRatio> collect = dataList.stream().map(e -> {
+                RefMarketValueRatio mvr = e.getAsset().stream()
+                        .filter(o -> k.equals(o.getRef().getValue())).findFirst().orElse(null);
+                MarketValueRatio data = new MarketValueRatio();
+                data.setDate(e.getDate());
+                data.setMarketValue(Optional.ofNullable(mvr).map(RefMarketValueRatio::getMarketValue).orElse(BigDecimal.ZERO));
+                data.setRatio(Optional.ofNullable(mvr).map(RefMarketValueRatio::getRatio).orElse(BigDecimal.ZERO));
+                return data;
+            }).collect(Collectors.toList());
+            dataset.put(k, collect);
+            mapping.add(new ValueLabelVO(k, v));
+        });
+        return MapUtil.<String, Object>builder().put("dataset", dataset).put("productNameMapping", mapping).put("table", dataList).build();
+    }
+
+    public List<LeverageChangeVO> getLeverageChange(LeverageChangeParams params) {
+        BizHandler<LeverageChangeParams, List<LeverageChangeVO>> bizHandler = this.factor.getBizHandlerInstance(LEVERAGE_CHANGE);
+        List<LeverageChangeVO> dataList = bizHandler.bizHandle(params);
+        if (CollUtil.isEmpty(dataList)) {
+            return ListUtil.empty();
+        }
+        return dataList;
+    }
+
+    public PositionInfoVO getPositionParams(PositionInfoParams params) {
+        BizHandler<PositionInfoParams, PositionInfoVO> bizHandler = this.factor.getBizHandlerInstance(POSITION_PARAMS);
+        return bizHandler.bizHandle(params);
+    }
+
+    public List<PositionListVO> getPosition(PositionListParams params) {
+        BizHandler<PositionListParams, List<PositionListVO>> bizHandler = this.factor.getBizHandlerInstance(POSITION_LIST);
+        List<PositionListVO> dataList = bizHandler.bizHandle(params);
+        if (CollUtil.isEmpty(dataList)) {
+            return ListUtil.empty();
+        }
+        return dataList;
+    }
+
+    public List<HolderInfoVO> getHolderInfo(HolderInfoParams params) {
+        List<HolderInfoVO> resultList = ListUtil.list(true);
+        List<PubliclyFundHolderInfoDO> dataList = this.fundPositionBaseService.mfHolderInfoList(params.getFundId(), params.getStartDate(), params.getEndDate());
+        for (PubliclyFundHolderInfoDO data : dataList) {
+            HolderInfoVO vo = new HolderInfoVO();
+            vo.setDate(DateUtil.formatDate(data.getEndDate()));
+            vo.setAverageHoldShares(Optional.ofNullable(data.getAverageHoldShares()).map(BigDecimal::toPlainString).orElse(null));
+            vo.setEtfFeederHoldRatio(Optional.ofNullable(data.getEtfFeederHoldRatio()).map(BigDecimal::toPlainString).orElse(null));
+            vo.setHolderAccountNumber(Optional.ofNullable(data.getHolderAccountNumber()).map(Object::toString).orElse(null));
+            vo.setIndividualHoldRatio(Optional.ofNullable(data.getIndividualHoldRatio()).map(BigDecimal::toPlainString).orElse(null));
+            vo.setInstitutionHoldRatio(Optional.ofNullable(data.getInstitutionHoldRatio()).map(BigDecimal::toPlainString).orElse(null));
+            vo.setProfessionalHoldRatio(Optional.ofNullable(data.getProfessionalHoldRatio()).map(BigDecimal::toPlainString).orElse(null));
+            vo.setTop10HolderAmount(Optional.ofNullable(data.getTop10HolderAmount()).map(BigDecimal::toPlainString).orElse(null));
+            vo.setTop10HoldersProportion(Optional.ofNullable(data.getTop10HoldersProportion()).map(BigDecimal::toPlainString).orElse(null));
+            // 计算总份额=机构持有份额+个人持有份额+未明确投资者持有份额
+            BigDecimal total = NumberUtil.add(ListUtil.of(data.getInstitutionHoldShares(), data.getIndividualHoldShares(), data.getUndefinedHoldShares())
+                    .stream().filter(Objects::nonNull).toArray(BigDecimal[]::new));
+            vo.setTotal(total.toPlainString());
+            resultList.add(vo);
+        }
+        // 日期倒序
+        resultList.sort((o1, o2) -> o2.getDate().compareTo(o1.getDate()));
+        return resultList;
+    }
+}

+ 11 - 12
src/main/java/com/smppw/analysis/application/service/CommonService.java

@@ -1,12 +1,11 @@
-package com.smppw.analysis.application.service;
+package com.smppw.analysis.application.service.performance;
 
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.map.MapUtil;
-import com.smppw.analysis.application.dto.HeadIndicatorParams;
-import com.smppw.analysis.application.dto.HeadIndicatorVO;
-import com.smppw.analysis.application.dto.IndicatorParams;
+import com.smppw.analysis.application.dto.performance.HeadIndicatorParams;
+import com.smppw.analysis.application.dto.performance.IndicatorParams;
 import com.smppw.common.pojo.enums.Indicator;
 import com.smppw.common.pojo.enums.TimeRange;
 import com.smppw.constants.Consts;
@@ -24,25 +23,25 @@ public class CommonService {
         this.performanceService = performanceService;
     }
 
-    public HeadIndicatorVO getIndicator(HeadIndicatorParams params) {
-        HeadIndicatorVO vo = new HeadIndicatorVO();
+    public Map<String, Object> getIndicator(HeadIndicatorParams params) {
+        Map<String, Object> res = MapUtil.newHashMap();
         // 构建参数体
         IndicatorParams param = prepareParams(params);
         List<Indicator> indicators = ListUtil.of(Indicator.AnnualReturn, Indicator.AnnualStdDev, Indicator.MaxDrawdown, Indicator.SharpeRatio);
         List<Indicator> geoIndicators = ListUtil.of(Indicator.IntervalReturn);
         Map<String, Object> dataset = performanceService.calcIndicators(param, indicators, geoIndicators);
         Map<String, Object> indicatorMap = MapUtil.get(dataset, params.getSecId(), Map.class);
-        vo.setAnnualReturn(MapUtil.getStr(indicatorMap, Indicator.AnnualReturn.name()));
-        vo.setAnnualStdDev(MapUtil.getStr(indicatorMap, Indicator.AnnualStdDev.name()));
-        vo.setMaxDrawdown(MapUtil.getStr(indicatorMap, Indicator.MaxDrawdown.name()));
-        vo.setSharpeRatio(MapUtil.getStr(indicatorMap, Indicator.SharpeRatio.name()));
-        return null;
+        res.put(Indicator.AnnualReturn.name(), indicatorMap.get(Indicator.AnnualReturn.name()));
+        res.put(Indicator.AnnualStdDev.name(), indicatorMap.get(Indicator.AnnualStdDev.name()));
+        res.put(Indicator.MaxDrawdown.name(), indicatorMap.get(Indicator.MaxDrawdown.name()));
+        res.put(Indicator.SharpeRatio.name(), indicatorMap.get(Indicator.SharpeRatio.name()));
+        return res;
     }
 
     private IndicatorParams prepareParams(HeadIndicatorParams params) {
         IndicatorParams rateRiskIndicatorParams = new IndicatorParams();
         rateRiskIndicatorParams.setSecIds(CollectionUtil.newArrayList());
-        rateRiskIndicatorParams.setFundId(CollectionUtil.newArrayList(params.getSecId()));
+        rateRiskIndicatorParams.setRefIds(CollectionUtil.newArrayList(params.getSecId()));
         rateRiskIndicatorParams.setBenchmarkId(Consts.BENCHMARK);
         rateRiskIndicatorParams.setNavType(params.getNavType());
         rateRiskIndicatorParams.setRaiseType(params.getRaiseType());

+ 54 - 42
src/main/java/com/smppw/analysis/application/service/PerformanceService.java

@@ -1,4 +1,4 @@
-package com.smppw.analysis.application.service;
+package com.smppw.analysis.application.service.performance;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollectionUtil;
@@ -6,9 +6,9 @@ import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.NumberUtil;
 import cn.hutool.core.util.StrUtil;
-import com.smppw.analysis.application.dto.BaseParams;
-import com.smppw.analysis.application.dto.IndicatorParams;
-import com.smppw.analysis.application.dto.TrendParams;
+import com.smppw.analysis.application.dto.performance.BaseParams;
+import com.smppw.analysis.application.dto.performance.IndicatorParams;
+import com.smppw.analysis.application.dto.performance.TrendParams;
 import com.smppw.analysis.domain.service.BaseIndicatorServiceV2;
 import com.smppw.common.exception.APIException;
 import com.smppw.common.exception.DataException;
@@ -20,6 +20,7 @@ import com.smppw.common.pojo.dto.indicator.CalcMultipleSecMultipleTimeRangeIndic
 import com.smppw.common.pojo.dto.indicator.DateIntervalDto;
 import com.smppw.common.pojo.enums.*;
 import com.smppw.constants.Consts;
+import com.smppw.constants.MapFieldConstants;
 import com.smppw.core.reta.calc.PerformanceConsistency;
 import com.smppw.utils.StrategyHandleUtils;
 import org.slf4j.Logger;
@@ -29,7 +30,10 @@ import org.springframework.stereotype.Service;
 import java.math.BigDecimal;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
+import java.util.function.BinaryOperator;
+import java.util.stream.Collectors;
 
 @Service
 public class PerformanceService {
@@ -48,7 +52,7 @@ public class PerformanceService {
         try {
             // 1.参数处理
             this.checkParams(params);
-            List<String> refIds = params.getFundId();
+            List<String> refIds = params.getRefIds();
             List<String> secIds = this.getSecIdsByParams(params);
             // 差集求指数id集合,包括基�
             List<String> indexIds = CollectionUtil.subtractToList(secIds, refIds);
@@ -106,15 +110,22 @@ public class PerformanceService {
     }
 
     public Map<String, Object> trend(TrendParams params) {
-        List<TrendType> trendTypes = ListUtil.toList(TrendType.Ret, TrendType.Nav, TrendType.ExtraNav, TrendType.OrigNav, TrendType.ExtraRetAri, TrendType.NetValueChange, TrendType.ExtraRetGeo, TrendType.DrawdownTrend);
+        List<TrendType> trendTypes = ListUtil.toList(TrendType.Ret, TrendType.Nav, TrendType.ExtraNav, TrendType.OrigNav,
+                TrendType.ExtraRetAri, TrendType.NetValueChange, TrendType.ExtraRetGeo, TrendType.DrawdownTrend);
         List<TrendType> indexTrendTypes = ListUtil.toList(TrendType.Ret, TrendType.OrigNav);
         return this.handleTrends(params, trendTypes, indexTrendTypes);
     }
 
+    public Map<String, Object> dynamicDown(TrendParams params) {
+        List<TrendType> trendTypes = ListUtil.toList(TrendType.DrawdownTrend, TrendType.ExtraDrawdownTrend);
+        List<TrendType> indexTrendTypes = ListUtil.toList(TrendType.DrawdownTrend);
+        return this.handleTrends(params, trendTypes, indexTrendTypes);
+    }
+
     public Map<String, Object> handleTrends(TrendParams params, List<TrendType> trendTypes, List<TrendType> indexTrendTypes) {
         // 1、参数处理
         this.checkParams(params);
-        List<String> refIds = params.getFundId();
+        List<String> refIds = params.getRefIds();
         List<String> secIds = this.getSecIdsByParams(params);
         List<String> indexIds = CollectionUtil.subtractToList(secIds, refIds);
 
@@ -137,50 +148,33 @@ public class PerformanceService {
         // 处理走势图
         Map<String, List<Map<String, Object>>> dataListMap = MapUtil.newHashMap(true);
         Map<String, List<Map<String, Object>>> indexDataListMap = MapUtil.newHashMap(true);
-        Map<String, Map<String, Object>> trendListMap = MapUtil.newHashMap(true);
-        Map<String, Map<String, Object>> indexTrendListMap = MapUtil.newHashMap(true);
+        Map<String, Map<TrendType, List<Double>>> trendListMap = MapUtil.newHashMap(true);
         for (String refId : refIds) {
             IndicatorCalcPropertyDto dto = Optional.ofNullable(trendMap.get(refId)).filter(e -> !e.isEmpty()).map(e -> e.get(0)).orElse(null);
             List<String> tempDateList = Optional.ofNullable(dto).map(IndicatorCalcPropertyDto::getDateList).orElse(ListUtil.empty());
             // 基金走势序列
-            Map<TrendType, List<Double>> tempTrendTypeListMap = Optional.ofNullable(dto).map(IndicatorCalcPropertyDto::getSecData).map(IndicatorCalcSecDataDto::getTrendValueMap).orElse(MapUtil.empty());
-            trendListMap.put(refId, MapUtil.<String, Object>builder().put("dates", tempDateList).put("trend", tempTrendTypeListMap).build());
-            dataListMap.put(refId, this.buildDateValue(tempDateList, tempTrendTypeListMap, trendTypes));
+            Map<TrendType, List<Double>> trendTypeListMap = Optional.ofNullable(dto).map(IndicatorCalcPropertyDto::getSecData).map(IndicatorCalcSecDataDto::getTrendValueMap).orElse(MapUtil.empty());
+            trendListMap.put(refId, trendTypeListMap);
+            dataListMap.put(refId, this.buildDateValue(tempDateList, trendTypeListMap, trendTypes));
             // 指数净值和收益序列
             Map<String, Map<TrendType, List<Double>>> tempIndexTrendTypeListMap = Optional.ofNullable(dto).map(IndicatorCalcPropertyDto::getIndexData).map(IndicatorCalcIndexDataDto::getIndexTrendValueMap).orElse(MapUtil.empty());
             for (String indexId : indexIds) {
                 if (indexDataListMap.containsKey(indexId) && !indexId.equals(params.getBenchmarkId())) {
                     continue;
                 }
-                indexTrendListMap.put(indexId, MapUtil.<String, Object>builder().put("dates", tempDateList).put("trend", tempIndexTrendTypeListMap.get(indexId)).build());
                 indexDataListMap.put(indexId, this.buildDateValue(tempDateList, tempIndexTrendTypeListMap.get(indexId), indexTrendTypes));
             }
         }
-//        Map<String, List<Map<String, Object>>> valuesMap = this.multiSecMergeDateValue(refIds, trendMap, dataListMap, trendTypes, multiSec);
         Map<String, List<Map<String, Object>>> dataset = MapUtil.<String, List<Map<String, Object>>>builder().putAll(dataListMap).build();
-//        Map<String, String> productNameMapping = this.secId2NameService.query(secIds);
-//        Map<String, Object> extInfos = MapUtil.<String, Object>builder().put("endDate", params.getEndDate()).put("startDate", params.getStartDate()).build();
-//        if (masterId != null || !multiSec) {
-//            // 求基金和基准的年化收益率
-//            String fundId = refIds.get(0);
-//            Map<String, Object> map = trendListMap.get(fundId);
-//            Map<String, Object> indexMap = Optional.ofNullable(indexTrendListMap.get(params.getBenchmarkId())).orElse(MapUtil.newHashMap());
-//            List<String> dateList = MapUtil.get(map, "dates", List.class);
-//            Map<TrendType, List<Double>> trendTypeListMap = MapUtil.get(map, "trend", Map.class);
-//            Map<TrendType, List<Double>> indexTrendTypeListMap = Optional.ofNullable(MapUtil.get(indexMap, "trend", Map.class)).orElse(MapUtil.newHashMap());
-//            List<DateValue> dateValues = this.buildDateValue(dateList, trendTypeListMap.get(TrendType.Nav));
-//            List<DateValue> indexDateValues = this.buildDateValue(dateList, indexTrendTypeListMap.get(TrendType.OrigNav));
-//            Double annual = IndicatorFactory.getInstance().get(Indicator.AnnualReturn).calc(dateValues);
-//            Double annualBenchmark = IndicatorFactory.getInstance().get(Indicator.AnnualReturn).calc(indexDateValues);
-//            // 求最大回撤和超额最大回撤,业绩走势图不返回
-//            Double maxDown = this.handleMaxAndMin(trendTypeListMap.get(TrendType.DrawdownTrend), (o1, o2) -> o1 > o2 ? o2 : o1);
-//            Double maxExtraDown = this.handleMaxAndMin(trendTypeListMap.get(TrendType.ExtraDrawdownTrend), (o1, o2) -> o1 > o2 ? o2 : o1);
-//            Map<String, Object> extInfos1 = MapUtil.<String, Object>builder("maxDown", maxDown).put("maxExtraDown", maxExtraDown)
-//                    .put("annual", annual).put("annualBenchmark", annualBenchmark).build();
-//            extInfos.putAll(extInfos1);
-//            dataset.putAll(indexDataListMap);
-//        }
-        return MapUtil.newHashMap();
+        Map<String, Object> extInfos = MapUtil.<String, Object>builder().put("endDate", params.getEndDate()).put("startDate", params.getStartDate()).build();
+        for (String refId : refIds) {
+            Map<TrendType, List<Double>> trendTypeListMap = trendListMap.getOrDefault(refId, MapUtil.empty());
+            Double maxDown = this.handleMaxAndMin(trendTypeListMap.get(TrendType.DrawdownTrend), (o1, o2) -> o1 > o2 ? o2 : o1);
+            Double maxExtraDown = this.handleMaxAndMin(trendTypeListMap.get(TrendType.ExtraDrawdownTrend), (o1, o2) -> o1 > o2 ? o2 : o1);
+            Map<String, Object> data = MapUtil.<String, Object>builder().put("maxDown", maxDown).put("maxExtraDown", maxExtraDown).build();
+            extInfos.put(refId, data);
+        }
+        return MapUtil.<String, Object>builder().put(MapFieldConstants.FIELD_DATASET, dataset).putAll(extInfos).build();
     }
 
     /**
@@ -194,7 +188,7 @@ public class PerformanceService {
      * @return /
      */
     private <P extends BaseParams> CalcMultipleSecMultipleTimeRangeIndicatorReq buildCalcReq(P params, List<Indicator> indicators, List<Indicator> geoIndicators, Map<String, List<DateIntervalDto>> dateIntervalMap) {
-        List<String> refIds = params.getFundId();
+        List<String> refIds = params.getRefIds();
         List<String> secIds = this.getSecIdsByParams(params);
         // 差集求指数id集合,包括基准
         List<String> indexIds = CollectionUtil.subtractToList(secIds, refIds);
@@ -235,7 +229,7 @@ public class PerformanceService {
      */
     protected <P extends BaseParams> CalcMultipleSecMultipleTimeRangeIndicatorReq buildCalcReq(P params, List<Indicator> indicatorList, List<Indicator> geoIndicatorList,
                                                                                                DateIntervalType dateIntervalType, Frequency frequency) {
-        List<String> refIds = params.getFundId();
+        List<String> refIds = params.getRefIds();
         DateIntervalDto dateInterval = this.buildDateIntervalByParams(params, dateIntervalType, frequency);
         Map<String, List<DateIntervalDto>> dateIntervalMap = MapUtil.newHashMap(true);
         for (String refId : refIds) {
@@ -284,8 +278,8 @@ public class PerformanceService {
      * @param <P>    类型参数
      */
     protected <P extends BaseParams> void checkParams(P params) {
-        if (params.getFundId() == null || params.getFundId().isEmpty()) {
-            throw new APIException("fundId 参数不能为空");
+        if (params.getRefIds() == null || params.getRefIds().isEmpty()) {
+            throw new APIException("refIds 参数不能为空");
         }
         if (StrUtil.isBlank(params.getStartDate()) && StrUtil.isBlank(params.getEndDate())) {
             throw new DataException(null);
@@ -309,7 +303,7 @@ public class PerformanceService {
      */
     protected <P extends BaseParams> List<String> getSecIdsByParams(P params) {
         List<String> tempList = ListUtil.list(true);
-        tempList.addAll(params.getFundId());
+        tempList.addAll(params.getRefIds());
         if (StrUtil.isNotBlank(params.getBenchmarkId())) {
             tempList.add(params.getBenchmarkId());
         }
@@ -378,4 +372,22 @@ public class PerformanceService {
         }
         return dataList;
     }
+
+    /**
+     * 利用jdk8的流操作计算最大最小值
+     *
+     * @param dataList 待计算的数据序列
+     * @param operator 求最大还最小值的操作函数
+     * @return /
+     */
+    private Double handleMaxAndMin(List<Double> dataList, BinaryOperator<Double> operator) {
+        if (dataList == null || dataList.isEmpty()) {
+            return 0d;
+        }
+        List<Double> collect = dataList.stream().filter(Objects::nonNull).collect(Collectors.toList());
+        if (collect.isEmpty()) {
+            return 0d;
+        }
+        return collect.stream().reduce(operator).orElse(0d);
+    }
 }

+ 216 - 0
src/main/java/com/smppw/analysis/application/service/position/AbstractAnalysisBizHandler.java

@@ -0,0 +1,216 @@
+package com.smppw.analysis.application.service.position;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.text.CharSequenceUtil;
+import com.smppw.analysis.application.dto.position.*;
+import com.smppw.analysis.application.dto.position.stock.StockAllocationVO;
+import com.smppw.analysis.domain.entity.IndexSecWeightInfoDO;
+import com.smppw.analysis.domain.entity.SwSecIndustryInfoDO;
+import com.smppw.analysis.domain.event.SaveCacheEvent;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.domain.service.FundPositionAnalysisService;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.exception.APIException;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.constants.RedisConst;
+import com.smppw.constants.SecType;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/7/14 18:14
+ * @description 有基准的业绩分析
+ */
+public abstract class AbstractAnalysisBizHandler<P extends BaseAnalysisParams, R> extends AbstractNonSynthesizeBizHandler<P, R> {
+    protected static final Executor DEFAULT_EXECUTOR = ForkJoinPool.commonPool();
+    protected final FundPositionAnalysisService analysisService;
+
+    public AbstractAnalysisBizHandler(AnalysisProperty property, CacheFactory cacheFactory,
+                                      PositionLoadFactory factory, FundPositionAnalysisService analysisService) {
+        super(property, cacheFactory, factory);
+        this.analysisService = analysisService;
+    }
+
+    @Override
+    protected void checkParams(P params) {
+        super.checkParams(params);
+        if (CharSequenceUtil.isBlankOrUndefined(params.getBenchmarkId())) {
+            throw new APIException("基准不能为空!");
+        }
+    }
+
+    /**
+     * 用来过滤大类的钩子函数,可以区分股票、期货和债券,以及后续可能新增的基金
+     *
+     * @return /
+     */
+    protected AssetCategoryEnum filterAsset() {
+        return AssetCategoryEnum.STOCK;
+    }
+
+    @Override
+    protected R afterNonSynthesizeLoadData(P params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos) {
+        this.current = System.currentTimeMillis();
+        // 得到持仓信息
+        List<FundPositionDetail> dataList = positionInfos.stream().filter(e -> SecType.PUBLICLY_OFFERED_FUNDS.equals(e.getFundType()) || (e.getLevel() != null && e.getLevel() >= 4))
+                .filter(e -> this.filterAsset().name().equals(e.getAsset().getValue())).collect(Collectors.toList());
+        if (CollUtil.isEmpty(positionInfos)) {
+            return null;
+        }
+        String benchmarkId = params.getBenchmarkId();
+        // 日期升序
+        List<String> dateList = positionInfos.stream().map(FundPositionDetail::getDate).distinct().sorted().collect(Collectors.toList());
+        // 基准每个估值日的权重数据
+        Map<String, List<IndexSecWeightInfoDO>> indexSecWeightMap = this.getSecDateWeight(benchmarkId, dateList);
+        // 数据处理
+        return this.afterAnalysisLoadData(benchmarkId, dataList, indexSecWeightMap);
+    }
+
+    /**
+     * 获取行业与股票对应关系 (A股)
+     *
+     * @return /
+     */
+    protected Map<ValueLabelVO, List<String>> getIndustrySecMap() {
+        Object obj = this.cacheGateway.get(RedisConst.POSITION_STOCK_INDUSTRY_KEY);
+        List<SwSecIndustryInfoDO> dataList = ListUtil.list(false);
+        if (obj != null) {
+            dataList = this.convertList(obj, SwSecIndustryInfoDO.class);
+        } else {
+            List<SwSecIndustryInfoDO> industryInfos = this.analysisService.listSecIndustryInfo();
+            if (industryInfos != null) {
+                dataList.addAll(industryInfos);
+            }
+            SaveCacheEvent<List<SwSecIndustryInfoDO>> event = new SaveCacheEvent<>(this, industryInfos,
+                    t -> this.cacheGateway.set(RedisConst.POSITION_STOCK_INDUSTRY_KEY, t, RedisConst.POSITION_DAY_TTL, TimeUnit.SECONDS));
+            this.applicationContext.publishEvent(event);
+        }
+        return dataList.stream().collect(Collectors.groupingBy(e -> new ValueLabelVO(e.getIndustryCode(), e.getIndustryName()),
+                Collectors.mapping(SwSecIndustryInfoDO::getSecCode, Collectors.toList())));
+    }
+
+    /**
+     * 获取指数权重
+     *
+     * @param benchmarkId 基准id
+     * @param dateList    估值日期序列
+     * @return 对应日期的指数成分股占比
+     */
+    private Map<String, List<IndexSecWeightInfoDO>> getSecDateWeight(String benchmarkId, List<String> dateList) {
+        String key = RedisConst.POSITION_INDEX_WEIGHT_KEY + benchmarkId;
+        Map<String, List<IndexSecWeightInfoDO>> indexSecDateWeightMap = MapUtil.newHashMap(dateList.size());
+        Map<String, Object> hmget = this.cacheGateway.hget(key);
+        hmget.forEach((k, v) -> {
+            if (dateList.contains(k)) {
+                List<IndexSecWeightInfoDO> tempList = this.convertList(v, IndexSecWeightInfoDO.class);
+                indexSecDateWeightMap.put(k, tempList);
+            }
+        });
+
+        // 过滤不存在的日期的数据,重新从数据库获取并放入缓存
+        List<String> dates = hmget.keySet().stream().map(Object::toString).distinct().collect(Collectors.toList());
+        List<String> realDates = CollUtil.subtractToList(dateList, dates);
+        for (String date : realDates) {
+            List<IndexSecWeightInfoDO> indexSecWeightList;
+            Object hget = this.cacheGateway.hget(key, date);
+            if (hget == null) {
+                indexSecWeightList = this.analysisService.listIndexSecWeight(benchmarkId, date);
+                SaveCacheEvent<List<IndexSecWeightInfoDO>> event = new SaveCacheEvent<>(this, indexSecWeightList,
+                        t -> this.cacheGateway.hset(key, date, t, RedisConst.POSITION_MONTH_TTL, TimeUnit.SECONDS));
+                this.applicationContext.publishEvent(event);
+            } else {
+                indexSecWeightList = this.convertList(hget, IndexSecWeightInfoDO.class);
+            }
+            indexSecDateWeightMap.putIfAbsent(date, indexSecWeightList);
+        }
+        return indexSecDateWeightMap;
+    }
+
+    /**
+     * 得到数据化的逻辑处理
+     *
+     * @param benchmarkId     基准id
+     * @param positionDetails 持仓详情
+     * @param indexWeightMap  指数权重
+     * @return /
+     */
+    protected abstract R afterAnalysisLoadData(String benchmarkId, List<FundPositionDetail> positionDetails,
+                                               Map<String, List<IndexSecWeightInfoDO>> indexWeightMap);
+
+    /**
+     * 构建分析结果集
+     *
+     * @param positionDetails 当前日期下的
+     * @param total           股票总市值
+     * @param categoryCodes   有分类的所有股票代码
+     * @param secWeightList   指数成分占比
+     * @param secCodes        分类下的所有股票代码
+     * @return /
+     */
+    protected CategoryConstraint buildCategoryConstraint(List<FundPositionDetail> positionDetails, BigDecimal total, List<String> categoryCodes,
+                                                         List<IndexSecWeightInfoDO> secWeightList, List<String> secCodes) {
+        if (categoryCodes == null) {
+            categoryCodes = ListUtil.list(false);
+        }
+        CategoryConstraint constraint = new CategoryConstraint();
+        BigDecimal pupil = BigDecimal.ZERO;
+        if (total != null && total.compareTo(BigDecimal.ZERO) > 0) {
+            // 找到同风格的基金成分
+            BigDecimal indexWeight = secWeightList.stream().filter(e -> secCodes.contains(e.getSecCode())).map(IndexSecWeightInfoDO::getWeight)
+                    .reduce(BigDecimal::add).map(e -> e.divide(BigDecimal.valueOf(100), 6, RoundingMode.HALF_UP)).orElse(BigDecimal.ZERO);
+            List<FundPositionDetail> collect = positionDetails.stream().filter(e -> secCodes.contains(e.getRef().getValue())).collect(Collectors.toList());
+            categoryCodes.addAll(collect.stream().map(e -> e.getRef().getValue()).distinct().collect(Collectors.toList()));
+            BigDecimal bull = collect.stream().filter(e -> Objects.equals(1, e.getSharesNature())).map(FundPositionDetail::getMarketValue)
+                    .reduce(BigDecimal::add).map(e -> e.divide(total, 6, RoundingMode.HALF_UP)).orElse(BigDecimal.ZERO);
+            BigDecimal bear = collect.stream().filter(e -> Objects.equals(2, e.getSharesNature())).map(FundPositionDetail::getMarketValue).reduce(BigDecimal::add)
+                    .map(BigDecimal::abs).map(e -> BigDecimal.valueOf(-1).multiply(e)).map(e -> e.divide(total, 6, RoundingMode.HALF_UP)).orElse(BigDecimal.ZERO);
+            constraint.setBull(bull);
+            constraint.setBear(bear);
+            // 多空
+            pupil = bull.add(bear);
+            constraint.setBenchmark(indexWeight);
+        }
+        constraint.setPupil(pupil);
+        return constraint;
+    }
+
+    /**
+     * 返回归因分析结果,包括处理其他分类
+     *
+     * @param date               当前估值日期
+     * @param positionDetails    当前估值日期的持仓数据
+     * @param total              总市值
+     * @param indexSecWeightList 指数成分占比
+     * @param constraintList     分析结果集
+     * @param categoryCodes      所有已有的分类股票代码集合,用来获取other代码
+     * @return /
+     */
+    protected StockAllocationVO getStockAllocationVO(String date, List<FundPositionDetail> positionDetails,
+                                                     BigDecimal total, List<IndexSecWeightInfoDO> indexSecWeightList,
+                                                     List<CategoryConstraint> constraintList, List<String> categoryCodes) {
+        List<String> secCodes = positionDetails.stream().map(e -> e.getRef().getValue()).distinct().collect(Collectors.toList());
+        List<String> otherCodes = CollUtil.subtractToList(secCodes, categoryCodes);
+        if (CollUtil.isNotEmpty(otherCodes)) {
+            CategoryConstraint constraint = this.buildCategoryConstraint(positionDetails, total, categoryCodes, indexSecWeightList, otherCodes);
+            constraint.setCategory(OTHER);
+            constraintList.add(constraint);
+        }
+        // 按多空降序
+        constraintList.sort(((o1, o2) -> o2.getPupil().compareTo(o1.getPupil())));
+        StockAllocationVO vo = new StockAllocationVO();
+        vo.setDate(date);
+        vo.setIndustries(constraintList);
+        return vo;
+    }
+}

+ 189 - 0
src/main/java/com/smppw/analysis/application/service/position/AbstractBizHandler.java

@@ -0,0 +1,189 @@
+package com.smppw.analysis.application.service.position;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.smppw.analysis.application.dto.position.BaseParams;
+import com.smppw.analysis.application.dto.position.FundPositionBaseInfo;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.PositionConstants;
+import com.smppw.analysis.domain.event.SaveCacheEvent;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.domain.gateway.CacheGateway;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.exception.APIException;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.constants.RedisConst;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/7 15:04
+ * @description 抽象的业务处理器,加载数据用本地缓存存储
+ */
+public abstract class AbstractBizHandler<P extends BaseParams, R> implements BizHandler<P, R>, ApplicationContextAware {
+    protected static final ValueLabelVO OTHER = PositionConstants.OTHER_ASSET;
+    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
+    protected final CacheGateway<Object> cacheGateway;
+    protected final PositionLoadFactory factory;
+    protected long current;
+    protected ApplicationContext applicationContext;
+    private long executeTime;
+
+    public AbstractBizHandler(AnalysisProperty property, CacheFactory cacheFactory, PositionLoadFactory factory) {
+        this.factory = factory;
+        this.cacheGateway = cacheFactory.getCacheGateway(property.getCacheType());
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+
+    /**
+     * 模板方法,是否返回所有数据,默认否。否的时候做时间过滤
+     *
+     * @return /
+     */
+    protected boolean isAllPositionData() {
+        return false;
+    }
+
+    @Override
+    public long getExecuteTime() {
+        return this.executeTime;
+    }
+
+    /**
+     * 参数校验,子类可以覆盖重写
+     *
+     * @param params 请求参数
+     */
+    protected void checkParams(P params) {
+        if (CharSequenceUtil.isBlankOrUndefined(params.getFundId())) {
+            throw new APIException("基金id不能为空!");
+        }
+        if (CharSequenceUtil.isBlankOrUndefined(params.getStartDate()) || CharSequenceUtil.isBlankOrUndefined(params.getEndDate())) {
+            throw new APIException("时间区间不能为空!");
+        }
+    }
+
+    /**
+     * 一个简单的数据转换,object对象转list
+     *
+     * @param obj   待转换对象
+     * @param clazz 泛型类型
+     * @param <T>   类型参数
+     * @return 对应泛型类型的实体的集合
+     */
+    protected <T> List<T> convertList(Object obj, Class<T> clazz) {
+        try {
+            return JSONUtil.toList(JSONUtil.toJsonStr(obj), clazz);
+        } catch (Exception e) {
+            logger.warn(StrUtil.format("数据转换list失败:{}", e.getMessage()));
+        }
+        return ListUtil.empty();
+    }
+
+    /**
+     * 一个简单的数据转换,object对象转bean
+     *
+     * @param obj   待转换对象
+     * @param clazz 泛型类型
+     * @param <T>   类型参数
+     * @return 对应泛型类型实体
+     */
+    protected <T> T convertBean(Object obj, Class<T> clazz) {
+        try {
+            return JSONUtil.toBean(JSONUtil.toJsonStr(obj), clazz);
+        } catch (Exception e) {
+            logger.warn(StrUtil.format("数据转换实体失败:{}", e.getMessage()));
+        }
+        return null;
+    }
+
+    /**
+     * 接口业务处理
+     *
+     * @param params 请求参数
+     * @return R类型数据
+     */
+    @Override
+    public R bizHandle(P params) {
+        this.current = System.currentTimeMillis();
+        try {
+            this.checkParams(params);
+            List<FundPositionBaseInfo> fundPositionBaseInfos = this.loadFundPositionBaseInfo(params);
+            List<FundPositionDetail> positionInfos = this.loadPositionData(params);
+            if (!this.isAllPositionData() && CollUtil.isNotEmpty(positionInfos)) {
+                // 数量变化处理后时段过滤
+                positionInfos = positionInfos.stream().filter(e -> params.getStartDate().compareTo(e.getDate()) <= 0)
+                        .filter(e -> params.getEndDate().compareTo(e.getDate()) >= 0).collect(Collectors.toList());
+            }
+            if (CollUtil.isNotEmpty(positionInfos)) {
+                positionInfos = positionInfos.stream().filter(e -> e.getRef() != null).collect(Collectors.toList());
+            }
+            return this.afterLoadData(params, fundPositionBaseInfos, positionInfos);
+        } catch (Exception e) {
+            logger.warn(String.format("接口【%s】请求错误:%s", JSONUtil.toJsonStr(params), e.getMessage()));
+            return null;
+        } finally {
+            // 会不会获取到别的线程的数据?
+            this.executeTime = System.currentTimeMillis() - this.current;
+        }
+    }
+
+    /**
+     * 提供给子类实现的数据转换逻辑接口
+     *
+     * @param params        请求参数
+     * @param baseInfos     基金的基本持仓信息,总资产和净资产
+     * @param positionInfos 获取到的所有持仓信息,过滤了资产大类为null、成分为null的数据
+     * @return /
+     */
+    protected abstract R afterLoadData(P params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos);
+
+    /**
+     * 同步的获取基金的持仓信息,获取后放入缓存
+     * synchronized 关键字被优化过,所以这里可以直接使用。当并发特别大时可以考虑细化该锁
+     *
+     * @param params 请求参数
+     * @return /
+     */
+    private synchronized List<FundPositionBaseInfo> loadFundPositionBaseInfo(P params) {
+        PositionLoad loadInstance = this.factory.getPositionLoadInstance(params.getFundId());
+        return loadInstance.getFundPositionBaseInfo(params);
+    }
+
+    /**
+     * 同步的获取基金的持仓信息,获取后放入缓存
+     * synchronized 关键字被优化过,所以这里可以直接使用。当并发特别大时可以考虑细化该锁
+     *
+     * @param params 请求参数
+     * @return /
+     */
+    private synchronized List<FundPositionDetail> loadPositionData(P params) {
+        String key = RedisConst.POSITION_DETAIL_KEY;
+        String item = params.getFundId();
+        Object hget = this.cacheGateway.hget(key, item);
+        if (hget != null) {
+            return this.convertList(hget, FundPositionDetail.class);
+        }
+        PositionLoad loadInstance = this.factory.getPositionLoadInstance(params.getFundId());
+        List<FundPositionDetail> positionInfoList = loadInstance.loadPositionDetail(params);
+        SaveCacheEvent<List<FundPositionDetail>> event = new SaveCacheEvent<>(this, positionInfoList,
+                t -> this.cacheGateway.hset(key, item, t, 1, TimeUnit.DAYS));
+        this.applicationContext.publishEvent(event);
+        return positionInfoList;
+    }
+}

+ 73 - 0
src/main/java/com/smppw/analysis/application/service/position/AbstractNonSynthesizeBizHandler.java

@@ -0,0 +1,73 @@
+package com.smppw.analysis.application.service.position;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import com.smppw.analysis.application.dto.position.BaseParams;
+import com.smppw.analysis.application.dto.position.CategoryConstraint;
+import com.smppw.analysis.application.dto.position.FundPositionBaseInfo;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.constants.SecType;
+import com.smppw.utils.BigDecimalUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/15 19:13
+ * @description 非综合类的组件抽象
+ */
+public abstract class AbstractNonSynthesizeBizHandler<P extends BaseParams, R> extends AbstractBizHandler<P, R> {
+    public AbstractNonSynthesizeBizHandler(AnalysisProperty property, CacheFactory cacheFactory, PositionLoadFactory factory) {
+        super(property, cacheFactory, factory);
+    }
+
+    @Override
+    protected R afterLoadData(P params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos) {
+        if (CollUtil.isNotEmpty(positionInfos)) {
+            // 过滤掉没有识别到大类的、多空轧差后数量为null的和私募基金的subType!=20的
+            positionInfos = positionInfos.stream().filter(e -> e.getAsset() != null).filter(e -> e.getNumber() != null)
+                    .filter(e -> SecType.PUBLICLY_OFFERED_FUNDS.equals(e.getFundType()) || (e.getSubType() != null && e.getSubType() != 20))
+                    .collect(Collectors.toList());
+        }
+        return this.afterNonSynthesizeLoadData(params, baseInfos, positionInfos);
+    }
+
+    protected abstract R afterNonSynthesizeLoadData(P params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos);
+
+    @Deprecated
+    protected CategoryConstraint calcCategoryWeight(BigDecimal total, Map<ValueLabelVO, List<Map<String, Object>>> indexStyleMap,
+                                                    ValueLabelVO category, List<Map<String, Object>> categoryList) {
+        // 同风格(行业、风格或流动性)指数成分权重汇总
+        BigDecimal indexWeight = BigDecimal.ZERO;
+        if (indexStyleMap != null && indexStyleMap.get(category) != null) {
+            indexWeight = indexStyleMap.get(category).stream().map(e -> MapUtil.get(e, "weight", BigDecimal.class)).reduce(BigDecimal::add)
+                    .map(e -> e.divide(new BigDecimal("100"), 18, RoundingMode.HALF_UP)).orElse(BigDecimal.ZERO);
+        }
+        // 同风格(行业、风格或流动性)多头市值汇总
+        BigDecimal bullMv = categoryList.stream().filter(e -> Objects.equals("1", MapUtil.getStr(e, "sharesNature")))
+                .map(e -> MapUtil.get(e, "marketValue", BigDecimal.class)).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
+        // 同风格(行业、风格或流动性)空头市值汇总,并取负数
+        BigDecimal bearMv = categoryList.stream().filter(e -> Objects.equals("2", MapUtil.getStr(e, "sharesNature")))
+                .map(e -> MapUtil.get(e, "marketValue", BigDecimal.class)).reduce(BigDecimal::add)
+                .map(e -> BigDecimalUtils.toBigDecimal("-1").multiply(e)).orElse(BigDecimal.ZERO);
+        CategoryConstraint temp = new CategoryConstraint();
+        temp.setCategory(category);
+        // 求权重
+        BigDecimal bull = total == null || total.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : bullMv.divide(total, 6, RoundingMode.HALF_UP);
+        BigDecimal bear = total == null || total.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : bearMv.divide(total, 6, RoundingMode.HALF_UP);
+        temp.setBull(bull);
+        temp.setBear(bear);
+        // 多空
+        temp.setPupil(bull.subtract(bear.abs()));
+        temp.setBenchmark(indexWeight.setScale(6, RoundingMode.HALF_UP));
+        return temp;
+    }
+}

+ 10 - 0
src/main/java/com/smppw/analysis/application/service/position/AbstractPositionLoad.java

@@ -0,0 +1,10 @@
+package com.smppw.analysis.application.service.position;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/8 9:23
+ * @description 抽象实现
+ */
+public abstract class AbstractPositionLoad implements PositionLoad {
+
+}

+ 21 - 0
src/main/java/com/smppw/analysis/application/service/position/BizHandler.java

@@ -0,0 +1,21 @@
+package com.smppw.analysis.application.service.position;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+
+/**
+ * @author wangzaijun
+ * @date 2023/7/14 17:07
+ * @description 持仓分析业务处理接口
+ */
+public interface BizHandler<P extends BaseParams, R> {
+    R bizHandle(P params);
+
+    /**
+     * 获取执行时长
+     *
+     * @return 时长,单位ms
+     */
+    default long getExecuteTime() {
+        return 0L;
+    }
+}

+ 26 - 0
src/main/java/com/smppw/analysis/application/service/position/BizHandlerConstants.java

@@ -0,0 +1,26 @@
+package com.smppw.analysis.application.service.position;
+
+/**
+ * @author wangzaijun
+ * @date 2023/7/14 17:35
+ * @description 持仓分析服务名静态常量
+ */
+public final class BizHandlerConstants {
+    public static final String ASSET_ALLOCATION = "assetAllocation";
+    public static final String LEVERAGE_CHANGE = "leverageChange";
+    public static final String POSITION_LIST = "positionList";
+    public static final String POSITION_PARAMS = "positionParams";
+    public static final String CHANGE_NUMBER = "changeNumber";
+    public static final String STOCK_CONCENTRATION = "stockConcentration";
+    public static final String INDUSTRY_ALLOCATION = "industryAllocation";
+    public static final String LIQUIDITY_ALLOCATION = "liquidityAllocation";
+    public static final String RISK_EXPOSURE = "riskExposure";
+    public static final String STOCK_PERFORMANCE_ATTRIBUTION = "stockPerformanceAttribution";
+    public static final String STYLE_ALLOCATION = "styleAllocation";
+    public static final String MARGINAL_RISK_CONTRIBUTION = "marginalRiskContribution";
+    public static final String BOND_CONCENTRATION = "bondConcentration";
+
+    private BizHandlerConstants() {
+
+    }
+}

+ 35 - 0
src/main/java/com/smppw/analysis/application/service/position/BizHandlerFactor.java

@@ -0,0 +1,35 @@
+package com.smppw.analysis.application.service.position;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.text.CharSequenceUtil;
+import com.smppw.analysis.application.dto.position.BaseParams;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+@Component
+public class BizHandlerFactor {
+    private static final BizHandler<BaseParams, Object> EMPTY = p -> null;
+    private static final Map<String, BizHandler<? extends BaseParams, ?>> BIZ_HANDLER_STRATEGY = MapUtil.newHashMap(32);
+
+    public BizHandlerFactor(Map<String, BizHandler<? extends BaseParams, ?>> bizHandlerMap) {
+        BIZ_HANDLER_STRATEGY.putAll(bizHandlerMap);
+    }
+
+    @SuppressWarnings("unchecked")
+    public <P extends BaseParams, R> BizHandler<P, R> getBizHandlerInstance(String strategyKey) {
+        BizHandler<P, R> empty = (BizHandler<P, R>) EMPTY;
+        if (CharSequenceUtil.isBlank(strategyKey)) {
+            return empty;
+        }
+        BizHandler<? extends BaseParams, ?> bizHandler = BIZ_HANDLER_STRATEGY.get(strategyKey);
+        if (bizHandler == null) {
+            return empty;
+        }
+        try {
+            return (BizHandler<P, R>) bizHandler;
+        } catch (Exception e) {
+            return empty;
+        }
+    }
+}

+ 32 - 0
src/main/java/com/smppw/analysis/application/service/position/PositionLoad.java

@@ -0,0 +1,32 @@
+package com.smppw.analysis.application.service.position;
+
+import com.smppw.analysis.application.dto.position.BaseParams;
+import com.smppw.analysis.application.dto.position.FundPositionBaseInfo;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+
+import java.util.List;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/7 9:33
+ * @description 数据加载接口,提供统一的获取规定数据的接口
+ */
+public interface PositionLoad {
+    /**
+     * 获取基金持仓基本信息,包括总资产、资产净值等
+     *
+     * @param params 请求参数
+     * @param <P>    类型参数
+     * @return 区间内每个估值日期的基本信息
+     */
+    <P extends BaseParams> List<FundPositionBaseInfo> getFundPositionBaseInfo(P params);
+
+    /**
+     * 获取基金持仓详情,后续可以把市值、占比和标的提取出来,加快部分接口接口响应
+     *
+     * @param <P>    类型参数
+     * @param params 请求参数
+     * @return 区间内每个估值日期的成份持仓详情
+     */
+    <P extends BaseParams> List<FundPositionDetail> loadPositionDetail(P params);
+}

+ 59 - 0
src/main/java/com/smppw/analysis/application/service/position/PositionLoadFactory.java

@@ -0,0 +1,59 @@
+package com.smppw.analysis.application.service.position;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.map.MapUtil;
+import com.smppw.analysis.application.dto.position.BaseParams;
+import com.smppw.analysis.application.dto.position.FundPositionBaseInfo;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.domain.service.BaseInfoService;
+import com.smppw.constants.SecType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/7 14:04
+ * @description 价值持仓数据的工厂
+ */
+@Component
+public class PositionLoadFactory {
+    private static final Logger LOGGER = LoggerFactory.getLogger(PositionLoadFactory.class);
+    private static final Map<String, PositionLoad> LOAD_STRATEGY = MapUtil.newHashMap(8);
+    private final BaseInfoService secTypeService;
+
+    public PositionLoadFactory(Map<String, PositionLoad> loadMap, BaseInfoService secTypeService) {
+        LOAD_STRATEGY.putAll(loadMap);
+        this.secTypeService = secTypeService;
+    }
+
+    /**
+     * 提供一个根据基金id获取持仓数据加载接口的方法,没有找到的话返回一个空数据的策略接口并记录日志
+     * 仅支持私有基金和公募基金,不支持私募基金了
+     *
+     * @param fundId 基金id
+     * @return 数据加载策略
+     */
+    public PositionLoad getPositionLoadInstance(String fundId) {
+        List<String> supportTypes = ListUtil.of(SecType.PUBLICLY_OFFERED_FUNDS, SecType.PRIVATE_FUND);
+        String fundType = this.secTypeService.getFundType(fundId);
+        if (fundType == null || !supportTypes.contains(fundType)) {
+            LOGGER.warn(String.format("fundType[%s-%s] not find positionLoad strategy.", fundId, fundType));
+            return new PositionLoad() {
+                @Override
+                public <P extends BaseParams> List<FundPositionBaseInfo> getFundPositionBaseInfo(P params) {
+                    return ListUtil.empty();
+                }
+                @Override
+                public <P extends BaseParams> List<FundPositionDetail> loadPositionDetail(P params) {
+                    return ListUtil.empty();
+                }
+            };
+        } else {
+            return LOAD_STRATEGY.get(fundType);
+        }
+    }
+}

+ 30 - 0
src/main/java/com/smppw/analysis/application/service/position/PrivatePositionLoad.java

@@ -0,0 +1,30 @@
+//package com.smppw.fofapi.service.position;
+//
+//import com.smppw.fofapi.conts.SecType;
+//import com.smppw.analysis.application.dto.position.BaseParams;
+//import com.smppw.analysis.application.dto.position.FundPositionDetail;
+//import com.smppw.fofapi.service.CmFundPositionService;
+//import org.springframework.stereotype.Component;
+//
+//import java.util.List;
+//
+///**
+// * @author wangzaijun
+// * @date 2023/6/7 9:52
+// * @description 私有基金持仓数据获取逻辑,直接继承私募基金处理接口
+// */
+//@Component(SecType.PRIVATE_FUND)
+//public class PrivatePositionLoad extends PrivatelyFundPositionLoad {
+//    public PrivatePositionLoad(CmFundPositionService cmFundPositionService) {
+//        super(cmFundPositionService);
+//    }
+//
+//    @Override
+//    public <P extends BaseParams> List<FundPositionDetail> loadPositionDetail(P params) {
+//        List<FundPositionDetail> dataList = super.loadPositionDetail(params);
+//        for (FundPositionDetail detail : dataList) {
+//            detail.setFundType(SecType.PRIVATE_FUND);
+//        }
+//        return dataList;
+//    }
+//}

+ 126 - 0
src/main/java/com/smppw/analysis/application/service/position/PrivatelyFundPositionLoad.java

@@ -0,0 +1,126 @@
+//package com.smppw.analysis.application.service.position;
+//
+//import cn.hutool.core.collection.ListUtil;
+//import cn.hutool.core.date.DateUtil;
+//import cn.hutool.core.util.StrUtil;
+//import com.smppw.analysis.application.dto.position.*;
+//import org.springframework.stereotype.Component;
+//
+//import java.math.BigDecimal;
+//import java.util.List;
+//
+///**
+// * @author wangzaijun
+// * @date 2023/6/7 9:52
+// * @description 私募基金持仓数据获取逻辑,私募基金的支持会在基础上做自建基金,不直接支持私募基金的持仓分析
+// */
+//@Component(SecType.PRIVATELY_OFFERED_FUND)
+//public class PrivatelyFundPositionLoad extends AbstractPositionLoad {
+//    private final CmFundPositionService cmFundPositionService;
+//
+//    public PrivatelyFundPositionLoad(CmFundPositionService cmFundPositionService) {
+//        this.cmFundPositionService = cmFundPositionService;
+//    }
+//
+//    @Override
+//    public <P extends BaseParams> List<FundPositionBaseInfo> getFundPositionBaseInfo(P params) {
+//        List<CmUserValuationTableDO> dataList = this.cmFundPositionService.fundPositionBaseInfos(params.getFundId(),
+//                params.getStartDate(), params.getEndDate());
+//        List<FundPositionBaseInfo> resultList = ListUtil.list(false);
+//        for (CmUserValuationTableDO temp : dataList) {
+//            if (temp == null || temp.getValuationDate() == null) {
+//                continue;
+//            }
+//            FundPositionBaseInfo info = new FundPositionBaseInfo();
+//            info.setFundId(temp.getFundId());
+//            info.setDate(DateUtil.formatDate(temp.getValuationDate()));
+//            info.setTotalAsset(temp.getTotalMarketValue());
+//            info.setAssetNv(temp.getNetAssetMarketValue());
+//            info.setIncrement(temp.getIncrement());
+//            resultList.add(info);
+//        }
+//        return resultList;
+//    }
+//
+//    @Override
+//    public <P extends BaseParams> List<FundPositionDetail> loadPositionDetail(P params) {
+//        List<CmFundPositionDetailDO> dataList = this.cmFundPositionService.fundPositionDetailList(params.getFundId());
+//        List<FundPositionDetail> resultList = ListUtil.list(false);
+//        for (CmFundPositionDetailDO temp : dataList) {
+//            if (temp == null || temp.getValuationDate() == null || temp.getSecuritiesCode() == null || temp.getSubType() == null) {
+//                continue;
+//            }
+//            FundPositionDetail detail = new FundPositionDetail();
+//            detail.setFundId(temp.getFundId());
+//            detail.setFundType(SecType.PRIVATELY_OFFERED_FUND);
+//            detail.setSubjectCode(temp.getSubjectCode());
+//            detail.setLevel(temp.getLevel());
+//            detail.setSubType(temp.getSubType());
+//            String date = DateUtil.formatDate(temp.getValuationDate());
+//            detail.setDate(date);
+//            detail.setRef(new ValueLabelVO(temp.getSecuritiesCode(), temp.getSecuritiesName()));
+//            detail.setAsset(this.buildAssetValueLabel(temp));
+//            detail.setMarketValue(temp.getMarketValue());
+//            detail.setRatioNv(temp.getMarketValueRatio());
+//            detail.setNumber(temp.getSecuritiesAmount());
+//            BigDecimal increment = temp.getIncrement();
+//            detail.setValueChange(increment);
+//            detail.setCost(temp.getNetCost());
+//            detail.setSuspension(temp.getHaltInfo());
+//            detail.setSharesNature(temp.getLongShort());
+//            detail.setSecId(temp.getSecId());
+//            resultList.add(detail);
+//        }
+//        // 分组合并数据,统一给私募基金做轧差处理
+////        List<FundPositionDetail> newResultList = ListUtil.list(false);
+////        List<FundPositionDetail> collect = resultList.stream()
+////                .filter(e -> e.getSubType() == null || e.getSubType() == 20).collect(Collectors.toList());
+////        if (CollUtil.isNotEmpty(collect)) {
+////            newResultList.addAll(collect);
+////        }
+////        resultList.stream().filter(e -> e.getSubType() != null && e.getSubType() != 20)
+////                .collect(Collectors.groupingBy(e -> e.getDate() + e.getRef().toString()))
+////                .forEach((ref, transfer) -> transfer.stream().reduce((a, b) -> {
+////                    a.setMarketValue(a.getMarketValue().add(b.getMarketValue()));
+////                    a.setRatioNv(a.getRatioNv().add(b.getRatioNv()));
+////                    a.setNumber(a.getNumber().add(b.getNumber()));
+////                    a.setValueChange(a.getValueChange().add(b.getValueChange()));
+////                    a.setCost(a.getCost().add(b.getCost()));
+////                    a.setSuspension(a.getSuspension());
+////                    a.setSharesNature(1); // 合并后全为1
+////                    a.setSecId(a.getSecId());
+////                    return a;
+////                }).ifPresent(newResultList::add));
+////        newResultList.sort(Comparator.comparing(FundPositionDetail::getDate));
+//        return resultList;
+//    }
+//
+//    /**
+//     * 构建资产类别关系映射
+//     *
+//     * @param detail 持仓详情信息
+//     * @return 资产类别关系映射
+//     */
+//    private ValueLabelVO buildAssetValueLabel(CmFundPositionDetailDO detail) {
+//        Integer secType = detail.getSecType();
+//        String subjectCode = detail.getSubjectCode();
+//        if (secType == null) {
+//            return null;
+//        }
+//        AssetCategoryEnum categoryEnum = null;
+//        if (secType == 0) {
+//            categoryEnum = AssetCategoryEnum.STOCK;
+//        } else if (secType == 1 && !StrUtil.containsAny(subjectCode, PositionConstants.SALE_CODES)) {
+//            // 去掉债券下的正回购和逆回购
+//            categoryEnum = AssetCategoryEnum.BOND;
+//        } else if (secType == 3 || secType == 7) {
+//            categoryEnum = AssetCategoryEnum.FUTURE;
+//        } else if (secType == 6) {
+//            categoryEnum = AssetCategoryEnum.FUND;
+//            // 去掉现金
+////        } else if (secType == 2) {
+////            categoryEnum = AssetCategoryEnum.CASH;
+//        }
+//        return AssetCategoryEnum.buildValueLabelByAsset(categoryEnum);
+//    }
+//}

+ 92 - 0
src/main/java/com/smppw/analysis/application/service/position/PubliclyFundPositionLoad.java

@@ -0,0 +1,92 @@
+package com.smppw.analysis.application.service.position;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import com.smppw.analysis.application.dto.position.*;
+import com.smppw.analysis.domain.dao.PubliclyFundPositionDao;
+import com.smppw.analysis.domain.entity.FundPositionBaseInfoDO;
+import com.smppw.analysis.domain.entity.FundPositionDetailDO;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.constants.SecType;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/7 9:52
+ * @description 公募基金持仓数据获取逻辑
+ */
+@Component(SecType.PUBLICLY_OFFERED_FUNDS)
+public class PubliclyFundPositionLoad extends AbstractPositionLoad {
+    private static final Map<Integer, AssetCategoryEnum> ASSET_TYPE_MAPPER = MapUtil.newHashMap();
+
+    static {
+        ASSET_TYPE_MAPPER.put(0, AssetCategoryEnum.STOCK);
+        ASSET_TYPE_MAPPER.put(1, AssetCategoryEnum.BOND);
+        ASSET_TYPE_MAPPER.put(3, AssetCategoryEnum.FUTURE);
+        ASSET_TYPE_MAPPER.put(6, AssetCategoryEnum.FUND); // 公募
+        ASSET_TYPE_MAPPER.put(7, AssetCategoryEnum.FUND); // 私募
+    }
+
+    private final PubliclyFundPositionDao publiclyFundPositionDao;
+
+    public PubliclyFundPositionLoad(PubliclyFundPositionDao publiclyFundPositionDao) {
+        this.publiclyFundPositionDao = publiclyFundPositionDao;
+    }
+
+    @Override
+    public <P extends BaseParams> List<FundPositionBaseInfo> getFundPositionBaseInfo(P params) {
+        List<FundPositionBaseInfoDO> doList = this.publiclyFundPositionDao.fundPositionBaseInfos(params.getFundId(),
+                params.getStartDate(), params.getEndDate());
+        List<FundPositionBaseInfo> resultList = ListUtil.list(false);
+        for (FundPositionBaseInfoDO temp : doList) {
+            if (temp == null || temp.getReportDate() == null) {
+                continue;
+            }
+            String date = DateUtil.formatDate(temp.getReportDate());
+            FundPositionBaseInfo info = new FundPositionBaseInfo();
+            info.setFundId(temp.getFundId());
+            info.setDate(date);
+            info.setTotalAsset(temp.getTotalAsset());
+            info.setAssetNv(temp.getAssetNv());
+            resultList.add(info);
+        }
+        return resultList;
+    }
+
+    @Override
+    public <P extends BaseParams> List<FundPositionDetail> loadPositionDetail(P params) {
+        List<FundPositionDetailDO> dataList = this.publiclyFundPositionDao.positionDetailList(params.getFundId());
+        List<FundPositionDetail> resultList = ListUtil.list(false);
+        for (FundPositionDetailDO temp : dataList) {
+            if (temp == null || temp.getValuationDate() == null) {
+                continue;
+            }
+            // 公募基金没有成本、估值增值、增值比例和停牌信息
+            FundPositionDetail detail = new FundPositionDetail();
+            detail.setFundId(temp.getFundId());
+            detail.setFundType(SecType.PUBLICLY_OFFERED_FUNDS);
+            detail.setDate(DateUtil.formatDate(temp.getValuationDate()));
+            String secCode = temp.getSecuritiesCode();
+            AssetCategoryEnum categoryEnum = ASSET_TYPE_MAPPER.get(temp.getSecType());
+            ValueLabelVO ref = new ValueLabelVO(secCode, temp.getSecuritiesName());
+            if (categoryEnum == AssetCategoryEnum.FUTURE && StrUtil.containsAny(secCode, PositionConstants.BOND_BREED_ARR)) {
+                // 公募基金国债期货名称特殊处理
+                ref = new ValueLabelVO(secCode, secCode);
+            }
+            detail.setRef(ref);
+            detail.setAsset(AssetCategoryEnum.buildValueLabelByAsset(categoryEnum));
+            detail.setMarketValue(temp.getMarketValue());
+            detail.setRatioNv(temp.getMarketValueRatio());
+            detail.setNumber(temp.getSecuritiesAmount());
+            detail.setSharesNature(temp.getSharesNature());
+            detail.setSecId(temp.getSecId());
+            resultList.add(detail);
+        }
+        return resultList;
+    }
+}

+ 584 - 0
src/main/java/com/smppw/analysis/application/service/position/bond/BondPositionService.java

@@ -0,0 +1,584 @@
+//package com.smppw.fofapi.service.position.bond;
+//
+//import cn.hutool.core.collection.CollectionUtil;
+//import cn.hutool.core.collection.ListUtil;
+//import cn.hutool.core.date.DateUnit;
+//import cn.hutool.core.date.DateUtil;
+//import cn.hutool.core.map.MapUtil;
+//import com.smppw.fofapi.conts.SecType;
+//import com.smppw.fofapi.dos.valuation.CmFundPositionDetailDO;
+//import com.smppw.fofapi.dos.valuation.CmUserValuationTableDO;
+//import com.smppw.fofapi.enums.valuation.HoldingType;
+//import com.smppw.fofapi.mapper.baseinfo.write.hfdbcore.BondBasicInformationDOMapper;
+//import com.smppw.fofapi.pojo.ValueLabelVO;
+//import com.smppw.fofapi.pojo.core.FundPositionBaseInfoDO;
+//import com.smppw.fofapi.pojo.core.FundPositionDetailDO;
+//import com.smppw.fofapi.pojo.dos.core.BondBasicInformationDO;
+//import com.smppw.fofapi.pojo.dos.core.BondCreditGradingInfo;
+//import com.smppw.fofapi.pojo.dos.core.BondIndustryInfo;
+//import com.smppw.analysis.application.dto.position.MarketValueRatio;
+//import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+//import com.smppw.analysis.application.dto.position.bond.*;
+//import com.smppw.analysis.application.dto.position.stock.ConcentrationVO;
+//import com.smppw.fofapi.service.CmFundPositionService;
+//import com.smppw.fofapi.service.info.type.SecTypeService;
+//import com.smppw.fofapi.service.position.FundPositionBaseService;
+//import com.smppw.fofapi.util.BigDecimalUtils;
+//import com.smppw.fofapi.util.BinarySearchUtil;
+//import lombok.extern.slf4j.Slf4j;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.stereotype.Service;
+//
+//import javax.annotation.Resource;
+//import java.math.BigDecimal;
+//import java.util.*;
+//import java.util.stream.Collectors;
+//
+///**
+// * @author Rain
+// * @date 2023/6/12 10:05
+// * @description
+// */
+//@Service
+//@Slf4j
+//public class BondPositionService {
+//
+//    /**
+//     * 国债
+//     */
+//    private static final String COUNTRY_BOND = "countryBond";
+//    /**
+//     * 公司债
+//     */
+//    private static final String COMPANY_BOND = "companyBond";
+//    /**
+//     * 可转债
+//     */
+//    private static final String CONVERTIBLE_BOND = "convertibleBond";
+//    /**
+//     * 金融债
+//     */
+//    private static final String FINANCIAL_BOND = "financialBond";
+//    /**
+//     * 资产支持证券
+//     */
+//    private static final String ASSET_BACK_BOND = "assetBackBond";
+//    /**
+//     * 信用风险缓释工具
+//     */
+//    private static final String CREDIT_RISK_BOND = "creditRiskBond";
+//    /**
+//     * 其他
+//     */
+//    private static final String OTHERS = "others";
+//    private static final Map<String, List<Integer>> BOND_DESC_MAP = new HashMap<String, List<Integer>>() {
+//        private static final long serialVersionUID = -6269769194874256694L;
+//
+//        {
+//            put(COUNTRY_BOND, CollectionUtil.newArrayList(4, 5, 16));
+//            put(COMPANY_BOND, CollectionUtil.newArrayList(1, 6, 13, 14, 15, 17, 18, 19, 20, 22, 24, 26, 32, 33, 34, 35, 36, 40, 23, 31));
+//            put(CONVERTIBLE_BOND, CollectionUtil.newArrayList(10, 28, 29));
+//            put(FINANCIAL_BOND, CollectionUtil.newArrayList(2, 3, 9, 27, 39));
+//            put(ASSET_BACK_BOND, CollectionUtil.newArrayList(7, 8, 12, 21, 25));
+//            put(CREDIT_RISK_BOND, CollectionUtil.newArrayList(37, 38));
+//        }
+//    };
+//    private static final Map<Long, String> POSITION_NAME = MapUtil.newHashMap(true);
+//    private static final Map<Long, String> CREDIT_GRADING_NAME = MapUtil.newHashMap(true);
+//
+//    static {
+//        POSITION_NAME.put(1L, "最大持仓");
+//        POSITION_NAME.put(3L, "前三大持仓");
+//        POSITION_NAME.put(5L, "前五大持仓");
+//        POSITION_NAME.put(-1L, "债券持仓");
+//    }
+//
+//    static {
+//        CREDIT_GRADING_NAME.put(1000L, "AAA+");
+//        CREDIT_GRADING_NAME.put(999L, "AAA");
+//        CREDIT_GRADING_NAME.put(990L, "AAA-");
+//        CREDIT_GRADING_NAME.put(980L, "AA+");
+//        CREDIT_GRADING_NAME.put(970L, "AA");
+//        CREDIT_GRADING_NAME.put(960L, "AA-");
+//        CREDIT_GRADING_NAME.put(950L, "A+");
+//        CREDIT_GRADING_NAME.put(940L, "A");
+//        CREDIT_GRADING_NAME.put(930L, "A-");
+//        CREDIT_GRADING_NAME.put(899L, "BBB+");
+//        CREDIT_GRADING_NAME.put(895L, "BBB");
+//        CREDIT_GRADING_NAME.put(891L, "BBB-");
+//        CREDIT_GRADING_NAME.put(880L, "BB+");
+//        CREDIT_GRADING_NAME.put(870L, "BB");
+//        CREDIT_GRADING_NAME.put(860L, "BB-");
+//        CREDIT_GRADING_NAME.put(850L, "B+");
+//        CREDIT_GRADING_NAME.put(840L, "B");
+//        CREDIT_GRADING_NAME.put(830L, "B-");
+//        CREDIT_GRADING_NAME.put(799L, "CCC");
+//        CREDIT_GRADING_NAME.put(770L, "CC");
+//        CREDIT_GRADING_NAME.put(760L, "C");
+//
+//    }
+//
+//    private final Map<String, String> NAME_MAPPING = new HashMap<String, String>() {
+//        private static final long serialVersionUID = -8630175938931089463L;
+//
+//        {
+//            put(COUNTRY_BOND, "国债");
+//            put(COMPANY_BOND, "企业(公司)债");
+//            put(CONVERTIBLE_BOND, "可转债");
+//            put(FINANCIAL_BOND, "金融债");
+//            put(ASSET_BACK_BOND, "资产支持证券");
+//            put(CREDIT_RISK_BOND, "信用风险缓释工具");
+//            put(OTHERS, "其他");
+//        }
+//    };
+//    List<String> bondTypeList = CollectionUtil.newArrayList(COUNTRY_BOND, COMPANY_BOND, CONVERTIBLE_BOND, FINANCIAL_BOND, ASSET_BACK_BOND,
+//            CREDIT_RISK_BOND, OTHERS);
+//    @Autowired
+//    private SecTypeService secTypeService;
+//    @Autowired
+//    private CmFundPositionService cmFundPositionService;
+//    @Autowired
+//    private FundPositionBaseService fundPositionBaseService;
+//    @Resource
+//    private BondBasicInformationDOMapper bondBasicInformationDOMapper;
+//    @Autowired
+//    private ConcentrationBizBondHandler concentrationBizBondHandler;
+//
+//    public Map<String, Object> getBondSortAllocation(BondSortAllocationParam param) {
+//        String fundType = this.secTypeService.getFundType(param.getFundId());
+//        List<BondBasicInformationDO> bondBasicInformationDOS = bondBasicInformationDOMapper.selectAllMappings();
+//        Map<String, Integer> collect = bondBasicInformationDOS.stream().collect(Collectors.toMap(BondBasicInformationDO::getSecId, BondBasicInformationDO::getBondNature, (p1, p2) -> p2));
+//        if (SecType.PUBLICLY_OFFERED_FUNDS.equals(fundType)) {
+//            List<BondSortAssetDTO> list = CollectionUtil.newArrayList();
+//            List<FundPositionBaseInfoDO> fundPositionBaseInfoDOS = this.fundPositionBaseService.fundPositionBaseInfos(param.getFundId(),
+//                    param.getStartDate(), param.getEndDate());
+//            if (CollectionUtil.isNotEmpty(fundPositionBaseInfoDOS)) {
+//                Map<String, BigDecimal> netMarketValueMap = fundPositionBaseInfoDOS.stream()
+//                        .collect(TreeMap::new, (map, item) -> map.put(DateUtil.formatDate(item.getReportDate()), item.getAssetNv()), TreeMap::putAll);
+//                List<FundPositionDetailDO> fundPositionDetailDOS = this.fundPositionBaseService.positionDetailList(param.getFundId());
+//                if (CollectionUtil.isNotEmpty(fundPositionBaseInfoDOS)) {
+//                    handlePublicFundNetValue(collect, list, netMarketValueMap, fundPositionDetailDOS);
+//                }
+//                Map<String, List<BondSortAssetDTO>> treeMap = filterUnNeedKey(list);
+//                return MapUtil.<String, Object>builder().put("dataset", treeMap)
+//                        .put("productNameMapping", NAME_MAPPING)
+//                        .build();
+//            }
+//        } else if (SecType.PRIVATE_FUND.equals(fundType)) {
+//            List<BondSortAssetDTO> list = CollectionUtil.newArrayList();
+//            List<CmUserValuationTableDO> cmUserValuationTableDOS = cmFundPositionService.fundPositionBaseInfos(param.getFundId(),
+//                    param.getStartDate(), param.getEndDate());
+//            if (CollectionUtil.isNotEmpty(cmUserValuationTableDOS)) {
+//                Map<String, BigDecimal> netMarketValueMap = cmUserValuationTableDOS.stream()
+//                        .collect(TreeMap::new, (map, item) -> map.put(DateUtil.formatDate(item.getValuationDate()), item.getNetAssetMarketValue()), TreeMap::putAll);
+//                List<CmFundPositionDetailDO> cmFundPositionDetailDOS = cmFundPositionService.fundPositionDetailList(param.getFundId());
+//                if (CollectionUtil.isNotEmpty(cmFundPositionDetailDOS)) {
+//                    List<CmFundPositionDetailDO> filterPositionDetails = cmFundPositionDetailDOS.stream().filter(p -> p.getSecType() != null)
+//                            .filter(p -> Objects.nonNull(p.getLevel()))
+//                            .filter(p -> p.getLevel() >= 4)
+//                            .filter(p -> p.getSecType() == 1)
+//                            .filter(p -> Objects.nonNull(p.getSubType()) && p.getSubType() != HoldingType.BondBack.getId())
+//                            .collect(Collectors.toList());
+//                    handlePrivateFundNetValue(collect, list, netMarketValueMap, filterPositionDetails);
+//                }
+//            }
+//            Map<String, List<BondSortAssetDTO>> treeMap = filterUnNeedKey(list);
+//            return MapUtil.<String, Object>builder().put("dataset", treeMap)
+//                    .put("productNameMapping", NAME_MAPPING)
+//                    .build();
+//        }
+//        return null;
+//    }
+//
+//    private void handlePrivateFundNetValue(Map<String, Integer> collect, List<BondSortAssetDTO> list, Map<String, BigDecimal> netMarketValueMap, List<CmFundPositionDetailDO> filterPositionDetails) {
+//        if (CollectionUtil.isNotEmpty(filterPositionDetails)) {
+//            Map<String, List<CmFundPositionDetailDO>> filterByDateMap = filterPositionDetails.stream().collect(Collectors.groupingBy(p -> DateUtil.formatDate(p.getValuationDate()), TreeMap::new, Collectors.toList()));
+//            for (Map.Entry<String, BigDecimal> entry : netMarketValueMap.entrySet()) {
+//                String date = entry.getKey();
+//                List<CmFundPositionDetailDO> cmFundPositionDetailDOS1 = filterByDateMap.get(date);
+//                if (CollectionUtil.isNotEmpty(cmFundPositionDetailDOS1)) {
+//                    for (CmFundPositionDetailDO cmFundPositionDetailDO : cmFundPositionDetailDOS1) {
+//                        String secId = cmFundPositionDetailDO.getSecId();
+//                        Integer integer = collect.get(secId);
+//                        String bondTypeBySecId = getBondTypeBySecId(integer);
+//                        cmFundPositionDetailDO.setBondType(bondTypeBySecId);
+//                    }
+//                    Map<String, List<CmFundPositionDetailDO>> bondTypeMap = cmFundPositionDetailDOS1.stream().collect(Collectors.groupingBy(p -> p.getBondType()));
+//                    for (String type : bondTypeList) {
+//                        BondSortAssetDTO bondSortAssetDTO = new BondSortAssetDTO();
+//                        bondSortAssetDTO.setBondType(type);
+//                        bondSortAssetDTO.setValuationDate(date);
+//                        List<CmFundPositionDetailDO> cmFundPositionDetailDOS2 = bondTypeMap.get(type);
+//                        if (CollectionUtil.isNotEmpty(cmFundPositionDetailDOS2)) {
+//                            BigDecimal curSumMarketValue = cmFundPositionDetailDOS2.stream()
+//                                    .map(CmFundPositionDetailDO::getMarketValue).filter(Objects::nonNull)
+//                                    .reduce(BigDecimal::add).orElse(null);
+//                            bondSortAssetDTO.setMarketValue(curSumMarketValue);
+//                            bondSortAssetDTO.setNetValueRatio(BigDecimalUtils.divide(curSumMarketValue, entry.getValue()));
+//                            list.add(bondSortAssetDTO);
+//                        } else {
+//                            bondSortAssetDTO.setMarketValue(null);
+//                            bondSortAssetDTO.setNetValueRatio(null);
+//                            list.add(bondSortAssetDTO);
+//                        }
+//                    }
+//                } else {
+//                    handleEmptyBondAsset(list, date);
+//                }
+//            }
+//        }
+//    }
+//
+//    private void handlePublicFundNetValue(Map<String, Integer> collect, List<BondSortAssetDTO> list, Map<String, BigDecimal> netMarketValueMap, List<FundPositionDetailDO> fundPositionDetailDOS) {
+//        List<FundPositionDetailDO> filterPositionDetails = fundPositionDetailDOS.stream().filter(p -> p.getSecType() != null)
+//                .filter(p -> p.getSecType() == 1).collect(Collectors.toList());
+//        Map<String, List<FundPositionDetailDO>> filterByDateMap = filterPositionDetails.stream().collect(Collectors.groupingBy(p -> DateUtil.formatDate(p.getValuationDate()), TreeMap::new, Collectors.toList()));
+//        for (Map.Entry<String, BigDecimal> entry : netMarketValueMap.entrySet()) {
+//            String date = entry.getKey();
+//            List<FundPositionDetailDO> cmFundPositionDetailDOS1 = filterByDateMap.get(date);
+//            if (CollectionUtil.isNotEmpty(cmFundPositionDetailDOS1)) {
+//                // 判断是哪种类型
+//                for (FundPositionDetailDO cmFundPositionDetailDO : cmFundPositionDetailDOS1) {
+//                    String secId = cmFundPositionDetailDO.getSecId();
+//                    Integer integer = collect.get(secId);
+//                    String bondTypeBySecId = getBondTypeBySecId(integer);
+//                    cmFundPositionDetailDO.setBondType(bondTypeBySecId);
+//                }
+//                Map<String, List<FundPositionDetailDO>> bondTypeMap = cmFundPositionDetailDOS1.stream().collect(Collectors.groupingBy(p -> p.getBondType()));
+//                for (String type : bondTypeList) {
+//                    BondSortAssetDTO bondSortAssetDTO = new BondSortAssetDTO();
+//                    bondSortAssetDTO.setBondType(type);
+//                    bondSortAssetDTO.setValuationDate(date);
+//                    List<FundPositionDetailDO> cmFundPositionDetailDOS2 = bondTypeMap.get(type);
+//                    if (CollectionUtil.isNotEmpty(cmFundPositionDetailDOS2)) {
+//                        BigDecimal ratio = cmFundPositionDetailDOS2.stream()
+//                                .map(FundPositionDetailDO::getMarketValueRatio).filter(Objects::nonNull)
+//                                .reduce(BigDecimal::add).orElse(null);
+//                        bondSortAssetDTO.setNetValueRatio(ratio);
+//                        BigDecimal marketValue = cmFundPositionDetailDOS2.stream().map(FundPositionDetailDO::getMarketValue).filter(Objects::nonNull)
+//                                .reduce(BigDecimal::add).orElse(null);
+//                        bondSortAssetDTO.setMarketValue(marketValue);
+//                        list.add(bondSortAssetDTO);
+//                    } else {
+//                        bondSortAssetDTO.setMarketValue(null);
+//                        bondSortAssetDTO.setNetValueRatio(null);
+//                        list.add(bondSortAssetDTO);
+//                    }
+//
+//                }
+//            } else {
+//                handleEmptyBondAsset(list, date);
+//            }
+//        }
+//    }
+//
+//    private void handleEmptyBondAsset(List<BondSortAssetDTO> list, String date) {
+//        for (String type : bondTypeList) {
+//            BondSortAssetDTO bondSortAssetDTO = new BondSortAssetDTO();
+//            bondSortAssetDTO.setBondType(type);
+//            bondSortAssetDTO.setValuationDate(date);
+//            bondSortAssetDTO.setMarketValue(null);
+//            bondSortAssetDTO.setNetValueRatio(null);
+//            list.add(bondSortAssetDTO);
+//        }
+//    }
+//
+//    private Map<String, List<BondSortAssetDTO>> filterUnNeedKey(List<BondSortAssetDTO> list) {
+//        Map<String, List<BondSortAssetDTO>> treeMap = list.stream()
+//                .collect(Collectors.groupingBy(BondSortAssetDTO::getBondType, LinkedHashMap::new, Collectors.toList()));
+//        List<String> needFilterKey = CollectionUtil.newArrayList();
+//        for (Map.Entry<String, List<BondSortAssetDTO>> entry : treeMap.entrySet()) {
+//            String key = entry.getKey();
+//            List<BondSortAssetDTO> value = entry.getValue();
+//            if (CollectionUtil.isNotEmpty(value)) {
+//                int emptyCount = 0;
+//                for (BondSortAssetDTO bondSortAssetDTO : value) {
+//                    if (bondSortAssetDTO.getMarketValue() == null && bondSortAssetDTO.getNetValueRatio() == null) {
+//                        emptyCount++;
+//                    }
+//                }
+//                if (emptyCount == value.size()) {
+//                    needFilterKey.add(key);
+//                }
+//            }
+//        }
+//        if (CollectionUtil.isNotEmpty(needFilterKey)) {
+//            for (String key : needFilterKey) {
+//                treeMap.remove(key);
+//            }
+//        }
+//        return treeMap;
+//    }
+//
+//    private String getBondTypeBySecId(Integer secId) {
+//        if (secId == null) {
+//            return OTHERS;
+//        }
+//        for (Map.Entry<String, List<Integer>> entry : BOND_DESC_MAP.entrySet()) {
+//            String key = entry.getKey();
+//            List<Integer> value = entry.getValue();
+//            if (value.contains(secId)) {
+//                return key;
+//            }
+//        }
+//        return OTHERS;
+//    }
+//
+//    public Map<String, Object> getBondConcentration(BondSortAllocationParam param) {
+//        Map<String, Object> dataset = MapUtil.newHashMap();
+//        List<ConcentrationVO> concentrationVOS = concentrationBizBondHandler.bizHandle(param);
+//        List<ConcentrationBondVO> concentrationVOList = transfer2Bond(concentrationVOS);
+//        Set<String> bondIdSet = CollectionUtil.newHashSet();
+//        // 获取到所有的债券信息
+//        if (CollectionUtil.isNotEmpty(concentrationVOS)) {
+//            for (ConcentrationVO concentrationVO : concentrationVOS) {
+//                List<RefMarketValueRatio> position = concentrationVO.getPosition();
+//                if (CollectionUtil.isNotEmpty(position)) {
+//                    List<String> collect = position.stream().map(RefMarketValueRatio::getSecId).distinct().collect(Collectors.toList());
+//                    if (CollectionUtil.isNotEmpty(collect)) {
+//                        bondIdSet.addAll(collect);
+//                    }
+//                }
+//            }
+//        }
+//        Map<String, BondBasicInformationDO> collect = MapUtil.newHashMap();
+//        if (CollectionUtil.isNotEmpty(bondIdSet)) {
+//            List<BondBasicInformationDO> bondBasicInformationDOS = bondBasicInformationDOMapper.queryBondBasicInfo(CollectionUtil.newArrayList(bondIdSet));
+//            collect = bondBasicInformationDOS.stream()
+//                    .collect(Collectors.toMap(BondBasicInformationDO::getSecId, p -> p, (p1, p2) -> p2));
+//        }
+//
+//        Map<String, BondIndustryInfo> industryInfoMap = MapUtil.newHashMap();
+//        if (CollectionUtil.isNotEmpty(bondIdSet)) {
+//            List<BondIndustryInfo> bondIndustryInfos = bondBasicInformationDOMapper.queryBondIndustry(CollectionUtil.newArrayList(bondIdSet));
+//            if (CollectionUtil.isNotEmpty(bondIndustryInfos)) {
+//                industryInfoMap = bondIndustryInfos.stream().collect(Collectors.toMap(BondIndustryInfo::getSecId, p -> p, (p1, p2) -> p2));
+//            }
+//        }
+//
+//        for (ConcentrationBondVO concentrationBondVO : concentrationVOList) {
+//            List<RefBondMarketValueRatio> position = concentrationBondVO.getPosition();
+//            for (RefBondMarketValueRatio refBondMarketValueRatio : position) {
+//                String secId = refBondMarketValueRatio.getSecId();
+//                BondBasicInformationDO bondBasicInformationDO = collect.get(secId);
+//                if (bondBasicInformationDO != null) {
+//                    refBondMarketValueRatio.setBondType(bondBasicInformationDO.getBondType());
+//                    refBondMarketValueRatio.setNominalInterestRate(bondBasicInformationDO.getNominalInterestRate());
+//                    refBondMarketValueRatio.setCreditRating(bondBasicInformationDO.getCreditRating());
+//                    BondIndustryInfo bondIndustryInfo = industryInfoMap.get(secId);
+//                    refBondMarketValueRatio.setIndustryName(bondIndustryInfo == null ? null : bondIndustryInfo.getFirstIndustryName());
+//                    Date maturityDate = bondBasicInformationDO.getMaturityDate();
+//                    if (maturityDate != null) {
+//                        String date = concentrationBondVO.getDate();
+//                        long between = DateUtil.between(DateUtil.parse(date), maturityDate, DateUnit.DAY);
+//                        refBondMarketValueRatio.setRemainder(between / 365d);
+//                    }
+//                }
+//            }
+//        }
+//
+//        for (Long integer : POSITION_NAME.keySet()) {
+//            List<MarketValueRatio> tempList = ListUtil.list(true);
+//            for (ConcentrationBondVO vo : concentrationVOList) {
+//                MarketValueRatio mvr = new MarketValueRatio();
+//                mvr.setDate(vo.getDate());
+//                List<RefBondMarketValueRatio> resList = vo.getPosition();
+//                if (integer != -1L) {
+//                    // 不为-1时取前n条记录求和
+//                    resList = vo.getPosition().stream().limit(integer).collect(Collectors.toList());
+//                }
+//                BigDecimal marketValue = resList.stream().map(RefBondMarketValueRatio::getMarketValue)
+//                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
+//                BigDecimal ratio = resList.stream().map(RefBondMarketValueRatio::getRatio).filter(Objects::nonNull)
+//                        .reduce(BigDecimal::add).orElse(null);
+//                mvr.setMarketValue(marketValue);
+//                mvr.setRatio(ratio);
+//                tempList.add(mvr);
+//            }
+//            dataset.put(integer.toString(), tempList);
+//        }
+//        return MapUtil.<String, Object>builder().put("dataset", dataset)
+//                .put("productNameMapping", POSITION_NAME).put("table", concentrationVOList).build();
+//
+//    }
+//
+//    private List<ConcentrationBondVO> transfer2Bond(List<ConcentrationVO> concentrationVOS) {
+//        List<ConcentrationBondVO> res = CollectionUtil.newArrayList();
+//        if (CollectionUtil.isNotEmpty(concentrationVOS)) {
+//            for (ConcentrationVO concentrationVO : concentrationVOS) {
+//                ConcentrationBondVO item = new ConcentrationBondVO();
+//                item.setAssetNv(concentrationVO.getAssetNv());
+//                item.setDate(concentrationVO.getDate());
+//                List<RefMarketValueRatio> position = concentrationVO.getPosition();
+//                List<RefBondMarketValueRatio> list = CollectionUtil.newArrayList();
+//                if (CollectionUtil.isNotEmpty(position)) {
+//                    Map<ValueLabelVO, List<RefMarketValueRatio>> collect = position.stream().collect(Collectors.groupingBy(p -> p.getRef()));
+//                    for (Map.Entry<ValueLabelVO, List<RefMarketValueRatio>> entry : collect.entrySet()) {
+//                        ValueLabelVO key = entry.getKey();
+//                        List<RefMarketValueRatio> value = entry.getValue();
+//                        if (CollectionUtil.isNotEmpty(value)) {
+//                            if (value.size() == 1) {
+//                                RefMarketValueRatio refMarketValueRatio = value.get(0);
+//                                RefBondMarketValueRatio refBondMarketValueRatio = new RefBondMarketValueRatio();
+//                                refBondMarketValueRatio.setMarketValue(refMarketValueRatio.getMarketValue());
+//                                refBondMarketValueRatio.setRatio(refMarketValueRatio.getRatio());
+//                                refBondMarketValueRatio.setRef(refMarketValueRatio.getRef());
+//                                refBondMarketValueRatio.setSecId(refMarketValueRatio.getSecId());
+//                                list.add(refBondMarketValueRatio);
+//                            } else {
+//                                // 需要轧差
+//                                BigDecimal totalMarket = null;
+//                                BigDecimal ratio = null;
+//                                for (RefMarketValueRatio refMarketValueRatio : value) {
+//                                    totalMarket = BigDecimalUtils.add(totalMarket, refMarketValueRatio.getMarketValue());
+//                                    ratio = BigDecimalUtils.add(ratio, refMarketValueRatio.getRatio());
+//                                }
+//                                RefBondMarketValueRatio refBondMarketValueRatio = new RefBondMarketValueRatio();
+//                                refBondMarketValueRatio.setMarketValue(totalMarket);
+//                                refBondMarketValueRatio.setRatio(ratio);
+//                                refBondMarketValueRatio.setRef(value.get(0).getRef());
+//                                refBondMarketValueRatio.setSecId(value.get(0).getSecId());
+//                                list.add(refBondMarketValueRatio);
+//                            }
+//                        }
+//                    }
+//                }
+//                list.sort(Comparator.comparing(RefBondMarketValueRatio::getMarketValue).reversed());
+//                item.setPosition(list);
+//                res.add(item);
+//            }
+//        }
+//        return res;
+//    }
+//
+//    public Map<String, Object> getBondCreditGrading(BondSortAllocationParam param) {
+//        Map<String, Object> dataset = MapUtil.newHashMap(true);
+//        List<ConcentrationVO> concentrationVOS = concentrationBizBondHandler.bizHandle(param);
+//        List<CreditGradingBondVO> creditGradingBondVOS = transfer2BondCreditGrading(concentrationVOS);
+//        // 获取到评级
+//        Set<String> bondIdSet = CollectionUtil.newHashSet();
+//        if (CollectionUtil.isNotEmpty(concentrationVOS)) {
+//            for (ConcentrationVO concentrationVO : concentrationVOS) {
+//                List<RefMarketValueRatio> position = concentrationVO.getPosition();
+//                if (CollectionUtil.isNotEmpty(position)) {
+//                    List<String> collect = position.stream().map(RefMarketValueRatio::getSecId).distinct().collect(Collectors.toList());
+//                    if (CollectionUtil.isNotEmpty(collect)) {
+//                        bondIdSet.addAll(collect);
+//                    }
+//                }
+//            }
+//        }
+//        Map<String, List<BondCreditGradingInfo>> collect = MapUtil.newHashMap();
+//        if (CollectionUtil.isNotEmpty(bondIdSet)) {
+//            List<BondCreditGradingInfo> bondCreditGradingInfos = bondBasicInformationDOMapper.queryCreditGradingByBondId(CollectionUtil.newArrayList(bondIdSet));
+//            if (CollectionUtil.isNotEmpty(bondCreditGradingInfos)) {
+//                collect = bondCreditGradingInfos.stream().collect(Collectors.groupingBy(BondCreditGradingInfo::getSecId));
+//            }
+//        }
+//        for (Map.Entry<String, List<BondCreditGradingInfo>> entry : collect.entrySet()) {
+//            entry.getValue().sort(Comparator.comparing(BondCreditGradingInfo::getCrDate));
+//        }
+//        Map<String, BondIndustryInfo> industryInfoMap = MapUtil.newHashMap();
+//        if (CollectionUtil.isNotEmpty(bondIdSet)) {
+//            List<BondIndustryInfo> bondIndustryInfos = bondBasicInformationDOMapper.queryBondIndustry(CollectionUtil.newArrayList(bondIdSet));
+//            if (CollectionUtil.isNotEmpty(bondIndustryInfos)) {
+//                industryInfoMap = bondIndustryInfos.stream().collect(Collectors.toMap(BondIndustryInfo::getSecId, p -> p, (p1, p2) -> p2));
+//            }
+//        }
+//        for (CreditGradingBondVO creditGradingBondVO : creditGradingBondVOS) {
+//            String date = creditGradingBondVO.getDate();
+//            List<RefCreditMarketValueRatio> position = creditGradingBondVO.getPosition();
+//            BigDecimal curTotal = position.stream().map(RefCreditMarketValueRatio::getMarketValue)
+//                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
+//            for (RefCreditMarketValueRatio item : position) {
+//                String secId = item.getSecId();
+//                item.setRatioPerBond(BigDecimalUtils.divide(item.getMarketValue(), curTotal));
+//                List<BondCreditGradingInfo> bondCreditGradingInfos1 = collect.get(secId);
+//                if (CollectionUtil.isNotEmpty(bondCreditGradingInfos1)) {
+//
+//                    List<String> dateList = bondCreditGradingInfos1.stream().map(BondCreditGradingInfo::getCrDate).collect(Collectors.toList());
+//                    Integer nearestIndex = BinarySearchUtil.getMostLeftLessNav(dateList, date);
+//                    int mostLeftLessNav = nearestIndex > 0 ? nearestIndex - 1 : nearestIndex;
+//                    BondCreditGradingInfo lastGradingInfo = bondCreditGradingInfos1.get(mostLeftLessNav);
+//                    BondCreditGradingInfo nearestGradingInfo = bondCreditGradingInfos1.get(nearestIndex);
+//                    item.setMainCode(lastGradingInfo.getMainCode());
+//                    item.setLastCrCode(lastGradingInfo.getCrCode());
+//                    item.setLastCrDate(lastGradingInfo.getCrDate());
+//                    if (lastGradingInfo.getCrCode() == null) {
+//                        item.setLastCrDesc(null);
+//                    } else {
+//                        item.setLastCrDesc(CREDIT_GRADING_NAME.getOrDefault(Long.parseLong(lastGradingInfo.getCrCode()), null));
+//                    }
+//                    item.setNearestCrCode(nearestGradingInfo.getCrCode());
+//                    item.setNearestCrDate(nearestGradingInfo.getCrDate());
+//                    if (nearestGradingInfo.getCrCode() == null) {
+//                        item.setNearestCrDesc(null);
+//                    } else {
+//                        item.setNearestCrDesc(CREDIT_GRADING_NAME.getOrDefault(Long.parseLong(nearestGradingInfo.getCrCode()), null));
+//                    }
+//                    item.setCrAnticipate(nearestGradingInfo.getCrAnticipate());
+//                    BondIndustryInfo bondIndustryInfo = industryInfoMap.get(secId);
+//                    if (bondIndustryInfo != null) {
+//                        item.setIndustryName(bondIndustryInfo.getFirstIndustryName());
+//                    }
+//                }
+//            }
+//        }
+//        // 处理汇总计算
+//        for (Long integer : CREDIT_GRADING_NAME.keySet()) {
+//            List<MarketValueRatio> tempList = ListUtil.list(true);
+//            for (CreditGradingBondVO vo : creditGradingBondVOS) {
+//                MarketValueRatio mvr = new MarketValueRatio();
+//                mvr.setDate(vo.getDate());
+//                List<RefCreditMarketValueRatio> resList = vo.getPosition();
+//                BigDecimal ratio = resList.stream().filter(Objects::nonNull).filter(p -> String.valueOf(integer).equals(p.getNearestCrCode()))
+//                        .map(RefCreditMarketValueRatio::getRatio)
+//                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
+//                mvr.setRatio(ratio);
+//                tempList.add(mvr);
+//            }
+//            // 过滤掉当天没有的评级
+//            int emptyCount = 0;
+//            for (MarketValueRatio marketValueRatio : tempList) {
+//                if (marketValueRatio.getMarketValue() == null && marketValueRatio.getRatio() == null) {
+//                    emptyCount++;
+//                }
+//            }
+//            if (emptyCount != tempList.size()) {
+//                dataset.put(integer.toString(), tempList);
+//            }
+//        }
+//        Map<Long, String> needCreditMapping = MapUtil.newHashMap(true);
+//        for (String code : dataset.keySet()) {
+//            needCreditMapping.put(Long.parseLong(code), CREDIT_GRADING_NAME.get(Long.parseLong(code)));
+//        }
+//        return MapUtil.<String, Object>builder().put("dataset", dataset)
+//                .put("productNameMapping", needCreditMapping).put("table", creditGradingBondVOS).build();
+//    }
+//
+//    private List<CreditGradingBondVO> transfer2BondCreditGrading(List<ConcentrationVO> concentrationVOS) {
+//        List<CreditGradingBondVO> res = CollectionUtil.newArrayList();
+//        if (CollectionUtil.isNotEmpty(concentrationVOS)) {
+//            for (ConcentrationVO concentrationVO : concentrationVOS) {
+//                CreditGradingBondVO item = new CreditGradingBondVO();
+//                item.setAssetNv(concentrationVO.getAssetNv());
+//                item.setDate(concentrationVO.getDate());
+//                List<RefMarketValueRatio> position = concentrationVO.getPosition();
+//                List<RefCreditMarketValueRatio> list = CollectionUtil.newArrayList();
+//                if (CollectionUtil.isNotEmpty(position)) {
+//                    for (RefMarketValueRatio refMarketValueRatio : position) {
+//                        RefCreditMarketValueRatio refBondMarketValueRatio = new RefCreditMarketValueRatio();
+//                        refBondMarketValueRatio.setMarketValue(refMarketValueRatio.getMarketValue());
+//                        refBondMarketValueRatio.setRatio(refMarketValueRatio.getRatio());
+//                        refBondMarketValueRatio.setRef(refMarketValueRatio.getRef());
+//                        refBondMarketValueRatio.setSecId(refMarketValueRatio.getSecId());
+//                        list.add(refBondMarketValueRatio);
+//                    }
+//                }
+//                item.setPosition(list);
+//                res.add(item);
+//            }
+//        }
+//        return res;
+//    }
+//}

+ 75 - 0
src/main/java/com/smppw/analysis/application/service/position/bond/ConcentrationBizBondHandler.java

@@ -0,0 +1,75 @@
+package com.smppw.analysis.application.service.position.bond;
+
+import cn.hutool.core.collection.ListUtil;
+import com.smppw.analysis.application.dto.position.AssetCategoryEnum;
+import com.smppw.analysis.application.dto.position.FundPositionBaseInfo;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+import com.smppw.analysis.application.dto.position.bond.BondSortAllocationParam;
+import com.smppw.analysis.application.dto.position.stock.ConcentrationVO;
+import com.smppw.analysis.application.service.position.AbstractNonSynthesizeBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/8 14:07
+ * @description 股票集中度业务数据转换
+ */
+@Component(BizHandlerConstants.BOND_CONCENTRATION)
+public class ConcentrationBizBondHandler extends AbstractNonSynthesizeBizHandler<BondSortAllocationParam, List<ConcentrationVO>> {
+    public ConcentrationBizBondHandler(AnalysisProperty property, CacheFactory cacheFactory, PositionLoadFactory factory) {
+        super(property, cacheFactory, factory);
+    }
+
+    @Override
+    protected List<ConcentrationVO> afterNonSynthesizeLoadData(BondSortAllocationParam params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos) {
+        String fundId = params.getFundId();
+        if(StringUtils.isNotBlank(fundId)){
+            if(fundId.startsWith("MF")){
+                positionInfos = positionInfos.stream().filter(e -> AssetCategoryEnum.BOND.name().equals(e.getAsset().getValue()))
+                        .collect(Collectors.toList());
+            }else {
+                positionInfos = positionInfos.stream().filter(e -> AssetCategoryEnum.BOND.name().equals(e.getAsset().getValue()))
+                        .filter(p->Objects.nonNull(p.getLevel()))
+                        .filter(p->p.getLevel()>=4)
+                        .collect(Collectors.toList());
+            }
+        }
+
+        List<ConcentrationVO> resultList = ListUtil.list(false);
+        // 按日期分组处理
+        Map<String, List<FundPositionDetail>> collect = positionInfos.stream().collect(Collectors.groupingBy(FundPositionDetail::getDate));
+        collect.forEach((k, v) -> {
+            // 基金在当前日期的净资产值
+            BigDecimal bigDecimal = baseInfos.stream().filter(e -> k.equals(e.getDate())).findFirst().map(FundPositionBaseInfo::getAssetNv).orElse(null);
+            // 得到每个日期下的所有债券成分,按占比降序
+            List<RefMarketValueRatio> position = v.stream().map(e -> {
+                BigDecimal ratioNv = e.getRatioNv();
+                if (ratioNv == null) {
+                    return null;
+                }
+                return new RefMarketValueRatio(e.getRef(), e.getMarketValue(), ratioNv, e.getSecId());
+            }).filter(Objects::nonNull).sorted((o1, o2) -> o2.getRatio().compareTo(o1.getRatio())).collect(Collectors.toList());
+            ConcentrationVO vo = new ConcentrationVO();
+            vo.setDate(k);
+            vo.setAssetNv(bigDecimal);
+            vo.setPosition(position);
+            resultList.add(vo);
+        });
+        // 按日期升序
+        resultList.sort(Comparator.comparing(ConcentrationVO::getDate));
+        return resultList;
+    }
+}

+ 289 - 0
src/main/java/com/smppw/analysis/application/service/position/future/MarginalRiskContributionBizHandler.java

@@ -0,0 +1,289 @@
+package com.smppw.analysis.application.service.position.future;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.IterUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import com.smppw.analysis.application.dto.position.AssetCategoryEnum;
+import com.smppw.analysis.application.dto.position.FundPositionBaseInfo;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.PositionConstants;
+import com.smppw.analysis.application.dto.position.future.FutureDailyRet;
+import com.smppw.analysis.application.dto.position.future.MarginalRiskContribution;
+import com.smppw.analysis.application.dto.position.future.MarginalRiskContributionParams;
+import com.smppw.analysis.application.dto.position.future.MarginalRiskContributionVO;
+import com.smppw.analysis.application.service.position.AbstractBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.entity.FutureDailyPriceDO;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.analysis.infrastructure.persistence.BaseUnderlyingMapper;
+import com.smppw.analysis.infrastructure.persistence.IndexesTradeDateDoMapper;
+import com.smppw.common.exception.APIException;
+import com.smppw.common.pojo.FundFuturesOption;
+import com.smppw.common.pojo.ValueLabelDo;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.utils.BigDecimalUtils;
+import org.apache.commons.math3.linear.Array2DRowRealMatrix;
+import org.apache.commons.math3.linear.RealMatrix;
+import org.apache.commons.math3.stat.correlation.Covariance;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/19 11:23
+ * @description 私募基金期货边际风险贡献
+ */
+@Component(BizHandlerConstants.MARGINAL_RISK_CONTRIBUTION)
+public class MarginalRiskContributionBizHandler extends AbstractBizHandler<MarginalRiskContributionParams, List<MarginalRiskContributionVO>> {
+    private final BaseUnderlyingMapper baseUnderlyingMapper;
+    private final IndexesTradeDateDoMapper indexesTradeDateDoMapper;
+
+    public MarginalRiskContributionBizHandler(AnalysisProperty property, CacheFactory cacheFactory,
+                                              BaseUnderlyingMapper baseUnderlyingMapper,
+                                              PositionLoadFactory factory, IndexesTradeDateDoMapper indexesTradeDateDoMapper) {
+        super(property, cacheFactory, factory);
+        this.baseUnderlyingMapper = baseUnderlyingMapper;
+        this.indexesTradeDateDoMapper = indexesTradeDateDoMapper;
+    }
+
+    @Override
+    protected List<MarginalRiskContributionVO> afterLoadData(MarginalRiskContributionParams params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("持仓数据取数耗时:" + (System.currentTimeMillis() - this.current) + "ms");
+        }
+        String fundType = positionInfos.get(0).getFundType();
+        if (!PositionConstants.PRIVATELY_TYPES.contains(fundType)) {
+            throw new APIException(StrUtil.format("该产品【{}】不支持期货边际风险贡献功能!", params.getFundId()));
+        }
+        List<MarginalRiskContributionVO> resultList = ListUtil.list(false);
+        // 得到期货持仓信息,必须是期货,不能包括期权
+        positionInfos = positionInfos.stream().filter(e -> e.getLevel() != null && e.getLevel() >= 4).filter(e -> e.getAsset() != null)
+                .filter(e -> PositionConstants.FUTURE_SECOND_TYPES.contains(e.getSubType()))
+                .filter(e -> e.getNumber() != null && e.getNumber().compareTo(BigDecimal.ZERO) > 0)
+                .filter(e -> AssetCategoryEnum.FUTURE.name().equals(e.getAsset().getValue())).collect(Collectors.toList());
+        if (CollUtil.isEmpty(positionInfos)) {
+            return resultList;
+        }
+        List<ValueLabelDo> breedNameList = this.baseUnderlyingMapper.getOptionTypeList();
+        // 区分金融期货和商品期货,单独取数
+        Map<ValueLabelDo, List<String>> financeRefMap = MapUtil.newHashMap();
+        Map<ValueLabelDo, List<String>> commodityRefMap = MapUtil.newHashMap();
+        this.distRef(breedNameList, positionInfos, financeRefMap, commodityRefMap);
+        // 按日期分组
+        Map<String, List<FundPositionDetail>> positionMap = positionInfos.stream().collect(Collectors.groupingBy(FundPositionDetail::getDate));
+        Map<String, List<FutureDailyRet>> dateRetMap = MapUtil.newHashMap();
+        positionMap.forEach((k, v) -> {
+            List<String> dateList = this.getRetDates(k);
+            String firstDate = dateList.stream().min(Comparator.naturalOrder()).orElse(k);
+            // 所有品种的近31个交易日日收益率
+            List<FutureDailyRet> dataList = this.getFutureDailyRets(k, dateList, firstDate, financeRefMap, commodityRefMap);
+            dateRetMap.put(k, dataList);
+        });
+        if (logger.isDebugEnabled()) {
+            logger.debug("取数耗时:" + (System.currentTimeMillis() - this.current) + "ms");
+        }
+        positionMap.forEach((k, v) -> {
+            List<String> dateList = getRetDates(k);
+            // 所有品种的近31个交易日日收益率
+            List<FutureDailyRet> dataList = dateRetMap.get(k);
+            // 得到计算风险贡献的数据,计算风险贡献
+            List<ValueLabelDo> breeds = ListUtil.list(true);
+            List<Double> weightList = ListUtil.list(true);
+            double[][] retsMatrix = new double[breedNameList.size()][dateList.size() - 1];
+            this.getMatrixData(breedNameList, v, dateList, financeRefMap, commodityRefMap, dataList, breeds, weightList, retsMatrix);
+            if (CollUtil.isNotEmpty(weightList)) {
+                MarginalRiskContributionVO vo = this.calcRiskContAndBuildVo(k, breeds, weightList, retsMatrix);
+                resultList.add(vo);
+            }
+        });
+        // 按日期升序
+        resultList.sort(Comparator.comparing(MarginalRiskContributionVO::getDate));
+        return resultList;
+    }
+
+    private List<String> getRetDates(String k) {
+        String startDate = DateUtil.formatDate(DateUtil.offsetDay(DateUtil.parseDate(k), -90));
+        return this.indexesTradeDateDoMapper.listTradeDates(startDate, k).stream()
+                .sorted(Comparator.reverseOrder()).limit(31).sorted().collect(Collectors.toList());
+    }
+
+    private MarginalRiskContributionVO calcRiskContAndBuildVo(String k, List<ValueLabelDo> breeds, List<Double> weightList, double[][] retsMatrix) {
+        int size = weightList.size();
+        double[][] covList = this.getCovByRet(retsMatrix);
+        double[] realWeightList = new double[size];
+        Double totalW = weightList.stream().reduce(Double::sum).orElse(0d);
+        for (int i = 0; i < size; i++) {
+            Double weight = weightList.get(i);
+            realWeightList[i] = totalW.compareTo(0d) != 0 ? weight / totalW : 1.0d / size;
+        }
+        double std = this.getStd(covList, realWeightList);
+        double[] riskContList = this.getBreedRiskContribution(covList, realWeightList);
+
+        List<MarginalRiskContribution> breedRiskContList = ListUtil.list(false);
+        for (int i = 0; i < breeds.size(); i++) {
+            ValueLabelDo breed = breeds.get(i);
+            String breedValue = breed.getValue();
+            ValueLabelVO asset;
+            if (PositionConstants.BOND_BREEDS.contains(breedValue)) {
+                asset = new ValueLabelVO(FundFuturesOption.Bond.getNameEnDesc(), FundFuturesOption.Bond.getNameDesc());
+            } else if (PositionConstants.STOCK_INDEX_BREEDS.contains(breedValue)) {
+                asset = new ValueLabelVO(FundFuturesOption.StockIndex.getNameEnDesc(), FundFuturesOption.StockIndex.getNameDesc());
+            } else {
+                asset = new ValueLabelVO(FundFuturesOption.Product.getNameEnDesc(), FundFuturesOption.Product.getNameDesc());
+            }
+            MarginalRiskContribution riskCont = new MarginalRiskContribution();
+            riskCont.setAsset(asset);
+            riskCont.setRef(new ValueLabelVO(breedValue, breed.getLabel()));
+            riskCont.setMarketValue(null);
+            riskCont.setRatio(BigDecimalUtils.toBigDecimal(weightList.get(i)));
+            riskCont.setRiskCont(BigDecimalUtils.toBigDecimal(riskContList[i] / std));
+            breedRiskContList.add(riskCont);
+        }
+        // 按贡献度降序
+        breedRiskContList.sort((o1, o2) -> o2.getRiskCont().compareTo(o1.getRiskCont()));
+        MarginalRiskContributionVO vo = new MarginalRiskContributionVO();
+        vo.setDate(k);
+        vo.setRiskContList(breedRiskContList);
+        return vo;
+    }
+
+    private void getMatrixData(List<ValueLabelDo> breedNameList, List<FundPositionDetail> v, List<String> dateList,
+                               Map<ValueLabelDo, List<String>> financeRefMap, Map<ValueLabelDo, List<String>> commodityRefMap,
+                               List<FutureDailyRet> dataList, List<ValueLabelDo> breeds, List<Double> weightList, double[][] retsMatrix) {
+        for (int i = 0; i < breedNameList.size(); i++) {
+            ValueLabelDo breed = breedNameList.get(i);
+            List<String> refIds = ListUtil.list(false);
+            List<String> finance = financeRefMap.getOrDefault(breed, ListUtil.empty());
+            if (CollUtil.isNotEmpty(finance)) {
+                refIds.addAll(finance);
+            }
+            List<String> commdiity = commodityRefMap.getOrDefault(breed, ListUtil.empty());
+            if (CollUtil.isNotEmpty(commdiity)) {
+                refIds.addAll(commdiity);
+            }
+            List<String> ids = refIds.stream().filter(Objects::nonNull).collect(Collectors.toList());
+            if (CollUtil.isEmpty(ids)) {
+                continue;
+            }
+            List<FundPositionDetail> details = v.stream().filter(e -> ids.contains(e.getRef().getValue()) || ids.contains(e.getSecId())).collect(Collectors.toList());
+            // 品种总市值比之和
+            Double weight = details.stream().map(FundPositionDetail::getRatioNv).map(BigDecimal::doubleValue).reduce(Double::sum).orElse(0d);
+            if (weight.compareTo(0d) == 0) {
+                continue;
+            }
+            List<FutureDailyRet> reteList = dataList.stream().filter(e -> ids.contains(e.getRefId())).collect(Collectors.toList());
+            int size = dateList.size();
+            double[] rets = new double[size - 1];
+            for (int j = 1; j < size; j++) {
+                String date = dateList.get(j);
+                double ret = 0d;
+                for (String refId : ids) {
+                    Double refRet = reteList.stream().filter(e -> refId.equals(e.getRefId())).filter(e -> date.equals(e.getPriceDate()))
+                            .findFirst().map(FutureDailyRet::getRet).orElse(0d);
+                    double refWeight = details.stream().filter(e -> refId.equals(e.getSecId()) || refId.equals(e.getRef().getValue()))
+                            .map(FundPositionDetail::getRatioNv).map(BigDecimal::doubleValue).reduce(Double::sum).orElse(0d);
+                    double v1 = refWeight / weight;
+                    ret += refRet * v1;
+                }
+                rets[j] = ret;
+            }
+            if (IterUtil.isAllNull(Collections.singleton(rets)) || rets.length != 30) {
+                continue;
+            }
+            weightList.add(weight);
+            retsMatrix[i] = rets;
+            breeds.add(breed);
+        }
+    }
+
+    private List<FutureDailyRet> getFutureDailyRets(String k, List<String> dateList, String firstDate, Map<ValueLabelDo, List<String>> financeRefMap, Map<ValueLabelDo, List<String>> commodityRefMap) {
+        List<FutureDailyRet> dataList = ListUtil.list(false);
+        List<String> financeRefIds = financeRefMap.values().stream().flatMap(Collection::stream).distinct().collect(Collectors.toList());
+        List<String> commodityRefIds = commodityRefMap.values().stream().flatMap(Collection::stream).distinct().collect(Collectors.toList());
+        if (CollUtil.isNotEmpty(financeRefIds)) {
+            // 金融期货直接取张跌幅
+            List<FutureDailyPriceDO> tempList = this.baseUnderlyingMapper.getFinanceSecClosePrice(financeRefIds, firstDate, k);
+            List<FutureDailyRet> collect = tempList.stream().map(e -> {
+                Double ret = e.getClosePriceRet();
+                if (ret != null) {
+                    ret = ret / 100d;
+                }
+                return new FutureDailyRet(e.getRefId(), e.getPriceDate(), ret);
+            }).collect(Collectors.toList());
+            if (CollUtil.isNotEmpty(collect)) {
+                dataList.addAll(collect);
+            }
+        }
+        if (CollUtil.isNotEmpty(commodityRefIds)) {
+            // 商品期货只能取收盘价实时计算
+            List<FutureDailyPriceDO> tempList = this.baseUnderlyingMapper.getProductSecClosePrice(commodityRefIds, firstDate, k);
+            if (CollUtil.isNotEmpty(tempList)) {
+                Map<String, List<FutureDailyPriceDO>> collect = tempList.stream().collect(Collectors.groupingBy(FutureDailyPriceDO::getRefId));
+                collect.forEach((refId, l) -> {
+                    for (int i = 0; i < dataList.size(); i++) {
+                        String date = dateList.get(i);
+                        String prevDate = i == 0 ? dateList.get(i) : dateList.get(i - 1);
+                        FutureDailyPriceDO temp = l.stream().filter(e -> date.equals(e.getPriceDate())).findFirst().orElse(null);
+                        FutureDailyPriceDO temp1 = l.stream().filter(e -> prevDate.equals(e.getPriceDate())).findFirst().orElse(null);
+                        double ret = 0;
+                        if (i != 0 && temp != null && temp1 != null) {
+                            ret = temp.getClosePrice() / temp1.getClosePrice() - 1;
+                        }
+                        dataList.add(new FutureDailyRet(refId, date, ret));
+                    }
+                });
+            }
+        }
+        return dataList;
+    }
+
+    private void distRef(List<ValueLabelDo> breedNameList, List<FundPositionDetail> v, Map<ValueLabelDo, List<String>> financeRefMap, Map<ValueLabelDo, List<String>> commodityRefMap) {
+        // 遍历品种获取各品种下的合约
+        breedNameList.forEach(breed -> {
+            String breedValue = breed.getValue();
+            List<FundPositionDetail> details = v.stream().filter(e -> {
+                String futureCode = e.getRef().getValue();
+                String tradingCode = futureCode.substring(0, 2);
+                if (!tradingCode.matches("[A-Za-z]+")) {
+                    tradingCode = tradingCode.substring(0, 1);
+                }
+                return breedValue.equals(tradingCode);
+            }).collect(Collectors.toList());
+            if (PositionConstants.STOCK_INDEX_BREEDS.contains(breedValue) || PositionConstants.BOND_BREEDS.contains(breedValue)) {
+                List<String> refIds = details.stream().map(e -> e.getRef().getValue()).distinct().collect(Collectors.toList());
+                financeRefMap.put(breed, refIds);
+            } else {
+                List<String> refIds = details.stream().map(FundPositionDetail::getSecId).distinct().collect(Collectors.toList());
+                commodityRefMap.put(breed, refIds);
+            }
+        });
+    }
+
+    private double[] getBreedRiskContribution(double[][] retsArr, double[] weightArr) {
+        RealMatrix retsArrMatrix = new Array2DRowRealMatrix(retsArr);
+        RealMatrix weightArrMatrix = new Array2DRowRealMatrix(weightArr);
+        RealMatrix matrix = weightArrMatrix.transpose().multiply(retsArrMatrix);
+        return matrix.getData()[0];
+    }
+
+    private double getStd(double[][] retsMatrix, double[] weight) {
+        RealMatrix retsArrMatrix = new Array2DRowRealMatrix(retsMatrix);
+        RealMatrix weightArrMatrix = new Array2DRowRealMatrix(weight);
+        RealMatrix matrix = weightArrMatrix.transpose().multiply(retsArrMatrix).multiply(weightArrMatrix);
+        return Math.sqrt(matrix.getData()[0][0]);
+    }
+
+    private double[][] getCovByRet(double[][] retsArr) {
+        RealMatrix retsArrMatrix = new Array2DRowRealMatrix(retsArr);
+        RealMatrix covMatrix = new Covariance(retsArrMatrix, true).getCovarianceMatrix();
+        return covMatrix.getData();
+    }
+}

+ 70 - 0
src/main/java/com/smppw/analysis/application/service/position/stock/BarraSensitivityComponent.java

@@ -0,0 +1,70 @@
+package com.smppw.analysis.application.service.position.stock;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.smppw.analysis.application.dto.position.stock.BarraSensitivityParams;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.common.pojo.enums.BarraStyleRespEnum;
+import com.smppw.common.pojo.enums.RaiseType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/12 16:46
+ * @description barra敏感度分析,这里仅支持公募
+ */
+@Component
+public class BarraSensitivityComponent {
+    private static final Logger LOGGER = LoggerFactory.getLogger(BarraSensitivityComponent.class);
+    private final String pyBaseUrl;
+
+    public BarraSensitivityComponent(AnalysisProperty property) {
+        this.pyBaseUrl = property.getPyUrl();
+    }
+
+    public Map<String, Object> bizHandle(BarraSensitivityParams params) {
+        // 沪深300、中证500和中证1000
+        Map<String, String> benchmarkMap = MapUtil.builder("IN00000008", "000300.SH")
+                .put("IN0000007M", "000905.SH").put("IN0000008O", "000852.SH").build();
+        try {
+            RaiseType raiseType = RaiseType.Public;
+            Map<String, Object> req = MapUtil.newHashMap();
+            req.put("fundIds", params.getFundId());
+            req.put("benchmarkId", benchmarkMap.get(params.getBenchmarkId()));
+            req.put("valuationDate", params.getDate());
+            req.put("raiseType", raiseType);
+            String body = HttpUtil.post(this.pyBaseUrl + "GetBarraPosAttribution", JSONUtil.toJsonStr(req));
+            JSONObject entries = JSONUtil.parseObj(body);
+            JSONObject fund = entries.getJSONObject("fund");
+            JSONObject index = entries.getJSONObject("index");
+            JSONObject expose = entries.getJSONObject("expose");
+            Set<String> strings = expose.keySet();
+            Map<String, BigDecimal> exposure = MapUtil.newHashMap(false);
+            for (String string : strings) {
+                exposure.put(string, expose.getBigDecimal(string));
+            }
+            exposure = MapUtil.sortByValue(exposure, true);
+
+            List<ValueLabelVO> collect = Arrays.stream(BarraStyleRespEnum.values())
+                    .map(e -> new ValueLabelVO(e.getName(), e.getYName())).collect(Collectors.toList());
+            return MapUtil.<String, Object>builder().put("fund", fund).put("benchmark", index)
+                    .put("exposure", exposure).put("productNameMapping", collect).build();
+        } catch (Exception e) {
+            LOGGER.error("持仓分析Barra敏感度错误!" + e.getMessage());
+            return MapUtil.newHashMap();
+        }
+    }
+}

+ 50 - 0
src/main/java/com/smppw/analysis/application/service/position/stock/ChangeNumberBizHandler.java

@@ -0,0 +1,50 @@
+package com.smppw.analysis.application.service.position.stock;
+
+import cn.hutool.core.collection.ListUtil;
+import com.smppw.analysis.application.dto.position.AssetCategoryEnum;
+import com.smppw.analysis.application.dto.position.FundPositionBaseInfo;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.stock.ChangeNumberParams;
+import com.smppw.analysis.application.dto.position.stock.ChangeNumberVO;
+import com.smppw.analysis.application.service.position.AbstractNonSynthesizeBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.pojo.dto.NewDateValue;
+import com.smppw.constants.SecType;
+import org.springframework.stereotype.Component;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/8 15:46
+ * @description 持股数量变动
+ */
+@Component(BizHandlerConstants.CHANGE_NUMBER)
+public class ChangeNumberBizHandler extends AbstractNonSynthesizeBizHandler<ChangeNumberParams, List<ChangeNumberVO>> {
+    public ChangeNumberBizHandler(AnalysisProperty property, CacheFactory cacheFactory, PositionLoadFactory factory) {
+        super(property, cacheFactory, factory);
+    }
+
+    @Override
+    protected List<ChangeNumberVO> afterNonSynthesizeLoadData(ChangeNumberParams params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos) {
+        positionInfos = positionInfos.stream().filter(e -> SecType.PUBLICLY_OFFERED_FUNDS.equals(e.getFundType()) || (e.getLevel() != null && e.getLevel() >= 4))
+                .filter(e -> AssetCategoryEnum.STOCK.name().equals(e.getAsset().getValue())).collect(Collectors.toList());
+        List<ChangeNumberVO> resultList = ListUtil.list(false);
+        Map<String, List<FundPositionDetail>> collect = positionInfos.stream().collect(Collectors.groupingBy(FundPositionDetail::getDate));
+        collect.forEach((k, v) -> {
+            long count = v.stream().map(FundPositionDetail::getRef).distinct().count();
+            ChangeNumberVO vo = new ChangeNumberVO();
+            vo.setDate(k);
+            vo.setValue(String.valueOf(count));
+            resultList.add(vo);
+        });
+        resultList.sort(Comparator.comparing(NewDateValue::getDate));
+        return resultList;
+    }
+}

+ 71 - 0
src/main/java/com/smppw/analysis/application/service/position/stock/ConcentrationBizHandler.java

@@ -0,0 +1,71 @@
+package com.smppw.analysis.application.service.position.stock;
+
+import cn.hutool.core.collection.ListUtil;
+import com.smppw.analysis.application.dto.position.AssetCategoryEnum;
+import com.smppw.analysis.application.dto.position.FundPositionBaseInfo;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.RefMarketValueRatio;
+import com.smppw.analysis.application.dto.position.stock.ConcentrationParams;
+import com.smppw.analysis.application.dto.position.stock.ConcentrationVO;
+import com.smppw.analysis.application.service.position.AbstractNonSynthesizeBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.constants.SecType;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/8 14:07
+ * @description 股票集中度业务数据转换
+ */
+@Component(BizHandlerConstants.STOCK_CONCENTRATION)
+public class ConcentrationBizHandler extends AbstractNonSynthesizeBizHandler<ConcentrationParams, List<ConcentrationVO>> {
+
+    public ConcentrationBizHandler(AnalysisProperty property, CacheFactory cacheFactory, PositionLoadFactory factory) {
+        super(property, cacheFactory, factory);
+    }
+
+    @Override
+    protected List<ConcentrationVO> afterNonSynthesizeLoadData(ConcentrationParams params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos) {
+        positionInfos = positionInfos.stream().filter(e -> SecType.PUBLICLY_OFFERED_FUNDS.equals(e.getFundType()) || (e.getLevel() != null && e.getLevel() >= 4))
+                .filter(e -> AssetCategoryEnum.STOCK.name().equals(e.getAsset().getValue())).collect(Collectors.toList());
+        List<ConcentrationVO> resultList = ListUtil.list(false);
+        // 按日期分组处理
+        Map<String, List<FundPositionDetail>> collect = positionInfos.stream().collect(Collectors.groupingBy(FundPositionDetail::getDate));
+        collect.forEach((k, v) -> {
+            // 基金在当前日期的净资产值
+            BigDecimal bigDecimal = baseInfos.stream().filter(e -> k.equals(e.getDate())).findFirst().map(FundPositionBaseInfo::getAssetNv).orElse(null);
+            if (bigDecimal != null && bigDecimal.abs().compareTo(BigDecimal.ZERO) > 0) {
+                // 按成分分组,多空轧差
+                Map<ValueLabelVO, List<BigDecimal>> listMap = v.stream().collect(Collectors.groupingBy(FundPositionDetail::getRef,
+                        Collectors.mapping(FundPositionDetail::getMarketValue, Collectors.toList())));
+                List<RefMarketValueRatio> position = ListUtil.list(false);
+                listMap.forEach((r, m) -> {
+                    BigDecimal mv = m.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
+                    RefMarketValueRatio mvr = new RefMarketValueRatio(r, mv, mv.divide(bigDecimal, 10, RoundingMode.HALF_UP));
+                    position.add(mvr);
+                });
+                // 按占比降序
+                position.sort((o1, o2) -> o2.getRatio().compareTo(o1.getRatio()));
+                ConcentrationVO vo = new ConcentrationVO();
+                vo.setDate(k);
+                vo.setAssetNv(bigDecimal);
+                vo.setPosition(position);
+                resultList.add(vo);
+            }
+        });
+        // 按日期升序
+        resultList.sort(Comparator.comparing(ConcentrationVO::getDate));
+        return resultList;
+    }
+}

+ 67 - 0
src/main/java/com/smppw/analysis/application/service/position/stock/IndustryAllocationBizHandler.java

@@ -0,0 +1,67 @@
+package com.smppw.analysis.application.service.position.stock;
+
+import cn.hutool.core.collection.ListUtil;
+import com.smppw.analysis.application.dto.position.CategoryConstraint;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.stock.StockAllocationParams;
+import com.smppw.analysis.application.dto.position.stock.StockAllocationVO;
+import com.smppw.analysis.application.service.position.AbstractAnalysisBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.entity.IndexSecWeightInfoDO;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.domain.service.FundPositionAnalysisService;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.pojo.ValueLabelVO;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/8 17:19
+ * @description 行业配置
+ */
+@Component(BizHandlerConstants.INDUSTRY_ALLOCATION)
+public class IndustryAllocationBizHandler extends AbstractAnalysisBizHandler<StockAllocationParams, List<StockAllocationVO>> {
+    public IndustryAllocationBizHandler(AnalysisProperty property, CacheFactory cacheFactory,
+                                        PositionLoadFactory factory, FundPositionAnalysisService analysisService) {
+        super(property, cacheFactory, factory, analysisService);
+    }
+
+    @Override
+    protected List<StockAllocationVO> afterAnalysisLoadData(String benchmarkId, List<FundPositionDetail> positionDetails,
+                                                            Map<String, List<IndexSecWeightInfoDO>> indexWeightMap) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("行业配置 数据准备耗时:" + (System.currentTimeMillis() - this.current) + "ms");
+        }
+        List<StockAllocationVO> resultList = ListUtil.list(false);
+        // 行业与A股对应关系
+        Map<ValueLabelVO, List<String>> secIndustryMap = this.getIndustrySecMap();
+        // 日期分组
+        Map<String, List<FundPositionDetail>> positionMap = positionDetails.stream().collect(Collectors.groupingBy(FundPositionDetail::getDate));
+        positionMap.forEach((k, v) -> {
+            // 股票市值
+            BigDecimal total = v.stream().map(FundPositionDetail::getMarketValue).filter(Objects::nonNull)
+                    .reduce(BigDecimal::add).map(BigDecimal::abs).orElse(null);
+            List<IndexSecWeightInfoDO> indexSecWeightList = indexWeightMap.getOrDefault(k, ListUtil.empty());
+            List<CategoryConstraint> constraintList = ListUtil.list(false);
+            List<String> categoryCodes = ListUtil.list(false);
+            secIndustryMap.forEach((c, l) -> {
+                CategoryConstraint constraint = this.buildCategoryConstraint(v, total, categoryCodes, indexSecWeightList, l);
+                constraint.setCategory(c);
+                constraintList.add(constraint);
+            });
+            StockAllocationVO vo = this.getStockAllocationVO(k, v, total, indexSecWeightList, constraintList, categoryCodes);
+            resultList.add(vo);
+        });
+        // 按日期升序
+        resultList.sort(Comparator.comparing(StockAllocationVO::getDate));
+        return resultList;
+    }
+}

+ 80 - 0
src/main/java/com/smppw/analysis/application/service/position/stock/IndustryAllocationPreferenceComponent.java

@@ -0,0 +1,80 @@
+package com.smppw.analysis.application.service.position.stock;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.map.MapUtil;
+import com.smppw.analysis.application.dto.position.CategoryConstraint;
+import com.smppw.analysis.application.dto.position.stock.IndustryAllocationPreferenceVO;
+import com.smppw.analysis.application.dto.position.stock.StockAllocationParams;
+import com.smppw.analysis.application.dto.position.stock.StockAllocationVO;
+import com.smppw.analysis.application.service.position.BizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerFactor;
+import com.smppw.common.pojo.ValueLabelVO;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static com.smppw.analysis.application.service.position.BizHandlerConstants.INDUSTRY_ALLOCATION;
+
+/**
+ * @author wangzaijun
+ * @date 2023/7/14 17:36
+ * @description 行业配置业绩表现
+ */
+@Component
+public class IndustryAllocationPreferenceComponent {
+    private final BizHandlerFactor factor;
+
+    public IndustryAllocationPreferenceComponent(BizHandlerFactor factor) {
+        this.factor = factor;
+    }
+
+    public List<IndustryAllocationPreferenceVO> bizHandle(StockAllocationParams params) {
+        List<IndustryAllocationPreferenceVO> resultList = ListUtil.list(false);
+        BizHandler<StockAllocationParams, List<StockAllocationVO>> bizHandler = this.factor.getBizHandlerInstance(INDUSTRY_ALLOCATION);
+        List<StockAllocationVO> dataList = bizHandler.bizHandle(params);
+        // 取所有行业
+        List<ValueLabelVO> industryList = ListUtil.list(false);
+        for (StockAllocationVO vo : dataList) {
+            List<ValueLabelVO> collect = vo.getIndustries().stream().map(CategoryConstraint::getCategory).distinct().collect(Collectors.toList());
+            CollUtil.addAllIfNotContains(industryList, collect);
+        }
+        boolean flag = true;
+        // 每个日期数据处理
+        for (StockAllocationVO temp : dataList) {
+            List<BigDecimal> offsetList = ListUtil.list(false);
+            Map<String, String> industryMap = MapUtil.newHashMap();
+            // 处理所有行业的行业偏离
+            for (ValueLabelVO industry : industryList) {
+                CategoryConstraint categoryConstraint = temp.getIndustries().stream().filter(e -> industry.equals(e.getCategory())).findFirst().orElse(null);
+                BigDecimal pupil = Optional.ofNullable(categoryConstraint).map(CategoryConstraint::getPupil).orElse(BigDecimal.ZERO);
+                BigDecimal benchmark = Optional.ofNullable(categoryConstraint).map(CategoryConstraint::getBenchmark).orElse(BigDecimal.ZERO);
+                BigDecimal offset = pupil.subtract(benchmark);
+                industryMap.put(industry.getValue(), offset.toPlainString());
+                offsetList.add(offset);
+            }
+            // 对应日期内按偏离度降序
+            MapUtil.sortByValue(industryMap, true);
+            // 求平均行业偏离
+            double avg = offsetList.stream().mapToDouble(e -> Double.parseDouble(e.toPlainString())).average().orElse(0.0);
+            IndustryAllocationPreferenceVO vo = new IndustryAllocationPreferenceVO();
+            vo.setDate(temp.getDate());
+            vo.setIndustryPreference(industryMap);
+            vo.setAvg(String.valueOf(avg));
+            // 第一个日期保存所有行业关系,方便取值
+            if (flag) {
+                vo.setIndustryList(industryList);
+                flag = false;
+            }
+            resultList.add(vo);
+        }
+        // 按日期升序
+        resultList.sort(Comparator.comparing(IndustryAllocationPreferenceVO::getDate));
+        return resultList;
+    }
+}

+ 166 - 0
src/main/java/com/smppw/analysis/application/service/position/stock/NewLiquidityAllocationBizHandler.java

@@ -0,0 +1,166 @@
+package com.smppw.analysis.application.service.position.stock;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import com.smppw.analysis.application.dto.position.CategoryConstraint;
+import com.smppw.analysis.application.dto.position.CompositionCategory;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.PositionLiquidityEnum;
+import com.smppw.analysis.application.dto.position.stock.StockAllocationParams;
+import com.smppw.analysis.application.dto.position.stock.StockAllocationVO;
+import com.smppw.analysis.application.service.position.AbstractAnalysisBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.entity.IndexSecWeightInfoDO;
+import com.smppw.analysis.domain.entity.SecLiquidityInfoDO;
+import com.smppw.analysis.domain.entity.SecTurnoverInfoDO;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.domain.service.FundPositionAnalysisService;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.constants.SecType;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/9 14:20
+ * @description 流动性分析
+ */
+@Component(BizHandlerConstants.LIQUIDITY_ALLOCATION)
+public class NewLiquidityAllocationBizHandler extends AbstractAnalysisBizHandler<StockAllocationParams, List<StockAllocationVO>> {
+
+
+    public NewLiquidityAllocationBizHandler(AnalysisProperty property, CacheFactory cacheFactory,
+                                            PositionLoadFactory factory, FundPositionAnalysisService analysisService) {
+        super(property, cacheFactory, factory, analysisService);
+    }
+
+    @Override
+    protected List<StockAllocationVO> afterAnalysisLoadData(String benchmarkId, List<FundPositionDetail> positionDetails,
+                                                            Map<String, List<IndexSecWeightInfoDO>> indexWeightMap) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("流动性分析配置 数据准备耗时:" + (System.currentTimeMillis() - this.current) + "ms");
+        }
+        List<ValueLabelVO> allCategoryList = Arrays.stream(PositionLiquidityEnum.values())
+                .map(e -> new ValueLabelVO(e.name(), e.getDesc())).collect(Collectors.toList());
+        // 日期分组
+        Map<String, List<FundPositionDetail>> positionMap = positionDetails.stream().collect(Collectors.groupingBy(FundPositionDetail::getDate));
+        // 日期升序
+        List<String> dateList = positionMap.keySet().stream().sorted().collect(Collectors.toList());
+        List<String> realDates = dateList;
+        String fundType = positionDetails.get(0).getFundType();
+        if (SecType.PUBLICLY_OFFERED_FUNDS.equals(fundType)) {
+            realDates = dateList.stream().map(this::getRealDate).distinct().collect(Collectors.toList());
+        }
+        // 流动性标准
+        List<SecLiquidityInfoDO> secLiquidityInfos = this.analysisService.listSecLiquidityInfo(realDates);
+        // 5日换手率
+        List<SecTurnoverInfoDO> secTurnoverInfos = this.analysisService.listSecTurnoverInfo(realDates);
+
+        Map<String, Map<ValueLabelVO, List<Map<String, Object>>>> secStyleMap = MapUtil.newHashMap();
+        Map<String, Map<ValueLabelVO, List<Map<String, Object>>>> indexSecStyleMap = MapUtil.newHashMap();
+        // 数据转换
+        for (String date : dateList) {
+            String realDate = SecType.PUBLICLY_OFFERED_FUNDS.equals(fundType) ? this.getRealDate(date) : date;
+            List<SecTurnoverInfoDO> turnoverInfos = secTurnoverInfos.stream().filter(e -> realDate.equals(e.getTradingDay())).collect(Collectors.toList());
+            SecLiquidityInfoDO liquidityInfo = secLiquidityInfos.stream().filter(e -> realDate.equals(e.getTradingDay())).findFirst().orElse(null);
+            List<CompositionCategory> categoryList = ListUtil.list(false);
+            turnoverInfos.forEach(e -> {
+                Double secRate = e.getTurnoverValue();
+                PositionLiquidityEnum liquidity = PositionLiquidityEnum.OTHER;
+                if (secRate != null && liquidityInfo != null) {
+                    if (liquidityInfo.getLowestOne() != null && secRate >= liquidityInfo.getLowestOne()) {
+                        liquidity = PositionLiquidityEnum.HIGH;
+                    } else if (liquidityInfo.getLowestTwo() != null && secRate >= liquidityInfo.getLowestTwo()) {
+                        liquidity = PositionLiquidityEnum.HIGHER;
+                    } else if (liquidityInfo.getLowestThree() != null && secRate >= liquidityInfo.getLowestThree()) {
+                        liquidity = PositionLiquidityEnum.SAME_AS;
+                    } else if (liquidityInfo.getLowestFour() != null && secRate >= liquidityInfo.getLowestFour()) {
+                        liquidity = PositionLiquidityEnum.LOWER;
+                    } else {
+                        liquidity = PositionLiquidityEnum.LOW;
+                    }
+                }
+                CompositionCategory category = CompositionCategory.builder().secCode(e.getSecCode())
+                        .category(new ValueLabelVO(liquidity.name(), liquidity.getDesc())).build();
+                categoryList.add(category);
+            });
+
+            // 映射风格(行业、风格或流动性)
+            List<Map<String, Object>> list = positionMap.get(date).stream().map(e -> {
+                Map<String, Object> temp = MapUtil.newHashMap();
+                // 找到当前成分所在风格(行业、风格或流动性),-1 表示其他
+                ValueLabelVO style = categoryList.stream().filter(o -> o.getSecCode().equals(e.getRef().getValue()))
+                        .findFirst().map(CompositionCategory::getCategory).orElse(OTHER);
+                temp.put("ref", e.getRef().getValue());
+                temp.put("style", style);
+                temp.put("sharesNature", e.getSharesNature());
+                temp.put("marketValue", e.getMarketValue());
+                return temp;
+            }).collect(Collectors.toList());
+            Map<ValueLabelVO, List<Map<String, Object>>> styleMap = list.stream()
+                    .collect(Collectors.groupingBy(e -> MapUtil.get(e, "style", ValueLabelVO.class)));
+            secStyleMap.put(date, styleMap);
+            List<Map<String, Object>> indexList = indexWeightMap.get(date).stream().map(e -> {
+                Map<String, Object> temp = MapUtil.newHashMap();
+                // 找到当前成分所在风格(行业、风格或流动性),-1 表示其他
+                ValueLabelVO style = categoryList.stream().filter(o -> o.getSecCode().equals(e.getSecCode()))
+                        .findFirst().map(CompositionCategory::getCategory).orElse(OTHER);
+                temp.put("ref", e.getSecCode());
+                temp.put("style", style);
+                temp.put("weight", e.getWeight());
+                return temp;
+            }).collect(Collectors.toList());
+            Map<ValueLabelVO, List<Map<String, Object>>> indexStyleMap = indexList.stream()
+                    .collect(Collectors.groupingBy(e -> MapUtil.get(e, "style", ValueLabelVO.class)));
+            indexSecStyleMap.put(date, indexStyleMap);
+        }
+        if (this.logger.isDebugEnabled()) {
+            this.logger.debug(StrUtil.format("所有数据准备:{} ms", System.currentTimeMillis() - this.current));
+        }
+
+        List<StockAllocationVO> resultList = ListUtil.list(false);
+        positionMap.forEach((k, v) -> {
+            // 股票市值
+            BigDecimal total = v.stream().map(FundPositionDetail::getMarketValue).filter(Objects::nonNull)
+                    .reduce(BigDecimal::add).map(BigDecimal::abs).orElse(null);
+            List<CategoryConstraint> constraintList = ListUtil.list(false);
+            Map<ValueLabelVO, List<Map<String, Object>>> styleMap = secStyleMap.get(k);
+            Map<ValueLabelVO, List<Map<String, Object>>> indexStyleMap = indexSecStyleMap.get(k);
+            for (ValueLabelVO category : allCategoryList) {
+                // 找到同风格的基金成分
+                List<Map<String, Object>> l = ListUtil.empty();
+                if (styleMap != null && styleMap.get(category) != null) {
+                    l = styleMap.get(category);
+                }
+                CategoryConstraint constraint = this.calcCategoryWeight(total, indexStyleMap, category, l);
+                constraintList.add(constraint);
+            }
+            // 按多空降序
+            constraintList.sort(((o1, o2) -> o2.getPupil().compareTo(o1.getPupil())));
+            StockAllocationVO vo = new StockAllocationVO();
+            vo.setDate(k);
+            vo.setIndustries(constraintList);
+            resultList.add(vo);
+        });
+        // 按日期升序
+        resultList.sort(Comparator.comparing(StockAllocationVO::getDate));
+        return resultList;
+    }
+
+    /**
+     * 找当前估值日期最近的交易日(往前找)
+     *
+     * @param date 估值日期
+     * @return /
+     */
+    private String getRealDate(String date) {
+        String s = this.analysisService.getLatestTradeDate(date);
+        return s == null ? date : s;
+    }
+}

+ 134 - 0
src/main/java/com/smppw/analysis/application/service/position/stock/RiskExposureBizHandler.java

@@ -0,0 +1,134 @@
+package com.smppw.analysis.application.service.position.stock;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import com.smppw.analysis.application.dto.position.*;
+import com.smppw.analysis.application.dto.position.stock.RiskExposureParams;
+import com.smppw.analysis.application.dto.position.stock.RiskExposureVO;
+import com.smppw.analysis.application.service.position.AbstractNonSynthesizeBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.exception.APIException;
+import com.smppw.constants.SecType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/13 8:49
+ * @description 私募基金股票风险敞口走势
+ */
+@Component(BizHandlerConstants.RISK_EXPOSURE)
+public class RiskExposureBizHandler extends AbstractNonSynthesizeBizHandler<RiskExposureParams, List<RiskExposureVO>> {
+    private static final List<RiskExposure> RISK_EXPOSURES = ListUtil.list(true);
+
+    static {
+        // 个股多头
+        RISK_EXPOSURES.add(new RiskExposure(RiskExposureEnum.STOCK_BULL, true,
+                e -> AssetCategoryEnum.STOCK.name().equals(e.getAsset().getValue())
+                        && e.getSharesNature() == 1 && e.getLevel() >= 4));
+        // 个股空头
+        RISK_EXPOSURES.add(new RiskExposure(RiskExposureEnum.STOCK_BEAR, false,
+                e -> AssetCategoryEnum.STOCK.name().equals(e.getAsset().getValue())
+                        && e.getSharesNature() == 2 && e.getLevel() >= 4));
+        // 股指多头
+        RISK_EXPOSURES.add(new RiskExposure(RiskExposureEnum.FUTURE_BULL, true,
+                e -> AssetCategoryEnum.FUTURE.name().equals(e.getAsset().getValue())
+                        && e.getSharesNature() == 1
+                        && e.getSubType() == 31
+                        && e.getLevel() >= 4));
+        // 股指空头
+        RISK_EXPOSURES.add(new RiskExposure(RiskExposureEnum.FUTURE_BEAR, false,
+                e -> AssetCategoryEnum.FUTURE.name().equals(e.getAsset().getValue())
+                        && e.getSharesNature() == 2
+                        && e.getSubType() == 31
+                        && e.getLevel() >= 4));
+        // ETF融券
+        RISK_EXPOSURES.add(new RiskExposure(RiskExposureEnum.ETF, false,
+                e -> e.getSubjectCode() != null && e.getSubjectCode().contains("2101")
+                        && e.getLevel() == 3
+                        && e.getRef() != null && e.getRef().getLabel() != null && e.getRef().getLabel().contains("ETF")));
+    }
+
+    public RiskExposureBizHandler(AnalysisProperty property, CacheFactory cacheFactory, PositionLoadFactory factory) {
+        super(property, cacheFactory, factory);
+    }
+
+    @Override
+    protected List<RiskExposureVO> afterNonSynthesizeLoadData(RiskExposureParams params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos) {
+        if (CollUtil.isEmpty(positionInfos)) {
+            return ListUtil.empty();
+        }
+        List<String> types = ListUtil.of(SecType.PRIVATE_FUND, SecType.PRIVATELY_OFFERED_FUND);
+        String fundType = positionInfos.get(0).getFundType();
+        if (!types.contains(fundType)) {
+            throw new APIException("股票风险敞口接口仅支持私募基金和自建基金!");
+        }
+        List<RiskExposureVO> resultList = ListUtil.list(false);
+        // 按日期分组处理
+        Map<String, List<FundPositionDetail>> collect = positionInfos.stream().collect(Collectors.groupingBy(FundPositionDetail::getDate));
+        collect.forEach((k, v) -> {
+            BigDecimal total = baseInfos.stream().filter(e -> k.equals(e.getDate())).map(FundPositionBaseInfo::getAssetNv)
+                    .filter(Objects::nonNull).findFirst().map(BigDecimal::abs).orElse(null);
+            if (total != null && total.abs().compareTo(BigDecimal.ZERO) > 0) {
+                List<RefMarketValueRatio> mvrs = ListUtil.list(true);
+                BigDecimal exposureMv = BigDecimal.ZERO;
+                // 是否全部都null,
+                boolean allNullFlag = true;
+                for (RiskExposure riskExposure : RISK_EXPOSURES) {
+                    BigDecimal mv = v.stream().filter(riskExposure.getPredicate()).map(FundPositionDetail::getMarketValue)
+                            .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
+                    BigDecimal ratio = null;
+                    if (mv != null) {
+                        ratio = mv.divide(total, 4, RoundingMode.HALF_UP);
+                        exposureMv = riskExposure.operatePlus ? exposureMv.add(mv.abs()) : exposureMv.subtract(mv.abs());
+                        allNullFlag = false;
+                    }
+                    mvrs.add(new RefMarketValueRatio(RiskExposureEnum.getValueLabel(riskExposure.exposureEnum), mv, ratio));
+                }
+                // 敞口 市值不为null才添加到结果集中
+                if (!allNullFlag) {
+                    BigDecimal exposureRatio = exposureMv.divide(total, 4, RoundingMode.HALF_UP);
+                    mvrs.add(new RefMarketValueRatio(RiskExposureEnum.getValueLabel(RiskExposureEnum.EXPOSURE), null, exposureRatio));
+                    RiskExposureVO vo = new RiskExposureVO();
+                    vo.setDate(k);
+                    vo.setMvr(mvrs);
+                    resultList.add(vo);
+                }
+            }
+        });
+        if (CollUtil.isNotEmpty(resultList)) {
+            resultList.sort(Comparator.comparing(RiskExposureVO::getDate).reversed());
+        }
+        return resultList;
+    }
+
+    @Getter
+    @AllArgsConstructor
+    private static class RiskExposure {
+        /**
+         * 类型
+         */
+        private final RiskExposureEnum exposureEnum;
+        /**
+         * 是否加法
+         */
+        private final boolean operatePlus;
+        /**
+         * 数据过滤条件
+         */
+        private final Predicate<FundPositionDetail> predicate;
+    }
+}

+ 247 - 0
src/main/java/com/smppw/analysis/application/service/position/stock/StockPerformanceAttributionBizHandler.java

@@ -0,0 +1,247 @@
+package com.smppw.analysis.application.service.position.stock;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.map.MapUtil;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.stock.StockPerformanceAttributionParams;
+import com.smppw.analysis.application.dto.position.stock.StockPerformanceAttributionVO;
+import com.smppw.analysis.application.service.position.AbstractAnalysisBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.entity.IndexSecWeightInfoDO;
+import com.smppw.analysis.domain.entity.SecClosePriceDO;
+import com.smppw.analysis.domain.event.SaveCacheEvent;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.domain.service.FundPositionAnalysisService;
+import com.smppw.analysis.domain.service.NavService;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.common.pojo.dto.DateValue;
+import com.smppw.common.pojo.enums.NavType;
+import com.smppw.constants.RedisConst;
+import com.smppw.constants.SecType;
+import com.smppw.utils.BigDecimalUtils;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/13 13:38
+ * @description 股票业绩归因
+ */
+@Component(BizHandlerConstants.STOCK_PERFORMANCE_ATTRIBUTION)
+public class StockPerformanceAttributionBizHandler extends AbstractAnalysisBizHandler<StockPerformanceAttributionParams, List<StockPerformanceAttributionVO>> {
+    private final NavService navService;
+
+    public StockPerformanceAttributionBizHandler(AnalysisProperty property, CacheFactory cacheFactory,
+                                                 PositionLoadFactory factory, FundPositionAnalysisService analysisService, NavService navService) {
+        super(property, cacheFactory, factory, analysisService);
+        this.navService = navService;
+    }
+
+    @Override
+    protected List<StockPerformanceAttributionVO> afterAnalysisLoadData(String benchmarkId, List<FundPositionDetail> positionDetails,
+                                                                        Map<String, List<IndexSecWeightInfoDO>> indexWeightMap) {
+        String fundType = positionDetails.get(0).getFundType();
+        Map<ValueLabelVO, List<String>> industrySecMap = this.getIndustrySecMap();
+        Map<String, List<FundPositionDetail>> positionMap = positionDetails.stream().collect(Collectors.groupingBy(FundPositionDetail::getDate));
+        List<String> dateList = positionMap.keySet().stream().sorted().collect(Collectors.toList());
+        // 数据获取和转换
+        Map<String, Map<String, Double>> dateSecRetMap = MapUtil.newHashMap();
+        if (SecType.PUBLICLY_OFFERED_FUNDS.equals(fundType)) {
+            List<String> realDates = dateList.stream().map(this::getRealDate).collect(Collectors.toList());
+            List<SecClosePriceDO> dataList = this.analysisService.listSecClosePrice(realDates);
+            List<SecClosePriceDO> tempList = this.analysisService.listSciTechSecClosePrice(realDates);
+            if (CollUtil.isNotEmpty(tempList)) {
+                dataList.addAll(tempList);
+            }
+            Map<String, Map<String, Double>> collect = dataList.stream().collect(Collectors.groupingBy(SecClosePriceDO::getTradingDay,
+                    Collectors.toMap(SecClosePriceDO::getSecCode, e -> e.getRet6m() == null ? 0d : e.getRet6m() / 100d)));
+            dateSecRetMap.putAll(collect);
+        } else {
+            // 全量数据获取
+            List<SecClosePriceDO> multiSecPrices = this.getMultiSecPrices(dateList);
+            List<Map<String, CompletableFuture<Map<String, Double>>>> futures = ListUtil.list(false);
+            for (int i = 0; i < dateList.size(); i++) {
+                String prevDate = i == 0 ? dateList.get(0) : dateList.get(i - 1);
+                String date = dateList.get(i);
+                // 获取所有股票代码
+                List<FundPositionDetail> fundPositionDetails = positionMap.get(date);
+                List<IndexSecWeightInfoDO> secWeightList = indexWeightMap.getOrDefault(date, ListUtil.empty());
+                List<String> secCodes = fundPositionDetails.stream().map(e -> e.getRef().getValue()).distinct().collect(Collectors.toList());
+                if (CollUtil.isNotEmpty(secWeightList)) {
+                    List<String> indexSecCodes = secWeightList.stream().map(IndexSecWeightInfoDO::getSecCode).distinct().collect(Collectors.toList());
+                    CollUtil.addAllIfNotContains(secCodes, indexSecCodes);
+                }
+                // 对于计算密集型,可以用默认的线程池(计算每个股票的收益率)
+                CompletableFuture<Map<String, Double>> future = CompletableFuture.supplyAsync(
+                        () -> this.calcSecDateRet(prevDate, date, secCodes, multiSecPrices), DEFAULT_EXECUTOR);
+                futures.add(MapUtil.<String, CompletableFuture<Map<String, Double>>>builder().put(date, future).build());
+            }
+            for (Map<String, CompletableFuture<Map<String, Double>>> future : futures) {
+                future.forEach((k, v) -> dateSecRetMap.put(k, v.join()));
+            }
+        }
+        if (logger.isDebugEnabled()) {
+            logger.debug("业绩归因数据处理耗时:" + (System.currentTimeMillis() - this.current) + "ms");
+        }
+        // 业务处理
+        List<CompletableFuture<StockPerformanceAttributionVO>> futures = ListUtil.list(false);
+        for (int i = 0; i < dateList.size(); i++) {
+            String prevDate = i == 0 ? dateList.get(0) : dateList.get(i - 1);
+            String date = dateList.get(i);
+            List<FundPositionDetail> fundPositionDetails = positionMap.get(date);
+            List<IndexSecWeightInfoDO> secWeightList = indexWeightMap.getOrDefault(date, ListUtil.empty());
+            Map<String, Double> secRetMap = dateSecRetMap.getOrDefault(date, MapUtil.empty());
+            // 计算业绩归因
+            CompletableFuture<StockPerformanceAttributionVO> future = CompletableFuture.supplyAsync(
+                    () -> this.calcPerformanceAttribution(date, prevDate, benchmarkId, fundPositionDetails, industrySecMap, secWeightList, secRetMap), DEFAULT_EXECUTOR);
+            futures.add(future);
+        }
+        List<StockPerformanceAttributionVO> resultList = ListUtil.list(false);
+        for (CompletableFuture<StockPerformanceAttributionVO> future : futures) {
+            resultList.add(future.join());
+        }
+        resultList.sort(Comparator.comparing(StockPerformanceAttributionVO::getDate));
+        return resultList;
+    }
+
+    private StockPerformanceAttributionVO calcPerformanceAttribution(String date, String prevDate, String benchmarkId,
+                                                                     List<FundPositionDetail> fundPositionDetails, Map<ValueLabelVO, List<String>> industrySecMap,
+                                                                     List<IndexSecWeightInfoDO> secWeightList, Map<String, Double> secRetMap) {
+        Double totalMv = fundPositionDetails.stream().map(FundPositionDetail::getMarketValue).filter(Objects::nonNull)
+                .reduce(BigDecimal::add).map(BigDecimal::abs).map(BigDecimal::doubleValue).orElse(null);
+        List<StockPerformanceAttributionVO.Effect> effects = ListUtil.list(true);
+        double indexRet = this.getIndexRet(benchmarkId, prevDate, date);
+        industrySecMap.forEach((c, l) -> {
+            // 计算各行业下的效应
+            StockPerformanceAttributionVO.Effect effect = this.calcEffect(c, fundPositionDetails, totalMv, secWeightList, secRetMap, indexRet, l);
+            effects.add(effect);
+        });
+        // 按超额收益降序
+        effects.sort(((o1, o2) -> o2.getRatio().compareTo(o1.getRatio())));
+        StockPerformanceAttributionVO vo = new StockPerformanceAttributionVO();
+        vo.setDate(date);
+        vo.setEffect(effects);
+        return vo;
+    }
+
+    private StockPerformanceAttributionVO.Effect calcEffect(ValueLabelVO category, List<FundPositionDetail> positionDetails, Double totalMv,
+                                                            List<IndexSecWeightInfoDO> secWeightList, Map<String, Double> secRetMap, double indexRet, List<String> l) {
+        List<IndexSecWeightInfoDO> collect = secWeightList.stream().filter(e -> l.contains(e.getSecCode())).collect(Collectors.toList());
+        // 指数成分的行业收益与权重汇总
+        double indexWeight = collect.stream().map(IndexSecWeightInfoDO::getWeight)
+                .map(BigDecimal::doubleValue).reduce(Double::sum).map(e -> e / 100d).orElse(0d);
+        double indexIndustryRet = collect.stream().map(e -> indexWeight * secRetMap.getOrDefault(e.getSecCode(), 0d))
+                .reduce(Double::sum).orElse(0d);
+        double secWeight = 0d;
+        double industryRet = 0d;
+        if (totalMv != null && totalMv.compareTo(0d) > 0) {
+            // 基金的成分的行业收益与占市值比汇总
+            List<ValueLabelVO> collect1 = positionDetails.stream().filter(e -> l.contains(e.getRef().getValue()))
+                    .map(e -> new ValueLabelVO(e.getRef().getValue(), e.getMarketValue().toPlainString())).collect(Collectors.toList());
+            secWeight = collect1.stream().map(ValueLabelVO::getLabel).map(Double::parseDouble).reduce(Double::sum).map(e -> e / totalMv).orElse(0d);
+            industryRet = collect1.stream().map(ValueLabelVO::getValue).map(e -> indexWeight * secRetMap.getOrDefault(e, 0d))
+                    .reduce(Double::sum).orElse(0d);
+        }
+        StockPerformanceAttributionVO.Effect effect = new StockPerformanceAttributionVO.Effect();
+        effect.setRef(category);
+        effect.setRatio(BigDecimalUtils.toBigDecimal(secWeight));
+        // (行业权重 - 基准行业权重) * (基金行业收益 * 基准行业权重 - 基准总收益)
+        double allocation = (secWeight - indexWeight) * (indexIndustryRet * indexWeight - indexRet);
+        effect.setAllocation(allocation);
+        // 行业权重 * (行业收益 * 行业权重 - 基准行业收益 * 基准行业权重)
+        double stock = secWeight * (industryRet * secWeight - indexIndustryRet * indexWeight);
+        effect.setStock(stock);
+        // (组合行业权重-基准行业权重)*基准总收益
+        double timing = (secWeight - indexWeight) * indexRet;
+        effect.setTiming(timing);
+        // (1+持仓组合收益)/(1+基准指数收益)-1 = 配置+选股+择时
+        double total = allocation + stock + timing;
+        effect.setExactRet(total);
+        return effect;
+    }
+
+    private double getIndexRet(String benchmarkId, String prevDate, String date) {
+        double indexRet = 0d;
+        if (!prevDate.equals(date)) {
+            // 上一个日期也往前找交易日
+            String realDate = this.getRealDate(prevDate);
+            Map<String, List<DateValue>> indexNavMap = navService.getMarketIndexNavMap(ListUtil.of(benchmarkId), realDate, date, NavType.CumulativeNav);
+            if (MapUtil.isNotEmpty(indexNavMap)) {
+                List<DateValue> dateValues = indexNavMap.get(benchmarkId);
+                if (CollUtil.isNotEmpty(dateValues)) {
+                    indexRet = dateValues.get(dateValues.size() - 1).getValue() / dateValues.get(0).getValue() - 1d;
+                }
+            }
+        }
+        return indexRet;
+    }
+
+    private Map<String, Double> calcSecDateRet(String prevDate, String date, List<String> secCodes, List<SecClosePriceDO> multiSecPrices) {
+        Map<String, Double> secRetMap = MapUtil.newHashMap();
+        // 过滤价格
+        List<SecClosePriceDO> prices = multiSecPrices.stream().filter(e -> prevDate.compareTo(e.getTradingDay()) <= 0)
+                .filter(e -> date.compareTo(e.getTradingDay()) >= 0).collect(Collectors.toList());
+        for (String secCode : secCodes) {
+            double ret = 0d;
+            if (!date.equals(prevDate)) {
+                List<SecClosePriceDO> tempList = prices.stream().filter(e -> secCode.equals(e.getSecCode()))
+                        .sorted(Comparator.comparing(SecClosePriceDO::getTradingDay)).collect(Collectors.toList());
+                if (CollUtil.isNotEmpty(tempList)) {
+                    Double prevClose = tempList.get(0).getClosePrice();
+                    Double close = tempList.get(tempList.size() - 1).getClosePrice();
+                    if (prevClose != null && close != null) {
+                        ret = close / prevClose - 1.0d;
+                    }
+                }
+            }
+            secRetMap.put(secCode, ret);
+        }
+        return secRetMap;
+    }
+
+    private List<SecClosePriceDO> getMultiSecPrices(List<String> dateList) {
+        List<SecClosePriceDO> resultList = ListUtil.list(false);
+        if (CollUtil.isEmpty(dateList)) {
+            return resultList;
+        }
+        String firstDate = dateList.get(0);
+        String lastDate = dateList.get(dateList.size() - 1);
+        String item = firstDate + lastDate;
+        Object hget = this.cacheGateway.hget(RedisConst.POSITION_STOCK_PRICE_KEY, item);
+        if (hget != null) {
+            resultList = this.convertList(hget, SecClosePriceDO.class);
+        } else {
+            resultList = this.analysisService.listSecClosePrice(firstDate, lastDate);
+            List<SecClosePriceDO> tempList = this.analysisService.listSciTechSecClosePrice(firstDate, lastDate);
+            if (CollUtil.isNotEmpty(tempList)) {
+                resultList.addAll(tempList);
+            }
+            SaveCacheEvent<List<SecClosePriceDO>> event = new SaveCacheEvent<>(this, resultList,
+                    t -> this.cacheGateway.hset(RedisConst.POSITION_STOCK_PRICE_KEY, item, t, RedisConst.POSITION_YEAR_TTL, TimeUnit.SECONDS));
+            this.applicationContext.publishEvent(event);
+        }
+        return resultList;
+    }
+
+    /**
+     * 找当前估值日期最近的交易日(往前找)
+     *
+     * @param date 估值日期
+     * @return /
+     */
+    private String getRealDate(String date) {
+        String s = this.analysisService.getLatestTradeDate(date);
+        return s == null ? date : s;
+    }
+}

+ 102 - 0
src/main/java/com/smppw/analysis/application/service/position/stock/StyleAllocationBizHandler.java

@@ -0,0 +1,102 @@
+package com.smppw.analysis.application.service.position.stock;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.date.DateField;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import com.smppw.analysis.application.dto.position.CategoryConstraint;
+import com.smppw.analysis.application.dto.position.CompositionCategory;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.PositionStyleEnum;
+import com.smppw.analysis.application.dto.position.stock.StockAllocationParams;
+import com.smppw.analysis.application.dto.position.stock.StockAllocationVO;
+import com.smppw.analysis.application.service.position.AbstractAnalysisBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.entity.IndexSecWeightInfoDO;
+import com.smppw.analysis.domain.entity.SecStyleInfoDO;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.domain.service.FundPositionAnalysisService;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.constants.Consts;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/9 14:20
+ * @description 风格配置
+ */
+@Component(BizHandlerConstants.STYLE_ALLOCATION)
+public class StyleAllocationBizHandler extends AbstractAnalysisBizHandler<StockAllocationParams, List<StockAllocationVO>> {
+    public StyleAllocationBizHandler(AnalysisProperty property, CacheFactory cacheFactory,
+                                     PositionLoadFactory factory, FundPositionAnalysisService analysisService) {
+        super(property, cacheFactory, factory, analysisService);
+    }
+
+    @Override
+    protected List<StockAllocationVO> afterAnalysisLoadData(String benchmarkId, List<FundPositionDetail> positionDetails,
+                                                            Map<String, List<IndexSecWeightInfoDO>> indexWeightMap) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("风格配置 数据准备耗时:" + (System.currentTimeMillis() - this.current) + "ms");
+        }
+        // 日期分组
+        Map<String, List<FundPositionDetail>> positionMap = positionDetails.stream().collect(Collectors.groupingBy(FundPositionDetail::getDate));
+        // 日期升序
+        List<String> dateList = positionMap.keySet().stream().sorted().collect(Collectors.toList());
+        // 估值日往后找日期,今年用去年的
+        List<String> dates = dateList.stream().map(this::getRealDate).distinct().collect(Collectors.toList());
+        // 这里不用缓存,缓存会更慢
+        List<SecStyleInfoDO> styleInfos = this.analysisService.listSecStyleInfo(dates);
+        Map<String, List<SecStyleInfoDO>> styleInfoMap = styleInfos.stream().collect(Collectors.groupingBy(SecStyleInfoDO::getEndDate));
+
+        List<StockAllocationVO> resultList = ListUtil.list(false);
+        positionMap.forEach((k, v) -> {
+            String realDate = this.getRealDate(k);
+            // 股票市值
+            BigDecimal total = v.stream().map(FundPositionDetail::getMarketValue).filter(Objects::nonNull)
+                    .reduce(BigDecimal::add).map(BigDecimal::abs).orElse(null);
+            List<CategoryConstraint> constraintList = ListUtil.list(false);
+            List<SecStyleInfoDO> tempList = styleInfoMap.getOrDefault(realDate, ListUtil.empty());
+            Map<ValueLabelVO, List<String>> secStyleMap = tempList.stream().map(e -> {
+                PositionStyleEnum style = PositionStyleEnum.getStyle(e.getMarket(), e.getBalance());
+                return CompositionCategory.builder().secCode(e.getSecCode())
+                        .category(style == null ? OTHER : new ValueLabelVO(style.name(), style.getName())).build();
+            }).collect(Collectors.groupingBy(CompositionCategory::getCategory, Collectors.mapping(CompositionCategory::getSecCode, Collectors.toList())));
+            List<IndexSecWeightInfoDO> indexSecWeightList = indexWeightMap.getOrDefault(k, ListUtil.empty());
+            List<String> categoryCodes = ListUtil.list(false);
+            secStyleMap.forEach((category, l) -> {
+                CategoryConstraint constraint = this.buildCategoryConstraint(v, total, categoryCodes, indexSecWeightList, l);
+                constraint.setCategory(category);
+                constraintList.add(constraint);
+            });
+            StockAllocationVO vo = this.getStockAllocationVO(k, v, total, indexSecWeightList, constraintList, categoryCodes);
+            resultList.add(vo);
+        });
+        // 按日期升序
+        resultList.sort(Comparator.comparing(StockAllocationVO::getDate));
+        return resultList;
+    }
+
+    /**
+     * 估值日往后找风格日期,今年用去年的
+     *
+     * @param date 估值日期
+     * @return 风格截止日期
+     */
+    private String getRealDate(String date) {
+        Date now = new Date();
+        String year = StrUtil.subBefore(date, "-", false);
+        String current = DateUtil.format(now, Consts.YEAR_DATE_PATTERN);
+        if (current.equals(year)) {
+            DateTime offset = DateUtil.offset(now, DateField.YEAR, -1);
+            year = DateUtil.format(offset, Consts.YEAR_DATE_PATTERN);
+        }
+        return year + "-12-31";
+    }
+}

+ 92 - 0
src/main/java/com/smppw/analysis/application/service/position/synthesize/AssetAllocationBizHandler.java

@@ -0,0 +1,92 @@
+package com.smppw.analysis.application.service.position.synthesize;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import com.smppw.analysis.application.dto.position.*;
+import com.smppw.analysis.application.dto.position.synthesize.AssetAllocationParams;
+import com.smppw.analysis.application.dto.position.synthesize.AssetAllocationVO;
+import com.smppw.analysis.application.service.position.AbstractBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.dao.PubliclyFundPositionDao;
+import com.smppw.analysis.domain.entity.FundPositionBaseInfoDO;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.exception.APIException;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.constants.SecType;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/7 9:51
+ * @description 综合类 业务逻辑处理接口
+ */
+@Component(BizHandlerConstants.ASSET_ALLOCATION)
+public class AssetAllocationBizHandler extends AbstractBizHandler<AssetAllocationParams, List<AssetAllocationVO>> {
+    private static final ValueLabelVO RETURN_SALE = new ValueLabelVO(PositionConstants.ASSET_RETURN_SALE, PositionConstants.ASSET_RETURN_SALE_DESC);
+    private static final ValueLabelVO MONEY = new ValueLabelVO(PositionConstants.ASSET_MONEY, PositionConstants.ASSET_MONEY_DESC);
+//    private static final ValueLabelVO SALE = new ValueLabelVO(PositionConstants.ASSET_SALE, PositionConstants.ASSET_SALE_DESC);
+    private static final ValueLabelVO NV = new ValueLabelVO("NV", "净资产");
+    private final PubliclyFundPositionDao fundPositionBaseService;
+
+    public AssetAllocationBizHandler(AnalysisProperty property, CacheFactory cacheFactory,
+                                     PositionLoadFactory factory, PubliclyFundPositionDao fundPositionBaseService) {
+        super(property, cacheFactory, factory);
+        this.fundPositionBaseService = fundPositionBaseService;
+    }
+
+    @Override
+    protected List<AssetAllocationVO> afterLoadData(AssetAllocationParams params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos) {
+        List<AssetAllocationVO> resultList = ListUtil.list(false);
+        String fundType = positionInfos.get(0).getFundType();
+        if (SecType.PUBLICLY_OFFERED_FUNDS.equals(fundType)) {
+            // 公募基金特殊处理
+            List<FundPositionBaseInfoDO> doList = this.fundPositionBaseService.fundPositionBaseInfos(params.getFundId(),
+                    params.getStartDate(), params.getEndDate());
+            for (FundPositionBaseInfoDO temp : doList) {
+                AssetAllocationVO vo = new AssetAllocationVO();
+                vo.setDate(DateUtil.formatDate(temp.getReportDate()));
+                List<RefMarketValueRatio> assetList = ListUtil.list(false);
+                this.handleMvr(assetList, AssetCategoryEnum.buildValueLabelByAsset(AssetCategoryEnum.STOCK), null, temp.getMvOfEquity(), temp.getRinOfEquity());
+                this.handleMvr(assetList, AssetCategoryEnum.buildValueLabelByAsset(AssetCategoryEnum.BOND), null, temp.getMvOfFixedIncome(), temp.getRinOfFixedIncome());
+                this.handleMvr(assetList, AssetCategoryEnum.buildValueLabelByAsset(AssetCategoryEnum.FUND), null, temp.getMvOfFund(), temp.getRinOfFund());
+                this.handleMvr(assetList, AssetCategoryEnum.buildValueLabelByAsset(AssetCategoryEnum.FUTURE), null, temp.getMvOfFixedDeriva(), temp.getRinOfDeriva());
+                this.handleMvr(assetList, RETURN_SALE, null, temp.getMvOfReturnSale(), temp.getRinOfReturnSale());
+                this.handleMvr(assetList, MONEY, null, temp.getMvOfMoney(), temp.getRinOfMoney());
+                // 每个估值日内数据按占比降序
+                assetList.sort(Comparator.comparing(RefMarketValueRatio::getRatio).reversed());
+                // 降序后把其他和净资产添加到结果中
+                this.handleMvr(assetList, OTHER, temp.getAssetNv(), null, null);
+                assetList.add(new RefMarketValueRatio(NV, temp.getAssetNv(), BigDecimal.ONE));
+                vo.setAsset(assetList);
+                resultList.add(vo);
+            }
+        } else {
+            throw new APIException(StrUtil.format("当前基金类型【{} - {}】不支持做大类资产漂移分析!", params.getFundId(), fundType));
+        }
+        // 按日期升序
+        resultList.sort(Comparator.comparing(AssetAllocationVO::getDate));
+        return resultList;
+    }
+
+    private void handleMvr(List<RefMarketValueRatio> assetList, ValueLabelVO asset, BigDecimal assetNv, BigDecimal mvOf, BigDecimal rinOf) {
+        if (OTHER.equals(asset)) {
+            BigDecimal totalMv = assetList.stream().map(RefMarketValueRatio::getMarketValue).filter(Objects::nonNull)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            BigDecimal totalRatio = assetList.stream().map(RefMarketValueRatio::getRatio).filter(Objects::nonNull)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            mvOf = assetNv == null ? null : assetNv.subtract(totalMv);
+            rinOf = BigDecimal.ONE.subtract(totalRatio);
+        }
+        if (mvOf != null || rinOf != null) {
+            assetList.add(new RefMarketValueRatio(asset, mvOf, rinOf));
+        }
+    }
+}

+ 44 - 0
src/main/java/com/smppw/analysis/application/service/position/synthesize/LeverageChangeBizHandler.java

@@ -0,0 +1,44 @@
+package com.smppw.analysis.application.service.position.synthesize;
+
+import com.smppw.analysis.application.dto.position.FundPositionBaseInfo;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.synthesize.LeverageChangeParams;
+import com.smppw.analysis.application.dto.position.synthesize.LeverageChangeVO;
+import com.smppw.analysis.application.service.position.AbstractBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/8 8:39
+ * @description 杠杆变化 接口逻辑处理
+ */
+@Component(BizHandlerConstants.LEVERAGE_CHANGE)
+public class LeverageChangeBizHandler extends AbstractBizHandler<LeverageChangeParams, List<LeverageChangeVO>> {
+    public LeverageChangeBizHandler(AnalysisProperty property, CacheFactory cacheFactory, PositionLoadFactory factory) {
+        super(property, cacheFactory, factory);
+    }
+
+    @Override
+    protected List<LeverageChangeVO> afterLoadData(LeverageChangeParams params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos) {
+        return baseInfos.stream().map(e -> {
+            LeverageChangeVO vo = new LeverageChangeVO();
+            vo.setDate(e.getDate());
+            vo.setTotal(Optional.ofNullable(e.getTotalAsset()).map(BigDecimal::toPlainString).orElse(null));
+            vo.setNv(Optional.ofNullable(e.getAssetNv()).map(BigDecimal::toPlainString).orElse(null));
+            if (e.getAssetNv() != null && e.getTotalAsset() != null && e.getAssetNv().abs().compareTo(BigDecimal.ZERO) > 0) {
+                vo.setLeverage(e.getTotalAsset().divide(e.getAssetNv(), 10, RoundingMode.HALF_UP).toPlainString());
+            }
+            return vo;
+        }).collect(Collectors.toList());
+    }
+}

+ 152 - 0
src/main/java/com/smppw/analysis/application/service/position/synthesize/PositionListBizHandler.java

@@ -0,0 +1,152 @@
+package com.smppw.analysis.application.service.position.synthesize;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.NumberUtil;
+import com.smppw.analysis.application.dto.position.FundPositionBaseInfo;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.synthesize.PositionListParams;
+import com.smppw.analysis.application.dto.position.synthesize.PositionListVO;
+import com.smppw.analysis.application.service.position.AbstractBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.exception.APIException;
+import com.smppw.constants.SecType;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/8 9:47
+ * @description 其他特殊的综合组件
+ */
+@Service(BizHandlerConstants.POSITION_LIST)
+public class PositionListBizHandler extends AbstractBizHandler<PositionListParams, List<PositionListVO>> {
+    public PositionListBizHandler(AnalysisProperty property, CacheFactory cacheFactory, PositionLoadFactory factory) {
+        super(property, cacheFactory, factory);
+    }
+
+    @Override
+    protected boolean isAllPositionData() {
+        return true;
+    }
+
+    @Override
+    protected void checkParams(PositionListParams params) {
+        super.checkParams(params);
+        if (CharSequenceUtil.isBlankOrUndefined(params.getDate())) {
+            throw new APIException("估值日期不能为空!");
+        }
+    }
+
+    @Override
+    protected List<PositionListVO> afterLoadData(PositionListParams params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos) {
+        // 公募基金或者私募基金级别大于等于4级,并且数量不为null的
+        positionInfos = positionInfos.stream().filter(e -> SecType.PUBLICLY_OFFERED_FUNDS.equals(e.getFundType()) || e.getLevel() >= 4)
+                .filter(e -> SecType.PUBLICLY_OFFERED_FUNDS.equals(e.getFundType()) || (e.getSubType() != null && e.getSubType() != 20))
+                .filter(e -> e.getAsset() != null).collect(Collectors.toList());
+        // 按日期分组,求估值增值之和,得到每个日期的估值增值之和
+        Map<String, BigDecimal> collect = baseInfos.stream().filter(e -> e.getIncrement() != null)
+                .collect(Collectors.groupingBy(FundPositionBaseInfo::getDate,
+                        Collectors.reducing(BigDecimal.ZERO, FundPositionBaseInfo::getIncrement, BigDecimal::add)));
+        for (FundPositionDetail detail : positionInfos) {
+            BigDecimal increment = detail.getValueChange();
+            // 增值比例
+            if (increment != null) {
+                // 先取基金主体表存储的估值增值
+                BigDecimal bigDecimal = baseInfos.stream().filter(e -> detail.getDate().equals(e.getDate())).findFirst()
+                        .map(FundPositionBaseInfo::getIncrement).orElse(null);
+                // 没有的话取当前日期的成分累计估值增值
+                if (bigDecimal == null) {
+                    bigDecimal = collect.get(detail.getDate());
+                }
+                if (bigDecimal != null && bigDecimal.abs().compareTo(BigDecimal.ZERO) > 0) {
+                    detail.setValueChangeRatio(increment.divide(bigDecimal, 4, RoundingMode.HALF_UP));
+                }
+            }
+        }
+        // 数量变化处理
+        this.handleNumberChange(positionInfos);
+        List<PositionListVO> resultList = ListUtil.list(true);
+        for (FundPositionDetail detail : positionInfos) {
+            PositionListVO vo = new PositionListVO();
+            vo.setDate(detail.getDate());
+            vo.setSuspension(detail.getSuspension());
+            vo.setName(detail.getRef().getLabel());
+            vo.setType(detail.getAsset().getLabel());
+            vo.setCost(detail.getCost() == null ? null : detail.getCost().toPlainString());
+            vo.setNum(detail.getNumber() == null ? null : detail.getNumber().toPlainString());
+            vo.setNumChange(detail.getNumberChange() == null ? null : detail.getNumberChange().toPlainString());
+            vo.setValueChange(detail.getValueChange() == null ? null : detail.getValueChange().toPlainString());
+            vo.setValueChangeRatio(detail.getValueChangeRatio() == null ? null : detail.getValueChangeRatio().toPlainString());
+            vo.setMarketValue(detail.getMarketValue() == null ? null : detail.getMarketValue().toPlainString());
+            if (detail.getRatioNv() != null) {
+                vo.setWeight(detail.getRatioNv());
+            } else {
+                BigDecimal total = baseInfos.stream().filter(e -> detail.getDate().equals(e.getDate())).findFirst()
+                        .map(FundPositionBaseInfo::getAssetNv).orElse(null);
+                if (total != null && total.abs().compareTo(BigDecimal.ZERO) > 0 && detail.getMarketValue() != null) {
+                    vo.setWeight(detail.getMarketValue().divide(total, 10, RoundingMode.HALF_UP));
+                }
+            }
+            resultList.add(vo);
+        }
+        // 权重降序
+        List<PositionListVO> tempList = resultList.stream().filter(e -> e.getWeight() != null)
+                .sorted((o1, o2) -> o2.getWeight().compareTo(o1.getWeight())).collect(Collectors.toList());
+        // 日期过滤
+        tempList = tempList.stream().filter(e -> params.getDate().equals(e.getDate())).collect(Collectors.toList());
+        // 大类过滤
+        if (CharSequenceUtil.isNotBlank(params.getType()) && !"ALL".equals(params.getType())) {
+            tempList = tempList.stream().filter(e -> params.getAssetCategory().getName().equals(e.getType()))
+                    .collect(Collectors.toList());
+        }
+        // 数量过滤
+        if (NumberUtil.isInteger(params.getNum()) && !"-1".equals(params.getNum())) {
+            tempList = tempList.stream().limit(Long.parseLong(params.getNum())).collect(Collectors.toList());
+        }
+        if ("-1".equals(params.getNum())) {
+            List<PositionListVO> nullList = resultList.stream().filter(e -> e.getWeight() == null).collect(Collectors.toList());
+            tempList = CollUtil.unionAll(tempList, nullList);
+        }
+        return tempList;
+    }
+
+    /**
+     * 数量变化逻辑处理,本期数量-上一期数量
+     *
+     * @param dataList /
+     */
+    protected void handleNumberChange(List<FundPositionDetail> dataList) {
+        String minDate = dataList.stream().map(FundPositionDetail::getDate).min(Comparator.naturalOrder()).orElse(null);
+        Map<String, BigDecimal> prevMap = MapUtil.newHashMap();
+        for (FundPositionDetail temp : dataList) {
+            if (temp.getRef() == null) {
+                continue;
+            }
+            String ref = temp.getRef().getValue();
+            if (minDate.equals(temp.getDate())) {
+                prevMap.put(ref, temp.getNumber());
+                continue;
+            }
+            BigDecimal prev = prevMap.get(ref);
+            BigDecimal number = temp.getNumber();
+            if (prev != null && number != null) {
+                temp.setNumberChange(number.subtract(prev));
+            }
+            if (number != null) {
+                prevMap.put(ref, number);
+            }
+        }
+    }
+}

+ 55 - 0
src/main/java/com/smppw/analysis/application/service/position/synthesize/PositionParamsBizHandler.java

@@ -0,0 +1,55 @@
+package com.smppw.analysis.application.service.position.synthesize;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
+import com.smppw.analysis.application.dto.position.AssetCategoryEnum;
+import com.smppw.analysis.application.dto.position.FundPositionBaseInfo;
+import com.smppw.analysis.application.dto.position.FundPositionDetail;
+import com.smppw.analysis.application.dto.position.synthesize.PositionInfoParams;
+import com.smppw.analysis.application.dto.position.synthesize.PositionInfoVO;
+import com.smppw.analysis.application.service.position.AbstractBizHandler;
+import com.smppw.analysis.application.service.position.BizHandlerConstants;
+import com.smppw.analysis.application.service.position.PositionLoadFactory;
+import com.smppw.analysis.domain.gateway.CacheFactory;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.pojo.ValueLabelVO;
+import com.smppw.constants.SecType;
+import org.springframework.stereotype.Service;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author wangzaijun
+ * @date 2023/6/8 9:47
+ * @description 其他特殊的综合组件
+ */
+@Service(BizHandlerConstants.POSITION_PARAMS)
+public class PositionParamsBizHandler extends AbstractBizHandler<PositionInfoParams, PositionInfoVO> {
+    public PositionParamsBizHandler(AnalysisProperty property, CacheFactory cacheFactory, PositionLoadFactory factory) {
+        super(property, cacheFactory, factory);
+    }
+
+    @Override
+    protected PositionInfoVO afterLoadData(PositionInfoParams params, List<FundPositionBaseInfo> baseInfos, List<FundPositionDetail> positionInfos) {
+        if (CollUtil.isEmpty(positionInfos)) {
+            return new PositionInfoVO();
+        }
+        String fundType = positionInfos.get(0).getFundType();
+        List<ValueLabelVO> types = ListUtil.list(true);
+        // 公募基金没有 全部 选项
+        if (!SecType.PUBLICLY_OFFERED_FUNDS.equals(fundType)) {
+            types.add(new ValueLabelVO("ALL", "全部"));
+        }
+        for (AssetCategoryEnum value : AssetCategoryEnum.values()) {
+            types.add(new ValueLabelVO(value.name(), value.getName()));
+        }
+        List<String> dateList = positionInfos.stream().filter(e -> e.getAsset() != null).map(FundPositionDetail::getDate)
+                .distinct().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
+        PositionInfoVO vo = new PositionInfoVO();
+        vo.setDates(dateList);
+        vo.setTypes(types);
+        return vo;
+    }
+}

+ 9 - 10
src/main/java/com/smppw/analysis/client/FundApi.java

@@ -1,11 +1,10 @@
 package com.smppw.analysis.client;
 
-import com.smppw.analysis.application.dto.HeadIndicatorParams;
-import com.smppw.analysis.application.dto.HeadIndicatorVO;
-import com.smppw.analysis.application.dto.IndicatorParams;
-import com.smppw.analysis.application.dto.TrendParams;
-import com.smppw.analysis.application.service.CommonService;
-import com.smppw.analysis.application.service.PerformanceService;
+import com.smppw.analysis.application.dto.performance.HeadIndicatorParams;
+import com.smppw.analysis.application.dto.performance.IndicatorParams;
+import com.smppw.analysis.application.dto.performance.TrendParams;
+import com.smppw.analysis.application.service.performance.CommonService;
+import com.smppw.analysis.application.service.performance.PerformanceService;
 import com.smppw.common.pojo.vo.ResultVo;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -15,11 +14,11 @@ import java.util.Map;
 
 @RestController
 @RequestMapping("/v1/api/fund")
-public class FundApi {
+public class FundPerformanceApi {
     private final CommonService commonService;
     private final PerformanceService performanceService;
 
-    public FundApi(CommonService commonService, PerformanceService performanceService) {
+    public FundPerformanceApi(CommonService commonService, PerformanceService performanceService) {
         this.commonService = commonService;
         this.performanceService = performanceService;
     }
@@ -37,8 +36,8 @@ public class FundApi {
     }
 
     @GetMapping("/head/indicator")
-    public ResultVo<HeadIndicatorVO> headIndicator(HeadIndicatorParams params) {
-        HeadIndicatorVO indicator = this.commonService.getIndicator(params);
+    public ResultVo<Map<String, Object>> headIndicator(HeadIndicatorParams params) {
+        Map<String, Object> indicator = this.commonService.getIndicator(params);
         return ResultVo.ok(indicator);
     }
 }

+ 0 - 0
src/main/java/com/smppw/analysis/client/FundPositionApi.java


Некоторые файлы не были показаны из-за большого количества измененных файлов