5 | {% with html_id = class.path %}
6 |
7 | {% if not root or config.show_root_heading %}
8 |
9 | {% if root %}
10 | {% set show_full_path = config.show_root_full_path %}
11 | {% set root_members = True %}
12 | {% elif root_members %}
13 | {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %}
14 | {% set root_members = False %}
15 | {% else %}
16 | {% set show_full_path = config.show_object_full_path %}
17 | {% endif %}
18 |
19 | {% filter heading(heading_level,
20 | id=html_id,
21 | class="doc doc-heading",
22 | toc_label=class.name) %}
23 |
24 |
{% if show_full_path %}{{ class.path }}{% else %}{{ class.name }}{% endif %}
25 |
26 | {% with properties = class.properties %}
27 | {% include "properties.html" with context %}
28 | {% endwith %}
29 |
30 | {% endfilter %}
31 |
32 | {% else %}
33 | {% if config.show_root_toc_entry %}
34 | {% filter heading(heading_level,
35 | id=html_id,
36 | toc_label=class.path,
37 | hidden=True) %}
38 | {% endfilter %}
39 | {% endif %}
40 | {% set heading_level = heading_level - 1 %}
41 | {% endif %}
42 |
43 |
44 | {% with docstring_sections = class.docstring_sections %}
45 | {% include "docstring.html" with context %}
46 | {% endwith %}
47 |
48 | {% if config.show_source and class.source %}
49 |
50 | Source code in {{ class.relative_file_path }}
51 | {{ class.source.code|highlight(language="python", linestart=class.source.line_start, linenums=False) }}
52 |
53 | {% endif %}
54 |
55 | {% with obj = class %}
56 | {% set root = False %}
57 | {% set heading_level = heading_level + 1 %}
58 | {% include "children.html" with context %}
59 | {% endwith %}
60 |
61 |
62 | {% endwith %}
63 |
64 |
65 | {% endif %}
66 |
--------------------------------------------------------------------------------
/docs/src/mkdocstrings/templates/python/material/method.html:
--------------------------------------------------------------------------------
1 | {{ log.debug() }}
2 | {% if config.show_if_no_docstring or method.has_contents %}
3 |
4 |
5 | {% with html_id = method.path %}
6 |
7 | {% if not root or config.show_root_heading %}
8 |
9 | {% if root %}
10 | {% set show_full_path = config.show_root_full_path %}
11 | {% set root_members = True %}
12 | {% elif root_members %}
13 | {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %}
14 | {% set root_members = False %}
15 | {% else %}
16 | {% set show_full_path = config.show_object_full_path %}
17 | {% endif %}
18 |
19 | {% filter heading(heading_level,
20 | id=html_id,
21 | class="doc doc-heading",
22 | toc_label=method.name ~ "()") %}
23 |
24 | {% filter highlight(language="python", inline=True) %}
25 | {% if show_full_path %}{{ method.path }}{% else %}{{ method.name }}{% endif %}
26 | {% with signature = method.signature %}{% include "signature.html" with context %}{% endwith %}
27 | {% endfilter %}
28 |
29 | {% with properties = method.properties %}
30 | {% include "properties.html" with context %}
31 | {% endwith %}
32 |
33 | {% endfilter %}
34 |
35 | {% else %}
36 | {% if config.show_root_toc_entry %}
37 | {% filter heading(heading_level,
38 | id=html_id,
39 | toc_label=method.path,
40 | hidden=True) %}
41 | {% endfilter %}
42 | {% endif %}
43 | {% set heading_level = heading_level - 1 %}
44 | {% endif %}
45 |
46 |
47 | {% with docstring_sections = method.docstring_sections %}
48 | {% include "docstring.html" with context %}
49 | {% endwith %}
50 |
51 | {% if config.show_source and method.source %}
52 |
53 | Source code in {{ method.relative_file_path }}
54 | {{ method.source.code|highlight(language="python", linestart=method.source.line_start, linenums=False) }}
55 |
56 | {% endif %}
57 |
58 |
59 | {% endwith %}
60 |
61 |
62 | {% endif %}
63 |
--------------------------------------------------------------------------------
/pure_ocean_breeze.egg-info/PKG-INFO:
--------------------------------------------------------------------------------
1 | Metadata-Version: 2.2
2 | Name: pure_ocean_breeze
3 | Version: 6.1.0
4 | Summary: stock factor test
5 | Home-page: https://github.com/chen-001/pure_ocean_breeze.git
6 | Author: chenzongwei
7 | Author-email: winterwinter999@163.com
8 | License: MIT
9 | Project-URL: Documentation, https://chen-001.github.io/pure_ocean_breeze/
10 | Requires-Python: >=3
11 | Description-Content-Type: text/markdown
12 | License-File: LICENSE
13 | Requires-Dist: pandas<=1.5.3
14 | Requires-Dist: scipy
15 | Requires-Dist: statsmodels
16 | Requires-Dist: plotly
17 | Requires-Dist: pyarrow
18 | Requires-Dist: loguru
19 | Requires-Dist: knockknock
20 | Requires-Dist: dcube
21 | Requires-Dist: tenacity
22 | Requires-Dist: pickledb
23 | Requires-Dist: pymysql
24 | Requires-Dist: sqlalchemy
25 | Requires-Dist: requests
26 | Requires-Dist: bs4
27 | Requires-Dist: wrapt_timeout_decorator
28 | Requires-Dist: pyfinance
29 | Requires-Dist: texttable
30 | Requires-Dist: numpy_ext
31 | Requires-Dist: xpinyin
32 | Requires-Dist: cufflinks
33 | Requires-Dist: clickhouse_sqlalchemy
34 | Requires-Dist: tradetime
35 | Requires-Dist: deprecation
36 | Requires-Dist: questdb==1.1.0
37 | Requires-Dist: mpire
38 | Requires-Dist: py7zr
39 | Requires-Dist: unrar
40 | Requires-Dist: rarfile
41 | Requires-Dist: chardet
42 | Requires-Dist: cachier
43 | Requires-Dist: polars
44 | Requires-Dist: polars_ols
45 | Requires-Dist: python_docx
46 | Requires-Dist: psycopg2-binary
47 | Provides-Extra: windows
48 | Requires-Dist: psycopg2; extra == "windows"
49 | Provides-Extra: macos
50 | Requires-Dist: psycopg2-binary; extra == "macos"
51 | Provides-Extra: linux
52 | Requires-Dist: psycopg2-binary; extra == "linux"
53 | Dynamic: author
54 | Dynamic: author-email
55 | Dynamic: description
56 | Dynamic: description-content-type
57 | Dynamic: home-page
58 | Dynamic: license
59 | Dynamic: project-url
60 | Dynamic: provides-extra
61 | Dynamic: requires-dist
62 | Dynamic: requires-python
63 | Dynamic: summary
64 |
65 | # pure_ocean_breeze
66 | #### **众人的因子框架**
67 |
68 | #### 作者😉
69 | >* 量价选股因子黄金矿工💁♂️
70 | >* 挖因子兼新技术爱好者💁♂️
71 | >* 感谢每一个教我写代码和教我挖因子的人💐
72 | >* 欢迎交流技术优化&因子灵感&工作信息:
73 |
74 | #### 相关链接🔗
75 | >* [PyPi](https://pypi.org/project/pure-ocean-breeze/)
76 | >* [Github同步到Pypi操作手册](https://github.com/chen-001/pure_ocean_breeze/blob/master/Github同步Pypi操作手册/Github同步Pypi操作手册.md)
77 |
--------------------------------------------------------------------------------
/docs/src/mkdocstrings/templates/python/material/function.html:
--------------------------------------------------------------------------------
1 | {{ log.debug() }}
2 | {% if config.show_if_no_docstring or function.has_contents %}
3 |
4 |
5 | {% with html_id = function.path %}
6 |
7 | {% if not root or config.show_root_heading %}
8 |
9 | {% if root %}
10 | {% set show_full_path = config.show_root_full_path %}
11 | {% set root_members = True %}
12 | {% elif root_members %}
13 | {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %}
14 | {% set root_members = False %}
15 | {% else %}
16 | {% set show_full_path = config.show_object_full_path %}
17 | {% endif %}
18 |
19 | {% filter heading(heading_level,
20 | id=html_id,
21 | class="doc doc-heading",
22 | toc_label=function.name ~ "()") %}
23 |
24 | {% filter highlight(language="python", inline=True) %}
25 | {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %}
26 | {% with signature = function.signature %}{% include "signature.html" with context %}{% endwith %}
27 | {% endfilter %}
28 |
29 | {% with properties = function.properties %}
30 | {% include "properties.html" with context %}
31 | {% endwith %}
32 |
33 | {% endfilter %}
34 |
35 | {% else %}
36 | {% if config.show_root_toc_entry %}
37 | {% filter heading(heading_level,
38 | id=html_id,
39 | toc_label=function.path,
40 | hidden=True) %}
41 | {% endfilter %}
42 | {% endif %}
43 | {% set heading_level = heading_level - 1 %}
44 | {% endif %}
45 |
46 |
47 | {% with docstring_sections = function.docstring_sections %}
48 | {% include "docstring.html" with context %}
49 | {% endwith %}
50 |
51 | {% if config.show_source and function.source %}
52 |
53 | Source code in {{ function.relative_file_path }}
54 | {{ function.source.code|highlight(language="python", linestart=function.source.line_start, linenums=False) }}
55 |
56 | {% endif %}
57 |
58 |
59 | {% endwith %}
60 |
61 |
62 | {% endif %}
63 |
--------------------------------------------------------------------------------
/Github同步Pypi操作手册/Github同步Pypi操作手册.md:
--------------------------------------------------------------------------------
1 | ### Github同步Pypi操作手册
2 | ---
3 | * #### 步骤一 ➡️ 创建workflow
4 | >1. 在**repository**主页找到**Actions**
5 | >
6 | >2. 在**Actions**中找到**Publish Python Package**
7 | >
8 | >3. 点击**Configure**创建**workflow**
9 | >
10 | >4. 此时自动在主分支下创建了`.github/workflows/python-publish.yml`文件
11 | >
12 | >👏**至此第一步结束啦👏**
13 |
14 | * #### 步骤二 ➡️ 设置自动更新动作
15 | >1. **解释**:`python-publish.yml`中的内容即更新动作
16 | >2. 复制如下模板,替换`python-publish.yml`中的内容
17 | >>```yml
18 | >># action的名称
19 | >>name: Upload Python Package
20 | >>
21 | >>on:
22 | >> # 当setup.py分支有push时,触发action
23 | >> push:
24 | >> paths:
25 | >> - 'setup.py'
26 | >>
27 | >>jobs:
28 | >> deploy:
29 | >> runs-on: ubuntu-latest
30 | >> steps:
31 | >> - uses: actions/checkout@v3
32 | >> - name: Set up Python
33 | >> uses: actions/setup-python@v4
34 | >> with:
35 | >> python-version: '3.x'
36 | >> - name: Install dependencies
37 | >> run: |
38 | >> python -m pip install --upgrade pip
39 | >> pip install build
40 | >> - name: Build package
41 | >> run: python -m build
42 | >> - name: Publish package
43 | >> uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
44 | >> with:
45 | >> user: ${{secrets.PYPI_USERNAME}}
46 | >> password: ${{secrets.PYPI_PASSWORD}}
47 | >>```
48 | >3. 提交创建`python-publish.yml`文件
49 | >
50 | >
51 | >👏**至此第二步结束啦👏**
52 |
53 | * #### 步骤三 ➡️ 设置Pypi账号密码
54 | >1. **解释**:为了不在`python-publish.yml`文件中暴露自己Pypi的账号密码,需要在Github的仓库密钥中设置自己的账号密码。对应在上述`python-publish.yml`文件中的
55 | >>```yml
56 | >>user: ${{secrets.PYPI_USERNAME}}
57 | >>password: ${{secrets.PYPI_PASSWORD}}
58 | >>```
59 | >2. 打开**repository**的**settings**(⚠️注意,并非个人账户的settings)
60 | >
61 | >3. 选择左侧**Secrets**下的**Actions**,然后点击右上角的**New repository secret**
62 | >
63 | >4. 添加新secret,pypi的账号,在Name中输入**PYPI_USERNAME**,在Value中输入自己Pypi账号
64 | >
65 | >5. 再添加一个secret,pypi的密码,在Name中输入**PYPI_PASSWORD**,在Value中输入自己的Pypi密码
66 | >
67 | >
68 | >
69 | >👏**至此第三步结束啦👏**
70 |
71 | * #### 步骤四 ➡️ 上传`setup.py`文件
72 | >1. 上传`setup.py`文件
73 | >
74 | >
75 | >👏**至此第四步结束啦👏**
76 |
77 | * #### 使用说明
78 | >1. 更新Pypi新版本:更新`setup.py`文件并push,会触发前面设置的Action,从而自动更新Pypi新版本
79 | >2. 其他文件的更新:只要不包含`setup.py`的更新,就不会触发更新Pypi版本的Action
--------------------------------------------------------------------------------
/更新日志/version4.md:
--------------------------------------------------------------------------------
1 | ## 更新日志🗓 — v4
2 |
3 |
4 | * v4.0.8 — 2023.10.14
5 |
6 | > 1. 修复了Questdb读取到None时不能识别的bug
7 | > 2. 修正了read_daily计算振幅的公式错误
8 | > 3. 修复了read_market中读取开盘价时的错误
9 | > 4. 升级了pure_moon因子回测结果展示,删去了因子截面标准差,新增了各组月均超额收益项
10 | > 5. 给follow_tests新增了groups_num参数,用于指定分组数量;新增了without_industry参数,指定不在行业成分股内做测试
11 |
12 |
13 | * v4.0.7 — 2023.7.17
14 |
15 | > 1. 给pure_fall_frequent和pure_fall_nature新增了use_mpire参数,可以使用mpire库开启并行
16 | > 2. 修复了pure_fall_frequent的select_one_calculate方法中,可能存在的返回类型为字符串的问题
17 | > 3. 修复了pure_fall_nature中全新因子运算完拼接时的bug
18 |
19 |
20 | * v4.0.6 — 2023.7.14
21 |
22 | > 1. 修复了pure_dawn读取已有因子值时`__call__`方法返回错误的问题
23 | > 2. 修复了pure_fall_frequent中关于计算因子值的bug
24 | > 3. 修复了optimize_many_days函数和pure_linprog的run方法不受STATES['START']影响的bug
25 | > 4. 随着时间的推移,将STATES['START']值修改为20140101
26 | > 5. 更新了依赖库
27 |
28 |
29 | * v4.0.5 — 2023.7.7
30 |
31 | > 1. 修复了pure_fall_nature中缺少fields参数和参数传递的bug
32 | > 2. 修复了pure_fall_nature中拼接新旧因子时的错误
33 | > 3. 删去了pure_fall_nature中get_daily_factors的系统通知
34 | > 4. 将pure_fall_frequent中的并行改为使用concurrent
35 | > 5. 对pure_fall_frequent中的get_daily_factors新增了防止系统通知报错的功能
36 | > 6. 对pure_coldwinter加入了缓存机制,改进了相关系数的计算方法,大幅提高了运算速度
37 | > 7. 对pure_coldwinter剔除的风格因子默认新增了传统反转因子、传统波动因子、传统换手因子
38 | > 8. 将get_abs的median参数替换为quantile参数,表示计算到截面某个分位点的距离
39 | > 9. 将clip_mad中的参数n默认值改为5
40 |
41 |
42 | * v4.0.4 — 2023.7.4
43 |
44 | > 1. 优化了show_corrs_with_old的结果展示排序
45 | > 2. 修复了pure_fall_nature中关于money与amount的错误
46 | > 3. 给pure_fall_nature的get_daily_factors方法增加了fields参数,用于指定要读取的字段,以节约内存(使用duckdb实现)
47 | > 4. 将pure_fall_nature中的并行方法改为使用concurrent
48 |
49 |
50 | * v4.0.3 — 2023.7.4
51 |
52 | > 1. 新增了全新的因子成果数据库FactorDone,每个最终复合因子,都附带其细分因子
53 | > 2. 对show_corr函数的默认计算方式进行了提速,并新增old_way参数,用于使用旧版方式计算
54 | > 3. 将show_corr、show_corrs、show_corrs_with_old函数中相关系数的默认计算方式调整为pearson相关系数
55 | > 4. 对show_corrs_with_old的内容进行了升级,以支持计算因子与已有因子的细分因子之间的相关系数
56 |
57 |
58 | * v4.0.2 — 2023.7.3
59 |
60 | > 1. 暂时取消了对numpy的强制依赖
61 |
62 |
63 | * v4.0.1 — 2023.7.3
64 |
65 | > 1. 修正了convert_tick_by_tick_data_to_parquet中成交额与成交量的混淆错误
66 | > 2. 修复了database_update_zxindustry_member中的一些异常
67 | > 3. 修复了pure_fall_nature中的一些异常
68 | > 4. 取消了对matplotlib库的依赖
69 |
70 |
71 | * v4.0.0 — 2023.6.28
72 |
73 | > 1. 修复了初始化时可能产生的报错,将初始化函数更名为ini,可通过如下语句初始化
74 | >
75 | > ```python
76 | > import pure_ocean_breeze as p
77 | >
78 | > p.ini()
79 | > ```
80 | > 2. 初始化函数与`Homeplace`参数新增了存储逐笔数据的路径
81 | > 3. 增加了使用逐笔数据计算因子值的代码框架`pure_fall_nature`,并可以使用逐笔数据合成任意秒线、分钟线、小时线
82 | > 4. write_data部分新增了`convert_tick_by_tick_data_to_parquet`、`convert_tick_by_tick_data_daily`、`convert_tick_by_tick_data_monthly`三个更新逐笔数据的函数
83 | > 5. 修复了database_update_daily_files中关于更新pe_ttm数据的错误
84 | > 6. 修复了pure_moon中截止日期非最新日期时,关于多头超均收益的错误
85 | > 7. 修复了pure_fall_frequent中确实日期仅为异常日期时的bug
86 | > 8. ClickHouseClient中新增了秒级数据的show_all_dates
87 | > 9. 将对pandas的依赖限制在了1.5.3及以下版本
--------------------------------------------------------------------------------
/database/on_mysql/ricequant_to_mysql.py:
--------------------------------------------------------------------------------
1 | from pure_ocean_breeze.pure_ocean_breeze import *
2 |
3 | '''执行本代码之前,请先删除之前的两个数据minute_data和minute_data_alter'''
4 |
5 | #分钟数据文件所在的路径
6 | mipath='/Users/chenzongwei/pythoncode/数据库/Ricequant-minbar/equities'
7 |
8 | #新建数据库
9 | sql=sqlConfig()
10 | #每只股票一张表
11 | sql.add_new_database('minute_data_stock')
12 | #每天所有股票一张表
13 | sql.add_new_database('minute_data_stock_alter')
14 | #每个指数一张表
15 | sql.add_new_database('minute_data_index')
16 | #每天所有指数一张表
17 | sql.add_new_database('minute_data_index_alter')
18 |
19 | #连接4个数据库
20 | sqls=sqlConfig('minute_data_stock')
21 | sqlsa=sqlConfig('minute_data_stock_alter')
22 | sqli=sqlConfig('minute_data_index')
23 | sqlia=sqlConfig('minute_data_index_alter')
24 |
25 | #遍历所有分钟数据文件
26 | files=os.listdir(mipath)
27 | files=[mipath+'/'+i for i in files]
28 | fails=[]
29 |
30 | for path in tqdm.tqdm(files):
31 | #识别代码属于股票还是指数
32 | code,kind=convert_code(path)
33 | #一些数据预处理
34 | df=read_h5_new(path)
35 | df=df.rename(columns={'datetime':'date','volume':'amount','total_turnover':'money'})
36 | df=df[['date','open','high','low','close','amount','money']].sort_values('date')
37 | df.date=df.date.astype(str).str.slice(stop=8).astype(int)
38 | df=df.groupby('date').apply(lambda x:x.assign(num=list(range(1,x.shape[0]+1))))
39 | df=(np.around(df,2)*100).ffill().dropna().astype(int).assign(code=code)
40 | #所有的日期
41 | dates=list(set(df.date))
42 | try:
43 | #股票
44 | if kind=='stock':
45 | #写入每只股票一张表
46 | df.drop(columns=['code']).to_sql(
47 | name=code,con=sqls.engine,if_exists='replace',index=False,
48 | dtype={'date':INT,'open':INT,'high':INT,'low':INT,'close':INT,'amount':BIGINT,'money':BIGINT,'num':INT})
49 | #把每天写入每天所有股票一张表
50 | for date in dates:
51 | dfi=df[df.date==date]
52 | dfi.drop(columns=['date']).to_sql(
53 | name=str(date),con=sqlsa.engine,if_exists='append',index=False,
54 | dtype={'code':VARCHAR(9),'open':INT,'high':INT,'low':INT,'close':INT,'amount':BIGINT,'money':BIGINT,'num':INT})
55 | #指数
56 | else:
57 | #写入每个指数一张表
58 | df.drop(columns=['code']).to_sql(
59 | name=code,con=sqli.engine,if_exists='replace',index=False,
60 | dtype={'date':INT,'open':INT,'high':INT,'low':INT,'close':INT,'amount':BIGINT,'money':BIGINT,'num':INT})
61 | #把每天写入每天所有指数一张表
62 | for date in dates:
63 | dfi=df[df.date==date]
64 | dfi.drop(columns=['date']).to_sql(
65 | name=str(date),con=sqlia.engine,if_exists='append',index=False,
66 | dtype={'code':VARCHAR(9),'open':INT,'high':INT,'low':INT,'close':INT,'amount':BIGINT,'money':BIGINT,'num':INT})
67 | except Exception:
68 | fails.append(code)
69 | logger.warning(f'{code}失败了,请检查')
70 | logger.success('全部搬运到mysql里啦')
--------------------------------------------------------------------------------
/pure_ocean_breeze/jason/data/dicts.py:
--------------------------------------------------------------------------------
1 | __updated__ = "2023-10-14 15:32:40"
2 |
3 | # 申万一级行业的代码与名字对照表
4 | INDUS_DICT = {
5 | k: v
6 | for k, v in zip(
7 | [
8 | "801170.SI",
9 | "801010.SI",
10 | "801140.SI",
11 | "801080.SI",
12 | "801780.SI",
13 | "801110.SI",
14 | "801230.SI",
15 | "801950.SI",
16 | "801180.SI",
17 | "801040.SI",
18 | "801740.SI",
19 | "801890.SI",
20 | "801770.SI",
21 | "801960.SI",
22 | "801200.SI",
23 | "801120.SI",
24 | "801710.SI",
25 | "801720.SI",
26 | "801880.SI",
27 | "801750.SI",
28 | "801050.SI",
29 | "801790.SI",
30 | "801150.SI",
31 | "801980.SI",
32 | "801030.SI",
33 | "801730.SI",
34 | "801160.SI",
35 | "801130.SI",
36 | "801210.SI",
37 | "801970.SI",
38 | "801760.SI",
39 | ],
40 | [
41 | "交通运输",
42 | "农林牧渔",
43 | "轻工制造",
44 | "电子",
45 | "银行",
46 | "家用电器",
47 | "综合",
48 | "煤炭",
49 | "房地产",
50 | "钢铁",
51 | "国防军工",
52 | "机械设备",
53 | "通信",
54 | "石油石化",
55 | "商贸零售",
56 | "食品饮料",
57 | "建筑材料",
58 | "建筑装饰",
59 | "汽车",
60 | "计算机",
61 | "有色金属",
62 | "非银金融",
63 | "医药生物",
64 | "美容护理",
65 | "基础化工",
66 | "电力设备",
67 | "公用事业",
68 | "纺织服饰",
69 | "社会服务",
70 | "环保",
71 | "传媒",
72 | ],
73 | )
74 | }
75 |
76 | # 常用宽基指数的代码和名字
77 | INDEX_DICT = {
78 | "000300.SH": "沪深300",
79 | "000905.SH": "中证500",
80 | "000852.SH": "中证1000",
81 | "399303.SZ": "国证2000",
82 | }
83 |
84 | # 中信一级行业代码和名字对照表
85 | ZXINDUS_DICT = {
86 | "CI005001.INDX": "石油石化",
87 | "CI005002.INDX": "煤炭",
88 | "CI005003.INDX": "有色金属",
89 | "CI005004.INDX": "电力及公用事业",
90 | "CI005005.INDX": "电力及公用事业",
91 | "CI005005.INDX": "钢铁",
92 | "CI005006.INDX": "基础化工",
93 | "CI005007.INDX": "建筑",
94 | "CI005008.INDX": "建材",
95 | "CI005009.INDX": "轻工制造",
96 | "CI005010.INDX": "机械",
97 | "CI005011.INDX": "电力设备及新能源",
98 | "CI005012.INDX": "国防军工",
99 | "CI005013.INDX": "汽车",
100 | "CI005014.INDX": "商贸零售",
101 | "CI005015.INDX": "消费者服务",
102 | "CI005016.INDX": "家电",
103 | "CI005017.INDX": "纺织服装",
104 | "CI005018.INDX": "医药",
105 | "CI005019.INDX": "食品饮料",
106 | "CI005020.INDX": "农林牧渔",
107 | "CI005021.INDX": "银行",
108 | "CI005022.INDX": "非银行金融",
109 | "CI005023.INDX": "房地产",
110 | "CI005024.INDX": "交通运输",
111 | "CI005025.INDX": "电子",
112 | "CI005026.INDX": "通信",
113 | "CI005027.INDX": "计算机",
114 | "CI005028.INDX": "传媒",
115 | "CI005029.INDX": "综合",
116 | "CI005030.INDX": "综合金融",
117 | }
118 |
--------------------------------------------------------------------------------
/docs/罗盘/数据/dicts.md:
--------------------------------------------------------------------------------
1 | ::: pure_ocean_breeze.data.dicts
2 | ## 申万一级行业的代码与名字对照表
3 | ```python
4 | INDUS_DICT = {
5 | k: v
6 | for k, v in zip(
7 | [
8 | "801170.SI",
9 | "801010.SI",
10 | "801140.SI",
11 | "801080.SI",
12 | "801780.SI",
13 | "801110.SI",
14 | "801230.SI",
15 | "801950.SI",
16 | "801180.SI",
17 | "801040.SI",
18 | "801740.SI",
19 | "801890.SI",
20 | "801770.SI",
21 | "801960.SI",
22 | "801200.SI",
23 | "801120.SI",
24 | "801710.SI",
25 | "801720.SI",
26 | "801880.SI",
27 | "801750.SI",
28 | "801050.SI",
29 | "801790.SI",
30 | "801150.SI",
31 | "801980.SI",
32 | "801030.SI",
33 | "801730.SI",
34 | "801160.SI",
35 | "801130.SI",
36 | "801210.SI",
37 | "801970.SI",
38 | "801760.SI",
39 | ],
40 | [
41 | "交通运输",
42 | "农林牧渔",
43 | "轻工制造",
44 | "电子",
45 | "银行",
46 | "家用电器",
47 | "综合",
48 | "煤炭",
49 | "房地产",
50 | "钢铁",
51 | "国防军工",
52 | "机械设备",
53 | "通信",
54 | "石油石化",
55 | "商贸零售",
56 | "食品饮料",
57 | "建筑材料",
58 | "建筑装饰",
59 | "汽车",
60 | "计算机",
61 | "有色金属",
62 | "非银金融",
63 | "医药生物",
64 | "美容护理",
65 | "基础化工",
66 | "电力设备",
67 | "公用事业",
68 | "纺织服饰",
69 | "社会服务",
70 | "环保",
71 | "传媒",
72 | ],
73 | )
74 | }
75 | ```
76 |
77 | ## 常用宽基指数的代码和名字
78 | ```python
79 | INDEX_DICT = {
80 | "000300.SH": "沪深300",
81 | "000905.SH": "中证500",
82 | "000852.SH": "中证1000",
83 | "399303.SZ": "国证2000"
84 | }
85 | ```
86 |
87 | ## 中信一级行业代码和名字对照表
88 | ```python
89 | ZXINDUS_DICT = {
90 | "CI005001.INDX": "石油石化",
91 | "CI005002.INDX": "煤炭",
92 | "CI005003.INDX": "有色金属",
93 | "CI005004.INDX": "电力及公用事业",
94 | "CI005005.INDX": "电力及公用事业",
95 | "CI005005.INDX": "钢铁",
96 | "CI005006.INDX": "基础化工",
97 | "CI005007.INDX": "建筑",
98 | "CI005008.INDX": "建材",
99 | "CI005009.INDX": "轻工制造",
100 | "CI005010.INDX": "机械",
101 | "CI005011.INDX": "电力设备及新能源",
102 | "CI005012.INDX": "国防军工",
103 | "CI005013.INDX": "汽车",
104 | "CI005014.INDX": "商贸零售",
105 | "CI005015.INDX": "消费者服务",
106 | "CI005016.INDX": "家电",
107 | "CI005017.INDX": "纺织服装",
108 | "CI005018.INDX": "医药",
109 | "CI005019.INDX": "食品饮料",
110 | "CI005020.INDX": "农林牧渔",
111 | "CI005021.INDX": "银行",
112 | "CI005022.INDX": "非银行金融",
113 | "CI005023.INDX": "房地产",
114 | "CI005024.INDX": "交通运输",
115 | "CI005025.INDX": "电子",
116 | "CI005026.INDX": "通信",
117 | "CI005027.INDX": "计算机",
118 | "CI005028.INDX": "传媒",
119 | "CI005029.INDX": "综合",
120 | "CI005030.INDX": "综合金融",
121 | }
122 | ```
123 |
124 |
--------------------------------------------------------------------------------
/docs/src/mkdocstrings/templates/python/material/children.html:
--------------------------------------------------------------------------------
1 | {{ log.debug() }}
2 | {% if obj.children %}
3 |
4 |
5 |
6 | {% if config.group_by_category %}
7 |
8 | {% with %}
9 |
10 | {% if config.show_category_heading %}
11 | {% set extra_level = 1 %}
12 | {% else %}
13 | {% set extra_level = 0 %}
14 | {% endif %}
15 |
16 | {% if config.show_category_heading and obj.attributes|any("has_contents") %}
17 | {% filter heading(heading_level, id=html_id ~ "-attributes") %}Attributes{% endfilter %}
18 | {% endif %}
19 | {% with heading_level = heading_level + extra_level %}
20 | {% for attribute in obj.attributes|sort(attribute="name") %}
21 | {% include "attribute.html" with context %}
22 | {% endfor %}
23 | {% endwith %}
24 |
25 | {% if config.show_category_heading and obj.classes|any("has_contents") %}
26 | {% filter heading(heading_level, id=html_id ~ "-classes") %}Classes{% endfilter %}
27 | {% endif %}
28 | {% with heading_level = heading_level + extra_level %}
29 | {% for class in obj.classes|sort(attribute="name") %}
30 | {% include "class.html" with context %}
31 | {% endfor %}
32 | {% endwith %}
33 |
34 | {% if config.show_category_heading and obj.functions|any("has_contents") %}
35 | {% filter heading(heading_level, id=html_id ~ "-functions") %}Functions{% endfilter %}
36 | {% endif %}
37 | {% with heading_level = heading_level + extra_level %}
38 | {% for function in obj.functions|sort(attribute="name") %}
39 | {% include "function.html" with context %}
40 | {% endfor %}
41 | {% endwith %}
42 |
43 | {% if config.show_category_heading and obj.methods|any("has_contents") %}
44 | {% filter heading(heading_level, id=html_id ~ "-methods") %}Methods{% endfilter %}
45 | {% endif %}
46 | {% with heading_level = heading_level + extra_level %}
47 | {% for method in obj.methods|sort(attribute="name") %}
48 | {% include "method.html" with context %}
49 | {% endfor %}
50 | {% endwith %}
51 |
52 | {% if config.show_category_heading and obj.modules|any("has_contents") %}
53 | {% filter heading(heading_level, id=html_id ~ "-modules") %}Modules{% endfilter %}
54 | {% endif %}
55 | {% with heading_level = heading_level + extra_level %}
56 | {% for module in obj.modules|sort(attribute="name") %}
57 | {% include "module.html" with context %}
58 | {% endfor %}
59 | {% endwith %}
60 |
61 | {% endwith %}
62 |
63 | {% else %}
64 |
65 | {% for child in obj.children|sort(attribute="name") %}
66 | {% if child.category == "attribute" %}
67 | {% with attribute = child %}
68 | {% include "attribute.html" with context %}
69 | {% endwith %}
70 |
71 | {% elif child.category == "class" %}
72 | {% with class = child %}
73 | {% include "class.html" with context %}
74 | {% endwith %}
75 |
76 | {% elif child.category == "function" %}
77 | {% with function = child %}
78 | {% include "function.html" with context %}
79 | {% endwith %}
80 |
81 | {% elif child.category == "method" %}
82 | {% with method = child %}
83 | {% include "method.html" with context %}
84 | {% endwith %}
85 |
86 | {% elif child.category == "module" %}
87 | {% with module = child %}
88 | {% include "module.html" with context %}
89 | {% endwith %}
90 |
91 | {% endif %}
92 |
93 | {% endfor %}
94 |
95 | {% endif %}
96 |
97 |
98 |
99 | {% endif %}
100 |
--------------------------------------------------------------------------------
/docs/src/mkdocstrings/loggers.py:
--------------------------------------------------------------------------------
1 | """Logging functions."""
2 |
3 | import logging
4 | from pathlib import Path
5 | from typing import Callable, Optional
6 |
7 | from jinja2 import contextfunction
8 | from jinja2.runtime import Context
9 | from mkdocs.utils import warning_filter
10 |
11 | from mkdocstrings.handlers import base
12 |
13 |
14 | class LoggerAdapter(logging.LoggerAdapter):
15 | """A logger adapter to prefix messages."""
16 |
17 | def __init__(self, prefix, logger):
18 | """
19 | Initialize the object.
20 |
21 | Arguments:
22 | prefix: The string to insert in front of every message.
23 | logger: The logger instance.
24 | """
25 | super().__init__(logger, {})
26 | self.prefix = prefix
27 |
28 | def process(self, msg, kwargs):
29 | """
30 | Process the message.
31 |
32 | Arguments:
33 | msg: The message:
34 | kwargs: Remaining arguments.
35 |
36 | Returns:
37 | The processed message.
38 | """
39 | return f"{self.prefix}: {msg}", kwargs
40 |
41 |
42 | class TemplateLogger:
43 | """
44 | A wrapper class to allow logging in templates.
45 |
46 | Attributes:
47 | debug: Function to log a DEBUG message.
48 | info: Function to log an INFO message.
49 | warning: Function to log a WARNING message.
50 | error: Function to log an ERROR message.
51 | critical: Function to log a CRITICAL message.
52 | """
53 |
54 | def __init__(self, logger: LoggerAdapter):
55 | """
56 | Initialize the object.
57 |
58 | Arguments:
59 | logger: A logger adapter.
60 | """
61 | self.debug = get_template_logger_function(logger.debug)
62 | self.info = get_template_logger_function(logger.info)
63 | self.warning = get_template_logger_function(logger.warning)
64 | self.error = get_template_logger_function(logger.error)
65 | self.critical = get_template_logger_function(logger.critical)
66 |
67 |
68 | def get_template_logger_function(logger_func: Callable) -> Callable:
69 | """
70 | Create a wrapper function that automatically receives the Jinja template context.
71 |
72 | Arguments:
73 | logger_func: The logger function to use within the wrapper.
74 |
75 | Returns:
76 | A function.
77 | """
78 |
79 | @contextfunction
80 | def wrapper(context: Context, msg: Optional[str] = None) -> str:
81 | """
82 | Log a message.
83 |
84 | Arguments:
85 | context: The template context, automatically provided by Jinja.
86 | msg: The message to log.
87 |
88 | Returns:
89 | An empty string.
90 | """
91 | template_path = get_template_path(context)
92 | logger_func(f"{template_path}: {msg or 'Rendering'}")
93 | return ""
94 |
95 | return wrapper
96 |
97 |
98 | def get_template_path(context: Context) -> str:
99 | """
100 | Return the path to the template currently using the given context.
101 |
102 | Arguments:
103 | context: The template context.
104 |
105 | Returns:
106 | The relative path to the template.
107 | """
108 | filename = context.environment.get_template(context.name).filename
109 | if filename:
110 | try:
111 | return str(Path(filename).relative_to(base.TEMPLATES_DIR))
112 | except ValueError:
113 | return filename
114 | return context.name
115 |
116 |
117 | def get_logger(name: str) -> LoggerAdapter:
118 | """
119 | Return a pre-configured logger.
120 |
121 | Arguments:
122 | name: The name to use with `logging.getLogger`.
123 |
124 | Returns:
125 | A logger configured to work well in MkDocs.
126 | """
127 | logger = logging.getLogger(f"mkdocs.plugins.{name}")
128 | logger.addFilter(warning_filter)
129 | return LoggerAdapter(name, logger)
130 |
131 |
132 | def get_template_logger() -> TemplateLogger:
133 | """
134 | Return a logger usable in templates.
135 |
136 | Returns:
137 | A template logger.
138 | """
139 | return TemplateLogger(get_logger("mkdocstrings.templates"))
140 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: pure_ocean_breeze documentation
2 | site_url: https://chen-001.github.io/pure_ocean_breeze/
3 | repo_url: https://github.com/chen-001/pure_ocean_breeze
4 | site_description: 关于量价因子的一片星空
5 | site_author: chenzongwei
6 |
7 | nav:
8 | - 星空: index.md
9 |
10 | - 罗盘:
11 |
12 | - 初始化:
13 | - initialize: 罗盘/初始化/initialize.md
14 | - 配置&参数:
15 | - state: 罗盘/配置&参数/state.md
16 | - homeplace: 罗盘/配置&参数/homeplace.md
17 | - 数据:
18 | - database: 罗盘/数据/database.md
19 | - dicts: 罗盘/数据/dicts.md
20 | - read_data: 罗盘/数据/read_data.md
21 | - write_data: 罗盘/数据/write_data.md
22 | - tools: 罗盘/数据/tools.md
23 | - 加工&测试&评价:
24 | - process: 罗盘/加工&测试&评价/process.md
25 | - comment: 罗盘/加工&测试&评价/comment.md
26 | - 通讯:
27 | - mail: 罗盘/通讯/mail.md
28 |
29 | - 火把:
30 | - 项目: 火把/project.md
31 | - 构想&未来: 火把/futures.md
32 | - 分享: 火把/shares.md
33 | - 作者: 火把/author.md
34 |
35 | # theme:
36 | # name: material
37 | # language: 'zh'
38 | # # locale: zh_CN
39 | # analytics: {gtag: 'G-274394082'}
40 | # markdown_extensions:
41 | # - pymdownx.arithmatex:
42 | # generic: true
43 | extra_javascript:
44 | - https://polyfill.io/v3/polyfill.min.js?features=es6
45 | - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
46 | - https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@3.0.8/js/maodian.js
47 | - js/baidu.js
48 | - https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-MML-AM_CHTML
49 |
50 |
51 | # extra:
52 | # search:
53 | # language: 'jp'
54 |
55 | plugins:
56 | - autorefs
57 | - mkdocstrings:
58 | handlers:
59 | python:
60 | setup_commands:
61 | - import sys
62 | - sys.path.append("docs")
63 | selection:
64 | new_path_syntax: yes
65 | watch:
66 | - docs/src/mkdocstrings
67 | - search:
68 | lang:
69 | - en
70 | - ja
71 | separator: '[\s\-\.]+'
72 |
73 | extra_css:
74 | - css/mkdocstrings.css
75 |
76 | theme:
77 | name: material
78 | # custom_dir: overrides
79 | logo: pics/一身本事.png
80 | palette:
81 | - scheme: default # 日间模式
82 | primary: indigo # 上方的
83 | accent: red # 链接等可交互元件的高亮色
84 | toggle:
85 | icon: material/weather-night # 图标
86 | name: 切换至夜间模式 # 鼠标悬浮提示
87 | - scheme: slate # 夜间模式
88 | primary: teal
89 | accent: red
90 | toggle:
91 | icon: material/weather-sunny
92 | name: 切换至日间模式
93 | features:
94 | - content.code.annotate # 代码块内的注释
95 | - navigation.tabs # 使用Tab来进行分类
96 | - navigation.top # 返回顶部的按钮 在上滑时出现
97 | - navigation.indexes # Tab会有一个index.md 而不是在打开Tab时打开第一篇文章
98 | - navigation.expand # 打开Tab时左侧目录全部展开
99 | - search.suggest # 搜索输入一些字母时推荐补全整个单词
100 | - search.highlight # 搜索出的文章关键词加入高亮
101 | language: zh # 一些提示性的文字会变成中文
102 | # icon:
103 | # repo: fontawesome/brands/github
104 |
105 | edit_uri: "" # 编辑按钮跳转的链接
106 |
107 | copyright: Copyright © 2022 chenzongwei # 左下角的版权声明
108 |
109 | extra:
110 | analytics:
111 | provider: google
112 | property: G-8R9VRSN3S0
113 | social: # icons
114 | - icon: fontawesome/brands/github
115 | link: https://github.com/chen-001/pure_ocean_breeze
116 | name: GitHub项目主页
117 | - icon: fontawesome/brands/python
118 | link: https://pypi.org/project/pure-ocean-breeze/
119 | name: Pypi项目主页
120 | search:
121 | language: 'jp'
122 |
123 |
124 |
125 | markdown_extensions:
126 | - md_in_html
127 | - attr_list # 给图片后面添加{width="300"}设置大小
128 | - pymdownx.arithmatex: # latex支持
129 | generic: true
130 | - footnotes
131 | - toc:
132 | permalink: true # 固定标题位置为当前位置
133 | toc_depth: 3
134 | - pymdownx.highlight: # 代码块高亮
135 | linenums: true # 显示行号
136 | auto_title: true # 显示编程语言名称
137 | - admonition
138 | - pymdownx.details
139 | - pymdownx.superfences:
140 | custom_fences:
141 | - name: mermaid
142 | class: mermaid
143 | # format: !!python/name:pymdownx.superfences.fence_code_format
144 | - pymdownx.tabbed:
145 | alternate_style: true
146 | - meta # 支持Markdown文件上方自定义标题标签等
147 |
148 |
--------------------------------------------------------------------------------
/pure_ocean_breeze/jason/data/database.py:
--------------------------------------------------------------------------------
1 | __updated__ = "2025-02-26 15:30:49"
2 |
3 | import pandas as pd
4 | import psycopg2 as pg
5 | import numpy as np
6 | from typing import Union
7 | from tenacity import retry, stop_after_attempt, wait_fixed
8 | import questdb.ingress as qdbing
9 |
10 |
11 | class Questdb(object):
12 | """Questdb数据库连接和操作类
13 |
14 | Questdb的写入方式都为追加,因此如果想replace之前的数据,请手动删除表格
15 | Questdb的web console为127.0.0.1:9000,作者已经修改为127.0.0.1:9001
16 | """
17 |
18 | def __init__(
19 | self,
20 | user: str = "admin",
21 | password: str = "quest",
22 | host: str = "192.168.200.60",
23 | port: str = "8812",
24 | database: str = "qdb",
25 | ) -> None:
26 | """初始化Questdb连接参数
27 |
28 | Parameters
29 | ----------
30 | user : str, optional
31 | 用户名, by default "admin"
32 | password : str, optional
33 | 密码, by default "quest"
34 | host : str, optional
35 | 地址, by default "192.168.200.60"
36 | port : str, optional
37 | 端口, by default "8812"
38 | database : str, optional
39 | 数据库, by default "qdb"
40 | web_port : str, optional
41 | questdb控制台的端口号,安装questdb软件时默认为9000,本库默认为9000, by default 9000
42 | """
43 | self.user = user
44 | self.password = password
45 | self.host = host
46 | self.port = port
47 | self.database = database
48 |
49 | def connect(self):
50 | """连接数据库
51 |
52 | Returns
53 | -------
54 | connection
55 | 数据库连接
56 | """
57 | conn = pg.connect(
58 | user=self.user,
59 | password=self.password,
60 | host=self.host,
61 | port=self.port,
62 | database=self.database,
63 | )
64 | return conn
65 |
66 | def do_order(self, sql_order: str) -> any:
67 | """执行任意一句sql语句
68 |
69 | Parameters
70 | ----------
71 | sql_order : str
72 | sql命令
73 |
74 | Returns
75 | -------
76 | any
77 | 返回结果
78 | """
79 | conn = self.connect()
80 | cur = conn.cursor()
81 | return cur.execute(sql_order)
82 |
83 | @retry(stop=stop_after_attempt(10))
84 | def get_data(
85 | self, sql_order: str, only_array: bool = 0
86 | ) -> Union[pd.DataFrame, np.ndarray]:
87 | """以sql命令的方式,从数据库中读取数据
88 |
89 | Parameters
90 | ----------
91 | sql_order : str
92 | sql命令
93 | only_array : bool, optional
94 | 是否只返回数组, by default 0
95 |
96 | Returns
97 | -------
98 | Union[pd.DataFrame, np.ndarray]
99 | 读取的结果
100 | """
101 | conn = self.connect()
102 | cursor = conn.cursor()
103 | cursor.execute(sql_order)
104 | df_data = cursor.fetchall()
105 | if not only_array:
106 | columns = [i[0] for i in cursor.description]
107 | df = pd.DataFrame(df_data, columns=columns)
108 | return df
109 | else:
110 | return np.array(df_data)
111 |
112 | @retry(stop=stop_after_attempt(3000), wait=wait_fixed(0.01))
113 | def write_via_df(
114 | self,
115 | df: pd.DataFrame,
116 | table_name: str,
117 | symbols: Union[str, bool, list[int], list[str]] = None,
118 | tuple_col: Union[str, list[str]] = None,
119 | ) -> None:
120 | """通过questdb的python库直接将dataframe写入quested数据库
121 |
122 | Parameters
123 | ----------
124 | df : pd.DataFrame
125 | 要写入的dataframe
126 | table_name : str
127 | questdb中该表的表名
128 | symbols : Union[str, bool, list[int], list[str]], optional
129 | 为symbols的那些列的名称, by default None
130 | tuple_col : Union[str, list[str]], optional
131 | 数据类型为tuple或list的列的名字, by default None
132 | """
133 | if tuple_col is None:
134 | ...
135 | elif isinstance(tuple_col, str):
136 | df[tuple_col] = df[tuple_col].apply(str)
137 | else:
138 | for t in tuple_col:
139 | df[t] = df[t].apply(str)
140 |
141 | if symbols is not None:
142 | with qdbing.Sender(self.host, 9009) as sender:
143 | sender.dataframe(df, table_name=table_name, symbols=symbols)
144 | else:
145 | with qdbing.Sender(self.host, 9009) as sender:
146 | sender.dataframe(df, table_name=table_name)
147 |
148 |
149 |
150 |
151 |
--------------------------------------------------------------------------------
/pure_ocean_breeze/jason/labor/comment.py:
--------------------------------------------------------------------------------
1 | __updated__ = "2025-06-25 20:30:28"
2 |
3 | import numpy as np
4 | import pandas as pd
5 | import matplotlib as mpl
6 |
7 | mpl.rcParams.update(mpl.rcParamsDefault)
8 | import matplotlib.pyplot as plt
9 |
10 | plt.rcParams["axes.unicode_minus"] = False
11 | from typing import Tuple
12 | from pure_ocean_breeze.jason.data.read_data import (
13 | read_index,
14 | )
15 | from pure_ocean_breeze.jason.state.decorators import do_on_dfs
16 | from pure_ocean_breeze.jason.state.states import STATES
17 |
18 |
19 |
20 | def comments_on_twins(
21 | nets: pd.Series, rets: pd.Series, counts_one_year: int = 50
22 | ) -> pd.Series:
23 | """输入月频的收益率序列和净值序列,给出评价
24 | 评价指标包括年化收益率、总收益率、年化波动率、年化夏普比率、最大回撤率、胜率
25 | 输入2个pd.Series,时间是索引
26 |
27 | Parameters
28 | ----------
29 | nets : pd.Series
30 | 净值序列,index为时间
31 | rets : pd.Series
32 | 收益率序列,index为时间
33 | counts_one_year : int
34 | 一年内有多少次交易, by default 50
35 |
36 | Returns
37 | -------
38 | `pd.Series`
39 | 包含年化收益率、总收益率、年化波动率、年化夏普比率、最大回撤率、胜率的评价指标
40 | """
41 | series = nets.copy()
42 | series1 = rets.copy()
43 | duration = (series.index[-1] - series.index[0]).days
44 | year = duration / 365
45 | ret_yearly = series.iloc[-1] / year
46 | max_draw = -((series+1) / (series+1).expanding(1).max()-1).min()
47 | vol = np.std(series1) * (counts_one_year**0.5)
48 | sharpe = ret_yearly / vol
49 | wins = series1[series1 > 0]
50 | win_rate = len(wins) / len(series1)
51 | return pd.Series(
52 | [series.iloc[-1], ret_yearly, vol, sharpe, win_rate, max_draw],
53 | index=["总收益率", "年化收益率", "年化波动率", "信息比率", "胜率", "最大回撤率"],
54 | )
55 |
56 |
57 | def comments_on_twins_periods(
58 | nets: pd.Series, rets: pd.Series, periods: int
59 | ) -> pd.Series:
60 | """输入其他频率的收益率序列和净值序列,给出评价
61 | 评价指标包括年化收益率、总收益率、年化波动率、年化夏普比率、最大回撤率、胜率
62 | 输入2个pd.Series,时间是索引
63 |
64 | Parameters
65 | ----------
66 | nets : pd.Series
67 | 净值序列,index为时间
68 | rets : pd.Series
69 | 收益率序列,index为时间
70 | periods : int
71 | 收益率序列的频率,如5天一次,则为5
72 |
73 | Returns
74 | -------
75 | `pd.Series`
76 | 包含年化收益率、总收益率、年化波动率、年化夏普比率、最大回撤率、胜率的评价指标
77 | """
78 | series = nets.copy()
79 | series1 = rets.copy()
80 | duration = (series.index[-1] - series.index[0]).days
81 | year = duration / 365
82 | ret_yearly = series.iloc[-1] / year
83 | max_draw = -((series+1) / (series+1).expanding(1).max()-1).min()
84 | vol = np.std(series1) * (252**0.5) * (periods**0.5)
85 | sharpe = ret_yearly / vol
86 | wins = series1[series1 > 0]
87 | win_rate = len(wins) / len(series1)
88 | return pd.Series(
89 | [series.iloc[-1], ret_yearly, vol, sharpe, win_rate, max_draw],
90 | index=["总收益率", "年化收益率", "年化波动率", "信息比率", "胜率", "最大回撤率"],
91 | )
92 |
93 |
94 | @do_on_dfs
95 | def make_relative_comments(
96 | ret_fac: pd.Series,
97 | hs300: bool = 0,
98 | zz500: bool = 0,
99 | zz1000: bool = 0,
100 | day: int = STATES['START'],
101 | show_nets: bool = 0,
102 | ) -> pd.Series:
103 | """对于一个给定的收益率序列,计算其相对于某个指数的超额表现
104 |
105 | Parameters
106 | ----------
107 | ret_fac : pd.Series
108 | 给定的收益率序列,index为时间
109 | hs300 : bool, optional
110 | 为1则相对沪深300指数行情, by default 0
111 | zz500 : bool, optional
112 | 为1则相对中证500指数行情, by default 0
113 | zz1000 : bool, optional
114 | 为1则相对中证1000指数行情, by default 0
115 | day : int, optional
116 | 起始日期,形如20130101, by default STATES['START']
117 | show_nets : bool, optional
118 | 返回值中包括超额净值数据, by default 0
119 |
120 | Returns
121 | -------
122 | `pd.Series`
123 | 评价指标包括年化收益率、总收益率、年化波动率、年化夏普比率、最大回撤率、胜率
124 |
125 | Raises
126 | ------
127 | `IOError`
128 | 如果没指定任何一个指数,将报错
129 | """
130 |
131 | if hs300:
132 | net_index = read_index(close=1,hs300=1,every_stock=0,start=day).resample("W").last()
133 | if zz500:
134 | net_index = read_index(close=1,zz500=1,every_stock=0,start=day).resample("W").last()
135 | if zz1000:
136 | net_index = read_index(close=1,zz1000=1,every_stock=0,start=day).resample("W").last()
137 | if (hs300 + zz500 + zz1000) == 0:
138 | raise IOError("你总得指定一个股票池吧?")
139 | ret_index = net_index.pct_change()
140 | if day is not None:
141 | ret_index = ret_index[ret_index.index >= pd.Timestamp(day)]
142 | ret = ret_fac - ret_index
143 | ret = ret.dropna()
144 | net = ret.cumsum()
145 | rtop = pd.Series(0, index=[net.index.min() - pd.DateOffset(weeks=1)])
146 | net = pd.concat([rtop, net]).resample("W").last()
147 | ret = pd.concat([rtop, ret]).resample("W").last()
148 | com = comments_on_twins(net, ret)
149 | if show_nets:
150 | return com, net
151 | else:
152 | return com
153 |
154 |
155 | @do_on_dfs
156 | def make_relative_comments_plot(
157 | ret_fac: pd.Series,
158 | hs300: bool = 0,
159 | zz500: bool = 0,
160 | zz1000: bool = 0,
161 | day: int = STATES['START'],
162 | ) -> pd.Series:
163 | """对于一个给定的收益率序列,计算其相对于某个指数的超额表现,然后绘图,并返回超额净值序列
164 |
165 | Parameters
166 | ----------
167 | ret_fac : pd.Series
168 | 给定的收益率序列,index为时间
169 | hs300 : bool, optional
170 | 为1则相对沪深300指数行情, by default 0
171 | zz500 : bool, optional
172 | 为1则相对中证500指数行情, by default 0
173 | zz1000 : bool, optional
174 | 为1则相对中证1000指数行情, by default 0
175 | day : int, optional
176 | 起始日期,形如20130101, by default STATES['START']
177 |
178 | Returns
179 | -------
180 | `pd.Series`
181 | 超额净值序列
182 |
183 | Raises
184 | ------
185 | `IOError`
186 | 如果没指定任何一个指数,将报错
187 | """
188 | if hs300:
189 | net_index = read_index(close=1,hs300=1,every_stock=0,start=day).resample("W").last()
190 | if zz500:
191 | net_index = read_index(close=1,zz500=1,every_stock=0,start=day).resample("W").last()
192 | if zz1000:
193 | net_index = read_index(close=1,zz1000=1,every_stock=0,start=day).resample("W").last()
194 | if (hs300 + zz500 + zz1000) == 0:
195 | raise IOError("你总得指定一个股票池吧?")
196 | ret_index = net_index.pct_change()
197 | if day is not None:
198 | ret_index = ret_index[ret_index.index >= pd.Timestamp(day)]
199 | ret = ret_fac - ret_index
200 | ret = ret.dropna()
201 | net = ret.cumsum()
202 | rtop = pd.Series(0, index=[net.index.min() - pd.DateOffset(weeks=1)])
203 | net = pd.concat([rtop, net]).resample("W").last().ffill()
204 | net.iplot()
205 | return net
206 |
--------------------------------------------------------------------------------
/docs/src/mkdocs_autorefs/references.py:
--------------------------------------------------------------------------------
1 | """Cross-references module."""
2 |
3 | import re
4 | from html import escape, unescape
5 | from typing import Any, Callable, List, Match, Tuple, Union
6 | from xml.etree.ElementTree import Element
7 |
8 | from markdown import Markdown
9 | from markdown.extensions import Extension
10 | from markdown.inlinepatterns import REFERENCE_RE, ReferenceInlineProcessor
11 |
12 | AUTO_REF_RE = re.compile(r'[^"<>]*)\1>(?P.*?)')
13 | """
14 | A regular expression to match mkdocstrings' special reference markers
15 | in the [`on_post_page` hook][mkdocs_autorefs.plugin.AutorefsPlugin.on_post_page].
16 | """
17 |
18 | EvalIDType = Tuple[Any, Any, Any]
19 |
20 |
21 | class AutoRefInlineProcessor(ReferenceInlineProcessor):
22 | """A Markdown extension."""
23 |
24 | def __init__(self, *args, **kwargs): # noqa: D107
25 | super().__init__(REFERENCE_RE, *args, **kwargs)
26 |
27 | # Code based on
28 | # https://github.com/Python-Markdown/markdown/blob/8e7528fa5c98bf4652deb13206d6e6241d61630b/markdown/inlinepatterns.py#L780
29 |
30 | def handleMatch(self, m, data) -> Union[Element, EvalIDType]: # noqa: N802 (parent's casing)
31 | """
32 | Handle an element that matched.
33 |
34 | Arguments:
35 | m: The match object.
36 | data: The matched data.
37 |
38 | Returns:
39 | A new element or a tuple.
40 | """
41 | text, index, handled = self.getText(data, m.end(0))
42 | if not handled:
43 | return None, None, None
44 |
45 | identifier, end, handled = self.evalId(data, index, text)
46 | if not handled:
47 | return None, None, None
48 |
49 | if re.search(r"[/ \x00-\x1f]", identifier): # type: ignore
50 | # Do nothing if the matched reference contains:
51 | # - a space, slash or control character (considered unintended);
52 | # - specifically \x01 is used by Python-Markdown HTML stash when there's inline formatting,
53 | # but references with Markdown formatting are not possible anyway.
54 | return None, m.start(0), end
55 |
56 | return self.makeTag(identifier, text), m.start(0), end
57 |
58 | def evalId(self, data: str, index: int, text: str) -> EvalIDType: # noqa: N802 (parent's casing)
59 | """
60 | Evaluate the id portion of `[ref][id]`.
61 |
62 | If `[ref][]` use `[ref]`.
63 |
64 | Arguments:
65 | data: The data to evaluate.
66 | index: The starting position.
67 | text: The text to use when no identifier.
68 |
69 | Returns:
70 | A tuple containing the identifier, its end position, and whether it matched.
71 | """
72 | m = self.RE_LINK.match(data, pos=index)
73 | if not m:
74 | return None, index, False
75 | identifier = m.group(1) or text
76 | end = m.end(0)
77 | return identifier, end, True
78 |
79 | def makeTag(self, identifier: str, text: str) -> Element: # noqa: N802,W0221 (parent's casing, different params)
80 | """
81 | Create a tag that can be matched by `AUTO_REF_RE`.
82 |
83 | Arguments:
84 | identifier: The identifier to use in the HTML property.
85 | text: The text to use in the HTML tag.
86 |
87 | Returns:
88 | A new element.
89 | """
90 | el = Element("span")
91 | el.set("data-mkdocstrings-identifier", identifier)
92 | el.text = text
93 | return el
94 |
95 |
96 | def relative_url(url_a: str, url_b: str) -> str:
97 | """
98 | Compute the relative path from URL A to URL B.
99 |
100 | Arguments:
101 | url_a: URL A.
102 | url_b: URL B.
103 |
104 | Returns:
105 | The relative URL to go from A to B.
106 | """
107 | parts_a = url_a.split("/")
108 | url_b, anchor = url_b.split("#", 1)
109 | parts_b = url_b.split("/")
110 |
111 | # remove common left parts
112 | while parts_a and parts_b and parts_a[0] == parts_b[0]:
113 | parts_a.pop(0)
114 | parts_b.pop(0)
115 |
116 | # go up as many times as remaining a parts' depth
117 | levels = len(parts_a) - 1
118 | parts_relative = [".."] * levels + parts_b
119 | relative = "/".join(parts_relative)
120 | return f"{relative}#{anchor}"
121 |
122 |
123 | def fix_ref(url_mapper: Callable[[str], str], from_url: str, unmapped: List[str]) -> Callable:
124 | """
125 | Return a `repl` function for [`re.sub`](https://docs.python.org/3/library/re.html#re.sub).
126 |
127 | In our context, we match Markdown references and replace them with HTML links.
128 |
129 | When the matched reference's identifier was not mapped to an URL, we append the identifier to the outer
130 | `unmapped` list. It generally means the user is trying to cross-reference an object that was not collected
131 | and rendered, making it impossible to link to it. We catch this exception in the caller to issue a warning.
132 |
133 | Arguments:
134 | url_mapper: A callable that gets an object's site URL by its identifier,
135 | such as [mkdocs_autorefs.plugin.AutorefsPlugin.get_item_url][].
136 | from_url: The URL of the base page, from which we link towards the targeted pages.
137 | unmapped: A list to store unmapped identifiers.
138 |
139 | Returns:
140 | The actual function accepting a [`Match` object](https://docs.python.org/3/library/re.html#match-objects)
141 | and returning the replacement strings.
142 | """
143 |
144 | def inner(match: Match):
145 | identifier = match["identifier"]
146 | title = match["title"]
147 |
148 | try:
149 | url = relative_url(from_url, url_mapper(unescape(identifier)))
150 | except KeyError:
151 | unmapped.append(identifier)
152 | if title == identifier:
153 | return f"[{identifier}][]"
154 | return f"[{title}][{identifier}]"
155 |
156 | return f'{title}'
157 |
158 | return inner
159 |
160 |
161 | def fix_refs(
162 | html: str,
163 | from_url: str,
164 | url_mapper: Callable[[str], str],
165 | ) -> Tuple[str, List[str]]:
166 | """
167 | Fix all references in the given HTML text.
168 |
169 | Arguments:
170 | html: The text to fix.
171 | from_url: The URL at which this HTML is served.
172 | url_mapper: A callable that gets an object's site URL by its identifier,
173 | such as [mkdocs_autorefs.plugin.AutorefsPlugin.get_item_url][].
174 |
175 | Returns:
176 | The fixed HTML.
177 | """
178 | unmapped = [] # type: ignore
179 | html = AUTO_REF_RE.sub(fix_ref(url_mapper, from_url, unmapped), html)
180 | return html, unmapped
181 |
182 |
183 | class AutorefsExtension(Extension):
184 | """Extension that inserts auto-references in Markdown."""
185 |
186 | def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent method's name)
187 | """
188 | Register the extension.
189 |
190 | Add an instance of our [`AutoRefInlineProcessor`][mkdocs_autorefs.references.AutoRefInlineProcessor] to the Markdown parser.
191 |
192 | Arguments:
193 | md: A `markdown.Markdown` instance.
194 | """
195 | md.inlinePatterns.register(
196 | AutoRefInlineProcessor(md),
197 | "mkdocstrings",
198 | priority=168, # Right after markdown.inlinepatterns.ReferenceInlineProcessor
199 | )
200 |
--------------------------------------------------------------------------------
/docs/src/mkdocs_autorefs/plugin.py:
--------------------------------------------------------------------------------
1 | """
2 | This module contains the "mkdocs-autorefs" plugin.
3 |
4 | After each page is processed by the Markdown converter, this plugin stores absolute URLs of every HTML anchors
5 | it finds to later be able to fix unresolved references.
6 | It stores them during the [`on_page_content` event hook](https://www.mkdocs.org/user-guide/plugins/#on_page_content).
7 |
8 | Just before writing the final HTML to the disc, during the
9 | [`on_post_page` event hook](https://www.mkdocs.org/user-guide/plugins/#on_post_page),
10 | this plugin searches for references of the form `[identifier][]` or `[title][identifier]` that were not resolved,
11 | and fixes them using the previously stored identifier-URL mapping.
12 | """
13 |
14 | import logging
15 | from typing import Callable, Dict, Optional
16 |
17 | from mkdocs.config import Config
18 | from mkdocs.plugins import BasePlugin
19 | from mkdocs.structure.pages import Page
20 | from mkdocs.structure.toc import AnchorLink
21 | from mkdocs.utils import warning_filter
22 |
23 | from mkdocs_autorefs.references import AutorefsExtension, fix_refs
24 |
25 | log = logging.getLogger(f"mkdocs.plugins.{__name__}")
26 | log.addFilter(warning_filter)
27 |
28 |
29 | class AutorefsPlugin(BasePlugin):
30 | """
31 | An `mkdocs` plugin.
32 |
33 | This plugin defines the following event hooks:
34 |
35 | - `on_config`
36 | - `on_page_content`
37 | - `on_post_page`
38 |
39 | Check the [Developing Plugins](https://www.mkdocs.org/user-guide/plugins/#developing-plugins) page of `mkdocs`
40 | for more information about its plugin system.
41 | """
42 |
43 | scan_toc: bool = True
44 | current_page: Optional[str] = None
45 |
46 | def __init__(self) -> None:
47 | """Initialize the object."""
48 | super().__init__()
49 | self._url_map: Dict[str, str] = {}
50 | self.get_fallback_anchor: Callable[[str], Optional[str]] = lambda identifier: None
51 |
52 | def register_anchor(self, page: str, anchor: str):
53 | """
54 | Register that an anchor corresponding to an identifier was encountered when rendering the page.
55 |
56 | Arguments:
57 | page: The relative URL of the current page. Examples: `'foo/bar/'`, `'foo/index.html'`
58 | anchor: The HTML anchor (without '#') as a string.
59 | """
60 | self._url_map[anchor] = f"{page}#{anchor}"
61 |
62 | def get_item_url(self, anchor: str) -> str:
63 | """
64 | Return a site-relative URL with anchor to the identifier, if it's present anywhere.
65 |
66 | Arguments:
67 | anchor: The identifier (one that [collect][mkdocstrings.handlers.base.BaseCollector.collect] can accept).
68 |
69 | Returns:
70 | A site-relative URL.
71 |
72 | Raises:
73 | KeyError: If there isn't an item by this identifier anywhere on the site.
74 | """
75 | try:
76 | return self._url_map[anchor]
77 | except KeyError:
78 | new_anchor = self.get_fallback_anchor(anchor)
79 | if new_anchor and new_anchor in self._url_map:
80 | return self._url_map[new_anchor]
81 | raise
82 |
83 | def on_config(self, config: Config, **kwargs) -> Config: # noqa: W0613,R0201 (unused arguments, cannot be static)
84 | """
85 | Instantiate our Markdown extension.
86 |
87 | Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config).
88 | In this hook, we instantiate our [`AutorefsExtension`][mkdocs_autorefs.references.AutorefsExtension]
89 | and add it to the list of Markdown extensions used by `mkdocs`.
90 |
91 | Arguments:
92 | config: The MkDocs config object.
93 | kwargs: Additional arguments passed by MkDocs.
94 |
95 | Returns:
96 | The modified config.
97 | """
98 | log.debug("Adding AutorefsExtension to the list")
99 | config["markdown_extensions"].append(AutorefsExtension())
100 | return config
101 |
102 | def on_page_markdown(self, markdown: str, page: Page, **kwargs) -> str: # noqa: W0613 (unused arguments)
103 | """
104 | Remember which page is the current one.
105 |
106 | Arguments:
107 | markdown: Input Markdown.
108 | page: The related MkDocs page instance.
109 | kwargs: Additional arguments passed by MkDocs.
110 |
111 | Returns:
112 | The same Markdown. We only use this hook to map anchors to URLs.
113 | """
114 | self.current_page = page.url
115 | return markdown
116 |
117 | def on_page_content(self, html: str, page: Page, **kwargs) -> str: # noqa: W0613 (unused arguments)
118 | """
119 | Map anchors to URLs.
120 |
121 | Hook for the [`on_page_content` event](https://www.mkdocs.org/user-guide/plugins/#on_page_content).
122 | In this hook, we map the IDs of every anchor found in the table of contents to the anchors absolute URLs.
123 | This mapping will be used later to fix unresolved reference of the form `[title][identifier]` or
124 | `[identifier][]`.
125 |
126 | Arguments:
127 | html: HTML converted from Markdown.
128 | page: The related MkDocs page instance.
129 | kwargs: Additional arguments passed by MkDocs.
130 |
131 | Returns:
132 | The same HTML. We only use this hook to map anchors to URLs.
133 | """
134 | if self.scan_toc:
135 | log.debug(f"Mapping identifiers to URLs for page {page.file.src_path}")
136 | for item in page.toc.items:
137 | self.map_urls(page.url, item)
138 | return html
139 |
140 | def map_urls(self, base_url: str, anchor: AnchorLink) -> None:
141 | """
142 | Recurse on every anchor to map its ID to its absolute URL.
143 |
144 | This method populates `self.url_map` by side-effect.
145 |
146 | Arguments:
147 | base_url: The base URL to use as a prefix for each anchor's relative URL.
148 | anchor: The anchor to process and to recurse on.
149 | """
150 | self.register_anchor(base_url, anchor.id)
151 | for child in anchor.children:
152 | self.map_urls(base_url, child)
153 |
154 | def on_post_page(self, output: str, page: Page, **kwargs) -> str: # noqa: W0613 (unused arguments)
155 | """
156 | Fix cross-references.
157 |
158 | Hook for the [`on_post_page` event](https://www.mkdocs.org/user-guide/plugins/#on_post_page).
159 | In this hook, we try to fix unresolved references of the form `[title][identifier]` or `[identifier][]`.
160 | Doing that allows the user of `mkdocstrings` to cross-reference objects in their documentation strings.
161 | It uses the native Markdown syntax so it's easy to remember and use.
162 |
163 | We log a warning for each reference that we couldn't map to an URL, but try to be smart and ignore identifiers
164 | that do not look legitimate (sometimes documentation can contain strings matching
165 | our [`AUTO_REF_RE`][mkdocs_autorefs.references.AUTO_REF_RE] regular expression that did not intend to reference anything).
166 | We currently ignore references when their identifier contains a space or a slash.
167 |
168 | Arguments:
169 | output: HTML converted from Markdown.
170 | page: The related MkDocs page instance.
171 | kwargs: Additional arguments passed by MkDocs.
172 |
173 | Returns:
174 | Modified HTML.
175 | """
176 | log.debug(f"Fixing references in page {page.file.src_path}")
177 |
178 | fixed_output, unmapped = fix_refs(output, page.url, self.get_item_url)
179 |
180 | if unmapped and log.isEnabledFor(logging.WARNING):
181 | for ref in unmapped:
182 | log.warning(
183 | f"{page.file.src_path}: Could not find cross-reference target '[{ref}]'",
184 | )
185 |
186 | return fixed_output
187 |
--------------------------------------------------------------------------------
/docs/src/mkdocstrings/plugin.py:
--------------------------------------------------------------------------------
1 | """
2 | This module contains the "mkdocstrings" plugin for MkDocs.
3 |
4 | The plugin instantiates a Markdown extension ([`MkdocstringsExtension`][mkdocstrings.extension.MkdocstringsExtension]),
5 | and adds it to the list of Markdown extensions used by `mkdocs`
6 | during the [`on_config` event hook](https://www.mkdocs.org/user-guide/plugins/#on_config).
7 |
8 | Once the documentation is built, the [`on_post_build` event hook](https://www.mkdocs.org/user-guide/plugins/#on_post_build)
9 | is triggered and calls the [`handlers.teardown()` method][mkdocstrings.handlers.base.Handlers.teardown]. This method is
10 | used to teardown the handlers that were instantiated during documentation buildup.
11 |
12 | Finally, when serving the documentation, it can add directories to watch
13 | during the [`on_serve` event hook](https://www.mkdocs.org/user-guide/plugins/#on_serve).
14 | """
15 |
16 | import os
17 | from typing import Callable, Optional, Tuple
18 |
19 | from livereload import Server
20 | from mkdocs.config import Config
21 | from mkdocs.config.config_options import Type as MkType
22 | from mkdocs.plugins import BasePlugin
23 | from mkdocs.utils import write_file
24 |
25 | from mkdocs_autorefs.plugin import AutorefsPlugin
26 | from mkdocstrings.extension import MkdocstringsExtension
27 | from mkdocstrings.handlers.base import BaseHandler, Handlers
28 | from mkdocstrings.loggers import get_logger
29 |
30 | log = get_logger(__name__)
31 |
32 | SELECTION_OPTS_KEY: str = "selection"
33 | """The name of the selection parameter in YAML configuration blocks."""
34 | RENDERING_OPTS_KEY: str = "rendering"
35 | """The name of the rendering parameter in YAML configuration blocks."""
36 |
37 |
38 | class MkdocstringsPlugin(BasePlugin):
39 | """
40 | An `mkdocs` plugin.
41 |
42 | This plugin defines the following event hooks:
43 |
44 | - `on_config`
45 | - `on_post_build`
46 | - `on_serve`
47 |
48 | Check the [Developing Plugins](https://www.mkdocs.org/user-guide/plugins/#developing-plugins) page of `mkdocs`
49 | for more information about its plugin system.
50 | """
51 |
52 | config_scheme: Tuple[Tuple[str, MkType]] = (
53 | ("watch", MkType(list, default=[])), # type: ignore
54 | ("handlers", MkType(dict, default={})),
55 | ("default_handler", MkType(str, default="python")),
56 | ("custom_templates", MkType(str, default=None)),
57 | )
58 | """
59 | The configuration options of `mkdocstrings`, written in `mkdocs.yml`.
60 |
61 | Available options are:
62 |
63 | - __`watch`__: A list of directories to watch. Only used when serving the documentation with mkdocs.
64 | Whenever a file changes in one of directories, the whole documentation is built again, and the browser refreshed.
65 | - __`default_handler`__: The default handler to use. The value is the name of the handler module. Default is "python".
66 | - __`handlers`__: Global configuration of handlers. You can set global configuration per handler, applied everywhere,
67 | but overridable in each "autodoc" instruction. Example:
68 |
69 | ```yaml
70 | plugins:
71 | - mkdocstrings:
72 | handlers:
73 | python:
74 | selection:
75 | selection_opt: true
76 | rendering:
77 | rendering_opt: "value"
78 | setup_commands:
79 | - "import os"
80 | - "import django"
81 | - "os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'my_djang_app.settings')"
82 | - "django.setup()"
83 | rust:
84 | selection:
85 | selection_opt: 2
86 | ```
87 | """
88 |
89 | css_filename = "assets/_mkdocstrings.css"
90 |
91 | def __init__(self) -> None:
92 | """Initialize the object."""
93 | super().__init__()
94 | self._handlers: Optional[Handlers] = None
95 |
96 | @property
97 | def handlers(self) -> Handlers:
98 | """
99 | Get the instance of [mkdocstrings.handlers.base.Handlers][] for this plugin/build.
100 |
101 | Raises:
102 | RuntimeError: If the plugin hasn't been initialized with a config.
103 |
104 | Returns:
105 | An instance of [mkdocstrings.handlers.base.Handlers][] (the same throughout the build).
106 | """
107 | if not self._handlers:
108 | raise RuntimeError("The plugin hasn't been initialized with a config yet")
109 | return self._handlers
110 |
111 | def on_serve(self, server: Server, builder: Callable = None, **kwargs) -> Server: # noqa: W0613 (unused arguments)
112 | """
113 | Watch directories.
114 |
115 | Hook for the [`on_serve` event](https://www.mkdocs.org/user-guide/plugins/#on_serve).
116 | In this hook, we add the directories specified in the plugin's configuration to the list of directories
117 | watched by `mkdocs`. Whenever a change occurs in one of these directories, the documentation is built again
118 | and the site reloaded.
119 |
120 | Arguments:
121 | server: The `livereload` server instance.
122 | builder: The function to build the site.
123 | kwargs: Additional arguments passed by MkDocs.
124 |
125 | Returns:
126 | The server instance.
127 | """
128 | if builder is None:
129 | # The builder parameter was added in mkdocs v1.1.1.
130 | # See issue https://github.com/mkdocs/mkdocs/issues/1952.
131 | builder = list(server.watcher._tasks.values())[0]["func"] # noqa: W0212 (protected member)
132 | for element in self.config["watch"]:
133 | log.debug(f"Adding directory '{element}' to watcher")
134 | server.watch(element, builder)
135 | return server
136 |
137 | def on_config(self, config: Config, **kwargs) -> Config: # noqa: W0613 (unused arguments)
138 | """
139 | Instantiate our Markdown extension.
140 |
141 | Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config).
142 | In this hook, we instantiate our [`MkdocstringsExtension`][mkdocstrings.extension.MkdocstringsExtension]
143 | and add it to the list of Markdown extensions used by `mkdocs`.
144 |
145 | We pass this plugin's configuration dictionary to the extension when instantiating it (it will need it
146 | later when processing markdown to get handlers and their global configurations).
147 |
148 | Arguments:
149 | config: The MkDocs config object.
150 | kwargs: Additional arguments passed by MkDocs.
151 |
152 | Returns:
153 | The modified config.
154 | """
155 | log.debug("Adding extension to the list")
156 |
157 | theme_name = None
158 | if config["theme"].name is None:
159 | theme_name = os.path.dirname(config["theme"].dirs[0])
160 | else:
161 | theme_name = config["theme"].name
162 |
163 | extension_config = {
164 | "theme_name": theme_name,
165 | "mdx": config["markdown_extensions"],
166 | "mdx_configs": config["mdx_configs"],
167 | "mkdocstrings": self.config,
168 | }
169 | self._handlers = Handlers(extension_config)
170 |
171 | try:
172 | # If autorefs plugin is explicitly enabled, just use it.
173 | autorefs = config["plugins"]["autorefs"]
174 | log.debug(f"Picked up existing autorefs instance {autorefs!r}")
175 | except KeyError:
176 | # Otherwise, add a limited instance of it that acts only on what's added through `register_anchor`.
177 | autorefs = AutorefsPlugin()
178 | autorefs.scan_toc = False
179 | config["plugins"]["autorefs"] = autorefs
180 | log.debug(f"Added a subdued autorefs instance {autorefs!r}")
181 | # Add collector-based fallback in either case.
182 | autorefs.get_fallback_anchor = self._handlers.get_anchor
183 |
184 | mkdocstrings_extension = MkdocstringsExtension(extension_config, self._handlers, autorefs)
185 | config["markdown_extensions"].append(mkdocstrings_extension)
186 |
187 | config["extra_css"].insert(0, self.css_filename) # So that it has lower priority than user files.
188 |
189 | return config
190 |
191 | def on_post_build(self, config: Config, **kwargs) -> None: # noqa: W0613,R0201 (unused arguments, cannot be static)
192 | """
193 | Teardown the handlers.
194 |
195 | Hook for the [`on_post_build` event](https://www.mkdocs.org/user-guide/plugins/#on_post_build).
196 | This hook is used to teardown all the handlers that were instantiated and cached during documentation buildup.
197 |
198 | For example, the [Python handler's collector][mkdocstrings.handlers.python.PythonCollector] opens a subprocess
199 | in the background and keeps it open to feed it the "autodoc" instructions and get back JSON data. Therefore,
200 | it must close it at some point, and it does it in its
201 | [`teardown()` method][mkdocstrings.handlers.python.PythonCollector.teardown] which is indirectly called by
202 | this hook.
203 |
204 | Arguments:
205 | config: The MkDocs config object.
206 | kwargs: Additional arguments passed by MkDocs.
207 | """
208 | if self._handlers:
209 | css_content = "\n".join(handler.renderer.extra_css for handler in self.handlers.seen_handlers)
210 | write_file(css_content.encode("utf-8"), os.path.join(config["site_dir"], self.css_filename))
211 |
212 | log.debug("Tearing handlers down")
213 | self._handlers.teardown()
214 |
215 | def get_handler(self, handler_name: str) -> BaseHandler:
216 | """
217 | Get a handler by its name. See [mkdocstrings.handlers.base.Handlers.get_handler][].
218 |
219 | Arguments:
220 | handler_name: The name of the handler.
221 |
222 | Returns:
223 | An instance of a subclass of [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler].
224 | """
225 | return self.handlers.get_handler(handler_name)
226 |
--------------------------------------------------------------------------------
/docs/src/mkdocstrings/extension.py:
--------------------------------------------------------------------------------
1 | """
2 | This module holds the code of the Markdown extension responsible for matching "autodoc" instructions.
3 |
4 | The extension is composed of a Markdown [block processor](https://python-markdown.github.io/extensions/api/#blockparser)
5 | that matches indented blocks starting with a line like '::: identifier'.
6 |
7 | For each of these blocks, it uses a [handler][mkdocstrings.handlers.base.BaseHandler] to collect documentation about
8 | the given identifier and render it with Jinja templates.
9 |
10 | Both the collection and rendering process can be configured by adding YAML configuration under the "autodoc"
11 | instruction:
12 |
13 | ```yaml
14 | ::: some.identifier
15 | handler: python
16 | selection:
17 | option1: value1
18 | option2:
19 | - value2a
20 | - value2b
21 | rendering:
22 | option_x: etc
23 | ```
24 | """
25 | import re
26 | from collections import ChainMap
27 | from typing import Mapping, MutableSequence, Sequence, Tuple
28 | from xml.etree.ElementTree import Element
29 |
30 | import yaml
31 | from jinja2.exceptions import TemplateNotFound
32 | from markdown import Markdown
33 | from markdown.blockparser import BlockParser
34 | from markdown.blockprocessors import BlockProcessor
35 | from markdown.extensions import Extension
36 | from markdown.treeprocessors import Treeprocessor
37 |
38 | from mkdocs_autorefs.plugin import AutorefsPlugin
39 | from mkdocstrings.handlers.base import CollectionError, CollectorItem, Handlers
40 | from mkdocstrings.loggers import get_logger
41 |
42 | log = get_logger(__name__)
43 |
44 |
45 | class AutoDocProcessor(BlockProcessor):
46 | """
47 | Our "autodoc" Markdown block processor.
48 |
49 | It has a [`test` method][mkdocstrings.extension.AutoDocProcessor.test] that tells if a block matches a criterion,
50 | and a [`run` method][mkdocstrings.extension.AutoDocProcessor.run] that processes it.
51 |
52 | It also has utility methods allowing to get handlers and their configuration easily, useful when processing
53 | a matched block.
54 | """
55 |
56 | regex = re.compile(r"^(?P#{1,6} *|)::: ?(?P.+?) *$", flags=re.MULTILINE)
57 |
58 | def __init__(
59 | self, parser: BlockParser, md: Markdown, config: dict, handlers: Handlers, autorefs: AutorefsPlugin
60 | ) -> None:
61 | """
62 | Initialize the object.
63 |
64 | Arguments:
65 | parser: A `markdown.blockparser.BlockParser` instance.
66 | md: A `markdown.Markdown` instance.
67 | config: The [configuration][mkdocstrings.plugin.MkdocstringsPlugin.config_scheme]
68 | of the `mkdocstrings` plugin.
69 | handlers: A [mkdocstrings.handlers.base.Handlers][] instance.
70 | autorefs: A [mkdocs_autorefs.plugin.AutorefsPlugin][] instance.
71 | """
72 | super().__init__(parser=parser)
73 | self.md = md
74 | self._config = config
75 | self._handlers = handlers
76 | self._autorefs = autorefs
77 | self._updated_env = False
78 |
79 | def test(self, parent: Element, block: str) -> bool:
80 | """
81 | Match our autodoc instructions.
82 |
83 | Arguments:
84 | parent: The parent element in the XML tree.
85 | block: The block to be tested.
86 |
87 | Returns:
88 | Whether this block should be processed or not.
89 | """
90 | return bool(self.regex.search(block))
91 |
92 | def run(self, parent: Element, blocks: MutableSequence[str]) -> None:
93 | """
94 | Run code on the matched blocks.
95 |
96 | The identifier and configuration lines are retrieved from a matched block
97 | and used to collect and render an object.
98 |
99 | Arguments:
100 | parent: The parent element in the XML tree.
101 | blocks: The rest of the blocks to be processed.
102 | """
103 | block = blocks.pop(0)
104 | match = self.regex.search(block)
105 |
106 | if match:
107 | if match.start() > 0:
108 | self.parser.parseBlocks(parent, [block[: match.start()]])
109 | # removes the first line
110 | block = block[match.end() :] # type: ignore
111 |
112 | block, the_rest = self.detab(block)
113 |
114 | if match:
115 | identifier = match["name"]
116 | heading_level = match["heading"].count("#")
117 | log.debug(f"Matched '::: {identifier}'")
118 |
119 | html, headings = self._process_block(identifier, block, heading_level)
120 | el = Element("div", {"class": "mkdocstrings"})
121 | # The final HTML is inserted as opaque to subsequent processing, and only revealed at the end.
122 | el.text = self.md.htmlStash.store(html)
123 | # So we need to duplicate the headings directly (and delete later), just so 'toc' can pick them up.
124 | el.extend(headings)
125 |
126 | for heading in headings:
127 | self._autorefs.register_anchor(self._autorefs.current_page, heading.attrib["id"])
128 |
129 | parent.append(el)
130 |
131 | if the_rest:
132 | # This block contained unindented line(s) after the first indented
133 | # line. Insert these lines as the first block of the master blocks
134 | # list for future processing.
135 | blocks.insert(0, the_rest)
136 |
137 | def _process_block(self, identifier: str, yaml_block: str, heading_level: int = 0) -> Tuple[str, Sequence[Element]]:
138 | """
139 | Process an autodoc block.
140 |
141 | Arguments:
142 | identifier: The identifier of the object to collect and render.
143 | yaml_block: The YAML configuration.
144 | heading_level: Suggested level of the the heading to insert (0 to ignore).
145 |
146 | Raises:
147 | CollectionError: When something wrong happened during collection.
148 | TemplateNotFound: When a template used for rendering could not be found.
149 |
150 | Returns:
151 | Rendered HTML and the list of heading elements encoutered.
152 | """
153 | config = yaml.safe_load(yaml_block) or {}
154 | handler_name = self._handlers.get_handler_name(config)
155 |
156 | log.debug(f"Using handler '{handler_name}'")
157 | handler_config = self._handlers.get_handler_config(handler_name)
158 | handler = self._handlers.get_handler(handler_name, handler_config)
159 |
160 | selection, rendering = get_item_configs(handler_config, config)
161 | if heading_level:
162 | rendering = ChainMap(rendering, {"heading_level": heading_level}) # like setdefault
163 |
164 | log.debug("Collecting data")
165 | try:
166 | data: CollectorItem = handler.collector.collect(identifier, selection)
167 | except CollectionError:
168 | log.error(f"Could not collect '{identifier}'")
169 | raise
170 |
171 | if not self._updated_env:
172 | log.debug("Updating renderer's env")
173 | handler.renderer._update_env(self.md, self._config) # noqa: W0212 (protected member OK)
174 | self._updated_env = True
175 |
176 | log.debug("Rendering templates")
177 | try:
178 | rendered = handler.renderer.render(data, rendering)
179 | except TemplateNotFound as exc:
180 | theme_name = self._config["theme_name"]
181 | log.error(
182 | f"Template '{exc.name}' not found for '{handler_name}' handler and theme '{theme_name}'.",
183 | )
184 | raise
185 |
186 | return (rendered, handler.renderer.get_headings())
187 |
188 |
189 | def get_item_configs(handler_config: dict, config: dict) -> Tuple[Mapping, Mapping]:
190 | """
191 | Get the selection and rendering configuration merged into the global configuration of the given handler.
192 |
193 | Arguments:
194 | handler_config: The global configuration of a handler. It can be an empty dictionary.
195 | config: The configuration to merge into the global handler configuration.
196 |
197 | Returns:
198 | Two dictionaries: selection and rendering. The local configurations are merged into the global ones.
199 | """
200 | item_selection_config = ChainMap(config.get("selection", {}), handler_config.get("selection", {}))
201 | item_rendering_config = ChainMap(config.get("rendering", {}), handler_config.get("rendering", {}))
202 | return item_selection_config, item_rendering_config
203 |
204 |
205 | class _PostProcessor(Treeprocessor):
206 | def run(self, root: Element):
207 | carry_text = ""
208 | for el in reversed(root): # Reversed mainly for the ability to mutate during iteration.
209 | if el.tag == "div" and el.get("class") == "mkdocstrings":
210 | # Delete the duplicated headings along with their container, but keep the text (i.e. the actual HTML).
211 | carry_text = (el.text or "") + carry_text
212 | root.remove(el)
213 | elif carry_text:
214 | el.tail = (el.tail or "") + carry_text
215 | carry_text = ""
216 | if carry_text:
217 | root.text = (root.text or "") + carry_text
218 |
219 |
220 | class MkdocstringsExtension(Extension):
221 | """
222 | Our Markdown extension.
223 |
224 | It cannot work outside of `mkdocstrings`.
225 | """
226 |
227 | def __init__(self, config: dict, handlers: Handlers, autorefs: AutorefsPlugin, **kwargs) -> None:
228 | """
229 | Initialize the object.
230 |
231 | Arguments:
232 | config: The configuration items from `mkdocs` and `mkdocstrings` that must be passed to the block processor
233 | when instantiated in [`extendMarkdown`][mkdocstrings.extension.MkdocstringsExtension.extendMarkdown].
234 | handlers: A [mkdocstrings.handlers.base.Handlers][] instance.
235 | autorefs: A [mkdocs_autorefs.plugin.AutorefsPlugin][] instance.
236 | kwargs: Keyword arguments used by `markdown.extensions.Extension`.
237 | """
238 | super().__init__(**kwargs)
239 | self._config = config
240 | self._handlers = handlers
241 | self._autorefs = autorefs
242 |
243 | def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent method's name)
244 | """
245 | Register the extension.
246 |
247 | Add an instance of our [`AutoDocProcessor`][mkdocstrings.extension.AutoDocProcessor] to the Markdown parser.
248 |
249 | Arguments:
250 | md: A `markdown.Markdown` instance.
251 | """
252 | md.parser.blockprocessors.register(
253 | AutoDocProcessor(md.parser, md, self._config, self._handlers, self._autorefs),
254 | "mkdocstrings",
255 | priority=75, # Right before markdown.blockprocessors.HashHeaderProcessor
256 | )
257 | md.treeprocessors.register(
258 | _PostProcessor(md.parser),
259 | "mkdocstrings_post",
260 | priority=4, # Right after 'toc'.
261 | )
262 |
--------------------------------------------------------------------------------
/docs/src/mkdocstrings/handlers/python.py:
--------------------------------------------------------------------------------
1 | """
2 | This module implements a handler for the Python language.
3 |
4 | The handler collects data with [`pytkdocs`](https://github.com/pawamoy/pytkdocs).
5 | """
6 |
7 | import json
8 | import os
9 | import sys
10 | from collections import ChainMap
11 | from subprocess import PIPE, Popen # noqa: S404 (what other option, more secure that PIPE do we have? sockets?)
12 | from typing import Any, List, Optional
13 |
14 | from markdown import Markdown
15 |
16 | from mkdocstrings.handlers.base import BaseCollector, BaseHandler, BaseRenderer, CollectionError, CollectorItem
17 | from mkdocstrings.loggers import get_logger
18 |
19 | log = get_logger(__name__)
20 |
21 |
22 | class PythonRenderer(BaseRenderer):
23 | """
24 | The class responsible for loading Jinja templates and rendering them.
25 |
26 | It defines some configuration options, implements the `render` method,
27 | and overrides the `update_env` method of the [`BaseRenderer` class][mkdocstrings.handlers.base.BaseRenderer].
28 |
29 | Attributes:
30 | fallback_theme: The theme to fallback to.
31 | default_config: The default rendering options,
32 | see [`default_config`][mkdocstrings.handlers.python.PythonRenderer.default_config].
33 | """
34 |
35 | fallback_theme = "material"
36 |
37 | default_config: dict = {
38 | "show_root_heading": False,
39 | "show_root_toc_entry": True,
40 | "show_root_full_path": True,
41 | "show_root_members_full_path": False,
42 | "show_object_full_path": False,
43 | "show_category_heading": False,
44 | "show_if_no_docstring": False,
45 | "show_signature_annotations": False,
46 | "show_source": True,
47 | "group_by_category": True,
48 | "heading_level": 2,
49 | }
50 | """
51 | The default rendering options.
52 |
53 | Option | Type | Description | Default
54 | ------ | ---- | ----------- | -------
55 | **`show_root_heading`** | `bool` | Show the heading of the object at the root of the documentation tree. | `False`
56 | **`show_root_toc_entry`** | `bool` | If the root heading is not shown, at least add a ToC entry for it. | `True`
57 | **`show_root_full_path`** | `bool` | Show the full Python path for the root object heading. | `True`
58 | **`show_object_full_path`** | `bool` | Show the full Python path of every object. | `False`
59 | **`show_root_members_full_path`** | `bool` | Show the full Python path of objects that are children of the root object (for example, classes in a module). When False, `show_object_full_path` overrides. | `False`
60 | **`show_category_heading`** | `bool` | When grouped by categories, show a heading for each category. | `False`
61 | **`show_if_no_docstring`** | `bool` | Show the object heading even if it has no docstring or children with docstrings. | `False`
62 | **`show_signature_annotations`** | `bool` | Show the type annotations in methods and functions signatures. | `False`
63 | **`show_source`** | `bool` | Show the source code of this object. | `True`
64 | **`group_by_category`** | `bool` | Group the object's children by categories: attributes, classes, functions, methods, and modules. | `True`
65 | **`heading_level`** | `int` | The initial heading level to use. | `2`
66 | """ # noqa: E501
67 |
68 | def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignore missing docstring)
69 | final_config = ChainMap(config, self.default_config)
70 |
71 | template = self.env.get_template(f"{data['category']}.html")
72 |
73 | # Heading level is a "state" variable, that will change at each step
74 | # of the rendering recursion. Therefore, it's easier to use it as a plain value
75 | # than as an item in a dictionary.
76 | heading_level = final_config["heading_level"]
77 |
78 | return template.render(
79 | **{"config": final_config, data["category"]: data, "heading_level": heading_level, "root": True},
80 | )
81 |
82 | def get_anchor(self, data: CollectorItem) -> str: # noqa: D102 (ignore missing docstring)
83 | return data.get("path")
84 |
85 | def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore missing docstring)
86 | super().update_env(md, config)
87 | self.env.trim_blocks = True
88 | self.env.lstrip_blocks = True
89 | self.env.keep_trailing_newline = False
90 |
91 |
92 | class PythonCollector(BaseCollector):
93 | """
94 | The class responsible for loading Jinja templates and rendering them.
95 |
96 | It defines some configuration options, implements the `render` method,
97 | and overrides the `update_env` method of the [`BaseRenderer` class][mkdocstrings.handlers.base.BaseRenderer].
98 | """
99 |
100 | default_config: dict = {"filters": ["!^_[^_]"]}
101 | """
102 | The default selection options.
103 |
104 | Option | Type | Description | Default
105 | ------ | ---- | ----------- | -------
106 | **`filters`** | `List[str]` | Filter members with regular expressions. | `[ "!^_[^_]" ]`
107 | **`members`** | `Union[bool, List[str]]` | Explicitly select the object members. | *`pytkdocs` default: `True`*
108 |
109 | If `members` is a list of names, filters are applied only on the members children (not the members themselves).
110 | If `members` is `False`, none are selected.
111 | If `members` is `True` or an empty list, filters are applied on all members and their children.
112 |
113 | Members affect only the first layer of objects, while filters affect the whole object-tree recursively.
114 |
115 | Every filters is run against every object name. An object can be un-selected by a filter and re-selected by the
116 | next one:
117 |
118 | - `"!^_"`: exclude all objects starting with an underscore
119 | - `"^__"`: but select all objects starting with **two** underscores
120 |
121 | Obviously one could use a single filter instead: `"!^_[^_]"`, which is the default.
122 | """
123 |
124 | def __init__(self, setup_commands: Optional[List[str]] = None) -> None:
125 | """
126 | Initialize the object.
127 |
128 | When instantiating a Python collector, we open a subprocess in the background with `subprocess.Popen`.
129 | It will allow us to feed input to and read output from this subprocess, keeping it alive during
130 | the whole documentation generation. Spawning a new Python subprocess for each "autodoc" instruction would be
131 | too resource intensive, and would slow down `mkdocstrings` a lot.
132 |
133 | Arguments:
134 | setup_commands: A list of python commands as strings to be executed in the subprocess before `pytkdocs`.
135 | """
136 | log.debug("Opening 'pytkdocs' subprocess")
137 | env = os.environ.copy()
138 | env["PYTHONUNBUFFERED"] = "1"
139 |
140 | if setup_commands:
141 | # prevent the Python interpreter or the setup commands
142 | # from writing to stdout as it would break pytkdocs output
143 | commands = [
144 | "import sys",
145 | "from io import StringIO",
146 | "from pytkdocs.cli import main as pytkdocs",
147 | "sys.stdout = StringIO()", # redirect stdout to memory buffer
148 | *setup_commands,
149 | "sys.stdout.flush()",
150 | "sys.stdout = sys.__stdout__", # restore stdout
151 | "pytkdocs(['--line-by-line'])",
152 | ]
153 | cmd = [sys.executable, "-c", "; ".join(commands)]
154 | else:
155 | cmd = [sys.executable, "-m", "pytkdocs", "--line-by-line"]
156 |
157 | self.process = Popen( # noqa: S603,S607 (we trust the input, and we don't want to use the absolute path)
158 | cmd,
159 | universal_newlines=True,
160 | stderr=PIPE,
161 | stdout=PIPE,
162 | stdin=PIPE,
163 | bufsize=-1,
164 | env=env,
165 | )
166 |
167 | def collect(self, identifier: str, config: dict) -> CollectorItem:
168 | """
169 | Collect the documentation tree given an identifier and selection options.
170 |
171 | In this method, we feed one line of JSON to the standard input of the subprocess that was opened
172 | during instantiation of the collector. Then we read one line of JSON on its standard output.
173 |
174 | We load back the JSON text into a Python dictionary.
175 | If there is a decoding error, we log it as error and raise a CollectionError.
176 |
177 | If the dictionary contains an `error` key, we log it as error (with the optional `traceback` value),
178 | and raise a CollectionError.
179 |
180 | If the dictionary values for keys `loading_errors` and `parsing_errors` are not empty,
181 | we log them as warnings.
182 |
183 | Then we pick up the only object within the `objects` list (there's always only one, because we collect
184 | them one by one), rebuild it's categories lists
185 | (see [`rebuild_category_lists()`][mkdocstrings.handlers.python.rebuild_category_lists]),
186 | and return it.
187 |
188 | Arguments:
189 | identifier: The dotted-path of a Python object available in the Python path.
190 | config: Selection options, used to alter the data collection done by `pytkdocs`.
191 |
192 | Raises:
193 | CollectionError: When there was a problem collecting the object documentation.
194 |
195 | Returns:
196 | The collected object-tree.
197 | """
198 | final_config = ChainMap(config, self.default_config)
199 |
200 | log.debug("Preparing input")
201 | json_input = json.dumps({"objects": [{"path": identifier, **final_config}]})
202 |
203 | log.debug("Writing to process' stdin")
204 | self.process.stdin.write(json_input + "\n") # type: ignore
205 | self.process.stdin.flush() # type: ignore
206 |
207 | log.debug("Reading process' stdout")
208 | stdout = self.process.stdout.readline() # type: ignore
209 |
210 | log.debug("Loading JSON output as Python object")
211 | try:
212 | result = json.loads(stdout)
213 | except json.decoder.JSONDecodeError as exception:
214 | log.error(f"Error while loading JSON: {stdout}")
215 | raise CollectionError(str(exception)) from exception
216 |
217 | error = result.get("error")
218 | if error:
219 | message = f"Collection failed: {error}"
220 | if "traceback" in result:
221 | message += f"\n{result['traceback']}"
222 | log.error(message)
223 | raise CollectionError(error)
224 |
225 | for loading_error in result["loading_errors"]:
226 | log.warning(loading_error)
227 |
228 | for errors in result["parsing_errors"].values():
229 | for parsing_error in errors:
230 | log.warning(parsing_error)
231 |
232 | # We always collect only one object at a time
233 | result = result["objects"][0]
234 |
235 | log.debug("Rebuilding categories and children lists")
236 | rebuild_category_lists(result)
237 |
238 | return result
239 |
240 | def teardown(self) -> None:
241 | """Terminate the opened subprocess, set it to `None`."""
242 | log.debug("Tearing process down")
243 | self.process.terminate()
244 |
245 |
246 | class PythonHandler(BaseHandler):
247 | """The Python handler class, nothing specific here."""
248 |
249 |
250 | def get_handler(
251 | theme: str, # noqa: W0613 (unused argument config)
252 | custom_templates: Optional[str] = None,
253 | setup_commands: Optional[List[str]] = None,
254 | **config: Any,
255 | ) -> PythonHandler:
256 | """
257 | Simply return an instance of `PythonHandler`.
258 |
259 | Arguments:
260 | theme: The theme to use when rendering contents.
261 | custom_templates: Directory containing custom templates.
262 | setup_commands: A list of commands as strings to be executed in the subprocess before `pytkdocs`.
263 | config: Configuration passed to the handler.
264 |
265 | Returns:
266 | An instance of `PythonHandler`.
267 | """
268 | return PythonHandler(
269 | collector=PythonCollector(setup_commands=setup_commands),
270 | renderer=PythonRenderer("python", theme, custom_templates),
271 | )
272 |
273 |
274 | def rebuild_category_lists(obj: dict) -> None:
275 | """
276 | Recursively rebuild the category lists of a collected object.
277 |
278 | Since `pytkdocs` dumps JSON on standard output, it must serialize the object-tree and flatten it to reduce data
279 | duplication and avoid cycle-references. Indeed, each node of the object-tree has a `children` list, containing
280 | all children, and another list for each category of children: `attributes`, `classes`, `functions`, `methods`
281 | and `modules`. It replaces the values in category lists with only the paths of the objects.
282 |
283 | Here, we reconstruct these category lists by picking objects in the `children` list using their path.
284 |
285 | For each object, we recurse on every one of its children.
286 |
287 | Arguments:
288 | obj: The collected object, loaded back from JSON into a Python dictionary.
289 | """
290 | for category in ("attributes", "classes", "functions", "methods", "modules"):
291 | obj[category] = [obj["children"][path] for path in obj[category]]
292 | obj["children"] = [child for _, child in obj["children"].items()]
293 | for child in obj["children"]:
294 | rebuild_category_lists(child)
295 |
--------------------------------------------------------------------------------
/pure_ocean_breeze/jason/data/read_data.py:
--------------------------------------------------------------------------------
1 | __updated__ = "2025-06-25 21:06:31"
2 |
3 | import os
4 | import numpy as np
5 | import pandas as pd
6 | from typing import Union
7 | import rust_pyfunc as rp
8 |
9 | from pure_ocean_breeze.jason.state.states import STATES
10 | from pure_ocean_breeze.jason.state.homeplace import HomePlace
11 | from pure_ocean_breeze.jason.state.decorators import *
12 | from pure_ocean_breeze.jason.data.tools import boom_one
13 | from cachier import cachier
14 |
15 | try:
16 | homeplace = HomePlace()
17 | except Exception:
18 | print("您暂未初始化,功能将受限")
19 |
20 |
21 | @cachier()
22 | def read_daily(
23 | open: bool = 0,
24 | close: bool = 0,
25 | high: bool = 0,
26 | low: bool = 0,
27 | vwap: bool = 0,
28 | tr: bool = 0,
29 | sharenum: bool = 0,
30 | total_sharenum: bool = 0,
31 | amount: bool = 0,
32 | money: bool = 0,
33 | flow_cap: bool = 0,
34 | total_cap: bool = 0,
35 | adjfactor: bool = 0,
36 | state: bool = 0,
37 | state_loose: bool = 0,
38 | unadjust: bool = 0,
39 | ret: bool = 0,
40 | ret_inday: bool = 0,
41 | ret_night: bool = 0,
42 | vol: bool = 0,
43 | vol_inday: bool = 0,
44 | vol_night: bool = 0,
45 | swing: bool = 0,
46 | stop_up: bool = 0,
47 | stop_down: bool = 0,
48 | up_down_limit_status:bool=0,
49 | swindustry_dummy: bool = 0,
50 | start: Union[int, str] = STATES["START"],
51 | ) -> pd.DataFrame:
52 | """直接读取常用的量价读取日频数据,默认为复权价格,
53 | 在 open,close,high,low,tr,sharenum,volume 中选择一个参数指定为1
54 |
55 | Parameters
56 | ----------
57 | open : bool, optional
58 | 为1则选择读取开盘价, by default 0
59 | close : bool, optional
60 | 为1则选择读取收盘价, by default 0
61 | high : bool, optional
62 | 为1则选择读取最高价, by default 0
63 | low : bool, optional
64 | 为1则选择读取最低价, by default 0
65 | vwap : bool, optional
66 | 为1则选择读取日均成交价, by default 0
67 | tr : bool, optional
68 | 为1则选择读取换手率, by default 0
69 | sharenum : bool, optional
70 | 为1则选择读取流通股数, by default 0
71 | total_sharenum : bool, optional
72 | 为1则表示读取总股数, by default 0
73 | amount : bool, optional
74 | 为1则选择读取成交量, by default 0
75 | money : bool, optional
76 | 为1则表示读取成交额, by default 0
77 | flow_cap : bool, optional
78 | 为1则选择读取流通市值, by default 0
79 | total_cap : bool, optional
80 | 为1则选择读取总市值, by default 0
81 | adjfactor : bool, optional
82 | 为1则选择读取复权因子, by default 0
83 | state : bool, optional
84 | 为1则选择读取当日交易状态是否正常,1表示正常交易,空值则不是, by default 0
85 | state_loose : bool, optional
86 | 为1则选择读取当日交易状态是否正常,1表示正常交易,空值则不是, by default 0
87 | unadjust : bool, optional
88 | 为1则将上述价格改为不复权价格, by default 0
89 | ret : bool, optional
90 | 为1则选择读取日间收益率, by default 0
91 | ret_inday : bool, optional
92 | 为1则表示读取日内收益率, by default 0
93 | ret_night : bool, optional
94 | 为1则表示读取隔夜波动率, by default 0
95 | vol : bool, optional
96 | 为1则选择读取滚动20日日间波动率, by default 0
97 | vol_inday : bool, optional
98 | 为1则表示读取滚动20日日内收益率波动率, by default 0
99 | vol_night : bool, optional
100 | 为1则表示读取滚动20日隔夜收益率波动率, by default 0
101 | swing : bool, optional
102 | 为1则表示读取振幅, by default 0
103 | stop_up : bool, optional
104 | 为1则表示读取每只股票涨停价, by default 0
105 | stop_down : bool, optional
106 | 为1则表示读取每只股票跌停价, by default 0
107 | swindustry_dummy : bool, optional
108 | 为1则表示读取申万一级行业哑变量, by default 0
109 | start : Union[int,str], optional
110 | 起始日期,形如20130101, by default STATES["START"]
111 |
112 | Returns
113 | -------
114 | `pd.DataFrame`
115 | 一个columns为股票代码,index为时间,values为目标数据的pd.DataFrame
116 |
117 | Raises
118 | ------
119 | `IOError`
120 | open,close,high,low,tr,sharenum,volume 都为0时,将报错
121 | """
122 |
123 | if not unadjust:
124 | if open:
125 | df = pd.read_parquet(
126 | homeplace.daily_data_file + "opens.parquet"
127 | ) * read_daily(state=1, start=start)
128 | elif close:
129 | df = pd.read_parquet(
130 | homeplace.daily_data_file + "closes.parquet"
131 | ) * read_daily(state=1, start=start)
132 | elif high:
133 | df = pd.read_parquet(
134 | homeplace.daily_data_file + "highs.parquet"
135 | ) * read_daily(state=1, start=start)
136 | elif low:
137 | df = pd.read_parquet(
138 | homeplace.daily_data_file + "lows.parquet"
139 | ) * read_daily(state=1, start=start)
140 | elif vwap:
141 | df = (
142 | pd.read_parquet(homeplace.daily_data_file + "vwaps.parquet")
143 | * read_daily(adjfactor=1, start=start)
144 | *read_daily(state=1, start=start)
145 | )
146 | elif tr:
147 | df = pd.read_parquet(homeplace.daily_data_file + "trs.parquet").replace(
148 | 0, np.nan
149 | ) *read_daily(state=1, start=start)
150 | elif sharenum:
151 | df = pd.read_parquet(homeplace.daily_data_file + "sharenums.parquet")
152 | elif total_sharenum:
153 | df = pd.read_parquet(homeplace.daily_data_file + "total_sharenums.parquet")
154 | elif amount:
155 | df = pd.read_parquet(
156 | homeplace.daily_data_file + "amounts.parquet"
157 | ) * read_daily(state=1, start=start)
158 | elif money:
159 | df = pd.read_parquet(
160 | homeplace.daily_data_file + "moneys.parquet"
161 | )*read_daily(state=1, start=start)
162 | elif flow_cap:
163 | df = pd.read_parquet(homeplace.daily_data_file + "flow_caps.parquet")
164 | elif total_cap:
165 | df = pd.read_parquet(homeplace.daily_data_file + "total_caps.parquet")
166 | elif adjfactor:
167 | df=pd.read_parquet(homeplace.daily_data_file+'adjfactors.parquet')
168 | elif state:
169 | df = pd.read_parquet(homeplace.daily_data_file + "states.parquet")
170 | elif state_loose:
171 | df = pd.read_parquet(homeplace.daily_data_file + "states_loose.parquet")
172 | elif ret:
173 | df = read_daily(close=1, start=start)
174 | df = df / df.shift(1) - 1
175 | elif ret_inday:
176 | df = read_daily(close=1, start=start) / read_daily(open=1, start=start) - 1
177 | elif ret_night:
178 | df = (
179 | read_daily(open=1, start=start)
180 | / read_daily(close=1, start=start).shift(1)
181 | - 1
182 | )
183 | elif vol:
184 | df = read_daily(ret=1, start=start)
185 | df = df.rolling(20, min_periods=10).std()
186 | elif vol_inday:
187 | df = read_daily(ret_inday=1, start=start)
188 | df = df.rolling(20, min_periods=10).std()
189 | elif vol_night:
190 | df = read_daily(ret_night=1, start=start)
191 | df = df.rolling(20, min_periods=10).std()
192 | elif swing:
193 | df = (
194 | read_daily(high=1, start=start) - read_daily(low=1, start=start)
195 | ) / read_daily(close=1, start=start).shift(1)
196 | elif stop_up:
197 | df = (
198 | pd.read_parquet(homeplace.daily_data_file + "stop_ups.parquet")
199 | * read_daily(adjfactor=1, start=start)
200 | * read_daily(state=1, start=start)
201 | )
202 | elif stop_down:
203 | df = (
204 | pd.read_parquet(homeplace.daily_data_file + "stop_downs.parquet")
205 | * read_daily(adjfactor=1, start=start)
206 | * read_daily(state=1, start=start)
207 | )
208 | elif up_down_limit_status:
209 | df=pd.read_parquet(homeplace.daily_data_file+'up_down_limit_status.parquet')
210 | elif swindustry_dummy:
211 | df=pd.read_parquet(homeplace.daily_data_file+'sw_industry_level1_dummies.parquet')
212 | else:
213 | raise IOError("阁下总得读点什么吧?🤒")
214 | else:
215 | if open:
216 | df = pd.read_parquet(
217 | homeplace.daily_data_file + "opens_unadj.parquet"
218 | ) * read_daily(state=1, start=start)
219 | elif close:
220 | df = pd.read_parquet(
221 | homeplace.daily_data_file + "closes_unadj.parquet"
222 | ) * read_daily(state=1, start=start)
223 | elif high:
224 | df = pd.read_parquet(
225 | homeplace.daily_data_file + "highs_unadj.parquet"
226 | ) * read_daily(state=1, start=start)
227 | elif low:
228 | df = pd.read_parquet(
229 | homeplace.daily_data_file + "lows_unadj.parquet"
230 | ) * read_daily(state=1, start=start)
231 | elif vwap:
232 | df = pd.read_parquet(
233 | homeplace.daily_data_file + "vwaps.parquet"
234 | ) * read_daily(state=1, start=start)
235 | elif stop_up:
236 | df = pd.read_parquet(
237 | homeplace.daily_data_file + "stop_ups.parquet"
238 | ) * read_daily(state=1, start=start)
239 | elif stop_down:
240 | df = pd.read_parquet(
241 | homeplace.daily_data_file + "stop_downs.parquet"
242 | ) * read_daily(state=1, start=start)
243 | else:
244 | raise IOError("阁下总得读点什么吧?🤒")
245 | if "date" not in df.columns:
246 | df = df[df.index >= pd.Timestamp(str(start))]
247 | return df.dropna(how="all")
248 |
249 |
250 | def get_industry_dummies(
251 | daily: bool = 0,
252 | weekly: bool = 0,
253 | start: int = STATES["START"],
254 | ) -> dict:
255 | """生成30/31个行业的哑变量矩阵,返回一个字典
256 |
257 | Parameters
258 | ----------
259 | daily : bool, optional
260 | 返回日频的哑变量, by default 0
261 | weekly : bool, optional
262 | 返回week频的哑变量, by default 0
263 | start : int, optional
264 | 起始日期, by default STATES["START"]
265 |
266 | Returns
267 | -------
268 | `Dict`
269 | 各个行业及其哑变量构成的字典
270 |
271 | Raises
272 | ------
273 | `ValueError`
274 | 如果未指定频率,将报错
275 | """
276 | homeplace = HomePlace()
277 | name='sw_industry_level1_dummies.parquet'
278 | if weekly:
279 | industry_dummy = pd.read_parquet(homeplace.daily_data_file + name)
280 | industry_dummy = (
281 | industry_dummy.set_index("date")
282 | .groupby("code")
283 | .resample("W")
284 | .last()
285 | .fillna(0)
286 | .drop(columns=["code"])
287 | .reset_index()
288 | )
289 | elif daily:
290 | industry_dummy = pd.read_parquet(homeplace.daily_data_file + name).fillna(0)
291 | else:
292 | raise ValueError("您总得指定一个频率吧?🤒")
293 | industry_dummy = industry_dummy[industry_dummy.date >= pd.Timestamp(str(start))]
294 | ws = list(industry_dummy.columns)[2:]
295 | ress = {}
296 | for w in ws:
297 | df = industry_dummy[["date", "code", w]]
298 | df = df.pivot(index="date", columns="code", values=w)
299 | df = df.replace(0, np.nan)
300 | ress[w] = df
301 | return ress
302 |
303 |
304 | @cachier()
305 | def read_index(
306 | open: bool = 0,
307 | close: bool = 0,
308 | high: bool = 0,
309 | low: bool = 0,
310 | start: int = STATES["START"],
311 | every_stock: bool = 1,
312 | sh50: bool = 0,
313 | hs300: bool = 0,
314 | zz500: bool = 0,
315 | zz1000: bool = 0,
316 | ) -> Union[pd.DataFrame, pd.Series]:
317 | """读取中证全指日行情数据
318 |
319 | Parameters
320 | ----------
321 | open : bool, optional
322 | 读取开盘点数, by default 0
323 | close : bool, optional
324 | 读取收盘点数, by default 0
325 | high : bool, optional
326 | 读取最高点数, by default 0
327 | low : bool, optional
328 | 读取最低点数, by default 0
329 | start : int, optional
330 | 读取的起始日期, by default STATES["START"]
331 | every_stock : bool, optional
332 | 是否修改为index是时间,columns是每只股票代码,每一列值都相同的形式, by default 1
333 | sh50 : bool, optional
334 | 是否读取上证50, by default 0
335 | hs300 : bool, optional
336 | 是否读取沪深300, by default 0
337 | zz500 : bool, optional
338 | 是否读取中证500, by default 0
339 | zz1000 : bool, optional
340 | 是否读取中证1000, by default 0
341 |
342 | Returns
343 | -------
344 | Union[pd.DataFrame,pd.Series]
345 | 读取market_index的行情数据
346 |
347 | Raises
348 | ------
349 | IOError
350 | 如果没有指定任何指数,将报错
351 | """
352 | homeplace=HomePlace()
353 | if open:
354 | # 米筐的第一分钟是集合竞价,第一分钟的收盘价即为当天开盘价
355 | df = pd.read_parquet(homeplace.daily_data_file + "index_opens.parquet")
356 | elif close:
357 | df = pd.read_parquet(homeplace.daily_data_file + "index_closes.parquet")
358 | elif high:
359 | df = pd.read_parquet(homeplace.daily_data_file + "index_highs.parquet")
360 | elif low:
361 | df = pd.read_parquet(homeplace.daily_data_file + "index_lows.parquet")
362 | else:
363 | raise IOError("总得指定一个指标吧?🤒")
364 | if sh50:
365 | df = df["000016.SH"]
366 | elif hs300:
367 | df = df["000300.SH"]
368 | elif zz500:
369 | df = df["000905.SH"]
370 | elif zz1000:
371 | df = df["000852.SH"]
372 | else:
373 | raise IOError("总得指定一个指数吧?🤒")
374 | if every_stock:
375 | tr = read_daily(tr=1, start=start)
376 | df = pd.DataFrame({k: list(df) for k in list(tr.columns)}, index=df.index)
377 | return df
378 |
379 |
380 | @cachier()
381 | def moon_read_dummy(freq):
382 | def deal_dummy(industry_dummy):
383 | industry_dummy = industry_dummy.drop(columns=["code"]).reset_index()
384 | industry_ws = [f"w{i}" for i in range(1, industry_dummy.shape[1] - 1)]
385 | col = ["code", "date"] + industry_ws
386 | industry_dummy.columns = col
387 | industry_dummy = industry_dummy[
388 | industry_dummy.date >= pd.Timestamp(str(STATES["START"]))
389 | ]
390 | return industry_dummy
391 |
392 | # week_here
393 | swindustry_dummy = (
394 | pd.read_parquet(
395 | homeplace.daily_data_file + "sw_industry_level1_dummies.parquet"
396 | )
397 | .fillna(0)
398 | .set_index("date")
399 | .groupby("code")
400 | .resample(freq)
401 | .last()
402 | )
403 | return deal_dummy(swindustry_dummy)
404 |
405 |
406 |
407 |
408 | def read_trade(symbol:str, date:int,with_retreat:int=0)->pd.DataFrame:
409 | file_name = "%s_%d_%s.csv" % (symbol, date, "transaction")
410 | file_path = os.path.join("/ssd_data/stock", str(date), "transaction", file_name)
411 | df= pd.read_csv(
412 | file_path,
413 | dtype={"symbol": str},
414 | usecols=[
415 | "exchtime",
416 | "price",
417 | "volume",
418 | "turnover",
419 | "flag",
420 | "index",
421 | "localtime",
422 | "ask_order",
423 | "bid_order",
424 | ],
425 | memory_map=True,
426 | engine="c",
427 | low_memory=False,
428 | )
429 | if not with_retreat:
430 | df=df[df.flag!=32]
431 | df.exchtime=pd.to_timedelta(df.exchtime/1e6,unit='s')+pd.Timestamp('1970-01-01 08:00:00')
432 | return df
433 |
434 | def read_market(symbol:str, date:int)->pd.DataFrame:
435 | file_name = "%s_%d_%s.csv" % (symbol, date, "market_data")
436 | file_path = os.path.join("/ssd_data/stock", str(date), "market_data", file_name)
437 | df= pd.read_csv(
438 | file_path,
439 | dtype={"symbol": str},
440 | memory_map=True,
441 | engine="c",
442 | low_memory=False,
443 | )
444 | df.exchtime=pd.to_timedelta(df.exchtime/1e6,unit='s')+pd.Timestamp('1970-01-01 08:00:00')
445 | return df
446 |
447 | def read_market_pair(symbol:str, date:int)->tuple[pd.DataFrame,pd.DataFrame]:
448 | df=read_market(symbol,date)
449 | df = df[df.last_prc != 0]
450 | ask_prc_cols = [f"ask_prc{i}" for i in range(1, 11)]
451 | ask_vol_cols = [f"ask_vol{i}" for i in range(1, 11)]
452 | asks = pd.concat(
453 | [
454 | pd.melt(
455 | df[ask_prc_cols + ["exchtime"]],
456 | id_vars=["exchtime"],
457 | value_name="price",
458 | )
459 | .rename(columns={"variable": "number"})
460 | .set_index("exchtime"),
461 | pd.melt(
462 | df[ask_vol_cols + ["exchtime"]],
463 | id_vars=["exchtime"],
464 | value_name="vol",
465 | )
466 | .drop(columns=["variable"])
467 | .set_index("exchtime"),
468 | ],
469 | axis=1,
470 | )
471 | asks=asks[asks.price!=0]
472 | asks.number=asks.number.str.slice(7).astype(int)
473 | asks=asks.reset_index().sort_values(by=["exchtime", "number"]).reset_index(drop=True)
474 |
475 | bid_prc_cols = [f"bid_prc{i}" for i in range(1, 11)]
476 | bid_vol_cols = [f"bid_vol{i}" for i in range(1, 11)]
477 | bids = pd.concat(
478 | [
479 | pd.melt(
480 | df[bid_prc_cols + ["exchtime"]],
481 | id_vars=["exchtime"],
482 | value_name="price",
483 | )
484 | .rename(columns={"variable": "number"})
485 | .set_index("exchtime"),
486 | pd.melt(
487 | df[bid_vol_cols + ["exchtime"]],
488 | id_vars=["exchtime"],
489 | value_name="vol",
490 | )
491 | .drop(columns=["variable"])
492 | .set_index("exchtime"),
493 | ],
494 | axis=1,
495 | )
496 | bids=bids[bids.price!=0]
497 | bids.number=bids.number.str.slice(7).astype(int)
498 | bids=bids.reset_index().sort_values(by=["exchtime", "number"]).reset_index(drop=True)
499 | return asks, bids
500 |
501 |
502 | def adjust_afternoon(df: pd.DataFrame,only_inday:int=1) -> pd.DataFrame:
503 | start='09:30:00' if only_inday else '09:00:00'
504 | end='14:57:00' if only_inday else '15:00:00'
505 | if df.index.name=='exchtime':
506 | df1=df.between_time(start,'11:30:00')
507 | df2=df.between_time('13:00:00',end)
508 | df2.index=df2.index-pd.Timedelta(minutes=90)
509 | df=pd.concat([df1,df2])
510 | elif 'exchtime' in df.columns:
511 | df1=df.set_index('exchtime').between_time(start,'11:30:00')
512 | df2=df.set_index('exchtime').between_time('13:00:00',end)
513 | df2.index=df2.index-pd.Timedelta(minutes=90)
514 | df=pd.concat([df1,df2]).reset_index()
515 | return df
516 |
517 |
518 | def query_backup_df(path):
519 | df=rp.query_backup_fast(path)
520 | df=pd.DataFrame(df['factors']).assign(code=df['code'],date=df['date'])
521 | df=df[['code','date']+list(df.columns[:-2])]
522 | return df
--------------------------------------------------------------------------------
/docs/src/mkdocstrings/handlers/base.py:
--------------------------------------------------------------------------------
1 | """
2 | Base module for handlers.
3 |
4 | This module contains the base classes for implementing collectors, renderers, and the combination of the two: handlers.
5 |
6 | It also provides two methods:
7 |
8 | - `get_handler`, that will cache handlers into the `HANDLERS_CACHE` dictionary.
9 | - `teardown`, that will teardown all the cached handlers, and then clear the cache.
10 | """
11 |
12 | import copy
13 | import importlib
14 | import re
15 | import textwrap
16 | from abc import ABC, abstractmethod
17 | from pathlib import Path
18 | from typing import Any, Dict, Iterable, List, Optional, Sequence
19 | from xml.etree.ElementTree import Element, tostring
20 |
21 | from jinja2 import Environment, FileSystemLoader
22 | from markdown import Markdown
23 | from markdown.extensions import Extension
24 | from markdown.extensions.codehilite import CodeHiliteExtension
25 | from markdown.treeprocessors import Treeprocessor
26 | from markupsafe import Markup
27 | from pymdownx.highlight import Highlight, HighlightExtension
28 |
29 | from mkdocstrings.loggers import get_template_logger
30 |
31 | CollectorItem = Any
32 |
33 | TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
34 |
35 |
36 | class CollectionError(Exception):
37 | """An exception raised when some collection of data failed."""
38 |
39 |
40 | class ThemeNotSupported(Exception):
41 | """An exception raised to tell a theme is not supported."""
42 |
43 |
44 | class Highlighter(Highlight):
45 | """Code highlighter that tries to match the Markdown configuration."""
46 |
47 | _highlight_config_keys = frozenset(
48 | "use_pygments guess_lang css_class pygments_style noclasses linenums language_prefix".split(),
49 | )
50 |
51 | def __init__(self, md: Markdown):
52 | """Configure to match a `markdown.Markdown` instance.
53 |
54 | Arguments:
55 | md: The Markdown instance to read configs from.
56 | """
57 | config = {}
58 | for ext in md.registeredExtensions:
59 | if isinstance(ext, HighlightExtension) and (ext.enabled or not config):
60 | config = ext.getConfigs()
61 | break # This one takes priority, no need to continue looking
62 | if isinstance(ext, CodeHiliteExtension) and not config:
63 | config = ext.getConfigs()
64 | config["language_prefix"] = config["lang_prefix"]
65 | self._css_class = config.pop("css_class", "highlight")
66 | super().__init__(**{k: v for k, v in config.items() if k in self._highlight_config_keys})
67 |
68 | def highlight( # noqa: W0221 (intentionally different params, we're extending the functionality)
69 | self,
70 | src: str,
71 | language: str = None,
72 | *,
73 | inline: bool = False,
74 | dedent: bool = True,
75 | linenums: Optional[bool] = None,
76 | **kwargs,
77 | ) -> str:
78 | """
79 | Highlight a code-snippet.
80 |
81 | Arguments:
82 | src: The code to highlight.
83 | language: Explicitly tell what language to use for highlighting.
84 | inline: Whether to highlight as inline.
85 | dedent: Whether to dedent the code before highlighting it or not.
86 | linenums: Whether to add line numbers in the result.
87 | **kwargs: Pass on to `pymdownx.highlight.Highlight.highlight`.
88 |
89 | Returns:
90 | The highlighted code as HTML text, marked safe (not escaped for HTML).
91 | """
92 | if dedent:
93 | src = textwrap.dedent(src)
94 |
95 | kwargs.setdefault("css_class", self._css_class)
96 | old_linenums = self.linenums
97 | if linenums is not None:
98 | self.linenums = linenums
99 | try:
100 | result = super().highlight(src, language, inline=inline, **kwargs)
101 | finally:
102 | self.linenums = old_linenums
103 |
104 | if inline:
105 | return Markup(f'{result.text}')
106 | return Markup(result)
107 |
108 |
109 | def do_any(seq: Sequence, attribute: str = None) -> bool:
110 | """
111 | Check if at least one of the item in the sequence evaluates to true.
112 |
113 | The `any` builtin as a filter for Jinja templates.
114 |
115 | Arguments:
116 | seq: An iterable object.
117 | attribute: The attribute name to use on each object of the iterable.
118 |
119 | Returns:
120 | A boolean telling if any object of the iterable evaluated to True.
121 | """
122 | if attribute is None:
123 | return any(seq)
124 | return any(_[attribute] for _ in seq)
125 |
126 |
127 | class BaseRenderer(ABC):
128 | """
129 | The base renderer class.
130 |
131 | Inherit from this class to implement a renderer.
132 |
133 | You will have to implement the `render` method.
134 | You can also override the `update_env` method, to add more filters to the Jinja environment,
135 | making them available in your Jinja templates.
136 |
137 | To define a fallback theme, add a `fallback_theme` class-variable.
138 | To add custom CSS, add an `extra_css` variable or create an 'style.css' file beside the templates.
139 | """
140 |
141 | fallback_theme: str = ""
142 | extra_css = ""
143 |
144 | def __init__(self, directory: str, theme: str, custom_templates: Optional[str] = None) -> None:
145 | """
146 | Initialize the object.
147 |
148 | If the given theme is not supported (it does not exist), it will look for a `fallback_theme` attribute
149 | in `self` to use as a fallback theme.
150 |
151 | Arguments:
152 | directory: The name of the directory containing the themes for this renderer.
153 | theme: The name of theme to use.
154 | custom_templates: Directory containing custom templates.
155 | """
156 | paths = []
157 |
158 | themes_dir = TEMPLATES_DIR / directory
159 |
160 | paths.append(themes_dir / theme)
161 |
162 | if self.fallback_theme:
163 | paths.append(themes_dir / self.fallback_theme)
164 |
165 | for path in paths:
166 | css_path = path / "style.css"
167 | if css_path.is_file():
168 | self.extra_css += "\n" + css_path.read_text(encoding="utf-8")
169 | break
170 |
171 | if custom_templates is not None:
172 | paths.insert(0, Path(custom_templates) / directory / theme)
173 |
174 | self.env = Environment(
175 | autoescape=True,
176 | loader=FileSystemLoader(paths),
177 | auto_reload=False, # Editing a template in the middle of a build is not useful.
178 | ) # type: ignore
179 | self.env.filters["any"] = do_any
180 | self.env.globals["log"] = get_template_logger()
181 |
182 | self._headings = []
183 | self._md = None # To be populated in `update_env`.
184 |
185 | @abstractmethod
186 | def render(self, data: CollectorItem, config: dict) -> str:
187 | """
188 | Render a template using provided data and configuration options.
189 |
190 | Arguments:
191 | data: The collected data to render.
192 | config: The rendering options.
193 |
194 | Returns:
195 | The rendered template as HTML.
196 | """ # noqa: DAR202 (excess return section)
197 |
198 | def get_anchor(self, data: CollectorItem) -> Optional[str]:
199 | """
200 | Return the canonical identifier (HTML anchor) for a collected item.
201 |
202 | This must match what the renderer would've actually rendered,
203 | e.g. if rendering the item contains `...` then the return value should be "foo".
204 |
205 | Arguments:
206 | data: The collected data.
207 |
208 | Returns:
209 | The HTML anchor (without '#') as a string, or None if this item doesn't have an anchor.
210 | """ # noqa: DAR202 (excess return section)
211 |
212 | def do_convert_markdown(self, text: str, heading_level: int, html_id: str = "") -> Markup:
213 | """
214 | Render Markdown text; for use inside templates.
215 |
216 | Arguments:
217 | text: The text to convert.
218 | heading_level: The base heading level to start all Markdown headings from.
219 | html_id: The HTML id of the element that's considered the parent of this element.
220 |
221 | Returns:
222 | An HTML string.
223 | """
224 | treeprocessors = self._md.treeprocessors
225 | treeprocessors["mkdocstrings_headings"].shift_by = heading_level
226 | treeprocessors["mkdocstrings_ids"].id_prefix = html_id and html_id + "--"
227 | try:
228 | return Markup(self._md.convert(text))
229 | finally:
230 | treeprocessors["mkdocstrings_headings"].shift_by = 0
231 | treeprocessors["mkdocstrings_ids"].id_prefix = ""
232 | self._md.reset()
233 |
234 | def do_heading(
235 | self,
236 | content: str,
237 | heading_level: int,
238 | *,
239 | hidden: bool = False,
240 | toc_label: Optional[str] = None,
241 | **attributes: str,
242 | ) -> Markup:
243 | """
244 | Render an HTML heading and register it for the table of contents. For use inside templates.
245 |
246 | Arguments:
247 | content: The HTML within the heading.
248 | heading_level: The level of heading (e.g. 3 -> `h3`).
249 | hidden: If True, only register it for the table of contents, don't render anything.
250 | toc_label: The title to use in the table of contents ('data-toc-label' attribute).
251 | attributes: Any extra HTML attributes of the heading.
252 |
253 | Returns:
254 | An HTML string.
255 | """
256 | # First, produce the "fake" heading, for ToC only.
257 | el = Element(f"h{heading_level}", attributes)
258 | if toc_label is None:
259 | toc_label = content.unescape() if isinstance(el, Markup) else content
260 | el.set("data-toc-label", toc_label)
261 | self._headings.append(el)
262 |
263 | if hidden:
264 | return Markup('').format(attributes["id"])
265 |
266 | # Now produce the actual HTML to be rendered. The goal is to wrap the HTML content into a heading.
267 | # Start with a heading that has just attributes (no text), and add a placeholder into it.
268 | el = Element(f"h{heading_level}", attributes)
269 | el.append(Element("mkdocstrings-placeholder"))
270 | # Tell the 'toc' extension to make its additions if configured so.
271 | toc = self._md.treeprocessors["toc"]
272 | if toc.use_anchors:
273 | toc.add_anchor(el, attributes["id"])
274 | if toc.use_permalinks:
275 | toc.add_permalink(el, attributes["id"])
276 |
277 | # The content we received is HTML, so it can't just be inserted into the tree. We had marked the middle
278 | # of the heading with a placeholder that can never occur (text can't directly contain angle brackets).
279 | # Now this HTML wrapper can be "filled" by replacing the placeholder.
280 | html_with_placeholder = tostring(el, encoding="unicode")
281 | assert (
282 | html_with_placeholder.count("") == 1
283 | ), f"Bug in mkdocstrings: failed to replace in {html_with_placeholder!r}"
284 | html = html_with_placeholder.replace("", content)
285 | return Markup(html)
286 |
287 | def get_headings(self) -> Sequence[Element]:
288 | """
289 | Return and clear the headings gathered so far.
290 |
291 | Returns:
292 | A list of HTML elements.
293 | """
294 | result = list(self._headings)
295 | self._headings.clear()
296 | return result
297 |
298 | def update_env(self, md: Markdown, config: dict) -> None: # noqa: W0613 (unused argument 'config')
299 | """
300 | Update the Jinja environment.
301 |
302 | Arguments:
303 | md: The Markdown instance. Useful to add functions able to convert Markdown into the environment filters.
304 | config: Configuration options for `mkdocs` and `mkdocstrings`, read from `mkdocs.yml`. See the source code
305 | of [mkdocstrings.plugin.MkdocstringsPlugin.on_config][] to see what's in this dictionary.
306 | """
307 | self._md = md
308 | self.env.filters["highlight"] = Highlighter(md).highlight
309 | self.env.filters["convert_markdown"] = self.do_convert_markdown
310 | self.env.filters["heading"] = self.do_heading
311 |
312 | def _update_env(self, md: Markdown, config: dict):
313 | extensions = config["mdx"] + [_MkdocstringsInnerExtension(self._headings)]
314 |
315 | new_md = Markdown(extensions=extensions, extension_configs=config["mdx_configs"])
316 | # MkDocs adds its own (required) extension that's not part of the config. Propagate it.
317 | if "relpath" in md.treeprocessors:
318 | new_md.treeprocessors.register(md.treeprocessors["relpath"], "relpath", priority=0)
319 |
320 | self.update_env(new_md, config)
321 |
322 |
323 | class BaseCollector(ABC):
324 | """
325 | The base collector class.
326 |
327 | Inherit from this class to implement a collector.
328 |
329 | You will have to implement the `collect` method.
330 | You can also implement the `teardown` method.
331 | """
332 |
333 | @abstractmethod
334 | def collect(self, identifier: str, config: dict) -> CollectorItem:
335 | """
336 | Collect data given an identifier and selection configuration.
337 |
338 | In the implementation, you typically call a subprocess that returns JSON, and load that JSON again into
339 | a Python dictionary for example, though the implementation is completely free.
340 |
341 | Arguments:
342 | identifier: An identifier for which to collect data. For example, in Python,
343 | it would be 'mkdocstrings.handlers' to collect documentation about the handlers module.
344 | It can be anything that you can feed to the tool of your choice.
345 | config: Configuration options for the tool you use to collect data. Typically called "selection" because
346 | these options modify how the objects or documentation are "selected" in the source code.
347 |
348 | Returns:
349 | Anything you want, as long as you can feed it to the renderer's `render` method.
350 | """ # noqa: DAR202 (excess return section)
351 |
352 | def teardown(self) -> None:
353 | """
354 | Teardown the collector.
355 |
356 | This method should be implemented to, for example, terminate a subprocess
357 | that was started when creating the collector instance.
358 | """
359 |
360 |
361 | class BaseHandler:
362 | """
363 | The base handler class.
364 |
365 | Inherit from this class to implement a handler.
366 |
367 | It's usually just a combination of a collector and a renderer, but you can make it as complex as you need.
368 | """
369 |
370 | def __init__(self, collector: BaseCollector, renderer: BaseRenderer) -> None:
371 | """
372 | Initialize the object.
373 |
374 | Arguments:
375 | collector: A collector instance.
376 | renderer: A renderer instance.
377 | """
378 | self.collector = collector
379 | self.renderer = renderer
380 |
381 |
382 | class Handlers:
383 | """
384 | A collection of handlers.
385 |
386 | Do not instantiate this directly. [The plugin][mkdocstrings.plugin.MkdocstringsPlugin] will keep one instance of
387 | this for the purpose of caching. Use [mkdocstrings.plugin.MkdocstringsPlugin.get_handler][] for convenient access.
388 | """
389 |
390 | def __init__(self, config: dict) -> None:
391 | """
392 | Initialize the object.
393 |
394 | Arguments:
395 | config: Configuration options for `mkdocs` and `mkdocstrings`, read from `mkdocs.yml`. See the source code
396 | of [mkdocstrings.plugin.MkdocstringsPlugin.on_config][] to see what's in this dictionary.
397 | """
398 | self._config = config
399 | self._handlers: Dict[str, BaseHandler] = {}
400 |
401 | def get_anchor(self, identifier: str) -> Optional[str]:
402 | """
403 | Return the canonical HTML anchor for the identifier, if any of the seen handlers can collect it.
404 |
405 | Arguments:
406 | identifier: The identifier (one that [collect][mkdocstrings.handlers.base.BaseCollector.collect] can accept).
407 |
408 | Returns:
409 | A string - anchor without '#', or None if there isn't any identifier familiar with it.
410 | """
411 | for handler in self._handlers.values():
412 | try:
413 | anchor = handler.renderer.get_anchor(handler.collector.collect(identifier, {}))
414 | except CollectionError:
415 | continue
416 | else:
417 | if anchor is not None:
418 | return anchor
419 | return None
420 |
421 | def get_handler_name(self, config: dict) -> str:
422 | """
423 | Return the handler name defined in an "autodoc" instruction YAML configuration, or the global default handler.
424 |
425 | Arguments:
426 | config: A configuration dictionary, obtained from YAML below the "autodoc" instruction.
427 |
428 | Returns:
429 | The name of the handler to use.
430 | """
431 | config = self._config["mkdocstrings"]
432 | if "handler" in config:
433 | return config["handler"]
434 | return config["default_handler"]
435 |
436 | def get_handler_config(self, name: str) -> dict:
437 | """
438 | Return the global configuration of the given handler.
439 |
440 | Arguments:
441 | name: The name of the handler to get the global configuration of.
442 |
443 | Returns:
444 | The global configuration of the given handler. It can be an empty dictionary.
445 | """
446 | handlers = self._config["mkdocstrings"].get("handlers", {})
447 | if handlers:
448 | return handlers.get(name, {})
449 | return {}
450 |
451 | def get_handler(self, name: str, handler_config: Optional[dict] = None) -> BaseHandler:
452 | """
453 | Get a handler thanks to its name.
454 |
455 | This function dynamically imports a module named "mkdocstrings.handlers.NAME", calls its
456 | `get_handler` method to get an instance of a handler, and caches it in dictionary.
457 | It means that during one run (for each reload when serving, or once when building),
458 | a handler is instantiated only once, and reused for each "autodoc" instruction asking for it.
459 |
460 | Arguments:
461 | name: The name of the handler. Really, it's the name of the Python module holding it.
462 | handler_config: Configuration passed to the handler.
463 |
464 | Returns:
465 | An instance of a subclass of [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler],
466 | as instantiated by the `get_handler` method of the handler's module.
467 | """
468 | if name not in self._handlers:
469 | if handler_config is None:
470 | handler_config = self.get_handler_config(name)
471 | module = importlib.import_module(f"mkdocstrings.handlers.{name}")
472 | self._handlers[name] = module.get_handler(
473 | self._config["theme_name"],
474 | self._config["mkdocstrings"]["custom_templates"],
475 | **handler_config,
476 | ) # type: ignore
477 | return self._handlers[name]
478 |
479 | @property
480 | def seen_handlers(self) -> Iterable[BaseHandler]:
481 | """
482 | Get the handlers that were encountered so far throughout the build.
483 |
484 | Returns:
485 | An iterable of instances of [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler]
486 | (usable only to loop through it).
487 | """
488 | return self._handlers.values()
489 |
490 | def teardown(self) -> None:
491 | """Teardown all cached handlers and clear the cache."""
492 | for handler in self.seen_handlers:
493 | handler.collector.teardown()
494 | self._handlers.clear()
495 |
496 |
497 | class _IdPrependingTreeprocessor(Treeprocessor):
498 | def __init__(self, md, id_prefix: str):
499 | super().__init__(md)
500 | self.id_prefix = id_prefix
501 |
502 | def run(self, root: Element):
503 | if not self.id_prefix:
504 | return
505 | for el in root.iter():
506 | id_attr = el.get("id")
507 | if id_attr:
508 | el.set("id", self.id_prefix + id_attr)
509 |
510 | href_attr = el.get("href")
511 | if href_attr and href_attr.startswith("#"):
512 | el.set("href", "#" + self.id_prefix + href_attr[1:])
513 |
514 | name_attr = el.get("name")
515 | if name_attr:
516 | el.set("name", self.id_prefix + name_attr)
517 |
518 | if el.tag == "label":
519 | for_attr = el.get("for")
520 | if for_attr:
521 | el.set("for", self.id_prefix + for_attr)
522 |
523 |
524 | class _HeadingShiftingTreeprocessor(Treeprocessor):
525 | regex = re.compile(r"([Hh])([1-6])")
526 |
527 | def __init__(self, md: Markdown, shift_by: int):
528 | super().__init__(md)
529 | self.shift_by = shift_by
530 |
531 | def run(self, root: Element):
532 | if not self.shift_by:
533 | return
534 | for el in root.iter():
535 | match = self.regex.fullmatch(el.tag)
536 | if match:
537 | level = int(match[2]) + self.shift_by
538 | level = max(1, min(level, 6))
539 | el.tag = f"{match[1]}{level}"
540 |
541 |
542 | class _HeadingReportingTreeprocessor(Treeprocessor):
543 | regex = re.compile(r"[Hh][1-6]")
544 |
545 | def __init__(self, md: Markdown, headings: List[Element]):
546 | super().__init__(md)
547 | self.headings = headings
548 |
549 | def run(self, root: Element):
550 | for el in root.iter():
551 | if self.regex.fullmatch(el.tag):
552 | el = copy.copy(el)
553 | # 'toc' extension's first pass (which we require to build heading stubs/ids) also edits the HTML.
554 | # Undo the permalink edit so we can pass this heading to the outer pass of the 'toc' extension.
555 | if len(el) > 0 and el[-1].get("class") == self.md.treeprocessors["toc"].permalink_class:
556 | del el[-1]
557 | self.headings.append(el)
558 |
559 |
560 | class _MkdocstringsInnerExtension(Extension):
561 | def __init__(self, headings: List[Element]):
562 | super().__init__()
563 | self.headings = headings
564 |
565 | def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent method's name)
566 | """
567 | Register the extension.
568 |
569 | Arguments:
570 | md: A `markdown.Markdown` instance.
571 | """
572 | md.registerExtension(self)
573 | md.treeprocessors.register(
574 | _HeadingShiftingTreeprocessor(md, 0),
575 | "mkdocstrings_headings",
576 | priority=12,
577 | )
578 | md.treeprocessors.register(
579 | _IdPrependingTreeprocessor(md, ""),
580 | "mkdocstrings_ids",
581 | priority=4, # Right after 'toc' (needed because that extension adds ids to headers).
582 | )
583 | md.treeprocessors.register(
584 | _HeadingReportingTreeprocessor(md, self.headings),
585 | "mkdocstrings_headings_list",
586 | priority=1, # Close to the end.
587 | )
588 |
--------------------------------------------------------------------------------
/更新日志/version3.md:
--------------------------------------------------------------------------------
1 | ## 更新日志🗓 — v3
2 |
3 |
4 | * v3.9.9 — 2023.6.26
5 |
6 | > 1. download_single_daily和database_update_daily_files新增更新pe_ttm部分
7 | > 2. read_daily新增读取pettm数据
8 | > 3. 修复了pure_moon回测时组数非10组时的画图错误
9 |
10 |
11 | * v3.9.8 — 2023.6.21
12 |
13 | > 1. 修复了read_daily函数中start参数异常的bug
14 | > 2. 给read_h5增加了容错性
15 | > 3. 新增了add_suffix函数,用于给纯数字组成的股票代码添加后缀
16 | > 4. 修复了clip_mad中的偶尔异常
17 | > 5. 修复了to_tradeends在2013年以前异常的bug
18 | > 6. 给pure_fall_frequent在截面整体计算时增加了容错性;并允许select_one_calculate方法返回空值
19 |
20 |
21 | * v3.9.7 — 2023.6.14
22 |
23 | > 1. 将read_daily中交易状态的数据,非正常交易(即值不为1的部分)全部改为nan;将非正常交易日的其他数据改为nan
24 | > 2. 修复了pure_dawn中,使用whole_cross时的bug
25 | > 3. 给clip_mad增加了keep_trend参数,用于将超出3倍mad范围外的异常值替换为均匀分布在3倍~3.5倍区间内的值
26 | > 4. 给pure_moon新增了group_rets_skews和group_rets_skew,用于展示不同分组之间收益的偏度
27 | > 5. 修复了pure_moonnight中comment_yearly方法的结果错误
28 | > 6. 给pure_fall_frequent的from_cross_via_zip装饰器新增了对返回dataframe的支持
29 |
30 |
31 | * v3.9.6 — 2023.6.9
32 |
33 | > 1. 使用read_daily函数读取换手率时,为0的值替换为nan
34 | > 2. 给corr_two_daily新增了daily和method参数,用于指定是否每天计算以及计算方式
35 | > 3. 给cov_two_daily新增了daily参数,用于指定是否每天计算
36 | > 4. 给clip_mad函数新增了replace参数,用于指定将超出临界值的值替换为临界值(否则删除)
37 | > 5. 修复了download_single_daily下载换手率数据中的bug
38 | > 6. 修复了pure_moon中回测绩效因子覆盖率指标的计算错误
39 |
40 |
41 | * v3.9.5 — 2023.5.16
42 |
43 | > 1. 给ClickHouseClient的get_data方法、Questdb的write_via_df方法、get_data_with_tuple方法新增了retry(用以支持pure_fall_frequent的并行)
44 | > 2. 修正了database_update_daily_files中检查历史文件的错误
45 | > 3. 将pure_coldwinter中展示的相关系数统一为spearman相关系数
46 | > 4. 修复了pure_dawn更新历史数据的bug
47 |
48 |
49 | * v3.9.4 — 2023.5.9
50 |
51 | > 1. 优化了read_daily函数中start参数的类型提示
52 | > 2. 删去了一些不必要的加载
53 | > 3. 给pure_moonnight、test_on_index_four_out、test_on_300500、test_on_index_four、pure_fama、follow_tests新增了total_cap参数,表示加权和行业市值中性化时使用总市值,而非流通市值
54 | > 4. 将test_on_index_four_out、test_on_300500、test_on_index_four、pure_fama、follow_tests中的value_weighted或index_member_value_weighted参数默认值修改为1
55 | > 5. 新增了symmetrically_orthogonalize函数,用于给一组因子做对称正交
56 | > 6. 新增了icir_weight函数,用于给一组因子做ICIR加权合成
57 | > 7. 新增了scipy_weight函数,用于给一组因子做优化求解版本的ICIR加权合成
58 | > 8. 新增pure_moon_b、pure_week_b、pure_moon_c、pure_week_c类,用于辅助回测
59 |
60 |
61 | * v3.9.3 — 2023.4.24
62 |
63 | > 1. 给pure_moonnight回测绩效,新增“多头超均收益”和“多头收益占比”两个指标,删去了原有的“中心化后偏度”和“二阶自相关性”指标
64 | > 2. 给test_on_index_four_out、test_on_300500、test_on_index_four新增了value_weighted参数,用来决定指数成分股内的测试是否用流通市值加权
65 | > 3. 给follow_tests新增index_member_value_weighted参数,用来决定指数成分股内的测试是否用流通市值加权
66 |
67 |
68 | * v3.9.2 — 2023.4.24
69 |
70 | > 1. 修正了download_single_daily和database_update_daily_files函数中的bug
71 | > 2. 给make_relative_comments和make_relative_comments_plot新增了all_a参数,表示相对中证全指的超额
72 | > 3. 新增test_on_index_four_out函数,用于输出因子在指数成分股内的多头超额绩效表现到xlsx文件中
73 |
74 | * v3.9.1 — 2023.3.25
75 |
76 | > 1. 给read_market和read_index_single函数新增了questdb_host参数
77 | > 2. 将database_update_barra_files改为从米筐更新,将原database_update_barra_files函数改为database_update_barra_files_dcube
78 | > 3. 给pure_fall_frequent和pure_star新增了questdb_host参数,用于指定questdb数据库的host
79 | > 4. 修复了follow_tests中手续费部分绩效写入excel的bug
80 | > 5. 删去了对依赖库numpy的版本限制
81 |
82 | * v3.9.0 — 2023.3.23
83 |
84 | > 1. 删去了Questdb中的write_via_df_old方法
85 | > 2. 给read_daily函数新增了如下参数
86 | > * vwap:平均成交价
87 | > * total_sharenum:总股数
88 | > * amount:原volumes,成交量
89 | > * total_cap:总市值
90 | > * adjfactor:复权因子
91 | > * stop_up:涨停价
92 | > * stop_down:跌停价
93 | > * zxindustry_dummy_code:中信行业哑变量代码版
94 | > * zxindustry_dummy_name:中信行业哑变量名称版
95 | > * swindustry_dummy:申万行业哑变量
96 | > * hs300_member_weight:沪深300成分股权重
97 | > * zz500_member_weight:中证500成分股权重
98 | > * zz1000_member_weight:中证1000成分股权重
99 | > 3. 修复了ge t_list_std中计算的错误
100 | > 4. 在download_single_day和database_update_daily_files中新增了对上述新数据的支持
101 | > 5. 新增了database_update_index_weight函数来更新函数成分股权重
102 | > 6. 给如下回测相关的类和函数新增了trade_cost_double_side参数,用于设置双边手续费率
103 | > * daily_factor_on_industry
104 | > * group_test_on_industry
105 | > * pure_moonnight
106 | > * pure_newyear
107 | > * test_on_300500
108 | > * test_on_index_four
109 | > 7. 给follow_tests函数新增了trade_cost_double_side_list,用于一次性比较多个不同手续费率下的策略表现
110 | > 8. 给如下回测相关的类和函数新增了opens_average_first_day参数,用于设置买入时以当日成交均价(vwap)买入
111 | > * pure_moonnight
112 | > * follow_tests
113 | > * test_on_300500
114 | > * test_on_index_four
115 | > 9. 对pure_moonnight中的回测,不再向后补全一期开收盘价
116 | > 10. 调整了pure_moonnight换手率的展示方式,改为多头组的月均换手率
117 | > 11. 给pure_moonnight新增了comment_yearly方法,用于逐年产生绩效评价
118 | > 12. 新增pure_moon_a和pure_week_a类,用于辅助回测
119 | > 13. 新增了pure_linprog,用于对打分预测的指标,进行线性规划求解
120 |
121 | * v3.8.6 — 2023.3.17
122 |
123 | > 1. 修正了get_fac_via_corr中riskmetrics方法的权重值
124 | > 2. 将get_fac_via_corr的非daily状态下的返回值index改到了月底
125 | > 3. 新增了boom_one函数,仅计算20天均值
126 | > 4. 修复了show_corrs_with_old指定new=1时,method参数失效的bug
127 | > 5. 将依赖库中numpy版本限制在1.23.5以下
128 |
129 | * v3.8.5 — 2023.3.16
130 |
131 | > 1. 自动识别操作系统,并安装对应库
132 |
133 | * v3.8.4 — 2023.3.16
134 |
135 | > 1. 修复了在python3.8以下版本报错的bug
136 |
137 | * v3.8.3 — 2023.3.16
138 |
139 | > 1. 改变调用加载方式,不初始化也可以使用部分功能
140 | > 1. 引入Dict类型代替dict
141 | * v3.8.2 — 2023.3.15
142 | > 1. 新增了get_list_std_weighted,用来计算多个df的加权标准差
143 | > 2. 调整了get_fac_via_corr、show_corr、pure_moon的spearman相关系数的计算方法
144 | > 3. 修复了FactorReader初始化的bug
145 | > 4. 将FactorReader的update_factor方法改为了可以在外部调用
146 | > 5. 给FactorReader新增了add_token方法
147 | > 6. 调整了make_relative_comments和make_relative_comments_plot的综合指数的走势的计算方法
148 | > 7. 去除了show_covs中的冗余参数
149 | > 8. 给show_corrs_with_old新增了only_new参数,默认仅测试新因子与其余旧因子之间的相关性
150 | > 9. 给pure_snowtrain新增了show_corr方法,用来更好展示因子与风格因子之间的相关性
151 | > 10. 修复了test_on_300500和test_on_index_four方法中分组数量不为10的bug
152 |
153 | * v3.8.1 — 2023.3.8
154 |
155 | > 1. 修复了get_fac_via_corr和get_fac_cross_via_func更新时使用数据错误的问题
156 | > 1. 新增了FactorReader模块,可向云端数据库写入新数据,功能与factor_reader==0.3.3的写入部分一致
157 | * v3.8.0 — 2023.3.1
158 | > 1. 修复了同一host下的questdb多次备份的bug
159 | > 1. 修复了Questdb恢复备份的名称不同的bug
160 | > 1. 修复了pure_fall_frequent被打断继续后,读取备份表格异常的bug
161 | * v3.7.9 — 2023.2.28
162 | > 1. 修复了func_two_daily的history参数为None时,无法计算的bug
163 | > 1. 给pure_dawn的run方法新增了history、daily和whole_cross参数,分别用于指定历史文件、是否每日计算、是否使用横截面全部股票一起计算
164 | > 1. 给pure_dawn新增了for_cross_via_zip的参数,用于处理返回多个series的情形
165 | * v3.7.8 — 2023.2.27
166 | > 1. 给pure_fall_frequent的读取questdb备份表时增加了去除重复行的功能
167 | > 1. 将database_update_minute_data_to_postgresql移至future_version模块中,删去了其默认加载
168 | > 1. 优化了database_update_minute_data_to_clickhouse_and_questdb、database_update_minute_data_to_questdb、database_update_barra_files、database_update_money_flow、database_update_zxindustry_member在无更新数据或重复更新时的警告提示
169 | * v3.7.7 — 2023.2.23
170 | > 1. 修复了Questdb的get_data_with_tuple方法,读取数据异常的bug
171 | > 1. 对pure_fall_frequent的get_daily_factors方法新增了n_jobs参数,用于开启基于mpire的并行加速
172 | > 1. 删去了pure_fall_frequent的get_daily_factors方法中,tqdm_inside的参数,只能在外部添加总进度条
173 | > 1. 新增了mpire依赖库,暂时删去了numpy的依赖库
174 | * v3.7.6 — 2023.2.23
175 | > 1. 修复了pure_fall_frequent打断后重读备份表的bug
176 | > 1. Questdb新增了copy_all_tables和upload_all_copies方法,用于备份和恢复数据库
177 | * v3.7.5 — 2023.2.23
178 | > 1. import pure_ocean_breeze时如遇到错误,增加显示了错误原因
179 | > 1. import pure_ocean_breeze时新增了自动检查更新功能
180 | > 1. Questdb的write_via_df方法新增了tuple_col参数,用于写入数值类型为元组或列表的列
181 | > 1. Questdb新增了get_data_with_tuple方法,用于读取数据类型包含元组或列表的表格
182 | > 1. database_read_primary_factors函数新增了name2参数,用于读取多个parquet文件存储的初级因子
183 | > 1. 删去和警告了pure_fall中冗余的方法
184 | > 1. 修正了pure_fall_frequent中,因备份写入方式变化导致的bug
185 | > 1. 移除了pure_fall_flexible的默认加载,转移至future_version中
186 | > 1. 补充了pure_snowstrain的说明文档
187 | > 1. 新增了依赖库questdb
188 | * v3.7.4 — 2023.2.22
189 | > 1. Questdb的写入方式将原write_via_df改为write_via_df_old,新增了通过questdb.ingress.Sender写入的方式write_via_df,并将所有写入方式都改为了write_via_df
190 | > 1. 修复了使用分钟数据计算因子值时,单日数据重复写入的bug
191 | * v3.7.3 — 2023.2.21
192 | > 1. 删去了do_on_dfs装饰器对get_list_std函数的支持
193 | > 1. 修复了database_update_minute_data_to_clickhouse_and_questdb和database_update_minute_data_to_questdb在端口号为9000的设备上的写入bug,并增加了web_port参数,用于手动指定端口参数
194 | > 1. 给do_on_dfs装饰器支持了对tuple的识别
195 | * v3.7.2 — 2023.2.3
196 | > 1. 修复了Questdb通过csv写入数据只能向本地数据库写入的bug
197 | > 1. 对make_relative_comments和make_relative_comments_plot的进行了提速,并新增了多种股票池混合的方式,可以指定多个参数为1,当且仅当hs300和zz500同时为1时,基准指数为中证800指数000906.SH
198 | > 1. 对daily_factor_on300500删去了zz800参数,新增支持多个参数同时为1,此时为合并股票池
199 | > 1. 修复了remove_unavailable对月频因子报错的bug
200 | > 1. 给test_on_300500和test_on_index_four新增了group_num参数,用于指定分组回测时的分组数量
201 | > 1. 修复了do_on_dfs装饰器中,第一个参数不能为None的bug
202 | * v3.7.1 — 2023.1.22
203 | > 1. 给MetaSQLDriver的get_data增加了尝试10次后再报错的设定,以避免偶然出现的连接失败错误
204 | > 1. 修复了pure_fall_frequent中使用questdb调取数据的命令语句的bug
205 | > 1. 修复了pure_fall_frequent中使用questdb调取数据的数据类型的bug
206 | * v3.7.0 — 2023.1.20
207 | > 1. 修复了Questdb通过csv写入表格时,进程占用文件,导致无法删除的bug
208 | > 1. 给read_market、read_index_single、database_update_minute_data_to_clickhouse_and_questdb、database_update_minute_data_to_questdb增加了自动调整web_port参数值的机制
209 | > 1. 给pure_fall_frequent增加了questdb_web_port参数值,用于指定本台设备questdb的web_port值
210 | > 1. 修复了pure_rollingols中betas属性显示异常的bug
211 | * v3.6.9 — 2023.1.19
212 | > 1. 对drop_duplicates_index函数,增加了保留原来index名字的功能
213 | > 1. 删去了pure_moon回测中的弹窗提示
214 | > 1. 补充了一些依赖库
215 | > 1. 修正了一些函数说明
216 | * v3.6.8 — 2023.1.13
217 | > 1. 修复了初始化存在的bug
218 | * v3.6.7 — 2023.1.13
219 | > 1. 修复了Questdb中关于web_port参数的bug
220 | > 1. 修复了database_update_minute_data_to_questdb的bug
221 | >
222 | * v3.6.6 — 2023.1.10
223 | > 1. 新增了database_update_industry_rets_for_stock函数,用于生成每只股票当天对应的一级行业的收益率
224 | > 1. 给read_daily函数新增了swindustry_ret和zxindustry_ret参数,可以读取每只股票当天对应的一级行业的收益率数据
225 | * v3.6.5 — 2023.1.6
226 | > 1. 给Questdb初始化新增了web_port参数,用于表示控制台的端口号
227 | > 1. 给read_daily函数新增了money参数用于读取每日个股成交额、illiquidity参数用于读取每日个股非流动性
228 | > 1. 新增了database_update_illiquidity函数用于更新每天非流动性数据
229 | > 1. 删去了pure_moon中的select_data_time方法,在set_basic_data函数中新增了基础数据为None时从本地读入的方法
230 | > 1. 优化了pure_moonnight因子值时间超过基础数据时间时,结果的展示方式
231 | > 1. 优化了pure_moonnight的运算逻辑,对回测进行提速,并恢复了time_start和time_end参数的使用,可以为每次回测单独设定回测区间
232 | > 1. 修复了do_on_dfs装饰器在仅作用于一个目标时,参数不生效的bug
233 | * v3.6.4 — 2022.12.29
234 | > 1. 新增了do_on_dfs装饰器,用于将一个作用于单个dataframe的函数,改造为可以分别对多个dataframe运算,dataframe须处于第一个参数的位置,此外如果对每个dataframe,后续的某个参数各不相同,可使用列表依次输入。
235 | > 2. 修复了clip函数的bug
236 | > 3. 新增了judge_factor_by_third函数,用于依据第三个指标的正负,对两个因子进行筛选合成,第三个指标为正则取因子1的值,为负则取因子2的值
237 | > 4. 给pure_fall_frequent的run函数新增了many_days参数,用于对每天取之前的n天的分钟数据来计算因子,当groupby_target指定为`['date','code']`或`['code']`时,则不涉及截面,每只股票各自算自己的因子值,返回值应为float、list、tuple;当groupby_target指定为`[]`时,则取多天的截面数据,返回值应为pd.Series(index为股票代码,values为因子值),或多个这样的pd.Series
238 | > 5. 给pure_fall_frequent新增了drop_table函数,用于删去计算到一半被打断后,不想要的questdb暂存表格
239 | > 6. 新增get_group函数,使用groupby的方法,将一组因子值改为截面上的分组值,此方法相比qcut的方法更加稳健,但速度更慢一些
240 | * v3.6.3 — 2022.12.25
241 | > 1. 修复了database_read_final_factors函数读取周频因子数据的bug
242 | > 1. 修复了merge_many及其他相关函数的bug
243 | > 1. 将corr_two_daily、com_two_daily、func_two_daily的默认并行数改为了1
244 | > 1. 将show_corrs函数不再使用print打印相关系数表格,而是直接返回,此外print_bool参数的函数改为绝对是否将返回值的数值变为百分数
245 | > 1. 修复了show_corrs_with_old时的数字混乱bug
246 | * v3.6.2 — 2022.12.18
247 | > 1. 给read_daily函数增加了vol_daily参数,用于读取使用分钟收益率的标准差计算的每日波动率
248 | > 1. 使read_daily读出的数据不再包含全为空值的行
249 | > 1. 修复了因子标号跳跃导致的show_corrs_with_old函数显示不全的bug
250 | * v3.6.1 — 2022.12.14
251 | > 1. 修复了drop_duplicates_index的bug
252 | > 1. 给pure_moonnight增加了without_breakpoint参数,用于控制iplot画图忽略空格
253 | > 1. 新增了和pure_moon功能一模一样的pure_week,并修复了在同一进程中,不能使用pure_moonnight同时进行月频回测和周频回测的bug
254 | * v3.6.0 — 2022.12.13
255 | > 1. 读取市场指数数据的函数read_market不再限定只能读取中证全指的数据,改为可以通过market_code参数读取任何指数数据
256 | > 1. 修复了read_market函数中使用questdb读取最高价、最低价、开盘价数据的bug,以及不能指定起始时间的bug
257 | > 1. 给read_index_single函数增加了说明
258 | > 1. 向读取h5相关文件函数添加了下线警告
259 | > 1. 给convert_code函数增加了把wind代码转化为米筐代码的功能
260 | > 1. 修复了drop_duplicates_index函数在某些情况下可能出现bug的问题
261 | > 1. 新增了对因子去极值的函数clip,以及三个细分函数clip_mad、clip_three_sigma、clip_percentile
262 | > 1. 修复了pure_moon绩效输出到excel中时,信息比率写入错误的bug
263 | > 1. 给pure_fall_frequent新增了project参数,用于标记每个因子所属于的项目,便于管理
264 | > 1. 修复了pure_fall_frequent打断后重新运行后可能在的bug
265 | > 1. 删去了pure_fall_frequent中关于“共x段”的显示
266 | * v3.5.9 — 2022.11.29
267 | > 1. 修复了读取不复权价格数据时的bug
268 | >
269 | > 2. 新增了将所有因子截面上减去最小值,使其变为非负数的函数all_pos
270 | >
271 | > 3. 新增了对日票或月频因子值剔除st股、停牌股、上市不足60天的函数remove_unavailable
272 | >
273 | > 4. pure_moon类中set_basic_data函数的参数值不再必填
274 | >
275 | > 5. 修复了pure_moon不做行业市值中性化时,factors_out属性的columns异常的问题
276 | >
277 | > 6. 将pure_moon回测的绩效指标的`RankIC均值t值`改为了`RankIC.t`
278 | >
279 | > 7. pure_moon回测新增了6个绩效指标
280 | > > * self.factor_cover:因子覆盖率,为因子的数量与复权开盘价数据的数量的比值
281 | > > * self.pos_neg_rate:因子正值占比,为原始因子值中,为正数的数量占非零值数量的比值
282 | > > * self.factor_cross_skew:因子截面偏度,为原始因子值每个截面的偏度序列的均值,为0则为正态分布,为正则右偏(右边尾巴长),小于零则左偏(左边尾巴长)
283 | > > * self.factor_cross_skew_after_neu:中性化后偏度,对因子进行行业市值中性化之后,平均截面偏度
284 | > > * self.corr_itself:一阶自相关性,原始因子值与其上一期的因子值之间的对应期截面spearman相关系数的均值
285 | > > * self.corr_itself_shift2:二阶自相关性,原始因子值与其上两期的因子值之间的对应期截面spearman相关系数的均值
286 | > 8. 取消了原来的每月换手率变化曲线,新增了每月原始因子值的截面标准差的柱状图
287 | > 8. pure_moon中的回测结果使用cufflinks画图时(即iplot为1时),默认不再显示图例,即ilegend默认值改为了0
288 | > 8. 调整了pure_moon类中set_basic_data函数的形参名称
289 | > 8. 修复了pure_fall_frequent中,暂存至questdb时,表名中带有特殊符号会无法删除表格的bug
290 | >
291 | * v3.5.8 — 2022.11.19
292 | > 1. 给database_read_final_factors和database_save_final_factors新增了`freq`参数,可以指定为'月'或'周',即可存入月频因子数据(滚动20天)和周频因子(滚动5天)数据
293 | > 2. 新增了`计算连续期数`函数,可以用于计算某个指标连续大于或连续小于某个阈值的期数
294 | > 3. 优化了test_on_index_four中,对多头超额净值曲线的显示
295 | * v3.5.7 — 2022.11.17
296 | > 1. 新增了将日频因子进行面板操作以月度化的函数get_fac_via_corr、get_fac_cross_via_func
297 | > 1. 给pure_moon的评价指标新增了RankIC胜率
298 | > 1. 修复了在分钟数据某日出现特殊情况时,会导致pure_fall_frequent在写入questdb时发生错误的bug
299 | * v3.5.6 — 2022.11.14
300 | > 1. 删去了`Homeplace.__slots__`中的minute_data_file、daily_enddate、minute_enddate,以修复新用户初始化后导入失败的bug
301 | * v3.5.5 — 2022.11.13
302 | > 1. 删去了change_index_name函数,可以通过形如`df.index.name='date'`的代码直接指定
303 | > 1. 在write_data模块中,新增了连接米筐失败时,输出失败原因,提供是否等待的选项,并设置了在30秒、60秒、60秒内重连三次的尝试
304 | > 1. 修复了rankic_test_on_industr、follow_tests函数在不指定comments_writer和net_values_writer时报错的bug
305 | > 1. 在pure_ocean_breeze.withs.requires中连接米筐失败时,将给出原因
306 | * v3.5.4 — 2022.11.11
307 | > 1. 给merge_many增加了how参数,可以指定拼接的方式,默认为outer
308 | > 1. 新增zip_many_dfs函数,用于将多个dataframe拼在一起,相同index和columns指向的那个values,连在一起,组成列表
309 | > 1. 新增get_values函数,用于一次性取出一个values为列表的dataframe的所有值,分别设置一个新的dataframe
310 | > 1. 给comment_on_rets_and_nets和comment_on_twins新增参数counts_one_year,用于设置序列的频率
311 | > 1. 新增了boom_fours函数,用于对一个列表的因子值分别算boom_four
312 | * v3.5.3 — 2022.11.6
313 | > 1. 将所有通过is_notebook判断使用何种进度条的函数都改为了通过tqdm.auto模块自动判断
314 | > 1. 将重复更新日频数据database_update_daily_files函数设置为不再报错
315 | > 1. 修复了pure_fall_frequent只更新一天数据时不写入questdb的bug
316 | > 1. 修复了使用pure_fall_frequent进行截面操作时,使用for_cross_via_zip装饰器,只返回一个Series时的bug
317 | > 1. 将pure_ocean_breeze.withs.requires中导入的进度条模块改为tqdm.auto
318 | > 1. 向依赖库中新增了tradetime,删去了SciencePlots、alphalens和cachier
319 | * v3.5.2 — 2022.11.6
320 | > 1. pure_moon和pure_moonnight新增freq参数,可以选择'M',或'W',进行月频测试或周频测试
321 | > 1. 新增frequency_controller类,对回测中不同频率的操作和参数进行控制
322 | > 1. 修复了预存储月度交易状态和st状态的错误
323 | > 1. 优化了pure_moon中部分算法
324 | > 1. pure_ocean_breeze.withs.requires中新增了`import tradetime as tt`用于进行交易日历上的相关操作
325 | * v3.5.1 — 2022.11.5
326 | > 1. 修复了database_update_minute_data_to_clickhouse_and_questdb中的get_price接口变化导致的bug
327 | > 1. 删去了plt.style的设定代码
328 | > 1. 修复了pure_ocean_breeze.withs.requires中导入Iterable的bug
329 | * v3.5.0 — 2022.11.5
330 | > 1. 替换了所有的feather文件读写和存储,改为parquet格式,优化了索引相关的操作
331 | > 1. 优化了pure_moon中关于因子处理的步骤
332 | > 1. legacy_version中收录了v3p4,即3.4.8版本,为最后一个通过feather文件读写的版本
333 | > 1. 新增了feather_to_parquet_all函数,一键将数据库中的所有feather文件都转化为parquet文件
334 | > 1. 删去了依赖包feather,新增了依赖包pyarrow和clickhouse_sqlalchemy
335 | * v3.4.8 — 2022.11.4
336 | > 1. 修复了pure_moon市值加权回测的bug
337 | > 1. 优化了pure_moon的on_paper参数下,学术化评价指标的内容
338 | > 1. 给pure_moonnight增加了
339 | > > * 识别pure_ocean_breeze.state.states中的ON_PAPER参数,用来对环境中所有的回测展示学术化的评价指标
340 | > > * 识别pure_ocean_breeze.state.states中的MOON_START参数,用来对环境中所有的回测的因子规定起点
341 | > > * 识别pure_ocean_breeze.state.states中的MOON_END参数,用来对环境中所有的回测的因子规定终点
342 | > 4. 给pure_ocean_breeze.state.states中增加了ON_PAPER、MOON_START、MOON_END全局参数
343 | > 4. 新增了feather_to_parquet文件,可以将某一文件夹下的所有feather文件转化为parquet文件
344 | > 4. 修复了pure_cloud中的bug
345 | > 4. 修复了pure_ocean_breeze.future_version.in_thoughts模块的导入错误
346 | > 4. 扩展了pure_moonnight的输入因子的类型范围,当前pure_fallmount、pure_snowtrain和dataframe都可以直接输入
347 | * v3.4.7 — 2022.11.4
348 | > 1. 删去了h5py这一依赖项,改为只有在调用read_h5和read_h5_new函数时才会import,并从pure_ocean_breeze.withs.requires中剔除
349 | > 1. 修复了collections中Iterable加载在python3.11版本中的bug
350 | > 1. 修复了pure_fall_frequent.for_cross_via_zip装饰器的bug
351 | > 1. 剔除了对pretty_errors的依赖项
352 | * v3.4.6 — 2022.11.2
353 | > 1. 给read_money_flow增加了whole参数,用于读入当天各类型投资者买卖总量
354 | > 1. 新增了same_columns和same_index函数,取很多dataframe,都只保留共同的columns或index的部分
355 | > 1. 新增了show_x_with_func函数,用于考察两个因子同期截面上的某种关系,并返回一个时间序列
356 | > 1. 新增了show_cov和show_covs函数,考察两个因子或多个因子截面协方差关系
357 | > 1. 将pure_moon修改为,默认不再读入申万一行业哑变量数据,只读入中信一级行业哑变量数据
358 | > 1. 优化了pure_moon回测结果展示图的位置
359 | > 1. 修复了pure_fall_frequent进行截面计算时的bug
360 | > 1. 给pure_fall_frequent新增了for_cross_via_str和for_cross_via_zip装饰器,用于简化截面计算时的对返回值处理的代码
361 | * v3.4.5 — 2022.11.1
362 | > 1. 修复了read_daily中读取不复权最低价时的bug
363 | > 1. 修复了database_update_daily_files更新st股哑变量时的bug
364 | > 1. 修复了pure_moon读取月度状态文件的bug
365 | > 1. 修复了pure_fall_frequent计算单一一天因子值时的bug
366 | > 1. 给pure_ocean_breeze.withs.requires模块中增加了time库
367 | * v3.4.4 — 2022.10.31
368 | > 1. 修复了test_on_index_four使用matplotlib画图不显示的bug
369 | > 1. 新增了择时回测框架pure_star
370 | * v3.4.3 — 2022.10.31
371 | > 1. 给read_index_three增加了国证2000指数的行情数据
372 | > 1. 给make_relative_comments增加了gz2000参数,用于计算相对国证2000指数的超额收益;增加了show_nets参数,用于同时返回多头超额评价指标和超额净值数据
373 | > 1. 给make_relative_comments_plot增加了gz2000参数,用于绘制相当于国证2000指数的超额净值走势图
374 | > 1. 给show_corr增加了show_series参数,将返回值从相关系数的均值改为了相关系数的序列,并取消绘图
375 | > 1. 给pure_moon和pure_moonnight增加了swindustry_dummy和zxindustry_dummy参数,用于自己输入申万一级行业哑变量数据和中信一级行业哑变量数据
376 | > 1. 修复了不同回测结果的IC序列相同的bug
377 | > 1. 给pure_fall_frequent增加了ignore_history_in_questdb的参数,用于被打断后,忽略在questdb中的暂存记录,重新计算;新增了groupby_target参数,用于指定groupby分组计算因子值时,分组的依据(即df.groupby().apply()中groupby里的参数),此改进使得可以进行截面上的构造和计算
378 | > 1. 给test_on_300500增加了gz2000参数,用于测试在国证2000成分股内的多空和多头超额效果
379 | > 1. 恢复了test_on_index_four中的gz2000参数,用于测试国证2000成分股内的效果;优化了多头超额的绩效展示,整合了超额净值曲线图与一张上,使用cufflinks进行显示(可以通过iplot参数关闭cufflinks显示)
380 | * v3.4.2 — 2022.10.29
381 | > 1. 新增了is_notebook函数,可以判断当前环境是否为notebook
382 | > 1. 将除write_data模块以外,其他模块的进度条,都改为可以自动识别环境为notebook,如果是notebook,则自动使用tqdm_notebook的进度条
383 | * v3.4.1 — 2022.10.26
384 | > 1. 给func_two_daily、corr_two_daily、cov_two_daily增加了history参数,用于将计算出的结果记录在本地
385 | > 1. 给show_corrs、show_corrs_with_old函数增加了method参数,可以修改求相关系数的方式
386 | > 1. 暂时删去了test_on_300500的国证2000的参数
387 | > 1. 给test_on_300500和test_on_index_four新增了iplot参数,决定是否使用cufflinks画图
388 | * v3.4.0 — 2022.10.25
389 | > 1. 修复了拼接多个dataframe的函数merge_many中的bug
390 | > 1. 修复了导入process模块时的bug
391 | > 1. 新增了同时测试因子在单个宽基指数成分股上的多空和多头超额表现的函数test_on_3005000
392 | > 1. 新增了同时测试因子在4个宽基指数成分股上的多空和多头超额表现的函数test_on_index_four
393 | * v3.3.9 — 2022.10.24
394 | > 1. 修复了使用clickhouse中get_data时的连接bug
395 | > 1. 修复了拼接多个dataframe的函数merge_many中的bug
396 | > 1. 给func_two_daily、corr_two_daily增加了n_jobs参数,用于决定并行数量
397 | > 1. 新增了滚动求两因子协方差的函数cov_two_daily
398 | > 1. 新增了求目标因子与已发研报因子之间的相关系数的函数show_corrs_with_old
399 | > 1. 修复了pure_fall_frequent计算因子值被打断后,从questdb读取已经计算的因子值时潜在的bug
400 | * v3.3.8 — 2022.10.21
401 | > 1. 对clickhouse、questdb、postgresql数据库的get_data方法增加了只获取np.ndarray的参数
402 | > 1. 给pure_moon增加wind_out属性,用于输出每个时期股票所属分组
403 | > 1. 将pure_fall_frequent中,计算单日因子值的进度条删去
404 | * v3.3.7 — 2022.10.10
405 | > 1. 修复了使用pure_fall(mysql)的分钟数据,更新因子值时的,读取之前因子数据的bug
406 | * v3.3.6 — 2022.10.09
407 | > 1. 新增了剔除北交所因子数据的函数debj
408 | > 1. 新增了对因子做横截面zscore标准化的函数standardlize
409 | > 1. 新增了统计dataframe中有多少(非0)非空数据的函数count_value
410 | > 1. 优化了检测dataframe中是否存在空值的函数detect_nan的计算方法
411 | > 1. 新增了使用若干因子对目标因子正交化的函数de_cross
412 | > 1. 对pure_moon和pure_moonnight新增了使用cufflinks展示回测结果和绘图的参数iplot和ilegend,优化了结果展示的样式
413 | > 1. 修复了pure_fama中,不包含市场因子时的潜在bug
414 | > 1. 对pure_fama中的coefficients属性进行保护
415 | > 1. 新增了pure_rollingols类,用于对若干个因子,对应股票下,进行固定时间窗口的滚动回归
416 | > 1. 调整了一些工具函数的分类,以减少循环引用bug的可能性
417 | * v3.3.5 — 2022.10.07
418 | > 1. 给pure_fall_frequent增加了中途写入questdb防止运算被打断的功能,并可以在打断后通过识别questdb中的数据继续计算,该临时表将在运算全部完成,并成功写入feather文件后删除
419 | > 1. 在Questdb的写入函数write_via_csv的临时csv文件名中,加入了随机数,以避免同时写入文件时的冲突
420 | > 1. 给drop_duplicates_index增加说明,并移至data.tools模块
421 | > 1. 修复了一个讲分钟数据写入postgresql的bug
422 | > 1. 修复了市值中性化函数decap,读取流通市值数据的bug
423 | > 1. 给pure_moon模块的初始化函数参数no_read_indu增加默认参数0,以修复条件双变量排序类pure_newyear的bug
424 | > 1. 修复了pure_fall_frequent中,tqdm_inside不为1时,无法计算第一个交易日的数据的bug
425 | * v3.3.4 — 2022.10.06
426 | > 1. 删去了回测类pure_moon和pure_moonnight中起始日期startdate参数,以避免输入因子值起点不同,会导致缓存失效的bug
427 | > 1. 优化了pure_fama的参数逻辑,在输入add_markert_series参数时,自动将add_market参数指定为1
428 | > 1. 新增了merge_many函数,将多个index为时间,columns位股票代码的dataframe拼接在一起,变成一个长表
429 | > 1. 新增函数func_two_daily,用于对两个index为时间,columns为股票代码的dataframe,每只股票下,各自沿着时间序列,做某个函数操作,最终得到一个index为时间,columns为股票代码的dataframe
430 | > 1. 新增func_two_daily的特例函数,corr_two_daily,求两个因子同一股票滚动窗口下的时序相关系数
431 | * v3.3.3 — 2022.10.01
432 | > 1. 将读取300、500、1000指数的行情read_index_three改为从分钟数据读取
433 | > 1. 给读取市场行情(中证全指)行情read_market增加从questdb读取
434 | > 1. 删除了pure_fall_frequent中使用postgresql中分钟数据计算因子值的选项
435 | > 1. 修复了pure_fall_frequent中使用questdb中的分钟数据计算因子值的部分
436 | * v3.3.2 — 2022.10.01
437 | > 1. 修复了读取隔夜收益率的bug
438 | > 1. 将更新特质收益率数据的起始时间点改为2010年1月1日,并修复了其中的bug
439 | > 1. 给pure_moon和pure_moonnight增加了no_read_indu参数,使回测时不必读入行业哑变量数据,便于调试
440 | > 1. 给pure_moon和pure_moonnight增加了only_cap参数,使回测时只做市值中性化,而不做行业中性化
441 | > 1. 优化了pure_moon和pure_moonnight的参数逻辑,当neutralize和boxcox均为0时,自动开启no_read_indu参数;当no_read_indu和only_cap任一为0时,自动开启另一个参数
442 | * v3.3.1 — 2022.10.01
443 | > 1. 给一键导入库的requires中,增加了import pyfinance.ols as go
444 | > 1. 增加了用于fama三因子与特质收益率相关的类pure_fama,可以计算各期因子收益率、个股各期特质收益率、个股各期因子暴露、超额收益率等内容
445 | > 1. 更新每日数据函数database_update_daily_files增加了更新每日的市盈率pe和市净率pb的数据
446 | > 1. 新增更新每日以20日回归,市场收益率、流通市值分三份、市净率分三份,计算的特质收益率数据
447 | > 1. 一键读取日频数据函数read_daily新增参数ret(日间收益率)、ret_inday(日内收益率)、ret_night(隔夜收益率)、vol(滚动20日波动率)、vol_inday(滚动20日日内收益率波动率)、vol_night(滚动20日隔夜收益率波动率)、swing(振幅)、pb(市净率)、pe(市盈率)、iret(20日回归,市场、流通市值、市净率三因子特质收益率)、ivol(滚动20日,20日回归,市场、流通市值、市净率三因子特质波动率)
448 | > 1. 增加了将dataframe第一列设置为index的函数set_index_first
449 | > 1. 增加了修改index的名字的函数change_index_name
450 | > 1. 丰富了回测类pure_moon和pure_moonnight的评价指标及绘图,新增属性self.ics(ic时间序列)、self.rankics(rankic时间序列)、factor_turnover_rates(每月换手率时间序列)、factor_turnover_rate(平均每月换手率)、group_rets_std(每组组内收益率的标准差)、group_rets_stds(每组组内收益率的标准差的时间序列),新增Rank IC时序图和换手率时序图,美化了评价指标的输出形式
451 | * v3.3.0 — 2022.09.26
452 | > 1. 修复了单独更新questdb内分钟数据的函数database_update_minute_data_to_questdb中的bug
453 | > 1. 新增了依据index去重的函数drop_duplicates_index
454 | > 1. 修复了更新日频数据可能重复的潜在bug
455 | * v3.2.9 — 2022.09.26
456 | > 1. 给pure_helper增加说明
457 | > 1. 修复了以mysql分钟数据更新因子值的类pure_fall的出现重复数据的潜在bug
458 | * v3.2.8 — 2022.09.26
459 | > 1. 修复了用分钟数据计算因子值时,数据重复的潜在bug
460 | > 1. 增加了将因子值改为分组组号的函数to_group
461 | > 1. 增加了根据因子b对因子a进行排序,并在组内使用某种操作的类pure_helper
462 | > 1. 对回测框架的pure_moonnight的输出因子值增加保护
463 | * v3.2.7 — 2022.09.20
464 | > 1. 修复了回测框架pure_moonnight不设置sheetname就无法回测的bug,改为不设置sheeetname就不会写入excel
465 | > 1. 给因子后续必要测试follow_tests增加写入多头在主要指数上的超额净值序列
466 | * v3.2.6 — 2022.09.19
467 | > 1. 通过import pure_ocean_breeze导入库的时候,不再自动导入pure_ocean_breeze.state.states模块内的内容,可通过pure_ocean_breeze.states来调用
468 | > 2. 新增了对因子一键进行必要后续测试的函数follow_tests,包括输出各个分组表现、与常用风格因子相关系数、barra纯净化、在沪深300、中证500、中证1000指数上的多空绩效和多头超额表现、在各个一级行业上的Rank IC值和各个一级行业买n只股票的超额表现
469 | > 3. 在pure_ocean_breeze.state.states模块中,新增COMMENTS_WRITER和NET_VALUES_WRITER参数,用于管理全局所有的pu re_moonnight和follow_tests的绩效记录和净值序列的记录
470 | > 4. 修复了更新日频数据函数database_update_daily_files中,读取旧数据部分的潜在bug
471 | > 5. 删去了pure_ocean_breeze.labor.comment模块中的,输出十组每组绩效表现的函数comments_ten,给pure_moonnight类新增函数pure_moonnight.comments_ten(),用于输入十分组各组绩效表现
472 | > 6. 修复了将因子限定在指数成分股内的函数daily_factor_on300500中,读取指数成分股数据时的潜在bug
473 | > 7. 给讲因子限定在各个一级行业成分股内的函数daily_factor_on_industry,增加了申万一级行业和中信一级行业可选的参数
474 | > 8. 将在各个一级行业上进行分组多空测试的函数group_test_on_swindustry更名为group_test_on_industry,并增加申万一级行业和中信一级行业可选的参数
475 | > 9. 将在各个一级行业上进行Rank IC测试的函数rankic_test_on_swindustry更名为rankic_test_on_industry,并增加申万一级行业和中信一级行业可选的参数
476 | > 10. 修复了在各个一级行业上进行购买n只股票的多头超额测试的函数long_test_on_industry内的中性化bug、读取中信哑变量的bug、股票上市天数的bug、计算收益率序列的bug、读取各个行业指数的bug、中信行业名称bug
477 | > 11. 修复了行业市值中性化函数decap_industry中读取流通市值数据的bug
478 | > 12. 修复了使用clickhouse或questdb的分钟数据更新因子值的类pure_fall_frequent更新因子值时,在每段第一个交易日时的bug
479 | * v3.2.5 — 2022.09.16
480 | > 1. 修复了读取日频数据函数read_daily由于索引名称更改导致的bug
481 | > 1. 修复了缓存机制导致同一内核中,无法转换中信行业和申万行业的bug
482 | > 1. 给用clickhouse的分钟数据计算因子值的类pure_fall_frequent增加了notebook进度条功能,当tqdm_inside指定为-1时,即使用tqdm.tqdm_notebook功能
483 | * v3.2.4 — 2022.09.15
484 | > 1. 改善了以clickhouse和questdb分钟数据计算因子的循环逻辑,将需要计算的时间拆分为多段相邻时间来计算,并补充了起始第一天的计算
485 | > 1. 将保存最终因子值的函数database_save_final_factors增加了去除全空行的功能
486 | * v3.2.3 — 2022.09.13
487 | > 1. 修复了日频数据更新中的bug
488 | > 2. 修复了中信一级行业成分股数据更新的bug
489 | > 3. 将日频数据更新限制时间调整回了17点前
490 |
491 | * v3.2.2 — 2022.09.13
492 | >1. 将日频数据更新的限制时间点由17点前,改为18点前
493 | >1. 修复了调用旧版本时legacy_version的bug
494 | * v3.2.1 — 2022.09.13
495 | >1. 在更新日频数据中加入17点前,仅更新至上一个交易日的限制
496 | >1. 将更新申万一级行业成分股的函数更名为database_update_swindustry_member
497 | >1. 修复了调用旧版本v3p1时的bug
498 | * v3.2.0 — 2022.09.13
499 | >1. 将以mat格式存储的文件,全部转化为feather格式
500 | >1. read_daily函数不再对停牌日的价格进行处理
501 | >1. 删去了更新日频数据的参数,改为只能更新到最新日期
502 | >1. 删去了更新辅助字典的记录,改为从已有的数据中识别上次更新的截止日期
503 | >1. 更新中加入了去重,避免重复更新引起的潜在bug
504 | >1. 在储存最终因子值的函数database_save_final_factors中加入了去重
505 | >1. 增加了中信一级行业成分股数据的函数
506 | >1. 删去了初始化中关于辅助字典的内容,不再创建database_config.db
507 | >1. 删去了初始化中关于日频数据截止日期和分钟数据截止日期的设置要求
508 | >1. 新增在行业上的超额收益测试函数long_test_on_industry,可以选择申万或中信一级行业,默认使用中信
509 | >1. 保留原有风格的行业超额收益测试函数long_test_on_swindustry和long_test_on_zxindustry
510 | >1. 给行业市值中性化函数decap_industry增加中信和申万可选参数,默认使用中信
511 | >1. 简化pure_moon/pure_moonnight回测框架的基础数据处理流程
512 | >1. 给pure_moon/pure_moonnight回测框架的行业中性化部分,增加了中信和申万一级行业可选的参数,默认使用中信
513 | >1. 给pure_moon回测框架增加了设置回测时用到的基础数据的函数set_basic_data
514 | >1. 给pure_moonnight增加了输入基础数据的参数,包括上市天数、是否st、是否正常交易、复权开盘价、复权收盘价、月末流通市值
515 | >1. 删去了pure_moon/pure_moonnight回测框架的by10参数,日频状态月频化时只能使用比较大小的方式
516 | >1. 在旧版本合集legacy_version模块中,收录了最后一个可以处理mat文件的版本3.1.6,使用import pure_ocean_breeze.legacy_version.v3p1 as p即可调用3.1.6版本
517 | * v3.1.6 — 2022.09.03
518 | >1. 修复了用mysql更新因子值时的潜在bug
519 | * v3.1.5 — 2022.09.03
520 | >1. 修复了withs模块的bug
521 | * v3.1.4 — 2022.09.03
522 | >1. 将with模块改为withs模块,避免与关键字冲突
523 | >1. 将更新的源限定为pypi
524 | * v3.1.3 — 2022.09.03
525 | >1. 加入了with模块,可以通过`from pure_ocean_breeze.with.requires import *`加载所有依赖库
526 | >1. 将自动检查新版本改为了需要手动调用check_update()函数来检查
527 | >1. 将行业市值中性化函数decap_industry()改为了可手动指定频率,如果未指定,再自动识别
528 | >1. 修复了限定因子在申万一级行业上的函数daily_factor_on_swindustry()的bug
529 | >1. 修复了使用mysql更新因子值时的表名bug
530 | >1. 修复了使用clickhouse、questdb、postgresql数据库更新因子值时的潜在bug
531 | * v3.1.2 — 2022.08.31
532 | >1. 修复了导入时循环引用的bug
533 | * v3.1.1 — 2022.08.30
534 | >1. 增加了自动检测新版本的功能,在导入库时将自动检测并输出结果
535 | >1. 增加了导入库的时间段统计,可使用函数show_use_times()查看当前设备各个时间段导入次数
536 | >1. 增加了升级库的函数upgrade(),也可以简写为up(),将框架升级为最新版
537 | >1. 将米筐rqdatac改为了非必要加载库
538 | >1. 给read_daily函数增加了一键读入上市天数、流通市值、是否st股、交易状态是否正常的选项
539 | >1. 给读入申万行业指数read_swindustry_prices和中信行业指数read_zxindustry_prices加入起始日期参数
540 | >1. 给获取申万一级行业哑变量的函数get_industry_dummies增加起始日期参数
541 | >1. 增加将一个因子值拆分在各个申万一级行业上的因子值的函数daily_factor_on_swindustry
542 | >1. 增加分别在每个申万一级行业上测试因子分组回测表现的函数group_test_on_swindustry
543 | >1. 增加专门计算因子值在各个申万一级行业上的Rank IC值,并绘制柱状图的函数rankic_test_on_swindustry
544 | >1. 增加对每个申万一级行业成分股,使用某因子挑选出最多头的n值股票,考察其超额收益绩效、每月超额收益、每月每个行业的多头名单的函数long_test_on_swindustry
545 | * v3.1.0 — 2022.08.28
546 | >1. 增加了资金流相关数据,包括一键读入资金流数据的函数read_money_flow和更新数据的函数
547 | >2. 调整了读取因子值和写入因子值函数所属的模块
548 | * v3.0.9 — 2022.08.28
549 | >1. 恢复了read_market函数,将一键读入wind全A指数改为一键读入中证全指000985.SH
550 | * v3.0.8 — 2022.08.25
551 | >1. 修复了更新宽基指数成分股的bug
552 | * v3.0.7 — 2022.08.25
553 | >1. 向数据库模块(pure_ocean_breeze.data.database)中增加了数据库元类,包含不同数据库通用的功能与属性
554 | >1. 向数据库模块中增加使用postgresql与psycopg2引擎的数据库类,包含相关数据库通用的属性与连接方式
555 | >1. 增加postgresql数据库与questdb数据库模块
556 | >1. 统一化mysql数据库的命名
557 | >1. 修复了识别代码为股票与基金代码的函数bug
558 | >1. 增设同时更新clickhouse与questdb分钟数据的函数,以及单独更新questdb和单独更新postgresql的分钟数据的函数
559 | >1. 删去了mysql每只股票/基金一张表的更新部分,仅保留每天一张表
560 | >1. 将mysql的数据存储改为FLOAT,不再乘以100
561 | >1. 以分钟数据计算因子值的部分,增设使用questdb和postgresql计算的选项
562 | * v3.0.6 — 2022.08.21
563 | >1. 修复了使用sql更新因子值的文件读入bug
564 | * v3.0.5 — 2022.08.19
565 | >1. 将旧版本模块与未来版本模块改为了须单独导入
566 | >2. 向本地数据库增设了中信一级行业指数数据
567 | >3. 增加了中信一级行业的米筐代码与行业名称之间的对应字典
568 | * v3.0.4 — 2022.08.18
569 | >1. 增设了旧版本模块(pure_ocean_breeze.legacy_version),目前包括三个旧版本:
570 | >>* v2.6.9(发布时间2022-08-16)
571 | >>* v1.20.7(发布时间2022-07-11)
572 | >>* v1.10.7(发布时间2022-04-04)
573 | >2. 增设了未来版本模块(pure_ocean_breeze.future_version),目前包括两个部分:
574 | >>* pure_ocean_breeze.future_version.half_way(尚未完工的部分)
575 | >>* pure_ocean_breeze.future_version.in_thoughts(仍主要在构思或推敲阶段的部分)
576 | * v3.0.3 — 2022.08.18
577 | >1. 调整了部分模块的名称,使其可以被mkdocstrings识别
578 | >1. 增设了主流宽基指数wind代码与简称的对应字典
579 | >1. 增设专门v2模块,以便在v3版本中调用旧版本代码
580 | * v3.0.2 — 2022.08.18
581 | >1. 剔除常用风格因子的模块(pure_coldwinter/pure_snowtrain)可以增加自定义因子或选择不剔除某些因子
582 | >1. 发送邮件的模块,改为可以不带附件,发送纯文本邮件
583 | * v3.0.1 — 2022.08.17
584 | >1. 删去了回测框架中读入最高价、最低价、成交量和换手率的部分
585 | >1. 将申万行业哑变量的读入时间提前,从而给回测提速60%
586 | * v3.0.0 — 2022.08.17
587 | >1. 上线了[说明文档](https://chen-001.github.io/pure_ocean_breeze/)
588 | >2. 将v2中的pure_ocean_breeze模块拆分为不同功能的几个模块
589 | >>* initialize (初始化)
590 | >>* state (配置&参数)
591 | >>* data (数据)
592 | >>* labor (加工&测试&评价)
593 | >>* mail(通讯)
594 | >3. 修复了主要指数成分股处理的bug,并将其改为日频
595 | >4. 增加了国证2000成分股的哑变量
596 | >5. 删去了初始化中的分钟数据文件路径
--------------------------------------------------------------------------------