diff --git a/app/static/index.html b/app/static/index.html
index d386778..2cd175f 100644
--- a/app/static/index.html
+++ b/app/static/index.html
@@ -936,6 +936,50 @@
+
+
@@ -1992,6 +2036,163 @@
showToast(`已导出 ${periodCount} 个周期数据`, 'success');
}
+ // 批量导出数据
+ async function batchExportData() {
+ const dataType = document.getElementById('batchExportDataType').value;
+ const period = document.getElementById('batchExportPeriod').value;
+ const endDate = document.getElementById('batchExportEndTime').value;
+
+ // 获取所有配置的品种
+ try {
+ const res = await fetch(`${API}/config/symbols`);
+ const data = await res.json();
+
+ if (!data.symbols || data.symbols.length === 0) {
+ showToast('暂无可导出的品种,请先添加品种配置', 'error');
+ return;
+ }
+
+ // 过滤指定数据类型的品种
+ const symbols = data.symbols.filter(s => s.data_type === dataType);
+ if (symbols.length === 0) {
+ showToast(`暂无${dataType === 'futures' ? '期货' : '股票'}品种配置`, 'error');
+ return;
+ }
+
+ const symbolList = symbols.map(s => s.symbol);
+
+ // 显示进度条
+ const progressContainer = document.getElementById('batchExportProgress');
+ const progressFill = document.getElementById('batchExportProgressFill');
+ const progressText = document.getElementById('batchExportProgressText');
+ const resultContainer = document.getElementById('batchExportResult');
+
+ progressContainer.classList.remove('hidden');
+ resultContainer.classList.add('hidden');
+
+ const endDateDisplay = endDate || new Date().toISOString().slice(0, 10);
+ addLog(`开始批量导出: ${dataType === 'futures' ? '期货' : '股票'} ${symbolList.length} 个品种, 结束日期: ${endDateDisplay}`);
+
+ let successCount = 0;
+ let failedCount = 0;
+ let totalCandles = 0;
+ const failedSymbols = [];
+
+ // 逐个获取并导出数据
+ for (let i = 0; i < symbolList.length; i++) {
+ const symbol = symbolList[i];
+ const progress = ((i + 1) / symbolList.length * 100).toFixed(0);
+ progressFill.style.width = `${progress}%`;
+ progressText.textContent = `正在导出: ${symbol} (${i + 1}/${symbolList.length})`;
+
+ try {
+ // 构建查询参数
+ const params = new URLSearchParams();
+ if (period) params.append('period', period);
+ if (endDate) {
+ const endDateTime = new Date(endDate);
+ endDateTime.setHours(23, 59, 59, 999);
+ params.append('end_time', endDateTime.toISOString());
+ }
+
+ const queryString = params.toString();
+ const url = `${API}/data/latest/${symbol}${queryString ? '?' + queryString : ''}`;
+
+ const fetchRes = await fetch(url);
+ if (!fetchRes.ok) {
+ failedCount++;
+ failedSymbols.push(symbol);
+ continue;
+ }
+
+ const fetchData = await fetchRes.json();
+
+ if (!fetchData.timeframes || fetchData.timeframes.length === 0) {
+ failedCount++;
+ failedSymbols.push(symbol);
+ continue;
+ }
+
+ // 导出单个品种数据
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
+ const filename = `${symbol}_${period || '多周期'}_截至${endDateDisplay}_${timestamp}.json`;
+
+ const exportObj = {
+ symbol: fetchData.symbol || symbol,
+ type: fetchData.type || dataType,
+ period: period || 'all',
+ end_date: endDateDisplay,
+ current_price: fetchData.current_price,
+ timestamp: fetchData.timestamp || new Date().toISOString(),
+ timeframes: {}
+ };
+
+ fetchData.timeframes.forEach(tf => {
+ exportObj.timeframes[tf.period] = tf.candles || [];
+ });
+
+ const jsonStr = JSON.stringify(exportObj, null, 2);
+ const blob = new Blob([jsonStr], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ const candleCount = fetchData.timeframes.reduce((sum, tf) => sum + (tf.candles ? tf.candles.length : 0), 0);
+ successCount++;
+ totalCandles += candleCount;
+
+ // 添加小延迟避免浏览器阻止多次下载
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ } catch (e) {
+ console.error(`导出 ${symbol} 失败:`, e);
+ failedCount++;
+ failedSymbols.push(symbol);
+ }
+ }
+
+ // 显示结果
+ progressContainer.classList.add('hidden');
+ resultContainer.classList.remove('hidden');
+
+ let resultHtml = `
+
+
批量导出完成
+
+
成功: ${successCount} 个品种
+
失败: ${failedCount} 个品种
+
总计: ${totalCandles} 条K线
+
+ `;
+
+ if (failedSymbols.length > 0) {
+ resultHtml += `
+
+
失败的品种:
+
${failedSymbols.join(', ')}
+
+ `;
+ }
+
+ resultHtml += '
';
+ resultContainer.innerHTML = resultHtml;
+
+ addLog(`批量导出完成: 成功 ${successCount}, 失败 ${failedCount}, 总计 ${totalCandles} 条K线`, successCount > 0 ? 'success' : 'error');
+ showToast(`批量导出完成: ${successCount} 个品种成功`, successCount > 0 ? 'success' : 'error');
+
+ } catch (e) {
+ console.error('批量导出失败:', e);
+ showToast(`批量导出失败: ${e.message}`, 'error');
+ addLog(`批量导出失败: ${e.message}`, 'error');
+ }
+ }
+
function renderKlineChart(timeframe, symbol) {
const resultContainer = document.getElementById('queryResult');
@@ -3001,6 +3202,7 @@
function initDefaultDate() {
const today = new Date().toISOString().slice(0, 10);
document.getElementById('queryEndTime').value = today;
+ document.getElementById('batchExportEndTime').value = today;
}
// 初始化
diff --git a/data/buffer.db b/data/buffer.db
index ea73a4e..54c9ba6 100644
Binary files a/data/buffer.db and b/data/buffer.db differ
diff --git a/data/futures_analysis.db b/data/futures_analysis.db
index bd04e8c..bbe6acd 100644
Binary files a/data/futures_analysis.db and b/data/futures_analysis.db differ