This commit is contained in:
薇薇安 2026-01-18 19:44:24 +08:00
parent d59fa97036
commit e3ecaf1232
7 changed files with 290 additions and 48 deletions

View File

@ -275,10 +275,15 @@ async def get_realtime_positions():
if margin > 0: if margin > 0:
pnl_percent = (unrealized_pnl / margin) * 100 pnl_percent = (unrealized_pnl / margin) * 100
# 尝试从数据库获取开仓时间、止损止盈价格 # 尝试从数据库获取开仓时间、止损止盈价格(以及交易规模字段)
entry_time = None entry_time = None
stop_loss_price = None stop_loss_price = None
take_profit_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: try:
from database.models import Trade from database.models import Trade
db_trades = Trade.get_by_symbol(pos.get('symbol'), status='open') 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') stop_loss_price = db_trade.get('stop_loss_price')
take_profit_price = db_trade.get('take_profit_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 break
except Exception as e: except Exception as e:
logger.debug(f"获取数据库信息失败: {e}") logger.debug(f"获取数据库信息失败: {e}")
@ -302,14 +312,21 @@ async def get_realtime_positions():
"side": "BUY" if position_amt > 0 else "SELL", "side": "BUY" if position_amt > 0 else "SELL",
"quantity": abs(position_amt), "quantity": abs(position_amt),
"entry_price": entry_price, "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, "mark_price": mark_price,
"pnl": unrealized_pnl, "pnl": unrealized_pnl,
"pnl_percent": pnl_percent, # 基于保证金的盈亏百分比 "pnl_percent": pnl_percent, # 基于保证金的盈亏百分比
"leverage": int(pos.get('leverage', 1)), "leverage": int(pos.get('leverage', 1)),
"entry_time": entry_time, # 开仓时间 "entry_time": entry_time, # 开仓时间
"stop_loss_price": stop_loss_price, # 止损价格(如果可用) "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)} 个有效持仓") logger.info(f"格式化后 {len(formatted_positions)} 个有效持仓")

View File

@ -231,6 +231,11 @@ async def get_trade_stats(
"win_rate": len(win_trades) / len(meaningful_trades) * 100 if meaningful_trades else 0, # 基于有意义的交易计算胜率 "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), "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, "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": { "filters": {
"start_timestamp": start_timestamp, "start_timestamp": start_timestamp,
"end_timestamp": end_timestamp, "end_timestamp": end_timestamp,

View File

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

View File

@ -24,20 +24,34 @@ CREATE TABLE IF NOT EXISTS `trades` (
`side` VARCHAR(10) NOT NULL COMMENT 'BUY, SELL', `side` VARCHAR(10) NOT NULL COMMENT 'BUY, SELL',
`quantity` DECIMAL(20, 8) NOT NULL, `quantity` DECIMAL(20, 8) NOT NULL,
`entry_price` 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), `exit_price` DECIMAL(20, 8),
`entry_time` INT UNSIGNED NOT NULL COMMENT '入场时间Unix时间戳秒数', `entry_time` INT UNSIGNED NOT NULL COMMENT '入场时间Unix时间戳秒数',
`exit_time` INT UNSIGNED NULL COMMENT '平仓时间Unix时间戳秒数', `exit_time` INT UNSIGNED NULL COMMENT '平仓时间Unix时间戳秒数',
`pnl` DECIMAL(20, 8) DEFAULT 0, `pnl` DECIMAL(20, 8) DEFAULT 0,
`pnl_percent` DECIMAL(10, 4) DEFAULT 0, `pnl_percent` DECIMAL(10, 4) DEFAULT 0,
`leverage` INT DEFAULT 10, `leverage` INT DEFAULT 10,
`entry_order_id` BIGINT NULL COMMENT '币安开仓订单号(用于对账)',
`exit_order_id` BIGINT NULL COMMENT '币安平仓订单号(用于对账)',
`entry_reason` TEXT, `entry_reason` TEXT,
`exit_reason` VARCHAR(50) COMMENT 'stop_loss, take_profit, trailing_stop, manual', `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', `status` VARCHAR(20) DEFAULT 'open' COMMENT 'open, closed, cancelled',
`created_at` INT UNSIGNED NOT NULL DEFAULT (UNIX_TIMESTAMP()) COMMENT '创建时间Unix时间戳秒数', `created_at` INT UNSIGNED NOT NULL DEFAULT (UNIX_TIMESTAMP()) COMMENT '创建时间Unix时间戳秒数',
INDEX `idx_symbol` (`symbol`), INDEX `idx_symbol` (`symbol`),
INDEX `idx_entry_time` (`entry_time`), INDEX `idx_entry_time` (`entry_time`),
INDEX `idx_status` (`status`), 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='交易记录表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易记录表';
-- 账户快照表 -- 账户快照表

View File

@ -88,7 +88,22 @@ class Trade:
"""交易记录模型""" """交易记录模型"""
@staticmethod @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: Args:
@ -101,31 +116,73 @@ class Trade:
entry_order_id: 币安开仓订单号可选用于对账 entry_order_id: 币安开仓订单号可选用于对账
stop_loss_price: 实际使用的止损价格考虑了ATR等动态计算 stop_loss_price: 实际使用的止损价格考虑了ATR等动态计算
take_profit_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() entry_time = get_beijing_time()
# 检查字段是否存在兼容旧数据库schema # 自动计算 notional/margin若调用方没传
try: try:
db.execute_one("SELECT stop_loss_price FROM trades LIMIT 1") if notional_usdt is None and quantity is not None and entry_price is not None:
has_stop_take_fields = True notional_usdt = float(quantity) * float(entry_price)
except: except Exception:
has_stop_take_fields = False 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
if has_stop_take_fields: def _has_column(col: str) -> bool:
db.execute_update( try:
"""INSERT INTO trades db.execute_one(f"SELECT {col} FROM trades LIMIT 1")
(symbol, side, quantity, entry_price, leverage, entry_reason, status, entry_time, entry_order_id, stop_loss_price, take_profit_price) return True
VALUES (%s, %s, %s, %s, %s, %s, 'open', %s, %s, %s, %s)""", except Exception:
(symbol, side, quantity, entry_price, leverage, entry_reason, entry_time, entry_order_id, stop_loss_price, take_profit_price) return False
)
else: # 动态构建 INSERT兼容不同schema
# 兼容旧schema columns = ["symbol", "side", "quantity", "entry_price", "leverage", "entry_reason", "status", "entry_time"]
db.execute_update( values = [symbol, side, quantity, entry_price, leverage, entry_reason, "open", entry_time]
"""INSERT INTO trades
(symbol, side, quantity, entry_price, leverage, entry_reason, status, entry_time, entry_order_id) if _has_column("entry_order_id"):
VALUES (%s, %s, %s, %s, %s, %s, 'open', %s, %s)""", columns.append("entry_order_id")
(symbol, side, quantity, entry_price, leverage, entry_reason, entry_time, 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'] return db.execute_one("SELECT LAST_INSERT_ID() as id")['id']
@staticmethod @staticmethod

View File

@ -266,6 +266,15 @@ const TradeList = () => {
{stats.avg_pnl.toFixed(2)} USDT {stats.avg_pnl.toFixed(2)} USDT
</div> </div>
</div> </div>
{"total_notional_usdt" in stats && (
<div className="stat-card">
<div className="stat-label">总交易量名义</div>
<div className="stat-value">{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT</div>
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
口径入场价×数量
</div>
</div>
)}
</div> </div>
)} )}
@ -281,6 +290,7 @@ const TradeList = () => {
<th>交易对</th> <th>交易对</th>
<th>方向</th> <th>方向</th>
<th>数量</th> <th>数量</th>
<th>名义</th>
<th>保证金</th> <th>保证金</th>
<th>入场价</th> <th>入场价</th>
<th>出场价</th> <th>出场价</th>
@ -295,12 +305,18 @@ const TradeList = () => {
</thead> </thead>
<tbody> <tbody>
{trades.map(trade => { {trades.map(trade => {
// entry_value_usdtleverage使/ // /使notional_usdt / margin_usdt退
const entryValue = trade.entry_value_usdt !== undefined const notional = trade.notional_usdt !== undefined && trade.notional_usdt !== null
? parseFloat(trade.entry_value_usdt) ? parseFloat(trade.notional_usdt)
: (parseFloat(trade.quantity || 0) * parseFloat(trade.entry_price || 0)) : (
trade.entry_value_usdt !== undefined && trade.entry_value_usdt !== null
? parseFloat(trade.entry_value_usdt)
: (parseFloat(trade.quantity || 0) * parseFloat(trade.entry_price || 0))
)
const leverage = parseFloat(trade.leverage || 10) const leverage = parseFloat(trade.leverage || 10)
const margin = leverage > 0 ? entryValue / leverage : 0 const margin = trade.margin_usdt !== undefined && trade.margin_usdt !== null
? parseFloat(trade.margin_usdt)
: (leverage > 0 ? notional / leverage : 0)
// / // /
const pnl = parseFloat(trade.pnl || 0) const pnl = parseFloat(trade.pnl || 0)
@ -349,6 +365,7 @@ const TradeList = () => {
<td>{trade.symbol}</td> <td>{trade.symbol}</td>
<td className={trade.side === 'BUY' ? 'buy' : 'sell'}>{trade.side}</td> <td className={trade.side === 'BUY' ? 'buy' : 'sell'}>{trade.side}</td>
<td>{parseFloat(trade.quantity).toFixed(4)}</td> <td>{parseFloat(trade.quantity).toFixed(4)}</td>
<td>{notional >= 0.01 ? notional.toFixed(2) : notional.toFixed(4)} USDT</td>
<td>{margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT</td> <td>{margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT</td>
<td>{parseFloat(trade.entry_price).toFixed(4)}</td> <td>{parseFloat(trade.entry_price).toFixed(4)}</td>
<td>{trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'}</td> <td>{trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'}</td>
@ -374,12 +391,18 @@ const TradeList = () => {
{/* 移动端卡片 */} {/* 移动端卡片 */}
<div className="trades-cards"> <div className="trades-cards">
{trades.map(trade => { {trades.map(trade => {
// // /
const entryValue = trade.entry_value_usdt !== undefined const notional = trade.notional_usdt !== undefined && trade.notional_usdt !== null
? parseFloat(trade.entry_value_usdt) ? parseFloat(trade.notional_usdt)
: (parseFloat(trade.quantity || 0) * parseFloat(trade.entry_price || 0)) : (
trade.entry_value_usdt !== undefined && trade.entry_value_usdt !== null
? parseFloat(trade.entry_value_usdt)
: (parseFloat(trade.quantity || 0) * parseFloat(trade.entry_price || 0))
)
const leverage = parseFloat(trade.leverage || 10) const leverage = parseFloat(trade.leverage || 10)
const margin = leverage > 0 ? entryValue / leverage : 0 const margin = trade.margin_usdt !== undefined && trade.margin_usdt !== null
? parseFloat(trade.margin_usdt)
: (leverage > 0 ? notional / leverage : 0)
const pnl = parseFloat(trade.pnl || 0) const pnl = parseFloat(trade.pnl || 0)
const pnlPercent = margin > 0 ? (pnl / margin) * 100 : 0 const pnlPercent = margin > 0 ? (pnl / margin) * 100 : 0
@ -422,6 +445,10 @@ const TradeList = () => {
<span className="trade-card-label">数量</span> <span className="trade-card-label">数量</span>
<span className="trade-card-value">{parseFloat(trade.quantity).toFixed(4)}</span> <span className="trade-card-value">{parseFloat(trade.quantity).toFixed(4)}</span>
</div> </div>
<div className="trade-card-field">
<span className="trade-card-label">名义</span>
<span className="trade-card-value">{notional >= 0.01 ? notional.toFixed(2) : notional.toFixed(4)} USDT</span>
</div>
<div className="trade-card-field"> <div className="trade-card-field">
<span className="trade-card-label">保证金</span> <span className="trade-card-label">保证金</span>
<span className="trade-card-value">{margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT</span> <span className="trade-card-value">{margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT</span>

View File

@ -317,6 +317,23 @@ class PositionManager:
quantity = filled_quantity # 使用实际成交数量 quantity = filled_quantity # 使用实际成交数量
logger.info(f"{symbol} [开仓] ✓ 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f}), 成交数量: {quantity:.4f}") logger.info(f"{symbol} [开仓] ✓ 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f}), 成交数量: {quantity:.4f}")
# 分步止盈(基于“实际成交价 + 已计算的止损/止盈”)
if side == 'BUY':
take_profit_1 = entry_price + (entry_price - stop_loss_price) # 盈亏比1:1
else:
take_profit_1 = entry_price - (stop_loss_price - entry_price) # 盈亏比1:1
take_profit_2 = take_profit_price
# 交易规模:名义/保证金用于统计总交易量与UI展示
try:
notional_usdt = float(entry_price) * float(quantity)
except Exception:
notional_usdt = None
try:
margin_usdt = (float(notional_usdt) / float(leverage)) if notional_usdt is not None and float(leverage) > 0 else notional_usdt
except Exception:
margin_usdt = None
# 记录到数据库(只有在订单真正成交后才保存) # 记录到数据库(只有在订单真正成交后才保存)
trade_id = None trade_id = None
if DB_AVAILABLE and Trade: if DB_AVAILABLE and Trade:
@ -329,7 +346,14 @@ class PositionManager:
entry_price=entry_price, # 使用实际成交价格 entry_price=entry_price, # 使用实际成交价格
leverage=leverage, leverage=leverage,
entry_reason=entry_reason, entry_reason=entry_reason,
entry_order_id=entry_order_id # 保存币安订单号 entry_order_id=entry_order_id, # 保存币安订单号
stop_loss_price=stop_loss_price,
take_profit_price=take_profit_price,
take_profit_1=take_profit_1,
take_profit_2=take_profit_2,
atr=atr,
notional_usdt=notional_usdt,
margin_usdt=margin_usdt,
) )
logger.info(f"{symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})") logger.info(f"{symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})")
except Exception as e: except Exception as e:
@ -343,16 +367,6 @@ class PositionManager:
elif not Trade: elif not Trade:
logger.warning(f"Trade模型未导入无法保存 {symbol} 交易记录") logger.warning(f"Trade模型未导入无法保存 {symbol} 交易记录")
# 计算分步止盈价格
# 第一目标盈亏比1:1保守了结50%仓位)
if side == 'BUY':
take_profit_1 = entry_price + (entry_price - stop_loss_price) # 盈亏比1:1
else:
take_profit_1 = entry_price - (stop_loss_price - entry_price) # 盈亏比1:1
# 第二目标原始止盈价激进剩余50%仓位)
take_profit_2 = take_profit_price
# 记录持仓信息(包含动态止损止盈和分步止盈) # 记录持仓信息(包含动态止损止盈和分步止盈)
from datetime import datetime from datetime import datetime
position_info = { position_info = {
@ -1483,7 +1497,10 @@ class PositionManager:
quantity=quantity, quantity=quantity,
entry_price=entry_price, entry_price=entry_price,
leverage=binance_position.get('leverage', 10), leverage=binance_position.get('leverage', 10),
entry_reason='manual_entry' # 标记为手动开仓 entry_reason='manual_entry', # 标记为手动开仓
# 手动开仓无法拿到策略侧ATR/分步止盈,这里尽量补齐“规模字段”
notional_usdt=(float(entry_price) * float(quantity)) if entry_price and quantity else None,
margin_usdt=((float(entry_price) * float(quantity)) / float(binance_position.get('leverage', 10))) if entry_price and quantity and float(binance_position.get('leverage', 10) or 0) > 0 else None,
) )
logger.info(f"{symbol} [状态同步] ✓ 数据库记录已创建 (ID: {trade_id})") logger.info(f"{symbol} [状态同步] ✓ 数据库记录已创建 (ID: {trade_id})")