feat(trades, database, frontend): 增强时间筛选功能与交易记录展示

在 `trades.py` 中更新了时间筛选逻辑,新增 `created` 选项以支持按创建时间筛选交易记录。在 `models.py` 中调整了查询逻辑,确保在无 `created_at` 字段时回退为 `entry_time`。前端组件 `StatsDashboard.jsx` 和 `TradeList.jsx` 中相应更新了展示逻辑,增加了创建时间的显示,提升了用户体验与数据准确性。
This commit is contained in:
薇薇安 2026-02-21 00:31:51 +08:00
parent 3ce8493af2
commit 6f9e55aaee
4 changed files with 50 additions and 8 deletions

View File

@ -79,7 +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'(原逻辑)"),
time_filter: str = Query("exit", description="时间范围按哪种时间筛选: 'exit'(按平仓时间), 'entry'(按开仓时间,适合策略分析), 'created'(按创建时间), 'both'"),
include_sync: bool = Query(True, 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="返回记录数限制"),
@ -143,6 +143,10 @@ async def get_trades(
'manual': '手动平仓',
'stop_loss': '自动平仓(止损)',
'take_profit': '自动平仓(止盈)',
'take_profit_partial_then_take_profit': '自动平仓(分步止盈后止盈)',
'take_profit_partial_then_stop': '自动平仓(分步止盈后止损)',
'take_profit_partial_then_trailing_stop': '自动平仓(分步止盈后移动止损)',
'early_take_profit': '自动平仓(早止盈)',
'trailing_stop': '自动平仓(移动止损)',
'sync': '同步平仓'
}
@ -191,7 +195,7 @@ 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'"),
time_filter: str = Query("exit", description="时间范围按哪种时间: 'exit'(按平仓时间), 'entry'(按开仓时间), 'created'(按创建时间), 'both'"),
include_sync: bool = Query(True, description="是否包含 entry_reason 为 sync_recovered 的补建/同步单(默认与订单记录一致)"),
reconciled_only: bool = Query(True, description="仅统计可对账记录,与币安一致,避免系统盈利/币安亏损偏差"),
):

View File

@ -1048,7 +1048,8 @@ class Trade:
获取交易记录
time_filter: 时间范围按哪种时间筛选
- "exit": 按平仓时间已平仓用 exit_time未平仓用 entry_time今天= 今天平掉的单 + 今天开的未平仓更符合直觉
- "entry": 按开仓时间
- "entry": 按开仓时间实际入场时间适合策略分析何时进场持仓时长
- "created": 按创建时间记录写入 DB 的时间略早于或等于成交时间 created_at 时回退为 entry
- "both": 原逻辑COALESCE(exit_time, entry_time)
limit: 最多返回条数None 表示不限制
reconciled_only: 仅可对账 entry_order_id已平仓的还有 exit_order_id SQL 中过滤以减轻负载
@ -1072,6 +1073,10 @@ class Trade:
query += " AND (entry_reason IS NULL OR entry_reason != 'sync_recovered')"
query += " AND (exit_reason IS NULL OR exit_reason != 'sync')"
# 按创建时间筛选时若表无 created_at 则回退为 entry_time
use_created = (time_filter == "created" and _table_has_column("trades", "created_at"))
time_col = "COALESCE(created_at, entry_time)" if use_created else None
if start_timestamp is not None and end_timestamp is not None:
if time_filter == "exit":
query += " AND ((status = 'closed' AND exit_time >= %s AND exit_time <= %s) OR (status != 'closed' AND entry_time >= %s AND entry_time <= %s))"
@ -1079,6 +1084,9 @@ class Trade:
elif time_filter == "entry":
query += " AND entry_time >= %s AND entry_time <= %s"
params.extend([start_timestamp, end_timestamp])
elif use_created:
query += " AND " + time_col + " >= %s AND " + time_col + " <= %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])
@ -1089,6 +1097,9 @@ class Trade:
elif time_filter == "entry":
query += " AND entry_time >= %s"
params.append(start_timestamp)
elif use_created:
query += " AND " + time_col + " >= %s"
params.append(start_timestamp)
else:
query += " AND COALESCE(exit_time, entry_time) >= %s"
params.append(start_timestamp)
@ -1099,6 +1110,9 @@ class Trade:
elif time_filter == "entry":
query += " AND entry_time <= %s"
params.append(end_timestamp)
elif use_created:
query += " AND " + time_col + " <= %s"
params.append(end_timestamp)
else:
query += " AND COALESCE(exit_time, entry_time) <= %s"
params.append(end_timestamp)
@ -1116,7 +1130,10 @@ class Trade:
query += " AND exit_reason = %s"
params.append(exit_reason)
query += " ORDER BY COALESCE(exit_time, entry_time) DESC, id DESC"
if use_created:
query += " ORDER BY " + time_col + " DESC, id DESC"
else:
query += " ORDER BY COALESCE(exit_time, entry_time) DESC, id DESC"
if limit is not None and limit > 0:
query += " LIMIT %s"
params.append(int(limit))

