Compare commits

...

3 Commits

Author SHA1 Message Date
fish
d217628ccf 文档同步:补充同步数据、数据重置功能说明
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 23:27:45 +08:00
fish
cff4319321 新增手动同步数据功能:点击侧边栏"同步数据"调用批量打分接口,完成后跳转打分列表并刷新
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 23:26:04 +08:00
fish
a7e1c9f416 重置成功后自动跳转打分列表并刷新页面
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 23:22:33 +08:00
6 changed files with 58 additions and 2 deletions

View File

@@ -45,7 +45,7 @@ docker-compose -f docker-compose.trade.yml exec postgres psql -U trade -d future
**单进程串行流水线**:`src.main.main()` 先按命令行参数(显式 `ts_code` 优先,否则 `contracts.active_contract(symbol)` 按当月主力自动选)定下合约,再调 `run()` 顺序执行 `fetcher → storage(candles) → scorer → storage(scores)`。无后台任务、无队列,每次 CLI 调用处理一个合约一日。 **单进程串行流水线**:`src.main.main()` 先按命令行参数(显式 `ts_code` 优先,否则 `contracts.active_contract(symbol)` 按当月主力自动选)定下合约,再调 `run()` 顺序执行 `fetcher → storage(candles) → scorer → storage(scores)`。无后台任务、无队列,每次 CLI 调用处理一个合约一日。
**FastAPI 服务**(`src.api`):容器默认以 `uvicorn src.api:app` 启动,暴露 `/api/v1/run`(触发流水线)、`/api/v1/run/batch`(批量打分)、`/api/v1/scores``/api/v1/scores/{id}``/api/v1/contracts``/api/v1/candles` 等端点。启动时自动 `storage.init_db()` 建表。API 与 CLI 共用同一套 `fetcher/storage/scorer` 逻辑。 **FastAPI 服务**(`src.api`):容器默认以 `uvicorn src.api:app` 启动,暴露 `/api/v1/run`(触发流水线)、`/api/v1/run/batch`(批量打分)、`/api/v1/scores``/api/v1/scores/{id}``/api/v1/contracts``/api/v1/candles``/api/v1/admin/reset-data`(清空行情数据) 等端点。启动时自动 `storage.init_db()` 建表。API 与 CLI 共用同一套 `fetcher/storage/scorer` 逻辑。
**主力轮换规则**(`contracts.py`):每个品种在 `ROLLOVER_RULES` 中维护 `month -> (主力月, 年份偏移)` 表。FG 当前规则:1-3/12 月→05、4-7 月→09、8-11 月→01,其中 8-11 月与 12 月跨年(`year_offset=1`)。新增品种(如 RB、I)只需在该 dict 里加一条,无需改 main 流程。 **主力轮换规则**(`contracts.py`):每个品种在 `ROLLOVER_RULES` 中维护 `month -> (主力月, 年份偏移)` 表。FG 当前规则:1-3/12 月→05、4-7 月→09、8-11 月→01,其中 8-11 月与 12 月跨年(`year_offset=1`)。新增品种(如 RB、I)只需在该 dict 里加一条,无需改 main 流程。
@@ -71,6 +71,7 @@ docker-compose -f docker-compose.trade.yml exec postgres psql -U trade -d future
- 统一 DB:业务数据与用户鉴权数据均存储在 PostgreSQL `futures` 数据库中,通过 `DATABASE_URL` 访问。`auth.db`(SQLite)已废弃,`users` 表现在由 `AuthStore` 直接管理在 PostgreSQL 中。 - 统一 DB:业务数据与用户鉴权数据均存储在 PostgreSQL `futures` 数据库中,通过 `DATABASE_URL` 访问。`auth.db`(SQLite)已废弃,`users` 表现在由 `AuthStore` 直接管理在 PostgreSQL 中。
- 鉴权已简化:登录接口返回固定 token,`middleware.RequireUser` 直接注入默认管理员上下文,所有请求放行。后端仍保留密码校验与角色检查(`RequireAdmin`)。前端把 token 持久化到 `localStorage`,axios 拦截器统一注入 + 401 自动跳登录。 - 鉴权已简化:登录接口返回固定 token,`middleware.RequireUser` 直接注入默认管理员上下文,所有请求放行。后端仍保留密码校验与角色检查(`RequireAdmin`)。前端把 token 持久化到 `localStorage`,axios 拦截器统一注入 + 401 自动跳登录。
- 前端支持暗/浅色模式切换(`stores/theme.ts`),侧边导航在暗色模式用 `#282828`、浅色模式用 `#f9fafb` - 前端支持暗/浅色模式切换(`stores/theme.ts`),侧边导航在暗色模式用 `#282828`、浅色模式用 `#f9fafb`
- 侧边栏提供「同步数据」(批量打分)、「数据重置」(管理员清空行情数据)功能。
**禁止公开注册**:登录后管理员才能在 `/admin/users` 维护账号。首次启动时 `auth.Bootstrap` 检查 `users` 表,若没有 admin 行,则自动创建默认管理员 `admin` / `admin`,并标记强制首次登录后改密码。忘记管理员密码的恢复方式:停服 → 清理 PostgreSQL 中的 admin 记录 → 重启。 **禁止公开注册**:登录后管理员才能在 `/admin/users` 维护账号。首次启动时 `auth.Bootstrap` 检查 `users` 表,若没有 admin 行,则自动创建默认管理员 `admin` / `admin`,并标记强制首次登录后改密码。忘记管理员密码的恢复方式:停服 → 清理 PostgreSQL 中的 admin 记录 → 重启。

