# n_income 因子生命周期分析

## 概述

本文档详细分析 `src/experiment/regression.py` 中 `n_income` 因子的完整生命周期，从字符串定义到最终参与模型训练的全过程。

## 因子定义

在 `src/experiment/regression.py` 第 55 行定义：

```python
FACTOR_DEFINITIONS = {
    # ... 其他因子 ...
    "n_income": "n_income",
}
```

`n_income` 是一个简单符号表达式，代表**净利润**（Net Income）财务指标。这是一个**点-in-time（PIT）数据**，需要从财务报表中获取。

---

## 第一阶段：因子注册

### 1.1 注册入口

**位置**: `src/experiment/regression.py:134`

```python
def create_factors_with_strings(engine: FactorEngine) -> List[str]:
    for name, expr in FACTOR_DEFINITIONS.items():
        engine.add_factor(name, expr)  # 注册 n_income
```

### 1.2 add_factor 实现

**位置**: `src/factors/engine/factor_engine.py:62-86`

```python
def add_factor(
    self,
    name: str,
    expr: Union[str, Node],
) -> "FactorEngine":
    """注册一个因子表达式"""
    # 如果是字符串，先解析为 Node
    if isinstance(expr, str):
        expr = self.parser.parse(expr)  # 关键步骤
    
    # 创建执行计划
    plan = self.planner.create_plan(expr, name)
    
    # 缓存执行计划
    self._factor_plans[name] = plan
    return self
```

对于 `n_income = "n_income"`，流程如下：
1. 检测到是字符串，调用 `parser.parse("n_income")`
2. 解析结果为 `Symbol("n_income")` 节点
3. 通过 `planner.create_plan()` 生成执行计划
4. 缓存计划供后续计算使用

---

## 第二阶段：表达式解析

### 2.1 解析器入口

**位置**: `src/factors/parser.py:38-52`

```python
def parse(self, formula: str) -> Node:
    """解析因子表达式字符串为 Node"""
    tree = ast.parse(formula, mode='eval')
    return self._visit(tree.body)
```

### 2.2 符号节点处理

**位置**: `src/factors/parser.py:96-99`

```python
def _visit_Name(self, node: ast.Name) -> Symbol:
    """处理名称节点（如变量名 n_income）"""
    return Symbol(node.id)
```

对于 `"n_income"` 字符串：
1. `ast.parse("n_income", mode='eval')` 生成 AST
2. AST 节点类型为 `ast.Name`，id 为 `"n_income"`
3. 调用 `_visit_Name()` 创建 `Symbol("n_income")`

### 2.3 Symbol 节点定义

**位置**: `src/factors/dsl.py:82-102`

```python
@dataclass
class Symbol(Node):
    """符号节点，代表命名变量"""
    name: str
    
    def dependencies(self) -> Set[str]:
        return {self.name}
```

`Symbol("n_income")` 的依赖集合为 `{"n_income"}`。

---

## 第三阶段：执行计划生成

### 3.1 计划器入口

**位置**: `src/factors/engine/planner.py:40-74`

```python
def create_plan(
    self,
    node: Node,
    output_name: Optional[str] = None,
) -> ExecutionPlan:
    """创建完整执行计划"""
    # 1. 提取依赖
    deps = self.extractor.extract_dependencies(node)
    
    # 2. 翻译为 Polars 表达式
    polars_expr = self.translator.translate(node)
    
    # 3. 推导数据规格
    data_specs = self._infer_data_specs(deps, schema_cache=self.schema_cache)
    
    return ExecutionPlan(
        data_specs=data_specs,
        polars_expr=polars_expr,
        dependencies=deps,
        output_name=output_name,
    )
```

### 3.2 提取依赖

**位置**: `src/factors/compiler.py:19-32`

```python
def extract_dependencies(self, node: Node) -> Set[str]:
    """从 AST 提取所有依赖的原始字段名"""
    deps: Set[str] = set()
    self._extract(node, deps)
    return deps

def _extract(self, node: Node, deps: Set[str]):
    if isinstance(node, Symbol):
        deps.add(node.name)  # n_income 被加入依赖集
    # ... 其他节点类型处理
```

对于 `Symbol("n_income")`，提取的依赖为 `{"n_income"}`。

### 3.3 推导数据规格

**位置**: `src/factors/engine/planner.py:86-148`

