diff --git a/backend/app/models/paper_trading.py b/backend/app/models/paper_trading.py index 33229d2..c10872d 100644 --- a/backend/app/models/paper_trading.py +++ b/backend/app/models/paper_trading.py @@ -5,6 +5,7 @@ from enum import Enum from datetime import datetime from sqlalchemy import Column, Integer, String, Float, DateTime, JSON, Text, Enum as SQLEnum from app.models.database import Base +from app.utils.datetime_utils import get_beijing_time class OrderStatus(str, Enum): @@ -88,7 +89,7 @@ class PaperOrder(Base): trailing_stop_base_profit = Column(Float, default=0) # 移动止损触发时的盈利百分比 # 时间戳 - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=lambda: get_beijing_time()) opened_at = Column(DateTime, nullable=True) # 开仓时间 closed_at = Column(DateTime, nullable=True) # 平仓时间 @@ -99,6 +100,12 @@ class PaperOrder(Base): def to_dict(self): """转换为字典""" + # 格式化时间字段(北京时间字符串) + def format_time(dt): + if dt is None: + return None + return dt.strftime('%Y-%m-%d %H:%M:%S') + return { 'id': self.id, 'order_id': self.order_id, @@ -125,8 +132,8 @@ class PaperOrder(Base): 'breakeven_triggered': self.breakeven_triggered, 'trailing_stop_triggered': getattr(self, 'trailing_stop_triggered', 0), 'trailing_stop_base_profit': getattr(self, 'trailing_stop_base_profit', 0), - 'created_at': self.created_at.isoformat() if self.created_at else None, - 'opened_at': self.opened_at.isoformat() if self.opened_at else None, - 'closed_at': self.closed_at.isoformat() if self.closed_at else None, + 'created_at': format_time(self.created_at), + 'opened_at': format_time(self.opened_at), + 'closed_at': format_time(self.closed_at), 'entry_reasons': self.entry_reasons, } diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index 7866544..0d29fa5 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -2,25 +2,14 @@ 模拟交易服务 - 订单管理和盈亏统计 """ import uuid -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta from typing import Dict, Any, List, Optional from app.models.paper_trading import PaperOrder, OrderStatus, OrderSide, SignalGrade, EntryType from app.services.db_service import db_service from app.config import get_settings from app.utils.logger import logger - - -def get_beijing_time() -> datetime: - """ - 获取东八区(北京时间)当前时间 - - Returns: - 东八区的当前时间 - """ - utc_time = datetime.utcnow().replace(tzinfo=timezone.utc) - beijing_tz = timezone(timedelta(hours=8)) - return utc_time.astimezone(beijing_tz).replace(tzinfo=None) +from app.utils.datetime_utils import get_beijing_time class PaperTradingService: diff --git a/backend/app/utils/datetime_utils.py b/backend/app/utils/datetime_utils.py new file mode 100644 index 0000000..4b170d5 --- /dev/null +++ b/backend/app/utils/datetime_utils.py @@ -0,0 +1,27 @@ +""" +时间工具模块 +提供统一的时间处理函数 +""" +from datetime import datetime, timezone, timedelta + + +def get_beijing_time() -> datetime: + """ + 获取东八区(北京时间)当前时间 + + Returns: + 东八区的当前时间(无时区信息的naive datetime) + """ + utc_time = datetime.utcnow().replace(tzinfo=timezone.utc) + beijing_tz = timezone(timedelta(hours=8)) + return utc_time.astimezone(beijing_tz).replace(tzinfo=None) + + +def get_utc_time() -> datetime: + """ + 获取UTC当前时间 + + Returns: + UTC当前时间(无时区信息的naive datetime) + """ + return datetime.utcnow() diff --git a/frontend/trading.html b/frontend/trading.html index 39c5728..091a203 100644 --- a/frontend/trading.html +++ b/frontend/trading.html @@ -2149,13 +2149,15 @@ formatTime(timeStr) { if (!timeStr) return '-'; - const date = new Date(timeStr); - return date.toLocaleString('zh-CN', { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }); + // 后端已返回格式化的北京时间字符串,直接显示 + // 格式: "2026-03-13 18:30:00" -> 显示为 "03-13 18:30" + const parts = timeStr.split(' '); + if (parts.length >= 2) { + const datePart = parts[0].substring(5); // "03-13" + const timePart = parts[1].substring(0, 5); // "18:30" + return `${datePart} ${timePart}`; + } + return timeStr; }, formatStatus(status) { @@ -2507,21 +2509,16 @@ return (this.totalUnrealizedPnl / this.totalPosition) * 100; }, - // 累计收益率 + // 累计收益率(使用 account.initial_balance 作为基准,与顶部收益率一致) totalReturn() { - if (this.dailyReturns.length === 0) return 0; - const firstDay = this.dailyReturns[0]; - const lastDay = this.dailyReturns[this.dailyReturns.length - 1]; - if (firstDay.balance === 0) return 0; - return ((lastDay.balance - firstDay.balance) / firstDay.balance) * 100; + if (!this.account.initial_balance || this.account.initial_balance === 0) return 0; + return ((this.account.current_balance - this.account.initial_balance) / this.account.initial_balance) * 100; }, // 累计收益额 totalReturnAmount() { - if (this.dailyReturns.length === 0) return 0; - const lastDay = this.dailyReturns[this.dailyReturns.length - 1]; - const firstDay = this.dailyReturns[0]; - return lastDay.balance - firstDay.balance; + if (!this.account.initial_balance) return 0; + return this.account.current_balance - this.account.initial_balance; }, // 盈利天数