1
This commit is contained in:
parent
977669302f
commit
2b5906ca6d
|
|
@ -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 **无法**绕过,需等解封并降请求量 |
|
||||
|
|
@ -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` 并挂上保护单,则不受影响。
|
||||
|
||||
**结论**:拿不到余额/持仓只影响「本机展示与同步、新开仓、补挂单」;**已挂在币安上的止盈止损单会照常执行**,可放心。
|
||||
|
|
@ -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')
|
||||
|
|
@ -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()
|
||||
|
|
@ -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% 的老订单,以避免进一步扩大损失。")
|
||||
|
|
@ -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()
|
||||
57
backend/database/dedupe_trades_by_entry_order.sql
Normal file
57
backend/database/dedupe_trades_by_entry_order.sql
Normal file
|
|
@ -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;
|
||||
-- 期望结果:空
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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())
|
||||
|
|
@ -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())
|
||||
|
|
@ -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())
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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())
|
||||
|
|
@ -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()
|
||||
|
|
@ -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 中性关闭等)与当前「高盈亏比 + 严过滤」设计一致,无硬伤;可按实盘表现再微调单笔仓位或每日次数。
|
||||
Loading…
Reference in New Issue
Block a user