diff --git a/.env.example b/.env.example deleted file mode 100644 index 31c80d4..0000000 --- a/.env.example +++ /dev/null @@ -1,19 +0,0 @@ -APP_ENV=local -HTTP_ADDR=127.0.0.1:8080 -PGHOST=192.168.100.127 -PGPORT=5432 -PGDATABASE=veyra -PGUSER= -PGPASSWORD= -PGSSLMODE=disable -PGADMIN_DATABASE=postgres -REDIS_ADDR=192.168.100.127:6379 -REDIS_USERNAME= -# Redis 无用户名;将实际密码填写到本地 .env。 -REDIS_PASSWORD= -REDIS_DB=0 -# 开发阶段身份:与数据库登录凭据完全独立;密码为 12–72 字节。 -ADMIN_LOGIN=admin -ADMIN_PASSWORD= -DEV_USER_LOGIN=demo -DEV_USER_PASSWORD= diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 01dd4dc..d7300ce 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -24,12 +24,23 @@ jobs: with: go-version-file: go.mod cache-dependency-path: go.sum + - uses: actions/setup-python@v5 + with: + python-version: "3.12" - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - - run: npm ci - - run: make build - - run: make test + cache-dependency-path: | + web/admin/package-lock.json + apps/mobile/package-lock.json + - run: npm --prefix web/admin ci + - run: npm --prefix apps/mobile ci + - run: npm --prefix apps/mobile run typecheck + - run: npm --prefix apps/mobile run test + - run: npm --prefix apps/mobile run build + - run: npm --prefix web/admin run build + - run: go build -trimpath -o bin/veyra . + - run: go test -race ./... - run: go vet ./... - - run: test -z "$(gofmt -l cmd internal)" + - run: test -z "$(gofmt -l main.go internal)" diff --git a/.gitignore b/.gitignore index 15d1a68..83899e6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,9 @@ coverage/ !internal/adminui/dist/ internal/adminui/dist/* !internal/adminui/dist/.keep + +# Expo 本地设备状态与生成类型 +.expo/ +expo-env.d.ts + +/logs/ diff --git a/AGENTS.md b/AGENTS.md index 4fb9eba..f6e06c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,8 +2,9 @@ 项目背景、已完成功能和验证状态见 `docs/development-state.md`。继续开发前阅读该文件,完成工作后更新事实与待办,不记录密码。 -- 后端使用 Go,遵循 gofmt、go vet、惯用错误处理、context 传递、明确依赖注入及 cmd/internal 分层。 -- 后端采用仓库根目录的单 Go module:cmd 放入口,internal 按 model、service、repository、handler、config、database 等职责组织;管理端位于 web/admin。避免为单体项目重复嵌套 services/api、业务域目录和 platform 层,不引入单模块 go.work。 +- 后端使用 Go,遵循 gofmt、go vet、惯用错误处理、context 传递、明确依赖注入及入口/internal 分层。 +- 后端采用仓库根目录的单 Go module:仅保留根目录 main.go 一个入口和一个 main 函数,internal 按 model、service、repository、handler、config、database 等职责组织;管理端位于 web/admin。避免为单体项目重复嵌套 services/api、业务域目录和 platform 层,不引入单模块 go.work。 +- 不使用 Makefile;后端直接 go run / go build,前端直接使用 npm scripts。数据库和开发身份维护通过同一个可执行文件的 -command 参数切换。 - HTTP handler 只做协议适配,业务规则归 service/domain,SQL 归 repository。不得将 Gin Context 传入业务层。 - PostgreSQL 所有表(包括迁移、任务和关联表)必须以 `veyra_` 开头,表与字段使用 snake_case。 - 主键 `id` 使用字符串 UUID:Go 类型 `string`,数据库 `varchar(36)` 并校验 UUID 格式。外键保持相同类型。 @@ -16,5 +17,9 @@ - 凭据通过环境配置,禁止提交 .env、打印连接串或口令。所有数据库变更使用版本化迁移。 - 功能与真实数据能力必须如实呈现;不得用演示结果冒充在线统计。 - 更改后完成对应测试、go vet、类型检查与构建。原始两个设计文件保留。 -- PostgreSQL 开发地址为 `192.168.100.127:5432`,数据库名 `veyra`;Redis 为 `192.168.100.127:6379`,无用户名、有密码。实际凭据只从本地 `.env` 读取。 +- PostgreSQL 开发地址为 `192.168.100.127:5432`,数据库名 `veyra`;Redis 为 `192.168.100.127:6379`,无用户名、有密码。实际凭据只从本地 `configs/.env` 读取。 - 本机与数据库处于同一内网。若工具返回 `Operation not permitted`,应区分执行沙箱权限限制与主机网络故障,不因此修改服务地址。 + +- 后端环境文件与模板位于 configs/;管理端和 App 分别维护自己的 npm 配置、锁文件与依赖,不在根目录建立 npm workspace。 + +- 后端日志使用 zap:控制台易读,当前运行目录 logs/ 下自动写文件与轮转;日志不提交,不记录密钥、口令、Cookie 或请求正文。 diff --git a/Makefile b/Makefile deleted file mode 100644 index d53afa1..0000000 --- a/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -.PHONY: api build db-create db-migrate admin-create user-create test check admin-dev admin-build -export GOCACHE := $(CURDIR)/.cache/go-build - -api: admin-build - go run ./cmd/api -env-file .env -build: admin-build - mkdir -p bin - go build -trimpath -o bin/veyra-api ./cmd/api - go build -trimpath -o bin/veyra-db ./cmd/db - go build -trimpath -o bin/veyra-user ./cmd/user -db-create: - go run ./cmd/db -env-file .env -action create -db-migrate: - go run ./cmd/db -env-file .env -action migrate -admin-create: - go run ./cmd/user -env-file .env -role ADMIN -user-create: - go run ./cmd/user -env-file .env -role USER -test: - go test -race ./... -check: - go vet ./... - npm run build --workspace @veyra/admin -admin-dev: - npm run dev --workspace @veyra/admin -admin-build: - npm run build --workspace @veyra/admin diff --git a/README.md b/README.md index 1904d69..ea46711 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 见势 · Veyra -AI 量化投资辅助产品。原始交互原型与全栈设计文档保留在根目录;当前交付包括工程基础、开发登录、管理员用户查询与账户初始化。 +AI 量化投资辅助产品。原始交互原型与全栈设计文档保留在根目录;当前交付包括工程基础、开发登录、管理员用户查询、账户初始化、后台供应商配置与 Expo App 基础流程。 ## 已实现 @@ -14,32 +14,71 @@ AI 量化投资辅助产品。原始交互原型与全栈设计文档保留在 - App 开发登录与 Bearer 会话;管理端和 App 会话互相隔离,数据库只存凭据哈希。 - 账户创建、列表与汇总;模拟账户初始现金与流水原子入账,金额使用整数分,API 使用十进制字符串。真实记录账户现金为未知。 +- 后台配置主模型、备用模型、审核模型与行情源,初始留空且关闭;密钥加密存储,版本冲突保护、配置历史与审计原子保存。 + +- React Native + Expo App:五个主导航、开发登录、安全会话、账户创建与切换、真实现金账本展示;未接入模块明确提示。 + ## 启动 需要 Go 1.26.2 与支持 Vite 的 Node.js,已提交 Go 和 npm 锁文件。根目录执行: ```sh -cp .env.example .env +cp configs/.env.example configs/.env ``` -将 PostgreSQL 用户名与密码填写到 `.env`。默认数据库名称 `veyra`,默认管理数据库 `postgres`;创建数据库需要连接角色具有 CREATEDB 权限。Redis 无用户名,有密码:在本地 `.env` 配置 `REDIS_PASSWORD`,保持 `REDIS_USERNAME` 为空。`PGSSLMODE=disable` 是本地模板值,如服务器要求 TLS 则修改。 +将 PostgreSQL 用户名与密码填写到 `configs/.env`。默认数据库名称 `veyra`,默认管理数据库 `postgres`;创建数据库需要连接角色具有 CREATEDB 权限。Redis 无用户名,有密码:在本地 `configs/.env` 配置 `REDIS_PASSWORD`,保持 `REDIS_USERNAME` 为空。`PGSSLMODE=disable` 是本地模板值,如服务器要求 TLS 则修改。 ```sh go mod download -npm ci -make db-create -make db-migrate -make admin-create -make api +npm --prefix web/admin ci +go run . -command db-create +go run . -command db-migrate +go run . -command user-create -role ADMIN +npm --prefix web/admin run build +go run . ``` -启动后访问 `http://127.0.0.1:8080`,管理端由 Go embed 提供,页面与 API 同源。需要前端热更新时另开终端运行 `make admin-dev`,访问 Vite 输出的本地地址;开发代理将 `/api` 转发到 `127.0.0.1:8080`。API 可在数据库未配置时启动并如实报告不可就绪。 +启动后访问 `http://127.0.0.1:8080`,管理端由 Go embed 提供,页面与 API 同源。需要前端热更新时另开终端运行 `npm --prefix web/admin run dev`,访问 Vite 输出的本地地址;开发代理将 `/api` 转发到 `127.0.0.1:8080`。API 可在数据库未配置时启动并如实报告不可就绪。 -管理端初始登录名与密码由 `.env` 的 `ADMIN_LOGIN` / `ADMIN_PASSWORD` 配置,密码要求 12–72 字节。开发环境的本地 `.env` 已单独生成随机密码,模板不包含密码。`make admin-create` 创建身份,不覆盖已有管理员。App 测试身份使用 `DEV_USER_LOGIN` / `DEV_USER_PASSWORD` 和 `make user-create` 初始化。数据库的登录账号不用于产品登录。 +管理端初始登录名与密码由 `configs/.env` 的 `ADMIN_LOGIN` / `ADMIN_PASSWORD` 配置,管理员密码要求 8–72 字节。凭据只保存在本地 `configs/.env`,模板不包含密码。`go run . -command user-create -role ADMIN` 创建身份,不覆盖已有管理员。App 测试身份使用 `DEV_USER_LOGIN` / `DEV_USER_PASSWORD` 和 `go run . -command user-create -role USER` 初始化。数据库的登录账号不用于产品登录。 -已有数据库只需 `make db-migrate` 应用新增的 `000002_identity_account.sql`,无需重新建库。 +已有数据库只需 `go run . -command db-migrate` 应用所有尚未执行的版本化迁移(当前最新为 `000004_llm_configuration.sql`),无需重新建库。 -`make build` 先构建前端并复制到 embed 目录,再输出 `bin/veyra-api`、`bin/veyra-db` 和 `bin/veyra-user`。运行 `./bin/veyra-api -env-file .env` 无需外置前端文件或 Node.js。修改前端必须重新构建 Go 二进制;直接 `go build` 不会自动编译前端。清空构建产物的 checkout 仍可编译基础 API,管理端返回明确的 503 提示直到完成前端构建。 +生产编译直接使用 npm 与 Go: + +```sh +npm --prefix web/admin run build +go build -trimpath -o bin/veyra . +./bin/veyra +``` + +`npm --prefix web/admin run build` 将前端产物复制到 Go embed 目录,`go build` 输出唯一的 `veyra` 可执行文件。默认读取当前工作目录的 `configs/.env`,也可使用 `-env-file` 指定文件或空值以仅使用进程环境。修改管理端后先重建前端,再重建 Go 二进制;未构建前端时管理端返回明确的 503。 + +常规开发启动直接运行 `go run .`,也可以在 IDE 运行根目录 `main.go`;仓库只有一个 main 函数,不依赖 Makefile。服务启动不自动建库、迁移或修改身份。 + +开发管理员修改使用本地 `configs/.env` 的 `ADMIN_LOGIN` / `ADMIN_PASSWORD`: + +```sh +go run . -command admin-set -previous-login <原管理员登录名> +``` + +已有登录名不变时可省略 `-previous-login`。此命令仅限 local/test,保留原管理员 ID、以 bcrypt 保存新密码并撤销旧会话;登录名冲突时全部回滚。管理员密码为 8–72 字节,App 开发用户仍为 12–72 字节。 + + +### App 开发 + +App 位于 `apps/mobile`,使用 Expo Router + React Native + TypeScript,配色为白底、石墨黑与柔紫。初始化 USER 身份后填写 App 的公开 API 地址并启动: + +```sh +go run . -command user-create -role USER +npm --prefix apps/mobile ci +cp apps/mobile/.env.example apps/mobile/.env +npm --prefix apps/mobile run start +``` + +在 `apps/mobile/.env` 设置 `EXPO_PUBLIC_API_URL`。iOS 模拟器可用后端本机地址,Android 模拟器使用 `10.0.2.2`,真机使用开发电脑内网地址。App 登录身份与管理员隔离;凭据不打包到客户端,供应商信息仍由管理端配置。 + +`npm --prefix apps/mobile run typecheck && npm --prefix apps/mobile run test` 执行 TypeScript 与客户端测试;`npm --prefix apps/mobile run build` 导出两个平台的 Hermes bundle,不生成 APK/IPA。运行和当前功能边界见 [App 说明](apps/mobile/README.md)。 ### 账户 API 示例 @@ -53,28 +92,34 @@ make api 真实记录账户请求省略 `initial_cash`,返回 `cash_balance: null`、`ledger_status: INCOMPLETE`。同一用户每种模式只可创建一个账户;重复创建返回 409,不重复生成初始资金。`GET /api/v1/accounts` 返回当前用户账户,`GET /api/v1/accounts/{id}/summary` 查询归属当前用户的账户,其他用户的 ID 返回 404。 +### 后台供应商配置 + +「模型配置」填写主模型、备用模型和审核模型;「系统设置」填写行情源。供应商名称、HTTPS 地址、模型名称和密钥初始均为空,不预选供应商。未配置时保持关闭;模型启用需要名称、地址、模型名称和密钥,行情源启用需要名称和地址。保存或启用不代表连接已验证,当前尚未实现供应商调用与连通性测试。协议、模型目录、响应存储策略和认证开关均为后台可修改的配置,不预填厂家地址或模型。未填写模型时可先保存关闭状态的连接地址和密钥。模型目录需与接口同源,避免把认证信息指向另一个站点。 + +`SECRET_ENCRYPTION_KEY` 是部署层加密主密钥,使用 Base64 编码的 32 字节随机值,在本地 `configs/.env` 或部署秘密配置中持久保存。本机已生成此值,仓库模板留空。可用 `openssl rand -base64 32` 为新环境生成;主密钥必须与数据库一起备份,切勿在已有密文后直接替换。未配置主密钥时仍可保存无密钥的关闭配置,提交密钥会被拒绝。 + +密钥采用 AES-256-GCM 加密,绑定配置角色;查询只返回 `has_api_key`。密钥留空保留原值,勾选清除后删除当前密钥;修改接口地址时必须重新填写或清除密钥。历史保留加密快照用于追溯,审计日志不含明文密钥。保存使用 `expected_version` 防止覆盖其他管理员的修改;所有接口限制管理员,同源 Origin 保护写操作。 + ### 开发登录边界 密码登录和命令行身份初始化仅在 `APP_ENV=local/test` 开启;其他环境拒绝开发登录。正式 App 短信登录、访问凭据刷新、管理端 MFA 与细分 RBAC 尚未实现。当前会话为 8 小时、服务端可撤销;Cookie 写操作要求同来源 Origin。开发模式可使用 Vite 的同源代理,部署代理的 HTTPS 信任配置将在正式认证阶段实现。 ```sh -make test -make check +go test -race ./... +go vet ./... ``` PostgreSQL 集成测试需显式提供测试数据库,测试在独立临时 schema 内运行并清理: ```sh -VEYRA_TEST_DATABASE_URL='postgres://user:password@host:5432/test_db?sslmode=disable' make test +VEYRA_TEST_DATABASE_URL='postgres://user:password@host:5432/test_db?sslmode=disable' go test -race ./... ``` ## 目录 ```text -cmd/ - api/ API 入口与依赖装配 - db/ 建库与迁移命令 - user/ 开发身份初始化命令 +main.go 唯一入口,启动服务或执行明确的初始化命令 +configs/ 后端环境配置与模板(私有配置不提交) internal/ config/ 配置加载与校验 database/migrations/ 数据库连接与版本化迁移 @@ -85,20 +130,30 @@ internal/ adminui/ 管理端 embed 资源与静态服务 apperror/ 跨层业务错误 ratelimit/ Redis 登录限流 + secret/ 供应商凭据加密 integration/ 数据库集成测试 web/admin/ Vite React 管理端 +apps/mobile/ React Native Expo App api/openapi.yaml HTTP 契约 go.mod / go.sum 根目录唯一 Go module ``` -仓库采用根目录 Go 单模块结构,管理端放在 `web/admin`,无需 `go.work`。文件按职责划分,调用方向为 handler → service → repository;接口在 service 中定义,由 repository 实现,避免 service 依赖具体数据库。 +仓库采用根目录 Go 单模块结构,管理端放在 `web/admin`,无需 `go.work`。两个前端各自维护 package.json、package-lock.json 和 node_modules,根目录不设置 npm workspace。文件按职责划分,调用方向为 handler → service → repository;接口在 service 中定义,由 repository 实现,避免 service 依赖具体数据库。 采用显式 SQL 和结构体 db tag,避免 ORM 隐式推断表名。迁移打包到 Go 二进制,已执行迁移不可改写,后续修改新增版本。API 启动不自动建库或迁移。健康检查不会返回连接串和供应商错误细节。 ## 待完成 -用户已确认执行首次建库迁移。此次新增第二版迁移和身份初始化需在可访问数据库的终端运行;当前执行环境禁止访问目标内网地址和监听本地端口。单元测试及无监听的 HTTP 测试可以本地执行,真实数据库集成测试只有配置测试连接时才会运行。 +2026-10-08 已验证 PostgreSQL、Redis 内网连接,应用全部四版迁移并初始化开发管理员。真实 PostgreSQL 集成测试已在临时 schema 中执行;测试不会修改开发账户或供应商配置。 -尚未接入候选审核、交易计划、成交账本、App UI、行情、AI、Worker 或回测。下一步建设候选修订、审核发布与计划闭环,再落实成交账本和风险监控。 +尚未接入候选审核、交易计划、成交账本、行情、AI、Worker 或回测。App 已有基础界面和登录/账户真实 API,偏好、机会、计划、持仓明细、提醒与复盘仍待相应业务接口。下一步建设候选修订、审核发布与计划闭环,再落实成交账本和风险监控。 管理端基础组件按 [shadcn/ui 的 Vite 集成方式](https://ui.shadcn.com/docs/installation/vite)维护在 `src/components/ui`,Tailwind 使用[官方 Vite 插件](https://tailwindcss.com/docs/installation/using-vite)。 + +## 日志与 A 股开发数据 + +后端使用 zap,默认在运行目录创建 `logs/veyra.log`,控制台易读、文件 JSON,自动轮转压缩。通过 `configs/.env` 的 `LOG_*` 配置目录、级别和保留规则。 + +后台「系统设置」配置公开行情适配器与地址,启用后启动同步、每五分钟更新沪深报价;「数据健康」查询股票和同步结果。研究回测可直接按代码、交易所和日期获取不复权日线,无需逐只新建股票或导入 CSV。当前公开源为开发研究用途;供应商时间、历史快照与失败状态如实展示。策略筛选、正式行情授权和实际交易规则仍待接入。 + +App 在「机会」→「查看自动同步的 A 股行情」查看数据。当前预览 Metro `exp://192.168.100.2:8081`,API 为 `http://192.168.100.2:8083`,同一内网使用 Expo Go;开发身份读取后端本地 `DEV_USER_*` 配置。尚未完成真机界面验证。 diff --git a/api/openapi.yaml b/api/openapi.yaml index 31ddf0b..5916b55 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1,8 +1,8 @@ openapi: 3.0.3 info: title: Veyra API - version: 0.2.0 - description: 工程基础、开发身份和账户初始化;密码登录仅限 local/test。 + version: 0.12.0 + description: 开发身份、账户、供应商配置、候选、计划、成交账本、站内通知、研究策略、Python 回测与公开 A 股开发行情。模拟工作流仅限 local/test;正式行情授权、完整交易规则及远程推送未完成。 servers: - url: http://127.0.0.1:8080 paths: @@ -10,27 +10,35 @@ paths: get: operationId: getLiveness responses: - '200': + "200": description: API 进程存活,与依赖是否就绪无关 content: application/json: schema: type: object - required: [status] + required: + - status properties: - status: {type: string, enum: [alive]} + status: + type: string + enum: + - alive /readyz: get: operationId: getReadiness responses: - '200': {$ref: '#/components/responses/Health'} - '503': {$ref: '#/components/responses/Health'} + "200": + $ref: "#/components/responses/Health" + "503": + $ref: "#/components/responses/Health" /api/v1/system/health: get: operationId: getSystemHealth responses: - '200': {$ref: '#/components/responses/Health'} - '503': {$ref: '#/components/responses/Health'} + "200": + $ref: "#/components/responses/Health" + "503": + $ref: "#/components/responses/Health" /api/admin/v1/auth/login: post: operationId: adminLogin @@ -39,24 +47,34 @@ paths: required: true content: application/json: - schema: {$ref: '#/components/schemas/LoginRequest'} + schema: + $ref: "#/components/schemas/LoginRequest" responses: - '200': + "200": description: 已创建管理端会话 headers: - Set-Cookie: {schema: {type: string}, description: veyra_admin_session; HttpOnly; SameSite=Strict; Path=/api/admin} + Set-Cookie: + schema: + type: string + description: veyra_admin_session; HttpOnly; SameSite=Strict; Path=/api/admin content: application/json: schema: allOf: - - {$ref: '#/components/schemas/Envelope'} + - $ref: "#/components/schemas/Envelope" - properties: - data: {$ref: '#/components/schemas/AdminLogin'} - '401': {$ref: '#/components/responses/Error'} - '403': {$ref: '#/components/responses/Error'} - '422': {$ref: '#/components/responses/Error'} - '429': {$ref: '#/components/responses/Error'} - '503': {$ref: '#/components/responses/Error'} + data: + $ref: "#/components/schemas/AdminLogin" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "429": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" /api/v1/auth/login: post: operationId: developmentAppLogin @@ -65,125 +83,1470 @@ paths: required: true content: application/json: - schema: {$ref: '#/components/schemas/LoginRequest'} + schema: + $ref: "#/components/schemas/LoginRequest" responses: - '200': + "200": description: 已创建 App 会话 content: application/json: schema: allOf: - - {$ref: '#/components/schemas/Envelope'} + - $ref: "#/components/schemas/Envelope" - properties: - data: {$ref: '#/components/schemas/AppLogin'} - '401': {$ref: '#/components/responses/Error'} - '422': {$ref: '#/components/responses/Error'} - '429': {$ref: '#/components/responses/Error'} - '503': {$ref: '#/components/responses/Error'} + data: + $ref: "#/components/schemas/AppLogin" + "401": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "429": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" /api/admin/v1/me: get: operationId: getAdminIdentity - security: [{adminCookie: []}] + security: + - adminCookie: [] responses: - '200': {$ref: '#/components/responses/Identity'} - '401': {$ref: '#/components/responses/Error'} - '503': {$ref: '#/components/responses/Error'} + "200": + $ref: "#/components/responses/Identity" + "401": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" /api/v1/me: get: operationId: getAppIdentity - security: [{appBearer: []}] + security: + - appBearer: [] responses: - '200': {$ref: '#/components/responses/Identity'} - '401': {$ref: '#/components/responses/Error'} - '503': {$ref: '#/components/responses/Error'} + "200": + $ref: "#/components/responses/Identity" + "401": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" /api/admin/v1/auth/logout: post: operationId: adminLogout description: 同源 Origin 请求,撤销会话并清除管理端 Cookie。 - security: [{adminCookie: []}] + security: + - adminCookie: [] responses: - '200': {$ref: '#/components/responses/Logout'} - '401': {$ref: '#/components/responses/Error'} - '403': {$ref: '#/components/responses/Error'} - '503': {$ref: '#/components/responses/Error'} + "200": + $ref: "#/components/responses/Logout" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" /api/v1/auth/logout: post: operationId: appLogout - security: [{appBearer: []}] + security: + - appBearer: [] responses: - '200': {$ref: '#/components/responses/Logout'} - '401': {$ref: '#/components/responses/Error'} - '503': {$ref: '#/components/responses/Error'} + "200": + $ref: "#/components/responses/Logout" + "401": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" /api/admin/v1/users: get: operationId: listAppUsers description: 最近创建的 100 位 App 用户,不包含密码、登录名或后台权限。 - security: [{adminCookie: []}] + security: + - adminCookie: [] responses: - '200': + "200": description: App 用户列表 content: application/json: schema: allOf: - - {$ref: '#/components/schemas/Envelope'} + - $ref: "#/components/schemas/Envelope" - properties: data: type: array - items: {$ref: '#/components/schemas/User'} - '401': {$ref: '#/components/responses/Error'} - '503': {$ref: '#/components/responses/Error'} + items: + $ref: "#/components/schemas/User" + "401": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" /api/v1/accounts: get: operationId: listOwnAccounts - security: [{appBearer: []}] + security: + - appBearer: [] responses: - '200': + "200": description: 当前用户账户列表 content: application/json: schema: allOf: - - {$ref: '#/components/schemas/Envelope'} + - $ref: "#/components/schemas/Envelope" - properties: data: type: array - items: {$ref: '#/components/schemas/AccountSummary'} - '401': {$ref: '#/components/responses/Error'} - '503': {$ref: '#/components/responses/Error'} + items: + $ref: "#/components/schemas/AccountSummary" + "401": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" post: operationId: createOwnAccount description: simulation 要求 initial_cash,manual_real 必须省略;每种模式仅一个账户。 - security: [{appBearer: []}] + security: + - appBearer: [] requestBody: required: true content: application/json: - schema: {$ref: '#/components/schemas/CreateAccount'} + schema: + $ref: "#/components/schemas/CreateAccount" responses: - '201': {$ref: '#/components/responses/AccountSummary'} - '401': {$ref: '#/components/responses/Error'} - '409': {$ref: '#/components/responses/Error'} - '422': {$ref: '#/components/responses/Error'} - '503': {$ref: '#/components/responses/Error'} + "201": + $ref: "#/components/responses/AccountSummary" + "401": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" /api/v1/accounts/{id}/summary: get: operationId: getOwnAccountSummary - security: [{appBearer: []}] + security: + - appBearer: [] parameters: - name: id in: path required: true - schema: {type: string, format: uuid} + schema: + type: string + format: uuid responses: - '200': {$ref: '#/components/responses/AccountSummary'} - '401': {$ref: '#/components/responses/Error'} - '404': {$ref: '#/components/responses/Error'} - '503': {$ref: '#/components/responses/Error'} + "200": + $ref: "#/components/responses/AccountSummary" + "401": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/providers: + get: + operationId: listProviderConfigurations + security: + - adminCookie: [] + description: 列出已保存配置,初始为空数组;不返回密钥或密文。 + responses: + "200": + description: 已保存的供应商配置 + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Envelope" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/ProviderConfig" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/providers/{key}: + put: + operationId: saveProviderConfiguration + security: + - adminCookie: [] + description: 同源 Origin 写入;首次 expected_version 为 0,后续使用当前版本。省略 api_key 保留密钥,clear_api_key 清除。修改接口地址须重新填写或清除已有密钥。配置保存不代表连接已验证。 + parameters: + - name: key + in: path + required: true + schema: + type: string + enum: + - model_primary + - model_backup + - model_review + - market_primary + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ProviderInput" + responses: + "200": + description: 配置、历史与审计已原子保存 + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Envelope" + - properties: + data: + $ref: "#/components/schemas/ProviderConfig" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/audit-events: + get: + operationId: listAuditEvents + security: + - adminCookie: [] + description: 最近 100 条后台操作审计,详情不含密钥。 + responses: + "200": + description: 审计事件 + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Envelope" + - properties: + data: + type: array + items: + $ref: "#/components/schemas/AuditEvent" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/candidates: + get: + operationId: adminCandidates + description: 最近 100 条候选;管理端列出最新修订及当前发布版本,App 仅显示当前有效发布。仅模拟数据。 + security: + - adminCookie: [] + parameters: + - in: query + name: limit + schema: + type: integer + minimum: 1 + maximum: 100 + default: 100 + - in: query + name: status + schema: + type: string + enum: + - DRAFT + - PENDING_REVIEW + - PUBLISHED + - RETURNED + - WITHDRAWN + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + post: + operationId: createCandidate + description: 保存独立模拟草稿,要求同源 Origin。 + security: + - adminCookie: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CandidateDraft" + responses: + "201": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/candidates/{id}: + get: + operationId: adminCandidateDetail + description: 读取修订详情;App 不能读取未发布内容,历史发布可查看失效状态及撤回原因。 + security: + - adminCookie: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/candidates: + get: + operationId: appCandidates + description: 最近 100 条候选;管理端列出最新修订及当前发布版本,App 仅显示当前有效发布。仅模拟数据。 + security: + - appBearer: [] + parameters: + - in: query + name: limit + schema: + type: integer + minimum: 1 + maximum: 100 + default: 100 + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/candidates/{id}: + get: + operationId: appCandidateDetail + description: 读取修订详情;App 不能读取未发布内容,历史发布可查看失效状态及撤回原因。 + security: + - appBearer: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/candidates/{id}/revisions: + post: + operationId: reviseCandidate + description: 创建下一份独立修订,保留原文;待审版本需先退回;要求同源 Origin。 + security: + - adminCookie: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CandidateRevisionInput" + responses: + "201": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/candidates/{id}/submit: + post: + operationId: submitCandidate + description: 提交审核;要求具体意见、版本匹配和同源 Origin。审核、审计、事件同事务。 + security: + - adminCookie: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CandidateAction" + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/candidates/{id}/publish: + post: + operationId: publishCandidate + description: 审核发布并替代旧发布;要求具体意见、版本匹配和同源 Origin。审核、审计、事件同事务。 + security: + - adminCookie: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CandidateAction" + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/candidates/{id}/return: + post: + operationId: returnCandidate + description: 退回,等待创建新修订;要求具体意见、版本匹配和同源 Origin。审核、审计、事件同事务。 + security: + - adminCookie: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CandidateAction" + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/candidates/{id}/withdraw: + post: + operationId: withdrawCandidate + description: 撤回当前有效发布;要求具体意见、版本匹配和同源 Origin。审核、审计、事件同事务。 + security: + - adminCookie: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CandidateAction" + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/plan-previews: + post: + operationId: previewPlan + description: 模拟买入预算预览,不保留现金。数量为字符串且须为 100 股整数倍;预计费用由用户输入。 + security: + - appBearer: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PlanPreviewInput" + responses: + "201": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/plan-previews/{id}/confirm: + post: + operationId: confirmPlan + description: 确认时重检所有权、候选版本、时效与可用现金。确认同一预览幂等,保留预算但不扣现金。 + security: + - appBearer: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EmptyObject" + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/plans: + get: + operationId: listPlans + description: 读取本人最近 100 条计划,候选撤回或过期后失效并释放预算。 + security: + - appBearer: [] + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/plans/{id}/cancel: + post: + operationId: cancelPlan + description: 版本保护的取消,释放保留预算,已记录成交保留。 + security: + - appBearer: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ExpectedVersion" + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/accounts/{id}/budget: + get: + operationId: getBudget + description: 模拟账本现金、有效计划保留及可用金额,均为十进制字符串。 + security: + - appBearer: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/fills: + post: + operationId: recordFill + description: 录入已发生的模拟成交;同账户 idempotency_key 重试幂等,不同内容拒绝。按发生时间顺序录入;模拟可卖时间须手工指定,不推断真实交易日。 + security: + - appBearer: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/FillInput" + responses: + "201": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/fills/{id}/reverse: + post: + operationId: reverseFill + description: 只冲正账户最新未冲正成交;原成交和费用记录不可覆盖。反向现金、批次恢复、审计、事件同事务,原计划不自动重开。 + security: + - appBearer: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ReversalInput" + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/accounts/{id}/fills: + get: + operationId: listFills + description: 本人模拟账户最近 100 条成交,包括已冲正原记录;金额与数量为字符串。 + security: + - appBearer: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/accounts/{id}/portfolio: + get: + operationId: getPortfolio + description: 真实读取模拟账本现金、FIFO 剩余成本和含费用已实现盈亏;市值与浮盈亏为 null。 + security: + - appBearer: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/notifications: + get: + operationId: listNotifications + description: 本人最近 100 条站内通知,不代表远程推送已发送。 + security: + - appBearer: [] + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/v1/notifications/{id}/read: + post: + operationId: markNotificationRead + description: 本人通知标记已读,可重复。 + security: + - appBearer: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EmptyObject" + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/notification-tasks: + get: + operationId: listNotificationTasks + description: 最近 100 条持久化事件及站内通知数量;已处理不一定有通知收件人。 + security: + - adminCookie: [] + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/simulation-quotes: + get: + operationId: listSimulationQuotes + description: 最新 100 个手工模拟价格快照,不代表在线行情。 + security: + - adminCookie: [] + responses: + "200": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + post: + operationId: recordSimulationQuote + description: 录入手工模拟价格,要求同源 Origin;当前版本、有效计划与候选均重新核验,触发仅产生站内提醒。 + security: + - adminCookie: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SimulationQuoteInput" + responses: + "201": + description: 操作成功,data 包含记录或列表 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "404": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + /api/admin/v1/strategies: + get: + operationId: listStrategies + security: + - adminCookie: [] + responses: + "200": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + post: + operationId: createStrategy + security: + - adminCookie: [] + responses: + "201": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/StrategyInput" + /api/admin/v1/strategies/{id}: + get: + operationId: getStrategy + security: + - adminCookie: [] + responses: + "200": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + parameters: + - name: id + in: path + required: true + schema: &a1 + type: string + format: uuid + /api/admin/v1/strategies/{id}/versions: + post: + operationId: reviseStrategy + security: + - adminCookie: [] + responses: + "201": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + parameters: + - name: id + in: path + required: true + schema: *a1 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/StrategyInput" + /api/admin/v1/strategies/{id}/deploy: + post: + operationId: selectResearchVersion + security: + - adminCookie: [] + responses: + "200": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + parameters: + - name: id + in: path + required: true + schema: *a1 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/StrategyDeployment" + /api/admin/v1/backtests: + get: + operationId: listBacktests + security: + - adminCookie: [] + responses: + "200": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + post: + operationId: queueManualBacktest + security: + - adminCookie: [] + responses: + "202": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/BacktestInput" + /api/admin/v1/backtests/{id}: + get: + operationId: getBacktest + security: + - adminCookie: [] + responses: + "200": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + parameters: + - name: id + in: path + required: true + schema: *a1 + /api/admin/v1/backtests/from-market: + post: + operationId: queueMarketBacktest + security: + - adminCookie: [] + responses: + "202": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/MarketBacktestInput" + /api/v1/instruments: + get: + operationId: searchInstrumentsApp + security: + - appBearer: [] + responses: + "200": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + parameters: + - name: q + in: query + schema: &a2 + type: string + /api/v1/market/history: + get: + operationId: getMarketHistoryApp + security: + - appBearer: [] + responses: + "200": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + parameters: + - name: symbol + in: query + required: true + schema: *a2 + - name: exchange + in: query + required: true + schema: *a2 + - name: start + in: query + required: true + schema: *a2 + - name: end + in: query + required: true + schema: *a2 + /api/admin/v1/instruments: + get: + operationId: searchInstrumentsAdmin + security: + - adminCookie: [] + responses: + "200": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + parameters: + - name: q + in: query + schema: *a2 + /api/admin/v1/market/history: + get: + operationId: getMarketHistoryAdmin + security: + - adminCookie: [] + responses: + "200": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" + parameters: + - name: symbol + in: query + required: true + schema: *a2 + - name: exchange + in: query + required: true + schema: *a2 + - name: start + in: query + required: true + schema: *a2 + - name: end + in: query + required: true + schema: *a2 + /api/admin/v1/market/syncs: + get: + operationId: listMarketSyncs + security: + - adminCookie: [] + responses: + "200": + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Envelope" + "401": + $ref: "#/components/responses/Error" + "403": + $ref: "#/components/responses/Error" + "409": + $ref: "#/components/responses/Error" + "422": + $ref: "#/components/responses/Error" + "503": + $ref: "#/components/responses/Error" components: securitySchemes: - adminCookie: {type: apiKey, in: cookie, name: veyra_admin_session} - appBearer: {type: http, scheme: bearer} + adminCookie: + type: apiKey + in: cookie + name: veyra_admin_session + appBearer: + type: http + scheme: bearer responses: Error: description: 标准业务错误 @@ -191,129 +1554,767 @@ components: application/json: schema: type: object - required: [error, meta] + required: + - error + - meta properties: error: type: object - required: [code, message, retryable] + required: + - code + - message + - retryable properties: - code: {type: string} - message: {type: string} - retryable: {type: boolean} - meta: {$ref: '#/components/schemas/Meta'} + code: + type: string + message: + type: string + retryable: + type: boolean + meta: + $ref: "#/components/schemas/Meta" Identity: description: 当前身份 content: application/json: schema: allOf: - - {$ref: '#/components/schemas/Envelope'} + - $ref: "#/components/schemas/Envelope" - properties: - data: {$ref: '#/components/schemas/User'} + data: + $ref: "#/components/schemas/User" Logout: description: 会话已撤销 content: application/json: schema: allOf: - - {$ref: '#/components/schemas/Envelope'} + - $ref: "#/components/schemas/Envelope" - properties: data: type: object - required: [revoked] + required: + - revoked properties: - revoked: {type: boolean} + revoked: + type: boolean AccountSummary: description: 账户与账本估算现金 content: application/json: schema: allOf: - - {$ref: '#/components/schemas/Envelope'} + - $ref: "#/components/schemas/Envelope" - properties: - data: {$ref: '#/components/schemas/AccountSummary'} + data: + $ref: "#/components/schemas/AccountSummary" Health: description: PostgreSQL 和 Redis 都正常时为 200,任一不可用或未配置时为 503 content: application/json: schema: - $ref: '#/components/schemas/Health' + $ref: "#/components/schemas/Health" schemas: Meta: type: object - required: [request_id] + required: + - request_id properties: - request_id: {type: string, format: uuid} - server_time: {type: string, format: date-time} + request_id: + type: string + format: uuid + server_time: + type: string + format: date-time Envelope: type: object - required: [data, meta] + required: + - data + - meta properties: data: {} - meta: {$ref: '#/components/schemas/Meta'} + meta: + $ref: "#/components/schemas/Meta" User: type: object - required: [id, display_name, status, created_at, updated_at] + required: + - id + - display_name + - status + - created_at + - updated_at properties: - id: {type: string, format: uuid} - display_name: {type: string} - status: {type: string, enum: [ACTIVE, DISABLED]} - created_at: {type: string, format: date-time} - updated_at: {type: string, format: date-time} + id: + type: string + format: uuid + display_name: + type: string + status: + type: string + enum: + - ACTIVE + - DISABLED + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time LoginRequest: type: object additionalProperties: false - required: [login_name, password] + required: + - login_name + - password properties: - login_name: {type: string, minLength: 3, maxLength: 64} - password: {type: string, minLength: 1, maxLength: 72, writeOnly: true} + login_name: + type: string + minLength: 3 + maxLength: 64 + password: + type: string + minLength: 1 + maxLength: 72 + writeOnly: true AdminLogin: type: object - required: [user, expires_at] + required: + - user + - expires_at properties: - user: {$ref: '#/components/schemas/User'} - expires_at: {type: string, format: date-time} + user: + $ref: "#/components/schemas/User" + expires_at: + type: string + format: date-time AppLogin: allOf: - - {$ref: '#/components/schemas/AdminLogin'} + - $ref: "#/components/schemas/AdminLogin" - type: object - required: [access_token] + required: + - access_token properties: - access_token: {type: string} + access_token: + type: string CreateAccount: type: object additionalProperties: false - required: [mode, name] + required: + - mode + - name properties: - mode: {type: string, enum: [simulation, manual_real]} - name: {type: string, minLength: 1, maxLength: 80} - initial_cash: {type: string, description: CNY 十进制字符串,最多两位小数,范围 0 至 1000000000.00} + mode: + type: string + enum: + - simulation + - manual_real + name: + type: string + minLength: 1 + maxLength: 80 + initial_cash: + type: string + description: CNY 十进制字符串,最多两位小数,范围 0 至 1000000000.00 AccountSummary: type: object - required: [id, user_id, mode, name, currency, ledger_status, created_at, cash_balance] + required: + - id + - user_id + - mode + - name + - currency + - ledger_status + - created_at + - cash_balance properties: - id: {type: string, format: uuid} - user_id: {type: string, format: uuid} - mode: {type: string, enum: [simulation, manual_real]} - name: {type: string} - currency: {type: string, enum: [CNY]} - ledger_status: {type: string, enum: [COMPLETE, INCOMPLETE]} - created_at: {type: string, format: date-time} - cash_balance: {type: string, nullable: true, description: CNY 十进制字符串,现金历史不完整时为 null} + id: + type: string + format: uuid + user_id: + type: string + format: uuid + mode: + type: string + enum: + - simulation + - manual_real + name: + type: string + currency: + type: string + enum: + - CNY + ledger_status: + type: string + enum: + - COMPLETE + - INCOMPLETE + created_at: + type: string + format: date-time + cash_balance: + type: string + nullable: true + description: CNY 十进制字符串,现金历史不完整时为 null Health: type: object - required: [status, checked_at, components] + required: + - status + - checked_at + - components properties: - status: {type: string, enum: [ready, not_ready]} - checked_at: {type: string, format: date-time} + status: + type: string + enum: + - ready + - not_ready + checked_at: + type: string + format: date-time components: type: object - required: [postgresql, redis] + required: + - postgresql + - redis properties: - postgresql: {$ref: '#/components/schemas/Component'} - redis: {$ref: '#/components/schemas/Component'} + postgresql: + $ref: "#/components/schemas/Component" + redis: + $ref: "#/components/schemas/Component" Component: type: object - required: [status] + required: + - status properties: - status: {type: string, enum: [ready, unavailable, not_configured]} + status: + type: string + enum: + - ready + - unavailable + - not_configured + ProviderInput: + type: object + additionalProperties: false + required: + - name + - base_url + - model_name + - enabled + - timeout_seconds + - clear_api_key + - expected_version + properties: + name: + type: string + maxLength: 80 + base_url: + type: string + maxLength: 2048 + description: 留空或无用户信息、query、fragment 的 HTTPS 地址 + model_name: + type: string + maxLength: 120 + wire_api: + type: string + enum: + - responses + - chat_completions + default: responses + model_catalog_url: + type: string + maxLength: 2048 + disable_response_storage: + type: boolean + description: Omitted values preserve existing settings; defaults to true on create + requires_openai_auth: + type: boolean + description: Omitted values preserve existing settings; defaults to true on create + enabled: + type: boolean + timeout_seconds: + type: integer + minimum: 1 + maximum: 30 + api_key: + type: string + minLength: 1 + maxLength: 4096 + writeOnly: true + clear_api_key: + type: boolean + expected_version: + type: integer + format: int64 + minimum: 0 + market_adapter: + type: string + enum: + - "" + - eastmoney_public + - sina_public + history_base_url: *a2 + quote_base_url: *a2 + ProviderConfig: + type: object + properties: + id: + type: string + format: uuid + config_key: + type: string + enum: + - model_primary + - model_backup + - model_review + - market_primary + name: + type: string + base_url: + type: string + model_name: + type: string + wire_api: + type: string + enum: + - responses + - chat_completions + model_catalog_url: + type: string + disable_response_storage: + type: boolean + requires_openai_auth: + type: boolean + enabled: + type: boolean + timeout_seconds: + type: integer + has_api_key: + type: boolean + version: + type: integer + format: int64 + last_test_status: + type: string + enum: + - NOT_CONFIGURED + - NOT_TESTED + - READY + - FAILED + last_tested_at: + type: string + format: date-time + nullable: true + last_test_message: + type: string + updated_by: + type: string + format: uuid + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + market_adapter: + type: string + enum: + - "" + - eastmoney_public + - sina_public + history_base_url: *a2 + quote_base_url: *a2 + AuditEvent: + type: object + properties: + id: + type: string + format: uuid + actor_id: + type: string + format: uuid + action: + type: string + object_type: + type: string + object_id: + type: string + format: uuid + request_id: + type: string + format: uuid + details: + type: object + additionalProperties: true + created_at: + type: string + format: date-time + EmptyObject: + type: object + additionalProperties: false + ExpectedVersion: + type: object + required: + - expected_version + additionalProperties: false + properties: + expected_version: + type: integer + format: int64 + minimum: 1 + CandidateAction: + type: object + additionalProperties: false + required: + - expected_version + - reason + properties: + expected_version: + type: integer + format: int64 + minimum: 1 + reason: + type: string + minLength: 1 + maxLength: 1000 + CandidateContent: + type: object + required: + - symbol + - exchange + - name + - strategy_code + - title + - thesis + - evidence + - counter_evidence + - invalidation + - reference_plan + properties: + symbol: + type: string + pattern: ^[0-9]{6}$ + exchange: + type: string + enum: + - SSE + - SZSE + name: + type: string + maxLength: 80 + strategy_code: + type: string + enum: + - trend_following + - pullback + - event_driven + title: + type: string + maxLength: 120 + thesis: + type: string + maxLength: 2000 + counter_evidence: + type: string + maxLength: 1000 + invalidation: + type: string + maxLength: 1000 + evidence: + type: array + minItems: 1 + maxItems: 8 + items: + type: object + required: + - source_url + - locator + - summary + properties: + source_url: + type: string + format: uri + description: HTTPS 来源链接,系统当前不自动获取或验证来源内容 + locator: + type: string + maxLength: 200 + summary: + type: string + maxLength: 1000 + reference_plan: + type: object + required: + - entry_low + - entry_high + - stop_loss + - target_price + properties: + entry_low: + type: string + description: 精确元,最多两位小数 + entry_high: + type: string + stop_loss: + type: string + target_price: + type: string + CandidateDraft: + type: object + required: + - content + - observed_at + - valid_until + properties: + content: + $ref: "#/components/schemas/CandidateContent" + observed_at: + type: string + format: date-time + description: 最近 24 小时,不能超过允许的时钟误差 + valid_until: + type: string + format: date-time + description: 未来最长 7 天 + CandidateRevisionInput: + allOf: + - $ref: "#/components/schemas/CandidateDraft" + - type: object + required: + - expected_version + properties: + expected_version: + type: integer + format: int64 + minimum: 1 + PlanPreviewInput: + type: object + additionalProperties: false + required: + - account_id + - candidate_revision_id + - expected_candidate_version + - quantity + - fee_budget + properties: + account_id: + type: string + format: uuid + candidate_revision_id: + type: string + format: uuid + expected_candidate_version: + type: integer + format: int64 + minimum: 1 + quantity: + type: string + pattern: ^[1-9][0-9]{0,6}$ + description: 模拟规则为 100 股整数倍,上限 1000000 + fee_budget: + type: string + description: 用户输入的预计费用,允许 0 + FillInput: + type: object + additionalProperties: false + required: + - account_id + - idempotency_key + - side + - symbol + - exchange + - name + - quantity + - price + - fee + - occurred_at + properties: + account_id: + type: string + format: uuid + plan_id: + type: string + format: uuid + nullable: true + description: 仅买入,属于相同账户;候选失效不阻止记录已发生事实,需填写偏离说明 + idempotency_key: + type: string + format: uuid + side: + type: string + enum: + - BUY + - SELL + symbol: + type: string + pattern: ^[0-9]{6}$ + exchange: + type: string + enum: + - SSE + - SZSE + name: + type: string + maxLength: 80 + quantity: + type: string + description: 正整数字符串,上限 1000000;模拟买入须为 100 的整数倍 + price: + type: string + description: 正数元,最多两位小数 + fee: + type: string + description: 实际录入费用,允许 0 + occurred_at: + type: string + format: date-time + sellable_at: + type: string + format: date-time + nullable: true + description: 买入必填的模拟可卖时间,不早于成交;卖出须为空 + note: + type: string + maxLength: 1000 + ReversalInput: + type: object + additionalProperties: false + required: + - reason + properties: + reason: + type: string + minLength: 1 + maxLength: 1000 + SimulationQuoteInput: + type: object + additionalProperties: false + required: + - symbol + - exchange + - price + - observed_at + properties: + symbol: + type: string + pattern: ^[0-9]{6}$ + exchange: + type: string + enum: + - SSE + - SZSE + price: + type: string + description: 精确正数元,最多两位小数 + observed_at: + type: string + format: date-time + description: 最近 1 分钟,不接受倒序快照 + StrategyInput: + type: object + required: + - code + - name + - description + - parameters + properties: + code: *a2 + name: *a2 + description: *a2 + expected_version: &a3 + type: integer + parameters: + type: object + required: + - short_window + - long_window + - fee_bps + - slippage_bps + - lot_size + properties: + short_window: *a3 + long_window: *a3 + fee_bps: *a3 + slippage_bps: *a3 + lot_size: *a3 + StrategyDeployment: + type: object + required: + - status + - expected_version + - reason + properties: + version_id: *a1 + status: + type: string + enum: + - ACTIVE + - PAUSED + expected_version: *a3 + reason: *a2 + HistoricalBar: + type: object + required: + - date + - open + - high + - low + - close + properties: + date: + type: string + format: date + open: *a2 + high: *a2 + low: *a2 + close: *a2 + BacktestInput: + type: object + required: + - strategy_version_id + - dataset_name + - symbol + - initial_cash + - bars + properties: + strategy_version_id: *a1 + dataset_name: *a2 + symbol: *a2 + initial_cash: *a2 + bars: + type: array + minItems: 4 + maxItems: 1000 + items: + $ref: "#/components/schemas/HistoricalBar" + MarketBacktestInput: + type: object + required: + - strategy_version_id + - symbol + - exchange + - start + - end + - initial_cash + properties: + strategy_version_id: *a1 + symbol: *a2 + exchange: + type: string + enum: + - SSE + - SZSE + start: + type: string + format: date + end: + type: string + format: date + initial_cash: *a2 + ErrorEnvelope: + type: object + properties: + error: + type: object + properties: + code: *a2 + message: *a2 + request_id: *a2 diff --git a/apps/mobile/.env.example b/apps/mobile/.env.example new file mode 100644 index 0000000..c9c3eed --- /dev/null +++ b/apps/mobile/.env.example @@ -0,0 +1,4 @@ +# 仅公开 API 地址,禁止在 EXPO_PUBLIC_* 中存放数据库/Redis/供应商凭据。 +# iOS 模拟器可用 http://127.0.0.1:8080,Android 模拟器使用 http://10.0.2.2:8080。 +# 真机填写开发电脑的内网 IP,后端 HTTP_ADDR 需允许局域网访问。 +EXPO_PUBLIC_API_URL= diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore new file mode 100644 index 0000000..5873d9a --- /dev/null +++ b/apps/mobile/.gitignore @@ -0,0 +1,6 @@ + +# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb +# The following patterns were generated by expo-cli + +expo-env.d.ts +# @end expo-cli \ No newline at end of file diff --git a/apps/mobile/README.md b/apps/mobile/README.md new file mode 100644 index 0000000..04a42a7 --- /dev/null +++ b/apps/mobile/README.md @@ -0,0 +1,37 @@ +# 见势 · Veyra App + +React Native + Expo + TypeScript,白底、石墨黑与柔紫。源码路由采用 Expo Router,服务端查询使用 TanStack Query,当前账户使用 Zustand。 + +当前已接通开发登录、原生安全会话恢复/过期/退出、账户查询、创建和切换。五个主导航与提醒入口已建立;候选、计划、成交、持仓明细、通知、偏好与复盘业务尚未开放,页面明确展示实际状态,不展示原型股票或预设收益。 + +## 开发运行 + +在仓库根目录执行 `npm --prefix apps/mobile ci`,使用 Node 22.18 或更高版本(测试使用 Node 的 TypeScript 支持)。Go 后端初始化完成后运行 `go run . -command user-create -role USER` 创建 App 的 USER 测试身份,再运行 `go run .`。管理员身份无法登录 App,登录信息从本地环境配置获取,不打包到客户端。 + +```sh +cp apps/mobile/.env.example apps/mobile/.env +npm --prefix apps/mobile run start +``` + +填写 `apps/mobile/.env` 中 `EXPO_PUBLIC_API_URL`: + +- iOS 模拟器:`http://127.0.0.1:8080`。 +- Android 默认模拟器:`http://10.0.2.2:8080`。 +- 真机:开发电脑的内网 IP,例如 `http://<开发电脑内网IP>:8080`,后端 `HTTP_ADDR` 需绑定可访问的监听地址;与数据库地址无关。 + +Expo 的 `i` / `a` 可打开已经安装的 iOS / Android 模拟器,也可用兼容 SDK 的 Expo Go 扫码运行。地址变化后重启 Metro。接口地址留空时会明确提示未配置。正式环境应配置 HTTPS;当前开发密码接口只在 local/test 后端开放。 + +`EXPO_PUBLIC_*` 会进入客户端包,只允许公开 API 地址,禁止放入数据库、Redis、模型或行情源密钥。供应商由管理员在后台配置,App 不直接调用供应商。 + +## 验证与产物 + +```sh +npm --prefix apps/mobile run typecheck && npm --prefix apps/mobile run test +npm --prefix apps/mobile run build +``` + +类型检查与测试命令 检查 TypeScript 与 API/金额边界测试;构建命令 导出 iOS 和 Android Metro/Hermes bundle 到本目录 `dist`,验证路由和原生依赖可打包。导出包不等于 APK、IPA,也不代表已完成真机或商店验证;后续原生包需通过开发构建或 EAS/本地编译生成。 + +依赖按 [Expo SDK 配套版本](https://docs.expo.dev/versions/v55.0.0/)选取,原生模块通过 `expo install` 安装。当前锁定 SDK 55 / React Native 0.83.10 / React 19.2.4。管理端与 App 统一使用 React 19.2.4,分别独立安装依赖。SDK 55 默认建议 React 19.2.0;这里显式固定同 minor 的补丁版本并按 [Expo 版本比较配置](https://docs.expo.dev/versions/latest/config/package-json/#installexclude)配置 expo.install.exclude,仅排除 React 的建议版本比较。React Native 0.83.10 的公开 peer 约束是 ^19.2.0;继续执行类型、客户端、双平台 bundle 与依赖重复检查,真机验证仍待完成。App 的 package.json、锁文件和依赖全部位于本目录,Metro 与 Babel 使用本目录的 Expo Router。 + +会话使用 [Expo SecureStore](https://docs.expo.dev/versions/v55.0.0/sdk/securestore/),不会写进 Zustand、URL 或日志。退出撤销服务端会话;网络中断保留原始会话供恢复,401 或到期清理身份与查询缓存。旧会话的迟到响应不会污染新登录用户。切换账户会中断旧查询、清理账户详情缓存;金额使用十进制字符串与整数校验,不做浮点账本计算。 diff --git a/apps/mobile/app.json b/apps/mobile/app.json new file mode 100644 index 0000000..7423545 --- /dev/null +++ b/apps/mobile/app.json @@ -0,0 +1,10 @@ +{ + "expo":{ + "name":"见势 · Veyra","slug":"veyra","scheme":"veyra","version":"0.1.0","orientation":"portrait","userInterfaceStyle":"light", + "platforms":["ios","android"], + "ios":{"supportsTablet":true,"bundleIdentifier":"cn.sundynix.veyra"}, + "android":{"package":"cn.sundynix.veyra"}, + "plugins":["expo-router","expo-secure-store"], + "experiments":{"typedRoutes":true} + } +} diff --git a/apps/mobile/app/(tabs)/_layout.tsx b/apps/mobile/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..6a690cc --- /dev/null +++ b/apps/mobile/app/(tabs)/_layout.tsx @@ -0,0 +1,12 @@ +import {Tabs} from "expo-router" +import Ionicons from "@expo/vector-icons/Ionicons" +import {colors} from "../../src/components/ui" +export default function TabLayout(){ + return + }}/> + }}/> + }}/> + }}/> + }}/> + +} diff --git a/apps/mobile/app/(tabs)/index.tsx b/apps/mobile/app/(tabs)/index.tsx new file mode 100644 index 0000000..9fd7e84 --- /dev/null +++ b/apps/mobile/app/(tabs)/index.tsx @@ -0,0 +1,6 @@ +import {router} from "expo-router" +import {Text,View} from "react-native" +import {AccountSummary} from "../../src/components/AccountSummary" +import {Body,Button,Panel,Screen,Title,styles} from "../../src/components/ui" +import {useSession} from "../../src/state/session" +export default function Home(){const {session}=useSession();return 见势 <Text style={{fontSize:18,fontWeight:"400"}}>· Veyra</Text>)} -
开发环境
第一阶段 · 工程与服务基础
+
开发环境
模拟业务流程已接通
工作空间 {page}
{user.display_name}LOCAL · 开发环境
{logoutError &&

{logoutError}

} -

