|
@@ -3,57 +3,173 @@ use fundit::fundCalculator
|
|
use fundit::dataPuller
|
|
use fundit::dataPuller
|
|
|
|
|
|
/*
|
|
/*
|
|
- * 批量计算私募基金收益
|
|
|
|
- *
|
|
|
|
- *
|
|
|
|
- * cal_hedge_fund_returns("'HF000004KN','HF00018WXG','HF000103EU'", true)
|
|
|
|
- *
|
|
|
|
|
|
+ * 根据私募基金净值序列计算月收益序列(适合提供给指标运算)
|
|
|
|
+ *
|
|
|
|
+ * Create: 20240907 Joey
|
|
|
|
+ * TODO: missing pulling data from local
|
|
|
|
+ * TODO: ONLY support month return now
|
|
|
|
+ *
|
|
|
|
+ * @param fund_ids: 基金ID STRING VECTOR
|
|
|
|
+ * @param isFromMySQL: 净值来源 1 - 远程MySQL、 0 - 本地 DolphinDB
|
|
|
|
+ *
|
|
*/
|
|
*/
|
|
-def cal_hedge_fund_returns(fund_ids, isFromMySQL) {
|
|
|
|
|
|
+def cal_hedge_fund_returns(fund_ids, isFromMySQL){
|
|
|
|
|
|
|
|
+ tb_rets = null;
|
|
|
|
+
|
|
// 用于保证老基金也能取到所有历史净值
|
|
// 用于保证老基金也能取到所有历史净值
|
|
- very_old_price_date = 1990.01.01
|
|
|
|
-
|
|
|
|
- // 基金基本信息,包括初始净值
|
|
|
|
- tb_fund_info = get_fund_info(fund_ids)
|
|
|
|
|
|
+ very_old_price_date = 1990.01.01;
|
|
|
|
+
|
|
|
|
+ if(isFromMySQL){
|
|
|
|
+
|
|
|
|
+ // 基金基本信息,包括初始净值
|
|
|
|
+ tb_fund_info = get_fund_info(fund_ids);
|
|
|
|
+
|
|
|
|
+ // 基金净值
|
|
|
|
+ tb_nav = SELECT * FROM get_hedge_fund_nav_by_price_date(fund_ids, very_old_price_date, true);
|
|
|
|
+
|
|
|
|
+ tb_month_end = table(100:0, ['fund_id', 'price_date'], [STRING, DATE]);
|
|
|
|
+ // 填充好各基金有效期内所有月份的最后一天
|
|
|
|
+ for( f in tb_fund_info )
|
|
|
|
+ {
|
|
|
|
+ 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)) ;
|
|
|
|
+ }
|
|
|
|
|
|
- // 基金净值
|
|
|
|
- tb_nav = get_hedge_fund_nav_by_price_date(fund_ids, very_old_price_date, isFromMySQL)
|
|
|
|
|
|
+ UPDATE tb_month_end SET end_date = price_date.month();
|
|
|
|
|
|
|
|
+ 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
|
|
|
|
+ FROM tb_nav
|
|
|
|
+ GROUP BY fund_id, monthEnd(price_date);
|
|
|
|
+
|
|
|
|
+ // 完整月末日期的净值序列(包括缺失数据为NULL)
|
|
|
|
+ tb_monthly_nav = SELECT me.fund_id, me.end_date, n.price_date, n.cumulative_nav
|
|
|
|
+ FROM tb_month_end me
|
|
|
|
+ LEFT JOIN tb_monthly_nav n ON me.fund_id = n.fund_id AND me.end_date = n.end_date
|
|
|
|
+ ORDER BY me.fund_id, me.end_date;
|
|
|
|
+
|
|
|
|
+ // 补一下成立日的初始净值
|
|
|
|
+ // NOTE: DolphinDB 遇见 EXISTS 语句时,似乎主表的 alias 失效,只好用全名
|
|
|
|
+ INSERT INTO tb_monthly_nav
|
|
|
|
+ SELECT fund_id, inception_date.month(), inception_date, ifNull(ini_value, 1)
|
|
|
|
+ FROM tb_fund_info fi
|
|
|
|
+ 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);
|
|
|
|
+
|
|
|
|
+ // 算 ratios 之前先把时间顺序排好
|
|
|
|
+ tb_monthly_nav.sortBy!(['fund_id', 'end_date', 'price_date'], [1, 1, 1]);
|
|
|
|
+
|
|
|
|
+ // 计算月收益
|
|
|
|
+ tb_rets = SELECT fund_id, end_date, price_date, cumulative_nav, cumulative_nav.ratios() - 1 AS ret
|
|
|
|
+ FROM tb_monthly_nav
|
|
|
|
+ CONTEXT BY fund_id;
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // the records without return calculated but do have nav are still useful for some calculations
|
|
|
|
+ return ( SELECT * FROM tb_rets WHERE cumulative_nav > 0 );
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/*
|
|
|
|
+ * 月末 fund_performance 表计算
|
|
|
|
+ *
|
|
|
|
+ * @param fund_ids: 逗号分隔的ID
|
|
|
|
+ * @param end_date:
|
|
|
|
+ *
|
|
|
|
+ * Example: cal_fund_performance("'HF000004KN','HF00018WXG','HF000103EU'", '2024-06', true);
|
|
|
|
+ */
|
|
|
|
+def cal_fund_performance(fund_ids, month_end) {
|
|
|
|
+
|
|
|
|
+ // 获取必要的基金月度净值
|
|
|
|
+ tb_nav = get_nav_for_hedge_fund_performance(fund_ids, month_end);
|
|
|
|
+
|
|
|
|
+ tb_rets = SELECT fund_id, price_date.month() AS end_date, price_date, cumulative_nav,
|
|
|
|
+ cumulative_nav \ nav_1m - 1 AS ret_1m,
|
|
|
|
+ cumulative_nav \ nav_3m - 1 AS ret_3m,
|
|
|
|
+ cumulative_nav \ nav_6m - 1 AS ret_6m,
|
|
|
|
+ cumulative_nav \ nav_1y - 1 AS ret_1y,
|
|
|
|
+ cumulative_nav \ nav_2y - 1 AS ret_2y,
|
|
|
|
+ cumulative_nav \ nav_3y - 1 AS ret_3y,
|
|
|
|
+ cumulative_nav \ nav_4y - 1 AS ret_4y,
|
|
|
|
+ cumulative_nav \ nav_5y - 1 AS ret_5y,
|
|
|
|
+ cumulative_nav \ nav_10y - 1 AS ret_10y,
|
|
|
|
+ cumulative_nav \ nav_ytd - 1 AS ret_ytd,
|
|
|
|
+ cumulative_nav \ nav_incep - 1 AS ret_incep, inception_date
|
|
|
|
+ FROM tb_nav;
|
|
|
|
+
|
|
|
|
+ // NOTE: this is to keep consistance with MySQL, even it is NOT complied with GIPS standard
|
|
|
|
+ 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,
|
|
|
|
+ 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,
|
|
|
|
+ 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,
|
|
|
|
+ ret_ytd_a = (1 + ret_ytd).pow(12\int(temporalFormat(end_date, 'MM')))-1,
|
|
|
|
+ ret_incep_a = (1 + ret_incep).pow(12\(end_date - inception_date.month())) - 1,
|
|
|
|
+ ret_incep_a_all = (1 + ret_incep).pow(12\(end_date - inception_date.month()))- 1,
|
|
|
|
+ ret_incep_a_gips = iif(end_date - inception_date.month() < 12, ret_incep,
|
|
|
|
+ (1 + ret_incep).pow(12\(end_date - inception_date.month()))- 1);
|
|
|
|
+
|
|
|
|
+ return tb_rets;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+/*
|
|
|
|
+ * 批量计算公募历史基金月度收益(fund_performance)
|
|
|
|
+ * NOTE: 任何数据频率快于或等于月度的净值数据都可以用此函数一次性计算完整历史记录。双月频、季频甚至更低频率的基金只能按月计算
|
|
|
|
+ *
|
|
|
|
+ * cal_mutual_fund_performance("'HF000004KN','HF00018WXG','HF000103EU'", true)
|
|
|
|
+ *
|
|
|
|
+ */
|
|
|
|
+def cal_mutual_fund_performance(fund_ids, isFromMySQL) {
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+/* 找到不必用pivot table 填充数据的办法了
|
|
|
|
+
|
|
// NOTE: mySQL currently uses calendar day, while the codes below takes business day. it might cause a few different numbers calcuated
|
|
// NOTE: mySQL currently uses calendar day, while the codes below takes business day. it might cause a few different numbers calcuated
|
|
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
|
|
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
|
|
FROM tb_nav
|
|
FROM tb_nav
|
|
- GROUP BY fund_id, businessMonthEnd(price_date)
|
|
|
|
|
|
+ GROUP BY fund_id, businessMonthEnd(price_date);
|
|
|
|
+
|
|
|
|
+ // 补一下成立日净值
|
|
|
|
+ // NOTE: DolphinDB 遇见 EXISTS 语句时,似乎主表的 alias 失效,只好用全名
|
|
|
|
+ INSERT INTO tb_monthly_nav
|
|
|
|
+ SELECT businessMonthEnd(inception_date), fund_id, inception_date.month(), inception_date, ifNull(ini_value, 1)
|
|
|
|
+ FROM tb_fund_info
|
|
|
|
+ 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);
|
|
|
|
|
|
// 为了把不数据库里不存在的nav记录填空,不得不先做个pivot;然后才能正确计算ratios (ret_1m)
|
|
// 为了把不数据库里不存在的nav记录填空,不得不先做个pivot;然后才能正确计算ratios (ret_1m)
|
|
|
|
+ // TODO: much better way is to have a "full list" of dates, then LEFT JOIN nav table, then ffill()
|
|
tb_rets_1m = (SELECT cumulative_nav FROM tb_monthly_nav PIVOT BY end_date, fund_id).ffill!().ratios()-1
|
|
tb_rets_1m = (SELECT cumulative_nav FROM tb_monthly_nav PIVOT BY end_date, fund_id).ffill!().ratios()-1
|
|
|
|
|
|
// 取被pivot掉的fund_Ids
|
|
// 取被pivot掉的fund_Ids
|
|
v_col_name = tb_rets_1m.columnNames()[1:]
|
|
v_col_name = tb_rets_1m.columnNames()[1:]
|
|
tb_tmp = tb_rets_1m.unpivot("end_date", v_col_name).rename!("valueType" "value", "fund_id" "ret_1m")
|
|
tb_tmp = tb_rets_1m.unpivot("end_date", v_col_name).rename!("valueType" "value", "fund_id" "ret_1m")
|
|
-
|
|
|
|
|
|
+*/
|
|
|
|
+
|
|
|
|
+ // 计算月收益
|
|
|
|
+ tb_tmp = cal_hedge_fund_returns(fund_ids, isFromMySQL);
|
|
|
|
+
|
|
tb_rets = SELECT fund_id, end_date, ret_1m,
|
|
tb_rets = SELECT fund_id, end_date, ret_1m,
|
|
(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,
|
|
(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,
|
|
(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,
|
|
(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,
|
|
(1 + ret_1m).mprod(60) - 1 AS ret_5y, (1 + ret_1m).mprod(120) - 1 AS ret_10y
|
|
(1 + ret_1m).mprod(60) - 1 AS ret_5y, (1 + ret_1m).mprod(120) - 1 AS ret_10y
|
|
FROM tb_tmp
|
|
FROM tb_tmp
|
|
- // WHERE ret_1m IS NOT NULL
|
|
|
|
- CONTEXT BY fund_id
|
|
|
|
-
|
|
|
|
|
|
+ CONTEXT BY fund_id;
|
|
|
|
+
|
|
|
|
+ // NOTE: this is to keep consistance with MySQL, even it is NOT complied with GIPS standard
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
- ret_5y_a = (1 + ret_5y).pow(1\5) - 1, ret_10y_a = (1 + ret_10y).pow(1\10) - 1
|
|
|
|
|
|
+ ret_5y_a = (1 + ret_5y).pow(1\5) - 1, ret_10y_a = (1 + ret_10y).pow(1\10) - 1;
|
|
|
|
|
|
// ytd 不会用上面的CONTEXT BY语句实现
|
|
// ytd 不会用上面的CONTEXT BY语句实现
|
|
- 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, (a.cumulative_nav \ b.cumulative_nav).pow(12\(a.end_date - b.end_date)) - 1 AS ret_ytd_a
|
|
|
|
- FROM tb_monthly_nav a
|
|
|
|
- INNER JOIN tb_monthly_nav b ON a.fund_Id = b.fund_id
|
|
|
|
|
|
+ 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,
|
|
|
|
+ (a.cumulative_nav \ b.cumulative_nav).pow(12\(a.end_date - b.end_date)) - 1 AS ret_ytd_a
|
|
|
|
+ FROM tb_rets a
|
|
|
|
+ INNER JOIN tb_rets b ON a.fund_Id = b.fund_id
|
|
AND b.end_date = a.price_date.yearEnd().datetimeAdd(-1y).month()
|
|
AND b.end_date = a.price_date.yearEnd().datetimeAdd(-1y).month()
|
|
|
|
|
|
// since inception 不会用上面的CONTEXT BY语句实现
|
|
// since inception 不会用上面的CONTEXT BY语句实现
|
|
tb_ret_incep = SELECT a.fund_id, a.end_date, a.price_date, cumulative_nav, -1 + cumulative_nav \ ini_value AS ret_incep
|
|
tb_ret_incep = SELECT a.fund_id, a.end_date, a.price_date, cumulative_nav, -1 + cumulative_nav \ ini_value AS ret_incep
|
|
- FROM tb_monthly_nav a
|
|
|
|
|
|
+ FROM tb_rets a
|
|
INNER JOIN tb_fund_info fi ON a.fund_id = fi.fund_id
|
|
INNER JOIN tb_fund_info fi ON a.fund_id = fi.fund_id
|
|
|
|
+
|
|
|
|
|
|
UPDATE tb_ret_incep SET ret_incep_a = (1 + ret_incep).pow(12\(end_date - end_date.first())) - 1 CONTEXT BY fund_Id
|
|
UPDATE tb_ret_incep SET ret_incep_a = (1 + ret_incep).pow(12\(end_date - end_date.first())) - 1 CONTEXT BY fund_Id
|
|
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
|
|
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
|