448 lines
15 KiB
JavaScript
448 lines
15 KiB
JavaScript
// 高级功能:多策略对比、统计分析、参数热力图
|
|
|
|
// ===== 多策略对比功能 =====
|
|
|
|
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);
|
|
});
|