a
This commit is contained in:
parent
6bf4ea7a06
commit
d9830f395b
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 <div className="loading">加载中...</div>
|
||||
|
||||
return (
|
||||
<div className="trade-list">
|
||||
<h2>交易记录</h2>
|
||||
|
||||
{/* 筛选面板 */}
|
||||
<div className="filter-panel">
|
||||
<div className="filter-section">
|
||||
<label>快速筛选:</label>
|
||||
<div className="period-buttons">
|
||||
<button
|
||||
className={period === '1d' ? 'active' : ''}
|
||||
onClick={() => handlePeriodChange('1d')}
|
||||
>
|
||||
最近1天
|
||||
</button>
|
||||
<button
|
||||
className={period === '7d' ? 'active' : ''}
|
||||
onClick={() => handlePeriodChange('7d')}
|
||||
>
|
||||
最近7天
|
||||
</button>
|
||||
<button
|
||||
className={period === '30d' ? 'active' : ''}
|
||||
onClick={() => handlePeriodChange('30d')}
|
||||
>
|
||||
最近30天
|
||||
</button>
|
||||
<button
|
||||
className={period === null && !useCustomDate ? 'active' : ''}
|
||||
onClick={() => handlePeriodChange(null)}
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="filter-section">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useCustomDate}
|
||||
onChange={handleCustomDateToggle}
|
||||
/>
|
||||
自定义时间段
|
||||
</label>
|
||||
{useCustomDate && (
|
||||
<div className="date-inputs">
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
placeholder="开始日期"
|
||||
/>
|
||||
<span>至</span>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
placeholder="结束日期"
|
||||
min={startDate}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="filter-section">
|
||||
<label>交易对:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={symbol}
|
||||
onChange={(e) => setSymbol(e.target.value)}
|
||||
placeholder="如: BTCUSDT"
|
||||
style={{ width: '150px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-section">
|
||||
<label>状态:</label>
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => setStatus(e.target.value)}
|
||||
style={{ width: '120px' }}
|
||||
>
|
||||
<option value="">全部</option>
|
||||
<option value="open">持仓中</option>
|
||||
<option value="closed">已平仓</option>
|
||||
<option value="cancelled">已取消</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="filter-actions">
|
||||
<button className="btn-primary" onClick={loadData}>
|
||||
查询
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={handleReset}>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stats && (
|
||||
<div className="stats-summary">
|
||||
<div className="stat-card">
|
||||
|
|
@ -57,38 +201,42 @@ const TradeList = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<table className="trades-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>交易对</th>
|
||||
<th>方向</th>
|
||||
<th>数量</th>
|
||||
<th>入场价</th>
|
||||
<th>出场价</th>
|
||||
<th>盈亏</th>
|
||||
<th>状态</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trades.map(trade => (
|
||||
<tr key={trade.id}>
|
||||
<td>{trade.symbol}</td>
|
||||
<td className={trade.side === 'BUY' ? 'buy' : 'sell'}>{trade.side}</td>
|
||||
<td>{parseFloat(trade.quantity).toFixed(4)}</td>
|
||||
<td>{parseFloat(trade.entry_price).toFixed(4)}</td>
|
||||
<td>{trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'}</td>
|
||||
<td className={parseFloat(trade.pnl) >= 0 ? 'positive' : 'negative'}>
|
||||
{parseFloat(trade.pnl).toFixed(2)} USDT
|
||||
</td>
|
||||
<td>
|
||||
<span className={`status ${trade.status}`}>{trade.status}</span>
|
||||
</td>
|
||||
<td>{new Date(trade.entry_time).toLocaleString('zh-CN')}</td>
|
||||
{trades.length === 0 ? (
|
||||
<div className="no-data">暂无交易记录</div>
|
||||
) : (
|
||||
<table className="trades-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>交易对</th>
|
||||
<th>方向</th>
|
||||
<th>数量</th>
|
||||
<th>入场价</th>
|
||||
<th>出场价</th>
|
||||
<th>盈亏</th>
|
||||
<th>状态</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trades.map(trade => (
|
||||
<tr key={trade.id}>
|
||||
<td>{trade.symbol}</td>
|
||||
<td className={trade.side === 'BUY' ? 'buy' : 'sell'}>{trade.side}</td>
|
||||
<td>{parseFloat(trade.quantity).toFixed(4)}</td>
|
||||
<td>{parseFloat(trade.entry_price).toFixed(4)}</td>
|
||||
<td>{trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'}</td>
|
||||
<td className={parseFloat(trade.pnl) >= 0 ? 'positive' : 'negative'}>
|
||||
{parseFloat(trade.pnl).toFixed(2)} USDT
|
||||
</td>
|
||||
<td>
|
||||
<span className={`status ${trade.status}`}>{trade.status}</span>
|
||||
</td>
|
||||
<td>{new Date(trade.entry_time).toLocaleString('zh-CN')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user