From d9830f395b0c7e805377eec02909dcc6fa6c4e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Tue, 13 Jan 2026 22:12:24 +0800 Subject: [PATCH] a --- backend/api/routes/trades.py | 97 ++++++++++-- frontend/src/components/TradeList.css | 121 +++++++++++++++ frontend/src/components/TradeList.jsx | 214 ++++++++++++++++++++++---- frontend/src/services/api.js | 6 + 4 files changed, 393 insertions(+), 45 deletions(-) diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index 2410695..8b20e85 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -3,6 +3,7 @@ """ from fastapi import APIRouter, Query, HTTPException from typing import Optional +from datetime import datetime, timedelta import sys from pathlib import Path @@ -15,20 +16,74 @@ from database.models import Trade router = APIRouter() +def get_date_range(period: Optional[str] = None): + """ + 根据时间段参数返回开始和结束日期 + + Args: + period: 时间段 ('1d', '7d', '30d', 'custom') + + Returns: + (start_date, end_date) 元组,格式为 'YYYY-MM-DD HH:MM:SS' + """ + end_date = datetime.now() + + if period == '1d': + start_date = end_date - timedelta(days=1) + elif period == '7d': + start_date = end_date - timedelta(days=7) + elif period == '30d': + start_date = end_date - timedelta(days=30) + else: + return None, None + + return start_date.strftime('%Y-%m-%d 00:00:00'), end_date.strftime('%Y-%m-%d %H:%M:%S') + + @router.get("/") async def get_trades( - start_date: Optional[str] = Query(None), - end_date: Optional[str] = Query(None), - symbol: Optional[str] = Query(None), - status: Optional[str] = Query(None), - limit: int = Query(100, ge=1, le=1000) + start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS)"), + end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS)"), + period: Optional[str] = Query(None, description="快速时间段筛选: '1d'(最近1天), '7d'(最近7天), '30d'(最近30天)"), + symbol: Optional[str] = Query(None, description="交易对筛选"), + status: Optional[str] = Query(None, description="状态筛选: 'open', 'closed', 'cancelled'"), + limit: int = Query(100, ge=1, le=1000, description="返回记录数限制") ): - """获取交易记录""" + """ + 获取交易记录 + + 支持两种筛选方式: + 1. 快速时间段筛选:使用 period 参数 ('1d', '7d', '30d') + 2. 自定义时间段筛选:使用 start_date 和 end_date 参数 + + 如果同时提供了 period 和 start_date/end_date,period 优先级更高 + """ try: + # 如果提供了 period,使用快速时间段筛选 + if period: + period_start, period_end = get_date_range(period) + if period_start and period_end: + start_date = period_start + end_date = period_end + + # 格式化日期(如果只提供了日期,添加时间部分) + if start_date and len(start_date) == 10: # YYYY-MM-DD + start_date = f"{start_date} 00:00:00" + if end_date and len(end_date) == 10: # YYYY-MM-DD + end_date = f"{end_date} 23:59:59" + trades = Trade.get_all(start_date, end_date, symbol, status) + return { "total": len(trades), - "trades": trades[:limit] + "trades": trades[:limit], + "filters": { + "start_date": start_date, + "end_date": end_date, + "period": period, + "symbol": symbol, + "status": status + } } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -36,13 +91,26 @@ async def get_trades( @router.get("/stats") async def get_trade_stats( - start_date: Optional[str] = Query(None), - end_date: Optional[str] = Query(None), - symbol: Optional[str] = Query(None) + start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS)"), + end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS)"), + period: Optional[str] = Query(None, description="快速时间段筛选: '1d', '7d', '30d'"), + symbol: Optional[str] = Query(None, description="交易对筛选") ): """获取交易统计""" try: - from fastapi import HTTPException + # 如果提供了 period,使用快速时间段筛选 + if period: + period_start, period_end = get_date_range(period) + if period_start and period_end: + start_date = period_start + end_date = period_end + + # 格式化日期 + if start_date and len(start_date) == 10: + start_date = f"{start_date} 00:00:00" + if end_date and len(end_date) == 10: + end_date = f"{end_date} 23:59:59" + trades = Trade.get_all(start_date, end_date, symbol, None) closed_trades = [t for t in trades if t['status'] == 'closed'] win_trades = [t for t in closed_trades if float(t['pnl']) > 0] @@ -56,9 +124,14 @@ async def get_trade_stats( "win_rate": len(win_trades) / len(closed_trades) * 100 if closed_trades else 0, "total_pnl": sum(float(t['pnl']) for t in closed_trades), "avg_pnl": sum(float(t['pnl']) for t in closed_trades) / len(closed_trades) if closed_trades else 0, + "filters": { + "start_date": start_date, + "end_date": end_date, + "period": period, + "symbol": symbol + } } return stats except Exception as e: - from fastapi import HTTPException raise HTTPException(status_code=500, detail=str(e)) diff --git a/frontend/src/components/TradeList.css b/frontend/src/components/TradeList.css index b76c00d..925357f 100644 --- a/frontend/src/components/TradeList.css +++ b/frontend/src/components/TradeList.css @@ -5,6 +5,127 @@ box-shadow: 0 2px 4px rgba(0,0,0,0.1); } +.filter-panel { + background: #f8f9fa; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 2rem; + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + align-items: flex-end; +} + +.filter-section { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.filter-section label { + font-size: 0.9rem; + font-weight: 500; + color: #555; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.period-buttons { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.period-buttons button { + padding: 0.5rem 1rem; + border: 1px solid #ddd; + background: white; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.3s; +} + +.period-buttons button:hover { + background: #e9ecef; + border-color: #2196F3; +} + +.period-buttons button.active { + background: #2196F3; + color: white; + border-color: #2196F3; +} + +.date-inputs { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.date-inputs input[type="date"] { + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9rem; +} + +.date-inputs span { + color: #666; + font-size: 0.9rem; +} + +.filter-section input[type="text"], +.filter-section select { + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9rem; +} + +.filter-actions { + display: flex; + gap: 0.5rem; + margin-left: auto; +} + +.btn-primary, +.btn-secondary { + padding: 0.5rem 1.5rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.3s; +} + +.btn-primary { + background: #2196F3; + color: white; +} + +.btn-primary:hover { + background: #1976D2; +} + +.btn-secondary { + background: #6c757d; + color: white; +} + +.btn-secondary:hover { + background: #5a6268; +} + +.no-data { + text-align: center; + padding: 3rem; + color: #999; + font-size: 1.1rem; +} + .stats-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index 0e04370..db05db9 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -6,16 +6,41 @@ const TradeList = () => { const [trades, setTrades] = useState([]) const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) + + // 筛选状态 + const [period, setPeriod] = useState(null) // '1d', '7d', '30d', null + const [startDate, setStartDate] = useState('') + const [endDate, setEndDate] = useState('') + const [symbol, setSymbol] = useState('') + const [status, setStatus] = useState('') + const [useCustomDate, setUseCustomDate] = useState(false) useEffect(() => { loadData() }, []) const loadData = async () => { + setLoading(true) try { + const params = { + limit: 100 + } + + // 如果使用快速时间段筛选 + if (!useCustomDate && period) { + params.period = period + } else if (useCustomDate) { + // 使用自定义日期 + if (startDate) params.start_date = startDate + if (endDate) params.end_date = endDate + } + + if (symbol) params.symbol = symbol + if (status) params.status = status + const [tradesData, statsData] = await Promise.all([ - api.getTrades({ limit: 100 }), - api.getTradeStats() + api.getTrades(params), + api.getTradeStats(params) ]) setTrades(tradesData.trades || []) setStats(statsData) @@ -26,12 +51,131 @@ const TradeList = () => { } } + const handlePeriodChange = (newPeriod) => { + setPeriod(newPeriod) + setUseCustomDate(false) + setStartDate('') + setEndDate('') + } + + const handleCustomDateToggle = () => { + setUseCustomDate(!useCustomDate) + if (!useCustomDate) { + setPeriod(null) + } + } + + const handleReset = () => { + setPeriod(null) + setStartDate('') + setEndDate('') + setSymbol('') + setStatus('') + setUseCustomDate(false) + } + if (loading) return
加载中...
return (

交易记录

+ {/* 筛选面板 */} +
+
+ +
+ + + + +
+
+ +
+ + {useCustomDate && ( +
+ setStartDate(e.target.value)} + placeholder="开始日期" + /> + + setEndDate(e.target.value)} + placeholder="结束日期" + min={startDate} + /> +
+ )} +
+ +
+ + setSymbol(e.target.value)} + placeholder="如: BTCUSDT" + style={{ width: '150px' }} + /> +
+ +
+ + +
+ +
+ + +
+
+ {stats && (
@@ -57,38 +201,42 @@ const TradeList = () => {
)} - - - - - - - - - - - - - - - {trades.map(trade => ( - - - - - - - - - + {trades.length === 0 ? ( +
暂无交易记录
+ ) : ( +
交易对方向数量入场价出场价盈亏状态时间
{trade.symbol}{trade.side}{parseFloat(trade.quantity).toFixed(4)}{parseFloat(trade.entry_price).toFixed(4)}{trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'}= 0 ? 'positive' : 'negative'}> - {parseFloat(trade.pnl).toFixed(2)} USDT - - {trade.status} - {new Date(trade.entry_time).toLocaleString('zh-CN')}
+ + + + + + + + + + - ))} - -
交易对方向数量入场价出场价盈亏状态时间
+ + + {trades.map(trade => ( + + {trade.symbol} + {trade.side} + {parseFloat(trade.quantity).toFixed(4)} + {parseFloat(trade.entry_price).toFixed(4)} + {trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'} + = 0 ? 'positive' : 'negative'}> + {parseFloat(trade.pnl).toFixed(2)} USDT + + + {trade.status} + + {new Date(trade.entry_time).toLocaleString('zh-CN')} + + ))} + + + )}
) } diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index a7f06b5..d37cb58 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -38,12 +38,18 @@ export const api = { getTrades: async (params = {}) => { const query = new URLSearchParams(params).toString(); const response = await fetch(`${API_BASE_URL}/api/trades?${query}`); + if (!response.ok) { + throw new Error('获取交易记录失败'); + } return response.json(); }, getTradeStats: async (params = {}) => { const query = new URLSearchParams(params).toString(); const response = await fetch(`${API_BASE_URL}/api/trades/stats?${query}`); + if (!response.ok) { + throw new Error('获取交易统计失败'); + } return response.json(); },