Joey 6 miesięcy temu
rodzic
commit
59e90bd544
1 zmienionych plików z 98 dodań i 43 usunięć
  1. 98 43
      modules/indicatorCalculator.dos

+ 98 - 43
modules/indicatorCalculator.dos

@@ -53,7 +53,7 @@ defg aggCVaR(returns, method, confidenceLevel) {
 }
 
 /*
- *   最大回撤
+ *   回撤
  * 
  * 
  */
@@ -107,10 +107,11 @@ def get_benchmark_return(benchmarks, end_day) {
  *   NOTE: standard deviation of Java version is noncompliant-GIPS annulized number
  *     
  *   Create:  20240904                                                                     Joey
- *            TODO: max drawdowns are the same with SWAGGER, but are off with SQL
+ *            TODO: SQL is wrong for max drawdowns
  *            TODO: var, cvar, calmar are off; std dev, skewness, kurtosis are slightly off
- *            TODO: since inception date
- *              
+ *            TODO: SQL is missing for portfolio since inception date return
+ *            TODO: Java calculates max drawdown even there is no nav
+ *            TODO: Java ytd worst month could be wrong (i.e. portfolio 166002, 2024-03)
  *     
  */
 def cal_basic_performance(entity_info, ret, trailing_month) {
@@ -139,8 +140,8 @@ def cal_basic_performance(entity_info, ret, trailing_month) {
         
         // 不会用上面的办法算最大回撤, VaR, CVaR
         t_var = SELECT entity_id, end_date, ret,
-                       cummax(1 - nav \ nav.cummax()) AS drawdown,
-                       cumpercentile(ret, 5, 'linear') AS var
+                       cummax(1 - nav \ cummax(nav)) AS drawdown,
+                       - cumpercentile(ret, 5, 'linear') AS var
                 FROM ret WHERE ret > -1
                 CONTEXT BY entity_id;
 
@@ -162,7 +163,7 @@ def cal_basic_performance(entity_info, ret, trailing_month) {
                     iif(cumcount(entity_id) > 5, tmoving(skew{, false}, end_date, ret, 12), null) AS skewness,
                     iif(cumcount(entity_id) > 5, tmoving(kurtosis{, false}, end_date, ret, 12)-3, null) AS kurtosis,
                     cummin(ret) AS wrst_month,
-                    maxDrawdown(nav) AS drawdown
+                    cummax(1 - nav \ cummax(nav)) AS drawdown
         FROM ret WHERE ret > -1
         CONTEXT BY entity_id, end_date.year()
         ORDER BY entity_id, end_date;
@@ -308,7 +309,7 @@ def cal_omega_sortino_kappa(ret, risk_free, trailing_month) {
  *    Winning Ratio, Tracking Error, Information Ratio
  *    
  *    NOTE: mcount is very unique in mFun, because it doesn't support minPeriods(BUG?), while others default minPeriods = window.
- *          As a result, we have to live with lots of records having winrate but no tracking error and info ratio
+ *          As a result, we have to delete records having winrate but no tracking error and info ratio for the sake of consisence
  *   
  *    TODO: Win Rate incept is off, because Java incorrectly takes all end_date as denominator even when benchmark has no price
  *          Information Ratio is way off!
@@ -320,7 +321,8 @@ def cal_benchmark_tracking(ret, benchmarks, bmk_ret, trailing_month) {
 
         t0 = SELECT t.entity_id, t.end_date, t.price_date,
                     t.ret, bmk.ret AS ret_bmk,
-                    t.entity_id.cumcount() AS cnt, (t.ret - bmk.ret) AS exc_ret, bm.benchmark_id
+                    t.entity_id.cumcount() AS cnt,
+                    t.ret - bmk.ret AS exc_ret, bm.benchmark_id
              FROM ret t
              INNER JOIN benchmarks bm ON t.entity_id = bm.entity_id AND t.end_date = bm.end_date
              INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date AND bm.benchmark_id = bmk.benchmark_id
@@ -329,10 +331,10 @@ def cal_benchmark_tracking(ret, benchmarks, bmk_ret, trailing_month) {
              CONTEXT BY t.entity_id, bm.benchmark_id;
 
         t = SELECT entity_id, end_date, benchmark_id,
-                   cumcount(iif(exc_ret >= 0, 1, null)) \ cnt AS winrate,
-                   exc_ret.cumstd() AS track_error, 
-                   iif(exc_ret.cumstd() == 0, null, exc_ret.cumavg() \ exc_ret.cumstd()) AS info
-            FROM t0 
+                   iif(cnt > 5, cumcount(iif(exc_ret >= 0, 1, null)) \ cnt, null) AS winrate,
+                   iif(cnt > 5, exc_ret.cumstd(), null) AS track_error, 
+                   iif(cnt > 5, iif(exc_ret.cumstd() == 0, null, exc_ret.cumavg() \ exc_ret.cumstd()), 5) AS info
+            FROM t0
             CONTEXT BY entity_id, benchmark_id
             ORDER BY entity_id, end_date, benchmark_id;
 
@@ -340,7 +342,7 @@ def cal_benchmark_tracking(ret, benchmarks, bmk_ret, trailing_month) {
 
         t0 = SELECT t.entity_id, t.end_date, t.price_date,
                     t.ret, bmk.ret AS ret_bmk,
-                    t.entity_id.cumcount() AS cnt, (t.ret - bmk.ret) AS exc_ret, bm.benchmark_id
+                    t.entity_id.cumcount() AS cnt, t.ret - bmk.ret AS exc_ret, bm.benchmark_id
              FROM ret t
              INNER JOIN benchmarks bm ON t.entity_id = bm.entity_id AND t.end_date = bm.end_date
              INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date AND bm.benchmark_id = bmk.benchmark_id
@@ -349,10 +351,10 @@ def cal_benchmark_tracking(ret, benchmarks, bmk_ret, trailing_month) {
              CONTEXT BY t.entity_id, bm.benchmark_id, t.end_date.year();
 
         t = SELECT entity_id, end_date, benchmark_id,
-                   cumcount(iif(exc_ret >= 0, 1, null)) \ cnt AS winrate,
-                   exc_ret.cumstd() AS track_error, 
-                   iif(exc_ret.cumstd() == 0, null, exc_ret.cumavg() \ exc_ret.cumstd()) AS info
-            FROM t0 
+                   iif(cnt > 5, cumcount(iif(exc_ret >= 0, 1, null)) \ cnt, null) AS winrate,
+                   iif(cnt > 5, exc_ret.cumstd(), null) AS track_error, 
+                   iif(cnt > 5, iif(exc_ret.cumstd() == 0, null, exc_ret.cumavg() \ exc_ret.cumstd()), null) AS info
+            FROM t0
             CONTEXT BY entity_id, benchmark_id, end_date.year()
             ORDER BY entity_id, end_date, benchmark_id;
     } else {
@@ -361,7 +363,8 @@ def cal_benchmark_tracking(ret, benchmarks, bmk_ret, trailing_month) {
 
         t0 = SELECT t.entity_id, t.end_date, t.price_date,
                     t.ret, bmk.ret AS ret_bmk,
-                    t.entity_id.mcount(win) AS cnt, (t.ret - bmk.ret) AS exc_ret, bm.benchmark_id
+                    t.entity_id.mcount(win) AS cnt,
+                    t.ret - bmk.ret AS exc_ret, bm.benchmark_id
              FROM ret t
              INNER JOIN benchmarks bm ON t.entity_id = bm.entity_id AND t.end_date = bm.end_date
              INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date AND bm.benchmark_id = bmk.benchmark_id
@@ -370,15 +373,15 @@ def cal_benchmark_tracking(ret, benchmarks, bmk_ret, trailing_month) {
              CONTEXT BY t.entity_id, bm.benchmark_id;
 
         t = SELECT entity_id, end_date, benchmark_id,
-                   mcount(iif(exc_ret >= 0, 1, null), win) \ cnt AS winrate, 
-                   mstd(exc_ret, win) AS track_error, 
-                   iif(mstd(exc_ret, win) == 0, null, mavg(exc_ret, win) \ mstd(exc_ret, win)) AS info
-            FROM t0 
+                   iif(cnt > 5, mcount(iif(exc_ret >= 0, 1, null), win) \ cnt, null) AS winrate, 
+                   iif(cnt > 5, mstd(exc_ret, win), null) AS track_error, 
+                   iif(cnt > 5, iif(mstd(exc_ret, win) == 0, null, mavg(exc_ret, win) \ mstd(exc_ret, win)), null) AS info
+            FROM t0
             CONTEXT BY entity_id, benchmark_id
             ORDER BY entity_id, end_date, benchmark_id;
     }
 
-    return t;
+    return t; //SELECT * FROM t WHERE track_error IS NOT NULL;
 }
 
 /*
@@ -396,7 +399,9 @@ def cal_alpha_beta(ret, benchmarks, bmk_ret, risk_free, trailing_month) {
 
     if(trailing_month == 'incep') {
 
-        beta = SELECT entity_id, end_date, benchmark_id, ret.cumbeta(ret_bmk) AS beta FROM t CONTEXT BY entity_id, benchmark_id;
+        beta = SELECT entity_id, end_date, benchmark_id,
+                      iif(cumcount(end_date) > 5, ret.cumbeta(ret_bmk), null) AS beta
+               FROM t CONTEXT BY entity_id, benchmark_id;
 
         alpha = SELECT t.entity_id, t.end_date, t.benchmark_id, beta.beta AS beta, 
                        (t.ret - rfr.ret).cumavg() - beta.beta * (t.ret_bmk - rfr.ret).cumavg() AS alpha
@@ -408,7 +413,9 @@ def cal_alpha_beta(ret, benchmarks, bmk_ret, risk_free, trailing_month) {
 
     } else if(trailing_month == 'ytd') {
 
-        beta = SELECT entity_id, end_date, benchmark_id, ret.cumbeta(ret_bmk) AS beta FROM t CONTEXT BY entity_id, benchmark_id, end_date.year();
+        beta = SELECT entity_id, end_date, benchmark_id,
+                      iif(cumcount(end_date) > 5, ret.cumbeta(ret_bmk), null) AS beta
+               FROM t CONTEXT BY entity_id, benchmark_id, end_date.year();
 
         alpha = SELECT t.entity_id, t.end_date, t.benchmark_id, beta.beta AS beta, 
                        (t.ret - rfr.ret).cumavg() - beta.beta * (t.ret_bmk - rfr.ret).cumavg() AS alpha
@@ -422,7 +429,9 @@ def cal_alpha_beta(ret, benchmarks, bmk_ret, risk_free, trailing_month) {
 
         win = trailing_month$INT;
 
-        beta = SELECT entity_id, end_date, benchmark_id, ret.mbeta(ret_bmk, win) AS beta FROM t CONTEXT BY entity_id, benchmark_id;
+        beta = SELECT entity_id, end_date, benchmark_id, 
+                      iif(mcount(end_date, win) > 5, ret.mbeta(ret_bmk, win), null) AS beta 
+               FROM t CONTEXT BY entity_id, benchmark_id;
 
         alpha = SELECT t.entity_id, t.end_date, t.benchmark_id, beta.beta AS beta, 
                        (t.ret - rfr.ret).mavg(win) - beta.beta * (t.ret_bmk - rfr.ret).mavg(win) AS alpha
@@ -806,6 +815,9 @@ def get_effective_benchmarks(benchmarks, end_day, trailing_month, isEffectiveOnl
  */
 def cal_indicators_with_benchmark(entity_info, benchmark_mapping, end_day, tb_ret, index_ret, risk_free, month) {
 
+    if(entity_info.isVoid() || entity_info.size() == 0 || benchmark_mapping.isVoid() || benchmark_mapping.size() == 0 ) return null;
+    if(tb_ret.isVoid() || tb_ret.size() == 0 || index_ret.isVoid() || index_ret.size() == 0 || risk_free.isVoid() || risk_free.size() == 0 ) return null;
+
     // sorting for correct first() and last() value
     ret = SELECT * FROM tb_ret WHERE ret > -1 AND end_date <= end_day.month() ORDER BY entity_id, price_date;
 
@@ -877,6 +889,9 @@ def cal_indicators_with_benchmark(entity_info, benchmark_mapping, end_day, tb_re
  */
 def cal_indicators(entity_info, benchmarks, end_day, tb_ret, benchmark_ret, risk_free, month) {
 
+    if(entity_info.isVoid() || entity_info.size() == 0 || benchmarks.isVoid() || benchmarks.size() == 0 ) return null;
+    if(tb_ret.isVoid() || tb_ret.size() == 0 || benchmark_ret.isVoid() || benchmark_ret.size() == 0 || risk_free.isVoid() || risk_free.size() == 0 ) return null;
+
     // sorting for correct first() and last() value
     ret = SELECT * FROM tb_ret WHERE end_date <= end_day.month() ORDER BY entity_id, price_date;
 
@@ -938,7 +953,7 @@ def cal_indicators(entity_info, benchmarks, end_day, tb_ret, benchmark_ret, risk
  *   
  * 
  */
-def cal_trailing2(func, entity_info, benchmarks, end_day, tb_ret, bmk_ret, risk_free_rate) {
+def cal_trailing(func, entity_info, benchmarks, end_day, tb_ret, bmk_ret, risk_free_rate) {
 
     r_incep = null;
     r_ytd = null;
@@ -992,9 +1007,9 @@ def cal_trailing2(func, entity_info, benchmarks, end_day, tb_ret, bmk_ret, risk_
  *   @param risk_free <TABLE>: historical risk free rate table, NEED COLUMNS fund_id, end_date, ret
  * 
  */
-def cal_trailing_indicators2(entity_info, benchmarks, end_day, tb_ret, bmk_ret, risk_free_rate) {
+def cal_trailing_indicators(entity_info, benchmarks, end_day, tb_ret, bmk_ret, risk_free_rate) {
 	
-    return cal_trailing2(cal_indicators, entity_info, benchmarks, end_day, tb_ret, bmk_ret, risk_free_rate);
+    return cal_trailing(cal_indicators, entity_info, benchmarks, end_day, tb_ret, bmk_ret, risk_free_rate);
 
 }
 
@@ -1010,14 +1025,14 @@ def cal_trailing_indicators2(entity_info, benchmarks, end_day, tb_ret, bmk_ret,
  *   
  * 
  */
-def cal_trailing_bfi_indicators2(entity_info, benchmarks, end_day, tb_ret, bmk_ret, risk_free_rate) {
+def cal_trailing_bfi_indicators(entity_info, benchmarks, end_day, tb_ret, bmk_ret, risk_free_rate) {
 
-    return cal_trailing2(cal_indicators_with_benchmark, entity_info, benchmarks, end_day, tb_ret, bmk_ret, risk_free_rate);
+    return cal_trailing(cal_indicators_with_benchmark, entity_info, benchmarks, end_day, tb_ret, bmk_ret, risk_free_rate);
 }
 
 
 /*
- *   Calculate fund indicators for one date
+ *   Calculate historcial fund trailing indicators 
  * 
  *   @param entity_type <STRING>: MF, HF
  *   @param fund_ids <STRING>: 逗号和单引号分隔的fund_id
@@ -1052,8 +1067,12 @@ def cal_fund_indicators(entity_type, fund_ids, end_day, isFromNav) {
         // 从fund_performance表里读月收益
         tb_ret = get_monthly_ret('FD', fund_ids, very_old_date, end_day, true);
         tb_ret.rename!(['fund_id'], ['entity_id']);
+        v_end_date = tb_ret.end_date.temporalParse('yyyy-MM');
+        tb_ret.replaceColumn!('end_date', v_end_date);
     }
 
+    if(tb_ret.isVoid() || tb_ret.size() == 0) { return null; }
+
     // 取基金和基准的对照表
     primary_benchmark = SELECT fund_id AS entity_id, end_date, iif(benchmark_id.isNull(), 'IN00000008', benchmark_id) AS benchmark_id 
                         FROM get_fund_primary_benchmark(fund_ids, start_month.temporalFormat('yyyy-MM'), end_day.month().temporalFormat('yyyy-MM')) ;
@@ -1061,10 +1080,14 @@ def cal_fund_indicators(entity_type, fund_ids, end_day, isFromNav) {
     // 取所有出现的基准月收益
     bmk_ret = get_benchmark_return(primary_benchmark, end_day);
 
+    if(bmk_ret.isVoid() || bmk_ret.size() == 0) { return null; }
+
     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);
 
+    if(risk_free_rate.isVoid() || risk_free_rate.size() == 0) { return null; }
+
     // 标准的指标
-    t0 = cal_trailing_indicators2(fund_info, primary_benchmark, end_day, tb_ret, bmk_ret, risk_free_rate);
+    t0 = cal_trailing_indicators(fund_info, primary_benchmark, end_day, tb_ret, bmk_ret, risk_free_rate);
 
     // Morningstar 指标
     //t1 = cal_trailing_ms_indicators(fund_info, tb_ret, end_day, risk_free_rate);
@@ -1078,7 +1101,7 @@ def cal_fund_indicators(entity_type, fund_ids, end_day, isFromNav) {
 }
 
 /*
- *   Calculate fund BFI indicators for one date
+ *   Calculate historcial fund trailing BFI indicators 
  * 
  *   @param entity_type <STRING>: MF, HF
  *   @param fund_ids <STRING>: 逗号和单引号分隔的fund_id
@@ -1114,8 +1137,12 @@ def cal_fund_bfi_indicators(entity_type, fund_ids, end_day, isFromNav) {
         // 从fund_performance表里读月收益
         tb_ret = get_monthly_ret('FD', fund_ids, very_old_date, end_day, true);
         tb_ret.rename!(['fund_id'], ['entity_id']);
+        v_end_date = tb_ret.end_date.temporalParse('yyyy-MM');
+        tb_ret.replaceColumn!('end_date', v_end_date);
     }
 
+    if(tb_ret.isVoid() || tb_ret.size() == 0) { return null; }
+
     // 取基金和基准的对照表
     bfi_benchmark = SELECT fund_id AS entity_id, end_date.temporalParse('yyyy-MM') AS end_date, factor_id AS benchmark_id 
                            FROM get_fund_bfi_factors(fund_ids, start_month.temporalFormat('yyyy-MM'), end_day.temporalFormat('yyyy-MM'));
@@ -1126,7 +1153,9 @@ def cal_fund_bfi_indicators(entity_type, fund_ids, end_day, isFromNav) {
 
     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);
 
-    t0 = cal_trailing_bfi_indicators2(fund_info, bfi_benchmark, end_day, tb_ret, bmk_ret, risk_free_rate);
+    if(risk_free_rate.isVoid() || risk_free_rate.size() == 0) { return null; }
+
+    t0 = cal_trailing_bfi_indicators(fund_info, bfi_benchmark, end_day, tb_ret, bmk_ret, risk_free_rate);
 
     // BFI stands for "Best Fit Index"
     v_table_name = ['BFI-INCEP', 'BFI-YTD', 'BFI-6M', 'BFI-1Y', 'BFI-2Y', 'BFI-3Y', 'BFI-4Y', 'BFI-5Y', 'BFI-10Y'];
@@ -1135,7 +1164,7 @@ def cal_fund_bfi_indicators(entity_type, fund_ids, end_day, isFromNav) {
 }
 
 /*
- *  Calculate portfolio indicators for one date
+ *   Calculate historcial portfolio trailing indicators 
  *  
  *  @param portfolio_ids <STRING>: comma-delimited portfolio ids
  *  @param end_day <DATE>: the date
@@ -1148,6 +1177,7 @@ def cal_fund_bfi_indicators(entity_type, fund_ids, end_day, isFromNav) {
 def cal_portfolio_indicators(portfolio_ids, end_day, cal_method, isFromNav) {
 
     very_old_date = 1990.01.01;
+    start_month = very_old_date.month();
 
     portfolio_info = get_portfolio_info(portfolio_ids);
 
@@ -1158,7 +1188,9 @@ def cal_portfolio_indicators(portfolio_ids, end_day, cal_method, isFromNav) {
     if(isFromNav == true) {
         // 从净值开始计算收益
         tb_raw_ret = SELECT * FROM cal_portfolio_nav(portfolio_ids, very_old_date, cal_method) WHERE price_date <= end_day;
-       
+
+        if(tb_raw_ret.isVoid() || tb_raw_ret.size() == 0) return null;
+
         // funky thing is you can't use "AS" for the grouping columns?
         tb_ret = SELECT portfolio_id, price_date.month(), price_date.last() AS price_date, (1+ret).prod()-1 AS ret, nav.last() AS nav
                  FROM tb_raw_ret
@@ -1170,17 +1202,30 @@ def cal_portfolio_indicators(portfolio_ids, end_day, cal_method, isFromNav) {
         // 从pf_portfolio_performance表里读月收益
         tb_ret = get_monthly_ret('PF', portfolio_ids, very_old_date, end_day, true);
         tb_ret.rename!(['portfolio_id'], ['entity_id']);
+        v_end_date = tb_ret.end_date.temporalParse('yyyy-MM');
+        tb_ret.replaceColumn!('end_date', v_end_date);
     }
 
+    if(tb_ret.isVoid() || tb_ret.size() == 0) return null;
+
     // 沪深300做基准,同SQL保持一致
-    primary_benchmark = SELECT entity_id, 'IN00000008' AS benchmark_id FROM portfolio_info;
+    t_dates = table(start_month..end_day.month() AS end_date);
+    primary_benchmark = SELECT ei.entity_id, dt.end_date, 'IN00000008' AS benchmark_id 
+                        FROM portfolio_info ei JOIN t_dates dt
+                        WHERE dt.end_date >= ei.inception_date.month();
+
+    if(primary_benchmark.isVoid() || primary_benchmark.size() == 0) { return null; }
 
     // 取所有出现的基准月收益
     bmk_ret = get_benchmark_return(primary_benchmark, end_day);
 
+    if(bmk_ret.isVoid() || bmk_ret.size() == 0) { return null; }
+
     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);
 
-    t0 = cal_trailing_indicators2(portfolio_info, primary_benchmark, end_day, tb_ret, bmk_ret, risk_free_rate);
+    if(risk_free_rate.isVoid() || risk_free_rate.size() == 0) { return null; }
+
+    t0 = cal_trailing_indicators(portfolio_info, primary_benchmark, end_day, tb_ret, bmk_ret, risk_free_rate);
 
     v_table_name = ['PBI-INCEP', 'PBI-YTD', 'PBI-6M', 'PBI-1Y', 'PBI-2Y', 'PBI-3Y', 'PBI-4Y', 'PBI-5Y', 'PBI-10Y'];
     
@@ -1190,7 +1235,7 @@ def cal_portfolio_indicators(portfolio_ids, end_day, cal_method, isFromNav) {
 
 
 /*
- *  Calculate portfolio bfi indicators for one date
+ *   Calculate historcial portfolio trailing BFI indicators 
  *  
  *  @param portfolio_ids <STRING>: comma-delimited portfolio ids
  *  @param end_day <DATE>: the date
@@ -1217,6 +1262,8 @@ def cal_portfolio_bfi_indicators(portfolio_ids, end_day, cal_method, isFromNav)
     if(isFromNav == true) {
         // 从净值开始计算收益
         tb_raw_ret = SELECT * FROM cal_portfolio_nav(portfolio_ids, very_old_date, cal_method) WHERE price_date <= end_day;
+
+        if(tb_raw_ret.isVoid() || tb_raw_ret.size() == 0) return null;
        
         // funky thing is you can't use "AS" for the grouping columns?
         tb_ret = SELECT portfolio_id, price_date.month(), price_date.last() AS price_date, (1+ret).prod()-1 AS ret, nav.last() AS nav
@@ -1229,19 +1276,27 @@ def cal_portfolio_bfi_indicators(portfolio_ids, end_day, cal_method, isFromNav)
         // 从pf_portfolio_performance表里读月收益
         tb_ret = get_monthly_ret('PF', portfolio_ids, very_old_date, end_day, true);
         tb_ret.rename!(['portfolio_id'], ['entity_id']);
+        v_end_date = tb_ret.end_date.temporalParse('yyyy-MM');
+        tb_ret.replaceColumn!('end_date', v_end_date);
     }
 
+    if(tb_ret.isVoid() || tb_ret.size() == 0) return null;
+
     // 取组合和基准的对照表
-    bfi_benchmark = SELECT portfolio_id AS entity_id, end_date, factor_id AS benchmark_id 
+    bfi_benchmark = SELECT portfolio_id AS entity_id, end_date.temporalParse('yyyy-MM') AS end_date, factor_id AS benchmark_id 
                     FROM get_portfolio_bfi_factors(portfolio_ids, start_month.temporalFormat('yyyy-MM'), end_day.temporalFormat('yyyy-MM'));
 
     if(bfi_benchmark.isVoid() || bfi_benchmark.size() == 0) { return null; }
 
     bmk_ret = get_benchmark_return(bfi_benchmark, end_day);
 
+    if(bmk_ret.isVoid() || bmk_ret.size() == 0) { return null; }
+
     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);
 
-    t0 = cal_trailing_bfi_indicators(portfolio_info, bfi_benchmark, tb_ret, end_day, bmk_ret, risk_free_rate);
+    if(risk_free_rate.isVoid() || risk_free_rate.size() == 0) { return null; }
+    
+    t0 = cal_trailing_bfi_indicators(portfolio_info, bfi_benchmark, end_day, tb_ret, bmk_ret, risk_free_rate);
 
     v_table_name = ['PBI-INCEP', 'PBI-YTD', 'PBI-6M', 'PBI-1Y', 'PBI-2Y', 'PBI-3Y', 'PBI-4Y', 'PBI-5Y', 'PBI-10Y'];