feat(trades, database, frontend): 增强交易记录同步与展示功能

在 `trades.py` 中更新了 `include_sync` 参数的默认值为 `True`,以便于订单记录与币安一致,并添加了提示信息以指导用户如何补全缺失的订单号。在 `models.py` 中新增了 `get_trades_missing_entry_order_id` 方法,用于获取缺少 `entry_order_id` 的记录,确保在同步时能够补全数据。前端组件 `StatsDashboard.jsx` 和 `TradeList.jsx` 中相应调整了开仓时间的展示逻辑和无交易记录时的提示信息,提升了用户体验与数据准确性。
This commit is contained in:
薇薇安 2026-02-20 12:17:01 +08:00
parent 22830355c6
commit 9b81832af2
4 changed files with 63 additions and 12 deletions

View File

@ -80,7 +80,7 @@ async def get_trades(
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 的历史同步单"),
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="返回记录数限制"),
):
@ -173,6 +173,9 @@ async def get_trades(
"reconciled_only": reconciled_only,
}
}
# 仅可对账且无记录时提示:可先同步币安订单补全订单号,或取消「仅可对账」查看全部
if reconciled_only and len(formatted_trades) == 0:
result["hint"] = "当前没有可对账记录。可尝试1) 调用「同步币安订单」接口补全开仓/平仓订单号2) 使用 reconciled_only=false 查看全部记录(含无订单号的补建单)。"
logger.debug(f"返回交易记录: {len(result['trades'])}")
return result
@ -189,7 +192,7 @@ async def get_trade_stats(
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 的历史同步单"),
include_sync: bool = Query(True, description="是否包含 entry_reason 为 sync_recovered 的补建/同步单(默认与订单记录一致)"),
reconciled_only: bool = Query(True, description="仅统计可对账记录,与币安一致,避免系统盈利/币安亏损偏差"),
):
"""获取交易统计(默认按平仓时间统计:今日=今日平仓的盈亏,与订单记录筛选一致)"""
@ -759,19 +762,25 @@ async def sync_trades_from_binance(
time_window_start = order_time_sec - 3600
time_window_end = order_time_sec + 3600
trades_no_entry_id = Trade.get_all(
trades_in_window = Trade.get_all(
account_id=aid,
symbol=symbol,
start_timestamp=time_window_start,
end_timestamp=time_window_end,
time_filter="entry", # 使用 entry_time 过滤
time_filter="entry",
reconciled_only=False,
)
# 过滤出没有 entry_order_id 的记录
trades_no_entry_id = [
t for t in trades_no_entry_id
t for t in trades_in_window
if not t.get("entry_order_id") or str(t.get("entry_order_id")).strip() in ("", "0")
]
# 并入缺少 entry_order_id 的其它记录(含 entry_time 为 NULL 的旧记录),便于补全后「仅可对账」能显示
extra = Trade.get_trades_missing_entry_order_id(symbol, aid, limit=50)
seen_ids = {t["id"] for t in trades_no_entry_id}
for t in extra:
if t.get("id") not in seen_ids:
trades_no_entry_id.append(t)
seen_ids.add(t["id"])
# 按价格和数量匹配(允许 5% 误差)
matched_trade = None

View File

@ -906,6 +906,38 @@ class Trade:
(exit_order_id,)
)
@staticmethod
def get_trades_missing_entry_order_id(symbol: str, account_id: int, limit: int = 50):
"""
获取该 symbol+account 下缺少 entry_order_id 的记录用于从币安同步订单时补全
包含 entry_time NULL 的旧记录避免仅可对账下看不到今日有单却未补全的情况
"""
if not symbol or account_id is None:
return []
try:
if not _table_has_column("trades", "entry_order_id"):
return []
if _table_has_column("trades", "account_id"):
return db.execute_query(
"""SELECT * FROM trades
WHERE account_id = %s AND symbol = %s
AND (entry_order_id IS NULL OR entry_order_id = 0 OR entry_order_id = '')
ORDER BY id DESC
LIMIT %s""",
(int(account_id), symbol.strip(), int(limit)),
)
return db.execute_query(
"""SELECT * FROM trades
WHERE symbol = %s
AND (entry_order_id IS NULL OR entry_order_id = 0 OR entry_order_id = '')
ORDER BY id DESC
LIMIT %s""",
(symbol.strip(), int(limit)),
)
except Exception as e:
logger.debug(f"get_trades_missing_entry_order_id 失败: {e}")
return []
@staticmethod
def get_by_client_order_id(client_order_id, account_id: int = None):
"""根据 clientOrderId 获取交易记录(可选按 account_id 隔离)"""

View File

@ -738,9 +738,9 @@ const StatsDashboard = () => {
{/* 止损止盈比例 */}
{trade.entry_time && (
<div className="entry-time">开仓时间: {formatEntryTime(trade.entry_time)}</div>
)}
<div className="entry-time">
开仓时间: {trade.entry_time ? formatEntryTime(trade.entry_time) : '—'}
</div>
</div>
<div className="trade-protection-col">
<div className="stop-take-info">

View File

@ -110,9 +110,12 @@ const TradeList = () => {
try {
const result = await api.syncTradesFromBinance(syncDays, syncAllSymbols)
const backfillHint = (result.entry_order_id_filled || result.exit_order_id_filled)
? `,补全开仓订单号 ${result.entry_order_id_filled || 0} 个、平仓订单号 ${result.exit_order_id_filled || 0} 个(勾选「仅可对账」后可见)`
: ''
setSyncResult({
success: true,
message: `同步完成:共处理 ${result.total_orders || 0} 个订单,更新 ${result.updated_trades || 0} 条记录${result.created_trades ? `,创建 ${result.created_trades} 条新记录` : ''}`,
message: `同步完成:共处理 ${result.total_orders || 0} 个订单,更新 ${result.updated_trades || 0} 条记录${result.created_trades ? `,创建 ${result.created_trades} 条新记录` : ''}${backfillHint}`,
details: result
})
//
@ -810,7 +813,14 @@ const TradeList = () => {
{
trades.length === 0 ? (
<div className="no-data">暂无交易记录</div>
<div className="no-data">
<div>暂无交易记录</div>
{reconciledOnly && (
<p style={{ marginTop: '10px', fontSize: '13px', color: '#666', maxWidth: '420px' }}>
若币安今日有订单但此处为空可先点击右上角同步订单补全开仓/平仓订单号或取消勾选仅可对账查看全部记录
</p>
)}
</div>
) : (
<>
{/* 桌面端表格:用横向滚动包裹,避免整页过宽 */}