View File

@ -741,6 +741,9 @@ const StatsDashboard = () => {
<div className="entry-time">
开仓时间: {(trade.entry_time || trade.created_at) ? formatEntryTime(trade.entry_time || trade.created_at) : '—'}
</div>
<div className="entry-time">
创建时间: {trade.created_at ? formatEntryTime(trade.created_at) : '—'}
</div>
</div>
<div className="trade-protection-col">
<div className="stop-take-info">

View File

@ -361,7 +361,7 @@ const TradeList = () => {
<div>
<h2 style={{ margin: 0 }}>交易记录</h2>
<p style={{ color: '#666', fontSize: '14px', marginTop: '5px', marginBottom: '0' }}>
说明每条记录代表一笔完整的交易开仓+平仓统计总盈亏时每条记录只计算一次默认仅可对账只显示有开仓/平仓订单号的记录统计与币安一致默认按平仓时间今天= 今天平掉的单 + 今天开的未平仓
说明每条记录代表一笔完整的交易开仓+平仓统计总盈亏时每条记录只计算一次默认仅可对账只显示有开仓/平仓订单号的记录统计与币安一致时间依据按平仓时间=今日平仓+今日开仓未平仓按开仓时间=实际入场时间适合策略分析按创建时间=记录写入 DB 时间
</p>
</div>
<div style={{
@ -435,10 +435,17 @@ const TradeList = () => {
<button
className={timeFilter === 'entry' ? 'active' : ''}
onClick={() => setTimeFilter('entry')}
title="选「今天」= 今天开仓的(含未平仓)"
title="选「今天」= 今天开仓的(含未平仓)。开仓时间=实际入场时间,适合策略分析"
>
按开仓时间
</button>
<button
className={timeFilter === 'created' ? 'active' : ''}
onClick={() => setTimeFilter('created')}
title="选「今天」= 今天创建记录的。创建时间=记录写入 DB 时间"
>
按创建时间
</button>
</div>
</div>
<div className="filter-section">
@ -541,15 +548,20 @@ const TradeList = () => {
</select>
</div>
<div className="filter-section">
<label>平仓原因</label>
<label title="确认系统自动止盈:筛选「止盈」可只看自动止盈记录;表格中平仓类型列会显示「自动平仓(止盈)」等">
平仓原因
</label>
<select
value={exitReason}
onChange={(e) => setExitReason(e.target.value)}
style={{ width: '120px' }}
style={{ width: '140px' }}
title="选「止盈」可确认系统自动止盈记录"
>
<option value="">全部</option>
<option value="stop_loss">止损</option>
<option value="take_profit">止盈</option>
<option value="take_profit_partial_then_take_profit">分步止盈后止盈</option>
<option value="early_take_profit">早止盈</option>
<option value="trailing_stop">移动止损</option>
<option value="manual">手动平仓</option>
<option value="sync">同步平仓</option>
@ -842,6 +854,7 @@ const TradeList = () => {
<th>平仓类型</th>
<th>币安订单号</th>
<th>自定义订单号</th>
<th>创建时间</th>
<th>入场时间</th>
<th>平仓时间</th>
</tr>
@ -940,6 +953,7 @@ const TradeList = () => {
<td>{trade.exit_reason_display || '-'}</td>
<td className="order-id" style={{ fontSize: '12px' }}>{formatOrderIds()}</td>
<td className="order-id" style={{ fontSize: '12px' }} title={trade.client_order_id || ''}>{trade.client_order_id || '-'}</td>
<td>{formatTime(trade.created_at)}</td>
<td>{formatTime(trade.entry_time)}</td>
<td>{trade.exit_time ? formatTime(trade.exit_time) : '-'}</td>
</tr>
@ -1078,6 +1092,10 @@ const TradeList = () => {
)}
</div>
<div className="trade-card-footer">
<div className="trade-time-item">
<span className="time-label">创建:</span>
<span>{formatTime(trade.created_at)}</span>
</div>
<div className="trade-time-item">
<span className="time-label">入场:</span>
<span>{formatTime(trade.entry_time)}</span>