fix: 增加数据导出功能

alphaFuthures
Lxy 1 week ago
parent 56c87386bf
commit 693a359010

@ -135,40 +135,65 @@ def get_latest(
symbol: str,
data_type: str = "futures",
period: Optional[str] = None,
end_time: Optional[str] = None,
db: Session = Depends(get_db),
):
"""
从缓存获取最新数据
可指定单个 period不指定则返回所有已缓存周期
可指定单个 period,不指定则返回所有已缓存周期
可选指定 end_time 过滤K线数据(ISO格式),默认为当前时间
end_time 可以是日期(YYYY-MM-DD)或日期时间(YYYY-MM-DDTHH:MM:SS)
"""
cached = get_cached_data(db, symbol, data_type, [period] if period else None)
if not cached:
raise HTTPException(status_code=404, detail=f"未找到 {symbol} 的缓存数据")
timeframes = []
for p, candles in cached["timeframes"].items():
# 转换数据格式: time -> datetime
normalized_candles = []
for c in candles:
candle_dict = dict(c)
if 'time' in candle_dict and 'datetime' not in candle_dict:
candle_dict['datetime'] = candle_dict.pop('time')
normalized_candles.append(candle_dict)
timeframes.append(TimeframeData(
period=p,
candles=[CandleItem(**c) for c in normalized_candles],
candle_count=len(normalized_candles),
fetched_at=cached.get("timestamp", ""),
))
return SymbolDataResponse(
symbol=symbol,
data_type=data_type,
current_price=cached.get("current_price"),
timeframes=timeframes,
source="cache" if cached.get("is_fresh", False) else "cache_stale",
)
import traceback
# 解析时间参数,默认为当前时间
end_dt = None
if end_time:
try:
# 尝试解析ISO格式时间
end_dt = datetime.fromisoformat(end_time)
logger.info(f"成功解析 end_time: {end_time} -> {end_dt}")
except Exception as e:
logger.error(f"end_time 解析失败: {end_time}, 错误: {e}")
logger.error(f"错误堆栈: {traceback.format_exc()}")
raise HTTPException(status_code=400, detail=f"end_time 格式错误: {str(e)}")
try:
cached = get_cached_data(db, symbol, data_type, [period] if period else None, end_time=end_dt)
if not cached:
raise HTTPException(status_code=404, detail=f"未找到 {symbol} 的缓存数据")
timeframes = []
for p, candles in cached["timeframes"].items():
# 转换数据格式: time -> datetime
normalized_candles = []
for c in candles:
candle_dict = dict(c)
if 'time' in candle_dict and 'datetime' not in candle_dict:
candle_dict['datetime'] = candle_dict.pop('time')
normalized_candles.append(candle_dict)
timeframes.append(TimeframeData(
period=p,
candles=[CandleItem(**c) for c in normalized_candles],
candle_count=len(normalized_candles),
fetched_at=cached.get("timestamp", ""),
))
return SymbolDataResponse(
symbol=symbol,
data_type=data_type,
current_price=cached.get("current_price"),
timeframes=timeframes,
source="cache" if cached.get("is_fresh", False) else "cache_stale",
)
except HTTPException:
raise
except Exception as e:
logger.error(f"获取数据失败: symbol={symbol}, period={period}, end_time={end_time}")
logger.error(f"错误: {e}")
logger.error(f"错误堆栈: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")
@router.get("/latest/{symbol}/{period}")

