diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index 193d134..3ab1d6e 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -22,41 +22,47 @@ router = APIRouter() logger = logging.getLogger(__name__) -def get_date_range(period: Optional[str] = None): +def get_timestamp_range(period: Optional[str] = None): """ - 根据时间段参数返回开始和结束日期 + 根据时间段参数返回开始和结束时间戳(Unix时间戳秒数) Args: - period: 时间段 ('1d', '7d', '30d', 'custom') + period: 时间段 ('1d', '7d', '30d', 'today', 'week', 'month', 'custom') Returns: - (start_date, end_date) 元组,格式为 'YYYY-MM-DD HH:MM:SS' + (start_timestamp, end_timestamp) 元组,Unix时间戳(秒数) """ - # 使用当前时间作为结束时间,确保包含最新的单子 - end_date = datetime.now() + # 使用当前时间作为结束时间(Unix时间戳秒数) + beijing_tz = timezone(timedelta(hours=8)) + now = datetime.now(beijing_tz) + end_timestamp = int(now.timestamp()) if period == '1d': - # 最近1天:从今天00:00:00到现在(确保包含今天的所有单子) - # 这样即使查询时已经是晚上,也能查到今天早上开的单子 - start_date = end_date.replace(hour=0, minute=0, second=0, microsecond=0) + # 最近1天:当前时间减去24小时 + start_timestamp = end_timestamp - 24 * 3600 elif period == '7d': - # 最近7天:从7天前00:00:00到现在 - start_date = (end_date - timedelta(days=7)).replace(hour=0, minute=0, second=0, microsecond=0) + # 最近7天:当前时间减去7*24小时 + start_timestamp = end_timestamp - 7 * 24 * 3600 elif period == '30d': - # 最近30天:从30天前00:00:00到现在 - start_date = (end_date - timedelta(days=30)).replace(hour=0, minute=0, second=0, microsecond=0) + # 最近30天:当前时间减去30*24小时 + start_timestamp = end_timestamp - 30 * 24 * 3600 + elif period == 'today': + # 今天:从今天00:00:00到现在 + today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + start_timestamp = int(today_start.timestamp()) + elif period == 'week': + # 本周:从本周一00:00:00到现在 + days_since_monday = now.weekday() # 0=Monday, 6=Sunday + week_start = (now - timedelta(days=days_since_monday)).replace(hour=0, minute=0, second=0, microsecond=0) + start_timestamp = int(week_start.timestamp()) + elif period == 'month': + # 本月:从本月1日00:00:00到现在 + month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + start_timestamp = int(month_start.timestamp()) else: return None, None - # 开始时间:使用计算出的开始日期的00:00:00 - start_date_str = start_date.strftime('%Y-%m-%d %H:%M:%S') - # 结束时间:使用当前时间,确保包含最新单子,北京时间 - # 使用北京时间(东八区) - beijing_tz = timezone(timedelta(hours=8)) - end_date = datetime.now(beijing_tz) - end_date_str = end_date.strftime('%Y-%m-%d %H:%M:%S') - - return start_date_str, end_date_str + return start_timestamp, end_timestamp @router.get("") @@ -64,7 +70,7 @@ def get_date_range(period: Optional[str] = None): async def get_trades( 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天)"), + period: Optional[str] = Query(None, description="快速时间段筛选: '1d'(最近1天), '7d'(最近7天), '30d'(最近30天), 'today'(今天), 'week'(本周), 'month'(本月)"), symbol: Optional[str] = Query(None, description="交易对筛选"), trade_type: Optional[str] = Query(None, description="交易类型筛选: 'buy', 'sell'"), exit_reason: Optional[str] = Query(None, description="平仓原因筛选: 'stop_loss', 'take_profit', 'trailing_stop', 'manual', 'sync'"), @@ -75,28 +81,47 @@ async def get_trades( 获取交易记录 支持两种筛选方式: - 1. 快速时间段筛选:使用 period 参数 ('1d', '7d', '30d') - 2. 自定义时间段筛选:使用 start_date 和 end_date 参数 + 1. 快速时间段筛选:使用 period 参数 ('1d', '7d', '30d', 'today', 'week', 'month') + 2. 自定义时间段筛选:使用 start_date 和 end_date 参数(会转换为Unix时间戳) 如果同时提供了 period 和 start_date/end_date,period 优先级更高 """ 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}") + + start_timestamp = None + end_timestamp = None + # 如果提供了 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 - logger.info(f"使用快速时间段筛选: {period} -> {start_date} 至 {end_date}") + period_start, period_end = get_timestamp_range(period) + if period_start is not None and period_end is not None: + start_timestamp = period_start + end_timestamp = period_end + logger.info(f"使用快速时间段筛选: {period} -> {start_timestamp} ({datetime.fromtimestamp(start_timestamp)}) 至 {end_timestamp} ({datetime.fromtimestamp(end_timestamp)})") + elif start_date or end_date: + # 自定义时间段:将日期字符串转换为Unix时间戳 + beijing_tz = timezone(timedelta(hours=8)) + if start_date: + if len(start_date) == 10: # YYYY-MM-DD + start_date = f"{start_date} 00:00:00" + try: + dt = datetime.strptime(start_date, '%Y-%m-%d %H:%M:%S') + dt = dt.replace(tzinfo=beijing_tz) + start_timestamp = int(dt.timestamp()) + except ValueError: + logger.warning(f"无效的开始日期格式: {start_date}") + if end_date: + if len(end_date) == 10: # YYYY-MM-DD + end_date = f"{end_date} 23:59:59" + try: + dt = datetime.strptime(end_date, '%Y-%m-%d %H:%M:%S') + dt = dt.replace(tzinfo=beijing_tz) + end_timestamp = int(dt.timestamp()) + except ValueError: + logger.warning(f"无效的结束日期格式: {end_date}") - # 格式化日期(如果只提供了日期,添加时间部分) - 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, trade_type, exit_reason) + trades = Trade.get_all(start_timestamp, end_timestamp, symbol, status, trade_type, exit_reason) logger.info(f"查询到 {len(trades)} 条交易记录") # 格式化交易记录,添加平仓类型的中文显示 @@ -124,8 +149,10 @@ async def get_trades( "total": len(trades), "trades": formatted_trades, "filters": { - "start_date": start_date, - "end_date": end_date, + "start_timestamp": start_timestamp, + "end_timestamp": end_timestamp, + "start_date": datetime.fromtimestamp(start_timestamp).strftime('%Y-%m-%d %H:%M:%S') if start_timestamp else None, + "end_date": datetime.fromtimestamp(end_timestamp).strftime('%Y-%m-%d %H:%M:%S') if end_timestamp else None, "period": period, "symbol": symbol, "status": status @@ -143,26 +170,45 @@ async def get_trades( async def get_trade_stats( 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'"), + period: Optional[str] = Query(None, description="快速时间段筛选: '1d', '7d', '30d', 'today', 'week', 'month'"), symbol: Optional[str] = Query(None, description="交易对筛选") ): """获取交易统计""" try: logger.info(f"获取交易统计请求: start_date={start_date}, end_date={end_date}, period={period}, symbol={symbol}") + + start_timestamp = None + end_timestamp = None + # 如果提供了 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 + period_start, period_end = get_timestamp_range(period) + if period_start is not None and period_end is not None: + start_timestamp = period_start + end_timestamp = period_end + elif start_date or end_date: + # 自定义时间段:将日期字符串转换为Unix时间戳 + beijing_tz = timezone(timedelta(hours=8)) + if start_date: + if len(start_date) == 10: # YYYY-MM-DD + start_date = f"{start_date} 00:00:00" + try: + dt = datetime.strptime(start_date, '%Y-%m-%d %H:%M:%S') + dt = dt.replace(tzinfo=beijing_tz) + start_timestamp = int(dt.timestamp()) + except ValueError: + logger.warning(f"无效的开始日期格式: {start_date}") + if end_date: + if len(end_date) == 10: # YYYY-MM-DD + end_date = f"{end_date} 23:59:59" + try: + dt = datetime.strptime(end_date, '%Y-%m-%d %H:%M:%S') + dt = dt.replace(tzinfo=beijing_tz) + end_timestamp = int(dt.timestamp()) + except ValueError: + logger.warning(f"无效的结束日期格式: {end_date}") - # 格式化日期 - 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) + trades = Trade.get_all(start_timestamp, end_timestamp, 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] @@ -176,8 +222,10 @@ async def get_trade_stats( "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, + "start_timestamp": start_timestamp, + "end_timestamp": end_timestamp, + "start_date": datetime.fromtimestamp(start_timestamp).strftime('%Y-%m-%d %H:%M:%S') if start_timestamp else None, + "end_date": datetime.fromtimestamp(end_timestamp).strftime('%Y-%m-%d %H:%M:%S') if end_timestamp else None, "period": period, "symbol": symbol } diff --git a/backend/database/init.sql b/backend/database/init.sql index 84653a5..a743680 100644 --- a/backend/database/init.sql +++ b/backend/database/init.sql @@ -25,15 +25,15 @@ CREATE TABLE IF NOT EXISTS `trades` ( `quantity` DECIMAL(20, 8) NOT NULL, `entry_price` DECIMAL(20, 8) NOT NULL, `exit_price` DECIMAL(20, 8), - `entry_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - `exit_time` TIMESTAMP NULL, + `entry_time` INT UNSIGNED NOT NULL COMMENT '入场时间(Unix时间戳秒数)', + `exit_time` INT UNSIGNED NULL COMMENT '平仓时间(Unix时间戳秒数)', `pnl` DECIMAL(20, 8) DEFAULT 0, `pnl_percent` DECIMAL(10, 4) DEFAULT 0, `leverage` INT DEFAULT 10, `entry_reason` TEXT, `exit_reason` VARCHAR(50) COMMENT 'stop_loss, take_profit, trailing_stop, manual', `status` VARCHAR(20) DEFAULT 'open' COMMENT 'open, closed, cancelled', - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `created_at` INT UNSIGNED NOT NULL DEFAULT (UNIX_TIMESTAMP()) COMMENT '创建时间(Unix时间戳秒数)', INDEX `idx_symbol` (`symbol`), INDEX `idx_entry_time` (`entry_time`), INDEX `idx_status` (`status`), diff --git a/backend/database/models.py b/backend/database/models.py index 39c7c45..afc5e55 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -10,8 +10,8 @@ import logging BEIJING_TZ = timezone(timedelta(hours=8)) def get_beijing_time(): - """获取当前北京时间(UTC+8)""" - return datetime.now(BEIJING_TZ).replace(tzinfo=None) + """获取当前北京时间(UTC+8)的Unix时间戳(秒)""" + return int(datetime.now(BEIJING_TZ).timestamp()) logger = logging.getLogger(__name__) @@ -233,17 +233,26 @@ class Trade: ) @staticmethod - def get_all(start_date=None, end_date=None, symbol=None, status=None, trade_type=None, exit_reason=None): - """获取交易记录""" + def get_all(start_timestamp=None, end_timestamp=None, symbol=None, status=None, trade_type=None, exit_reason=None): + """获取交易记录 + + Args: + start_timestamp: 开始时间(Unix时间戳秒数,可选) + end_timestamp: 结束时间(Unix时间戳秒数,可选) + symbol: 交易对(可选) + status: 状态(可选) + trade_type: 交易类型(可选) + exit_reason: 平仓原因(可选) + """ query = "SELECT * FROM trades WHERE 1=1" params = [] - if start_date: + if start_timestamp is not None: query += " AND created_at >= %s" - params.append(start_date) - if end_date: + params.append(start_timestamp) + if end_timestamp is not None: query += " AND created_at <= %s" - params.append(end_date) + params.append(end_timestamp) if symbol: query += " AND symbol = %s" params.append(symbol) diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index df6a843..d83ac51 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -8,7 +8,7 @@ const TradeList = () => { const [loading, setLoading] = useState(true) // 筛选状态 - const [period, setPeriod] = useState(null) // '1d', '7d', '30d', null + const [period, setPeriod] = useState(null) // '1d', '7d', '30d', 'today', 'week', 'month', null const [startDate, setStartDate] = useState('') const [endDate, setEndDate] = useState('') const [symbol, setSymbol] = useState('') @@ -98,6 +98,24 @@ const TradeList = () => {