returnCalculator.dos 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. module fundit::returnCalculator
  2. use fundit::fundCalculator
  3. use fundit::dataPuller
  4. /*
  5. * 根据私募基金净值序列计算月收益序列(适合提供给指标运算)
  6. *
  7. * Create: 20240907 Joey
  8. * TODO: missing pulling data from local
  9. * TODO: ONLY support month return now
  10. *
  11. * @param fund_ids: 基金ID STRING VECTOR
  12. * @param isFromMySQL: 净值来源 1 - 远程MySQL、 0 - 本地 DolphinDB
  13. *
  14. */
  15. def cal_hedge_fund_returns(fund_ids, isFromMySQL){
  16. tb_rets = null;
  17. // 用于保证老基金也能取到所有历史净值
  18. very_old_price_date = 1990.01.01;
  19. if(isFromMySQL){
  20. // 基金基本信息,包括初始净值
  21. tb_fund_info = get_fund_info(fund_ids);
  22. // 基金净值
  23. tb_nav = SELECT * FROM get_hedge_fund_nav_by_price_date(fund_ids, very_old_price_date, true);
  24. tb_month_end = table(100:0, ['fund_id', 'price_date'], [STRING, DATE]);
  25. // 填充好各基金有效期内所有月份的最后一天
  26. for( f in tb_fund_info )
  27. {
  28. INSERT INTO tb_month_end SELECT fund_id, price_date FROM table(f.fund_id.take(1) AS fund_id).cj(table(temporalSeq(f.inception_date, today(), 'M') AS price_date)) ;
  29. }
  30. UPDATE tb_month_end SET end_date = price_date.month();
  31. tb_monthly_nav = SELECT fund_id, monthEnd(price_date).month().last() AS end_date, price_date.last() AS price_date, cumulative_nav.last() AS cumulative_nav
  32. FROM tb_nav
  33. GROUP BY fund_id, monthEnd(price_date);
  34. // 完整月末日期的净值序列(包括缺失数据为NULL)
  35. tb_monthly_nav = SELECT me.fund_id, me.end_date, n.price_date, n.cumulative_nav
  36. FROM tb_month_end me
  37. LEFT JOIN tb_monthly_nav n ON me.fund_id = n.fund_id AND me.end_date = n.end_date
  38. ORDER BY me.fund_id, me.end_date;
  39. // 补一下成立日的初始净值
  40. // NOTE: DolphinDB 遇见 EXISTS 语句时,似乎主表的 alias 失效,只好用全名
  41. INSERT INTO tb_monthly_nav
  42. SELECT fund_id, inception_date.month(), inception_date, ifNull(ini_value, 1)
  43. FROM tb_fund_info fi
  44. WHERE NOT EXISTS ( SELECT * FROM tb_monthly_nav n WHERE fund_id = tb_fund_info.fund_id AND n.price_date = tb_fund_info.inception_date);
  45. // 算 ratios 之前先把时间顺序排好
  46. tb_monthly_nav.sortBy!(['fund_id', 'end_date', 'price_date'], [1, 1, 1]);
  47. // 计算月收益
  48. tb_rets = SELECT fund_id, end_date, price_date, cumulative_nav, cumulative_nav.ratios() - 1 AS ret
  49. FROM tb_monthly_nav
  50. CONTEXT BY fund_id;
  51. }
  52. // the records without return calculated but do have nav are still useful for some calculations
  53. return ( SELECT * FROM tb_rets WHERE cumulative_nav > 0 );
  54. }
  55. /*
  56. * 月末 fund_performance 表计算
  57. *
  58. * @param fund_ids: 逗号分隔的ID
  59. * @param end_date:
  60. *
  61. * Example: cal_fund_performance("'HF000004KN','HF00018WXG','HF000103EU'", '2024-06', true);
  62. */
  63. def cal_fund_performance(fund_ids, month_end) {
  64. // 获取必要的基金月度净值
  65. tb_nav = get_nav_for_hedge_fund_performance(fund_ids, month_end);
  66. tb_rets = SELECT fund_id, price_date.month() AS end_date, price_date, cumulative_nav,
  67. cumulative_nav \ nav_1m - 1 AS ret_1m,
  68. cumulative_nav \ nav_3m - 1 AS ret_3m,
  69. cumulative_nav \ nav_6m - 1 AS ret_6m,
  70. cumulative_nav \ nav_1y - 1 AS ret_1y,
  71. cumulative_nav \ nav_2y - 1 AS ret_2y,
  72. cumulative_nav \ nav_3y - 1 AS ret_3y,
  73. cumulative_nav \ nav_4y - 1 AS ret_4y,
  74. cumulative_nav \ nav_5y - 1 AS ret_5y,
  75. cumulative_nav \ nav_10y - 1 AS ret_10y,
  76. cumulative_nav \ nav_ytd - 1 AS ret_ytd,
  77. cumulative_nav \ nav_incep - 1 AS ret_incep, inception_date
  78. FROM tb_nav;
  79. // NOTE: this is to keep consistance with MySQL, even it is NOT complied with GIPS standard
  80. UPDATE tb_rets SET ret_1m_a = (1 + ret_1m).pow(12\1) - 1, ret_3m_a = (1 + ret_3m).pow(12\3) - 1, ret_6m_a = (1 + ret_6m).pow(12\6) - 1,
  81. ret_1y_a= ret_1y, ret_2y_a = (1 + ret_2y).pow(12\24) - 1, ret_3y_a = (1 + ret_3y).pow(12\36) - 1,
  82. ret_4y_a = (1 + ret_4y).pow(12\48) - 1, ret_5y_a = (1 + ret_5y).pow(12\60) - 1, ret_10y_a = (1 + ret_10y).pow(12\120) - 1,
  83. ret_ytd_a = (1 + ret_ytd).pow(12\int(temporalFormat(end_date, 'MM')))-1,
  84. ret_incep_a = (1 + ret_incep).pow(12\(end_date - inception_date.month())) - 1,
  85. ret_incep_a_all = (1 + ret_incep).pow(12\(end_date - inception_date.month()))- 1,
  86. ret_incep_a_gips = iif(end_date - inception_date.month() < 12, ret_incep,
  87. (1 + ret_incep).pow(12\(end_date - inception_date.month()))- 1);
  88. return tb_rets;
  89. }
  90. /*
  91. * 批量计算公募历史基金月度收益(fund_performance)
  92. * NOTE: 任何数据频率快于或等于月度的净值数据都可以用此函数一次性计算完整历史记录。双月频、季频甚至更低频率的基金只能按月计算
  93. *
  94. * cal_mutual_fund_performance("'HF000004KN','HF00018WXG','HF000103EU'", true)
  95. *
  96. */
  97. def cal_mutual_fund_performance(fund_ids, isFromMySQL) {
  98. /* 找到不必用pivot table 填充数据的办法了
  99. // NOTE: mySQL currently uses calendar day, while the codes below takes business day. it might cause a few different numbers calcuated
  100. tb_monthly_nav = SELECT fund_id, businessMonthEnd(price_date).month().last() AS end_date, price_date.last() AS price_date, cumulative_nav.last() AS cumulative_nav
  101. FROM tb_nav
  102. GROUP BY fund_id, businessMonthEnd(price_date);
  103. // 补一下成立日净值
  104. // NOTE: DolphinDB 遇见 EXISTS 语句时,似乎主表的 alias 失效,只好用全名
  105. INSERT INTO tb_monthly_nav
  106. SELECT businessMonthEnd(inception_date), fund_id, inception_date.month(), inception_date, ifNull(ini_value, 1)
  107. FROM tb_fund_info
  108. WHERE NOT EXISTS ( SELECT * FROM tb_monthly_nav n WHERE tb_fund_info.fund_id = n.fund_id AND tb_fund_info.inception_date = n.price_date);
  109. // 为了把不数据库里不存在的nav记录填空,不得不先做个pivot;然后才能正确计算ratios (ret_1m)
  110. // TODO: much better way is to have a "full list" of dates, then LEFT JOIN nav table, then ffill()
  111. tb_rets_1m = (SELECT cumulative_nav FROM tb_monthly_nav PIVOT BY end_date, fund_id).ffill!().ratios()-1
  112. // 取被pivot掉的fund_Ids
  113. v_col_name = tb_rets_1m.columnNames()[1:]
  114. tb_tmp = tb_rets_1m.unpivot("end_date", v_col_name).rename!("valueType" "value", "fund_id" "ret_1m")
  115. */
  116. // 计算月收益
  117. tb_tmp = cal_hedge_fund_returns(fund_ids, isFromMySQL);
  118. tb_rets = SELECT fund_id, end_date, ret_1m,
  119. (1 + ret_1m).mprod(3) - 1 AS ret_3m, (1 + ret_1m).mprod(6) - 1 AS ret_6m, (1 + ret_1m).mprod(12) - 1 AS ret_1y,
  120. (1 + ret_1m).mprod(24) - 1 AS ret_2y, (1 + ret_1m).mprod(36) - 1 AS ret_3y, (1 + ret_1m).mprod(48) - 1 AS ret_4y,
  121. (1 + ret_1m).mprod(60) - 1 AS ret_5y, (1 + ret_1m).mprod(120) - 1 AS ret_10y
  122. FROM tb_tmp
  123. CONTEXT BY fund_id;
  124. // NOTE: this is to keep consistance with MySQL, even it is NOT complied with GIPS standard
  125. UPDATE tb_rets SET ret_1m_a = (1 + ret_1m).pow(12) - 1, ret_3m_a = (1 + ret_3m).pow(4) - 1, ret_6m_a = (1 + ret_6m).pow(2) - 1, ret_1y_a= ret_1y,
  126. ret_2y_a = (1 + ret_2y).pow(1\2) - 1, ret_3y_a = (1 + ret_3y).pow(1\3) - 1, ret_4y_a = (1 + ret_4y).pow(1\4) - 1,
  127. ret_5y_a = (1 + ret_5y).pow(1\5) - 1, ret_10y_a = (1 + ret_10y).pow(1\10) - 1;
  128. // ytd 不会用上面的CONTEXT BY语句实现
  129. tb_ret_ytd = SELECT a.fund_id, a.end_date, a.price_date, a.cumulative_nav, -1 + a.cumulative_nav \ b.cumulative_nav AS ret_ytd,
  130. (a.cumulative_nav \ b.cumulative_nav).pow(12\(a.end_date - b.end_date)) - 1 AS ret_ytd_a
  131. FROM tb_rets a
  132. INNER JOIN tb_rets b ON a.fund_Id = b.fund_id
  133. AND b.end_date = a.price_date.yearEnd().datetimeAdd(-1y).month()
  134. // since inception 不会用上面的CONTEXT BY语句实现
  135. tb_ret_incep = SELECT a.fund_id, a.end_date, a.price_date, cumulative_nav, -1 + cumulative_nav \ ini_value AS ret_incep
  136. FROM tb_rets a
  137. INNER JOIN tb_fund_info fi ON a.fund_id = fi.fund_id
  138. UPDATE tb_ret_incep SET ret_incep_a = (1 + ret_incep).pow(12\(end_date - end_date.first())) - 1 CONTEXT BY fund_Id
  139. UPDATE tb_ret_incep SET ret_incep_a_gips = iif( end_date - end_date.first() < 12, ret_incep, ret_incep_a ), ret_incep_a_all = ret_incep_a CONTEXT BY fund_id
  140. // 只选需要更新的记录
  141. tb_fund_performance = SELECT a.fund_id, a.end_date.datetimeFormat("yyyy-MM") AS end_date, c.price_date, c.cumulative_nav,
  142. a.ret_1m, a.ret_1m_a, a.ret_3m, a.ret_3m_a, a.ret_6m, a.ret_6m_a,
  143. a.ret_1y, a.ret_1y_a, a.ret_2y, a.ret_2y_a, a.ret_3y, a.ret_3y_a,
  144. a.ret_4y, a.ret_4y_a, a.ret_5y, a.ret_5y_a, a.ret_10y, a.ret_10y_a,
  145. b.ret_ytd, b.ret_ytd_a, c.ret_incep, c.ret_incep_a, c.ret_incep_a_all, c.ret_incep_a_gips
  146. // , 123 AS creatorid, now() AS createtime, 123 AS updaterid, now() AS updatetime, 1 AS isvalid
  147. FROM tb_rets a
  148. LEFT JOIN tb_ret_ytd b ON a.fund_id = b.fund_id AND a.end_date = b.end_date
  149. LEFT JOIN tb_ret_incep c ON a.fund_id = c.fund_id AND a.end_date = c.end_date
  150. WHERE c.price_date IS NOT NULL
  151. ORDER BY a.fund_id, c.price_date
  152. /*
  153. // 把这些数据写回mySQL数据表
  154. save_table(tb_fund_performance, "mfdb.fund_performance", true)
  155. // 把这些数据写到本地数据表
  156. save_table(tb_fund_performance, "mfdb.fund_performance", false)
  157. */
  158. return tb_fund_performance
  159. }
  160. /*
  161. * 批量计算私募基金周收益
  162. * TODO: 需要用每周最后一个交易日?
  163. *
  164. * cal_hedge_fund_weekly_returns("'HF000004KN','HF00018WXG','HF000103EU'", true)
  165. *
  166. */
  167. def cal_hedge_fund_weekly_returns(fund_ids, isFromMySQL) {
  168. // 用于保证老基金也能取到所有历史净值
  169. very_old_price_date = 1990.01.01
  170. tb_nav = get_hedge_fund_nav_by_price_date(fund_ids, very_old_price_date, isFromMySQL)
  171. UPDATE tb_nav SET year_week = price_date.year()$STRING + (price_date.weekOfYear()$STRING).lpad(2, "0")
  172. tb_weekly_nav = SELECT fund_id, year_week, price_date.last() AS price_date, cumulative_nav.last() AS cumulative_nav
  173. FROM tb_nav
  174. GROUP BY fund_id, year_week
  175. ORDER BY fund_id, year_week
  176. // 这里选最简单的计算方式:不补任何净值空洞,净值前值日期不做任何限制
  177. // TODO: 可以考虑将月收益也改为这种方式
  178. tb_rets_1w = SELECT fund_id, year_week, price_date, cumulative_nav, cumulative_nav.ratios()-1 AS ret_1w
  179. FROM tb_weekly_nav
  180. ORDER BY fund_id, year_week
  181. return tb_rets_1w
  182. }
  183. /*
  184. * 批量计算私募基金区间收益
  185. * TODO: mySQL version 向前取4天,向后不做限制。这里的逻辑是向前取4个交易日
  186. *
  187. * get_trailing_return(tb_last_nav, tb_nav, -7d, "ret_1w")
  188. *
  189. */
  190. def get_trailing_return(table_last_nav, table_nav, duration, return_column_name) {
  191. tb = SELECT a.fund_id, a.price_date.last() AS price_date, a.cumulative_nav.last() \ b.cumulative_nav.last() - 1 AS ret
  192. FROM table_last_nav a
  193. INNER JOIN table_nav b ON a.fund_id = b.fund_id
  194. WHERE b.price_date <= a.price_date.datetimeAdd(duration)
  195. AND b.price_date >= a.price_date.datetimeAdd(duration).datetimeAdd(-4d).businessDay()
  196. GROUP by a.fund_id
  197. ORDER BY fund_id
  198. tb.rename!("ret", return_column_name)
  199. return tb
  200. }
  201. /*
  202. * 批量计算私募基金最新收益
  203. *
  204. *
  205. * cal_hedge_fund_weekly_returns("'HF000004KN','HF00018WXG','HF000103EU'", true)
  206. *
  207. */
  208. def cal_hedge_fund_latest_returns(fund_ids, isFromMySQL) {
  209. // 用于保证老基金也能取到所有历史净值
  210. very_old_price_date = 1990.01.01
  211. tb_nav = get_hedge_fund_nav_by_price_date(fund_ids, very_old_price_date, isFromMySQL)
  212. tb_last_nav = SELECT fund_id, price_date.last() AS price_date, cumulative_nav.last() AS cumulative_nav
  213. FROM tb_nav
  214. GROUP BY fund_id
  215. ORDER BY fund_id
  216. // 近1期收益,对应mySQL fund_latest_nav_performance 中的 net_value_change
  217. // 因为是倒序,所以算出来的 ratios() = n0 / n1, 要把它改换成 n1 / n0 - 1 的收益
  218. tb_last_return = SELECT TOP 2 fund_id, price_date.first() AS price_date, price_date AS pre_preice_date,
  219. cumulative_nav.first() AS cumulative_nav, 1\cumulative_nav.ratios() - 1 AS net_value_change
  220. FROM ( SELECT * FROM tb_nav ORDER BY price_date DESC )
  221. CONTEXT BY fund_id
  222. ORDER BY fund_id
  223. tb_last_return = SELECT * FROM tb_last_return WHERE net_value_change IS NOT NULL
  224. // 近1交易日收益
  225. tb_1d = SELECT a.fund_id, a.price_date, a.cumulative_nav \ b.cumulative_nav - 1 AS ret_1d
  226. FROM tb_last_nav a
  227. INNER JOIN tb_nav b ON a.fund_id = b.fund_id AND b.price_date = a.price_date.datetimeAdd(-1d).businessDay()
  228. ORDER BY fund_id
  229. // 近1周、1/3/6月、1/2/3/4/5/10年收益
  230. tb_1w = get_trailing_return(tb_last_nav, tb_nav, -7d, "ret_1w")
  231. tb_1m = get_trailing_return(tb_last_nav, tb_nav, -1M, "ret_1m")
  232. tb_3m = get_trailing_return(tb_last_nav, tb_nav, -3M, "ret_3m")
  233. tb_6m = get_trailing_return(tb_last_nav, tb_nav, -6M, "ret_6m")
  234. tb_1y = get_trailing_return(tb_last_nav, tb_nav, -1y, "ret_1y")
  235. tb_2y = get_trailing_return(tb_last_nav, tb_nav, -2y, "ret_2y")
  236. tb_3y = get_trailing_return(tb_last_nav, tb_nav, -3y, "ret_3y")
  237. tb_4y = get_trailing_return(tb_last_nav, tb_nav, -4y, "ret_4y")
  238. tb_5y = get_trailing_return(tb_last_nav, tb_nav, -5y, "ret_5y")
  239. tb_10y = get_trailing_return(tb_last_nav, tb_nav, -10y, "ret_10y")
  240. // ytd return
  241. tb_ytd = SELECT a.fund_id, a.price_date.last() AS price_date, a.cumulative_nav.last() \ b.cumulative_nav.last() - 1 AS ret_ytd
  242. FROM tb_last_nav a
  243. INNER JOIN tb_nav b ON a.fund_id = b.fund_id
  244. WHERE b.price_date < a.price_date.yearBegin()
  245. AND b.price_date >= a.price_date.yearBegin().datetimeAdd(-4d)
  246. GROUP by a.fund_id
  247. // since inception return
  248. tb_fund_info = get_fund_info(fund_ids)
  249. tb_incep = SELECT a.fund_id, a.price_date, -1 + cumulative_nav \ ini_value AS ret_incep, fi.inception_date
  250. FROM tb_last_nav a
  251. INNER JOIN tb_fund_info fi ON a.fund_id = fi.fund_id
  252. // annulized since reception return
  253. UPDATE tb_incep SET ret_incep_a = (1 + ret_incep).pow(365.25\(price_date-inception_date)) - 1
  254. UPDATE tb_incep SET ret_incep_a_all = ret_incep_a,
  255. ret_incep_a_gips = iif((price_date-inception_date)<365, ret_incep, ret_incep_a)
  256. // 最大回撤
  257. tb_drawdown_1m = SELECT a.fund_id, max( 1 - b.cumulative_nav \ b.cumulative_nav.cummax() ) AS drawdown_1m
  258. FROM tb_last_return a
  259. INNER JOIN tb_nav b ON a.fund_id = b.fund_id
  260. WHERE b.price_date >= a.price_date.datetimeAdd(-1M)
  261. GROUP BY a.fund_id
  262. tb_drawdown_3m = SELECT a.fund_id, max( 1 - b.cumulative_nav \ b.cumulative_nav.cummax() ) AS drawdown_3m
  263. FROM tb_last_return a
  264. INNER JOIN tb_nav b ON a.fund_id = b.fund_id
  265. WHERE b.price_date >= a.price_date.datetimeAdd(-3M)
  266. GROUP BY a.fund_id
  267. tb_drawdown_incep = SELECT fund_id, max( 1 - cumulative_nav \ cumulative_nav.cummax() ) AS drawdown_incep
  268. FROM tb_nav GROUP BY fund_id
  269. tb_rets = SELECT a.fund_id, a.price_date.datetimeFormat("yyyy-MM") AS end_date, a.price_date, a.pre_preice_date, a.cumulative_nav,
  270. a.net_value_change, d1.ret_1d, w1.ret_1w, m1.ret_1m, m3.ret_3m, m6.ret_6m,
  271. y1.ret_1y, y2.ret_2y, y3.ret_3y, y4.ret_4y, y5.ret_5y, y10.ret_10y,
  272. ytd.ret_ytd, incep.ret_incep, incep.ret_incep_a, incep.ret_incep_a_all, incep.ret_incep_a_gips,
  273. dd_m1.drawdown_1m AS maxdrawdown_1m, dd_m3.drawdown_3m AS maxdrawdown_3m,
  274. dd_incep.drawdown_incep AS maxdrawdown_incep,
  275. iif(dd_incep.drawdown_incep == 0, null,incep.ret_incep_a \ dd_incep.drawdown_incep) AS calmarratio_incep
  276. FROM tb_last_return a
  277. LEFT JOIN tb_1d d1 ON a.fund_id = d1.fund_id
  278. LEFT JOIN tb_1w w1 ON a.fund_id = w1.fund_id
  279. LEFT JOIN tb_1m m1 ON a.fund_id = m1.fund_id
  280. LEFT JOIN tb_3m m3 ON a.fund_id = m3.fund_id
  281. LEFT JOIN tb_6m m6 ON a.fund_id = m6.fund_id
  282. LEFT JOIN tb_1y y1 ON a.fund_id = y1.fund_id
  283. LEFT JOIN tb_2y y2 ON a.fund_id = y2.fund_id
  284. LEFT JOIN tb_3y y3 ON a.fund_id = y3.fund_id
  285. LEFT JOIN tb_4y y4 ON a.fund_id = y4.fund_id
  286. LEFT JOIN tb_5y y5 ON a.fund_id = y5.fund_id
  287. LEFT JOIN tb_10y y10 ON a.fund_id = y10.fund_id
  288. LEFT JOIN tb_ytd ytd ON a.fund_id = ytd.fund_id
  289. LEFT JOIN tb_incep incep ON a.fund_id = incep.fund_id
  290. LEFT JOIN tb_drawdown_1m dd_m1 ON a.fund_id = dd_m1.fund_id
  291. LEFT JOIN tb_drawdown_3m dd_m3 ON a.fund_id = dd_m3.fund_id
  292. LEFT JOIN tb_drawdown_incep dd_incep ON a.fund_id = dd_incep.fund_id
  293. ORDER BY a.fund_id
  294. // 忽略掉非GIPS标准的所有年化收益字段(包括ytd_a)
  295. 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,
  296. 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
  297. return tb_rets
  298. }