This commit is contained in:
薇薇安 2026-01-17 20:01:49 +08:00
parent 35b3777f3e
commit a3aed32224
4 changed files with 164 additions and 75 deletions

View File

@ -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_dateperiod 优先级更高
"""
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
}

View File

@ -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`),

View File

@ -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)

View File

@ -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 = () => {
<div className="filter-section">
<label>快速筛选</label>
<div className="period-buttons">
<button
className={period === 'today' ? 'active' : ''}
onClick={() => handlePeriodChange('today')}
>
今天
</button>
<button
className={period === 'week' ? 'active' : ''}
onClick={() => handlePeriodChange('week')}
>
本周
</button>
<button
className={period === 'month' ? 'active' : ''}
onClick={() => handlePeriodChange('month')}
>
本月
</button>
<button
className={period === '1d' ? 'active' : ''}
onClick={() => handlePeriodChange('1d')}
@ -280,11 +298,18 @@ const TradeList = () => {
const pnlPercent = margin > 0 ? (pnl / margin) * 100 : 0
//
const formatTime = (timeStr) => {
if (!timeStr) return '-'
// Unix
const formatTime = (timeValue) => {
if (!timeValue) return '-'
try {
const date = new Date(timeStr)
if (isNaN(date.getTime())) return timeStr
let date
// Unix
if (typeof timeValue === 'number') {
date = new Date(timeValue * 1000)
} else {
date = new Date(timeValue)
}
if (isNaN(date.getTime())) return String(timeValue)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
@ -294,7 +319,7 @@ const TradeList = () => {
timeZone: 'Asia/Shanghai'
})
} catch (e) {
return timeStr
return String(timeValue)
}
}
@ -350,11 +375,18 @@ const TradeList = () => {
const pnlPercent = margin > 0 ? (pnl / margin) * 100 : 0
//
const formatTime = (timeStr) => {
if (!timeStr) return '-'
// Unix
const formatTime = (timeValue) => {
if (!timeValue) return '-'
try {
const date = new Date(timeStr)
if (isNaN(date.getTime())) return timeStr
let date
// Unix
if (typeof timeValue === 'number') {
date = new Date(timeValue * 1000)
} else {
date = new Date(timeValue)
}
if (isNaN(date.getTime())) return String(timeValue)
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
@ -363,7 +395,7 @@ const TradeList = () => {
timeZone: 'Asia/Shanghai'
})
} catch (e) {
return timeStr
return String(timeValue)
}
}