diff --git a/config-svrobo4.yml b/config-svrobo4.yml index 2ace1f2d61afa1e799fd51fcd366de1a3bc41af2..7671fc4cb869d5fbe4e7ddf6c27674ecca9fd512 100644 --- a/config-svrobo4.yml +++ b/config-svrobo4.yml @@ -41,10 +41,11 @@ py-jftech: datum: basic.datum.HkDatum hold-report: portfolios.holder.DivHoldReportor mpt: portfolios.builder.PoemPortfoliosBuilder -# email: -# server: smtphz.qiye.163.com -# user: jft-ra@thizgroup.com -# password: 5dbb#30ec6d3 + dividend-holder: portfolios.holder.DividendPortfoliosHolder + email: + server: smtphz.qiye.163.com + user: jft-ra@thizgroup.com + password: 5dbb#30ec6d3 mulit-process: max-workers: ${MAX_PROCESS:4} basic: # åŸºç¡€ä¿¡æ¯æ¨¡å— @@ -236,7 +237,9 @@ robo-executor: # 执行器相关 end-step: ${BACKTEST_END_STEP:3} # å›žæµ‹ä»Žå“ªä¸€æ¥æ‰§è¡Œå®ŒæˆåŽç»“æŸæ‰§è¡Œ 1:è®¡ç®—èµ„äº§æ± ï¼›2:计算最优投组:3:计算å†å¹³è¡¡ä¿¡å·ä»¥åŠæŒä»“投组 clean-up: true real: # 实盘执行器 - start-date: 2022-09-01 # 实盘开始时间 + export: ${EXPORT_ENABLE:on} # 是å¦å¼€å¯æŠ¥å‘Š + start-date: 2023-01-01 # 实盘开始时间 + include-date: [] diff --git a/config-svrobo5.yml b/config-svrobo5.yml index 133e6c3bcf845043dde89fb88a2bb3dcec3d92bd..dd79b6472a08d8c96c5ecee47538fcef29c5c170 100644 --- a/config-svrobo5.yml +++ b/config-svrobo5.yml @@ -41,10 +41,11 @@ py-jftech: datum: basic.datum.DefaultDatum hold-report: portfolios.holder.DivHoldReportor mpt: portfolios.builder.PoemPortfoliosBuilder -# email: -# server: smtphz.qiye.163.com -# user: jft-ra@thizgroup.com -# password: 5dbb#30ec6d3 + dividend-holder: portfolios.holder.DividendPortfoliosHolder + email: + server: smtphz.qiye.163.com + user: jft-ra@thizgroup.com + password: 5dbb#30ec6d3 mulit-process: max-workers: ${MAX_PROCESS:4} basic: # åŸºç¡€ä¿¡æ¯æ¨¡å— @@ -59,6 +60,7 @@ basic: # åŸºç¡€ä¿¡æ¯æ¨¡å— - 'TEMUSGI LX Equity' real: - 'FGFSACU LX Equity' + - 'TEMUSGI LX Equity' # navs: # å‡€å€¼æ¨¡å— # exrate: # 汇率,如果ä¸å¼€å¯ï¼Œæ•´ä¸ªè¿™å—注释掉 # - from: EUR # 需è¦è½¬æ¢çš„è´§å¸ç±»åž‹ @@ -239,7 +241,9 @@ robo-executor: # 执行器相关 end-step: ${BACKTEST_END_STEP:3} # å›žæµ‹ä»Žå“ªä¸€æ¥æ‰§è¡Œå®ŒæˆåŽç»“æŸæ‰§è¡Œ 1:è®¡ç®—èµ„äº§æ± ï¼›2:计算最优投组:3:计算å†å¹³è¡¡ä¿¡å·ä»¥åŠæŒä»“投组 clean-up: true real: # 实盘执行器 - start-date: 2022-09-01 # 实盘开始时间 + export: ${EXPORT_ENABLE:on} # 是å¦å¼€å¯æŠ¥å‘Š + start-date: 2023-01-01 # 实盘开始时间 + include-date: [] diff --git a/config-svrobo6.yml b/config-svrobo6.yml index 2bab776d86adaa7b9db6afe7bd7b4553a310d07b..498c6afdca648f5af2eb1b84867193658f63a5af 100644 --- a/config-svrobo6.yml +++ b/config-svrobo6.yml @@ -42,10 +42,10 @@ py-jftech: hold-report: portfolios.holder.DivHoldReportor mpt: portfolios.builder.PoemPortfoliosBuilder dividend-holder: portfolios.holder.InvTrustPortfoliosHolder -# email: -# server: smtphz.qiye.163.com -# user: jft-ra@thizgroup.com -# password: 5dbb#30ec6d3 + email: + server: smtphz.qiye.163.com + user: jft-ra@thizgroup.com + password: 5dbb#30ec6d3 mulit-process: max-workers: ${MAX_PROCESS:4} basic: # åŸºç¡€ä¿¡æ¯æ¨¡å— @@ -60,6 +60,7 @@ basic: # åŸºç¡€ä¿¡æ¯æ¨¡å— - 'TEMUSGI LX Equity' real: - 'FGFSACU LX Equity' + - 'TEMUSGI LX Equity' # navs: # å‡€å€¼æ¨¡å— # exrate: # 汇率,如果ä¸å¼€å¯ï¼Œæ•´ä¸ªè¿™å—注释掉 # - from: EUR # 需è¦è½¬æ¢çš„è´§å¸ç±»åž‹ @@ -183,7 +184,7 @@ reports: # 报告模å—相关 - month-div-rate-report # æœˆåº¦é…æ¯çŽ‡æ¯”è¾ƒ - year-div-rate-report # å¹´åº¦é…æ¯çŽ‡æ¯”è¾ƒ real-daily: - file-name: svROBO5_portfolios + file-name: svROBO6_portfolios include-report: - daily-hold-report - daily-signal-report @@ -192,13 +193,13 @@ reports: # 报告模å—相关 - wenwen.tang@thizgroup.com copies: ${DAILY_EMAIL_COPIES} subject: - default: "ROBO5_TAIBEI-实盘版-æ¯æ—¥æŠ•組推薦_{today}" - rebalance: "ROBO5_TAIBEI-实盘版-æ¯æ—¥æŠ•組推薦_{today}_今日有調倉信號ï¼ï¼ï¼" + default: "ROBO6_TAIBEI-实盘版-æ¯æ—¥æŠ•組推薦_{today}" + rebalance: "ROBO6_TAIBEI-实盘版-æ¯æ—¥æŠ•組推薦_{today}_今日有調倉信號ï¼ï¼ï¼" content: default: "Dear All: 附件是今天生æˆçš„æŽ¨è–¦çµ„åˆï¼Œè«‹é©—收,è¬è¬ï¼ 注>:該郵件為自動發é€ï¼Œå¦‚有å•題請è¯ç¹«çŸ½è°·åœ˜éšŠ telan_qian@chifufund.com" rebalance: "Dear All: 附件是今天生æˆçš„æŽ¨è–¦çµ„åˆä»¥åŠèª¿å€‰ä¿¡è™Ÿï¼Œè«‹é©—收,è¬è¬ï¼ 注>:該郵件為自動發é€ï¼Œå¦‚有å•題請è¯ç¹«çŸ½è°·åœ˜éšŠ telan_qian@chifufund.com" daily-monitor: - file-name: svROBO5_monitor + file-name: svROBO6_monitor include-report: - name: relative-range-report # 相对区间收益报告 min-date: ~ @@ -226,11 +227,11 @@ reports: # 报告模å—相关 receives: - wenwen.tang@thizgroup.com copies: ${MONITOR_EMAIL_COPIES} - subject: "SVROBO5-实盘版-æ¯æ—¥ç›‘测_{today}" + subject: "SVROBO6-实盘版-æ¯æ—¥ç›‘测_{today}" content: "Dear All: 附件是今天生æˆçš„监测数æ®ï¼Œè«‹é©—收,è¬è¬ï¼ 注>:該郵件為自動發é€ï¼Œå¦‚有å•題請è¯ç¹«çŸ½è°·åœ˜éšŠ telan_qian@chifufund.com" robo-executor: # 执行器相关 - use: ${ROBO_EXECUTOR:backtest} # 执行哪个执行器,优先å–系统环境å˜é‡ROBO_EXECUTOR的值,默认backtest - sync-data: ${SYNC_DATA:off} # 是å¦å¼€å¯åŒæ¥èµ„æ–™æ•°æ® + use: ${ROBO_EXECUTOR:real} # 执行哪个执行器,优先å–系统环境å˜é‡ROBO_EXECUTOR的值,默认backtest + sync-data: ${SYNC_DATA:on} # 是å¦å¼€å¯åŒæ¥èµ„æ–™æ•°æ® backtest: # 回测执行器相关 start-date: 2022-09-30 # 回测起始日期 end-date: 2023-03-01 # å›žæµ‹æˆªæ¢æ—¥æœŸ @@ -239,7 +240,9 @@ robo-executor: # 执行器相关 end-step: ${BACKTEST_END_STEP:3} # å›žæµ‹ä»Žå“ªä¸€æ¥æ‰§è¡Œå®ŒæˆåŽç»“æŸæ‰§è¡Œ 1:è®¡ç®—èµ„äº§æ± ï¼›2:计算最优投组:3:计算å†å¹³è¡¡ä¿¡å·ä»¥åŠæŒä»“投组 clean-up: true real: # 实盘执行器 - start-date: 2022-09-01 # 实盘开始时间 + export: ${EXPORT_ENABLE:on} # 是å¦å¼€å¯æŠ¥å‘Š + start-date: 2023-01-01 # 实盘开始时间 + include-date: [] diff --git a/portfolios/builder.py b/portfolios/builder.py index d0219781d9831b5f6758dc1f8e30f1f2cddb080c..83b9bd8ba6311c0501ef949c16cd88a2b241b77c 100644 --- a/portfolios/builder.py +++ b/portfolios/builder.py @@ -1,9 +1,10 @@ import json import logging -from datetime import datetime as dt +from datetime import datetime as dt, timedelta from typing import List -from py_jftech import component, autowired, format_date +import pandas as pd +from py_jftech import component, autowired, format_date, prev_workday, is_workday from pymysql import IntegrityError, constants from api import PortfoliosBuilder, PortfoliosRisk, AssetPool, Navs, PortfoliosType, Datum, SolveType, SolverFactory, \ @@ -143,3 +144,78 @@ class SignalReportor(RoboReportor): 'weight': weight }) return result + + +@component(bean_name='daily-hold-report') +class DailyHoldReportor(RoboReportor): + + @autowired + def __init__(self, datum: Datum = None): + 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]: + # 月åˆè°ƒä»“,实际相当于调仓信å·åœ¨ä¸Šæœˆæœˆæœ« + first_day = max_date.replace(day=1) + prev_month = first_day - timedelta(days=1) + prev_month.replace(day=prev_month.day) + prev_month = prev_month if is_workday(prev_month) else prev_workday(prev_month) + portfolio = rmp.get_one(prev_month, type=PortfoliosType.NORMAL, risk=PortfoliosRisk.FT3) + result = {} + if portfolio: + datum_ids = list(json.loads(portfolio['portfolio']).keys()) + datums = pd.DataFrame(self._datum.get_datums(type=DatumType.FUND, datum_ids=datum_ids)) + datums.set_index('id', inplace=True) + + result['risk'] = [portfolio['risk'] for i in datum_ids] + result['rebalance_type'] = [portfolio['type'] for i in datum_ids] + + result['weight'] = [format(i, '.0%') for i in json.loads(portfolio['portfolio']).values()] + result['asset_ids'] = [datums.loc[int(i)]['ftTicker'] for i in datum_ids] + result['name'] = [datums.loc[int(i)]['chineseName'] for i in datum_ids] + result['lipper_id'] = [datums.loc[int(i)]['lipperKey'] for i in datum_ids] + result['date'] = [max_date for i in datum_ids] + result['rebalance_date'] = [portfolio['date'] for i in datum_ids] + result = pd.DataFrame(result) + result = result[ + ['lipper_id', 'asset_ids', 'name', 'weight', 'risk', 'date', 'rebalance_type', 'rebalance_date']] + return result.to_dict('records') + + return [] + + +@component(bean_name='daily-signal-report') +class DailySignalReportor(RoboReportor): + + @autowired + def __init__(self, datum: Datum = None): + 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]: + portfolio = rmp.get_one(max_date, type=PortfoliosType.NORMAL, risk=PortfoliosRisk.FT3) + result = {} + if portfolio: + datum_ids = list(json.loads(portfolio['portfolio']).keys()) + datums = pd.DataFrame(self._datum.get_datums(type=DatumType.FUND, datum_ids=datum_ids)) + datums.set_index('id', inplace=True) + + result['risk'] = [portfolio['risk'] for i in datum_ids] + result['rebalance_type'] = [portfolio['type'] for i in datum_ids] + + result['weight'] = [format(i, '.0%') for i in json.loads(portfolio['portfolio']).values()] + result['asset_ids'] = [datums.loc[int(i)]['ftTicker'] for i in datum_ids] + result['name'] = [datums.loc[int(i)]['chineseName'] for i in datum_ids] + result['lipper_id'] = [datums.loc[int(i)]['lipperKey'] for i in datum_ids] + result['date'] = [max_date for i in datum_ids] + result = pd.DataFrame(result) + result = result[['lipper_id', 'asset_ids', 'name', 'weight', 'risk', 'date', 'rebalance_type']] + return result.to_dict('records') + + return [] diff --git a/robo_executor.py b/robo_executor.py index 64dd883205dc60206791d093c4cffe93a2aa04d7..c8c78942678bb185c050cf3194eb5b1eb8c46600 100644 --- a/robo_executor.py +++ b/robo_executor.py @@ -1,13 +1,13 @@ import logging import sys from concurrent.futures import wait -from datetime import datetime as dt +from datetime import datetime as dt, timedelta from typing import List import pandas as pd from py_jftech import ( component, autowired, get_config, filter_weekend, asynchronized, - parse_date, workday_range + parse_date, workday_range, is_workday, prev_workday, format_date ) from api import ( @@ -135,12 +135,15 @@ class BacktestExecutor(RoboExecutor): @component(bean_name='real') class RealExecutor(RoboExecutor): - @autowired + @autowired(names={'daily_export': 'daily-real-export', 'monitor_export': 'daily-monitor-export'}) def __init__(self, builder: PortfoliosBuilder = None, hold: PortfoliosHolder = None, syncs: List[DataSync] = None, - ): + daily_export: RoboExportor = None, monitor_export: RoboExportor = None, pool: AssetPool = None, ): self._builder = builder + self._pool = pool self._hold = hold self._syncs = syncs + self._daily_export = daily_export + self._monitor_export = monitor_export self._config = get_config(__name__)['real'] @property @@ -164,7 +167,39 @@ class RealExecutor(RoboExecutor): def include_date(self): return [dt.combine(x, dt.min.time()) for x in self._config['include-date']] + @property + def export(self): + return self._config['export'] if 'export' in self._config else False + def start_exec(self): 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(filter_weekend(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() + first_day = date.replace(day=1) + prev_month = first_day - timedelta(days=1) + prev_month.replace(day=prev_month.day) + prev_month = prev_month if is_workday(prev_month) else prev_workday(prev_month) + self._pool.get_pool(prev_month) + self._builder.get_portfolios(prev_month, risk) + next_month = date.replace(day=28) + timedelta(days=4) + prev_month = next_month.replace(day=1) - timedelta(days=1) + prev_month = prev_month if is_workday(prev_month) else prev_workday(prev_month) + if date.day == prev_month.day: + self._pool.get_pool(date) + self._builder.get_portfolios(date, risk) + logger.info( + f"build risk[{risk.name}] real for date[{format_date(date)}] success, use[{(dt.now() - now).seconds}s]") + if self.export: + now = dt.now() + # æ¯æ—¥å®žç›˜æŠ¥å‘Š + self._daily_export.export(max_date=date) + logger.info( + f'export email for date[{format_date(date)}] send success, use[{(dt.now() - now).seconds}s]') + else: + logger.info(f'today[{format_date(date)}] is a rest day, do not execute the daily real robo.')