-
+
+
@@ -25,40 +52,88 @@
diff --git a/web-ui/src/components/IntradayChart.vue b/web-ui/src/components/IntradayChart.vue
new file mode 100644
index 0000000..a08c45a
--- /dev/null
+++ b/web-ui/src/components/IntradayChart.vue
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
diff --git a/web-ui/src/components/MoodRadar.vue b/web-ui/src/components/MoodRadar.vue
new file mode 100644
index 0000000..4b41a18
--- /dev/null
+++ b/web-ui/src/components/MoodRadar.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
diff --git a/web-ui/src/components/Sparkline.vue b/web-ui/src/components/Sparkline.vue
new file mode 100644
index 0000000..f913473
--- /dev/null
+++ b/web-ui/src/components/Sparkline.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
diff --git a/web-ui/src/components/StockDialog.vue b/web-ui/src/components/StockDialog.vue
new file mode 100644
index 0000000..622d74c
--- /dev/null
+++ b/web-ui/src/components/StockDialog.vue
@@ -0,0 +1,520 @@
+
+
+
+
+
+
+
+
+
+ {{ name || `${market}${code}` }}
+ {{ market }}{{ code }}
+ {{ quote.server_time.slice(0, 8) }}
+
+
+ {{ fmt2(quote?.price) }}
+
+ {{ fmt2((quote?.price ?? 0) - (quote?.pre_close ?? 0)) }}
+ {{ fmtPctSigned(changePct) }}
+
+
+
+
+
+
+
+
+
+
+
五档盘口
+
+
+
+ | 卖{{ lv.level }} |
+
+ {{ fmt2(lv.price) }}
+ |
+
+
+ {{ lv.vol ? Math.round(lv.vol) : '-' }}
+ |
+
+
+ |
+
+
+ | 买{{ lv.level }} |
+
+ {{ fmt2(lv.price) }}
+ |
+
+
+ {{ lv.vol ? Math.round(lv.vol) : '-' }}
+ |
+
+
+
+
+
量{{ fmtVol(quote.vol) }}
+
额{{ fmtAmount(quote.amount) }}
+
高{{ fmt2(quote.high) }}
+
低{{ fmt2(quote.low) }}
+
开{{ fmt2(quote.open) }}
+
昨收{{ fmt2(quote.pre_close) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
加载中…
+
+
+ 分时:{{ minuteError }}
+
+
+
+ 日K:{{ dailyError }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-ui/src/components/StockKline.vue b/web-ui/src/components/StockKline.vue
new file mode 100644
index 0000000..f2da1d1
--- /dev/null
+++ b/web-ui/src/components/StockKline.vue
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+
diff --git a/web-ui/src/echarts-setup.ts b/web-ui/src/echarts-setup.ts
index e7fea42..1264c9f 100644
--- a/web-ui/src/echarts-setup.ts
+++ b/web-ui/src/echarts-setup.ts
@@ -2,12 +2,14 @@
// 用到的:candlestick(K线)、line(净值/回撤曲线)、markPoint(买卖点标注)、heatmap(寻优热力图)。
import * as echarts from 'echarts/core'
-import { BarChart, CandlestickChart, HeatmapChart, LineChart } from 'echarts/charts'
+import { BarChart, CandlestickChart, HeatmapChart, LineChart, RadarChart } from 'echarts/charts'
import {
DataZoomComponent,
GridComponent,
LegendComponent,
+ MarkLineComponent,
MarkPointComponent,
+ RadarComponent,
TitleComponent,
TooltipComponent,
VisualMapComponent,
@@ -20,12 +22,15 @@ echarts.use([
LineChart,
BarChart,
HeatmapChart,
+ RadarChart,
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent,
DataZoomComponent,
MarkPointComponent,
+ MarkLineComponent,
+ RadarComponent,
VisualMapComponent,
])
diff --git a/web-ui/src/format.ts b/web-ui/src/format.ts
index b41fbbc..9da9363 100644
--- a/web-ui/src/format.ts
+++ b/web-ui/src/format.ts
@@ -11,3 +11,33 @@ export function fmtPct(v: number | null | undefined): string {
if (v === null || v === undefined || !Number.isFinite(v)) return '-'
return `${(v * 100).toFixed(2)}%`
}
+
+/** 成交额/市值大数(元 → 亿/万亿,A股口径)。 */
+export function fmtAmount(v: number | null | undefined): string {
+ if (v === null || v === undefined || !Number.isFinite(v)) return '-'
+ if (Math.abs(v) >= 1e12) return `${(v / 1e12).toFixed(2)}万亿`
+ if (Math.abs(v) >= 1e8) return `${(v / 1e8).toFixed(2)}亿`
+ if (Math.abs(v) >= 1e4) return `${(v / 1e4).toFixed(2)}万`
+ return v.toFixed(0)
+}
+
+/** 成交量(手 → 万手/亿手)。 */
+export function fmtVol(v: number | null | undefined): string {
+ if (v === null || v === undefined || !Number.isFinite(v)) return '-'
+ if (Math.abs(v) >= 1e8) return `${(v / 1e8).toFixed(2)}亿手`
+ if (Math.abs(v) >= 1e4) return `${(v / 1e4).toFixed(2)}万手`
+ return v.toFixed(0)
+}
+
+/** 涨跌幅带号(接受百分数如 2.35 → "+2.35%")。 */
+export function fmtPctSigned(v: number | null | undefined, digits = 2): string {
+ if (v === null || v === undefined || !Number.isFinite(v)) return '-'
+ const s = v.toFixed(digits)
+ return v > 0 ? `+${s}%` : `${s}%`
+}
+
+/** 按涨跌方向取色 class(涨红/跌绿/平灰)。 */
+export function dirClass(v: number | null | undefined): string {
+ if (v === null || v === undefined || !Number.isFinite(v) || v === 0) return 'flat'
+ return v > 0 ? 'up' : 'down'
+}
diff --git a/web-ui/src/indicators.ts b/web-ui/src/indicators.ts
new file mode 100644
index 0000000..4b4e319
--- /dev/null
+++ b/web-ui/src/indicators.ts
@@ -0,0 +1,150 @@
+// 技术指标前端计算库(与后端 MyTT/indicator.py 同口径的常用子集)。
+// 输入均为按时间正序的价格数组,输出与输入等长(前期无法计算的点为 null)。
+
+/** 简单移动平均 MA(n)。 */
+export function ma(data: number[], n: number): Array
{
+ const out: Array = []
+ let sum = 0
+ for (let i = 0; i < data.length; i++) {
+ sum += data[i]
+ if (i >= n) sum -= data[i - n]
+ out.push(i >= n - 1 ? sum / n : null)
+ }
+ return out
+}
+
+/** 指数移动平均 EMA(n),首值用前 n 项均值(无则首个有效值)。 */
+export function ema(data: number[], n: number): Array {
+ const out: Array = []
+ const k = 2 / (n + 1)
+ let prev: number | null = null
+ let seed = 0
+ for (let i = 0; i < data.length; i++) {
+ if (prev === null) {
+ seed += data[i]
+ if (i === n - 1) {
+ prev = seed / n
+ out.push(prev)
+ } else {
+ out.push(null)
+ }
+ } else {
+ prev = data[i] * k + prev * (1 - k)
+ out.push(prev)
+ }
+ }
+ return out
+}
+
+/** 国内 SMA(X, N, M):X*M/N + 前值*(N-M)/N,首值 = 首个 X。 */
+function smaCN(data: number[], n: number, m: number): Array {
+ const out: Array = []
+ let prev: number | null = null
+ for (const v of data) {
+ if (prev === null) {
+ prev = v
+ out.push(v)
+ } else {
+ prev = (v * m) / n + (prev * (n - m)) / n
+ out.push(prev)
+ }
+ }
+ return out
+}
+
+/** 布林带 BOLL(20, 2):中轨 MA20,上下轨 ±2×标准差。 */
+export function boll(
+ data: number[],
+ n = 20,
+ p = 2,
+): { mid: Array; upper: Array; lower: Array } {
+ const mid = ma(data, n)
+ const upper: Array = []
+ const lower: Array = []
+ for (let i = 0; i < data.length; i++) {
+ if (mid[i] === null) {
+ upper.push(null)
+ lower.push(null)
+ continue
+ }
+ let ssum = 0
+ for (let j = i - n + 1; j <= i; j++) ssum += (data[j] - (mid[i] as number)) ** 2
+ const sd = Math.sqrt(ssum / n)
+ upper.push((mid[i] as number) + p * sd)
+ lower.push((mid[i] as number) - p * sd)
+ }
+ return { mid, upper, lower }
+}
+
+/** MACD(12,26,9):DIF、DEA、MACD 柱(= 2×(DIF-DEA),国内口径)。 */
+export function macd(
+ closes: number[],
+ fast = 12,
+ slow = 26,
+ signal = 9,
+): { dif: Array; dea: Array; hist: Array } {
+ const ef = ema(closes, fast)
+ const es = ema(closes, slow)
+ const dif = closes.map((_, i) =>
+ ef[i] !== null && es[i] !== null ? (ef[i] as number) - (es[i] as number) : null,
+ )
+ // DEA = DIF 的 EMA(9):剔除前缀 null 后计算再回填
+ const firstIdx = dif.findIndex((v) => v !== null)
+ const dea: Array = new Array(closes.length).fill(null)
+ if (firstIdx >= 0) {
+ const valid = dif.slice(firstIdx) as number[]
+ const deaValid = ema(valid, signal)
+ for (let i = 0; i < deaValid.length; i++) dea[firstIdx + i] = deaValid[i]
+ }
+ const hist = dif.map((v, i) =>
+ v !== null && dea[i] !== null ? 2 * (v - (dea[i] as number)) : null,
+ )
+ return { dif, dea, hist }
+}
+
+/** KDJ(9,3,3):RSV→K=SMA(RSV,3,1)→D=SMA(K,3,1)→J=3K-2D。 */
+export function kdj(
+ highs: number[],
+ lows: number[],
+ closes: number[],
+ n = 9,
+): { k: Array; d: Array; j: Array } {
+ const rsv: number[] = []
+ for (let i = 0; i < closes.length; i++) {
+ const hi = Math.max(...highs.slice(Math.max(0, i - n + 1), i + 1))
+ const lo = Math.min(...lows.slice(Math.max(0, i - n + 1), i + 1))
+ rsv.push(hi === lo ? 50 : ((closes[i] - lo) / (hi - lo)) * 100)
+ }
+ const k = smaCN(rsv, 3, 1)
+ // smaCN 等长输出;k 无 null(首值即有效),直接算
+ const kk = k as number[]
+ const dd = smaCN(kk, 3, 1) as number[]
+ const j = kk.map((v, i) => 3 * v - 2 * dd[i])
+ return { k: kk, d: dd, j }
+}
+
+/** RSI(n):Wilder 平滑(SMA(U,n,1)/SMA(D,n,1) 国内近似)。 */
+export function rsi(closes: number[], n = 14): Array {
+ const out: Array = []
+ let avgU = 0
+ let avgD = 0
+ for (let i = 0; i < closes.length; i++) {
+ if (i === 0) {
+ out.push(null)
+ continue
+ }
+ const ch = closes[i] - closes[i - 1]
+ const u = Math.max(ch, 0)
+ const d = Math.max(-ch, 0)
+ if (i <= n) {
+ avgU += u / n
+ avgD += d / n
+ out.push(i === n ? (avgD === 0 ? 100 : 100 - (100 * avgU) / (avgU + avgD)) : null)
+ } else {
+ avgU = (avgU * (n - 1) + u) / n
+ avgD = (avgD * (n - 1) + d) / n
+ out.push(avgD === 0 ? 100 : 100 - (100 * avgU) / (avgU + avgD))
+ }
+ }
+ return out
+}
diff --git a/web-ui/src/market.ts b/web-ui/src/market.ts
index 01b20ae..1f84feb 100644
--- a/web-ui/src/market.ts
+++ b/web-ui/src/market.ts
@@ -19,6 +19,9 @@ export function detectMarket(code: string): Market {
const c = code.trim()
if (!/^\d{6}$/.test(c)) return 'SZ'
+ // 通达信板块指数(沪市):880 统计 / 881 行业 / 885 概念等
+ if (/^88[0-9]/.test(c)) return 'SH'
+
// 北交所:43/83/87/92(含920段)/93 + 4xx/8xx(三板/小盘)
if (/^(43|83|87|92|93|4|8)/.test(c)) return 'BJ'
diff --git a/web-ui/src/router.ts b/web-ui/src/router.ts
index 4a12be5..a038336 100644
--- a/web-ui/src/router.ts
+++ b/web-ui/src/router.ts
@@ -2,16 +2,21 @@ import { createRouter, createWebHistory } from 'vue-router'
import BacktestView from './views/BacktestView.vue'
import CompareView from './views/CompareView.vue'
+import DashboardView from './views/DashboardView.vue'
import OptimizeView from './views/OptimizeView.vue'
import PortfolioView from './views/PortfolioView.vue'
import ServerSettingsView from './views/ServerSettingsView.vue'
import SignalRadarView from './views/SignalRadarView.vue'
import StrategiesView from './views/StrategiesView.vue'
+import WatchlistView from './views/WatchlistView.vue'
-// 单标的回测(/)+ 组合回测(/portfolio)+ 参数寻优(/optimize)+ 结果对比(/compare)
-// + 策略库(/strategies)+ 信号雷达(/signals)+ 服务器设置(/settings)。
+// 行情终端:市场看板(/)+ 自选(/watchlist)。
+// 分析工具:单标的回测(/backtest)+ 组合回测(/portfolio)+ 参数寻优(/optimize)
+// + 结果对比(/compare)+ 策略库(/strategies)+ 信号雷达(/signals)+ 设置(/settings)。
const routes = [
- { path: '/', name: 'backtest', component: BacktestView },
+ { path: '/', name: 'dashboard', component: DashboardView },
+ { path: '/watchlist', name: 'watchlist', component: WatchlistView },
+ { path: '/backtest', name: 'backtest', component: BacktestView },
{ path: '/portfolio', name: 'portfolio', component: PortfolioView },
{ path: '/optimize', name: 'optimize', component: OptimizeView },
{ path: '/compare', name: 'compare', component: CompareView },
diff --git a/web-ui/src/stores/quotes.ts b/web-ui/src/stores/quotes.ts
new file mode 100644
index 0000000..84c5c06
--- /dev/null
+++ b/web-ui/src/stores/quotes.ts
@@ -0,0 +1,76 @@
+// 实时行情 SSE 状态(Pinia,全局单连接)。
+// App.vue 挂载时 connect(),所有视图共享同一份 quotes 快照。
+// 断线指数退避重连(上限 30s),状态徽标挂在侧边栏底部。
+
+import { defineStore } from 'pinia'
+import { computed, reactive, ref } from 'vue'
+
+import type { SecurityQuote, SseMessage } from '../types'
+
+export type SseStatus = 'connecting' | 'open' | 'closed'
+
+const STREAM_URL = '/api/v1/stream/quotes'
+const MAX_RETRY_DELAY = 30_000
+
+export const useQuoteStore = defineStore('quotes', () => {
+ /** symbol(SH600000)→ 最新行情快照。 */
+ const quotes = reactive(new Map())
+ const status = ref('closed')
+ const lastTs = ref('')
+ let es: EventSource | null = null
+ let retryTimer: number | null = null
+ let retryCount = 0
+
+ const quoteCount = computed(() => quotes.size)
+
+ function handleMsg(ev: MessageEvent) {
+ try {
+ const msg = JSON.parse(ev.data) as SseMessage
+ if (msg.type === 'quotes_updated' && msg.quotes) {
+ for (const q of msg.quotes) quotes.set(q.symbol, q)
+ if (msg.ts) lastTs.value = msg.ts
+ }
+ status.value = 'open'
+ retryCount = 0
+ } catch {
+ // 非 JSON 帧(服务端注释/心跳),忽略
+ }
+ }
+
+ function connect() {
+ if (es) return
+ status.value = 'connecting'
+ es = new EventSource(STREAM_URL)
+ es.onmessage = handleMsg
+ es.onopen = () => {
+ status.value = 'open'
+ retryCount = 0
+ }
+ es.onerror = () => {
+ // EventSource 断开后进入 readyState=CONNECTING 自行重试;但若服务已停,
+ // 会无限静默重试——这里手动接管:关闭后按退避重连,同时更新状态徽标。
+ close(false)
+ retryCount += 1
+ const delay = Math.min(1000 * 2 ** (retryCount - 1), MAX_RETRY_DELAY)
+ status.value = 'closed'
+ retryTimer = window.setTimeout(connect, delay)
+ }
+ }
+
+ function close(markClosed = true) {
+ if (retryTimer !== null) {
+ window.clearTimeout(retryTimer)
+ retryTimer = null
+ }
+ es?.close()
+ es = null
+ if (markClosed) status.value = 'closed'
+ }
+
+ /** 取单只行情(无数据时返回 undefined)。 */
+ function getQuote(symbol: string): SecurityQuote | undefined {
+ return quotes.get(symbol)
+ }
+
+ return { quotes, status, lastTs, quoteCount, connect, close, getQuote }
+})
diff --git a/web-ui/src/style.css b/web-ui/src/style.css
index c9f8ca9..150669d 100644
--- a/web-ui/src/style.css
+++ b/web-ui/src/style.css
@@ -104,3 +104,59 @@ label {
a {
color: var(--accent);
}
+
+/* 行情涨跌着色(format.dirClass 的落点) */
+.up {
+ color: var(--up);
+}
+.down {
+ color: var(--down);
+}
+.flat {
+ color: var(--text-muted);
+}
+.mono {
+ font-family: var(--font-mono);
+}
+
+/* 通用面板卡片(看板/自选页网格单元) */
+.card {
+ background: var(--bg-panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 12px 14px;
+ overflow: auto;
+}
+.card h3 {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-muted);
+ margin-bottom: 8px;
+}
+
+/* 行情数据表 */
+.qtable {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 12.5px;
+}
+.qtable th {
+ text-align: right;
+ color: var(--text-dim);
+ font-weight: 500;
+ padding: 5px 8px;
+ border-bottom: 1px solid var(--border);
+ white-space: nowrap;
+}
+.qtable td {
+ text-align: right;
+ padding: 4px 8px;
+ border-bottom: 1px solid var(--border);
+ white-space: nowrap;
+ font-family: var(--font-mono);
+}
+.qtable th:first-child,
+.qtable td:first-child {
+ text-align: left;
+ font-family: inherit;
+}
diff --git a/web-ui/src/types.ts b/web-ui/src/types.ts
index 4d5b7e8..7ba3974 100644
--- a/web-ui/src/types.ts
+++ b/web-ui/src/types.ts
@@ -403,3 +403,120 @@ export interface ServerSwitchResult {
host: string
message: string
}
+
+// ── 行情终端:实时五档(SSE / POST /api/v1/security/quotes) ─────────────────
+
+/** 单只标的实时五档行情(后端 SecurityQuote 白名单投影,SSE 与 REST 同构)。 */
+export interface SecurityQuote {
+ symbol: string // "SH600000"
+ market: string // SH/SZ/BJ
+ code: string
+ price: number | null
+ pre_close: number | null
+ open: number | null
+ high: number | null
+ low: number | null
+ vol: number | null // 总成交量(手)
+ cur_vol: number | null
+ amount: number | null // 成交额(元)
+ s_vol: number | null // 内盘
+ b_vol: number | null // 外盘
+ rise_speed: number | null // 涨速
+ limit_up: number | null
+ limit_down: number | null
+ decimal_point: number | null
+ server_time: string
+ trading_status: number | null
+ bid1: number | null
+ bid_vol1: number | null
+ bid2: number | null
+ bid_vol2: number | null
+ bid3: number | null
+ bid_vol3: number | null
+ bid4: number | null
+ bid_vol4: number | null
+ bid5: number | null
+ bid_vol5: number | null
+ ask1: number | null
+ ask_vol1: number | null
+ ask2: number | null
+ ask_vol2: number | null
+ ask3: number | null
+ ask_vol3: number | null
+ ask4: number | null
+ ask_vol4: number | null
+ ask5: number | null
+ ask_vol5: number | null
+}
+
+/** SSE 消息:quotes_updated / hello。 */
+export interface SseMessage {
+ type: 'quotes_updated' | 'hello'
+ ts?: string
+ count?: number
+ quotes?: SecurityQuote[]
+ subscribers?: number
+}
+
+// ── 行情终端:市场统计(GET /api/v1/market/stat) ────────────────────────────
+
+export interface MarketStat {
+ up_count: number
+ down_count: number
+ neutral_count: number
+ suspended_count: number
+ total_count: number
+ total_amount: number
+ total_volume: number
+ total_market_cap: number
+ limit_up_count: number
+ limit_down_count: number
+}
+
+// ── 行情终端:分时(GET /api/v1/minute) ─────────────────────────────────────
+
+export interface MinutePoint {
+ datetime: string
+ price: number
+ vol: number
+}
+
+// ── 行情终端:自选(GET/POST/DELETE /api/v1/watchlist) ──────────────────────
+
+export interface WatchItem {
+ market: string
+ code: string
+ symbol: string // SH600000
+ name: string
+ group_name: string
+ created_at: string
+ sort_order: number
+}
+
+export interface WatchlistResponse {
+ items: WatchItem[]
+ count: number
+}
+
+// ── 行情终端:板块列表(GET /api/v1/board-mac/list,MAC 协议,防御式取列) ────
+
+/** 板块行(MAC 协议字段随版本浮动,全部可选,渲染端容错)。 */
+export interface BoardRow {
+ code?: string
+ name?: string
+ price?: number
+ pre_close?: number
+ change_pct?: number
+ sort_value?: number
+ [key: string]: unknown
+}
+
+// ── 行情终端:排行行情(GET /api/v1/mac/quote-list,MAC 协议,防御式取列) ────
+
+export interface RankRow {
+ code?: string
+ name?: string
+ price?: number
+ change_pct?: number
+ [key: string]: unknown
+}
diff --git a/web-ui/src/views/DashboardView.vue b/web-ui/src/views/DashboardView.vue
new file mode 100644
index 0000000..33c7ce3
--- /dev/null
+++ b/web-ui/src/views/DashboardView.vue
@@ -0,0 +1,995 @@
+
+
+
+
+
+
+
+
+ {{ idx.name }}
+
+ {{ fmtPctSigned(idxPct(idx.symbol)) }}
+
+
+
+
+ {{ idxQuote(idx.symbol)?.price?.toFixed(2) ?? '—' }}
+
+
+ {{ fmtAmount(idxQuote(idx.symbol)?.amount) }}
+
+
+
+
+
+
+
+
+
+
市场统计
+
{{ statError }}
+
+
+
+ 涨 {{ stat.up_count }}
+ 平 {{ stat.neutral_count }}
+ 跌 {{ stat.down_count }}
+ 停 {{ stat.suspended_count }}
+
+
+
涨停{{ stat.limit_up_count }}
+
跌停{{ stat.limit_down_count }}
+
总成交{{ fmtAmount(stat.total_amount) }}
+
总市值{{ fmtAmount(stat.total_market_cap) }}
+
+
+
加载中…
+
+
+
+
+
市场情绪 {{ moodScore }}
+
+
+
+
+
+
+
涨跌分布 (全市场 {{ dist?.total ?? '…' }} 只)
+
{{ distError }}
+
+
+
+
{{ bucketRange(hoverBucket) }}
+
{{ dist.counts[hoverBucket] }} 只
+
占 {{ bucketShare(dist.counts[hoverBucket]) }}
+
+
+
{{ distLoading ? '全市场分布计算中…(约 5s)' : '加载中…' }}
+
+
+
+
+
行业板块 热
+
{{ boardsError }}
+
+
+
{{ b.name }}
+
+
{{ fmtPctSigned(boardPct(b)) }}
+
+
+
冷
+
+
+
{{ b.name }}
+
+
{{ fmtPctSigned(boardPct(b)) }}
+
+
+
+
+
+
+
概念板块 热
+
{{ boardsError }}
+
+
+
{{ b.name }}
+
+
{{ fmtPctSigned(boardPct(b)) }}
+
+
+
冷
+
+
+
{{ b.name }}
+
+
{{ fmtPctSigned(boardPct(b)) }}
+
+
+
+
+
+
+
+ 排行榜
+
+
+
+
+
{{ rankError }}
+
+
+
+ | {{ i + 1 }}. {{ r.name }} |
+ {{ fmtAmount(Number(r.amount ?? 0)) }} |
+ {{ rankExtra(r).text }} |
+
+
+
+
+
+
+
+
+ 涨停雷达
+ {{ limitRows.length }}+
+ (≥9.8%)
+
+
{{ distError }}
+
+
+
+ | {{ i + 1 }}. {{ r.name }} |
+ {{ fmtAmount(Number(r.amount ?? 0)) }} |
+ {{ fmtPctSigned(r.change_pct) }} |
+
+
+
+
+ {{ distLoading ? '全市场扫描中…' : '今日暂无 ≥9.8% 个股' }}
+
+
+
+
+
+
异动雷达
+
{{ unusualError }}
+
+
+
+ | 时间 |
+ 股票 |
+ 异动 |
+ 数值 |
+
+
+
+
+ | {{ r.time }} |
+ {{ r.name }} |
+ {{ r.desc }} |
+ {{ r.value }} |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-ui/src/views/OptimizeView.vue b/web-ui/src/views/OptimizeView.vue
index b1474f0..3826df2 100644
--- a/web-ui/src/views/OptimizeView.vue
+++ b/web-ui/src/views/OptimizeView.vue
@@ -2,8 +2,8 @@
// 参数寻优主页面:左配置(选标的 + 策略 + 寻优参数)/ 右报告(排名表 + 热力图)。
// 取行情已整合进「开始寻优」。另有「一键寻优所有策略」:用各策略预设网格逐策略寻优再全局排名。
-import { computed, onMounted, ref, watch } from 'vue'
-import { useRouter } from 'vue-router'
+import { computed, nextTick, onMounted, ref, watch } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
import GradeBadge from '../components/GradeBadge.vue'
import OptimizeHeatmap from '../components/OptimizeHeatmap.vue'
@@ -18,6 +18,7 @@ import { useBacktestStore } from '../stores/backtest'
const store = useBacktestStore()
const router = useRouter()
+const route = useRoute()
// SymbolPicker 实例引用,用于触发取行情
const symbolPicker = ref | null>(null)
@@ -86,10 +87,22 @@ const selectedStrategy = computed(
() => store.strategies.find((s) => s.name === strategy.value) ?? null,
)
-onMounted(() => {
+onMounted(async () => {
store.loadStrategies().catch((e) => {
store.error = `加载策略列表失败:${e instanceof Error ? e.message : e}`
})
+
+ // 个股弹窗「一键寻优」入口:/optimize?code=601088&autoAll=1
+ // 填入代码后自动触发「一键寻优所有策略」(等 nextTick 让 SymbolPicker
+ // 的 v-model 同步,onRunAll 里的 loadBars 才会取到正确标的)。
+ const qCode = String(route.query.code ?? '')
+ if (route.query.autoAll === '1' && /^\d{6}$/.test(qCode)) {
+ code.value = qCode
+ await nextTick()
+ if (!store.optimizeRunning && !store.optimizeAllRunning) {
+ onRunAll()
+ }
+ }
})
// 网格点数(前端预校验,提示用户)
@@ -172,12 +185,12 @@ function buildBacktestQuery(strategyName: string, params: Record) {
// 通过 query 传递参数,单标的页接收后自动填充
- router.push({ path: '/', query: buildBacktestQuery(strategy.value, params) })
+ router.push({ path: '/backtest', query: buildBacktestQuery(strategy.value, params) })
}
// 一键寻优结果点击「查看」→ 跳转单标的页用该策略 + 参数回测
function onViewAll(strategyName: string, params: Record) {
- router.push({ path: '/', query: buildBacktestQuery(strategyName, params) })
+ router.push({ path: '/backtest', query: buildBacktestQuery(strategyName, params) })
}
function pct(v: number | null | undefined): string {
diff --git a/web-ui/src/views/SignalRadarView.vue b/web-ui/src/views/SignalRadarView.vue
index 1f94b30..b23d9e3 100644
--- a/web-ui/src/views/SignalRadarView.vue
+++ b/web-ui/src/views/SignalRadarView.vue
@@ -126,7 +126,7 @@ function signalSeq(r: SignalScanRow): string {
function onLoad(r: SignalScanRow) {
const codeOnly = r.symbol.includes(':') ? r.symbol.split(':').pop()! : r.symbol
router.push({
- path: '/',
+ path: '/backtest',
query: {
strategy: r.strategy,
params: JSON.stringify(r.params),
diff --git a/web-ui/src/views/StrategiesView.vue b/web-ui/src/views/StrategiesView.vue
index 8561551..a3fe114 100644
--- a/web-ui/src/views/StrategiesView.vue
+++ b/web-ui/src/views/StrategiesView.vue
@@ -256,7 +256,7 @@ function onLoad(s: SavedStrategy) {
const rawSymbol = (ctx.symbol as string) || ''
const codeOnly = rawSymbol.includes(':') ? rawSymbol.split(':').pop()! : rawSymbol
router.push({
- path: '/',
+ path: '/backtest',
query: {
strategy: s.strategy,
params,
diff --git a/web-ui/src/views/WatchlistView.vue b/web-ui/src/views/WatchlistView.vue
new file mode 100644
index 0000000..921ae9b
--- /dev/null
+++ b/web-ui/src/views/WatchlistView.vue
@@ -0,0 +1,347 @@
+
+
+
+
+
+
+
{{ listError }}
+
+
+
+
+
+ | 名称 |
+ 现价 |
+ 涨跌幅 |
+ 涨跌额 |
+ 成交量 |
+ 成交额 |
+ 最高 |
+ 最低 |
+ 今开 |
+ 昨收 |
+ 分时 |
+ |
+
+
+
+
+ | {{ emptyHint }} |
+
+
+ |
+ {{ item.name || item.code }}
+ {{ item.symbol }}
+ |
+ {{ fmt2(q(item)?.price) }} |
+ {{ fmtPctSigned(pct(item)) }} |
+
+ {{ q(item)?.price && q(item)?.pre_close ? fmt2(q(item)!.price! - q(item)!.pre_close!) : '-' }}
+ |
+ {{ fmtVol(q(item)?.vol) }} |
+ {{ fmtAmount(q(item)?.amount) }} |
+ {{ fmt2(q(item)?.high) }} |
+ {{ fmt2(q(item)?.low) }} |
+ {{ fmt2(q(item)?.open) }} |
+ {{ fmt2(q(item)?.pre_close) }} |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+