|
@@ -1,5 +1,8 @@
|
|
|
module fundit::indicatorCalculator
|
|
|
|
|
|
+use fundit::dataPuller
|
|
|
+use fundit::returnCalculator
|
|
|
+use fundit::navCalculator
|
|
|
/*
|
|
|
* Annulized multiple
|
|
|
*/
|
|
@@ -8,17 +11,17 @@ def get_annulization_multiple(freq) {
|
|
|
ret = 1;
|
|
|
|
|
|
if (freq == 'd') {
|
|
|
- ret = 252; // We have differences here between Java and DolphinDB, Java uses 365.25 days
|
|
|
+ ret = 252; // We have differences here between Java and DolphinDB, Java uses 365.25 days
|
|
|
} else if (freq == 'w') {
|
|
|
- ret = 52;
|
|
|
+ ret = 52;
|
|
|
} else if (freq == 'm') {
|
|
|
- ret = 12;
|
|
|
+ ret = 12;
|
|
|
} else if (freq == 'q') {
|
|
|
- ret = 4;
|
|
|
+ ret = 4;
|
|
|
} else if (freq == 's') {
|
|
|
- ret = 2;
|
|
|
+ ret = 2;
|
|
|
} else if (freq == 'a') {
|
|
|
- ret = 1;
|
|
|
+ ret = 1;
|
|
|
}
|
|
|
|
|
|
return ret;
|
|
@@ -38,21 +41,21 @@ def get_annulization_multiple(freq) {
|
|
|
*/
|
|
|
def cal_basic_performance(ret, freq) {
|
|
|
|
|
|
- t = SELECT entity_id, max(end_date) AS end_date, max(price_date) AS price_date, min(price_date) AS min_date,
|
|
|
- //(nav.last() \ nav.first() - 1).round(6) AS trailing_ret,
|
|
|
- ((1+ret).prod()-1).round(6) AS trailing_ret,
|
|
|
- iif(price_date.max().month()-price_date.min().month()>12,
|
|
|
- //(nav.last() \ nav.first()).pow(365 \(max(price_date) - min(price_date)))-1,
|
|
|
- //(nav.last() \ nav.first() - 1)).round(6) AS trailing_ret_a,
|
|
|
+ t = SELECT entity_id, max(end_date) AS end_date, max(price_date) AS price_date, min(price_date) AS min_date,
|
|
|
+ //(nav.last() \ nav.first() - 1).round(6) AS trailing_ret,
|
|
|
+ ((1+ret).prod()-1).round(6) AS trailing_ret,
|
|
|
+ iif(price_date.max().month()-price_date.min().month()>12,
|
|
|
+ //(nav.last() \ nav.first()).pow(365 \(max(price_date) - min(price_date)))-1,
|
|
|
+ //(nav.last() \ nav.first() - 1)).round(6) AS trailing_ret_a,
|
|
|
((1+ret).prod()-1) * sqrt(get_annulization_multiple(freq)),
|
|
|
- ((1+ret).prod()-1)).round(6) AS trailing_ret_a,
|
|
|
+ ((1+ret).prod()-1)).round(6) AS trailing_ret_a,
|
|
|
ret.std() AS std_dev,
|
|
|
ret.skew(false) AS skewness,
|
|
|
ret.kurtosis(false) - 3 AS kurtosis,
|
|
|
ret.min() AS wrst_month,
|
|
|
max( 1 - nav \ nav.cummax() ) AS drawdown
|
|
|
FROM ret
|
|
|
- GROUP BY entity_id;
|
|
|
+ GROUP BY entity_id;
|
|
|
|
|
|
// var & cvar require return NOT NULL
|
|
|
// NOTE: DolphinDB supports 4 different ways: normal, logNormal, historical, monteCarlo. we use historical
|
|
@@ -61,7 +64,7 @@ def cal_basic_performance(ret, freq) {
|
|
|
ret.CVaR('historical', 0.95) AS cvar
|
|
|
FROM ret
|
|
|
WHERE ret.ret > - 1
|
|
|
- GROUP BY entity_id;
|
|
|
+ GROUP BY entity_id;
|
|
|
|
|
|
return (SELECT * FROM t LEFT JOIN t1 ON t.entity_id = t1.entity_id AND t.end_date = t1.end_date AND t.price_date = t1.price_date);
|
|
|
|
|
@@ -74,8 +77,8 @@ def cal_basic_performance(ret, freq) {
|
|
|
*
|
|
|
*/
|
|
|
def cal_LPM(ret, risk_free_rate) {
|
|
|
-
|
|
|
- t = SELECT *, count(entity_id) AS cnt FROM ret WHERE ret > -1 CONTEXT BY entity_id;
|
|
|
+
|
|
|
+ t = SELECT *, count(entity_id) AS cnt FROM ret WHERE ret > -1 CONTEXT BY entity_id;
|
|
|
|
|
|
lpm = SELECT t.entity_id, max(t.end_date) AS end_date,
|
|
|
(sum (rfr.ret - t.ret) \ (t.cnt[0])).pow(1\1) AS lpm1,
|
|
@@ -111,7 +114,7 @@ def cal_omega_sortino_kappa(ret, risk_free_rate) {
|
|
|
INNER JOIN risk_free_rate rfr ON t.end_date = rfr.end_date
|
|
|
GROUP BY t.entity_id;
|
|
|
|
|
|
- return tb;
|
|
|
+ return tb;
|
|
|
}
|
|
|
|
|
|
|
|
@@ -121,7 +124,7 @@ def cal_omega_sortino_kappa(ret, risk_free_rate) {
|
|
|
*/
|
|
|
def cal_alpha_beta(ret, bmk_ret, risk_free) {
|
|
|
|
|
|
- t = SELECT t.entity_id, t.end_date, t.ret, bmk.ret AS ret_bmk
|
|
|
+ t = SELECT t.entity_id, t.end_date, t.ret, bmk.ret AS ret_bmk
|
|
|
FROM ret t
|
|
|
INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date
|
|
|
WHERE t.ret > -1
|
|
@@ -145,7 +148,7 @@ def cal_alpha_beta(ret, bmk_ret, risk_free) {
|
|
|
*/
|
|
|
def cal_benchmark_tracking(ret, bmk_ret) {
|
|
|
|
|
|
- t0 = SELECT t.entity_id, t.end_date, t.ret, bmk.ret AS ret_bmk, count(entity_id) AS cnt, (t.ret - bmk.ret) AS exc_ret
|
|
|
+ t0 = SELECT t.entity_id, t.end_date, t.ret, bmk.ret AS ret_bmk, count(entity_id) AS cnt, (t.ret - bmk.ret) AS exc_ret
|
|
|
FROM ret t
|
|
|
INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date
|
|
|
WHERE t.ret > -1
|
|
@@ -167,7 +170,7 @@ def cal_benchmark_tracking(ret, bmk_ret) {
|
|
|
*/
|
|
|
def cal_sharpe(ret, std_dev, risk_free_rate) {
|
|
|
|
|
|
- sharpe = SELECT t.entity_id, (t.ret - rfr.ret).mean() / std.std_dev[0] AS sharpe
|
|
|
+ sharpe = SELECT t.entity_id, (t.ret - rfr.ret).mean() / std.std_dev[0] AS sharpe
|
|
|
FROM ret t
|
|
|
INNER JOIN std_dev std ON t.entity_id = std.entity_id
|
|
|
INNER JOIN risk_free_rate rfr ON t.end_date = rfr.end_date
|
|
@@ -182,13 +185,13 @@ def cal_sharpe(ret, std_dev, risk_free_rate) {
|
|
|
*/
|
|
|
def cal_treynor(ret, risk_free_rate, beta) {
|
|
|
|
|
|
- t = SELECT *, count(entity_id) AS cnt
|
|
|
- FROM ret t
|
|
|
- INNER JOIN risk_free_rate rfr ON t.end_date = rfr.end_date
|
|
|
- WHERE t.ret > -1
|
|
|
- AND rfr.ret > -1
|
|
|
- CONTEXT BY t.entity_id;
|
|
|
-
|
|
|
+ t = SELECT *, count(entity_id) AS cnt
|
|
|
+ FROM ret t
|
|
|
+ INNER JOIN risk_free_rate rfr ON t.end_date = rfr.end_date
|
|
|
+ WHERE t.ret > -1
|
|
|
+ AND rfr.ret > -1
|
|
|
+ CONTEXT BY t.entity_id;
|
|
|
+
|
|
|
treynor = SELECT t.entity_id, ((1 + t.ret).prod().pow(12\iif(t.cnt[0]<12, 12, t.cnt[0])) - (1 + t.rfr_ret).prod().pow(12\iif(t.cnt[0]<12, 12, t.cnt[0]))) / beta.beta[0] AS treynor
|
|
|
FROM t
|
|
|
INNER JOIN beta AS beta ON t.entity_id = beta.entity_id
|
|
@@ -203,7 +206,7 @@ def cal_treynor(ret, risk_free_rate, beta) {
|
|
|
*/
|
|
|
def cal_jensen(ret, bmk_ret, risk_free_rate, beta) {
|
|
|
|
|
|
- jensen = SELECT t.entity_id, t.ret.mean() - rfr.ret.mean() - beta.beta[0] * (bmk.ret.mean() - rfr.ret.mean()) AS jensen
|
|
|
+ jensen = SELECT t.entity_id, t.ret.mean() - rfr.ret.mean() - beta.beta[0] * (bmk.ret.mean() - rfr.ret.mean()) AS jensen
|
|
|
FROM ret t
|
|
|
INNER JOIN bmk_ret bmk ON t.end_date = bmk.end_date
|
|
|
INNER JOIN risk_free_rate rfr ON t.end_date = rfr.end_date
|
|
@@ -220,7 +223,7 @@ def cal_jensen(ret, bmk_ret, risk_free_rate, beta) {
|
|
|
*/
|
|
|
def cal_calmar(ret_a){
|
|
|
|
|
|
- calmar = SELECT entity_id, trailing_ret_a \ drawdown AS calmar
|
|
|
+ calmar = SELECT entity_id, trailing_ret_a \ drawdown AS calmar
|
|
|
FROM ret_a;
|
|
|
|
|
|
return calmar;
|
|
@@ -242,6 +245,28 @@ def cal_m2(ret, bmk_ret, risk_free_rate) {
|
|
|
return m2;
|
|
|
}
|
|
|
|
|
|
+/*
|
|
|
+ * Morningstar Return, Morningstar Risk-Adjusted Return
|
|
|
+ *
|
|
|
+ * TODO: Tax and loads are NOT taken care of
|
|
|
+ * TODO: Assume Chinese methodology using 3, 5, 10 as number of traling years
|
|
|
+ *
|
|
|
+ * NOTE: Morningstar methodology requires monthly return for calculation, so that "12" is hard-coded here
|
|
|
+ *
|
|
|
+ *
|
|
|
+ */
|
|
|
+def cal_ms_return(ret, risk_free_rate) {
|
|
|
+
|
|
|
+ r = SELECT t.entity_id, t.end_date.max() AS end_date, t.price_date.max() AS price_date, t.price_date.min() AS min_date,
|
|
|
+ ((1 + t.ret)\(1 + rfr.ret)).prod().pow(12\(t.end_date.max() - t.end_date.min()))-1 AS ms_ret_a,
|
|
|
+ (1 + t.ret).pow(-2).mean().pow(-12/2)-1 AS ms_rar_a
|
|
|
+ FROM ret t
|
|
|
+ INNER JOIN risk_free_rate rfr ON t.end_date = rfr.end_date
|
|
|
+ GROUP BY t.entity_id;
|
|
|
+
|
|
|
+ return r;
|
|
|
+}
|
|
|
+
|
|
|
|
|
|
/*
|
|
|
* Monthly Since_inception_date Indicator Calculation
|
|
@@ -326,6 +351,7 @@ def cal_indicators(mutable ret, index_ret, risk_free, freq) {
|
|
|
/*
|
|
|
* Calculate trailing 6m, ytd, 1y, 2y, 3y, 4y, 5y, 10y and since inception indicators
|
|
|
*
|
|
|
+ * @param: entity_info <TABLE>: basic information of entity, NEED COLUMNS entity_id, inception_date
|
|
|
* @param: ret <TABLE>: 收益表,NEED COLUMNS entity_id, price_dat, end_date, nav
|
|
|
* @param: end_day <DATE>: 计算截止日期
|
|
|
* @param index_ret <TABLE>: historical benchmark return table, NEED COLUMNS fund_id, end_date, ret
|
|
@@ -334,43 +360,151 @@ def cal_indicators(mutable ret, index_ret, risk_free, freq) {
|
|
|
|
|
|
*
|
|
|
*/
|
|
|
-def cal_all_trailing_indicators(mutable tb_ret, end_day, bmk_ret, risk_free_rate, freq) {
|
|
|
-
|
|
|
+def cal_all_trailing_indicators(entity_info, mutable tb_ret, end_day, bmk_ret, risk_free_rate, freq) {
|
|
|
+
|
|
|
+ r_incep = null;
|
|
|
+ r_ytd = null;
|
|
|
+ r_6m = null;
|
|
|
+ r_1y = null;
|
|
|
+ r_2y = null;
|
|
|
+ r_3y = null;
|
|
|
+ r_4y = null;
|
|
|
+ r_5y = null;
|
|
|
+ r_10y = null;
|
|
|
+ r_ms_3y = null;
|
|
|
+ r_ms_5y = null;
|
|
|
+ r_ms_10y = null;
|
|
|
+
|
|
|
// since inception
|
|
|
- r_incep = cal_indicators(tb_ret, bmk_ret, risk_free_rate, 'm');
|
|
|
-
|
|
|
+ if(tb_ret.size() > 0) {
|
|
|
+ r_incep = cal_indicators(tb_ret, bmk_ret, risk_free_rate, 'm');
|
|
|
+ }
|
|
|
+
|
|
|
// ytd
|
|
|
tb_ret_ytd = SELECT * FROM tb_ret WHERE end_date >= end_day.yearBegin().month();
|
|
|
- r_ytd = cal_indicators(tb_ret_ytd, bmk_ret, risk_free_rate, 'm');
|
|
|
-
|
|
|
+ if(tb_ret_ytd.size() > 0) {
|
|
|
+ r_ytd = cal_indicators(tb_ret_ytd, bmk_ret, risk_free_rate, 'm');
|
|
|
+ }
|
|
|
+
|
|
|
// trailing 6m
|
|
|
- tb_ret_6m = SELECT * FROM tb_ret WHERE end_date > end_day.month()-6;
|
|
|
- r_6m = cal_indicators(tb_ret_6m, bmk_ret, risk_free_rate, 'm');
|
|
|
+ tb_ret_6m = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
|
|
|
+ WHERE r.end_date > end_day.month()-6 AND (end_day.month() - ei.inception_date.month()) >= 6;
|
|
|
+ if(tb_ret_6m.size() > 0) {
|
|
|
+ r_6m = cal_indicators(tb_ret_6m, bmk_ret, risk_free_rate, 'm');
|
|
|
+ }
|
|
|
|
|
|
// trailing 1y
|
|
|
- tb_ret_1y = SELECT * FROM tb_ret WHERE end_date > end_day.month()-12;
|
|
|
- r_1y = cal_indicators(tb_ret_1y, bmk_ret, risk_free_rate, 'm');
|
|
|
+ tb_ret_1y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
|
|
|
+ WHERE r.end_date > end_day.month()-12 AND (end_day.month() - ei.inception_date.month()) >= 12;
|
|
|
+ if(tb_ret_1y.size() > 0) {
|
|
|
+ r_1y = cal_indicators(tb_ret_1y, bmk_ret, risk_free_rate, 'm');
|
|
|
+ }
|
|
|
|
|
|
// trailing 2y
|
|
|
- tb_ret_2y = SELECT * FROM tb_ret WHERE end_date > end_day.month()-24;
|
|
|
- r_2y = cal_indicators(tb_ret_2y, bmk_ret, risk_free_rate, 'm');
|
|
|
+ tb_ret_2y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
|
|
|
+ WHERE r.end_date > end_day.month()-24 AND (end_day.month() - ei.inception_date.month()) >= 24;
|
|
|
+ if(tb_ret_2y.size() > 0) {
|
|
|
+ r_2y = cal_indicators(tb_ret_2y, bmk_ret, risk_free_rate, 'm');
|
|
|
+ }
|
|
|
|
|
|
// trailing 3y
|
|
|
- tb_ret_3y = SELECT * FROM tb_ret WHERE end_date > end_day.month()-36;
|
|
|
- r_3y = cal_indicators(tb_ret_3y, bmk_ret, risk_free_rate, 'm');
|
|
|
-
|
|
|
+ tb_ret_3y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
|
|
|
+ WHERE r.end_date > end_day.month()-36 AND (end_day.month() - ei.inception_date.month()) >= 36;
|
|
|
+ if(tb_ret_3y.size() > 0) {
|
|
|
+ r_3y = cal_indicators(tb_ret_3y, bmk_ret, risk_free_rate, 'm');
|
|
|
+ r_ms_3y = cal_ms_return(tb_ret_3y, risk_free_rate);
|
|
|
+ }
|
|
|
+
|
|
|
// trailing 4y
|
|
|
- tb_ret_4y = SELECT * FROM tb_ret WHERE end_date > end_day.month()-48;
|
|
|
- r_4y = cal_indicators(tb_ret_4y, bmk_ret, risk_free_rate, 'm');
|
|
|
+ tb_ret_4y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
|
|
|
+ WHERE r.end_date > end_day.month()-48 AND (end_day.month() - ei.inception_date.month()) >= 48;
|
|
|
+ if(tb_ret_4y.size() > 0) {
|
|
|
+ r_4y = cal_indicators(tb_ret_4y, bmk_ret, risk_free_rate, 'm');
|
|
|
+ }
|
|
|
|
|
|
// trailing 5y
|
|
|
- tb_ret_5y = SELECT * FROM tb_ret WHERE end_date > end_day.month()-60;
|
|
|
- r_5y = cal_indicators(tb_ret_5y, bmk_ret, risk_free_rate, 'm');
|
|
|
-
|
|
|
+ tb_ret_5y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
|
|
|
+ WHERE r.end_date > end_day.month()-60 AND (end_day.month() - ei.inception_date.month()) >= 60;
|
|
|
+ if(tb_ret_5y.size() > 0) {
|
|
|
+ r_5y = cal_indicators(tb_ret_5y, bmk_ret, risk_free_rate, 'm');
|
|
|
+ r_ms_5y = cal_ms_return(tb_ret_5y, risk_free_rate);
|
|
|
+ }
|
|
|
+
|
|
|
// trailing 10y
|
|
|
- tb_ret_10y = SELECT * FROM tb_ret WHERE end_date > end_day.month()-120;
|
|
|
- r_10y = cal_indicators(tb_ret_10y, bmk_ret, risk_free_rate, 'm');
|
|
|
+ tb_ret_10y = SELECT * FROM tb_ret r INNER JOIN entity_info ei ON r.entity_id = ei.entity_id
|
|
|
+ WHERE r.end_date > end_day.month()-120 AND (end_day.month() - ei.inception_date.month()) >= 120;
|
|
|
+ if(tb_ret_10y.size() > 0) {
|
|
|
+ r_10y = cal_indicators(tb_ret_10y, bmk_ret, risk_free_rate, 'm');
|
|
|
+ r_ms_10y = cal_ms_return(tb_ret_10y, risk_free_rate);
|
|
|
+ }
|
|
|
+
|
|
|
+ return r_incep, r_ytd, r_6m, r_1y, r_2y, r_3y, r_4y, r_5y, r_10y, r_ms_3y, r_ms_5y, r_ms_10y;
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+ * Calculate fund indicators for month-end production
|
|
|
+ *
|
|
|
+ * @param entity_type <STRING>: MF, HF
|
|
|
+ * @param fund_ids <STRING>: 逗号和单引号分隔的fund_id
|
|
|
+ * @param end_day <DATE>: 要计算的日期
|
|
|
+ * @param isFromNav <BOOL>: 用净值实时计算还是从表中取月收益
|
|
|
+ * @param isFromSQL <BOOL>: TODO: 从MySQL还是本地DolphinDB取净值/收益数据
|
|
|
+ *
|
|
|
+ */
|
|
|
+def cal_fund_indicators(entity_type, fund_ids, end_day, isFromNav) {
|
|
|
+
|
|
|
+ very_old_date = 1990.01.01;
|
|
|
+
|
|
|
+ fund_info = get_fund_info(fund_ids);
|
|
|
+ fund_info.rename!('fund_id', 'entity_id');
|
|
|
+
|
|
|
+ if(isFromNav == true) {
|
|
|
+ // 从净值开始计算收益
|
|
|
+ tb_ret = SELECT * FROM cal_fund_monthly_returns(entity_type, fund_ids, true) WHERE price_date <= end_day;
|
|
|
+ tb_ret.rename!(['fund_id', 'cumulative_nav'], ['entity_id', 'nav']);
|
|
|
+ } else {
|
|
|
+ // 从fund_performance表里读月收益
|
|
|
+ tb_ret = get_monthly_ret('FD', fund_ids, very_old_date, end_day, true);
|
|
|
+ tb_ret.rename!(['fund_id'], ['entity_id']);
|
|
|
+ }
|
|
|
+
|
|
|
+ bmk_ret = SELECT fund_id, temporalParse(end_date, 'yyyy-MM') AS end_date, ret FROM get_monthly_ret('IX', "'IN00000008'", very_old_date, end_day, true);
|
|
|
|
|
|
- return r_incep, r_ytd, r_6m, r_1y, r_2y, r_3y, r_4y, r_5y, r_10y;
|
|
|
+ risk_free_rate = SELECT fund_id, temporalParse(end_date, 'yyyy-MM') AS end_date, ret FROM get_risk_free_rate(very_old_date, end_day);
|
|
|
+
|
|
|
+ return cal_all_trailing_indicators(fund_info, tb_ret, end_day, bmk_ret, risk_free_rate, 'm');
|
|
|
}
|
|
|
|
|
|
+def cal_portfolio_indicators(portfolio_ids, end_day, cal_method, isFromNav) {
|
|
|
+
|
|
|
+ very_old_date = 1990.01.01;
|
|
|
+
|
|
|
+ portfolio_info = get_portfolio_info(portfolio_ids);
|
|
|
+ portfolio_info.rename!('portfolio_id', 'entity_id');
|
|
|
+
|
|
|
+ if(isFromNav == true) {
|
|
|
+ // 从净值开始计算收益
|
|
|
+ tb_raw_ret = SELECT * FROM cal_portfolio_return(portfolio_ids, very_old_date, cal_method) WHERE price_date <= end_day;
|
|
|
+
|
|
|
+ // funky thing is you can't use "AS" for the grouping columns?
|
|
|
+ tb_ret = SELECT portfolio_id, price_date.month(), price_date.last() AS price_date, (1+ret).prod()-1 AS ret, nav.last() AS nav
|
|
|
+ FROM tb_raw_ret
|
|
|
+ WHERE price_date <= end_day
|
|
|
+ GROUP BY portfolio_id, price_date.month();
|
|
|
+ tb_ret.rename!(['portfolio_id', 'month_price_date'], ['entity_id', 'end_date']);
|
|
|
+
|
|
|
+ } else {
|
|
|
+ // 从pf_portfolio_performance表里读月收益
|
|
|
+ tb_ret = get_monthly_ret('PF', portfolio_ids, very_old_date, end_day, true);
|
|
|
+ tb_ret.rename!(['portfolio_id'], ['entity_id']);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ bmk_ret = SELECT fund_id, temporalParse(end_date, 'yyyy-MM') AS end_date, ret FROM get_monthly_ret('IX', "'IN00000008'", very_old_date, end_day, true);
|
|
|
+
|
|
|
+ risk_free_rate = SELECT fund_id, temporalParse(end_date, 'yyyy-MM') AS end_date, ret FROM get_risk_free_rate(very_old_date, end_day);
|
|
|
+
|
|
|
+ return cal_all_trailing_indicators(portfolio_info, tb_ret, end_day, bmk_ret, risk_free_rate, 'm');
|
|
|
+
|
|
|
+
|
|
|
+}
|