Files

411 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 全局变量
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: '金额 (元)'
}
}
}
}
});
}