Commit ae553a7d authored by wenwen.tang's avatar wenwen.tang 😕

update

parent 0052584f
import sys
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime as dt from datetime import datetime as dt
from enum import Enum, unique from enum import Enum, unique
from typing import List from typing import List
from py_jftech import get_config from py_jftech import get_config, parse_date
@unique
class BacktestStep(Enum):
ASSET_POOL = 1
NORMAL_PORTFOLIO = 2
HOLD_PORTFOLIO = 3
def within(self, step: Enum):
return self.value <= step.value
def without(self, step: Enum):
return self.value >= step.value
@unique @unique
...@@ -36,6 +50,11 @@ class SolveType(Enum): ...@@ -36,6 +50,11 @@ class SolveType(Enum):
POEM = 2 POEM = 2
@unique
class LoggerType(Enum):
SIGNAL = 'signal'
class DataSync(ABC): class DataSync(ABC):
''' '''
数据同步服务,需要同步数据的服务,可以实现该接口 数据同步服务,需要同步数据的服务,可以实现该接口
...@@ -49,6 +68,37 @@ class DataSync(ABC): ...@@ -49,6 +68,37 @@ class DataSync(ABC):
pass pass
class Cleanable(ABC):
'''
可清除服务
'''
@property
@abstractmethod
def clean_name(self):
'''
清除数据的名称
'''
pass
@property
@abstractmethod
def clean_step(self):
'''
清除数据所属的步骤
'''
pass
@abstractmethod
def clean_up(self, min_date=None, risk: PortfoliosRisk = None):
'''
清理指定的数据
:param min_date: 指定的起始时间
:param risk: 指定的风险等级
'''
pass
# TODO: 动态更新资料,控制更新基金池 # TODO: 动态更新资料,控制更新基金池
class Datum(ABC): class Datum(ABC):
''' '''
...@@ -475,6 +525,18 @@ class RoboExecutor(ABC): ...@@ -475,6 +525,18 @@ class RoboExecutor(ABC):
def use_name(): def use_name():
return get_config('robo-executor')['use'] return get_config('robo-executor')['use']
@property
def curt_date(self):
'''
:return: 当前运行的日期
'''
if len(sys.argv) > 1:
try:
return parse_date(sys.argv[1])
except Exception as e:
logger.warning(f'get curt date from argv failure.', e)
return dt.combine(dt.today().date(), dt.min.time())
class RoboReportor(ABC): class RoboReportor(ABC):
''' '''
...@@ -514,3 +576,31 @@ class RoboExportor(ABC): ...@@ -514,3 +576,31 @@ class RoboExportor(ABC):
:param min_date: 指定开始日期 :param min_date: 指定开始日期
''' '''
pass pass
class DataLogger(ABC):
@abstractmethod
def save_record(self, date: dt, risk: PortfoliosRisk, type: LoggerType, datas: dict, exist_merge=True):
'''
保存数据日志记录
:param date: 要保存的数据记录日期
:param risk: 要保存的数据记录风险等级
:param type: 要保存的数据记录类型
:param datas: 要保存的数据记录
:param exist_merge: 如果要保存的记录存在,需要处理的方式,与之前的数据做合并处理,则为Ture, 否则会直接覆盖
'''
pass
@abstractmethod
def load_records(self, max_date=None, min_date=None, risk: PortfoliosRisk = None, type: LoggerType = None):
'''
获取数据日志记录
:param max_date: 截止日期
:param min_date: 起始日期
:param risk: 风险等级
:param type: 日志类型
:return: 日志数据列表
'''
pass
...@@ -30,14 +30,16 @@ py-jftech: ...@@ -30,14 +30,16 @@ py-jftech:
level: ${LOG_LEVEL:INFO} level: ${LOG_LEVEL:INFO}
handlers: ${LOG_HANDLERS:[ console ]} handlers: ${LOG_HANDLERS:[ console ]}
database: database:
host: ${MYSQL_HOST:192.168.68.81} host: ${MYSQL_HOST:::1}
port: ${MYSQL_PORT:3306} port: ${MYSQL_PORT:3306}
user: ${MYSQL_USER:root} user: ${MYSQL_USER:root}
password: ${MYSQL_PWD:changeit} password: ${MYSQL_PWD:changeit}
dbname: ${MYSQL_DBNAME:jftech_robo} dbname: ${MYSQL_DBNAME:jftech_robo}
injectable: injectable:
types: name:
api.PortfoliosBuilder: portfolios.builder.PoemPortfoliosBuilder backtest: robo_executor.BacktestExecutor
hold-report: portfolios.holder.DivHoldReportor
mpt: portfolios.builder.PoemPortfoliosBuilder
# email: # email:
# server: smtphz.qiye.163.com # server: smtphz.qiye.163.com
# user: jft-ra@thizgroup.com # user: jft-ra@thizgroup.com
...@@ -77,7 +79,7 @@ portfolios: # 投组模块 ...@@ -77,7 +79,7 @@ portfolios: # 投组模块
min-interval-days: 10 # 两次实际调仓最小间隔期,单位交易日 min-interval-days: 10 # 两次实际调仓最小间隔期,单位交易日
dividend-rate: 0.09 #设定年化配息率 dividend-rate: 0.09 #设定年化配息率
dividend-drift-rate: 0.1 #超过基准配息率上下10%触发配息率重置 dividend-drift-rate: 0.1 #超过基准配息率上下10%触发配息率重置
dividend-date: 15 #配息日,每月10 dividend-date: 15 #配息日,每月15
dividend-adjust-day: [1,4,7,10] #每年的首个季度调整配息 dividend-adjust-day: [1,4,7,10] #每年的首个季度调整配息
solver: # 解算器相关 solver: # 解算器相关
tol: 1E-10 # 误差满足条件 tol: 1E-10 # 误差满足条件
...@@ -88,20 +90,20 @@ portfolios: # 投组模块 ...@@ -88,20 +90,20 @@ portfolios: # 投组模块
asset: 8 # 单一资产最多缺少多少交易日数据,则踢出资产池 asset: 8 # 单一资产最多缺少多少交易日数据,则踢出资产池
day: 0.5 # 单一交易日最多缺少百分之多少净值,则删除该交易日 day: 0.5 # 单一交易日最多缺少百分之多少净值,则删除该交易日
normal-ratio: #US_STOCK:US_HY_BOND:US_IG_BOND三者分别对应低中高风险所占比率 normal-ratio: #US_STOCK:US_HY_BOND:US_IG_BOND三者分别对应低中高风险所占比率
US_STOCK: [ 0.3, 0.5, 0.7 ] US_STOCK: [ 0.5, 0.5, 0.7 ]
US_HY_BOND: [ 0.6, 0.4, 0.2 ] US_HY_BOND: [ 0.4, 0.4, 0.2 ]
US_IG_BOND: [ 0.1, 0.1, 0.1 ] US_IG_BOND: [ 0.1, 0.1, 0.1 ]
riskctl-ratio: riskctl-ratio:
US_STOCK: [ 0.2, 0.4, 0.6 ] US_STOCK: [ 0.2, 0.4, 0.6 ]
US_HY_BOND: [ 0.5, 0.3, 0.1 ] US_HY_BOND: [ 0.5, 0.3, 0.1 ]
US_IG_BOND: [ 0.3, 0.3, 0.3 ] US_IG_BOND: [ 0.3, 0.3, 0.3 ]
matrix-rtn-days: 20 # 计算回报率矩阵时,回报率滚动天数 matrix-rtn-days: 20 # 计算回报率矩阵时,回报率滚动天数
asset-count: [1,3] # 投组资产个数。e.g. count 或 [min, max] 分别表示 最大最小都为count 或 最小为min 最大为max,另外这里也可以类似上面给不同风险等级分别配置 asset-count: [3,3] # 投组资产个数。e.g. count 或 [min, max] 分别表示 最大最小都为count 或 最小为min 最大为max,另外这里也可以类似上面给不同风险等级分别配置
mpt: # mpt计算相关 mpt: # mpt计算相关
cvar-beta: 0.2 # 计算Kbeta 需要用到 cvar-beta: 0.2 # 计算Kbeta 需要用到
quantile: 0.9 # 分位点,也可以给不同风险等级分别配置 quantile: 0.9 # 分位点,也可以给不同风险等级分别配置
low-weight: 0.05 # 最低权重 low-weight: 0.05 # 最低权重
high-weight: [ 1 ] # 最高权重比例,可给一个值,也可以给多个值,当多个值时,第一个表示只有一个资产时权重,第二个表示只有两个资产时权重,以此类推,最后一个表示其他资产个数时的权重 # high-weight: [ 1 ] # 最高权重比例,可给一个值,也可以给多个值,当多个值时,第一个表示只有一个资产时权重,第二个表示只有两个资产时权重,以此类推,最后一个表示其他资产个数时的权重
poem: # poem相关 poem: # poem相关
cvar-scale-factor: 0.1 # 计算时用到的系数 cvar-scale-factor: 0.1 # 计算时用到的系数
reports: # 报告模块相关 reports: # 报告模块相关
...@@ -114,11 +116,12 @@ reports: # 报告模块相关 ...@@ -114,11 +116,12 @@ reports: # 报告模块相关
- FKRCX US Equity - FKRCX US Equity
- FTNRACU LX Equity - FTNRACU LX Equity
benchmark: # benchmark报告 benchmark: # benchmark报告
init-amount: 100 # 初始金额 ft:
stock-rate: # stock型基金比例 init-amount: 100 # 初始金额
RR3: 0.3 stock-rate: # stock型基金比例
RR4: 0.5 RR3: 0.3
RR5: 0.7 RR4: 0.5
RR5: 0.7
fixed-range: # 固定区间收益率 fixed-range: # 固定区间收益率
range-dates: # 固定起始截止日期 range-dates: # 固定起始截止日期
- start: 2008-01-01 - start: 2008-01-01
...@@ -137,12 +140,16 @@ reports: # 报告模块相关 ...@@ -137,12 +140,16 @@ reports: # 报告模块相关
end: 2020-03-23 end: 2020-03-23
relative-range: # 相对区间收益率 relative-range: # 相对区间收益率
range-dates: # 相对时间周期 range-dates: # 相对时间周期
- days: 1
name: '一天'
- weeks: 1
name: '一周'
- months: 1 - months: 1
name: '月' name: '一月'
- months: 3 - months: 3
name: '月' name: '三月'
- months: 6 - months: 6
name: '月' name: '六月'
- years: 1 - years: 1
name: '一年' name: '一年'
- years: 2 - years: 2
...@@ -153,23 +160,78 @@ reports: # 报告模块相关 ...@@ -153,23 +160,78 @@ reports: # 报告模块相关
name: '五年' name: '五年'
- years: 10 - years: 10
name: '十年' name: '十年'
backtest: # 回测导出曹策略 - dates: ~
exist-build: on # 如果报告文件存在,是否重新构建文件 name: '成立以来'
save-path: ${EXPORT_PATH:excels} # 导出报告文件存放路径,如果以./或者../开头,则会以执行python文件为根目录,如果以/开头,则为系统绝对路径,否则,以项目目录为根目录 exports:
file-name: ${EXPORT_FILENAME:real} backtest: # 回测导出曹策略
include-report: # 需要导出的报告类型列表,下面的顺序,也代表了excel中sheet的顺序 save-path: ${EXPORT_PATH:excels} # 导出报告文件存放路径,如果以./或者../开头,则会以执行python文件为根目录,如果以/开头,则为系统绝对路径,否则,以项目目录为根目录
- hold-report # 持仓报告 file-name: ${EXPORT_FILENAME:real} # 导出报告的文件名
- signal-report # 信号报告 save-config: ${EXPORT_CONFIG:off} # 是否保存配置文件
- benckmark-report # benckmark报告 include-report: # 需要导出的报告类型列表,下面的顺序,也代表了excel中sheet的顺序
- indicators-report # 各种特殊指标报告 # - funds-report # 基金资料
- fixed-range-report # 固定区间收益报告 # - navs-report # 净值报告
- relative-range-report # 相对区间收益报告 - hold-report # 持仓报告
- signal-report # 信号报告
- benckmark-report # benckmark报告
- combo-report # 持仓对比
- indicators-report # 各种特殊指标报告
- fixed-range-report # 固定区间收益报告
- relative-range-report # 相对区间收益报告
- year-range-report # 单年区间业绩报告
- month-div-rate-report # 月度配息率比较
- year-div-rate-report # 年度配息率比较
real-daily:
file-name: svROBO5_portfolios
include-report:
- daily-hold-report
- daily-signal-report
email:
receives:
- wenwen.tang@thizgroup.com
copies: ${DAILY_EMAIL_COPIES}
subject:
default: "ROBO5_TAIBEI-实盘版-每日投組推薦_{today}"
rebalance: "ROBO5_TAIBEI-实盘版-每日投組推薦_{today}_今日有調倉信號!!!"
content:
default: "Dear All: 附件是今天生成的推薦組合,請驗收,謝謝! 注>:該郵件為自動發送,如有問題請聯繫矽谷團隊 telan_qian@chifufund.com"
rebalance: "Dear All: 附件是今天生成的推薦組合以及調倉信號,請驗收,謝謝! 注>:該郵件為自動發送,如有問題請聯繫矽谷團隊 telan_qian@chifufund.com"
daily-monitor:
file-name: svROBO5_monitor
include-report:
- name: relative-range-report # 相对区间收益报告
min-date: ~
- name: contribution-report # 贡献率报告
min-date: {days: 30}
- name: high-weight-report # 高风险资产占比
min-date: {days: 30}
- name: asset-pool-report # 基金池
min-date: {days: 30}
- name: combo-report # 持仓报告
min-date: {days: 40}
- name: mpt-report
min-date: {days: 30}
- name: signal-report
min-date: ~
- name: crisis-one-report
min-date: {days: 30}
- name: crisis-two-report
min-date: {days: 30}
- name: market-right-report
min-date: {days: 30}
- name: drift-buy-report
min-date: {days: 30}
email:
receives:
- wenwen.tang@thizgroup.com
copies: ${MONITOR_EMAIL_COPIES}
subject: "SVROBO5-实盘版-每日监测_{today}"
content: "Dear All: 附件是今天生成的监测数据,請驗收,謝謝! 注>:該郵件為自動發送,如有問題請聯繫矽谷團隊 telan_qian@chifufund.com"
robo-executor: # 执行器相关 robo-executor: # 执行器相关
use: ${ROBO_EXECUTOR:backtest} # 执行哪个执行器,优先取系统环境变量ROBO_EXECUTOR的值,默认backtest use: ${ROBO_EXECUTOR:backtest} # 执行哪个执行器,优先取系统环境变量ROBO_EXECUTOR的值,默认backtest
sync-data: ${SYNC_DATA:off} # 是否开启同步资料数据 sync-data: ${SYNC_DATA:off} # 是否开启同步资料数据
backtest: # 回测执行器相关 backtest: # 回测执行器相关
start-date: 2012-10-16 # 回测起始日期 start-date: 2012-10-16 # 回测起始日期
end-date: 2023-03-03 # 回测截止日期 end-date: 2023-03-01 # 回测截止日期
sealing-period: 10 #调仓封闭期 sealing-period: 10 #调仓封闭期
start-step: ${BACKTEST_START_STEP:1} # 回测从哪一步开始执行 1:计算资产池;2:计算最优投组:3:计算再平衡信号以及持仓投组 start-step: ${BACKTEST_START_STEP:1} # 回测从哪一步开始执行 1:计算资产池;2:计算最优投组:3:计算再平衡信号以及持仓投组
end-step: ${BACKTEST_END_STEP:3} # 回测从哪一步执行完成后结束执行 1:计算资产池;2:计算最优投组:3:计算再平衡信号以及持仓投组 end-step: ${BACKTEST_END_STEP:3} # 回测从哪一步执行完成后结束执行 1:计算资产池;2:计算最优投组:3:计算再平衡信号以及持仓投组
......
...@@ -60,6 +60,10 @@ class MptPortfoliosBuilder(PortfoliosBuilder): ...@@ -60,6 +60,10 @@ class MptPortfoliosBuilder(PortfoliosBuilder):
solver = self._factory.create_solver(risk, type) solver = self._factory.create_solver(risk, type)
navs_group = solver.reset_navs(day) navs_group = solver.reset_navs(day)
for category, navs in navs_group.items(): for category, navs in navs_group.items():
# count = solver.get_config('asset-count')[0]
# nav_count = len(navs.columns)
# if count <= nav_count:
# pass
solver.set_navs(navs) solver.set_navs(navs)
solver.set_category(category) solver.set_category(category)
logger.debug({ logger.debug({
...@@ -132,7 +136,7 @@ class SignalReportor(RoboReportor): ...@@ -132,7 +136,7 @@ class SignalReportor(RoboReportor):
result.append({ result.append({
'risk': PortfoliosRisk(signal['risk']).name, 'risk': PortfoliosRisk(signal['risk']).name,
'rebalance_date': signal['date'], 'rebalance_date': signal['date'],
'portfolio_type': PortfoliosType(signal['portfolio_type']).name, 'portfolio_type': PortfoliosType.NORMAL.name,
'ft_ticker': datums[fund_id]['ftTicker'], 'ft_ticker': datums[fund_id]['ftTicker'],
'blooberg_ticker': datums[fund_id]['bloombergTicker'], 'blooberg_ticker': datums[fund_id]['bloombergTicker'],
'fund_name': datums[fund_id]['chineseName'], 'fund_name': datums[fund_id]['chineseName'],
......
...@@ -12,8 +12,11 @@ __COLUMNS__ = { ...@@ -12,8 +12,11 @@ __COLUMNS__ = {
'rhp_rebalance': 'rebalance', 'rhp_rebalance': 'rebalance',
'rhp_portfolios': 'portfolios', 'rhp_portfolios': 'portfolios',
'rhp_nav': 'nav', 'rhp_nav': 'nav',
'rhp_fund_av': 'fund_av',
'rhp_fund_div': 'fund_div', 'rhp_fund_div': 'fund_div',
'rhp_asset_nav': 'asset_nav', 'rhp_asset_nav': 'asset_nav',
'rhp_port_div': 'port_div',
'v_nav_div_acc': 'acc_av',
} }
......
import datetime
import json import json
import logging import logging
from datetime import datetime as dt, date
from typing import List
import pandas as pd import pandas as pd
from py_jftech import ( from py_jftech import (
component, autowired, get_config, next_workday, format_date, is_workday component, autowired, get_config, next_workday, format_date, is_workday
) )
from api import PortfoliosHolder, PortfoliosRisk, Navs, RoboExecutor, PortfoliosType, PortfoliosBuilder from api import PortfoliosHolder, PortfoliosRisk, Navs, RoboExecutor, PortfoliosType, PortfoliosBuilder, RoboReportor
from portfolios.dao import robo_hold_portfolios as rhp from portfolios.dao import robo_hold_portfolios as rhp
from portfolios.utils import format_weight from portfolios.utils import format_weight
...@@ -69,7 +70,7 @@ class DividendPortfoliosHolder(PortfoliosHolder): ...@@ -69,7 +70,7 @@ class DividendPortfoliosHolder(PortfoliosHolder):
fund_div_tuple = self.get_navs_and_div(fund_ids=tuple(set(weight) | set(share)), day=day) fund_div_tuple = self.get_navs_and_div(fund_ids=tuple(set(weight) | set(share)), day=day)
navs = fund_div_tuple[0] navs = fund_div_tuple[0]
dividend_acc = last_nav['div_acc'] dividend_acc = last_nav['div_acc']
nav = round(sum([navs[x] * y for x, y in share.items()]), 4) + last_nav['fund_div'] fund_av = round(sum([navs[x] * y for x, y in share.items()]), 4) + last_nav['fund_div']
# 每年的首个季度调整配息 # 每年的首个季度调整配息
if day.month in self._config.get('dividend-adjust-day'): if day.month in self._config.get('dividend-adjust-day'):
asset_nav = last_nav['asset_nav'] asset_nav = last_nav['asset_nav']
...@@ -81,7 +82,7 @@ class DividendPortfoliosHolder(PortfoliosHolder): ...@@ -81,7 +82,7 @@ class DividendPortfoliosHolder(PortfoliosHolder):
dividend = last_nav['asset_nav'] * self.month_dividend dividend = last_nav['asset_nav'] * self.month_dividend
else: else:
dividend = self._last_div dividend = self._last_div
nav = nav - dividend fund_av = fund_av - dividend
dividend_acc = dividend + dividend_acc dividend_acc = dividend + dividend_acc
self._last_div = dividend self._last_div = dividend
else: else:
...@@ -90,19 +91,21 @@ class DividendPortfoliosHolder(PortfoliosHolder): ...@@ -90,19 +91,21 @@ class DividendPortfoliosHolder(PortfoliosHolder):
dividend = last_nav['dividend'] dividend = last_nav['dividend']
else: else:
dividend = self._last_div dividend = self._last_div
nav = nav - self._last_div fund_av = fund_av - self._last_div
dividend_acc = self._last_div + dividend_acc dividend_acc = self._last_div + dividend_acc
nav = last_nav['nav'] * (fund_av + dividend) / last_nav['asset_nav']
else: else:
nav = self.init_nav fund_av = self.init_nav
fund_div_tuple = self.get_navs_and_div(fund_ids=tuple(weight), day=day) fund_div_tuple = self.get_navs_and_div(fund_ids=tuple(weight), day=day)
navs = fund_div_tuple[0] navs = fund_div_tuple[0]
dividend = nav * self.month_dividend dividend = fund_av * self.month_dividend
self._last_div = dividend self._last_div = dividend
nav = nav - dividend fund_av = fund_av - dividend
dividend_acc = dividend + dividend_acc dividend_acc = dividend + dividend_acc
share = {x: nav * w / navs[x] for x, w in weight.items()} nav = self.init_nav
share = {x: fund_av * w / navs[x] for x, w in weight.items()}
fund_dividend = 0 fund_dividend = 0
asset_nav = nav + fund_dividend + dividend asset_nav = fund_av + fund_dividend + dividend
rhp.insert({ rhp.insert({
'date': day, 'date': day,
'risk': risk, 'risk': risk,
...@@ -114,7 +117,9 @@ class DividendPortfoliosHolder(PortfoliosHolder): ...@@ -114,7 +117,9 @@ class DividendPortfoliosHolder(PortfoliosHolder):
'weight': weight, 'weight': weight,
'share': share, 'share': share,
}, },
'fund_av': fund_av,
'nav': nav, 'nav': nav,
'port_div': 0,
'asset_nav': asset_nav, 'asset_nav': asset_nav,
}) })
...@@ -123,16 +128,22 @@ class DividendPortfoliosHolder(PortfoliosHolder): ...@@ -123,16 +128,22 @@ class DividendPortfoliosHolder(PortfoliosHolder):
fund_div_tuple = self.get_navs_and_div(fund_ids=tuple(share), day=day) fund_div_tuple = self.get_navs_and_div(fund_ids=tuple(share), day=day)
navs = fund_div_tuple[0] navs = fund_div_tuple[0]
fund_dividend = fund_div_tuple[1] fund_dividend = fund_div_tuple[1]
nav = round(sum([navs[x] * y for x, y in share.items()]), 4) fund_av = round(sum([navs[x] * y for x, y in share.items()]), 4)
weight = {x: round(y * navs[x] / nav, 2) for x, y in share.items()} weight = {x: round(y * navs[x] / fund_av, 2) for x, y in share.items()}
weight = format_weight(weight) weight = format_weight(weight)
dividend = last_nav['dividend'] dividend = last_nav['dividend']
port_div = 0
fund_dividend = last_nav['fund_div'] + sum( fund_dividend = last_nav['fund_div'] + sum(
map(lambda k: share[k] * fund_dividend[k], filter(lambda k: k in fund_dividend, share.keys()))) map(lambda k: share[k] * fund_dividend[k], filter(lambda k: k in fund_dividend, share.keys())))
dividend_acc = last_nav['div_acc'] dividend_acc = last_nav['div_acc']
if self.is_dividend_date(day): if self.is_dividend_date(day):
port_div = dividend
asset_nav = fund_av + fund_dividend
dividend = 0 dividend = 0
asset_nav = nav + fund_dividend + dividend nav = last_nav['nav'] * (asset_nav + port_div) / last_nav['asset_nav']
else:
asset_nav = fund_av + fund_dividend + dividend
nav = last_nav['nav'] * asset_nav / last_nav['asset_nav']
rhp.insert({ rhp.insert({
'date': day, 'date': day,
'risk': risk, 'risk': risk,
...@@ -145,7 +156,9 @@ class DividendPortfoliosHolder(PortfoliosHolder): ...@@ -145,7 +156,9 @@ class DividendPortfoliosHolder(PortfoliosHolder):
'weight': weight, 'weight': weight,
'share': share, 'share': share,
}, },
'fund_av': fund_av,
'nav': nav, 'nav': nav,
'port_div': port_div,
'asset_nav': asset_nav, 'asset_nav': asset_nav,
}) })
...@@ -162,7 +175,7 @@ class DividendPortfoliosHolder(PortfoliosHolder): ...@@ -162,7 +175,7 @@ class DividendPortfoliosHolder(PortfoliosHolder):
def is_dividend_date(self, day): def is_dividend_date(self, day):
div_date = self._config['dividend-date'] div_date = self._config['dividend-date']
div_date = datetime.date(day.year, day.month, div_date) div_date = date(day.year, day.month, div_date)
if is_workday(div_date): if is_workday(div_date):
return div_date.day == day.day return div_date.day == day.day
else: else:
...@@ -179,3 +192,22 @@ class DividendPortfoliosHolder(PortfoliosHolder): ...@@ -179,3 +192,22 @@ class DividendPortfoliosHolder(PortfoliosHolder):
@property @property
def init_nav(self): def init_nav(self):
return self._config['init-nav'] return self._config['init-nav']
@component(bean_name='hold-report')
class DivHoldReportor(RoboReportor):
@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:
holds['signal_type'] = 'INIT'
holds['cash'] = holds['dividend']+holds['fund_div']
holds['real_av'] = holds['asset_nav']
holds = holds[
['date', 'signal_type', 'fund_av', 'fund_div', 'cash', 'real_av', 'port_div', 'acc_av', 'nav']]
return holds.to_dict('records')
return []
...@@ -212,11 +212,8 @@ class DefaultSolver(Solver): ...@@ -212,11 +212,8 @@ class DefaultSolver(Solver):
min_count = min(min_count, len(self.rtn_annualized)) min_count = min(min_count, len(self.rtn_annualized))
low_weight = self.get_config('mpt.low-weight') low_weight = self.get_config('mpt.low-weight')
high_weight = self.get_config('mpt.high-weight') high_weight = self.get_weight()
if isinstance(high_weight, list):
high_weight = high_weight[min(len(self.navs.columns), min_count, len(high_weight)) - 1]
model = ConcreteModel() model = ConcreteModel()
model.indices = range(0, len(self.navs.columns)) model.indices = range(0, len(self.navs.columns))
model.w = Var(model.indices, domain=NonNegativeReals) model.w = Var(model.indices, domain=NonNegativeReals)
model.z = Var(model.indices, domain=Binary) model.z = Var(model.indices, domain=Binary)
......
from datetime import datetime as dt, timedelta import json
from datetime import datetime as dt
from typing import List from typing import List
from urllib.parse import urlencode
import pandas as pd import pandas as pd
from py_jftech import component, autowired, prev_workday, filter_weekend, next_workday, get_config import requests
from py_jftech import component, filter_weekend, next_workday, get_config, format_date
from api import RoboReportor, PortfoliosRisk, RoboExecutor, Navs, Datum, DatumType from api import RoboReportor
from reports.dao import robo_benckmark as rb from reports.dao import robo_benckmark as rb
config = get_config(__name__)
@component(bean_name='benckmark-report')
class BenchmarkReportor(RoboReportor):
@autowired(names={'executor': RoboExecutor.use_name()}) @component(bean_name='benckmark-report')
def __init__(self, executor: RoboExecutor = None, navs: Navs = None, datum: Datum = None): class BenchmarkAlligamReportor(RoboReportor):
self._exec = executor
self._navs = navs
self._datum = datum
self._config = get_config(__name__)
@property @property
def report_name(self) -> str: def report_name(self) -> str:
return 'benchmark' return 'BENCHMARK_ALLIGAM'
@property @property
def risks(self): def module_name(self) -> str:
return self._config['stock-rate'].keys() return 'divrobo'
@property @property
def init_amount(self): def risk(self):
return self._config['init-amount'] return 'alligam'
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) @property
init_amount = last['nav'] if last else self.init_amount def base_params(self):
stock_rate = self.stock_rate(risk) return {
other_rate = 1 - stock_rate 'subjectKeys': 879,
'size': 200,
five_rtn = 0 'sourceType': 'BLOOMBERG'
last_day = None }
fund_ids = None
def sync_benchmark(self, start_date=None):
for index, row in nav_rtn.iterrows(): params = {
if last_day is None or fund_ids is None: **self.base_params,
fund_ids = list(row.iloc[:nav_index].dropna().index) 'page': 0
stock_count = len([x for x in fund_ids if asset_types[x] == 'STOCK']) }
stock_average = init_amount * stock_rate / stock_count if start_date:
other_average = init_amount * other_rate / (len(fund_ids) - stock_count) params['startDate'] = format_date(start_date)
nav_rtn.loc[index, f'{risk}_result'] = init_amount while True:
nav_rtn.loc[index, f'{risk}_re'] = 1 response = requests.get(f'https://jdcprod.thiztech.com/api/datas/asset-value?{urlencode(params)}').json()
for fund_id in fund_ids: if not response['success']:
if fund_id and asset_types[fund_id] == 'STOCK': raise Exception(f'''request jdc alligam failed: {response['status']}''')
nav_rtn.loc[index, f'stock_{fund_id}'] = stock_average rb.batch_insert([{
else: 'date': dt.fromtimestamp(x['date'] / 1000),
nav_rtn.loc[index, f'other_{fund_id}'] = other_average 'module': self.module_name,
'risk': self.risk,
'nav': x['calibrateValue'],
'remarks': json.dumps({
'av': x['originValue'],
'div': x['dividend'] if 'dividend' in x else 0
}, ensure_ascii=False)
} for x in response['body']['content']])
if response['body']['last']:
break
else: else:
for fund_id in fund_ids: params = {**params, 'page': params['page'] + 1}
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]: def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]:
for risk in self.risks: max_date = filter_weekend(max_date)
last = rb.get_last_one(max_date=max_date, risk=risk) min_date = filter_weekend(min_date) if min_date else None
if not last or last['date'] < filter_weekend(max_date): last = rb.get_last_one(module=self.module_name, risk=self.risk, max_date=max_date)
benchmarks = pd.DataFrame(self.build_benchmark(risk=risk, day=max_date)) if not last or last['date'] < max_date:
if last: self.sync_benchmark(start_date=next_workday(last['date']) if last else None)
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 = pd.DataFrame(rb.get_list(max_date=max_date, min_date=min_date))
result = result.pivot_table(index='date', columns='risk', values='nav') result['av'] = result['remarks'].apply(lambda x: json.loads(x)['av'])
result.reset_index(inplace=True) result['div'] = result['remarks'].apply(lambda x: json.loads(x)['div'])
result['acc'] = result.apply(lambda row: result[result['date'] <= row['date']]['div'].sum() + row['av'], axis=1)
result = result[['date', 'av', 'div', 'acc', 'nav']]
result.rename(columns={'nav': f'{self.risk}_nav', 'av': f'{self.risk}_av', 'div': f'{self.risk}_div', 'acc': f'{self.risk}_acc'}, inplace=True)
return result.to_dict('records') return result.to_dict('records')
...@@ -3,40 +3,36 @@ from typing import List ...@@ -3,40 +3,36 @@ from typing import List
import pandas as pd import pandas as pd
from py_jftech import component, autowired from py_jftech import component, autowired
from empyrical import annual_return, annual_volatility, max_drawdown, sharpe_ratio
from api import RoboReportor, Navs from api import RoboReportor
@component(bean_name='combo-report') @component(bean_name='combo-report')
class ComboDatasReport(RoboReportor): class DivAlligamComboDatasReportor(RoboReportor):
@autowired(names={'hold_reportor': 'hold-report', 'benchmark': 'benckmark-report'}) @autowired(names={'hold_reportor': 'hold-report', 'benchmark': 'benckmark-report'})
def __init__(self, hold_reportor: RoboReportor = None, benchmark: RoboReportor = None, navs: Navs = None): def __init__(self, hold_reportor: RoboReportor = None, benchmark: RoboReportor = None):
self._hold_reportor = hold_reportor self._hold_reportor = hold_reportor
self._benchmark = benchmark self._benchmark = benchmark
self._navs = navs
@property @property
def report_name(self) -> str: def report_name(self) -> str:
return '混合数据' return '对比报告'
def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: 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)) holds = pd.DataFrame(self._hold_reportor.load_report(max_date=max_date, min_date=min_date))
if not holds.empty: if not holds.empty:
holds['risk'] = holds.apply(lambda row: row.risk, axis=1) holds.set_index('date', inplace=True)
datas = holds.pivot_table(index='date', columns='risk', values='nav') holds = holds[['real_av', 'acc_av', 'nav']]
holds.rename(columns={'real_av': 'av', 'acc_av': 'acc'}, inplace=True)
benchmark = pd.DataFrame(self._benchmark.load_report(max_date=max_date, min_date=min_date)) benchmark = pd.DataFrame(self._benchmark.load_report(max_date=max_date, min_date=min_date))
datas = datas.join(benchmark.set_index('date')) benchmark.set_index('date', inplace=True)
benchmark = benchmark[['alligam_av', 'alligam_acc', 'alligam_nav']]
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 = holds.join(benchmark)
datas.fillna(method='ffill', inplace=True) datas.fillna(method='ffill', inplace=True)
datas.dropna(inplace=True)
datas.reset_index(inplace=True) datas.reset_index(inplace=True)
return datas.to_dict('records') return datas.to_dict('records')
return [] return []
import math
from datetime import datetime as dt
from typing import List
import pandas as pd
from py_jftech import component, autowired, filter_weekend, prev_workday
from api import RoboReportor, PortfoliosRisk, PortfoliosHolder, Datum, DatumType, Navs, RoboExecutor
@component(bean_name='contribution-report')
class ContributionReportor(RoboReportor):
@autowired
def __init__(self, hold: PortfoliosHolder = None, datum: Datum = None, navs: Navs = None, exec: RoboExecutor = None):
self._hold = hold
self._datum = datum
self._navs = navs
self._exec = exec
@property
def report_name(self) -> str:
return '贡献率'
def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]:
max_date = filter_weekend(max_date)
min_date = filter_weekend(min_date) if min_date is not None else self._exec.start_date
result = pd.DataFrame()
for risk in PortfoliosRisk.values():
buy_date = None
sell_date = max_date
while buy_date is None or sell_date > min_date:
last_date = sell_date if sell_date == max_date else prev_workday(sell_date)
buy_date = self._hold.get_last_rebalance_date(risk=risk, max_date=last_date)
weight = self._hold.get_portfolios_weight(day=last_date, risk=risk)
datums = pd.DataFrame(self._datum.get_datums(type=DatumType.FUND, datum_ids=tuple(weight.keys())))
datums = datums[['id', 'ftTicker', 'bloombergTicker', 'chineseName']]
datums.columns = ['id', 'ft_ticker', 'bloomberg_ticker', 'name']
datums['ratio'] = datums.apply(lambda row: weight[row.id], axis=1)
datums['hold'] = (sell_date - buy_date).days
navs = pd.DataFrame(self._navs.get_fund_navs(fund_ids=tuple(weight.keys()), max_date=sell_date, min_date=buy_date))
navs = navs.pivot_table(columns='fund_id', index='nav_date', values='nav_cal')
rtns = navs.iloc[-1] / navs.iloc[0] - 1
rtns.name = 'rtns'
datums = datums.join(rtns, on='id')
datums['risk'] = risk.name
datums['buy_date'] = buy_date
datums['sell_date'] = sell_date if sell_date != max_date else math.nan
datums.drop('id', axis=1, inplace=True)
result = pd.concat([result, datums], ignore_index=True)
sell_date = buy_date if buy_date < sell_date else prev_workday(buy_date)
return result.to_dict('records') if not result.empty else []
DROP TABLE IF EXISTS robo_benchmark;
CREATE TABLE IF NOT EXISTS robo_benchmark CREATE TABLE IF NOT EXISTS robo_benchmark
( (
rb_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, rb_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
rb_module VARCHAR(255) NOT NULL COMMENT '模块',
rb_date DATETIME NOT NULL COMMENT '日期', rb_date DATETIME NOT NULL COMMENT '日期',
rb_risk VARCHAR(255) NOT NULL COMMENT '风险等级', rb_risk VARCHAR(255) NOT NULL COMMENT '风险等级',
rb_nav DOUBLE(16, 4) NOT NULL COMMENT '资产值', rb_nav DOUBLE(16, 4) NOT NULL COMMENT '资产值',
rb_re TINYINT NOT NULL DEFAULT 0 COMMENT '是否再分配', rb_remarks JSON DEFAULT NULL COMMENT '其他信息',
rb_create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, rb_create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
rb_update_time DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, rb_update_time DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (rb_id), PRIMARY KEY (rb_id),
UNIQUE INDEX (rb_date, rb_risk), UNIQUE INDEX (rb_module, rb_date, rb_risk),
INDEX (rb_date, rb_risk),
INDEX (rb_risk) INDEX (rb_risk)
) ENGINE = InnoDB ) ENGINE = InnoDB
AUTO_INCREMENT = 0 AUTO_INCREMENT = 0
DEFAULT CHARSET = utf8mb4 COMMENT 'BENCHMARK数据表'; DEFAULT CHARSET = utf8mb4 COMMENT 'BENCHMARK数据表';
\ No newline at end of file
ALTER TABLE robo_benchmark ADD COLUMN v_rb_re TINYINT GENERATED ALWAYS AS (IF(rb_remarks->>'$.re' = 'true', 1, 0)) COMMENT '是否再分配' AFTER rb_remarks;
ALTER TABLE robo_benchmark ADD INDEX v_rb_re(`v_rb_re`);
ALTER TABLE robo_benchmark DROP INDEX v_rb_re;
ALTER TABLE robo_benchmark DROP COLUMN v_rb_re;
DROP TABLE IF EXISTS robo_data_logger;
CREATE TABLE IF NOT EXISTS robo_data_logger
(
rdl_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
rdl_date DATETIME NOT NULL COMMENT '日期',
rdl_risk VARCHAR(255) NOT NULL COMMENT '风险等级',
rdl_type VARCHAR(255) NOT NULL COMMENT '数据类别',
rdl_datas JSON NOT NULL COMMENT '日志数据',
rdl_create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
rdl_update_time DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (rdl_id),
UNIQUE INDEX (rdl_date, rdl_risk, rdl_type),
INDEX (rdl_risk, rdl_type),
INDEX (rdl_type)
) ENGINE = InnoDB
AUTO_INCREMENT = 0
DEFAULT CHARSET = utf8mb4 COMMENT '数据日志表';
\ No newline at end of file
...@@ -2,10 +2,11 @@ from py_jftech import read, write, where, mapper_columns, format_date ...@@ -2,10 +2,11 @@ from py_jftech import read, write, where, mapper_columns, format_date
__COLUMNS__ = { __COLUMNS__ = {
'rb_id': 'id', 'rb_id': 'id',
'rb_module': 'module',
'rb_date': 'date', 'rb_date': 'date',
'rb_risk': 'risk', 'rb_risk': 'risk',
'rb_nav': 'nav', 'rb_nav': 'nav',
'rb_re': 're', 'rb_remarks': 'remarks',
} }
...@@ -17,16 +18,18 @@ def batch_insert(datas): ...@@ -17,16 +18,18 @@ def batch_insert(datas):
@read(one=True) @read(one=True)
def get_last_one(max_date=None, risk=None, re: bool = None): def get_last_one(module=None, max_date=None, risk=None, re: bool = None):
sql = f"rb_date <= '{format_date(max_date)}'" if max_date else None sqls = []
if max_date:
sqls.append(f"rb_date <= '{format_date(max_date)}'")
return f''' return f'''
select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_benchmark 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 {where(*sqls, rb_module=module, rb_risk=risk, v_rb_re=re)} order by rb_date desc limit 1
''' '''
@read @read
def get_list(max_date=None, min_date=None, risk=None, re: bool = None): def get_list(max_date=None, min_date=None, module=None, risk=None, re: bool = None):
sqls = [] sqls = []
if max_date: if max_date:
sqls.append(f"rb_date <= '{format_date(max_date)}'") sqls.append(f"rb_date <= '{format_date(max_date)}'")
...@@ -34,5 +37,5 @@ def get_list(max_date=None, min_date=None, risk=None, re: bool = None): ...@@ -34,5 +37,5 @@ def get_list(max_date=None, min_date=None, risk=None, re: bool = None):
sqls.append(f"rb_date >= '{format_date(min_date)}'") sqls.append(f"rb_date >= '{format_date(min_date)}'")
return f''' return f'''
select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_benchmark 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 {where(*sqls, rb_module=module, rb_risk=risk, v_rb_re=re)} order by rb_risk, rb_date
''' '''
import json
from py_jftech import read, write, where, mapper_columns, format_date
from api import PortfoliosRisk, LoggerType
__COLUMNS__ = {
'rdl_id': 'id',
'rdl_date': 'date',
'rdl_risk': 'risk',
'rdl_type': 'type',
'rdl_datas': 'datas',
}
@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_data_logger({','.join([x for x in __COLUMNS__.keys() if x != 'rb_id'])}) values {values}'''
@write
def insert(datas):
datas = mapper_columns(datas=datas, columns=__COLUMNS__)
return f'''
insert into robo_data_logger({','.join([x for x in datas.keys()])})
values ({','.join([f"'{x[1]}'" for x in datas.items()])})
'''
def update(id, datas):
return f'''
update robo_data_logger set rdl_datas = '{json.dumps(datas, ensure_ascii=False)}' where rdl_id = {id}
'''
@read(one=True)
def get_one(date, risk: PortfoliosRisk, type: LoggerType):
return f'''
select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_data_logger
{where(rdl_date=date, rdl_risk=risk, rdl_type=type)}
'''
@read(one=True)
def get_last_one(max_date=None, risk: PortfoliosRisk = None, type: LoggerType = None, like_type=False):
sqls = []
if max_date:
sqls.append(f"rdl_date <= '{format_date(max_date)}'")
if like_type and type:
sqls.append(f"rdl_type like '{type.value}%'")
return f'''
select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_data_logger
{where(*sqls, rdl_risk=risk, rdl_type=type if not like_type else None)} order by rdl_date, rdl_id desc limit 1
'''
@read
def get_list(min_date=None, max_date=None, risk: PortfoliosRisk = None, type: LoggerType = None, like_type=False):
sqls = []
if max_date:
sqls.append(f"rdl_date <= '{format_date(max_date)}'")
if min_date:
sqls.append(f"rdl_date >= '{format_date(min_date)}'")
if like_type and type:
sqls.append(f"rdl_type like '{type.value}%'")
return f'''
select {','.join([f"{x[0]} as {x[1]}" for x in __COLUMNS__.items()])} from robo_data_logger
{where(*sqls, rdl_risk=risk, rdl_type=type if not like_type else None)} order by rdl_risk, rdl_date, rdl_id
'''
@write
def delete(min_date=None, risk: PortfoliosRisk = None, type: LoggerType = None, like_type=False):
sqls = []
if min_date:
sqls.append(f"rdl_date >= '{format_date(min_date)}'")
if like_type and type:
sqls.append(f"rdl_type like '{type.value}%'")
delete_where = where(*sqls, rdl_risk=risk, rdl_type=type if not like_type else None)
if delete_where:
return f"delete from robo_data_logger {delete_where}"
else:
return "truncate table robo_data_logger"
import json
from datetime import datetime as dt
from py_jftech import component
from api import DataLogger, LoggerType, PortfoliosRisk, Cleanable, BacktestStep
from reports.dao import robo_data_logger as rdl
@component(bean_name='data-logger')
class DatabaseLogger(DataLogger, Cleanable):
def save_record(self, date: dt, risk: PortfoliosRisk, type: LoggerType, datas: dict, exist_merge=True):
assert date is not None, "save record, date cannot be null"
assert risk is not None, "save record, risk cannot be null"
assert type is not None, "save record, type cannot be null"
assert datas is not None, "save record, dates cannot be null"
exist = rdl.get_one(date=date, risk=risk, type=type)
if exist:
save_datas = datas
if exist_merge:
save_datas = {**json.loads(exist['datas']), **datas}
rdl.update(exist['id'], save_datas)
else:
rdl.insert({
'date': date,
'risk': risk,
'type': type,
'datas': datas
})
def load_records(self, max_date=None, min_date=None, risk: PortfoliosRisk = None, type: LoggerType = None):
result = rdl.get_list(max_date=max_date, min_date=min_date, risk=risk, type=type, like_type=True)
return [{**x, 'datas': json.loads(x['datas'])} for x in result]
def clean_up(self, min_date=None, risk: PortfoliosRisk = None):
rdl.delete(min_date=min_date, risk=risk)
@property
def clean_step(self):
return BacktestStep.HOLD_PORTFOLIO
@property
def clean_name(self):
return 'data logger'
import math
from datetime import datetime as dt
from typing import List
import pandas as pd
from py_jftech import component, autowired
from api import RoboReportor
@component(bean_name='month-div-rate-report')
class MonthDivRateReportor(RoboReportor):
@autowired(names={'hold_reportor': 'hold-report', 'benchmark': 'benckmark-report'})
def __init__(self, hold_reportor: RoboReportor = None, benchmark: RoboReportor = None):
self._hold_reportor = hold_reportor
self._benchmark = benchmark
@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))
benchmark = pd.DataFrame(self._benchmark.load_report(max_date=max_date, min_date=min_date))
if not holds.empty and not benchmark.empty:
holds['divrobo'] = round(holds['port_div'] * 12 / holds['real_av'].shift() * 100, 2)
holds = holds[['date', 'divrobo']]
holds.replace(0, math.nan, inplace=True)
holds.dropna(inplace=True)
holds['date'] = holds['date'].dt.to_period('m')
holds.set_index('date', inplace=True)
benchmark['alligam'] = round(benchmark['alligam_div'] * 12 / benchmark['alligam_av'].shift() * 100, 2)
benchmark = benchmark[['date', 'alligam']]
benchmark.replace(0, math.nan, inplace=True)
benchmark.dropna(inplace=True)
benchmark['date'] = benchmark['date'].dt.to_period('m')
benchmark.set_index('date', inplace=True)
result = holds.join(benchmark)
result.reset_index(inplace=True)
return result.to_dict('records')
return []
@component(bean_name='year-div-rate-report')
class YearDivRateReportor(RoboReportor):
@autowired(names={'month_div_rate': 'month-div-rate-report'})
def __init__(self, month_div_rate: RoboReportor = None):
self._month_div_rate = month_div_rate
@property
def report_name(self) -> str:
return '年度配息率比较'
def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]:
month_datas = pd.DataFrame(self._month_div_rate.load_report(max_date=max_date, min_date=min_date))
if not month_datas.empty:
result = pd.DataFrame(columns=month_datas.columns)
month_datas['year'] = month_datas['date'].dt.year
month_datas.set_index('date', inplace=True)
result = round(month_datas.groupby(by='year').mean(), 2)
result.loc['平均'] = round(month_datas.drop(columns='year').mean(), 2)
result.reset_index(inplace=True)
return result.to_dict('records')
return []
import os import os
from abc import abstractmethod
from copy import deepcopy
from datetime import datetime as dt from datetime import datetime as dt
from typing import List
from abc import abstractmethod, ABCMeta
from tempfile import TemporaryDirectory
from shutil import copyfile from shutil import copyfile
from copy import deepcopy from tempfile import TemporaryDirectory
from typing import List
import pandas as pd import pandas as pd
from dateutil.relativedelta import relativedelta
from py_jftech import component, autowired, get_config, get_instance_name, get_project_path, format_date, sendmail from py_jftech import component, autowired, get_config, get_instance_name, get_project_path, format_date, sendmail
from api import RoboReportor, RoboExportor from api import RoboReportor, RoboExportor, RoboExecutor
def include_report(): def include_report():
...@@ -19,35 +20,52 @@ def include_report(): ...@@ -19,35 +20,52 @@ def include_report():
class DefaultExportor(RoboExportor): class DefaultExportor(RoboExportor):
@autowired @autowired
def __init__(self, reportors: List[RoboReportor] = None): def __init__(self, reportors: List[RoboReportor] = None, exec: RoboExecutor = None):
self._reportors = {get_instance_name(x): x for x in reportors} self._reportors = {get_instance_name(x): x for x in reportors}
self._exec = exec
def export(self, max_date=dt.today(), min_date=None): def export(self, max_date=dt.today(), min_date=None):
if not self.include_report: if not self.include_report:
return None return None
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
filename = f"{self.file_name}_{format_date(max_date)}.xlsx" filename = f"{self.file_name}_{format_date(self._exec.curt_date)}"
if min_date: filepath = os.path.join(tmpdir, f"{filename}.xlsx")
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: with pd.ExcelWriter(filepath) as writer:
for reportor_name in self.include_report: for reportor_name in self.include_report:
reportor = self._reportors[reportor_name] mindate = min_date
datas = pd.DataFrame(reportor.load_report(max_date=max_date, min_date=min_date)) if isinstance(reportor_name, dict):
reportor = self._reportors[reportor_name['name']]
if reportor_name['min-date'] is None:
mindate = None
elif isinstance(reportor_name['min-date'], dict):
mindate = max_date - relativedelta(**reportor_name['min-date'])
datas = pd.DataFrame(reportor.load_report(max_date=max_date, min_date=mindate))
else:
reportor = self._reportors[reportor_name]
datas = pd.DataFrame(reportor.load_report(max_date=max_date, min_date=mindate))
sheet_name = reportor.report_name
if mindate and mindate > self._exec.start_date:
sheet_name = f'{sheet_name}(近{(max_date-mindate).days}天)'
if not datas.empty: if not datas.empty:
datas.to_excel(writer, sheet_name=reportor.report_name, index=False) datas.to_excel(writer, sheet_name=sheet_name, index=False)
email = self.get_email(filepath) email = self.get_email(filepath)
if email: if email and 'receives' in email and email['receives']:
receives = email['receives'] receives = email['receives']
copies = email['copies'] if 'copies' in email else [] copies = email['copies'] if 'copies' in email and email['copies'] is not None else []
attach_paths = [filepath] attach_paths = [filepath]
subject = email['subject'].format(today=format_date(dt.today()), max_date=max_date, min_date=min_date) subject = email['subject'].format(today=format_date(dt.today()))
content = email['content'].format(today=format_date(dt.today()), max_date=max_date, min_date=min_date) content = email['content'].format(today=format_date(dt.today()))
sendmail(receives=receives, copies=copies, attach_paths=attach_paths, subject=subject, content=content) sendmail(receives=receives, copies=copies, attach_paths=attach_paths, subject=subject, content=content)
if self.save_path is not None: if self.save_path is not None:
os.makedirs(self.save_path, exist_ok=True) os.makedirs(self.save_path, exist_ok=True)
save_file = os.path.join(self.save_path, filename) save_file = os.path.join(self.save_path, f"{filename}.xlsx")
copyfile(filepath, save_file) copyfile(filepath, save_file)
if self.save_config:
profile_active = os.environ.get('PROFILE_ACTIVE')
config_name = f'config-{profile_active}.yml' if profile_active is not None else 'config.yml'
src_path = f'{get_project_path()}{os.path.sep}{config_name}'
save_path = os.path.join(self.save_path, f"{filename}.yml")
copyfile(src_path, save_path)
def get_email(self, file): def get_email(self, file):
return deepcopy(self.config['email']) if 'email' in self.config else None return deepcopy(self.config['email']) if 'email' in self.config else None
...@@ -75,6 +93,10 @@ class DefaultExportor(RoboExportor): ...@@ -75,6 +93,10 @@ class DefaultExportor(RoboExportor):
def include_report(self): def include_report(self):
return self.config['include-report'] if 'include-report' in self.config else [] return self.config['include-report'] if 'include-report' in self.config else []
@property
def save_config(self):
return self.config['save-config'] if 'save-config' in self.config else False
@property @property
@abstractmethod @abstractmethod
def config(self): def config(self):
...@@ -93,17 +115,17 @@ class BacktestExportor(DefaultExportor): ...@@ -93,17 +115,17 @@ class BacktestExportor(DefaultExportor):
return self.__config['backtest'] return self.__config['backtest']
@component(bean_name='real-daily-export') @component(bean_name='daily-real-export')
class RealDailyExportor(DefaultExportor): class DailyRealExportor(DefaultExportor):
@autowired(names={'signal_reportor': 'daily-signal-report'}) @autowired(names={'signal_reportor': 'daily-signal-report'})
def __init__(self, signal_reportor: RoboReportor = None): def __init__(self, signal_reportor: RoboReportor = None):
super(RealDailyExportor, self).__init__() super(DailyRealExportor, self).__init__()
self.__config = get_config(__name__) self.__config = deepcopy(get_config(__name__))
self._signal_reportor = signal_reportor self._signal_reportor = signal_reportor
def get_email(self, file): def get_email(self, file):
result = super(RealDailyExportor, self).get_email(file) result = super(DailyRealExportor, self).get_email(file)
if result is None: if result is None:
return None return None
content = pd.read_excel(file, sheet_name=None) content = pd.read_excel(file, sheet_name=None)
...@@ -118,3 +140,15 @@ class RealDailyExportor(DefaultExportor): ...@@ -118,3 +140,15 @@ class RealDailyExportor(DefaultExportor):
@property @property
def config(self): def config(self):
return self.__config['real-daily'] return self.__config['real-daily']
@component(bean_name='daily-monitor-export')
class DailyMonitorExportor(DefaultExportor):
def __init__(self):
super(DailyMonitorExportor, self).__init__()
self.__config = deepcopy(get_config(__name__))
@property
def config(self):
return self.__config['daily-monitor']
...@@ -25,8 +25,8 @@ class FixedRangeReport(RoboReportor): ...@@ -25,8 +25,8 @@ class FixedRangeReport(RoboReportor):
def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: 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 = pd.DataFrame(self._combo.load_report(max_date=max_date, min_date=min_date))
datas.set_index('date', inplace=True)
if not datas.empty: if not datas.empty:
datas.set_index('date', inplace=True)
result = pd.DataFrame(columns=datas.columns) result = pd.DataFrame(columns=datas.columns)
for range in self.range_dates: for range in self.range_dates:
start = filter_weekend(range['start']) start = filter_weekend(range['start'])
...@@ -34,6 +34,7 @@ class FixedRangeReport(RoboReportor): ...@@ -34,6 +34,7 @@ class FixedRangeReport(RoboReportor):
if not datas[start:end].empty: if not datas[start:end].empty:
row_name = f"{format_date(start)}~{format_date(end)}" 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.loc[row_name] = datas[start:end].values[-1] / datas[start:end].values[0] - 1
result = round(result, 4) * 100 result = round(result, 4) * 100
result.reset_index(inplace=True) result.reset_index(inplace=True)
result.rename(columns={'index': 'range-date'}, inplace=True) result.rename(columns={'index': 'range-date'}, inplace=True)
......
...@@ -21,8 +21,8 @@ class IndicatorsReportor(RoboReportor): ...@@ -21,8 +21,8 @@ class IndicatorsReportor(RoboReportor):
def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: 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 = pd.DataFrame(self._combo.load_report(max_date=max_date, min_date=min_date))
datas.set_index('date', inplace=True)
if not datas.empty: if not datas.empty:
datas.set_index('date', inplace=True)
returns = round(datas.pct_change(), 5) returns = round(datas.pct_change(), 5)
indicators = { indicators = {
'annual_return': list(annual_return(returns, period='daily', annualization=None) * 100), 'annual_return': list(annual_return(returns, period='daily', annualization=None) * 100),
......
...@@ -26,13 +26,13 @@ class RelativeRangeReport(RoboReportor): ...@@ -26,13 +26,13 @@ class RelativeRangeReport(RoboReportor):
def load_report(self, max_date=dt.today(), min_date=None) -> List[dict]: 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 = pd.DataFrame(self._combo.load_report(max_date=max_date, min_date=min_date))
datas.set_index('date', inplace=True)
if not datas.empty: if not datas.empty:
datas.set_index('date', inplace=True)
result = pd.DataFrame(columns=datas.columns) result = pd.DataFrame(columns=datas.columns)
for range in self.range_dates: for range in self.range_dates:
kwargs = range.copy() kwargs = range.copy()
del kwargs['name'] del kwargs['name']
start = filter_weekend(max_date - relativedelta(**kwargs)) start = filter_weekend(max_date - relativedelta(**kwargs)) if kwargs and ('dates' not in kwargs or kwargs['dates'] is not None) else datas.index[0]
end = filter_weekend(max_date) end = filter_weekend(max_date)
row_name = f"{range['name']}({format_date(start)}~{format_date(end)})" 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.loc[row_name] = datas[start:end].values[-1] / datas[start:end].values[0] - 1
......
import unittest
import logging import logging
import tempfile import unittest
from datetime import datetime as dt from datetime import datetime as dt
from py_jftech import autowired, to_str, parse_date, prev_workday from py_jftech import autowired, to_str, parse_date, prev_workday
from api import RoboReportor, RoboExportor from api import RoboReportor, RoboExportor
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -13,32 +13,61 @@ class ReportTest(unittest.TestCase): ...@@ -13,32 +13,61 @@ class ReportTest(unittest.TestCase):
@autowired(names={'reportor': 'benckmark-report'}) @autowired(names={'reportor': 'benckmark-report'})
def test_benchmark_report(self, reportor: RoboReportor = None): def test_benchmark_report(self, reportor: RoboReportor = None):
result = reportor.load_report(max_date=parse_date('2022-11-01')) result = reportor.load_report(max_date=parse_date('2023-03-01'))
logger.info(to_str(result)) logger.info(to_str(result))
@autowired(names={'reportor': 'indicators-report'}) @autowired(names={'reportor': 'indicators-report'})
def test_indicator_report(self, reportor: RoboReportor = None): def test_indicator_report(self, reportor: RoboReportor = None):
result = reportor.load_report(max_date=parse_date('2022-11-01')) result = reportor.load_report(max_date=parse_date('2023-03-01'))
logger.info(to_str(result)) logger.info(to_str(result))
@autowired(names={'reportor': 'fixed-range-report'}) @autowired(names={'reportor': 'fixed-range-report'})
def test_fixed_report(self, reportor: RoboReportor = None): def test_fixed_report(self, reportor: RoboReportor = None):
result = reportor.load_report(max_date=parse_date('2022-11-01')) result = reportor.load_report(max_date=parse_date('2023-03-01'))
logger.info(to_str(result)) logger.info(to_str(result))
@autowired(names={'reportor': 'relative-range-report'}) @autowired(names={'reportor': 'relative-range-report'})
def test_relative_report(self, reportor: RoboReportor = None): def test_relative_report(self, reportor: RoboReportor = None):
result = reportor.load_report(max_date=parse_date('2022-11-01')) result = reportor.load_report(max_date=parse_date('2023-03-01'))
logger.info(to_str(result))
@autowired(names={'reportor': 'contribution-report'})
def test_contribution_report(self, reportor: RoboReportor = None):
result = reportor.load_report(max_date=parse_date('2023-03-21'))
logger.info(to_str(result))
@autowired(names={'reportor': 'combo-report'})
def test_combo_report(self, reportor: RoboReportor = None):
result = reportor.load_report(max_date=parse_date('2023-03-01'))
logger.info(to_str(result))
@autowired(names={'reportor': 'year-range-report'})
def test_year_report(self, reportor: RoboReportor = None):
result = reportor.load_report(max_date=parse_date('2023-03-01'))
logger.info(to_str(result))
@autowired(names={'reportor': 'month-div-rate-report'})
def test_year_report(self, reportor: RoboReportor = None):
result = reportor.load_report(max_date=parse_date('2023-03-01'))
logger.info(to_str(result))
@autowired(names={'reportor': 'year-div-rate-report'})
def test_year_report(self, reportor: RoboReportor = None):
result = reportor.load_report(max_date=parse_date('2023-03-01'))
logger.info(to_str(result)) logger.info(to_str(result))
@autowired(names={'exportor': 'backtest-export'}) @autowired(names={'exportor': 'backtest-export'})
def test_backtest_export(self, exportor: RoboExportor = None): def test_backtest_export(self, exportor: RoboExportor = None):
exportor.export(max_date=parse_date('2022-11-01')) exportor.export(max_date=parse_date('2023-03-01'))
@autowired(names={'exportor': 'real-daily-export'}) @autowired(names={'exportor': 'daily-real-export'})
def test_daliy_export(self, exportor: RoboExportor = None): def test_daily_export(self, exportor: RoboExportor = None):
exportor.export(max_date=prev_workday(dt.today())) exportor.export(max_date=prev_workday(dt.today()))
@autowired(names={'exportor': 'daily-monitor-export'})
def test_daily_monitor(self, exportor: RoboExportor = None):
exportor.export(max_date=parse_date('2023-03-27'))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
from datetime import datetime as dt
from typing import List
import pandas as pd
from py_jftech import component, autowired
from api import RoboReportor
@component(bean_name='year-range-report')
class YearRangeReport(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))
if not datas.empty:
datas['year'] = datas['date'].dt.year
datas.set_index('date', inplace=True)
result = pd.DataFrame(columns=datas.columns)
last_data = None
for year, group_data in datas.groupby(by='year', group_keys=False):
group_data = group_data.drop(columns='year')
if last_data is None:
last_data = group_data.iloc[0]
result.loc[year] = group_data.iloc[-1] / last_data - 1
last_data = group_data.iloc[-1]
result.drop(columns='year', inplace=True)
result = round(result * 100, 4)
result.reset_index(inplace=True)
result.rename(columns={'index': 'year'}, inplace=True)
return result.to_dict('records')
return []
...@@ -2,7 +2,6 @@ import logging ...@@ -2,7 +2,6 @@ import logging
import sys import sys
from concurrent.futures import wait from concurrent.futures import wait
from datetime import datetime as dt from datetime import datetime as dt
from enum import Enum, unique
from typing import List from typing import List
import pandas as pd import pandas as pd
...@@ -13,25 +12,12 @@ from py_jftech import ( ...@@ -13,25 +12,12 @@ from py_jftech import (
from api import ( from api import (
RoboExecutor, Datum, AssetPool, PortfoliosBuilder, RoboExecutor, Datum, AssetPool, PortfoliosBuilder,
PortfoliosRisk, PortfoliosHolder, DataSync, RoboExportor PortfoliosRisk, PortfoliosHolder, DataSync, RoboExportor, BacktestStep
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@unique
class BacktestStep(Enum):
ASSET_POOL = 1
NORMAL_PORTFOLIO = 2
HOLD_PORTFOLIO = 3
def within(self, step: Enum):
return self.value <= step.value
def without(self, step: Enum):
return self.value >= step.value
@component(bean_name='backtest') @component(bean_name='backtest')
class BacktestExecutor(RoboExecutor): class BacktestExecutor(RoboExecutor):
...@@ -127,10 +113,10 @@ class BacktestExecutor(RoboExecutor): ...@@ -127,10 +113,10 @@ class BacktestExecutor(RoboExecutor):
now = dt.now() now = dt.now()
wait([self.async_build_hold(x) for x in PortfoliosRisk]) 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(f"build hold portfolios success, use[{(dt.now() - now).seconds}s]")
# logger.info("start to export report".center(50, '-')) logger.info("start to export report".center(50, '-'))
# now = dt.now() now = dt.now()
# file = self._export.export(max_date=self.end_date, min_date=self.start_date) 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].") logger.info(f"report file exported successfully. use[{(dt.now() - now).seconds}s].")
@asynchronized(isolate=True) @asynchronized(isolate=True)
def async_build_risk_date(self, asset_id): def async_build_risk_date(self, asset_id):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment