diff --git a/api.py b/api.py index 37c051f4253b846ce194c3cb9ee31a3f222bb67c..f7404238a8bf436f040cbea32bab3b31be62183a 100644 --- a/api.py +++ b/api.py @@ -1,7 +1,6 @@ from abc import ABC, abstractmethod from datetime import datetime as dt from enum import Enum, unique -from typing import List, Dict from py_jftech import get_config @@ -13,30 +12,18 @@ class DatumType(Enum): ECO = 'ECO' -@unique -class AssetRiskDateType(Enum): - START_DATE = 1 - STOP_DATE = 2 - - @unique class AssetPoolType(Enum): OPTIMIZE = 1 - RISK = 2 @unique class PortfoliosRisk(Enum): FT3 = 3 - FT6 = 6 - FT9 = 9 @unique class PortfoliosType(Enum): - CRISIS_1 = 'crisis_1' - CRISIS_2 = 'crisis_2' - RIGHT_SIDE = 'right_side' NORMAL = 'normal' CUSTOM = 'custom' @@ -48,37 +35,6 @@ class SolveType(Enum): POEM = 2 -@unique -class SignalType(Enum): - NONE = -1 - INIT = 0 - CRISIS_EXP = 1 - CRISIS_ONE = 2 - CRISIS_TWO = 3 - MARKET_RIGHT = 4 - HIGH_BUY = 5 - LOW_BUY = 6 - DRIFT_BUY = 7 - - -# ä¿¡å·å¤„ç†ä¼˜å…ˆçº§ -SignalType.CRISIS_ONE.level = 1 -SignalType.CRISIS_TWO.level = 1 -SignalType.MARKET_RIGHT.level = 3 -SignalType.HIGH_BUY.level = 4 -SignalType.LOW_BUY.level = 5 -SignalType.DRIFT_BUY.level = 5 -SignalType.INIT.level = 6 -# 对应需è¦å†å¹³è¡¡çš„æŠ•组类型 -SignalType.CRISIS_ONE.p_type = PortfoliosType.CRISIS_1 -SignalType.CRISIS_TWO.p_type = PortfoliosType.CRISIS_2 -SignalType.MARKET_RIGHT.p_type = PortfoliosType.RIGHT_SIDE -SignalType.HIGH_BUY.p_type = PortfoliosType.NORMAL -SignalType.LOW_BUY.p_type = PortfoliosType.NORMAL -SignalType.DRIFT_BUY.p_type = PortfoliosType.NORMAL -SignalType.INIT.p_type = PortfoliosType.RIGHT_SIDE - - class DataSync(ABC): ''' æ•°æ®åŒæ¥æœåŠ¡ï¼Œéœ€è¦åŒæ¥æ•°æ®çš„æœåŠ¡ï¼Œå¯ä»¥å®žçŽ°è¯¥æŽ¥å£ @@ -232,48 +188,6 @@ class AssetOptimize(ABC): pass -class AssetRisk(ABC): - ''' - ewma相关æœåŠ¡ - ''' - - @abstractmethod - def get_risk_pool(self, day): - ''' - èŽ·å–æŒ‡å®šæ—¥æœŸçš„é£ŽæŽ§æ± - :param day: 指定的日期 - :return: 风控id列表 - ''' - pass - - @abstractmethod - def is_risk(self, id, day) -> bool: - ''' - åˆ¤æ–æŒ‡å®šçš„idï¼Œåœ¨æŒ‡å®šçš„æ—¥æœŸï¼Œæ˜¯å¤„äºŽé£ŽæŽ§çŠ¶æ€ - :param id: 指定的资产id - :param day: 指定的日期 - :return: 如果处于风控状æ€åˆ™è¿”回True,å¦åˆ™è¿”回False - ''' - pass - - @abstractmethod - def build_risk_date(self, asset_id, day=dt.today()): - ''' - 构建指定资产的所有风险时间点 - :param asset_id: 指定的资产id - :param day: æž„å»ºçš„æˆªæ¢æ—¥æœŸ - ''' - pass - - @abstractmethod - def clear(self, day=None): - ''' - 清除指定日期之åŽçš„资产风控ewmaæ•°æ®ï¼Œå¦‚果没有给日期,则全部清空 - :param day: 指定清除的开始日期,å¯é€‰ - ''' - pass - - class AssetPool(ABC): ''' èµ„äº§æ± ç›¸å…³æœåŠ¡ @@ -383,6 +297,27 @@ class Solver(ABC): ''' pass + @abstractmethod + def set_navs(self, navs): + ''' + æ ¹æ®æŒ‡å®šçš„navs,é‡ç½®å½“å‰è§£ç®—器 + :param navs: 指定的navs + ''' + pass + + @abstractmethod + def set_category(self, category): + ''' + æ ¹æ®æŒ‡å®šçš„category,é‡ç½®å½“å‰è§£ç®—器 + :param category: 指定的category + ''' + pass + + @property + @abstractmethod + def category(self): + pass + @property @abstractmethod def navs(self): @@ -391,6 +326,15 @@ class Solver(ABC): ''' pass + @property + @abstractmethod + def transfer_type(self): + """ + 得出调仓类型 + @return: + """ + pass + class SolverFactory(ABC): ''' @@ -462,15 +406,6 @@ class PortfoliosHolder(ABC): ''' pass - @abstractmethod - def get_rebalance_date_by_signal(self, signal_id): - ''' - èŽ·å–æŒ‡å®šè°ƒä»“ä¿¡å·è§¦å‘的实际调仓日期 - :param signal_id: æŒ‡å®šçš„è°ƒä»“ä¿¡å· - :return: 实际调仓日期 - ''' - pass - @property @abstractmethod def interval_days(self): @@ -490,92 +425,6 @@ class PortfoliosHolder(ABC): pass -class DriftSolver(ABC): - ''' - 漂移解算器 - ''' - - @abstractmethod - def get_drift(self, day, risk: PortfoliosRisk): - ''' - èŽ·å–æŒ‡å®šæ—¥æœŸï¼ŒæŒ‡å®šé£Žé™©ç‰çº§çš„æ¼‚移计算结果 - :param day: 指定日期 - :param risk: 指定风险ç‰çº§ - :return: 漂移计算结果 - ''' - pass - - -class RebalanceSignal(ABC): - ''' - 控制信å·ï¼Œå‘起是å¦è°ƒä»“æœåŠ¡ - ''' - - @abstractmethod - def get_signal(self, day, risk: PortfoliosRisk): - pass - - @property - @abstractmethod - def signal_type(self) -> SignalType: - ''' - 返回信å·ç±»åž‹ - :return: ä¿¡å·ç±»åž‹ - ''' - pass - - -class RebalanceRuler(ABC): - ''' - å†å¹³è¡¡ä¿¡å·åˆ†é…å™¨ï¼Œæ ¹æ®æ—¢å®šçš„规则,å†ä¼—多信å·ä¸ï¼Œé€‰å‡ºè¿›è¡Œå†å¹³è¡¡çš„ä¿¡å· - ''' - - @abstractmethod - def take_next_signal(self, day, risk: PortfoliosRisk): - ''' - å–出指定日期,指定风险ç‰çº§çš„å†å¹³è¡¡ä¿¡å·æ•°æ®ï¼Œæ³¨æ„å–出消费åŽï¼Œæ— 法退回,éžå¹‚ç‰å‡½æ•° - :param day: 指定日期 - :param risk: 指定风险ç‰çº§ - :return: 如果å˜åœ¨ï¼Œåˆ™è¿”回å–出的å†å¹³è¡¡ä¿¡å·ä¿¡æ¯ï¼Œå¦åˆ™è¿”回None - ''' - pass - - @abstractmethod - def commit_signal(self, sign_id): - ''' - æäº¤ä¿¡å·IDä¸ºå·²æ¶ˆè´¹çŠ¶æ€ - :param sign_id: ä¿¡å·ID - ''' - pass - - @abstractmethod - def get_signal_type(self, sign_id) -> SignalType | Dict[int, SignalType]: - ''' - èŽ·å–æŒ‡å®šid的信å·ç±»åž‹ - :param sign_id: ä¿¡å·id, å¯ä»¥å¤šä¸ªï¼Œä½¿ç”¨å…ƒç¥–包裹 - :return: ä¿¡å·ç±»åž‹ - ''' - pass - - @abstractmethod - def get_signal_date(self, sign_id): - ''' - èŽ·å–æŒ‡å®šidçš„ä¿¡å·æ—¥æœŸ - :param sign_id: ä¿¡å·id, å¯ä»¥å¤šä¸ªï¼Œä½¿ç”¨å…ƒç¥–包裹 - :return: ä¿¡å·æ—¥æœŸ - ''' - pass - - @abstractmethod - def clear_signal(self, day=None, risk: PortfoliosRisk = None): - ''' - 清除指定风险ç‰çº§ï¼ŒæŒ‡å®šæ—¥æœŸä¹‹åŽçš„è°ƒä»“ä¿¡å· - :param day: 指定清除的开始日期,å¯é€‰ï¼Œå¦‚果没给,则清除全部日期 - :param risk: 指定风险ç‰çº§ï¼Œå¦‚果没给,则清除全部风险ç‰çº§ - ''' - pass - - class RoboExecutor(ABC): ''' ROBO执行器,整åˆä»¥ä¸Šé€»è¾‘,进行实盘或回测 @@ -599,43 +448,3 @@ class RoboExecutor(ABC): @staticmethod def use_name(): return get_config('robo-executor')['use'] - - -class RoboReportor(ABC): - ''' - 投组报告器 - ''' - - @property - @abstractmethod - def report_name(self) -> str: - ''' - 返回报告åç§° - :return: 报告åç§° - ''' - pass - - @abstractmethod - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - ''' - èŽ·å–æŒ‡å®šæ—¥æœŸçš„æŠ¥å‘Š - :param max_date: æŒ‡å®šæˆªæ¢æ—¥æœŸ - :param min_date: 指定开始日期 - :return: æŠ¥å‘Šæ•°æ® - ''' - pass - - -class RoboExportor(ABC): - ''' - 投组导出器 - ''' - - @abstractmethod - def export(self, max_date=dt.today(), min_date=None): - ''' - æ ¹æ®å‚数以åŠé…ç½®ä¿¡æ¯æ‰§è¡Œå¯¼å‡ºç›¸å…³æ“作 - :param max_date: æŒ‡å®šæˆªæ¢æ—¥æœŸ - :param min_date: 指定开始日期 - ''' - pass diff --git a/asset_pool/asset_optimize.py b/asset_pool/asset_optimize.py index e98738646abdc6931d5debd11affc168b76a8565..0e301d065324baf47d99cca7ff16c578f158b549 100644 --- a/asset_pool/asset_optimize.py +++ b/asset_pool/asset_optimize.py @@ -1,11 +1,12 @@ import json from abc import ABC, abstractmethod -from datetime import datetime as dt, timedelta +from sys import exception import pandas as pd from dateutil.relativedelta import relativedelta from empyrical import sortino_ratio -from py_jftech import filter_weekend, dict_remove, get_config, component, autowired, get_quarter_start, next_workday, is_workday +from py_jftech import filter_weekend, dict_remove, get_config, component, autowired, next_workday, \ + is_workday from api import AssetOptimize, Navs, Datum, AssetPoolType, DatumType from asset_pool.dao import robo_assets_pool as rop @@ -30,41 +31,9 @@ class SortinoAssetOptimize(AssetOptimize, ABC): return result def find_optimize(self, fund_ids, day): - assert self._config, "find optimize, but not found sortino config." - pct_change = pd.DataFrame(self.get_pct_change(fund_ids, day)) - pct_change.set_index('date', inplace=True) - sortino = pd.DataFrame() - for item in self._config: - ratio = dict(sortino_ratio(pct_change.truncate(before=(day - relativedelta(**dict_remove(item, ('weight', 'name'))))))) - sortino = pd.concat([sortino, pd.DataFrame([ratio], index=[item['name']])]) - sortino = sortino.T - sortino['score'] = sortino.apply(lambda r: sum([x['weight'] * r[x['name']] for x in self._config]), axis=1) - sortino.sort_values('score', ascending=False, inplace=True) - return pct_change.columns[sortino.index[0]] + pass def get_optimize_pool(self, day): - opt_pool = rop.get_one(day=day, type=AssetPoolType.OPTIMIZE) - if opt_pool is not None: - return json.loads(opt_pool['asset_ids']) - last_one = rop.get_last_one(day=day, type=AssetPoolType.OPTIMIZE) - start = get_quarter_start(day or dt.today()) - if not last_one or start > last_one['date'] or self.has_incept_asset(last_one['date'] + timedelta(1), day) or self.has_change(day): - pool = [] - min_dates = self.nav_min_dates - max_incept_date = sorted([(day - relativedelta(**x)) for x in self.delta_kwargs])[0] - max_incept_date = max_incept_date if is_workday(max_incept_date) else next_workday(max_incept_date) - for fund_group in self.get_groups(): - fund_group = [x for x in fund_group if min_dates[x] <= max_incept_date] - if len(fund_group) > 1: - pool.append(self.find_optimize(tuple(fund_group), day)) - elif len(fund_group) == 1: - pool.append(fund_group[0]) - rop.insert(day, AssetPoolType.OPTIMIZE, sorted(pool)) - last_one = rop.get_last_one(day=day, type=AssetPoolType.OPTIMIZE) - return json.loads(last_one['asset_ids']) - - @abstractmethod - def has_incept_asset(self, start_date, end_date): pass @property @@ -94,41 +63,81 @@ class SortinoAssetOptimize(AssetOptimize, ABC): return False -@component -class FundSortinoAssetOptimize(SortinoAssetOptimize): - ''' +@component(bean_name='dividend') +class FundDividendSortinoAssetOptimize(SortinoAssetOptimize): + """ æ ¹æ®ç´¢æè¯ºæ¯”率计算基金优选的优选实现 - ''' + 以美国资产为主:US_STOCKã€US_HY_BONDã€US_IG_BOND + Sortino ratio对资产进行排åºï¼Œé€‰å‡ºæŽ’åé å‰çš„资产(éžä¸€ç±»é€‰ä¸€åªï¼‰ + """ @autowired def __init__(self, navs: Navs = None, datum: Datum = None): super().__init__() self._navs = navs self._datum = datum + self._conf = get_config(__name__) + @property + def asset_include(self): + return self._conf['asset-include'] + + @property + def optimize_count(self): + return self._conf['optimize-count'] @property def nav_min_dates(self) -> dict: return self._navs.get_nav_start_date() - def has_incept_asset(self, start_date, end_date): - start_date = sorted([(start_date - relativedelta(**x)) for x in self.delta_kwargs])[0] - end_date = sorted([(end_date - relativedelta(**x)) for x in self.delta_kwargs])[0] - return len([x for x in self.nav_min_dates.items() if start_date <= x[1] <= end_date]) > 0 - def has_change(self, day): return self._datum.update_change(day) + def find_optimize(self, fund_ids, day): + assert self._config, "find optimize, but not found sortino config." + pct_change = pd.DataFrame(self.get_pct_change(fund_ids, day)) + pct_change.set_index('date', inplace=True) + sortino = pd.DataFrame() + for item in self._config: + ratio = dict(sortino_ratio( + pct_change.truncate(before=(day - relativedelta(**dict_remove(item, ('weight', 'name'))))))) + sortino = pd.concat([sortino, pd.DataFrame([ratio], index=[item['name']])]) + sortino = sortino.T + sortino['score'] = sortino.apply(lambda r: sum([x['weight'] * r[x['name']] for x in self._config]), axis=1) + sortino.sort_values('score', ascending=False, inplace=True) + # å–得分数高的å‰optimize_count个 + return pct_change.columns[sortino.index[0:self.optimize_count]].values + + def get_optimize_pool(self, day): + opt_pool = rop.get_one(day=day, type=AssetPoolType.OPTIMIZE) + if opt_pool is not None: + return json.loads(opt_pool['asset_ids']) + last_one = rop.get_last_one(day=day, type=AssetPoolType.OPTIMIZE) + if not last_one or day > last_one['date']: + pool = [] + min_dates = self.nav_min_dates + max_incept_date = sorted([(day - relativedelta(**x)) for x in self.delta_kwargs])[0] + max_incept_date = max_incept_date if is_workday(max_incept_date) else next_workday(max_incept_date) + for fund_group in self.get_groups(): + fund_group = [x for x in fund_group if min_dates[x] <= max_incept_date] + if len(fund_group) > self.optimize_count: + pool.extend(self.find_optimize(tuple(fund_group), day)) + elif len(fund_group) <= self.optimize_count: + pool.extend(fund_group) + rop.insert(day, AssetPoolType.OPTIMIZE, sorted(pool)) + last_one = rop.get_last_one(day=day, type=AssetPoolType.OPTIMIZE) + return json.loads(last_one['asset_ids']) + def get_groups(self): funds = pd.DataFrame(self._datum.get_datums(type=DatumType.FUND)) - min_dates = self._navs.get_nav_start_date() result = [] for (category, asset_type), fund_group in funds.groupby(by=['category', 'assetType']): - result.append(tuple(fund_group['id'])) + if category in self.asset_include: + result.append(tuple(fund_group['id'])) return result def get_pct_change(self, fund_ids, day): if not self._config: - raise BusinessException(f"find optimize, but not found sortino config.") + raise exception(f"find optimize, but not found sortino config.") start = filter_weekend( sorted([day - relativedelta(days=1, **dict_remove(x, ('weight', 'name'))) for x in self._config])[0]) fund_navs = pd.DataFrame(self._navs.get_fund_navs(fund_ids=tuple(fund_ids), min_date=start, max_date=day)) diff --git a/asset_pool/asset_risk.py b/asset_pool/asset_risk.py deleted file mode 100644 index 85fe4636783f323196c671d409ff9582c930a121..0000000000000000000000000000000000000000 --- a/asset_pool/asset_risk.py +++ /dev/null @@ -1,141 +0,0 @@ -import json -import logging -from datetime import datetime as dt - -import pandas as pd -from dateutil.relativedelta import relativedelta -from py_jftech import component, autowired, get_config, format_date, transaction, asynchronized -from scipy.stats import norm - -from api import AssetRisk, Navs, AssetRiskDateType as DateType, Datum, AssetPoolType, RoboExecutor, DatumType -from asset_pool.dao import asset_risk_dates as ard, asset_ewma_value as aev, robo_assets_pool as rap - -logger = logging.getLogger(__name__) - - -@component -class CvarEwmaAssetRisk(AssetRisk): - ''' - CVARæ–¹å¼å†³å®šé£ŽæŽ§å¼€å§‹ã€‚风控开始åŽï¼Œå¼€å§‹è®¡ç®—ewmaå¯»æ‰¾é£ŽæŽ§ç»“æŸæ—¥æœŸï¼Œä¹Ÿå°±æ˜¯ewma的起始日期 - EWMAæ–¹å¼å†³å®šé£ŽæŽ§ç»“æŸï¼šé£ŽæŽ§ç»“æŸåŽï¼Œå°±å¯ä»¥æ‰¾åˆ°é£ŽæŽ§æœŸçš„æœ€ä½Žç‚¹æ—¥æœŸï¼Œè¯¥æ—¥æœŸä½œä¸ºä¸‹ä¸€è½®cvar计算的起始日期 - ''' - - @autowired(names={'executor': RoboExecutor.use_name()}) - def __init__(self, navs: Navs = None, datum: Datum = None, executor: RoboExecutor = None): - self._navs = navs - self._datum = datum - self._executor = executor - self._config = get_config(__name__) - - @property - def risk_start_date(self): - return self._executor.start_date - relativedelta(months=self._config['advance-months']) - - def get_risk_pool(self, day): - asset_pool = rap.get_one(day, AssetPoolType.RISK) - if not asset_pool: - result = {x['id']: self.async_is_risk(x['id'], day) for x in self._datum.get_datums(type=DatumType.FUND, risk=(3, 4, 5))} - risk_ids = [x[0] for x in result.items() if x[1].result()] - rap.insert(day, AssetPoolType.RISK, risk_ids) - asset_pool = rap.get_one(day, AssetPoolType.RISK) - return json.loads(asset_pool['asset_ids']) - - @asynchronized - def async_is_risk(self, id, day): - return self.is_risk(id, day) - - def is_risk(self, id, day) -> bool: - asset_pool = rap.get_one(day, AssetPoolType.RISK) - if asset_pool: - return id in json.loads(asset_pool['asset_ids']) - last = ard.get_last_one(fund_id=id) - if last and last['date'] < day: - self.build_risk_date(id, day) - result = ard.get_last_one(id, day) - return DateType(result['type']) is DateType.START_DATE if result else True - - def build_risk_date(self, asset_id, day=dt.today()): - risk_date = not None - try: - logger.debug(f"start build risk date for asset[{asset_id}] to date[{format_date(day)}]") - while risk_date is not None: - risk_date = self.get_next_date(asset_id, day=day) - except Exception as e: - logger.exception(f"build risk date for asset[{asset_id}] after date[{risk_date}] to date[{format_date(day)}] error", e) - - @transaction - def clear(self, day=None): - ard.delete(day) - aev.delete(day) - - def get_next_date(self, asset_id, day=dt.today()): - last = ard.get_last_one(asset_id, day) - if not last or DateType(last['type']) is DateType.START_DATE: - start_date = last['date'] if last else self.risk_start_date - ewma = pd.DataFrame(self.get_ewma_value(asset_id, min_date=start_date, max_date=day)) - total = self._config['ewma']['condition-total'] - meet = self._config['ewma']['condition-meet'] - threshold = self._config['ewma']['threshold'] - if len(ewma) < total: - return None - for index in range(total, len(ewma) - 1): - sub_ewma = ewma[index - total:index] - if len(sub_ewma[sub_ewma['ewma'] >= threshold]) >= meet: - stop_date = sub_ewma.iloc[-1]['date'] - ard.insert(asset_id, DateType.STOP_DATE, stop_date) - return {'date': stop_date, 'type': DateType.STOP_DATE} - elif DateType(last['type']) is DateType.STOP_DATE: - last_start = ard.get_last_one(asset_id, last['date'], type=DateType.START_DATE) - start_date = last_start['date'] if last_start else self.risk_start_date - rtns = pd.DataFrame(self.get_income_return(asset_id, min_date=start_date, max_date=day)) - risk_rtns = rtns[rtns.date <= last['date']] - cvar_start_date = risk_rtns.loc[risk_rtns.nav.idxmin()].date - for index, row in rtns[rtns.date >= cvar_start_date].iterrows(): - tigger = False - cvar_rtns = rtns[(rtns.date >= cvar_start_date) & (rtns.date <= row['date'])] - if row.nav < rtns[rtns.date == cvar_start_date].iloc[0].nav: - tigger = True - elif row['rtn'] <= self._config['cvar']['threshold'] and len(cvar_rtns) >= self._config['cvar']['min-volume']: - # 当日回报率å°äºŽç‰äºŽé˜€å€¼å¹¶ä¸”有足够cvar累计计算数æ®ï¼Œåˆ™è®¡ç®—cvaråˆ¤æ– - alpha = 1 - self._config['cvar']['coef'] - mean = cvar_rtns['rtn'].mean() - std = cvar_rtns['rtn'].std() - cvar = mean - std * norm.pdf(norm.ppf(alpha)) / alpha - tigger = row['rtn'] < cvar - if tigger: - ard.insert(asset_id, DateType.START_DATE, row['date']) - return {'date': row['date'], 'type': DateType.START_DATE} - return None - - def get_ewma_value(self, id, min_date=None, max_date=None): - rtn = pd.DataFrame(self.get_income_return(id, min_date=min_date or self.risk_start_date, max_date=max_date)) - if rtn.empty: - return [] - rtn.sort_values('date', inplace=True) - - last_one = aev.get_last_one(id, max_date=max_date) - if not last_one: - aev.insert(asset_id=id, date=rtn.iloc[0].date, value=rtn.iloc[0].rtn) - last_one = aev.get_last_one(id, max_date=max_date) - last_day = last_one['date'] - if last_day < max_date: - ewma = last_one['value'] - factor = self._config['ewma']['factor'] - for index, row in rtn[rtn['date'] > last_day].iterrows(): - ewma = factor * row['rtn'] + (1 - factor) * ewma - aev.insert(id, row['date'], ewma) - - result = aev.get_list(id, min_date=min_date, max_date=max_date) - return [{'date': x['date'], 'ewma': x['value']} for x in result] - - def get_income_return(self, asset_id, min_date=None, max_date=None): - fund_navs = pd.DataFrame(self._navs.get_fund_navs(fund_ids=asset_id, max_date=max_date)) - if not fund_navs.empty: - fund_navs['rtn'] = fund_navs['nav_cal'] / fund_navs['nav_cal'].shift(self._config['rtn-days']) - 1 - fund_navs.dropna(inplace=True) - if min_date: - fund_navs = fund_navs[fund_navs.nav_date >= pd.to_datetime(min_date)] - fund_navs.rename(columns={'nav_date': 'date', 'nav_cal': 'nav'}, inplace=True) - fund_navs = fund_navs[['date', 'nav', 'rtn']] - return fund_navs.to_dict('records') - return [] diff --git a/asset_pool/dao/asset_ewma_value.py b/asset_pool/dao/asset_ewma_value.py deleted file mode 100644 index 1d52d519d51fa5873c9f08b54519f2b9b3a09ce6..0000000000000000000000000000000000000000 --- a/asset_pool/dao/asset_ewma_value.py +++ /dev/null @@ -1,56 +0,0 @@ -from py_jftech import read, write, where, format_date - -__COLUMNS__ = { - 'aev_id': 'id', - 'aev_date': 'date', - 'aev_asset_id': 'asset_id', - 'aev_value': 'value', -} - - -@write -def insert(asset_id, date, value): - return f''' - insert into asset_ewma_value(aev_date, aev_asset_id, aev_value) - values ('{format_date(date)}', {asset_id}, {value}) - ''' - - -@write -def delete(day=None): - if day: - return f"delete from asset_ewma_value where aev_date >= '{format_date(day)}'" - else: - return 'truncate table asset_ewma_value' - - -@read(one=True) -def get_last_one(asset_id, max_date=None): - sqls = [] - if max_date: - sqls.append(f"aev_date <= '{format_date(max_date)}'") - return f''' - select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from asset_ewma_value - {where(*sqls, aev_asset_id=asset_id)} order by aev_date desc limit 1 - ''' - - -@read -def get_list(asset_id, min_date=None, max_date=None): - sqls = [] - if min_date: - sqls.append(f"aev_date >= '{format_date(min_date)}'") - if max_date: - sqls.append(f"aev_date <= '{format_date(max_date)}'") - return f'''select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from asset_ewma_value {where(*sqls, aev_asset_id=asset_id)}''' - - -@read -def get_last(asset_id, max_date=None, limit=1): - sqls = [] - if max_date: - sqls.append(f"aev_date <= '{format_date(max_date)}'") - return f''' - select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from asset_ewma_value - {where(*sqls, aev_asset_id=asset_id)} order by aev_date desc limit {limit} - ''' diff --git a/asset_pool/dao/asset_risk_dates.py b/asset_pool/dao/asset_risk_dates.py deleted file mode 100644 index 5ae60e077b729974a1d6389fafd4d0ebc5f0c44e..0000000000000000000000000000000000000000 --- a/asset_pool/dao/asset_risk_dates.py +++ /dev/null @@ -1,52 +0,0 @@ -from py_jftech import read, write, where, format_date - -from api import AssetRiskDateType as DateType - -__COLUMNS__ = { - 'ard_id': 'id', - 'ard_date': 'date', - 'ard_type': 'type', - 'ard_asset_id': 'asset_id', -} - - -@write -def insert(asset_id, type: DateType, date): - return f''' - insert into asset_risk_dates(ard_asset_id, ard_type, ard_date) - values ({asset_id}, {type.value}, '{format_date(date)}') - ''' - - -@read -def get_list(asset_ids=None, max_date=None, min_date=None): - sqls = [] - if max_date: - sqls.append(f"rap_date <= '{format_date(max_date)}'") - if min_date: - sqls.append(f"rap_date >= '{format_date(min_date)}'") - return f''' - select {','.join([f"`{x[0]}` as `{x[1]}`" for x in __COLUMNS__.items()])} from asset_risk_dates - {where(*sqls, ard_asset_id=asset_ids)} order by ard_asset_id, ard_date - ''' - - -@read(one=True) -def get_last_one(fund_id, date=None, type: DateType = None): - kwargs = { - 'ard_asset_id': fund_id, - 'ard_type': type.value if type is not None else None - } - sql = f"ard_date <= '{format_date(date)}'" if date else None - return f''' - select {','.join([f"`{x[0]}` as `{x[1]}`" for x in __COLUMNS__.items()])} - from asset_risk_dates {where(sql, **kwargs)} order by ard_date desc, ard_type asc limit 1 - ''' - - -@write -def delete(day=None): - if day: - return f"delete from asset_risk_dates where ard_date >= '{format_date(day)}'" - else: - return 'truncate table asset_risk_dates' diff --git a/asset_pool/dao/mysql.sql b/asset_pool/dao/mysql.sql index 3692409a5a62f12b0265f65abb8b2a03f95587e8..afdd5f736a4e9d1a33c6a5f21fc8337ece6581d7 100644 --- a/asset_pool/dao/mysql.sql +++ b/asset_pool/dao/mysql.sql @@ -13,35 +13,3 @@ CREATE TABLE IF NOT EXISTS robo_assets_pool AUTO_INCREMENT = 0 DEFAULT CHARSET = utf8mb4 COMMENT 'èµ„äº§æ± '; - -CREATE TABLE IF NOT EXISTS asset_risk_dates -( - ard_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - ard_date DATETIME NOT NULL COMMENT '风控日期', - ard_type TINYINT NOT NULL COMMENT '日期类型', - ard_asset_id BIGINT UNSIGNED NOT NULL COMMENT '资产ID', - ard_create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - ard_update_time DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (ard_id), - INDEX (ard_date), - INDEX (ard_type), - INDEX (ard_asset_id) -) ENGINE = InnoDB - AUTO_INCREMENT = 0 - DEFAULT CHARSET = utf8mb4 COMMENT '资产风控日期表'; - - -CREATE TABLE IF NOT EXISTS asset_ewma_value -( - aev_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - aev_date DATETIME NOT NULL COMMENT '日期', - aev_asset_id BIGINT UNSIGNED NOT NULL COMMENT '资产ID', - aev_value DOUBLE NOT NULL COMMENT 'ewma值', - aev_create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - aev_update_time DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (aev_id), - UNIQUE INDEX (aev_asset_id, aev_date), - INDEX (aev_date) -) ENGINE = InnoDB - AUTO_INCREMENT = 0 - DEFAULT CHARSET = utf8mb4 COMMENT '资产EWMAæ•°æ®'; \ No newline at end of file diff --git a/asset_pool/pool.py b/asset_pool/pool.py index b086879189dba40f45b8fd917a869fda7d8046ab..9876c4ee400f746fddae99b4891b33b398f6cdf5 100644 --- a/asset_pool/pool.py +++ b/asset_pool/pool.py @@ -1,10 +1,8 @@ from datetime import datetime as dt -from typing import List -import pandas as pd -from py_jftech import component, autowired, workday_range, parse_date +from py_jftech import component, autowired -from api import AssetPool, AssetOptimize, AssetRisk, RoboReportor, Datum, DatumType, RoboExecutor +from api import AssetPool, AssetOptimize from asset_pool.dao import robo_assets_pool as rap @@ -12,42 +10,11 @@ from asset_pool.dao import robo_assets_pool as rap class FundAssetPool(AssetPool): @autowired - def __init__(self, optimize: AssetOptimize = None, risk: AssetRisk = None): + def __init__(self, optimize: AssetOptimize = None): self._optimize = optimize - self._risk = risk def get_pool(self, day=dt.today()): - opti_pool = self._optimize.get_optimize_pool(day) - risk_pool = self._risk.get_risk_pool(day) - return [x for x in opti_pool if x not in risk_pool] + return self._optimize.get_optimize_pool(day) def clear(self, day=None): rap.delete(day) - - -@component(bean_name='asset-pool-report') -class AssetPoolReportor(RoboReportor): - - @autowired - def __init__(self, optimize: AssetOptimize = None, risk: AssetRisk = None, datum: Datum = None, executor: RoboExecutor = None): - self._optimize = optimize - self._risk = risk - self._datum = datum - self._executor = executor - - @property - def report_name(self) -> str: - return 'åŸºé‡‘æ± ' - - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - datums = self._datum.get_datums(type=DatumType.FUND) - result = [] - for date in workday_range(min_date or self._executor.start_date, max_date): - opts = self._optimize.get_optimize_pool(date) - risk = self._risk.get_risk_pool(date) - datas = {x['bloombergTicker']: 0 if x['id'] in risk else 1 if x['id'] in opts else -1 for x in datums} - result.append({'date': date, **datas}) - return result - - - diff --git a/asset_pool/test_case.py b/asset_pool/test_case.py index 6b72242a154e914092b3f3ec2fd56f5adeba3b6e..a9b3b4b8398c3535516a380facf20f26a75efb37 100644 --- a/asset_pool/test_case.py +++ b/asset_pool/test_case.py @@ -1,27 +1,18 @@ import logging import unittest -from py_jftech import autowired, parse_date, to_str +from py_jftech import autowired, parse_date -from api import AssetPool, RoboReportor, AssetRisk +from api import AssetOptimize logger = logging.getLogger(__name__) class AssetPoolTest(unittest.TestCase): - @autowired - def test_asset_pool(self, pool: AssetPool = None): - result = pool.get_pool(parse_date('2008-11-05')) - - @autowired(names={'report': 'asset-pool-report'}) - def test_pool_report(self, report: RoboReportor = None): - result = report.load_report(max_date=parse_date('2009-12-31')) - logger.info(to_str(result)) - - @autowired - def test_next_risk_date(self, risk: AssetRisk = None): - risk.build_risk_date(asset_id=46) + @autowired(names={'asset': 'dividend'}) + def test_dividend_asset_optimize(self, asset: AssetOptimize = None): + asset.get_optimize_pool(parse_date('2023-03-01')) if __name__ == '__main__': diff --git a/basic/datum.py b/basic/datum.py index e95fc9a5207cab949d484d3e33217e911eb63e4a..dcd4ee2e2060925ab888cbedd081fcacd2cdf496 100644 --- a/basic/datum.py +++ b/basic/datum.py @@ -1,12 +1,10 @@ import json import os -from datetime import datetime as dt -from typing import List import pandas as pd -from py_jftech import component, parse_date, get_config, to_tuple, autowired, get_project_path, transaction +from py_jftech import component, parse_date, get_config, to_tuple, get_project_path, transaction -from api import DatumType, Datum, PortfoliosRisk, RoboReportor, RoboExecutor +from api import DatumType, Datum, PortfoliosRisk, RoboExecutor from basic.dao import robo_base_datum as rbd @@ -55,7 +53,8 @@ class DefaultDatum(Datum): datum_ids = tuple(set(datum_ids or []) | {x['id'] for x in datums}) result = rbd.get_base_datums(type=type, crncy=crncy, risk=risk, datum_ids=datum_ids) result = [{**json.loads(x['datas']), 'id': x['id']} for x in result] - return [self.format_datum(x) for x in result if not exclude or x['id'] in (datum_ids or []) or x['bloombergTicker'] not in self.excludes] + return [self.format_datum(x) for x in result if + not exclude or x['id'] in (datum_ids or []) or x['bloombergTicker'] not in self.excludes] def get_high_risk_datums(self, risk: PortfoliosRisk): risk3 = self.get_datums(type=DatumType.FUND, risk=3) @@ -80,22 +79,3 @@ class DefaultDatum(Datum): })) return True return False - - -@component(bean_name='funds-report') -class FundReportor(RoboReportor): - - @autowired - def __init__(self, datum: Datum = None): - self._datum = datum - - @property - def report_name(self) -> str: - return '基金资料' - - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - datums = self._datum.get_datums(type=DatumType.FUND) - datums = pd.DataFrame(datums) - datums = datums[ - ['id', 'ftTicker', 'bloombergTicker', 'chineseName', 'englishName', 'lipperKey', 'isin', 'currency', 'risk', 'inceptDate', 'category', 'assetType']] - return datums.to_dict('records') diff --git a/basic/navs.py b/basic/navs.py index 4aa49d8c341d281fbf3a49584535439e454b35d7..39b6903b2a1103701a68c51cc4fe33c47819208e 100644 --- a/basic/navs.py +++ b/basic/navs.py @@ -4,7 +4,7 @@ from typing import List import pandas as pd from py_jftech import get_config, component, autowired, to_tuple -from api import Navs, Datum, DatumType, RoboReportor +from api import Navs, Datum, DatumType from basic.dao import robo_exrate as re, robo_fund_navs as rfn, robo_index_datas as rid, robo_eco_datas as red @@ -88,34 +88,3 @@ class DefaultNavs(Navs): else: return red.get_last(eco_id=datum_id, max_date=max_date, count=count, by_release_date=by_release_date) - -@component(bean_name='navs-report') -class NavsReportor(RoboReportor): - - @autowired - def __init__(self, datum: Datum = None, navs: Navs = None): - self._datum = datum; - self._navs = navs - self._config = get_config('reports.navs') - - @property - def report_name(self) -> str: - return "基金净值" - - @property - def tickers(self): - return self._config['tickers'] if 'tickers' in self._config else None - - @property - def type(self): - return DatumType[self._config['type']] - - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - asset_ids = {x['id']: x for x in self._datum.get_datums(ticker=self.tickers, type=self.type)} - if self.type == DatumType.FUND: - result = pd.DataFrame(self._navs.get_fund_navs(fund_ids=tuple(asset_ids.keys()), max_date=max_date, min_date=min_date)) - result = result.pivot_table(index='nav_date', columns='fund_id', values='nav_cal') - result.rename(columns={x[0]: x[1]['bloombergTicker'] for x in asset_ids.items()}, inplace=True) - result.reset_index(inplace=True) - return result.to_dict('records') - return [] diff --git a/basic/test_case.py b/basic/test_case.py index a3cc48e9ad244cab426bc9c85a021291f4271cad..4e8b091b90d3a44b1dcbf384aa65919f6ab2d4da 100644 --- a/basic/test_case.py +++ b/basic/test_case.py @@ -4,7 +4,7 @@ from typing import List from py_jftech import autowired, parse_date, to_str -from api import Navs, Datum, PortfoliosRisk, DataSync, RoboReportor +from api import Navs, Datum, PortfoliosRisk, DataSync logger = logging.getLogger(__name__) @@ -42,16 +42,6 @@ class BasicTest(unittest.TestCase): for sync in syncs: sync.do_sync() - @autowired(names={'report': 'navs-report'}) - def test_export_navs(self, report: RoboReportor = None): - result = report.load_report() - logger.info(to_str(result)) - - @autowired(names={'report': 'funds-report'}) - def test_export_funds(self, report: RoboReportor = None): - result = report.load_report() - logger.info(to_str(result)) - if __name__ == '__main__': unittest.main() diff --git a/config-svrobo3.yml b/config-svrobo3.yml deleted file mode 100644 index b46d7d975fc5fa4eadbfaf633469946f358dc667..0000000000000000000000000000000000000000 --- a/config-svrobo3.yml +++ /dev/null @@ -1,286 +0,0 @@ -py-jftech: - logger: - version: 1 - formatters: - brief: - format: "%(asctime)s - %(levelname)s - %(message)s" - simple: - format: "%(asctime)s - %(filename)s - %(levelname)s - %(message)s" - handlers: - console: - class: logging.StreamHandler - formatter: simple - level: DEBUG - stream: ext://sys.stdout - file: - class: logging.handlers.TimedRotatingFileHandler - level: INFO - formatter: brief - filename: ${LOG_FILE:logs/info.log} - interval: 1 - backupCount: 30 - encoding: utf8 - when: D -# loggers: -# basic.sync: -# level: DEBUG -# handlers: [console] -# propagate: no - root: - level: ${LOG_LEVEL:INFO} - handlers: ${LOG_HANDLERS:[ console ]} - database: - host: ${MYSQL_HOST:localhost} - port: ${MYSQL_PORT:3306} - user: ${MYSQL_USER:root} - password: ${MYSQL_PWD:123456} - dbname: ${MYSQL_DBNAME:jftech_robo} - injectable: - types: - api.PortfoliosBuilder: portfolios.builder.PoemPortfoliosBuilder - email: - server: smtphz.qiye.163.com - user: jft-ra@thizgroup.com - password: 5dbb#30ec6d3 - mulit-process: - max-workers: ${MAX_PROCESS:8} -basic: # åŸºç¡€ä¿¡æ¯æ¨¡å— - sync: - start-date: 2007-01-01 # åŒæ¥æ•°æ®å¼€å§‹æ—¥æœŸ - datum: # èµ„æ–™æ¨¡å— - change: - date: ${DATUM_CHANGE_DATE} - file: ${DATUM_CHANGE_FILE} - excludes: # 排除的资料å½åšticker - backtest: - - 'FKUQX US Equity' - - 'FTAAUSH LX Equity' - - 'FTJAPAU LX Equity' - - 'TEGAUH1 LX Equity' - - 'TMEEAAU LX Equity' - - 'TEUSAAU LX Equity' - - 'FTEAUH1 LX Equity' - - 'TFIAAUS LX Equity' - real: - - 'FGFSACU LX Equity' - - 'TMEEAAU LX Equity' - - 'FTEAMUH LX Equity' - - 'FKUTX US Equity' - - 'TEMUSGI LX Equity' - - 'TEMFIAI LX Equity' - - 'TEMGROA LX Equity' - - 'TEMFMEA LX Equity' - - 'TEMDGAA LX Equity' - - 'TEMJAAU LX Equity' - - 'TEMFHAC LX Equity' - - 'TEMLATA LX Equity' - - 'LEPEUAA ID Equity' - - 'LGBOAAU ID Equity' - navs: # å‡€å€¼æ¨¡å— - exrate: # 汇率,如果ä¸å¼€å¯ï¼Œæ•´ä¸ªè¿™å—注释掉 - - from: EUR # 需è¦è½¬æ¢çš„è´§å¸ç±»åž‹ - ticker: EURUSD BGN Curncy # 汇率值的å½åšticker -asset-pool: # èµ„äº§æ± æ¨¡å— - asset-optimize: # èµ„äº§ä¼˜é€‰æ¨¡å— - sortino-weight: # sortino计算需è¦çš„æƒé‡ï¼Œä¸‹é¢æ¯ä¸€æ¡ä¸ºä¸€æ¬¡è®¡ç®—,e.g. months: 3, weight: 0.5 表示 3个月数æ®ä½¿ç”¨æƒé‡0.5æ¥è®¡ç®—分值 - - months: 3 - weight: 0.5 - - months: 6 - weight: 0.3 - - years: 1 - weight: 0.2 - asset-risk: # èµ„äº§é£ŽæŽ§æ¨¡å— - advance-months: 3 # 计算资产风控,需è¦å‡€å€¼æå‰å¼€å§‹æ—¶é—´å¤šå°‘个月 - rtn-days: 5 # 滚动计算回报率的天数 - ewma: # ewma相关 - condition-total: 6 # æŸ¥çœ‹å¤šå°‘å¤©çš„æ•°æ® - condition-meet: 4 # 有多少天满足æ¡ä»¶åˆ™è§¦å‘ - factor: 0.3 # ewmaè®¡ç®—å› å - threshold: 0 # 满足æ¡ä»¶é˜€å€¼ - cvar: # cvar相关 - min-volume: 30 # 计算cvar时净值最少数æ®é‡ - threshold: -0.03 # 满足æ¡ä»¶é˜€å€¼ - coef: 0.95 # 计算cvar的系数 -portfolios: # æŠ•ç»„æ¨¡å— - holder: # æŒä»“投组相关 - init-nav: 100 # åˆå§‹é‡‘é¢ - min-interval-days: 10 # 两次实际调仓最å°é—´éš”æœŸï¼Œå•ä½äº¤æ˜“æ—¥ - solver: # 解算器相关 - tol: 1E-10 # 误差满足æ¡ä»¶ - navs: # å‡€å€¼è¦æ±‚ - range: # 需è¦å‡€å€¼æ•°æ®çš„区间, days: 90 表示90自然日,months: 3 表示3个自然月 - days: 90 - max-nan: # 最大缺失净值æ¡ä»¶ - asset: 8 # å•一资产最多缺少多少交易日数æ®ï¼Œåˆ™è¸¢å‡ºèµ„äº§æ± - day: 0.5 # å•ä¸€äº¤æ˜“æ—¥æœ€å¤šç¼ºå°‘ç™¾åˆ†ä¹‹å¤šå°‘å‡€å€¼ï¼Œåˆ™åˆ é™¤è¯¥äº¤æ˜“æ—¥ - risk: # 资产风险ç‰çº§è¦æ±‚,å¯åˆ†å¼€å†™ä¹Ÿå¯ä»¥åˆå¹¶å†™ï¼Œe.g. risk:[ 2, 3 ] 则表示 所有投组资产风险ç‰çº§éƒ½æ˜¯ 2 或 3 - ft3: [ 2, 3 ] # ç±»ä¼¼è¿™æ ·å†™ï¼Œåªè¦æ±‚ft3的投组资产风险ç‰çº§ - ft6: [ 2, 3, 4 ] - ft9: [ 2, 3, 4, 5 ] - matrix-rtn-days: 20 # 计算回报率矩阵时,回报率滚动天数 - asset-count: 5 # 投组资产个数。e.g. count 或 [min, max] 分别表示 最大最å°éƒ½ä¸ºcount 或 最å°ä¸ºmin 最大为max,å¦å¤–这里也å¯ä»¥ç±»ä¼¼ä¸Šé¢ç»™ä¸åŒé£Žé™©ç‰çº§åˆ†åˆ«é…ç½® - mpt: # mpt计算相关 - cvar-beta: 0.2 # 计算Kbeta 需è¦ç”¨åˆ° - quantile: 0.9 # 分ä½ç‚¹ï¼Œä¹Ÿå¯ä»¥ç»™ä¸åŒé£Žé™©ç‰çº§åˆ†åˆ«é…ç½® - low-weight: 0.05 # 最低æƒé‡ - high-weight: [ 1, 0.6, 0.35 ] # 最高æƒé‡æ¯”例,å¯ç»™ä¸€ä¸ªå€¼ï¼Œä¹Ÿå¯ä»¥ç»™å¤šä¸ªå€¼ï¼Œå½“å¤šä¸ªå€¼æ—¶ï¼Œç¬¬ä¸€ä¸ªè¡¨ç¤ºåªæœ‰ä¸€ä¸ªèµ„产时æƒé‡ï¼Œç¬¬äºŒä¸ªè¡¨ç¤ºåªæœ‰ä¸¤ä¸ªèµ„产时æƒé‡ï¼Œä»¥æ¤ç±»æŽ¨ï¼Œæœ€åŽä¸€ä¸ªè¡¨ç¤ºå…¶ä»–资产个数时的æƒé‡ - poem: # poem相关 - cvar-scale-factor: 0.1 # 计算时用到的系数 - right_side: # 这里表示å³ä¾§ç±»åž‹æŠ•ç»„ç”¨åˆ°çš„å‚æ•°ï¼Œè¿™é‡Œæœ‰åˆ™ä¼˜å…ˆç”¨è¿™é‡Œçš„傿•°ï¼Œå¦‚果没有,则用上é¢é»˜è®¤å‚æ•°ï¼Œå‚æ•°å«ä¹‰å’Œä¸Šé¢ä¸€è‡´ - navs: - risk: [1, 2] - exclude-asset-type: ['STOCK', 'BALANCED'] # 排除的资产类型 - mpt: - quantile: 0.3 - crisis_1: # 屿œº1ç›¸å…³ï¼Œè¿™é‡Œæœ‰åˆ™ä¼˜å…ˆç”¨è¿™é‡Œçš„å‚æ•°ï¼Œå¦‚果没有,则用上é¢é»˜è®¤å‚æ•°ï¼Œå‚æ•°å«ä¹‰å’Œä¸Šé¢ä¸€è‡´ - navs: - risk: [1, 2] - mpt: - quantile: 0.1 - crisis_2: # 屿œº2ç›¸å…³ï¼Œè¿™é‡Œæœ‰åˆ™ä¼˜å…ˆç”¨è¿™é‡Œçš„å‚æ•°ï¼Œå¦‚果没有,则用上é¢é»˜è®¤å‚æ•°ï¼Œå‚æ•°å«ä¹‰å’Œä¸Šé¢ä¸€è‡´ - navs: - risk: [ 1, 2 ] - mpt: - quantile: 0.1 -rebalance: # å†å¹³è¡¡æ¨¡å— - drift-solver: # drift解算器相关 - date-curve: # 日期曲线drift相关 - diff-threshold: 0.4 # æƒé‡ç›¸å·®åˆå§‹é˜€å€¼ - init-factor: 0.000000002 # æƒé‡ç›¸å·®é€’å‡å› å - high-weight: # 高风险资产æƒé‡drift相关 - coef: 0.2 # drift系数 - ruler: # å†å¹³è¡¡ä¿¡å·é€‰ç”¨è§„则 - disable-period: 10 # ç¦æ¢ä¹°å…¥æœŸï¼Œäº¤æ˜“æ—¥ -# normal: 10 # æ ‡å‡†æŠ•ç»„ç¦æ¢ä¹°å…¥æœŸ -# crisis_1: 15 # 屿œº1æŠ•ç»„ç¦æ¢ä¹°å…¥æœŸ -# crisis_2: 15 # 屿œº2æŠ•ç»„ç¦æ¢ä¹°å…¥æœŸ -# right_side: 15 # å³ä¾§æŠ•ç»„ç¦æ¢ä¹°å…¥æœŸ - signals: # ä¿¡å·ç›¸å…³ - crisis-signal: # 屿œºä¿¡å·ç›¸å…³ - exp-years: 3 # é¢„è¦æœŸæ—¶é•¿ï¼Œå•ä½è‡ªç„¶å¹´ï¼Œç‚¹åˆ°ç‚¹è®¡ç®— - exp-init: 2022-03-04 # è®¾ç½®èµ·å§‹å±æœºé¢„è¦å¼€å§‹æ—¶é—´ï¼Œå¦‚果关é—åˆå§‹é¢„è¦èµ·ï¼Œæ³¨é‡Šåˆ°è¿™ä¸€æ¡å³å¯ - inversion-years: 1 # 利率倒挂计算时长,å•ä½è‡ªç„¶å¹´ï¼Œç‚¹åˆ°ç‚¹å–值 - inversion-threshold: 0.3 # 利率倒挂触å‘阀值 - crisis-1: # 屿œº1相关 - mean-count: 850 # spx去多少交易日计算平å‡å€¼ - threshold: -0.05 # spx计算åŽè·Œç ´é˜€å€¼ - crisis-2: # 屿œº2相关 - negative-growth: 1 # 实际利率负增长时长,å•ä½å¹´ï¼Œç‚¹åˆ°ç‚¹å–值 - fed-months: 3 # fed 滚动月份,点到点å–值 - fed-threshold: -0.75 # fed判æ–阀值 - right-side: # 市场å³ä¾§ç›¸å…³ - rtn-days: 5 # 计算spx回报率滚动天数,交易日 - min-threshold: -0.05 # spxå›žæŠ¥çŽ‡è·Œç ´é˜€å€¼ - coef: 0.95 # 计算cvar的系数 - cvar-min-volume: 30 # 计算cvar至少需è¦å¤šå°‘äº¤æ˜“æ—¥æ•°æ® - high-low-buy: # 高低买入相关 - threshold: # [ 低买入阀值,高买入阀值 ] - ft3: [ 0.5, 0.8 ] - ft6: [ 0.55, 0.8 ] - ft9: [ 0.55, 0.8 ] -reports: # 报告模å—相关 - navs: - type: FUND - tickers: - - TEMTECI LX Equity - - TEPLX US Equity - - FRDPX US Equity - - FKRCX US Equity - - FTNRACU LX Equity - benchmark: # benchmark报告 - init-amount: 100 # åˆå§‹é‡‘é¢ - stock-rate: # stock型基金比例 - RR3: 0.3 - RR4: 0.5 - RR5: 0.7 - fixed-range: # 固定区间收益率 - range-dates: # å›ºå®šèµ·å§‹æˆªæ¢æ—¥æœŸ - - start: 2008-01-01 - end: 2008-10-27 - - start: 2011-05-02 - end: 2011-10-04 - - start: 2013-05-08 - end: 2013-06-24 - - start: 2014-09-03 - end: 2014-12-16 - - start: 2015-04-28 - end: 2016-01-21 - - start: 2018-01-26 - end: 2018-10-29 - - start: 2020-01-20 - end: 2020-03-23 - relative-range: # 相对区间收益率 - range-dates: # 相对时间周期 - - months: 1 - name: '一个月' - - months: 3 - name: '三个月' - - months: 6 - name: 'å…个月' - - years: 1 - name: '一年' - - years: 2 - name: '两年' - - years: 3 - name: '三年' - - years: 5 - name: '五年' - - years: 10 - name: 'åå¹´' - exports: - backtest: # 回测导出曹ç–ç•¥ - save-path: ${EXPORT_PATH:excels} # å¯¼å‡ºæŠ¥å‘Šæ–‡ä»¶å˜æ”¾è·¯å¾„,如果以./或者../开头,则会以执行pythonæ–‡ä»¶ä¸ºæ ¹ç›®å½•ï¼Œå¦‚æžœä»¥/开头,则为系统ç»å¯¹è·¯å¾„,å¦åˆ™ï¼Œä»¥é¡¹ç›®ç›®å½•ä¸ºæ ¹ç›®å½• - file-name: ${EXPORT_FILENAME:real} - include-report: # 需è¦å¯¼å‡ºçš„æŠ¥å‘Šç±»åž‹åˆ—表,下é¢çš„顺åºï¼Œä¹Ÿä»£è¡¨äº†excelä¸sheetçš„é¡ºåº - # - funds-report # 基金资料 - # - navs-report # 净值报告 - - hold-report # æŒä»“报告 - - signal-report # ä¿¡å·æŠ¥å‘Š - - asset-pool-report # åŸºé‡‘æ± æŠ¥å‘Š - - mpt-report # 最优投组报告 - - benckmark-report # benckmark报告 - - indicators-report # å„ç§ç‰¹æ®ŠæŒ‡æ ‡æŠ¥å‘Š - - fixed-range-report # 固定区间收益报告 - - relative-range-report # 相对区间收益报告 - real-daily: - file-name: ${EXPORT_FILENAME:svrobo3_portfolios} - include-report: - - daily-hold-report - - daily-signal-report - email: - receives: - - jichao@thizgroup.com -# copies: -# - Tony.Wu.Home@gmail.com -# - jinghan.yang@chifuinvestments.com -# - will.xu@thizgroup.com -# - brody_wu@chifufund.com -# - telan_qian@chifufund.com -# - tina.yang@thizgroup.com - subject: - default: "ROBO_TAIBEI-实盘版-æ¯æ—¥æŠ•組推薦_{today}" - rebalance: "ROBO_TAIBEI-实盘版-æ¯æ—¥æŠ•組推薦_{today}_今日有調倉信號ï¼ï¼ï¼" - content: - default: "Dear All: 附件是今天生æˆçš„æŽ¨è–¦çµ„åˆï¼Œè«‹é©—收,è¬è¬ï¼ 注>:該郵件為自動發é€ï¼Œå¦‚有å•題請è¯ç¹«çŸ½è°·åœ˜éšŠ telan_qian@chifufund.com" - rebalance: "Dear All: 附件是今天生æˆçš„æŽ¨è–¦çµ„åˆä»¥åŠèª¿å€‰ä¿¡è™Ÿï¼Œè«‹é©—收,è¬è¬ï¼ 注>:該郵件為自動發é€ï¼Œå¦‚有å•題請è¯ç¹«çŸ½è°·åœ˜éšŠ telan_qian@chifufund.com" -robo-executor: # 执行器相关 - use: ${ROBO_EXECUTOR:real} # 执行哪个执行器,优先å–系统环境å˜é‡ROBO_EXECUTOR的值,默认backtest - sync-data: ${SYNC_DATA:on} # 是å¦å¼€å¯åŒæ¥èµ„æ–™æ•°æ® - backtest: # 回测执行器相关 - start-date: 2022-09-01 # 回测起始日期 - end-date: 2023-02-20 # å›žæµ‹æˆªæ¢æ—¥æœŸ - start-step: ${BACKTEST_START_STEP:4} # 回测从哪一æ¥å¼€å§‹æ‰§è¡Œ 1:计算资产ewmaï¼›2:è®¡ç®—èµ„äº§æ± ï¼›3:计算最优投组:4:计算å†å¹³è¡¡ä¿¡å·ä»¥åŠæŒä»“投组 - end-step: ${BACKTEST_END_STEP:4} # å›žæµ‹ä»Žå“ªä¸€æ¥æ‰§è¡Œå®ŒæˆåŽç»“æŸæ‰§è¡Œ 1:计算资产ewmaï¼›2:è®¡ç®—èµ„äº§æ± ï¼›3:计算最优投组:4:计算å†å¹³è¡¡ä¿¡å·ä»¥åŠæŒä»“投组 - clean-up: off - real: # 实盘执行器 - start-date: 2022-09-01 # 实盘开始时间 - include-date: - - 2023-02-18 - - 2023-03-25 - - 2023-06-17 - - 2023-09-23 - - 2008-01-05 - - - - diff --git a/config-svrobo4.yml b/config-svrobo4.yml deleted file mode 100644 index e8f9a73630de7f74040cb56eea8d207f23bb8f50..0000000000000000000000000000000000000000 --- a/config-svrobo4.yml +++ /dev/null @@ -1,265 +0,0 @@ -py-jftech: - logger: - version: 1 - formatters: - brief: - format: "%(asctime)s - %(levelname)s - %(message)s" - simple: - format: "%(asctime)s - %(filename)s - %(levelname)s - %(message)s" - handlers: - console: - class: logging.StreamHandler - formatter: simple - level: DEBUG - stream: ext://sys.stdout - file: - class: logging.handlers.TimedRotatingFileHandler - level: INFO - formatter: brief - filename: ${LOG_FILE:logs/info.log} - interval: 1 - backupCount: 30 - encoding: utf8 - when: D -# loggers: -# basic.sync: -# level: DEBUG -# handlers: [console] -# propagate: no - root: - level: ${LOG_LEVEL:INFO} - handlers: ${LOG_HANDLERS:[ console ]} - database: - host: ${MYSQL_HOST:localhost} - port: ${MYSQL_PORT:3306} - user: ${MYSQL_USER:root} - password: ${MYSQL_PWD:123456} - dbname: ${MYSQL_DBNAME:jftech_robo} - injectable: - types: - api.PortfoliosBuilder: portfolios.builder.PoemPortfoliosBuilder - email: - server: smtphz.qiye.163.com - user: jft-ra@thizgroup.com - password: 5dbb#30ec6d3 - mulit-process: - max-workers: ${MAX_PROCESS:8} -basic: # åŸºç¡€ä¿¡æ¯æ¨¡å— - sync: - start-date: 2007-01-01 # åŒæ¥æ•°æ®å¼€å§‹æ—¥æœŸ - datum: # èµ„æ–™æ¨¡å— - change: - date: ${DATUM_CHANGE_DATE} - file: ${DATUM_CHANGE_FILE} - excludes: # 排除的资料å½åšticker - backtest: - - 'FKUQX US Equity' - - 'FTAAUSH LX Equity' - - 'FTJAPAU LX Equity' - - 'TEGAUH1 LX Equity' - - 'TMEEAAU LX Equity' - - 'TEUSAAU LX Equity' - - 'FTEAUH1 LX Equity' - - 'TFIAAUS LX Equity' - real: - - 'FGFSACU LX Equity' - - 'TMEEAAU LX Equity' - - 'FTEAMUH LX Equity' - - 'FKUTX US Equity' - - 'TEMUSGI LX Equity' - - 'TEMFIAI LX Equity' - - 'TEMGROA LX Equity' - - 'TEMFMEA LX Equity' - - 'TEMDGAA LX Equity' - - 'TEMJAAU LX Equity' - - 'TEMFHAC LX Equity' - - 'TEMLATA LX Equity' - - 'LEPEUAA ID Equity' - - 'LGBOAAU ID Equity' - navs: # å‡€å€¼æ¨¡å— - exrate: # 汇率,如果ä¸å¼€å¯ï¼Œæ•´ä¸ªè¿™å—注释掉 - - from: EUR # 需è¦è½¬æ¢çš„è´§å¸ç±»åž‹ - ticker: EURUSD BGN Curncy # 汇率值的å½åšticker -asset-pool: # èµ„äº§æ± æ¨¡å— - asset-optimize: # èµ„äº§ä¼˜é€‰æ¨¡å— - sortino-weight: # sortino计算需è¦çš„æƒé‡ï¼Œä¸‹é¢æ¯ä¸€æ¡ä¸ºä¸€æ¬¡è®¡ç®—,e.g. months: 3, weight: 0.5 表示 3个月数æ®ä½¿ç”¨æƒé‡0.5æ¥è®¡ç®—分值 - - months: 3 - weight: 0.5 - - months: 6 - weight: 0.3 - - years: 1 - weight: 0.2 - asset-risk: # èµ„äº§é£ŽæŽ§æ¨¡å— - advance-months: 3 # 计算资产风控,需è¦å‡€å€¼æå‰å¼€å§‹æ—¶é—´å¤šå°‘个月 - rtn-days: 5 # 滚动计算回报率的天数 - ewma: # ewma相关 - condition-total: 6 # æŸ¥çœ‹å¤šå°‘å¤©çš„æ•°æ® - condition-meet: 4 # 有多少天满足æ¡ä»¶åˆ™è§¦å‘ - factor: 0.3 # ewmaè®¡ç®—å› å - threshold: 0 # 满足æ¡ä»¶é˜€å€¼ - cvar: # cvar相关 - min-volume: 30 # 计算cvar时净值最少数æ®é‡ - threshold: -0.03 # 满足æ¡ä»¶é˜€å€¼ - coef: 0.95 # 计算cvar的系数 -portfolios: # æŠ•ç»„æ¨¡å— - holder: # æŒä»“投组相关 - init-nav: 100 # åˆå§‹é‡‘é¢ - min-interval-days: 10 # 两次实际调仓最å°é—´éš”æœŸï¼Œå•ä½äº¤æ˜“æ—¥ - solver: # 解算器相关 - tol: 1E-10 # 误差满足æ¡ä»¶ - navs: # å‡€å€¼è¦æ±‚ - range: # 需è¦å‡€å€¼æ•°æ®çš„区间, days: 90 表示90自然日,months: 3 表示3个自然月 - days: 90 - max-nan: # 最大缺失净值æ¡ä»¶ - asset: 8 # å•一资产最多缺少多少交易日数æ®ï¼Œåˆ™è¸¢å‡ºèµ„äº§æ± - day: 0.5 # å•ä¸€äº¤æ˜“æ—¥æœ€å¤šç¼ºå°‘ç™¾åˆ†ä¹‹å¤šå°‘å‡€å€¼ï¼Œåˆ™åˆ é™¤è¯¥äº¤æ˜“æ—¥ - risk: # 资产风险ç‰çº§è¦æ±‚,å¯åˆ†å¼€å†™ä¹Ÿå¯ä»¥åˆå¹¶å†™ï¼Œe.g. risk:[ 2, 3 ] 则表示 所有投组资产风险ç‰çº§éƒ½æ˜¯ 2 或 3 - ft3: [ 2, 3 ] # ç±»ä¼¼è¿™æ ·å†™ï¼Œåªè¦æ±‚ft3的投组资产风险ç‰çº§ - ft6: [ 2, 3, 4 ] - ft9: [ 2, 3, 4, 5 ] - matrix-rtn-days: 20 # 计算回报率矩阵时,回报率滚动天数 - asset-count: 5 # 投组资产个数。e.g. count 或 [min, max] 分别表示 最大最å°éƒ½ä¸ºcount 或 最å°ä¸ºmin 最大为max,å¦å¤–这里也å¯ä»¥ç±»ä¼¼ä¸Šé¢ç»™ä¸åŒé£Žé™©ç‰çº§åˆ†åˆ«é…ç½® - mpt: # mpt计算相关 - cvar-beta: 0.2 # 计算Kbeta 需è¦ç”¨åˆ° - quantile: 0.9 # 分ä½ç‚¹ï¼Œä¹Ÿå¯ä»¥ç»™ä¸åŒé£Žé™©ç‰çº§åˆ†åˆ«é…ç½® - low-weight: 0.05 # 最低æƒé‡ - high-weight: [ 1, 0.6, 0.35 ] # 最高æƒé‡æ¯”例,å¯ç»™ä¸€ä¸ªå€¼ï¼Œä¹Ÿå¯ä»¥ç»™å¤šä¸ªå€¼ï¼Œå½“å¤šä¸ªå€¼æ—¶ï¼Œç¬¬ä¸€ä¸ªè¡¨ç¤ºåªæœ‰ä¸€ä¸ªèµ„产时æƒé‡ï¼Œç¬¬äºŒä¸ªè¡¨ç¤ºåªæœ‰ä¸¤ä¸ªèµ„产时æƒé‡ï¼Œä»¥æ¤ç±»æŽ¨ï¼Œæœ€åŽä¸€ä¸ªè¡¨ç¤ºå…¶ä»–资产个数时的æƒé‡ - poem: # poem相关 - cvar-scale-factor: 0.1 # 计算时用到的系数 - right_side: # 这里表示å³ä¾§ç±»åž‹æŠ•ç»„ç”¨åˆ°çš„å‚æ•°ï¼Œè¿™é‡Œæœ‰åˆ™ä¼˜å…ˆç”¨è¿™é‡Œçš„傿•°ï¼Œå¦‚果没有,则用上é¢é»˜è®¤å‚æ•°ï¼Œå‚æ•°å«ä¹‰å’Œä¸Šé¢ä¸€è‡´ - navs: - risk: [1, 2] - exclude-asset-type: ['STOCK', 'BALANCED'] # 排除的资产类型 - mpt: - quantile: 0.3 - crisis_1: # 屿œº1ç›¸å…³ï¼Œè¿™é‡Œæœ‰åˆ™ä¼˜å…ˆç”¨è¿™é‡Œçš„å‚æ•°ï¼Œå¦‚果没有,则用上é¢é»˜è®¤å‚æ•°ï¼Œå‚æ•°å«ä¹‰å’Œä¸Šé¢ä¸€è‡´ - navs: - risk: [1, 2] - mpt: - quantile: 0.1 - crisis_2: # 屿œº2ç›¸å…³ï¼Œè¿™é‡Œæœ‰åˆ™ä¼˜å…ˆç”¨è¿™é‡Œçš„å‚æ•°ï¼Œå¦‚果没有,则用上é¢é»˜è®¤å‚æ•°ï¼Œå‚æ•°å«ä¹‰å’Œä¸Šé¢ä¸€è‡´ - navs: - risk: [ 1, 2 ] - mpt: - quantile: 0.1 -rebalance: # å†å¹³è¡¡æ¨¡å— - drift-solver: # drift解算器相关 - date-curve: # 日期曲线drift相关 - diff-threshold: 0.4 # æƒé‡ç›¸å·®åˆå§‹é˜€å€¼ - init-factor: 0.000000002 # æƒé‡ç›¸å·®é€’å‡å› å - high-weight: # 高风险资产æƒé‡drift相关 - coef: 0.2 # drift系数 - ruler: # å†å¹³è¡¡ä¿¡å·é€‰ç”¨è§„则 - disable-period: 10 # ç¦æ¢ä¹°å…¥æœŸï¼Œäº¤æ˜“æ—¥ -# normal: 10 # æ ‡å‡†æŠ•ç»„ç¦æ¢ä¹°å…¥æœŸ -# crisis_1: 15 # 屿œº1æŠ•ç»„ç¦æ¢ä¹°å…¥æœŸ -# crisis_2: 15 # 屿œº2æŠ•ç»„ç¦æ¢ä¹°å…¥æœŸ -# right_side: 15 # å³ä¾§æŠ•ç»„ç¦æ¢ä¹°å…¥æœŸ - signals: # ä¿¡å·ç›¸å…³ - crisis-signal: # 屿œºä¿¡å·ç›¸å…³ - exp-years: 3 # é¢„è¦æœŸæ—¶é•¿ï¼Œå•ä½è‡ªç„¶å¹´ï¼Œç‚¹åˆ°ç‚¹è®¡ç®— - exp-init: 2022-03-04 # è®¾ç½®èµ·å§‹å±æœºé¢„è¦å¼€å§‹æ—¶é—´ï¼Œå¦‚果关é—åˆå§‹é¢„è¦èµ·ï¼Œæ³¨é‡Šåˆ°è¿™ä¸€æ¡å³å¯ - inversion-years: 1 # 利率倒挂计算时长,å•ä½è‡ªç„¶å¹´ï¼Œç‚¹åˆ°ç‚¹å–值 - inversion-threshold: 0.3 # 利率倒挂触å‘阀值 - crisis-1: # 屿œº1相关 - mean-count: 850 # spx去多少交易日计算平å‡å€¼ - threshold: -0.05 # spx计算åŽè·Œç ´é˜€å€¼ - crisis-2: # 屿œº2相关 - negative-growth: 1 # 实际利率负增长时长,å•ä½å¹´ï¼Œç‚¹åˆ°ç‚¹å–值 - fed-months: 3 # fed 滚动月份,点到点å–值 - fed-threshold: -0.75 # fed判æ–阀值 - right-side: # 市场å³ä¾§ç›¸å…³ - rtn-days: 5 # 计算spx回报率滚动天数,交易日 - min-threshold: -0.05 # spxå›žæŠ¥çŽ‡è·Œç ´é˜€å€¼ - coef: 0.95 # 计算cvar的系数 - cvar-min-volume: 30 # 计算cvar至少需è¦å¤šå°‘äº¤æ˜“æ—¥æ•°æ® - high-low-buy: # 高低买入相关 - threshold: # [ 低买入阀值,高买入阀值 ] - ft3: [ 0.5, 0.8 ] - ft6: [ 0.55, 0.8 ] - ft9: [ 0.55, 0.8 ] -reports: # 报告模å—相关 - navs: - type: FUND - tickers: - - TEMTECI LX Equity - - TEPLX US Equity - - FRDPX US Equity - - FKRCX US Equity - - FTNRACU LX Equity - benchmark: # benchmark报告 - init-amount: 100 # åˆå§‹é‡‘é¢ - stock-rate: # stock型基金比例 - RR3: 0.3 - RR4: 0.5 - RR5: 0.7 - fixed-range: # 固定区间收益率 - range-dates: # å›ºå®šèµ·å§‹æˆªæ¢æ—¥æœŸ - - start: 2008-01-01 - end: 2008-10-27 - - start: 2011-05-02 - end: 2011-10-04 - - start: 2013-05-08 - end: 2013-06-24 - - start: 2014-09-03 - end: 2014-12-16 - - start: 2015-04-28 - end: 2016-01-21 - - start: 2018-01-26 - end: 2018-10-29 - - start: 2020-01-20 - end: 2020-03-23 - relative-range: # 相对区间收益率 - range-dates: # 相对时间周期 - - months: 1 - name: '一个月' - - months: 3 - name: '三个月' - - months: 6 - name: 'å…个月' - - years: 1 - name: '一年' - - years: 2 - name: '两年' - - years: 3 - name: '三年' - - years: 5 - name: '五年' - - years: 10 - name: 'åå¹´' - backtest: # 回测导出曹ç–ç•¥ - exist-build: on # 如果报告文件å˜åœ¨ï¼Œæ˜¯å¦é‡æ–°æž„建文件 - save-path: ${EXPORT_PATH:excels} # å¯¼å‡ºæŠ¥å‘Šæ–‡ä»¶å˜æ”¾è·¯å¾„,如果以./或者../开头,则会以执行pythonæ–‡ä»¶ä¸ºæ ¹ç›®å½•ï¼Œå¦‚æžœä»¥/开头,则为系统ç»å¯¹è·¯å¾„,å¦åˆ™ï¼Œä»¥é¡¹ç›®ç›®å½•ä¸ºæ ¹ç›®å½• - file-name: ${EXPORT_FILENAME:real} - include-report: # 需è¦å¯¼å‡ºçš„æŠ¥å‘Šç±»åž‹åˆ—表,下é¢çš„顺åºï¼Œä¹Ÿä»£è¡¨äº†excelä¸sheetçš„é¡ºåº -# - funds-report # 基金资料 -# - navs-report # 净值报告 - - hold-report # æŒä»“报告 - - signal-report # ä¿¡å·æŠ¥å‘Š - - asset-pool-report # åŸºé‡‘æ± æŠ¥å‘Š - - mpt-report # 最优投组报告 - - benckmark-report # benckmark报告 - - indicators-report # å„ç§ç‰¹æ®ŠæŒ‡æ ‡æŠ¥å‘Š - - fixed-range-report # 固定区间收益报告 - - relative-range-report # 相对区间收益报告 -robo-executor: # 执行器相关 - use: ${ROBO_EXECUTOR:real} # 执行哪个执行器,优先å–系统环境å˜é‡ROBO_EXECUTOR的值,默认backtest - sync-data: ${SYNC_DATA:off} # 是å¦å¼€å¯åŒæ¥èµ„æ–™æ•°æ® - backtest: # 回测执行器相关 - start-date: 2022-09-01 # 回测起始日期 - end-date: 2023-02-20 # å›žæµ‹æˆªæ¢æ—¥æœŸ - start-step: ${BACKTEST_START_STEP:4} # 回测从哪一æ¥å¼€å§‹æ‰§è¡Œ 1:计算资产ewmaï¼›2:è®¡ç®—èµ„äº§æ± ï¼›3:计算最优投组:4:计算å†å¹³è¡¡ä¿¡å·ä»¥åŠæŒä»“投组 - end-step: ${BACKTEST_END_STEP:4} # å›žæµ‹ä»Žå“ªä¸€æ¥æ‰§è¡Œå®ŒæˆåŽç»“æŸæ‰§è¡Œ 1:计算资产ewmaï¼›2:è®¡ç®—èµ„äº§æ± ï¼›3:计算最优投组:4:计算å†å¹³è¡¡ä¿¡å·ä»¥åŠæŒä»“投组 - clean-up: off - real: # 实盘执行器 - start-date: 2022-09-01 # 实盘开始时间 - include-date: - - 2023-02-18 - - 2023-03-25 - - 2023-06-17 - - 2023-09-23 - - 2008-01-05 - - - - diff --git a/config-svrobo5.yml b/config-svrobo5.yml new file mode 100644 index 0000000000000000000000000000000000000000..45cdd7d466eb6056427960b083892751ab0689f7 --- /dev/null +++ b/config-svrobo5.yml @@ -0,0 +1,119 @@ +py-jftech: + logger: + version: 1 + formatters: + brief: + format: "%(asctime)s - %(levelname)s - %(message)s" + simple: + format: "%(asctime)s - %(filename)s - %(levelname)s - %(message)s" + handlers: + console: + class: logging.StreamHandler + formatter: simple + level: DEBUG + stream: ext://sys.stdout + file: + class: logging.handlers.TimedRotatingFileHandler + level: INFO + formatter: brief + filename: ${LOG_FILE:logs/info.log} + interval: 1 + backupCount: 30 + encoding: utf8 + when: D +# loggers: +# basic.sync: +# level: DEBUG +# handlers: [console] +# propagate: no + root: + level: ${LOG_LEVEL:INFO} + handlers: ${LOG_HANDLERS:[ console ]} + database: + host: ${MYSQL_HOST:192.168.68.81} + port: ${MYSQL_PORT:3306} + user: ${MYSQL_USER:root} + password: ${MYSQL_PWD:changeit} + dbname: ${MYSQL_DBNAME:jftech_robo} + injectable: + types: + api.PortfoliosBuilder: portfolios.builder.PoemPortfoliosBuilder + email: + server: smtphz.qiye.163.com + user: jft-ra@thizgroup.com + password: 5dbb#30ec6d3 + mulit-process: + max-workers: ${MAX_PROCESS:4} +basic: # åŸºç¡€ä¿¡æ¯æ¨¡å— + sync: + start-date: 2007-01-01 # åŒæ¥æ•°æ®å¼€å§‹æ—¥æœŸ + datum: # èµ„æ–™æ¨¡å— + change: + date: ${DATUM_CHANGE_DATE} + file: ${DATUM_CHANGE_FILE} + excludes: # 排除的资料å½åšticker + backtest: + - 'TEMUSGI LX Equity' + real: + - 'FGFSACU LX Equity' +# navs: # å‡€å€¼æ¨¡å— +# exrate: # 汇率,如果ä¸å¼€å¯ï¼Œæ•´ä¸ªè¿™å—注释掉 +# - from: EUR # 需è¦è½¬æ¢çš„è´§å¸ç±»åž‹ +# ticker: EURUSD BGN Curncy # 汇率值的å½åšticker +asset-pool: # èµ„äº§æ± æ¨¡å— + asset-optimize: # èµ„äº§ä¼˜é€‰æ¨¡å— + sortino-weight: # sortino计算需è¦çš„æƒé‡ï¼Œä¸‹é¢æ¯ä¸€æ¡ä¸ºä¸€æ¬¡è®¡ç®—,e.g. months: 3, weight: 0.5 表示 3个月数æ®ä½¿ç”¨æƒé‡0.5æ¥è®¡ç®—分值 + - months: 3 + weight: 0.5 + - months: 6 + weight: 0.3 + - years: 1 + weight: 0.2 + asset-include: ['US_STOCK','US_IG_BOND','US_HY_BOND'] + optimize-count: 3 #基金优选个数 +portfolios: # æŠ•ç»„æ¨¡å— + holder: # æŒä»“投组相关 + init-nav: 100 # åˆå§‹é‡‘é¢ + min-interval-days: 10 # 两次实际调仓最å°é—´éš”æœŸï¼Œå•ä½äº¤æ˜“æ—¥ + solver: # 解算器相关 + tol: 1E-10 # 误差满足æ¡ä»¶ + navs: # å‡€å€¼è¦æ±‚ + range: # 需è¦å‡€å€¼æ•°æ®çš„区间, days: 90 表示90自然日,months: 3 表示3个自然月 + days: 90 + max-nan: # 最大缺失净值æ¡ä»¶ + asset: 8 # å•一资产最多缺少多少交易日数æ®ï¼Œåˆ™è¸¢å‡ºèµ„äº§æ± + day: 0.5 # å•ä¸€äº¤æ˜“æ—¥æœ€å¤šç¼ºå°‘ç™¾åˆ†ä¹‹å¤šå°‘å‡€å€¼ï¼Œåˆ™åˆ é™¤è¯¥äº¤æ˜“æ—¥ + normal-ratio: #US_STOCK:US_HY_BOND:US_IG_BOND三者分别对应低ä¸é«˜é£Žé™©æ‰€å 比率 + US_STOCK: [ 0.3, 0.5, 0.7 ] + US_HY_BOND: [ 0.6, 0.4, 0.2 ] + US_IG_BOND: [ 0.1, 0.1, 0.1 ] + dividend-rate: 0.09 + riskctl-ratio: + US_STOCK: [ 0.2, 0.4, 0.6 ] + US_HY_BOND: [ 0.5, 0.3, 0.1 ] + US_IG_BOND: [ 0.3, 0.3, 0.3 ] + dividend-rate: 0.09 + matrix-rtn-days: 20 # 计算回报率矩阵时,回报率滚动天数 + asset-count: [1,3] # 投组资产个数。e.g. count 或 [min, max] 分别表示 最大最å°éƒ½ä¸ºcount 或 最å°ä¸ºmin 最大为max,å¦å¤–这里也å¯ä»¥ç±»ä¼¼ä¸Šé¢ç»™ä¸åŒé£Žé™©ç‰çº§åˆ†åˆ«é…ç½® + mpt: # mpt计算相关 + cvar-beta: 0.2 # 计算Kbeta 需è¦ç”¨åˆ° + quantile: 0.9 # 分ä½ç‚¹ï¼Œä¹Ÿå¯ä»¥ç»™ä¸åŒé£Žé™©ç‰çº§åˆ†åˆ«é…ç½® + low-weight: 0.05 # 最低æƒé‡ + high-weight: [ 1 ] # 最高æƒé‡æ¯”例,å¯ç»™ä¸€ä¸ªå€¼ï¼Œä¹Ÿå¯ä»¥ç»™å¤šä¸ªå€¼ï¼Œå½“å¤šä¸ªå€¼æ—¶ï¼Œç¬¬ä¸€ä¸ªè¡¨ç¤ºåªæœ‰ä¸€ä¸ªèµ„产时æƒé‡ï¼Œç¬¬äºŒä¸ªè¡¨ç¤ºåªæœ‰ä¸¤ä¸ªèµ„产时æƒé‡ï¼Œä»¥æ¤ç±»æŽ¨ï¼Œæœ€åŽä¸€ä¸ªè¡¨ç¤ºå…¶ä»–资产个数时的æƒé‡ + poem: # poem相关 + cvar-scale-factor: 0.1 # 计算时用到的系数 +robo-executor: # 执行器相关 + use: ${ROBO_EXECUTOR:backtest} # 执行哪个执行器,优先å–系统环境å˜é‡ROBO_EXECUTOR的值,默认backtest + sync-data: ${SYNC_DATA:off} # 是å¦å¼€å¯åŒæ¥èµ„æ–™æ•°æ® + backtest: # 回测执行器相关 + start-date: 2022-09-01 # 回测起始日期 + end-date: 2023-03-03 # å›žæµ‹æˆªæ¢æ—¥æœŸ + start-step: ${BACKTEST_START_STEP:3} # 回测从哪一æ¥å¼€å§‹æ‰§è¡Œ 1:è®¡ç®—èµ„äº§æ± ï¼›2:计算最优投组:3:计算å†å¹³è¡¡ä¿¡å·ä»¥åŠæŒä»“投组 + end-step: ${BACKTEST_END_STEP:3} # å›žæµ‹ä»Žå“ªä¸€æ¥æ‰§è¡Œå®ŒæˆåŽç»“æŸæ‰§è¡Œ 1:è®¡ç®—èµ„äº§æ± ï¼›2:计算最优投组:3:计算å†å¹³è¡¡ä¿¡å·ä»¥åŠæŒä»“投组 + clean-up: true + real: # 实盘执行器 + start-date: 2022-09-01 # 实盘开始时间 + + + + diff --git a/portfolios/builder.py b/portfolios/builder.py index 4d3f1e68e8fff663222e8c57aef9be013cdde274..29008e6856bed0105e9bf1c5ce00a922f7438951 100644 --- a/portfolios/builder.py +++ b/portfolios/builder.py @@ -1,12 +1,10 @@ import json import logging -from datetime import datetime as dt -from typing import List from py_jftech import component, autowired, format_date from pymysql import IntegrityError, constants -from api import PortfoliosBuilder, PortfoliosRisk, AssetPool, Navs, PortfoliosType, Datum, SolveType, SolverFactory, RoboReportor, DatumType +from api import PortfoliosBuilder, PortfoliosRisk, AssetPool, Navs, PortfoliosType, Datum, SolveType, SolverFactory from portfolios.dao import robo_mpt_portfolios as rmp logger = logging.getLogger(__name__) @@ -26,7 +24,7 @@ class MptPortfoliosBuilder(PortfoliosBuilder): try: portfolio = rmp.get_one(day, type, risk) if not portfolio: - result, detail = self.build_portfolio(day, type) + result = self.build_portfolio(day, type) for build_risk, datas in result.items(): try: rmp.insert({ @@ -45,41 +43,37 @@ class MptPortfoliosBuilder(PortfoliosBuilder): return {int(x[0]): x[1] for x in result.items()} return None except Exception as e: - logger.exception(f"build protfolio of type[{type.name}] and risk[{risk.name}] with date[{format_date(day)}] failure.", e) + logger.exception( + f"build protfolio of type[{type.name}] and risk[{risk.name}] with date[{format_date(day)}] failure.", e) raise e def build_portfolio(self, day, type: PortfoliosType): result = {} - detail = {} + portfolios = {} for risk in PortfoliosRisk: logger.info( f"start to build protfolio of type[{type.name}] and risk[{risk.name}] with date[{format_date(day)}]") solver = self._factory.create_solver(risk, type) - solver.reset_navs(day) - logger.debug({ - 'Khist': len(solver.rtn_history), - 'beta': solver.get_config('mpt.cvar-beta'), - 'Kbeta': solver.k_beta, - }) - max_rtn, max_var, minCVaR_whenMaxR = solver.solve_max_rtn() - min_rtn, min_var, maxCVaR_whenMinV = solver.solve_min_rtn() - portfolio, cvar = solver.solve_mpt(min_rtn, max_rtn) - result[risk] = { - 'solve': SolveType.MPT, - 'portfolio': json.dumps(portfolio), - 'cvar': cvar - } if portfolio else { - 'solve': SolveType.INFEASIBLE - } - detail[risk] = { - 'max_rtn': max_rtn, - 'max_var': max_var, - 'minCVaR_whenMaxR': minCVaR_whenMaxR, - 'min_rtn': min_rtn, - 'min_var': min_var, - 'maxCVaR_whenMinV': maxCVaR_whenMinV, - } - return result, detail + navs_group = solver.reset_navs(day) + for category, navs in navs_group.items(): + solver.set_navs(navs) + solver.set_category(category) + logger.debug({ + 'Khist': len(solver.rtn_history), + 'beta': solver.get_config('mpt.cvar-beta'), + 'Kbeta': solver.k_beta, + }) + max_rtn, max_var, minCVaR_whenMaxR = solver.solve_max_rtn() + min_rtn, min_var, maxCVaR_whenMinV = solver.solve_min_rtn() + portfolio, cvar = solver.solve_mpt(min_rtn, max_rtn) + portfolios = {**portfolios, **portfolio} + result[risk] = { + 'solve': SolveType.MPT, + 'portfolio': json.dumps(portfolios), + } if portfolios else { + 'solve': SolveType.INFEASIBLE + } + return result def clear(self, day=None, risk: PortfoliosRisk = None): rmp.delete(min_date=day, risk=risk) @@ -89,62 +83,24 @@ class MptPortfoliosBuilder(PortfoliosBuilder): class PoemPortfoliosBuilder(MptPortfoliosBuilder): def build_portfolio(self, day, type: PortfoliosType): - result, detail = super(PoemPortfoliosBuilder, self).build_portfolio(day, type) + result = {} + portfolios = {} for risk in PortfoliosRisk: - if result[risk]['solve'] is SolveType.INFEASIBLE: - continue solver = self._factory.create_solver(risk, type) - solver.reset_navs(day) - min_rtn = detail[risk]['min_rtn'] - max_rtn = detail[risk]['max_rtn'] - mpt_cvar = result[risk]['cvar'] - maxCVaR_whenMinV = detail[risk]['maxCVaR_whenMinV'] - portfolio, cvar = solver.solve_poem(min_rtn, max_rtn, mpt_cvar, maxCVaR_whenMinV) - if portfolio: + navs_group = solver.reset_navs(day) + for category, navs in navs_group.items(): + solver.set_navs(navs) + solver.set_category(category) + max_rtn, max_var, minCVaR_whenMaxR = solver.solve_max_rtn() + min_rtn, min_var, maxCVaR_whenMinV = solver.solve_min_rtn() + mpt_portfolio, mpt_cvar = solver.solve_mpt(min_rtn, max_rtn) + portfolio, cvar = solver.solve_poem(min_rtn, max_rtn, mpt_cvar, maxCVaR_whenMinV) + if not portfolio: + portfolio = mpt_portfolio + portfolios = {**portfolios, **portfolio } + if portfolios: result[risk] = { 'solve': SolveType.POEM, - 'portfolio': json.dumps(portfolio), - 'cvar': cvar + 'portfolio': json.dumps(portfolios), } - detail[risk]['mpt_cvar'] = mpt_cvar - return result, detail - - -@component(bean_name='mpt-report') -class MptReportor(RoboReportor): - - @autowired - def __init__(self, datum: Datum = None): - self._datum = datum - - @property - def report_name(self) -> str: - return '最优投组' - - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - results = [] - datums = {x['id']: x for x in self._datum.get_datums(type=DatumType.FUND, exclude=False)} - for portfolio in rmp.get_list(max_date=max_date, min_date=min_date): - solve_type = SolveType(portfolio['solve']) - datas = { - 'date': portfolio['date'], - 'risk': PortfoliosRisk(portfolio['risk']).name, - 'type': PortfoliosType(portfolio['type']).name, - 'solve': solve_type.name, - 'cvar': portfolio['cvar'] - } - if solve_type is not SolveType.INFEASIBLE: - for asset_id, weight in json.loads(portfolio['portfolio']).items(): - datum = datums[int(asset_id)] - results.append({ - **datas, - 'ft_ticker': datum['ftTicker'], - 'lipper_id': datum['lipperKey'], - 'bloomberg_ticker': datum['bloombergTicker'], - 'name': datum['chineseName'], - 'weight': weight, - }) - else: - results.append(datas) - return results - + return result diff --git a/portfolios/holder.py b/portfolios/holder.py index 2f3a9b755adca96e125d61b1ae0fb1121d791499..d04e755b78bc228238a02c38c5c649d759f284b4 100644 --- a/portfolios/holder.py +++ b/portfolios/holder.py @@ -1,36 +1,28 @@ import json import logging -from datetime import datetime as dt -from typing import List -from functools import reduce import pandas as pd from py_jftech import ( - component, autowired, get_config, next_workday, prev_workday, transaction, workday_range, format_date + component, autowired, get_config, next_workday, format_date ) -from api import PortfoliosHolder, PortfoliosRisk, RebalanceRuler, Navs, SignalType, RoboExecutor, PortfoliosType, RoboReportor, Datum, DatumType +from api import PortfoliosHolder, PortfoliosRisk, Navs, RoboExecutor, PortfoliosType from portfolios.dao import robo_hold_portfolios as rhp from portfolios.utils import format_weight logger = logging.getLogger(__name__) -@component(bean_name='next-re') -class NextReblanceHolder(PortfoliosHolder): +@component(bean_name='dividend-holder') +class DividendPortfoliosHolder(PortfoliosHolder): @autowired(names={'executor': RoboExecutor.use_name()}) - def __init__(self, rule: RebalanceRuler, navs: Navs = None, executor: RoboExecutor = None): - self._rule = rule + def __init__(self, navs: Navs = None, executor: RoboExecutor = None): self._navs = navs self._executor = executor self._config = get_config(__name__) def get_portfolio_type(self, day, risk: PortfoliosRisk) -> PortfoliosType: - hold = rhp.get_one(day, risk) - if hold: - signal_type = self._rule.get_signal_type(hold['signal_id']) - return signal_type.p_type if signal_type else PortfoliosType.NORMAL return PortfoliosType.NORMAL def get_last_rebalance_date(self, risk: PortfoliosRisk, max_date=None): @@ -38,10 +30,6 @@ class NextReblanceHolder(PortfoliosHolder): last = rhp.get_last_one(max_date=max_date, risk=risk, rebalance=True) return last['date'] if last else None - def get_rebalance_date_by_signal(self, signal_id): - last = rhp.get_last_one(signal_id=signal_id, rebalance=True) - return last['date'] if last else None - def get_portfolios_weight(self, day, risk: PortfoliosRisk): hold = rhp.get_one(day, risk) if hold: @@ -56,50 +44,16 @@ class NextReblanceHolder(PortfoliosHolder): last_nav = rhp.get_last_one(max_date=day, risk=risk) start = next_workday(last_nav['date'] if last_nav else self._executor.start_date) try: + if not last_nav: + pass while start <= day: logger.info(f"start to build hold portfolio[{risk.name}] for date[{format_date(start)}]") - signal = None - if last_nav: - last_re_date = self.get_last_rebalance_date(risk=risk, max_date=start) - if len(workday_range(last_re_date, start)) > self.interval_days: - signal = self._rule.take_next_signal(prev_workday(start), risk) - else: - signal = self._rule.take_next_signal(prev_workday(start), risk) - if signal and not signal['effective']: - logger.info(f"start to rebalance hold portfolio[{risk.name}] for date[{format_date(start)}] " - f"with signal[{SignalType(signal['type']).name}]") - self.do_rebalance(start, risk, signal, last_nav) - elif last_nav and signal is None: - self.no_rebalance(start, risk, last_nav) + self.no_rebalance(start, risk, last_nav) start = next_workday(start) last_nav = rhp.get_last_one(max_date=day, risk=risk) except Exception as e: logger.exception(f"build hold portfolio[{risk.name}] for date[{format_date(start)}] failure.", e) - @transaction - def do_rebalance(self, day, risk: PortfoliosRisk, signal, last_nav): - weight = {int(x[0]): x[1] for x in json.loads(signal['portfolio']).items()} - if last_nav: - share = {int(x): y for x, y in json.loads(last_nav['portfolios'])['share'].items()} - navs = self.get_navs(fund_ids=tuple(set(weight) | set(share)), day=day) - nav = round(sum([navs[x] * y for x, y in share.items()]), 4) - else: - nav = self.init_nav - navs = self.get_navs(fund_ids=tuple(weight), day=day) - share = {x: nav * w / navs[x] for x, w in weight.items()} - rhp.insert({ - 'date': day, - 'risk': risk, - 'signal_id': signal['id'], - 'rebalance': True, - 'portfolios': { - 'weight': weight, - 'share': share, - }, - 'nav': nav, - }) - self._rule.commit_signal(signal['id']) - def no_rebalance(self, day, risk: PortfoliosRisk, last_nav): share = {int(x): y for x, y in json.loads(last_nav['portfolios'])['share'].items()} navs = self.get_navs(fund_ids=tuple(share), day=day) @@ -134,62 +88,3 @@ class NextReblanceHolder(PortfoliosHolder): @property def init_nav(self): return self._config['init-nav'] - - -@component(bean_name='hold-report') -class HoldReportor(RoboReportor): - - @autowired - def __init__(self, rule: RebalanceRuler = None): - self._rule = rule - - @property - def report_name(self) -> str: - return '投组净值' - - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - holds = pd.DataFrame(rhp.get_list(max_date=max_date, min_date=min_date)) - if not holds.empty: - signal_types = self._rule.get_signal_type(tuple(set(holds['signal_id']))) - holds['signal_type'] = holds.apply(lambda row: signal_types[row['signal_id']].name, axis=1) - holds['risk'] = holds.apply(lambda row: PortfoliosRisk(row['risk']).name, axis=1) - holds = holds[['risk', 'date', 'nav', 'signal_type']] - return holds.to_dict('records') - return [] - - -@component(bean_name='daily-hold-report') -class DailyHoldReportor(RoboReportor): - - @autowired - def __init__(self, rule: RebalanceRuler = None, datum: Datum = None): - self._rule = rule - self._datum = datum - - @property - def report_name(self) -> str: - return 'æ¯æ—¥æŒä»“ä¿¡æ¯' - - def load_report(self, max_date=prev_workday(dt.today()), min_date=None) -> List[dict]: - holds = pd.DataFrame(rhp.get_list(max_date=max_date, min_date=min_date)) - holds = holds[holds['date'].dt.date == max_date.date()] - if not holds.empty: - signal_types = self._rule.get_signal_type(tuple(set(holds['signal_id']))) - signal_dates = self._rule.get_signal_date(tuple(set(holds['signal_id']))) - datum_ids = reduce(lambda x, y: x | y, holds['portfolios'].apply(lambda x: set(json.loads(x)['weight'].keys()))) - datums = pd.DataFrame(self._datum.get_datums(type=DatumType.FUND, datum_ids=datum_ids)) - datums.set_index('id', inplace=True) - - holds['rebalance_type'] = holds.apply(lambda row: signal_types[row['signal_id']].name, axis=1) - holds['rebalance_date'] = holds.apply(lambda row: signal_dates[row['signal_id']], axis=1) - holds['risk'] = holds.apply(lambda row: PortfoliosRisk(row['risk']).name, axis=1) - holds['portfolios'] = holds.apply(lambda row: [x for x in json.loads(row['portfolios'])['weight'].items()], axis=1) - holds = holds.explode('portfolios', ignore_index=True) - holds['weight'] = holds.apply(lambda row: row['portfolios'][1], axis=1) - holds['asset_ids'] = holds.apply(lambda row: datums.loc[int(row['portfolios'][0])]['ftTicker'], axis=1) - holds['name'] = holds.apply(lambda row: datums.loc[int(row['portfolios'][0])]['chineseName'], axis=1) - holds['lipper_id'] = holds.apply(lambda row: datums.loc[int(row['portfolios'][0])]['lipperKey'], axis=1) - - holds = holds[['risk', 'date', 'asset_ids', 'weight', 'rebalance_type', 'rebalance_date', 'name', 'lipper_id']] - return holds.to_dict('records') - return [] diff --git a/portfolios/solver.py b/portfolios/solver.py index 602f15bd2ed754f566df5ff792121295fd1dbb2e..23d8b2933d9f523f459cfb99c6757ad8db28d474 100644 --- a/portfolios/solver.py +++ b/portfolios/solver.py @@ -5,7 +5,7 @@ from logging import DEBUG, getLogger import pandas as pd from dateutil.relativedelta import relativedelta from numpy import NAN -from py_jftech import component, autowired, get_config, is_workday +from py_jftech import component, autowired, get_config from pyomo.environ import * from api import SolverFactory as Factory, PortfoliosRisk, PortfoliosType, AssetPool, Navs, Solver, Datum, DatumType @@ -36,6 +36,8 @@ class DefaultSolver(Solver): @autowired def __init__(self, risk: PortfoliosRisk, type: PortfoliosType, assets: AssetPool = None, navs: Navs = None, datum: Datum = None): + self._category = None + self._transfer_type = None self.__navs = None self.risk = risk self.type = type or PortfoliosType.NORMAL @@ -82,6 +84,21 @@ class DefaultSolver(Solver): def quantile(self): return self.get_config('mpt.quantile') + @property + def category(self): + return self._category + + @property + def transfer_type(self): + self._transfer_type = self.get_config("normal-ratio") + return self._transfer_type + + def set_navs(self, navs): + self.__navs = navs + + def set_category(self, category): + self._category = category + def solve_max_rtn(self): model = self.create_model() model.objective = Objective(expr=sum([model.w[i] * self.rtn_annualized[i] for i in model.indices]), @@ -165,7 +182,7 @@ class DefaultSolver(Solver): df_w = pd.DataFrame(data=weight_list, index=id_list, columns=['weight']) df_w.replace(0, NAN, inplace=True) df_w.dropna(axis=0, inplace=True) - df_w['weight'] = pd.Series(format_weight(dict(df_w['weight']))) + df_w['weight'] = pd.Series(format_weight(dict(df_w['weight']), self.get_weight())) dict_w = df_w.to_dict()['weight'] return dict_w @@ -184,6 +201,10 @@ class DefaultSolver(Solver): port_r_hist.sort() return sum(port_r_hist[0: self.k_beta]) / self.k_beta + def get_weight(self): + # todo æ ¹æ®self.risk找é…ç½® + return self.transfer_type[self.category][0] + def create_model(self): count = self.get_config('asset-count') min_count = count[0] if isinstance(count, list) else count @@ -194,13 +215,12 @@ class DefaultSolver(Solver): high_weight = self.get_config('mpt.high-weight') if isinstance(high_weight, list): high_weight = high_weight[min(len(self.navs.columns), min_count, len(high_weight)) - 1] - model = ConcreteModel() model.indices = range(0, len(self.navs.columns)) model.w = Var(model.indices, domain=NonNegativeReals) model.z = Var(model.indices, domain=Binary) - model.cons_sum_weight = Constraint(expr=sum([model.w[i] for i in model.indices]) == 1) + model.cons_sum_weight = Constraint(expr=sum([model.w[i] for i in model.indices]) == high_weight) model.cons_num_asset = Constraint( expr=inequality(min_count, sum([model.z[i] for i in model.indices]), max_count, strict=False)) model.cons_bounds_low = Constraint(model.indices, rule=lambda m, i: m.z[i] * low_weight <= m.w[i]) @@ -209,28 +229,29 @@ class DefaultSolver(Solver): def reset_navs(self, day): asset_ids = self._assets.get_pool(day) - asset_risk = self.get_config('navs.risk') - datum = self._datum.get_datums(type=DatumType.FUND, datum_ids=asset_ids, risk=asset_risk) - exclude = self.get_config('navs.exclude-asset-type') or [] - asset_ids = list(set(asset_ids) & set([x['id'] for x in datum if x['assetType'] not in exclude])) - - min_date = day - relativedelta(**self.get_config('navs.range')) - navs = pd.DataFrame(self._navs.get_fund_navs(fund_ids=asset_ids, max_date=day, min_date=min_date)) - navs = navs[navs['nav_date'].dt.day_of_week < 5] - navs['nav_date'] = pd.to_datetime(navs['nav_date']) - navs = navs.pivot_table(index='nav_date', columns='fund_id', values='nav_cal') - navs = navs.sort_index() - - navs_nan = navs.isna().sum() - navs.drop(columns=[x for x in navs_nan.index if navs_nan.loc[x] >= self.get_config('navs.max-nan.asset')], - inplace=True) - navs_nan = navs.apply(lambda r: r.isna().sum() / len(r), axis=1) - navs.drop(index=[x for x in navs_nan.index if navs_nan.loc[x] >= self.get_config('navs.max-nan.day')], - inplace=True) - navs.fillna(method='ffill', inplace=True) - if navs.iloc[0].isna().sum() > 0: - navs.fillna(method='bfill', inplace=True) - self.__navs = navs + datum = self._datum.get_datums(type=DatumType.FUND, datum_ids=asset_ids) + asset_ids_group = {k: [d['id'] for d in datum if d['category'] == k] for k in set(d['category'] for d in datum)} + navs_group = {} + for category, asset_ids in asset_ids_group.items(): + min_date = day - relativedelta(**self.get_config('navs.range')) + navs = pd.DataFrame(self._navs.get_fund_navs(fund_ids=asset_ids, max_date=day, min_date=min_date)) + navs = navs[navs['nav_date'].dt.day_of_week < 5] + navs['nav_date'] = pd.to_datetime(navs['nav_date']) + navs = navs.pivot_table(index='nav_date', columns='fund_id', values='nav_cal') + navs = navs.sort_index() + + navs_nan = navs.isna().sum() + navs.drop(columns=[x for x in navs_nan.index if navs_nan.loc[x] >= self.get_config('navs.max-nan.asset')], + inplace=True) + navs_nan = navs.apply(lambda r: r.isna().sum() / len(r), axis=1) + navs.drop(index=[x for x in navs_nan.index if navs_nan.loc[x] >= self.get_config('navs.max-nan.day')], + inplace=True) + navs.fillna(method='ffill', inplace=True) + if navs.iloc[0].isna().sum() > 0: + navs.fillna(method='bfill', inplace=True) + navs_group[category] = navs + self.__navs = navs_group + return navs_group def get_config(self, name): def load_config(config): @@ -244,7 +265,8 @@ class DefaultSolver(Solver): value = load_config(self._config[self.type.value] if self.type is not PortfoliosType.NORMAL else self._config) if value is None: value = load_config(self._config) - return value[f'ft{self.risk.value}'] if value and isinstance(value, dict) and f'ft{self.risk.value}' in value else value + return value[f'ft{self.risk.value}'] if value and isinstance(value, + dict) and f'ft{self.risk.value}' in value else value def debug_solve_result(self, model): if logger.isEnabledFor(DEBUG): diff --git a/portfolios/test_case.py b/portfolios/test_case.py index 48fdc9b009b7d7875f9c74ae8e9992d0cdaeb6cd..36c67dc8ba000a3d2736f7c6995f5dccb016a37d 100644 --- a/portfolios/test_case.py +++ b/portfolios/test_case.py @@ -1,9 +1,9 @@ import logging import unittest -from py_jftech import autowired, parse_date, to_str +from py_jftech import autowired, parse_date -from api import PortfoliosBuilder, PortfoliosType, PortfoliosRisk, PortfoliosHolder, RoboReportor +from api import PortfoliosBuilder, PortfoliosType, PortfoliosRisk, PortfoliosHolder class PortfoliosTest(unittest.TestCase): @@ -23,28 +23,18 @@ class PortfoliosTest(unittest.TestCase): portfolio = builder.get_portfolios(parse_date('2022-11-07'), PortfoliosRisk.FT9) self.logger.info(portfolio) - @autowired(names={'hold': 'next-re'}) + @autowired(names={'hold': 'dividend-holder'}) def test_has_hold(self, hold: PortfoliosHolder = None): self.logger.info(hold.has_hold(PortfoliosRisk.FT3)) - @autowired(names={'hold': 'next-re'}) + @autowired(names={'hold': 'dividend-holder'}) def test_build_hold(self, hold: PortfoliosHolder = None): hold.build_hold_portfolio(parse_date('2023-02-23'), PortfoliosRisk.FT9) - @autowired(names={'reportor': 'hold-report'}) - def test_hold_report(self, reportor: RoboReportor = None): - report = reportor.load_report() - self.logger.info(to_str(report)) - - @autowired(names={'hold': 'next-re'}) + @autowired(names={'hold': 'dividend-holder'}) def test_clear(self, hold: PortfoliosHolder = None): hold.clear() - @autowired(names={'reportor': 'daily-hold-report'}) - def test_daily_hold_report(self, reportor: RoboReportor = None): - report = reportor.load_report() - self.logger.info(to_str(report)) - if __name__ == '__main__': unittest.main() diff --git a/portfolios/utils.py b/portfolios/utils.py index 1d1b9e3e0a2946d7a4168fbf632dc5d4705c832b..3461163b9c8745c845b7a0581549a14fb905083d 100644 --- a/portfolios/utils.py +++ b/portfolios/utils.py @@ -1,20 +1,26 @@ import pandas as pd -def format_weight(weight: dict) -> dict: +def format_weight(weight: dict, to=1) -> dict: + """ + 对æƒé‡çš„å°æ•°ç‚¹è¿›è¡Œæˆªå–,到指定æƒé‡ + @param weight: + @param to: 指定æƒé‡ + @return: + """ weight_series = pd.Series(weight) weight_series = weight_series.fillna(0) minidx = weight_series[weight_series > 0].idxmin() maxidx = weight_series.idxmax() weight_series = weight_series.apply(lambda x: round(x, 2)) - if weight_series.sum() == 1: + if weight_series.sum() == to: return dict(weight_series) - elif weight_series.sum() < 1: - weight_series[minidx] += 1 - weight_series.sum() - elif weight_series.sum() > 1: - weight_series[maxidx] += 1 - weight_series.sum() + elif weight_series.sum() < to: + weight_series[minidx] += to - weight_series.sum() + elif weight_series.sum() > to: + weight_series[maxidx] += to - weight_series.sum() return dict(weight_series.apply(lambda x: round(float(x), 2))) if __name__ == '__main__': - print(format_weight({19: 0.13, 27: 0.17, 31: 0.35, 56: 0.36})) + print(format_weight({19: 0.13, 27: 0.17, 56: 0.36})) diff --git a/rebalance/__init__.py b/rebalance/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/rebalance/base_signal.py b/rebalance/base_signal.py deleted file mode 100644 index 96330fe709f0fb4d52d110136980c8e1c5ee514a..0000000000000000000000000000000000000000 --- a/rebalance/base_signal.py +++ /dev/null @@ -1,34 +0,0 @@ -from abc import ABC, abstractmethod - -from py_jftech import autowired - -from api import RebalanceSignal, PortfoliosBuilder, PortfoliosRisk -from rebalance.dao import robo_rebalance_signal as rrs - - -class BaseRebalanceSignal(RebalanceSignal, ABC): - - @autowired - def __init__(self, builder: PortfoliosBuilder = None): - self._builder = builder - - def get_signal(self, day, risk: PortfoliosRisk): - signal = rrs.get_one(type=self.signal_type, risk=risk, date=day) - if signal: - return signal - trigger = self.is_trigger(day, risk) - if trigger: - portfolio = self._builder.get_portfolios(day, risk, self.signal_type.p_type) - id = rrs.insert({ - 'date': day, - 'type': self.signal_type, - 'risk': risk, - 'portfolio_type': self.signal_type.p_type, - 'portfolio': portfolio - }) - return rrs.get_by_id(id) - return None - - @abstractmethod - def is_trigger(self, day, risk: PortfoliosRisk) -> bool: - pass diff --git a/rebalance/dao/__init__.py b/rebalance/dao/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/rebalance/dao/mysql.sql b/rebalance/dao/mysql.sql deleted file mode 100644 index fd1de3695e7a93a68989127c1d2f999f0f513031..0000000000000000000000000000000000000000 --- a/rebalance/dao/mysql.sql +++ /dev/null @@ -1,37 +0,0 @@ -CREATE TABLE IF NOT EXISTS robo_rebalance_signal -( - rrs_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - rrs_date DATETIME NOT NULL COMMENT 'ä¿¡å·æ—¥æœŸ', - rrs_type TINYINT NOT NULL COMMENT 'ä¿¡å·ç±»åž‹', - rrs_risk TINYINT NOT NULL COMMENT '风险ç‰çº§', - rrs_p_type VARCHAR(255) DEFAULT NULL COMMENT '投组类型', - rrs_p_weight JSON DEFAULT NULL COMMENT '投组信æ¯', - rrs_effective TINYINT NOT NULL DEFAULT 0 COMMENT '是å¦ç”Ÿæ•ˆ', - rrs_create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - rrs_update_time DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (rrs_id), - INDEX (rrs_date), - INDEX (rrs_type), - INDEX (rrs_risk) -) ENGINE = InnoDB - AUTO_INCREMENT = 0 - DEFAULT CHARSET = utf8mb4 COMMENT 'å†å¹³è¡¡ä¿¡å·è¡¨'; - - - -CREATE TABLE IF NOT EXISTS robo_weight_drift -( - rwd_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - rwd_date DATETIME NOT NULL COMMENT '日期', - rwd_risk TINYINT NOT NULL COMMENT '风险ç‰çº§', - rwd_weight DOUBLE NOT NULL COMMENT '高风险资产æƒé‡', - rwd_drift DOUBLE NOT NULL COMMENT '资产æƒé‡æ¼‚移计算值', - rwd_create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - rwd_update_time DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (rwd_id), - UNIQUE INDEX (rwd_date, rwd_risk), - INDEX (rwd_risk) -) ENGINE = InnoDB - AUTO_INCREMENT = 0 - DEFAULT CHARSET = utf8mb4 COMMENT '高风险资产æƒé‡æ¼‚移表'; - diff --git a/rebalance/dao/robo_rebalance_signal.py b/rebalance/dao/robo_rebalance_signal.py deleted file mode 100644 index 956577c144bf5c2cd82234862026660d07342042..0000000000000000000000000000000000000000 --- a/rebalance/dao/robo_rebalance_signal.py +++ /dev/null @@ -1,102 +0,0 @@ -from py_jftech import read, write, where, format_date, mapper_columns, to_tuple - -from api import SignalType, PortfoliosRisk - -__COLUMNS__ = { - 'rrs_id': 'id', - 'rrs_date': 'date', - 'rrs_type': 'type', - 'rrs_risk': 'risk', - 'rrs_p_type': 'portfolio_type', - 'rrs_p_weight': 'portfolio', - 'rrs_effective': 'effective', -} - - -@read -def get_list(min_date=None, max_date=None, risk: PortfoliosRisk = None, type: SignalType = None, effective: bool = None): - sqls = [] - if min_date: - sqls.append(f"rrs_date >= '{format_date(min_date)}'") - if max_date: - sqls.append(f"rrs_date <= '{format_date(max_date)}'") - return f''' - select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_rebalance_signal - {where(*sqls, rrs_risk=risk, rrs_type=type, rrs_effective=effective)} order by rrs_risk, rrs_date - ''' - - -@read -def get_by_ids(ids): - return f'''select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_rebalance_signal {where(rrs_id=to_tuple(ids))}''' - - -@read(one=True) -def get_by_id(id): - return f'''select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_rebalance_signal {where(rrs_id=id)}''' - - -@read(one=True) -def get_one(type: SignalType, risk: PortfoliosRisk, date): - return f''' - select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_rebalance_signal - {where(rrs_date=date, rrs_type=type, rrs_risk=risk)} - ''' - - -@read(one=True) -def get_first_after(type: SignalType, risk: PortfoliosRisk, min_date, effective=None): - return f''' - select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_rebalance_signal - {where(f"rrs_date >= '{format_date(min_date)}'", rrs_type=type, rrs_risk=risk, rrs_effective=effective)} order by rrs_date limit 1 - ''' - - -@read(one=True) -def get_last_one(max_date, risk: PortfoliosRisk, type: SignalType = None, effective=None): - return f''' - select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_rebalance_signal - {where(f"rrs_date <= '{format_date(max_date)}'", rrs_type=type, rrs_risk=risk, rrs_effective=effective)} order by rrs_date desc limit 1 - ''' - - -def get_count(risk: PortfoliosRisk = None, day=None, effective=None): - @read(one=True) - def exec(): - return f"select count(*) as `count` from robo_rebalance_signal {where(rrs_risk=risk, rrs_date=day, rrs_effective=effective)}" - - result = exec() - return result['count'] - - -@write -def insert(datas): - datas = mapper_columns(datas=datas, columns=__COLUMNS__) - return f''' - insert into robo_rebalance_signal({','.join([x for x in datas.keys()])}) - values ({','.join([f"'{x[1]}'" for x in datas.items()])}) - ''' - - -@write -def update(id, datas): - datas = mapper_columns(datas=datas, columns=__COLUMNS__) - return f''' - update robo_rebalance_signal - set {','.join([f"{x[0]} = '{x[1]}'" for x in datas.items()])} - where rrs_id = {id} - ''' - - -@write -def delete_by_id(id): - return f"delete from robo_rebalance_signal where rrs_id = {id}" - - -@write -def delete(min_date=None, risk: PortfoliosRisk = None): - if min_date is None and risk is None: - return 'truncate table robo_rebalance_signal' - else: - sql = f"rrs_date >= '{format_date(min_date)}'" if min_date else None - return f"delete from robo_rebalance_signal {where(sql, rrs_risk=risk)}" diff --git a/rebalance/dao/robo_weight_drift.py b/rebalance/dao/robo_weight_drift.py deleted file mode 100644 index c431756db0249e4caca2af9a48d3fba1dca91292..0000000000000000000000000000000000000000 --- a/rebalance/dao/robo_weight_drift.py +++ /dev/null @@ -1,43 +0,0 @@ -from py_jftech import read, write, where, format_date, mapper_columns - -from api import PortfoliosRisk - -__COLUMNS__ = { - 'rwd_id': 'id', - 'rwd_date': 'date', - 'rwd_risk': 'risk', - 'rwd_weight': 'weight', - 'rwd_drift': 'drift', -} - - -@read(one=True) -def get_one(day, risk: PortfoliosRisk): - return f"select {','.join([f'{x[0]} as {x[1]}' for x in __COLUMNS__.items()])} from robo_weight_drift {where(rwd_date=day, rwd_risk=risk)}" - - -@read(one=True) -def get_last_one(max_date, risk: PortfoliosRisk): - return f''' - select {','.join([f'{x[0]} as {x[1]}' for x in __COLUMNS__.items()])} from robo_weight_drift - {where(f"rwd_date <= '{format_date(max_date)}'", rwd_risk=risk)} order by rwd_date desc limit 1 - ''' - - -@write -def insert(datas): - datas = mapper_columns(datas=datas, columns=__COLUMNS__) - return f''' - insert into robo_weight_drift({','.join([x for x in datas.keys()])}) - values ({','.join([f"'{x[1]}'" for x in datas.items()])}) - ''' - - -@write -def lock(): - return "lock tables robo_weight_drift read" - - -@write -def unlock(): - return "UNLOCK TABLES" diff --git a/rebalance/drift_solver.py b/rebalance/drift_solver.py deleted file mode 100644 index 2856421962e6106a0e5bd1dbe583b7582549c372..0000000000000000000000000000000000000000 --- a/rebalance/drift_solver.py +++ /dev/null @@ -1,59 +0,0 @@ -from py_jftech import component, autowired, get_config, workday_range, next_workday - -from api import DriftSolver, PortfoliosRisk, PortfoliosBuilder, Datum, RoboExecutor -from rebalance.dao import robo_rebalance_signal as rrs, robo_weight_drift as rwd - - -@component(bean_name='date-curve') -class DateCurve(DriftSolver): - - def __init__(self): - self._config = get_config(__name__)['date-curve'] - - @property - def diff_threshold(self): - return self._config['diff-threshold'] - - @property - def init_factor(self): - return self._config['init-factor'] - - def get_drift(self, day, risk: PortfoliosRisk): - last_re = rrs.get_last_one(max_date=day, risk=risk, effective=True) - result = self.diff_threshold - self.init_factor * (day - last_re['date']).days ** 4 - return max(0, result) - - -@component(bean_name='high-weight') -class PortfolioHighWeight(DriftSolver): - - @autowired(names={'executor': RoboExecutor.use_name()}) - def __init__(self, builder: PortfoliosBuilder = None, datum: Datum = None, executor: RoboExecutor = None): - self._builder = builder - self._datum = datum - self._executor = executor - self._config = get_config(__name__)['high-weight'] - - @property - def drift_coef(self): - return self._config['coef'] - - def get_drift(self, day, risk: PortfoliosRisk): - drift = rwd.get_one(day, risk) - if not drift: - datum_ids = [x['id'] for x in self._datum.get_high_risk_datums(risk)] - last_one = rwd.get_last_one(max_date=day, risk=risk) - start = (next_workday(last_one['date'])) if last_one else self._executor.start_date - last_drift = last_one['drift'] if last_one else 0 - for date in workday_range(start, day): - portfolio = self._builder.get_portfolios(date, risk) - weight = round(sum([x[1] for x in portfolio.items() if x[0] in datum_ids]), 2) - last_drift = round((weight * self.drift_coef + (1 - self.drift_coef) * last_drift) if last_drift else weight, 2) - rwd.insert({ - 'date': date, - 'risk': risk, - 'weight': weight, - 'drift': last_drift, - }) - drift = rwd.get_last_one(day, risk) - return drift['drift'] diff --git a/rebalance/ruler.py b/rebalance/ruler.py deleted file mode 100644 index 2b6cfecaa5ddd3917eeae4f180a858bebe52540b..0000000000000000000000000000000000000000 --- a/rebalance/ruler.py +++ /dev/null @@ -1,179 +0,0 @@ -import json -from datetime import datetime as dt -from typing import List, Dict -from functools import reduce - -import pandas as pd -from py_jftech import component, autowired, get_config, workday_range, next_workday, to_tuple, prev_workday - -from api import RebalanceRuler, PortfoliosRisk, RebalanceSignal, SignalType, PortfoliosType, PortfoliosHolder, RoboReportor, Datum, DatumType -from rebalance.dao import robo_rebalance_signal as rrs - - -@component -class LevelRebalanceRuler(RebalanceRuler): - ''' - 定义: - 1.定义所有调仓类型为éžNORMAL类型的信å·ä¸ºæ¸…ä»“ä¿¡å· - 2.定义所有调仓类型为NORMAL类型的信å·ä¸ºåŠ ä»“ä¿¡å· - 3.定义æŒä¹…ä¿¡å·ä¸ºä¸Šæ¬¡é€‰ç”¨è°ƒä»“çš„ä¿¡å·æ—¶é—´åˆ°å½“剿—¶é—´å†…,该信å·éƒ½æœ‰æ•ˆ - 4.定义临时信å·ä¸ºä»…当天有效 - 规则: - 1.所有清仓信å·ä¸ºæŒä¹…ä¿¡å·ï¼Œæ‰€æœ‰åŠ ä»“ä¿¡å·ä¸ºä¸´æ—¶ä¿¡å· - 2.对于æŒä¹…ä¿¡å·è§„则如下: - 2.1 上一次选用信å·åˆ°å½“剿—¶é—´å†…ï¼Œæ˜¯å¦æœ‰æŒä¹…ä¿¡å· - 2.2 如果有,则看级别是å¦é«˜äºŽä¸Šä¸€æ¬¡é€‰ç”¨ä¿¡å· - 2.3 å¦‚æžœé«˜äºŽï¼Œåˆ™è¾“å‡ºè¯¥ä¿¡å· - 3.如果没有æŒä¹…ä¿¡å·ï¼Œåˆ™ä»Žä¸´æ—¶ä¿¡å·ä¸æ ¹æ®çº§åˆ«æŽ’åºæ‰¾å‡ºç¬¬ä¸€ä¸ªï¼Œä½œä¸ºè¾“å‡ºä¿¡å· - ''' - - @autowired - def __init__(self, signals: List[RebalanceSignal] = None, hold: PortfoliosHolder = None): - self._signals = signals - self._hold = hold - self._config = get_config(__name__) - - @property - def disable_period(self): - result = self._config['disable-period'] - if isinstance(result, dict): - return {PortfoliosType(x[0]): x[1] for x in result.items()} - else: - return {t: result for t in PortfoliosType} - - def without_disable_period(self, day, risk: PortfoliosRisk) -> bool: - last_re = rrs.get_last_one(max_date=day, risk=risk, effective=True) - if SignalType(last_re['type']).p_type in self.disable_period: - return len(workday_range(last_re['date'], day)) > self.disable_period[SignalType(last_re['type']).p_type] - return False - - def take_next_signal(self, day, risk: PortfoliosRisk): - last_re = rrs.get_last_one(max_date=day, risk=risk, effective=True) - if not last_re: - builder = [x for x in self._signals if x.signal_type is SignalType.INIT][0] - return builder.get_signal(day, risk) - - risk_signals = [x for x in self._signals if x.signal_type.p_type is not PortfoliosType.NORMAL] - buy_signals = [x for x in self._signals if x.signal_type.p_type is PortfoliosType.NORMAL] - - last_signal = rrs.get_last_one(max_date=day, risk=risk) - start = next_workday(last_signal['date']) - signals = rrs.get_list(min_date=last_re['date'], risk=risk, effective=False) - signals = {SignalType(x['type']): x for x in signals if SignalType(x['type']) is not SignalType.NONE and SignalType(x['type']).level > SignalType(last_re['type']).level} - while start <= day: - # æ£€æŸ¥é£ŽæŽ§ä¿¡å· - today_signals = {x.signal_type: x.get_signal(start, risk) for x in risk_signals if x.signal_type.level <= SignalType(last_re['type']).level} - today_signals = {x[0]: x[1] for x in today_signals.items() if x[1] is not None} - signals = {**signals, **today_signals} - start = next_workday(start) - # ä¸Šæ¬¡å®žé™…è°ƒä»“ç±»åž‹ä¸ºå±æœºä¿¡å·ï¼Œæœ¬æ¬¡å±æœºä¿¡å·ä¸è°ƒä»“ - if signals and SignalType(last_re['type']) in [SignalType.CRISIS_ONE, SignalType.CRISIS_TWO]: - signals = {x[0]: x[1] for x in signals.items() if x[0] not in [SignalType.CRISIS_ONE, SignalType.CRISIS_TWO]} - # 检查买入信å·ï¼Œåªæœ‰å½“å¤©éœ€è¦æ£€æŸ¥ - if not signals and self.without_disable_period(day, risk): - signals = {x.signal_type: x.get_signal(day, risk) for x in buy_signals} - signals = {x[0]: x[1] for x in signals.items() if x[1] is not None} - if signals: - if SignalType(last_signal['type']) is SignalType.NONE: - rrs.delete_by_id(last_signal['id']) - return signals[sorted(signals.keys(), key=lambda x: x.level)[0]] - if SignalType(last_signal['type']) is SignalType.NONE: - rrs.update(last_signal['id'], {'date': day}) - else: - rrs.insert({ - 'date': day, - 'type': SignalType.NONE, - 'risk': risk - }) - return None - - def get_signal_type(self, sign_id) -> SignalType | Dict[int, SignalType]: - sign_id = to_tuple(sign_id) - if len(sign_id) > 1: - return {x['id']: SignalType(x['type']) for x in rrs.get_by_ids(sign_id)} - else: - signal = rrs.get_by_id(sign_id[0]) - return SignalType(signal['type']) if signal else None - - def get_signal_date(self, sign_id): - sign_id = to_tuple(sign_id) - if len(sign_id) > 1: - return {x['id']: x['date'] for x in rrs.get_by_ids(sign_id)} - else: - signal = rrs.get_by_id(sign_id[0]) - return signal['date'] if signal else None - - def commit_signal(self, sign_id): - rrs.update(sign_id, {'effective': True}) - - def clear_signal(self, day=None, risk: PortfoliosRisk = None): - rrs.delete(min_date=day, risk=risk) - - -@component(bean_name='signal-report') -class SignalReportor(RoboReportor): - - @autowired - def __init__(self, hold: PortfoliosHolder = None, datum: Datum = None): - self._hold = hold - self._datum = datum - - @property - def report_name(self) -> str: - return '调仓信å·' - - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - result = [] - datums = {str(x['id']): x for x in self._datum.get_datums(type=DatumType.FUND, exclude=False)} - for signal in rrs.get_list(max_date=max_date, min_date=min_date, effective=True): - rebalance_date = self._hold.get_rebalance_date_by_signal(signal['id']) - for fund_id, weight in json.loads(signal['portfolio']).items(): - result.append({ - 'risk': PortfoliosRisk(signal['risk']).name, - 'type': SignalType(signal['type']).name, - 'signal_date': signal['date'], - 'rebalance_date': rebalance_date, - 'portfolio_type': PortfoliosType(signal['portfolio_type']).name, - 'ft_ticker': datums[fund_id]['ftTicker'], - 'blooberg_ticker': datums[fund_id]['bloombergTicker'], - 'fund_name': datums[fund_id]['chineseName'], - 'weight': weight - }) - return result - - -@component(bean_name='daily-signal-report') -class DailySignalReportor(RoboReportor): - - @autowired - def __init__(self, hold: PortfoliosHolder = None, datum: Datum = None): - self._hold = hold - self._datum = datum - - @property - def report_name(self) -> str: - return 'æ¯æ—¥è°ƒä»“ä¿¡å·' - - def load_report(self, max_date=prev_workday(dt.today()), min_date=None) -> List[dict]: - signals = pd.DataFrame(rrs.get_list(max_date=max_date, min_date=min_date)) - signals = signals[(signals['date'].dt.date == max_date.date()) & (signals['type'] != SignalType.NONE.value)] - if not signals.empty: - datum_ids = reduce(lambda x, y: x | y, signals['portfolio'].apply(lambda x: set(json.loads(x).keys()))) - datums = pd.DataFrame(self._datum.get_datums(type=DatumType.FUND, datum_ids=datum_ids)) - datums.set_index('id', inplace=True) - - signals['risk'] = signals.apply(lambda row: PortfoliosRisk(row['risk']).name, axis=1) - signals['rebalance_type'] = signals.apply(lambda row: SignalType(row['type']).name, axis=1) - signals['portfolio_type'] = signals.apply(lambda row: PortfoliosType(row['portfolio_type']).name, axis=1) - signals['portfolio'] = signals.apply(lambda row: [x for x in json.loads(row['portfolio']).items()], axis=1) - signals = signals.explode('portfolio', ignore_index=True) - - signals['weight'] = signals.apply(lambda row: row['portfolio'][1], axis=1) - signals['asset_ids'] = signals.apply(lambda row: datums.loc[int(row['portfolio'][0])]['ftTicker'], axis=1) - signals['name'] = signals.apply(lambda row: datums.loc[int(row['portfolio'][0])]['chineseName'], axis=1) - signals['lipper_id'] = signals.apply(lambda row: datums.loc[int(row['portfolio'][0])]['lipperKey'], axis=1) - - signals = signals[['risk', 'date', 'rebalance_type', 'asset_ids', 'lipper_id', 'name', 'weight']] - return signals.to_dict('records') - - return [] diff --git a/rebalance/signals/__init__.py b/rebalance/signals/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/rebalance/signals/crisis_signal.py b/rebalance/signals/crisis_signal.py deleted file mode 100644 index cd54f85204456539bf0136822f42e1d089bb7a16..0000000000000000000000000000000000000000 --- a/rebalance/signals/crisis_signal.py +++ /dev/null @@ -1,132 +0,0 @@ -from abc import ABC - -import pandas as pd -from dateutil.relativedelta import relativedelta -from py_jftech import get_config, autowired, component - -from api import PortfoliosRisk, SignalType, Navs -from rebalance.base_signal import BaseRebalanceSignal -from rebalance.dao import robo_rebalance_signal as rrs - - -class CrisisSignal(BaseRebalanceSignal, ABC): - - @autowired - def __init__(self, navs: Navs = None): - super().__init__() - self._navs = navs - self._config = get_config(__name__) - - @property - def exp_init(self): - return pd.to_datetime(self._config['exp-init']) if 'exp-init' in self._config else None - - @property - def exp_years(self): - return self._config['exp-years'] if 'exp-years' in self._config else 1 - - @property - def inversion_years(self): - return self._config['inversion-years'] if 'inversion-years' in self._config else 1 - - @property - def inversion_threshold(self): - return self._config['inversion-threshold'] if 'inversion-threshold' in self._config else 0.3 - - def get_exp_start_date(self, day, risk: PortfoliosRisk): - assert day, "get crisis exp start date, day can not be none" - assert risk, "get crisis exp start date, PortfoliosRisk can not be none" - exp_date = day - relativedelta(years=self.exp_years) - if self.exp_init and self.exp_init >= exp_date: - return self.exp_init - exp_signal = rrs.get_first_after(type=SignalType.CRISIS_EXP, risk=risk, min_date=exp_date) - if not exp_signal: - inversion_date = day - relativedelta(years=self.inversion_years) - ten_before = self._navs.get_last_index_close(max_date=inversion_date, ticker='USGG10YR Index') - ten_today = self._navs.get_last_index_close(max_date=day, ticker='USGG10YR Index') - two_before = self._navs.get_last_index_close(max_date=inversion_date, ticker='USGG2YR Index') - two_today = self._navs.get_last_index_close(max_date=day, ticker='USGG2YR Index') - if ten_today['close'] - two_today['close'] <= ten_before['close'] - two_before['close'] and \ - ten_today['close'] - two_today['close'] <= self.inversion_threshold: - last_signal = rrs.get_last_one(max_date=day, risk=risk) - if SignalType(last_signal['type']) is SignalType.NONE: - rrs.update(last_signal['id'], { - 'date': day, - 'type': SignalType.CRISIS_EXP, - }) - else: - rrs.insert({ - 'date': day, - 'type': SignalType.CRISIS_EXP, - 'risk': risk, - }) - exp_signal = rrs.get_first_after(type=SignalType.CRISIS_EXP, risk=risk, min_date=exp_date) - return exp_signal['date'] if exp_signal else None - - -@component(bean_name='crisis_one') -class LastRateCrisisOneSignal(CrisisSignal, BaseRebalanceSignal): - ''' - (close / 850ma – 1) < threshold,threshold=-0.05ã€-0.1 - ''' - @property - def mean_count(self): - return self._config['crisis-1']['mean-count'] - - @property - def threshold(self): - return self._config['crisis-1']['threshold'] - - @property - def signal_type(self): - return SignalType.CRISIS_ONE - - def is_trigger(self, day, risk: PortfoliosRisk) -> bool: - exp_date = self.get_exp_start_date(day, risk) - if exp_date: - crisis_one = rrs.get_first_after(type=SignalType.CRISIS_ONE, risk=risk, min_date=exp_date) - if not crisis_one: - spx = self._navs.get_last_index_close(max_date=day, ticker='SPX Index', count=self.mean_count) - spx = pd.DataFrame(spx) - spx.sort_values(by='date', inplace=True) - return spx.iloc[-1]['close'] / spx['close'].mean() - 1 < self.threshold - return False - - -@component(bean_name='crisis_two') -class CrisisTwoSignal(CrisisSignal, BaseRebalanceSignal): - - @property - def negative_growth_years(self): - return self._config['crisis-2']['negative-growth'] - - @property - def fed_months(self): - return self._config['crisis-2']['fed-months'] - - @property - def fed_threshold(self): - return self._config['crisis-2']['fed-threshold'] - - @property - def signal_type(self): - return SignalType.CRISIS_TWO - - def is_trigger(self, day, risk: PortfoliosRisk) -> bool: - exp_date = self.get_exp_start_date(day, risk) - if exp_date: - crisis_two = rrs.get_first_after(type=SignalType.CRISIS_TWO, risk=risk, min_date=exp_date) - if not crisis_two: - ng_date = day - relativedelta(years=self.negative_growth_years) - ten_today = self._navs.get_last_index_close(max_date=day, ticker='USGG10YR Index') - cpi_today = self._navs.get_last_eco_values(max_date=day, ticker='CPI YOY Index', by_release_date=True) - ten_before = self._navs.get_last_index_close(max_date=ng_date, ticker='USGG10YR Index') - cpi_before = self._navs.get_last_eco_values(max_date=ng_date, ticker='CPI YOY Index', by_release_date=True) - before = ten_before['close'] - cpi_before['indicator'] - today = ten_today['close'] - cpi_today['indicator'] - - fed_today = self._navs.get_last_eco_values(max_date=day, ticker='FDTR Index', by_release_date=True) - fed_before = self._navs.get_last_eco_values(max_date=day - relativedelta(months=self.fed_months), ticker='FDTR Index', by_release_date=True) - - return today <= before and fed_today['indicator'] - fed_before['indicator'] < self.fed_threshold - return False diff --git a/rebalance/signals/curve_drift.py b/rebalance/signals/curve_drift.py deleted file mode 100644 index 1d473fc58df32ceff2c96f5624a5dc707955bf1a..0000000000000000000000000000000000000000 --- a/rebalance/signals/curve_drift.py +++ /dev/null @@ -1,44 +0,0 @@ -from py_jftech import component, autowired -from dateutil.relativedelta import relativedelta - -from api import PortfoliosRisk, SignalType, Datum, PortfoliosHolder, DriftSolver -from rebalance.base_signal import BaseRebalanceSignal -from rebalance.dao import robo_rebalance_signal as rrs - - -@component(bean_name='curve-drift') -class CurveDrift(BaseRebalanceSignal): - - @autowired(names={'solver': 'date-curve'}) - def __init__(self, datum: Datum = None, hold: PortfoliosHolder = None, solver: DriftSolver = None): - super().__init__() - self._datum = datum - self._hold = hold - self._solver = solver - - @property - def exclude_last_type(self): - return [ - SignalType.CRISIS_ONE, - SignalType.CRISIS_TWO, - SignalType.MARKET_RIGHT, - SignalType.INIT - ] - - def is_trigger(self, day, risk: PortfoliosRisk) -> bool: - last_re = rrs.get_last_one(max_date=day, risk=risk, effective=True) - if last_re is None or SignalType(last_re['type']) in self.exclude_last_type: - return False - if last_re['date'] + relativedelta(days=120) <= day: - return True - hr_datums = self._datum.get_high_risk_datums(risk) - datum_ids = [x['id'] for x in hr_datums] - normal_portfolio = self._builder.get_portfolios(day, risk) - normal_weight = round(sum([x[1] for x in normal_portfolio.items() if x[0] in datum_ids]), 2) - hold_portfolio = self._hold.get_portfolios_weight(day, risk) - hold_weight = round(sum([x[1] for x in hold_portfolio.items() if x[0] in datum_ids]), 2) - return normal_weight - hold_weight >= self._solver.get_drift(day, risk) - - @property - def signal_type(self) -> SignalType: - return SignalType.DRIFT_BUY diff --git a/rebalance/signals/high_low_buy.py b/rebalance/signals/high_low_buy.py deleted file mode 100644 index 3b283f9533bc53039890370d682b11c16e7783a4..0000000000000000000000000000000000000000 --- a/rebalance/signals/high_low_buy.py +++ /dev/null @@ -1,68 +0,0 @@ -from py_jftech import component, autowired, get_config - -from api import SignalType, PortfoliosRisk, DriftSolver -from rebalance.base_signal import BaseRebalanceSignal -from rebalance.dao import robo_rebalance_signal as rrs - - -@component(bean_name='high-buy') -class HighBuySignal(BaseRebalanceSignal): - - @autowired(names={'solver': 'high-weight'}) - def __init__(self, solver: DriftSolver = None): - super().__init__() - self._config = get_config(__name__) - self._solver = solver - - @property - def include_last_type(self): - return [ - SignalType.CRISIS_ONE, - SignalType.CRISIS_TWO, - SignalType.MARKET_RIGHT, - SignalType.LOW_BUY, - SignalType.INIT - ] - - @property - def signal_type(self) -> SignalType: - return SignalType.HIGH_BUY - - def get_threshold(self, risk: PortfoliosRisk): - threshold = self._config['threshold'] - if isinstance(threshold, dict): - threshold = threshold[f'ft{risk.value}'] - return threshold - - def is_trigger(self, day, risk: PortfoliosRisk) -> bool: - last_re = rrs.get_last_one(max_date=day, risk=risk, effective=True) - if last_re is None or SignalType(last_re['type']) not in self.include_last_type: - return False - drift = self._solver.get_drift(day, risk) - threshold = self.get_threshold(risk) - return drift >= threshold[1] - - -@component(bean_name='low-buy') -class LowBuySignal(HighBuySignal): - - @property - def include_last_type(self): - return [ - SignalType.CRISIS_ONE, - SignalType.CRISIS_TWO, - SignalType.MARKET_RIGHT, - SignalType.INIT - ] - - @property - def signal_type(self) -> SignalType: - return SignalType.LOW_BUY - - def is_trigger(self, day, risk: PortfoliosRisk) -> bool: - last_re = rrs.get_last_one(max_date=day, risk=risk, effective=True) - if last_re is None or SignalType(last_re['type']) not in self.include_last_type: - return False - drift = self._solver.get_drift(day, risk) - threshold = self.get_threshold(risk) - return threshold[0] <= drift < threshold[1] diff --git a/rebalance/signals/init_signal.py b/rebalance/signals/init_signal.py deleted file mode 100644 index a14a7b201f269c4e087fd09adb7904350fbba56f..0000000000000000000000000000000000000000 --- a/rebalance/signals/init_signal.py +++ /dev/null @@ -1,27 +0,0 @@ -from py_jftech import component, autowired - -from api import PortfoliosRisk, SignalType, RoboExecutor -from rebalance.base_signal import BaseRebalanceSignal -from rebalance.dao import robo_rebalance_signal as rrs - - -@component(bean_name='init') -class InitSignalBuilder(BaseRebalanceSignal): - - @autowired(names={'executor': RoboExecutor.use_name()}) - def __init__(self, executor: RoboExecutor = None): - super(InitSignalBuilder, self).__init__() - self._executor = executor - - @property - def signal_type(self) -> SignalType: - return SignalType.INIT - - def get_signal(self, day, risk: PortfoliosRisk): - signal = rrs.get_last_one(max_date=day, risk=risk, type=SignalType.INIT) - if signal: - return None if signal['effective'] else signal - return super().get_signal(self._executor.start_date, risk) - - def is_trigger(self, day, risk: PortfoliosRisk) -> bool: - return True diff --git a/rebalance/signals/right_side.py b/rebalance/signals/right_side.py deleted file mode 100644 index 1ff582f6865fa104a89f95f496de710bbeab6a19..0000000000000000000000000000000000000000 --- a/rebalance/signals/right_side.py +++ /dev/null @@ -1,93 +0,0 @@ -import pandas as pd -from py_jftech import component, autowired, get_config -from scipy.stats import norm - -from api import SignalType, PortfoliosRisk, Navs -from rebalance.base_signal import BaseRebalanceSignal -from rebalance.dao import robo_rebalance_signal as rrs - - -@component(bean_name='market-right') -class MarketRight(BaseRebalanceSignal): - - @autowired - def __init__(self, navs: Navs = None): - super().__init__() - self._navs = navs - self._config = get_config(__name__) - - @property - def rtn_days(self): - return self._config['rtn-days'] - - @property - def min_threshold(self): - return self._config['min-threshold'] - - @property - def coef(self): - return self._config['coef'] - - @property - def signal_type(self) -> SignalType: - return SignalType.MARKET_RIGHT - - @property - def cvar_min_volume(self): - return self._config['cvar-min-volume'] - - @property - def exclude_last_type(self): - return [ - SignalType.CRISIS_ONE, - SignalType.CRISIS_TWO, - SignalType.INIT, - SignalType.MARKET_RIGHT - ] - - def is_trigger(self, day, risk: PortfoliosRisk) -> bool: - last_re = rrs.get_last_one(risk=risk, max_date=day, effective=True) - if last_re is not None and SignalType(last_re['type']) in self.exclude_last_type: - return False - spx = self.load_spx_close_rtns(day) - if spx[-1]['rtn'] > self.min_threshold: - return False - cvar = self.get_cvar(day, risk, spx=spx) - return cvar is not None and spx[-1]['rtn'] < cvar - - def get_cvar(self, day, risk: PortfoliosRisk, spx=None): - if spx is None: - spx = self.load_spx_close_rtns(day) - start_date = self.find_cvar_start_date(day, risk, spx=spx) - if start_date: - spx = pd.DataFrame(spx) - spx = spx[(spx.date >= start_date) & (spx.date <= day)] - if len(spx) >= self.cvar_min_volume: - alpha = round(1 - self.coef, 2) - mean = spx.rtn.mean() - std = spx.rtn.std() - return mean - std * norm.pdf(norm.ppf(alpha)) / alpha - return None - - def find_cvar_start_date(self, day, risk: PortfoliosRisk, spx=None): - if spx is None: - spx = self.load_spx_close_rtns(day) - spx = pd.DataFrame(spx) - last_right = rrs.get_last_one(type=(SignalType.MARKET_RIGHT, SignalType.INIT), max_date=day, risk=risk, - effective=True) - last_buy = rrs.get_first_after(type=(SignalType.LOW_BUY, SignalType.HIGH_BUY), risk=risk, effective=True, - min_date=last_right['date']) - if not last_buy or not last_right or last_buy['date'] <= last_right['date']: - return None - spx = spx[(spx['date'] >= last_right['date']) & (spx['date'] <= last_buy['date'])] - if not spx.empty and len(spx) > 2: - return spx.loc[spx.close.idxmin()].date - return None - - def load_spx_close_rtns(self, day): - spx = pd.DataFrame(self._navs.get_index_close(ticker='SPX Index', max_date=day)) - spx.sort_values('date', inplace=True) - spx['rtn'] = spx['close'] / spx['close'].shift(self.rtn_days) - 1 - spx.dropna(inplace=True) - spx = spx[['date', 'close', 'rtn']] - return spx.to_dict('records') diff --git a/rebalance/test_case.py b/rebalance/test_case.py deleted file mode 100644 index 62fa8d2493b2ae0f84d7411740a89c9aacfa0a62..0000000000000000000000000000000000000000 --- a/rebalance/test_case.py +++ /dev/null @@ -1,68 +0,0 @@ -import logging -import unittest - -from dateutil.relativedelta import relativedelta -from py_jftech import autowired, parse_date, to_str, next_workday - -from api import RebalanceSignal, PortfoliosRisk, RebalanceRuler, RoboReportor - -logger = logging.getLogger(__name__) - - -class RebalanceTest(unittest.TestCase): - - @autowired(names={'builder': 'crisis_one'}) - def test_crisis_one(self, builder: RebalanceSignal = None): - start = parse_date('2008-03-12') - end = start + relativedelta(years=3) - while start < end: - signal = builder.is_trigger(start, PortfoliosRisk.FT9) - if signal: - logger.info(start) - start = next_workday(start) - - @autowired(names={'builder': 'crisis_two'}) - def test_crisis_two(self, builder: RebalanceSignal = None): - start = parse_date('2008-01-02') - end = start + relativedelta(years=3) - while start < end: - signal = builder.is_trigger(start, PortfoliosRisk.FT9) - if signal: - logger.info(start) - start = next_workday(start) - - @autowired(names={'builder': 'market-right'}) - def test_market_right(self, builder: RebalanceSignal = None): - signal = builder.get_signal(parse_date('2008-01-07'), PortfoliosRisk.FT9) - logger.info(signal) - - @autowired(names={'builder': 'curve-drift'}) - def test_curve_drift(self, builder: RebalanceSignal = None): - signal = builder.get_signal(parse_date('2022-11-07'), PortfoliosRisk.FT3) - logger.info(signal) - - @autowired(names={'builder': 'high-buy'}) - def test_high_buy(self, builder: RebalanceSignal = None): - builder.get_signal(parse_date('2022-09-10'), PortfoliosRisk.FT3) - - @autowired - def test_rebalance_builder(self, builder: RebalanceRuler = None): - builder.take_next_signal(parse_date('2020-04-29'), PortfoliosRisk.FT9) - - @autowired(names={'reportor': 'signal-report'}) - def test_signal_report(self, reportor: RoboReportor = None): - result = reportor.load_report() - logger.info(to_str(result, show_line=10)) - - @autowired(names={'reportor': 'daily-signal-report'}) - def test_daily_signal_report(self, reportor: RoboReportor = None): - result = reportor.load_report(max_date=parse_date('2022-11-21')) - logger.info(to_str(result, show_line=10)) - - @autowired - def test_clear_signal(self, ruler: RebalanceRuler = None): - ruler.clear_signal() - - -if __name__ == '__main__': - unittest.main() diff --git a/reports/__init__.py b/reports/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/reports/benchmark.py b/reports/benchmark.py deleted file mode 100644 index 8d5d072ce029f9a294dd17d883f519c8596f52e5..0000000000000000000000000000000000000000 --- a/reports/benchmark.py +++ /dev/null @@ -1,125 +0,0 @@ -from datetime import datetime as dt, timedelta -from typing import List - -import pandas as pd -from py_jftech import component, autowired, prev_workday, filter_weekend, next_workday, get_config - -from api import RoboReportor, PortfoliosRisk, RoboExecutor, Navs, Datum, DatumType -from reports.dao import robo_benckmark as rb - - -@component(bean_name='benckmark-report') -class BenchmarkReportor(RoboReportor): - - @autowired(names={'executor': RoboExecutor.use_name()}) - def __init__(self, executor: RoboExecutor = None, navs: Navs = None, datum: Datum = None): - self._exec = executor - self._navs = navs - self._datum = datum - self._config = get_config(__name__) - - @property - def report_name(self) -> str: - return 'benchmark' - - @property - def risks(self): - return self._config['stock-rate'].keys() - - @property - def init_amount(self): - return self._config['init-amount'] - - def stock_rate(self, risk): - return self._config['stock-rate'][risk] - - def load_nav_rtn(self, risk, day): - last = rb.get_last_one(risk=risk, max_date=day, re=True) - start_date = next_workday(last['date']) if last else self._exec.start_date - - datums = {x['id']: x for x in self._datum.get_datums(type=DatumType.FUND)} - navs = pd.DataFrame(self._navs.get_fund_navs(fund_ids=tuple(datums.keys()), min_date=prev_workday(start_date - timedelta(10)), max_date=day)) - navs = navs.pivot_table(index='nav_date', columns='fund_id', values='nav_cal') - navs.fillna(method='ffill', inplace=True) - nav_index = navs.shape[1] - for i in range(nav_index): - navs[f'rtn_{navs.columns[i]}'] = navs[navs.columns[i]] / navs[navs.columns[i]].shift() - 1 - navs = navs[navs.index >= start_date] - return navs, nav_index - - def find_datum_asset(self): - return {x['id']: x['assetType'] for x in self._datum.get_datums(type=DatumType.FUND)} - - def build_benchmark(self, risk, day=dt.today()): - nav_rtn, nav_index = self.load_nav_rtn(risk=risk, day=day) - asset_types = {x['id']: x['assetType'] for x in self._datum.get_datums(type=DatumType.FUND)} - - last = rb.get_last_one(risk=risk, max_date=day, re=True) - init_amount = last['nav'] if last else self.init_amount - stock_rate = self.stock_rate(risk) - other_rate = 1 - stock_rate - - five_rtn = 0 - last_day = None - fund_ids = None - - for index, row in nav_rtn.iterrows(): - if last_day is None or fund_ids is None: - fund_ids = list(row.iloc[:nav_index].dropna().index) - stock_count = len([x for x in fund_ids if asset_types[x] == 'STOCK']) - stock_average = init_amount * stock_rate / stock_count - other_average = init_amount * other_rate / (len(fund_ids) - stock_count) - nav_rtn.loc[index, f'{risk}_result'] = init_amount - nav_rtn.loc[index, f'{risk}_re'] = 1 - for fund_id in fund_ids: - if fund_id and asset_types[fund_id] == 'STOCK': - nav_rtn.loc[index, f'stock_{fund_id}'] = stock_average - else: - nav_rtn.loc[index, f'other_{fund_id}'] = other_average - else: - for fund_id in fund_ids: - if fund_id and asset_types[fund_id] == 'STOCK': - nav_rtn.loc[index, f'stock_{fund_id}'] = nav_rtn.loc[last_day, f'stock_{fund_id}'] * ( - 1 + nav_rtn.loc[index, f'rtn_{fund_id}']) - else: - nav_rtn.loc[index, f'other_{fund_id}'] = nav_rtn.loc[last_day, f'other_{fund_id}'] * ( - 1 + nav_rtn.loc[index, f'rtn_{fund_id}']) - nav_rtn.loc[index, f'{risk}_result'] = nav_rtn.loc[index][-len(fund_ids):].sum() - nav_rtn.loc[index, f'{risk}_re'] = 0 - if five_rtn == 5: - five_rtn = 0 - fund_ids = list(row.iloc[:nav_index].dropna().index) - stock_count = len([x for x in fund_ids if asset_types[x] == 'STOCK']) - - stock_average = nav_rtn.loc[index, f'{risk}_result'] * stock_rate / stock_count - other_average = nav_rtn.loc[index, f'{risk}_result'] * other_rate / (len(fund_ids) - stock_count) - nav_rtn.loc[index, f'{risk}_re'] = 1 - for fund_id in fund_ids: - if fund_id and asset_types[fund_id] == 'STOCK': - nav_rtn.loc[index, f'stock_{fund_id}'] = stock_average - else: - nav_rtn.loc[index, f'other_{fund_id}'] = other_average - five_rtn += 1 - last_day = index - - result = nav_rtn.reindex(columns=[f'{risk}_result', f'{risk}_re']) - result.reset_index(inplace=True) - result['risk'] = risk - result.rename(columns={f'{risk}_result': 'nav', f'{risk}_re': 're', 'nav_date': 'date'}, inplace=True) - result['nav'] = round(result['nav'], 4) - return result.to_dict('records') - - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - for risk in self.risks: - last = rb.get_last_one(max_date=max_date, risk=risk) - if not last or last['date'] < filter_weekend(max_date): - benchmarks = pd.DataFrame(self.build_benchmark(risk=risk, day=max_date)) - if last: - benchmarks = benchmarks[benchmarks.date > last['date']] - if not benchmarks.empty: - rb.batch_insert(benchmarks.to_dict('records')) - result = pd.DataFrame(rb.get_list(max_date=max_date, min_date=min_date)) - result = result.pivot_table(index='date', columns='risk', values='nav') - result.reset_index(inplace=True) - return result.to_dict('records') - diff --git a/reports/combo_datas.py b/reports/combo_datas.py deleted file mode 100644 index 8f3739ef3007acf5bc29d201b6dbfb45957d81d9..0000000000000000000000000000000000000000 --- a/reports/combo_datas.py +++ /dev/null @@ -1,42 +0,0 @@ -from datetime import datetime as dt -from typing import List - -import pandas as pd -from py_jftech import component, autowired -from empyrical import annual_return, annual_volatility, max_drawdown, sharpe_ratio - -from api import RoboReportor, Navs - - -@component(bean_name='combo-report') -class ComboDatasReport(RoboReportor): - - @autowired(names={'hold_reportor': 'hold-report', 'benchmark': 'benckmark-report'}) - def __init__(self, hold_reportor: RoboReportor = None, benchmark: RoboReportor = None, navs: Navs = None): - self._hold_reportor = hold_reportor - self._benchmark = benchmark - self._navs = navs - - @property - def report_name(self) -> str: - return 'æ··åˆæ•°æ®' - - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - holds = pd.DataFrame(self._hold_reportor.load_report(max_date=max_date, min_date=min_date)) - if not holds.empty: - holds['risk'] = holds.apply(lambda row: row.risk, axis=1) - datas = holds.pivot_table(index='date', columns='risk', values='nav') - - benchmark = pd.DataFrame(self._benchmark.load_report(max_date=max_date, min_date=min_date)) - datas = datas.join(benchmark.set_index('date')) - - spx = pd.DataFrame(self._navs.get_index_close(ticker='SPX Index', min_date=min_date, max_date=max_date)) - spx = spx.pivot_table(index='date', columns='index_id', values='close') - spx.columns = ['SPX'] - datas = datas.join(spx) - - datas.fillna(method='ffill', inplace=True) - datas.reset_index(inplace=True) - return datas.to_dict('records') - return [] - diff --git a/reports/dao/__init__.py b/reports/dao/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/reports/dao/mysql.sql b/reports/dao/mysql.sql deleted file mode 100644 index 9e18160ddd46ecb58ee80c7d446d9fda83f35887..0000000000000000000000000000000000000000 --- a/reports/dao/mysql.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE IF NOT EXISTS robo_benchmark -( - rb_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - rb_date DATETIME NOT NULL COMMENT '日期', - rb_risk VARCHAR(255) NOT NULL COMMENT '风险ç‰çº§', - rb_nav DOUBLE(16, 4) NOT NULL COMMENT '资产值', - rb_re TINYINT NOT NULL DEFAULT 0 COMMENT '是å¦å†åˆ†é…', - rb_create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - rb_update_time DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (rb_id), - UNIQUE INDEX (rb_date, rb_risk), - INDEX (rb_risk) -) ENGINE = InnoDB - AUTO_INCREMENT = 0 - DEFAULT CHARSET = utf8mb4 COMMENT 'BENCHMARKæ•°æ®è¡¨'; \ No newline at end of file diff --git a/reports/dao/robo_benckmark.py b/reports/dao/robo_benckmark.py deleted file mode 100644 index ffdb988354e629ca803f6318dfbc3937e4120f18..0000000000000000000000000000000000000000 --- a/reports/dao/robo_benckmark.py +++ /dev/null @@ -1,38 +0,0 @@ -from py_jftech import read, write, where, mapper_columns, format_date - -__COLUMNS__ = { - 'rb_id': 'id', - 'rb_date': 'date', - 'rb_risk': 'risk', - 'rb_nav': 'nav', - 'rb_re': 're', -} - - -@write -def batch_insert(datas): - datas = [mapper_columns(x, __COLUMNS__) for x in datas] - values = ','.join([f'''({','.join([(f"'{x[j]}'" if j in x and x[j] is not None else 'null') for j in __COLUMNS__.keys() if j != 'rb_id'])})''' for x in datas]) - return f'''insert into robo_benchmark({','.join([x for x in __COLUMNS__.keys() if x != 'rb_id'])}) values {values}''' - - -@read(one=True) -def get_last_one(max_date=None, risk=None, re: bool = None): - sql = f"rb_date <= '{format_date(max_date)}'" if max_date else None - return f''' - select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_benchmark - {where(sql, rb_risk=risk, rb_re=re)} order by rb_date desc limit 1 - ''' - - -@read -def get_list(max_date=None, min_date=None, risk=None, re: bool = None): - sqls = [] - if max_date: - sqls.append(f"rb_date <= '{format_date(max_date)}'") - if min_date: - sqls.append(f"rb_date >= '{format_date(min_date)}'") - return f''' - select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_benchmark - {where(*sqls, rb_risk=risk, rb_re=re)} order by rb_risk, rb_date - ''' diff --git a/reports/exports.py b/reports/exports.py deleted file mode 100644 index 6f356133db98c35510994d97d95d576e5fb793a0..0000000000000000000000000000000000000000 --- a/reports/exports.py +++ /dev/null @@ -1,120 +0,0 @@ -import os -from datetime import datetime as dt -from typing import List -from abc import abstractmethod, ABCMeta -from tempfile import TemporaryDirectory -from shutil import copyfile -from copy import deepcopy - -import pandas as pd -from py_jftech import component, autowired, get_config, get_instance_name, get_project_path, format_date, sendmail - -from api import RoboReportor, RoboExportor - - -def include_report(): - return get_config(__name__)['include-report'] - - -class DefaultExportor(RoboExportor): - - @autowired - def __init__(self, reportors: List[RoboReportor] = None): - self._reportors = {get_instance_name(x): x for x in reportors} - - def export(self, max_date=dt.today(), min_date=None): - if not self.include_report: - return None - with TemporaryDirectory() as tmpdir: - filename = f"{self.file_name}_{format_date(max_date)}.xlsx" - if min_date: - filename = f"{self.file_name}_{format_date(min_date)}_to_{format_date(max_date)}.xlsx" - filepath = os.path.join(tmpdir, filename) - with pd.ExcelWriter(filepath) as writer: - for reportor_name in self.include_report: - reportor = self._reportors[reportor_name] - datas = pd.DataFrame(reportor.load_report(max_date=max_date, min_date=min_date)) - if not datas.empty: - datas.to_excel(writer, sheet_name=reportor.report_name, index=False) - email = self.get_email(filepath) - if email: - receives = email['receives'] - copies = email['copies'] if 'copies' in email else [] - attach_paths = [filepath] - subject = email['subject'].format(today=format_date(dt.today()), max_date=max_date, min_date=min_date) - content = email['content'].format(today=format_date(dt.today()), max_date=max_date, min_date=min_date) - sendmail(receives=receives, copies=copies, attach_paths=attach_paths, subject=subject, content=content) - if self.save_path is not None: - os.makedirs(self.save_path, exist_ok=True) - save_file = os.path.join(self.save_path, filename) - copyfile(filepath, save_file) - - def get_email(self, file): - return deepcopy(self.config['email']) if 'email' in self.config else None - - @property - def save_path(self): - if 'save-path' not in self.config: - return None - save_path: str = self.config['save-path'] - if save_path.startswith('.'): - return os.path.abspath(os.path.join(os.path.dirname(__file__), save_path)) - elif save_path.startswith('/'): - return os.path.abspath(save_path) - return os.path.abspath(os.path.join(get_project_path(), save_path)) - - @property - def exist_build(self): - return self.config['exist-build'] if 'exist-build' in self.config else False - - @property - def file_name(self): - return self.config['file-name'] if 'file-name' in self.config else 'export' - - @property - def include_report(self): - return self.config['include-report'] if 'include-report' in self.config else [] - - @property - @abstractmethod - def config(self): - pass - - -@component(bean_name='backtest-export') -class BacktestExportor(DefaultExportor): - - def __init__(self): - super(BacktestExportor, self).__init__() - self.__config = deepcopy(get_config(__name__)) - - @property - def config(self): - return self.__config['backtest'] - - -@component(bean_name='real-daily-export') -class RealDailyExportor(DefaultExportor): - - @autowired(names={'signal_reportor': 'daily-signal-report'}) - def __init__(self, signal_reportor: RoboReportor = None): - super(RealDailyExportor, self).__init__() - self.__config = get_config(__name__) - self._signal_reportor = signal_reportor - - def get_email(self, file): - result = super(RealDailyExportor, self).get_email(file) - if result is None: - return None - content = pd.read_excel(file, sheet_name=None) - if self._signal_reportor.report_name in content: - result['subject'] = str(result['subject']['rebalance']) - result['content'] = result['content']['rebalance'] - else: - result['subject'] = result['subject']['default'] - result['content'] = result['content']['rebalance'] - return result - - @property - def config(self): - return self.__config['real-daily'] diff --git a/reports/fixed_range.py b/reports/fixed_range.py deleted file mode 100644 index 42b575d28551ff6b47b1a87a638fa8f39610eacb..0000000000000000000000000000000000000000 --- a/reports/fixed_range.py +++ /dev/null @@ -1,41 +0,0 @@ -from datetime import datetime as dt -from typing import List - -import pandas as pd -from py_jftech import component, autowired, get_config, format_date, filter_weekend - -from api import RoboReportor - - -@component(bean_name='fixed-range-report') -class FixedRangeReport(RoboReportor): - - @autowired(names={'combo': 'combo-report'}) - def __init__(self, combo: RoboReportor = None): - self._combo = combo - self._config = get_config(__name__) - - @property - def report_name(self) -> str: - return '固定区间收益率' - - @property - def range_dates(self): - return self._config['range-dates'] - - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - datas = pd.DataFrame(self._combo.load_report(max_date=max_date, min_date=min_date)) - datas.set_index('date', inplace=True) - if not datas.empty: - result = pd.DataFrame(columns=datas.columns) - for range in self.range_dates: - start = filter_weekend(range['start']) - end = filter_weekend(range['end']) - if not datas[start:end].empty: - row_name = f"{format_date(start)}~{format_date(end)}" - result.loc[row_name] = datas[start:end].values[-1] / datas[start:end].values[0] - 1 - result = round(result, 4) * 100 - result.reset_index(inplace=True) - result.rename(columns={'index': 'range-date'}, inplace=True) - return result.to_dict('records') - return [] diff --git a/reports/indicators.py b/reports/indicators.py deleted file mode 100644 index 766d204d5802b5e2a8fe4d698be73b9b565031a8..0000000000000000000000000000000000000000 --- a/reports/indicators.py +++ /dev/null @@ -1,38 +0,0 @@ -from datetime import datetime as dt -from typing import List - -import pandas as pd -from empyrical import annual_return, annual_volatility, max_drawdown, sharpe_ratio -from py_jftech import component, autowired - -from api import RoboReportor - - -@component(bean_name='indicators-report') -class IndicatorsReportor(RoboReportor): - - @autowired(names={'combo': 'combo-report'}) - def __init__(self, combo: RoboReportor = None): - self._combo = combo - - @property - def report_name(self) -> str: - return 'æŒ‡æ ‡' - - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - datas = pd.DataFrame(self._combo.load_report(max_date=max_date, min_date=min_date)) - datas.set_index('date', inplace=True) - if not datas.empty: - returns = round(datas.pct_change(), 5) - indicators = { - 'annual_return': list(annual_return(returns, period='daily', annualization=None) * 100), - 'annual_volatility': annual_volatility(returns, period='daily', annualization=None) * 100, - 'max_drawdown': max_drawdown(returns, out=None) * 100, - 'sharp': sharpe_ratio(returns, risk_free=0, period='daily', annualization=None), - } - indicators['calmar'] = abs(indicators['annual_return'] / indicators['max_drawdown']) - result = pd.DataFrame(indicators.values(), index=indicators.keys(), columns=list(returns.columns)).round(2) - result.reset_index(inplace=True) - result.rename(columns={'index': 'indicators'}, inplace=True) - return result.to_dict('records') - return [] diff --git a/reports/relative_range.py b/reports/relative_range.py deleted file mode 100644 index ba47a6c3df4f18bc66bda4781c7f69a27eced3ea..0000000000000000000000000000000000000000 --- a/reports/relative_range.py +++ /dev/null @@ -1,43 +0,0 @@ -from datetime import datetime as dt -from typing import List - -import pandas as pd -from dateutil.relativedelta import relativedelta -from py_jftech import component, autowired, get_config, format_date, filter_weekend - -from api import RoboReportor - - -@component(bean_name='relative-range-report') -class RelativeRangeReport(RoboReportor): - - @autowired(names={'combo': 'combo-report'}) - def __init__(self, combo: RoboReportor = None): - self._combo = combo - self._config = get_config(__name__) - - @property - def report_name(self) -> str: - return '相对区间收益率' - - @property - def range_dates(self): - return self._config['range-dates'] - - def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: - datas = pd.DataFrame(self._combo.load_report(max_date=max_date, min_date=min_date)) - datas.set_index('date', inplace=True) - if not datas.empty: - result = pd.DataFrame(columns=datas.columns) - for range in self.range_dates: - kwargs = range.copy() - del kwargs['name'] - start = filter_weekend(max_date - relativedelta(**kwargs)) - end = filter_weekend(max_date) - row_name = f"{range['name']}({format_date(start)}~{format_date(end)})" - result.loc[row_name] = datas[start:end].values[-1] / datas[start:end].values[0] - 1 - result = round(result, 4) * 100 - result.reset_index(inplace=True) - result.rename(columns={'index': 'range-date'}, inplace=True) - return result.to_dict('records') - return [] diff --git a/reports/test_case.py b/reports/test_case.py deleted file mode 100644 index f0023b2d5af4793719f8b763998a732ca29a6798..0000000000000000000000000000000000000000 --- a/reports/test_case.py +++ /dev/null @@ -1,44 +0,0 @@ -import unittest -import logging -import tempfile -from datetime import datetime as dt - -from py_jftech import autowired, to_str, parse_date, prev_workday -from api import RoboReportor, RoboExportor - -logger = logging.getLogger(__name__) - - -class ReportTest(unittest.TestCase): - - @autowired(names={'reportor': 'benckmark-report'}) - def test_benchmark_report(self, reportor: RoboReportor = None): - result = reportor.load_report(max_date=parse_date('2022-11-01')) - logger.info(to_str(result)) - - @autowired(names={'reportor': 'indicators-report'}) - def test_indicator_report(self, reportor: RoboReportor = None): - result = reportor.load_report(max_date=parse_date('2022-11-01')) - logger.info(to_str(result)) - - @autowired(names={'reportor': 'fixed-range-report'}) - def test_fixed_report(self, reportor: RoboReportor = None): - result = reportor.load_report(max_date=parse_date('2022-11-01')) - logger.info(to_str(result)) - - @autowired(names={'reportor': 'relative-range-report'}) - def test_relative_report(self, reportor: RoboReportor = None): - result = reportor.load_report(max_date=parse_date('2022-11-01')) - logger.info(to_str(result)) - - @autowired(names={'exportor': 'backtest-export'}) - def test_backtest_export(self, exportor: RoboExportor = None): - exportor.export(max_date=parse_date('2022-11-01')) - - @autowired(names={'exportor': 'real-daily-export'}) - def test_daliy_export(self, exportor: RoboExportor = None): - exportor.export(max_date=prev_workday(dt.today())) - - -if __name__ == '__main__': - unittest.main() diff --git a/robo_executor.py b/robo_executor.py index 01ddaf3209bde87ad5e360954156ac0eedf0073d..0878dc97630fd54b7fdd58e719b22b943eb34c16 100644 --- a/robo_executor.py +++ b/robo_executor.py @@ -1,5 +1,4 @@ import logging -import os import sys from concurrent.futures import wait from datetime import datetime as dt @@ -9,12 +8,12 @@ from typing import List import pandas as pd from py_jftech import ( component, autowired, get_config, filter_weekend, asynchronized, - workday_range, format_date, prev_workday, parse_date, is_workday + parse_date ) from api import ( - RoboExecutor, AssetRisk, Datum, AssetPool, PortfoliosBuilder, DatumType, RoboExportor, - PortfoliosRisk, PortfoliosHolder, PortfoliosType, RebalanceRuler, DataSync + RoboExecutor, Datum, AssetPool, PortfoliosBuilder, + PortfoliosRisk, PortfoliosHolder, DataSync ) logger = logging.getLogger(__name__) @@ -22,10 +21,9 @@ logger = logging.getLogger(__name__) @unique class BacktestStep(Enum): - EWMA_VALUE = 1 - ASSET_POOL = 2 - NORMAL_PORTFOLIO = 3 - HOLD_PORTFOLIO = 4 + ASSET_POOL = 1 + NORMAL_PORTFOLIO = 2 + HOLD_PORTFOLIO = 3 def within(self, step: Enum): return self.value <= step.value @@ -37,19 +35,29 @@ class BacktestStep(Enum): @component(bean_name='backtest') class BacktestExecutor(RoboExecutor): - @autowired(names={'export': 'backtest-export'}) - def __init__(self, risk: AssetRisk = None, datum: Datum = None, pool: AssetPool = None, syncs: List[DataSync] = None, - builder: PortfoliosBuilder = None, hold: PortfoliosHolder = None, rule: RebalanceRuler = None, export: RoboExportor = None): - self._risk = risk + @autowired(names={'optimize': 'dividend'}) + def __init__(self, datum: Datum = None, pool: AssetPool = None, + syncs: List[DataSync] = None, + builder: PortfoliosBuilder = None, hold: PortfoliosHolder = None): self._datum = datum self._pool = pool self._builder = builder self._hold = hold - self._rule = rule self._syncs = syncs - self._export = export self._config = get_config(__name__)['backtest'] + @staticmethod + def get_first_business_day(start_date, end_date): + # ç”Ÿæˆæ—¥æœŸèŒƒå›´å¹¶è½¬æ¢ä¸ºDataFrame + dates = pd.date_range(start_date, end_date, freq='MS') + df = pd.DataFrame({'dates': dates}) + # æå–æ¯ä¸ªæœˆçš„第一个工作日 + df['first_business_day'] = df['dates'].apply( + lambda x: pd.date_range(start=x, end=x + pd.offsets.MonthEnd(0), freq='B')[0] + ) + # 返回第一个工作日列表 + return list(df['first_business_day']) + @property def start_date(self): return pd.to_datetime(filter_weekend(self._config['start-date'])) @@ -75,19 +83,16 @@ class BacktestExecutor(RoboExecutor): return self._config['clean-up'] if 'clean-up' in self._config else True def clear_datas(self): - if self.start_step.within(BacktestStep.EWMA_VALUE) and self.end_step.without(BacktestStep.EWMA_VALUE): - logger.info('start to clear fund ewma value'.center(50, '-')) - self._risk.clear() if self.start_step.within(BacktestStep.ASSET_POOL) and self.end_step.without(BacktestStep.ASSET_POOL): logger.info('start to clear asset pool'.center(50, '-')) self._pool.clear() - if self.start_step.within(BacktestStep.NORMAL_PORTFOLIO) and self.end_step.without(BacktestStep.NORMAL_PORTFOLIO): + if self.start_step.within(BacktestStep.NORMAL_PORTFOLIO) and self.end_step.without( + BacktestStep.NORMAL_PORTFOLIO): logger.info('start to clear normal portfolios'.center(50, '-')) self._builder.clear() if self.start_step.within(BacktestStep.HOLD_PORTFOLIO) and self.end_step.without(BacktestStep.HOLD_PORTFOLIO): logger.info('start to clear hold portfolios'.center(50, '-')) self._hold.clear() - self._rule.clear_signal() def start_exec(self): if self.is_sync_data: @@ -95,24 +100,19 @@ class BacktestExecutor(RoboExecutor): sync.do_sync() if self.is_clean_up: self.clear_datas() - if self.start_step.within(BacktestStep.EWMA_VALUE) and self.end_step.without(BacktestStep.EWMA_VALUE): - logger.info("start to build fund ewma value.".center(50, '-')) - now = dt.now() - wait([self.async_build_risk_date(x['id']) for x in self._datum.get_datums(type=DatumType.FUND, risk=(3, 4, 5))]) - logger.info(f"build fund ewma value success, use[{(dt.now() - now).seconds}s]") if self.start_step.within(BacktestStep.ASSET_POOL) and self.end_step.without(BacktestStep.ASSET_POOL): logger.info("start to build asset pool".center(50, '-')) now = dt.now() - workdays = workday_range(self.start_date, self.end_date) - for date in workdays: - self._risk.get_risk_pool(date) + workdays = self.get_first_business_day(self.start_date, self.end_date) for date in workdays: self._pool.get_pool(date) - logger.info(f"build asset pool success, use[{(dt.now() - now).seconds}s]") - if self.start_step.within(BacktestStep.NORMAL_PORTFOLIO) and self.end_step.without(BacktestStep.NORMAL_PORTFOLIO): + logger.info(f"build asset pool success, use[{(dt.now() - now).seconds}s]") + if self.start_step.within(BacktestStep.NORMAL_PORTFOLIO) and self.end_step.without( + BacktestStep.NORMAL_PORTFOLIO): logger.info("start to build normal portfolios".center(50, '-')) now = dt.now() - wait([self.async_build_portfolios(day, risk) for risk in PortfoliosRisk for day in workday_range(self.start_date, self.end_date)]) + wait([self.async_build_portfolios(day, risk) for risk in PortfoliosRisk for day in + self.get_first_business_day(self.start_date, self.end_date)]) logger.info(f"build normal portfolios success, use[{(dt.now() - now).seconds}s]") if self.start_step.within(BacktestStep.HOLD_PORTFOLIO) and self.end_step.without(BacktestStep.HOLD_PORTFOLIO): logger.info("start to build hold portfolios".center(50, '-')) @@ -120,9 +120,6 @@ class BacktestExecutor(RoboExecutor): wait([self.async_build_hold(x) for x in PortfoliosRisk]) logger.info(f"build hold portfolios success, use[{(dt.now() - now).seconds}s]") logger.info("start to export report".center(50, '-')) - now = dt.now() - file = self._export.export(max_date=self.end_date, min_date=self.start_date) - logger.info(f"report file[{os.path.basename(file)}] exported successfully. use[{(dt.now() - now).seconds}s].") @asynchronized(isolate=True) def async_build_risk_date(self, asset_id): @@ -141,11 +138,11 @@ class BacktestExecutor(RoboExecutor): class RealExecutor(RoboExecutor): @autowired - def __init__(self, builder: PortfoliosBuilder = None, hold: PortfoliosHolder = None, syncs: List[DataSync] = None, ruler: RebalanceRuler = None): + def __init__(self, builder: PortfoliosBuilder = None, hold: PortfoliosHolder = None, syncs: List[DataSync] = None, + ): self._builder = builder self._hold = hold self._syncs = syncs - self._ruler = ruler self._config = get_config(__name__)['real'] @property @@ -173,18 +170,3 @@ class RealExecutor(RoboExecutor): if self.is_sync_data: for sync in self._syncs: sync.do_sync() - date = self.curt_date - if is_workday(date) or date in self.include_date: - date = prev_workday(date) - for risk in PortfoliosRisk: - logger.info(f"start to build risk[{risk.name}] real for date[{format_date(date)}]".center(50, '-')) - now = dt.now() - # å› ä¸ºæ¯å¤©éƒ½å¿…须有NORMAL最优投组,ä¸ç®¡ç”¨ä¸ç”¨ - self._builder.get_portfolios(date, risk) - self._hold.build_hold_portfolio(date, risk) - self._ruler.take_next_signal(date, risk) - # å¦‚æžœå½“å‰æŒä»“为风控投组,则还è¦è®¡ç®—风控投组,ä¸ç®¡ç”¨ä¸ç”¨ - p_type = self._hold.get_portfolio_type(date, risk) - if p_type is not PortfoliosType.NORMAL: - self._builder.get_portfolios(date, risk, type=p_type) - logger.info(f"build risk[{risk.name}] real for date[{format_date(date)}] success, use[{(dt.now() - now).seconds}s]")