module fundit::rbsaCalculator
use fundit::sqlUtilities;
use fundit::performanceDataPuller;
use fundit::operationDataPuller;
/*
* RBSA 计算
* @param: ret
: historical return (double) vector which contains the same number of return as index
* index_ret : historical index return table which each row is an index
* is_long : true - long-only, false - long-short
* @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
*
*/
defg cal_rbsa(ret, index_ret, is_long) {
// 窗口长度
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(is_long == true, 0, -2))
// 上限
ub=(ret join 1) join array(float, n, n*10, iif(is_long == true, 1, 2))
res = osqp( q, P, A, lb, ub)
return res
}
/*
* 滚动 rbsa
* @param ret : return table, at least with "effective_date" and "ret" as columns
* @param index_ret : index return table, with "effective_date" and all index ids as columns
* @param is_long : boolean. true means weightings could be negative values
* @param window : number of return in a window
* @param step : rolling step
*
* TODO: use rolling()
*
* @return with "effective_date", "index_id" and "weights" columns
*/
def cal_rolling_rbsa(ret, index_ret, is_long, window, step) {
// 找到所有指数全有数据的最早日期
v_start_date = EXEC effective_date.max() AS start_date
FROM (SELECT entity_id, effective_date.min() AS effective_date FROM index_ret WHERE ret IS NOT NULL GROUP BY entity_id);
m_index_ret = SELECT ret FROM index_ret WHERE effective_date >= v_start_date PIVOT BY effective_date, entity_id;
t = SELECT * FROM ej(ret, m_index_ret, 'effective_date') ORDER BY ret.effective_date;
t.nullFill!(0)
// not sure why this doesn't work
// rolling(cal_rbsa{,,is_long}, (t.ret, t.slice(, ret.cols():).matrix()), window, step)
// 指数个数
n = m_index_ret.cols() - 1
// 计算起始位置
i = (t.size() - window) % step
// 运行rbsa计算次数
cnt = (t.size() - i - window) / step + 1;
tb = table(max(cnt,1):0, ["effective_date", "price_date", "index_id", "weights", "alpha", "r2", "adj_r2"], [STRING, DATE, STRING, DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
if(t.size() >= max(window, step) && cnt > 0) {
do {
alpha = 0;
r2 = 0;
adj_r2 = 0;
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, is_long);
if(res[0] == 'solved') {
m_predict_ret = t_index_ret.matrix() ** res[1][0:n];
alpha = v_ret.mean() - m_predict_ret.mean();
SSR = sum2(m_predict_ret - v_ret.mean());
SST = sum2(v_ret - v_ret.mean());
if(SST == 0) {
// 当SST=0, 先计算SSE再计算SST
SSE = sum2(v_ret - m_predict_ret);
SST = SSE + SSR;
}
if(SST != 0) {
r2 = SSR/SST;
adj_r2 = 1 - (1 - r2) * (window - 1) / (window - n - 1);
}
for(j in 1..n) {
tb.tableInsert(t.effective_date[i+window-1], t.price_date[i+window-1], m_index_ret.colNames()[j], res[1][j-1].round(4), alpha, r2, adj_r2);
}
}
// 往前推进step个收益
i = i + step
cnt -= 1
} while( cnt > 0)
}
return tb
}
/*
* 计算单基金或组合的RBSA
*
* @param entity_type : 目标基金/组合的类型
* @param entity_id : 目标基金/组合的ID
* @param index_ids : 基准指数IDs
* @param freq : m, w, d
* @param start_day
* @param end_day
* @param is_long : 是否只考虑纯多头
* @param window : 窗口(必须多于基准指数个数)
* @param step : 步长
*
* @return : entity_id, effective_date, price_date, index_id, weights, alternative_id, level, alpha, r2, adj_r2
*
* TODO: 数字与界面和数据库都对不上
*
* Example: cal_single_entity_RBSA('MF', 'MF00003PW1', ['IN00000008', 'IN00000077', 'IN0000007G', 'IN0000009M'], 'w', 1900.01.01, 2024.11.15, true, 24, 24);
* cal_single_entity_RBSA('PF', 166002, ['FA00000VML', 'FA00000VMM', 'FA00000VMN', 'FA00000VMO', 'IN0000007G'], 'w', 2020.01.01, 2024.11.08, true, 24, 24);
* cal_single_entity_RBSA('MF', 'MF000200KQ', ['IN00000008', 'IN00000077', 'IN0000007G', 'IN0000009M'], 'w', 1900.01.01, 2024.11.16, true, 24, 24);
*/
def cal_single_entity_RBSA(entity_type, entity_id, index_ids, freq='w', start_day=1900.01.01, end_day=2099.12.31, is_long=true, window=24, step=24) {
// entity_type='MF'
// entity_id= 'MF00003PW1'
// index_ids=['IN00000008', 'IN00000077', 'IN0000007G', 'IN0000009M']
// freq='w'
// start_day=2001.01.19
// end_day=2024.11.16
// is_long=true
// window=48
// step=13
tb_result = table(100:0, ["entity_id", "effective_date", "index_id", "weights", "alpha", "r2", "adj_r2"],
[iif(entity_type=='PF', INT, STRING), STRING, STRING, DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
v_entity = array(iif(entity_type=='PF', INT, STRING));
v_entity.append!(entity_id);
entity_ret = get_entity_return(entity_type, v_entity, freq, start_day, end_day, true);
// 数据长度不够,按照顺序依次分别用母基金(4), 指数(3)的数据来代替 (level=2, 基金经理的数据在Java里好像没用?)
level = 1
alternative_id = NULL;
if(entity_ret.isVoid() || entity_ret.size() < window) {
if(entity_type IN ['MF', 'HF']) {
fund_info = get_fund_info(v_entity);
p_fund_id = fund_info.p_fund_id[0];
primary_benchmark_id = fund_info.benchmark_id[0];
if(p_fund_id != NULL) {
entity_ret = get_entity_return(entity_type, v_entity.replace(entity_id, p_fund_id) , freq, start_day, end_day, true);
alternative_id = p_fund_id;
level = 4;
} else if(primary_benchmark_id != NULL) {
entity_ret = get_entity_return(entity_type, v_entity.replace(entity_id, primary_benchmark_id) , freq, start_day, end_day, true);
alternative_id = primary_benchmark_id;
level = 3;
} else {
return tb_result;
}
} else if(entity_type == 'PF'){
portfolio_info = get_portfolio_info(v_entity);
primary_benchmark_id = portfolio_info.benchmark_id[0];
if(primary_benchmark_id != NULL) {
entity_ret = get_entity_return(entity_type, v_entity.replace(entity_id, primary_benchmark_id) , freq, start_day, end_day, true);
alternative_id = primary_benchmark_id;
level = 3;
} else
return tb_result;
}
}
// 因为用来做基准指数的可能是指数、因子、基金等等任何时间序列数据,所以不用填 entity_type
index_ret = get_entity_return(NULL, index_ids, freq, start_day, end_day, true);
if(index_ret.isVoid() || index_ret.size() == 0) return tb_result;
tb_result = SELECT entity_id, effective_date, price_date, index_id, weights, alternative_id, level, alpha, r2, adj_r2
FROM cal_rolling_rbsa(entity_ret, index_ret, is_long, window, step);
return tb_result;
}
/*
* 计算单基金或组合的RBSA
*
* @param entity_type : MF, HF, PF
* @param entity_ret : [COLUMNS] entity_id, price_date, ret
* @param index_ret : [COLUMNS] entity_id, price_date, ret
* @param freq : m, w, d
* @param start_day
* @param end_day
* @param is_long : 是否只考虑纯多头
* @param window : 窗口(必须多于基准指数个数)
* @param step : 步长
*
* @return : entity_id, effective_date, price_date, index_id, weights, alternative_id, level, alpha, r2, adj_r2
*
* TODO: 数字与界面和数据库都对不上
*
*/
def cal_entity_RBSA(entity_type, entity_ret, index_ret, freq='w', start_day=1900.01.01, end_day=2099.12.31, is_long=true, window=24, step=24) {
// entity_type='MF'
// freq='w'
// start_day=2024.01.05
// end_day=today()
// is_long=true
// window=48
// step=13
tb_result = table(100:0, ["entity_id", "effective_date", "index_id", "weights", "alpha", "r2", "adj_r2"],
[iif(entity_type=='PF', INT, STRING), STRING, STRING, DOUBLE, DOUBLE, DOUBLE, DOUBLE]);
if(index_ret.isVoid() || index_ret.size() == 0) return tb_result;
tb_entity_ret = entity_ret;
// 数据长度不够,按照顺序依次分别用母基金(4), 指数(3)的数据来代替 (level=2, 基金经理的数据在Java里好像没用?)
level = 1
alternative_id = NULL;
if(tb_entity_ret.isVoid() || tb_entity_ret.size() < window) {
if(entity_type IN ['MF', 'HF']) {
fund_info = get_fund_info(tb_entity_ret.entity_id);
p_fund_id = fund_info.p_fund_id;
primary_benchmark_id = fund_info.benchmark_id;
if(p_fund_id != NULL) {
tb_entity_ret = SELECT entity_id, price_date, ret FROM get_entity_return(entity_type, p_fund_id , freq, start_day, end_day, true);
alternative_id = p_fund_id[0];
level = 4;
} else if(primary_benchmark_id != NULL) {
tb_entity_ret = SELECT entity_id, price_date, ret FROM get_entity_return(entity_type, primary_benchmark_id, freq, start_day, end_day, true);
alternative_id = primary_benchmark_id[0];
level = 3;
} else {
return tb_result;
}
}
}
tb_entity_ret.addColumn('effective_date', STRING);
tb_index_ret = index_ret;
tb_index_ret.addColumn('effective_date', STRING);
if(freq == 'm') {
UPDATE tb_entity_ret SET effective_date = price_date.temporalFormat('yyyy-MM');
UPDATE tb_index_ret SET effective_date = price_date.temporalFormat('yyyy-MM');
} else if(freq == 'w') {
UPDATE tb_entity_ret SET effective_date = get_year_week(price_date);
UPDATE tb_index_ret SET effective_date = get_year_week(price_date);
} else {
UPDATE tb_entity_ret SET effective_date = price_date$STRING;
UPDATE tb_index_ret SET effective_date = price_date$STRING;
}
tb_result = SELECT entity_ret.entity_id[0] AS entity_id, effective_date, price_date, index_id, weights, alternative_id, level, alpha, r2, adj_r2
FROM cal_rolling_rbsa(tb_entity_ret, tb_index_ret, is_long, window, step);
return tb_result;
}