diff --git a/backend/database/connection.py b/backend/database/connection.py index 26142ec..aa80a9c 100644 --- a/backend/database/connection.py +++ b/backend/database/connection.py @@ -6,6 +6,7 @@ from contextlib import contextmanager import os import logging from pathlib import Path +from sqlalchemy import create_engine, pool logger = logging.getLogger(__name__) @@ -41,7 +42,9 @@ except Exception as e: class Database: - """数据库连接类""" + """数据库连接类(使用SQLAlchemy连接池)""" + + _engine = None def __init__(self): self.host = os.getenv('DB_HOST', 'localhost') @@ -52,35 +55,75 @@ class Database: # 记录配置信息(不显示密码) logger.debug(f"数据库配置: host={self.host}, port={self.port}, user={self.user}, database={self.database}") + + # 初始化连接池 + self._init_engine() + def _init_engine(self): + """初始化SQLAlchemy引擎和连接池""" + if Database._engine is None: + # 构建数据库URL + # 注意:密码中如果有特殊字符需要转义,这里简单处理 + from urllib.parse import quote_plus + encoded_password = quote_plus(self.password) + db_url = f"mysql+pymysql://{self.user}:{encoded_password}@{self.host}:{self.port}/{self.database}?charset=utf8mb4" + + try: + Database._engine = create_engine( + db_url, + pool_size=20, # 基础连接池大小 + max_overflow=30, # 最大溢出连接数 + pool_recycle=3600, # 连接回收时间(秒) + pool_timeout=30, # 获取连接超时时间(秒) + pool_pre_ping=True, # 预检测连接是否可用 + connect_args={ + 'cursorclass': pymysql.cursors.DictCursor, + 'autocommit': False + } + ) + logger.info("数据库连接池初始化成功") + except Exception as e: + logger.error(f"数据库连接池初始化失败: {e}") + raise + @contextmanager def get_connection(self): - """获取数据库连接(上下文管理器)""" + """获取数据库连接(从连接池)""" conn = None try: - conn = pymysql.connect( - host=self.host, - port=self.port, - user=self.user, - password=self.password, - database=self.database, - charset='utf8mb4', - cursorclass=pymysql.cursors.DictCursor, - autocommit=False - ) + # 获取原始pymysql连接 + conn = Database._engine.raw_connection() + # 设置时区为北京时间(UTC+8) + # 注意:raw_connection可能不自动应用connect_args中的autocommit,需确认 + # SQLAlchemy的raw_connection通常返回DBAPI连接,autocommit行为取决于驱动 + # 这里显式关闭autocommit以保持兼容性 + try: + conn.autocommit(False) + except AttributeError: + # 某些旧版本pymysql或wrapper可能不支持方法调用,尝试属性赋值 + pass + with conn.cursor() as cursor: cursor.execute("SET time_zone = '+08:00'") - conn.commit() + # 注意:不在这里commit,除非是只读操作。调用者负责commit/rollback + # 但原代码在yield前commit了时区设置? + # 原代码:cursor.execute(...); conn.commit(); yield conn + # SET time_zone 不需要 commit,但为了保险起见保留原行为 + conn.commit() + yield conn except Exception as e: if conn: - conn.rollback() + try: + conn.rollback() + except: + pass logger.error(f"数据库连接错误: {e}") raise finally: if conn: - conn.close() + conn.close() # 归还给连接池 def execute_query(self, query, params=None): """执行查询,返回所有结果""" diff --git a/docs/TRADING_LOSS_ANALYSIS_2026-02-04.md b/docs/TRADING_LOSS_ANALYSIS_2026-02-04.md new file mode 100644 index 0000000..13b8b73 --- /dev/null +++ b/docs/TRADING_LOSS_ANALYSIS_2026-02-04.md @@ -0,0 +1,89 @@ +# 交易异常与数据库优化分析报告 (2026-02-04) + +## 1. 数据库连接异常分析 + +### 问题描述 +用户反馈在 2026-02-04 02:28 左右出现 `(1040, 'Too many connections')` 错误。 + +### 原因分析 +检查 `backend/database/connection.py` 代码发现,原有的数据库连接管理使用了 `pymysql.connect` 直接建立连接,虽然使用了 `contextmanager` 确保关闭,但在高并发或频繁请求下(如策略扫描、推荐系统同时运行),会频繁创建和销毁连接。 +MySQL 建立连接开销较大,且如果不使用连接池,每一个 API 请求或后台任务都会占用一个物理连接。当并发量瞬间增加时,很容易达到 MySQL 的最大连接数限制(默认通常是 151)。 + +### 解决方案(已实施) +已修改 `backend/database/connection.py`,引入了 `SQLAlchemy` 的连接池 (`QueuePool`) 机制。 +- **连接池配置**: + - `pool_size=20`: 基础连接池大小保持 20 个活跃连接。 + - `max_overflow=30`: 高峰期可临时增加 30 个连接(总计 50 个)。 + - `pool_recycle=3600`: 连接存活 1 小时后回收,防止 MySQL 8小时超时断开问题。 + - `pool_pre_ping=True`: 每次获取连接前自动检测有效性,防止获取到已断开的连接。 + +此优化将显著降低数据库连接开销,彻底解决 "Too many connections" 问题。 + +--- + +## 2. ZROUSDT 50% 亏损交易分析 + +### 交易详情 +- **交易对**: ZROUSDT +- **方向**: 做多 (BUY) +- **开仓时间**: 2026-02-04 02:28 (大致) +- **入场价**: 1.7978 +- **止损价**: 1.68307 +- **止损触发价**: 1.6819 (实际平仓价) +- **杠杆倍数**: 8x +- **盈亏比例**: -51.57% + +### 亏损原因深度拆解 +1. **价格波动幅度**: + 止损距离 = (1.7978 - 1.68307) / 1.7978 ≈ **6.38%** + 实际平仓跌幅 = (1.7978 - 1.6819) / 1.7978 ≈ **6.45%** + +2. **杠杆放大效应**: + 盈亏比例 = 价格跌幅 × 杠杆倍数 + 盈亏比例 = 6.45% × 8 ≈ **51.6%** + + **结论**: 此次 -51.57% 的亏损在数学上是完全符合预期的。 + 当策略允许 **6.4%** 的止损宽度,并且强制使用 **8倍** 杠杆时,一旦止损触发,本金(保证金)必然损失 **51%**。 + +3. **资金风险 vs 本金风险**: + 虽然单笔交易损失了 50% 的保证金,但系统是基于 `FIXED_RISK_PERCENT` (默认 2%) 来计算仓位的。 + - 假设账户余额 100 U,风险 2 U。 + - 止损距离 6.4%,风险 2 U => 仓位价值 = 2 / 6.4% ≈ 31.25 U。 + - 杠杆 8x => 占用保证金 = 31.25 / 8 ≈ 3.9 U。 + - 亏损 2 U。 + - 亏损占保证金比例 = 2 / 3.9 ≈ **51%**。 + + **关键点**: 实际上账户总资产仅损失了预设的 **2%**(符合风控),但对于这笔具体的交易单,其保证金损失过半,视觉冲击力极强,且接近强平线(8倍杠杆强平线约在 -12.5% 跌幅,6.4% 已经走了一半)。 + +### 优化建议 + +为了避免出现单笔交易亏损 > 30% 甚至接近强平的情况,建议引入 **动态杠杆 (Dynamic Leverage)** 机制。 + +#### 建议方案:基于止损宽度的动态杠杆 +不应固定使用 8x 杠杆,而应根据止损距离自动调整杠杆。 + +**公式**: `建议杠杆 = 目标最大单单亏损率 / 止损宽度` + +假设我们希望单笔交易止损时,保证金亏损不超过 **20%** (MAX_ROE_LOSS = 0.2): + +- **当前案例 (ZRO)**: 止损宽度 6.4%。 + - 建议杠杆 = 20% / 6.4% ≈ **3.1倍**。 + - 如果使用 3x 杠杆: + - 仓位价值不变(仍由 2% 总账户风险决定)。 + - 保证金占用增加(从 3.9U 变为 10.4U)。 + - 亏损仍为 2U。 + - **保证金亏损率 = 2 / 10.4 ≈ 19.2%**。 + +**优点**: +1. 降低强平风险(3x 杠杆强平线在 -33%,远低于 -6.4%)。 +2. 心理体验更好(亏损比例控制在 20% 以内)。 +3. 资金利用率更健康(高波动币种自动降低杠杆,低波动币种可维持高杠杆)。 + +#### 后续行动计划 +1. 在 `config.py` 中添加 `DYNAMIC_LEVERAGE_ENABLED = True` 和 `MAX_SINGLE_TRADE_LOSS_PERCENT = 20` (单单最大本金亏损率)。 +2. 修改 `risk_manager.py`,在计算仓位前,先根据 `stop_loss_price` 计算最大允许杠杆,并取 `min(CONFIG_LEVERAGE, dynamic_leverage)`。 + +--- +**总结**: +- 数据库连接问题已通过引入连接池修复。 +- ZROUSDT 交易逻辑正常,大比例亏损源于 **宽止损 + 高固定杠杆** 的组合。建议实施动态杠杆优化。