navCalculator.dos 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. module fundit::navCalculator
  2. use fundit::sqlUtilities
  3. use fundit::operationDataPuller
  4. use fundit::performanceDataPuller
  5. use fundit::portfolioDataPuller
  6. /*
  7. * 转交易表为交易日的持仓截面表
  8. * NOTE: 假定所有基金证券都是T+1买入,也就是第一天没有收益
  9. * 返回每个有交易的日期,以及当天会被纳入净值收益计算的各持仓份额(比如买入基金当天的份额数为0,卖出基金当天的份额是卖前份额)
  10. *
  11. * Example: convert_transaction_to_snapshot("166002,166114", 2024.10.31);
  12. */
  13. def convert_transaction_to_snapshot(portfolio_ids, end_day) {
  14. s_portfolio_ids = ids_to_string(portfolio_ids);
  15. // 取数据库中的持仓交易表
  16. tb_transaction = get_portfolio_holding_history(s_portfolio_ids);
  17. // 所有交易日期
  18. tb_date = SELECT DISTINCT portfolio_id, holding_date FROM tb_transaction;
  19. // 所有基金证券id
  20. tb_id = SELECT DISTINCT portfolio_id, fund_id FROM tb_transaction;
  21. tmp = SELECT tb_date.portfolio_id, tb_date.holding_date, tb_id.fund_id FROM ej(tb_date, tb_id, 'portfolio_id');
  22. // 取各交易日期时的持仓截面, Window Join 的上限设成-1d 是因为买入基金当日无收益,所以计算份额时要排除掉
  23. tb = wj(tmp, tb_transaction.sortBy!('holding_date'), duration('-50y'):duration('-1d'), <[t.fund_share.sum() AS shares]>, ['portfolio_id', 'fund_id', 'holding_date']);
  24. tb.addColumn('nav', DOUBLE);
  25. // 买入的基金份额记为0, 保留原始买入净值
  26. UPDATE tb
  27. SET shares = 0, nav = tb_transaction.nav
  28. FROM ej(tb, tb_transaction, ['portfolio_id', 'holding_date', 'fund_id'],, isNull(tb.shares));
  29. // 删除没用的数据;防一手脏数据
  30. DELETE FROM tb WHERE shares IS NULL OR shares.round(0) < 0;
  31. // 补上个虚拟的未来截面,以免buy-n-hold的证券信息损失;用0当NAV也是没办法,DolphinDB不能SELECT出个全NULL的列
  32. INSERT INTO tb
  33. SELECT portfolio_id, end_day, fund_id, fund_share.sum(), 0
  34. FROM tb_transaction
  35. GROUP BY portfolio_id, fund_id
  36. HAVING fund_share.sum().round(0) > 0;
  37. return tb.sortBy!(['portfolio_id', 'holding_date', 'fund_id'], [1, 1, 1]);
  38. }
  39. /*
  40. * 计算FOF类组合净值
  41. * NOTE: 与MySQL逻辑一致,用户界面输入的交易净值会被暂时忽略,因为我们无法确保同一基金同一时间被输入的净值是相同的;
  42. * 忽略手工净值会导致收益不精确或无法计算的问题,但可能错误的净值将导致错误的结果,两害取其轻。
  43. *
  44. *
  45. * Create: 20241101 用于代替 sp_cal_portfolio_nav Joey
  46. *
  47. * @param portfolio_info <TABLE>: NEED COLUMNS portfolio_id, sec_id, start_cal_date, end_cal_date, org_id
  48. *
  49. * Example:cal_portfolio_nav(get_portfolio_list_by_fund_nav_updatetime([166002,166114], 2024.10.28, true));
  50. *
  51. */
  52. def cal_portfolio_nav(portfolio_info) {
  53. if(portfolio_info.isVoid() || portfolio_info.size() == 0) return NULL;
  54. // 取持仓截面get_nav_for_return_calculation
  55. tb_snapshot = convert_transaction_to_snapshot(portfolio_info.portfolio_id, today()).rename!('fund_id', 'sec_id');
  56. if(tb_snapshot.isVoid() || tb_snapshot.size() == 0) return NULL;
  57. // 分别对应:私募,公募,私有基金,股票,市场指数,图译指数,私有指数,图译因子
  58. v_universe = ['HF', 'MF', 'CF', 'EQ', 'MI', 'FI', 'CI', 'FA'];
  59. v_prefix = ['HF%', 'MF%', 'CF%', 'EQ%', 'IN%', 'IN%', 'CI%', 'FA%'];
  60. d_universe = dict(v_universe, v_prefix);
  61. tb_nav = table(100:0, ['sec_id', 'price_date', 'cumulative_nav', 'nav'], [SYMBOL, DATE, DOUBLE, DOUBLE]);
  62. // 取计算所需的所有持仓净值数据
  63. for(u in d_universe.keys()) {
  64. // 取涉及到的所有基金证券最早持仓日期
  65. s_json = (SELECT sec_id, start_cal_date.min() AS price_date FROM portfolio_info WHERE sec_id LIKE d_universe[u] GROUP BY sec_id).toStdJson();
  66. // 取涉及到的所有基金证券有用净值
  67. // TODO: need consider inception date nav
  68. tmp_nav = get_nav_for_return_calculation(u, 'd', s_json);
  69. if(tmp_nav.isVoid() || tmp_nav.size() == 0) continue;
  70. INSERT INTO tb_nav SELECT * FROM tmp_nav;
  71. }
  72. // 补一下最新截面(虽然是个”假的”截面)
  73. tb_latest_snapshot = SELECT sec_id, holding_date, nav.mean().round(6) AS nav
  74. FROM tb_snapshot
  75. WHERE holding_date = today()
  76. AND NOT EXISTS ( SELECT 1 FROM tb_nav WHERE sec_id = tb_snapshot.sec_id AND price_date = tb_snapshot.holding_date )
  77. GROUP BY sec_id, holding_date;
  78. // Funky DolphinDB, INSERT INTO Table1 (Columns) SELECT Columns FROM Table2 会报列数不匹配的奇葩错误
  79. // this is the way to get around it
  80. INSERT INTO tb_nav (sec_id, price_date, cumulative_nav) VALUES (tb_latest_snapshot.sec_id, tb_latest_snapshot.holding_date, tb_latest_snapshot.nav);
  81. // 在各证券持仓时段中,填充所有无净值的但其它证券有净值的合理日期
  82. // 比如 2024-01-10 ~ 2024-01-20区间,组合持有基金A和基金B,基金A有每日净值
  83. // 而基金B只有01-12和01-19两期周五净值,那么基金B需要填充除这两天以外的所有日期
  84. tb_holding_date_range = SELECT p.portfolio_id, p.sec_id, n.price_date.max() AS oldest_date, today() AS latest_date
  85. FROM portfolio_info p
  86. INNER JOIN tb_nav n ON n.sec_id = p.sec_id
  87. WHERE n.price_date < p.start_cal_date
  88. GROUP BY p.portfolio_id, p.sec_id;
  89. // 所有净值日期+前值日期
  90. tb_date = SELECT DISTINCT dr.portfolio_id, n.price_date
  91. FROM tb_holding_date_range dr
  92. INNER JOIN tb_nav n ON dr.sec_id = n.sec_id
  93. WHERE n.price_date >= dr.oldest_date
  94. AND n.price_date <= dr.latest_date;
  95. // 所有基金证券id
  96. tb_id = SELECT DISTINCT portfolio_id, sec_id FROM tb_snapshot;
  97. // NOTE: 因为同一个组合下的持仓私募基金的净值前值日期会不一样, 所以在 tb_date里会混入多余的脏数据,导致某些私募的净值前值及日期被赋予错误的数据
  98. // 好消息是最后返回的收益及净值会把这些错误的前值筛掉,但最好想个办法在这里清除掉
  99. tb_holdings = SELECT id.portfolio_id, dt.price_date, id.sec_id, n.cumulative_nav, n.nav
  100. FROM tb_id id
  101. INNER JOIN tb_date dt ON id.portfolio_id = dt.portfolio_id
  102. INNER JOIN tb_holding_date_range dr ON dr.portfolio_id = id.portfolio_id AND dr.sec_id = id.sec_id
  103. LEFT JOIN tb_nav n ON n.sec_id = id.sec_id AND n.price_date = dt.price_date
  104. WHERE dt.price_date >= dr.oldest_date AND dt.price_date <= dr.latest_date
  105. ORDER BY id.portfolio_id, dt.price_date, id.sec_id;
  106. // 清一下内存
  107. tb_nav = null;
  108. // 为收益计算填充净值
  109. UPDATE tb_holdings SET cumulative_nav = cumulative_nav.ffill(), nav = nav.ffill()
  110. CONTEXT BY portfolio_id, sec_id;
  111. tb_holdings.addColumn(['ret', 'shares', 'market_value', 'total_mkt_value', 'weight'], [DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
  112. // 计算各持仓证券收益
  113. UPDATE tb_holdings SET ret = (cumulative_nav.ratios()-1).round(7)
  114. CONTEXT BY portfolio_id, sec_id;
  115. // 把交易日截面的份额数用于组合收益表
  116. UPDATE tb_holdings
  117. SET shares = ss.shares
  118. FROM ej(tb_holdings AS pr, tb_snapshot AS ss, ['portfolio_id', 'price_date', 'sec_id'], ['portfolio_id', 'holding_date', 'sec_id']);
  119. // 填充份额数为空的无交易日期,这段时间所有证券基金处于 buy-n-hold
  120. UPDATE tb_holdings
  121. SET shares = shares.bfill()
  122. CONTEXT BY portfolio_id, sec_id;
  123. // 记录每个组合最早的净值计算日期
  124. tb_port_first_cal_date = SELECT portfolio_id, start_cal_date.min() AS first_cal_date, end_cal_date.max() AS latest_cal_date, org_id[0] AS org_id
  125. FROM portfolio_info GROUP BY portfolio_id;
  126. // 计算各日期的持仓资产及总资产
  127. UPDATE tb_holdings
  128. SET market_value = (cumulative_nav * shares).round(6)
  129. FROM ej(tb_holdings, tb_port_first_cal_date, 'portfolio_id')
  130. WHERE org_id = '1';
  131. UPDATE tb_holdings
  132. SET market_value = (nav * shares).round(6)
  133. FROM ej(tb_holdings, tb_port_first_cal_date, 'portfolio_id')
  134. WHERE org_id = '2';
  135. UPDATE tb_holdings
  136. SET total_mkt_value = market_value.sum()
  137. CONTEXT BY portfolio_id, price_date;
  138. // 计算各持仓的权重
  139. UPDATE tb_holdings
  140. SET weight = (market_value \ total_mkt_value).round(6)
  141. WHERE total_mkt_value <> 0;
  142. // 组合收益计算: RET = ∑( weight_i * ret_i )
  143. tb_portfolio_ret = SELECT portfolio_id, price_date, (weight * ret).sum().round(7) AS ret
  144. FROM tb_holdings
  145. GROUP BY portfolio_id, price_date;
  146. // 取组合净值前值
  147. s_json = (SELECT portfolio_id, price_date.max() AS price_date
  148. FROM ej(tb_portfolio_ret, tb_port_first_cal_date, 'portfolio_id')
  149. WHERE tb_portfolio_ret.price_date < tb_port_first_cal_date.first_cal_date
  150. GROUP BY portfolio_id).toStdJson();
  151. tb_pre_nav = get_portfolio_nav_by_date(s_json, true);
  152. tb_portfolio_ret.addColumn('nav', DOUBLE);
  153. // start_cal_date 是最早净值日期
  154. UPDATE tb_portfolio_ret
  155. SET nav = 1, ret = 0
  156. FROM ej(tb_portfolio_ret, ej(tb_port_first_cal_date, tb_pre_nav, 'portfolio_id'), ['portfolio_id', 'price_date'], ['portfolio_id', 'first_cal_date'])
  157. WHERE tb_pre_nav.cumulative_nav IS NULL;
  158. // start_cal_date 是最早净值日期,用它作为初始净值日期
  159. UPDATE tb_pre_nav
  160. SET price_date = first_cal_date, cumulative_nav = 1
  161. FROM ej(tb_pre_nav, tb_port_first_cal_date, 'portfolio_id')
  162. WHERE cumulative_nav IS NULL;
  163. tb_portfolio_ret.sortBy!(['portfolio_id', 'price_date'], [1, 1]);
  164. // 通过收益反算净值: nav_i = nav_0 * ∏(1 + ret_i)
  165. UPDATE tb_portfolio_ret
  166. SET nav = (tb_pre_nav.cumulative_nav * (1+ret).cumprod()).round(6)
  167. FROM ej(tb_portfolio_ret, tb_pre_nav, 'portfolio_id')
  168. CONTEXT BY portfolio_id;
  169. // 返回有用的数据
  170. return (SELECT DISTINCT tb_portfolio_ret.*
  171. FROM ej(tb_portfolio_ret, tb_port_first_cal_date, 'portfolio_id')
  172. WHERE price_date >= first_cal_date AND price_date <= latest_cal_date
  173. ORDER BY portfolio_id, price_date);
  174. }