VEYRA / {overview ? "SYSTEM OVERVIEW" : "WORKSPACE"}

{page === "总览" ? "把每一步,建立在依据之上。" : page}

{overview ? "查看服务连接与建设进度,从可靠的基础开始。" : "该工作区的业务功能正在建设中。"}

{overview && }
+

VEYRA / {overview ? "SYSTEM OVERVIEW" : "WORKSPACE"}

{page === "总览" ? "把每一步,建立在依据之上。" : page}

{overview ? "查看服务连接与建设进度,从可靠的基础开始。" : page === "模型配置" || page === "系统设置" ? "配置供应商连接信息,按需启用。" : page === "审计日志" ? "查看后台配置的操作记录。" : page === "通知任务" ? "查看持久化事件与站内通知处理状态。" : page === "候选审核" ? "录入证据、审核修订并发布模拟候选。" : page === "策略中心" ? "管理研究参数与版本历史。" : page === "回测研究" ? "提交历史数据,查看可追溯的 Python 回测结果。" : "该工作区的业务功能正在建设中。"}

{overview && }
{overview ? <> {error &&
无法读取当前服务状态。请检查 API 是否启动。{error}
}
{[ @@ -53,9 +61,9 @@ export default function App({user,onLogout,onSessionExpired}:{user:User;onLogout {title:"PostgreSQL",subtitle:"业务事实与版本化迁移",icon:Database,value:loading ? "检查中" : health ? label[health.components.postgresql.status] : "未知"}, {title:"Redis",subtitle:"缓存与订阅索引",icon:Activity,value:loading ? "检查中" : health ? label[health.components.redis.status] : "未知"}, ].map(({title,subtitle,icon:Icon,value})=>
{title}

{value}

{subtitle}

)}
-
第一条产品闭环建设中
先验证完整流程,再扩大策略与数据范围。
    {["管理端审核并发布观察候选","用户查看证据,确认买入计划","条件满足后发送提醒","记录实际成交,建立持仓监控","记录退出成交,生成成本后复盘"].map((v,i)=>
  1. 0{i+1}{v}待实现
  2. )}
数据接入后,开始观察。候选数量、策略表现与账户指标将在业务功能与数据接入完成后展示。
当前状态来源:Go API 实时探测
最近检查:{health ? new Date(health.checked_at).toLocaleString("zh-CN") : "尚未取得状态"}
- : page==="用户管理" ? :

{page}尚未开放

该模块完成业务接口、权限与验证后开放。

} -
见势 · Veyra / 可追溯的投资决策工程基础 v0.1
+
第一条产品闭环模拟流程已接通
先验证完整流程,再扩大策略与数据范围。
    {["管理端审核并发布观察候选","用户查看证据,确认买入计划","条件满足后发送提醒","记录实际成交,建立持仓监控","记录退出成交,生成成本后复盘"].map((v,i)=>
  1. 0{i+1}{v}已接通模拟流程
  2. )}
数据接入后,开始观察。候选数量、策略表现与账户指标将在业务功能与数据接入完成后展示。
当前状态来源:Go API 实时探测
最近检查:{health ? new Date(health.checked_at).toLocaleString("zh-CN") : "尚未取得状态"}
+ {page==="数据健康"&&<>} : page==="候选审核" ? : page==="回测研究" ? : page==="策略中心" ? : page==="通知任务" ? : page==="用户管理" ? : page==="模型配置" ? : page==="系统设置" ? : page==="审计日志" ? :

{page}尚未开放

该模块完成业务接口、权限与验证后开放。

} +
见势 · Veyra / 可追溯的投资决策模拟业务闭环 v0.8
diff --git a/web/admin/src/components/AuditPanel.tsx b/web/admin/src/components/AuditPanel.tsx new file mode 100644 index 0000000..a3bb22d --- /dev/null +++ b/web/admin/src/components/AuditPanel.tsx @@ -0,0 +1,12 @@ +import {useEffect,useState} from "react" +import {ApiError} from "@/lib/auth" +import {getAuditEvents,type AuditEvent} from "@/lib/providers" +import {Card,CardContent} from "@/components/ui/card" +export function AuditPanel({onSessionExpired}:{onSessionExpired:()=>void}){ + const [events,setEvents]=useState(null) + const [error,setError]=useState("") + useEffect(()=>{const controller=new AbortController();getAuditEvents(controller.signal).then(setEvents).catch(e=>{if(controller.signal.aborted)return;if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"日志读取失败")});return()=>controller.abort()},[onSessionExpired]) + if(error)return

{error}

+ if(!events)return

正在读取日志…

+ return

最近 100 条操作记录。密钥内容不写入日志。

{events.length===0?

暂无操作记录

:{events.map(event=>)}
时间操作操作者对象记录详情
{new Date(event.created_at).toLocaleString("zh-CN")}{event.action==="PROVIDER_CONFIG_SAVED"?"保存供应商配置":event.action}{event.actor_id}{event.object_type}{event.object_id}
{JSON.stringify(event.details,null,2)}
}
+} diff --git a/web/admin/src/components/BacktestsPanel.tsx b/web/admin/src/components/BacktestsPanel.tsx new file mode 100644 index 0000000..b534b83 --- /dev/null +++ b/web/admin/src/components/BacktestsPanel.tsx @@ -0,0 +1,20 @@ +import {useEffect,useState} from "react" +import {ApiError,request} from "@/lib/auth" +import {Button} from "@/components/ui/button" +import {Card,CardContent,CardHeader,CardTitle} from "@/components/ui/card" +import type {Strategy,StrategyDetail} from "./StrategiesPanel" +type Result={engine_version:string;final_equity:string;cash:string;remaining_quantity:string;total_return_bps:number;max_drawdown_bps:number;trades:{date:string;side:string;quantity:string;price:string;fee:string;realized_pnl:string|null}[];equity_curve:{date:string;equity:string}[]} +type Run={id:string;strategy_version_id:string;status:string;input_hash:string;failure:string;created_at:string;finished_at:string|null;result:Result|null;input?:{dataset_name:string;symbol:string;initial_cash:string;parameters:unknown;bars:unknown[]}} +const labels:Record={PENDING:"排队中",RUNNING:"执行中",COMPLETED:"已完成",FAILED:"失败"} +const field="mt-2 w-full rounded-lg border bg-background px-3 py-2 text-sm" +export function BacktestsPanel({onSessionExpired}:{onSessionExpired:()=>void}){ + const [strategies,setStrategies]=useState([]),[strategy,setStrategy]=useState(null),[version,setVersion]=useState(""),[list,setList]=useState([]),[detail,setDetail]=useState(null),[error,setError]=useState(""),[busy,setBusy]=useState(false),[form,setForm]=useState({dataset_name:"",symbol:"",initial_cash:"",csv:"",exchange:"SSE",start:"",end:""}),[mode,setMode]=useState("market") + function fail(e:unknown){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"请求失败")} + useEffect(()=>{const c=new AbortController();request("/api/admin/v1/strategies",{signal:c.signal}).then(setStrategies).catch(e=>{if(!c.signal.aborted)fail(e)});return()=>c.abort()},[]) + useEffect(()=>{const c=new AbortController();let running=false;async function load(){if(running)return;running=true;try{const runs=await request("/api/admin/v1/backtests",{signal:c.signal});if(!c.signal.aborted)setList(runs);if(detail&&(detail.status==="PENDING"||detail.status==="RUNNING")){const r=await request(`/api/admin/v1/backtests/${detail.id}`,{signal:c.signal});if(!c.signal.aborted)setDetail(r)}}catch(e){if(!c.signal.aborted)fail(e)}finally{running=false}}void load();const timer=setInterval(()=>void load(),3000);return()=>{c.abort();clearInterval(timer)}},[detail?.id,detail?.status]) + async function select(id:string){setVersion("");setStrategy(null);if(!id)return;try{const s=await request(`/api/admin/v1/strategies/${id}`);setStrategy(s)}catch(e){fail(e)}} + async function submit(){setBusy(true);setError("");try{if(mode==="market"){const result=await request("/api/admin/v1/backtests/from-market",{method:"POST",body:JSON.stringify({strategy_version_id:version,symbol:form.symbol,exchange:form.exchange,start:form.start,end:form.end,initial_cash:form.initial_cash})});setDetail(result);setList(await request("/api/admin/v1/backtests"));return} const lines=form.csv.trim().split(/\r?\n/).filter(v=>v.trim());if(lines[0]?.trim()!=="date,open,high,low,close")throw new Error("CSV 首行须为 date,open,high,low,close。");const bars=lines.slice(1).map((line,index)=>{const v=line.split(",").map(s=>s.trim());if(v.length!==5)throw new Error(`CSV 第 ${index+2} 行应有五列。`);return{date:v[0],open:v[1],high:v[2],low:v[3],close:v[4]}});const result=await request("/api/admin/v1/backtests",{method:"POST",body:JSON.stringify({strategy_version_id:version,dataset_name:form.dataset_name,symbol:form.symbol,initial_cash:form.initial_cash,bars})});setDetail(result);setList(await request("/api/admin/v1/backtests"))}catch(e){fail(e)}finally{setBusy(false)}} + async function open(id:string){setError("");try{setDetail(await request(`/api/admin/v1/backtests/${id}`))}catch(e){fail(e)}} + const result=detail?.result + return

自动获取所选股票的日线行情,执行均线交叉历史模拟。也保留手工 CSV 用于研究复验。信号使用之前已完成的日线,次日开盘模拟成交;计入费率与滑点。数据来源、复权、停牌及涨跌停须由输入数据说明,当前引擎没有模拟这些成交限制,结果不能代表可实现收益。

{error&&

{error}

}创建研究任务
{e.preventDefault();void submit()}} className="space-y-4">
{([["dataset_name","数据来源与处理说明"],["symbol","六位股票代码"],["initial_cash","初始模拟现金(元)"]] as const).map(([key,label])=>)}
{mode==="market"?
{(["start","end"] as const).map(k=>)}
: