diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py index 253b878..0ef1fa1 100644 --- a/backend/api/routes/account.py +++ b/backend/api/routes/account.py @@ -275,10 +275,15 @@ async def get_realtime_positions(): if margin > 0: pnl_percent = (unrealized_pnl / margin) * 100 - # 尝试从数据库获取开仓时间、止损止盈价格 + # 尝试从数据库获取开仓时间、止损止盈价格(以及交易规模字段) entry_time = None stop_loss_price = None take_profit_price = None + take_profit_1 = None + take_profit_2 = None + atr_value = None + db_margin_usdt = None + db_notional_usdt = None try: from database.models import Trade db_trades = Trade.get_by_symbol(pos.get('symbol'), status='open') @@ -290,6 +295,11 @@ async def get_realtime_positions(): # 尝试从数据库获取止损止盈价格(如果存储了) stop_loss_price = db_trade.get('stop_loss_price') take_profit_price = db_trade.get('take_profit_price') + take_profit_1 = db_trade.get('take_profit_1') + take_profit_2 = db_trade.get('take_profit_2') + atr_value = db_trade.get('atr') + db_margin_usdt = db_trade.get('margin_usdt') + db_notional_usdt = db_trade.get('notional_usdt') break except Exception as e: logger.debug(f"获取数据库信息失败: {e}") @@ -302,14 +312,21 @@ async def get_realtime_positions(): "side": "BUY" if position_amt > 0 else "SELL", "quantity": abs(position_amt), "entry_price": entry_price, - "entry_value_usdt": entry_value_usdt, # 开仓时的USDT数量 + # 兼容旧字段:entry_value_usdt 仍保留(前端已有使用) + "entry_value_usdt": entry_value_usdt, + # 新字段:名义/保证金(若DB有则优先使用DB;否则使用实时计算) + "notional_usdt": db_notional_usdt if db_notional_usdt is not None else entry_value_usdt, + "margin_usdt": db_margin_usdt if db_margin_usdt is not None else margin, "mark_price": mark_price, "pnl": unrealized_pnl, "pnl_percent": pnl_percent, # 基于保证金的盈亏百分比 "leverage": int(pos.get('leverage', 1)), "entry_time": entry_time, # 开仓时间 "stop_loss_price": stop_loss_price, # 止损价格(如果可用) - "take_profit_price": take_profit_price # 止盈价格(如果可用) + "take_profit_price": take_profit_price, # 止盈价格(如果可用) + "take_profit_1": take_profit_1, + "take_profit_2": take_profit_2, + "atr": atr_value, }) logger.info(f"格式化后 {len(formatted_positions)} 个有效持仓") diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index e28d269..50156e0 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -231,6 +231,11 @@ async def get_trade_stats( "win_rate": len(win_trades) / len(meaningful_trades) * 100 if meaningful_trades else 0, # 基于有意义的交易计算胜率 "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, + # 总交易量(名义下单量口径):优先使用 notional_usdt(新字段),否则回退 entry_price * quantity + "total_notional_usdt": sum( + float(t.get('notional_usdt') or (float(t.get('entry_price', 0)) * float(t.get('quantity', 0)))) + for t in trades + ), "filters": { "start_timestamp": start_timestamp, "end_timestamp": end_timestamp, diff --git a/backend/database/add_trade_size_and_risk_fields.sql b/backend/database/add_trade_size_and_risk_fields.sql new file mode 100644 index 0000000..5484b90 --- /dev/null +++ b/backend/database/add_trade_size_and_risk_fields.sql @@ -0,0 +1,105 @@ +-- 为 trades 表添加“交易规模/风险”字段: +-- 1) 保证金(margin_usdt) +-- 2) 名义下单量(notional_usdt) +-- 3) 实际止损/止盈/分步止盈与ATR(用于仪表板展示真实值) +-- +-- 说明: +-- - 可重复执行:列已存在会自动跳过 +-- - 默认统计口径使用“入场时”的名义/保证金 +-- + +SET @dbname = DATABASE(); +SET @tablename = 'trades'; + +-- 1) notional_usdt(名义下单量) +SET @columnname = 'notional_usdt'; +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE (TABLE_SCHEMA = @dbname) AND (TABLE_NAME = @tablename) AND (COLUMN_NAME = @columnname) + ) > 0, + "SELECT 'Column notional_usdt already exists.' AS result;", + CONCAT( + "ALTER TABLE `", @tablename, "` ", + "ADD COLUMN `", @columnname, "` DECIMAL(20, 8) NULL ", + "COMMENT '名义下单量(USDT):入场价×数量(用于统计总交易量)';" + ) +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- 2) margin_usdt(保证金) +SET @columnname2 = 'margin_usdt'; +SET @preparedStatement2 = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE (TABLE_SCHEMA = @dbname) AND (TABLE_NAME = @tablename) AND (COLUMN_NAME = @columnname2) + ) > 0, + "SELECT 'Column margin_usdt already exists.' AS result;", + CONCAT( + "ALTER TABLE `", @tablename, "` ", + "ADD COLUMN `", @columnname2, "` DECIMAL(20, 8) NULL ", + "COMMENT '保证金(USDT):名义下单量/杠杆(用于统计与盈亏口径统一)';" + ) +)); +PREPARE alterIfNotExists2 FROM @preparedStatement2; +EXECUTE alterIfNotExists2; +DEALLOCATE PREPARE alterIfNotExists2; + +-- 3) take_profit_1(分步止盈1) +SET @columnname3 = 'take_profit_1'; +SET @preparedStatement3 = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE (TABLE_SCHEMA = @dbname) AND (TABLE_NAME = @tablename) AND (COLUMN_NAME = @columnname3) + ) > 0, + "SELECT 'Column take_profit_1 already exists.' AS result;", + CONCAT( + "ALTER TABLE `", @tablename, "` ", + "ADD COLUMN `", @columnname3, "` DECIMAL(20, 8) NULL ", + "COMMENT '第一目标止盈价(用于展示与分步止盈)';" + ) +)); +PREPARE alterIfNotExists3 FROM @preparedStatement3; +EXECUTE alterIfNotExists3; +DEALLOCATE PREPARE alterIfNotExists3; + +-- 4) take_profit_2(分步止盈2) +SET @columnname4 = 'take_profit_2'; +SET @preparedStatement4 = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE (TABLE_SCHEMA = @dbname) AND (TABLE_NAME = @tablename) AND (COLUMN_NAME = @columnname4) + ) > 0, + "SELECT 'Column take_profit_2 already exists.' AS result;", + CONCAT( + "ALTER TABLE `", @tablename, "` ", + "ADD COLUMN `", @columnname4, "` DECIMAL(20, 8) NULL ", + "COMMENT '第二目标止盈价(用于展示与分步止盈)';" + ) +)); +PREPARE alterIfNotExists4 FROM @preparedStatement4; +EXECUTE alterIfNotExists4; +DEALLOCATE PREPARE alterIfNotExists4; + +-- 5) atr(开仓时使用的ATR值) +SET @columnname5 = 'atr'; +SET @preparedStatement5 = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE (TABLE_SCHEMA = @dbname) AND (TABLE_NAME = @tablename) AND (COLUMN_NAME = @columnname5) + ) > 0, + "SELECT 'Column atr already exists.' AS result;", + CONCAT( + "ALTER TABLE `", @tablename, "` ", + "ADD COLUMN `", @columnname5, "` DECIMAL(20, 8) NULL ", + "COMMENT '开仓时使用的ATR(用于展示动态止损止盈依据)';" + ) +)); +PREPARE alterIfNotExists5 FROM @preparedStatement5; +EXECUTE alterIfNotExists5; +DEALLOCATE PREPARE alterIfNotExists5; + +SELECT 'Migration completed: Added notional_usdt, margin_usdt, take_profit_1, take_profit_2, atr to trades table.' AS result; + diff --git a/backend/database/init.sql b/backend/database/init.sql index d60ebe9..c5400e2 100644 --- a/backend/database/init.sql +++ b/backend/database/init.sql @@ -24,20 +24,34 @@ CREATE TABLE IF NOT EXISTS `trades` ( `side` VARCHAR(10) NOT NULL COMMENT 'BUY, SELL', `quantity` DECIMAL(20, 8) NOT NULL, `entry_price` DECIMAL(20, 8) NOT NULL, + `notional_usdt` DECIMAL(20, 8) NULL COMMENT '名义下单量(USDT):入场价×数量(用于统计总交易量)', + `margin_usdt` DECIMAL(20, 8) NULL COMMENT '保证金(USDT):名义下单量/杠杆', `exit_price` DECIMAL(20, 8), `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_order_id` BIGINT NULL COMMENT '币安开仓订单号(用于对账)', + `exit_order_id` BIGINT NULL COMMENT '币安平仓订单号(用于对账)', `entry_reason` TEXT, `exit_reason` VARCHAR(50) COMMENT 'stop_loss, take_profit, trailing_stop, manual', + `strategy_type` VARCHAR(50) COMMENT '策略类型: trend_following, mean_reversion', + `duration_minutes` INT COMMENT '持仓持续时间(分钟)', + `atr` DECIMAL(20, 8) NULL COMMENT '开仓时使用的ATR(用于展示动态止损止盈依据)', + `stop_loss_price` DECIMAL(20, 8) NULL COMMENT '实际使用的止损价格(考虑了ATR等动态计算)', + `take_profit_price` DECIMAL(20, 8) NULL COMMENT '实际使用的止盈价格(考虑了ATR等动态计算)', + `take_profit_1` DECIMAL(20, 8) NULL COMMENT '第一目标止盈价(用于展示与分步止盈)', + `take_profit_2` DECIMAL(20, 8) NULL COMMENT '第二目标止盈价(用于展示与分步止盈)', `status` VARCHAR(20) DEFAULT 'open' COMMENT 'open, closed, cancelled', `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`), - INDEX `idx_symbol_status` (`symbol`, `status`) + INDEX `idx_symbol_status` (`symbol`, `status`), + INDEX `idx_entry_order_id` (`entry_order_id`), + INDEX `idx_exit_order_id` (`exit_order_id`), + INDEX `idx_strategy_type` (`strategy_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易记录表'; -- 账户快照表 diff --git a/backend/database/models.py b/backend/database/models.py index 96fed02..2a705ac 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -88,7 +88,22 @@ class Trade: """交易记录模型""" @staticmethod - def create(symbol, side, quantity, entry_price, leverage=10, entry_reason=None, entry_order_id=None, stop_loss_price=None, take_profit_price=None): + def create( + symbol, + side, + quantity, + entry_price, + leverage=10, + entry_reason=None, + entry_order_id=None, + stop_loss_price=None, + take_profit_price=None, + take_profit_1=None, + take_profit_2=None, + atr=None, + notional_usdt=None, + margin_usdt=None, + ): """创建交易记录(使用北京时间) Args: @@ -101,31 +116,73 @@ class Trade: entry_order_id: 币安开仓订单号(可选,用于对账) stop_loss_price: 实际使用的止损价格(考虑了ATR等动态计算) take_profit_price: 实际使用的止盈价格(考虑了ATR等动态计算) + take_profit_1: 第一目标止盈价(可选) + take_profit_2: 第二目标止盈价(可选) + atr: 开仓时使用的ATR值(可选) + notional_usdt: 名义下单量(USDT,可选) + margin_usdt: 保证金(USDT,可选) """ entry_time = get_beijing_time() - # 检查字段是否存在(兼容旧数据库schema) + # 自动计算 notional/margin(若调用方没传) try: - db.execute_one("SELECT stop_loss_price FROM trades LIMIT 1") - has_stop_take_fields = True - except: - has_stop_take_fields = False - - if has_stop_take_fields: - db.execute_update( - """INSERT INTO trades - (symbol, side, quantity, entry_price, leverage, entry_reason, status, entry_time, entry_order_id, stop_loss_price, take_profit_price) - VALUES (%s, %s, %s, %s, %s, %s, 'open', %s, %s, %s, %s)""", - (symbol, side, quantity, entry_price, leverage, entry_reason, entry_time, entry_order_id, stop_loss_price, take_profit_price) - ) - else: - # 兼容旧schema - db.execute_update( - """INSERT INTO trades - (symbol, side, quantity, entry_price, leverage, entry_reason, status, entry_time, entry_order_id) - VALUES (%s, %s, %s, %s, %s, %s, 'open', %s, %s)""", - (symbol, side, quantity, entry_price, leverage, entry_reason, entry_time, entry_order_id) - ) + if notional_usdt is None and quantity is not None and entry_price is not None: + notional_usdt = float(quantity) * float(entry_price) + except Exception: + pass + try: + if margin_usdt is None and notional_usdt is not None: + lv = float(leverage) if leverage else 0 + margin_usdt = (float(notional_usdt) / lv) if lv and lv > 0 else float(notional_usdt) + except Exception: + pass + + def _has_column(col: str) -> bool: + try: + db.execute_one(f"SELECT {col} FROM trades LIMIT 1") + return True + except Exception: + return False + + # 动态构建 INSERT(兼容不同schema) + columns = ["symbol", "side", "quantity", "entry_price", "leverage", "entry_reason", "status", "entry_time"] + values = [symbol, side, quantity, entry_price, leverage, entry_reason, "open", entry_time] + + if _has_column("entry_order_id"): + columns.append("entry_order_id") + values.append(entry_order_id) + + if _has_column("notional_usdt"): + columns.append("notional_usdt") + values.append(notional_usdt) + + if _has_column("margin_usdt"): + columns.append("margin_usdt") + values.append(margin_usdt) + + if _has_column("atr"): + columns.append("atr") + values.append(atr) + + if _has_column("stop_loss_price"): + columns.append("stop_loss_price") + values.append(stop_loss_price) + + if _has_column("take_profit_price"): + columns.append("take_profit_price") + values.append(take_profit_price) + + if _has_column("take_profit_1"): + columns.append("take_profit_1") + values.append(take_profit_1) + + if _has_column("take_profit_2"): + columns.append("take_profit_2") + values.append(take_profit_2) + + placeholders = ", ".join(["%s"] * len(columns)) + sql = f"INSERT INTO trades ({', '.join(columns)}) VALUES ({placeholders})" + db.execute_update(sql, tuple(values)) return db.execute_one("SELECT LAST_INSERT_ID() as id")['id'] @staticmethod diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index d9a2f83..903666c 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -266,6 +266,15 @@ const TradeList = () => { {stats.avg_pnl.toFixed(2)} USDT + {"total_notional_usdt" in stats && ( +