```python
def _infer_data_specs(
    self,
    dependencies: Set[str],
    schema_cache: SchemaCache,
) -> List[DataSpec]:
    """推导数据规格"""
    table_fields: Dict[str, List[str]] = defaultdict(list)
    
    for field in dependencies:
        # 使用 SchemaCache 查找字段所属表
        table = schema_cache.get_table_for_field(field)
        table_fields[table].append(field)
    
    # 为每张表创建 DataSpec
    for table, fields in table_fields.items():
        if schema_cache.is_pit_table(table):  # PIT 表（财务数据）
            spec = DataSpec(
                table=table,
                columns=fields,
                join_type="asof_backward",
                left_on="trade_date",
                right_on="f_ann_date",
            )
        else:  # 普通表（行情数据）
            spec = DataSpec(
                table=table,
                columns=fields,
                join_type="standard",
            )
        specs.append(spec)
```

对于 `n_income`：
1. `schema_cache.get_table_for_field("n_income")` 返回 `"financial_income"`
2. `schema_cache.is_pit_table("financial_income")` 返回 `True`
3. 生成 DataSpec：
   - `table="financial_income"`
   - `columns=["n_income"]`
   - `join_type="asof_backward"`
   - `left_on="trade_date"`
   - `right_on="f_ann_date"`

### 3.4 SchemaCache 实现

**位置**: `src/data/catalog.py`

```python
class SchemaCache:
    """缓存数据库表结构信息，提供字段到表的映射"""
    
    def get_table_for_field(self, field: str) -> Optional[str]:
        """根据字段名获取表名"""
        return self._field_to_table.get(field)
    
    def is_pit_table(self, table: str) -> bool:
        """判断是否为 PIT（Point-in-Time）表"""
        info = self._table_info.get(table)
        if info and info.table_type == TableType.PIT:
            return True
        return False
```

表类型识别逻辑：
- **PIT 表**: 包含 `ann_date` 或 `f_ann_date` 字段（财务数据表）
- **DAILY 表**: 包含 `trade_date` 字段（行情数据表）

---

## 第四阶段：数据获取与拼接

### 4.1 compute() 执行入口

**位置**: `src/factors/engine/factor_engine.py:88-120`

```python
def compute(
    self,
    factor_names: List[str],
    start_date: str,
    end_date: str,
) -> pl.DataFrame:
    """计算指定因子"""
    # 1. 获取所有需要的执行计划
    plans = [self._factor_plans[name] for name in factor_names]
    
    # 2. 合并数据规格（去重）
    merged_specs = self._merge_data_specs(plans)
    
    # 3. 从路由器获取核心宽表
    core_wide = self.router.fetch_data(
        specs=merged_specs,
        start_date=start_date,
        end_date=end_date,
    )
    
    # 4. 执行计算
    result = self._execute_with_dependencies(factor_names, core_wide)
    
    return result
```

### 4.2 数据路由器 fetch_data

**位置**: `src/factors/engine/data_router.py:48-100`

```python
def fetch_data(
    self,
    specs: List[DataSpec],
    start_date: str,
    end_date: str,
) -> pl.DataFrame:
    """根据 DataSpec 列表获取并组装数据"""
    # 分离标准表和 asof 表
    standard_specs = [s for s in specs if s.join_type == "standard"]
    asof_specs = [s for s in specs if s.join_type == "asof_backward"]
    
    # 加载标准表（行情数据）
    standard_frames = []
    for spec in standard_specs:
        df = self._load_table_from_spec(spec, start_date, end_date)
        standard_frames.append(df)
    
    # 以第一张标准表为基础（通常是 daily）
    base_df = standard_frames[0] if standard_frames else None
    
    # 使用 FinancialLoader 加载和拼接财务数据
    if asof_specs and base_df is not None:
        for spec in asof_specs:
            financial_df = self.financial_loader.load_financial_data(
                table_name=spec.table,
                columns=spec.columns,
                start_date=start_date,
                end_date=end_date,
            )
            base_df = self.financial_loader.merge_financial_with_price(
                price_df=base_df,
                financial_df=financial_df,
                date_col="trade_date",
                f_ann_col="f_ann_date",
            )
    
    return base_df
```

### 4.3 财务数据加载

**位置**: `src/data/financial_loader.py:26-83`

