Ver código fonte

支持M* return & risk, 支持上下行捕获率,重构

Joey 1 semana atrás
pai
commit
6119aa6176
1 arquivos alterados com 187 adições e 74 exclusões
  1. 187 74
      modules/indicatorCalculator.dos

+ 187 - 74
modules/indicatorCalculator.dos

@@ -27,7 +27,37 @@ def get_annulization_multiple(freq) {
   return ret;
 }
 
+/*
+ *   取主基准和BFI的历史月收益率
+ *   
+ *   @param benchmarks <TABLE>: entity-benchmark 的对应关系表
+ *   @param end_day <DATE>: 收益的截止日期
+ * 
+ *   @return <TABLE>: benchmark_id, end_date, ret
+ *   
+ */
+def get_benchmark_return(benchmarks, end_day) {
+
+    s_index_ids = '';
+    s_factor_ids = '';
+
+    // 前缀为 IN 的 benchmark id
+    t_index_id = SELECT DISTINCT benchmark_id FROM benchmarks WHERE benchmark_id LIKE 'IN%';
+    s_index_ids = iif(isVoid(t_index_id), "", "'" + t_index_id.benchmark_id.concat("','") + "'");
+
+   // 前缀为 FA 的 benchmark id
+   t_factor_id = SELECT DISTINCT benchmark_id FROM benchmarks WHERE benchmark_id LIKE 'FA%';
+   s_factor_ids = iif(isVoid(t_factor_id), "",  "'" + t_factor_id.benchmark_id.concat("','") + "'");
+
+
+    // 目前指数的月度业绩存在 fund_performance 表
+    t_bmk = SELECT fund_id AS benchmark_id, temporalParse(end_date, 'yyyy-MM') AS end_date, ret FROM get_monthly_ret('IX', s_index_ids, 1990.01.01, end_day, true);
+
+    // 而因子的月度业绩存在 cm_factor_performance 表
+    INSERT INTO t_bmk SELECT factor_id, temporalParse(end_date, 'yyyy-MM') AS end_date, ret FROM get_monthly_ret('FA', s_factor_ids, 1990.01.01, end_day, true);
 
+	return t_bmk;
+}
 /*
  *     Trailing Return, Standard Deviation, Skewness, Kurtosis, Max Drawdown, VaR, CVaR
  *     @param ret: 收益表,需要有 entity_id, price_dat, end_date, nav 
@@ -66,7 +96,7 @@ def cal_basic_performance(ret, freq) {
          WHERE ret.ret > - 1
          GROUP BY entity_id;
 
-   return (SELECT * FROM t LEFT JOIN t1 ON t.entity_id = t1.entity_id AND t.end_date = t1.end_date AND t.price_date = t1.price_date);
+    return (SELECT * FROM t LEFT JOIN t1 ON t.entity_id = t1.entity_id AND t.end_date = t1.end_date AND t.price_date = t1.price_date);
 
 }
 
@@ -122,23 +152,24 @@ def cal_omega_sortino_kappa(ret, risk_free) {
  *   Alpha & Beta
  *   NOTE: alpha of Java version is noncompliant-GIPS annulized number
  */
