Metadata-Version: 2.3
Name: GlamQuant
Version: 0.0.1
Summary: 对接 pms-app 的本地客户端 SDK（组合运营报表数据）
Author: wgkkk
Author-email: wgkkk <86173408+nlnlbnl@users.noreply.github.com>
Requires-Dist: httpx>=0.27.0
Requires-Dist: pandas>=2.0.0
Requires-Dist: pydantic>=2.0
Requires-Dist: pydantic-settings>=2.0.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# GlamQuant

对接 **pms-app** 的本地客户端 SDK：配置好服务地址（`base_url`）与鉴权 token 后，
按业务端点调用组合运营报表等数据接口，返回 `pandas.DataFrame`。底层客户端对
用户不可见 —— 只需通过顶层函数设置 `base_url` / `token`，再调用业务函数即可。

pms-app 的约定：

- 响应包 `{status, msg, data}`，`status == 0` 表示成功
- 鉴权用自定义 `token` 请求头（JWT）；服务端亦可配置 IP 白名单免 token
- 管理：`uv`
- HTTP 客户端：`httpx`（超时 / 重试 / 认证头自动注入 / 401 自动处理）

## 安装

```bash
uv add GlamQuant
# 或
pip install GlamQuant
```

## 快速开始（组合运营报表）

```python
import glamquant as gf

# 1. 配置共享默认客户端：base_url 手动指定（本地 / 生产切换）
gf.configure(base_url="http://localhost:18000", token="<pms JWT>")
# 生产环境：gf.configure(base_url="https://pms-prod.example.com", token="<pms JWT>")

# 2. 调用业务端点（模块级函数），返回 DataFrame
df = gf.portfolio.get_metrics_data(
    portf_ids=[
        "6b167a721a15174cb623733a3a3c4f5b",
        "e02abf50efe51f950b6831855478f0fe",
    ],
    metric_id="M1000002",
    begdate="20260809",
    enddate="20260909",
    date_type=1,   # 1 交易日 / 2 自然日
)
print(df)
```

返回 `pandas.DataFrame`：`dt` 解析为日期列，每个组合 ID 一列（数值，无数据为 `NaN`）；
空结果返回空 `DataFrame`。

### 配置 base_url / token（可直接单独设置）

底层客户端对用户不可见，所有业务函数共用同一个默认客户端，随时可改地址或换 token：

```python
import glamquant as gf

gf.set_base_url("http://localhost:8000")     # 本地
# gf.set_base_url("https://pms-prod.example.com")   # 切生产
gf.set_token("<pms JWT>")                    # 换 token

df = gf.portfolio.get_metrics_data(["6b167a72..."], "M1000002")
```

`base_url` 也可不显式设置，走环境变量：

```bash
export GLAMQUANT_BASE_URL=http://localhost:18000
```

```python
gf.configure(token="<pms JWT>")   # base_url 自动读环境变量
```

### 无 token（IP 白名单本地联调）

若在 pms 侧把调用方 IP 加入 `WHITELIST_IPS`，可不设 token，客户端不发送任何认证头：

```python
gf.set_base_url("http://localhost:18000")   # 只设地址，不带 token
df = gf.portfolio.get_metrics_data(["6b167a72..."], "M1000002")
```

## 业务端点一览

| 模块 | 函数 | 说明 |
|---|---|---|
| `portfolio` | `get_metrics_data(portf_ids, metric_id, begdate, enddate, date_type)` | 组合运营报表：多产品单指标日期序列 |

> `portf_ids` 最多 100 个（本地即校验），`begdate`/`enddate` 用 `YYYYMMDD`，
> `date_type`：1 交易日 / 2 自然日。

### 数据 → DataFrame 转换

各业务函数内部已把 pms 返回的记录转成 `DataFrame`。若自行用底层拿到原始记录
（`list[dict]`，每行含日期列与若干数值列），可调用专用转换函数：

```python
import glamquant as gf

records = [...]   # [{"dt": "2024-10-08", "<portf_id>": 1.0019}, ...]
df = gf.records_to_dataframe(records)          # dt 解析为日期，其余列数值化（NaN 填充）
df = gf.records_to_dataframe(records, dt_col="date")   # 自定义日期列名
```

## 附带数据源模块

### iFinD（同花顺）

独立数据模块，复用 SDK 传输/认证基础设施：

```python
import os
os.environ["IFIND_REFRESH_TOKEN"] = "你的refresh_token"
from glamquant import ifind

df = ifind.real_time_quotation(["300033.SZ"], ["open", "latest"])
df = ifind.history_quotation(["300033.SZ"], ["open", "close"], "2026-01-01", "2026-08-26")
```

认证用 `RefreshTokenAuth`（长期 refresh_token 换短期 access_token，失效自动续期）。

### eastmoney（直连东财，实验性）

```python
from glamquant import eastmoney
df = eastmoney.stock_daily("000001.SZ", start_date="20260801", end_date="20260825")
```

> ⚠️ 东财公开接口非官方、无鉴权与配额，并对非 curl 客户端做 TLS 指纹拦截，底层经系统 `curl` 请求；接入正式数据源后应弃用。

## 配置

通过环境变量（前缀 `GLAMQUANT_`）或 `.env` 文件覆盖：

| 变量 | 默认值 | 说明 |
|---|---|---|
| `GLAMQUANT_BASE_URL` | 无（必填/手动指定） | pms-app 地址，如 `http://localhost:18000` |
| `GLAMQUANT_TIMEOUT` | `60.0` | 请求超时（秒） |
| `GLAMQUANT_MAX_RETRIES` | `3` | 最大重试次数（5xx/429/网络异常） |
| `GLAMQUANT_MAX_AUTH_RETRIES` | `1` | 401 后自动重登次数（`0` 关闭） |

## 异常

```python
from glamquant import (
    FinApiError, PMSAPIError, AuthError, RateLimitError,
    ParamError, APICallError, NetworkError, DataError,
)
```

| 场景 | 异常 |
|---|---|
| pms 返回 `status != 0` | `PMSAPIError`（`code` 为 status） |
| token 无效 / 401 | `AuthError` |
| 参数校验失败（如组合数超限） | `ParamError` |
| 非 2xx / 服务端错误 | `APICallError` |
| 网络异常（超时/断连） | `NetworkError` |
| 响应解析失败 | `DataError` |

## 开发

```bash
uv sync --all-groups   # 安装依赖（含 dev）
uv run pytest          # 运行测试
uv build               # 构建 wheel 与 sdist
```

## 打包与发布

### 发布到 PyPI

```bash
# 1. 升版本号：编辑 pyproject.toml 的 version（PyPI 不允许覆盖已发布版本）
#    当前版本 0.2.1

# 2. 重新构建
rm -rf dist
uv build

# 3. 本地验证 wheel 可安装、可导入（推荐，先自测再上传）
pip install dist/glamquant-*.whl
python -c "import glamquant; print('OK')"

# 4. 上传（token 用环境变量，避免写入 .pypirc）
export UV_PUBLISH_TOKEN='你的_pypi_token'     # 形如 pypi-xxxxxxx
uv publish

# 5. 从 PyPI 装回来验证
pip install GlamQuant
python -c "import glamquant; print('OK')"
```
