diff --git a/docs/market-insights-roadmap.md b/docs/market-insights-roadmap.md index 5a6d5f0..49b3d01 100644 --- a/docs/market-insights-roadmap.md +++ b/docs/market-insights-roadmap.md @@ -12,7 +12,7 @@ | ⑨ | **市场宽度分时** `/sentiment` | 指数新高但上涨家数背离的顶部信号 | 依赖 ① 的采样器 | ✅ 第三批(随①) | | ⑥ | **板块资金日历** | 哪天钱涌向了哪个板块 | board summary 主力净额逐日采样 | ⏳ 后续批次 | | ⑤ | **板块相关性热力图** `/hotspots` | 哪些板块同涨同跌(抱团 vs 分散) | 热点滚动已缓存的 60 日涨跌矩阵求两两相关 | ✅ 第四批(热点滚动页内「相关性」视图) | -| ③ | **异动雷达时间线** `/radar` | 异动密度骤增 = 盘面转折点 | `/mac/unusual` 现成,纯前端 | ✅ 第四批 | +| ③ | ~~异动雷达时间线~~ | ~~异动密度骤增 = 盘面转折点~~ | `/mac/unusual` 盘中数据源质量差(盘中几乎无记录),**整项取消**(页面/导航/端点已移除) | ❌ 已取消 | | ⑧ | **量能仪表盘** `/sentiment` | 放量/缩量(两市累计成交 vs 5日同期均值) | 指数 5 分钟线现成 | ✅ 第四批(并入市场情绪页) | | ⑩ | **AI 盘面早报/复盘** | 把以上所有数据"自动读"给你听 | LLM 管道 + ai-history 归档现成 | ⏳ 收尾(必须做) | diff --git a/src/easy_tdx/web/routers/mac_quotes.py b/src/easy_tdx/web/routers/mac_quotes.py index cf76be7..1c8cbd2 100644 --- a/src/easy_tdx/web/routers/mac_quotes.py +++ b/src/easy_tdx/web/routers/mac_quotes.py @@ -55,15 +55,3 @@ async def auction( """获取集合竞价数据。""" df = await client.get_auction(market=market_value_from_str(market), code=code) 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) diff --git a/web-ui/src/App.vue b/web-ui/src/App.vue index 3363098..5a87b62 100644 --- a/web-ui/src/App.vue +++ b/web-ui/src/App.vue @@ -32,7 +32,6 @@ const sseLabel: Record = { 大盘日历 涨停生态 市场情绪 - 异动雷达 自选行情 期货持仓排名 diff --git a/web-ui/src/api.ts b/web-ui/src/api.ts index 901d9c2..41a9702 100644 --- a/web-ui/src/api.ts +++ b/web-ui/src/api.ts @@ -6,6 +6,7 @@ import type { BacktestRequest, BacktestResult, Bar, + BoardFundDay, BoardOverviewResp, BoardRow, Category, @@ -610,15 +611,6 @@ export async function fetchBoardHotspot( return body.data } -/** 市场异动流(火箭发射/大笔买入/封涨停板/打开跌停板/快速反弹等)。 */ -export async function fetchUnusual(market: 'SH' | 'SZ', count = 50): Promise[]> { - 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 协议价格列是 close(无 price/change_pct),这里统一归一化为 * price/change_pct,渲染端不再做多候选探测。sortBy 对应后端 SortType。 */ @@ -865,6 +857,15 @@ export async function fetchHotspotCorrelation( return body.data } +/** 行业主力净流入逐日排行(FundFlowSampler 每交易日 14:45 后采样,需积累)。 */ +export async function fetchBoardFundHistory(days = 15): Promise { + 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 表示尚无采样)。 */ export async function fetchSentimentToday(): Promise { const resp = await fetch(`${BASE}/market/sentiment/today`) diff --git a/web-ui/src/router.ts b/web-ui/src/router.ts index fdc6a5a..6ed591f 100644 --- a/web-ui/src/router.ts +++ b/web-ui/src/router.ts @@ -12,7 +12,6 @@ import LlmHistoryView from './views/LlmHistoryView.vue' import LlmSettingsView from './views/LlmSettingsView.vue' import OptimizeView from './views/OptimizeView.vue' import PortfolioView from './views/PortfolioView.vue' -import RadarView from './views/RadarView.vue' import SentimentView from './views/SentimentView.vue' import ServerSettingsView from './views/ServerSettingsView.vue' import SignalRadarView from './views/SignalRadarView.vue' @@ -39,8 +38,6 @@ const routes = [ { path: '/limitup', name: 'limitup', component: LimitUpView }, // 市场情绪(宽度分时 + 涨停温度计;采样器盘中逐分钟积累) { path: '/sentiment', name: 'sentiment', component: SentimentView }, - // 异动雷达(沪深异动流时间线:封板/炸板/大笔买入…) - { path: '/radar', name: 'radar', component: RadarView }, { path: '/backtest', name: 'backtest', component: BacktestView }, { path: '/portfolio', name: 'portfolio', component: PortfolioView }, { path: '/optimize', name: 'optimize', component: OptimizeView }, diff --git a/web-ui/src/views/DashboardView.vue b/web-ui/src/views/DashboardView.vue index 051ec59..91dafb1 100644 --- a/web-ui/src/views/DashboardView.vue +++ b/web-ui/src/views/DashboardView.vue @@ -10,7 +10,6 @@ import { fetchIndexBars, fetchMarketStat, fetchRankList, - fetchUnusual, fetchMinute, formatError, } from '../api' @@ -310,43 +309,6 @@ function barWidth(pct: number, list: BoardRow[]): string { 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([]) -const unusualError = ref('') - -function normalizeUnusual(rows: Record[], 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) ─────────────────────── const gainers = ref([]) @@ -442,7 +404,6 @@ const sessionLabel = computed(() => { function refreshAll() { loadStat() loadBoards() - loadUnusual() loadRanks() loadIdxSparks() loadIndexContext() @@ -458,7 +419,6 @@ function tickIfActive() { function slowTickIfActive() { if (autoPaused.value) return - loadUnusual() loadRanks() loadBoards() loadIdxSparks() @@ -731,29 +691,6 @@ function openBoard(code: string | undefined, name: string | undefined) { - -
-

异动雷达

-
{{ unusualError }}
- - - - - - - - - - - - - - - - - -
时间股票异动数值
{{ r.time }}{{ r.name }}{{ r.desc }}{{ r.value }}
-
-// 异动雷达(/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([]) -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() - 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() -let densityChart: echarts.ECharts | null = null - -function renderDensity() { - if (!densityEl.value) return - const perMinute = new Map() - 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() -}) - - - - -