From 2b5906ca6d4d248345d9342416911bd578c50992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Sun, 15 Feb 2026 13:35:33 +0800 Subject: [PATCH] 1 --- API_KEY_与IP限频说明.md | 72 ------ BINANCE_IP_BAN_1003.md | 68 ------ analyze_bad_trades.py | 41 ---- analyze_json.py | 70 ------ analyze_losses.py | 89 -------- analyze_zro.py | 45 ---- .../database/dedupe_trades_by_entry_order.sql | 57 +++++ backend/database/models.py | 42 ++++ debug_config.py | 58 ----- debug_sltp.py | 61 ----- fix_db_positions.py | 131 ----------- fix_trade_records.py | 190 ---------------- inspect_db.py | 34 --- test_db_pool.py | 42 ---- trading_system/binance_client.py | 9 +- trading_system/config.py | 1 + trading_system/position_manager.py | 128 +++++++---- update_atr_config.py | 52 ----- update_db_schema.py | 32 --- update_global_config.py | 70 ------ update_risk_config_db.py | 77 ------- update_stop_loss_orders.py | 215 ------------------ update_strategy_config.py | 34 --- 山寨币策略配置评估与优化建议.md | 157 ------------- 24 files changed, 189 insertions(+), 1586 deletions(-) delete mode 100644 API_KEY_与IP限频说明.md delete mode 100644 BINANCE_IP_BAN_1003.md delete mode 100644 analyze_bad_trades.py delete mode 100644 analyze_json.py delete mode 100644 analyze_losses.py delete mode 100644 analyze_zro.py create mode 100644 backend/database/dedupe_trades_by_entry_order.sql delete mode 100644 debug_config.py delete mode 100644 debug_sltp.py delete mode 100644 fix_db_positions.py delete mode 100644 fix_trade_records.py delete mode 100644 inspect_db.py delete mode 100644 test_db_pool.py delete mode 100644 update_atr_config.py delete mode 100644 update_db_schema.py delete mode 100644 update_global_config.py delete mode 100644 update_risk_config_db.py delete mode 100644 update_stop_loss_orders.py delete mode 100644 update_strategy_config.py delete mode 100644 山寨币策略配置评估与优化建议.md diff --git a/API_KEY_与IP限频说明.md b/API_KEY_与IP限频说明.md deleted file mode 100644 index 9db3cd8..0000000 --- a/API_KEY_与IP限频说明.md +++ /dev/null @@ -1,72 +0,0 @@ -# API Key 使用方式与 IP 限频说明 - -## 一、获取余额、市场报价:每个账号用自己的 API Key 吗? - -**是的。每个账号都用自己配置的 API Key。** - -### 1. 后端 Web API(前端请求「我的余额」「持仓」等) - -- 请求会带上当前登录用户/选择的 **account_id**。 -- 后端用 `Account.get_credentials(account_id)` 取**该账号**的 `BINANCE_API_KEY` / `BINANCE_API_SECRET`。 -- 用这份 Key 创建 `BinanceClient`,再调 `get_account_balance()`、`get_ticker_24h()`、持仓、下单等。 -- **结论**:账号 A 的请求用 A 的 Key,账号 B 用 B 的 Key,互不混用。 - -### 2. 交易进程(自动交易 / 扫描) - -- 每个账号一个独立进程(由 supervisor 按 account 起多个 `trading_system.main`)。 -- 每个进程启动时通过环境变量 **`ATS_ACCOUNT_ID`** 指定自己是哪个账号。 -- 配置通过 `ConfigManager.for_account(account_id)` 读取,其中 **BINANCE_API_KEY / BINANCE_API_SECRET** 来自该 account_id 在 `accounts` 表里配置的密钥。 -- 该进程内所有调用(**获取余额、获取交易对列表、获取行情 ticker/K 线、下单**)都用**同一个 BinanceClient**,即**该账号的 API Key**。 -- **结论**:每个账号的扫描与交易都只用自己的 Key,没有“统一一个 Key”的情况。 - -### 3. 推荐服务(仅行情、不下单) - -- `recommendations_main` deliberately 使用 **空 API Key**(`BinanceClient(api_key="", api_secret="")`),只调**公开接口**(行情、K 线等),不占用任何账号的 Key,也不读 account 的密钥。 - ---- - -## 二、-1003「IP 被封」和「轮转 API Key」的关系 - -错误示例:`APIError(code=-1003): Way too many requests; IP(43.199.103.162) banned until ...` - -- 这里限制的是 **IP**,不是某个 API Key。 -- 所有从你这台服务器(同一出口 IP)发出的请求,不管用哪个账号的 Key,都会算在**同一个 IP** 上。 -- 所以:**换用不同账号的 API Key 轮转,不能绕过或解除 IP 封禁**;封禁期间用哪个 Key 都会失败。 - -轮转 Key 只有在币安按 **API Key** 维度限频时才有用;当前你遇到的是 **IP 维度** 的封禁,所以轮转无法解决“等多久才能用”的问题,只能等该 IP 解封时间到期。 - ---- - -## 三、为什么多账号时更容易触发 IP 封禁? - -- 每个账号一个进程,**每个进程都会自己做一整轮市场扫描**: - - `get_all_usdt_pairs()` - - 对大量交易对再请求 K 线、ticker 等 -- 若有 3 个账号,同一台机器上就是 **3 份完整扫描**,请求量约等于单账号的 3 倍,且都从**同一 IP** 发出,更容易触发「Way too many requests」和 IP 封禁。 - ---- - -## 四、可以怎么做来减少封禁 / 尽快恢复使用? - -1. **解封前** - - 等日志里 `banned until` 的时间戳过期(参见 `BINANCE_IP_BAN_1003.md` 里用脚本算「还要等多久」)。 - -2. **解封后降低同一 IP 的请求量** - - 拉大 **SCAN_INTERVAL**(如 900 → 1200 或 1800 秒)。 - - 适当减小 **MAX_SCAN_SYMBOLS** / **TOP_N_SYMBOLS**。 - - 多账号时,考虑**只用一个进程做扫描、结果写入 Redis,各账号进程只读**,这样行情/扫描请求只打一份,从同一 IP 出去的请求会少很多(需改架构,可后续做)。 - -3. **轮转 API Key** - - 对**解除当前这次 IP 封禁**没有帮助。 - - 若未来遇到的是「按 Key 限频」而不是「按 IP 封禁」,再考虑按 Key 轮转或分摊请求才有意义。 - ---- - -## 五、总结表 - -| 能力 | 谁在调用 | 用的 API Key | -|----------------|----------------|----------------------| -| 获取余额 | 后端 / 交易进程 | 当前 account_id 的 Key | -| 获取市场报价等 | 交易进程 | 当前 account_id 的 Key | -| 推荐服务行情 | recommendations_main | 空 Key(仅公开接口) | -| -1003 IP 封禁 | - | 轮转 Key **无法**绕过,需等解封并降请求量 | diff --git a/BINANCE_IP_BAN_1003.md b/BINANCE_IP_BAN_1003.md deleted file mode 100644 index fe8e6e0..0000000 --- a/BINANCE_IP_BAN_1003.md +++ /dev/null @@ -1,68 +0,0 @@ -# 币安 API -1003 限频/封禁说明 - -## 错误含义 - -- **APIError(code=-1003)**: `Way too many requests; IP ... banned until 1771041059726` -- 表示当前 IP 请求过于频繁,被**临时封禁**,直到指定时间戳(毫秒)后自动解除。 - -## 如何查看「还要等多久」 - -时间戳 `1771041059726` 是**毫秒**,表示封禁**解除时间**(Unix 时间戳,毫秒)。 - -在项目根目录执行: - -```bash -python3 -c " -from datetime import datetime, timezone -ts_ms = 1771041059726 # 替换成你日志里的时间戳 -utc = datetime.fromtimestamp(ts_ms/1000, tz=timezone.utc) -now = datetime.now(timezone.utc) -delta = utc - now -print('解封时间(UTC):', utc.strftime('%Y-%m-%d %H:%M:%S')) -print('当前时间(UTC):', now.strftime('%Y-%m-%d %H:%M:%S')) -if delta.total_seconds() > 0: - print('还需等待:', delta.days, '天', delta.seconds//3600, '小时', (delta.seconds%3600)//60, '分钟') -else: - print('已过解封时间,可重试;若仍报错可等几分钟或换网络。') -" -``` - -把上面脚本里的 `1771041059726` 换成你实际日志中的 `banned until` 后面的数字即可。 - -## 如何减少再次被限/封禁 - -1. **拉大扫描间隔**:全局配置里把 `SCAN_INTERVAL` 调大(如 900 → 1200 或 1800),降低整体请求频率。 -2. **缩小扫描范围**:适当减小 `MAX_SCAN_SYMBOLS`、`TOP_N_SYMBOLS`,减少单次扫描的 API 调用量。 -3. **并发已做限制**:`market_scanner` 已用信号量限制并发(如 3),避免同时打爆;若仍触限,可再减小并发或增加批次间延迟。 -4. **错误提示**:日志里「分析超时(10秒)」多是因为当时已被限频/封禁导致请求挂起或失败,解封后一般会恢复。 - -解封后若仍偶发 -1003,可先等 1~2 分钟再跑,或临时增大 `SCAN_INTERVAL` 再观察。 - ---- - -## 获取持仓/成交超时(TimeoutError) - -若日志出现「获取持仓信息最终失败 (已重试 7 次): TimeoutError」或「获取成交记录失败 XXX (已重试 5 次): TimeoutError」: - -1. **网络/限频**:与 -1003 类似,可能是当时网络抖动或请求排队,重试已用 60 秒超时 × 多轮;过几分钟通常恢复。 -2. **适当拉长只读超时**(可选):在运行交易进程的环境里设置环境变量 - `READ_ONLY_REQUEST_TIMEOUT=90`(默认 60 秒),只读接口(持仓、成交、交易对信息)单次等待时间会变长,**不影响下单/止损止盈的快速失败**。 -3. **本次已做**:获取交易对信息(如 ENAUSDT)增加 60 秒超时 + 3 次重试;获取成交记录后几次重试间隔改为 2 秒;开仓失败时会打出完整异常与堆栈,便于排查。 - ---- - -## 近期改动是否增加请求量? - -**不会。** 近期「止损/止盈按保证金封顶」(USE_MARGIN_CAP_FOR_SL、USE_MARGIN_CAP_FOR_TP)只改动了 **risk_manager 里 SL/TP 价格的计算方式**,没有新增任何对币安 API 的调用。 --1003 来自**原有**的请求:如 `get_open_positions`、`get_account_balance`、`sync_positions_with_binance`、策略轮询、持仓/挂单同步等。要降低 -1003 概率,请拉大 `SCAN_INTERVAL`、减小扫描/同步频率(见上文「如何减少再次被限/封禁」),或改用 WebSocket 获取行情/持仓(若已接入)。 - ---- - -## 拿不到余额时,止盈止损还能正常执行吗? - -**能。** 只要止损/止盈已经以**条件单**形式挂在币安(STOP_MARKET / TAKE_PROFIT_MARKET),触发与成交都由**币安撮合引擎**执行,不依赖本机是否还能调 API、能否拿到余额或持仓。 - -- **已挂在交易所的 SL/TP**:即使本机拿不到余额、持仓接口报错或 IP 被临时封禁,价格触及后仍会按交易所订单正常触发、平仓。 -- **依赖本机的部分**:若在封禁期间**新开仓**或需要**补挂** SL/TP,会因请求失败而无法下单;已存在仓位若之前已成功调用 `_ensure_exchange_sltp_orders` 并挂上保护单,则不受影响。 - -**结论**:拿不到余额/持仓只影响「本机展示与同步、新开仓、补挂单」;**已挂在币安上的止盈止损单会照常执行**,可放心。 diff --git a/analyze_bad_trades.py b/analyze_bad_trades.py deleted file mode 100644 index 6e2bc75..0000000 --- a/analyze_bad_trades.py +++ /dev/null @@ -1,41 +0,0 @@ - -import json -import sys - -def analyze_trades(file_path): - try: - with open(file_path, 'r', encoding='utf-8') as f: - trades = json.load(f) - except Exception as e: - print(f"Error reading file: {e}") - return - - print(f"Analyzing {len(trades)} trades...") - - tp_losses = [] - sync_exits = [] - - for t in trades: - pnl = t.get('盈亏', 0) - reason = t.get('离场原因', '') - duration = t.get('持仓时长分钟') - - if reason == 'take_profit' and pnl < 0: - tp_losses.append(t) - - if reason == 'sync' and duration == 0: - sync_exits.append(t) - - print("\n[Anomalies 1: Negative PnL with 'take_profit' reason]") - for t in tp_losses: - print(f"ID: {t.get('交易ID')} | Symbol: {t.get('交易对')} | Side: {t.get('方向')} | " - f"Entry: {t.get('入场价')} | Exit: {t.get('出场价')} | " - f"PnL: {t.get('盈亏')} | TP Price: {t.get('止盈价')}") - - print("\n[Anomalies 2: Immediate Sync Exits (Duration 0)]") - for t in sync_exits: - print(f"ID: {t.get('交易ID')} | Symbol: {t.get('交易对')} | PnL: {t.get('盈亏')} | " - f"Entry Time: {t.get('入场时间')} | Exit Time: {t.get('平仓时间')}") - -if __name__ == "__main__": - analyze_trades('/Users/vivian/work/python/auto_trade_sys/交易记录_2026-02-04T06-46-43.json') diff --git a/analyze_json.py b/analyze_json.py deleted file mode 100644 index 8769a35..0000000 --- a/analyze_json.py +++ /dev/null @@ -1,70 +0,0 @@ -import json -from datetime import datetime -import sys - -file_path = '/Users/vivian/work/python/auto_trade_sys/交易记录_2026-02-05T11-39-32.json' - -def parse_time(time_str): - if not time_str: - return None - try: - # Adjust format based on actual JSON content if needed - # Assuming ISO format or similar based on filename - return datetime.fromisoformat(time_str.replace('Z', '+00:00')) - except ValueError: - return None - -try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - closed_trades = [t for t in data if t.get('状态') == '已平仓'] - - total_trades = len(data) - closed_count = len(closed_trades) - - print(f"Total Trades in File: {total_trades}") - print(f"Closed Trades: {closed_count}") - - if closed_count == 0: - print("No closed trades to analyze.") - sys.exit(0) - - wins = [t for t in closed_trades if float(t.get('盈亏', 0)) > 0] - losses = [t for t in closed_trades if float(t.get('盈亏', 0)) <= 0] - - total_pnl = sum(float(t.get('盈亏', 0)) for t in closed_trades) - total_win_pnl = sum(float(t.get('盈亏', 0)) for t in wins) - total_loss_pnl = sum(float(t.get('盈亏', 0)) for t in losses) - - avg_win = total_win_pnl / len(wins) if wins else 0 - avg_loss = total_loss_pnl / len(losses) if losses else 0 - - print(f"\n--- Performance Analysis ---") - print(f"Win Count: {len(wins)}") - print(f"Loss Count: {len(losses)}") - win_rate = (len(wins) / closed_count * 100) if closed_count > 0 else 0 - print(f"Win Rate: {win_rate:.2f}%") - - print(f"Total PnL: {total_pnl:.4f} USDT") - print(f"Avg Win: {avg_win:.4f} USDT") - print(f"Avg Loss: {avg_loss:.4f} USDT") - - if avg_loss != 0: - rr_ratio = abs(avg_win / avg_loss) - print(f"Avg Win / Avg Loss Ratio (R:R): {rr_ratio:.2f}") - else: - print("Avg Win / Avg Loss Ratio (R:R): Infinite (No Losses)") - - # Duration Analysis (if fields exist) - # Assuming fields like '开仓时间' and '平仓时间' exist based on typical trade records - # If not, this part will be skipped or adjusted - - # Let's inspect one record keys to be sure for future reference - if closed_trades: - print(f"\nSample Record Keys: {list(closed_trades[0].keys())}") - -except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() diff --git a/analyze_losses.py b/analyze_losses.py deleted file mode 100644 index 534c721..0000000 --- a/analyze_losses.py +++ /dev/null @@ -1,89 +0,0 @@ - -import json -from datetime import datetime - -# Load trading history -try: - with open('/Users/vivian/work/python/auto_trade_sys/交易记录_2026-02-13T10-01-03.json', 'r') as f: - trades = json.load(f) -except FileNotFoundError: - print("Error: Trading history file not found.") - trades = [] - -# Load current positions -try: - with open('/Users/vivian/work/python/auto_trade_sys/持仓记录_2026-02-13T10-01-38.json', 'r') as f: - positions = json.load(f) -except FileNotFoundError: - print("Error: Current positions file not found.") - positions = [] - -print("=== 亏损分析报告 (Loss Analysis Report) ===") -print(f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") -print("-" * 50) - -# 1. Analyze Closed Trades (Focus on Losses) -print("\n[已平仓亏损分析 (Closed Losing Trades)]") -loss_trades = [t for t in trades if t.get('盈亏', 0) < 0] -if not loss_trades: - print("今天没有亏损交易 (No losing trades today).") -else: - for t in loss_trades: - symbol = t.get('交易对') - pnl = t.get('盈亏', 0) - pnl_pct = t.get('盈亏比例', 0) - margin = t.get('保证金', 0) - leverage = t.get('杠杆', 0) - exit_reason = t.get('离场原因', 'unknown') - - # Calculate expected stop loss based on old config (2.5% price move * leverage) - expected_sl_pct = 2.5 * leverage - - print(f"交易对: {symbol}") - print(f" 盈亏: {pnl:.2f} USDT ({pnl_pct:.2f}%)") - print(f" 保证金: {margin:.2f} USDT") - print(f" 杠杆: {leverage}x") - print(f" 离场原因: {exit_reason}") - if abs(pnl_pct) >= 15: - print(f" ⚠️ 严重亏损 (Severe Loss): >15% margin loss") - if abs(pnl_pct) >= expected_sl_pct - 5 and abs(pnl_pct) <= expected_sl_pct + 10: - print(f" ℹ️ 原因推测: 旧配置 MIN_STOP_LOSS_PRICE_PCT=2.5% (预期亏损 ~{expected_sl_pct}%)") - print("-" * 30) - -# 2. Analyze Current Positions -print("\n[当前持仓分析 (Current Positions Analysis)]") -if not positions: - print("当前无持仓 (No active positions).") -else: - high_risk_positions = [] - for p in positions: - symbol = p.get('symbol') - pnl = p.get('pnl', 0) - pnl_pct = p.get('pnl_percent', 0) - leverage = p.get('leverage', 0) - - print(f"交易对: {symbol}") - print(f" 当前盈亏: {pnl:.2f} USDT ({pnl_pct:.2f}%)") - print(f" 杠杆: {leverage}x") - - # Check against new 10% risk limit - if pnl_pct <= -10: - print(f" 🔴 建议平仓 (Recommended Close): 亏损超过 10% (新配置限制)") - high_risk_positions.append(symbol) - elif pnl_pct <= -5: - print(f" 🟠 风险警告 (Warning): 亏损接近 10%") - else: - print(f" 🟢 正常持有 (Holding)") - print("-" * 30) - - if high_risk_positions: - print(f"\n🚨 紧急建议: 请立即检查并考虑平仓以下 {len(high_risk_positions)} 个高风险持仓:") - print(f" {', '.join(high_risk_positions)}") - else: - print("\n✅ 所有持仓风险在可控范围内 (<10% 亏损).") - -print("\n=== 结论 (Conclusion) ===") -print("1. 今天的严重亏损 (-15% ~ -40%) 主要是由于旧配置 'MIN_STOP_LOSS_PRICE_PCT = 2.5%' 导致的。") -print(" 在 8x-10x 杠杆下,2.5% 的价格波动会导致 20%-25% 的本金亏损。") -print("2. 新配置 (0.5% 最小止损) 已生效,未来交易的止损将控制在 ~5% - 10% 本金亏损。") -print("3. 建议手动平仓当前亏损超过 10% 的老订单,以避免进一步扩大损失。") diff --git a/analyze_zro.py b/analyze_zro.py deleted file mode 100644 index ee8c016..0000000 --- a/analyze_zro.py +++ /dev/null @@ -1,45 +0,0 @@ -from backend.database.connection import db -import json -from datetime import datetime -import time - -def analyze_zro_trades(): - print("Querying ZROUSDT trades...") - # Get all columns - query = "SELECT * FROM trades WHERE symbol='ZROUSDT' ORDER BY id DESC LIMIT 5" - trades = db.execute_query(query) - - if not trades: - print("No ZROUSDT trades found.") - return - - # Print first trade keys to understand schema - print("Schema keys:", list(trades[0].keys())) - - for trade in trades: - print("-" * 50) - ts = trade.get('created_at') - dt = datetime.fromtimestamp(ts) if ts else "N/A" - - print(f"ID: {trade.get('id')}") - print(f"Time: {dt} ({ts})") - print(f"Symbol: {trade.get('symbol')}") - print(f"Side: {trade.get('side')}") - print(f"Entry: {trade.get('entry_price')}") - print(f"Exit: {trade.get('exit_price')}") - print(f"Qty: {trade.get('quantity')}") - print(f"Leverage: {trade.get('leverage')}") # Guessing key - print(f"Realized PnL: {trade.get('realized_pnl')}") - print(f"PnL % (DB): {trade.get('pnl_percent')}") - - # Calculate approximate ROE if leverage is known - leverage = trade.get('leverage', 1) - if leverage is None: leverage = 1 - - pnl_pct = trade.get('pnl_percent') - if pnl_pct: - roe = float(pnl_pct) * float(leverage) - print(f"Calc ROE (est): {roe:.2f}% (assuming leverage {leverage})") - -if __name__ == "__main__": - analyze_zro_trades() diff --git a/backend/database/dedupe_trades_by_entry_order.sql b/backend/database/dedupe_trades_by_entry_order.sql new file mode 100644 index 0000000..d333c22 --- /dev/null +++ b/backend/database/dedupe_trades_by_entry_order.sql @@ -0,0 +1,57 @@ +-- 按 entry_order_id + symbol 去重:同一开仓订单只保留一条(保留 id 最小的,即最早创建的) +-- 使用前请先备份 trades 表;建议先执行「1. 查看重复」确认后再执行「2. 删除重复」 +-- 说明:仅处理 entry_order_id 非空的重复;无开仓订单号的重复记录(如 sync_recovered 脏数据)需人工按 symbol/时间判断后删除。 + +-- ========== 1. 查看重复(只读,不写库)========== +-- 列出所有 (entry_order_id, symbol) 出现多于一次的组,以及每组中的记录 +SELECT + t.entry_order_id, + t.symbol, + COUNT(*) AS cnt, + GROUP_CONCAT(t.id ORDER BY t.id) AS ids, + GROUP_CONCAT(CONCAT(t.id, '(', t.status, ',entry=', FROM_UNIXTIME(t.entry_time), ',exit=', IFNULL(FROM_UNIXTIME(t.exit_time), 'NULL'), ')') ORDER BY t.id SEPARATOR ' | ') AS detail +FROM trades t +WHERE t.entry_order_id IS NOT NULL +GROUP BY t.entry_order_id, t.symbol +HAVING COUNT(*) > 1; + +-- 若有多账号,按 account_id 也分组查看(可选): +-- SELECT account_id, entry_order_id, symbol, COUNT(*) AS cnt, GROUP_CONCAT(id ORDER BY id) AS ids +-- FROM trades WHERE entry_order_id IS NOT NULL +-- GROUP BY account_id, entry_order_id, symbol HAVING COUNT(*) > 1; + + +-- ========== 2. 删除重复(保留每组 id 最小的那条,删除同组其余行)========== +-- 执行前请确认上面查询结果符合预期;建议先备份: CREATE TABLE trades_backup_YYYYMMDD AS SELECT * FROM trades; + +DELETE t +FROM trades t +INNER JOIN ( + SELECT entry_order_id, symbol, MIN(id) AS keep_id + FROM trades + WHERE entry_order_id IS NOT NULL + GROUP BY entry_order_id, symbol + HAVING COUNT(*) > 1 +) g ON t.entry_order_id = g.entry_order_id AND t.symbol = g.symbol AND t.id <> g.keep_id; + +-- 若有多账号,按 account_id 去重(取消下面注释并注释掉上面的 DELETE): +/* +DELETE t +FROM trades t +INNER JOIN ( + SELECT account_id, entry_order_id, symbol, MIN(id) AS keep_id + FROM trades + WHERE entry_order_id IS NOT NULL + GROUP BY account_id, entry_order_id, symbol + HAVING COUNT(*) > 1 +) g ON t.account_id = g.account_id AND t.entry_order_id = g.entry_order_id AND t.symbol = g.symbol AND t.id <> g.keep_id; +*/ + + +-- ========== 3. 再次检查(应无重复)========== +SELECT entry_order_id, symbol, COUNT(*) AS cnt +FROM trades +WHERE entry_order_id IS NOT NULL +GROUP BY entry_order_id, symbol +HAVING COUNT(*) > 1; +-- 期望结果:空 diff --git a/backend/database/models.py b/backend/database/models.py index d54b050..4a160d1 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -431,6 +431,14 @@ class GlobalStrategyConfig: class Trade: """交易记录模型""" + @staticmethod + def get_by_client_order_id(client_order_id): + """根据自定义订单号(clientOrderId)获取交易记录""" + return db.execute_one( + "SELECT * FROM trades WHERE client_order_id = %s", + (client_order_id,) + ) + @staticmethod def create( symbol, @@ -497,6 +505,40 @@ class Trade: columns = ["symbol", "side", "quantity", "entry_price", "leverage", "entry_reason", "status", "entry_time"] values = [symbol, side, quantity, entry_price, leverage, entry_reason, "open", entry_time] + # 在真正执行 INSERT 之前,利用 entry_order_id / client_order_id 做一次幂等去重 + try: + if entry_order_id is not None and _has_column("entry_order_id"): + try: + existing = Trade.get_by_entry_order_id(entry_order_id) + except Exception: + existing = None + if existing: + try: + logger.debug( + f"Trade.create: entry_order_id={entry_order_id} 已存在 (id={existing.get('id')}, " + f"symbol={existing.get('symbol')}, status={existing.get('status')}),直接复用" + ) + except Exception: + pass + return existing.get("id") + if client_order_id and _has_column("client_order_id"): + try: + existing = Trade.get_by_client_order_id(client_order_id) + except Exception: + existing = None + if existing: + try: + logger.debug( + f"Trade.create: client_order_id={client_order_id!r} 已存在 (id={existing.get('id')}, " + f"symbol={existing.get('symbol')}, status={existing.get('status')}),直接复用" + ) + except Exception: + pass + return existing.get("id") + except Exception: + # 去重失败不影响后续正常插入,由数据库唯一约束兜底 + pass + if _has_column("account_id"): columns.insert(0, "account_id") values.insert(0, int(account_id or DEFAULT_ACCOUNT_ID)) diff --git a/debug_config.py b/debug_config.py deleted file mode 100644 index e343847..0000000 --- a/debug_config.py +++ /dev/null @@ -1,58 +0,0 @@ - -import sys -import os -import asyncio -from pathlib import Path - -# Add project root to path -project_root = Path(os.getcwd()) -sys.path.insert(0, str(project_root)) -sys.path.insert(0, str(project_root / 'backend')) - -# Mock db for simple config read if possible, or use real one -from backend.config_manager import ConfigManager -from backend.database.connection import db - -async def main(): - print("Initializing ConfigManager...") - # Initialize ConfigManager with account_id=1 - config_manager = ConfigManager.for_account(1) - - # Force load from DB - try: - config_manager.reload() - print("Config reloaded from DB.") - except Exception as e: - print(f"Error reloading config: {e}") - - print("\n=== Current TRADING_CONFIG (Merged) ===") - from trading_system import config - - # Manually merge DB config into trading_system.config.TRADING_CONFIG to simulate runtime - for k, v in config_manager._cache.items(): - config.TRADING_CONFIG[k] = v - - tp_pct = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT') - tp1_pct = config.TRADING_CONFIG.get('TAKE_PROFIT_1_PERCENT') - - print(f"TAKE_PROFIT_PERCENT: {tp_pct} (Type: {type(tp_pct)})") - print(f"TAKE_PROFIT_1_PERCENT: {tp1_pct} (Type: {type(tp1_pct)})") - - print("\n=== All Take Profit Related Configs ===") - for k, v in config.TRADING_CONFIG.items(): - if 'TAKE_PROFIT' in k: - print(f"{k}: {v}") - - print("\n=== Risk Related Configs ===") - print(f"FIXED_RISK_PERCENT: {config.TRADING_CONFIG.get('FIXED_RISK_PERCENT')}") - print(f"SIGNAL_STRENGTH_POSITION_MULTIPLIER: {config.TRADING_CONFIG.get('SIGNAL_STRENGTH_POSITION_MULTIPLIER')}") - print(f"USE_FIXED_RISK_SIZING: {config.TRADING_CONFIG.get('USE_FIXED_RISK_SIZING')}") - - # Check if there are any suspicious small values - print("\n=== Suspiciously Small Values (< 0.01) ===") - for k, v in config.TRADING_CONFIG.items(): - if isinstance(v, (int, float)) and 0 < v < 0.01: - print(f"{k}: {v}") - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/debug_sltp.py b/debug_sltp.py deleted file mode 100644 index fce6018..0000000 --- a/debug_sltp.py +++ /dev/null @@ -1,61 +0,0 @@ - -import asyncio -import logging -import sys -from pathlib import Path - -# Setup path -project_root = Path(__file__).parent -sys.path.insert(0, str(project_root)) -sys.path.insert(0, str(project_root / 'trading_system')) -sys.path.insert(0, str(project_root / 'backend')) - -# Mock config -from trading_system import config -config.TRADING_CONFIG = { - 'STOP_LOSS_PERCENT': 0.03, - 'TAKE_PROFIT_PERCENT': 0.05, - 'ATR_STOP_LOSS_MULTIPLIER': 2.5, - 'USE_ATR_STOP_LOSS': True -} - -from trading_system.risk_manager import RiskManager -from trading_system.position_manager import PositionManager - -# Mock classes -class MockClient: - pass - -async def test_risk_manager(): - print("Testing RiskManager...") - rm = RiskManager(None) - - entry_price = 100.0 - side = 'BUY' - quantity = 1.0 - leverage = 10 - stop_loss_pct = 0.03 - - # Case 1: No ATR, No Klines - sl = rm.get_stop_loss_price(entry_price, side, quantity, leverage, stop_loss_pct=stop_loss_pct) - print(f"Case 1 (Basic): SL = {sl}") - - # Case 2: With ATR - atr = 2.0 - sl_atr = rm.get_stop_loss_price(entry_price, side, quantity, leverage, stop_loss_pct=stop_loss_pct, atr=atr) - print(f"Case 2 (ATR={atr}): SL = {sl_atr}") - - # Case 3: SELL side - side = 'SELL' - sl_sell = rm.get_stop_loss_price(entry_price, side, quantity, leverage, stop_loss_pct=stop_loss_pct) - print(f"Case 3 (SELL): SL = {sl_sell}") - - # Case 4: Zero/None input - sl_none = rm.get_stop_loss_price(entry_price, side, quantity, leverage, stop_loss_pct=None) - print(f"Case 4 (None pct): SL = {sl_none}") - - sl_zero = rm.get_stop_loss_price(entry_price, side, quantity, leverage, stop_loss_pct=0) - print(f"Case 4 (Zero pct): SL = {sl_zero}") - -if __name__ == "__main__": - asyncio.run(test_risk_manager()) diff --git a/fix_db_positions.py b/fix_db_positions.py deleted file mode 100644 index 111b262..0000000 --- a/fix_db_positions.py +++ /dev/null @@ -1,131 +0,0 @@ - -import asyncio -import sys -import os -from pathlib import Path - -# Add project root to path -project_root = Path(__file__).parent -sys.path.insert(0, str(project_root)) -sys.path.insert(0, str(project_root / 'backend')) - -from database.connection import db -from database.models import Trade -from trading_system import config -from trading_system.risk_manager import RiskManager - -async def fix_missing_sltp(): - print("Checking for open trades with missing SL/TP across all accounts...") - - all_open_trades = [] - # Iterate through potential account IDs - for account_id in [1, 2, 3, 4]: - print(f"\nChecking Account {account_id}...") - - try: - # Check if Trade.get_all supports account_id argument - trades = Trade.get_all(status='open', account_id=account_id) - except TypeError: - print(f"Warning: Trade.get_all might not support account_id. Checking default.") - trades = Trade.get_all(status='open') - if account_id > 1: break - - if trades: - print(f"Found {len(trades)} open trades for Account {account_id}.") - all_open_trades.extend(trades) - else: - print(f"No open trades found for Account {account_id}.") - - open_trades = all_open_trades - if not open_trades: - print("No open trades found.") - return - - print(f"Found {len(open_trades)} open trades total.") - - # Initialize RiskManager (lightweight) - # We don't need a real client if we just use basic calculation - rm = RiskManager(None) - - updates = 0 - - for trade in open_trades: - t_id = trade['id'] - symbol = trade['symbol'] - entry_price = float(trade['entry_price'] or 0) - quantity = float(trade['quantity'] or 0) - side = trade['side'] - leverage = int(trade['leverage'] or 10) - sl = trade.get('stop_loss_price') - tp = trade.get('take_profit_price') - - if entry_price <= 0: - print(f"Skipping trade {t_id} ({symbol}): Invalid entry price {entry_price}") - continue - - needs_update = False - - # Calculate defaults - # Config defaults: SL 3%, TP 10% (or 30%?) - # Logic from position_manager: - stop_loss_pct = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.03) - if stop_loss_pct > 1: stop_loss_pct /= 100.0 - - take_profit_pct = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.10) - if take_profit_pct > 1: take_profit_pct /= 100.0 - - # Calculate SL - if sl is None or float(sl) <= 0: - print(f"Trade {t_id} ({symbol}) missing SL. Calculating...") - # Use RiskManager logic manually or call it - # We'll use the basic margin-based logic - position_value = entry_price * quantity - margin = position_value / leverage if leverage > 0 else position_value - stop_loss_amount = margin * stop_loss_pct - - if side == 'BUY': - new_sl = entry_price - (stop_loss_amount / quantity) - else: - new_sl = entry_price + (stop_loss_amount / quantity) - - sl = new_sl - needs_update = True - print(f" -> Calculated SL: {sl:.4f} (Margin: {margin:.2f}, SL Amount: {stop_loss_amount:.2f})") - - # Calculate TP - if tp is None or float(tp) <= 0: - print(f"Trade {t_id} ({symbol}) missing TP. Calculating...") - # Use basic logic - # Default TP is usually SL distance * 3 or fixed pct - # Position manager uses config TAKE_PROFIT_PERCENT - - if take_profit_pct is None or take_profit_pct == 0: - take_profit_pct = stop_loss_pct * 3.0 - - tp_price = rm.get_take_profit_price( - entry_price, side, quantity, leverage, - take_profit_pct=take_profit_pct, - atr=None, - stop_distance=abs(entry_price - float(sl)) - ) - tp = tp_price - needs_update = True - print(f" -> Calculated TP: {tp:.4f}") - - if needs_update: - try: - db.execute_update( - "UPDATE trades SET stop_loss_price = %s, take_profit_price = %s WHERE id = %s", - (sl, tp, t_id) - ) - print(f"✓ Updated trade {t_id} ({symbol})") - updates += 1 - except Exception as e: - print(f"❌ Failed to update trade {t_id}: {e}") - else: - print(f"Trade {t_id} ({symbol}) OK.") - - print(f"Fixed {updates} trades.") - -if __name__ == "__main__": - asyncio.run(fix_missing_sltp()) diff --git a/fix_trade_records.py b/fix_trade_records.py deleted file mode 100644 index ef7c8c9..0000000 --- a/fix_trade_records.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python3 -import asyncio -import logging -import os -import sys -from datetime import datetime, timedelta - -# Add project root to path -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'backend')) - -from backend.database.connection import db -from backend.database.models import Trade, Account -from trading_system.binance_client import BinanceClient - -# Setup logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -logger = logging.getLogger("fix_trades") - -async def main(): - # Loop through active accounts - # Based on previous check, active accounts are 2, 3, 4 - active_account_ids = [2, 3, 4] - - # Check columns once - existing_columns = set() - try: - cols = db.execute_query("DESCRIBE trades") - existing_columns = {row['Field'] for row in cols} - except Exception as e: - logger.error(f"Failed to describe trades table: {e}") - return - - if 'realized_pnl' not in existing_columns: - logger.info("Adding 'realized_pnl' column to trades table...") - db.execute_update("ALTER TABLE trades ADD COLUMN realized_pnl DECIMAL(20, 8) NULL COMMENT '已实现盈亏'") - - if 'commission' not in existing_columns: - logger.info("Adding 'commission' column to trades table...") - db.execute_update("ALTER TABLE trades ADD COLUMN commission DECIMAL(20, 8) NULL COMMENT '手续费'") - - total_fixed = 0 - - for account_id in active_account_ids: - logger.info(f"Processing Account ID: {account_id}") - - # Get account credentials - creds = Account.get_credentials(account_id) - if not creds: - logger.error(f"No account credentials found for account {account_id}") - continue - - api_key, api_secret, use_testnet, status = creds - - if not api_key or not api_secret: - logger.warning(f"Skipping account {account_id}: No API key/secret") - continue - - if status != 'active': - logger.warning(f"Skipping account {account_id}: Status is {status}") - continue - - client = BinanceClient(api_key, api_secret, testnet=use_testnet) - try: - # Check for proxy in environment - proxy = os.environ.get('HTTP_PROXY') or os.environ.get('HTTPS_PROXY') - requests_params = {'proxy': proxy} if proxy else None - - await client.connect(requests_params=requests_params) - except Exception as e: - logger.error(f"Failed to connect to Binance for account {account_id}: {e}") - continue - - try: - # Get recent closed trades from DB (last 30 days) for this account - thirty_days_ago = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d %H:%M:%S') - - # Check if entry_time is int (unix timestamp) or string/datetime based on schema check - # Schema says entry_time is 'int unsigned', so it's a timestamp. - thirty_days_ago_ts = int((datetime.now() - timedelta(days=30)).timestamp()) - - query = """ - SELECT * FROM trades - WHERE status = 'closed' - AND account_id = %s - AND entry_time > %s - ORDER BY id DESC - """ - - trades = db.execute_query(query, (account_id, thirty_days_ago_ts)) - logger.info(f"Found {len(trades)} closed trades for account {account_id} from last 30 days.") - - updated_count = 0 - - for trade in trades: - symbol = trade['symbol'] - trade_id = trade['id'] - entry_time = trade['entry_time'] # Should be int - side = trade['side'] - - entry_ts_ms = entry_time * 1000 - - try: - # Get recent trades from Binance - recent_trades = await client.get_recent_trades(symbol, limit=50) - - # Filter trades after entry time - closing_trades = [ - t for t in recent_trades - if t.get('time', 0) > entry_ts_ms and float(t.get('realizedPnl', 0)) != 0 - ] - - if not closing_trades: - continue - - # Calculate actual values - total_pnl = 0.0 - total_comm = 0.0 - total_qty = 0.0 - total_val = 0.0 - - for t in closing_trades: - pnl_val = float(t.get('realizedPnl', 0)) - comm_val = float(t.get('commission', 0)) - qty_val = float(t.get('qty', 0)) - price_val = float(t.get('price', 0)) - - total_pnl += pnl_val - total_comm += comm_val - total_qty += qty_val - total_val += qty_val * price_val - - if total_qty == 0: - continue - - avg_exit_price = total_val / total_qty - - # Check if values differ significantly from DB - db_pnl = float(trade.get('pnl') or 0) - db_exit_price = float(trade.get('exit_price') or 0) - - needs_update = False - if abs(db_pnl - total_pnl) > 0.01: - needs_update = True - - if 'realized_pnl' not in trade or trade.get('realized_pnl') is None: - needs_update = True - - if needs_update: - logger.info(f"Fixing trade {trade_id} ({symbol}): PnL {db_pnl:.4f} -> {total_pnl:.4f}, ExitPrice {db_exit_price:.4f} -> {avg_exit_price:.4f}") - - # Recalculate pnl_percent based on entry price - entry_price = float(trade.get('entry_price', 1)) - if entry_price == 0: - entry_price = 1 - - if side == 'BUY': - pnl_percent = ((avg_exit_price - entry_price) / entry_price) * 100 - else: - pnl_percent = ((entry_price - avg_exit_price) / entry_price) * 100 - - # Update DB - update_sql = """ - UPDATE trades - SET pnl = %s, - pnl_percent = %s, - exit_price = %s, - realized_pnl = %s, - commission = %s - WHERE id = %s - """ - db.execute_update(update_sql, (total_pnl, pnl_percent, avg_exit_price, total_pnl, total_comm, trade_id)) - updated_count += 1 - - except Exception as e: - logger.error(f"Error processing trade {trade_id} ({symbol}): {e}") - - logger.info(f"Account {account_id}: Fixed {updated_count} trades.") - total_fixed += updated_count - - if client.client: - await client.client.close_connection() - - except Exception as e: - logger.error(f"Error processing account {account_id}: {e}") - - logger.info(f"Total fixed trades: {total_fixed}") - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/inspect_db.py b/inspect_db.py deleted file mode 100644 index 97d28b9..0000000 --- a/inspect_db.py +++ /dev/null @@ -1,34 +0,0 @@ - -import os -import sys -from sqlalchemy import create_engine, text, inspect - -# Database connection -DB_USER = os.getenv('DB_USER', 'root') -DB_PASSWORD = os.getenv('DB_PASSWORD', '12345678') -DB_HOST = os.getenv('DB_HOST', 'localhost') -DB_PORT = os.getenv('DB_PORT', '3306') -DB_NAME = 'auto_trade_sys_new' - -DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" - -def inspect_db(): - try: - engine = create_engine(DATABASE_URL) - inspector = inspect(engine) - table_names = inspector.get_table_names() - print(f"Tables: {table_names}") - - if 'trading_config' in table_names: - columns = inspector.get_columns('trading_config') - print("\nColumns in trading_config:") - for col in columns: - print(f" - {col['name']} ({col['type']})") - else: - print("\ntrading_config table NOT found!") - - except Exception as e: - print(f"Error: {e}") - -if __name__ == "__main__": - inspect_db() diff --git a/test_db_pool.py b/test_db_pool.py deleted file mode 100644 index aa1a68a..0000000 --- a/test_db_pool.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -import sys -import logging -import traceback -import pymysql -from backend.database.connection import Database - -# Configure logging -logging.basicConfig(level=logging.DEBUG) -logger = logging.getLogger(__name__) - -def test_connection(): - print("Initializing Database...") - db = Database() - - print("Testing get_connection...") - try: - with db.get_connection() as conn: - print(f"Connection type: {type(conn)}") - # Try to force DictCursor here if the fix in connection.py isn't enough, - # but we want to test if connection.py does it automatically. - - with conn.cursor() as cursor: - cursor.execute("SELECT 1 as val") - result = cursor.fetchone() - print(f"Query result: {result}") - print(f"Result type: {type(result)}") - - if isinstance(result, dict): - print("SUCCESS: Result is a dictionary") - else: - print("FAILURE: Result is NOT a dictionary") - # Debug: try to find how to set it - if hasattr(conn, 'connection'): - print(f"Inner connection type: {type(conn.connection)}") - - except Exception as e: - print("Caught exception:") - traceback.print_exc() - -if __name__ == "__main__": - test_connection() diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 0cd7235..c3aa960 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -766,9 +766,10 @@ class BinanceClient: Returns: 持仓列表 """ + # 保持 7 次重试,不增加对币安的总请求次数;仅拉长重试间隔(退避)以给网络恢复时间 retries = 7 last_error = None - + read_timeout = getattr(config, 'READ_ONLY_REQUEST_TIMEOUT', 60) for attempt in range(retries): try: @@ -819,7 +820,8 @@ class BinanceClient: if is_network_error: if attempt < retries - 1: - wait = 2 if attempt >= 2 else 1 + # 退避拉长间隔,不增加重试次数:避免对币安请求频率上升,同时给网络恢复时间 + wait = 3 if attempt >= 4 else (2 if attempt >= 2 else 1) logger.warning(f"获取持仓信息失败 (第 {attempt + 1}/{retries} 次): {_format_exception(e)},{wait}秒后重试...") await asyncio.sleep(wait) continue @@ -848,6 +850,7 @@ class BinanceClient: Returns: 成交记录列表 """ + # 保持 5 次重试,不增加对币安的总请求次数;仅拉长重试间隔(退避) retries = 5 last_error = None attempts_made = 0 @@ -865,7 +868,7 @@ class BinanceClient: isinstance(e, BinanceAPIException) and (e.code == -1021 or str(e.code).startswith('5')) ) if is_retryable and attempt < retries - 1: - wait = 2 if attempt >= 2 else 1 # 后几次多等 1 秒,给网络恢复时间 + wait = 3 if attempt >= 3 else (2 if attempt >= 1 else 1) logger.warning(f"获取成交记录失败 {symbol} (第 {attempt + 1}/{retries} 次): {_format_exception(e)},{wait}秒后重试...") await asyncio.sleep(wait) continue diff --git a/trading_system/config.py b/trading_system/config.py index 0717817..64f3ee9 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -400,6 +400,7 @@ def reload_config(): CONNECTION_TIMEOUT = int(os.getenv('CONNECTION_TIMEOUT', '30')) # 连接超时时间(秒) CONNECTION_RETRIES = int(os.getenv('CONNECTION_RETRIES', '3')) # 连接重试次数 # 仅用于 get_open_positions / get_recent_trades 等只读接口的单次等待时间,不影响下单/止损止盈的快速失败 +# 调大此值会延长单次请求最大等待时间,在同步/查询持仓时可能阻塞事件循环,影响实时性;保持 60 秒,通过增加重试+退避应对偶发超时 READ_ONLY_REQUEST_TIMEOUT = int(os.getenv('READ_ONLY_REQUEST_TIMEOUT', '60')) # 获取持仓时过滤掉名义价值低于此值的仓位(USDT),与币安仪表板不一致时可调低或设为 0 POSITION_MIN_NOTIONAL_USDT = float(os.getenv('POSITION_MIN_NOTIONAL_USDT', '1.0')) diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 79a0f18..a882895 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -888,21 +888,34 @@ class PositionManager: strategy_type = position_info.get('strategyType', 'trend_following') - Trade.update_exit( - trade_id=trade_id, - exit_price=exit_price, - exit_reason=reason, - pnl=pnl, - pnl_percent=pnl_percent, - exit_order_id=None, # 同步平仓时没有订单号 - strategy_type=strategy_type, - duration_minutes=duration_minutes - ) - logger.info(f"{symbol} [平仓] ✓ 数据库状态已更新") - updated = True - except Exception as e: - err_msg = str(e).strip() or f"{type(e).__name__}" - logger.error(f"{symbol} [平仓] ❌ 更新数据库状态失败: {err_msg}") + # 网络/DB 超时时可重试,避免 TimeoutError 导致状态未更新 + db_update_retries = 3 + for db_attempt in range(db_update_retries): + try: + Trade.update_exit( + trade_id=trade_id, + exit_price=exit_price, + exit_reason=reason, + pnl=pnl, + pnl_percent=pnl_percent, + exit_order_id=None, # 同步平仓时没有订单号 + strategy_type=strategy_type, + duration_minutes=duration_minutes + ) + logger.info(f"{symbol} [平仓] ✓ 数据库状态已更新") + updated = True + break + except Exception as e: + err_msg = str(e).strip() or f"{type(e).__name__}" + if db_attempt < db_update_retries - 1: + wait_sec = 2 + logger.warning( + f"{symbol} [平仓] 更新数据库失败 (第 {db_attempt + 1}/{db_update_retries} 次): {err_msg}," + f"{wait_sec}秒后重试" + ) + await asyncio.sleep(wait_sec) + else: + logger.error(f"{symbol} [平仓] ❌ 更新数据库状态失败: {err_msg}") # 清理本地记录 await self._stop_position_monitoring(symbol) @@ -1109,28 +1122,41 @@ class PositionManager: # 获取策略类型(从开仓原因或持仓信息中获取) strategy_type = position_info.get('strategyType', 'trend_following') # 默认趋势跟踪 - Trade.update_exit( - trade_id=trade_id, - exit_price=exit_price_float, - exit_reason=reason, - pnl=pnl, - pnl_percent=pnl_percent, - exit_order_id=exit_order_id, # 保存币安平仓订单号 - strategy_type=strategy_type, - duration_minutes=duration_minutes, - realized_pnl=realized_pnl, - commission=commission, - commission_asset=commission_asset - ) - logger.info( - f"{symbol} [平仓] ✓ 数据库记录已更新 " - f"(盈亏: {pnl:.2f} USDT, {pnl_percent:.2f}%, 原因: {reason})" - ) - except Exception as e: - logger.error(f"❌ 更新平仓记录到数据库失败: {e}") - logger.error(f" 错误类型: {type(e).__name__}") - import traceback - logger.error(f" 错误详情:\n{traceback.format_exc()}") + # 网络/DB 超时时重试,避免 TimeoutError 导致平仓记录未更新 + db_update_retries = 3 + for db_attempt in range(db_update_retries): + try: + Trade.update_exit( + trade_id=trade_id, + exit_price=exit_price_float, + exit_reason=reason, + pnl=pnl, + pnl_percent=pnl_percent, + exit_order_id=exit_order_id, # 保存币安平仓订单号 + strategy_type=strategy_type, + duration_minutes=duration_minutes, + realized_pnl=realized_pnl, + commission=commission, + commission_asset=commission_asset + ) + logger.info( + f"{symbol} [平仓] ✓ 数据库记录已更新 " + f"(盈亏: {pnl:.2f} USDT, {pnl_percent:.2f}%, 原因: {reason})" + ) + break + except Exception as e: + err_msg = str(e).strip() or f"{type(e).__name__}" + if db_attempt < db_update_retries - 1: + wait_sec = 2 + logger.warning( + f"{symbol} [平仓] 更新数据库失败 (第 {db_attempt + 1}/{db_update_retries} 次): {err_msg},{wait_sec}秒后重试" + ) + await asyncio.sleep(wait_sec) + else: + logger.error(f"❌ 更新平仓记录到数据库失败: {e}") + logger.error(f" 错误类型: {type(e).__name__}") + import traceback + logger.error(f" 错误详情:\n{traceback.format_exc()}") else: logger.warning(f"{symbol} 没有关联的数据库交易ID,无法更新平仓记录") elif not DB_AVAILABLE: @@ -1344,19 +1370,30 @@ class PositionManager: logger.warning(f"{symbol} 止损或止盈价格为空,跳过挂保护单: stop_loss={stop_loss}, take_profit={take_profit}") return - # 验证止损价格是否合理 + # 验证止损价格是否合理;若无效则自动修正为最小安全距离,避免跳过挂单导致无保护 entry_price = position_info.get("entryPrice") if entry_price: try: entry_price_val = float(entry_price) stop_loss_val = float(stop_loss) # 验证止损价格方向:BUY时止损价应低于入场价,SELL时止损价应高于入场价 + min_gap_pct = 0.005 # 最小 0.5% 距离,与 risk_manager 一致 if side == "BUY" and stop_loss_val >= entry_price_val: - logger.error(f"{symbol} ❌ 止损价格错误: BUY时止损价({stop_loss_val:.8f})应低于入场价({entry_price_val:.8f})") - return - if side == "SELL" and stop_loss_val <= entry_price_val: - logger.error(f"{symbol} ❌ 止损价格错误: SELL时止损价({stop_loss_val:.8f})应高于入场价({entry_price_val:.8f})") - return + safe_sl = entry_price_val * (1 - min_gap_pct) + logger.warning( + f"{symbol} 止损价({stop_loss_val:.8f})>=入场价({entry_price_val:.8f}) 无效(BUY)," + f"已修正为 {safe_sl:.8f} 并继续挂单" + ) + stop_loss = safe_sl + position_info["stopLoss"] = stop_loss + elif side == "SELL" and stop_loss_val <= entry_price_val: + safe_sl = entry_price_val * (1 + min_gap_pct) + logger.warning( + f"{symbol} 止损价({stop_loss_val:.8f})<=入场价({entry_price_val:.8f}) 无效(SELL)," + f"已修正为 {safe_sl:.8f} 并继续挂单" + ) + stop_loss = safe_sl + position_info["stopLoss"] = stop_loss except Exception as e: logger.warning(f"{symbol} 验证止损价格时出错: {e}") @@ -2946,11 +2983,12 @@ class PositionManager: logger.info(f" → {symbol} 无止损/止盈单,将补建记录、自动挂止损止盈并纳入监控") # 不再因 is_clearly_manual 或 无 SL/TP 跳过,一律补建 + 挂 SL/TP + 监控 entry_reason_sync = "sync_recovered_unknown_origin" if sync_unknown_origin else "sync_recovered" - # 仅当 DB 中已有「同 symbol、同 entry_order_id、且 status=open」的记录时才跳过,避免已平仓旧单占用 order_id 导致误跳过补建 + # 同一开仓订单在 DB 中已有记录(无论 open 或 closed)则跳过,避免重复建单(如 PYTHUSDT 同单多笔) if entry_order_id and hasattr(Trade, "get_by_entry_order_id"): try: existing = Trade.get_by_entry_order_id(entry_order_id) - if existing and existing.get("status") == "open" and existing.get("symbol") == symbol: + if existing and existing.get("symbol") == symbol: + logger.debug(f" {symbol} 开仓订单 {entry_order_id} 已有记录 (id={existing.get('id')}, status={existing.get('status')}),跳过补建") continue except Exception: pass diff --git a/update_atr_config.py b/update_atr_config.py deleted file mode 100644 index 05bfc1d..0000000 --- a/update_atr_config.py +++ /dev/null @@ -1,52 +0,0 @@ -import sys -import os - -# Add project root to path -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from backend.database.connection import db - -def update_atr_config(): - print("Updating ATR configuration...") - - # 1. Update ATR_STOP_LOSS_MULTIPLIER - sql_check = "SELECT config_value FROM global_strategy_config WHERE config_key = 'ATR_STOP_LOSS_MULTIPLIER'" - try: - rows = db.execute_query(sql_check) - if rows: - print(f"Current ATR_STOP_LOSS_MULTIPLIER: {rows[0]['config_value']}") - sql_update = "UPDATE global_strategy_config SET config_value = '0.5' WHERE config_key = 'ATR_STOP_LOSS_MULTIPLIER'" - db.execute_update(sql_update) - print("Updated ATR_STOP_LOSS_MULTIPLIER to 0.5") - else: - sql_insert = "INSERT INTO global_strategy_config (config_key, config_value, config_type, category) VALUES ('ATR_STOP_LOSS_MULTIPLIER', '0.5', 'float', 'risk')" - db.execute_update(sql_insert) - print("Created ATR_STOP_LOSS_MULTIPLIER: 0.5") - - except Exception as e: - print(f"Error updating ATR_STOP_LOSS_MULTIPLIER: {e}") - import traceback - traceback.print_exc() - - # 2. Ensure USE_ATR_STOP_LOSS is True - try: - sql_check_use = "SELECT config_value FROM global_strategy_config WHERE config_key = 'USE_ATR_STOP_LOSS'" - rows_use = db.execute_query(sql_check_use) - - if rows_use: - if rows_use[0]['config_value'] != 'true': - sql_update_use = "UPDATE global_strategy_config SET config_value = 'true' WHERE config_key = 'USE_ATR_STOP_LOSS'" - db.execute_update(sql_update_use) - print("Enabled USE_ATR_STOP_LOSS") - else: - sql_insert_use = "INSERT INTO global_strategy_config (config_key, config_value, config_type, category) VALUES ('USE_ATR_STOP_LOSS', 'true', 'boolean', 'risk')" - db.execute_update(sql_insert_use) - print("Created USE_ATR_STOP_LOSS: true") - - except Exception as e: - print(f"Error updating USE_ATR_STOP_LOSS: {e}") - - print("Database update complete.") - -if __name__ == "__main__": - update_atr_config() diff --git a/update_db_schema.py b/update_db_schema.py deleted file mode 100644 index 2340869..0000000 --- a/update_db_schema.py +++ /dev/null @@ -1,32 +0,0 @@ - -import os -import sys - -# Add backend directory to path -sys.path.append(os.path.join(os.path.dirname(__file__), 'backend')) - -from database.connection import db - -def run_migration(): - print("Running migration to add realized_pnl and commission columns...") - - sqls = [ - "ALTER TABLE trades ADD COLUMN realized_pnl DECIMAL(20, 8) DEFAULT NULL COMMENT '币安实际结算盈亏(包含资金费率等)'", - "ALTER TABLE trades ADD COLUMN commission DECIMAL(20, 8) DEFAULT NULL COMMENT '交易手续费(USDT计价)'", - "ALTER TABLE trades ADD COLUMN commission_asset VARCHAR(10) DEFAULT NULL COMMENT '手续费币种(BNB/USDT)'" - ] - - for sql in sqls: - try: - print(f"Executing: {sql}") - db.execute_update(sql) - print("Success.") - except Exception as e: - print(f"Error executing SQL: {e}") - # Ignore duplicate column errors if IF NOT EXISTS fails for some reason - pass - - print("Migration completed.") - -if __name__ == "__main__": - run_migration() diff --git a/update_global_config.py b/update_global_config.py deleted file mode 100644 index 24a6f4c..0000000 --- a/update_global_config.py +++ /dev/null @@ -1,70 +0,0 @@ - -import sys -import os -from pathlib import Path - -# Add backend to sys.path -backend_dir = Path(__file__).parent / 'backend' -sys.path.insert(0, str(backend_dir)) - -try: - from database.models import GlobalStrategyConfig - from database.connection import db - - # Check DB connection - print("Checking DB connection...") - db.execute_one("SELECT 1") - print("DB connection successful.") - - configs_to_update = [ - # Key, Value, Type, Category, Description - ('TAKE_PROFIT_PERCENT', 0.80, 'number', 'risk', '第二目标/单目标止盈80%(保证金百分比)'), - ('TAKE_PROFIT_1_PERCENT', 0.30, 'number', 'risk', '第一目标止盈30%(保证金百分比)'), - ('STOP_LOSS_PERCENT', 0.10, 'number', 'risk', '止损10%(保证金百分比)'), - ('TRAILING_STOP_ACTIVATION', 0.30, 'number', 'risk', '移动止损激活阈值(保证金百分比)'), - ('TRAILING_STOP_PROTECT', 0.10, 'number', 'risk', '移动止损保护阈值(保证金百分比)'), - ('LEVERAGE', 8, 'number', 'risk', '基础杠杆倍数'), - ('RISK_REWARD_RATIO', 3.0, 'number', 'risk', '盈亏比目标'), - ('ATR_TAKE_PROFIT_MULTIPLIER', 6.0, 'number', 'risk', 'ATR止盈倍数'), - ('ATR_STOP_LOSS_MULTIPLIER', 3.0, 'number', 'risk', 'ATR止损倍数'), - ('USE_FIXED_RISK_SIZING', True, 'bool', 'risk', '是否使用固定风险仓位计算'), - ('FIXED_RISK_PERCENT', 0.03, 'number', 'risk', '每笔交易固定风险百分比'), - ('MAX_LEVERAGE_SMALL_CAP', 8, 'number', 'risk', '小众币最大杠杆'), - ('MIN_MARGIN_USDT', 2.0, 'number', 'risk', '最小保证金(USDT)') - ] - - print("Updating Global Strategy Config...") - for key, value, type_, category, desc in configs_to_update: - print(f"Updating {key} -> {value}") - GlobalStrategyConfig.set( - key=key, - value=value, - config_type=type_, - category=category, - description=desc, - updated_by='admin_script' - ) - - # Force Redis Cache Clear - try: - import redis - print("Clearing Redis Cache...") - r = redis.from_url('rediss://127.0.0.1:6379', decode_responses=True, ssl_cert_reqs='none') - r.delete("global_strategy_config") - print("Redis Cache Cleared.") - except Exception as e: - print(f"Redis clear failed: {e}") - - - print("Update complete.") - - # Verify updates - print("\nVerifying updates:") - for key, expected_value, _, _, _ in configs_to_update: - actual_value = GlobalStrategyConfig.get_value(key) - print(f"{key}: {actual_value} (Expected: {expected_value})") - -except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() diff --git a/update_risk_config_db.py b/update_risk_config_db.py deleted file mode 100644 index 587ed3d..0000000 --- a/update_risk_config_db.py +++ /dev/null @@ -1,77 +0,0 @@ - -import os -import sys -import logging -from sqlalchemy import create_engine, text - -# Set up logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# Database connection -DB_USER = os.getenv('DB_USER', 'root') -DB_PASSWORD = os.getenv('DB_PASSWORD', '12345678') -DB_HOST = os.getenv('DB_HOST', 'localhost') -DB_PORT = os.getenv('DB_PORT', '3306') -DB_NAME = 'auto_trade_sys_new' - -DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" - -def update_config(): - try: - engine = create_engine(DATABASE_URL) - with engine.connect() as conn: - # 1. Update MIN_STOP_LOSS_PRICE_PCT (Crucial Fix) - # Default was 0.025 (2.5%), which forces 25% margin loss @ 10x. - # Changing to 0.005 (0.5%), which allows 5% margin loss @ 10x. - conn.execute(text(""" - INSERT INTO trading_config (account_id, config_key, config_value, config_type, category, description, updated_at) - VALUES (1, 'MIN_STOP_LOSS_PRICE_PCT', '0.005', 'number', 'risk', '最小止损价格变动:0.5%(允许更紧的止损)', NOW()) - ON DUPLICATE KEY UPDATE config_value='0.005', updated_at=NOW(); - """)) - logger.info("Updated MIN_STOP_LOSS_PRICE_PCT to 0.005") - - # 2. Update MIN_TAKE_PROFIT_PRICE_PCT - # Default was 0.02 (2%), forcing 20% margin gain @ 10x. - # Changing to 0.006 (0.6%), allowing smaller TPs if needed. - conn.execute(text(""" - INSERT INTO trading_config (account_id, config_key, config_value, config_type, category, description, updated_at) - VALUES (1, 'MIN_TAKE_PROFIT_PRICE_PCT', '0.006', 'number', 'risk', '最小止盈价格变动:0.6%(允许更紧的止盈)', NOW()) - ON DUPLICATE KEY UPDATE config_value='0.006', updated_at=NOW(); - """)) - logger.info("Updated MIN_TAKE_PROFIT_PRICE_PCT to 0.006") - - # 3. Ensure STOP_LOSS_PERCENT is 0.1 (10% Margin) - conn.execute(text(""" - INSERT INTO trading_config (account_id, config_key, config_value, config_type, category, description, updated_at) - VALUES (1, 'STOP_LOSS_PERCENT', '0.1', 'number', 'risk', '止损:10%(相对于保证金)', NOW()) - ON DUPLICATE KEY UPDATE config_value='0.1', updated_at=NOW(); - """)) - logger.info("Ensured STOP_LOSS_PERCENT is 0.1") - - # 4. Ensure TAKE_PROFIT_PERCENT is 0.2 (20% Margin) - conn.execute(text(""" - INSERT INTO trading_config (account_id, config_key, config_value, config_type, category, description, updated_at) - VALUES (1, 'TAKE_PROFIT_PERCENT', '0.2', 'number', 'risk', '止盈:20%(相对于保证金)', NOW()) - ON DUPLICATE KEY UPDATE config_value='0.2', updated_at=NOW(); - """)) - logger.info("Ensured TAKE_PROFIT_PERCENT is 0.2") - - # 5. Ensure ATR_STOP_LOSS_MULTIPLIER is reasonable (2.0) - conn.execute(text(""" - INSERT INTO trading_config (account_id, config_key, config_value, config_type, category, description, updated_at) - VALUES (1, 'ATR_STOP_LOSS_MULTIPLIER', '2.0', 'number', 'risk', 'ATR止损倍数2.0', NOW()) - ON DUPLICATE KEY UPDATE config_value='2.0', updated_at=NOW(); - """)) - logger.info("Ensured ATR_STOP_LOSS_MULTIPLIER is 2.0") - - conn.commit() - logger.info("Configuration update complete.") - - except Exception as e: - logger.error(f"Error updating config: {e}") - import traceback - traceback.print_exc() - -if __name__ == "__main__": - update_config() diff --git a/update_stop_loss_orders.py b/update_stop_loss_orders.py deleted file mode 100644 index 761a53d..0000000 --- a/update_stop_loss_orders.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env python3 - -import asyncio -import logging -import sys -import os -from pathlib import Path -from typing import List, Dict, Optional - -# Add project root to sys.path -project_root = Path(__file__).parent -sys.path.insert(0, str(project_root)) -sys.path.insert(0, str(project_root / 'trading_system')) -sys.path.insert(0, str(project_root / 'backend')) - -# Configure logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger("UpdateSL") - -# Import system modules -try: - from trading_system.binance_client import BinanceClient - from trading_system.risk_manager import RiskManager - from trading_system import config - from backend.database.connection import Database - from backend.security.crypto import decrypt_str - - # Ensure config is loaded (defaults updated to 0.5) - from trading_system.config import _init_config_manager - _init_config_manager() -except ImportError as e: - logger.error(f"Import failed: {e}") - sys.exit(1) - -async def process_account(account_id, name, api_key, api_secret): - logger.info(f"=== Processing Account {account_id}: {name} ===") - - if not api_key or not api_secret: - logger.warning(f"Account {name} missing keys. Skipping.") - return - - # Initialize Client with explicit keys - client = BinanceClient(api_key=api_key, api_secret=api_secret) - - # Connect to Binance - try: - # Debug proxy settings - logger.info(f"Proxy Settings - HTTP: {os.environ.get('HTTP_PROXY')}, HTTPS: {os.environ.get('HTTPS_PROXY')}") - - # Increase timeout to 60s and retries - await client.connect(timeout=60, retries=5) - except Exception as e: - logger.error(f"Failed to connect to Binance for account {name}: {e}") - return - - # Initialize RiskManager - # Note: RiskManager uses global config for ATR settings. - # Since we updated config.py default ATR_STOP_LOSS_MULTIPLIER to 0.5, - # and account 1 DB config is also 0.5, this is safe. - risk_manager = RiskManager(client) - - try: - atr_multiplier = float(config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 0.5)) - logger.info(f"Target ATR Multiplier: {atr_multiplier}") - - # Get Open Positions - try: - positions = await client.get_open_positions() - logger.info(f"Found {len(positions)} open positions.") - except Exception as e: - logger.error(f"Failed to get positions: {e}") - return - - for position in positions: - symbol = position['symbol'] - amt = float(position['positionAmt']) - - if amt == 0: - continue - - # Determine side from amount - if amt > 0: - real_side = 'BUY' - else: - real_side = 'SELL' - - entry_price = float(position['entryPrice']) - mark_price = float(position['markPrice']) - leverage = int(position['leverage']) - quantity = abs(amt) - - logger.info(f"Processing {symbol} ({real_side}): Entry={entry_price}, Mark={mark_price}, Amt={amt}") - - # Fetch Klines for ATR - try: - # Get 1h klines for ATR calculation - klines = await client.get_klines(symbol, '1h', limit=100) - if not klines: - logger.warning(f"{symbol} - Failed to get klines, skipping.") - continue - - new_stop_loss = risk_manager.get_stop_loss_price( - entry_price=entry_price, - side=real_side, - quantity=quantity, - leverage=leverage, - stop_loss_pct=None, # Force ATR usage - klines=klines - ) - - if not new_stop_loss: - logger.warning(f"{symbol} - Could not calculate new stop loss.") - continue - - logger.info(f"{symbol} - New ATR Stop Loss Price: {new_stop_loss}") - - # Check if triggered - triggered = False - if real_side == 'BUY': - if mark_price <= new_stop_loss: - triggered = True - else: # SELL - if mark_price >= new_stop_loss: - triggered = True - - if triggered: - logger.warning(f"{symbol} - Price already beyond new stop loss! Executing MARKET CLOSE.") - try: - await client.place_order( - symbol=symbol, - side='SELL' if real_side == 'BUY' else 'BUY', - type='MARKET', - quantity=quantity, - reduceOnly=True - ) - logger.info(f"{symbol} - Market Close Order Placed.") - await client.cancel_all_orders(symbol) - logger.info(f"{symbol} - Cancelled all open orders.") - except Exception as e: - logger.error(f"{symbol} - Failed to close position: {e}") - else: - # Update Stop Loss Order - logger.info(f"{symbol} - Updating Stop Loss Order to {new_stop_loss}") - - # Cancel existing STOP_MARKET orders - try: - open_orders = await client.get_open_orders(symbol) - for order in open_orders: - if order['type'] == 'STOP_MARKET': - await client.cancel_order(symbol, order['orderId']) - logger.info(f"{symbol} - Cancelled old STOP_MARKET order {order['orderId']}") - except Exception as e: - logger.error(f"{symbol} - Failed to cancel orders: {e}") - - # Place new STOP_MARKET order - try: - stop_side = 'SELL' if real_side == 'BUY' else 'BUY' - await client.place_order( - symbol=symbol, - side=stop_side, - type='STOP_MARKET', - stopPrice=new_stop_loss, - quantity=quantity, - reduceOnly=True, - workingType='MARK_PRICE' - ) - logger.info(f"{symbol} - Placed new STOP_MARKET order at {new_stop_loss}") - except Exception as e: - logger.error(f"{symbol} - Failed to place new stop loss: {e}") - - except Exception as e: - logger.error(f"Error processing {symbol}: {e}") - import traceback - traceback.print_exc() - - finally: - await client.close() - -async def main(): - logger.info("Starting Multi-Account Stop Loss Update Script...") - - db = Database() - with db.get_connection() as conn: - with conn.cursor() as cursor: - # Get active accounts - cursor.execute("SELECT id, name, api_key_enc, api_secret_enc FROM accounts WHERE status='active'") - accounts = cursor.fetchall() - - logger.info(f"Found {len(accounts)} active accounts.") - - for acc in accounts: - # Assuming fetchall returns dicts because Database uses DictCursor usually, - # or we check type. backend/database/connection.py ensures DictCursor if possible? - # Let's handle both dict and tuple/list just in case, but based on previous tools, it returns dicts. - # But wait, previous `check_atr_db` output showed dicts. - - acc_id = acc['id'] - name = acc['name'] - api_key_enc = acc['api_key_enc'] - api_secret_enc = acc['api_secret_enc'] - - api_key = decrypt_str(api_key_enc) - api_secret = decrypt_str(api_secret_enc) - - await process_account(acc_id, name, api_key, api_secret) - - logger.info("All accounts processed.") - -if __name__ == "__main__": - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete(main()) diff --git a/update_strategy_config.py b/update_strategy_config.py deleted file mode 100644 index e0eeba5..0000000 --- a/update_strategy_config.py +++ /dev/null @@ -1,34 +0,0 @@ -import sys -import os -from pathlib import Path - -# Add project root to sys.path -project_root = Path(__file__).resolve().parent -sys.path.insert(0, str(project_root)) -sys.path.insert(0, str(project_root / 'backend')) - -try: - from backend.database.models import GlobalStrategyConfig - - updates = [ - ('ATR_STOP_LOSS_MULTIPLIER', 1.5, 'float', 'risk', 'ATR止损倍数(优化后1.5)'), - ('TAKE_PROFIT_1_PERCENT', 0.25, 'float', 'strategy', '第一目标止盈25%(优化后)'), - ('ATR_TAKE_PROFIT_MULTIPLIER', 2.0, 'float', 'risk', 'ATR止盈倍数'), - # Also ensure USE_ATR_STOP_LOSS is True - ('USE_ATR_STOP_LOSS', True, 'bool', 'risk', '开启ATR止损'), - # 2026-02-06 Optimization: Increase candidate pool - ('TOP_N_SYMBOLS', 20, 'int', 'scanner', '候选池大小(优化后20)'), - ('SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT', 15, 'int', 'scanner', '补单候选池(优化后15)'), - ('MAX_SCAN_SYMBOLS', 500, 'int', 'scanner', '最大扫描数量(优化后500)'), - ] - - print("Updating Global Strategy Config...") - for key, value, type_, category, desc in updates: - print(f"Setting {key} = {value}") - GlobalStrategyConfig.set(key, value, type_, category, description=desc, updated_by='system_optimizer') - - print("Done.") -except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() diff --git a/山寨币策略配置评估与优化建议.md b/山寨币策略配置评估与优化建议.md deleted file mode 100644 index 12af739..0000000 --- a/山寨币策略配置评估与优化建议.md +++ /dev/null @@ -1,157 +0,0 @@ -# 山寨币策略配置评估与优化建议 - -> **已落实**:以下高优先级与中优先级建议已在「山寨币策略」预设与 ConfigGuide 中全量修改(总仓 65%、大盘 -1%、第二止盈 55%、显式补项、文档对齐)。 -> **2026-02 补充**:MIN_STOP_LOSS_PRICE_PCT / MIN_TAKE_PROFIT_PRICE_PCT 已从 0.5%/0.6% 调整为 2.5%/2%,与代码默认及《盈利提升方案》一致,避免过紧止损/止盈导致扫损或过早止盈。 - -## 一、当前山寨币策略(altcoin)全部配置项 - -### 1. 风险与止盈止损 -| 配置项 | 当前值 | 说明 | -|--------|--------|------| -| ATR_STOP_LOSS_MULTIPLIER | 3.0 | ATR 止损倍数,3 倍给波动空间,减少噪音止损 | -| STOP_LOSS_PERCENT | 0.10 (10%) | 强平保护线(保证金比例) | -| MIN_STOP_LOSS_PRICE_PCT | **0.025 (2.5%)** | 最小价格止损距离(过小易被波动扫损,已从 0.5% 修正) | -| MIN_TAKE_PROFIT_PRICE_PCT | **0.02 (2%)** | 最小价格止盈距离(过小易过早止盈,已从 0.6% 修正) | -| RISK_REWARD_RATIO | 3.0 | 盈亏比目标 3:1 | -| TAKE_PROFIT_1_PERCENT | 0.30 (30%) | 第一目标止盈(保证金 30%) | -| TAKE_PROFIT_PERCENT | 0.80 (80%) | 第二目标止盈(保证金 80%) | -| MIN_RR_FOR_TP1 | 1.5 | 第一目标至少为止损距离的 1.5 倍 | -| MIN_HOLD_TIME_SEC | 0 | 无持仓时间锁 | -| USE_FIXED_RISK_SIZING | true | 固定风险仓位 | -| FIXED_RISK_PERCENT | 0.03 (3%) | 每笔风险 3% | -| USE_DYNAMIC_ATR_MULTIPLIER | false | 不按波动率动态调 ATR | - -### 2. 移动止损 -| 配置项 | 当前值 | 说明 | -|--------|--------|------| -| USE_TRAILING_STOP | true | 开启移动止损 | -| TRAILING_STOP_ACTIVATION | 0.30 (30%) | 盈利达保证金 30% 激活 | -| TRAILING_STOP_PROTECT | 0.10 (10%) | 回撤 10% 触发平仓 | - -### 3. 仓位与杠杆 -| 配置项 | 当前值 | 说明 | -|--------|--------|------| -| MAX_POSITION_PERCENT | 0.20 (20%) | 单笔最大保证金 20% | -| MAX_TOTAL_POSITION_PERCENT | 0.80 (80%) | 总保证金上限 80% | -| MAX_DAILY_ENTRIES | 15 | 每日最多 15 笔开仓 | -| MAX_OPEN_POSITIONS | 4 | 最多 4 个同时持仓 | -| LEVERAGE | 8 | 基础杠杆 8x | -| MAX_LEVERAGE | 20 | 动态杠杆上限 20x | -| MIN_LEVERAGE | 8 | 动态杠杆下限 8x | -| MAX_LEVERAGE_SMALL_CAP | 8 | 高波动/小众币杠杆上限 8x | -| USE_DYNAMIC_LEVERAGE | true | 开启动态杠杆 | - -**预设中未写、由后端默认兜底的:** -- MIN_POSITION_PERCENT:后端默认 0.02 (2%),合理 -- MIN_CHANGE_PERCENT:后端默认 2.0 (2%),扫描至少 2% 涨跌幅 -- SMART_ENTRY_ENABLED:后端默认 True,智能入场(限价+追价/市价兜底) - -### 4. 扫描与筛选 -| 配置项 | 当前值 | 说明 | -|--------|--------|------| -| MIN_VOLUME_24H | 30000000 | 24h 成交额 ≥ 3000 万美元 | -| MIN_VOLATILITY | 0.03 (3%) | 最小波动率 3% | -| TOP_N_SYMBOLS | 30 | 取前 30 个候选 | -| MAX_SCAN_SYMBOLS | 500 | 最多扫描 500 个 | -| MIN_SIGNAL_STRENGTH | 8 | 信号强度 ≥ 8 才下单 | -| EXCLUDE_MAJOR_COINS | true | 排除大市值币 | -| SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT | 20 | 补单额外候选数 | -| SCAN_INTERVAL | 900 | 扫描间隔 15 分钟 | -| PRIMARY_INTERVAL | '4h' | 主周期 4H | -| ENTRY_INTERVAL | '1h' | 入场周期 1H | -| CONFIRM_INTERVAL | '1d' | 确认周期 1D | - -### 5. 趋势与过滤 -| 配置项 | 当前值 | 说明 | -|--------|--------|------| -| AUTO_TRADE_ONLY_TRENDING | true | 仅趋势行情自动交易 | -| AUTO_TRADE_ALLOW_4H_NEUTRAL | false | 4H 中性不做自动交易 | -| MAX_RSI_FOR_LONG | 65 | 做多 RSI 上限(不追高) | -| MIN_RSI_FOR_SHORT | 30 | 做空 RSI 下限(不杀跌) | -| MAX_CHANGE_PERCENT_FOR_LONG | 25 | 24h 涨幅 >25% 不做多 | -| MAX_CHANGE_PERCENT_FOR_SHORT | 10 | 24h 涨幅 >10% 不做空 | -| BETA_FILTER_ENABLED | true | 大盘共振:BTC/ETH 跌屏蔽多单 | -| BETA_FILTER_THRESHOLD | -0.005 | 阈值 -0.5% | -| ENTRY_SHORT_TREND_FILTER_ENABLED | true | 15m 短周期方向过滤 | -| MAX_TREND_MOVE_BEFORE_ENTRY | 0.04 | 入场前趋势移动上限 4% | - -### 6. 风控与冷却 -| 配置项 | 当前值 | 说明 | -|--------|--------|------| -| SYMBOL_LOSS_COOLDOWN_ENABLED | true | 同 symbol 连亏冷却 | -| SYMBOL_MAX_CONSECUTIVE_LOSSES | 2 | 连亏 2 次触发冷却 | -| SYMBOL_LOSS_COOLDOWN_SEC | 3600 | 冷却 1 小时 | - ---- - -## 二、当前交易策略流程简述 - -1. **扫描**:每 15 分钟按 4H/1H/1D 多周期、MIN_VOLUME_24H / MIN_VOLATILITY 筛选,取 TOP_N 候选。 -2. **信号**:MACD 金叉/死叉 + EMA20/50 + 价格与 EMA 关系,多指标投票得 0–10 分;4H 定方向,禁止逆势。 -3. **过滤**:信号强度 ≥ 8、RSI 不追高/不杀跌、24h 涨跌幅限制、大盘共振、4H 非中性(或强信号 8+)、15m 短周期方向一致。 -4. **仓位**:固定风险 3%/笔,按 ATR 止损距离算仓位,受 MAX_POSITION_PERCENT / MAX_TOTAL / MIN_MARGIN 等约束;动态杠杆 8–20x(高波动币 8x)。 -5. **入场**:智能入场(限价 + 追价/市价兜底)。 -6. **出场**:ATR 止损 + 分步止盈(30% 第一目标、80% 第二目标)+ 移动止损(30% 激活、10% 保护)。 - -整体是**趋势跟踪 + 高盈亏比 + 严格过滤**,逻辑一致。 - ---- - -## 三、存在的问题 - -### 1. 总仓位偏高,回撤压力大 -- **MAX_TOTAL_POSITION_PERCENT: 0.80**:4 仓 × 20% = 80%,几乎满仓。 -- 若 4 笔同时回撤,心理和强平风险都偏大;且总仓 80% 与「单笔 20%」强绑定,缺乏缓冲。 - -**建议**:将 MAX_TOTAL_POSITION_PERCENT 降到 **0.60~0.70**,或保持 0.80 但把 MAX_POSITION_PERCENT 降到 0.15~0.18,单笔略减、总仓不变但更分散。 - -### 2. 第二目标止盈过远,实际很少触及 -- **TAKE_PROFIT_PERCENT: 0.80 (80%)**:第二目标要保证金 80% 盈利才触发。 -- 多数单会在 TP1(30%)或移动止损(30% 激活、10% 保护)结束,第二目标存在感弱。 - -**建议**:若希望第二目标偶尔能打到,可改为 **0.50~0.60**;若接受「主要吃 TP1 + 移动止损」,可维持 0.80 仅作理论目标,或在文案中说明「第二目标偏远,以第一目标和移动止损为主」。 - -### 3. 大盘共振过敏感 -- **BETA_FILTER_THRESHOLD: -0.005 (-0.5%)**:BTC/ETH 跌 0.5% 就屏蔽所有多单。 -- 日常波动常超过 0.5%,容易误杀多单机会。 - -**建议**:改为 **-0.01~-0.015**(-1%~-1.5%)再屏蔽多单,或增加「仅对强信号放宽」的逻辑(若代码支持)。 - -### 4. 预设未显式写的关键项 -- **MIN_CHANGE_PERCENT**、**MIN_POSITION_PERCENT**、**SMART_ENTRY_ENABLED** 等依赖后端默认,前端预设里看不到,容易造成「以为没开/没限制」的误解。 - -**建议**:在「山寨币策略」预设里**显式写出** MIN_CHANGE_PERCENT(如 2.0)、MIN_POSITION_PERCENT(如 0.02)、SMART_ENTRY_ENABLED(true),与后端一致并便于排查。 - -### 5. 文档与预设不一致 -- ConfigGuide 里写「盈亏比 4:1」「2.0×ATR 止损」等,与当前预设(3:1、3.0×ATR)不一致。 - -**建议**:以当前预设为准,更新 ConfigGuide/说明文档,避免误导。 - -### 6. 4H 中性 + 强信号 8 的例外 -- AUTO_TRADE_ALLOW_4H_NEUTRAL: false,但策略里对「信号强度 ≥ 8」允许在 4H 中性时尝试。 -- 逻辑合理,但若希望更保守,可考虑只在 4H 明确 up/down 时下单(即强信号也不在 4H 中性开仓)。 - ---- - -## 四、优化建议汇总 - -| 优先级 | 建议 | 说明 | -|--------|------|------| -| 高 | 降低 MAX_TOTAL_POSITION_PERCENT 至 0.60~0.70 | 降低满仓回撤与强平风险 | -| 高 | 大盘共振 BETA_FILTER_THRESHOLD 调为 -0.01 或 -0.015 | 减少正常波动下多单被误关 | -| 中 | 第二目标 TAKE_PROFIT_PERCENT 改为 0.50~0.60(可选) | 让第二目标更可触及,或保留 0.80 仅作说明 | -| 中 | 预设中显式写 MIN_CHANGE_PERCENT、MIN_POSITION_PERCENT、SMART_ENTRY_ENABLED | 前后端与使用预期一致,便于排错 | -| 低 | 同步更新 ConfigGuide 与当前预设 | 避免文档和实际策略不一致 | -| 低 | 视需求增加「仅趋势、禁止 4H 中性」的更强保守选项 | 进一步减少震荡市出手 | - ---- - -## 五、可选的具体改数建议(直接可改) - -若在全局配置里手动微调,可优先试: - -1. **MAX_TOTAL_POSITION_PERCENT**:0.80 → **0.65** -2. **BETA_FILTER_THRESHOLD**:-0.005 → **-0.01** -3. **TAKE_PROFIT_PERCENT**(第二目标):0.80 → **0.50** 或 **0.60**(按你是否想经常打到第二目标决定) - -其余参数(ATR 3.0、TP1 30%、移动止损 30%/10%、3% 固定风险、MIN_SIGNAL_STRENGTH 8、4H 中性关闭等)与当前「高盈亏比 + 严过滤」设计一致,无硬伤;可按实盘表现再微调单笔仓位或每日次数。