feat(trades): 添加时间筛选功能以优化交易记录查询

在 `trades.py` 中新增 `time_filter` 参数,允许用户按平仓时间或开仓时间筛选交易记录。更新 `Trade.get_all` 方法以支持该功能,并调整查询逻辑以符合新的时间筛选需求。同时,前端组件 `TradeList.jsx` 也进行了相应更新,增加了时间筛选按钮,提升了用户体验和数据查询的灵活性。
This commit is contained in:
薇薇安 2026-02-17 08:01:35 +08:00
parent 3a2536ae96
commit 42480ef886
3 changed files with 72 additions and 18 deletions

View File

@ -79,6 +79,7 @@ async def get_trades(
trade_type: Optional[str] = Query(None, description="交易类型筛选: 'buy', 'sell'"),
exit_reason: Optional[str] = Query(None, description="平仓原因筛选: 'stop_loss', 'take_profit', 'trailing_stop', 'manual', 'sync'"),
status: Optional[str] = Query(None, description="状态筛选: 'open', 'closed', 'cancelled'"),
time_filter: str = Query("exit", description="时间范围按哪种时间筛选: 'exit'(按平仓时间,今日=今天平掉的单+今天开的未平仓), 'entry'(按开仓时间), 'both'(原逻辑)"),
include_sync: bool = Query(False, description="是否包含 entry_reason 为 sync_recovered 的历史同步单"),
reconciled_only: bool = Query(True, description="仅返回可对账记录(有 entry_order_id已平仓的还有 exit_order_id与币安一致统计真实"),
limit: int = Query(100, ge=1, le=1000, description="返回记录数限制"),
@ -90,7 +91,7 @@ async def get_trades(
1. 快速时间段筛选使用 period 参数 ('1d', '7d', '30d', 'today', 'week', 'month')
2. 自定义时间段筛选使用 start_date end_date 参数会转换为Unix时间戳
如果同时提供了 period start_date/end_dateperiod 优先级更高
默认按平仓时间(time_filter=exit)今天= 今天平掉的单 + 今天开的未平仓更符合直觉
"""
try:
logger.info(f"获取交易记录请求: start_date={start_date}, end_date={end_date}, period={period}, symbol={symbol}, status={status}, limit={limit}, trade_type={trade_type}, exit_reason={exit_reason}")
@ -127,7 +128,7 @@ async def get_trades(
except ValueError:
logger.warning(f"无效的结束日期格式: {end_date}")
trades = Trade.get_all(start_timestamp, end_timestamp, symbol, status, trade_type, exit_reason, account_id=account_id)
trades = Trade.get_all(start_timestamp, end_timestamp, symbol, status, trade_type, exit_reason, account_id=account_id, time_filter=time_filter or "exit")
if not include_sync:
trades = [
t for t in trades
@ -208,10 +209,11 @@ async def get_trade_stats(
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', 'today', 'week', 'month'"),
symbol: Optional[str] = Query(None, description="交易对筛选"),
time_filter: str = Query("exit", description="时间范围按哪种时间: 'exit'(按平仓时间,今日统计=今日平仓的盈亏), 'entry'(按开仓时间), 'both'"),
include_sync: bool = Query(False, description="是否包含 entry_reason 为 sync_recovered 的历史同步单"),
reconciled_only: bool = Query(True, description="仅统计可对账记录,与币安一致,避免系统盈利/币安亏损偏差"),
):
"""获取交易统计(默认仅统计可对账记录,保证与币安一致)"""
"""获取交易统计(默认按平仓时间统计:今日=今日平仓的盈亏,与订单记录筛选一致)"""
try:
logger.info(f"获取交易统计请求: start_date={start_date}, end_date={end_date}, period={period}, symbol={symbol}")
@ -246,7 +248,7 @@ async def get_trade_stats(
except ValueError:
logger.warning(f"无效的结束日期格式: {end_date}")
trades = Trade.get_all(start_timestamp, end_timestamp, symbol, None, account_id=account_id)
trades = Trade.get_all(start_timestamp, end_timestamp, symbol, None, account_id=account_id, time_filter=time_filter or "exit")
if not include_sync:
trades = [
t for t in trades

View File

@ -936,8 +936,14 @@ class Trade:
return False
@staticmethod
def get_all(start_timestamp=None, end_timestamp=None, symbol=None, status=None, trade_type=None, exit_reason=None, account_id: int = None):
"""获取交易记录(仅包含本系统开仓的记录已通过清理脚本维护,不再在查询里筛选)"""
def get_all(start_timestamp=None, end_timestamp=None, symbol=None, status=None, trade_type=None, exit_reason=None, account_id: int = None, time_filter: str = "exit"):
"""
获取交易记录
time_filter: 时间范围按哪种时间筛选
- "exit": 按平仓时间已平仓用 exit_time未平仓用 entry_time今天= 今天平掉的单 + 今天开的未平仓更符合直觉
- "entry": 按开仓时间
- "both": 原逻辑COALESCE(exit_time, entry_time)
"""
query = "SELECT * FROM trades WHERE 1=1"
params = []
@ -949,12 +955,38 @@ class Trade:
except Exception:
pass
if start_timestamp is not None:
query += " AND COALESCE(exit_time, entry_time) >= %s"
params.append(start_timestamp)
if end_timestamp is not None:
query += " AND COALESCE(exit_time, entry_time) <= %s"
params.append(end_timestamp)
if start_timestamp is not None and end_timestamp is not None:
if time_filter == "exit":
# 按平仓时间:已平仓看 exit_time未平仓看 entry_time
query += " AND ((status = 'closed' AND exit_time >= %s AND exit_time <= %s) OR (status != 'closed' AND entry_time >= %s AND entry_time <= %s))"
params.extend([start_timestamp, end_timestamp, start_timestamp, end_timestamp])
elif time_filter == "entry":
query += " AND entry_time >= %s AND entry_time <= %s"
params.extend([start_timestamp, end_timestamp])
else:
query += " AND COALESCE(exit_time, entry_time) >= %s AND COALESCE(exit_time, entry_time) <= %s"
params.extend([start_timestamp, end_timestamp])
elif start_timestamp is not None:
if time_filter == "exit":
query += " AND ((status = 'closed' AND exit_time >= %s) OR (status != 'closed' AND entry_time >= %s))"
params.extend([start_timestamp, start_timestamp])
elif time_filter == "entry":
query += " AND entry_time >= %s"
params.append(start_timestamp)
else:
query += " AND COALESCE(exit_time, entry_time) >= %s"
params.append(start_timestamp)
elif end_timestamp is not None:
if time_filter == "exit":
query += " AND ((status = 'closed' AND exit_time <= %s) OR (status != 'closed' AND entry_time <= %s))"
params.extend([end_timestamp, end_timestamp])
elif time_filter == "entry":
query += " AND entry_time <= %s"
params.append(end_timestamp)
else:
query += " AND COALESCE(exit_time, entry_time) <= %s"
params.append(end_timestamp)
if symbol:
query += " AND symbol = %s"
params.append(symbol)
@ -968,10 +1000,9 @@ class Trade:
query += " AND exit_reason = %s"
params.append(exit_reason)
# 按平仓时间倒序排序,如果没有平仓时间则按入场时间倒序
# query += " ORDER BY COALESCE(exit_time, entry_time) DESC, entry_time DESC"
query += " ORDER BY id DESC"
logger.info(f"查询交易记录: {query}, {params}")
# 按平仓时间倒序(已平仓的按 exit_time未平仓的按 entry_time
query += " ORDER BY COALESCE(exit_time, entry_time) DESC, id DESC"
logger.info(f"查询交易记录: time_filter={time_filter}, {query}, {params}")
result = db.execute_query(query, params)
return result

View File

@ -21,10 +21,11 @@ const TradeList = () => {
const [tradeType, setTradeType] = useState('')
const [exitReason, setExitReason] = useState('')
const [reconciledOnly, setReconciledOnly] = useState(true) //
const [timeFilter, setTimeFilter] = useState('exit') // 'exit' =, 'entry'
useEffect(() => {
loadData()
}, [accountId, reconciledOnly]) // accountId
}, [accountId, reconciledOnly, timeFilter]) // accountId / 仅可对账 / 时间筛选方式 变化时重新加
const loadData = async () => {
setLoading(true)
@ -47,6 +48,7 @@ const TradeList = () => {
if (tradeType) params.trade_type = tradeType
if (exitReason) params.exit_reason = exitReason
params.reconciled_only = reconciledOnly
params.time_filter = timeFilter || 'exit'
const [tradesData, statsData] = await Promise.all([
api.getTrades(params),
@ -311,11 +313,30 @@ const TradeList = () => {
<div className="trade-list">
<h2>交易记录</h2>
<p style={{ color: '#666', fontSize: '14px', marginTop: '-10px', marginBottom: '20px' }}>
说明每条记录代表一笔完整的交易开仓+平仓统计总盈亏时每条记录只计算一次默认仅可对账只显示有开仓/平仓订单号的记录统计与币安一致
说明每条记录代表一笔完整的交易开仓+平仓统计总盈亏时每条记录只计算一次默认仅可对账只显示有开仓/平仓订单号的记录统计与币安一致默认按平仓时间今天= 今天平掉的单 + 今天开的未平仓
</p>
{/* 筛选面板 */}
<div className="filter-panel">
<div className="filter-section">
<label>时间依据</label>
<div className="period-buttons">
<button
className={timeFilter === 'exit' ? 'active' : ''}
onClick={() => setTimeFilter('exit')}
title="选「今天」= 今天平仓的已平仓单 + 今天开仓的未平仓单"
>
按平仓时间
</button>
<button
className={timeFilter === 'entry' ? 'active' : ''}
onClick={() => setTimeFilter('entry')}
title="选「今天」= 今天开仓的(含未平仓)"
>
按开仓时间
</button>
</div>
</div>
<div className="filter-section">
<label>快速筛选</label>
<div className="period-buttons">