This commit is contained in:
薇薇安 2026-02-15 13:35:33 +08:00
parent 977669302f
commit 2b5906ca6d
24 changed files with 189 additions and 1586 deletions

View File

@ -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 **无法**绕过,需等解封并降请求量 |

View File

@ -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可先等 12 分钟再跑,或临时增大 `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` 并挂上保护单,则不受影响。
**结论**:拿不到余额/持仓只影响「本机展示与同步、新开仓、补挂单」;**已挂在币安上的止盈止损单会照常执行**,可放心。

View File

@ -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')

View File

@ -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()

View File

@ -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% 的老订单,以避免进一步扩大损失。")

View File

@ -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()

View 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;
-- 期望结果:空

View File

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

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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()

View File

@ -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()

View File

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

View File

@ -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'))

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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())

View File

@ -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()

View File

@ -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 关系,多指标投票得 010 分4H 定方向,禁止逆势。
3. **过滤**:信号强度 ≥ 8、RSI 不追高/不杀跌、24h 涨跌幅限制、大盘共振、4H 非中性(或强信号 8+、15m 短周期方向一致。
4. **仓位**:固定风险 3%/笔,按 ATR 止损距离算仓位,受 MAX_POSITION_PERCENT / MAX_TOTAL / MIN_MARGIN 等约束;动态杠杆 820x高波动币 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.600.70**,或保持 0.80 但把 MAX_POSITION_PERCENT 降到 0.150.18,单笔略减、总仓不变但更分散。
### 2. 第二目标止盈过远,实际很少触及
- **TAKE_PROFIT_PERCENT: 0.80 (80%)**:第二目标要保证金 80% 盈利才触发。
- 多数单会在 TP130%或移动止损30% 激活、10% 保护)结束,第二目标存在感弱。
**建议**:若希望第二目标偶尔能打到,可改为 **0.500.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_ENABLEDtrue与后端一致并便于排查。
### 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.600.70 | 降低满仓回撤与强平风险 |
| 高 | 大盘共振 BETA_FILTER_THRESHOLD 调为 -0.01 或 -0.015 | 减少正常波动下多单被误关 |
| 中 | 第二目标 TAKE_PROFIT_PERCENT 改为 0.500.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 中性关闭等)与当前「高盈亏比 + 严过滤」设计一致,无硬伤;可按实盘表现再微调单笔仓位或每日次数。