|
@@ -0,0 +1,174 @@
|
|
|
+module fundit::navCalculator
|
|
|
+
|
|
|
+use fundit::dataPuller
|
|
|
+
|
|
|
+/*
|
|
|
+ * 转交易表为交易日的持仓截面表
|
|
|
+ * NOTE: 假定所有基金证券都是T+1买入,也就是第一天没有收益
|
|
|
+ *
|
|
|
+ *
|
|
|
+ */
|
|
|
+def convert_transaction_to_snapshot(portfolio_ids) {
|
|
|
+
|
|
|
+ // 取数据库中的持仓交易表
|
|
|
+ tb_transaction = get_portfolio_holding_history(portfolio_ids);
|
|
|
+
|
|
|
+ // 所有交易日期
|
|
|
+ tb_date = SELECT DISTINCT portfolio_id, holding_date FROM tb_transaction;
|
|
|
+
|
|
|
+ // 所有基金证券id
|
|
|
+ tb_id = SELECT DISTINCT portfolio_id, fund_id FROM tb_transaction;
|
|
|
+
|
|
|
+ tmp = SELECT tb_date.portfolio_id, tb_date.holding_date, tb_id.fund_id FROM fj(tb_date, tb_id, 'portfolio_id');
|
|
|
+
|
|
|
+ // 取各交易日期时的持仓截面, Window Join 的上限设成-1d 是因为买入基金当日无收益,所以计算份额时要排除掉
|
|
|
+ tb = wj(tmp, tb_transaction.sortBy!('holding_date'), duration('-50y'):duration('-1d'), <[t.fund_share.sum() AS shares]>, ['portfolio_id', 'fund_id', 'holding_date']);
|
|
|
+
|
|
|
+ tb.addColumn('nav', DOUBLE);
|
|
|
+ // 买入的基金份额记为0, 保留原始买入净值
|
|
|
+ UPDATE tb
|
|
|
+ SET shares = 0, nav = tb_transaction.nav
|
|
|
+ FROM ej(tb, tb_transaction, ['portfolio_id', 'holding_date', 'fund_id'],, isNull(tb.shares));
|
|
|
+
|
|
|
+ // 删除没用的数据;防一手脏数据
|
|
|
+ DELETE FROM tb WHERE shares IS NULL OR shares.round(0) < 0;
|
|
|
+
|
|
|
+ // 补上个虚拟的未来截面,以免buy-n-hold的证券信息损失;用0当NAV也是没办法,DolphinDB不能SELECT出个全NULL的列
|
|
|
+ INSERT INTO tb
|
|
|
+ SELECT portfolio_id, today()+1, fund_id, fund_share.sum(), 0
|
|
|
+ FROM tb_transaction
|
|
|
+ GROUP BY portfolio_id, fund_id
|
|
|
+ HAVING fund_share.sum().round(0) > 0;
|
|
|
+
|
|
|
+ return tb;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+ * 计算FOF类组合收益
|
|
|
+ *
|
|
|
+ *
|
|
|
+ * Create: 20240908 用于代替 sp_cal_portfolio_nav Joey
|
|
|
+ * TODO: 当用户手工输入同一基金同一时间的净值不同时,暂时用平均值代替(这会导致那个时间点附近的收益计算错误!)
|
|
|
+ *
|
|
|
+ * @param portfolio_ids <STRING>: 逗号分隔的组合ID
|
|
|
+ * @param start_date <DATE>: 持仓证券净值更新的起始日期
|
|
|
+ * @param cal_method <INT>: 净值使用方法:1-依赖累计净值,但份额数不是真实的、2:依赖单位净值,份额数是真实的
|
|
|
+ *
|
|
|
+ *
|
|
|
+ */
|
|
|
+ def cal_portfolio_return(portfolio_ids, start_date, cal_method) {
|
|
|
+
|
|
|
+ // 取持仓截面
|
|
|
+ tb_snapshot = convert_transaction_to_snapshot(portfolio_ids).rename!('fund_id', 'sec_id');;
|
|
|
+
|
|
|
+
|
|
|
+ // 取涉及到的所有基金证券最早持仓日期
|
|
|
+ s_json = (SELECT sec_id, holding_date.min() AS price_date FROM tb_snapshot GROUP BY sec_id).toStdJson();
|
|
|
+
|
|
|
+ // 取涉及到的所有基金证券有用净值
|
|
|
+ // TODO: need consider inception date nav
|
|
|
+ tb_nav = get_holding_nav(s_json);
|
|
|
+
|
|
|
+ // Buggy DolphinDB, INSERT INTO Table1 (Columns) SELECT Columns FROM Table2 会报列数不匹配的奇葩错误
|
|
|
+ tb_dup = SELECT sec_id, holding_date, nav.mean().round(6) AS nav
|
|
|
+ FROM tb_snapshot
|
|
|
+ WHERE NOT EXISTS ( SELECT 1 FROM tb_nav WHERE sec_id = tb_snapshot.sec_id AND price_date = tb_snapshot.holding_date )
|
|
|
+ GROUP BY sec_id, holding_date;
|
|
|
+
|
|
|
+ // 为了把不数据库里不存在的nav记录填空,不得不先做个pivot;然后才能正确计算ratios (ret_1m)
|
|
|
+ if ( cal_method == 1) {
|
|
|
+
|
|
|
+ // 补一下用户手工输入的交净值
|
|
|
+ // this is the way to get around the problem mentioned above
|
|
|
+ INSERT INTO tb_nav (sec_id, price_date, cumulative_nav) VALUES (tb_dup.sec_id, tb_dup.holding_date, tb_dup.nav);
|
|
|
+
|
|
|
+ // 统一叫NAV,省得麻烦
|
|
|
+ tmp_nav = (SELECT cumulative_nav AS nav FROM tb_nav PIVOT BY price_date, sec_id).ffill!();
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ // 补一下用户手工输入的交净值
|
|
|
+ INSERT INTO tb_nav (sec_id, price_date, nav) VALUES (tb_dup.sec_id, tb_dup.holding_date, tb_dup.nav);
|
|
|
+
|
|
|
+ tmp_nav = (SELECT nav FROM tb_nav PIVOT BY price_date, sec_id).ffill!();
|
|
|
+ }
|
|
|
+ // 计算每期收益
|
|
|
+ tmp_rets = tmp_nav.ratios()-1;
|
|
|
+
|
|
|
+ // 取被pivot掉的fund_Ids
|
|
|
+ v_col_name = tmp_rets.columnNames()[1:]
|
|
|
+
|
|
|
+ // 横表变回竖表
|
|
|
+ tb_navs = tmp_nav.unpivot("price_date", v_col_name).rename!("valueType" "value", "sec_id" "nav");
|
|
|
+ tb_rets = tmp_rets.unpivot("price_date", v_col_name).rename!("valueType" "value", "sec_id" "ret");
|
|
|
+
|
|
|
+ // 清一下内存
|
|
|
+ tmp_rets = null;
|
|
|
+ tmp_nav = null;
|
|
|
+ tb_nav = null;
|
|
|
+
|
|
|
+ // 清掉没用的空数据
|
|
|
+ DELETE FROM tb_navs WHERE nav IS NULL;
|
|
|
+ DELETE FROM tb_rets WHERE ret IS NULL;
|
|
|
+
|
|
|
+ // 在各证券持仓时段中,填充所有无净值的但其它证券有净值的合理日期
|
|
|
+ // 比如 2024-01-10 ~ 2024-01-20区间,组合持有基金A和基金B,基金A有每日净值
|
|
|
+ // 而基金B只有01-12和01-19两期周五净值,那么基金B需要填充除这两天以外的所有日期
|
|
|
+ tb_id_oldest_date = SELECT portfolio_id, sec_id, holding_date.min() AS oldest_date FROM tb_snapshot GROUP BY portfolio_id, sec_id;
|
|
|
+
|
|
|
+ tb_holdings = SELECT id.portfolio_id, r.price_date, id.sec_id, r.ret
|
|
|
+ FROM tb_id_oldest_date id
|
|
|
+ INNER JOIN tb_rets r ON id.sec_id = r.sec_id
|
|
|
+ WHERE r.price_date >= id.oldest_date
|
|
|
+ ORDER BY id.portfolio_id, r.price_date, id.sec_id;
|
|
|
+
|
|
|
+ tb_holdings.addColumn(['nav', 'shares', 'market_value', 'total_mkt_value', 'weight'], [DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
|
|
|
+
|
|
|
+ // 更新各持仓日期的基金净值
|
|
|
+ UPDATE tb_holdings pr
|
|
|
+ SET nav = tb_navs.nav
|
|
|
+ FROM ej(tb_holdings, tb_navs, ['sec_id', 'price_date']);
|
|
|
+
|
|
|
+ // 把交易日截面的份额数用于组合收益表
|
|
|
+ UPDATE tb_holdings
|
|
|
+ SET shares = ss.shares
|
|
|
+ FROM ej(tb_holdings AS pr, tb_snapshot AS ss, ['portfolio_id', 'price_date', 'sec_id'], ['portfolio_id', 'holding_date', 'sec_id']);
|
|
|
+
|
|
|
+ // 填充份额数为空的无交易日期,这段时间所有证券基金处于 buy-n-hold
|
|
|
+ UPDATE tb_holdings
|
|
|
+ SET shares = shares.bfill()
|
|
|
+ CONTEXT BY portfolio_id, sec_id;
|
|
|
+
|
|
|
+ // 计算各日期的持仓资产及总资产
|
|
|
+ UPDATE tb_holdings
|
|
|
+ SET market_value = (nav * shares).round(6);
|
|
|
+
|
|
|
+ UPDATE tb_holdings
|
|
|
+ SET total_mkt_value = market_value.sum()
|
|
|
+ CONTEXT BY portfolio_id, price_date;
|
|
|
+
|
|
|
+ // 计算各持仓的权重
|
|
|
+ UPDATE tb_holdings
|
|
|
+ SET weight = (market_value \ total_mkt_value).round(6)
|
|
|
+ WHERE total_mkt_value <> 0;
|
|
|
+
|
|
|
+ // 组合收益计算: RET = ∑( weight_i * ret_i )
|
|
|
+ tb_portfolio_ret = SELECT portfolio_id, price_date, (weight * ret).sum().round(6) AS ret
|
|
|
+ FROM tb_holdings
|
|
|
+ GROUP BY portfolio_id, price_date;
|
|
|
+
|
|
|
+ // 初始化净值
|
|
|
+ tb_portfolio_ret.addColumn('nav', DOUBLE);
|
|
|
+ UPDATE tb_portfolio_ret SET nav = 1 WHERE ret IS NULL;
|
|
|
+
|
|
|
+ // 通过收益反算净值: nav_i = nav_0 * ∏(1 + ret_i)
|
|
|
+ UPDATE tb_portfolio_ret SET nav = (1+ret).cumprod() WHERE nav IS NULL CONTEXT BY portfolio_id;
|
|
|
+
|
|
|
+ // 删掉没有用的数据
|
|
|
+ DELETE FROM tb_portfolio_ret WHERE price_date > today();
|
|
|
+
|
|
|
+ // 尤其是私募基金,有很多冗余
|
|
|
+
|
|
|
+ return tb_portfolio_ret;
|
|
|
+ }
|