Browse Source

支持基金经理BFI match

Joey 4 months ago
parent
commit
7f8fdde638
3 changed files with 248 additions and 120 deletions
  1. 147 104
      modules/bfiMatcher.dos
  2. 91 11
      modules/task_monthlyPerformance.dos
  3. 10 5
      modules/task_weeklyPerformnce.dos

+ 147 - 104
modules/bfiMatcher.dos

@@ -43,43 +43,6 @@ defg regressionT(y, x) {
  *   
  *   NOTE: 与 Java BFI 不同,这里为了与RBSA保持统一,用收益率来计算相关性;转化成月度时用了各周平均值
  */
-def cal_monthly_closity0(nav1, nav2, win) {
-
-    n1 = nav1;
-    n1.sortBy!(['entity_id', 'price_date'], [1, 1]);
-    n2 = nav2;
-    n2.sortBy!(['benchmark_id', 'price_date'], [1, 1]);
-
-    t0 = SELECT entity_id, end_date, n1.price_date, n1.nav AS nav1, n1.nav.ratios()-1 AS ret1,
-                n2.nav AS nav2, n2.nav.ratios()-1 AS ret2, tmoving(count, end_date, end_date, win) AS data_count
-         FROM n1
-         INNER JOIN n2 ON n1.end_date = n2.end_date
-         ORDER BY end_date;
-
-    t = SELECT entity_id, end_date, price_date,
-               tmcorr(t0.end_date, ret1, ret2, win) AS corr,
-               iif(tmstd(end_date, ret1-ret2, win) == 0, null, tmavg(end_date, ret1-ret2, win)\tmstd(end_date, ret1-ret2, win)) AS info, // 貌似没用
-               iif(data_count >= get_min_threshold('data_count'), tmoving(regressionT, end_date, [ret1, ret2], win), double(NULL)) AS t_value,
-               iif(data_count >= get_min_threshold('data_count'), tmbeta(end_date, ret1, ret2, win), double(NULL)) AS beta // 用 ols() 算的值和这个一样
-        FROM t0
-        ORDER BY price_date;
-
-	// 将每月各周的数据平均值作为月度数据返回
-    return SELECT entity_id, price_date.month().last() AS end_date, price_date.last() AS price_date,
-                  corr.avg() AS corr,
-                  info.avg() AS info,
-                  t_value.avg() AS t_value,
-                  beta.avg() AS beta
-           FROM t
-           GROUP BY entity_id, price_date.month();
-}
-
-
-/*
- *   计算 correlation & bfi-matching 所需要的数据指标(周数据计算,返回月度结果)
- *   
- *   NOTE: 与 Java BFI 不同,这里为了与RBSA保持统一,用收益率来计算相关性;转化成月度时用了各周平均值
- */
 def cal_monthly_closity(entity, nav1, nav2, win) {
 
     n1 = nav1;
@@ -87,36 +50,36 @@ def cal_monthly_closity(entity, nav1, nav2, win) {
     n2 = nav2;
     n2.sortBy!(['benchmark_id', 'price_date'], [1, 1]);
 
-    t_dates = SELECT entity_id, end_date FROM nav1 WHERE end_date >= entity.price_date.weekEnd();
+    t_dates = SELECT entity_id, end_date, price_date FROM nav1 WHERE end_date >= entity.end_date;
 
-    t0 = SELECT entity_id, end_date, n1.price_date, n1.nav AS nav1, n1.nav.ratios()-1 AS ret1,
+    t0 = SELECT entity_id, end_date, n1.nav AS nav1, n1.nav.ratios()-1 AS ret1,
                 n2.nav AS nav2, n2.nav.ratios()-1 AS ret2, tmoving(count, end_date, end_date, win) AS data_count
          FROM n1
          INNER JOIN n2 ON n1.end_date = n2.end_date
          ORDER BY end_date;
 
-	t_rt = table(100:0, ['entity_id', 'end_date', 'price_date', 'corr', 'info', 't_value', 'beta'], 
-				[entity.entity_id.type(), MONTH, DATE, DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
+	t_rt = table(100:0, ['entity_id', 'end_date', 'price_date', 'corr', /* 'info', */ 't_value', 'beta'], 
+				[entity.entity_id.type(), t_dates.end_date[0].type(), DATE, DOUBLE, /* DOUBLE, */ DOUBLE, DOUBLE]);
 				
-	for(dt in t_dates.end_date) {
+	for(dt in t_dates) {
 
-		if( (EXEC data_count FROM t0 WHERE end_date = dt)[0] >= get_min_threshold('data_count') ){
+		if( (EXEC data_count FROM t0 WHERE end_date = dt.end_date)[0] >= get_min_threshold('data_count') ){
 
-			rets = EXEC ret1, ret2 FROM t0 WHERE end_date BETWEEN(dt.temporalAdd(duration('-' + win$STRING)):dt);
+			rets = EXEC ret1, ret2 FROM t0 WHERE end_date BETWEEN(dt.end_date.temporalAdd(duration('-' + win$STRING)):dt.end_date);
 		
 			cor = corr(rets.ret1, rets.ret2);
-			info = mean(rets.ret1 - rets.ret2) \ std(rets.ret1 - rets.ret2); // 貌似没用
+			// info = mean(rets.ret1 - rets.ret2) \ std(rets.ret1 - rets.ret2); // 貌似没用
 		    t_value = regressionT(rets.ret1, rets.ret2);
 	    	bta = beta(rets.ret1, rets.ret2); // 用 ols() 算的值和这个一样
 	
-			INSERT INTO t_rt VALUES (entity.entity_id, dt, entity.price_date, cor, info, t_value, bta);
+			INSERT INTO t_rt VALUES (entity.entity_id, dt.end_date, dt.price_date, cor, /*info ,*/ t_value, bta);
 		}
 	}
 
 	// 将每月各周的数据平均值作为月度数据返回
     return SELECT entity_id, price_date.month().last() AS end_date, price_date.last() AS price_date,
                   corr.avg() AS corr,
-                  info.avg() AS info,
+                  // info.avg() AS info,
                   t_value.avg() AS t_value,
                   beta.avg() AS beta
            FROM t_rt
@@ -127,9 +90,63 @@ def cal_monthly_closity(entity, nav1, nav2, win) {
 /*
  *   计算目标和各资产类别指数及BFI因子的累计净值相关系数
  *   
+ *   @param entity_info <TABLE>: [COLUMNS] entity_id, end_date, price_date (这个 price_date 是需要计算的最早 price_date)
+ *   @param nav_entity <TABLE>: [COLUMNS] entity_id, end_date, price_date, nav
+ *   @param nav_index <TABLE>: [COLUMNS] entity_id, end_date, price_date, nav
+ *   @param entity_coe OUT <TABLE>: [COLUMNS] entity_id, end_date, index_id, coe_1y, coe_3y, coe_5y, t_value_1y, t_value_3y, t_value_5y, beta_1y, beta_3y, beta_5y
+ *   
+ *   NOTE: 1)整合 Java中 TampCalcCorrelationServiceImpl 和 BestFitIndexServiceImpl 做的计算, 取消了没用的 info_ratio
+ *         2)周度数据时,nav表中的 end_date=price_date.weekEnd(); 月度数据时, end_date=price_date.month()
+ *         3)暂时保留取NAV,现算return的方法
+ * 
+ */
+def cal_index_coe(entity_info, nav_entity, nav_index, mutable entity_coe) {
+
+    if(nav_entity.isVoid() || nav_entity.size() == 0 || nav_index.isVoid() || nav_index.size() == 0) return null;
+
+    v_indexes = nav_index.entity_id.distinct();
+
+    // 两次循环遍历所有entity和指数
+    for(entity in entity_info) {
+//entity= entity_info[0]
+
+        nav1 = SELECT entity_id, end_date, price_date, nav
+               FROM nav_entity WHERE entity_id = entity.entity_id;
+
+    	for(index in v_indexes) {
+//index=v_indexes[0]
+            nav2 = SELECT entity_id AS benchmark_id, end_date, price_date, nav 
+                   FROM nav_index WHERE entity_id = index;
+
+            if(nav2.isVoid() || nav2.size() == 0) continue; // 忽略已经停止更新的指数,或者是特殊的无风险利率 IN0000000M
+
+            closity_1y = cal_monthly_closity(entity, nav1, nav2, 1y);
+            closity_3y = cal_monthly_closity(entity, nav1, nav2, 3y);
+            closity_5y = cal_monthly_closity(entity, nav1, nav2, 5y);
+
+    		INSERT INTO entity_coe
+        	    SELECT c1.entity_id, c1.end_date, index,
+        	    	   c1.corr AS coe_1y, c3.corr AS coe_3y, c5.corr AS coe_5y, 
+        	    	   //c1.corr2 AS coe_1y_2, c3.corr2 AS coe_3y_2, c5.corr2 AS coe_5y_2, 
+        	           //c1.info AS info_ratio_1y, c3.info AS info_ratio_3y, c5.info AS info_ratio_5y,
+        	           c1.t_value AS t_value_1y, c3.t_value AS t_value_3y, c5.t_value AS t_value_5y,
+        	           c1.beta AS beta_1y, c3.beta AS beta_3y, c5.beta AS beta_5y
+                FROM closity_1y c1
+                LEFT JOIN closity_3y  c3 ON c1.end_date = c3.end_date
+                LEFT JOIN closity_5y c5 ON c1.end_date = c5.end_date;
+
+    	}
+    }
+
+}
+
+
+/*
+ *   计算基金/组合和各资产类别指数及BFI因子的累计净值相关系数 (用周收益计算)
+ *   
  *   @param entity_info <TABLE>: [COLUMNS] entity_id, price_date
  *   
- *   NOTE: 整合 Java中 TampCalcCorrelationServiceImpl 和 BestFitIndexServiceImpl 做的计算, 但只支持周数据计算
+ *   NOTE: 整合 Java中 TampCalcCorrelationServiceImpl 和 BestFitIndexServiceImpl 做的计算
  * 
  *   Example: cal_entity_index_coe('MF', get_fund_info(['MF00003PW1', 'MF00003PW2', 'MF00003RZI']).join(take(2024.09.30, 3) AS price_date).rename!('fund_id', 'entity_id'));
  *   		  cal_entity_index_coe('PF', get_portfolio_info([166002]).join(take(2024.09.30, 1) AS price_date).rename!('portfolio_id', 'entity_id'));
@@ -137,17 +154,6 @@ def cal_monthly_closity(entity, nav1, nav2, win) {
  */
 def cal_entity_index_coe(entity_type, entity_info) {
 
-// entity_type = 'PF'
-// entity_info = get_portfolio_info([166002]).join(take(2024.09.30, 1) AS price_date).rename!('portfolio_id', 'entity_id');
-
-// entity_type = 'MF'
-// entity_info =  get_fund_info(['MF00003PW1', 'MF00003PW2', 'MF00003RZI']).join(take(2024.09.30, 3) AS price_date).rename!('fund_id', 'entity_id');
-// entity_info = tb_cal_entity[i : min(size, i+batch_size)]
-
-// entity_type = 'HF'
-// entity_info =  get_fund_info(['HF0000A134', 'HF0000A12R', 'HF00017JVX']).join(take(2024.05.30, 3) AS price_date).rename!('fund_id', 'entity_id');
-
-
     if(entity_info.isVoid() || entity_info.size() == 0) return null;
 
     // 取数据集中最早日期作为因子的起始日期
@@ -158,6 +164,7 @@ def cal_entity_index_coe(entity_type, entity_info) {
 	nav_entity = get_nav_for_return_calculation(entity_type, 'w', s_json);
 
 	if(nav_entity.isVoid() || nav_entity.size() == 0) return null;
+	nav_entity = SELECT sec_id AS entity_id, price_date.weekEnd() AS end_date, price_date, cumulative_nav AS nav FROM nav_entity;
 
     // 取相关性计算及BFI用得到的指数/因子列表
     // 只有基金需要单独做相关性计算,目的是为基金推荐做数据准备
@@ -166,54 +173,26 @@ def cal_entity_index_coe(entity_type, entity_info) {
 	else {
 		v_indexes = get_bfi_index_list().factor_id;
 		// Portfolio_id 改回整型
-		v_port_id = nav_entity.sec_id$INT;
-		nav_entity.replaceColumn!('sec_id', v_port_id);
+		v_port_id = nav_entity.entity_id$INT;
+		nav_entity.replaceColumn!('entity_id', v_port_id);
 	}
 
     s_json2 = table(v_indexes AS sec_id, take(start_day.temporalAdd(-5y), v_indexes.size()) AS price_date).toStdJson();
 
     // 取指数及因子周点位
     nav_index = get_nav_for_return_calculation('FA', 'w', s_json2).unionAll(get_nav_for_return_calculation('MI', 'w', s_json2));
+    nav_index = SELECT sec_id AS entity_id, price_date.weekEnd() AS end_date, price_date, cumulative_nav AS nav FROM nav_index;
 
-	if(nav_index.isVoid() || nav_index.size() == 0) return null;
-
-	// 按照SQL 建表
+    	// 按照SQL 建表
     entity_coe = create_entity_index_coe(iif(entity_type == 'PF', true, false));
 
-    // 两次循环遍历所有entity和指数
-    for(entity in entity_info) {
-//entity= entity_info[0]
-        nav1 = SELECT sec_id AS entity_id, weekEnd(price_date) AS end_date, price_date, nav
-               FROM nav_entity WHERE sec_id = entity.entity_id;
+    t_ei = entity_info.join(entity_info.price_date.weekEnd() AS end_date);
+    cal_index_coe(t_ei, nav_entity, nav_index, entity_coe);
 
-    	for(index in v_indexes) {
-//index=v_indexes[0]
-            nav2 = SELECT sec_id AS benchmark_id, weekEnd(price_date) AS end_date, price_date, nav 
-                   FROM nav_index WHERE sec_id = index;
-
-            if(nav2.isVoid() || nav2.size() == 0) continue; // 忽略已经停止更新的指数,或者是特殊的无风险利率 IN0000000M
-
-            closity_1y = cal_monthly_closity(entity, nav1, nav2, 1y);
-            closity_3y = cal_monthly_closity(entity, nav1, nav2, 3y);
-            closity_5y = cal_monthly_closity(entity, nav1, nav2, 5y);
-
-    		INSERT INTO entity_coe
-        	    SELECT c1.entity_id, c1.end_date, index,
-        	    	   c1.corr AS coe_1y, c3.corr AS coe_3y, c5.corr AS coe_5y, 
-        	    	   //c1.corr2 AS coe_1y_2, c3.corr2 AS coe_3y_2, c5.corr2 AS coe_5y_2, 
-        	           c1.info AS info_ratio_1y, c3.info AS info_ratio_3y, c5.info AS info_ratio_5y,
-        	           c1.t_value AS t_value_1y, c3.t_value AS t_value_3y, c5.t_value AS t_value_5y,
-        	           c1.beta AS beta_1y, c3.beta AS beta_3y, c5.beta AS beta_5y
-                FROM closity_1y c1
-                LEFT JOIN closity_3y  c3 ON c1.end_date = c3.end_date
-                LEFT JOIN closity_5y c5 ON c1.end_date = c5.end_date;
-
-    	}
-    }
-
-	return entity_coe;
+    return entity_coe;
 }
 
+
 /*
  *   匹配BFI, 逻辑和 Java BestFitIndexServiceImpl 类似
  * 
@@ -252,8 +231,8 @@ def match_entity_bfi(entity_type, entity_info, entity_coe) {
 	v_strategy_3 = [6, 103]; // 私募固收,公募债券
 
 	// 只需要BFI因子的相关性数据
-	coe = SELECT *
-	      FROM ej(entity_info, ej(entity_coe, get_bfi_index_list(), 'index_id', 'factor_id'), 'entity_id')
+	coe = SELECT ei.strategy, entity_coe.*, l.*
+	      FROM ej(entity_info ei, ej(entity_coe, get_bfi_index_list() AS l, 'index_id', 'factor_id'), 'entity_id')
 		  ORDER BY entity_id, end_date, category_group_id, coe_1y DESC, order_id;
 
 	t_bfi_raw = table(1000:0, 
@@ -275,10 +254,9 @@ def match_entity_bfi(entity_type, entity_info, entity_coe) {
 					   coe_1y.rank(false) AS rank, coe_1y, square(coe_1y) AS r2, 
 					   'w', t_value_1y, beta_1y,
 					   maximum_num, order_id, factor_name
-				FROM entity_info ei
-				INNER JOIN coe ON ei.entity_id = coe.entity_id
-				WHERE ei.strategy IN v_special_strategy[i]
-				  AND coe.index_id IN v_special_rule[i].join(v_factor_cash)
+				FROM coe
+				WHERE strategy IN v_special_strategy[i]
+				  AND index_id IN v_special_rule[i].join(v_factor_cash)
 				  AND t_value_1y >= get_min_threshold('t_value') 
 	 			  AND coe_1y >= get_min_threshold('correlation')
 				  AND order_id IS NOT NULL
@@ -294,10 +272,8 @@ def match_entity_bfi(entity_type, entity_info, entity_coe) {
 				   coe_1y.rank(false) AS rank, coe_1y, square(coe_1y) AS r2, 
 				   'w', t_value_1y, beta_1y,
 				   maximum_num, order_id, factor_name
-			FROM entity_info ei
-			INNER JOIN coe ON ei.entity_id = coe.entity_id
+			FROM coe
 			WHERE t_value_1y >= get_min_threshold('t_value')
-
 			  AND coe_1y >= get_min_threshold('correlation')
 			  AND order_id IS NOT NULL
 			CONTEXT BY entity_id, end_date, category_group_id )
@@ -306,3 +282,70 @@ def match_entity_bfi(entity_type, entity_info, entity_coe) {
 	return SELECT * FROM t_bfi_raw ORDER BY entity_id, end_date, category_group_id;
 
 }
+
+
+/*
+ *   计算基金经理和各资产类别指数及BFI因子的累计净值相关系数 (用月收益计算)
+ *   参考基金/组合的 cal_entity_index_coe 和 match_entity_bfi 的主要代码
+ *   
+ *   @param entity_info <TABLE>: [COLUMNS] entity_id, curve_type, strategy, price_date
+ *   
+ * 
+ *   Example: match_mc_bfi('PL', get_personnel_info_for_perf(['PL000000AN', 'PL00000JOU']).join(take(2024.05.30, 5) AS price_date).rename!('manager_id', 'entity_id'));
+ *   
+ */
+def match_mc_bfi(entity_type, entity_info) {
+
+    if(entity_info.isVoid() || entity_info.size() == 0) return null;
+
+    // 取数据集中最早日期作为因子的起始日期
+    start_day = entity_info.price_date.min();
+
+    // 取数据集每个基金组合指定日期之前5年至今的周净值
+	s_json = (SELECT entity_id, curve_type, strategy, price_date.temporalAdd(-5y).temporalFormat('yyyy-MM') AS end_date FROM entity_info).toStdJson();
+	nav_entity = get_mc_nav_for_return_calculation(entity_type, s_json, 2);
+
+	if(nav_entity.isVoid() || nav_entity.size() == 0) return null;
+	// fund_manager_fitted_curve 和 company_fitted_curve 表里没有price_date, 这里用 businessMonthEnd 日期填充
+	nav_entity = SELECT entity_id, curve_type, strategy, 
+	                    temporalParse(end_date, 'yyyy-MM').month() AS end_date,
+	                    temporalParse(end_date+'-01', 'yyyy-MM-dd').businessMonthEnd() AS price_date, cumulative_nav AS nav 
+	             FROM nav_entity;
+
+    // 取相关性计算及BFI用得到的指数/因子列表
+    // 只有基金需要单独做相关性计算,目的是为基金推荐做数据准备
+	v_indexes = get_bfi_index_list().factor_id;
+
+    s_json2 = table(v_indexes AS sec_id, take(start_day.temporalAdd(-5y), v_indexes.size()) AS price_date).toStdJson();
+
+    // 取指数及因子周点位
+    nav_index = get_nav_for_return_calculation('FA', 'm', s_json2).unionAll(get_nav_for_return_calculation('MI', 'm', s_json2));
+    nav_index = SELECT sec_id AS entity_id, price_date.month() AS end_date, price_date, cumulative_nav AS nav FROM nav_index;
+
+    // 按照SQL 建表
+    t_factor_bfi = create_mc_factor_bfi();
+
+	v_curve_type = [1, 4, 7];
+    for(cur in v_curve_type) {
+
+		t_ei = SELECT entity_id, price_date.month() AS end_date, price_date FROM entity_info WHERE curve_type = cur AND strategy = 0;
+
+		t_ne = SELECT entity_id, end_date, price_date, nav FROM nav_entity WHERE curve_type = cur AND strategy = 0;
+
+		t_coe = create_mc_index_coe().dropColumns!(['curve_type', 'strategy']);
+    	
+	    cal_index_coe(t_ei, t_ne, nav_index, t_coe);
+
+		// 基金经理/公司全策略暂时借用公募混合基金的策略ID 102
+		t_ei.join!(take(102, t_ei.size()) AS strategy);
+		t_bfi_raw = match_entity_bfi(entity_type, t_ei, t_coe);
+
+		if(t_bfi_raw.isVoid() || t_bfi_raw.size() == 0) continue;
+
+		INSERT INTO t_factor_bfi
+			SELECT entity_id, cur, 0 AS strategy, end_date, factor_id, coe_1y AS coe, r2 AS r2, 'm' AS performance_flag, t_value_1y, beta_1y
+			FROM t_bfi_raw;
+    }
+
+    return t_factor_bfi;
+}

+ 91 - 11
modules/task_monthlyPerformance.dos

@@ -138,22 +138,24 @@ def CalRelativeRankingTask(entity_type, entity_ids, end_date, isFromMySQL=true)
 }
 
 /*
- *  计算基金经理和公司净值(月度)
+ *  计算并存储基金经理和公司月度净值
  * 
+ *  @return <TABLE>: [COLUMNS] entity_id, curve_type, strategy, end_date, price_date, ret, nav
  * 
  */
 def cal_and_save_mc_nav(entity_type, entity_date, is_save_local) {
 
-	rt = '';
+    tb_monthly_ret = table(1000:0, ['entity_id', 'curve_type', 'strategy', 'end_date', 'price_date', 'ret', 'nav'],
+    							   [SYMBOL, INT, INT, MONTH, DATE, DOUBLE, DOUBLE]);
 
 	if(entity_type == 'PL') s_entity_type = 'manager';
 	else if(entity_type == 'CO') s_entity_type = 'company';
-	else return rt;
+	else return tb_monthly_ret;
 
-	if(entity_date.isVoid() || entity_date.size() == 0) return rt;
+	if(entity_date.isVoid() || entity_date.size() == 0) return tb_monthly_ret;
 
     // 准备类似MySQL结构的数据表
-    tb_entity_nav = create_entity_fitted_curve();
+    tb_entity_nav = create_mc_fitted_curve();
 
     // 暂时与 MySQL 保持一致,只计算公募,私募,公私募综合三条时间序列。未来可细化至公、私募+主策略
     d_curve_type = dict(INT, INT);
@@ -176,18 +178,22 @@ def cal_and_save_mc_nav(entity_type, entity_date, is_save_local) {
 
 		s_json = tb_entity.toStdJson();
 		t_ret = get_mc_monthly_return(s_entity_type, s_json, 0, 1, true);
-
         
         for(cur in d_curve_type.keys()) {
 
 			tmp = SELECT entity_id, cur AS curve_type, 0 AS strategy, end_date, price_date, ret, incl_cal_cnt 
 			      FROM t_ret WHERE raise_type = d_curve_type[cur] AND strategy = -99; // 目前只需要全策略
-			// 取净值前值
+
+			// 计算月收益
         	tb_nav = cal_mc_nav_by_return(entity_type, tmp);
 
 	        INSERT INTO tb_entity_nav 
-	        	SELECT entity_id, curve_type, strategy, end_date, nav, incl_cal_cnt 
+	        	SELECT entity_id, curve_type, strategy, end_date, nav, incl_cal_cnt
 	            FROM ej(tb_nav, tmp, ['entity_id', 'curve_type', 'strategy', 'end_date']);
+
+			INSERT INTO tb_monthly_ret
+				SELECT entity_id, curve_type, strategy, end_date.temporalParse('yyyy-MM').month(), price_date, ret, nav
+				FROM ej(tmp, tb_nav, ['entity_id', 'curve_type', 'strategy', 'end_date']);
         }
 
         i += batch_size;
@@ -211,11 +217,27 @@ def cal_and_save_mc_nav(entity_type, entity_date, is_save_local) {
         } catch(ex) {
 
             //TODO: Log errors
-            rt = ex;
         }
     }
 
-    return rt;
+    return tb_monthly_ret;
+	
+}
+
+/*
+ *   计算并存储基金经理/公司的月度收益及指标
+ * 
+ * 
+ */
+def cal_and_save_mc_indicator(entity_type, entity_date, monthly_returns, is_save_local) {
+
+    rt = '';
+
+	if(!(entity_type IN ['PL', 'CO'])) return rt;
+	if(entity_date.isVoid() || entity_date.size() == 0) return rt;
+
+	indicators = cal_mc_monthly_indicators(entity_type, 'PBI', monthly_returns);
+
 	
 }
 
@@ -234,9 +256,12 @@ def CalManagerNavTask(updatetime) {
 
 	entity_date.rename!('manager_id', 'entity_id');
 
-	cal_and_save_mc_nav('PL', entity_date, is_save_local);
+	tb_monthly_ret = cal_and_save_mc_nav('PL', entity_date, is_save_local);
+
+	cal_and_save_mc_indicator('PL', entity_date, tb_monthly_ret, is_save_local);
 
 	entity_date = null;
+	tb_monthly_ret = null;
 }
 
 
@@ -260,3 +285,58 @@ def CalCompanyNavTask(updatetime) {
 	entity_date = null;
 }
 
+/*
+ * 
+ *  [定时任务]: 基金经理的BFI MATCHING
+ * 
+ * 
+ */
+def MatchManagerBFITask(updatetime) {
+
+	rt = '';
+
+	is_save_local = iif(updatetime <= get_ini_data_const()['updatetime'], true, false);
+
+	entity_type = 'PL';
+
+	// 31 sec
+	entity_date = get_mc_performance_by_updatetime(entity_type, updatetime);
+
+	if(entity_date.isVoid() || entity_date.size() == 0) return rt;
+
+	i = 0;
+	batch_size = 1000;
+	max_cnt = entity_date.size();
+
+	do {
+
+		t_entity_date = entity_date[i : min(max_cnt, i + batch_size)];
+		// 22 min per 1000 records, way too slow
+		t_bfi = match_mc_bfi(entity_type, t_entity_date);
+
+		t_max_r2 = SELECT entity_id , curve_type, strategy, factor_id.first() AS factor_id, end_date,
+			              string(NULL) AS performance_flag, coe.first() AS coe, r2.first() AS r2, concat(factor_name, ",") AS rz_portrait
+		           FROM ej(t_bfi, get_bfi_index_list(), 'factor_id')
+		           GROUP BY entity_id, curve_type, strategy, end_date;
+		try {
+			// 高度怀疑 pf_manager_factor_bfi 表只是中间表,没有用,这里就不存了
+
+			// 有效 factors 存到 xxx_factor_bfi_by_category_group 表
+			chg_columns_for_mysql(t_bfi, 'manager_id');
+			save_and_sync(t_bfi, 'raw_db.pf_manager_factor_bfi_by_category_group', );
+
+			// 有效因子中 R2 最大的因子存 xxx_max_r2 
+			chg_columns_for_mysql(t_max_r2, 'manager_id');
+			save_and_sync(t_max_r2, 'raw_db.pf_manager_factor_bfi_max_r2', );
+			
+		} catch (ex) {
+            //TODO: Log errors
+            rt += ex + '\n';
+		}
+
+		i += batch_size
+
+	} while (i < max_cnt);
+
+	return rt;
+}

+ 10 - 5
modules/task_weeklyPerformnce.dos

@@ -20,10 +20,11 @@ use fundit::dataSaver;
  * 
  *   Example: CalEntityRBSATask('MF', ['MF00003PW1'], 2024.10.14T10:00:00);
  *            CalEntityRBSATask('MF', NULL, 2024.11.25T10:00:00);
+ *            CalEntityRBSATask('PF', NULL, 2024.11.25T10:00:00);
  */
 def CalEntityRBSATask(entityType, entityIds, updateTime) {
-// entityType = 'MF'
-//entityIds = ['MF00003PW1']
+// entityType = 'PF'
+//entityIds = NULL
 //updateTime = 2024.10.14T10:00:00
 
 	t_cal = get_entity_list_by_latest_return_updatetime(entityType, entityIds, updateTime, true);
@@ -56,6 +57,10 @@ def CalEntityRBSATask(entityType, entityIds, updateTime) {
 		// 起始日期是最早更新的净值日期再向前推一个时间窗口
 		s_json = (SELECT entity_id, price_date.temporalAdd(-window, 'w') AS price_date FROM t).toStdJson();
 		t_entity_ret = get_entity_ret_by_date(entityType, s_json, 'w', true);
+		if(entityType == 'PF') {
+			v_entity_id = t_entity_ret.entity_id$INT;
+			t_entity_ret.replaceColumn!('entity_id', v_entity_id);
+		}
 
 		for(entity in t) {
 
@@ -103,7 +108,7 @@ def CalEntityRBSATask(entityType, entityIds, updateTime) {
  *   Example: MatchEntityBFITask('MF', 2024.11.20);
  */
 def MatchEntityBFITask(entityType, date) {
-//entityType = 'PF'
+//entityType = 'MF'
 //date = 2024.11.20
 
 	rt = '';
@@ -169,7 +174,7 @@ def MatchEntityBFITask(entityType, date) {
 
 			// 有效 factors 存到 xxx_factor_bfi_by_category_group 表
 			chg_columns_for_mysql(t_bfi, iif(entityType == 'PF', 'portfolio_id', 'fund_id'));
-			save_and_sync(t_bfi_candidates, iif(entityType == 'PF', 'raw_db.pf_portfolio_factor_bfi_by_category_group', 'raw_db.pf_fund_factor_bfi_by_category_group'), );
+			save_and_sync(t_bfi, iif(entityType == 'PF', 'raw_db.pf_portfolio_factor_bfi_by_category_group', 'raw_db.pf_fund_factor_bfi_by_category_group'), );
 
 			// 有效因子中 R2 最大的因子存 xxx_max_r2 
 			chg_columns_for_mysql(t_max_r2, iif(entityType == 'PF', 'portfolio_id', 'fund_id'));
@@ -278,7 +283,7 @@ def calEntityBfiIndicatorTask(entityType, date) {
             save_and_sync(tb_bfi_indicator, t_desc.table_name[0].strReplace(db_name, 'raw_db'), t_desc.table_name[0].strReplace(db_name, 'raw_db'));
 
             // 数据初始化时将指标存入本地,做排名之用
-            if(end_day <= get_ini_data_const['date']) {
+            if(end_day <= get_ini_data_const()['date']) {
             	save_table(tb_bfi_indicator, t_desc.table_name[0], false);
             }