returnCalculator.dos 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. module fundit::returnCalculator
  2. use fundit::sqlUtilities
  3. use fundit::operationDataPuller
  4. use fundit::performanceDataPuller
  5. /*
  6. * 通用月收益计算
  7. *
  8. * @param entity_info <TABLE>: [COLUMNS] entity_id, price_date, inception_date, ini_value
  9. * @param nav <TABLE>: [COLUMNS] entity_id, price_date, cumulative_nav
  10. *
  11. */
  12. def cal_monthly_returns_by_nav(entity_info, mutable nav) {
  13. tb_rets = null;
  14. if(nav.isVoid() || nav.size() == 0 || entity_info.isVoid() || entity_info.size() == 0 ) return null;
  15. // 所有月末日期和净值
  16. tb_monthly_nav = SELECT entity_id, price_date.month().last() AS end_date, price_date.last() AS price_date, cumulative_nav.last() AS cumulative_nav
  17. FROM nav.sortBy!(['entity_id', 'price_date'], [1, 1])
  18. GROUP BY entity_id, price_date.month();
  19. // 筛掉成立日之前的净值
  20. tb_monthly_nav = SELECT n.entity_id, end_date, price_date, cumulative_nav
  21. FROM tb_monthly_nav n
  22. INNER JOIN entity_info ei ON n.entity_id = ei.entity_id
  23. WHERE n.price_date >= ei.inception_date;
  24. // 补回来成立日初始净值
  25. INSERT INTO tb_monthly_nav
  26. SELECT entity_id, inception_date.month(), inception_date, ini_value
  27. FROM entity_info
  28. WHERE inception_date IS NOT NULL;
  29. if(tb_monthly_nav.isVoid() || tb_monthly_nav.size() == 0) { return tb_rets; }
  30. // 计算月收益
  31. tb_rets = SELECT entity_id, end_date, price_date, cumulative_nav, cumulative_nav.ratios() - 1 AS ret
  32. FROM tb_monthly_nav.sortBy!(['entity_id', 'price_date'], [1, 1])
  33. CONTEXT BY entity_id;
  34. // the records without return calculated but do have nav are still useful for some calculations (e.g. max drawdown)
  35. return ( SELECT * FROM tb_rets WHERE cumulative_nav > 0 );
  36. }
  37. /*
  38. * 根据最新更新的净值计算收益,并与数据库历史收益合并为完整收益, 数据源是MySQL
  39. *
  40. * @param entity_type <STRING>:
  41. * @param entity_info <TABLE>: COLUMN NEED entity_id, inception_date, benchmark_id, ini_value, price_date
  42. * @param start_date <DATETIME>: 净值(datasource='nav')、收益起始日(datasource='perf')、净值更新的最新日期(datasource='mix')
  43. * @param data_source <STRING>: nav(nav table), perf(xxx_performance table), mix(new updated nav + old performance table ret_1m)
  44. *
  45. *
  46. */
  47. def mix_monthly_returns(entity_type, entity_info) {
  48. ret = null;
  49. very_old_day = 1990.01.01;
  50. end_day = today();
  51. s_json = (SELECT entity_id AS sec_id, price_date FROM entity_info).toStdJson();
  52. // 取基金组合在包括各自的某净值日期的前值及之后的所有净值
  53. tb_nav = get_nav_for_return_calculation(entity_type, 'm', s_json);
  54. tb_nav.rename!('sec_id', 'entity_id');
  55. // 计算某净值日期所在月份及之后的所有月收益
  56. ret = cal_monthly_returns_by_nav(entity_info, tb_nav);
  57. if(ret.isVoid() || ret.size() == 0) return ret;
  58. // 筛掉引入的前值,这些记录用来计算第一期收益后就不再有用
  59. // 不知道为什么 delete ret from ej(ret, tb_entities, 'entity_id') where ret.price_date < tb_entities.price_date 会报错
  60. ret = SELECT ret.* FROM ej(ret, entity_info, 'entity_id')
  61. WHERE ret.price_date >= entity_info.price_date;
  62. // 取数据库中的所有历史收益
  63. historical_rets = get_monthly_ret(entity_type, entity_info.entity_id, very_old_day, end_day, true);
  64. // MIX 将新NAV计算的收益和数据库中的历史收益合并,相同月份时用新计算的收益代替
  65. INSERT INTO ret
  66. SELECT entity_id, end_date.temporalParse('yyyy-MM'), price_date, nav, ret
  67. FROM historical_rets h
  68. WHERE NOT EXISTS ( SELECT * FROM ret WHERE entity_id = historical_rets.entity_id AND end_date = historical_rets.end_date.temporalParse('yyyy-MM') );
  69. // 过滤掉成立日之前的收益
  70. return SELECT ret.*
  71. FROM ret INNER JOIN entity_info ei ON ret.entity_id = ei.entity_id
  72. WHERE ret.end_date >= ei.inception_date.month()
  73. ORDER BY entity_id, end_date, price_date;
  74. }
  75. /*
  76. * 根据基金净值序列计算周收益序列
  77. *
  78. * Create: 20240907 Joey
  79. * TODO: missing pulling data from local
  80. *
  81. * @param entity_type <STRING>: NAV universe, 'HF','MF','PF','EQ'... defined in get_nav_table_description()
  82. * @param entity_info <TABLE>: COLUMN NEED entity_id, price_date, inception_date
  83. *
  84. *
  85. */
  86. def cal_weekly_returns(entity_type, entity_info){
  87. tb_rets_1w = null;
  88. if(!(entity_type IN ['HF', 'MF', 'PF', 'MI', 'FI', 'FA'])) return tb_rets_1w;
  89. // 将每支证券ID+某个日期转为JSON用于调用sp
  90. s_json = (SELECT entity_id AS sec_id, price_date FROM entity_info).toStdJson();
  91. // 取基金组合在包括各自的某净值日期的前值及之后的所有净值
  92. tb_nav = get_nav_for_return_calculation(entity_type, 'w', s_json);
  93. tb_nav.rename!('sec_id', 'entity_id');
  94. UPDATE tb_nav SET year_week = get_year_week(price_date);
  95. tb_weekly_nav = SELECT entity_id, year_week, price_date.last() AS price_date, cumulative_nav.last() AS cumulative_nav
  96. FROM tb_nav n
  97. INNER JOIN entity_info ei ON n.entity_id = ei.entity_id
  98. WHERE n.price_date >= ei.inception_date // 筛掉成立日之前的净值
  99. GROUP BY entity_id, year_week
  100. ORDER BY entity_id, year_week;
  101. // 这里选最简单的计算方式:不补任何净值空洞,净值前值日期不做任何限制
  102. // TODO: 可以考虑将月收益也改为这种方式
  103. tb_rets_1w = SELECT entity_id, year_week, price_date, cumulative_nav, cumulative_nav.ratios()-1 AS ret_1w
  104. FROM tb_weekly_nav
  105. ORDER BY entity_id, year_week;
  106. return tb_rets_1w;
  107. }
  108. /*
  109. * 批量计算区间收益
  110. * TODO: mySQL version 向前取4天,向后不做限制。这里的逻辑是向前取4个交易日, 遇到大长假后可能取不到数据
  111. * 【老程序,可优化】
  112. *
  113. * get_trailing_return(tb_last_nav, tb_nav, -7d, "ret_1w")
  114. *
  115. */
  116. def get_trailing_return(table_last_nav, table_nav, duration, return_column_name) {
  117. tb = SELECT a.entity_id, a.price_date.last() AS price_date, a.cumulative_nav.last() \ b.cumulative_nav.last() - 1 AS ret
  118. FROM table_last_nav a
  119. INNER JOIN table_nav b ON a.entity_id = b.entity_id
  120. WHERE b.price_date <= a.price_date.datetimeAdd(duration)
  121. AND b.price_date >= a.price_date.datetimeAdd(duration).datetimeAdd(-4d).businessDay()
  122. GROUP BY a.entity_id
  123. ORDER BY entity_id;
  124. tb.rename!("ret", return_column_name);
  125. return tb;
  126. }
  127. /*
  128. * 批量计算最新收益
  129. *
  130. 【老程序,可优化】
  131. *
  132. */
  133. def cal_latest_performance(entity_type, entity_info, isFromMySQL) {
  134. // 用于保证老基金也能取到所有历史净值
  135. very_old_price_date = 1990.01.01
  136. tb_nav = get_nav_by_price_date(entity_type, entity_info.entity_id, very_old_price_date, isFromMySQL)
  137. tb_last_nav = SELECT entity_id, price_date.last() AS price_date, cumulative_nav.last() AS cumulative_nav
  138. FROM tb_nav
  139. GROUP BY entity_id
  140. ORDER BY entity_id
  141. // 近1期收益,对应mySQL fund_latest_nav_performance 中的 net_value_change
  142. // 因为是倒序,所以算出来的 ratios() = n0 / n1, 要把它改换成 n1 / n0 - 1 的收益
  143. tb_last_return = SELECT TOP 2 entity_id, price_date.first() AS price_date, price_date AS pre_preice_date,
  144. nav.first() AS nav, cumulative_nav.first() AS cumulative_nav, 1\cumulative_nav.ratios() - 1 AS net_value_change
  145. FROM ( SELECT * FROM tb_nav ORDER BY price_date DESC )
  146. CONTEXT BY entity_id
  147. ORDER BY entity_id
  148. tb_last_return = SELECT * FROM tb_last_return WHERE net_value_change IS NOT NULL
  149. // 近1交易日收益
  150. tb_1d = SELECT a.entity_id, a.price_date, a.cumulative_nav \ b.cumulative_nav - 1 AS ret_1d
  151. FROM tb_last_nav a
  152. INNER JOIN tb_nav b ON a.entity_id = b.entity_id AND b.price_date = a.price_date.datetimeAdd(-1d).businessDay()
  153. ORDER BY entity_id
  154. // 近1周、1/3/6月、1/2/3/4/5/10年收益
  155. tb_1w = get_trailing_return(tb_last_nav, tb_nav, -7d, "ret_1w")
  156. tb_1m = get_trailing_return(tb_last_nav, tb_nav, -1M, "ret_1m")
  157. tb_3m = get_trailing_return(tb_last_nav, tb_nav, -3M, "ret_3m")
  158. tb_6m = get_trailing_return(tb_last_nav, tb_nav, -6M, "ret_6m")
  159. tb_1y = get_trailing_return(tb_last_nav, tb_nav, -1y, "ret_1y")
  160. tb_2y = get_trailing_return(tb_last_nav, tb_nav, -2y, "ret_2y")
  161. tb_3y = get_trailing_return(tb_last_nav, tb_nav, -3y, "ret_3y")
  162. tb_4y = get_trailing_return(tb_last_nav, tb_nav, -4y, "ret_4y")
  163. tb_5y = get_trailing_return(tb_last_nav, tb_nav, -5y, "ret_5y")
  164. tb_10y = get_trailing_return(tb_last_nav, tb_nav, -10y, "ret_10y")
  165. // ytd return
  166. tb_ytd = SELECT a.entity_id, a.price_date.last() AS price_date, a.cumulative_nav.last() \ b.cumulative_nav.last() - 1 AS ret_ytd
  167. FROM tb_last_nav a
  168. INNER JOIN tb_nav b ON a.entity_id = b.entity_id
  169. WHERE b.price_date < a.price_date.yearBegin()
  170. AND b.price_date >= a.price_date.yearBegin().datetimeAdd(-4d)
  171. GROUP BY a.entity_id
  172. // since inception return
  173. tb_incep = SELECT a.entity_id, a.price_date, -1 + cumulative_nav \ ini_value AS ret_incep, fi.inception_date
  174. FROM tb_last_nav a
  175. INNER JOIN entity_info fi ON a.entity_id = fi.entity_id
  176. // annulized since reception return following GIPS rule
  177. UPDATE tb_incep SET ret_incep_a = iif((price_date-inception_date)<=365, ret_incep, (1 + ret_incep).pow(365.25\(price_date-inception_date)) - 1)
  178. UPDATE tb_incep SET ret_incep_a_all = ret_incep_a,
  179. ret_incep_a_gips = ret_incep_a
  180. // 最大回撤
  181. tb_drawdown_1m = SELECT a.entity_id, max( 1 - b.cumulative_nav \ b.cumulative_nav.cummax() ) AS drawdown_1m
  182. FROM tb_last_return a
  183. INNER JOIN tb_nav b ON a.entity_id = b.entity_id
  184. WHERE b.price_date >= a.price_date.datetimeAdd(-1M)
  185. GROUP BY a.entity_id
  186. tb_drawdown_3m = SELECT a.entity_id, max( 1 - b.cumulative_nav \ b.cumulative_nav.cummax() ) AS drawdown_3m
  187. FROM tb_last_return a
  188. INNER JOIN tb_nav b ON a.entity_id = b.entity_id
  189. WHERE b.price_date >= a.price_date.datetimeAdd(-3M)
  190. GROUP BY a.entity_id
  191. tb_drawdown_1y = SELECT a.entity_id, max( 1 - b.cumulative_nav \ b.cumulative_nav.cummax() ) AS drawdown_1y
  192. FROM tb_last_return a
  193. INNER JOIN tb_nav b ON a.entity_id = b.entity_id
  194. WHERE b.price_date >= a.price_date.datetimeAdd(-1y)
  195. GROUP BY a.entity_id
  196. tb_drawdown_incep = SELECT entity_id, max( 1 - cumulative_nav \ cumulative_nav.cummax() ) AS drawdown_incep
  197. FROM tb_nav GROUP BY entity_id
  198. tb_rets = SELECT a.entity_id, a.price_date.datetimeFormat("yyyy-MM") AS end_date, a.price_date, a.pre_preice_date, a.nav, a.cumulative_nav,
  199. a.net_value_change, d1.ret_1d, w1.ret_1w, m1.ret_1m, m3.ret_3m, m6.ret_6m,
  200. y1.ret_1y, y2.ret_2y, y3.ret_3y, y4.ret_4y, y5.ret_5y, y10.ret_10y,
  201. ytd.ret_ytd, incep.ret_incep, incep.ret_incep_a, incep.ret_incep_a_all, incep.ret_incep_a_gips,
  202. dd_m1.drawdown_1m AS maxdrawdown_1m, dd_m3.drawdown_3m AS maxdrawdown_3m, dd_y1.drawdown_1y AS maxdrawdown_1y,
  203. dd_incep.drawdown_incep AS maxdrawdown_incep,
  204. iif(dd_incep.drawdown_incep.round(4) == 0 || incep.ret_incep_a > 1000, NULL, incep.ret_incep_a \ dd_incep.drawdown_incep) AS calmarratio_incep
  205. FROM tb_last_return a
  206. LEFT JOIN tb_1d d1 ON a.entity_id = d1.entity_id
  207. LEFT JOIN tb_1w w1 ON a.entity_id = w1.entity_id
  208. LEFT JOIN tb_1m m1 ON a.entity_id = m1.entity_id
  209. LEFT JOIN tb_3m m3 ON a.entity_id = m3.entity_id
  210. LEFT JOIN tb_6m m6 ON a.entity_id = m6.entity_id
  211. LEFT JOIN tb_1y y1 ON a.entity_id = y1.entity_id
  212. LEFT JOIN tb_2y y2 ON a.entity_id = y2.entity_id
  213. LEFT JOIN tb_3y y3 ON a.entity_id = y3.entity_id
  214. LEFT JOIN tb_4y y4 ON a.entity_id = y4.entity_id
  215. LEFT JOIN tb_5y y5 ON a.entity_id = y5.entity_id
  216. LEFT JOIN tb_10y y10 ON a.entity_id = y10.entity_id
  217. LEFT JOIN tb_ytd ytd ON a.entity_id = ytd.entity_id
  218. LEFT JOIN tb_incep incep ON a.entity_id = incep.entity_id
  219. LEFT JOIN tb_drawdown_1m dd_m1 ON a.entity_id = dd_m1.entity_id
  220. LEFT JOIN tb_drawdown_3m dd_m3 ON a.entity_id = dd_m3.entity_id
  221. LEFT JOIN tb_drawdown_1y dd_y1 ON a.entity_id = dd_y1.entity_id
  222. LEFT JOIN tb_drawdown_incep dd_incep ON a.entity_id = dd_incep.entity_id
  223. ORDER BY a.entity_id
  224. // 忽略掉非GIPS标准的所有年化收益字段(包括ytd_a)
  225. UPDATE tb_rets SET ret_1y_a = ret_1y, ret_2y_a = (1 + ret_2y).pow(1\2) - 1, ret_3y_a = (1 + ret_3y).pow(1\3) - 1,
  226. ret_4y_a = (1 + ret_4y).pow(1\4) - 1, ret_5y_a = (1 + ret_5y).pow(1\5) - 1, ret_10y_a = (1 + ret_10y).pow(1\10) - 1
  227. return tb_rets
  228. }