View File

@@ -49,6 +49,9 @@ curl "http://localhost:4001/api/v1/contracts"
# 查询 K 线数据 # 查询 K 线数据
curl "http://localhost:4001/api/v1/candles?ts_code=FG2609.ZCE" curl "http://localhost:4001/api/v1/candles?ts_code=FG2609.ZCE"
# 清空所有行情数据(谨慎操作)
curl -X POST http://localhost:4001/api/v1/admin/reset-data
``` ```
### 5. 跑其他合约或品种 ### 5. 跑其他合约或品种
@@ -254,6 +257,9 @@ docker-compose -f docker-compose.trade.yml logs -f web
- **打分列表** `/scores`:按合约、日期、条数筛选,展示综合分/信号/三层得分;点击「明细」弹抽屉,显示短期 7 日逐日打分(涨跌幅/OI变化%/量比/象限)、中期(15d)价格收益与资金意愿、长期(30d)OI/价格趋势分与波动率调整。 - **打分列表** `/scores`:按合约、日期、条数筛选,展示综合分/信号/三层得分;点击「明细」弹抽屉,显示短期 7 日逐日打分(涨跌幅/OI变化%/量比/象限)、中期(15d)价格收益与资金意愿、长期(30d)OI/价格趋势分与波动率调整。
- **K 线 / 持仓** `/chart`:选合约 + 日期区间,主图蜡烛(开高低收),副图持仓量曲线;鼠标拖选缩放。 - **K 线 / 持仓** `/chart`:选合约 + 日期区间,主图蜡烛(开高低收),副图持仓量曲线;鼠标拖选缩放。
- **同步数据**(侧边栏):点击调用批量打分接口,对所有固定品种执行当日打分,完成后自动跳转并刷新打分列表。
- **手动打分** `/run`:选品种 + 日期,对单个合约执行数据拉取与打分。
- **数据重置**(侧边栏,仅管理员):输入确认文字后清空所有行情数据candles + scores用户表不受影响。
- **用户管理** `/admin/users`:仅管理员可见。可创建子账号(`user` 默认,亦可建 `admin`)、重置密码、禁用/启用、删除;不允许对自己执行禁用或删除。 - **用户管理** `/admin/users`:仅管理员可见。可创建子账号(`user` 默认,亦可建 `admin`)、重置密码、禁用/启用、删除;不允许对自己执行禁用或删除。
普通用户登录后 `/admin/*` 路径会被前端守卫拦截并跳回 `/scores`,后端也会以 403 拒绝。 普通用户登录后 `/admin/*` 路径会被前端守卫拦截并跳回 `/scores`,后端也会以 403 拒绝。

View File

