Metadata-Version: 2.4
Name: gsdatabase
Version: 0.3.0
Summary: GS 因子数据 SDK：tushare 风格接口，token 鉴权，DataFrame 返回
License: Proprietary
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28
Requires-Dist: urllib3>=1.26.18
Requires-Dist: pandas>=1.5
Requires-Dist: pyarrow>=12.0
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"

# gsdatabase

GS 因子数据 SDK。tushare 风格接口：token 鉴权，DataFrame 返回，对接门户 `/api/v1` 开放接口。

## 安装

```bash
pip install gsdatabase
```

## 快速开始

```python
import gsdatabase as gs

gs.set_token("gsk_...")        # 门户个人中心生成；一次设置写入 ~/.gsdatabase/token
api = gs.pro_api()

# 我能看哪些因子 / 组合
factors = api.factor_list()
portfolios = api.portfolio_list()

# 回答"最近一年沪深300里表现好的因子有哪些"
top = (api.factor_performance(universe="沪深300", start_date="20250723")
          .sort_values("hedge_annualized_return", ascending=False)
          .head(20))

# 单因子明细：分组回测净值 / 日频 IC
nav = api.factor_backtest_daily(schema_name="order_time_gather_rolling",
                                factor_name="order_time_gather_rolling_001",
                                start_date="20250101")
ic = api.factor_ic_daily(schema_name="order_time_gather_rolling",
                         factor_name="order_time_gather_rolling_001")

# 原始因子值（长表 date/symbol/value，大表建议限定日期区间）
df = api.alpha_factor(schema_name="order_time_gather_rolling",
                      factor_name="order_time_gather_rolling_001",
                      start_date="20250101", end_date="20250630")

# 整个 schema 的因子值宽表：(date, symbol) 索引，列 = 因子名
# 一年 × 30 因子约数千万行，分钟级耗时；单因子失败自动跳过并 warning
wide = api.alpha_factor_all("order_time_gather_rolling",
                            start_date="20250101", end_date="20251231")

# Barra 风格因子与基本面/量价描述符（宽表：一行一股一日，一列一因子）
# 先查字典挑因子；factors 缺省即取全部列，列越多单页行数越少，务必限定区间
meta = api.factor_meta(factor_table="alpha_financial_descriptors")
print(meta[["factor_name", "direction", "exp"]].head())

expo = api.barra_exposure(start_date="20250101", end_date="20250331",
                          factors=["size", "beta", "momentum"])
fin = api.alpha_financial_descriptor(start_date="20250101", end_date="20250331",
                                     factors=["bp", "ep", "roe"])
ret = api.barra_factor_return(start_date="20250101")   # 每日一行，全表不足万行

# 数据目录（免鉴权）与用量/配额
print(api.catalog())           # 全部数据集的参数与字段说明
usage = api.usage(days=30)     # 近 30 天调用统计
print(usage.attrs["quota"])    # {"daily_row_quota": ..., "used_today": ..., "remaining_today": ...}
```

## 数据集一览

| 方法 | 必填参数 | 说明 |
| --- | --- | --- |
| `factor_list()` | — | 可见因子清单 |
| `portfolio_list()` | — | 可见组合清单 |
| `factor_performance(universe, start_date, end_date)` | — | 因子绩效汇总（对冲收益/IC/ICIR） |
| `portfolio_performance(universe, start_date, end_date)` | — | 组合绩效汇总 |
| `factor_backtest_daily(schema_name, factor_name, ...)` | schema_name, factor_name | 分组回测日频净值 |
| `factor_ic_daily(schema_name, factor_name, ...)` | schema_name, factor_name | 日频 IC/RankIC |
| `portfolio_backtest_daily(portfolio_name, ...)` | portfolio_name | 组合日频净值 |
| `alpha_factor(schema_name, factor_name, ...)` | schema_name, factor_name | 原始因子值长表（大表推荐 arrow） |
| `alpha_factor_all(schema_name, ...)` | schema_name | 整 schema 因子值宽表（并发拉取） |
| `factor_meta(factor_table)` | — | Barra/描述符因子字典：含义、方向、构造方法 |
| `barra_exposure(start_date, end_date, symbols, factors)` | — | 个股 Barra 因子暴露（10 风格 + 31 行业，2006 年末起） |
| `barra_factor_return(start_date, end_date, factors)` | — | 日度纯因子收益率（截面回归，无个股维度） |
| `alpha_financial_descriptor(start_date, end_date, symbols, factors)` | — | 基本面因子值（155 个，2025 年起） |
| `alpha_trading_descriptor(start_date, end_date, symbols, factors)` | — | 量价因子值（79 个，2025 年起） |

后五个是宽表数据集：`factors` 参数点名因子列（缺省取全部），维度列 `date`/`symbol`
自动前置。服务端按投影后的列数反推单页行数，取全部 155 列时单页约 1.2 万行，
所以点名因子比取全表快得多。`alpha_*_descriptor` 目前只覆盖 2025 年以来的日度值，
更早区间请联系平台方获取。

日期参数同时接受 `YYYYMMDD` 与 `YYYY-MM-DD`，输出统一 `YYYYMMDD` 字符串。
方法名与服务端 dataset id 一一对应；服务端新增数据集时可直接
`api.query("new_dataset", **params)` 或 `api.new_dataset(**params)` 使用，无需升级 SDK。

## 服务地址（base_url）

三级优先级，从高到低：

1. 显式入参：`gs.pro_api(base_url="https://...")`
2. 环境变量：`GSDB_BASE_URL`
3. 默认值：`https://gsdatabase.net:21985`（生产公网入口）

```bash
# 本地开发直连开发后端（注意直连 FastAPI 的 8000，不走 Vite）
export GSDB_BASE_URL=http://127.0.0.1:8000
python your_script.py
```

## 分页与配额

- 服务端按 cursor 分页：json 单页上限 1 万行，arrow 单页上限 10 万行；SDK 自动续拉
  全部分页并 concat（`paginate=False` 关闭，只取一页），arrow 翻页默认走预取流水线
  （见下文）
- 传输层默认 arrow 二进制（更快更省带宽、dtype 保真），`gs.pro_api(fmt="json")` 可切换
- 限频（40201）SDK 按服务端返回的 `retry_after` 自动退避重试；当日行数配额用尽抛
  `QuotaError`，配额与用量见 `api.usage()`

## 传输压缩（0.3.0 起默认开启）

`format=arrow` 的响应默认用 **zstd** 压缩传输，代价约 +1.8ms/页 CPU。公网取数的
瓶颈是链路带宽，所以这一项的收益最直接——2026-08-05 公网实测（单流拉一年因子值，
118 万行 / 12 页）：

| | 传输量 | 墙钟 |
| --- | --- | --- |
| 0.2.x 现状（不压缩） | 34.2 MB | 14.0 s（两轮均值） |
| 0.3.0 默认（zstd） | 11.3 MB | 4.8 s |
| | **3.03×** | **2.95×** |

全量拉取（1039 万行 / 104 页）压缩比一致：301.5 MB → 99.2 MB（3.04×）。

```python
api = gs.pro_api()                      # 默认 zstd
api = gs.pro_api(codec="identity")      # 关闭压缩（0.2.x 的线上形态）
api = gs.pro_api(codec="lz4")           # 压缩比更低、解压更省 CPU
```

解压对使用方透明，返回的 DataFrame 与不压缩时逐值一致。三层自动降级：

- 本机 pyarrow 未编入该压缩 → 构造客户端时就退回不压缩；
- 服务端不认识 `codec` 参数（老版本返回 40101）→ 按同一 cursor 去掉参数重下，
  并在本会话内不再发送该参数；
- 解压失败或页被截断 → 退回不压缩重下同一 cursor（见下节的行数校验）。

## 翻页预取（0.3.0 起默认开启）

`format=arrow` 自动翻页时，SDK 在收到本页**响应头**的那一刻就在第二条连接上发出
下一页的请求，本页 body 的下载与服务端准备下一页因此重叠。翻页游标在响应头
`X-Next-Cursor` 里，不必等 body 收完才知道下一页是谁，这一步才成立。

- 页序与结果与串行逐页拉取**逐行一致**，接口、参数、返回类型都不变，使用方无感；
- 预取深度固定为 1（在途最多一个未消费的响应），客户端内存增量不超过一页；
- 两条连接都是 keep-alive 复用，翻多少页都不会反复握手；
- `paginate=False` 不预取（免得白拉一页还计进当日行数配额），`fmt="json"` 保持串行
  （json 的 `next_cursor` 在 body 里，拿不到就无从提前）；
- 页校验失败、限频退避、`codec` 降级等既有行为一律不变：本页要重下时，已预取的
  下一页会被废弃并重新预取。

**收益随每页传输时间反向变化**，值得说清楚：预取能重叠掉的是「服务端生成下一页 +
一个往返」（约 230ms），拿来盖住的是本页 body 的下载与解析。每页传输越慢，这
230ms 占总时间的比重越小，收益就越薄：

| 可用带宽 | 每页传输 | 实测加速 |
| --- | --- | --- |
| 20 MB/s | 120 ms | **1.48×** |
| 5 MB/s | 480 ms | 1.25× |
| 2 MB/s | 1200 ms | **1.11×** |

（本地注入带宽的敏感曲线。回环链路上另有 2.1× 量级的观测，但那主要来自与
`to_pandas` 解析重叠——把解析耗时设为 0 后只剩 1.02×，不能算作网络流水线的收益。）

2026-08-05 的公网实测正落在 2 MB/s 那一档：配对差 −0.19s ± 0.46s，也就是预期中的
约 1.11× 被链路自身的抖动盖住了（同配置两轮墙钟能差一倍），测不出来。预取机制本身
工作正常（命中率 100%，日志侧可见并发连接），只是这条链路上没多少可省的东西。

所以：公网上真正起作用的是上一节的压缩；预取的价值在带宽更充裕的场景（内网直连、
同机房、出口扩容之后），且受限链路上实测不会拖慢。

## 数据完整性（0.2.1）

`format=arrow` 的每一页在解析后都会与响应头 `X-Row-Count` 核对行数，不一致（或该头
本身损坏）即视为传输被截断，按**同一 cursor** 重下，重试耗尽才抛 `GsdbError`（code `-1`，
消息含 dataset / cursor / 尝试次数）。**不会静默少行。**

- **urllib3 2.x** 会校验 `Content-Length`，截断在传输层就报错——这是第二道防线；
- **urllib3 1.x** 没有这层（`resp.content` 会静默变短），此时完全由上述行数校验兜底，
  功能正常，只是多一次重传；进程内首次构造客户端时会给一条升级提示。

依赖因此声明为 `urllib3>=1.26.18` 而非强制 2.x：客户环境常被其他库（如 botocore）
钉在 1.x，强制升级会直接装不上，而正确性并不依赖它。

## 错误码

所有异常继承 `gsdatabase.GsdbError`（`e.code` / `e.msg`）：

| code | HTTP | 异常 | 含义 |
| --- | --- | --- | --- |
| 40001 | 401 | `AuthError` | token 缺失、无效或已吊销 |
| 40002 | 401 | `AuthError` | token 过期 |
| 40004 | 403 | `PermissionError_` | 无数据权限 |
| 40101 | 422 | `ParamError` | 参数错误 |
| 40102 | 404 | `ParamError` | dataset 不存在 |
| 40201 | 429 | `RateLimitError` | 频率超限（自动退避重试） |
| 40202 | 429 | `QuotaError` | 当日行数配额用尽 |
| 50000 | 500 | `ServerError` | 服务端错误（自动重试） |
| 50001 | 504 | `ServerError` | 查询超时（自动重试） |
| 50003 | 503 | `ServerError` | 依赖暂时不可用，如数据库抖动（按服务端 `retry_after` 自动退避重试） |
| -1 | — | `GsdbError` | 网络错误 / SDK 本地校验失败 |

## 发布（维护者）

- 发新版：同步提升 `pyproject.toml` 的 `version` 与 `gsdatabase/_version.py` 的
  `_FALLBACK` 并提交，再执行：

  ```bash
  scripts/release.sh          # 校验干净 → 打 tag v<version> → 构建 wheel 到 dist/
  ```

- 发给客户的永远是 tag 构建的 wheel，不是工作区代码。
- 发布前先跑单测与手动冒烟（冒烟默认跳过，需显式测试 token）：

  ```bash
  python -m pytest -q
  GSDB_TEST_TOKEN=gsk_xxx python scripts/smoke.py
  ```
