新建回测系统,并提交

This commit is contained in:
2026-01-17 21:37:42 +08:00
commit fe50ea935a
68 changed files with 108208 additions and 0 deletions

355
static/css/style.css Normal file
View File

@@ -0,0 +1,355 @@
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
/* 头部 */
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.1em;
opacity: 0.9;
}
/* 标签页导航 */
.tabs {
display: flex;
background: #f8f9fa;
border-bottom: 2px solid #e9ecef;
}
.tab-btn {
flex: 1;
padding: 15px 20px;
background: none;
border: none;
font-size: 1.1em;
cursor: pointer;
transition: all 0.3s;
color: #666;
font-weight: 500;
}
.tab-btn:hover {
background: #e9ecef;
}
.tab-btn.active {
background: white;
color: #667eea;
border-bottom: 3px solid #667eea;
}
/* 标签页内容 */
.tab-content {
display: none;
padding: 30px;
}
.tab-content.active {
display: block;
}
/* 区域头部 */
.section-header {
margin-bottom: 25px;
}
.section-header h2 {
color: #333;
margin-bottom: 15px;
}
.controls {
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.controls label {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
color: #555;
}
.controls select,
.controls input {
padding: 8px 12px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 0.95em;
transition: border-color 0.3s;
}
.controls select:focus,
.controls input:focus {
outline: none;
border-color: #667eea;
}
.btn-primary {
padding: 10px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 0.95em;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
padding: 10px 20px;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 0.95em;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.btn-secondary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(240, 147, 251, 0.4);
}
/* 统计卡片 */
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 20px;
margin-bottom: 25px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
border-radius: 12px;
color: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.stat-card h3 {
font-size: 0.9em;
opacity: 0.9;
margin-bottom: 10px;
}
.stat-value {
font-size: 1.8em;
font-weight: bold;
}
/* 表格样式 */
.table-container {
overflow-x: auto;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
table {
width: 100%;
border-collapse: collapse;
background: white;
}
thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
thead th {
padding: 15px;
text-align: left;
font-weight: 600;
font-size: 0.95em;
}
tbody tr {
border-bottom: 1px solid #e9ecef;
transition: background 0.2s;
}
tbody tr:hover {
background: #f8f9fa;
}
tbody td {
padding: 12px 15px;
font-size: 0.9em;
}
/* 颜色标记 */
.positive {
color: #28a745;
font-weight: 600;
}
.negative {
color: #dc3545;
font-weight: 600;
}
.neutral {
color: #6c757d;
}
.font-bold {
font-weight: 900 !important;
font-size: 1.05em;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
}
.badge-win {
background: #28a745;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85em;
font-weight: 600;
}
.badge-loss {
background: #dc3545;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85em;
font-weight: 600;
}
/* 图表容器 */
.chart-container {
position: relative;
height: 500px;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 对比网格 */
.comparison-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 25px;
}
.compare-card {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.compare-card h3 {
color: #667eea;
margin-bottom: 15px;
border-bottom: 2px solid #e9ecef;
padding-bottom: 10px;
}
.stats-list p {
margin: 8px 0;
padding: 8px;
background: #f8f9fa;
border-radius: 6px;
}
/* 分析网格 */
.analytics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 25px;
}
.chart-box {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.chart-box h3 {
color: #667eea;
margin-bottom: 15px;
text-align: center;
}
.chart-box canvas {
max-height: 300px;
}
/* 页脚 */
footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
margin-top: 30px;
}
/* 响应式设计 */
@media (max-width: 768px) {
header h1 {
font-size: 1.8em;
}
.tabs {
flex-direction: column;
}
.controls {
flex-direction: column;
align-items: stretch;
}
.stats-cards {
grid-template-columns: 1fr;
}
.chart-container {
height: 300px;
}
}

447
static/js/advanced.js Normal file
View File

@@ -0,0 +1,447 @@
// 高级功能:多策略对比、统计分析、参数热力图
// ===== 多策略对比功能 =====
async function initComparison() {
// 加载策略列表到两个下拉框
const response = await fetch('/api/equity/list');
const result = await response.json();
if (result.success) {
const select1 = document.getElementById('compare-strategy1');
const select2 = document.getElementById('compare-strategy2');
select1.innerHTML = '<option value="">请选择...</option>';
select2.innerHTML = '<option value="">请选择...</option>';
result.files.forEach(file => {
const option1 = document.createElement('option');
const option2 = document.createElement('option');
option1.value = file.filename;
option2.value = file.filename;
option1.textContent = file.strategy;
option2.textContent = file.strategy;
select1.appendChild(option1);
select2.appendChild(option2);
});
}
document.getElementById('compare-btn').addEventListener('click', compareStrategies);
}
async function compareStrategies() {
const file1 = document.getElementById('compare-strategy1').value;
const file2 = document.getElementById('compare-strategy2').value;
if (!file1 || !file2) {
alert('请选择两个策略进行对比');
return;
}
// 加载两个策略的数据
const [data1, data2] = await Promise.all([
fetch(`/api/equity/${file1}`).then(r => r.json()),
fetch(`/api/equity/${file2}`).then(r => r.json())
]);
if (data1.success && data2.success) {
// 显示统计数据
displayStrategyStats('strategy1-stats', data1.stats);
displayStrategyStats('strategy2-stats', data2.stats);
displayComparisonDiff('comparison-diff', data1.stats, data2.stats);
// 绘制对比图表
renderComparisonChart(data1.data, data2.data, file1, file2);
}
}
function displayStrategyStats(elementId, stats) {
const element = document.getElementById(elementId);
element.innerHTML = `
<p>初始资金: ¥${stats.initial_asset.toLocaleString()}</p>
<p>最终资金: ¥${stats.final_asset.toLocaleString()}</p>
<p>总收益率: <span class="${stats.total_return > 0 ? 'positive' : 'negative'}">${(stats.total_return * 100).toFixed(2)}%</span></p>
<p>最大回撤: <span class="negative">${(stats.max_drawdown * 100).toFixed(2)}%</span></p>
<p>回测天数: ${stats.num_days}天</p>
`;
}
function displayComparisonDiff(elementId, stats1, stats2) {
const element = document.getElementById(elementId);
const returnDiff = (stats1.total_return - stats2.total_return) * 100;
const ddDiff = (stats1.max_drawdown - stats2.max_drawdown) * 100;
element.innerHTML = `
<p>收益率差: <span class="${returnDiff > 0 ? 'positive' : 'negative'}">${returnDiff > 0 ? '+' : ''}${returnDiff.toFixed(2)}%</span></p>
<p>回撤差: <span class="${ddDiff > 0 ? 'negative' : 'positive'}">${ddDiff > 0 ? '+' : ''}${ddDiff.toFixed(2)}%</span></p>
<p>策略${returnDiff > 0 ? '1' : '2'}收益更高</p>
<p>策略${ddDiff < 0 ? '1' : '2'}回撤更小</p>
`;
}
function renderComparisonChart(data1, data2, label1, label2) {
const ctx = document.getElementById('comparison-chart').getContext('2d');
if (comparisonChart) {
comparisonChart.destroy();
}
// 采样数据
const step = Math.max(1, Math.floor(Math.max(data1.length, data2.length) / 200));
const labels = data1.filter((_, i) => i % step === 0).map(row => row.trade_date);
const assets1 = data1.filter((_, i) => i % step === 0).map(row => row.total_asset);
const assets2 = data2.filter((_, i) => i % step === 0).map(row => row.total_asset);
comparisonChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: label1.replace('_equity.csv', ''),
data: assets1,
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
borderWidth: 2,
fill: false,
tension: 0.4
},
{
label: label2.replace('_equity.csv', ''),
data: assets2,
borderColor: '#f093fb',
backgroundColor: 'rgba(240, 147, 251, 0.1)',
borderWidth: 2,
fill: false,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: '多策略资金曲线对比',
font: { size: 16 }
}
},
scales: {
y: {
title: {
display: true,
text: '总资产 (元)'
}
}
}
}
});
}
// ===== 统计分析功能 =====
async function initAnalytics() {
// 加载交易文件列表
const response = await fetch('/api/trades/list');
const result = await response.json();
if (result.success) {
const select = document.getElementById('analytics-file-select');
select.innerHTML = '<option value="">请选择...</option>';
result.files.forEach(file => {
const option = document.createElement('option');
option.value = file.filename;
option.textContent = `${file.time_display} - ${file.strategy_params}`;
select.appendChild(option);
});
}
document.getElementById('analytics-file-select').addEventListener('change', loadAnalyticsData);
document.getElementById('analytics-refresh-btn').addEventListener('click', initAnalytics);
}
async function loadAnalyticsData() {
const filename = document.getElementById('analytics-file-select').value;
if (!filename) return;
const response = await fetch(`/api/trades/${filename}`);
const result = await response.json();
if (result.success) {
renderProfitDistribution(result.data);
renderDrawdownAnalysis(result.data);
}
}
function renderProfitDistribution(data) {
const ctx = document.getElementById('profit-distribution-chart').getContext('2d');
if (profitDistChart) {
profitDistChart.destroy();
}
// 统计盈亏分布
const profits = data.map(row => row.profit_pct * 100);
const bins = [-50, -40, -30, -20, -10, 0, 10, 20, 30, 40, 50];
const counts = new Array(bins.length - 1).fill(0);
profits.forEach(p => {
for (let i = 0; i < bins.length - 1; i++) {
if (p >= bins[i] && p < bins[i + 1]) {
counts[i]++;
break;
}
}
});
profitDistChart = new Chart(ctx, {
type: 'bar',
data: {
labels: bins.slice(0, -1).map((b, i) => `${b}~${bins[i+1]}%`),
datasets: [{
label: '交易次数',
data: counts,
backgroundColor: counts.map((_, i) => bins[i] < 0 ? 'rgba(220, 53, 69, 0.7)' : 'rgba(40, 167, 69, 0.7)'),
borderColor: counts.map((_, i) => bins[i] < 0 ? '#dc3545' : '#28a745'),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: {
display: true,
text: '收益率分布直方图'
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '交易笔数'
}
}
}
}
});
}
function renderDrawdownAnalysis(data) {
const ctx = document.getElementById('drawdown-chart').getContext('2d');
if (drawdownChart) {
drawdownChart.destroy();
}
// 计算累计盈亏和回撤
let cumProfit = 0;
let maxProfit = 0;
const cumProfits = [];
const drawdowns = [];
data.forEach(row => {
cumProfit += row.profit_amount;
cumProfits.push(cumProfit);
maxProfit = Math.max(maxProfit, cumProfit);
drawdowns.push(cumProfit - maxProfit);
});
drawdownChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.map((_, i) => i + 1),
datasets: [
{
label: '累计盈亏',
data: cumProfits,
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
borderWidth: 2,
fill: true,
yAxisID: 'y'
},
{
label: '回撤',
data: drawdowns,
borderColor: '#dc3545',
backgroundColor: 'rgba(220, 53, 69, 0.1)',
borderWidth: 2,
fill: true,
yAxisID: 'y'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' },
title: {
display: true,
text: '累计盈亏与回撤分析'
}
},
scales: {
y: {
title: {
display: true,
text: '金额 (元)'
}
}
}
}
});
}
// ===== 参数热力图功能 =====
async function initHeatmap() {
// 加载优化结果文件列表
const response = await fetch('/api/optimization/list');
const result = await response.json();
if (result.success) {
const select = document.getElementById('heatmap-file-select');
select.innerHTML = '<option value="">请选择...</option>';
result.files.forEach(file => {
const option = document.createElement('option');
option.value = file.filename;
option.textContent = `${file.time_display} (${file.num_results}组参数)`;
select.appendChild(option);
});
}
document.getElementById('heatmap-generate-btn').addEventListener('click', generateHeatmap);
}
async function generateHeatmap() {
const filename = document.getElementById('heatmap-file-select').value;
if (!filename) {
alert('请选择优化结果文件');
return;
}
const response = await fetch(`/api/optimization/${filename}`);
const result = await response.json();
if (result.success) {
const xParam = document.getElementById('heatmap-x-param').value;
const yParam = document.getElementById('heatmap-y-param').value;
const metric = document.getElementById('heatmap-metric').value;
renderHeatmap(result.data, xParam, yParam, metric);
}
}
function renderHeatmap(data, xParam, yParam, metric) {
// 获取唯一的X和Y值
const xValues = [...new Set(data.map(row => row[xParam]))].sort((a, b) => a - b);
const yValues = [...new Set(data.map(row => row[yParam]))].sort((a, b) => a - b);
// 创建热力图矩阵
const matrix = [];
for (let y of yValues) {
const row = [];
for (let x of xValues) {
const point = data.find(d => d[xParam] === x && d[yParam] === y);
row.push(point ? point[metric] : null);
}
matrix.push(row);
}
// 使用Chart.js的matrix插件或简单的热力图
const ctx = document.getElementById('heatmap-chart').getContext('2d');
if (heatmapChart) {
heatmapChart.destroy();
}
// 简化版:使用散点图模拟热力图
const points = [];
data.forEach(row => {
if (row[xParam] !== undefined && row[yParam] !== undefined && row[metric] !== null) {
points.push({
x: row[xParam],
y: row[yParam],
v: row[metric]
});
}
});
// 找出最大最小值用于颜色映射
const values = points.map(p => p.v);
const minVal = Math.min(...values);
const maxVal = Math.max(...values);
heatmapChart = new Chart(ctx, {
type: 'scatter',
data: {
datasets: [{
label: metric,
data: points,
backgroundColor: points.map(p => {
const ratio = (p.v - minVal) / (maxVal - minVal);
const r = Math.floor(220 * (1 - ratio) + 40 * ratio);
const g = Math.floor(53 * (1 - ratio) + 167 * ratio);
const b = Math.floor(69 * (1 - ratio) + 69 * ratio);
return `rgba(${r}, ${g}, ${b}, 0.7)`;
}),
borderColor: '#fff',
borderWidth: 1,
pointRadius: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: {
display: true,
text: `参数热力图 - ${metric}`,
font: { size: 16 }
},
tooltip: {
callbacks: {
label: function(context) {
return `${metric}: ${context.raw.v.toFixed(4)}`;
}
}
}
},
scales: {
x: {
title: {
display: true,
text: xParam
}
},
y: {
title: {
display: true,
text: yParam
}
}
}
}
});
}
// 初始化所有高级功能
document.addEventListener('DOMContentLoaded', function() {
// 延迟加载,等待主功能初始化完成
setTimeout(() => {
initComparison();
initAnalytics();
initHeatmap();
}, 500);
});

410
static/js/app.js Normal file
View File

@@ -0,0 +1,410 @@
// 全局变量
let currentOptData = null;
let currentTradesData = null;
let currentEquityData = null;
let equityChart = null;
let comparisonChart = null;
let profitDistChart = null;
let drawdownChart = null;
let holdingPeriodChart = null;
let monthlyReturnChart = null;
let heatmapChart = null;
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initTabs();
loadOptimizationFiles();
loadTradesFiles();
loadEquityFiles();
// 绑定事件
document.getElementById('opt-file-select').addEventListener('change', loadOptimizationData);
document.getElementById('opt-sort-select').addEventListener('change', renderOptimizationTable);
document.getElementById('opt-limit').addEventListener('change', renderOptimizationTable);
document.getElementById('opt-refresh-btn').addEventListener('click', () => {
loadOptimizationFiles();
loadOptimizationData();
});
document.getElementById('trades-file-select').addEventListener('change', loadTradesData);
document.getElementById('trades-filter').addEventListener('change', renderTradesTable);
document.getElementById('trades-refresh-btn').addEventListener('click', () => {
loadTradesFiles();
loadTradesData();
});
document.getElementById('equity-file-select').addEventListener('change', loadEquityData);
document.getElementById('equity-refresh-btn').addEventListener('click', () => {
loadEquityFiles();
loadEquityData();
});
});
// 标签页切换
function initTabs() {
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const tabName = btn.getAttribute('data-tab');
// 更新按钮状态
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// 更新内容显示
tabContents.forEach(content => {
content.classList.remove('active');
if (content.id === tabName) {
content.classList.add('active');
}
});
});
});
}
// ===== 参数优化部分 =====
async function loadOptimizationFiles() {
try {
const response = await fetch('/api/optimization/list');
const result = await response.json();
if (result.success) {
const select = document.getElementById('opt-file-select');
select.innerHTML = '<option value="">请选择...</option>';
result.files.forEach(file => {
const option = document.createElement('option');
option.value = file.filename;
option.textContent = `${file.time_display} (${file.num_results}组参数)`;
select.appendChild(option);
});
}
} catch (error) {
console.error('加载优化文件列表失败:', error);
}
}
async function loadOptimizationData() {
const filename = document.getElementById('opt-file-select').value;
if (!filename) return;
try {
const response = await fetch(`/api/optimization/${filename}`);
const result = await response.json();
if (result.success) {
currentOptData = result;
updateOptimizationStats(result.stats);
renderOptimizationTable();
}
} catch (error) {
console.error('加载优化数据失败:', error);
}
}
function updateOptimizationStats(stats) {
document.getElementById('opt-total').textContent = stats.total_combinations || '-';
document.getElementById('opt-best-sharpe').textContent =
stats.best_sharpe !== null ? stats.best_sharpe.toFixed(4) : '-';
document.getElementById('opt-best-return').textContent =
stats.best_return !== null ? (stats.best_return * 100).toFixed(2) + '%' : '-';
document.getElementById('opt-worst-dd').textContent =
stats.worst_drawdown !== null ? (stats.worst_drawdown * 100).toFixed(2) + '%' : '-';
}
function renderOptimizationTable() {
if (!currentOptData) return;
const sortBy = document.getElementById('opt-sort-select').value;
const limit = parseInt(document.getElementById('opt-limit').value);
// 排序数据
let data = [...currentOptData.data];
data.sort((a, b) => {
if (sortBy === 'max_drawdown') {
return (b[sortBy] || -999) - (a[sortBy] || -999); // 回撤越小越好
} else {
return (b[sortBy] || -999) - (a[sortBy] || -999); // 其他指标越大越好
}
});
// 限制显示数量
data = data.slice(0, limit);
// 根据排序方式确定需要加粗的列索引
const sortColumnMap = {
'total_return': 5,
'annual_return': 6,
'sharpe': 7,
'max_drawdown': 8
};
const boldColumnIndex = sortColumnMap[sortBy] || -1;
// 渲染表格
const tbody = document.getElementById('opt-tbody');
tbody.innerHTML = '';
data.forEach((row, index) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${index + 1}</td>
<td>${row.param_ma_short || '-'}</td>
<td>${row.param_ma_long || '-'}</td>
<td>${row.param_hold_days || '-'}</td>
<td>${row.param_position_pct_per_stock || '-'}</td>
<td class="${row.total_return > 0 ? 'positive' : 'negative'}${boldColumnIndex === 5 ? ' font-bold' : ''}">
${(row.total_return * 100).toFixed(2)}%
</td>
<td class="${row.annual_return > 0 ? 'positive' : 'negative'}${boldColumnIndex === 6 ? ' font-bold' : ''}">
${(row.annual_return * 100).toFixed(2)}%
</td>
<td class="${boldColumnIndex === 7 ? 'font-bold' : ''}">${row.sharpe !== null ? row.sharpe.toFixed(4) : '-'}</td>
<td class="negative${boldColumnIndex === 8 ? ' font-bold' : ''}">${(row.max_drawdown * 100).toFixed(2)}%</td>
<td>${(row.avg_capital_utilization * 100).toFixed(2)}%</td>
<td>${row.total_trades || 0}</td>
`;
tbody.appendChild(tr);
});
}
// ===== 交易明细部分 =====
async function loadTradesFiles() {
try {
const response = await fetch('/api/trades/list');
const result = await response.json();
if (result.success) {
const select = document.getElementById('trades-file-select');
select.innerHTML = '<option value="">请选择...</option>';
result.files.forEach(file => {
const option = document.createElement('option');
option.value = file.filename;
option.textContent = `${file.time_display} - ${file.strategy_params}`;
select.appendChild(option);
});
}
} catch (error) {
console.error('加载交易文件列表失败:', error);
}
}
async function loadTradesData() {
const filename = document.getElementById('trades-file-select').value;
if (!filename) return;
try {
const response = await fetch(`/api/trades/${filename}`);
const result = await response.json();
if (result.success) {
currentTradesData = result;
updateTradesStats(result.stats);
renderTradesTable();
}
} catch (error) {
console.error('加载交易数据失败:', error);
}
}
function updateTradesStats(stats) {
document.getElementById('trades-total').textContent = stats.total_trades || '-';
document.getElementById('trades-win-rate').textContent = stats.win_rate || '-';
document.getElementById('trades-profit').textContent =
stats.total_profit !== null ? '¥' + stats.total_profit.toFixed(2) : '-';
document.getElementById('trades-avg').textContent =
stats.avg_profit !== null ? '¥' + stats.avg_profit.toFixed(2) : '-';
document.getElementById('trades-max-win').textContent =
stats.max_profit !== null ? '¥' + stats.max_profit.toFixed(2) : '-';
document.getElementById('trades-max-loss').textContent =
stats.max_loss !== null ? '¥' + stats.max_loss.toFixed(2) : '-';
}
function renderTradesTable() {
if (!currentTradesData) return;
const filter = document.getElementById('trades-filter').value;
// 筛选数据
let data = [...currentTradesData.data];
if (filter === 'win') {
data = data.filter(row => row.is_win === true);
} else if (filter === 'loss') {
data = data.filter(row => row.is_win === false);
}
// 渲染表格
const tbody = document.getElementById('trades-tbody');
tbody.innerHTML = '';
data.forEach((row, index) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${index + 1}</td>
<td>${row.ts_code}</td>
<td>¥${row.buy_price.toFixed(2)}</td>
<td>¥${row.sell_price.toFixed(2)}</td>
<td>${row.quantity}</td>
<td class="${row.profit_pct > 0 ? 'positive' : 'negative'}">
${(row.profit_pct * 100).toFixed(2)}%
</td>
<td class="${row.profit_amount > 0 ? 'positive' : 'negative'}">
¥${row.profit_amount.toFixed(2)}
</td>
<td>
<span class="${row.is_win ? 'badge-win' : 'badge-loss'}">
${row.is_win ? '盈利' : '亏损'}
</span>
</td>
`;
tbody.appendChild(tr);
});
}
// ===== 资金曲线部分 =====
async function loadEquityFiles() {
try {
const response = await fetch('/api/equity/list');
const result = await response.json();
if (result.success) {
const select = document.getElementById('equity-file-select');
select.innerHTML = '<option value="">请选择...</option>';
result.files.forEach(file => {
const option = document.createElement('option');
option.value = file.filename;
option.textContent = `${file.strategy} (${file.total_return})`;
select.appendChild(option);
});
}
} catch (error) {
console.error('加载资金曲线文件列表失败:', error);
}
}
async function loadEquityData() {
const filename = document.getElementById('equity-file-select').value;
if (!filename) return;
try {
const response = await fetch(`/api/equity/${filename}`);
const result = await response.json();
if (result.success) {
currentEquityData = result;
updateEquityStats(result.stats);
renderEquityChart(result.data);
}
} catch (error) {
console.error('加载资金曲线数据失败:', error);
}
}
function updateEquityStats(stats) {
document.getElementById('equity-initial').textContent =
stats.initial_asset !== null ? '¥' + stats.initial_asset.toLocaleString() : '-';
document.getElementById('equity-final').textContent =
stats.final_asset !== null ? '¥' + stats.final_asset.toLocaleString() : '-';
document.getElementById('equity-return').textContent =
stats.total_return !== null ? (stats.total_return * 100).toFixed(2) + '%' : '-';
document.getElementById('equity-dd').textContent =
stats.max_drawdown !== null ? (stats.max_drawdown * 100).toFixed(2) + '%' : '-';
}
function renderEquityChart(data) {
const ctx = document.getElementById('equity-chart').getContext('2d');
// 销毁旧图表
if (equityChart) {
equityChart.destroy();
}
// 准备数据每隔10个点取一个避免数据过多
const step = Math.max(1, Math.floor(data.length / 200));
const labels = data.filter((_, i) => i % step === 0).map(row => row.trade_date);
const assets = data.filter((_, i) => i % step === 0).map(row => row.total_asset);
const cash = data.filter((_, i) => i % step === 0).map(row => row.cash);
const marketValue = data.filter((_, i) => i % step === 0).map(row => row.market_value);
// 创建新图表
equityChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: '总资产',
data: assets,
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
},
{
label: '现金',
data: cash,
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
},
{
label: '持仓市值',
data: marketValue,
borderColor: '#ffc107',
backgroundColor: 'rgba(255, 193, 7, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: '资金曲线走势图',
font: {
size: 16
}
},
tooltip: {
mode: 'index',
intersect: false,
}
},
scales: {
x: {
display: true,
title: {
display: true,
text: '交易日期'
},
ticks: {
maxTicksLimit: 20
}
},
y: {
display: true,
title: {
display: true,
text: '金额 (元)'
}
}
}
}
});
}

