rbsaCalculator.dos 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. module fundit::rbsaCalculator
  2. use fundit::sqlUtilities;
  3. use fundit::performanceDataPuller;
  4. use fundit::operationDataPuller;
  5. /*
  6. * RBSA 计算
  7. * @param: ret <TABLE>: historical return (double) vector which contains the same number of return as index
  8. * index_ret <TABLE>: historical index return table which each row is an index
  9. * is_long <BOOL>: true - long-only, false - long-short
  10. * @return: table
  11. *
  12. * Create 20240703 模仿python代码在Dolphin中实现,具体计算逻辑完全不懂 Joey
  13. * 原代码见: http://gogs.fundit.cn/FundIt/FinanceCalcPython/src/dev36/pf_scical/v1/calc_rbsa_use_osqp.py
  14. * Python官方示例见:https://osqp.org/docs/examples/least-squares.html
  15. * Dolphin官方示例见:https://docs.dolphindb.cn/zh/funcs/o/osqp.html
  16. *
  17. */
  18. defg cal_rbsa(ret, index_ret, is_long) {
  19. // 窗口长度
  20. m = ret.size()
  21. // 指数个数
  22. n = index_ret.cols()
  23. P0 = matrix(float, n, m+n)
  24. P1 = concatMatrix([matrix(float, m, n), eye(m)])
  25. P = concatMatrix([P0, P1], false)
  26. q = array(float, m+n, (m+n)*10, 0)
  27. A0 = concatMatrix( [matrix(index_ret), -eye(m)])
  28. A1 = concatMatrix( [matrix(take(1, n)).transpose(), matrix(float, 1, m)])
  29. A2 = concatMatrix( [eye(n), matrix(float, n, m)])
  30. A = concatMatrix( [A0, A1, A2], false)
  31. // join 1 是为了限制所有权重加总为100%
  32. // 下限
  33. lb =(ret join 1) join array(float, n, n*10, iif(is_long == true, 0, -2))
  34. // 上限
  35. ub=(ret join 1) join array(float, n, n*10, iif(is_long == true, 1, 2))
  36. res = osqp( q, P, A, lb, ub)
  37. return res
  38. }
  39. /*
  40. * 滚动 rbsa
  41. * @param ret <TABLE>: return table, at least with "effective_date" and "ret" as columns
  42. * @param index_ret <TABLE>: index return table, with "effective_date" and all index ids as columns
  43. * @param is_long <BOOL>: boolean. true means weightings could be negative values
  44. * @param window <INT>: number of return in a window
  45. * @param step <INT>: rolling step
  46. *
  47. * TODO: use rolling()
  48. *
  49. * @return <TABLE> with "effective_date", "index_id" and "weights" columns
  50. */
  51. def cal_rolling_rbsa(ret, index_ret, is_long, window, step) {
  52. // 找到所有指数全有数据的最早日期
  53. v_start_date = EXEC effective_date.max() AS start_date
  54. FROM (SELECT entity_id, effective_date.min() AS effective_date FROM index_ret WHERE ret IS NOT NULL GROUP BY entity_id);
  55. m_index_ret = SELECT ret FROM index_ret WHERE effective_date >= v_start_date PIVOT BY effective_date, entity_id;
  56. t = SELECT * FROM ej(ret, m_index_ret, 'effective_date') ORDER BY ret.effective_date;
  57. t.nullFill!(0)
  58. // not sure why this doesn't work
  59. // rolling(cal_rbsa{,,is_long}, (t.ret, t.slice(, ret.cols():).matrix()), window, step)
  60. // 指数个数
  61. n = m_index_ret.cols() - 1
  62. // 计算起始位置
  63. i = (t.size() - window) % step
  64. // 运行rbsa计算次数
  65. cnt = (t.size() - i - window) / step + 1;
  66. tb = table(max(cnt,1):0, ["effective_date", "price_date", "index_id", "weights", "alpha", "r2", "adj_r2"], [STRING, DATE, STRING, DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
  67. if(t.size() >= max(window, step) && cnt > 0) {
  68. do {
  69. alpha = 0;
  70. r2 = 0;
  71. adj_r2 = 0;
  72. v_ret = t.ret[i:(i+window)];
  73. t_index_ret = t.slice( i:(i+window), ret.cols(): );
  74. // 传入window个收益
  75. res = cal_rbsa(v_ret, t_index_ret, is_long);
  76. if(res[0] == 'solved') {
  77. m_predict_ret = t_index_ret.matrix() ** res[1][0:n];
  78. alpha = v_ret.mean() - m_predict_ret.mean();
  79. SSR = sum2(m_predict_ret - v_ret.mean());
  80. SST = sum2(v_ret - v_ret.mean());
  81. if(SST == 0) {
  82. // 当SST=0, 先计算SSE再计算SST
  83. SSE = sum2(v_ret - m_predict_ret);
  84. SST = SSE + SSR;
  85. }
  86. if(SST != 0) {
  87. r2 = SSR/SST;
  88. adj_r2 = 1 - (1 - r2) * (window - 1) / (window - n - 1);
  89. }
  90. for(j in 1..n) {
  91. tb.tableInsert(t.effective_date[i+window-1], t.price_date[i+window-1], m_index_ret.colNames()[j], res[1][j-1].round(4), alpha, r2, adj_r2);
  92. }
  93. }
  94. // 往前推进step个收益
  95. i = i + step
  96. cnt -= 1
  97. } while( cnt > 0)
  98. }
  99. return tb
  100. }
  101. /*
  102. * 计算单基金或组合的RBSA
  103. *
  104. * @param entity_type <STRING>: 目标基金/组合的类型
  105. * @param entity_id <STRING>: 目标基金/组合的ID
  106. * @param index_ids <VECTOR>: 基准指数IDs
  107. * @param freq <STRING>: m, w, d
  108. * @param start_day <DATE>
  109. * @param end_day <DATE>
  110. * @param is_long <BOOL>: 是否只考虑纯多头
  111. * @param window <INT>: 窗口(必须多于基准指数个数)
  112. * @param step <INT>: 步长
  113. *
  114. * @return <TABLE>: entity_id, effective_date, price_date, index_id, weights, alternative_id, level, alpha, r2, adj_r2
  115. *
  116. * TODO: 数字与界面和数据库都对不上
  117. *
  118. * Example: cal_single_entity_RBSA('MF', 'MF00003PW1', ['IN00000008', 'IN00000077', 'IN0000007G', 'IN0000009M'], 'w', 1900.01.01, 2024.11.15, true, 24, 24);
  119. * cal_single_entity_RBSA('PF', 166002, ['FA00000VML', 'FA00000VMM', 'FA00000VMN', 'FA00000VMO', 'IN0000007G'], 'w', 2020.01.01, 2024.11.08, true, 24, 24);
  120. * cal_single_entity_RBSA('MF', 'MF000200KQ', ['IN00000008', 'IN00000077', 'IN0000007G', 'IN0000009M'], 'w', 1900.01.01, 2024.11.16, true, 24, 24);
  121. */
  122. def cal_single_entity_RBSA(entity_type, entity_id, index_ids, freq='w', start_day=1900.01.01, end_day=2099.12.31, is_long=true, window=24, step=24) {
  123. // entity_type='MF'
  124. // entity_id= 'MF00003PW1'
  125. // index_ids=['IN00000008', 'IN00000077', 'IN0000007G', 'IN0000009M']
  126. // freq='w'
  127. // start_day=2001.01.19
  128. // end_day=2024.11.16
  129. // is_long=true
  130. // window=48
  131. // step=13
  132. tb_result = table(100:0, ["entity_id", "effective_date", "index_id", "weights", "alpha", "r2", "adj_r2"],
  133. [iif(entity_type=='PF', INT, STRING), STRING, STRING, DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
  134. v_entity = array(iif(entity_type=='PF', INT, STRING));
  135. v_entity.append!(entity_id);
  136. entity_ret = get_entity_return(entity_type, v_entity, freq, start_day, end_day, true);
  137. // 数据长度不够,按照顺序依次分别用母基金(4), 指数(3)的数据来代替 (level=2, 基金经理的数据在Java里好像没用?)
  138. level = 1
  139. alternative_id = NULL;
  140. if(entity_ret.isVoid() || entity_ret.size() < window) {
  141. if(entity_type IN ['MF', 'HF']) {
  142. fund_info = get_fund_info(v_entity);
  143. p_fund_id = fund_info.p_fund_id[0];
  144. primary_benchmark_id = fund_info.benchmark_id[0];
  145. if(p_fund_id != NULL) {
  146. entity_ret = get_entity_return(entity_type, v_entity.replace(entity_id, p_fund_id) , freq, start_day, end_day, true);
  147. alternative_id = p_fund_id;
  148. level = 4;
  149. } else if(primary_benchmark_id != NULL) {
  150. entity_ret = get_entity_return(entity_type, v_entity.replace(entity_id, primary_benchmark_id) , freq, start_day, end_day, true);
  151. alternative_id = primary_benchmark_id;
  152. level = 3;
  153. } else {
  154. return tb_result;
  155. }
  156. } else if(entity_type == 'PF'){
  157. portfolio_info = get_portfolio_info(v_entity);
  158. primary_benchmark_id = portfolio_info.benchmark_id[0];
  159. if(primary_benchmark_id != NULL) {
  160. entity_ret = get_entity_return(entity_type, v_entity.replace(entity_id, primary_benchmark_id) , freq, start_day, end_day, true);
  161. alternative_id = primary_benchmark_id;
  162. level = 3;
  163. } else
  164. return tb_result;
  165. }
  166. }
  167. // 因为用来做基准指数的可能是指数、因子、基金等等任何时间序列数据,所以不用填 entity_type
  168. index_ret = get_entity_return(NULL, index_ids, freq, start_day, end_day, true);
  169. if(index_ret.isVoid() || index_ret.size() == 0) return tb_result;
  170. tb_result = SELECT entity_id, effective_date, price_date, index_id, weights, alternative_id, level, alpha, r2, adj_r2
  171. FROM cal_rolling_rbsa(entity_ret, index_ret, is_long, window, step);
  172. return tb_result;
  173. }
  174. /*
  175. * 计算单基金或组合的RBSA
  176. *
  177. * @param entity_type <STRING>: MF, HF, PF
  178. * @param entity_ret <TABLE>: [COLUMNS] entity_id, price_date, ret
  179. * @param index_ret <TABLE>: [COLUMNS] entity_id, price_date, ret
  180. * @param freq <STRING>: m, w, d
  181. * @param start_day <DATE>
  182. * @param end_day <DATE>
  183. * @param is_long <BOOL>: 是否只考虑纯多头
  184. * @param window <INT>: 窗口(必须多于基准指数个数)
  185. * @param step <INT>: 步长
  186. *
  187. * @return <TABLE>: entity_id, effective_date, price_date, index_id, weights, alternative_id, level, alpha, r2, adj_r2
  188. *
  189. * TODO: 数字与界面和数据库都对不上
  190. *
  191. */
  192. def cal_entity_RBSA(entity_type, entity_ret, index_ret, freq='w', start_day=1900.01.01, end_day=2099.12.31, is_long=true, window=24, step=24) {
  193. // entity_type='MF'
  194. // freq='w'
  195. // start_day=2024.01.05
  196. // end_day=today()
  197. // is_long=true
  198. // window=48
  199. // step=13
  200. tb_result = table(100:0, ["entity_id", "effective_date", "index_id", "weights", "alpha", "r2", "adj_r2"],
  201. [iif(entity_type=='PF', INT, STRING), STRING, STRING, DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
  202. if(index_ret.isVoid() || index_ret.size() == 0) return tb_result;
  203. tb_entity_ret = entity_ret;
  204. // 数据长度不够,按照顺序依次分别用母基金(4), 指数(3)的数据来代替 (level=2, 基金经理的数据在Java里好像没用?)
  205. level = 1
  206. alternative_id = NULL;
  207. if(tb_entity_ret.isVoid() || tb_entity_ret.size() < window) {
  208. if(entity_type IN ['MF', 'HF']) {
  209. fund_info = get_fund_info(tb_entity_ret.entity_id);
  210. p_fund_id = fund_info.p_fund_id;
  211. primary_benchmark_id = fund_info.benchmark_id;
  212. if(p_fund_id != NULL) {
  213. tb_entity_ret = SELECT entity_id, price_date, ret FROM get_entity_return(entity_type, p_fund_id , freq, start_day, end_day, true);
  214. alternative_id = p_fund_id[0];
  215. level = 4;
  216. } else if(primary_benchmark_id != NULL) {
  217. tb_entity_ret = SELECT entity_id, price_date, ret FROM get_entity_return(entity_type, primary_benchmark_id, freq, start_day, end_day, true);
  218. alternative_id = primary_benchmark_id[0];
  219. level = 3;
  220. } else {
  221. return tb_result;
  222. }
  223. }
  224. }
  225. tb_entity_ret.addColumn('effective_date', STRING);
  226. tb_index_ret = index_ret;
  227. tb_index_ret.addColumn('effective_date', STRING);
  228. if(freq == 'm') {
  229. UPDATE tb_entity_ret SET effective_date = price_date.temporalFormat('yyyy-MM');
  230. UPDATE tb_index_ret SET effective_date = price_date.temporalFormat('yyyy-MM');
  231. } else if(freq == 'w') {
  232. UPDATE tb_entity_ret SET effective_date = get_year_week(price_date);
  233. UPDATE tb_index_ret SET effective_date = get_year_week(price_date);
  234. } else {
  235. UPDATE tb_entity_ret SET effective_date = price_date$STRING;
  236. UPDATE tb_index_ret SET effective_date = price_date$STRING;
  237. }
  238. tb_result = SELECT entity_ret.entity_id[0] AS entity_id, effective_date, price_date, index_id, weights, alternative_id, level, alpha, r2, adj_r2
  239. FROM cal_rolling_rbsa(tb_entity_ret, tb_index_ret, is_long, window, step);
  240. return tb_result;
  241. }