-def cal_alpha_beta(ret, bmk_ret, risk_free) {
+def cal_alpha_beta(ret, benchmarks, bmk_ret, risk_free) {
 
-    t = SELECT t.entity_id, t.end_date, t.ret, bmk.ret AS ret_bmk
+    t = SELECT t.entity_id, t.end_date, t.ret, bm.benchmark_id, bmk.ret AS ret_bmk
         FROM ret t
-        INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date
+        INNER JOIN benchmarks bm ON t.entity_id = bm.entity_id
+        INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date AND bm.benchmark_id = bmk.benchmark_id
         WHERE t.ret > -1
           AND bmk.ret > -1;
 
-    beta = SELECT ret.beta(ret_bmk) AS beta FROM t GROUP BY entity_id;
+    beta = SELECT entity_id, benchmark_id, ret.beta(ret_bmk) AS beta FROM t GROUP BY entity_id, benchmark_id;
 
-    alpha = SELECT t.entity_id, (t.ret - rfr.ret).mean() - beta.beta[0] * (t.ret_bmk - rfr.ret).mean() AS alpha
+    alpha = SELECT t.entity_id, t.benchmark_id, beta.beta[0] AS beta, (t.ret - rfr.ret).mean() - beta.beta[0] * (t.ret_bmk - rfr.ret).mean() AS alpha
             FROM t 
-            INNER JOIN beta beta ON t.entity_id = beta.entity_id
+            INNER JOIN beta beta ON t.entity_id = beta.entity_id AND t.benchmark_id = beta.benchmark_id
             INNER JOIN risk_free rfr ON t.end_date = rfr.end_date
-            GROUP BY t.entity_id;
+            GROUP BY t.entity_id, t.benchmark_id;
 
-    return ( SELECT * FROM beta AS b INNER JOIN alpha AS a ON a.entity_id = b.entity_id  );
+    return alpha;
 }
 
 /*
@@ -146,25 +177,60 @@ def cal_alpha_beta(ret, bmk_ret, risk_free) {
  *    TODO: Information Ratio is way off!
  *          Not sure how to describe a giant number("inf"), for now 999 is used
  */
-def cal_benchmark_tracking(ret, bmk_ret) {
+def cal_benchmark_tracking(ret, benchmarks, bmk_ret) {
 
-     t0 = SELECT t.entity_id, t.end_date, t.ret, bmk.ret AS ret_bmk, count(entity_id) AS cnt, (t.ret - bmk.ret) AS exc_ret
+     t0 = SELECT t.entity_id, t.end_date, t.price_date,
+                 t.ret, bmk.ret AS ret_bmk, count(t.entity_id) AS cnt, (t.ret - bmk.ret) AS exc_ret, bm.benchmark_id
           FROM ret t
-          INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date
+          INNER JOIN benchmarks bm ON t.entity_id = bm.entity_id
+          INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date AND bm.benchmark_id = bmk.benchmark_id
           WHERE t.ret > -1
             AND bmk.ret > -1
-          CONTEXT BY t.entity_id;
+          CONTEXT BY t.entity_id, bm.benchmark_id;
 
-     t = SELECT entity_id, 
+     t = SELECT entity_id, end_date.max() AS end_date, price_date.max() AS price_date, price_date.min() AS min_date, benchmark_id,
                 exc_ret.bucketCount(0:999, 1) \ cnt[0] AS winrate,
                 exc_ret.std() AS track_error, 
                 exc_ret.mean() / exc_ret.std() AS info
-         FROM t0 GROUP BY entity_id
+         FROM t0 GROUP BY entity_id, benchmark_id;
 
      return t;
 }
 
 /*
+ *  Upside/Down Capture Return/Ratio
+ * 
+ */
+def cal_capture_ratio(ret, benchmarks, bmk_ret) {
+
+    t1 = SELECT t.entity_id, (1+t.ret).prod() AS upside_ret, (1+bmk.ret).prod() AS bmk_upside_ret, bmk.end_date.count() AS bmk_upside_cnt, bm.benchmark_id
+         FROM ret t
+         INNER JOIN benchmarks bm ON t.entity_id = bm.entity_id
+         INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date AND bm.benchmark_id = bmk.benchmark_id
+         WHERE t.ret > -1
+           AND bmk.ret >= 0
+         GROUP BY t.entity_id, bm.benchmark_id;
+
+    t2 = SELECT t.entity_id, (1+t.ret).prod() AS downside_ret, (1+bmk.ret).prod() AS bmk_downside_ret, bmk.end_date.count() AS bmk_downside_cnt, bm.benchmark_id
+         FROM ret t
+         INNER JOIN benchmarks bm ON t.entity_id = bm.entity_id
+         INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date AND bm.benchmark_id = bmk.benchmark_id
+         WHERE t.ret > -1
+           AND bmk.ret < 0
+         GROUP BY t.entity_id, bm.benchmark_id;
+
+    t = SELECT iif(isNull(t1.entity_id), t2.entity_id, t1.entity_id) AS entity_id,
+               iif(isNull(t1.benchmark_id), t2.benchmark_id, t1.benchmark_id) AS benchmark_id,
+                t1.upside_ret.pow(1 \ t1.bmk_upside_cnt)-1 AS upside_capture_ret,
+               (t1.upside_ret.pow(1 \ t1.bmk_upside_cnt)-1)/(t1.bmk_upside_ret.pow(1 \ t1.bmk_upside_cnt)-1) AS upside_capture_ratio,
+                t2.downside_ret.pow(1 \ t2.bmk_downside_cnt)-1 AS downside_capture_ret,
+               (t2.downside_ret.pow(1 \ t2.bmk_downside_cnt)-1)/(t2.bmk_downside_ret.pow(1 \ t2.bmk_downside_cnt)-1) AS downside_capture_ratio
+        FROM t1 FULL JOIN t2 ON t1.entity_id = t2.entity_id AND t1.benchmark_id = t2.benchmark_id;
+
+    return t;
+}
+
+/*
  *    Sharpe Ratio
  *    NOTE: Java version is noncompliant-GIPS annulized number
  */
@@ -192,10 +258,11 @@ def cal_treynor(ret, risk_free, beta) {
          AND rfr.ret > -1
        CONTEXT BY t.entity_id;
        
-    treynor = SELECT t.entity_id, ((1 + t.ret).prod().pow(12\iif(t.cnt[0]<12, 12, t.cnt[0])) - (1 + t.rfr_ret).prod().pow(12\iif(t.cnt[0]<12, 12, t.cnt[0]))) / beta.beta[0] AS treynor
+    treynor = SELECT t.entity_id, beta.benchmark_id,
+                    ((1 + t.ret).prod().pow(12\iif(t.cnt[0]<12, 12, t.cnt[0])) - (1 + t.rfr_ret).prod().pow(12\iif(t.cnt[0]<12, 12, t.cnt[0]))) / beta.beta[0] AS treynor
               FROM t
               INNER JOIN beta AS beta ON t.entity_id = beta.entity_id
-              GROUP BY t.entity_id;
+              GROUP BY t.entity_id, beta.benchmark_id;
 
     return treynor;
 }
@@ -206,12 +273,12 @@ def cal_treynor(ret, risk_free, beta) {
  */
 def cal_jensen(ret, bmk_ret, risk_free, beta) {
 
-    jensen = SELECT t.entity_id, t.ret.mean() - rfr.ret.mean() - beta.beta[0] * (bmk.ret.mean() - rfr.ret.mean()) AS jensen
+    jensen = SELECT t.entity_id, t.ret.mean() - rfr.ret.mean() - beta.beta[0] * (bmk.ret.mean() - rfr.ret.mean()) AS jensen, beta.benchmark_id
              FROM ret t
-             INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date
-             INNER JOIN risk_free rfr ON t.end_date = rfr.end_date
              INNER JOIN beta beta ON t.entity_id = beta.entity_id
-             GROUP BY t.entity_id;
+             INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date AND beta.benchmark_id = bmk.benchmark_id
+             INNER JOIN risk_free rfr ON t.end_date = rfr.end_date
+             GROUP BY t.entity_id, beta.benchmark_id;
                
     return jensen;
 }
@@ -234,13 +301,14 @@ def cal_calmar(ret_a){
  *     NOTE: M2 = sharpe * std(benchmark) + risk_free_rate
  *     NOTE: Java version is noncompliant-GIPS annulized number 
  */
-def cal_m2(ret, bmk_ret, risk_free) {
+def cal_m2(ret, benchmarks, bmk_ret, risk_free) {
 
-    m2 = SELECT t.entity_id, (t.ret - rfr.ret).mean() / t.ret.std() * bmk.ret.std() + rfr.ret.mean() AS m2
+    m2 = SELECT t.entity_id, (t.ret - rfr.ret).mean() / t.ret.std() * bmk.ret.std() + rfr.ret.mean() AS m2, bm.benchmark_id
          FROM ret t
-         INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date
+         INNER JOIN benchmarks bm ON t.entity_id = bm.entity_id
+         INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date AND bm.benchmark_id = bmk.benchmark_id
          INNER JOIN risk_free rfr ON t.end_date = rfr.end_date
-         GROUP BY t.entity_id;
+         GROUP BY t.entity_id, bm.benchmark_id;
 
     return m2;
 }
@@ -267,13 +335,12 @@ def cal_ms_return(ret, risk_free) {
     return r;
 }
 
-
 /*
- *   Monthly Since_inception_date Indicator Calculation
- *   @param: ret <TABLE>: 收益表,NEED COLUMNS entity_id, price_dat, end_date, nav 
+ *   Calculation for monthly indicators which need benchmark
+ *   @param ret <TABLE>: 收益表,NEED COLUMNS entity_id, price_dat, end_date, nav
+ *   @param benchmarks <TABLE>: entity-benchmark mapping table 
  *   @param index_ret <TABLE>: historical benchmark return table, NEED COLUMNS fund_id, end_date, ret
  *   @param risk_free <TABLE>: historical risk free rate table, NEED COLUMNS fund_id, end_date, ret
- *   @param freq <CHAR>: 数据频率,d, w, m, q, s, a
  *   
  *   @return: indicators table
  *   
@@ -282,24 +349,16 @@ def cal_ms_return(ret, risk_free) {
  *                     TODO: some datapoints require more data, we need a way to disable calculation for them
  *
  */
-def cal_indicators(mutable ret, index_ret, risk_free, freq) {
-
-    if (! freq IN ['d', 'w', 'm', 'q', 's', 'a']) return null;
+def cal_indicators_with_benchmark(mutable ret, benchmarks, index_ret, risk_free) {
 
     // sorting for correct first() and last() value
     ret.sortBy!(['entity_id', 'price_date'], [1, 1]);
 
-    // 收益、标准差、偏度、峰度、最大回撤、VaR, CVaR
-    rtn = cal_basic_performance(ret, freq);
-
     // alpha, beta
-    alpha_beta = cal_alpha_beta(ret, index_ret, risk_free);
+    alpha_beta = cal_alpha_beta(ret, benchmarks, index_ret, risk_free);
 
     // 胜率、跟踪误差、信息比率
-    bmk_tracking = cal_benchmark_tracking(ret, index_ret);
-
-    // 夏普
-    sharpe = cal_sharpe(ret, rtn, risk_free);
+    bmk_tracking = cal_benchmark_tracking(ret, benchmarks, index_ret);
 
     // 特雷诺
     treynor = cal_treynor(ret, risk_free, alpha_beta);
@@ -307,60 +366,108 @@ def cal_indicators(mutable ret, index_ret, risk_free, freq) {
     // 詹森指数
     jensen = cal_jensen(ret, index_ret, risk_free, alpha_beta);
 
+    // M2
+    m2 = cal_m2(ret, benchmarks, index_ret, risk_free);
+
+    // 上下行捕获率、收益
+    capture_r = cal_capture_ratio(ret, benchmarks, index_ret);
+
+    r = SELECT * FROM bmk_tracking a1
+                 LEFT JOIN alpha_beta ON a1.entity_id = alpha_beta.entity_id AND a1.benchmark_id = alpha_beta.benchmark_id
+                 LEFT JOIN treynor ON a1.entity_id = treynor.entity_id AND a1.benchmark_id = treynor.benchmark_id
+                 LEFT JOIN jensen ON a1.entity_id = jensen.entity_id AND a1.benchmark_id = jensen.benchmark_id
+                 LEFT JOIN m2 ON a1.entity_id = m2.entity_id AND a1.benchmark_id = m2.benchmark_id
+                 LEFT JOIN capture_r ON a1.entity_id = capture_r.entity_id AND a1.benchmark_id = capture_r.benchmark_id;
+
+    // 年化各数据点
+    // GIPS RULE: NO annulization for data less than 1 year
+    plainAnnu = get_annulization_multiple('m');
+    sqrtAnnu = sqrt(get_annulization_multiple('m'));
+
+    r.addColumn(['alpha_a', 'jensen_a', 'track_error_a', 'info_a', 'm2_a'],
+                [DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
+
+    UPDATE r 
+      SET alpha_a = alpha * iif(price_date.month() - min_date.month() >= 11, plainAnnu, 1),
+          jensen_a = jensen * iif(price_date.month() - min_date.month() >= 11, plainAnnu, 1),
+          track_error_a = track_error * iif(price_date.month() - min_date.month() >= 11, sqrtAnnu, 1),
+          info_a = info * iif(price_date.month() - min_date.month() >= 11, sqrtAnnu, 1),
+          m2_a = m2 * iif(price_date.month() - min_date.month() >= 11, plainAnnu, 1);
+    
+    return r.dropColumns!(['end_date', 'price_date', 'min_date']);
+}
+
+/*
+ *   Monthly standard indicator calculation
+ *   @param: ret <TABLE>: 收益表,NEED COLUMNS entity_id, price_dat, end_date, nav 
+ *   @param benchmarks <TABLE>: entity-benchmark mapping table
+ *   @param index_ret <TABLE>: historical benchmark return table, NEED COLUMNS fund_id, end_date, ret
+ *   @param risk_free <TABLE>: historical risk free rate table, NEED COLUMNS fund_id, end_date, ret
+ *   @param freq <CHAR>: 数据频率,d, w, m, q, s, a
+ *   
+ *   @return: indicators table
+ *   
+ *   
+ *   Create  20240904  模仿Java & python代码在Dolphin中实现,具体计算逻辑可能会有不同                          Joey
+ *                     TODO: some datapoints require more data, we need a way to disable calculation for them
+ *
+ */
+def cal_indicators(mutable ret, benchmarks, benchmark_ret, risk_free) {
+
+    // sorting for correct first() and last() value
+    ret.sortBy!(['entity_id', 'price_date'], [1, 1]);
+
+    // 收益、标准差、偏度、峰度、最大回撤、VaR, CVaR
+    rtn = cal_basic_performance(ret, 'm');
+
+    // 夏普
+    sharpe = cal_sharpe(ret, rtn, risk_free);
+
     // 卡玛比率
     calmar = cal_calmar(rtn);
 
     // 整合后的下行标准差、欧米伽、索提诺、卡帕
     lpms = cal_omega_sortino_kappa(ret, risk_free);
 
-    // M2
-    m2 = cal_m2(ret, index_ret, risk_free);
+    // 需要基准的指标们
+    indicator_with_benchmark = cal_indicators_with_benchmark(ret, benchmarks, benchmark_ret, risk_free);
 
     r = SELECT * FROM rtn a1
-                 LEFT JOIN alpha_beta ON a1.entity_id = alpha_beta.entity_id
-                 LEFT JOIN bmk_tracking ON a1.entity_id = bmk_tracking.entity_id
                  LEFT JOIN sharpe ON a1.entity_id = sharpe.entity_id
-                 LEFT JOIN treynor ON a1.entity_id = treynor.entity_id
-                 LEFT JOIN jensen ON a1.entity_id = jensen.entity_id
                  LEFT JOIN calmar ON a1.entity_id = calmar.entity_id
                  LEFT JOIN lpms ON a1.entity_id = lpms.entity_id
-                 LEFT JOIN m2 ON a1.entity_id = m2.entity_id
+                 LEFT JOIN indicator_with_benchmark ON a1.entity_id = indicator_with_benchmark.entity_id;
 
     // 年化各数据点
     // GIPS RULE: NO annulization for data less than 1 year
-    plainAnnu = get_annulization_multiple(freq);
-    sqrtAnnu = sqrt(get_annulization_multiple(freq));
+    plainAnnu = get_annulization_multiple('m');
+    sqrtAnnu = sqrt(get_annulization_multiple('m'));
 
-    r.addColumn(['std_dev_a', 'ds_dev_a', 'alpha_a', 'sharpe_a', 'sortino_a', 'jensen_a', 'track_error_a', 'info_a', 'm2_a'],
-                [DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
+    r.addColumn(['std_dev_a', 'ds_dev_a', 'sharpe_a', 'sortino_a'],
+                [DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
 
     UPDATE r 
       SET std_dev_a = std_dev * iif(price_date.month() - min_date.month() >= 11, sqrtAnnu, 1),
           ds_dev_a = ds_dev * iif(price_date.month() - min_date.month() >= 11, sqrtAnnu, 1),
-          alpha_a = alpha * iif(price_date.month() - min_date.month() >= 11, plainAnnu, 1),
           sharpe_a = sharpe * iif(price_date.month() - min_date.month() >= 11, sqrtAnnu, 1),
-          sortino_a = sortino * iif(price_date.month() - min_date.month() >= 11, sqrtAnnu, 1),
-          jensen_a = jensen * iif(price_date.month() - min_date.month() >= 11, plainAnnu, 1),
-          track_error_a = track_error * iif(price_date.month() - min_date.month() >= 11, sqrtAnnu, 1),
-          info_a = info * iif(price_date.month() - min_date.month() >= 11, sqrtAnnu, 1),
-          m2_a = m2 * iif(price_date.month() - min_date.month() >= 11, plainAnnu, 1);
+          sortino_a = sortino * iif(price_date.month() - min_date.month() >= 11, sqrtAnnu, 1);
     
     return r;
 }
 
+
 /*
  *   Calculate trailing 6m, ytd, 1y, 2y, 3y, 4y, 5y, 10y and since inception indicators
  *   
  *   @param: entity_info <TABLE>: basic information of entity, NEED COLUMNS entity_id, inception_date
+ *   @param benchmarks <TABLE>: entity-benchmark mapping table
  *   @param: ret <TABLE>: 收益表,NEED COLUMNS entity_id, price_dat, end_date, nav 
  *   @param: end_day <DATE>: 计算截止日期
  *   @param index_ret <TABLE>: historical benchmark return table, NEED COLUMNS fund_id, end_date, ret
  *   @param risk_free <TABLE>: historical risk free rate table, NEED COLUMNS fund_id, end_date, ret
- *   @param freq <CHAR>: 数据频率,d, w, m, q, s, a
-
  * 
  */
-def cal_all_trailing_indicators(entity_info, mutable tb_ret, end_day, bmk_ret, risk_free_rate, freq) {
+def cal_trailing_indicators(entity_info, benchmarks, mutable tb_ret, end_day, bmk_ret, risk_free_rate) {
 
     r_incep = null;
     r_ytd = null;
@@ -377,41 +484,41 @@ def cal_all_trailing_indicators(entity_info, mutable tb_ret, end_day, bmk_ret, r
 
     // since inception
     if(tb_ret.size() > 0) {
-        r_incep = cal_indicators(tb_ret, bmk_ret, risk_free_rate, 'm');
+        r_incep = cal_indicators(tb_ret, benchmarks, bmk_ret, risk_free_rate);
     }
 
     // ytd
     tb_ret_ytd = SELECT * FROM tb_ret WHERE end_date >= end_day.yearBegin().month();
     if(tb_ret_ytd.size() > 0) {
-        r_ytd = cal_indicators(tb_ret_ytd, bmk_ret, risk_free_rate, 'm');
+        r_ytd = cal_indicators(tb_ret_ytd, benchmarks, bmk_ret, risk_free_rate);
     }
 
     // trailing 6m
     tb_ret_6m = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
                 WHERE r.end_date > end_day.month()-6 AND (end_day.month() - ei.inception_date.month()) >= 6;
     if(tb_ret_6m.size() > 0) {
-        r_6m = cal_indicators(tb_ret_6m, bmk_ret, risk_free_rate, 'm');
+        r_6m = cal_indicators(tb_ret_6m, benchmarks, bmk_ret, risk_free_rate);
     }
     
     // trailing 1y
     tb_ret_1y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
                 WHERE r.end_date > end_day.month()-12 AND (end_day.month() - ei.inception_date.month()) >= 12;
     if(tb_ret_1y.size() > 0) {
-        r_1y = cal_indicators(tb_ret_1y, bmk_ret, risk_free_rate, 'm');
+        r_1y = cal_indicators(tb_ret_1y, benchmarks, bmk_ret, risk_free_rate);
     }
     
     // trailing 2y
     tb_ret_2y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
                 WHERE r.end_date > end_day.month()-24 AND (end_day.month() - ei.inception_date.month()) >= 24;
     if(tb_ret_2y.size() > 0) {
-        r_2y = cal_indicators(tb_ret_2y, bmk_ret, risk_free_rate, 'm');
+        r_2y = cal_indicators(tb_ret_2y, benchmarks, bmk_ret, risk_free_rate);
     }
     
     // trailing 3y
     tb_ret_3y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
                 WHERE r.end_date > end_day.month()-36 AND (end_day.month() - ei.inception_date.month()) >= 36;
     if(tb_ret_3y.size() > 0) {
-        r_3y = cal_indicators(tb_ret_3y, bmk_ret, risk_free_rate, 'm');
+        r_3y = cal_indicators(tb_ret_3y, benchmarks, bmk_ret, risk_free_rate);
         r_ms_3y = cal_ms_return(tb_ret_3y, risk_free_rate);
     }
 
@@ -419,14 +526,14 @@ def cal_all_trailing_indicators(entity_info, mutable tb_ret, end_day, bmk_ret, r
     tb_ret_4y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
                 WHERE r.end_date > end_day.month()-48 AND (end_day.month() - ei.inception_date.month()) >= 48;
     if(tb_ret_4y.size() > 0) {
-        r_4y = cal_indicators(tb_ret_4y, bmk_ret, risk_free_rate, 'm');
+        r_4y = cal_indicators(tb_ret_4y, benchmarks, bmk_ret, risk_free_rate);
     }
     
     // trailing 5y
     tb_ret_5y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
                 WHERE r.end_date > end_day.month()-60 AND (end_day.month() - ei.inception_date.month()) >= 60;
     if(tb_ret_5y.size() > 0) {
-        r_5y = cal_indicators(tb_ret_5y, bmk_ret, risk_free_rate, 'm');
+        r_5y = cal_indicators(tb_ret_5y, benchmarks, bmk_ret, risk_free_rate);
         r_ms_5y = cal_ms_return(tb_ret_5y, risk_free_rate);
     }
 
@@ -434,7 +541,7 @@ def cal_all_trailing_indicators(entity_info, mutable tb_ret, end_day, bmk_ret, r
     tb_ret_10y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
                 WHERE r.end_date > end_day.month()-120 AND (end_day.month() - ei.inception_date.month()) >= 120;
     if(tb_ret_10y.size() > 0) {
-        r_10y = cal_indicators(tb_ret_10y, bmk_ret, risk_free_rate, 'm');
+        r_10y = cal_indicators(tb_ret_10y, benchmarks, bmk_ret, risk_free_rate);
         r_ms_10y = cal_ms_return(tb_ret_10y, risk_free_rate);
     }
 
@@ -449,8 +556,10 @@ def cal_all_trailing_indicators(entity_info, mutable tb_ret, end_day, bmk_ret, r
  *   @param end_day <DATE>: 要计算的日期
  *   @param isFromNav <BOOL>: 用净值实时计算还是从表中取月收益
  *   @param isFromSQL <BOOL>: TODO: 从MySQL还是本地DolphinDB取净值/收益数据
+ *   
+ *   TODO: primary_benchmark_id seems not be used as benchmark, when it is FA00000VNB
  * 
- *   Example: cal_fund_indicators('FD', "'HF000004KN','HF000103EU','HF00018WXG'", 2024.06.28, true);
+ *   Example: cal_fund_indicators('HF', "'HF000004KN','HF000103EU','HF00018WXG'", 2024.06.28, true);
  * 
  */
 def cal_fund_indicators(entity_type, fund_ids, end_day, isFromNav) {
@@ -469,12 +578,16 @@ def cal_fund_indicators(entity_type, fund_ids, end_day, isFromNav) {
         tb_ret = get_monthly_ret('FD', fund_ids, very_old_date, end_day, true);
         tb_ret.rename!(['fund_id'], ['entity_id']);
     }
-    
-    bmk_ret = SELECT fund_id, temporalParse(end_date, 'yyyy-MM') AS end_date, ret FROM get_monthly_ret('IX', "'IN00000008'", very_old_date, end_day, true); 
+
+    // 取基金和基准的对照表
+    primary_benchmark = SELECT entity_id, iif(benchmark_id.isNull(), 'IN00000008', benchmark_id) AS benchmark_id FROM fund_info;
+
+    // 取所有出现的基准月收益
+    bmk_ret = get_benchmark_return(primary_benchmark, end_day);
 
     risk_free_rate = SELECT fund_id, temporalParse(end_date, 'yyyy-MM') AS end_date, ret FROM get_risk_free_rate(very_old_date, end_day);
 
-    return cal_all_trailing_indicators(fund_info, tb_ret, end_day, bmk_ret, risk_free_rate, 'm');
+    return cal_trailing_indicators(fund_info, primary_benchmark, tb_ret, end_day, bmk_ret, risk_free_rate);
 }
 
 /*
@@ -513,7 +626,7 @@ def cal_portfolio_indicators(portfolio_ids, end_day, cal_method, isFromNav) {
     }
 
     // 沪深300做基准,同SQL保持一致
-    bmk_ret = SELECT fund_id, temporalParse(end_date, 'yyyy-MM') AS end_date, ret FROM get_monthly_ret('IX', "'IN00000008'", very_old_date, end_day, true); 
+    bmk_ret = SELECT fund_id AS benchmark_id, 'PBI' AS benchmark_type, temporalParse(end_date, 'yyyy-MM') AS end_date, ret FROM get_monthly_ret('IX', "'IN00000008'", very_old_date, end_day, true); 
 
     risk_free_rate = SELECT fund_id, temporalParse(end_date, 'yyyy-MM') AS end_date, ret FROM get_risk_free_rate(very_old_date, end_day);