Metadata-Version: 2.4
Name: whichfirst
Version: 0.1.0
Summary: Which barrier came first? Audit the triple-barrier labels that OHLC data cannot identify.
Author: Zhongchang Huang
License: MIT
Project-URL: Homepage, https://github.com/val1813/whichfirst
Project-URL: Repository, https://github.com/val1813/whichfirst
Project-URL: Issues, https://github.com/val1813/whichfirst/issues
Keywords: triple-barrier,labeling,machine-learning,quantitative-finance,data-quality,backtesting,ohlc
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.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.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: LICENSE-CC-BY-4.0.md
Requires-Dist: pandas>=1.5
Provides-Extra: research
Requires-Dist: numpy>=1.24; extra == "research"
Requires-Dist: scipy>=1.10; extra == "research"
Requires-Dist: matplotlib>=3.7; extra == "research"
Requires-Dist: pyarrow>=15.0; extra == "research"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# whichfirst

**Which barrier came first? Sometimes the data cannot say — and your labels are quietly guessing.**

**止盈和止损同时被碰到，谁先发生？数据常常回答不了 —— 而你的标签正在悄悄猜。**

[![tests](https://img.shields.io/badge/tests-50%20passing-brightgreen)](#verify-everything-yourself-自己验证)
[![python](https://img.shields.io/badge/python-3.10%2B-blue)](#install-安装)
[![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
[![core deps](https://img.shields.io/badge/core%20dependencies-none-brightgreen)](#design-principles-设计原则)
[![pip](https://img.shields.io/badge/pip-whichfirst-blue)](#install-安装)

**[English](#the-60-second-version)** · **[中文](#一分钟看懂)**

---

## The 60-second version

If you train models on **triple-barrier labels** — take-profit, stop-loss, time
limit — some of your labels are not measurements. They are assumptions.

Here is one bar of daily data:

```
open 100    high 106    low 94    close 99        target 105    stop 95
```

The high cleared your target. The low broke your stop. **Both happened.** A daily
bar records the extremes, not their order, so it cannot tell you which came
first. Up-then-down is a win. Down-then-up is a loss.

Most labelling code returns a single label here. It has to invent one.

`whichfirst` returns the truth instead:

```python
from whichfirst import OHLCBar, audit_first_passage

audit_first_passage([OHLCBar(open=100, high=106, low=94, close=99)],
                    upper=105, lower=95)
# labels={-1, 1}   status='open_aware_dual_hit'   ← both remain possible
```

Point it at a whole training set and it tells you how much of your target is invented:

```text
2,998 events, horizon=10 sessions.
  evaluated      2,998
  unidentified   449 (14.98% of evaluated)   ← label cannot be recovered from these bars
```

**One label in seven, on a real dataset, with commonly used settings.**

---

## 一分钟看懂

如果你用**三重障碍法**（止盈、止损、时间限制）给数据打标签做机器学习，
那么你的一部分标签不是"测出来的"，是"假设出来的"。

看这一根日线：

```
开盘 100    最高 106    最低 94    收盘 99        止盈 105    止损 95
```

最高 106 碰到了止盈，最低 94 跌破了止损 —— **两件都发生了**。
但日线只记录极值、不记录顺序，所以它无法告诉你谁先到。
先涨后跌是赚，先跌后涨是亏。

**大多数打标代码在这里会返回一个标签。它只能编一个。**

`whichfirst` 返回真相：

```python
from whichfirst import OHLCBar, audit_first_passage

audit_first_passage([OHLCBar(open=100, high=106, low=94, close=99)],
                    upper=105, lower=95)
# labels={-1, 1}   status='open_aware_dual_hit'   ← 两种都可能
```

对整个训练集跑一遍，它会告诉你有多少标签是编造的：

```text
2,998 events, horizon=10 sessions.
  evaluated      2,998
  unidentified   449 (14.98% of evaluated)   ← 这些标签无法从K线恢复
```

**真实数据、常用参数下，七个标签里有一个是编的。**

---

## Why this matters more than it sounds · 为什么这比听起来严重

Label noise is not like feature noise. A wrong feature adds variance. A wrong
**label** teaches the model the opposite of the truth, and it does so
systematically.

标签噪声和特征噪声不是一回事。特征错了只是增加方差；**标签错了是在教模型
反过来的结论**，而且是系统性的。

| | English | 中文 |
|---|---|---|
| **Not random** | Ambiguous bars are the volatile ones. Your model learns its worst lessons exactly where the stakes are highest. | 歧义集中在高波动样本 —— 模型恰好在最要命的地方学到最错的东西 |
| **One-directional** | Every library resolves ties the same way every time. That is systematic bias, not noise that averages out. | 每个库的 tie 约定都固定不变 ⇒ 系统性偏差，不是能被平均掉的噪声 |
| **Invisible** | No exception, no warning, no NaN. Your accuracy is computed against the invented labels, so it looks fine. | 不报错、不告警、不产 NaN。准确率是拿编造的标签算的，所以看起来很正常 |
| **Survives validation** | Train, validation and test splits all inherit the same convention. Cross-validation cannot see a bias shared by every fold. | 训练/验证/测试集继承同一约定 ⇒ 交叉验证看不见所有折共享的偏差 |

That last row is the reason this package exists.
**You cannot detect this problem with better modelling.**
It has to be checked at the data layer, before training.

**最后一行是这个包存在的理由：这个问题无法靠"改进模型"发现，
只能在训练之前、在数据层检查。**

---

## How much of your data is affected · 影响面有多大

Prevalence is governed almost entirely by **barrier width**. Measured on a China
A-share panel, 10-session horizon, barriers at `k` × a 20-day volatility estimate
(full grid in [`results/daily_dual_hit_sensitivity.csv`](results/daily_dual_hit_sensitivity.csv)):

影响面几乎完全由**障碍宽度**决定。以下在 A 股面板上实测，持有 10 个交易日，
障碍设为 20 日波动率的 `k` 倍：

| Barrier width · 障碍宽度 | Unidentified · 不可识别比例 |
|---|---|
| 0.5 σ | **17.2 %** |
| 1.0 σ | 2.7 % |
| 2.0 σ | 0.2 % |

Narrow barriers are what short holding periods require.
**The tighter your strategy, the more of your training set is fabricated.**

窄障碍正是短持有期策略需要的。**你的策略越紧，训练集里编造的部分越多。**

Independently reproduced on a separate sample period (2024–2026): 16.6 % / 2.7 %
/ 0.2 % — the same three numbers.
（换样本期 2024–2026 独立复现：16.6% / 2.7% / 0.2%，三个数字一致。）

---

## Install · 安装

```bash
pip install whichfirst
```

From source · 从源码：

```bash
git clone https://github.com/val1813/whichfirst
cd whichfirst
pip install -e .
```

The core primitive imports nothing outside the standard library. `pandas` is
required only by the batch layer.
（核心原语只用标准库，`pandas` 仅批量层需要。）

---

## Quick start · 快速上手

### 1. One event · 单个样本

```python
from whichfirst import OHLCBar, audit_first_passage

bars = [OHLCBar(open=100, high=106, low=94, close=99)]
result = audit_first_passage(bars, upper=105, lower=95)

result.labels        # frozenset({-1, 1})
result.is_singleton  # False  ← this label is not trustworthy · 这个标签不可信
result.status        # 'open_aware_dual_hit'
```

### 2. A whole training set · 整个训练集

Generate the two input templates · 生成两张输入模板：

```bash
python -c "from whichfirst import write_template; write_template('.')"
```

This writes `events_template.csv` and `bars_template.csv`. Fill them in with your
own rows and save them as `events.csv` and `bars.csv` — the names used below.
（写出的是 `events_template.csv` 与 `bars_template.csv`，填好后另存为下面用到的
`events.csv` 和 `bars.csv`。）

`events.csv` — one row per labelled event. Barriers are **absolute prices**.
（一行一个样本，障碍填**绝对价格**。）

```csv
symbol,entry_date,upper,lower
600030,2026-08-03,29.00,27.00
000001,2026-08-03,12.00,11.20
```

`bars.csv` — the OHLC panel your labels come from.（你的行情面板。）

```csv
symbol,date,open,high,low,close
600030,2026-08-03,28.10,28.30,27.90,28.00
600030,2026-08-04,28.05,29.20,27.80,29.10
```

```python
from whichfirst import audit_events, load_events_csv, load_bars_csv

report = audit_events(load_events_csv("events.csv"),
                      load_bars_csv("bars.csv"),
                      horizon=10)

print(report.summary())
report.unidentified.to_csv("labels_to_fix.csv", index=False)
```

Use the two loaders rather than bare `pd.read_csv`: they force `symbol` to text,
so a zero-padded ticker such as `000001` is not read as the integer `1`. That
corruption fails as `no_entry_bar` on every affected row, which looks like
missing data rather than a dtype problem.

**请用这两个 loader 而不是裸 `pd.read_csv`** —— 它们强制 `symbol` 为文本，
否则 `000001` 会被读成整数 `1`，与行情面板永远对不上，
而报错是 `no_entry_bar`（看起来像"数据缺失"，实际是类型问题）。

```text
symbol entry_date  labels label  identified              status
600030 2026-08-03     [1]     1        True          upper_only
000001 2026-08-03 [-1, 1]  <NA>       False open_aware_dual_hit
```

### 3. What to do with the flagged rows · 拿到名单后怎么处置

The audit does not decide for you.（工具不替你决定。）

| Option · 选项 | Trade-off · 代价 |
|---|---|
| **Drop them** · 丢掉 | Costs sample size. Assumes nothing. · 损失样本量，但不做任何假设 |
| **Re-resolve from intraday data** · 用分钟线补判 | The only option that actually recovers the label · 唯一真正能恢复标签的做法。See [`tick_truth_one_day.py`](tick_truth_one_day.py) |
| **Keep with reduced weight** · 降权保留 | Treats the label as soft · 当作软标签，不丢信息 |

---

## Why the answer is correct, not just plausible · 为什么这个答案是对的

This is not a heuristic. The label set is **sharp**: exactly the labels
consistent with the recorded bar, no more and no fewer.
（这不是启发式规则。返回的标签集是**紧的** —— 恰好是与该 K 线相容的全部标签。）

**A label is uniquely identified when · 标签唯一可识别的两种情形：**

1. the first barrier-touching bar reaches only one barrier — or
   首个触及障碍的 K 线只碰到一侧；或
2. that bar **opens** already beyond one barrier. The open is monitored before
   the extremes, so the first touch is pinned. *This second case recovers labels
   that a high/low-only check throws away.*
   该 K 线**开盘价**已越过一侧 —— 开盘先于极值被观察到，所以首次触及是确定的。
   **这第二种情形能免费救回一批"只看最高/最低"会丢掉的标签。**

**A label is set-valued only when** that bar reaches both barriers *and* opens
strictly between them. Then both orderings are genuinely consistent with the
data, and no tie convention recovers the truth — it can only assert it.
（**只有**当该 K 线同时碰到两侧、且开盘价严格在两者之间时，标签才是集合值。
此时两种顺序都与数据相容，任何 tie 约定都不是"恢复"真相，而是"断言"。）

Three properties follow, each enforced by a test
（三条性质，每条都有测试强制）：

- **No silent substitution.** A dual hit never collapses to a single label.
  双碰绝不塌缩成单一标签。
- **No silent dropping.** Rows that cannot be evaluated are reported with a
  reason (`no_entry_bar`, `no_future_bars`, `invalid_barriers`, `invalid_ohlc`) —
  a shorter report must never read as "everything else is fine".
  无法评估的行会带原因报出 —— 报告变短绝不能读成"其余都没问题"。
- **No diluted statistics.** Unevaluable rows are excluded from the prevalence
  denominator, so skips cannot make your data look cleaner than it is.
  无法评估的行不进分母，所以 skip 不会让数据看起来比实际干净。

### Positioned against the literature · 文献定位

The [literature audit](literature_positioning_audit.md) records **15
full-text-verified papers** using triple-barrier labelling:

- **10 of 15** never state which bar field triggers a barrier.
  15 篇里有 10 篇从未说明哪个字段触发障碍。
- **13 of 15** never state how a same-bar dual touch is handled.
  15 篇里有 13 篇从未说明同一根 K 线双碰怎么处理。

Prior work is cited as foundation, not competition: López de Prado (2018,
*AFML* Ch. 3) for the canonical target; Broadie–Glasserman–Kou (1997) for the
continuous-versus-discrete monitoring distinction; the Manski
partial-identification tradition for set-valued targets; Kang & Kim (2025) and
Jadouli (2026) as the closest precursors on daily OHLC.

---

## What this package does not claim · 本包不主张什么

Stated plainly, because these limits decide whether the tool is right for you
（直说，因为这些边界决定这工具对你是否合适）：

- **Not an alpha claim.** The frozen classifier comparison in this repository
  finds **lower** macro-F1 for the corrected-label model. Identifying your target
  is a correctness result, not a performance one.
  **不是收益主张。** 本仓库的冻结对照实验发现，修正标签后的模型 macro-F1
  **反而更低**。识别标签是正确性结果，不是性能结果。
- **It does not recover the order.** It proves when the order is unrecoverable
  from the bars you supplied. Only finer data can resolve it.
  **它不恢复顺序**，只证明顺序在你给的 K 线下不可恢复。只有更细的数据能解决。
- **The tie is not a coin flip.** The ordering asymmetry is rejected on an
  A-share session and **not** replicated on a Binance window — the divergence is
  reported as unexplained rather than explained away.
  **双碰不是抛硬币。** 顺序对称性在 A 股被拒绝，在 Binance 窗口未复现 ——
  这个分歧被如实报为"未解释"，而不是被圆过去。
- **Market-agnostic but convention-dependent.** The core logic assumes only
  ordered OHLC bars. Session definitions and overnight gaps are yours to declare.
  **不绑市场，但依赖口径。** 核心逻辑只假设有序 OHLC；交易时段定义与隔夜跳空由你声明。

A full section of results that did not come out as hoped — including a
preregistered threshold that was **not met** — is kept in
[REPRODUCTION.md](REPRODUCTION.md), because a reader needs them to judge the rest.
（一整节"不如预期的结果"，包括一个**未达成的预注册门槛**，保留在
[REPRODUCTION.md](REPRODUCTION.md) 里 —— 读者需要它们才能判断其余部分。）

---

## Design principles · 设计原则

**The core has no dependencies.** [`label_audit.py`](label_audit.py) is 66 lines
and imports only `dataclasses` and `typing`. Identification logic you cannot read
in one sitting is logic you cannot audit.
（**核心零依赖**：66 行，只 import `dataclasses` 和 `typing`。
一次读不完的识别逻辑，就是无法审计的逻辑。）

**Barriers are absolute prices, never multiples.** The batch layer refuses to
infer a volatility convention on your behalf, because barrier width is the single
parameter prevalence is most sensitive to — 0.5 σ and 2.0 σ differ by two orders
of magnitude. Guessing it would mean choosing your result for you.
（**障碍只收绝对价格**：障碍宽度恰恰是影响面最敏感的参数，0.5σ 与 2.0σ 差两个
数量级。替你猜一个波动率约定，等于替你选定结论。）

**Refusal is a feature.** Every path that cannot produce a trustworthy answer
returns a reason instead of a number.
（**拒绝是特性**：任何无法给出可信答案的路径，都返回原因而不是数字。）

---

## Verify everything yourself · 自己验证

```bash
python -m pytest -q               # 50 tests, no data access required
sha256sum -c SHA256SUMS.txt       # 114 files, byte-level integrity
python build_exhibits.py          # regenerates every figure and table
```

Every exhibit is generated from frozen result JSON — no number in the manuscript
is hand-authored. The public-data chain replicates against official Binance
archives, each file checksum-verified before use, including a
[zero-shared-code independent verifier](independent_external_verifier.py).

（每张图表都由冻结的结果 JSON 生成，稿件里没有手写的数字。公开数据链对
Binance 官方归档复现，每个文件使用前校验，并含一个
[零共享代码的独立验证器](independent_external_verifier.py)。）

| Path | Contents |
|---|---|
| [`label_audit.py`](label_audit.py) | The audit primitive — start here · 审计原语，从这里开始 |
| [`batch_audit.py`](batch_audit.py) | Training-set audit, templates, reporting · 批量审计与模板 |
| [`whichfirst.py`](whichfirst.py) | Public API · 公开接口 |
| `results/` | Frozen outputs (JSON + CSV) · 冻结结果 |
| `exhibits/` | Generated figures and tables · 生成的图表 |
| `submission/` | Manuscript, frozen specs, audit records · 稿件与冻结规格 |

---

## Citation · 引用

```bibtex
@software{whichfirst,
  title  = {whichfirst: identifying first-passage triple-barrier labels from OHLC data},
  author = {Huang, Zhongchang},
  year   = {2026},
  url    = {https://github.com/val1813/whichfirst}
}
```

Machine-readable metadata in [`CITATION.cff`](CITATION.cff).

## License · 许可

Code under [MIT](LICENSE). Derived tables, figures and documentation under
[CC BY 4.0](LICENSE-CC-BY-4.0.md).
