Compare commits
2 Commits
76ddc495c1
...
fa5fa07ef6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa5fa07ef6 | ||
|
|
7b6732488a |
@@ -49,7 +49,7 @@ docker-compose -f docker-compose.trade.yml exec postgres psql -U trade -d future
|
|||||||
|
|
||||||
**主力轮换规则**(`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 流程。
|
||||||
|
|
||||||
**三层打分模型**(`scorer.py`):综合 = 短期(7日,0.4) + 中期(15日,0.35) + 长期(30日,0.25)。`score_daily()` 要求 DataFrame ≥31 行,`fetcher.fetch_contract` 默认拉一个合约的全历史(实际 100+ 行),按 `trade_date` 升序排列后供打分使用。打分结果通过 `dataclass ScoreResult` + `ScoreDetail` 流转,`storage.save_score` 把 detail 序列化为 `detail_json` 文本列。
|
**三层打分模型**(`scorer.py`):综合 = (短期(7日,0.4) + 中期(15日,0.35) + 长期(30日,0.25)) × 波动率惩罚系数。短期引入幅度因子(价格/OI变化率)和量能确认,按象限(增仓涨/跌、减仓涨/跌、持平)给基础分再叠加加成;中期资金意愿连续化(50 + (增仓涨-增仓跌)/15×50);长期加入30日价格趋势分与OI趋势分 4:6 合成;高波动品种综合分打 85-100 折。`score_daily()` 要求 DataFrame ≥31 行,`fetcher.fetch_contract` 默认拉一个合约的全历史(实际 100+ 行),按 `trade_date` 升序排列后供打分使用。打分结果通过 `dataclass ScoreResult` + `ScoreDetail` 流转,`storage.save_score` 把 detail 序列化为 `detail_json` 文本列。
|
||||||
|
|
||||||
**PostgreSQL 作为业务数据库**:docker-compose 编排 `postgres:18.3-alpine3.23`,`tushare` 与 `web` 服务均通过 `DATABASE_URL` 连接。`storage.py` 使用 `psycopg3` 驱动,`candles` 与 `scores` 表以 `ON CONFLICT (ts_code, trade_date) DO UPDATE` 实现幂等,可反复重跑同一天。`scores` 表主键为 `UUID DEFAULT uuidv7() PRIMARY KEY`(见 `models.py` 与 `storage.py`)。
|
**PostgreSQL 作为业务数据库**:docker-compose 编排 `postgres:18.3-alpine3.23`,`tushare` 与 `web` 服务均通过 `DATABASE_URL` 连接。`storage.py` 使用 `psycopg3` 驱动,`candles` 与 `scores` 表以 `ON CONFLICT (ts_code, trade_date) DO UPDATE` 实现幂等,可反复重跑同一天。`scores` 表主键为 `UUID DEFAULT uuidv7() PRIMARY KEY`(见 `models.py` 与 `storage.py`)。
|
||||||
|
|
||||||
|
|||||||
61
README.md
61
README.md
@@ -76,32 +76,39 @@ docker-compose -f docker-compose.trade.yml run --rm tushare python -m src.main -
|
|||||||
### 综合分数公式
|
### 综合分数公式
|
||||||
|
|
||||||
```
|
```
|
||||||
综合分数 = (短期动力 × 0.4) + (中期趋势 × 0.35) + (长期结构 × 0.25)
|
综合分数 = (短期动力 × 0.4 + 中期趋势 × 0.35 + 长期结构 × 0.25) × 波动率惩罚系数
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1. 短期动力(7 日窗口,权重 0.4)
|
### 1. 短期动力(7 日窗口,权重 0.4)
|
||||||
|
|
||||||
逐日打分后取均值:
|
逐日打分后取均值。每日评分 = (象限基础分 + 幅度加成) × 量能确认,产出 0-100 连续值。
|
||||||
|
|
||||||
| 持仓变化 | 价格方向 | 得分 |
|
**象限基础分**(持仓与价格方向):
|
||||||
|---------|---------|------|
|
|
||||||
| 增仓 | 上涨 | 100(多头主动进攻) |
|
| 象限 | 持仓变化 | 价格方向 | 基础分 |
|
||||||
| 增仓 | 下跌 | 0(空头主动进攻) |
|
|------|---------|---------|--------|
|
||||||
| 减仓 | 上涨 | 70(空头撤退) |
|
| accumulation(增仓上涨) | 增仓 | 上涨 | 75 |
|
||||||
| 减仓 | 下跌 | 30(多头撤退) |
|
| distribution(增仓下跌) | 增仓 | 下跌 | 25 |
|
||||||
| 持平(\|变化\|<1%) | 上涨 | 60 |
|
| covering(减仓上涨) | 减仓 | 上涨 | 65 |
|
||||||
| 持平(\|变化\|<1%) | 下跌 | 40 |
|
| liquidation(减仓下跌) | 减仓 | 下跌 | 20 |
|
||||||
|
| flat(持平) | \|变化\|<1% | 上涨 | 60 |
|
||||||
|
| flat(持平) | \|变化\|<1% | 下跌 | 40 |
|
||||||
|
|
||||||
|
**幅度加成**(根据 OI 变化率和涨跌幅放大有利方向得分):
|
||||||
|
- OI 变化率封顶 5%,价格涨跌幅封顶 3%
|
||||||
|
- 增仓上涨 / 减仓下跌(有利方向):加成 = (OI 幅度 + 价格幅度) / 2 × 20
|
||||||
|
- 持仓持平:加成 = 价格幅度 × 10
|
||||||
|
- 增仓下跌 / 减仓上涨(不利方向):无加成
|
||||||
|
|
||||||
|
**量能确认**:`量比 = 当日成交量 / 7 日均量`,系数范围 [0.9, 1.2],量比 1.5 以上封顶
|
||||||
|
|
||||||
### 2. 中期趋势(15 日窗口,权重 0.35)
|
### 2. 中期趋势(15 日窗口,权重 0.35)
|
||||||
|
|
||||||
```
|
```
|
||||||
价格信号 = (今收 - 15日前收) / 15日前收
|
价格收益率 = (今收 - 15日前收) / 15日前收
|
||||||
价格信号得分 = clamp(50 + 收益率×500, 0, 100)
|
价格信号分 = clamp(50 + 收益率 × 500, 0, 100)
|
||||||
|
|
||||||
资金意愿:
|
资金意愿 = 50 + (增仓上涨天数 - 增仓下跌天数) / 15 × 50 (连续值 0-100)
|
||||||
增仓上涨天数 > 增仓下跌天数 → 80
|
|
||||||
两者相当 → 50
|
|
||||||
增仓下跌天数 > 增仓上涨天数 → 20
|
|
||||||
|
|
||||||
模块得分 = 价格信号 × 0.6 + 资金意愿 × 0.4
|
模块得分 = 价格信号 × 0.6 + 资金意愿 × 0.4
|
||||||
```
|
```
|
||||||
@@ -109,15 +116,23 @@ docker-compose -f docker-compose.trade.yml run --rm tushare python -m src.main -
|
|||||||
### 3. 长期结构(30 日窗口,权重 0.25)
|
### 3. 长期结构(30 日窗口,权重 0.25)
|
||||||
|
|
||||||
```
|
```
|
||||||
持仓变化幅度 = (30日日均持仓 - 30日前持仓) / 30日前持仓
|
OI 趋势分 = clamp(50 + OI变化幅度 × 250, 0, 100) (权重 60%)
|
||||||
|
价格趋势分 = clamp(50 + 30日价格收益率 × 200, 0, 100) (权重 40%)
|
||||||
|
|
||||||
> 10% → 90(显著增仓)
|
模块得分 = OI 趋势分 × 0.6 + 价格趋势分 × 0.4
|
||||||
5%~10% → 70(温和增仓)
|
|
||||||
-5%~5% → 50(基本持平)
|
|
||||||
-10%~-5% → 30(温和减仓)
|
|
||||||
< -10% → 10(显著减仓)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 4. 波动率调整
|
||||||
|
|
||||||
|
基于近 30 日日收益率标准差和 ATR%(平均真实波幅/均价):
|
||||||
|
|
||||||
|
```
|
||||||
|
日波动率 ≤ 1.5% → 惩罚系数 = 1.0(无惩罚)
|
||||||
|
日波动率 > 1.5% → 惩罚系数 = max(0.85, 1.0 - (日波动率 - 1.5%) × 10)
|
||||||
|
```
|
||||||
|
|
||||||
|
高波动品种的综合分会被适当打折,最低打 85 折。
|
||||||
|
|
||||||
### 信号解读
|
### 信号解读
|
||||||
|
|
||||||
| 综合分数 | 信号 |
|
| 综合分数 | 信号 |
|
||||||
@@ -237,7 +252,7 @@ docker-compose -f docker-compose.trade.yml logs -f web
|
|||||||
|
|
||||||
### 3. 页面说明
|
### 3. 页面说明
|
||||||
|
|
||||||
- **打分列表** `/scores`:按合约、日期、条数筛选,展示综合分/信号/三层得分;点击「明细」弹抽屉,显示短期 7 日逐日打分、中期(15d)价格收益与资金意愿、长期(30d)持仓变化。
|
- **打分列表** `/scores`:按合约、日期、条数筛选,展示综合分/信号/三层得分;点击「明细」弹抽屉,显示短期 7 日逐日打分(涨跌幅/OI变化%/量比/象限)、中期(15d)价格收益与资金意愿、长期(30d)OI/价格趋势分与波动率调整。
|
||||||
- **K 线 / 持仓** `/chart`:选合约 + 日期区间,主图蜡烛(开高低收),副图持仓量曲线;鼠标拖选缩放。
|
- **K 线 / 持仓** `/chart`:选合约 + 日期区间,主图蜡烛(开高低收),副图持仓量曲线;鼠标拖选缩放。
|
||||||
- **用户管理** `/admin/users`:仅管理员可见。可创建子账号(`user` 默认,亦可建 `admin`)、重置密码、禁用/启用、删除;不允许对自己执行禁用或删除。
|
- **用户管理** `/admin/users`:仅管理员可见。可创建子账号(`user` 默认,亦可建 `admin`)、重置密码、禁用/启用、删除;不允许对自己执行禁用或删除。
|
||||||
|
|
||||||
|
|||||||
@@ -193,3 +193,10 @@ def list_candles(
|
|||||||
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/admin/reset-data")
|
||||||
|
def reset_data():
|
||||||
|
"""清空所有行情数据(仅限管理员调用)。"""
|
||||||
|
storage.truncate_all()
|
||||||
|
return {"status": "ok", "message": "已清空所有行情数据"}
|
||||||
|
|||||||
@@ -152,3 +152,14 @@ def get_latest_score(ts_code: str, db_url: str = DEFAULT_DB_URL) -> Optional[dic
|
|||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def truncate_all(db_url: str = DEFAULT_DB_URL):
|
||||||
|
"""清空所有行情数据(candles + scores),保留用户表。"""
|
||||||
|
conn = _get_conn(db_url)
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("TRUNCATE TABLE candles, scores")
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|||||||
22
web/backend/internal/handlers/reset.go
Normal file
22
web/backend/internal/handlers/reset.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Deps) AdminResetData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Post(d.TushareURL+"/api/v1/admin/reset-data", "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)
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ func New(d *handlers.Deps, dist fs.FS) http.Handler {
|
|||||||
r.Post("/admin/users", d.AdminCreateUser)
|
r.Post("/admin/users", d.AdminCreateUser)
|
||||||
r.Patch("/admin/users/{id}", d.AdminPatchUser)
|
r.Patch("/admin/users/{id}", d.AdminPatchUser)
|
||||||
r.Delete("/admin/users/{id}", d.AdminDeleteUser)
|
r.Delete("/admin/users/{id}", d.AdminDeleteUser)
|
||||||
|
r.Post("/admin/reset-data", d.AdminResetData)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
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'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const theme = useThemeStore()
|
const theme = useThemeStore()
|
||||||
@@ -12,6 +14,7 @@ const route = useRoute()
|
|||||||
const { isMobile } = useMobile()
|
const { isMobile } = useMobile()
|
||||||
|
|
||||||
const drawerOpen = ref(false)
|
const drawerOpen = ref(false)
|
||||||
|
const resetting = ref(false)
|
||||||
|
|
||||||
const showLayout = computed(() => route.meta.layout !== 'blank' && !!auth.token)
|
const showLayout = computed(() => route.meta.layout !== 'blank' && !!auth.token)
|
||||||
|
|
||||||
@@ -29,6 +32,26 @@ function logout() {
|
|||||||
function closeDrawer() {
|
function closeDrawer() {
|
||||||
drawerOpen.value = false
|
drawerOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleReset() {
|
||||||
|
closeDrawer()
|
||||||
|
try {
|
||||||
|
await ElMessageBox.prompt('此操作将清空所有行情数据(candles + scores),不可恢复。请输入"确认重置"后继续:', '数据重置', {
|
||||||
|
confirmButtonText: '确认重置',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputPattern: /^确认重置$/,
|
||||||
|
inputErrorMessage: '请输入"确认重置"',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
resetting.value = true
|
||||||
|
await resetAllData()
|
||||||
|
ElMessage.success('已清空所有行情数据')
|
||||||
|
} catch {
|
||||||
|
// user cancelled
|
||||||
|
} finally {
|
||||||
|
resetting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -47,6 +70,9 @@ function closeDrawer() {
|
|||||||
<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="/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>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
|
|||||||
5
web/frontend/src/api/admin.ts
Normal file
5
web/frontend/src/api/admin.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import client from './client'
|
||||||
|
|
||||||
|
export function resetAllData() {
|
||||||
|
return client.post('/admin/reset-data').then((r) => r.data)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user