新建回测系统,并提交
This commit is contained in:
410
static/js/app.js
Normal file
410
static/js/app.js
Normal 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: '金额 (元)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user