task_weeklyPerformance.dos 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. module fundit::task_weeklyPerformance
  2. use fundit::sqlUtilities;
  3. use fundit::operationDataPuller;
  4. use fundit::performanceDataPuller;
  5. use fundit::indicatorCalculator;
  6. use fundit::rbsaCalculator;
  7. use fundit::navCalculator;
  8. use fundit::bfiMatcher;
  9. use fundit::dataSaver;
  10. /*
  11. * 根据收益更新日期计算 RBSA (1h for funds; for portfolios)
  12. *
  13. * @param entityType <STRING>: MF, HF, PF (MF和HF等效)
  14. *
  15. * TODO: 是否要 monthly 运行,并写入 history 表?
  16. * portfolio 未测试
  17. *
  18. * Example: CalEntityRBSATask('MF', ['MF00003PW1'], 2024.10.14T10:00:00);
  19. * CalEntityRBSATask('MF', NULL, 2025.01.10);
  20. * CalEntityRBSATask('PF', NULL, 2025.01.10);
  21. */
  22. def CalEntityRBSATask(entityType, entityIds, updateTime) {
  23. // entityType = 'PF'
  24. //entityIds = NULL
  25. //updateTime = 2025.01.10T10:00:00
  26. t_cal = get_entity_list_by_latest_return_updatetime(entityType, entityIds, updateTime, true);
  27. window = 48;
  28. step = 13;
  29. if(t_cal.isVoid() || t_cal.size() == 0) return;
  30. d_rbsa = get_rbsa_index();
  31. // 拿到所有指数ID
  32. v_index = d_rbsa.values().flatten().distinct();
  33. // 因为用来做基准指数的可能是指数、因子、基金等等任何时间序列数据,所以不用填 entity_type
  34. t_index_ret = get_entity_return(NULL, v_index, 'w', t_cal.price_date.min().temporalAdd(-window, 'w'), today(), true);
  35. i = 0;
  36. batch_size = 200; // 1 hour total
  37. total_cnt = t_cal.size();
  38. do {
  39. tb_result = table(1000:0,
  40. ["entity_id", "asset_type_id", "index_id", "effective_date", "level", "alternative_id", "weighting"],
  41. [iif(entityType=='PF', INT, STRING), STRING, STRING, STRING, INT, STRING, DOUBLE]);
  42. t = t_cal[i: min(total_cnt, i+batch_size)];
  43. // 起始日期是最早更新的净值日期再向前推一个时间窗口+10(防止数据缺失的冗余)
  44. s_json = (SELECT entity_id, price_date.temporalAdd(-window-10, 'w') AS price_date FROM t).toStdJson();
  45. t_entity_ret = get_entity_ret_by_date(entityType, s_json, 'w', true);
  46. if(entityType == 'PF') {
  47. v_entity_id = t_entity_ret.entity_id$INT;
  48. t_entity_ret.replaceColumn!('entity_id', v_entity_id);
  49. }
  50. for(entity in t) {
  51. entity_ret = SELECT * FROM t_entity_ret WHERE entity_id = entity.entity_id;
  52. for(asset_type in d_rbsa.keys()) {
  53. //asset_type=d_rbsa.keys()[0]
  54. index_ret = SELECT entity_id, price_date, ret FROM t_index_ret WHERE entity_id IN d_rbsa[asset_type] AND price_date IS NOT NULL;
  55. // 起始日期是最早更新日期再向前推一个时间窗口
  56. res = cal_entity_RBSA(entityType, entity_ret, index_ret, 'w',
  57. entity.price_date.temporalAdd(-window-10, 'w')[0], today(), true, window, step);
  58. if(res.isVoid() || res.size() == 0) continue;
  59. // 每日任务只负责更新最新的rbsa结果
  60. latest_date = (EXEC price_date.max() AS price_date FROM res)[0];
  61. tb_result.tableInsert(SELECT entity_id, asset_type, index_id, price_date, level, alternative_id, weights
  62. FROM res WHERE price_date = latest_date);
  63. }
  64. }
  65. if(entityType IN ['MF', 'HF'])
  66. save_and_sync(tb_result, 'raw_db.pf_fund_rbsa_breakdown', 'raw_db.pf_fund_rbsa_breakdown');
  67. else
  68. save_and_sync(tb_result, 'raw_db.pf_portfolio_rbsa_breakdown', 'raw_db.pf_portfolio_rbsa_breakdown');
  69. i += batch_size;
  70. } while (i < total_cnt);
  71. }
  72. /*
  73. * [定时任务] 计算基金和组合的BFI
  74. *
  75. * @param entityType <STRING>: MF, HF, PF
  76. *
  77. *
  78. * TODO: max_r2 表在哪里被用到了?应该和基金推荐有关系
  79. *
  80. * Example: MatchEntityBFITask('PF', 2025.01.01);
  81. */
  82. def MatchEntityBFITask(entityType, date) {
  83. //entityType = 'MF'
  84. //date = 2024.12.01
  85. rt = '';
  86. if(find(['HF', 'MF', 'PF'], entityType) < 0) return null;
  87. // 取有最新周收益变动的基金列表 (1s)
  88. tb_cal_entity = get_entity_list_by_weekly_return_updatetime(entityType, NULL, date, true);
  89. if(tb_cal_entity.isVoid() || tb_cal_entity.size() == 0 ) return;
  90. i = 0;
  91. size = tb_cal_entity.size();
  92. batch_size = 100;
  93. do {
  94. t_tmp_entity = tb_cal_entity[i : min(size, i+batch_size)];
  95. i = i + batch_size;
  96. // 4 min per 1000 funds or 2 min per 1000 portfolios
  97. coe = cal_entity_index_coe(entityType, t_tmp_entity);
  98. if(coe.isVoid() || coe.size() == 0) continue;
  99. entity_info = get_entity_info(entityType, t_tmp_entity.entity_id);
  100. bfi_raw = match_entity_bfi(entityType, entity_info, coe);
  101. // 先存到数据库,落袋为安
  102. try {
  103. // 筛掉 correlation 绝对值不够阈值的记录
  104. t_coe = SELECT entity_id, end_date, index_id,
  105. iif(coe_1y.abs() < get_min_threshold('correlation'), double(NULL), coe_1y) AS coe_1y,
  106. iif(coe_3y.abs() < get_min_threshold('correlation'), double(NULL), coe_3y) AS coe_3y,
  107. iif(coe_5y.abs() < get_min_threshold('correlation'), double(NULL), coe_5y) AS coe_5y,
  108. t_value_1y, t_value_3y, t_value_5y, beta_1y, beta_3y, beta_5y
  109. FROM coe;
  110. DELETE FROM t_coe WHERE coe_1y IS NULL AND coe_3y IS NULL AND coe_5y IS NULL;
  111. // 候选因子
  112. t_bfi_candidates = SELECT entity_id, end_date, index_id AS factor_id, coe_1y AS coe, coe_1y.square() AS r2, 'w' AS performance_flag, t_value_1y, beta_1y
  113. FROM t_coe WHERE index_id LIKE 'FA%';
  114. chg_columns_for_mysql(t_coe, iif(entityType == 'PF', 'portfolio_id', 'fund_id'));
  115. // 只有基金需要存 index_coe 表
  116. if(entityType IN ['MF', 'HF']) save_and_sync(t_coe, 'raw_db.pf_fund_index_coe', );
  117. // 所有的 factors 存到 xxx_factor_bfi 表;NOTE: Java 把所有 factor 的数据都存起来,这里只存 correlation 达标的记录 (反正这个表没啥用?)
  118. chg_columns_for_mysql(t_bfi_candidates, iif(entityType == 'PF', 'portfolio_id', 'fund_id'));
  119. save_and_sync(t_bfi_candidates, iif(entityType == 'PF', 'raw_db.pf_portfolio_factor_bfi', 'raw_db.cm_fund_factor_bfi'), );
  120. if(bfi_raw.isVoid() || bfi_raw.size() == 0) continue;
  121. // 有效因子
  122. t_bfi = SELECT entity_id, end_date, factor_id, coe_1y AS coe, r2, performance_flag, t_value_1y, beta_1y
  123. FROM bfi_raw ORDER BY entity_id, end_date, r2 DESC;
  124. // 最大R2因子及所有有效因子标签
  125. t_max_r2 = SELECT entity_id, factor_id.first() AS factor_id, end_date,
  126. string(NULL) AS performance_flag, coe.first() AS coe, r2.first() AS r2, concat(factor_name, ",") AS rz_portrait
  127. FROM ej(t_bfi, get_bfi_index_list(), 'factor_id')
  128. GROUP BY entity_id, end_date;
  129. // 有效 factors 存到 xxx_factor_bfi_by_category_group 表
  130. chg_columns_for_mysql(t_bfi, iif(entityType == 'PF', 'portfolio_id', 'fund_id'));
  131. 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'), );
  132. // 有效因子中 R2 最大的因子存 xxx_max_r2
  133. chg_columns_for_mysql(t_max_r2, iif(entityType == 'PF', 'portfolio_id', 'fund_id'));
  134. save_and_sync(t_max_r2, iif(entityType == 'PF', 'raw_db.pf_portfolio_factor_bfi_max_r2', 'raw_db.pf_fund_factor_bfi_by_category_group_max_r2'), );
  135. } catch (ex) {
  136. //TODO: Log errors
  137. rt += ex;
  138. }
  139. } while (i<size)
  140. return rt;
  141. }
  142. /*
  143. * [定时任务] 计算BFI指标并存入数据库
  144. *
  145. * @param entityType <STRING>: 'MF', 'HF', 'PF' (MF和HF等效)
  146. * @param date <DATETIME>: BFI更新时间, 为空时缺省为当前时间的前1天;为1989.01.01或更早日期时代表初始化,指标会被存入本地数据库
  147. *
  148. *
  149. * Example: calEntityBfiIndicatorTask('MF', 2024.10.28);
  150. * calEntityBfiIndicatorTask('PF', 2024.10.28);
  151. */
  152. def calEntityBfiIndicatorTask(entityType, date) {
  153. // entityType = 'MF'
  154. // date = 2024.10.01
  155. rt = '';
  156. if(!(entityType IN ['MF', 'HF', 'PF'])) return null;
  157. very_old_day = 1900.01.01;
  158. if(date.isNothing() || date.isNull())
  159. end_day = temporalAdd(now(), -1d);
  160. else
  161. end_day = date;
  162. // 1989.01.01及以前的日期被认为从本地读数据
  163. isFromMySQL = iif(end_day <= 1989.01.01, false, true);
  164. // 取有最新bfi变动的基金列表 (1s)
  165. tb_cal_entities = get_entity_bfi_factors(entityType, NULL, very_old_day.month(), today().month(), end_day);
  166. if(tb_cal_entities.isVoid() || tb_cal_entities.size() == 0 ) return;
  167. v_uniq_entity_id = EXEC DISTINCT entity_id FROM tb_cal_entities;
  168. // 按照 MySQL 建好各表
  169. tb_bfi_indicator = create_entity_bfi_indicator(iif(entityType=='PF', true, false));
  170. // 分批跑
  171. i = 0;
  172. batch_size = 100;
  173. do {
  174. entities = SELECT * FROM tb_cal_entities WHERE entity_id IN v_uniq_entity_id[i : min(v_uniq_entity_id.size(), i+batch_size)];
  175. if(entities.isVoid() || entities.size() == 0) break;
  176. // 200ms
  177. entity_info = SELECT entity_id, end_date.temporalParse('yyyy-MM') AS end_date, inception_date, factor_id AS benchmark_id, ini_value
  178. FROM ej(entities, get_entity_info(entityType, entities.entity_id), 'entity_id');
  179. // 取月收益 (12s)
  180. rets = get_monthly_ret(entityType, entity_info.entity_id, very_old_day, entity_info.end_date.max().temporalFormat('yyyy-MM-dd').temporalParse('yyyy-MM-dd').monthEnd(), isFromMySQL);
  181. // 把 yyyy-MM 格式的 end_date 改成 dolphin 的 MONTH
  182. v_end_date = rets.end_date.temporalParse('yyyy-MM');
  183. rets.replaceColumn!('end_date', v_end_date);
  184. if(!rets.isVoid() && rets.size() > 0) {
  185. // 计算月度指标 (5s)
  186. indicators = cal_monthly_indicators(entityType, 'BFI', rets);
  187. // 仿照MySQL的表结构准备好记录 (1s)
  188. generate_entity_bfi_indicator(entity_info, indicators, true, tb_bfi_indicator);
  189. }
  190. i += batch_size;
  191. } while (i <= v_uniq_entity_id.size());
  192. if(! tb_bfi_indicator.isVoid() && tb_bfi_indicator.size() > 0) {
  193. // save data to MySQL
  194. try {
  195. t_desc = get_bfi_indicator_table_description(entityType);
  196. chg_columns_for_mysql(tb_bfi_indicator, t_desc.sec_id_col[0]);
  197. db_name = t_desc.table_name[0].split('.')[0];
  198. 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'));
  199. // 数据初始化时将指标存入本地,做排名之用
  200. if(end_day <= get_ini_data_const()['date'])
  201. save_table(tb_bfi_indicator, t_desc.table_name[0], false);
  202. } catch(ex) {
  203. //TODO: Log errors
  204. rt = ex;
  205. }
  206. }
  207. return rt;
  208. }
  209. /*
  210. * 计算并存储基金经理和公司月度净值
  211. *
  212. * @return <TABLE>: [COLUMNS] entity_id, curve_type, strategy, end_date, price_date, ret, nav
  213. *
  214. * NOTE: 基本上和 cal_and_save_mc_monthly_nav 一样
  215. *
  216. */
  217. def cal_and_save_mc_weekly_nav(entity_type, entity_date, is_save_local) {
  218. rt = '';
  219. if( !(entity_type in ['PL', 'CO']) ) return rt;
  220. if(entity_date.isVoid() || entity_date.size() == 0) return rt;
  221. // 准备类似MySQL结构的数据表
  222. tb_entity_nav = create_mc_nav();
  223. // 暂时与 MySQL 保持一致,只计算公募,私募,公私募综合三条时间序列。未来可细化至公、私募+主策略
  224. d_curve_type = dict(INT, INT);
  225. d_curve_type[1] = 1; // 私募
  226. d_curve_type[4] = 2; // 公募
  227. d_curve_type[7] = -99; // 公私募综合
  228. // 分批跑
  229. i = 0;
  230. batch_size = 1000;
  231. all_entity_id = entity_date.entity_id.distinct();
  232. do { // 14 sec
  233. tb_entity = SELECT entity_id, effective_date FROM entity_date
  234. WHERE entity_id IN all_entity_id[i : min(all_entity_id.size(), i+batch_size)];
  235. if(tb_entity.isVoid() || tb_entity.size() == 0) break;
  236. s_json = tb_entity.toStdJson();
  237. t_ret = get_mc_average_return(entity_type, 'w', s_json, 0, 1, true);
  238. for(cur in d_curve_type.keys()) {
  239. //cur=7
  240. tmp = SELECT entity_id, cur AS curve_type, 0 AS strategy, effective_date, price_date, ret, incl_cal_cnt
  241. FROM t_ret WHERE raise_type = d_curve_type[cur] AND strategy = -99; // 目前只需要全策略
  242. // 计算周收益
  243. tb_nav = cal_mc_nav_by_return(entity_type, tmp, 'w');
  244. INSERT INTO tb_entity_nav
  245. SELECT entity_id, curve_type, strategy, effective_date AS year_week, price_date, nav, ret, incl_cal_cnt
  246. FROM ej(tb_nav, tmp, ['entity_id', 'curve_type', 'strategy', 'effective_date']);
  247. }
  248. i += batch_size;
  249. } while (i <= all_entity_id.size());
  250. if(! tb_entity_nav.isVoid() && tb_entity_nav.size() > 0) {
  251. // save data to MySQL (12 sec)
  252. try {
  253. tb_entity_nav.rename!('entity_id', iif(entity_type == 'PL', 'manager_id', 'company_id'));
  254. save_and_sync(tb_entity_nav, iif(entity_type == 'PL', 'raw_db.manager_nav', 'raw_db.company_nav'), );
  255. // 数据初始化时将指标存入本地
  256. if(is_save_local == true) {
  257. save_table(tb_entity_nav, iif(entity_type == 'PL', 'mfdb.manager_nav', 'mfdb.company_nav'), false);
  258. }
  259. } catch(ex) {
  260. //TODO: Log errors
  261. rt += ex;
  262. }
  263. }
  264. return rt;
  265. }
  266. /*
  267. * [定时任务]: 基金经理/公司周净值计算
  268. *
  269. * Example: CalMCWeeklyNavTask('CO', 2024.11.04);
  270. */
  271. def CalMCWeeklyNavTask(entity_type, updatetime) {
  272. //updatetime = 2024.11.05;
  273. //entity_type = 'PL';
  274. if(!(entity_type IN ['PL', 'CO'])) return;
  275. is_save_local = iif(updatetime <= get_ini_data_const()['updatetime'], true, false);
  276. // 60 sec 简化起见,不区分curve_type, strategy; TODO: 性能能否优化?
  277. if(entity_type == 'PL') {
  278. entity_date = get_manager_list_by_fund_updatetime(updatetime, 'w');
  279. entity_date.rename!('manager_id', 'entity_id');
  280. }
  281. else {
  282. entity_date = get_company_list_by_fund_updatetime(updatetime, 'w');
  283. entity_date.rename!('company_id', 'entity_id');
  284. }
  285. // 15 sec
  286. cal_and_save_mc_weekly_nav(entity_type, entity_date, is_save_local);
  287. entity_date = null;
  288. }