def default(self, obj):
        # print("obj.__class__", obj.__class__, "isinstance(obj.__class__, DeclarativeMeta)", isinstance(obj.__class__, DeclarativeMeta))
        if isinstance(obj.__class__, DeclarativeMeta):
            # an SQLAlchemy class
            fields = {}
            for field in [
                    x for x in dir(obj)
                    if not x.startswith('_') and x != 'metadata'
            ]:
                data = obj.__getattribute__(field)
                try:
                    json.dumps(
                        data
                    )  # this will fail on non-encodable values, like other classes
                    fields[field] = data
                except TypeError:  # 添加了对datetime的处理
                    print(data)
                    if isinstance(data, datetime):
                        fields[field] = data.isoformat()
                    elif isinstance(data, date):
                        fields[field] = data.isoformat()
                    elif isinstance(data, timedelta):
                        fields[field] = (datetime.min +
                                         data).time().isoformat()
                    else:
                        fields[field] = None
            # a json-encodable dict
            return fields
        elif isinstance(obj, date):
            return json.dumps(date_2_str(obj))

        return json.JSONEncoder.default(self, obj)
    def get(self, status):
        """
        获取投资组合列表数据
        """
        args = paginate_parser.parse_args()
        page_no = args['page_no']
        count = args['count']
        user_id = session.get('user_id')

        # 获取各个组合 最新交易日
        date_latest_query = db.session.query(
            PortfolioValueDaily.pl_id,
            func.max(PortfolioValueDaily.trade_date).label(
                'trade_date_max')).group_by(
                    PortfolioValueDaily.pl_id).subquery('date_latest')
        # 获取各个投资组合 最新净值
        nav_latest_query = db.session.query(
            PortfolioValueDaily.pl_id, PortfolioValueDaily.trade_date,
            PortfolioValueDaily.rr, PortfolioValueDaily.nav).filter(
                PortfolioValueDaily.pl_id == date_latest_query.c.pl_id,
                PortfolioValueDaily.trade_date ==
                date_latest_query.c.trade_date_max).subquery('nav_latest')

        # 分页查询投资组合信息及最新净值
        if status == 'my':
            filter_c = PortfolioInfo.create_user_id == user_id
        elif status == 'all':
            filter_c = or_(PortfolioInfo.access_type == 'public',
                           PortfolioInfo.create_user_id == user_id)
        elif status == 'star':
            # TODO: 星标投资组合
            filter_c = not_(func.isnull(FavoriteCompare.update_time))
        else:
            filter_c = None

        if filter_c is None:
            raise KeyError('status 参数错误 status = %s' % status)
        else:
            pagination = PortfolioInfo.query.outerjoin(
                nav_latest_query,
                PortfolioInfo.pl_id == nav_latest_query.c.pl_id).add_columns(
                    nav_latest_query.c.trade_date,
                    nav_latest_query.c.rr,
                    nav_latest_query.c.nav,
                ).outerjoin(User).add_columns(User.username).outerjoin(
                    FavoritePortfolio,
                    and_(PortfolioInfo.pl_id == FavoritePortfolio.pl_id,
                         FavoritePortfolio.user_id == user_id)).add_columns(
                             func.if_(
                                 func.isnull(FavoritePortfolio.update_time), 0,
                                 1).label('favorite')).filter(
                                     filter_c).paginate(page_no, count)

            logger.debug('%d / %d 页  %d / %d 条数据',
                         pagination.page, pagination.pages,
                         len(pagination.items), pagination.total)
            ret_dic_list = [{
                'pl_id':
                data.PortfolioInfo.pl_id,
                'name':
                data.PortfolioInfo.name,
                'date_from':
                date_2_str(data.PortfolioInfo.date_from),
                'date_to':
                date_2_str(data.PortfolioInfo.date_to),
                'status':
                data.PortfolioInfo.status,
                'desc':
                data.PortfolioInfo.desc,
                'create_user_id':
                data.PortfolioInfo.create_user_id,
                'username':
                data.username,
                'favorite':
                data.favorite,
                'trade_date':
                date_2_str(data.trade_date),
                'rr':
                try_2_float(data.rr),
                'nav':
                try_2_float(data.nav),
                'access_type':
                data.PortfolioInfo.access_type,
            } for data in pagination.items]
            ret_dic = {
                'page': pagination.page,
                'pages': pagination.pages,
                'count': len(pagination.items),
                'total': pagination.total,
                'has_prev': pagination.has_prev,
                'has_next': pagination.has_next,
                'data': ret_dic_list,
            }
            return ret_dic
