data-backtest-live-common 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: data-backtest-live-common
3
+ Version: 0.2.0
4
+ Summary: 数据(Data)/回测(Backtest)/实盘(Live)三系统公共库:复权、A股交易规则全集(涨跌幅/时段/费用/交收/熔断/除权)、交易日历、原子写、无黑窗子进程
5
+ Author: dbl_common maintainers
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 data-backtest-live-common contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Keywords: quant,data,backtest,live-trading,a-share,tushare,akshare
29
+ Classifier: Development Status :: 4 - Beta
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: Intended Audience :: Financial and Insurance Industry
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Operating System :: Microsoft :: Windows
34
+ Classifier: Operating System :: POSIX :: Linux
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Programming Language :: Python :: 3.14
41
+ Classifier: Topic :: Office/Business :: Financial :: Investment
42
+ Requires-Python: >=3.10
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Requires-Dist: pandas
46
+ Requires-Dist: numpy
47
+ Requires-Dist: duckdb
48
+ Provides-Extra: test
49
+ Requires-Dist: pytest; extra == "test"
50
+ Dynamic: license-file
51
+
52
+ # data-backtest-live-common(导入名 `dbl_common`)
53
+
54
+ 数据(Data)/ 回测(Backtest)/ 实盘(Live Trading)三个系统**共用**的公共库。
55
+ 独立项目,像 akshare 一样 pip 安装后直接 import 使用。
56
+
57
+ - **发行名(pip 名)**:`data-backtest-live-common` —— 数据/回测/实盘三个英文单词 + common(用户 2026-09-26 指定口径)
58
+ - **导入名**:`dbl_common` —— 三词首字母 + common(对齐 `beautifulsoup4 → import bs4` 的短名惯例,免得每次 import 敲 24 个字符)
59
+ - **版本**:0.2.0 —— 2026-09-26 从 `quant_data_center/common` 抽出独立成库;**2026-09-27 并入原 `market_common`(A 股交易规则库)两库合一**,`f:/code/finance/market_common` 内容已全部迁入本库并停止维护(原目录删除被环境安全删除守卫拦截,只剩弃用标记,可随时人工删除)
60
+
61
+ ## 收录标准(硬规则)
62
+
63
+ 只收「**数据、回测、实盘三个系统都会用到**」的代码。
64
+ 单系统专属的公共代码放回各系统自己的 common 包
65
+ (例如 quant_data_center 的 throttle 限频 / pagination 翻页是采集专属,留在那边)。
66
+
67
+ ## 模块清单
68
+
69
+ | 模块 | 干什么 | 三系统各自的用途 |
70
+ |---|---|---|
71
+ | `adjust` | 本地复权引擎(hfq = raw×adj_factor 锚定上市日;qfq 锚定最新) | 数据:清洗层标准化;回测:价格连续序列;实盘:指标口径 |
72
+ | `boards` | 证券代码规范化 / 品种识别 / 板块判定(规则库最底层,纯函数零依赖) | 所有系统问「这代码是什么品种、哪个板块」 |
73
+ | `price_limit` | 涨跌停**规则全集**(主板/科创/创业/北交/ST/ETF/转债/B股/新股首日非对称档,含历史制度变更,返回 `LimitSpec` 值对象) | 数据:审核规则;回测:判板;实盘:委托价边界 |
74
+ | `vec` | 涨跌停**向量化引擎唯一实现**(`limit_prices`/`flags_from_bars`/`board_flags`/`hfq_ret_on`,spec 驱动,支持 ETF 三位报价) | 回测全市场判板(5000+ 只 × 数千日) |
75
+ | `trade_calendar` | 交易日历读取(Tushare cal Parquet;`cal_glob` 必传,不绑定任何系统路径) | 三系统同一套日历口径 |
76
+ | `trading_calendar` | 交易日历**注入契约**(`InjectedCalendar`:BaoStock CSV / QDC raw 层 Parquet,三态诚实) | 回测/实盘注入自己的日历数据源 |
77
+ | `trading_hours` | 交易时段(集合竞价/连续竞价/收盘集合竞价,`Phase` 枚举) | 实盘下单时段校验;回测撮合时点 |
78
+ | `settlement` | 交收制度(T+1/T+0/T+3,`STOCK_T1_START`) | 实盘可卖判断;回测成交约束 |
79
+ | `lots` / `ticks` | 申报单位(手数)/ 最小变动价位(含 snap/校验) | 实盘委托合法性;回测撮合精度 |
80
+ | `fees` | 交易费用(印花税/过户费**时间线**,佣金必显式传) | 回测净收益;实盘成本预估 |
81
+ | `circuit_breaker` | 2016 熔断制度(历史触发记录) | 回测跨 2016-01 必须剔除熔断时段 |
82
+ | `halts` | 盘中临时停牌(新股/转债临停规则与复牌时间) | 回测撮合跳过临停;实盘委托拦截 |
83
+ | `corporate_actions` | 除权除息参考价官方公式 + 理论因子跳变 + 数据中心核验入口(容差 0.5%;B股/转债拒答) | 数据:dividend 对账;回测:复权核验 |
84
+ | `proc` | 子进程无黑窗(`no_window`/`no_window_if_detached`/`pid_alive`,Windows) | 任何会起子进程的系统(调度/并行网格/网关进程) |
85
+ | `io_utils` | 原子写文件(tmp + os.replace + WinError5 重试) | 任何写「会被并发读」的状态/数据文件的系统 |
86
+
87
+ 依赖:`pandas` / `numpy` / `duckdb`(pip install 自动拉齐;已有这些包的系统可加 `--no-deps`)。
88
+
89
+ ## 安装(三种姿势)
90
+
91
+ ```bash
92
+ # ① 开发期(editable:改源码立即生效,无需重装)
93
+ "D:\Program Files\Python314\python.exe" -m pip install -e f:/code/finance/data-backtest-live-common --no-deps
94
+
95
+ # ② 正式(打 wheel 安装)
96
+ python -m pip wheel . --no-deps -w dist
97
+ python -m pip install dist/data_backtest_live_common-0.2.0-py3-none-any.whl
98
+
99
+ # ③ 未来发布 PyPI 后(像 akshare 一样)
100
+ pip install data-backtest-live-common
101
+ ```
102
+
103
+ quant_data_center 当前用姿势 ①(editable,装在 `D:\Program Files\Python314` 解释器上,
104
+ tests / uvicorn / scheduler 子进程全部可见)。
105
+
106
+ ## 开发与测试
107
+
108
+ ```bash
109
+ # 需要解释器里有 pandas / numpy / duckdb / pytest(vendor PYTHONPATH 或已 pip 安装)
110
+ $env:PYTHONPATH = "F:\code\finance\quant_data_center\vendor"
111
+ "D:\Program Files\Python314\python.exe" -m pytest tests -q
112
+ ```
113
+
114
+ ## 诚实边界
115
+
116
+ - `trade_calendar` 只读「已经落盘的日历 Parquet」,不联网、不猜测:文件缺失返回 `[]` 由调用方降级;**参数(cal_glob)缺失直接 ValueError**(程序员错误必须响亮);
117
+ - `price_limit` 的 ST 判定只有当前名单、没有逐日历史 → 5% 档用「恰好贴板」精确判定(详见模块 docstring 的血泪教训:用 `≥` 会把普通涨超 5% 的交易日成片误判);
118
+ - `adjust` 的 hfq 锚定上市日(历史行永不变,适合增量落盘),qfq 锚定最新(历史随分红改写,只适合展示);
119
+ - 1996-12-16 之前 A 股无涨跌停制度,`vec.flags_from_bars` 一律不判;
120
+ - `corporate_actions` 对 B 股(需当日汇率折算,属运行期数据非规则)与可转债(不除权)显式 raise 拒答,不猜;
121
+ - 新股「首 N 日不设涨跌幅」没有交易日序号时按自然日近似,`reason` 里如实标注「近似」。
122
+
123
+ ## 与兄弟项目的关系(互不 import)
124
+
125
+ | 项目 | 定位 | 关系 |
126
+ |---|---|---|
127
+ | `quant_data_center` | 数据系统 | 本库母体;现为 pip 依赖 `dbl_common` |
128
+ | 回测 / 实盘系统(未来) | — | 直接 `pip install` 本库即可(原需另装 market_common,2026-09-27 已并入) |
129
+
130
+ > **两库合一记录(2026-09-27)**:用户对比本库与 `market_common` 后指令「如有重复删除
131
+ > market」。实测仅 `price_limit` 领域同源重复(本库=引擎+简化规则,market=规则全集),
132
+ > 其余 10 个交易规则模块是独有内容 → **整体并入**:`price_limit` 取超集版(含
133
+ > `round_half_up` 浮点修正),向量化引擎统一进 `vec.py`(spec 驱动签名),83 项守卫
134
+ > 测试随迁为 `tests/test_trading_rules.py`,原 market_common 目录弃用(AI 删除时被环境
135
+ > 安全删除守卫拦截,内容已 100% 迁入本库并经测试验证,该目录可随时人工删除)。
136
+
137
+ ## 怎么加新模块(给 AI 的守则)
138
+
139
+ 1. 先判定「三个系统都用到吗」——不是就放回各系统自己的 common,别塞进来;
140
+ 2. 纯函数优先、零系统路径耦合(需要路径/配置就做成必传参数,参照 `trade_calendar.cal_glob`);
141
+ 3. 配 `tests/` 用例,锚点用真实制度事实(如创业板 2020-08-24 改 20%、round_half_up(2.675)=2.68);
142
+ 4. 版本号 +1(`pyproject.toml` 与 `dbl_common/__init__.py` 同步改),更新本 README 模块表;
143
+ 5. Windows 环境跑命令一律后台无窗(`Start-Process -WindowStyle Hidden`)。
@@ -0,0 +1,22 @@
1
+ data_backtest_live_common-0.2.0.dist-info/licenses/LICENSE,sha256=B01Wsyf9hVVsEvHxNQOkawcuyLt2jZwkJPJnmOTwn7I,1116
2
+ dbl_common/__init__.py,sha256=MkNAeU8OQuFHdvScjuh5F4wjwqZJSmN6Wiya6aWfV1I,5769
3
+ dbl_common/adjust.py,sha256=V8aRJatuGo_p0CtPZtuVUIZA2gBpavRxvtKHg89QV10,3305
4
+ dbl_common/boards.py,sha256=zAij-SQF7WdvzymNeyxgAge3DS6GdsKGEFHzR7-7x4o,7203
5
+ dbl_common/circuit_breaker.py,sha256=mNfBzeRBhpucjccczH9w6qPqg2sl-4CoLr39p2yB7sM,4029
6
+ dbl_common/corporate_actions.py,sha256=mrWuYcNwHJdf34C5g36BVQNgb4zMkF7N4MHx8u-LNQw,8061
7
+ dbl_common/fees.py,sha256=OlV0eayw48GV8dpClGi9caWQ26c8DqF_xq950-aOxoo,8083
8
+ dbl_common/halts.py,sha256=ngJnCBZ0wN_LQUM_VgUwcHRuvtDR9vsNDXP1Z03z35Y,9070
9
+ dbl_common/io_utils.py,sha256=a6Dx-UKRE4XCkxrLoexy4O7UXQ5RBxbDSbJvfc9GNhE,3162
10
+ dbl_common/lots.py,sha256=xy_rX3sHNYP-7DDdsCyA-6tQGSuULN5u54uZN0rPQns,5403
11
+ dbl_common/price_limit.py,sha256=vAxDt0fuwv7OQrKKJ6etDbCzCyCSDEkZqfHKanu_EkY,16993
12
+ dbl_common/proc.py,sha256=mrMeL7l0z-PrOZmtCcCM9KFk0JysgFBMpyxrIl076ZM,6024
13
+ dbl_common/settlement.py,sha256=TTmYS20MoXylCywg4Xshv3ZMoRuA5c6sAWXO0Gf4qfo,5219
14
+ dbl_common/ticks.py,sha256=MjGy8v6LqNkr69D5tgvVXArSSkKxwKhHt0dwg5WFXRk,2376
15
+ dbl_common/trade_calendar.py,sha256=638u9bkIsVuhs8jhc7Jj2O1hOGUYqd8UDrIOLESI0bw,5046
16
+ dbl_common/trading_calendar.py,sha256=LMROwgyD_PPgAca1ntnowFnkikbKSiBl_QnLlrgER8U,8470
17
+ dbl_common/trading_hours.py,sha256=5EiCUmAZAZsWTJin28nHqukYTesXxF0kzM1tEGWQqA0,7151
18
+ dbl_common/vec.py,sha256=K9vgYhOJLGXhDQMGtJXzQtR0sTKpittQCOMIYouZ1X0,6452
19
+ data_backtest_live_common-0.2.0.dist-info/METADATA,sha256=gyj0x4LTgC9hFrpUryPtWxcYYTXSj9-Vsnq79EwYBqg,9854
20
+ data_backtest_live_common-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
21
+ data_backtest_live_common-0.2.0.dist-info/top_level.txt,sha256=dDBhJoMO_TN-iJxJE2lF2FYZ_V2RViuA60QOhSPzq-A,11
22
+ data_backtest_live_common-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 data-backtest-live-common contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ dbl_common
dbl_common/__init__.py ADDED
@@ -0,0 +1,93 @@
1
+ # -*- coding: utf-8 -*-
2
+ """dbl_common — 数据 / 回测 / 实盘 三系统公共库(Data Backtest Live Common)
3
+
4
+ 命名口径(用户 2026-09-26 指定):
5
+ · 发行名(pip 名)= 数据/回测/实盘三个英文单词 + common → ``data-backtest-live-common``;
6
+ · 导入名 = 三词首字母 + common → ``dbl_common``(对齐 beautifulsoup4 → ``import bs4``
7
+ 的短名惯例,避免每次 import 敲 24 个字符)。
8
+
9
+ 收录标准(硬规则):**只收「数据、回测、实盘三个系统都会用到」的代码**;
10
+ 单系统专属的公共代码放回各系统自己的 common 包(如 quant_data_center 的
11
+ throttle 限频 / pagination 翻页是采集专属,留在那边,不进本库)。
12
+
13
+ 模块清单(2026-09-27 起 = 原 market_common 交易规则库整体并入,两库合一):
14
+ - adjust : 本地复权引擎(hfq = raw × adj_factor,锚定上市日,增量追加安全)
15
+ - boards : 证券代码规范化 / 品种识别 / 板块判定(规则库最底层,纯函数零依赖)
16
+ - price_limit : 涨跌停规则全集(主板/科创/创业/北交/ST/ETF/转债/B股/新股首日,含历史制度变更)
17
+ - vec : numpy 向量化涨跌停引擎(limit_prices / flags_from_bars / board_flags / hfq_ret_on)
18
+ - trade_calendar : 交易日历(Tushare cal Parquet 读取;cal_glob 必传,不绑定任何系统路径)
19
+ - trading_calendar : 交易日历注入契约(InjectedCalendar:BaoStock CSV / QDC raw 层 Parquet)
20
+ - trading_hours : 交易时段(集合竞价 / 连续竞价 / 收盘集合竞价)
21
+ - settlement : 交收制度(T+1 / T+0 / T+3)
22
+ - lots / ticks : 申报单位(手数)/ 价位(最小变动价位)
23
+ - fees : 交易费用(印花税 / 过户费时间线;佣金必显式传)
24
+ - circuit_breaker : 2016 熔断制度
25
+ - halts : 盘中临时停牌(新股 / 转债临停)
26
+ - corporate_actions: 除权除息参考价官方公式 + 理论因子跳变 + 数据中心核验入口
27
+ - proc : 子进程无黑窗唯一实现(no_window / no_window_if_detached / pid_alive)
28
+ - io_utils : 原子写文件(atomic_write_text / atomic_write_bytes,tmp+os.replace+重试)
29
+
30
+ 历史:
31
+ · 2026-09-26 从 quant_data_center/common 抽出独立成库(adjust/price_limit/
32
+ trade_calendar/proc/io_utils 五模块);
33
+ · 2026-09-27 用户指令「两库如有重复则删除 market」→ 实测仅 price_limit 领域
34
+ 同源重复(规则超集 + 引擎),其余 10 个交易规则模块为本库没有的独有内容 →
35
+ **整体并入**(price_limit 取超集版 + round_half_up 浮点修正;引擎统一进
36
+ vec.py;83 项守卫测试随迁为 tests/test_trading_rules.py),market_common
37
+ 目录删除。再往前溯源是 global_market_dashboard 的成熟代码「复制适配」。
38
+ """
39
+ from .boards import Board, BOARD_LABEL, SecurityType, board_of, exchange_of, \
40
+ normalize_code, security_type
41
+ from .price_limit import LimitSpec, limit_spec, round_half_up, \
42
+ limit_up_price, limit_down_price, price_decimals, candidates_of, \
43
+ LIMIT_ERA_START, ST_5PCT_START, GEM_20PCT_START, MAIN_REG_START, \
44
+ MAIN_IPO_4436_START, CB_20PCT_START, CENT_TOL, EPS
45
+ from .trading_hours import Phase, PHASE_LABEL, phase_at, is_auction, \
46
+ is_continuous, can_cancel
47
+ from .settlement import settle_rule, can_sell_today, STOCK_T1_START
48
+ from .lots import lot_rule, validate_qty
49
+ from .ticks import tick_size, snap_to_tick, is_valid_price
50
+ from .fees import FeeResult, calc_fees, stamp_tax, transfer_fee_rate, \
51
+ commission_cap, stamp_tax_timeline
52
+ from .circuit_breaker import is_active as cb_is_active, market_state as cb_state, \
53
+ CB_START, CB_END, LEVEL1, LEVEL2, HISTORICAL_TRIGGERS
54
+ from .halts import HaltRule, intraday_halt_rule, resume_time, halt_price
55
+ from .trading_calendar import InjectedCalendar, from_baostock_csv, \
56
+ from_parquet_dataset, weekend_is_trading_day, weekend_next_trading_day, \
57
+ weekend_prev_trading_day
58
+ from .corporate_actions import CorporateAction, from_per_ten, \
59
+ ex_right_price, factor_jump, validate_against_factor, check_applicable
60
+
61
+ __version__ = "0.2.0"
62
+
63
+ __all__ = [
64
+ # boards
65
+ "Board", "BOARD_LABEL", "SecurityType", "board_of", "exchange_of",
66
+ "normalize_code", "security_type",
67
+ # price_limit
68
+ "LimitSpec", "limit_spec", "round_half_up", "limit_up_price",
69
+ "limit_down_price", "price_decimals", "candidates_of",
70
+ "LIMIT_ERA_START", "ST_5PCT_START", "GEM_20PCT_START", "MAIN_REG_START",
71
+ "MAIN_IPO_4436_START", "CB_20PCT_START", "CENT_TOL", "EPS",
72
+ # trading_hours
73
+ "Phase", "PHASE_LABEL", "phase_at", "is_auction", "is_continuous", "can_cancel",
74
+ # settlement
75
+ "settle_rule", "can_sell_today", "STOCK_T1_START",
76
+ # lots / ticks
77
+ "lot_rule", "validate_qty", "tick_size", "snap_to_tick", "is_valid_price",
78
+ # fees
79
+ "FeeResult", "calc_fees", "stamp_tax", "transfer_fee_rate",
80
+ "commission_cap", "stamp_tax_timeline",
81
+ # circuit breaker
82
+ "cb_is_active", "cb_state", "CB_START", "CB_END", "LEVEL1", "LEVEL2",
83
+ "HISTORICAL_TRIGGERS",
84
+ # halts(盘中临时停牌)
85
+ "HaltRule", "intraday_halt_rule", "resume_time", "halt_price",
86
+ # calendar(注入契约;Tushare cal Parquet 读取见 dbl_common.trade_calendar)
87
+ "InjectedCalendar", "from_baostock_csv", "from_parquet_dataset",
88
+ "weekend_is_trading_day", "weekend_next_trading_day",
89
+ "weekend_prev_trading_day",
90
+ # corporate actions(除权除息参考价)
91
+ "CorporateAction", "from_per_ten", "ex_right_price", "factor_jump",
92
+ "validate_against_factor", "check_applicable",
93
+ ]
dbl_common/adjust.py ADDED
@@ -0,0 +1,74 @@
1
+ # -*- coding: utf-8 -*-
2
+ """dbl_common.adjust — 本地复权引擎(数据/回测/实盘共用)
3
+
4
+ 溯源:global_market_dashboard tushare_fetcher.compute_hfq →
5
+ quant_data_center/common/adjust.py → 2026-09-26 抽入本独立库(内容不变)。
6
+
7
+ 核心设计(为什么锚定上市日、而不是「÷ 最新因子」锚定最新):
8
+ adj_factor 的历史值**永不回改**(新分红只抬高未来行的因子),
9
+ 所以 hfq 的历史行也永不变 —— 增量追加天然安全。
10
+ 若锚定最新,每次分红都会改写全部历史,合并接缝处必出人工断崖。
11
+
12
+ 连续性保证:除权日 raw 跳空、因子同幅度反向跳变,乘积天然连续 ——
13
+ 这就是 92 只 known-list 断崖(sina/腾讯因子基准漂移)的治本项。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import pandas as pd
18
+
19
+
20
+ def compute_hfq(daily_df: pd.DataFrame, factor_df: pd.DataFrame) -> pd.DataFrame:
21
+ """本地后复权引擎:**hfq = raw × adj_factor**(锚定上市日)。
22
+
23
+ Args:
24
+ daily_df: 日线表,需含 date + OHLC 列
25
+ factor_df: 复权因子表,需含 date + adj_factor 列
26
+
27
+ Returns:
28
+ 合并后的 DataFrame,OHLC 已乘上 adj_factor(后复权);
29
+ 因子缺失/非法(≤0)的行 OHLC 置 NaN 如实缺失(不冒充数据),
30
+ 该行仍保留(volume/amount 是真实成交,不因价格缺失而丢)。
31
+ """
32
+ if daily_df is None or daily_df.empty:
33
+ return daily_df
34
+ m = daily_df.copy()
35
+ if factor_df is None or factor_df.empty:
36
+ for c in ("open", "high", "low", "close"):
37
+ if c in m.columns:
38
+ m[c] = float("nan")
39
+ m["adj_factor"] = float("nan")
40
+ return m
41
+ f = factor_df[["date", "adj_factor"]].copy()
42
+ f["adj_factor"] = pd.to_numeric(f["adj_factor"], errors="coerce")
43
+ m = m.merge(f, on="date", how="left").sort_values(
44
+ "date", kind="stable").reset_index(drop=True)
45
+ # 因子与日线同为逐交易日行,merge 应 1:1;个别日期缺因子时用前一交易日
46
+ # 因子兜底(因子在除权日之间保持不变,ffill 语义正确)。
47
+ m["adj_factor"] = m["adj_factor"].ffill()
48
+ bad = m["adj_factor"].isna() | (m["adj_factor"] <= 0)
49
+ for c in ("open", "high", "low", "close"):
50
+ if c in m.columns:
51
+ v = pd.to_numeric(m[c], errors="coerce") * m["adj_factor"]
52
+ m[c] = v.mask(bad)
53
+ return m
54
+
55
+
56
+ def compute_qfq(daily_df: pd.DataFrame, factor_df: pd.DataFrame) -> pd.DataFrame:
57
+ """本地前复权引擎:**qfq = raw × adj_factor ÷ 最新 adj_factor**(锚定最新价)。
58
+
59
+ 与 hfq 的区别:锚定最新价,历史行会随新分红而改写。
60
+ 适合「按最新价看历史」的展示场景,不适合增量落盘(接缝会断)。
61
+ """
62
+ m = compute_hfq(daily_df, factor_df)
63
+ if m is None or m.empty or "adj_factor" not in m.columns:
64
+ return m
65
+ valid = m["adj_factor"].dropna()
66
+ if valid.empty:
67
+ return m
68
+ latest = valid.iloc[-1]
69
+ if latest <= 0:
70
+ return m
71
+ for c in ("open", "high", "low", "close"):
72
+ if c in m.columns:
73
+ m[c] = pd.to_numeric(m[c], errors="coerce") / latest
74
+ return m
dbl_common/boards.py ADDED
@@ -0,0 +1,162 @@
1
+ # -*- coding: utf-8 -*-
2
+ """boards.py — 证券代码规范化 / 品种识别 / 板块判定(公共模块 · 纯函数零依赖)。
3
+
4
+ 为什么必须在公共模块最底层
5
+ --------------------------
6
+ 回测、实盘、模拟盘、数据中心都会问同一个问题:「这只代码是什么品种、哪个板块、
7
+ 适用哪套规则」。各系统各写一份就会口径漂移(GMD 项目实测:同一交易日历被写成
8
+ 5 份、同一涨跌停规则被写成 2 份说法)。本模块是**唯一真相源**。
9
+
10
+ 设计约定
11
+ --------
12
+ · **纯函数 + 零第三方依赖**:只用标准库,任何系统 vendor / sys.path 直接可用。
13
+ · **诚实边界**:代码前缀只能给出「最可能」的判定,无法识别的返回 None 并说明,
14
+ 绝不猜。特别地:6 位代码 000001 既可能是上证指数也可能是平安银行——
15
+ **不带交易所信息时一律按股票处理**,跟踪指数的调用方必须显式传
16
+ `security_type=INDEX`。
17
+ · 与时间相关的板块规则(如创业板 2020-08-24 注册制改革)不在本模块判定,
18
+ 见 `price_limit.py`(板块归属是静态的,制度是随时间变的——分开管)。
19
+
20
+ 代码前缀事实表(沪深交易所公开规则,2026 年口径)
21
+ ------------------------------------------------
22
+ · 60xxxx 沪市主板 A 股 00xxxx 深市主板 A 股(000/001/002/003)
23
+ · 300/301 深市创业板 688/689 沪市科创板
24
+ · 43/83/87/88/92 北交所(2021-11-15 开市,由新三板精选层平移)
25
+ · 900xxx 沪 B / 200xxx 深 B
26
+ · ETF:沪 51x/56x/58x(588=科创板ETF);深 15x(159=股票ETF 等)
27
+ · LOF:沪 501/502;深 16x
28
+ · 可转债:沪 11x(110/111/113/118);深 12x(123/127/128)
29
+ · 国债逆回购:沪 204xxx;深 1318xx
30
+ · 封闭式基金/其他:沪 50xxxx 等
31
+ """
32
+ from __future__ import annotations
33
+
34
+ import enum
35
+ from typing import Optional
36
+
37
+
38
+ class SecurityType(enum.Enum):
39
+ """品种大类(决定涨跌幅/交收/手数/价位整套规则)。"""
40
+
41
+ STOCK = "stock" # A 股股票(含各板块)
42
+ B_SHARE = "b_share" # B 股
43
+ INDEX = "index" # 指数(无涨跌幅限制)
44
+ ETF = "etf" # 交易型开放式指数基金
45
+ LOF = "lof" # 上市开放式基金
46
+ CONVERTIBLE_BOND = "cb" # 可转换公司债券
47
+ REPO = "repo" # 国债逆回购(质押式回购)
48
+ FUND_OTHER = "fund_other" # 封基/其他场内基金
49
+ UNKNOWN = "unknown"
50
+
51
+
52
+ class Board(enum.Enum):
53
+ """交易板块(股票类适用;决定涨跌幅档位与申报单位)。"""
54
+
55
+ MAIN_SH = "main_sh" # 沪市主板(60)
56
+ MAIN_SZ = "main_sz" # 深市主板(000/001/002/003)
57
+ GEM = "gem" # 创业板(300/301,2009-10-30 开市)
58
+ STAR = "star" # 科创板(688/689,2019-07-22 开市)
59
+ BSE = "bse" # 北交所(2021-11-15 开市)
60
+ B_SHARE = "b_share" # B 股(900 沪 / 200 深)
61
+ NONE = "none" # 非股票类
62
+
63
+
64
+ def normalize_code(code) -> str:
65
+ """任意常见代码形态 → 6 位数字字符串。
66
+
67
+ `sh600660` / `600660.SS` / `600660.SH` / `600660` / `sh.600660` → `600660`。
68
+ B 股 / 北交所同样归一到 6 位。非数字字符全部剔除后取末 6 位(前导补零)。
69
+ """
70
+ s = str(code).strip().upper()
71
+ for pre in ("SH", "SZ", "BJ"):
72
+ if s.startswith(pre):
73
+ s = s[len(pre):]
74
+ s = s.lstrip(".").split(".")[0] # "SH.600519"→"600519";"600519.SS"→"600519"
75
+ digits = "".join(ch for ch in s if ch.isdigit())
76
+ if not digits:
77
+ return ""
78
+ return digits.zfill(6)[-6:]
79
+
80
+
81
+ def exchange_of(code) -> Optional[str]:
82
+ """尽量识别交易所:`SH` / `SZ` / `BJ` / None(判不了就 None,不猜)。
83
+
84
+ 只对**前缀能唯一定位**的代码可靠(60/68/90/51/56/58/11/20x→SH;
85
+ 00/30/15/12/1318→SZ;43/83/87/88/92→BJ)。000001 这类深市前缀若调用方
86
+ 实际跟踪的是沪指数,请显式传参,本函数管不了语义。
87
+ """
88
+ c = normalize_code(code)
89
+ if not c:
90
+ return None
91
+ if c[:2] in ("60", "68") or c[:3] in ("900", "510", "511", "512", "513",
92
+ "515", "516", "517", "518", "560",
93
+ "561", "562", "563", "588", "589",
94
+ "110", "111", "113", "118", "204",
95
+ "501", "502", "506", "588"):
96
+ return "SH"
97
+ if c[:2] in ("00", "30", "15", "16", "20") or c[:3] in ("123", "127", "128") \
98
+ or c[:4] == "1318":
99
+ return "SZ"
100
+ if c[:2] in ("43", "83", "87", "88", "92"):
101
+ return "BJ"
102
+ return None
103
+
104
+
105
+ def security_type(code, explicit: Optional[SecurityType] = None) -> SecurityType:
106
+ """品种大类。`explicit` 优先(调用方明确知道时别让前缀猜)。"""
107
+ if explicit is not None:
108
+ return explicit
109
+ c = normalize_code(code)
110
+ if not c:
111
+ return SecurityType.UNKNOWN
112
+ if c[:3] in ("900",):
113
+ return SecurityType.B_SHARE
114
+ if c[:3] == "200":
115
+ return SecurityType.B_SHARE
116
+ if c[:3] in ("510", "511", "512", "513", "515", "516", "517", "518",
117
+ "560", "561", "562", "563", "588", "589", "159"):
118
+ return SecurityType.ETF
119
+ if c[:3] in ("501", "502", "160", "161", "162", "163", "164", "165",
120
+ "166", "167", "168"):
121
+ return SecurityType.LOF
122
+ if c[:3] in ("110", "111", "113", "118", "123", "127", "128"):
123
+ return SecurityType.CONVERTIBLE_BOND
124
+ if c[:3] == "204" or c[:4] == "1318":
125
+ return SecurityType.REPO
126
+ if c[:3] == "505" or c[:2] == "50":
127
+ return SecurityType.FUND_OTHER
128
+ if c[:2] in ("60", "00", "30", "68") or c[:3] in ("688", "689") \
129
+ or c[:2] in ("43", "83", "87", "88", "92"):
130
+ return SecurityType.STOCK
131
+ return SecurityType.UNKNOWN
132
+
133
+
134
+ def board_of(code, explicit_type: Optional[SecurityType] = None) -> Board:
135
+ """股票类板块。非股票类返回 `Board.NONE`。"""
136
+ st = security_type(code, explicit_type)
137
+ if st != SecurityType.STOCK:
138
+ return Board.NONE if st != SecurityType.B_SHARE else Board.B_SHARE
139
+ c = normalize_code(code)
140
+ if not c:
141
+ return Board.NONE
142
+ if c.startswith("688") or c.startswith("689"):
143
+ return Board.STAR
144
+ if c.startswith("300") or c.startswith("301"):
145
+ return Board.GEM
146
+ if c[:2] in ("43", "83", "87", "88", "92"):
147
+ return Board.BSE
148
+ if c[:2] == "60":
149
+ return Board.MAIN_SH
150
+ if c[:2] == "00":
151
+ return Board.MAIN_SZ
152
+ return Board.NONE
153
+
154
+
155
+ BOARD_LABEL = {
156
+ Board.MAIN_SH: "沪市主板", Board.MAIN_SZ: "深市主板", Board.GEM: "创业板",
157
+ Board.STAR: "科创板", Board.BSE: "北交所", Board.B_SHARE: "B股",
158
+ Board.NONE: "非股票类",
159
+ }
160
+
161
+ __all__ = ["SecurityType", "Board", "BOARD_LABEL", "normalize_code",
162
+ "exchange_of", "security_type", "board_of"]
@@ -0,0 +1,82 @@
1
+ # -*- coding: utf-8 -*-
2
+ """circuit_breaker.py — 指数熔断制度(2016 年实施 4 个交易日后暂停)。
3
+
4
+ 为什么公共模块要收录一段只活了 4 天的制度:**回测要跨 2016 年 1 月**。
5
+ 2016-01-04 与 2016-01-07 两天沪深 300 触发 7% 阈值**提前收市**——
6
+ 这两天「下午的价格根本不存在」,按全天数据回测撮合就是作弊。
7
+
8
+ 制度原文(指数熔断机制,2016-01-01 起):
9
+ · 触发基准:沪深 300 指数较前一交易日收盘价;
10
+ · ±5%(一级):熔断 15 分钟;14:45 及之后触发 → 直接收市;
11
+ · ±7%(二级):当日直接收市;
12
+ · 两级熔断每交易日最多各触发一次(15:00 前未触发过一级仍可触发)。
13
+ · 2016-01-08 起暂停实施(1月4日、1月7日两度触发二级熔断后连夜暂停)。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from typing import Optional
18
+
19
+ CB_START = "2016-01-01"
20
+ CB_END = "2016-01-07" # 实施的最后一个交易日(1月8日起暂停)
21
+ LEVEL1 = 0.05
22
+ LEVEL2 = 0.07
23
+ LEVEL1_HALT_MINUTES = 15
24
+ LEVEL1_CUTOFF = "14:45" # 此后触发一级 → 直接收市
25
+
26
+ # 实际触发历史(公开行情事实,回测对账锚点)
27
+ HISTORICAL_TRIGGERS = {
28
+ "2016-01-04": {"level": 2, "note": "沪深300跌7.02%,13:33 收市"},
29
+ "2016-01-07": {"level": 2, "note": "沪深300跌7.21%,09:59 收市"},
30
+ "2016-01-05": {"level": 1, "note": "午后触及-5.03%,熔断15分钟后恢复交易"},
31
+ }
32
+
33
+
34
+ def is_active(date: Optional[str]) -> bool:
35
+ """该交易日熔断制度是否有效(仅 2016-01-01 ~ 2016-01-07)。"""
36
+ if not date:
37
+ return False
38
+ d = str(date)[:10]
39
+ return CB_START <= d <= CB_END
40
+
41
+
42
+ def market_state(date: str, hs300_change_pct: Optional[float],
43
+ trigger_time: Optional[str] = None) -> dict:
44
+ """判定熔断状态(回测撮合闸门用)。
45
+
46
+ Args:
47
+ date: 交易日。
48
+ hs300_change_pct: 沪深300 当日涨跌幅(较前收,百分数如 -7.02 传 -7.02);
49
+ None = 无数据 → 「未判」,调用方必须显式披露,不许当没触发。
50
+ trigger_time: 触发时点 "HH:MM"(判一级熔断后 14:45 直接收市的分支)。
51
+
52
+ Returns:
53
+ {"active": bool, "halted": bool, "closed_early": bool,
54
+ "resume_time": "HH:MM"|None, "reason": str}
55
+ """
56
+ if not is_active(date):
57
+ return {"active": False, "halted": False, "closed_early": False,
58
+ "resume_time": None, "reason": "熔断制度未实施(仅 2016-01-01~01-07)"}
59
+ if hs300_change_pct is None:
60
+ return {"active": True, "halted": False, "closed_early": False,
61
+ "resume_time": None,
62
+ "reason": "熔断制度有效但沪深300涨跌幅未知——未判,须披露"}
63
+ chg = float(hs300_change_pct)
64
+ if chg <= -LEVEL2 * 100 or chg >= LEVEL2 * 100:
65
+ return {"active": True, "halted": True, "closed_early": True,
66
+ "resume_time": None,
67
+ "reason": f"沪深300 {chg:+.2f}% 触及±7%:当日直接收市"}
68
+ if chg <= -LEVEL1 * 100 or chg >= LEVEL1 * 100:
69
+ if trigger_time is not None and str(trigger_time)[:5] >= LEVEL1_CUTOFF:
70
+ return {"active": True, "halted": True, "closed_early": True,
71
+ "resume_time": None,
72
+ "reason": f"14:45 后触及±5%:直接收市(触发 {trigger_time})"}
73
+ return {"active": True, "halted": True, "closed_early": False,
74
+ "resume_time": None,
75
+ "reason": f"触及±5%:熔断 15 分钟(trigger={trigger_time},"
76
+ f"恢复时点请调用方按 trigger_time+15m 计算)"}
77
+ return {"active": True, "halted": False, "closed_early": False,
78
+ "resume_time": None, "reason": "未触及阈值"}
79
+
80
+
81
+ __all__ = ["CB_START", "CB_END", "LEVEL1", "LEVEL2", "is_active",
82
+ "market_state", "HISTORICAL_TRIGGERS"]