```python
def load_financial_data(
    self,
    table_name: str,
    columns: List[str],
    start_date: str,
    end_date: str,
) -> pl.DataFrame:
    """从数据库加载并清洗财务数据"""
    # 计算包含回看期的日期范围（默认1年）
    adjusted_start = self.get_date_range_with_lookback(start_date, lookback_days=365)
    
    # 从数据库查询
    query = f"""
        SELECT ts_code, f_ann_date, end_date, {', '.join(columns)}
        FROM {table_name}
        WHERE f_ann_date >= '{adjusted_start}' 
          AND f_ann_date <= '{end_date}'
        ORDER BY ts_code, f_ann_date
    """
    df = self.conn.execute(query).fetchdf()
    df = pl.DataFrame(df)
    
    # 数据清洗：仅保留 report_type==1（合并报表）
    if "report_type" in df.columns:
        df = df.filter(pl.col("report_type") == 1)
    
    # 去重：按 (ts_code, end_date) 取 update_flag 最大的记录
    if "update_flag" in df.columns:
        df = (
            df.sort(["ts_code", "end_date", "update_flag"], descending=[False, False, True])
            .unique(subset=["ts_code", "end_date"], keep="first")
        )
    
    return df
```

### 4.4 财务数据与行情数据拼接

**位置**: `src/data/financial_loader.py:85-136`

```python
def merge_financial_with_price(
    self,
    price_df: pl.DataFrame,
    financial_df: pl.DataFrame,
    date_col: str = "trade_date",
    f_ann_col: str = "f_ann_date",
) -> pl.DataFrame:
    """使用 asof join 将财务数据拼接到行情数据"""
    # 确保日期格式正确
    price_df = price_df.with_columns(pl.col(date_col).cast(pl.Date))
    financial_df = financial_df.with_columns(pl.col(f_ann_col).cast(pl.Date))
    
    # 使用 join_asof 进行 PIT 对齐
    # strategy='backward': 对于每个 trade_date，找 f_ann_date <= trade_date 的最新财务数据
    result = price_df.join_asof(
        financial_df,
        left_on=date_col,
        right_on=f_ann_col,
        by="ts_code",  # 按股票代码分组
        strategy="backward",  # 向后查找最新公告
    )
    
    return result
```

**拼接逻辑详解**：

| trade_date | ts_code | close | n_income (拼接后) | 来源 f_ann_date |
|-----------|---------|-------|------------------|-----------------|
| 2024-01-15 | 000001.SZ | 10.5 | 1,000,000 | 2024-01-10 |
| 2024-01-16 | 000001.SZ | 10.6 | 1,000,000 | 2024-01-10 |
| 2024-01-20 | 000001.SZ | 10.8 | 1,200,000 | 2024-01-18 |

上表展示了 `n_income` 的 PIT 拼接过程：
- 1月15日：使用前一次公告（1月10日）的净利润数据
- 1月16日：继续使用1月10日的数据（无新公告）
- 1月20日：使用最新公告（1月18日）的净利润数据

---

## 第五阶段：因子计算

### 5.1 执行计算

**位置**: `src/factors/engine/factor_engine.py:188-227`

```python
def _execute_with_dependencies(
    self,
    factor_names: List[str],
    core_wide: pl.DataFrame,
) -> pl.DataFrame:
    """按依赖顺序执行因子计算"""
    # 拓扑排序确定计算顺序
    sorted_factors = self._topological_sort(factor_names)
    
    # 创建结果 DataFrame
    result_exprs = []
    for name in sorted_factors:
        plan = self._factor_plans[name]
        # 执行 Polars 表达式
        expr = plan.polars_expr.alias(name)
        result_exprs.append(expr)
    
    # 一次性执行所有表达式
    result = core_wide.with_columns(result_exprs)
    
    return result
```

### 5.2 翻译为 Polars 表达式

**位置**: `src/factors/translator.py`

```python
def translate(self, node: Node) -> pl.Expr:
    """将 DSL 节点翻译为 Polars 表达式"""
    if isinstance(node, Symbol):
        # Symbol 直接转为 pl.col()
        return pl.col(node.name)
    # ... 其他节点类型
```

对于 `Symbol("n_income")`，翻译结果为 `pl.col("n_income")`。

### 5.3 n_income 计算

由于 `n_income` 是简单符号（无运算），计算过程：

```python
# core_wide 已经包含 n_income 列（从财务表拼接而来）
result = core_wide.with_columns([
    pl.col("n_income").alias("n_income")  # 直接引用
])
```

实际上，`n_income` 的值在数据拼接阶段已经确定，计算阶段只是确认输出列名。

---

## 第六阶段：参与模型训练

### 6.1 数据流回到训练流程

**位置**: `src/experiment/regression.py:176-183`

```python
def prepare_data(...) -> pl.DataFrame:
    factor_names = feature_cols + ["return_5"]  # 包含 n_income
    
    data = engine.compute(
        factor_names=factor_names,
        start_date=start_date,
        end_date=end_date,
    )
    return data
```

### 6.2 完整数据流

