diff --git a/frontend/index.html b/frontend/index.html index 0fc019b..09acd9f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,5 +1,5 @@ - + diff --git a/frontend/src/api/paper.ts b/frontend/src/api/paper.ts index 03ac63c..2dc2549 100644 --- a/frontend/src/api/paper.ts +++ b/frontend/src/api/paper.ts @@ -24,6 +24,14 @@ export interface PaperAccount { mode: string interval: string status: string + symbols?: string + initial_capital?: number + start_date?: string + end_date?: string + last_run_date?: string | null + next_run_at?: string | null + checkpoint_date?: string | null + error_msg?: string | null } export interface BalancePoint { @@ -54,6 +62,24 @@ export interface StrategySummary { commission: number } +export interface PositionRow { + symbol: string + volume: number + frozen: number + avg_price: number +} + +export interface PendingOrder { + strategy_id: string + symbol: string + side: string + price: number + volume: number + is_market: boolean + match_session: string + listing_days: number +} + export async function createPaper(req: PaperCreate): Promise { const { data } = await apiClient.post<{ account_id: number }>('/paper/create', req) return data.account_id @@ -78,3 +104,13 @@ export async function getStrategies(aid: number): Promise { const { data } = await apiClient.get(`/paper/${aid}/strategies`) return data } + +export async function getPositions(aid: number): Promise { + const { data } = await apiClient.get(`/paper/${aid}/positions`) + return data +} + +export async function getPending(aid: number): Promise { + const { data } = await apiClient.get(`/paper/${aid}/pending`) + return data +} diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png deleted file mode 100644 index 02251f4..0000000 Binary files a/frontend/src/assets/hero.png and /dev/null differ diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg deleted file mode 100644 index 770e9d3..0000000 --- a/frontend/src/assets/vue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue deleted file mode 100644 index c232865..0000000 --- a/frontend/src/components/HelloWorld.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/frontend/src/components/charts/DailyPnlChart.vue b/frontend/src/components/charts/DailyPnlChart.vue index 9a06670..cc8dc94 100644 --- a/frontend/src/components/charts/DailyPnlChart.vue +++ b/frontend/src/components/charts/DailyPnlChart.vue @@ -1,34 +1,36 @@ diff --git a/frontend/src/components/charts/EquityChart.vue b/frontend/src/components/charts/EquityChart.vue index 45a7970..ede661f 100644 --- a/frontend/src/components/charts/EquityChart.vue +++ b/frontend/src/components/charts/EquityChart.vue @@ -1,33 +1,38 @@ diff --git a/frontend/src/components/charts/KlineChart.vue b/frontend/src/components/charts/KlineChart.vue index 89988d1..91a200c 100644 --- a/frontend/src/components/charts/KlineChart.vue +++ b/frontend/src/components/charts/KlineChart.vue @@ -1,18 +1,20 @@ diff --git a/frontend/src/composables/useChart.ts b/frontend/src/composables/useChart.ts new file mode 100644 index 0000000..9846ecf --- /dev/null +++ b/frontend/src/composables/useChart.ts @@ -0,0 +1,28 @@ +/* ECharts 生命周期复用:init / setOption / resize / dispose 统一 */ +import { onMounted, onUnmounted, type Ref } from 'vue' +import * as echarts from 'echarts' + +export function useChart(el: Ref) { + let chart: echarts.ECharts | null = null + + function setOption(option: echarts.EChartsCoreOption): void { + chart?.setOption(option, true) + } + + function resize(): void { + chart?.resize() + } + + onMounted(() => { + if (el.value) chart = echarts.init(el.value) + window.addEventListener('resize', resize) + }) + + onUnmounted(() => { + window.removeEventListener('resize', resize) + chart?.dispose() + chart = null + }) + + return { setOption } +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index d4d571b..3a305d5 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -2,6 +2,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' +import 'element-plus/theme-chalk/dark/css-vars.css' import './style.css' import App from './App.vue' import { router } from './router' diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 0c29e99..f2dabc7 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -18,6 +18,7 @@ const routes: RouteRecordRaw[] = [ { path: 'factor/result/:id', name: 'fc-result', component: () => import('@/views/factor/Result.vue') }, { path: 'paper/new', name: 'paper-new', component: () => import('@/views/paper/New.vue') }, { path: 'paper/result/:id', name: 'paper-result', component: () => import('@/views/paper/Result.vue') }, + { path: 'paper/live/:aid', name: 'paper-live', component: () => import('@/views/paper/Live.vue') }, ], }, ] diff --git a/frontend/src/style.css b/frontend/src/style.css index 527d4fb..d8e8d11 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1,296 +1,4 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } -} - -body { - margin: 0; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); -} - -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#app { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} +/* 全局样式入口(原 Vite 脚手架样式已移除,重写为量化后台主题) */ +@import './styles/tokens.css'; +@import './styles/element-dark.css'; +@import './styles/reset.css'; diff --git a/frontend/src/styles/element-dark.css b/frontend/src/styles/element-dark.css new file mode 100644 index 0000000..433b8e7 --- /dev/null +++ b/frontend/src/styles/element-dark.css @@ -0,0 +1,138 @@ +/* Element Plus 深色覆盖 —— html.dark 已启用 EP 官方深色,此处精修到量化主题 */ +html.dark { + /* 主色 → 品牌 */ + --el-color-primary: #1890ff; + --el-color-primary-light-3: #2f7fce; + --el-color-primary-light-5: #1f5e9c; + --el-color-primary-light-7: #164670; + --el-color-primary-light-8: #113a5e; + --el-color-primary-light-9: #0e2f4d; + --el-color-primary-dark-2: #3a9eff; + + /* 背景层级 */ + --el-bg-color: var(--bg-card); + --el-bg-color-page: var(--bg); + --el-bg-color-overlay: var(--bg-overlay); + + /* 文字 */ + --el-text-color-primary: var(--text); + --el-text-color-regular: var(--text-2); + --el-text-color-secondary: var(--text-3); + --el-text-color-placeholder: var(--text-3); + --el-text-color-disabled: #4b5158; + + /* 边框 */ + --el-border-color: var(--border); + --el-border-color-light: var(--border-2); + --el-border-color-lighter: var(--border-2); + --el-border-color-extra-light: var(--border-2); + --el-border-color-dark: var(--border); + --el-border-color-darker: var(--border); + + /* 填充 */ + --el-fill-color: var(--bg-hover); + --el-fill-color-light: var(--bg-card); + --el-fill-color-lighter: var(--bg-card); + --el-fill-color-extra-light: var(--bg-card); + --el-fill-color-dark: var(--bg-hover); + --el-fill-color-darker: var(--bg-hover); + --el-fill-color-blank: transparent; + + /* 遮罩/禁用 */ + --el-mask-color: rgba(1, 4, 9, 0.72); + --el-disabled-bg-color: var(--bg-card); + --el-disabled-text-color: var(--text-3); + --el-disabled-border-color: var(--border-2); + + /* 语义色(danger 用红、success 用绿,契合涨跌但保持语义) */ + --el-color-success: #3fb950; + --el-color-warning: #d29922; + --el-color-danger: #f85149; + --el-color-error: #f85149; + --el-color-info: #8b949e; +} + +/* 组件细节精修 */ +html.dark { + /* 卡片更紧凑、去多余阴影 */ + .el-card { + background-color: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--r-md); + box-shadow: none; + } + .el-card__header { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + font-weight: 600; + color: var(--text); + } + .el-card__body { + padding: 16px; + } + + /* 表格密度 */ + .el-table { + --el-table-bg-color: transparent; + --el-table-tr-bg-color: transparent; + --el-table-row-hover-bg-color: var(--bg-hover); + --el-table-header-bg-color: var(--bg-hover); + --el-table-border-color: var(--border-2); + --el-table-border: 1px solid var(--border-2); + --el-table-text-color: var(--text); + --el-table-header-text-color: var(--text-2); + font-size: 13px; + } + .el-table th.el-table__cell { + background-color: var(--bg-hover) !important; + font-weight: 600; + } + .el-table .cell { + line-height: 1.5; + } + + /* 菜单 */ + .el-menu { + --el-menu-bg-color: transparent; + --el-menu-text-color: var(--text-2); + --el-menu-hover-bg-color: var(--bg-hover); + --el-menu-active-color: var(--brand); + border-right: none; + } + .el-menu-item.is-active, + .el-sub-menu.is-active > .el-sub-menu__title { + color: var(--brand); + } + + /* 输入/选择器贴主题 */ + .el-input__wrapper, + .el-textarea__inner, + .el-select__wrapper { + background-color: var(--bg) !important; + box-shadow: 0 0 0 1px var(--border) inset !important; + } + .el-input__wrapper:hover, + .el-select__wrapper:hover { + box-shadow: 0 0 0 1px var(--brand) inset !important; + } + .el-input.is-focus .el-input__wrapper, + .el-select__wrapper.is-focused { + box-shadow: 0 0 0 1px var(--brand) inset !important; + } + + /* 描述列表/标签页 边框 */ + .el-descriptions__border { + background-color: transparent; + } + + /* 滚动条贴深色 */ + .el-scrollbar__bar { + opacity: 0.5; + } + + /* 数字用等宽(涨跌色由业务层控制) */ + .num { + font-family: var(--mono); + font-variant-numeric: tabular-nums; + } +} diff --git a/frontend/src/styles/reset.css b/frontend/src/styles/reset.css new file mode 100644 index 0000000..c4fc750 --- /dev/null +++ b/frontend/src/styles/reset.css @@ -0,0 +1,91 @@ +/* 全局 reset —— 后台基础:深色 / 紧凑 / 高数据密度 */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#app { + height: 100%; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--sans); + font-size: 13px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +h1, h2, h3, h4 { + margin: 0; + font-weight: 600; + color: var(--text); +} + +p { margin: 0; } + +a { + color: var(--brand); + text-decoration: none; +} + +/* 深色滚动条 */ +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { background: #424a53; } + +/* ====== 布局工具类 ====== */ +.page { + display: flex; + flex-direction: column; + gap: var(--sp-4); +} + +.page-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--sp-3); + padding-bottom: var(--sp-2); + border-bottom: 1px solid var(--border); +} + +.page-title { + font-size: 16px; + font-weight: 600; + color: var(--text); +} + +.page-subtitle { + font-size: 12px; + color: var(--text-3); +} + +.section-title { + font-size: 13px; + font-weight: 600; + color: var(--text-2); + letter-spacing: 0.3px; +} + +/* 涨跌色工具 */ +.up { color: var(--up); } +.down { color: var(--down); } +.muted { color: var(--text-3); } + +/* 紧凑表单 */ +.el-form-item { margin-bottom: 16px; } + +/* 链接按钮无下划线 */ +.el-button.is-link { font-weight: 500; } diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css new file mode 100644 index 0000000..bee8d7a --- /dev/null +++ b/frontend/src/styles/tokens.css @@ -0,0 +1,51 @@ +/* 量化后台设计 token —— 深色 / 高密度 / 红涨绿跌(A股惯例) */ +:root { + /* 背景层级 */ + --bg: #0d1117; /* 主背景 */ + --bg-card: #161b22; /* 卡片/面板 */ + --bg-hover: #1c2128; /* 悬停/表头 */ + --bg-overlay: #1c2128; /* 弹层 */ + + /* 文字 */ + --text: #e6edf3; /* 主 */ + --text-2: #8b949e; /* 次 */ + --text-3: #6e7681; /* 弱/占位 */ + + /* 语义色:红涨绿跌 */ + --up: #f85149; /* 涨/红 */ + --down: #3fb950; /* 跌/绿 */ + --brand: #1890ff; /* 品牌/链接/激活 */ + --warn: #d29922; + --danger: #f85149; + + /* 边框 */ + --border: #30363d; + --border-2: #21262d; /* 更弱的分隔线 */ + + /* 间距 */ + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 24px; + --sp-6: 32px; + + /* 圆角 */ + --r-sm: 4px; + --r-md: 6px; + --r-lg: 8px; + + /* 阴影 */ + --shadow: 0 8px 24px rgba(0, 0, 0, 0.45); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); + + /* 字体 */ + --mono: ui-monospace, 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Roboto, sans-serif; + + /* 布局 */ + --sidebar-w: 208px; + --header-h: 52px; + + --ease: cubic-bezier(0.4, 0, 0.2, 1); +} diff --git a/frontend/src/utils/echartsDark.ts b/frontend/src/utils/echartsDark.ts new file mode 100644 index 0000000..e62cc57 --- /dev/null +++ b/frontend/src/utils/echartsDark.ts @@ -0,0 +1,49 @@ +/* ECharts 深色量化主题 —— 全站统一:深色轴线/文字/网格,红涨绿跌,净值品牌色 */ + +export const UP = '#f85149' // 红涨(A股惯例) +export const DOWN = '#3fb950' // 绿跌 +export const BRAND = '#1890ff' +export const TEXT = '#e6edf3' +export const TEXT_DIM = '#8b949e' +export const AXIS = '#30363d' +export const SPLIT = '#21262d' +export const TOOLTIP_BG = '#1c2128' + +/** 通用深色坐标轴片段(合并进 xAxis/yAxis) */ +export function darkAxis(): Record { + return { + axisLine: { lineStyle: { color: AXIS } }, + axisTick: { lineStyle: { color: AXIS } }, + axisLabel: { color: TEXT_DIM, fontSize: 11 }, + splitLine: { lineStyle: { color: SPLIT } }, + } +} + +/** 通用深色 tooltip */ +export function darkTooltip(): Record { + return { + trigger: 'axis', + backgroundColor: TOOLTIP_BG, + borderColor: AXIS, + borderWidth: 1, + textStyle: { color: TEXT, fontSize: 12 }, + } +} + +/** 通用深色 grid */ +export function darkGrid(): Record { + return { left: 48, right: 20, top: 40, bottom: 32 } +} + +/** 通用标题样式 */ +export function darkTitle(text: string): Record { + return { text, left: 'left', textStyle: { color: TEXT, fontSize: 13 } } +} + +/** K 线 itemStyle:红涨绿跌 */ +export const klineItemStyle = { + color: UP, // 阳线(收盘≥开盘) + color0: DOWN, // 阴线 + borderColor: UP, + borderColor0: DOWN, +} diff --git a/frontend/src/views/Layout.vue b/frontend/src/views/Layout.vue index 60550e7..ed19fb4 100644 --- a/frontend/src/views/Layout.vue +++ b/frontend/src/views/Layout.vue @@ -1,10 +1,19 @@ - + + diff --git a/frontend/src/views/backtest/New.vue b/frontend/src/views/backtest/New.vue index db0912f..55f83e0 100644 --- a/frontend/src/views/backtest/New.vue +++ b/frontend/src/views/backtest/New.vue @@ -77,10 +77,14 @@ async function onSubmit(): Promise { diff --git a/frontend/src/views/backtest/Optimize.vue b/frontend/src/views/backtest/Optimize.vue index 8b890e3..ff72011 100644 --- a/frontend/src/views/backtest/Optimize.vue +++ b/frontend/src/views/backtest/Optimize.vue @@ -90,10 +90,14 @@ async function onSubmit(): Promise { diff --git a/frontend/src/views/backtest/Progress.vue b/frontend/src/views/backtest/Progress.vue index fce622a..3a11f96 100644 --- a/frontend/src/views/backtest/Progress.vue +++ b/frontend/src/views/backtest/Progress.vue @@ -26,19 +26,30 @@ function pct(): number { + + diff --git a/frontend/src/views/backtest/Result.vue b/frontend/src/views/backtest/Result.vue index 3beb8ed..80b14a5 100644 --- a/frontend/src/views/backtest/Result.vue +++ b/frontend/src/views/backtest/Result.vue @@ -51,9 +51,15 @@ onMounted(async () => {