123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- module fundit::fundCalculator
- /*
- * RBSA 计算
- * @param: ret: historical return (double) vector which contains the same number of return as index
- * index_ret: historical index returen matrix which each row is an index
- * isLongShort: bool
- * @return: table
- *
- * Create 20240703 模仿python代码在Dolphin中实现,具体计算逻辑完全不懂 Joey
- * 原代码见: http://gogs.fundit.cn/FundIt/FinanceCalcPython/src/dev36/pf_scical/v1/calc_rbsa_use_osqp.py
- * Python官方示例见:https://osqp.org/docs/examples/least-squares.html
- * Dolphin官方示例见:https://docs.dolphindb.cn/zh/funcs/o/osqp.html
- * 取数据脚本:SELECT fund_id, GROUP_CONCAT(ret_1w SEPARATOR ' ')
- * FROM mfdb.`fund_performance_weekly`
- * WHERE fund_id IN ( 'MF00003PW1', 'IN00000008', 'IN00000077', 'IN0000009M', 'IN0000007G')
- * AND year_week BETWEEN 202411 AND 202422
- * GROUP BY fund_id
- *
- */
- def cal_rbsa(ret, index_ret, isLongShort) {
- // 窗口长度
- m = ret.size()
- // 指数个数
- n = index_ret.cols()
-
- P0 = matrix(float, n, m+n)
- P1 = concatMatrix([matrix(float, m, n), eye(m)])
- P = concatMatrix([P0, P1], false)
- q = array(float, m+n, (m+n)*10, 0)
-
- A0 = concatMatrix( [matrix(index_ret), -eye(m)])
- A1 = concatMatrix( [matrix(take(1, n)).transpose(), matrix(float, 1, m)])
- A2 = concatMatrix( [eye(n), matrix(float, n, m)])
- A = concatMatrix( [A0, A1, A2], false)
-
- // join 1 是为了限制所有权重加总为100%
- // 下限
- lb =(ret join 1) join array(float, n, n*10, iif(isLongShort == false, 0, -2))
- // 上限
- ub=(ret join 1) join array(float, n, n*10, iif(isLongShort == false, 1, 2))
-
- res = osqp( q, P, A, lb, ub)
-
- return res
- }
- /*
- * 滚动 rbsa
- * @param: ret: return table, at least with "date" and "ret" as columns
- * index_ret: index return pivot table, with "date" and all index ids as columns
- * isLongShort: boolean. true means weightings could be negative values
- * window: number of return in a window
- * step: rolling step
- * @return: table, with "date", "status" and "weights" columns. "weights" contains space-delimited numbers
- */
- def cal_rolling_rbsa(ret, index_ret, isLongShort, window, step) {
- t = SELECT *
- FROM ret INNER JOIN index_ret ON ret.date = index_ret.date
- ORDER BY ret.date
- t.nullFill!(0)
- // 指数个数
- n = index_ret.cols() - 1
-
- // 计算起始位置
- i = t.size() % step
- // 运行rbsa计算次数
- cnt = (t.size() - i) / step
-
- tb = table(max(cnt,1):0, ["date", "status", "weights"], [STRING, STRING, STRING])
- if(t.size() >= window && cnt > 0) {
- do {
-
- v_ret = t.ret[i:(i+window)]
-
- t_index_ret = t.slice( i:(i+window), ret.cols(): )
-
- // 传入window个收益
- res = cal_rbsa(v_ret, t_index_ret, isLongShort)
-
- // 取窗口内最后(新)的日期
- tb.tableInsert(t.date[i+window-1], res[0], res[1][0:n].round(6).concat(" ")$STRING)
-
- // 往前推进step个收益
- i = i + step
-
- cnt -= 1
-
- } while( cnt > 0)
-
- } else {
-
- tb.tableInsert(null, "error", "The number of joined returns must not be less than window size.")
- }
- return tb
- }
- /*
- * 年化收益率计算(只支持月收益)
- *
- * date_rets 是个日期排正序的收益率表
- */
- def cal_ret_annualized(date_rets, isGIPS) {
- date_format = "y-M"
-
- t = SELECT fund_id, (1 + ret).prod() - 1 AS ret_a, end_date.max().datetimeParse(date_format) - end_date.min().datetimeParse(date_format) AS date_diff
- FROM date_rets
- GROUP BY fund_id
-
- // GIPS 规则是不够一年不年化
- if(isGIPS == true) {
-
- UPDATE t SET ret_a = (1 + ret_a).pow(12 \ date_diff) - 1
- WHERE date_diff > 12
- } else {
-
- UPDATE t SET ret_a = (1 + ret_a).pow(12 \ date_diff) - 1
- }
- return t
-
- }
|