Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -17,6 +17,7 @@ const api = {
|
|||||||
trades: '/api/strategies/trades',
|
trades: '/api/strategies/trades',
|
||||||
positions: '/api/strategies/positions',
|
positions: '/api/strategies/positions',
|
||||||
equityCurve: '/api/strategies/equityCurve',
|
equityCurve: '/api/strategies/equityCurve',
|
||||||
|
logs: '/api/strategies/logs',
|
||||||
notifications: '/api/strategies/notifications'
|
notifications: '/api/strategies/notifications'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +222,19 @@ export function getStrategyEquityCurve (id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取策略运行日志
|
||||||
|
* @param {number} id - 策略ID
|
||||||
|
* @param {number} limit - 最大日志条数
|
||||||
|
*/
|
||||||
|
export function getStrategyLogs (id, limit = 200) {
|
||||||
|
return request({
|
||||||
|
url: api.logs,
|
||||||
|
method: 'get',
|
||||||
|
params: { id, limit }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strategy signal notifications (browser channel persistence).
|
* Strategy signal notifications (browser channel persistence).
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 运行策略 -->
|
<!-- 运行策略 -->
|
||||||
<div class="kpi-card kpi-strategies clickable" @click="$router.push('/trading-assistant')">
|
<div class="kpi-card kpi-strategies clickable" @click="goToStrategyManagement">
|
||||||
<div class="kpi-content">
|
<div class="kpi-content">
|
||||||
<div class="kpi-header">
|
<div class="kpi-header">
|
||||||
<span class="kpi-icon">
|
<span class="kpi-icon">
|
||||||
@@ -146,6 +146,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showSetupGuide && !hideSetupGuide" class="setup-guide-card">
|
||||||
|
<div class="setup-guide-copy">
|
||||||
|
<div class="setup-guide-title">{{ tt('dashboard.setupGuide.title', 'Bring your first live strategy online') }}</div>
|
||||||
|
<div class="setup-guide-desc">{{ tt('dashboard.setupGuide.desc', 'The latest frontend adds a clearer handoff from overview into strategy management. Use this entry point to create, launch, and monitor a strategy flow faster.') }}</div>
|
||||||
|
<div class="setup-guide-path">{{ tt('dashboard.setupGuide.path', 'Overview -> Strategy Manager -> Create Strategy') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="setup-guide-actions">
|
||||||
|
<a-button @click="goToStrategyManagement">
|
||||||
|
<a-icon type="appstore" />
|
||||||
|
{{ tt('dashboard.setupGuide.secondary', 'Open Strategy Manager') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" @click="goToStrategyCreate">
|
||||||
|
<a-icon type="plus" />
|
||||||
|
{{ tt('dashboard.setupGuide.primary', 'Create Strategy') }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 图表区域 - 第一行 -->
|
<!-- 图表区域 - 第一行 -->
|
||||||
<div class="chart-row">
|
<div class="chart-row">
|
||||||
<!-- 收益日历 -->
|
<!-- 收益日历 -->
|
||||||
@@ -501,6 +519,12 @@ import { mapState } from 'vuex'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
|
props: {
|
||||||
|
hideSetupGuide: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
summary: {
|
summary: {
|
||||||
@@ -549,6 +573,12 @@ export default {
|
|||||||
performance () {
|
performance () {
|
||||||
return this.summary.performance || {}
|
return this.summary.performance || {}
|
||||||
},
|
},
|
||||||
|
showSetupGuide () {
|
||||||
|
const strategyCount = Number(this.summary.indicator_strategy_count || 0)
|
||||||
|
const hasPositions = Array.isArray(this.summary.current_positions) && this.summary.current_positions.length > 0
|
||||||
|
const hasRecentTrades = Array.isArray(this.summary.recent_trades) && this.summary.recent_trades.length > 0
|
||||||
|
return strategyCount === 0 || (!hasPositions && !hasRecentTrades)
|
||||||
|
},
|
||||||
strategyStats () {
|
strategyStats () {
|
||||||
return this.summary.strategy_stats || []
|
return this.summary.strategy_stats || []
|
||||||
},
|
},
|
||||||
@@ -738,6 +768,16 @@ export default {
|
|||||||
if (this.hourlyChart) this.hourlyChart.dispose()
|
if (this.hourlyChart) this.hourlyChart.dispose()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
tt (key, fallback, params) {
|
||||||
|
const translated = this.$t(key, params)
|
||||||
|
return translated !== key ? translated : fallback
|
||||||
|
},
|
||||||
|
goToStrategyManagement () {
|
||||||
|
this.$router.push('/trading-assistant?tab=strategy')
|
||||||
|
},
|
||||||
|
goToStrategyCreate () {
|
||||||
|
this.$router.push('/trading-assistant?tab=strategy&mode=create')
|
||||||
|
},
|
||||||
async fetchData () {
|
async fetchData () {
|
||||||
try {
|
try {
|
||||||
const res = await getDashboardSummary()
|
const res = await getDashboardSummary()
|
||||||
@@ -1446,9 +1486,80 @@ export default {
|
|||||||
background: @bg-light;
|
background: @bg-light;
|
||||||
transition: background 0.3s;
|
transition: background 0.3s;
|
||||||
|
|
||||||
|
.setup-guide-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 20px 22px;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid #dce7f3;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(59, 130, 246, 0.14), transparent 36%),
|
||||||
|
radial-gradient(circle at bottom right, rgba(16, 185, 129, 0.1), transparent 34%),
|
||||||
|
linear-gradient(135deg, #ffffff 0%, #f8fbff 55%, #eef7ff 100%);
|
||||||
|
box-shadow: 0 16px 38px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-guide-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-guide-title {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-guide-desc {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-guide-path {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-guide-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
&.theme-dark {
|
&.theme-dark {
|
||||||
background: @bg-dark;
|
background: @bg-dark;
|
||||||
|
|
||||||
|
.setup-guide-card {
|
||||||
|
border-color: rgba(59, 130, 246, 0.16);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(37, 99, 235, 0.24), transparent 36%),
|
||||||
|
radial-gradient(circle at bottom right, rgba(16, 185, 129, 0.12), transparent 34%),
|
||||||
|
linear-gradient(135deg, #161b22 0%, #111827 58%, #0f172a 100%);
|
||||||
|
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-guide-title {
|
||||||
|
color: @text-primary-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-guide-desc {
|
||||||
|
color: @text-secondary-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-guide-path {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
.kpi-card {
|
.kpi-card {
|
||||||
background: @bg-card-dark;
|
background: @bg-card-dark;
|
||||||
border-color: @border-dark;
|
border-color: @border-dark;
|
||||||
@@ -1563,6 +1674,24 @@ export default {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.setup-guide-card {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 18px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-guide-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-guide-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.kpi-card {
|
.kpi-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: @bg-card-light;
|
background: @bg-card-light;
|
||||||
|
|||||||
@@ -369,6 +369,13 @@
|
|||||||
@click.stop="handlePublishIndicator(indicator)"
|
@click.stop="handlePublishIndicator(indicator)"
|
||||||
/>
|
/>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
<a-tooltip :title="tt('trading-assistant.createStrategy', 'Create Strategy')">
|
||||||
|
<a-icon
|
||||||
|
type="rocket"
|
||||||
|
class="action-icon create-strategy-icon"
|
||||||
|
@click.stop="handleCreateStrategyFromIndicator(indicator)"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="card-desc">{{ indicator.description || '' }}</span>
|
<span class="card-desc">{{ indicator.description || '' }}</span>
|
||||||
@@ -380,6 +387,69 @@
|
|||||||
<span>{{ $t('dashboard.indicator.empty') }}</span>
|
<span>{{ $t('dashboard.indicator.empty') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="indicator-section" :class="{ 'section-empty': purchasedIndicators.length === 0 }">
|
||||||
|
<div class="section-label">
|
||||||
|
<div class="section-label-left" @click="purchasedSectionCollapsed = !purchasedSectionCollapsed">
|
||||||
|
<a-icon :type="purchasedSectionCollapsed ? 'right' : 'down'" class="collapse-icon" />
|
||||||
|
<span>{{ $t('dashboard.indicator.section.purchased') }} ({{ purchasedIndicators.length }})</span>
|
||||||
|
</div>
|
||||||
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
icon="shop"
|
||||||
|
class="buy-indicator-btn"
|
||||||
|
@click.stop="goToIndicatorMarket"
|
||||||
|
>
|
||||||
|
{{ $t('menu.dashboard.community') }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<div v-show="!purchasedSectionCollapsed" class="section-content custom-scrollbar">
|
||||||
|
<div
|
||||||
|
v-for="indicator in purchasedIndicators"
|
||||||
|
:key="'purchased-' + indicator.id"
|
||||||
|
:class="['indicator-card', 'purchased-indicator', { 'indicator-active': isIndicatorActive('purchased-' + indicator.id) }]"
|
||||||
|
@click="toggleIndicator(indicator, 'purchased')"
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-name">
|
||||||
|
<a-icon type="shopping" class="purchased-icon" />
|
||||||
|
{{ indicator.name }}
|
||||||
|
</span>
|
||||||
|
<div class="card-actions">
|
||||||
|
<a-tooltip :title="isIndicatorActive('purchased-' + indicator.id) ? $t('dashboard.indicator.action.stop') : $t('dashboard.indicator.action.start')">
|
||||||
|
<a-icon
|
||||||
|
:type="isIndicatorActive('purchased-' + indicator.id) ? 'pause-circle' : 'play-circle'"
|
||||||
|
:class="['action-icon', 'toggle-icon', { active: isIndicatorActive('purchased-' + indicator.id) }]"
|
||||||
|
@click.stop="toggleIndicator(indicator, 'purchased')"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip :title="$t('dashboard.indicator.backtest.title')">
|
||||||
|
<a-icon
|
||||||
|
type="experiment"
|
||||||
|
class="action-icon backtest-icon"
|
||||||
|
@click.stop="handleOpenBacktest(indicator)"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip :title="$t('dashboard.indicator.backtest.historyTitle')">
|
||||||
|
<a-icon
|
||||||
|
type="clock-circle"
|
||||||
|
class="action-icon backtest-history-icon"
|
||||||
|
@click.stop="handleOpenBacktestHistory(indicator)"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="card-desc">{{ indicator.description || '' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="purchasedIndicators.length === 0" class="empty-indicators">
|
||||||
|
<a-icon type="shopping" />
|
||||||
|
<span>{{ $t('dashboard.indicator.emptyPurchased') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
@@ -3001,23 +3071,23 @@ getMarketColor,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-tab-content {
|
.mobile-tab-content {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden; /* 不在这里滚动,让section-content滚动 */
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-content {
|
.section-content {
|
||||||
flex: 1;
|
flex: none;
|
||||||
overflow-y: auto !important; /* 只有这里滚动 */
|
overflow: visible !important;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
min-height: 0; /* 使用flex: 1来占据剩余空间 */
|
min-height: auto;
|
||||||
height: 100%; /* 使用100%高度,让flex生效 */
|
height: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,485 @@
|
|||||||
|
<template>
|
||||||
|
<div class="performance-analysis" :class="{ 'theme-dark': isDark }">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div v-if="hasData" class="performance-shell">
|
||||||
|
<div class="metrics-grid">
|
||||||
|
<div class="metric-card" :class="getMetricClass(metrics.totalReturn)">
|
||||||
|
<div class="metric-label">{{ tt('trading-assistant.performance.totalReturn', 'Total Return') }}</div>
|
||||||
|
<div class="metric-value">{{ formatPercent(metrics.totalReturn) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card" :class="getMetricClass(metrics.annualReturn)">
|
||||||
|
<div class="metric-label">{{ tt('trading-assistant.performance.annualReturn', 'Annual Return') }}</div>
|
||||||
|
<div class="metric-value">{{ formatPercent(metrics.annualReturn) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card negative">
|
||||||
|
<div class="metric-label">{{ tt('trading-assistant.performance.maxDrawdown', 'Max Drawdown') }}</div>
|
||||||
|
<div class="metric-value">{{ formatPercent(metrics.maxDrawdown) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">{{ tt('trading-assistant.performance.sharpe', 'Sharpe') }}</div>
|
||||||
|
<div class="metric-value">{{ formatNumber(metrics.sharpe) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card" :class="getMetricClass(metrics.winRate - 0.5)">
|
||||||
|
<div class="metric-label">{{ tt('trading-assistant.performance.winRate', 'Win Rate') }}</div>
|
||||||
|
<div class="metric-value">{{ formatPercent(metrics.winRate) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card" :class="getMetricClass((metrics.profitFactor || 0) - 1)">
|
||||||
|
<div class="metric-label">{{ tt('trading-assistant.performance.profitFactor', 'Profit Factor') }}</div>
|
||||||
|
<div class="metric-value">{{ formatNumber(metrics.profitFactor) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">{{ tt('trading-assistant.performance.totalTrades', 'Trades') }}</div>
|
||||||
|
<div class="metric-value">{{ metrics.totalTrades || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-label">{{ tt('trading-assistant.performance.runningDays', 'Running Days') }}</div>
|
||||||
|
<div class="metric-value">{{ metrics.runningDays || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="chart-title">{{ tt('trading-assistant.performance.equityCurve', 'Equity Curve') }}</div>
|
||||||
|
<div ref="equityChart" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="chart-title">{{ tt('trading-assistant.performance.dailyReturns', 'Daily Returns') }}</div>
|
||||||
|
<div ref="dailyChart" class="chart-container chart-container-sm"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-empty
|
||||||
|
v-else-if="!loading"
|
||||||
|
class="performance-empty"
|
||||||
|
:description="tt('trading-assistant.performance.noData', 'No performance data yet')"
|
||||||
|
/>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { getStrategyEquityCurve } from '@/api/strategy'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PerformanceAnalysis',
|
||||||
|
props: {
|
||||||
|
strategyId: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
isDark: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
metrics: {},
|
||||||
|
equityData: [],
|
||||||
|
dailyReturns: [],
|
||||||
|
equityChartInstance: null,
|
||||||
|
dailyChartInstance: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasData () {
|
||||||
|
return this.equityData.length > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
strategyId: {
|
||||||
|
immediate: true,
|
||||||
|
handler (value) {
|
||||||
|
if (value) {
|
||||||
|
this.loadData()
|
||||||
|
} else {
|
||||||
|
this.resetState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isDark () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.renderCharts()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
|
this.disposeCharts()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
tt (key, fallback, params) {
|
||||||
|
const translated = this.$t(key, params)
|
||||||
|
return translated !== key ? translated : fallback
|
||||||
|
},
|
||||||
|
resetState () {
|
||||||
|
this.metrics = {}
|
||||||
|
this.equityData = []
|
||||||
|
this.dailyReturns = []
|
||||||
|
this.disposeCharts()
|
||||||
|
},
|
||||||
|
async loadData () {
|
||||||
|
if (!this.strategyId) {
|
||||||
|
this.resetState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await getStrategyEquityCurve(this.strategyId)
|
||||||
|
const rawData = res && res.data
|
||||||
|
const curve = Array.isArray(rawData)
|
||||||
|
? rawData
|
||||||
|
: (rawData && Array.isArray(rawData.equity_curve) ? rawData.equity_curve : [])
|
||||||
|
|
||||||
|
this.equityData = curve
|
||||||
|
.map(item => ({
|
||||||
|
time: item.time || item.timestamp || item.created_at,
|
||||||
|
equity: Number(item.equity ?? item.value ?? item.y ?? 0),
|
||||||
|
trade_count: Number(item.trade_count || 0),
|
||||||
|
win_count: Number(item.win_count || 0),
|
||||||
|
gross_profit: Number(item.gross_profit || 0),
|
||||||
|
gross_loss: Number(item.gross_loss || 0)
|
||||||
|
}))
|
||||||
|
.filter(item => Number.isFinite(item.equity))
|
||||||
|
|
||||||
|
this.computeMetrics()
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.renderCharts()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.resetState()
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computeMetrics () {
|
||||||
|
if (!this.equityData.length) {
|
||||||
|
this.metrics = {}
|
||||||
|
this.dailyReturns = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = this.equityData.map(item => item.equity)
|
||||||
|
const first = values[0] || 1
|
||||||
|
const last = values[values.length - 1] || first
|
||||||
|
const totalReturn = first > 0 ? (last - first) / first : 0
|
||||||
|
|
||||||
|
let peak = first || 1
|
||||||
|
let maxDrawdown = 0
|
||||||
|
const returns = []
|
||||||
|
|
||||||
|
for (let i = 1; i < values.length; i++) {
|
||||||
|
const prev = values[i - 1]
|
||||||
|
const current = values[i]
|
||||||
|
if (current > peak) {
|
||||||
|
peak = current
|
||||||
|
}
|
||||||
|
if (peak > 0) {
|
||||||
|
maxDrawdown = Math.min(maxDrawdown, (current - peak) / peak)
|
||||||
|
}
|
||||||
|
if (prev > 0) {
|
||||||
|
returns.push((current - prev) / prev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dailyReturns = returns
|
||||||
|
|
||||||
|
const periods = Math.max(values.length - 1, 1)
|
||||||
|
const annualFactor = 365 / periods
|
||||||
|
const annualReturn = first > 0 && last > 0 ? Math.pow(last / first, annualFactor) - 1 : 0
|
||||||
|
|
||||||
|
const mean = returns.length ? returns.reduce((sum, value) => sum + value, 0) / returns.length : 0
|
||||||
|
const variance = returns.length > 1
|
||||||
|
? returns.reduce((sum, value) => sum + ((value - mean) ** 2), 0) / (returns.length - 1)
|
||||||
|
: 0
|
||||||
|
const std = variance > 0 ? Math.sqrt(variance) : 0
|
||||||
|
const sharpe = std > 0 ? (mean / std) * Math.sqrt(365) : 0
|
||||||
|
|
||||||
|
const winningReturns = returns.filter(value => value > 0)
|
||||||
|
const losingReturns = returns.filter(value => value < 0)
|
||||||
|
const grossProfit = losingReturns.length || winningReturns.length
|
||||||
|
? winningReturns.reduce((sum, value) => sum + value, 0)
|
||||||
|
: this.equityData.reduce((sum, item) => sum + Math.max(item.gross_profit || 0, 0), 0)
|
||||||
|
const grossLoss = losingReturns.length
|
||||||
|
? Math.abs(losingReturns.reduce((sum, value) => sum + value, 0))
|
||||||
|
: Math.abs(this.equityData.reduce((sum, item) => sum + Math.min(item.gross_loss || 0, 0), 0))
|
||||||
|
|
||||||
|
this.metrics = {
|
||||||
|
totalReturn,
|
||||||
|
annualReturn: Number.isFinite(annualReturn) ? annualReturn : 0,
|
||||||
|
maxDrawdown: Math.abs(maxDrawdown),
|
||||||
|
sharpe: Number.isFinite(sharpe) ? sharpe : 0,
|
||||||
|
winRate: returns.length ? winningReturns.length / returns.length : 0,
|
||||||
|
profitFactor: grossLoss > 0 ? grossProfit / grossLoss : (grossProfit > 0 ? grossProfit : 0),
|
||||||
|
totalTrades: Math.max(values.length - 1, 0),
|
||||||
|
runningDays: values.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderCharts () {
|
||||||
|
if (!this.hasData) {
|
||||||
|
this.disposeCharts()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderEquityChart()
|
||||||
|
this.renderDailyChart()
|
||||||
|
},
|
||||||
|
renderEquityChart () {
|
||||||
|
if (!this.$refs.equityChart) return
|
||||||
|
if (!this.equityChartInstance) {
|
||||||
|
this.equityChartInstance = echarts.init(this.$refs.equityChart)
|
||||||
|
}
|
||||||
|
|
||||||
|
const textColor = this.isDark ? '#d1d4dc' : '#1f2937'
|
||||||
|
const axisColor = this.isDark ? '#4b5563' : '#dbe2ea'
|
||||||
|
const areaTop = this.isDark ? 'rgba(34, 197, 94, 0.35)' : 'rgba(34, 197, 94, 0.22)'
|
||||||
|
const areaBottom = this.isDark ? 'rgba(59, 130, 246, 0.04)' : 'rgba(59, 130, 246, 0.02)'
|
||||||
|
|
||||||
|
this.equityChartInstance.setOption({
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
grid: { left: 16, right: 16, top: 24, bottom: 30, containLabel: true },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: this.isDark ? '#111827' : '#ffffff',
|
||||||
|
borderColor: axisColor,
|
||||||
|
textStyle: { color: textColor },
|
||||||
|
formatter: (params) => {
|
||||||
|
const point = params && params[0]
|
||||||
|
if (!point) return ''
|
||||||
|
return `${point.axisValueLabel}<br/>${this.formatCurrency(point.data)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
axisLine: { lineStyle: { color: axisColor } },
|
||||||
|
axisLabel: { color: textColor, margin: 12 },
|
||||||
|
data: this.equityData.map(item => this.formatAxisTime(item.time))
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisTick: { show: false },
|
||||||
|
splitLine: { lineStyle: { color: axisColor, opacity: 0.55 } },
|
||||||
|
axisLabel: {
|
||||||
|
color: textColor,
|
||||||
|
formatter: value => this.formatCurrency(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
showSymbol: false,
|
||||||
|
lineStyle: { width: 3, color: '#22c55e' },
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: areaTop },
|
||||||
|
{ offset: 1, color: areaBottom }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
data: this.equityData.map(item => item.equity)
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
renderDailyChart () {
|
||||||
|
if (!this.$refs.dailyChart) return
|
||||||
|
if (!this.dailyChartInstance) {
|
||||||
|
this.dailyChartInstance = echarts.init(this.$refs.dailyChart)
|
||||||
|
}
|
||||||
|
|
||||||
|
const textColor = this.isDark ? '#d1d4dc' : '#1f2937'
|
||||||
|
const axisColor = this.isDark ? '#4b5563' : '#dbe2ea'
|
||||||
|
|
||||||
|
this.dailyChartInstance.setOption({
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
grid: { left: 16, right: 16, top: 20, bottom: 30, containLabel: true },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: this.isDark ? '#111827' : '#ffffff',
|
||||||
|
borderColor: axisColor,
|
||||||
|
textStyle: { color: textColor },
|
||||||
|
formatter: (params) => {
|
||||||
|
const point = params && params[0]
|
||||||
|
if (!point) return ''
|
||||||
|
return `${point.axisValueLabel}<br/>${this.formatPercent(point.data)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
axisLine: { lineStyle: { color: axisColor } },
|
||||||
|
axisLabel: { color: textColor },
|
||||||
|
data: this.dailyReturns.map((_, index) => `#${index + 1}`)
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisTick: { show: false },
|
||||||
|
splitLine: { lineStyle: { color: axisColor, opacity: 0.55 } },
|
||||||
|
axisLabel: {
|
||||||
|
color: textColor,
|
||||||
|
formatter: value => this.formatPercent(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
type: 'bar',
|
||||||
|
barWidth: '62%',
|
||||||
|
data: this.dailyReturns.map(value => ({
|
||||||
|
value,
|
||||||
|
itemStyle: {
|
||||||
|
color: value >= 0 ? '#22c55e' : '#ef4444',
|
||||||
|
borderRadius: value >= 0 ? [6, 6, 0, 0] : [0, 0, 6, 6]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleResize () {
|
||||||
|
if (this.equityChartInstance) {
|
||||||
|
this.equityChartInstance.resize()
|
||||||
|
}
|
||||||
|
if (this.dailyChartInstance) {
|
||||||
|
this.dailyChartInstance.resize()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disposeCharts () {
|
||||||
|
if (this.equityChartInstance) {
|
||||||
|
this.equityChartInstance.dispose()
|
||||||
|
this.equityChartInstance = null
|
||||||
|
}
|
||||||
|
if (this.dailyChartInstance) {
|
||||||
|
this.dailyChartInstance.dispose()
|
||||||
|
this.dailyChartInstance = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatAxisTime (time) {
|
||||||
|
if (!time) return '--'
|
||||||
|
const raw = Number(time)
|
||||||
|
const date = Number.isFinite(raw)
|
||||||
|
? new Date(raw < 1e12 ? raw * 1000 : raw)
|
||||||
|
: new Date(time)
|
||||||
|
if (Number.isNaN(date.getTime())) return '--'
|
||||||
|
return date.toLocaleDateString(this.$i18n.locale === 'zh-CN' ? 'zh-CN' : 'en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formatPercent (value) {
|
||||||
|
const number = Number(value || 0) * 100
|
||||||
|
return `${number >= 0 ? '+' : ''}${number.toFixed(2)}%`
|
||||||
|
},
|
||||||
|
formatCurrency (value) {
|
||||||
|
const number = Number(value || 0)
|
||||||
|
return `$${number.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||||
|
},
|
||||||
|
formatNumber (value) {
|
||||||
|
return Number(value || 0).toFixed(2)
|
||||||
|
},
|
||||||
|
getMetricClass (value) {
|
||||||
|
return {
|
||||||
|
positive: Number(value) > 0,
|
||||||
|
negative: Number(value) < 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.performance-analysis {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.performance-shell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid #e5edf5;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||||
|
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&.positive {
|
||||||
|
border-color: rgba(34, 197, 94, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.negative {
|
||||||
|
border-color: rgba(239, 68, 68, 0.28);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-section {
|
||||||
|
border: 1px solid #e5edf5;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container-sm {
|
||||||
|
height: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance-empty {
|
||||||
|
padding: 54px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.theme-dark {
|
||||||
|
.metric-card,
|
||||||
|
.chart-section {
|
||||||
|
background: linear-gradient(180deg, #151d2f 0%, #101827 100%);
|
||||||
|
border-color: #25324a;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value,
|
||||||
|
.chart-title {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="position-records">
|
<div class="position-records" :class="{ 'theme-dark': isDark }">
|
||||||
<div v-if="positions.length === 0 && !loading" class="empty-state">
|
<div v-if="positions.length === 0 && !loading" class="empty-state">
|
||||||
<a-empty :description="$t('trading-assistant.table.noPositions')" />
|
<a-empty :description="$t('trading-assistant.table.noPositions')" />
|
||||||
</div>
|
</div>
|
||||||
@@ -68,6 +68,10 @@ export default {
|
|||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
isDark: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
<template>
|
||||||
|
<div class="strategy-logs" :class="{ 'theme-dark': isDark }">
|
||||||
|
<div class="logs-toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<a-radio-group v-model="filterLevel" size="small" button-style="solid">
|
||||||
|
<a-radio-button value="all">
|
||||||
|
{{ tt('common.all', 'All') }} ({{ logs.length }})
|
||||||
|
</a-radio-button>
|
||||||
|
<a-radio-button value="trade">
|
||||||
|
{{ tt('trading-assistant.logs.level.trade', 'Trade') }} ({{ countByLevel('trade') }})
|
||||||
|
</a-radio-button>
|
||||||
|
<a-radio-button value="signal">
|
||||||
|
{{ tt('trading-assistant.logs.level.signal', 'Signal') }} ({{ countByLevel('signal') }})
|
||||||
|
</a-radio-button>
|
||||||
|
<a-radio-button value="error">
|
||||||
|
{{ tt('trading-assistant.logs.level.error', 'Error') }} ({{ countByLevel('error') }})
|
||||||
|
</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<a-switch size="small" :checked="autoRefresh" @change="toggleAutoRefresh" />
|
||||||
|
<span class="auto-refresh-label">{{ tt('trading-assistant.logs.autoRefresh', 'Auto refresh') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div ref="logsContainer" class="logs-container custom-scrollbar">
|
||||||
|
<div v-if="filteredLogs.length === 0" class="logs-empty">
|
||||||
|
<a-icon type="file-text" />
|
||||||
|
<p>{{ tt('trading-assistant.logs.noLogs', 'No logs yet') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="item in filteredLogs"
|
||||||
|
:key="item.id || `${item.timestamp}-${item.message}`"
|
||||||
|
class="log-entry"
|
||||||
|
:class="`level-${item.level || 'info'}`"
|
||||||
|
>
|
||||||
|
<span class="log-time">{{ formatTime(item.timestamp) }}</span>
|
||||||
|
<a-tag class="log-level" size="small" :color="getLevelColor(item.level)">
|
||||||
|
{{ getLevelText(item.level) }}
|
||||||
|
</a-tag>
|
||||||
|
<span class="log-message">{{ item.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getStrategyLogs } from '@/api/strategy'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StrategyLogs',
|
||||||
|
props: {
|
||||||
|
strategyId: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
isDark: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
logs: [],
|
||||||
|
filterLevel: 'all',
|
||||||
|
autoRefresh: false,
|
||||||
|
refreshTimer: null,
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredLogs () {
|
||||||
|
if (this.filterLevel === 'all') {
|
||||||
|
return this.logs
|
||||||
|
}
|
||||||
|
return this.logs.filter(item => item.level === this.filterLevel)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
strategyId: {
|
||||||
|
immediate: true,
|
||||||
|
handler (value) {
|
||||||
|
if (value) {
|
||||||
|
this.loadLogs()
|
||||||
|
} else {
|
||||||
|
this.logs = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
this.stopAutoRefresh()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
tt (key, fallback, params) {
|
||||||
|
const translated = this.$t(key, params)
|
||||||
|
return translated !== key ? translated : fallback
|
||||||
|
},
|
||||||
|
async loadLogs () {
|
||||||
|
if (!this.strategyId) return
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await getStrategyLogs(this.strategyId, 200)
|
||||||
|
if (res && res.code === 1 && Array.isArray(res.data)) {
|
||||||
|
this.logs = res.data
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToBottom()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleAutoRefresh (checked) {
|
||||||
|
this.autoRefresh = checked
|
||||||
|
if (checked) {
|
||||||
|
this.stopAutoRefresh()
|
||||||
|
this.refreshTimer = setInterval(() => {
|
||||||
|
this.loadLogs()
|
||||||
|
}, 5000)
|
||||||
|
} else {
|
||||||
|
this.stopAutoRefresh()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stopAutoRefresh () {
|
||||||
|
if (this.refreshTimer) {
|
||||||
|
clearInterval(this.refreshTimer)
|
||||||
|
this.refreshTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollToBottom () {
|
||||||
|
const element = this.$refs.logsContainer
|
||||||
|
if (element) {
|
||||||
|
element.scrollTop = element.scrollHeight
|
||||||
|
}
|
||||||
|
},
|
||||||
|
countByLevel (level) {
|
||||||
|
return this.logs.filter(item => item.level === level).length
|
||||||
|
},
|
||||||
|
formatTime (value) {
|
||||||
|
if (!value) return '--'
|
||||||
|
try {
|
||||||
|
const date = new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return date.toLocaleTimeString('en-GB', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getLevelColor (level) {
|
||||||
|
return {
|
||||||
|
info: 'blue',
|
||||||
|
warn: 'orange',
|
||||||
|
error: 'red',
|
||||||
|
trade: 'green',
|
||||||
|
signal: 'purple'
|
||||||
|
}[level] || 'default'
|
||||||
|
},
|
||||||
|
getLevelText (level) {
|
||||||
|
const key = `trading-assistant.logs.level.${level}`
|
||||||
|
const fallback = {
|
||||||
|
info: 'Info',
|
||||||
|
warn: 'Warn',
|
||||||
|
error: 'Error',
|
||||||
|
trade: 'Trade',
|
||||||
|
signal: 'Signal'
|
||||||
|
}[level] || String(level || 'info')
|
||||||
|
return this.tt(key, fallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.strategy-logs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 360px;
|
||||||
|
|
||||||
|
.logs-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-container {
|
||||||
|
max-height: 420px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid #e5edf5;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 220px;
|
||||||
|
color: #94a3b8;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 88px auto 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-trade {
|
||||||
|
background: rgba(34, 197, 94, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-signal {
|
||||||
|
background: rgba(168, 85, 247, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-error {
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
color: #64748b;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1.55;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.theme-dark {
|
||||||
|
.toolbar-right {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-container {
|
||||||
|
background: linear-gradient(180deg, #151d2f 0%, #101827 100%);
|
||||||
|
border-color: #25324a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-empty,
|
||||||
|
.log-time {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="trading-records">
|
<div class="trading-records" :class="{ 'theme-dark': isDark }">
|
||||||
<div v-if="records.length === 0 && !loading" class="empty-state">
|
<div v-if="records.length === 0 && !loading" class="empty-state">
|
||||||
<a-empty :description="$t('trading-assistant.table.noPositions')" />
|
<a-empty :description="$t('trading-assistant.table.noPositions')" />
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +55,10 @@ export default {
|
|||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
isDark: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@@ -1,5 +1,57 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="trading-assistant" :class="{ 'theme-dark': isDarkTheme }">
|
<div class="trading-assistant" :class="{ 'theme-dark': isDarkTheme }">
|
||||||
|
<div v-if="showAssistantGuide" class="assistant-guide-bar">
|
||||||
|
<div class="assistant-guide-copy">
|
||||||
|
<div class="assistant-guide-eyebrow">{{ tt('trading-assistant.guide.eyebrow', 'Trading Workflow') }}</div>
|
||||||
|
<div class="assistant-guide-title">{{ tt('trading-assistant.guide.title', 'Build, launch, and monitor strategies from one workspace') }}</div>
|
||||||
|
<div class="assistant-guide-desc">{{ tt('trading-assistant.guide.desc', 'The latest private frontend adds a clearer entry flow for strategy creation and management. This screen now mirrors that structure more closely.') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="assistant-guide-steps">
|
||||||
|
<div class="assistant-step-card">
|
||||||
|
<div class="assistant-step-index">1</div>
|
||||||
|
<div class="assistant-step-body">
|
||||||
|
<div class="assistant-step-title">{{ tt('trading-assistant.guide.step1Title', 'Choose an indicator') }}</div>
|
||||||
|
<div class="assistant-step-desc">{{ tt('trading-assistant.guide.step1Desc', 'Start from your own or purchased indicator, then define market and symbol scope.') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="assistant-step-card">
|
||||||
|
<div class="assistant-step-index">2</div>
|
||||||
|
<div class="assistant-step-body">
|
||||||
|
<div class="assistant-step-title">{{ tt('trading-assistant.guide.step2Title', 'Configure execution') }}</div>
|
||||||
|
<div class="assistant-step-desc">{{ tt('trading-assistant.guide.step2Desc', 'Set notifications, risk controls, and optional live-trading credentials.') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="assistant-step-card">
|
||||||
|
<div class="assistant-step-index">3</div>
|
||||||
|
<div class="assistant-step-body">
|
||||||
|
<div class="assistant-step-title">{{ tt('trading-assistant.guide.step3Title', 'Track runtime data') }}</div>
|
||||||
|
<div class="assistant-step-desc">{{ tt('trading-assistant.guide.step3Desc', 'Review positions, trade history, performance, and runtime logs after launch.') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="assistant-guide-actions">
|
||||||
|
<a-button @click="goToStrategyTab">
|
||||||
|
<a-icon type="appstore" />
|
||||||
|
{{ tt('trading-assistant.guide.secondary', 'Open Strategy Manager') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" @click="openCreateStrategyFromGuide">
|
||||||
|
<a-icon type="plus" />
|
||||||
|
{{ tt('trading-assistant.guide.primary', 'Create Strategy') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button class="assistant-guide-close" @click="dismissAssistantGuide">
|
||||||
|
<a-icon type="close" />
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-tabs v-model="topTab" class="top-level-tabs" :animated="false">
|
||||||
|
<a-tab-pane key="overview">
|
||||||
|
<span slot="tab">{{ tt('trading-assistant.tabs.overview', 'Overview') }}</span>
|
||||||
|
<dashboard-overview v-if="topTab === 'overview'" hide-setup-guide />
|
||||||
|
</a-tab-pane>
|
||||||
|
|
||||||
|
<a-tab-pane key="strategy">
|
||||||
|
<span slot="tab">{{ tt('trading-assistant.tabs.strategyManage', 'Strategy Manager') }}</span>
|
||||||
<a-row :gutter="24" class="strategy-layout">
|
<a-row :gutter="24" class="strategy-layout">
|
||||||
<!-- 左侧:策略列表 -->
|
<!-- 左侧:策略列表 -->
|
||||||
<a-col
|
<a-col
|
||||||
@@ -34,7 +86,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading">
|
||||||
<a-empty v-if="!loading && strategies.length === 0" :description="$t('trading-assistant.noStrategy')" />
|
<div v-if="!loading && strategies.length === 0" class="strategy-empty-state">
|
||||||
|
<a-empty :description="tt('trading-assistant.empty.title', 'No strategy yet')" />
|
||||||
|
<div class="strategy-empty-desc">
|
||||||
|
{{ tt('trading-assistant.empty.desc', 'Create a strategy from one of your indicators or open a prefilled strategy from indicator-analysis.') }}
|
||||||
|
</div>
|
||||||
|
<div class="strategy-empty-path">
|
||||||
|
{{ tt('trading-assistant.empty.path', 'Indicator Analysis -> Strategy Rocket -> Trading Assistant') }}
|
||||||
|
</div>
|
||||||
|
<a-button type="primary" @click="openCreateStrategyFromGuide">
|
||||||
|
<a-icon type="plus" />
|
||||||
|
{{ tt('trading-assistant.empty.primary', 'Create your first strategy') }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else class="strategy-grouped-list">
|
<div v-else class="strategy-grouped-list">
|
||||||
<!-- 策略组列表 -->
|
<!-- 策略组列表 -->
|
||||||
<div v-for="group in groupedStrategies.groups" :key="group.id" class="strategy-group">
|
<div v-for="group in groupedStrategies.groups" :key="group.id" class="strategy-group">
|
||||||
@@ -79,19 +144,30 @@
|
|||||||
<div
|
<div
|
||||||
v-for="item in group.strategies"
|
v-for="item in group.strategies"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:class="['strategy-list-item', { active: selectedStrategy && selectedStrategy.id === item.id }]"
|
:class="[
|
||||||
|
'strategy-list-item',
|
||||||
|
{ active: selectedStrategy && selectedStrategy.id === item.id },
|
||||||
|
{ 'strategy-list-item--strategy-group': groupByMode === 'strategy' }
|
||||||
|
]"
|
||||||
@click="handleSelectStrategy(item)">
|
@click="handleSelectStrategy(item)">
|
||||||
<div class="strategy-item-content">
|
<div class="strategy-item-content">
|
||||||
<div class="strategy-item-header">
|
<div class="strategy-item-header">
|
||||||
<div class="strategy-name-wrapper">
|
<div :class="['strategy-name-wrapper', { 'strategy-name-wrapper--grouped': groupByMode === 'symbol' }]">
|
||||||
<!-- 按策略分组:显示 Symbol -->
|
|
||||||
<template v-if="groupByMode === 'strategy'">
|
<template v-if="groupByMode === 'strategy'">
|
||||||
<span class="info-item" v-if="item.trading_config && item.trading_config.symbol">
|
<span class="strategy-name">{{ item.strategy_name }}</span>
|
||||||
<a-icon type="dollar" />
|
<a-tag
|
||||||
{{ item.trading_config.symbol }}
|
v-if="item.strategy_type === 'PromptBasedStrategy'"
|
||||||
</span>
|
color="purple"
|
||||||
|
size="small"
|
||||||
|
class="strategy-type-tag">
|
||||||
|
<a-icon type="robot" style="margin-right: 2px;" />
|
||||||
|
AI
|
||||||
|
</a-tag>
|
||||||
|
<a-tag v-if="item.strategy_mode === 'script'" size="small" color="green">
|
||||||
|
<a-icon type="code" style="margin-right: 2px;" />
|
||||||
|
{{ tt('trading-assistant.strategyMode.script', 'Script Strategy') }}
|
||||||
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<!-- 按 Symbol 分组:显示策略名称、周期、指标 -->
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="info-item strategy-name-text">
|
<span class="info-item strategy-name-text">
|
||||||
<a-icon type="thunderbolt" />
|
<a-icon type="thunderbolt" />
|
||||||
@@ -105,6 +181,27 @@
|
|||||||
<a-icon type="line-chart" style="margin-right: 2px;" />
|
<a-icon type="line-chart" style="margin-right: 2px;" />
|
||||||
{{ item.displayInfo.indicatorName }}
|
{{ item.displayInfo.indicatorName }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
|
<a-tag v-if="item.strategy_mode === 'script'" size="small" color="green">
|
||||||
|
<a-icon type="code" style="margin-right: 2px;" />
|
||||||
|
{{ tt('trading-assistant.strategyMode.script', 'Script Strategy') }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="strategy-item-info">
|
||||||
|
<template v-if="groupByMode === 'strategy'">
|
||||||
|
<span class="info-item" v-if="item.trading_config && item.trading_config.symbol">
|
||||||
|
<a-icon type="dollar" />
|
||||||
|
{{ item.trading_config.symbol }}
|
||||||
|
</span>
|
||||||
|
<span class="info-item" v-if="item.exchange_config && item.exchange_config.exchange_id">
|
||||||
|
<a-icon type="bank" />
|
||||||
|
{{ getExchangeDisplayName(item.exchange_config.exchange_id) }}
|
||||||
|
</span>
|
||||||
|
<span class="info-item" v-if="item.trading_config && item.trading_config.timeframe">
|
||||||
|
<a-icon type="clock-circle" />
|
||||||
|
{{ item.trading_config.timeframe }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<span
|
<span
|
||||||
class="status-label"
|
class="status-label"
|
||||||
@@ -116,7 +213,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="strategy-item-actions" @click.stop>
|
<div class="strategy-item-actions" @click.stop>
|
||||||
<a-dropdown :getPopupContainer="getDropdownContainer" :trigger="['click']">
|
<a-dropdown :getPopupContainer="getDropdownContainer" :trigger="['click']">
|
||||||
<a-menu slot="overlay" @click="({ key }) => handleMenuClick(key, item)">
|
<a-menu slot="overlay" @click="({ key }) => handleMenuClick(key, item)">
|
||||||
@@ -172,6 +268,10 @@
|
|||||||
<a-icon type="robot" style="margin-right: 2px;" />
|
<a-icon type="robot" style="margin-right: 2px;" />
|
||||||
AI
|
AI
|
||||||
</a-tag>
|
</a-tag>
|
||||||
|
<a-tag v-if="item.strategy_mode === 'script'" size="small" color="green">
|
||||||
|
<a-icon type="code" style="margin-right: 2px;" />
|
||||||
|
{{ tt('trading-assistant.strategyMode.script', 'Script Strategy') }}
|
||||||
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="strategy-item-info">
|
<div class="strategy-item-info">
|
||||||
@@ -228,8 +328,24 @@
|
|||||||
:lg="16"
|
:lg="16"
|
||||||
:xl="16"
|
:xl="16"
|
||||||
class="strategy-detail-col">
|
class="strategy-detail-col">
|
||||||
<div v-if="!selectedStrategy" class="empty-detail">
|
<div v-if="!selectedStrategy" class="strategy-empty-detail">
|
||||||
<a-empty :description="$t('trading-assistant.selectStrategy')" />
|
<div class="strategy-empty-detail-card">
|
||||||
|
<div class="strategy-empty-detail-icon">
|
||||||
|
<a-icon type="deployment-unit" />
|
||||||
|
</div>
|
||||||
|
<h3 class="strategy-empty-detail-title">
|
||||||
|
{{ tt('trading-assistant.emptyDetail.title', 'Select a strategy to inspect runtime details') }}
|
||||||
|
</h3>
|
||||||
|
<p class="strategy-empty-detail-hint">
|
||||||
|
{{ tt('trading-assistant.emptyDetail.hint', 'Open a strategy from the left list to review positions, trades, performance, and logs in one place.') }}
|
||||||
|
</p>
|
||||||
|
<div class="strategy-empty-detail-actions">
|
||||||
|
<a-button type="primary" @click="handleCreateStrategy">
|
||||||
|
<a-icon type="plus" />
|
||||||
|
{{ $t('trading-assistant.createStrategy') }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="strategy-detail-panel">
|
<div v-else class="strategy-detail-panel">
|
||||||
@@ -346,16 +462,25 @@
|
|||||||
:strategy-id="selectedStrategy.id"
|
:strategy-id="selectedStrategy.id"
|
||||||
:market-type="(selectedStrategy.trading_config && selectedStrategy.trading_config.market_type) || 'swap'"
|
:market-type="(selectedStrategy.trading_config && selectedStrategy.trading_config.market_type) || 'swap'"
|
||||||
:leverage="(selectedStrategy.trading_config && selectedStrategy.trading_config.leverage) || 1"
|
:leverage="(selectedStrategy.trading_config && selectedStrategy.trading_config.leverage) || 1"
|
||||||
:loading="loadingRecords" />
|
:loading="loadingRecords"
|
||||||
|
:is-dark="isDarkTheme" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="trades" :tab="$t('trading-assistant.tabs.tradingRecords')">
|
<a-tab-pane key="trades" :tab="$t('trading-assistant.tabs.tradingRecords')">
|
||||||
<trading-records :strategy-id="selectedStrategy.id" :loading="loadingRecords" />
|
<trading-records :strategy-id="selectedStrategy.id" :loading="loadingRecords" :is-dark="isDarkTheme" />
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="performance" :tab="tt('trading-assistant.tabs.performance', 'Performance')">
|
||||||
|
<performance-analysis :strategy-id="selectedStrategy.id" :is-dark="isDarkTheme" />
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="logs" :tab="tt('trading-assistant.tabs.logs', 'Logs')">
|
||||||
|
<strategy-logs :strategy-id="selectedStrategy.id" :is-dark="isDarkTheme" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
|
||||||
<!-- 创建/编辑策略弹窗 - 合并版本 -->
|
<!-- 创建/编辑策略弹窗 - 合并版本 -->
|
||||||
<a-modal
|
<a-modal
|
||||||
@@ -399,6 +524,7 @@
|
|||||||
v-decorator="['indicator_id', { rules: [{ required: true, message: $t('trading-assistant.validation.indicatorRequired') }] }]"
|
v-decorator="['indicator_id', { rules: [{ required: true, message: $t('trading-assistant.validation.indicatorRequired') }] }]"
|
||||||
:placeholder="$t('trading-assistant.placeholders.selectIndicator')"
|
:placeholder="$t('trading-assistant.placeholders.selectIndicator')"
|
||||||
show-search
|
show-search
|
||||||
|
optionLabelProp="label"
|
||||||
:filter-option="filterIndicatorOption"
|
:filter-option="filterIndicatorOption"
|
||||||
@focus="handleIndicatorSelectFocus"
|
@focus="handleIndicatorSelectFocus"
|
||||||
@change="handleIndicatorChange"
|
@change="handleIndicatorChange"
|
||||||
@@ -407,9 +533,13 @@
|
|||||||
<a-select-option
|
<a-select-option
|
||||||
v-for="indicator in availableIndicators"
|
v-for="indicator in availableIndicators"
|
||||||
:key="String(indicator.id)"
|
:key="String(indicator.id)"
|
||||||
:value="String(indicator.id)">
|
:value="String(indicator.id)"
|
||||||
|
:label="getIndicatorOptionLabel(indicator)">
|
||||||
<div class="indicator-option">
|
<div class="indicator-option">
|
||||||
|
<div class="indicator-option-main">
|
||||||
<span class="indicator-name">{{ indicator.name }}</span>
|
<span class="indicator-name">{{ indicator.name }}</span>
|
||||||
|
<span v-if="indicator.description" class="indicator-option-desc">{{ indicator.description }}</span>
|
||||||
|
</div>
|
||||||
<a-tag v-if="indicator.type" size="small" :color="getIndicatorTypeColor(indicator.type)">
|
<a-tag v-if="indicator.type" size="small" :color="getIndicatorTypeColor(indicator.type)">
|
||||||
{{ getIndicatorTypeName(indicator.type) }}
|
{{ getIndicatorTypeName(indicator.type) }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
@@ -419,12 +549,34 @@
|
|||||||
<div class="form-item-hint">
|
<div class="form-item-hint">
|
||||||
{{ $t('trading-assistant.form.indicatorHint') }}
|
{{ $t('trading-assistant.form.indicatorHint') }}
|
||||||
</div>
|
</div>
|
||||||
|
<a-alert
|
||||||
|
v-if="!loadingIndicators && availableIndicators.length === 0"
|
||||||
|
style="margin-top: 12px;"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
:message="tt('trading-assistant.indicatorEmpty.title', 'No indicator available yet')"
|
||||||
|
:description="tt('trading-assistant.indicatorEmpty.desc', 'Create or purchase an indicator first, then come back to build a strategy from it.')">
|
||||||
|
<template slot="action">
|
||||||
|
<a-button type="primary" size="small" @click="goToIndicatorAnalysisCreate">
|
||||||
|
<a-icon type="rocket" />
|
||||||
|
{{ tt('trading-assistant.indicatorEmpty.cta', 'Open Indicator Analysis') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</a-alert>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="selectedIndicator" :label="$t('trading-assistant.form.indicatorDescription')">
|
<a-form-item v-if="selectedIndicator" :label="$t('trading-assistant.form.indicatorDescription')">
|
||||||
|
<div class="selected-indicator-card">
|
||||||
|
<div class="selected-indicator-header">
|
||||||
|
<span class="selected-indicator-name">{{ selectedIndicator.name }}</span>
|
||||||
|
<a-tag v-if="selectedIndicator.type" size="small" :color="getIndicatorTypeColor(selectedIndicator.type)">
|
||||||
|
{{ getIndicatorTypeName(selectedIndicator.type) }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
<div class="indicator-description">
|
<div class="indicator-description">
|
||||||
{{ selectedIndicator.description || $t('trading-assistant.form.noDescription') }}
|
{{ selectedIndicator.description || $t('trading-assistant.form.noDescription') }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<!-- 指标参数配置 -->
|
<!-- 指标参数配置 -->
|
||||||
@@ -1541,14 +1693,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getStrategyList, startStrategy, stopStrategy, deleteStrategy, updateStrategy, createStrategy, testExchangeConnection, getStrategyEquityCurve, batchCreateStrategies, batchStartStrategies, batchStopStrategies, batchDeleteStrategies } from '@/api/strategy'
|
import { getStrategyList, startStrategy, stopStrategy, deleteStrategy, updateStrategy, createStrategy, testExchangeConnection, getStrategyEquityCurve, getStrategyPositions, batchCreateStrategies, batchStartStrategies, batchStopStrategies, batchDeleteStrategies } from '@/api/strategy'
|
||||||
import { getWatchlist, addWatchlist, searchSymbols, getHotSymbols } from '@/api/market'
|
import { getWatchlist, addWatchlist, searchSymbols, getHotSymbols } from '@/api/market'
|
||||||
import { listExchangeCredentials, getExchangeCredential, createExchangeCredential } from '@/api/credentials'
|
import { listExchangeCredentials, getExchangeCredential, createExchangeCredential } from '@/api/credentials'
|
||||||
import { getNotificationSettings } from '@/api/user'
|
import { getNotificationSettings } from '@/api/user'
|
||||||
import { baseMixin } from '@/store/app-mixin'
|
import { baseMixin } from '@/store/app-mixin'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
import DashboardOverview from '@/views/dashboard'
|
||||||
import TradingRecords from './components/TradingRecords.vue'
|
import TradingRecords from './components/TradingRecords.vue'
|
||||||
import PositionRecords from './components/PositionRecords.vue'
|
import PositionRecords from './components/PositionRecords.vue'
|
||||||
|
import PerformanceAnalysis from './components/PerformanceAnalysis.vue'
|
||||||
|
import StrategyLogs from './components/StrategyLogs.vue'
|
||||||
|
|
||||||
// 常见加密货币交易对
|
// 常见加密货币交易对
|
||||||
const CRYPTO_SYMBOLS = [
|
const CRYPTO_SYMBOLS = [
|
||||||
@@ -1593,10 +1748,20 @@ export default {
|
|||||||
name: 'TradingAssistant',
|
name: 'TradingAssistant',
|
||||||
mixins: [baseMixin],
|
mixins: [baseMixin],
|
||||||
components: {
|
components: {
|
||||||
|
DashboardOverview,
|
||||||
TradingRecords,
|
TradingRecords,
|
||||||
PositionRecords
|
PositionRecords,
|
||||||
|
PerformanceAnalysis,
|
||||||
|
StrategyLogs
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
showAssistantGuide () {
|
||||||
|
return !this.assistantGuideDismissed
|
||||||
|
},
|
||||||
|
assistantGuideStorageKey () {
|
||||||
|
const userId = (this.$store.getters.userInfo && this.$store.getters.userInfo.id) || 'guest'
|
||||||
|
return `trading-assistant-guide-dismissed:${userId}`
|
||||||
|
},
|
||||||
isAdvancedMode () {
|
isAdvancedMode () {
|
||||||
return this.creationMode === 'advanced'
|
return this.creationMode === 'advanced'
|
||||||
},
|
},
|
||||||
@@ -1656,17 +1821,19 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
totalPnl () {
|
totalPnl () {
|
||||||
if (this.currentEquity === null || !this.selectedStrategy || !this.selectedStrategy.initial_capital) {
|
const initialCapital = this.selectedStrategy?.initial_capital || this.selectedStrategy?.trading_config?.initial_capital
|
||||||
|
if (this.currentEquity === null || !this.selectedStrategy || !initialCapital) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return this.currentEquity - (this.selectedStrategy.initial_capital || 0)
|
return this.currentEquity - initialCapital
|
||||||
},
|
},
|
||||||
totalPnlPercent () {
|
totalPnlPercent () {
|
||||||
if (this.totalPnl === null || !this.selectedStrategy || !this.selectedStrategy.initial_capital) {
|
const initialCapital = this.selectedStrategy?.initial_capital || this.selectedStrategy?.trading_config?.initial_capital
|
||||||
|
if (this.totalPnl === null || !this.selectedStrategy || !initialCapital) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (this.selectedStrategy.initial_capital === 0) return 0
|
if (initialCapital === 0) return 0
|
||||||
return (this.totalPnl / this.selectedStrategy.initial_capital) * 100
|
return (this.totalPnl / initialCapital) * 100
|
||||||
},
|
},
|
||||||
getEquityColorClass () {
|
getEquityColorClass () {
|
||||||
if (this.totalPnl === null) return ''
|
if (this.totalPnl === null) return ''
|
||||||
@@ -1906,6 +2073,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
topTab: 'overview',
|
||||||
loading: false,
|
loading: false,
|
||||||
loadingRecords: false,
|
loadingRecords: false,
|
||||||
strategies: [],
|
strategies: [],
|
||||||
@@ -1989,7 +2157,9 @@ export default {
|
|||||||
addingSymbol: false,
|
addingSymbol: false,
|
||||||
hotSymbols: [],
|
hotSymbols: [],
|
||||||
loadingHotSymbols: false,
|
loadingHotSymbols: false,
|
||||||
searchTimer: null
|
searchTimer: null,
|
||||||
|
lastAutoStrategyName: '',
|
||||||
|
assistantGuideDismissed: false
|
||||||
// Market category is inferred from Step 1 watchlist symbol ("Market:SYMBOL").
|
// Market category is inferred from Step 1 watchlist symbol ("Market:SYMBOL").
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1997,6 +2167,17 @@ export default {
|
|||||||
this.form = this.$form.createForm(this)
|
this.form = this.$form.createForm(this)
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
this.restoreAssistantGuidePreference()
|
||||||
|
if (
|
||||||
|
this.$route &&
|
||||||
|
(
|
||||||
|
(this.$route.query && this.$route.query.tab === 'strategy') ||
|
||||||
|
(this.$route.query && this.$route.query.open === 'from-indicator') ||
|
||||||
|
(this.$route.query && this.$route.query.mode === 'create')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.topTab = 'strategy'
|
||||||
|
}
|
||||||
this.loadStrategies()
|
this.loadStrategies()
|
||||||
this.loadUserNotificationSettings()
|
this.loadUserNotificationSettings()
|
||||||
this.handleRouteCreateStrategyPrefill()
|
this.handleRouteCreateStrategyPrefill()
|
||||||
@@ -2005,6 +2186,35 @@ export default {
|
|||||||
this.stopEquityPolling()
|
this.stopEquityPolling()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
tt (key, fallback, params) {
|
||||||
|
const translated = this.$t(key, params)
|
||||||
|
return translated !== key ? translated : fallback
|
||||||
|
},
|
||||||
|
restoreAssistantGuidePreference () {
|
||||||
|
try {
|
||||||
|
this.assistantGuideDismissed = window.localStorage.getItem(this.assistantGuideStorageKey) === '1'
|
||||||
|
} catch (e) {
|
||||||
|
this.assistantGuideDismissed = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissAssistantGuide () {
|
||||||
|
this.assistantGuideDismissed = true
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(this.assistantGuideStorageKey, '1')
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
goToStrategyTab () {
|
||||||
|
this.topTab = 'strategy'
|
||||||
|
},
|
||||||
|
openCreateStrategyFromGuide () {
|
||||||
|
this.topTab = 'strategy'
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.handleCreateStrategy()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goToIndicatorAnalysisCreate () {
|
||||||
|
this.$router.push('/indicator-analysis')
|
||||||
|
},
|
||||||
async loadUserNotificationSettings () {
|
async loadUserNotificationSettings () {
|
||||||
// Load user's default notification settings from profile
|
// Load user's default notification settings from profile
|
||||||
try {
|
try {
|
||||||
@@ -2575,13 +2785,22 @@ export default {
|
|||||||
this.selectedMarketCategory = 'Crypto'
|
this.selectedMarketCategory = 'Crypto'
|
||||||
this.selectedSymbols = []
|
this.selectedSymbols = []
|
||||||
this.showAdvancedSettings = false
|
this.showAdvancedSettings = false
|
||||||
|
const defaultName = this.buildStrategyDefaultName()
|
||||||
|
this.lastAutoStrategyName = defaultName
|
||||||
|
|
||||||
this.form.resetFields()
|
this.form.resetFields()
|
||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
|
strategy_name: defaultName,
|
||||||
execution_mode: 'signal',
|
execution_mode: 'signal',
|
||||||
notify_channels: ['browser'],
|
notify_channels: ['browser'],
|
||||||
save_credential: false,
|
save_credential: false,
|
||||||
live_disclaimer_ack: false
|
live_disclaimer_ack: false,
|
||||||
|
initial_capital: 1000,
|
||||||
|
market_type: 'swap',
|
||||||
|
leverage: 5,
|
||||||
|
trade_direction: 'long',
|
||||||
|
timeframe: '15m',
|
||||||
|
cs_strategy_type: 'single'
|
||||||
})
|
})
|
||||||
this.liveDisclaimerAckUi = false
|
this.liveDisclaimerAckUi = false
|
||||||
this.showFormModal = true
|
this.showFormModal = true
|
||||||
@@ -2594,10 +2813,14 @@ export default {
|
|||||||
},
|
},
|
||||||
async handleRouteCreateStrategyPrefill () {
|
async handleRouteCreateStrategyPrefill () {
|
||||||
const query = (this.$route && this.$route.query) || {}
|
const query = (this.$route && this.$route.query) || {}
|
||||||
if (query.open !== 'from-indicator') {
|
const isIndicatorPrefill = query.open === 'from-indicator'
|
||||||
|
const isCreateMode = query.mode === 'create'
|
||||||
|
|
||||||
|
if (!isIndicatorPrefill && !isCreateMode) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.topTab = 'strategy'
|
||||||
this.handleCreateStrategy()
|
this.handleCreateStrategy()
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -2609,7 +2832,7 @@ export default {
|
|||||||
const indicatorId = query.indicator_id ? String(query.indicator_id) : ''
|
const indicatorId = query.indicator_id ? String(query.indicator_id) : ''
|
||||||
const market = query.market || 'Crypto'
|
const market = query.market || 'Crypto'
|
||||||
const symbol = query.symbol || ''
|
const symbol = query.symbol || ''
|
||||||
const timeframe = query.timeframe || '1D'
|
const timeframe = query.timeframe || ''
|
||||||
const fullSymbol = symbol ? (String(symbol).includes(':') ? String(symbol) : `${market}:${symbol}`) : ''
|
const fullSymbol = symbol ? (String(symbol).includes(':') ? String(symbol) : `${market}:${symbol}`) : ''
|
||||||
|
|
||||||
this.selectedMarketCategory = market
|
this.selectedMarketCategory = market
|
||||||
@@ -2617,8 +2840,9 @@ export default {
|
|||||||
this.handleMultiSymbolChange([fullSymbol])
|
this.handleMultiSymbolChange([fullSymbol])
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldValues = {
|
const fieldValues = {}
|
||||||
timeframe
|
if (timeframe) {
|
||||||
|
fieldValues.timeframe = timeframe
|
||||||
}
|
}
|
||||||
if (indicatorId) {
|
if (indicatorId) {
|
||||||
fieldValues.indicator_id = indicatorId
|
fieldValues.indicator_id = indicatorId
|
||||||
@@ -2633,6 +2857,7 @@ export default {
|
|||||||
this.form.setFieldsValue({
|
this.form.setFieldsValue({
|
||||||
strategy_name: strategyName
|
strategy_name: strategyName
|
||||||
})
|
})
|
||||||
|
this.lastAutoStrategyName = strategyName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2956,21 +3181,34 @@ export default {
|
|||||||
if (!this.selectedStrategy) {
|
if (!this.selectedStrategy) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
// 加载净值数据
|
|
||||||
try {
|
try {
|
||||||
const res = await getStrategyEquityCurve(this.selectedStrategy.id)
|
const [equityRes, positionsRes] = await Promise.all([
|
||||||
if (res.code === 1 && res.data) {
|
getStrategyEquityCurve(this.selectedStrategy.id),
|
||||||
// Local backend returns an array curve: [{ time, equity }, ...]
|
getStrategyPositions(this.selectedStrategy.id)
|
||||||
if (Array.isArray(res.data) && res.data.length > 0) {
|
])
|
||||||
const last = res.data[res.data.length - 1]
|
|
||||||
this.currentEquity = last.equity
|
let baseEquity = null
|
||||||
|
if (equityRes.code === 1 && equityRes.data) {
|
||||||
|
if (Array.isArray(equityRes.data) && equityRes.data.length > 0) {
|
||||||
|
const last = equityRes.data[equityRes.data.length - 1]
|
||||||
|
baseEquity = last.equity
|
||||||
} else {
|
} else {
|
||||||
const base = this.selectedStrategy.trading_config?.initial_capital || this.selectedStrategy.initial_capital
|
baseEquity = this.selectedStrategy.trading_config?.initial_capital || this.selectedStrategy.initial_capital || null
|
||||||
this.currentEquity = base || null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
|
let unrealizedPnl = 0
|
||||||
|
if (positionsRes.code === 1 && positionsRes.data) {
|
||||||
|
const positions = Array.isArray(positionsRes.data)
|
||||||
|
? positionsRes.data
|
||||||
|
: (positionsRes.data.positions || positionsRes.data.items || [])
|
||||||
|
positions.forEach(position => {
|
||||||
|
unrealizedPnl += parseFloat(position.unrealized_pnl || position.unrealizedPnl || 0)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.currentEquity = baseEquity !== null ? baseEquity + unrealizedPnl : null
|
||||||
|
} catch (error) {}
|
||||||
},
|
},
|
||||||
startEquityPolling () {
|
startEquityPolling () {
|
||||||
this.stopEquityPolling()
|
this.stopEquityPolling()
|
||||||
@@ -2979,10 +3217,10 @@ export default {
|
|||||||
// 初始加载一次
|
// 初始加载一次
|
||||||
this.loadStrategyDetails()
|
this.loadStrategyDetails()
|
||||||
|
|
||||||
// 每30秒轮询一次
|
// Dist polls more aggressively to keep the detail header responsive.
|
||||||
this.equityPollingTimer = setInterval(() => {
|
this.equityPollingTimer = setInterval(() => {
|
||||||
this.loadStrategyDetails()
|
this.loadStrategyDetails()
|
||||||
}, 30000)
|
}, 10000)
|
||||||
},
|
},
|
||||||
stopEquityPolling () {
|
stopEquityPolling () {
|
||||||
if (this.equityPollingTimer) {
|
if (this.equityPollingTimer) {
|
||||||
@@ -3009,6 +3247,7 @@ export default {
|
|||||||
this.showAdvancedSettings = false
|
this.showAdvancedSettings = false
|
||||||
this.executionModeUi = 'signal'
|
this.executionModeUi = 'signal'
|
||||||
this.liveDisclaimerAckUi = false
|
this.liveDisclaimerAckUi = false
|
||||||
|
this.lastAutoStrategyName = ''
|
||||||
|
|
||||||
this.form.resetFields()
|
this.form.resetFields()
|
||||||
},
|
},
|
||||||
@@ -3262,6 +3501,7 @@ export default {
|
|||||||
async handleIndicatorChange (indicatorId) {
|
async handleIndicatorChange (indicatorId) {
|
||||||
const idStr = String(indicatorId)
|
const idStr = String(indicatorId)
|
||||||
this.selectedIndicator = this.availableIndicators.find(ind => String(ind.id) === idStr)
|
this.selectedIndicator = this.availableIndicators.find(ind => String(ind.id) === idStr)
|
||||||
|
this.applyAutoStrategyName(this.selectedIndicator)
|
||||||
|
|
||||||
// 获取指标参数声明
|
// 获取指标参数声明
|
||||||
this.indicatorParams = []
|
this.indicatorParams = []
|
||||||
@@ -3367,8 +3607,29 @@ export default {
|
|||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
},
|
},
|
||||||
filterIndicatorOption (input, option) {
|
filterIndicatorOption (input, option) {
|
||||||
const text = option.componentOptions.children[0].children[0].text
|
const label = option.componentOptions?.propsData?.label || ''
|
||||||
return text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0
|
||||||
|
},
|
||||||
|
getIndicatorOptionLabel (indicator) {
|
||||||
|
if (!indicator) return ''
|
||||||
|
return indicator.description ? `${indicator.name} - ${indicator.description}` : indicator.name
|
||||||
|
},
|
||||||
|
buildStrategyDefaultName (indicator) {
|
||||||
|
const suffix = this.tt('trading-assistant.form.defaultStrategySuffix', 'Strategy')
|
||||||
|
if (indicator && indicator.name) {
|
||||||
|
return `${indicator.name} ${suffix}`.trim()
|
||||||
|
}
|
||||||
|
return this.tt('trading-assistant.form.defaultStrategyName', 'New Strategy')
|
||||||
|
},
|
||||||
|
applyAutoStrategyName (indicator) {
|
||||||
|
if (this.editingStrategy || !this.form) return
|
||||||
|
const nextName = this.buildStrategyDefaultName(indicator)
|
||||||
|
const currentName = this.form.getFieldValue('strategy_name')
|
||||||
|
if (currentName && currentName !== this.lastAutoStrategyName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.form.setFieldsValue({ strategy_name: nextName })
|
||||||
|
this.lastAutoStrategyName = nextName
|
||||||
},
|
},
|
||||||
filterSymbolOption (input, option) {
|
filterSymbolOption (input, option) {
|
||||||
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||||
@@ -3379,9 +3640,11 @@ export default {
|
|||||||
momentum: 'green',
|
momentum: 'green',
|
||||||
volatility: 'orange',
|
volatility: 'orange',
|
||||||
volume: 'purple',
|
volume: 'purple',
|
||||||
custom: 'default'
|
custom: 'cyan',
|
||||||
|
python: 'geekblue',
|
||||||
|
pine: 'magenta'
|
||||||
}
|
}
|
||||||
return colors[type] || 'default'
|
return colors[type] || 'cyan'
|
||||||
},
|
},
|
||||||
getIndicatorTypeName (type) {
|
getIndicatorTypeName (type) {
|
||||||
return this.$t(`trading-assistant.indicatorType.${type}`) || type
|
return this.$t(`trading-assistant.indicatorType.${type}`) || type
|
||||||
@@ -4070,11 +4333,218 @@ export default {
|
|||||||
|
|
||||||
.trading-assistant {
|
.trading-assistant {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
height: calc(100vh - 120px);
|
min-height: calc(100vh - 120px);
|
||||||
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
|
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
|
|
||||||
|
.assistant-guide-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) auto;
|
||||||
|
gap: 18px;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 20px 22px;
|
||||||
|
border: 1px solid #dce7f3;
|
||||||
|
border-radius: 24px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(59, 130, 246, 0.18), transparent 38%),
|
||||||
|
radial-gradient(circle at bottom right, rgba(16, 185, 129, 0.12), transparent 34%),
|
||||||
|
linear-gradient(135deg, #ffffff 0%, #f8fbff 55%, #eef7ff 100%);
|
||||||
|
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-eyebrow {
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-title {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-desc {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-steps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-step-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-step-index {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
flex: 0 0 30px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(135deg, #2563eb 0%, #10b981 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-step-body {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-step-title {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-step-desc {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-close {
|
||||||
|
align-self: flex-end;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #64748b;
|
||||||
|
border-color: rgba(148, 163, 184, 0.28);
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-level-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 360px);
|
||||||
|
|
||||||
|
/deep/ .ant-tabs-bar {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .ant-tabs-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .ant-tabs-tabpane-active {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.strategy-layout {
|
.strategy-layout {
|
||||||
height: calc(100vh - 120px);
|
height: calc(100vh - 420px);
|
||||||
|
min-height: 680px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 32px 18px 36px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-desc {
|
||||||
|
max-width: 320px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-path {
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-detail {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-detail-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
padding: 36px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 26px;
|
||||||
|
border: 1px solid #dce7f3;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top, rgba(59, 130, 246, 0.1), transparent 42%),
|
||||||
|
linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||||
|
box-shadow: 0 16px 38px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-detail-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
margin: 0 auto 18px;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: linear-gradient(135deg, #2563eb 0%, #10b981 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 30px;
|
||||||
|
box-shadow: 0 14px 24px rgba(37, 99, 235, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-detail-title {
|
||||||
|
margin: 0;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-detail-hint {
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 12px auto 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-detail-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移动端适配
|
// 移动端适配
|
||||||
@@ -4082,6 +4552,28 @@ export default {
|
|||||||
min-height: auto;
|
min-height: auto;
|
||||||
margin: -24px;
|
margin: -24px;
|
||||||
|
|
||||||
|
.assistant-guide-bar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 18px;
|
||||||
|
border-radius: 0 0 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-title {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-steps {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-actions {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-level-tabs {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.strategy-layout {
|
.strategy-layout {
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: calc(100vh - 120px);
|
min-height: calc(100vh - 120px);
|
||||||
@@ -4606,6 +5098,10 @@ export default {
|
|||||||
.strategy-item-actions {
|
.strategy-item-actions {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.strategy-list-item--strategy-group {
|
||||||
|
margin-left: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4665,6 +5161,10 @@ export default {
|
|||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.strategy-name-wrapper--grouped {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.exchange-tag {
|
.exchange-tag {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -4812,6 +5312,48 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.indicator-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-option-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-option-desc {
|
||||||
|
margin-top: 2px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-indicator-card {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid #e5eef7;
|
||||||
|
background: linear-gradient(180deg, #fbfdff 0%, #f5f9ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-indicator-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-indicator-name {
|
||||||
|
color: #1e3a5f;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.strategy-detail-col {
|
.strategy-detail-col {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -5173,6 +5715,48 @@ export default {
|
|||||||
background: linear-gradient(180deg, #0d1117 0%, #161b22 100%);
|
background: linear-gradient(180deg, #0d1117 0%, #161b22 100%);
|
||||||
color: var(--dark-text-color, #fff);
|
color: var(--dark-text-color, #fff);
|
||||||
|
|
||||||
|
.assistant-guide-bar {
|
||||||
|
border-color: rgba(59, 130, 246, 0.16);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(37, 99, 235, 0.26), transparent 36%),
|
||||||
|
radial-gradient(circle at bottom right, rgba(16, 185, 129, 0.14), transparent 34%),
|
||||||
|
linear-gradient(135deg, #161b22 0%, #111827 58%, #0f172a 100%);
|
||||||
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-eyebrow {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-title,
|
||||||
|
.assistant-step-title {
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-desc,
|
||||||
|
.assistant-step-desc {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-step-card {
|
||||||
|
background: rgba(15, 23, 42, 0.62);
|
||||||
|
border-color: rgba(148, 163, 184, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-guide-close {
|
||||||
|
color: #cbd5e1;
|
||||||
|
border-color: rgba(148, 163, 184, 0.18);
|
||||||
|
background: rgba(15, 23, 42, 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-desc {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-path {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
.creation-mode-toggle {
|
.creation-mode-toggle {
|
||||||
background: rgba(24, 144, 255, 0.08);
|
background: rgba(24, 144, 255, 0.08);
|
||||||
border-color: rgba(24, 144, 255, 0.2);
|
border-color: rgba(24, 144, 255, 0.2);
|
||||||
@@ -5255,6 +5839,22 @@ export default {
|
|||||||
|
|
||||||
// 右侧策略详情卡片
|
// 右侧策略详情卡片
|
||||||
.strategy-detail-col {
|
.strategy-detail-col {
|
||||||
|
.strategy-empty-detail-card {
|
||||||
|
border-color: rgba(59, 130, 246, 0.18);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top, rgba(37, 99, 235, 0.16), transparent 42%),
|
||||||
|
linear-gradient(180deg, #161b22 0%, #111827 100%);
|
||||||
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-detail-title {
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-empty-detail-hint {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
.strategy-header-card {
|
.strategy-header-card {
|
||||||
background: linear-gradient(135deg, #1e222d 0%, #1a1e28 100%);
|
background: linear-gradient(135deg, #1e222d 0%, #1a1e28 100%);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
@@ -5370,6 +5970,19 @@ export default {
|
|||||||
color: #868993;
|
color: #868993;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-indicator-card {
|
||||||
|
border-color: rgba(59, 130, 246, 0.16);
|
||||||
|
background: linear-gradient(180deg, rgba(37, 99, 235, 0.08) 0%, rgba(15, 23, 42, 0.24) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-indicator-name {
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-option-desc {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user