@ -188,10 +188,20 @@ def get_cached_data(
symbol: str,
data_type: str = "futures",
periods: Optional[List[str]] = None,
end_time: Optional[datetime] = None,
max_candles: int = 100,
) -> Optional[Dict]:
"""
从缓存中获取完整的多周期数据
Args:
db: 数据库会话
symbol: 品种代码
data_type: 数据类型
periods: 周期列表
end_time: 结束时间(可选),默认为当前时间
max_candles: 每个周期最大K线数量,默认100
Returns:
与采集脚本相同格式的数据 None
"""
@ -208,10 +218,51 @@ def get_cached_data(
newest = max(r.fetched_at for r in records)
is_fresh = (now - newest).total_seconds() < CACHE_TTL_SECONDS
# 如果未指定结束时间,默认为当前时间
filter_end_time = end_time if end_time else now
# 确保filter_end_time是naive datetime(无时区)
if filter_end_time.tzinfo is not None:
filter_end_time = filter_end_time.replace(tzinfo=None)
timeframes = {}
current_price = None
for r in records:
timeframes[r.period] = json.loads(r.candles_json)
candles = json.loads(r.candles_json)
# 过滤结束时间之前的K线数据
filtered_candles = []
for candle in candles:
candle_time = candle.get('datetime') or candle.get('time')
if candle_time:
# 解析K线时间
if isinstance(candle_time, str):
try:
candle_dt = datetime.fromisoformat(candle_time.replace('Z', '+00:00'))
# 转换为naive datetime进行比较
if candle_dt.tzinfo is not None:
candle_dt = candle_dt.replace(tzinfo=None)
except:
filtered_candles.append(candle)
continue
else:
candle_dt = candle_time
# 如果是aware datetime,转换为naive
if candle_dt.tzinfo is not None:
candle_dt = candle_dt.replace(tzinfo=None)
# 只保留结束时间之前的数据
if candle_dt <= filter_end_time:
filtered_candles.append(candle)
else:
filtered_candles.append(candle)
# 限制K线数量,超过max_candles则取最新的max_candles条
if len(filtered_candles) > max_candles:
filtered_candles = filtered_candles[-max_candles:]
timeframes[r.period] = filtered_candles
if current_price is None:
current_price = r.current_price

@ -917,6 +917,10 @@
<option value="daily">日线</option>
</select>
</div>
<div class="form-group">
<label class="form-label">结束日期</label>
<input type="date" class="form-input" id="queryEndTime">
</div>
<div class="form-group">
<button class="btn btn-primary" onclick="queryCache()" style="height: 42px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
@ -1876,20 +1880,43 @@
async function queryCache() {
const symbol = document.getElementById('querySymbol').value.trim();
const period = document.getElementById('queryPeriod').value;
const endDate = document.getElementById('queryEndTime').value;
if (!symbol) return showToast('请输入品种代码', 'error');
addLog(`查询缓存: ${symbol} ${period || '全部'}`);
addLog(`查询缓存: ${symbol} ${period || '全部'} 结束日期: ${endDate || '今天'}`);
try {
const url = period ? `${API}/data/latest/${symbol}/${period}` : `${API}/data/latest/${symbol}`;
// 构建查询参数
const params = new URLSearchParams();
if (period) params.append('period', period);
if (endDate) {
// 将日期转换为当天结束时间 23:59:59
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 res = await fetch(url);
const data = await res.json();
// 先检查响应状态
if (!res.ok) {
showToast(data.detail || '未找到缓存数据', 'error');
const errorText = await res.text();
console.error('API错误响应:', errorText);
try {
const errorData = JSON.parse(errorText);
showToast(errorData.detail || '查询失败', 'error');
} catch {
showToast(`服务器错误: ${res.status}`, 'error');
}
document.getElementById('btnExportData').disabled = true;
return;
}
const data = await res.json();
addLog(`查询成功: ${symbol}, 缓存 ${data.timeframes ? data.timeframes.length : 0} 个周期`, 'success');
@ -1905,6 +1932,7 @@
renderKlineChart(data.timeframes[0], symbol);
} catch (e) {
console.error('查询异常:', e);
showToast(`查询失败: ${e.message}`, 'error');
document.getElementById('btnExportData').disabled = true;
}
@ -1917,12 +1945,25 @@
}
const symbol = document.getElementById('querySymbol').value.trim() || 'unknown';
const period = document.getElementById('queryPeriod').value;
const endDate = document.getElementById('queryEndTime').value;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const filename = `${symbol}_多周期数据_${timestamp}.json`;
// 根据是否选择结束日期和周期,生成不同的文件名
let filename;
if (endDate) {
filename = `${symbol}_${period || '多周期'}_截至${endDate}_${timestamp}.json`;
} else {
const today = new Date().toISOString().slice(0, 10);
filename = `${symbol}_${period || '多周期'}_截至${today}_${timestamp}.json`;
}
const exportObj = {
symbol: currentQueryData.symbol || symbol,
type: currentQueryData.type || 'futures',
period: period || 'all',
end_date: endDate || new Date().toISOString().slice(0, 10),
current_price: currentQueryData.current_price,
timestamp: currentQueryData.timestamp || new Date().toISOString(),
timeframes: {}
@ -2956,6 +2997,14 @@
}
// Init
// 页面加载时设置默认日期为今天
function initDefaultDate() {
const today = new Date().toISOString().slice(0, 10);
document.getElementById('queryEndTime').value = today;
}
// 初始化
initDefaultDate();
loadConfig();
loadTasks();
</script>

Binary file not shown.

Binary file not shown.
Loading…
Cancel
Save