# AlphaGPT · 期货择时因子挖掘

> 本项目基于 **AlphaGPT** 框架改造，聚焦**期货择时**场景：把原本面向 A 股选股的通用因子
> 挖掘引擎，适配到「用户自定义 DataFrame + 期货缺失值处理 + 多空择时口径」。

用一个小型 GPT（Transformer 策略网络）**自动"写"择时因子公式**，把公式在真实行情上
**回测打分**，再用**强化学习（REINFORCE）**根据分数迭代，让模型越写越好。

最终产出一条用**你自己的列名**写成、经过**样本外验证**（按时间顺序切分，测试段不参与挖掘）的择时公式。

```
采样公式(Transformer) → 执行公式(栈式虚拟机) → 回测打分(仓位+夏普) → 策略梯度更新 → 循环
```

与传统"喂特征进 XGBoost / LSTM 出预测"的黑箱路线不同：**产出的不是黑箱预测，是一条能读的公式**——
这是符号回归相对黑箱模型的核心价值。

---

## 核心特性

- **公式即基因**：因子是一串离散 token（后缀表达式），可采样、可执行、可读
- **你的列名就是特征**：系统不内置因子计算，公式里出现的就是你 DataFrame 的列名
- **干净的样本外**：按时间顺序切分（默认前 80% 训练 / 后 20% 测试），挖公式只用训练段打分，测试段最后一次性回测，杜绝信息泄漏
- **期货缺失值处理**：非永续品种用 `valid_mask` 记录"可交易真相"，绝不 `fillna(0)`
- **可解释性**：复杂度惩罚逼出简洁公式 + 因子归类判断是否经典因子换皮
- **频率无关**：内核只认"第几根 K 线"，日线 / 分钟线通吃

---

## 安装

需要 Python 3.10+（推荐 3.11）。

```bash
pip install -r requirements.txt
```

依赖很轻：`torch / numpy / pandas / tqdm / matplotlib`。

---

## 快速上手

只需记住 3 个动词：**挖 / 看 / 用**。

```python
from timing_adapter import mine_timing, plot_result, make_factor

# 挖：喂一个 DataFrame（长表: date + instrument + 行情/因子列），得到一条公式
result = mine_timing(
    df,
    feature_cols=['open', 'high', 'low', 'close', 'volume'],
    price_col='settle',        # 期货用结算价
    steps=100,
)
print(result['formula_str'])   # 例: SUB(SUB(DELAY1(TS_MIN(...)),close),open)

# 看：训练曲线 + 最优公式净值曲线
plot_result(result, df)

# 用：把公式作用到数据上，算因子值或择时仓位(-1~1)
df_pos = make_factor(df, result['formula_str'],
                     feature_cols=result['vocab_feature_names'],
                     as_position=True)
```

完整流程见 [`demo_期货择时挖掘.ipynb`](demo_期货择时挖掘.ipynb)。

### 挖掘效果示例

在 72 个期货品种、约 3 年日线上挖掘的一个结果（`plot_result` 输出）：

<p align="center">
  <img src="assets/绩效.png" alt="挖掘效果：训练曲线 + 最优公式净值曲线" width="900">
</p>

- **左图 · 训练曲线**：随训练推进，平均奖励（橙）稳步上升并逼近训练段最优分数（蓝），
  说明模型确实在学着写出更高分的公式
- **右图 · 净值曲线**：最优公式的累计净值，红色虚线标出训练/测试分界，橙色阴影为**样本外区间**——
  只有分界线右侧那段才是真正没参与挖掘的表现

> 注：选公式只看训练段，样本外段是训练结束后一次性回测得到的。真正要认的是分界线右侧那段的表现。
> 实际结果取决于你的数据、品种与参数。（上图为旧口径示意图，重新挖掘后会自动带上样本外分界标注。）

---

## 数据格式

输入是一张**长表 DataFrame**，每行是"某品种在某个时间点"的一条记录：

| date | instrument | open | high | low | close | volume | settle | ... |
|------|-----------|------|------|-----|-------|--------|--------|-----|
| 2020-01-02 | AP8888.CZC | 7740 | 7791 | 7706 | 7741 | 93362 | 7752 | ... |

- **必需列**：`date`、`instrument`、以及作为收益基准的价格列（期货用 `settle`）
- **特征列**：用 `feature_cols` 指定哪些列当特征，可以是原始行情，也可以是你自己算的因子
- 不同品种上市时间不同没关系，缺失值由框架自动处理

> 📁 **数据需自备**：仓库不含行情数据。请把你的数据放到 `data/` 目录（demo 默认读取
> `data/期货数据.csv`），或在代码里改成你自己的路径。列名对齐上表即可。

加自己的因子只需在 df 里算好新列，再把列名加进 `feature_cols`：

```python
df['ma5'] = df.groupby('instrument')['close'].transform(lambda x: x.rolling(5).mean())
# 然后 feature_cols=[..., 'ma5']
```

---

## API 概览

统一从 `timing_adapter` 导入，4 个门面函数 + 1 个体检：

| 函数 | 作用 |
|------|------|
| `mine_timing(df, feature_cols, ...)` | **挖**：训练并返回最优公式 + 因子池 |
| `plot_result(result, df)` | **看**：训练曲线 + 净值曲线 |
| `make_factor(df, expr, feature_cols, ...)` | **用**：把公式作用到数据上，算因子值 / 仓位 |
| `classify_factor(df, expr, feature_cols)` | **归类**：判断因子本质是不是经典因子换皮 |
| `check_data(df, feature_cols)` | **体检**：查看数据加载与缺失值处理结果 |

