@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:title="$t('dashboard.indicator.backtest.historyTitle')"
|
||||
:visible="visible"
|
||||
:width="isMobile ? '100%' : 980"
|
||||
:maskClosable="true"
|
||||
@close="$emit('cancel')"
|
||||
>
|
||||
<div style="display:flex; gap: 12px; margin-bottom: 12px; align-items: center; flex-wrap: wrap;">
|
||||
<a-switch v-model="useCurrentFilters" />
|
||||
<span style="color:#8c8c8c;">
|
||||
{{ $t('dashboard.indicator.backtest.historyUseCurrent') }}
|
||||
</span>
|
||||
<a-button type="primary" :loading="loading" @click="loadRuns">
|
||||
{{ $t('dashboard.indicator.backtest.historyRefresh') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="selectedRowKeys.length === 0"
|
||||
:loading="analyzing"
|
||||
@click="handleAIAnalyze"
|
||||
>
|
||||
{{ $t('dashboard.indicator.backtest.historyAIAnalyze') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div v-if="!useCurrentFilters" style="display:flex; gap: 12px; margin-bottom: 12px; align-items: center; flex-wrap: wrap;">
|
||||
<a-input v-model="filterSymbol" style="width: 220px" :placeholder="$t('dashboard.indicator.backtest.historyFilterSymbol')" />
|
||||
<a-select v-model="filterTimeframe" style="width: 140px" :placeholder="$t('dashboard.indicator.backtest.historyFilterTimeframe')" allowClear>
|
||||
<a-select-option v-for="tf in timeframes" :key="tf" :value="tf">{{ tf }}</a-select-option>
|
||||
</a-select>
|
||||
<a-button :loading="loading" @click="loadRuns">{{ $t('dashboard.indicator.backtest.historyApply') }}</a-button>
|
||||
<span style="color:#8c8c8c;">{{ filterLabel }}</span>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="runs"
|
||||
:loading="loading"
|
||||
size="small"
|
||||
:pagination="{ pageSize: 10, size: 'small' }"
|
||||
rowKey="id"
|
||||
:scroll="{ x: 900 }"
|
||||
:rowSelection="{ selectedRowKeys: selectedRowKeys, onChange: onRowSelectionChange }"
|
||||
>
|
||||
<template slot="range" slot-scope="text, record">
|
||||
<span>{{ (record.start_date || '') }} ~ {{ (record.end_date || '') }}</span>
|
||||
</template>
|
||||
<template slot="status" slot-scope="text">
|
||||
<a-tag :color="text === 'success' ? 'green' : text === 'failed' ? 'red' : 'blue'">
|
||||
{{ text === 'success' ? $t('dashboard.indicator.backtest.historyStatusSuccess') : text === 'failed' ? $t('dashboard.indicator.backtest.historyStatusFailed') : text }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template slot="actions" slot-scope="text, record">
|
||||
<a-button type="link" size="small" :loading="detailLoadingId === record.id" @click="viewRun(record)">
|
||||
{{ $t('dashboard.indicator.backtest.historyView') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<a-empty v-if="!loading && runs.length === 0" :description="$t('dashboard.indicator.backtest.historyNoData')" />
|
||||
|
||||
<a-modal
|
||||
:title="$t('dashboard.indicator.backtest.historyAIAnalyzeTitle')"
|
||||
:visible="showAIResult"
|
||||
:footer="null"
|
||||
:width="isMobile ? '100%' : 900"
|
||||
@cancel="showAIResult = false"
|
||||
>
|
||||
<div v-if="analyzing" style="padding: 12px 0;">
|
||||
<a-spin />
|
||||
</div>
|
||||
<div v-else style="white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;">
|
||||
{{ aiResult || $t('dashboard.indicator.backtest.historyNoAIResult') }}
|
||||
</div>
|
||||
</a-modal>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request'
|
||||
|
||||
export default {
|
||||
name: 'BacktestHistoryDrawer',
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
userId: { type: [Number, String], default: 1 },
|
||||
indicatorId: { type: [Number, String], default: null },
|
||||
symbol: { type: String, default: '' },
|
||||
market: { type: String, default: '' },
|
||||
timeframe: { type: String, default: '' },
|
||||
isMobile: { type: Boolean, default: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
detailLoadingId: null,
|
||||
analyzing: false,
|
||||
showAIResult: false,
|
||||
aiResult: '',
|
||||
useCurrentFilters: true,
|
||||
filterSymbol: '',
|
||||
filterTimeframe: '',
|
||||
timeframes: ['1m', '5m', '15m', '30m', '1H', '4H', '1D', '1W'],
|
||||
runs: [],
|
||||
columns: [],
|
||||
selectedRowKeys: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filterLabel () {
|
||||
const parts = []
|
||||
if (this.indicatorId) parts.push(`indicatorId=${this.indicatorId}`)
|
||||
const m = this.useCurrentFilters ? this.market : (this.market || '')
|
||||
const s = this.useCurrentFilters ? this.symbol : (this.filterSymbol || '')
|
||||
const tf = this.useCurrentFilters ? this.timeframe : (this.filterTimeframe || '')
|
||||
if (m) parts.push(`market=${m}`)
|
||||
if (s) parts.push(`symbol=${s}`)
|
||||
if (tf) parts.push(`timeframe=${tf}`)
|
||||
return parts.length ? parts.join(' | ') : ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible (val) {
|
||||
if (val) {
|
||||
this.initColumns()
|
||||
this.useCurrentFilters = true
|
||||
this.filterSymbol = this.symbol || ''
|
||||
this.filterTimeframe = this.timeframe || ''
|
||||
this.selectedRowKeys = []
|
||||
this.aiResult = ''
|
||||
this.showAIResult = false
|
||||
this.loadRuns()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onRowSelectionChange (keys) {
|
||||
this.selectedRowKeys = keys || []
|
||||
},
|
||||
initColumns () {
|
||||
if (this.columns.length) return
|
||||
this.columns = [
|
||||
{ title: this.$t('dashboard.indicator.backtest.historyRunId'), dataIndex: 'id', key: 'id', width: 90 },
|
||||
{ title: this.$t('dashboard.indicator.backtest.historyCreatedAt'), dataIndex: 'created_at', key: 'created_at', width: 140 },
|
||||
{ title: this.$t('dashboard.indicator.backtest.tradeDirection'), dataIndex: 'trade_direction', key: 'trade_direction', width: 90 },
|
||||
{ title: this.$t('dashboard.indicator.backtest.leverage'), dataIndex: 'leverage', key: 'leverage', width: 90 },
|
||||
{ title: this.$t('dashboard.indicator.backtest.historyRange'), key: 'range', width: 220, scopedSlots: { customRender: 'range' } },
|
||||
{ title: this.$t('dashboard.indicator.backtest.historyStatus'), dataIndex: 'status', key: 'status', width: 90, scopedSlots: { customRender: 'status' } },
|
||||
{ title: this.$t('dashboard.indicator.backtest.historyActions'), key: 'actions', width: 90, scopedSlots: { customRender: 'actions' } }
|
||||
]
|
||||
},
|
||||
async loadRuns () {
|
||||
if (!this.userId) return
|
||||
this.loading = true
|
||||
try {
|
||||
const symbol = this.useCurrentFilters ? this.symbol : (this.filterSymbol || '')
|
||||
const timeframe = this.useCurrentFilters ? this.timeframe : (this.filterTimeframe || '')
|
||||
const market = this.market || ''
|
||||
const res = await request({
|
||||
url: '/api/indicator/backtest/history',
|
||||
method: 'post',
|
||||
data: {
|
||||
userid: this.userId,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
indicatorId: this.indicatorId,
|
||||
symbol,
|
||||
market,
|
||||
timeframe
|
||||
}
|
||||
})
|
||||
if (res && res.code === 1 && Array.isArray(res.data)) {
|
||||
this.runs = res.data
|
||||
} else {
|
||||
this.runs = []
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async viewRun (record) {
|
||||
if (!record || !record.id) return
|
||||
this.detailLoadingId = record.id
|
||||
try {
|
||||
const res = await request({
|
||||
url: '/api/indicator/backtest/get',
|
||||
method: 'post',
|
||||
data: { userid: this.userId, runId: record.id }
|
||||
})
|
||||
if (res && res.code === 1 && res.data) {
|
||||
this.$emit('view', res.data)
|
||||
}
|
||||
} finally {
|
||||
this.detailLoadingId = null
|
||||
}
|
||||
},
|
||||
async handleAIAnalyze () {
|
||||
if (!this.userId || !this.selectedRowKeys.length) return
|
||||
this.analyzing = true
|
||||
this.showAIResult = true
|
||||
this.aiResult = ''
|
||||
try {
|
||||
const lang = (this.$i18n && this.$i18n.locale) ? this.$i18n.locale : 'zh-CN'
|
||||
const res = await request({
|
||||
url: '/api/indicator/backtest/aiAnalyze',
|
||||
method: 'post',
|
||||
data: { userid: this.userId, runIds: this.selectedRowKeys, lang }
|
||||
})
|
||||
if (res && res.code === 1 && res.data && res.data.analysis) {
|
||||
this.aiResult = res.data.analysis
|
||||
} else {
|
||||
this.aiResult = res.msg || this.$t('dashboard.indicator.backtest.historyNoAIResult')
|
||||
}
|
||||
} finally {
|
||||
this.analyzing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:title="modalTitle"
|
||||
:visible="visible"
|
||||
:width="1100"
|
||||
:maskClosable="false"
|
||||
@cancel="$emit('cancel')"
|
||||
class="backtest-run-viewer"
|
||||
>
|
||||
<div v-if="!run || !run.result" style="padding: 12px 0;">
|
||||
<a-empty :description="$t('dashboard.indicator.backtest.historyNoData')" />
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<a-alert
|
||||
v-if="run.id"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 12px;"
|
||||
:message="$t('dashboard.indicator.backtest.savedRunId', { id: run.id })"
|
||||
/>
|
||||
|
||||
<!-- Metrics -->
|
||||
<div class="metrics-cards">
|
||||
<div class="metric-card" :class="{ positive: result.totalReturn > 0, negative: result.totalReturn < 0 }">
|
||||
<div class="metric-label">{{ $t('dashboard.indicator.backtest.totalReturn') }}</div>
|
||||
<div class="metric-value">{{ formatPercent(result.totalReturn) }}</div>
|
||||
<div class="metric-amount">{{ formatMoney(result.totalProfit) }}</div>
|
||||
</div>
|
||||
<div class="metric-card" :class="{ positive: result.annualReturn > 0, negative: result.annualReturn < 0 }">
|
||||
<div class="metric-label">{{ $t('dashboard.indicator.backtest.annualReturn') }}</div>
|
||||
<div class="metric-value">{{ formatPercent(result.annualReturn) }}</div>
|
||||
</div>
|
||||
<div class="metric-card negative">
|
||||
<div class="metric-label">{{ $t('dashboard.indicator.backtest.maxDrawdown') }}</div>
|
||||
<div class="metric-value">{{ formatPercent(result.maxDrawdown) }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">{{ $t('dashboard.indicator.backtest.sharpeRatio') }}</div>
|
||||
<div class="metric-value">{{ (result.sharpeRatio ?? 0).toFixed(2) }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">{{ $t('dashboard.indicator.backtest.winRate') }}</div>
|
||||
<div class="metric-value">{{ formatPercent(result.winRate) }}</div>
|
||||
</div>
|
||||
<div class="metric-card" :class="{ positive: result.profitFactor >= 1.5, negative: result.profitFactor < 1 }">
|
||||
<div class="metric-label">{{ $t('dashboard.indicator.backtest.profitFactor') }}</div>
|
||||
<div class="metric-value">{{ (result.profitFactor ?? 0).toFixed(2) }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">{{ $t('dashboard.indicator.backtest.totalTrades') }}</div>
|
||||
<div class="metric-value">{{ result.totalTrades ?? 0 }}</div>
|
||||
</div>
|
||||
<div class="metric-card negative">
|
||||
<div class="metric-label">{{ $t('dashboard.indicator.backtest.totalCommission') }}</div>
|
||||
<div class="metric-value">-${{ result.totalCommission ? result.totalCommission.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '0.00' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equity curve -->
|
||||
<div class="chart-section">
|
||||
<div class="chart-title">{{ $t('dashboard.indicator.backtest.equityCurve') }}</div>
|
||||
<div ref="equityChartRef" class="equity-chart"></div>
|
||||
</div>
|
||||
|
||||
<!-- Trades -->
|
||||
<div class="trades-section">
|
||||
<div class="chart-title">{{ $t('dashboard.indicator.backtest.tradeHistory') }}</div>
|
||||
<a-table
|
||||
:columns="tradeColumns"
|
||||
:data-source="result.trades || []"
|
||||
:pagination="{ pageSize: 10, size: 'small' }"
|
||||
size="small"
|
||||
:scroll="{ x: 800 }"
|
||||
:rowKey="rowKey"
|
||||
>
|
||||
<template slot="type" slot-scope="text">
|
||||
<a-tag :color="getTradeTypeColor(text)">
|
||||
{{ getTradeTypeText(text) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template slot="balance" slot-scope="text">
|
||||
<span style="color: #1890ff; font-weight: 500;">
|
||||
${{ text ? text.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '--' }}
|
||||
</span>
|
||||
</template>
|
||||
<template slot="profit" slot-scope="text">
|
||||
<span :style="{ color: text > 0 ? '#52c41a' : text < 0 ? '#f5222d' : '#666' }">
|
||||
{{ formatMoney(text) }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template slot="footer">
|
||||
<a-button @click="$emit('cancel')">{{ $t('dashboard.indicator.backtest.close') }}</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'BacktestRunViewer',
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
run: { type: Object, default: null }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
equityChart: null,
|
||||
tradeColumns: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
result () {
|
||||
return (this.run && this.run.result) ? this.run.result : {}
|
||||
},
|
||||
modalTitle () {
|
||||
const id = this.run && this.run.id ? `#${this.run.id}` : ''
|
||||
return `${this.$t('dashboard.indicator.backtest.historyTitle')} ${id}`.trim()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible (val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => {
|
||||
this.initColumns()
|
||||
this.renderEquityChart()
|
||||
})
|
||||
} else {
|
||||
if (this.equityChart) {
|
||||
this.equityChart.dispose()
|
||||
this.equityChart = null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowKey (record, index) {
|
||||
return index
|
||||
},
|
||||
initColumns () {
|
||||
if (this.tradeColumns.length) return
|
||||
this.tradeColumns = [
|
||||
{ title: this.$t('dashboard.indicator.backtest.tradeTime'), dataIndex: 'time', key: 'time', width: 160 },
|
||||
{ title: this.$t('dashboard.indicator.backtest.tradeType'), dataIndex: 'type', key: 'type', width: 150, scopedSlots: { customRender: 'type' } },
|
||||
{ title: this.$t('dashboard.indicator.backtest.price'), dataIndex: 'price', key: 'price', width: 110 },
|
||||
{ title: this.$t('dashboard.indicator.backtest.amount'), dataIndex: 'amount', key: 'amount', width: 100 },
|
||||
{ title: this.$t('dashboard.indicator.backtest.profit'), dataIndex: 'profit', key: 'profit', width: 110, scopedSlots: { customRender: 'profit' } },
|
||||
{ title: this.$t('dashboard.indicator.backtest.balance'), dataIndex: 'balance', key: 'balance', width: 120, scopedSlots: { customRender: 'balance' } }
|
||||
]
|
||||
},
|
||||
getTradeTypeColor (type) {
|
||||
const colorMap = {
|
||||
buy: 'green',
|
||||
sell: 'red',
|
||||
liquidation: 'orange',
|
||||
open_long: 'green',
|
||||
add_long: 'cyan',
|
||||
close_long: 'orange',
|
||||
close_long_stop: 'red',
|
||||
close_long_profit: 'lime',
|
||||
close_long_trailing: 'gold',
|
||||
reduce_long: 'volcano',
|
||||
open_short: 'red',
|
||||
add_short: 'magenta',
|
||||
close_short: 'blue',
|
||||
close_short_stop: 'red',
|
||||
close_short_profit: 'cyan',
|
||||
close_short_trailing: 'gold',
|
||||
reduce_short: 'volcano'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
},
|
||||
getTradeTypeText (type) {
|
||||
const textMap = {
|
||||
// New format - long
|
||||
open_long: this.$t('dashboard.indicator.backtest.openLong'),
|
||||
add_long: this.$t('dashboard.indicator.backtest.addLong'),
|
||||
close_long: this.$t('dashboard.indicator.backtest.closeLong'),
|
||||
close_long_stop: this.$t('dashboard.indicator.backtest.closeLongStop'),
|
||||
close_long_profit: this.$t('dashboard.indicator.backtest.closeLongProfit'),
|
||||
close_long_trailing: this.$t('dashboard.indicator.backtest.closeLongTrailing'),
|
||||
reduce_long: this.$t('dashboard.indicator.backtest.reduceLong'),
|
||||
// New format - short
|
||||
open_short: this.$t('dashboard.indicator.backtest.openShort'),
|
||||
add_short: this.$t('dashboard.indicator.backtest.addShort'),
|
||||
close_short: this.$t('dashboard.indicator.backtest.closeShort'),
|
||||
close_short_stop: this.$t('dashboard.indicator.backtest.closeShortStop'),
|
||||
close_short_profit: this.$t('dashboard.indicator.backtest.closeShortProfit'),
|
||||
close_short_trailing: this.$t('dashboard.indicator.backtest.closeShortTrailing'),
|
||||
reduce_short: this.$t('dashboard.indicator.backtest.reduceShort'),
|
||||
liquidation: this.$t('dashboard.indicator.backtest.liquidation')
|
||||
}
|
||||
return textMap[type] || type
|
||||
},
|
||||
formatPercent (value) {
|
||||
if (value === null || value === undefined) return '--'
|
||||
const sign = value >= 0 ? '+' : ''
|
||||
return `${sign}${Number(value).toFixed(2)}%`
|
||||
},
|
||||
formatMoney (value) {
|
||||
if (value === null || value === undefined) return '--'
|
||||
const sign = value >= 0 ? '+' : '-'
|
||||
return `${sign}$${Math.abs(value).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||
},
|
||||
renderEquityChart () {
|
||||
if (!this.$refs.equityChartRef) return
|
||||
if (this.equityChart) this.equityChart.dispose()
|
||||
this.equityChart = echarts.init(this.$refs.equityChartRef)
|
||||
|
||||
const data = this.result.equityCurve || []
|
||||
const dates = data.map(item => item.time || item.date)
|
||||
const equity = data.map(item => item.value !== undefined ? item.value : item.equity)
|
||||
const initialValue = equity[0] || 100000
|
||||
const finalValue = equity[equity.length - 1] || initialValue
|
||||
const isPositive = finalValue >= initialValue
|
||||
const mainColor = isPositive ? '#52c41a' : '#f5222d'
|
||||
const gradientColor = isPositive
|
||||
? [{ offset: 0, color: 'rgba(82, 196, 26, 0.35)' }, { offset: 1, color: 'rgba(82, 196, 26, 0.02)' }]
|
||||
: [{ offset: 0, color: 'rgba(245, 34, 45, 0.35)' }, { offset: 1, color: 'rgba(245, 34, 45, 0.02)' }]
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: '3%', right: '4%', bottom: '12%', top: '8%', containLabel: true },
|
||||
xAxis: { type: 'category', data: dates, boundaryGap: false },
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{
|
||||
name: this.$t('dashboard.indicator.backtest.strategy'),
|
||||
type: 'line',
|
||||
data: equity,
|
||||
smooth: 0.4,
|
||||
symbol: 'none',
|
||||
sampling: 'lttb',
|
||||
lineStyle: { width: 2.5, color: mainColor },
|
||||
areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, gradientColor) }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.equityChart.setOption(option)
|
||||
window.addEventListener('resize', () => this.equityChart && this.equityChart.resize())
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.backtest-run-viewer {
|
||||
:deep(.ant-modal-body) {
|
||||
padding: 16px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
.metric-card.positive { border-color: rgba(82, 196, 26, 0.35); }
|
||||
.metric-card.negative { border-color: rgba(245, 34, 45, 0.35); }
|
||||
.metric-label { color: #8c8c8c; font-size: 12px; }
|
||||
.metric-value { font-size: 18px; font-weight: 600; margin-top: 4px; }
|
||||
.metric-amount { color: #8c8c8c; font-size: 12px; margin-top: 4px; }
|
||||
|
||||
.chart-section { margin-top: 8px; }
|
||||
.chart-title { font-weight: 600; margin: 8px 0; color: #262626; }
|
||||
.equity-chart { width: 100%; height: 280px; }
|
||||
.trades-section { margin-top: 16px; }
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user