Metadata-Version: 2.4
Name: factor-audit
Version: 0.2.0
Summary: Audit A-share factor tests for silent defects: NW-inflated t-stats, survivorship bias, lookahead leakage, and cost infeasibility.
Author: factor-audit contributors
License: MIT
Project-URL: Homepage, https://github.com/val1813/factor-audit
Project-URL: Issues, https://github.com/val1813/factor-audit/issues
Keywords: quantitative-finance,factor-investing,backtesting,survivorship-bias,newey-west,multiple-testing,a-share
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas>=1.5
Requires-Dist: numpy>=1.21
Requires-Dist: scipy>=1.7
Provides-Extra: parquet
Requires-Dist: pyarrow>=10; extra == "parquet"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pyarrow>=10; extra == "dev"
Dynamic: license-file

# 因子审查器 · factor-audit

**你算出一个因子，IC 漂亮、t 值很高。但那个 t 值可能是假的。**

*You built a factor. The IC looks good, the t-stat is high. That t-stat may be fake.*

[中文](#中文) · [English](#english)

```
② Newey-West 调整（IC 自相关会放大 t）
   h=20  IC +0.0464  t朴素  +6.47 → t_NW  +2.06  ★高估 3.1x

⑥ 成本可行性（10 只 / h=20 / 往返 50.2bp）
   毛收益 +14.37%/年   换手 93.5%/期   成本 11.83%/年
   毛/成本 = 1.21x   门槛 3.00x   ❌ 不过
```

这个因子自己算是 `t=6.47`，看着很稳。**调整后只剩 2.06，而且扣完成本根本不过门。**

> **定位**：开源的因子审计系统。大多数阈值基于跨板块验证的统计规律，可直接使用；
> 但**成本类与时效类阈值由交易制度决定** —— 做非 A 股市场需要重新标定，
> 流程公开在下面，也可以找我们做。

---

<a name="中文"></a>

# 中文

## 这东西有什么用

一句话：**它检查你的因子检验有没有出错，而不是告诉你买什么。**

打个比方。你做了一道菜，尝一口觉得好吃。这个工具不评价菜好不好吃 ——
它检查你的**秤准不准、温度计有没有坏、食材有没有过期**。

因为量化研究里最常见的失败不是"想错了"，而是**算的时候有个静默的坑，
而结果恰好朝好的方向偏**。你不会收到任何警告。

### 它会替你抓这些

| 问题 | 通俗说 | 实测量级 |
|---|---|---|
| **t 值虚高** | 同一份信息被数了 20 次 | 高估 **1.8~3.1 倍** |
| **生存者偏差** | 退市股不在你的数据里 | 毛收益虚高 **45.88pp** |
| **前视污染** | 因子里混了未来信息 | 正常 IC 0.046，混了未来 **0.362** |
| **换皮因子** | 12 个因子其实只有 3 个方向 | 波动率四兄弟相关 **0.73~0.93** |
| **成本吃光** | 毛收益不够付手续费 | 门槛是 **3 倍**成本，不是 1 倍 |
| **收益靠几期运气** | 年化好看，去掉最好几期就翻负 | 实测 +24.62% → **−15.44%** |

### 它不做三件事

| 不做 | 为什么 |
|---|---|
| **不告诉你买什么** | 判断"能买"需要预期收益，而预期收益只能来自你的因子。我们看不到 |
| **不替你部署** | 实测「构造优化」在多数因子上只有 ±17bp、「摩擦削减」组合层只剩 0.112bp/月 —— 这两件事不值得卖 |
| **★不上传任何东西** | 全本地运行。因子值只在你自己的进程里，报告只写到你自己的磁盘 |

---

## 为什么有用

因为这些坑**不是"高级问题"，而是绝大多数人默认就踩着的**。

### ① t 值虚高：同一份信息被数了 20 次

你算 20 日持有期的 IC。今天的 IC 和明天的 IC，**共用了 19 天的收益** ——
它们不是两个独立证据，几乎是同一个。

不做调整，就等于把一份信息数了 20 次，t 值自然虚高。
Newey-West 调整就是修这个。**实测高估 1.8~3.1 倍。**

⇒ 一个 `t=6.47` 的因子，真实值 `t=2.06`。**前者能上，后者不能。**

### ② 生存者偏差：这条最隐蔽，因为它几乎不改 IC

同一个因子，两份数据（真实面板 954 万行、2018-2026、退市股占 11.5%）：

```
含退市股   IC +0.0657   t_NW +6.56   毛收益 −50.44%
纯生存者   IC +0.0696   t_NW +6.83   毛收益  −4.56%
```

**IC 只差 5.9%，但毛收益差 45.88pp** —— 把「年化亏 50%」显示成「亏 4.6%」。

**只看 IC 的人完全发现不了。** 而免费数据源默认就不给你退市股：

```
tdx (信通达)   退市股行情返回 {"code":0,"data":[],"message":"success"}
               —— 空数据 + "成功"，5628 只清单里一只退市股都没有
baostock       query_all_stock(今天) 返回 0 条，且 error_code=0
```

**「成功但为空」是数据源最危险的失败模式。** 你拿到一份纯生存者的数据，
零警告，而偏差恰好让结果变好看。

### ③ 换皮因子：Bonferroni 的分母要算对

你测了 12 个因子，其中 5 个是波动率的不同写法
（实测 `vol20 / vol60 / hl_range20 / downside_vol20` 相关 **0.73~0.93**）。

多重比较门槛随检验数变严：

```
1 个方向  → |t| > 1.96
3 个方向  → |t| > 2.39
12 个方向 → |t| > 2.87
59 个方向 → |t| > 3.34
```

**按 12 算是过度惩罚**（会误杀真因子），**但你得知道自己实际只测了 3 个方向。**
本工具先并簇再定门槛，并在同簇多个因子都过门时提醒你：**那是同一个发现。**

### ④ 信号时效：报"衰减形状"，不报"最优持有期"

你想知道"我这个信号能撑多久"。直觉做法是找 IC 曲线的峰 —— **但那个做法是错的。**

实测 6 个因子里 **5 个的 IC 单调上升**，峰在扫描范围之外：

```
低波(取负)     1.00 → 1.17 → 1.36 → 1.57 → 1.67 → 1.79   持续增强
低换手(取负)   1.00 → 1.23 → 1.50 → 1.79 → 1.96 → 2.16   持续增强
动量(20日)     1.00 → 1.11 → 1.24 → 1.15 → 0.99 → 0.65   快速衰减
短期反转(5日)  1.00 → 0.82 → 0.91 → 0.89 → 0.80 → 0.44   快速衰减
```

`argmax` 在单调曲线上必然返回扫描边界 —— 报出来的"最优持有期 120 天"
反映的是我们扫到哪，不是因子性质。

⇒ 改报**衰减形状**后区分度才出现，且 **5/6 个因子的形状在样本外保持**。
形状不稳定时，本工具**只说不可靠、不给持有期数字** ——
先否定自己再给建议，等于没否定。

### ⑤ 这些判据不是查文献抄的

阈值来自把 **59 个公开因子**（国泰君安 GTJA191 的 32 个 + WorldQuant Alpha101 的 27 个）
全部走完六关的实测：

- **成本安全系数 = 3**，不是 1 —— 实测有因子毛/成本 = 2.2 倍，仍然亏 87%
- **前视告警线 = 0.15** —— 实测正常因子 IC 0.046、故意前视 0.362、纯噪声 0.002
- **并簇阈值 = 0.60** —— 落在"同源 0.73~0.93"与"真独立 <0.3"之间

**那 59 个因子最后一个都没过门。** 最接近的 `alpha_070` 反向连过四关，
死在样本外（OOS t=1.90 < 1.96）和换手率一致性上。

---

## 怎么用

### 装

```bash
pip install factor-audit               # 只依赖 pandas / numpy / scipy
pip install "factor-audit[parquet]"    # 要读 parquet 面板时
```

### 跑

```bash
factor-audit --panel mypanel.parquet --demo                    # 先看报告长什么样
factor-audit --panel mypanel.parquet --factor myfactor.csv     # 单因子
factor-audit --panel mypanel.parquet --factors myfactors.csv   # 多因子（含并簇）
```

也可以在自己的研究流程里 import：

```python
from factor_audit import check_panel, audit_factor, audit_factors

print(check_panel(my_panel).text())        # 第一步：先体检数据

r = audit_factor(my_panel, my_factor_df)   # 第二步：再审因子
print(r.text())

r.nw[20]["t_nw"]          # 可直接引用的数字，不用解析文本
r.nw[20]["inflation"]     # t 值虚高倍数
r.lookahead_suspected     # 前视告警
r.passes_cost_gate        # 是否过成本门

r.concentration.flips_negative   # 去掉最好几期是否翻负
r.concentration.concentrated     # 收益是否集中在少数几期
r.concentration.win_rate         # 正收益期占比
r.concentration.skew             # 偏度
```

★ `r.concentration` 在样本期数不足 8 期时为 `None`（说不出分布就不报），
`r.decay` 同理。取值前先判空。

### 数据格式

**面板最低只要 `date / code / close` 三列。列名不用改**，常见别名自动映射：

```
  列名映射：trade_date→date, ts_code→code, 收盘→close, 换手率→turn, is_st→isST
```

支持 tushare（`trade_date/ts_code`）、baostock（`date/code`）、Wind 中文导出。
**映射结果会打印出来让你核对** —— 手工改列名容易把 high/low 弄反。

可选列缺了不报错，只会降级并告诉你影响：
`turn`（算成本）、`isST`（ST 涨跌停是 ±5% 不是 ±10%）、
`suspended`（停牌）、`n_days_listed`（识别新股）、`suspect_resume`（识别复牌）。

**因子 CSV 只要三列，不需要给我们公式：**

```csv
date,code,value
2026-08-10,600030,0.0123
2026-08-10,000001,-0.0456
```

多因子就是每个因子一列，**列名随你**（我们不需要知道它是什么）：

```csv
date,code,myvol,mymom,myrev
2026-08-10,600030,-0.021,0.043,-0.008
```

### 数据从哪来 —— 你自己接

我们**不内置数据源，也不分发行情**。三个理由，前两个是实测踩出来的：

1. **免费源不是可依赖的基础设施** —— 测并发时我把自己的 IP 弄进了 baostock
   黑名单（`10001011 黑名单用户`）。若这段代码在你机器上，你会认为是产品坏了
2. **baostock 非线程安全** —— 多线程共用全局 socket，直接
   `'utf-8' codec can't decode byte 0xf5` 然后卡死
3. **不分发行情就不碰版权** —— 免费 API 通常只授权自用，不授权再分发

**你自己的数据源大概率比免费源好**（Wind / 聚源 / 自建库），而且它挂了不是我们的问题。

---

## 报告长什么样

### 第一步：面板体检（必须在审因子之前）

```
====================================================================
面板体检
====================================================================

  207,416 行 / 287 只 / 2023-08-11 ~ 2026-08-10

  ⚠  退市股偏少
       仅 2 只（0.7%）已退市
       ⇒ A 股近年年均退市率约 1%，多年样本应更高。可能只含部分退市股
  ✅ 异常跳空        无单日涨跌超 35% 的跳空
  ✅ 每日覆盖        无瘦日（按年内中位数判定）
  ✅ 停牌标记        suspended 完整，其中停牌 368 行
  ✅ 时间连续性      725 个交易日无异常缺口
  ✅ ST 标记         isST 覆盖，ST 占 4.3%

  ✅ 无 BLOCK 项，可以继续审查因子
```

**顺序有意义**：先看到漂亮的 IC 再看到"你的数据有问题"，人会倾向于相信前者。

### 第二步：因子审查

```
① 可执行口径（T 日算、T+1 买）
   h=20  含当日 IC +0.0483 → 可执行 +0.0464  缩水 3.9%

② Newey-West 调整
   h=20  IC +0.0464  t朴素 +6.47 → t_NW +2.06  ★高估 3.1x  (n=684)

③ 分年度符号一致性
   2023  IC -0.0567      ← 符号翻转
   2024  IC +0.0566
   2025  IC +0.0737
   2026  IC +0.0351
   ⇒ ★样本期依赖

④ log(price) 中性化
   原始 IC +0.0464 → 中性化后 +0.0433  掉 6.6%

⑤ 信号时效（IC 随持有期的衰减形状）
   h=5  h=10  h=20  h=40  h=60  h=120
   相对强度  1.00  1.08  1.08  1.32  1.13  0.92
   ⇒ 形状：平稳
   样本内 持续增强 / 样本外 快速衰减  ★样本外改变
      ⇒ 时效建议不可靠：形状本身不稳定

⑥ 成本可行性
   毛/成本 = 1.21x   门槛 3.00x   ❌ 不过
   ⇒ 换手 94%/期 是因子自身属性（改不了）。按此换手，毛收益需达 35.5%/年 才过门

⑦ 收益来源集中度（夏普会骗人，偏度不会）
   全部 35 期年化   +14.37%
   去掉最好 3 期      -15.88%   ★翻负
   中位数期收益       +1.78%/期   ← 高于均值 ⇒ 有极端负值拖低均值
   正收益期占比       60%
   偏度 +0.54   峰度 +2.93
   ★ 去掉最好 3 期就翻负 ⇒ 收益不是系统性的，是少数几期贡献的
      贡献最大的期：2024-02-06 (+42.6%)、2024-09-05 (+23.1%)、2025-04-10 (+14.5%)
```

**⑦ 是这一版新加的，也是最容易被年化数字掩盖的一项。** 去掉最好几期就翻负，
说明那个年化不是选股能力，是少数几期的运气 —— 而年化和夏普都不会告诉你这件事。

### 多因子：你以为测了 5 个方向，其实只测了 3 个

```
★ 独立方向计数（截面秩相关 ≥ 0.6 视为同源）
   [低波20] 低波20, 低波60, 低振幅  ← 同源，只算 1 个方向
   [动量20] 动量20
   [反转5] 反转5

   你交来 5 个因子 → 实际只有 3 个独立方向
   Bonferroni 门（按 3 个独立方向）：|t_NW| > 2.39

因子            IC       t朴素    t_NW    高估    年符号   过门
低波20      +0.0811   +8.76   +2.49   3.5x    一致    ✅
低波60      +0.0748   +7.33   +2.05   3.6x   ★翻转    ❌
低振幅       +0.0747   +7.70   +2.16   3.6x    一致    ❌
动量20      -0.0464   -6.47   -2.06   3.1x   ★翻转    ❌
反转5       +0.0281   +4.40   +2.43   1.8x   ★翻转    ❌

   1/5 个因子过门
```

**注意「低波20 过门、低波60 不过」**：它们同属一簇。真实结论是
**"波动率这个方向有东西，但只算一个发现"** —— 不是"我找到了一个能用的因子"。

---

---

## ★ 这些阈值是怎么来的，以及为什么可能不适合你的市场

代码是 MIT 的，随便 fork。**但阈值不是数学推导，是实测结果** ——
它们来自 954 万行 A 股面板 + 59 个公开因子走完六关。

我们实测了这些阈值在**同一市场不同板块**上的稳定性，结果分成两类：

### 可移植的：统计结构类

```
                 vol20~vol60   vol20~hl20   vol20~mom20
沪深主板 n=6.0M    +0.786        +0.909       +0.112
创业板   n=2.2M    +0.748        +0.903       +0.138
科创板   n=0.7M    +0.764        +0.899       +0.199
```

**同源的还是同源（0.75~0.91），独立的还是独立（0.11~0.20）。**
并簇阈值 `0.60` 在三个板块上都落在同一个空档里。

前视告警线同理 —— 各板块 IC 均值 0.047~0.078，`0.15` 都在正常量级之上。

⇒ **这两个大概率可以直接用**，因为相关性结构是统计规律。

### ★必须重新标定的：成本与时效类

同一个因子，三个板块：

```
             毛收益      换手     毛/成本
沪深主板     −44.43%    97.9%   −3.59x
创业板        −4.13%    97.5%   −0.33x
科创板        +8.36%    97.0%   +0.68x
```

**毛/成本比从 −3.59x 到 +0.68x。** 波动水平也不同（20 日约 10.2% / 12.5% / 13.0%）。

原因是这类阈值由**交易制度**决定，不是统计规律：手续费、印花税、
涨跌幅限制、标的波动率。跨市场必然不同：

| 你的市场 | 直接用 A 股阈值会怎样 |
|---|---|
| **美股** | 成本远低于 50.2bp，`3×` 门槛会**过严** —— 杀掉本来可用的因子 |
| **商品期货** | 高频毛利空间远大于 A 股，`3×` 会把所有高频因子误杀 |
| **转债 / 港股** | 涨跌幅规则不同，`MAX_ABS_RET = 0.35` 的跳空判据会误报 |
| **A 股但机构成本** | 万一免五 + 融券，成本可能只有散户的 1/3 |

⇒ **成本门（`3×`）、跳空线（`0.35`）、新股天数（`10`）、
衰减分界（`1.5 / 0.8`）这四个必须按你的市场重标。**

### 怎么自己重标

流程写在这里，你完全可以自己跑：

1. 拿一批**已知无效**的因子（随机数、纯噪声）跑一遍，得到该市场 IC 的噪声底线
2. 拿一批**公认有效**的因子跑一遍，得到正常量级 → 前视线定在两者之间
3. 按你的真实成本（手续费 + 印花税 + 滑点）算往返 bp，代入成本门
4. 用**阳性对照**验证：故意造一个前视因子、一个只靠两期暴涨的因子，
   确认新阈值能抓到它们
5. 用**阴性对照**验证：正常因子不被误报

**第 4、5 步最容易被跳过，而跳过它就等于没标定** ——
我们自己在真实面板上被打回来三次（见下方校准记录）。

## 我们自己踩过的坑（校准记录）

在 **954 万行真实 A 股面板**上跑，暴露了体检自己的三个误报，都已修：

| 误报 | 根因 | 修法 |
|---|---|---|
| 119 个交易日被判 BLOCK | 用**全期**中位数比。A 股上市数从 2018 的 3268 只长到 2026 的 5421 只，2018 全年都"偏少" | 改按**年内**中位数 |
| `suspended` 全 False 报 ✅ | 只查了 null，漏了"列存在但全 False" | 全 False 单独告警 |
| 152 行跳空全报"复权错误" | 94 行是**复牌**、43 行是**新股上市初期**（前 5 日无涨跌幅限制），都是真实价格变动 | 有标记的先扣掉 |

修完后 **152 → 15 行**。剩下 15 行是真问题：价格 0.24~0.97 的退市轨股票，
在 38~58 天**未标注停牌**后复牌。

**⇒ 误报会让你忽略真告警，所以校准比多加检查更重要。**
一个报 119 天 BLOCK 的工具，你第一次跑完就再也不会信它。

---

---

## 需要针对你的市场做专属标定？

代码 MIT，随便用。**但如果你做的不是 A 股，成本类和时效类阈值需要重标** ——
上面那节列了自己动手的流程，你完全可以自己跑。

如果想省下这部分工作，我们提供三项服务：

| 服务 | 内容 | 适合谁 |
|---|---|---|
| **市场标定包** | 用你的历史面板跑一遍完整校准流程，输出一份《专属阈值标定报告》+ 一个可直接用的配置文件 | 做美股 / 港股 / 转债 / 商品期货的团队 |
| **审查报告解读** | 报告说"分年度符号翻转"之后怎么办 —— 是样本期依赖、风格轮动，还是构造有问题 | 拿到报告但不确定怎么处置的研究员 |
| **换皮检验** | 把你的因子与我们已走完六关的 59 个公开因子（GTJA191 + Alpha101）比对重叠度，看它是不是某个已知因子的变体 | 担心自己的 alpha 其实是成交额代理的人 |

**★ 第三项是唯一需要我们数据的** —— 它要一个已实测的基准因子库做对照，
fork 代码拿不到。前两项本质是把我们踩坑的经验直接给你。

标定服务按市场报价（取决于面板规模、市场数量、是否需要接你的数据库）。

**联系：154882199@qq.com** —— 说明你的市场、面板大概规模、想解决什么问题就行。
也欢迎直接开 [GitHub Issue](https://github.com/val1813/factor-audit/issues) 提问。

⚠ 一句实话：**现在还没有外部客户跑过这个工具。** 所以第一批联系我们的，
拿到的会是更多时间和更低价格，代价是要容忍不成熟的地方。

---

## Roadmap：已知坑位，待填

这个工具的定位是**因子检验领域已知缺陷的知识库 + 可执行代码**。
坑在进化 —— 十年前没人提"换皮因子"，三年前没人提"信号衰减形状"。

已实装：

- [x] Newey-West 自相关调整（实测高估 1.8~3.1x）
- [x] 生存者偏差检测（实测毛收益虚高 45.88pp）
- [x] 前视污染告警（正常 IC 0.046 / 前视 0.362 / 噪声 0.002）
- [x] 换皮因子并簇 + Bonferroni（按独立方向数定门槛）
- [x] 可执行口径（T 日算、T+1 买）
- [x] log(price) 中性化
- [x] 成本可行性（3× 安全系数）+ 拆解为换手/毛收益两个维度
- [x] 信号时效（衰减形状，不用 argmax）
- [x] 收益来源集中度（偏度 + 去掉最好几期是否翻负）
- [x] 面板体检六项

待填（**吸收标准：必须有明确失败案例 + 能给出可判定的阈值**）：

- [ ] **因子自身动量诊断** —— 因子收益序列的自相关过高 ⇒ 暴露漂移，
      可能在拟合短期噪声。*研究完成，缺我们自己的失败案例，暂不实装*
- [ ] **Fama-MacBeth 截距检验** —— 中性化后截距仍显著 ⇒ 漏了一个风险因子。
      *需要行业分类数据，客户面板未必有*
- [ ] **滚动窗口稳定性评分** —— 不只看年际符号，看滚动窗口的波动率
- [ ] **多市场基准库** —— 把 59 个因子在非 A 股市场跑一遍，扩展可移植性结论

**不吸收什么**：新的因子构造方法（那是造因子，不是审因子）、
没有明确判据的建议（写不成 `assert` 的东西无法自动化）。

新加的检查项**先作为可选、默认关闭**，积累反馈后再考虑提级 ——
现有这几关是拿 954 万行 + 59 个因子标定过的，不能被没踩实的新研究破坏。

**踩到新坑欢迎开 Issue**：如果你发现某个静默缺陷我们没查，
把案例发来（不需要给因子公式，给现象就行），验证成立我们就加进去。

## 已知限制

- 报告说的是**「这个因子的检验有没有问题」**，不是「这个因子能不能赚钱」
- 成本口径按 A 股散户实测（往返 50.2bp / 10 只持仓）。机构成本低得多，
  可传 `n_hold` 调整持仓数，但成本率目前是常数
- 独立簇 + Bonferroni 需要一次传入多个因子；单因子模式不算这一项
- 面板体检的阈值在 A 股上标定。其他市场需要重新标定，尤其涨跌停相关的判据

## 测试

```bash
pip install "factor-audit[dev]"
pytest        # 94 passed
```

测试锁住了几条不能退化的行为：Newey-West 必须**压低** t 而不是抬高、
前视告警在正常因子上不能误报、成本门安全系数必须是 3、
`lag1` 不能含 T 日到 T+1 的收益、上市数自然增长不能被判成采集中断。

已在干净虚拟环境验证：**pandas 3.0.5 / numpy 2.5.2，94 测试全过，零数据源依赖。**

## 许可

MIT

---

<a name="english"></a>

# English

## What it does

In one line: **it checks whether your factor test is sound — not what to buy.**

An analogy. You cooked a dish and it tastes good. This tool does not judge the
taste. It checks **whether your scale is calibrated, your thermometer works, and
your ingredients are past their date.**

Because the most common failure in quant research isn't bad thinking — it's a
**silent defect in the computation that happens to bias results favourably.**
You get no warning.

### What it catches

| Problem | Plain words | Measured magnitude |
|---|---|---|
| **Inflated t-stat** | The same information counted 20 times | **1.8–3.1×** overstated |
| **Survivorship bias** | Delisted names missing from your data | Gross return inflated **45.88pp** |
| **Lookahead leakage** | Future information mixed into the factor | Normal IC 0.046, leaky **0.362** |
| **Relabelled factors** | 12 factors are really 3 directions | Volatility siblings correlate **0.73–0.93** |
| **Costs eat it** | Gross return can't cover fees | Gate is **3×** cost, not 1× |
| **Return is a few lucky periods** | Good annualised figure flips negative once the best few periods are dropped | Measured +24.62% → **−15.44%** |

### Three things it does not do

| Not this | Why |
|---|---|
| **Tell you what to buy** | That needs expected return, which can only come from your factor. We never see it |
| **Deploy for you** | Measured: "construction optimisation" moves ±17bp on most factors; "friction reduction" leaves 0.112bp/month at portfolio level. Not worth selling |
| **★Upload anything** | Fully local. Factor values stay in your process; the report is written to your disk |

## Why it works

### 1. Inflated t-stats: one piece of information counted 20 times

You compute IC over a 20-day holding period. Today's IC and tomorrow's IC
**share 19 days of returns** — they are not two independent pieces of evidence.

Skipping the adjustment counts one observation twenty times. Newey-West fixes it.
**Measured overstatement: 1.8–3.1×.**

⇒ A factor at `t=6.47` is really `t=2.06`. **The first ships; the second doesn't.**

### 2. Survivorship bias: the sneakiest, because it barely moves IC

Same factor, two datasets (real panel, 9.54M rows, 2018–2026, 11.5% delisted):

```
With delisted    IC +0.0657   t_NW +6.56   gross −50.44%
Survivors only   IC +0.0696   t_NW +6.83   gross  −4.56%
```

**IC differs by 5.9%; gross return differs by 45.88pp** — turning "−50% a year"
into "−4.6%".

**Anyone reading only IC misses it entirely.** And free data sources omit
delisted names by default:

```
tdx        delisted quotes return {"code":0,"data":[],"message":"success"}
           — empty data plus "success"; zero delisted names among 5,628 listed
baostock   query_all_stock(today) returns 0 rows with error_code=0
```

**"Success but empty" is the most dangerous failure mode a data source has.**

### 3. Relabelled factors: get the Bonferroni denominator right

The multiple-testing bar tightens with the number of tests:

```
1 direction   → |t| > 1.96
3 directions  → |t| > 2.39
12 directions → |t| > 2.87
59 directions → |t| > 3.34
```

Counting 12 when you only tested 3 **over-penalises** and kills real factors.
But you need to know you only tested 3. This tool clusters first, then sets the
bar, and warns when several passing factors sit in the same cluster:
**that is one finding, not several.**

### 4. Signal decay: report the shape, not the "optimal horizon"

You want to know how long your signal lasts. The obvious approach is to find the
peak of the IC curve — **and that approach is wrong.**

Measured: **5 of 6 factors have monotonically rising IC**, with the peak outside
the scan range:

```
Low volatility   1.00 → 1.17 → 1.36 → 1.57 → 1.67 → 1.79   sustained
Low turnover     1.00 → 1.23 → 1.50 → 1.79 → 1.96 → 2.16   sustained
Momentum (20d)   1.00 → 1.11 → 1.24 → 1.15 → 0.99 → 0.65   decaying
Reversal (5d)    1.00 → 0.82 → 0.91 → 0.89 → 0.80 → 0.44   decaying
```

On a monotone curve `argmax` always returns the scan boundary — an "optimal
horizon of 120 days" reflects where we stopped looking, not the factor.

⇒ Reporting the **decay shape** instead produces real discrimination, and
**5 of 6 shapes hold out-of-sample**. When the shape is unstable the tool says
only that, and gives no horizon number — contradicting yourself immediately
after a warning cancels the warning.

### 5. These thresholds are measured, not cited

They come from running **59 public factors** (32 from GTJA191, 27 from
WorldQuant Alpha101) through the full gate set:

- **Cost safety factor = 3**, not 1 — a factor with gross/cost = 2.2× still lost 87%
- **Lookahead line = 0.15** — normal IC 0.046, deliberate leakage 0.362, pure noise 0.002
- **Cluster threshold = 0.60** — between "same source 0.73–0.93" and "independent <0.3"

**None of the 59 passed.** The closest, reversed `alpha_070`, cleared four gates
and died out-of-sample (OOS t=1.90 < 1.96) and on turnover consistency.

## How to use it

```bash
pip install factor-audit                # depends only on pandas / numpy / scipy
pip install "factor-audit[parquet]"     # to read parquet panels

factor-audit --panel mypanel.parquet --demo
factor-audit --panel mypanel.parquet --factor myfactor.csv
factor-audit --panel mypanel.parquet --factors myfactors.csv
```

```python
from factor_audit import check_panel, audit_factor

print(check_panel(my_panel).text())        # step 1: check the data
r = audit_factor(my_panel, my_factor_df)   # step 2: audit the factor
print(r.text())

r.nw[20]["t_nw"]          # numbers you can use directly, no text parsing
r.lookahead_suspected
r.passes_cost_gate

r.concentration.flips_negative   # does dropping the best few periods flip the sign
r.concentration.win_rate         # share of profitable periods
r.concentration.skew
```

`r.concentration` and `r.decay` are `None` when the sample is too short to say
anything (fewer than 8 periods) — check before use.

**The panel needs only `date / code / close`.** Column names are mapped
automatically (tushare, baostock, Wind Chinese exports), and the mapping is
printed so you can check it. Optional columns degrade gracefully rather than
erroring.

**Factor CSV needs three columns — no formula required:**

```csv
date,code,value
2026-08-10,600030,0.0123
```

For several factors, one column each; **name them whatever you like** — we don't
need to know what they are.

### You bring the data

We bundle **no data source** and distribute **no market data**. Two of the three
reasons were learned the hard way:

1. **Free sources are not dependable infrastructure** — testing concurrency got
   our IP blacklisted by baostock (`10001011`). Shipped to you, that reads as
   our product being broken
2. **baostock is not thread-safe** — threads share a global socket and it dies
   with `'utf-8' codec can't decode byte 0xf5`
3. **Distributing no quotes means no copyright exposure**

Your own source is probably better than a free one (Wind, Gildata, in-house),
and when it breaks it isn't our problem.

## Calibration record

Running against a **9.54M-row real A-share panel** exposed three false positives
in our own checks, all fixed:

| False positive | Root cause | Fix |
|---|---|---|
| 119 trading days flagged BLOCK | Thin-day used a **whole-period** median; A-share listings grew 3,268 → 5,421, so all of 2018 looked thin | Compare **within year** |
| `suspended` all-False reported OK | Only nulls were checked, not "column present but never True" | Separate warning |
| 152 jumps all called "unadjusted split" | 94 were flagged **resumptions**, 43 were **new listings** (no price limit for the first 5 days) — both real price moves | Exclude when flagged |

152 → 15 after the fixes. The remaining 15 are genuine: delisting-track names
priced 0.24–0.97 resuming after 38–58 day **unflagged** suspensions.

**⇒ False positives make you ignore real warnings, so calibration matters more
than adding checks.** A tool that flags 119 days of BLOCK loses your trust on
first run.

---

## Need calibration for your market?

The code is MIT — use it freely. **But if you don't trade A-shares, the cost and
timing thresholds need recalibration.** The section above lays out the procedure;
you can absolutely run it yourself.

If you'd rather not, we offer three things:

| Service | What you get | Who it's for |
|---|---|---|
| **Market calibration** | We run the full calibration on your historical panel and deliver a threshold report plus a ready-to-use config | Teams trading US equities, HK, convertibles, futures |
| **Report interpretation** | What to do after the report says "year-by-year sign flips" — sample-period dependence, style rotation, or a construction bug | Researchers holding a report but unsure how to act |
| **Relabelling check** | We compare your factor against the 59 public factors we've already run through the full gate set (GTJA191 + Alpha101) | Anyone worried their alpha is a turnover proxy |

**★ Only the third needs our data** — it requires an already-measured benchmark
library, which forking the code won't give you. The first two are our
hard-won calibration experience, applied to your market.

Calibration is quoted per market (depends on panel size, number of markets, and
whether we need to connect to your database).

**Contact: 154882199@qq.com** — tell us your market, rough panel size, and what
you're trying to settle. Or just open a
[GitHub Issue](https://github.com/val1813/factor-audit/issues).

⚠ Being straight with you: **no external user has run this tool yet.** The first
people to get in touch will get more of our time and a lower price, in exchange
for tolerating rough edges.

## Known limits

- The report says **whether the test is sound**, not whether the factor makes money
- Cost assumptions follow A-share retail (50.2bp round trip, 10 positions);
  institutional costs are far lower and the rate is currently a constant
- Clustering and Bonferroni need several factors passed in at once
- Panel-check thresholds are calibrated on A-shares; other markets need
  recalibration, especially the price-limit logic

## Tests

```bash
pip install "factor-audit[dev]"
pytest        # 94 passed
```

Verified in a clean virtual environment: **pandas 3.0.5 / numpy 2.5.2, 94 tests
passing, zero data-source dependencies.**

## License

MIT