### mine_timing 常用参数

| 参数 | 含义 | 默认 |
|------|------|------|
| `mode` | `cross_section`（多品种横截面）/ `single_asset`（单品种） | cross_section |
| `direction` | `long_short`（可做空 -1~1）/ `long_only`（0~1） | long_short |
| `position_map` | `tanh`（连续仓位）/ `threshold`（离散 -1/0/1，省手续费） | tanh |
| `fitness` | `sharpe` / `calmar` / `cumret` | sharpe |
| `cost_bps` | 双边交易成本（基点） | 13.0 |
| `hold_period` | 持有期（几根 K 线） | 1 |
| `complexity_penalty` | 复杂度惩罚（惩罚公式嵌套深度，逼出简洁公式） | 0.0 |
| `steps` / `batch_size` | 训练步数 / 每步采样公式条数 | 300 / 256 |
| `train_ratio` | 按时间顺序切分：前 `train_ratio` 训练、其余测试（不打乱） | 0.8 |

---

## 架构

三层结构，从下到上关注点从"通用引擎"收敛到"期货择时场景"：

```
第3层 用户接口    demo notebook: 喂 DataFrame + feature_cols → 得到公式
                    │
第2层 择时适配层   timing_adapter/  针对「自定义DataFrame + 期货缺失值」适配
                    │  复用底层
第1层 通用引擎     model_core/  场景无关的算子库/词表/虚拟机/回测器/策略网络
```

```
timing_adapter/          择时适配层（用户直接用这层）
├─ __init__.py             门面：mine_timing / make_factor / plot_result / classify_factor / check_data
├─ df_loader.py            长表 DataFrame → [N,F,T] 张量 + valid_mask（期货缺失值处理）
├─ timing_vocab.py         动态词表：用户特征列 → token 编号
├─ dynamic_vm.py           栈式虚拟机 + 公式生成器 + 合法性掩码
├─ run_timing.py           挖掘主入口 mine_timing（RL 训练循环 + 时间顺序样本外切分）
├─ apply.py                make_factor：公式落地应用
├─ viz.py                  check_data / plot_result
└─ explain.py              classify_factor：因子归类

model_core/              通用挖掘引擎（底层，一般不用直接碰）
├─ ops.py                  算子库（19 个算子）
├─ vocab.py / vm.py        词表 / 栈式虚拟机
├─ backtest.py             回测评分（因子 → 仓位 → 夏普）
├─ alphagpt.py / engine.py 策略网络 / 训练引擎（A股选股原路径）
└─ ...
```

一次挖掘的数据流：

```
[长表 DataFrame]
   │ 数据加载: 长表 → 魔方张量 [N品种,F特征,T时间] + valid_mask + target_ret
   ▼
[动态词表] 特征列 → token 编号
   ▼
[RL 训练循环]  采样公式(Transformer) → VM执行 → 回测打分 → REINFORCE更新
   │            只用训练段打分选公式；测试段锁住，训练结束后一次性算 OOS
   ▼
[最优公式 + OOS分 + 因子池]  →  make_factor 应用 / classify_factor 归类
```

---

## 常见场景

```python
# 省手续费：离散仓位（-1/0/1），只在信号够强时才动
mine_timing(df, feature_cols=[...], position_map='threshold')

# 要简洁好懂的公式：惩罚套娃嵌套
mine_timing(df, feature_cols=[...], complexity_penalty=0.3)

# 单品种择时：只留一个品种 + single_asset 模式
mine_timing(df_one, feature_cols=[...], mode='single_asset')

# 挖更久、更充分：加大步数和批量
mine_timing(df, feature_cols=[...], steps=500, batch_size=512)
```

> **口径一致**：挖掘时用的 `position_map` 应与应用时保持一致，否则"按连续仓位挑出的公式
> 拿去跑离散仓位"，优化目标和实际用的不是一回事，夏普会对不上。

---

## 输出文件

| 文件 | 内容 |
|------|------|
| `best_timing_strategy.json` | 最优公式（token + 可读字符串）、OOS 分、特征名、因子池 Top-N |
| `timing_factor_pool.json` | 因子池（按 OOS 去重排序的候选公式） |
| `training_history.json` | 训练历史（avg_reward / best_score 曲线） |

---

## 设计取舍（诚实说明）

- **干净的样本外**：reward 只用训练段分数，测试段全程不参与挖掘与选优，训练结束后一次性评估得到 OOS
- **多层防过拟合**：训练/测试时间顺序隔离 + 横截面 median 聚合 + 防退化惩罚（防空仓刷分）+ 复杂度惩罚
- **训练算法**：REINFORCE 策略梯度，用 batch 内标准化 advantage（无独立 baseline critic）
- **频率无关**：内核只操作"第几根 K 线"的序号，不含日历假设；夏普年化因子当前按日频（`√252`）
  计算，仅影响绝对值展示、不影响公式排名

---

## License

见 [LICENSE](LICENSE)。

---

## 支持一下 🙏

如果这个项目对你有帮助，欢迎请作者喝杯咖啡，你的支持是持续维护和更新的动力！

<p align="center">
  <img src="assets/二维码收款.jpg" alt="收款二维码" width="280">
</p>

<p align="center"><i>感谢每一位支持者 ❤️</i></p>


