refactor: 移除异动雷达 — 数据源盘中几乎无记录,功能不达预期

- 删除 /radar 页面与导航、看板「异动雷达」卡片、fetchUnusual 封装
- 移除 /mac/unusual HTTP 端点(MAC 协议层 get_unusual 与 mock 保留,
  属协议覆盖面);路线图 ③ 标记取消
This commit is contained in:
Justin Gu
2026-09-05 04:42:53 +08:00
parent 0a205ffbf3
commit 7f0f14532e
7 changed files with 11 additions and 403 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
| ⑨ | **市场宽度分时** `/sentiment` | 指数新高但上涨家数背离的顶部信号 | 依赖 ① 的采样器 | ✅ 第三批(随①) | | ⑨ | **市场宽度分时** `/sentiment` | 指数新高但上涨家数背离的顶部信号 | 依赖 ① 的采样器 | ✅ 第三批(随①) |
| ⑥ | **板块资金日历** | 哪天钱涌向了哪个板块 | board summary 主力净额逐日采样 | ⏳ 后续批次 | | ⑥ | **板块资金日历** | 哪天钱涌向了哪个板块 | board summary 主力净额逐日采样 | ⏳ 后续批次 |
| ⑤ | **板块相关性热力图** `/hotspots` | 哪些板块同涨同跌(抱团 vs 分散) | 热点滚动已缓存的 60 日涨跌矩阵求两两相关 | ✅ 第四批(热点滚动页内「相关性」视图) | | ⑤ | **板块相关性热力图** `/hotspots` | 哪些板块同涨同跌(抱团 vs 分散) | 热点滚动已缓存的 60 日涨跌矩阵求两两相关 | ✅ 第四批(热点滚动页内「相关性」视图) |
| ③ | **异动雷达时间线** `/radar` | 异动密度骤增 = 盘面转折点 | `/mac/unusual` 现成,纯前端 | ✅ 第四批 | | ③ | ~~异动雷达时间线~~ | ~~异动密度骤增 = 盘面转折点~~ | `/mac/unusual` 盘中数据源质量差(盘中几乎无记录),**整项取消**(页面/导航/端点已移除) | ❌ 已取消 |
| ⑧ | **量能仪表盘** `/sentiment` | 放量/缩量(两市累计成交 vs 5日同期均值) | 指数 5 分钟线现成 | ✅ 第四批(并入市场情绪页) | | ⑧ | **量能仪表盘** `/sentiment` | 放量/缩量(两市累计成交 vs 5日同期均值) | 指数 5 分钟线现成 | ✅ 第四批(并入市场情绪页) |
| ⑩ | **AI 盘面早报/复盘** | 把以上所有数据"自动读"给你听 | LLM 管道 + ai-history 归档现成 | ⏳ 收尾(必须做) | | ⑩ | **AI 盘面早报/复盘** | 把以上所有数据"自动读"给你听 | LLM 管道 + ai-history 归档现成 | ⏳ 收尾(必须做) |
-12
View File
@@ -55,15 +55,3 @@ async def auction(
"""获取集合竞价数据。""" """获取集合竞价数据。"""
df = await client.get_auction(market=market_value_from_str(market), code=code) df = await client.get_auction(market=market_value_from_str(market), code=code)
return _df_resp(df) return _df_resp(df)
@router.get("/mac/unusual", response_model=DataFrameResponse)
async def unusual(
market: str = Query(..., description="市场: SZ, SH"),
start: int = Query(0, ge=0, description="分页起始位置"),
count: int = Query(50, ge=1, le=500, description="返回数量"),
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取市场异动行情数据。"""
df = await client.get_unusual(market=market_value_from_str(market), start=start, count=count)
return _df_resp(df)
-1
View File
@@ -32,7 +32,6 @@ const sseLabel: Record<string, string> = {
<RouterLink to="/calendar" active-class="active">大盘日历</RouterLink> <RouterLink to="/calendar" active-class="active">大盘日历</RouterLink>
<RouterLink to="/limitup" active-class="active">涨停生态</RouterLink> <RouterLink to="/limitup" active-class="active">涨停生态</RouterLink>
<RouterLink to="/sentiment" active-class="active">市场情绪</RouterLink> <RouterLink to="/sentiment" active-class="active">市场情绪</RouterLink>
<RouterLink to="/radar" active-class="active">异动雷达</RouterLink>
<RouterLink to="/watchlist" active-class="active">自选行情</RouterLink> <RouterLink to="/watchlist" active-class="active">自选行情</RouterLink>
<RouterLink to="/ccpm" active-class="active">期货持仓排名</RouterLink> <RouterLink to="/ccpm" active-class="active">期货持仓排名</RouterLink>
<div class="nav-group">分析</div> <div class="nav-group">分析</div>
+10 -9
View File
@@ -6,6 +6,7 @@ import type {
BacktestRequest, BacktestRequest,
BacktestResult, BacktestResult,
Bar, Bar,
BoardFundDay,
BoardOverviewResp, BoardOverviewResp,
BoardRow, BoardRow,
Category, Category,
@@ -610,15 +611,6 @@ export async function fetchBoardHotspot(
return body.data return body.data
} }
/** 市场异动流(火箭发射/大笔买入/封涨停板/打开跌停板/快速反弹等)。 */
export async function fetchUnusual(market: 'SH' | 'SZ', count = 50): Promise<Record<string, unknown>[]> {
const params = new URLSearchParams({ market, count: String(count) })
const resp = await fetch(`${BASE}/mac/unusual?${params}`)
if (!resp.ok) await throwError(resp)
const body = (await resp.json()) as DataFrameResponse
return body.data
}
/** 排行榜(MAC 排行行情)。 /** 排行榜(MAC 排行行情)。
* MAC 协议价格列是 close(无 price/change_pct),这里统一归一化为 * MAC 协议价格列是 close(无 price/change_pct),这里统一归一化为
* price/change_pct,渲染端不再做多候选探测。sortBy 对应后端 SortType。 */ * price/change_pct,渲染端不再做多候选探测。sortBy 对应后端 SortType。 */
@@ -865,6 +857,15 @@ export async function fetchHotspotCorrelation(
return body.data return body.data
} }
/** 行业主力净流入逐日排行(FundFlowSampler 每交易日 14:45 后采样,需积累)。 */
export async function fetchBoardFundHistory(days = 15): Promise<BoardFundDay[]> {
const params = new URLSearchParams({ days: String(days) })
const resp = await fetch(`${BASE}/market/board-fund/history?${params}`)
if (!resp.ok) await throwError(resp)
const body = (await resp.json()) as { data: { count: number; days: BoardFundDay[] } }
return body.data.days
}
/** 当日情绪分钟曲线(采样器逐分钟落库;date=0 表示尚无采样)。 */ /** 当日情绪分钟曲线(采样器逐分钟落库;date=0 表示尚无采样)。 */
export async function fetchSentimentToday(): Promise<SentimentTodayResp> { export async function fetchSentimentToday(): Promise<SentimentTodayResp> {
const resp = await fetch(`${BASE}/market/sentiment/today`) const resp = await fetch(`${BASE}/market/sentiment/today`)
-3
View File
@@ -12,7 +12,6 @@ import LlmHistoryView from './views/LlmHistoryView.vue'
import LlmSettingsView from './views/LlmSettingsView.vue' import LlmSettingsView from './views/LlmSettingsView.vue'
import OptimizeView from './views/OptimizeView.vue' import OptimizeView from './views/OptimizeView.vue'
import PortfolioView from './views/PortfolioView.vue' import PortfolioView from './views/PortfolioView.vue'
import RadarView from './views/RadarView.vue'
import SentimentView from './views/SentimentView.vue' import SentimentView from './views/SentimentView.vue'
import ServerSettingsView from './views/ServerSettingsView.vue' import ServerSettingsView from './views/ServerSettingsView.vue'
import SignalRadarView from './views/SignalRadarView.vue' import SignalRadarView from './views/SignalRadarView.vue'
@@ -39,8 +38,6 @@ const routes = [
{ path: '/limitup', name: 'limitup', component: LimitUpView }, { path: '/limitup', name: 'limitup', component: LimitUpView },
// 市场情绪(宽度分时 + 涨停温度计;采样器盘中逐分钟积累) // 市场情绪(宽度分时 + 涨停温度计;采样器盘中逐分钟积累)
{ path: '/sentiment', name: 'sentiment', component: SentimentView }, { path: '/sentiment', name: 'sentiment', component: SentimentView },
// 异动雷达(沪深异动流时间线:封板/炸板/大笔买入…)
{ path: '/radar', name: 'radar', component: RadarView },
{ path: '/backtest', name: 'backtest', component: BacktestView }, { path: '/backtest', name: 'backtest', component: BacktestView },
{ path: '/portfolio', name: 'portfolio', component: PortfolioView }, { path: '/portfolio', name: 'portfolio', component: PortfolioView },
{ path: '/optimize', name: 'optimize', component: OptimizeView }, { path: '/optimize', name: 'optimize', component: OptimizeView },
-71
View File
@@ -10,7 +10,6 @@ import {
fetchIndexBars, fetchIndexBars,
fetchMarketStat, fetchMarketStat,
fetchRankList, fetchRankList,
fetchUnusual,
fetchMinute, fetchMinute,
formatError, formatError,
} from '../api' } from '../api'
@@ -310,43 +309,6 @@ function barWidth(pct: number, list: BoardRow[]): string {
return `${Math.min(100, (Math.abs(pct) / maxAbs) * 100)}%` return `${Math.min(100, (Math.abs(pct) / maxAbs) * 100)}%`
} }
// ── 异动事件流(轮询 60s) ───────────────────────────────────────────────────
interface UnusualRow {
time: string
code: string
name: string
desc: string
value: string
market: string
}
const unusualRows = ref<UnusualRow[]>([])
const unusualError = ref('')
function normalizeUnusual(rows: Record<string, unknown>[], market: 'SH' | 'SZ'): UnusualRow[] {
return rows.map((r) => ({
time: String(r.time ?? '').slice(0, 8),
code: String(r.code ?? ''),
name: String(r.name ?? ''),
desc: String(r.desc ?? ''),
value: String(r.value ?? ''),
market,
}))
}
async function loadUnusual() {
unusualError.value = ''
try {
const [sh, sz] = await Promise.all([fetchUnusual('SH', 60), fetchUnusual('SZ', 60)])
const merged = [...normalizeUnusual(sh, 'SH'), ...normalizeUnusual(sz, 'SZ')]
merged.sort((a, b) => (a.time < b.time ? 1 : a.time > b.time ? -1 : 0))
unusualRows.value = merged.slice(0, 80)
} catch (e) {
unusualError.value = formatError(e)
}
}
// ── 排行榜(涨幅/跌幅/成交额/换手 四 tab,轮询 60s) ─────────────────────── // ── 排行榜(涨幅/跌幅/成交额/换手 四 tab,轮询 60s) ───────────────────────
const gainers = ref<RankRow[]>([]) const gainers = ref<RankRow[]>([])
@@ -442,7 +404,6 @@ const sessionLabel = computed(() => {
function refreshAll() { function refreshAll() {
loadStat() loadStat()
loadBoards() loadBoards()
loadUnusual()
loadRanks() loadRanks()
loadIdxSparks() loadIdxSparks()
loadIndexContext() loadIndexContext()
@@ -458,7 +419,6 @@ function tickIfActive() {
function slowTickIfActive() { function slowTickIfActive() {
if (autoPaused.value) return if (autoPaused.value) return
loadUnusual()
loadRanks() loadRanks()
loadBoards() loadBoards()
loadIdxSparks() loadIdxSparks()
@@ -731,29 +691,6 @@ function openBoard(code: string | undefined, name: string | undefined) {
</div> </div>
</div> </div>
<!-- 异动事件流 -->
<div class="card unusual-card">
<h3>异动雷达</h3>
<div v-if="unusualError" class="err">{{ unusualError }}</div>
<table v-else class="qtable">
<thead>
<tr>
<th>时间</th>
<th style="text-align: left">股票</th>
<th style="text-align: left">异动</th>
<th>数值</th>
</tr>
</thead>
<tbody>
<tr v-for="(r, i) in unusualRows" :key="i" @click="openDialog(r.code, r.name, r.market)">
<td class="dim">{{ r.time }}</td>
<td style="text-align: left">{{ r.name }}</td>
<td style="text-align: left"><span class="tag">{{ r.desc }}</span></td>
<td class="mono">{{ r.value }}</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
<StockDialog <StockDialog
@@ -873,10 +810,6 @@ function openBoard(code: string | undefined, name: string | undefined) {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 10px; gap: 10px;
} }
.unusual-card {
grid-column: 1 / -1;
max-height: 320px;
}
.dist-card { .dist-card {
min-height: 220px; min-height: 220px;
} }
@@ -1078,10 +1011,6 @@ function openBoard(code: string | undefined, name: string | undefined) {
} }
/* 涨跌幅榜 */ /* 涨跌幅榜 */
.unusual-card tr,
.card table tr {
cursor: pointer;
}
/* 异动 */ /* 异动 */
.tag { .tag {
-306
View File
@@ -1,306 +0,0 @@
<script setup lang="ts">
// 异动雷达(/radar):沪深异动流时间线(封板/炸板/大笔买入/火箭发射…)
// + 每分钟异动密度柱。异动流为交易所最近交易日的盘中记录;15s 轮询、
// 交易时段外仅手动刷新;类型筛选 chips;单击行直达个股弹窗。
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import echarts from '../echarts-setup'
import { fetchUnusual, formatError } from '../api'
import StockDialog from '../components/StockDialog.vue'
type UnusualRow = {
index: number
market: number
code: string
name: string
time: string
desc: string
value: string
unusual_type: number
mkt: string
}
const rows = ref<UnusualRow[]>([])
const loading = ref(false)
const error = ref('')
const lastRefresh = ref('')
const activeType = ref('全部')
let timer = 0
async function load() {
loading.value = rows.value.length === 0
error.value = ''
try {
const [sh, sz] = await Promise.all([fetchUnusual('SH', 300), fetchUnusual('SZ', 300)])
rows.value = [...sh, ...sz]
.map((r) => ({ ...(r as unknown as UnusualRow), mkt: Number(r.market) === 1 ? 'SH' : 'SZ' }))
.sort((a, b) => b.time.localeCompare(a.time))
lastRefresh.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
renderDensity()
} catch (e) {
error.value = formatError(e)
} finally {
loading.value = false
}
}
const types = computed(() => {
const count = new Map<string, number>()
for (const r of rows.value) count.set(r.desc, (count.get(r.desc) ?? 0) + 1)
return ['全部', ...[...count.entries()].sort((a, b) => b[1] - a[1]).map(([t]) => t)]
})
const filtered = computed(() =>
activeType.value === '全部' ? rows.value : rows.value.filter((r) => r.desc === activeType.value),
)
function typeCount(t: string): number {
return t === '全部' ? rows.value.length : (rows.value.filter((r) => r.desc === t).length ?? 0)
}
// ── 密度图(每分钟异动条数) ─────────────────────────────────────────────────
const densityEl = ref<HTMLDivElement>()
let densityChart: echarts.ECharts | null = null
function renderDensity() {
if (!densityEl.value) return
const perMinute = new Map<string, number>()
for (const r of rows.value) {
const m = r.time.slice(0, 5)
perMinute.set(m, (perMinute.get(m) ?? 0) + 1)
}
const x = [...perMinute.keys()].sort()
const y = x.map((k) => perMinute.get(k) ?? 0)
densityChart ??= echarts.init(densityEl.value, 'dark')
densityChart.setOption(
{
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
grid: { left: 40, top: 8, bottom: 24, right: 10 },
xAxis: { type: 'category', data: x, axisLabel: { fontSize: 9 } },
yAxis: { type: 'value', name: '条/分', nameTextStyle: { fontSize: 9 } },
series: [
{
type: 'bar',
data: y,
itemStyle: { color: '#f5a623' },
barMaxWidth: 6,
},
],
},
true,
)
}
function onResize() {
densityChart?.resize()
}
function tick() {
const now = new Date()
const day = now.getDay()
if (day === 0 || day === 6 || document.hidden) return
const m = now.getHours() * 60 + now.getMinutes()
if ((m >= 555 && m <= 690) || (m >= 780 && m <= 905)) load()
}
const stockDlg = ref<{ market: string; code: string; name: string } | null>(null)
function openStock(r: UnusualRow) {
stockDlg.value = { market: r.mkt, code: r.code, name: r.name }
}
onMounted(() => {
load()
timer = window.setInterval(tick, 15_000)
window.addEventListener('resize', onResize)
})
onBeforeUnmount(() => {
window.clearInterval(timer)
window.removeEventListener('resize', onResize)
densityChart?.dispose()
})
</script>
<template>
<div class="radar-view">
<div class="view-head">
<h2>异动雷达</h2>
<span class="dim head-sub">沪深异动流 · 最近交易日盘中记录</span>
<span class="tb-spacer"></span>
<span v-if="lastRefresh" class="dim refresh-ts">{{ lastRefresh }}</span>
<button class="manual-refresh" @click="load"> 刷新</button>
</div>
<div v-if="error" class="err card">
加载失败{{ error }}
<button @click="load">重试</button>
</div>
<div v-else-if="loading" class="loading">异动流加载中</div>
<template v-else>
<div class="card chart-card">
<div ref="densityEl" class="density-chart"></div>
</div>
<div class="chips">
<button
v-for="t in types"
:key="t"
class="chip-s"
:class="{ on: activeType === t }"
@click="activeType = t"
>
{{ t }} <span class="mono dim">{{ typeCount(t) }}</span>
</button>
</div>
<div class="card list-card">
<div v-for="r in filtered" :key="`${r.mkt}${r.code}${r.time}${r.index}`" class="u-row" @click="openStock(r)">
<span class="mono u-time">{{ r.time }}</span>
<span class="u-name">{{ r.name }}</span>
<span class="mono dim u-code">{{ r.mkt }}·{{ r.code }}</span>
<span class="u-desc" :class="{ hot: r.desc.includes('涨停') || r.desc.includes('买'), cold: r.desc.includes('跌') }">{{ r.desc }}</span>
<span class="mono dim u-val">{{ r.value }}</span>
</div>
<div v-if="filtered.length === 0" class="empty dim">暂无对应类型的异动记录</div>
</div>
</template>
<StockDialog
v-if="stockDlg"
:market="stockDlg.market"
:code="stockDlg.code"
:name="stockDlg.name"
@close="stockDlg = null"
/>
</div>
</template>
<style scoped>
.radar-view {
height: 100%;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.view-head,
.err,
.loading,
.chart-card,
.chips {
flex-shrink: 0;
}
.view-head {
display: flex;
align-items: center;
gap: 8px;
}
.view-head h2 {
font-size: 17px;
font-weight: 700;
}
.head-sub {
font-size: 12px;
}
.tb-spacer {
flex: 1;
}
.refresh-ts {
font-family: var(--font-mono);
font-size: 11.5px;
}
.manual-refresh {
font-size: 12px;
padding: 4px 10px;
}
.err {
color: var(--up);
display: flex;
align-items: center;
gap: 10px;
}
.loading {
padding: 40px 0;
text-align: center;
color: var(--text-dim);
}
.chart-card {
padding: 6px;
}
.density-chart {
height: 110px;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.chip-s {
padding: 3px 10px;
font-size: 11.5px;
border-radius: 999px;
background: var(--bg-elevated);
border: 1px solid var(--border);
cursor: pointer;
}
.chip-s.on {
border-color: var(--accent);
color: var(--accent);
}
.list-card {
padding: 4px 0;
}
.u-row {
display: flex;
align-items: center;
gap: 12px;
padding: 5px 14px;
font-size: 12.5px;
border-bottom: 1px solid var(--border);
cursor: pointer;
}
.u-row:last-child {
border-bottom: none;
}
.u-row:hover {
background: var(--bg-elevated);
}
.u-time {
font-size: 11.5px;
color: var(--text-dim);
width: 62px;
}
.u-name {
font-weight: 600;
width: 110px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.u-code {
font-size: 11px;
width: 100px;
}
.u-desc {
flex: 1;
}
.u-desc.hot {
color: var(--up);
}
.u-desc.cold {
color: var(--down);
}
.u-val {
font-size: 11px;
}
.empty {
padding: 24px 0;
text-align: center;
font-size: 12px;
}
</style>