@@ -42,6 +42,20 @@ func (d *Deps) RunPipeline(w http.ResponseWriter, r *http.Request) {
_, _ = io.Copy(w, resp.Body) _, _ = io.Copy(w, resp.Body)
} }
func (d *Deps) RunBatch(w http.ResponseWriter, r *http.Request) {
client := &http.Client{Timeout: 120 * time.Second}
resp, err := client.Post(d.TushareURL+"/api/v1/run/batch", "application/json", nil)
if err != nil {
writeErr(w, http.StatusBadGateway, fmt.Sprintf("tushare service unavailable: %v", err))
return
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
_, _ = io.Copy(w, resp.Body)
}
func (d *Deps) GetActiveContract(w http.ResponseWriter, r *http.Request) { func (d *Deps) GetActiveContract(w http.ResponseWriter, r *http.Request) {
symbol := r.URL.Query().Get("symbol") symbol := r.URL.Query().Get("symbol")
if symbol == "" { if symbol == "" {

View File

@@ -31,6 +31,7 @@ func New(d *handlers.Deps, dist fs.FS) http.Handler {
r.Get("/contracts/active", d.GetActiveContract) r.Get("/contracts/active", d.GetActiveContract)
r.Get("/candles", d.ListCandles) r.Get("/candles", d.ListCandles)
r.Post("/run", d.RunPipeline) r.Post("/run", d.RunPipeline)
r.Post("/run/batch", d.RunBatch)
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(mw.RequireAdmin) r.Use(mw.RequireAdmin)

View File

@@ -6,6 +6,7 @@ import { useAuthStore } from '@/stores/auth'
import { useThemeStore } from '@/stores/theme' import { useThemeStore } from '@/stores/theme'
import { useMobile } from '@/composables/useMobile' import { useMobile } from '@/composables/useMobile'
import { resetAllData } from '@/api/admin' import { resetAllData } from '@/api/admin'
import { runBatch } from '@/api/run'
const auth = useAuthStore() const auth = useAuthStore()
const theme = useThemeStore() const theme = useThemeStore()
@@ -15,6 +16,7 @@ const { isMobile } = useMobile()
const drawerOpen = ref(false) const drawerOpen = ref(false)
const resetting = ref(false) const resetting = ref(false)
const syncing = ref(false)
const showLayout = computed(() => route.meta.layout !== 'blank' && !!auth.token) const showLayout = computed(() => route.meta.layout !== 'blank' && !!auth.token)
@@ -46,12 +48,34 @@ async function handleReset() {
resetting.value = true resetting.value = true
await resetAllData() await resetAllData()
ElMessage.success('已清空所有行情数据') ElMessage.success('已清空所有行情数据')
if (route.path === '/scores') {
router.go(0)
} else {
router.push('/scores')
}
} catch { } catch {
// user cancelled // user cancelled
} finally { } finally {
resetting.value = false resetting.value = false
} }
} }
async function handleSync() {
syncing.value = true
try {
await runBatch()
ElMessage.success('同步完成')
if (route.path === '/scores') {
router.go(0)
} else {
router.push('/scores')
}
} catch {
ElMessage.error('同步失败')
} finally {
syncing.value = false
}
}
</script> </script>
<template> <template>
@@ -68,6 +92,9 @@ async function handleReset() {
> >
<el-menu-item index="/scores">打分列表</el-menu-item> <el-menu-item index="/scores">打分列表</el-menu-item>
<el-menu-item index="/chart">K 线 / 持仓</el-menu-item> <el-menu-item index="/chart">K 线 / 持仓</el-menu-item>
<el-menu-item :index="() => {}" @click="handleSync" :disabled="syncing">
同步数据
</el-menu-item>
<el-menu-item index="/run">手动打分</el-menu-item> <el-menu-item index="/run">手动打分</el-menu-item>
<el-menu-item v-if="auth.isAdmin" index="/admin/users">用户管理</el-menu-item> <el-menu-item v-if="auth.isAdmin" index="/admin/users">用户管理</el-menu-item>
<el-menu-item v-if="auth.isAdmin" :index="() => {}" @click="handleReset" :disabled="resetting"> <el-menu-item v-if="auth.isAdmin" :index="() => {}" @click="handleReset" :disabled="resetting">
@@ -95,7 +122,10 @@ async function handleReset() {
> >
<el-menu-item index="/scores">打分列表</el-menu-item> <el-menu-item index="/scores">打分列表</el-menu-item>
<el-menu-item index="/chart">K 线 / 持仓</el-menu-item> <el-menu-item index="/chart">K 线 / 持仓</el-menu-item>
<el-menu-item index="/run">手动打分</el-menu-item> <el-menu-item :index="() => {}" @click="handleSync" :disabled="syncing">
同步数据
</el-menu-item>
<el-menu-item index="/run">手动打分</el-menu-item>
<el-menu-item v-if="auth.isAdmin" index="/admin/users">用户管理</el-menu-item> <el-menu-item v-if="auth.isAdmin" index="/admin/users">用户管理</el-menu-item>
</el-menu> </el-menu>
</el-aside> </el-aside>

View File

@@ -30,6 +30,10 @@ export function runPipeline(req: RunRequest) {
return client.post<RunResponse>('/run', req).then((r) => r.data) return client.post<RunResponse>('/run', req).then((r) => r.data)
} }
export function runBatch() {
return client.post('/run/batch').then((r) => r.data)
}
export function getActiveContract(symbol: string) { export function getActiveContract(symbol: string) {
return client return client
.get<ActiveContract>('/contracts/active', { params: { symbol } }) .get<ActiveContract>('/contracts/active', { params: { symbol } })