```
┌─────────────────────────────────────────────────────────────────────┐
│                         n_income 因子生命周期                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. 定义阶段                                                          │
│     regression.py:55  "n_income": "n_income"                         │
│                         ↓                                            │
│  2. 注册阶段 (FactorEngine.add_factor)                                │
│     parser.parse("n_income") → Symbol("n_income")                    │
│                         ↓                                            │
│  3. 计划阶段 (ExecutionPlanner.create_plan)                           │
│     extract_dependencies() → {"n_income"}                            │
│     _infer_data_specs()  → DataSpec(financial_income, asof_backward) │
│                         ↓                                            │
│  4. 数据获取阶段 (DataRouter.fetch_data)                              │
│     FinancialLoader.load_financial_data()                            │
│     从 financial_income 表读取 n_income 字段                         │
│                         ↓                                            │
│  5. 数据拼接阶段 (FinancialLoader.merge_financial_with_price)         │
│     join_asof(strategy='backward')                                   │
│     按 trade_date 和 f_ann_date 对齐                                │
│                         ↓                                            │
│  6. 计算阶段 (FactorEngine._execute_with_dependencies)                │
│     pl.col("n_income") → 输出到结果 DataFrame                        │
│                         ↓                                            │
│  7. 训练阶段                                                          │
│     作为特征列传入 LightGBM 模型                                      │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘
```

---

## 关键技术点总结

### 1. PIT 数据处理

财务数据是**低频、公告驱动**的数据，与**高频、连续**的行情数据不同。系统使用 **asof_backward** 策略处理：

- **asof**: As-Of，表示截至某个时点的有效数据
- **backward**: 向后查找，确保使用最新公告的数据
- **关键约束**: 不能使用未来数据（lookahead bias）

### 2. SchemaCache 动态路由

系统自动识别字段所属表，无需手动指定：

```python
# 系统自动识别
n_income → financial_income 表 (PIT)
close    → pro_bar 表 (主力行情表)
```

### 3. 财务数据清洗

`FinancialLoader` 自动处理：
- **报告类型过滤**: 仅使用合并报表（report_type=1）
- **去重策略**: 按 (ts_code, end_date) 取最新修订版（update_flag 最大）
- **日期对齐**: 使用公告日（f_ann_date）而非报告期（end_date）

### 4. 扩展性设计

添加新的财务因子只需在字典中添加一行：

```python
FACTOR_DEFINITIONS = {
    "n_income": "n_income",
    "revenue": "revenue",      # 营业收入
    "eps": "eps",              # 每股收益
    "roe": "roe",              # 净资产收益率
}
```

系统自动处理表路由、数据获取和拼接。

---

## 相关代码文件

| 文件 | 职责 |
|------|------|
| `src/experiment/regression.py` | 训练入口，因子定义 |
| `src/factors/engine/factor_engine.py` | 因子引擎统一入口 |
| `src/factors/parser.py` | 字符串表达式解析 |
| `src/factors/compiler.py` | AST 依赖提取 |
| `src/factors/engine/planner.py` | 执行计划生成 |
| `src/factors/engine/data_router.py` | 数据路由与组装 |
| `src/data/financial_loader.py` | 财务数据加载与拼接 |
| `src/data/catalog.py` | 数据库目录与表结构 |
| `src/data/api_wrappers/financial_data/api_income.py` | 利润表数据接口 |

---

## 附录：数据表结构

### financial_income（利润表）

```sql
CREATE TABLE financial_income (
    ts_code VARCHAR,        -- 股票代码
    f_ann_date DATE,        -- 公告日期（PIT关键字段）
    end_date DATE,          -- 报告期
    report_type INTEGER,    -- 报告类型（1=合并报表）
    update_flag INTEGER,    -- 更新标识（越大越新）
    n_income BIGINT,        -- 净利润（本因子使用的字段）
    revenue BIGINT,         -- 营业收入
    ...                     -- 其他财务字段
);
```

### pro_bar（主力行情表）

```sql
CREATE TABLE pro_bar (
    ts_code VARCHAR,        -- 股票代码
    trade_date DATE,        -- 交易日期
    open DOUBLE,            -- 开盘价
    high DOUBLE,            -- 最高价
    low DOUBLE,             -- 最低价
    close DOUBLE,           -- 收盘价
    vol BIGINT,             -- 成交量
    turnover_rate DOUBLE,   -- 换手率
    volume_ratio DOUBLE,    -- 量比
    ...                     -- 其他行情字段
);
```

**说明**: pro_bar 表通过 Tushare Pro Bar 接口获取，包含后复权数据和换手率、量比等指标，是主力行情数据表。