259
static/js/export.js Normal file
View File

@@ -0,0 +1,259 @@
// 导出功能模块
// 导出为Excel使用纯JS实现CSV导出
function exportToExcel(data, filename) {
// 将数据转换为CSV格式
const csv = convertToCSV(data);
// 创建Blob并下载
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function convertToCSV(data) {
if (!data || data.length === 0) return '';
// 获取表头
const headers = Object.keys(data[0]);
// 构建CSV内容
const csvContent = [
headers.join(','), // 表头行
...data.map(row =>
headers.map(header => {
let cell = row[header];
// 处理特殊字符
if (typeof cell === 'string' && (cell.includes(',') || cell.includes('"'))) {
cell = `"${cell.replace(/"/g, '""')}"`;
}
return cell;
}).join(',')
)
].join('\n');
return csvContent;
}
// 绑定导出按钮事件
document.addEventListener('DOMContentLoaded', function() {
// 延迟绑定,确保元素已加载
setTimeout(() => {
// 参数优化结果导出
const optExportBtn = document.getElementById('opt-export-btn');
if (optExportBtn) {
optExportBtn.addEventListener('click', () => {
if (currentOptData && currentOptData.data) {
const filename = `参数优化结果_${new Date().toISOString().slice(0,10)}.csv`;
exportToExcel(currentOptData.data, filename);
} else {
alert('请先加载数据');
}
});
}
// 交易明细导出
const tradesExportBtn = document.getElementById('trades-export-btn');
if (tradesExportBtn) {
tradesExportBtn.addEventListener('click', () => {
if (currentTradesData && currentTradesData.data) {
const filename = `交易明细_${new Date().toISOString().slice(0,10)}.csv`;
exportToExcel(currentTradesData.data, filename);
} else {
alert('请先加载数据');
}
});
}
}, 1000);
});
// 生成完整报告HTML格式
function generateFullReport() {
if (!currentOptData && !currentTradesData && !currentEquityData) {
alert('请先加载数据');
return;
}
let reportHTML = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>回测报告</title>
<style>
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #667eea;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
}
h2 {
color: #333;
margin-top: 30px;
border-left: 4px solid #667eea;
padding-left: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th, td {
padding: 12px;
text-align: left;
border: 1px solid #ddd;
}
th {
background: #667eea;
color: white;
}
tr:nth-child(even) {
background: #f8f9fa;
}
.positive {
color: #28a745;
font-weight: bold;
}
.negative {
color: #dc3545;
font-weight: bold;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.stat-box {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
}
.stat-box h3 {
margin: 0 0 10px 0;
font-size: 0.9em;
}
.stat-box p {
margin: 0;
font-size: 1.5em;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>📊 A股回测报告</h1>
<p>生成时间:${new Date().toLocaleString('zh-CN')}</p>
`;
// 添加优化结果部分
if (currentOptData) {
reportHTML += `
<h2>参数优化结果</h2>
<div class="stats-grid">
<div class="stat-box">
<h3>参数组合总数</h3>
<p>${currentOptData.stats.total_combinations || 0}</p>
</div>
<div class="stat-box">
<h3>最佳夏普比率</h3>
<p>${currentOptData.stats.best_sharpe ? currentOptData.stats.best_sharpe.toFixed(4) : 'N/A'}</p>
</div>
<div class="stat-box">
<h3>最佳总收益</h3>
<p>${currentOptData.stats.best_return ? (currentOptData.stats.best_return * 100).toFixed(2) + '%' : 'N/A'}</p>
</div>
</div>
<table>
<thead>
<tr>
<th>排名</th>
<th>短期均线</th>
<th>长期均线</th>
<th>持有天数</th>
<th>总收益率</th>
<th>夏普比率</th>
<th>最大回撤</th>
</tr>
</thead>
<tbody>
`;
currentOptData.data.slice(0, 10).forEach((row, index) => {
reportHTML += `
<tr>
<td>${index + 1}</td>
<td>${row.param_ma_short || '-'}</td>
<td>${row.param_ma_long || '-'}</td>
<td>${row.param_hold_days || '-'}</td>
<td class="${row.total_return > 0 ? 'positive' : 'negative'}">${(row.total_return * 100).toFixed(2)}%</td>
<td>${row.sharpe ? row.sharpe.toFixed(4) : '-'}</td>
<td class="negative">${(row.max_drawdown * 100).toFixed(2)}%</td>
</tr>
`;
});
reportHTML += `
</tbody>
</table>
`;
}
// 添加交易明细部分
if (currentTradesData) {
reportHTML += `
<h2>交易统计</h2>
<div class="stats-grid">
<div class="stat-box">
<h3>总交易次数</h3>
<p>${currentTradesData.stats.total_trades || 0}</p>
</div>
<div class="stat-box">
<h3>胜率</h3>
<p>${currentTradesData.stats.win_rate || '0%'}</p>
</div>
<div class="stat-box">
<h3>总盈亏</h3>
<p>¥${currentTradesData.stats.total_profit ? currentTradesData.stats.total_profit.toFixed(2) : '0'}</p>
</div>
</div>
`;
}
reportHTML += `
</div>
</body>
</html>
`;
// 下载HTML报告
const blob = new Blob([reportHTML], { type: 'text/html;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `回测报告_${new Date().toISOString().slice(0,10)}.html`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}