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 (
交易记录
+ {/* 筛选面板 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setSymbol(e.target.value)}
+ placeholder="如: BTCUSDT"
+ style={{ width: '150px' }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
{stats && (
@@ -57,38 +201,42 @@ const TradeList = () => {
)}
-
-
-
- | 交易对 |
- 方向 |
- 数量 |
- 入场价 |
- 出场价 |
- 盈亏 |
- 状态 |
- 时间 |
-
-
-
- {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')} |
+ {trades.length === 0 ? (
+ 暂无交易记录
+ ) : (
+
+
+
+ | 交易对 |
+ 方向 |
+ 数量 |
+ 入场价 |
+ 出场价 |
+ 盈亏 |
+ 状态 |
+ 时间 |
- ))}
-
-
+
+
+ {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();
},