# py-quant **Repository Path**: cj829/py-quant ## Basic Information - **Project Name**: py-quant - **Description**: No description available - **Primary Language**: Python - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-30 - **Last Updated**: 2026-09-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # A股量化数据采集器 **多数据源 → MySQL**,严格写入你现有的 `p_` 前缀表。 - 数据源:`baostock` / `akshare` / `tushare`(可配置切换) - 采集:股票列表、日线、分钟线(5/15/30/60) - 特性:断点续传、自动重连、增量/全量、防限流配额、企业微信通知 - 架构:Spring 风格三层(Controller → Service → Repository)+ FastAPI Web 服务 + APScheduler 定时调度 ## 项目结构 ``` PythonProject/ ├── pyproject.toml # 依赖 + 打包配置 ├── src/ │ └── stock_collector/ # 主包 │ ├── __main__.py # python -m 入口 │ ├── cli.py # 命令行入口(只负责启动 Web 服务) │ ├── collector.py # 采集主逻辑 │ ├── config.py # 配置(DB连接/数据源/采集参数) │ ├── datasource/ # 数据源层(baostock / akshare / tushare) │ ├── storage/ # 存储层(写入 / 进度) │ ├── model/ # SQLModel 实体 │ ├── repository/ # 数据访问层 │ ├── service/ # 业务逻辑层 │ ├── controller/ # HTTP 路由层 │ ├── scheduler/ # APScheduler 定时任务(从 p_job_info 读任务) │ ├── notify/ # 企业微信通知 │ ├── ratelimit.py # 每日配额 + 黑名单冻结状态 │ └── web/ # FastAPI 应用(app.py,托管 /ui 前端) ├── web-ui/ # 监控前端(Vue3 + TS + Vite + Element Plus) │ ├── src/views/Monitor.vue # 采集监控页 │ ├── src/views/Jobs.vue # 定时任务页 │ └── dist/ # 构建产物(npm run build,由 FastAPI 挂载 /ui) ├── scripts/ # 启停脚本(start.py / kill.py / web.py 等) ├── detect/ # 诊断/测试脚本(一次性工具) ├── tests/ │ └── test_akshare.py # AkShare 适配器自测 ├── sql/ │ └── init_db.sql # 建表 SQL(p_ 前缀) ├── logs/ # 运行日志(自动创建) └── README.md ``` ## 安装 ```bash # 使用项目虚拟环境 .venv\Scripts\pip.exe install -e . ``` > 会自动安装 baostock、akshare、tushare、pymysql、sqlalchemy、pandas、fastapi、uvicorn、sqlmodel、apscheduler。 ## 配置 编辑 `src/stock_collector/config.py`: ```python # 1. 数据库连接 DB_CONFIG = { "host": "127.0.0.1", "port": 3306, "user": "root", "password": "你的密码", # ← 改这里 "database": "你的数据库名", # ← 改这里 } # 2. 数据源(三选一) DATA_SOURCE = "baostock" # 可选: baostock / akshare / tushare # 3. 历史起点 HISTORY_START = "2015-01-01" ``` ## 访问授权与 SSO 免登 分两步,两个开关独立控制: ### 第一步:访问授权(默认开启,账号来自 `c_user` 表) ```python AUTH_ENABLED = True # 未登录拦截:页面跳 /login,/api/** 返回 401 # 账号来源:db = 查 common 库 c_user 表(默认);config = 只认下面常量;both = 先库后常量 AUTH_SOURCE = "db" AUTH_DB_PASSWORD_HASH = "md5" # c_user.password 的摘要算法:md5 / plain / sha256 AUTH_DB_ACTIVE_STATUS = [2] # 允许登录的 userStatus;空列表 = 不校验 AUTH_DB_FALLBACK_LOCAL = True # DB 连不上时才回落常量账号(防锁死入口) AUTH_USERNAME = "admin" # AUTH_SOURCE 含 config 时生效 AUTH_PASSWORD = "admin123" # ← 务必修改 AUTH_SECRET = "stock-collector-secret" # ← 会话签名密钥,务必改成随机串 AUTH_SESSION_TTL = 12 * 3600 # 会话有效期(秒) AUTH_COOKIE_SECURE = False # 全站 HTTPS 时置 True AUTH_PUBLIC_PATHS = [...] # 免鉴权路径(登录页、认证接口、/docs、XXL-Job 回调) ``` **账号表:`common.c_user`**(与业务系统共用,不新建账号体系) | 字段 | 用途 | |---|---| | `loginName` | 登录名(唯一索引) | | `password` | 密码摘要,默认 MD5 32 位(大小写不敏感) | | `name` | 展示名(顶栏显示) | | `userStatus` | 账号状态,正常 = 2(由 `AUTH_DB_ACTIVE_STATUS` 控制) | | `userRoleType` | 角色,7 = 管理员(顶栏显示「管理员」标签) | | `isDelete` | `bit(1)`,`0` = 未删除,已删除账号直接拒绝登录 | - 会话为**无状态签名 Cookie**(HMAC-SHA256),附带姓名/角色/用户 id,不占服务端存储 - 前端 axios 拦截器收到 401 会自动跳 `/login?next=原地址`,登录后回跳 - `GET /api/auth/info` 会返回 `authDb`(账号来源 + 库连通性 + 用户数),用于排查连库问题 - 想临时关闭鉴权(如内网调试):`AUTH_ENABLED = False`;想只用工单常量账号:`AUTH_SOURCE = "config"` ### 角色权限(RBAC,两张 `p_` 表) 账号在 `c_user`,**角色在本系统**: | 表 | 作用 | |---|---| | `p_role` | 角色字典:`admin`(管理员)/ `viewer`(只读) | | `p_user_role` | `login_name → role_code` 绑定 | ```python RBAC_ENABLED = True # False = 不校验角色,所有人等同管理员 AUTH_DEFAULT_ROLE = "viewer" # p_user_role 无记录时的默认角色 AUTH_ADMIN_ROLE_TYPES = [7] # 无绑定记录时,按 c_user.userRoleType 认定管理员(超管=7) ``` 权限矩阵: | 操作 | admin | viewer | |---|---|---| | 查看监控/进度/任务列表 | ✅ | ✅ | | 触发采集 `POST /api/collect` | ✅ | ❌ 403 | | 定时任务增删改、启停、手动触发 | ✅ | ❌ 403 | | 角色绑定/解绑 | ✅ | ❌ 403 | 管理接口:`GET /api/roles`、`GET /api/user-roles`、`POST /api/user-role`、`DELETE /api/user-role/{loginName}`。 前端按角色自动隐藏写操作按钮(只读用户看到「只读」标签)。 > 建表遵循规范:DDL 统一追加在 `sql/init_db.sql`(第 10、11 段),`p_` 前缀、snake_case、每字段 COMMENT、 > 自增主键、索引、逻辑删除 `deleted` + 审计字段 `create_time/update_time`,并同批初始化角色字典。 ### 第二步:SSO 免登(对接 csr 统一认证中心) 协议与 Java 侧 `ncore-sso-client` 完全一致(HMAC-SHA256 + Base64Url 签名、ticket 换票): ```python # 环境变量优先(与 Java 侧 ${NCORE_SSO_*:默认} 对齐),未设置时用默认值 SSO_ENABLED = True SSO_BASE_URL = "http://127.0.0.1:8080" # 认证中心地址 SSO_CLIENT_ID = "xxx" # 认证中心「应用凭据管理」登记的 AppId SSO_CLIENT_SECRET = "xxx" # 对应 AppSecret SSO_CALLBACK_URL = "http://127.0.0.1:5000/auth/sso" # 外显回调地址,必须与浏览器访问地址一致 SSO_USERNAME_MAPPING = {} # 中心登录名 → 本端用户名(未配置同名直通) SSO_ALLOWED_USERS = [] # 白名单,留空 = 不限制 SSO_AUTO_REDIRECT = True # 未登录直接跳中心(真·免登) ``` > **关键坑:ticket 参数名是 `xxl_sso_ticket`**,不是 `ticket`。 > csr 中心 `SsoLoginController.TICKET_PARAM = "xxl_sso_ticket"`(xxl-job-admin 的 > `LoginController` 也按这个名字取参);回调 `/auth/sso` 已兼容 > `xxl_sso_ticket / ticket / token / sso_ticket` 四种命名,换不到票会回落 `/login?sso=fail`。 接入流程: | 步骤 | 说明 | |---|---| | 1. 中心登记 | csr 后台「系统数据 → 应用凭据管理」新增应用,拿到 AppId / AppSecret | | 2. 填配置 | 写入 `config.py` 的 `SSO_*` 段,重启服务 | | 3. 访问 | 打开 `http://host:5000/ui` → 自动 302 到认证中心 → 登录成功回跳并免登 | | 4. 退出 | 顶栏「退出登录」→ 穿透到中心 `/sso/logout` 做单点登出 | 接口一览:`GET /login`(登录页)、`POST /api/login`(本地口令)、`GET /api/logout`、 `GET /api/auth/info`(登录态)、`GET /auth/login`(跳中心)、`GET /auth/sso`(换票回调)。 > 登录页是独立模板文件 `src/stock_collector/web/templates/login.html`(Jinja2 渲染), > 改样式/文案直接编辑该文件即可,重启服务生效(无需重新构建前端)。 > SSO 未配置完整时启动会打 WARN 并自动回落本地口令登录,不会锁死入口。 ## 运行 启动 Web 服务(采集统一通过 HTTP 接口触发,命令行不再直接执行采集): ```bash # 方式一:python -m(推荐) .venv\Scripts\python.exe -m stock_collector --port 5000 # 方式二:激活虚拟环境后 .venv\Scripts\activate python -m stock_collector --port 5000 # 方式三:脚本启动 .venv\Scripts\python.exe scripts\start.py ``` 启动后访问(未登录会先跳登录页): - `http://localhost:5000/login` — **登录页**(本地口令 + SSO 免登入口) - `http://localhost:5000/ui` — **采集监控页面**(Vue3 + Element Plus:实时批次进度、当前股票、速度、失败明细、触发采集) - `http://localhost:5000/ui/#/jobs` — 定时任务管理页面 - `http://localhost:5000/job` — 定时任务管理(内置简易 HTML 页) - `http://localhost:5000/docs` — Swagger 接口文档 > 前端源码在 `web-ui/`(Vue 3 + TypeScript + Vite + Element Plus,技术栈对齐 newcommon/new-web)。 > 开发:`cd web-ui && npm install && npm run dev`(5173 端口,/api 自动代理到 5000); > 构建部署:`npm run build` 后产物 `web-ui/dist`,由 FastAPI 自动挂载到 `/ui`,重启 Web 服务生效。 ### 实时监控说明 - 采集过程中批次进度**实时写入** `p_collect_task`(≥5 秒节流),页面每 5 秒轮询 `/api/overview` - `/api/overview` 返回当前批次的阶段(daily/min5...)、当前处理股票、done/total、失败数、速度、耗时 - 服务重启时自动把遗留的 `running` 僵尸批次结转为 `aborted`,避免状态卡死 ## API 接口 ### 触发采集 ``` POST /api/collect?task=daily&full=false&freq=5,15,30,60 ``` | 参数 | 取值 | 说明 | |------|------|------| | `task` | `stocks` / `daily` / `minute` / `all` | 采集内容(默认 all) | | `full` | `true` / `false` | 是否全量(默认 false = 增量) | | `freq` | `5,15,30,60` | 分钟周期(仅 task=minute/all 时有效) | 示例: ```bash # 每日增量(股票列表 + 日线 + 全部分钟周期) curl -X POST "http://localhost:5000/api/collect?task=all" # 日线全量 curl -X POST "http://localhost:5000/api/collect?task=daily&full=true" # 仅 5/15 分钟线全量 curl -X POST "http://localhost:5000/api/collect?task=minute&full=true&freq=5,15" ``` ### 查询接口 | 接口 | 说明 | |------|------| | `GET /api/overview` | 整体概览(实时批次+进度+失败+最近批次+系统,一次返回) | | `GET /api/progress` | 总体进度 + 运行状态 + 当前批次实时状态 | | `GET /api/status` | 采集任务是否在跑 | | `GET /api/errors?limit=20` | 失败明细 | | `GET /api/recent?limit=10` | 最近修改记录 | | `GET /api/task` | 任务批次执行记录 | | `GET /api/task/latest` | 最新一条任务批次记录 | | `GET /api/system` | 系统环境 | ### 定时任务(`/job` 管理页面) | 接口 | 说明 | |------|------| | `GET /api/job` | 任务列表 | | `POST /api/job` | 新增任务(JSON:jobDesc / scheduleType / scheduleConf / executorHandler / executorParam) | | `PUT /api/job/{id}` | 更新任务 | | `DELETE /api/job/{id}` | 删除任务 | | `POST /api/job/{id}/start` | 启动任务 | | `POST /api/job/{id}/stop` | 停止任务 | | `POST /api/job/{id}/trigger` | 手动触发一次 | Handler 可选:`collectAll` / `collectDaily` / `collectMinute` / `collectStocks`; 调度类型支持 `CRON`(如 `0 17 * * 1-5`)和 `FIX_RATE`。 ## XXL-Job 调度中心接入(可选) 除内置 APScheduler 外,本服务同时内置 **XXL-Job 执行器**(Python 版协议实现, 对应 Java demo `D:\workspace\newcommon\ncommon-all\ncommon-job`),可接入 ncommon 工程的 xxl-job-admin 统一管理定时任务。Web 服务启动时自动向调度中心注册(`xxljob/executor.py`)。 - 协议:Admin → 执行器 `/run` `/beat` `/idleBeat` `/kill` `/log`;执行器 → Admin `/api/registry`(30s 心跳)/ `/api/registryRemove` / `/api/callback` - 配置:`config.py` 的 `XXL_JOB_*`(Admin 地址、accessToken、执行器 AppName、注册地址端口, 端口须与 Web 端口一致);`XXL_JOB_ENABLED = False` 可整体关闭 - 调度中心侧:执行器管理确认 AppName 组(默认 `xxl-job-executor-sample`,建议新建正式组), 新建任务 JobHandler 填 `collectStocks / collectDaily / collectMinute / collectAll`, 任务参数支持 JSON 或 query string:`{"full":true,"freq":"5,15,30,60"}` 或 `full=true&freq=5` - 另有 `demoJobHandler` 联调测试任务(只打日志) - 注意:与内置 APScheduler 是两套并行调度,同一时间段避免两边同时触发(服务端有单批次互斥,重复触发会被拒绝) ## 数据源切换 改 `config.py` 的 `DATA_SOURCE` 即可,采集层无感知: | 数据源 | 特点 | |--------|------| | `baostock` | 需要登录,字段全,分钟线可回溯多年,但易限流 | | `akshare` | 无需登录,东方财富源,日线可全量,**分钟线仅近期** | | `tushare` | 需 token(`config.py` 中 `TUSHARE_TOKEN`),积分制 | ## 断点续传 每完成一只股票,进度写入 `p_collect_progress`: - 中断后重跑 → 自动从上次断点继续 - 当天已成功采集的股票,增量运行会**自动跳过** - 状态:`pending / running / done / error` 查看进度: ```sql SELECT ktype, status, COUNT(*) FROM p_collect_progress GROUP BY ktype, status; ``` ## 注意事项 1. **分钟线量大**:全量约 30-50GB,确保磁盘够 2. **涨跌停**:自动根据股票类型计算(主板±10%/创业板±20%/北交所±30%) 3. **防限流**:单连接串行采集(baostock 禁止并发连接),内置随机延迟 + 失败重试 + 黑名单识别 4. **每日配额**:内置每日 5 万次 API 配额计数,接近上限自动停止并保留进度,次日增量续跑 5. **黑名单冻结**:触发拉黑自动记录本年累计次数,冻结时长 = 本年累计次数 × 6 小时,冻结期自动拒绝启动 6. **失败告警**:连续失败 ≥50 只或失败率 >30% 时,企业微信告警并中止采集 ## 常见问题 **Q: 报 `Access denied`?** A: 检查 `config.py` 的 MySQL 密码和远程连接权限。 **Q: 报 IP 已被拉黑 / 网络接收错误?** A: 触发 baostock 风控拉黑。采集器会自动记录本次拉黑并停止;冻结时长 = 本年累计拉黑次数 × 6 小时,到期后重新运行即可自动恢复。也可切换另一个数据源。 **Q: 分钟线只能拿到近期数据?** A: akshare 的限制,改用 baostock 可回溯多年。 **Q: 如何只采集某几只股票?** A: 目前接口按全市场采集,可在 `cli.py` / `CollectService` 加 `--codes` 参数扩展。