Example #3
0
def update_pl_compare_result(cmp_id, date_from, date_to, **kwargs):
    """
    对给定 cmp_id 更新指定日期段内的比较结果
    PortfolioCompareInfo 中 params 格式
    compare_method:'>', '<', 'between'
    compare_method 为 between时,asset1 与两个参考数据进行比较,否则与1个参考数据进行比较
    compare_type:'rel.rr', 'abs.rr', 'abs.fix_point'
    compare_type 为 rel 与 资产或资产组合进行比较,abs 与数值进行比较;
    rr 代表与收益率进行比较,fix_point 代表与实际价格进行比较
    :param cmp_id:
    :param date_from:
    :param date_to:
    :param kwargs: 便于被调用时接受其他冗余参数使用,暂时无用
    :return:
    """
    result_df = None
    pl_comp_info = PortfolioCompareInfo.query.get(cmp_id)
    if pl_comp_info.params is None or pl_comp_info.params == "":
        raise ValueError('cmp_id: %s 无效' % cmp_id)
    # 获取参数,计算日期范围交集
    date_from = max([pl_comp_info.date_from, str_2_date(date_from)])
    date_to = min([pl_comp_info.date_to, str_2_date(date_to)])
    param_dic = json.loads(pl_comp_info.params)
    if 'date_start' in param_dic and len(param_dic['date_start']) >= 6:
        date_start = max([str_2_date(param_dic['date_start']), date_from])
    else:
        date_start = max([pl_comp_info.date_from, date_from])

    compare_method = param_dic['compare_method']
    if compare_method not in ('>', '<', 'between'):
        raise ValueError("cmp_id: %s param_dic['compare_method']: %s 无效" %
                         (cmp_id, param_dic['compare_method']))
    compare_type = param_dic['compare_type']
    if compare_type not in ('rel.rr', 'abs.rr', 'abs.fix_point'):
        raise ValueError("cmp_id: %s param_dic['compare_type']: %s 无效" %
                         (cmp_id, param_dic['compare_type']))
    has_value_asset = False  # 判断是否存在 value 类型的资产
    max_len_asset_n = None
    val_s_dic = {}
    need_repead_dic = {}
    # 获取相关数据,每条数据为一组 Series, 合并组成 df
    for asset_num in range(
            1, 4 if compare_method == 'between' else 3):  # 以后可能出现不止2个数值进行运算

        asset_type_n_key = 'asset_type_' + str(asset_num)
        asset_type = param_dic[asset_type_n_key]
        # 用来从 param_dic 中取出相应的参数
        asset_or_value_n_key = 'value_' + str(
            asset_num) if asset_type == 'value' else 'asset_' + str(asset_num)
        # 用来保持到计算结果 Series 中并对列进行命名,后续的比较计算均依赖于此名称
        asset_n_key = 'asset_' + str(asset_num)
        bind_db, table_name = ASSET_TYPE_TABLE_NAME_DIC[asset_type]

        if table_name is None:
            has_value_asset = True
            val_s = pd.Series(name=asset_n_key,
                              data=param_dic[asset_or_value_n_key])
            need_repeat = True

        elif table_name == 'pl_value_daily':
            engine = db.get_engine(
                bind=bind_db) if bind_db is not None else db.engine
            sql_str = """SELECT trade_date, rr FROM pl_value_daily 
            WHERE pl_id = %s AND trade_date BETWEEN %s AND %s 
            ORDER BY trade_date"""
            val_s = pd.read_sql(
                sql_str,
                engine,
                params=[param_dic[asset_or_value_n_key], date_start, date_to],
                index_col='trade_date')['rr'].rename(asset_n_key)
            val_s = (val_s + 1).cumprod()
            need_repeat = False
        else:
            engine = db.get_engine(
                bind=bind_db) if bind_db is not None else db.engine
            sql_str = """select trade_date, close from {0} 
            where wind_code = %s and trade_date between %s and %s 
            ORDER BY trade_date""".format(table_name)
            val_s = pd.read_sql(
                sql_str,
                engine,
                params=[param_dic[asset_or_value_n_key], date_start, date_to],
                index_col='trade_date')['close'].rename(asset_n_key)
            need_repeat = False

        # 加入字典
        val_s_dic[asset_n_key] = val_s
        need_repead_dic[asset_n_key] = need_repeat
        # 选择最长的一组数据进行记录,用于最后将 value型数据扩展长度
        if not need_repeat:
            if max_len_asset_n is None:
                max_len_asset_n = asset_n_key
            elif len(val_s_dic[max_len_asset_n]) < len(val_s):
                max_len_asset_n = asset_n_key

    # 如果存在 value 型数据则扩展期长度与最长的一组数据一致
    if has_value_asset:
        for asset_n_key, need_repeat in need_repead_dic.items():
            if need_repeat:
                max_len_asset_val_s = val_s_dic[max_len_asset_n]
                asset_val = val_s_dic[asset_n_key].repeat(
                    len(max_len_asset_val_s))
                asset_val.index = max_len_asset_val_s.index
                val_s_dic[asset_n_key] = asset_val

    # 合并数据
    data_df = pd.DataFrame(val_s_dic)
    # 只比较 pl_comp_info.date_from  pl_comp_info.date_to 之间的数据
    is_fit = (data_df.index >= pl_comp_info.date_from) & (data_df.index <=
                                                          pl_comp_info.date_to)
    data_sub_df = data_df[is_fit]
    if data_sub_df.shape[0] == 0:
        return result_df
    if compare_method == '>':
        func = compare_func_larger
    elif compare_method == '<':
        func = compare_func_larger
    elif compare_method == 'between':
        func = compare_func_between
    else:
        raise ValueError("cmp_id: %s param_dic['compare_method']: %s 暂不支持" %
                         (cmp_id, param_dic['compare_method']))

    calc_result_df = pd.DataFrame([
        pd.Series(
            func(y), name=x, index=['result', 'shift_value', 'shift_rate'])
        for x, y in data_sub_df.T.items()
    ],
                                  index=data_sub_df.index)
    result_df = data_sub_df.merge(calc_result_df,
                                  left_index=True,
                                  right_index=True)
    if result_df.shape[0] == 0:
        return None
    # data_sub_df['result'] = data_sub_df.apply(func, axis=1)
    result_df['cmp_id'] = cmp_id
    result_df.index.rename('trade_date', inplace=True)
    # 删除重复数据
    result_date_min = result_df.index.min()
    result_date_max = result_df.index.max()
    db.session.query(PortfolioCompareResult).filter(
        PortfolioCompareResult.cmp_id == cmp_id,
        PortfolioCompareResult.trade_date >= result_date_min,
        PortfolioCompareResult.trade_date <= result_date_max).delete()
    db.session.commit()
    # 插入数据
    result_df.to_sql('pl_compare_result', db.engine, if_exists='append')
    logger.debug('更新 cmp_id:%d %s - %s %d 条记录', cmp_id,
                 date_2_str(result_date_min), date_2_str(result_date_max),
                 result_df.shape[0])
    return result_date_max
    def get(self, _id, status):
        """
        投资组合资产分布比例
        """
        logger.info('pl_id=%s, status=%s', _id, status)
        if status == 'latest':
            result_list = db.session.query(
                PortfolioData.trade_date, PortfolioData.asset_type,
                func.sum(PortfolioData.weight).label('weight')).group_by(
                    PortfolioData.trade_date, PortfolioData.asset_type).filter(
                        and_(
                            PortfolioData.trade_date == (db.session.query(
                                func.max(PortfolioData.trade_date)).filter(
                                    PortfolioData.pl_id == _id)),
                            PortfolioData.pl_id == _id)).all()
        elif status == 'recent':
            # count = request.args.get('count', 5, type=int)
            args = paginate_parser.parse_args()
            count = args['count']
            data_list = [
                date_2_str(d[0])
                for d in db.session.query(PortfolioData.trade_date).filter(
                    PortfolioData.pl_id == _id).group_by(
                        PortfolioData.trade_date).limit(count).all()
            ]
            result_list = db.session.query(
                PortfolioData.trade_date, PortfolioData.asset_type,
                func.sum(PortfolioData.weight).label('weight')).group_by(
                    PortfolioData.trade_date, PortfolioData.asset_type).filter(
                        and_(PortfolioData.trade_date.in_(data_list),
                             PortfolioData.pl_id == _id)).all()
        elif status == 'all':
            result_list = db.session.query(
                PortfolioData.trade_date, PortfolioData.asset_type,
                func.sum(PortfolioData.weight).label('weight')).group_by(
                    PortfolioData.trade_date, PortfolioData.asset_type).all()
        else:
            raise KeyError('status 参数错误 status = %s' % status)

        # 合并数据结果
        ret_dic_list = []
        ret_dic_dic = {}
        for data in result_list:
            trade_date = data.trade_date
            if trade_date in ret_dic_dic:
                pl_list, asset_name_list = ret_dic_dic[trade_date]
            else:
                pl_list = []
                asset_name_list = []
                ret_dic_dic[trade_date] = (pl_list, asset_name_list)
                ret_dic_list.append({
                    'trade_date': date_2_str(trade_date),
                    'data': pl_list,
                    'name_list': asset_name_list
                })

            # 扩展
            pl_list.append({
                'name': data.asset_type,
                'value': try_2_float(data.weight),
            })
            asset_name_list.append(data.asset_type)

        ret_dic = {
            'count': len(ret_dic_list),
            'data': ret_dic_list,
        }
        # logger.debug("ret_dic:%s", ret_dic)
        return ret_dic
    def get_pl_data_list_by_date(_id, status, page_no, count):
        """
        获取制定投资组合的成分及权重数据(分页)
        status
        :param _id:
        :param status: latest 最近一次调仓数据, recent 最近几日调仓数据,日期逆序排列
        :return:
        """
        # logger.info('pl_id=%s, status=%s', _id, status)
        # PortfolioData.query.filter(PortfolioData.id == pl_id, PortfolioData.trade_date == 1)
        if status == 'latest':
            date_cur = db.session.query(func.max(
                PortfolioData.trade_date)).filter(
                    PortfolioData.pl_id == _id).scalar()
            # 最新调仓日期
            pagination = PortfolioData.query.filter(
                PortfolioData.pl_id == _id,
                PortfolioData.trade_date == date_cur).paginate(page_no, count)
        elif status == 'recent':
            pagination = PortfolioData.query.group_by(
                PortfolioData.trade_date).filter(
                    PortfolioData.pl_id == _id).order_by(
                        PortfolioData.trade_date.desc()).paginate(
                            page_no, count)
        else:
            raise KeyError('status = %s 不支持' % status)

        logger.debug('%d / %d 页  %d / %d 条数据', pagination.page,
                     pagination.pages, len(pagination.items), pagination.total)
        date_list = [data.trade_date for data in pagination.items]

        date_grouped_dic = {}
        ret_dic_list = []
        if len(date_list) > 0:
            # date_grouped_dic 中的 value 为 data_list 实际与 ret_dic_list 中对应日期的 data_list 为同一对象
            # 因此可以直接修改

            items = db.session.query(PortfolioData).filter(
                PortfolioData.pl_id == _id,
                PortfolioData.trade_date.in_(date_list)).all()
            for data in items:
                date_cur = date_2_str(data.trade_date)
                if date_cur in date_grouped_dic:
                    data_list = date_grouped_dic[date_cur]
                else:
                    data_list = []
                    date_grouped_dic[date_cur] = data_list
                    ret_dic_list.append({
                        'trade_date': date_cur,
                        'data': data_list
                    })

                # 获取资产及资产类别中文名称
                asset_name = get_asset_name(data.asset_type, data.asset_code)
                # 因为list是引用性数据,直接放入 ret_dic_list
                # 对 data_list 的修改直接反应到 ret_dic_list 中去
                data_list.append({
                    'id':
                    data.id,
                    'asset_code':
                    data.asset_code,
                    'asset_name':
                    asset_name,
                    'asset_type':
                    data.asset_type,
                    'trade_date':
                    date_cur,
                    'weight':
                    try_2_float(data.weight),
                    'weight_before':
                    try_2_float(data.weight_before),
                    'price_type':
                    data.price_type,
                    'direction':
                    data.direction,
                })

        ret_dic = {
            'page': pagination.page,
            'pages': pagination.pages,
            'count': len(pagination.items),
            'total': pagination.total,
            'has_prev': pagination.has_prev,
            'has_next': pagination.has_next,
            'data': ret_dic_list,
        }
        # logger.debug(ret_dic)
        return ret_dic
    def get_pl_data_list(_id, status, page_no, count):
        """
        获取制定投资组合的成分及权重数据(分页)
        :param _id:
        :param status:
        :param page_no:
        :param count:
        :return:
        """
        if status == 'latest':
            pagination = PortfolioData.query.filter(
                PortfolioData.pl_id == _id,
                PortfolioData.trade_date == (db.session.query(
                    func.max(PortfolioData.trade_date)).filter(
                        PortfolioData.pl_id == _id))).paginate(page_no, count)
        elif status == 'recent':
            pagination = PortfolioData.query.filter(
                PortfolioData.pl_id == _id).order_by(
                    PortfolioData.trade_date.desc()).paginate(page_no, count)
        else:
            raise KeyError('status = %s 不支持' % status)

        logger.debug('%d / %d 页  %d / %d 条数据', pagination.page,
                     pagination.pages, len(pagination.items), pagination.total)
        date_grouped_dic = {}
        ret_dic_list = []
        for data in pagination.items:
            date_cur = date_2_str(data.trade_date)
            if date_cur in date_grouped_dic:
                data_list = date_grouped_dic[date_cur]
            else:
                data_list = []
                date_grouped_dic[date_cur] = data_list
                ret_dic_list.append({
                    'trade_date': date_cur,
                    'data': data_list
                })

            # 获取资产及资产类别中文名称
            asset_name = get_asset_name(data.asset_type, data.asset_code)
            # 因为list是引用性数据,直接放入 ret_dic_list
            # 对 data_list 的修改直接反应到 ret_dic_list 中去
            data_list.append({
                'id': data.id,
                'asset_code': data.asset_code,
                'asset_name': asset_name,
                'asset_type': data.asset_type,
                'trade_date': date_cur,
                'weight': try_2_float(data.weight),
                'weight_before': try_2_float(data.weight_before),
                'price_type': data.price_type,
                'direction': data.direction,
            })

        ret_dic = {
            'page': pagination.page,
            'pages': pagination.pages,
            'count': len(pagination.items),
            'total': pagination.total,
            'has_prev': pagination.has_prev,
            'has_next': pagination.has_next,
            'data': ret_dic_list,
        }
        # logger.debug(ret_dic)
        return ret_dic
    def get(self, status):
        """
        获取比较列表数据(分页)
        """
        args = paginate_parser.parse_args()
        page_no = args['page_no']
        count = args['count']
        user_id = session.get('user_id')
        logger.debug('get_cmp_list user_id:%s', user_id)
        if status == 'my':
            filter_c = PortfolioCompareInfo.create_user_id == user_id
            having_c = None
        elif status == 'all':
            filter_c = or_(PortfolioCompareInfo.create_user_id == user_id,
                           PortfolioCompareInfo.access_type == 'public')
            having_c = None
        elif status == 'star':
            filter_c = and_(
                or_(PortfolioCompareInfo.create_user_id == user_id,
                    PortfolioCompareInfo.access_type == 'public'),
                not_(func.isnull(FavoriteCompare.update_time)))
            having_c = None
        elif status == 'verified':
            filter_c = or_(PortfolioCompareInfo.create_user_id == user_id,
                           PortfolioCompareInfo.access_type == 'public')
            having_c = column('complete_rate') >= 1
        elif status == 'unverified':
            filter_c = or_(PortfolioCompareInfo.create_user_id == user_id,
                           PortfolioCompareInfo.access_type == 'public')
            having_c = or_(
                column('complete_rate').is_(None),
                column('complete_rate') < 1)
        else:
            raise KeyError('status 参数错误 status = %s' % status)
        # 整理数据
        # logger.debug("data_list_df len:%d", data_list_df.shape[0])
        # data_list_df = data_list_df.where(data_list_df.notna(), None)
        # data_list = data_list_df.to_dict('record')
        # data_table_dic = {'data': data_list}
        # logger.debug(data_table_dic)

        query = PortfolioCompareInfo.query.outerjoin(
            PortfolioCompareResult
        ).group_by(PortfolioCompareResult.cmp_id).add_columns(
            func.count().label('tot_count'),
            func.min(
                PortfolioCompareResult.trade_date).label('trade_date_min'),
            func.max(
                PortfolioCompareResult.trade_date).label('trade_date_max'),
            func.sum(PortfolioCompareResult.result).label('fit_count'),
            (func.sum(PortfolioCompareResult.result) /
             func.count()).label('fit_rate'),
            ((func.max(PortfolioCompareResult.trade_date) -
              PortfolioCompareInfo.date_from) /
             (PortfolioCompareInfo.date_to - PortfolioCompareInfo.date_from)
             ).label('complete_rate')).outerjoin(User).add_columns(
                 User.username).outerjoin(
                     FavoriteCompare,
                     and_(
                         PortfolioCompareInfo.cmp_id == FavoriteCompare.cmp_id,
                         FavoriteCompare.user_id == user_id)).add_columns(
                             func.if_(
                                 func.isnull(FavoriteCompare.update_time), 0,
                                 1).label('favorite')).filter(
                                     and_(filter_c,
                                          PortfolioCompareInfo.is_del == 0))
        if having_c is None:
            pagination = query.paginate(page_no, count)
        else:
            pagination = query.having(having_c).paginate(page_no, count)

        logger.debug('%d / %d 页  %d / %d 条数据', pagination.page,
                     pagination.pages, len(pagination.items), pagination.total)
        ret_dic_list = [{
            'cmp_id':
            data.PortfolioCompareInfo.cmp_id,
            'name':
            data.PortfolioCompareInfo.name,
            'status':
            data.PortfolioCompareInfo.status,
            'params':
            data.PortfolioCompareInfo.params,
            'desc':
            data.PortfolioCompareInfo.desc,
            'date_from':
            date_2_str(data.PortfolioCompareInfo.date_from),
            'date_to':
            date_2_str(data.PortfolioCompareInfo.date_to),
            'trade_date_min':
            date_2_str(data.trade_date_min),
            'trade_date_max':
            date_2_str(data.trade_date_max),
            'create_user_id':
            data.PortfolioCompareInfo.create_user_id,
            'username':
            data.username,
            'favorite':
            data.favorite,
            'complete_rate':
            try_2_float(data.complete_rate),
        } for data in pagination.items]
        ret_dic = {
            'page': pagination.page,
            'pages': pagination.pages,
            'count': len(pagination.items),
            'total': pagination.total,
            'has_prev': pagination.has_prev,
            'has_next': pagination.has_next,
            'data': ret_dic_list,
        }
        return ret_dic
    def get(self, _id):
        """
        获取投资组合的绩效及统计信息
        """
        ret_data = {"general": {}, "performance": {}, "others": {}}
        pl_obj = db.session.query(PortfolioInfo, User.username).filter(
            PortfolioInfo.pl_id == _id,
            User.id == PortfolioInfo.create_user_id).first()
        ret_data['general']['pl_id'] = pl_obj.PortfolioInfo.pl_id
        ret_data['general']['name'] = pl_obj.PortfolioInfo.name
        ret_data['general']['date_from'] = date_2_str(
            pl_obj.PortfolioInfo.date_from)
        ret_data['general']['date_to'] = date_2_str(
            pl_obj.PortfolioInfo.date_to)
        ret_data['general'][
            'create_user_id'] = pl_obj.PortfolioInfo.create_user_id
        ret_data['general']['create_user_name'] = pl_obj.username
        ret_data['general']['created_at'] = datetime_2_str(
            pl_obj.PortfolioInfo.create_dt)
        ret_data['general']['access_type'] = pl_obj.PortfolioInfo.access_type
        ret_data['general']['status'] = pl_obj.PortfolioInfo.status
        ret_data['general']['is_del'] = pl_obj.PortfolioInfo.is_del
        ret_data['general']['desc'] = pl_obj.PortfolioInfo.desc

        # date_latest = db.session.query(func.max(PortfolioValueDaily.trade_date)).filter(
        #     PortfolioValueDaily.pl_id == _id).scalar()
        pl_df = pd.read_sql("select trade_date, nav from " +
                            PortfolioValueDaily.__tablename__ +
                            "  where pl_id = %s order by trade_date",
                            db.engine,
                            params=[_id],
                            index_col='trade_date')
        if pl_df.shape[0] > 0:
            stat_dic_dic = calc_performance(pl_df, freq=None)
            if 'nav' in stat_dic_dic:
                performance_dic = stat_dic_dic['nav']
                for key in list(performance_dic.keys()):
                    value = performance_dic[key]
                    if value is None:
                        performance_dic[key] = '-'
                    elif type(value) is date:
                        performance_dic[key] = date_2_str(value)
                    elif type(value) is not str and not np.isfinite(value):
                        performance_dic[key] = '-'
                ret_data['performance'] = stat_dic_dic['nav']
            else:
                logger.warning('stat_dic_dic:\n%s', stat_dic_dic)

        # 星标数量
        star_count = db.session.query(
            func.count()).filter(FavoritePortfolio.pl_id == _id).scalar()
        ret_data['others']['star_count'] = int(star_count)

        # 最新净值
        latest_nav_sub_query = db.session.query(
            PortfolioValueDaily.nav).filter(
                PortfolioValueDaily.pl_id == _id,
                PortfolioValueDaily.trade_date == (db.session.query(
                    func.max(PortfolioValueDaily.trade_date)).filter(
                        PortfolioValueDaily.pl_id == _id))).subquery()

        # 收益率排名
        max_trade_date_sub_query = db.session.query(
            PortfolioValueDaily.pl_id,
            func.max(PortfolioValueDaily.trade_date).label(
                'trade_date_max')).group_by(
                    PortfolioValueDaily.pl_id).subquery()
        latest_nav_list_sub_query = db.session.query(
            PortfolioValueDaily.pl_id, PortfolioValueDaily.nav).join(
                max_trade_date_sub_query,
                and_(
                    PortfolioValueDaily.pl_id ==
                    max_trade_date_sub_query.c.pl_id,
                    PortfolioValueDaily.trade_date ==
                    max_trade_date_sub_query.c.trade_date_max)).order_by(
                        PortfolioValueDaily.nav.asc()).subquery()

        # 净值排名
        nav_rank_count = db.session.query(func.count()).filter(
            latest_nav_list_sub_query.c.nav >= latest_nav_sub_query.c.nav
        ).scalar()
        # 总产品数量
        pl_count_has_nav = db.session.query(
            func.count(
                db.session.query(PortfolioValueDaily.pl_id).group_by(
                    PortfolioValueDaily.pl_id).subquery().c.pl_id)).scalar()
        ret_data['others']['rank'] = 0 if pl_count_has_nav == 0 else float(
            nav_rank_count / pl_count_has_nav)
        return ret_data