diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py
index 302fd1c..253b878 100644
--- a/backend/api/routes/account.py
+++ b/backend/api/routes/account.py
@@ -544,3 +544,145 @@ async def close_position(symbol: str):
logger.error(f"错误类型: {type(e).__name__}")
logger.error("=" * 60, exc_info=True)
raise HTTPException(status_code=500, detail=error_msg)
+
+
+@router.post("/positions/sync")
+async def sync_positions():
+ """同步币安实际持仓状态与数据库状态"""
+ try:
+ logger.info("=" * 60)
+ logger.info("收到持仓状态同步请求")
+ logger.info("=" * 60)
+
+ # 从数据库读取API密钥
+ api_key = TradingConfig.get_value('BINANCE_API_KEY')
+ api_secret = TradingConfig.get_value('BINANCE_API_SECRET')
+ use_testnet = TradingConfig.get_value('USE_TESTNET', False)
+
+ if not api_key or not api_secret:
+ error_msg = "API密钥未配置"
+ logger.warning(error_msg)
+ raise HTTPException(status_code=400, detail=error_msg)
+
+ # 导入必要的模块
+ try:
+ from binance_client import BinanceClient
+ except ImportError:
+ trading_system_path = project_root / 'trading_system'
+ sys.path.insert(0, str(trading_system_path))
+ from binance_client import BinanceClient
+
+ # 导入数据库模型
+ from database.models import Trade
+
+ # 创建客户端
+ client = BinanceClient(
+ api_key=api_key,
+ api_secret=api_secret,
+ testnet=use_testnet
+ )
+
+ logger.info("连接币安API...")
+ await client.connect()
+
+ try:
+ # 1. 获取币安实际持仓
+ binance_positions = await client.get_open_positions()
+ binance_symbols = {p['symbol'] for p in binance_positions if float(p.get('positionAmt', 0)) != 0}
+ logger.info(f"币安实际持仓: {len(binance_symbols)} 个")
+ if binance_symbols:
+ logger.info(f" 持仓列表: {', '.join(binance_symbols)}")
+
+ # 2. 获取数据库中状态为open的交易记录
+ db_open_trades = Trade.get_all(status='open')
+ db_open_symbols = {t['symbol'] for t in db_open_trades}
+ logger.info(f"数据库open状态: {len(db_open_symbols)} 个")
+ if db_open_symbols:
+ logger.info(f" 持仓列表: {', '.join(db_open_symbols)}")
+
+ # 3. 找出在数据库中open但在币安已不存在的持仓(需要更新为closed)
+ missing_in_binance = db_open_symbols - binance_symbols
+ updated_count = 0
+
+ if missing_in_binance:
+ logger.info(f"发现 {len(missing_in_binance)} 个持仓在数据库中是open但币安已不存在: {', '.join(missing_in_binance)}")
+
+ for symbol in missing_in_binance:
+ try:
+ # 获取该交易对的所有open记录
+ open_trades = Trade.get_by_symbol(symbol, status='open')
+
+ for trade in open_trades:
+ trade_id = trade['id']
+ entry_price = float(trade['entry_price'])
+ quantity = float(trade['quantity'])
+
+ # 获取当前价格作为平仓价格
+ ticker = await client.get_ticker_24h(symbol)
+ exit_price = float(ticker['price']) if ticker else entry_price
+
+ # 计算盈亏
+ if trade['side'] == 'BUY':
+ pnl = (exit_price - entry_price) * quantity
+ else:
+ pnl = (entry_price - exit_price) * quantity
+
+ # 计算基于保证金的盈亏百分比
+ leverage = float(trade.get('leverage', 10))
+ entry_value = entry_price * quantity
+ margin = entry_value / leverage if leverage > 0 else entry_value
+ pnl_percent_margin = (pnl / margin * 100) if margin > 0 else 0
+
+ # 更新数据库记录
+ Trade.update_exit(
+ trade_id=trade_id,
+ exit_price=exit_price,
+ exit_reason='sync', # 标记为同步平仓
+ pnl=pnl,
+ pnl_percent=pnl_percent_margin, # 使用基于保证金的盈亏百分比
+ exit_order_id=None
+ )
+ updated_count += 1
+ logger.info(
+ f"✓ {symbol} 已更新为closed (ID: {trade_id}, "
+ f"盈亏: {pnl:.2f} USDT, {pnl_percent_margin:.2f}% of margin)"
+ )
+ except Exception as e:
+ logger.error(f"❌ {symbol} 更新失败: {e}")
+ import traceback
+ logger.error(f" 错误详情:\n{traceback.format_exc()}")
+ else:
+ logger.info("✓ 数据库与币安状态一致,无需更新")
+
+ # 4. 检查币安有但数据库没有记录的持仓
+ missing_in_db = binance_symbols - db_open_symbols
+ if missing_in_db:
+ logger.info(f"发现 {len(missing_in_db)} 个持仓在币安存在但数据库中没有记录: {', '.join(missing_in_db)}")
+ logger.info(" 这些持仓可能是手动开仓的,建议手动处理")
+
+ result = {
+ "message": "持仓状态同步完成",
+ "binance_positions": len(binance_symbols),
+ "db_open_positions": len(db_open_symbols),
+ "updated_to_closed": updated_count,
+ "missing_in_binance": list(missing_in_binance),
+ "missing_in_db": list(missing_in_db)
+ }
+
+ logger.info("=" * 60)
+ logger.info("持仓状态同步完成!")
+ logger.info(f"结果: {result}")
+ logger.info("=" * 60)
+
+ return result
+
+ finally:
+ await client.disconnect()
+ logger.info("✓ 已断开币安API连接")
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ error_msg = f"同步持仓状态失败: {str(e)}"
+ logger.error(error_msg, exc_info=True)
+ raise HTTPException(status_code=500, detail=error_msg)
diff --git a/backend/config_manager.py b/backend/config_manager.py
index 54e391e..4acdbc7 100644
--- a/backend/config_manager.py
+++ b/backend/config_manager.py
@@ -119,8 +119,10 @@ class ConfigManager:
'TOP_N_SYMBOLS': self.get('TOP_N_SYMBOLS', 10),
# 风险控制
- 'STOP_LOSS_PERCENT': self.get('STOP_LOSS_PERCENT', 0.03),
- 'TAKE_PROFIT_PERCENT': self.get('TAKE_PROFIT_PERCENT', 0.05),
+ 'STOP_LOSS_PERCENT': self.get('STOP_LOSS_PERCENT', 0.08), # 默认8%(更宽松,避免被正常波动触发)
+ 'TAKE_PROFIT_PERCENT': self.get('TAKE_PROFIT_PERCENT', 0.15), # 默认15%(给趋势更多空间)
+ 'MIN_STOP_LOSS_PRICE_PCT': self.get('MIN_STOP_LOSS_PRICE_PCT', 0.02), # 默认2%
+ 'MIN_TAKE_PROFIT_PRICE_PCT': self.get('MIN_TAKE_PROFIT_PRICE_PCT', 0.03), # 默认3%
# 市场扫描(1小时主周期)
'SCAN_INTERVAL': self.get('SCAN_INTERVAL', 3600), # 1小时
diff --git a/backend/database/init.sql b/backend/database/init.sql
index 76a4df3..c26b765 100644
--- a/backend/database/init.sql
+++ b/backend/database/init.sql
@@ -141,8 +141,10 @@ INSERT INTO `trading_config` (`config_key`, `config_value`, `config_type`, `cate
('MAX_SCAN_SYMBOLS', '500', 'number', 'scan', '扫描的最大交易对数量(0表示扫描所有,建议100-500)'),
-- 风险控制
-('STOP_LOSS_PERCENT', '0.03', 'number', 'risk', '止损:3%'),
-('TAKE_PROFIT_PERCENT', '0.05', 'number', 'risk', '止盈:5%'),
+('STOP_LOSS_PERCENT', '0.08', 'number', 'risk', '止损:8%(相对于保证金,更宽松避免被正常波动触发)'),
+('TAKE_PROFIT_PERCENT', '0.15', 'number', 'risk', '止盈:15%(相对于保证金,给趋势更多空间)'),
+('MIN_STOP_LOSS_PRICE_PCT', '0.02', 'number', 'risk', '最小止损价格变动:2%(防止止损过紧)'),
+('MIN_TAKE_PROFIT_PRICE_PCT', '0.03', 'number', 'risk', '最小止盈价格变动:3%(防止止盈过紧)'),
-- 市场扫描(1小时主周期)
('SCAN_INTERVAL', '3600', 'number', 'scan', '扫描间隔:1小时(秒)'),
diff --git a/frontend/src/components/ConfigGuide.jsx b/frontend/src/components/ConfigGuide.jsx
index 825f9e3..3d93037 100644
--- a/frontend/src/components/ConfigGuide.jsx
+++ b/frontend/src/components/ConfigGuide.jsx
@@ -15,53 +15,59 @@ const ConfigGuide = () => {
一、预设方案说明
-
方案1:保守配置(默认)
-
适合新手或稳健型交易者,风险较低,交易频率适中
+
方案1:保守配置
+
适合新手或稳健型交易者,风险较低,止损止盈较宽松,避免被正常波动触发
- 扫描间隔: 3600秒(1小时)
- 最小涨跌幅: 2.0%
- 信号强度: 5/10
- 处理交易对: 10个
+ - 止损: 10% of margin(最小2%价格变动)
+ - 止盈: 20% of margin(最小3%价格变动)
- 效果:每小时扫描一次,只捕捉2%以上的波动,信号质量高,胜率较高但交易机会较少
+ 效果:每小时扫描一次,只捕捉2%以上的波动,止损止盈宽松,避免被正常波动触发,适合稳健交易
方案2:平衡配置(推荐)
-
平衡交易频率和信号质量,适合大多数交易者
+
平衡交易频率和信号质量,止损止盈适中,适合大多数交易者
- 扫描间隔: 600秒(10分钟)
- 最小涨跌幅: 1.5%
- 信号强度: 4/10
- 处理交易对: 12个
+ - 止损: 8% of margin(最小2%价格变动)
+ - 止盈: 15% of margin(最小3%价格变动)
- 效果:10分钟扫描一次,捕捉1.5%以上的波动,交易机会增加,信号质量仍然较高
+ 效果:10分钟扫描一次,捕捉1.5%以上的波动,止损止盈适中,平衡风险与收益,推荐使用
方案3:激进高频配置
-
适合晚间波动大时使用,交易频率高,需要密切监控
+
适合晚间波动大时使用,交易频率高,止损止盈较紧,快速止盈止损
- 扫描间隔: 300秒(5分钟)
- 最小涨跌幅: 1.0%
- 信号强度: 3/10
- - 处理交易对: 20个
+ - 处理交易对: 18个
+ - 止损: 5% of margin(最小1.5%价格变动)
+ - 止盈: 10% of margin(最小2%价格变动)
- 效果:5分钟扫描一次,捕捉1%以上的波动,交易机会大幅增加,但需要监控胜率和手续费
+ 效果:5分钟扫描一次,捕捉1%以上的波动,止损止盈较紧,快速锁定利润或止损,适合高频交易
- ⚠️ 风险提示:高频交易会增加手续费成本,建议在波动大的时段使用,并密切监控胜率
+ ⚠️ 风险提示:高频交易会增加手续费成本,止损止盈较紧可能被正常波动触发,建议在波动大的时段使用,并密切监控胜率
diff --git a/frontend/src/components/ConfigPanel.jsx b/frontend/src/components/ConfigPanel.jsx
index e41be40..a7411a2 100644
--- a/frontend/src/components/ConfigPanel.jsx
+++ b/frontend/src/components/ConfigPanel.jsx
@@ -10,42 +10,54 @@ const ConfigPanel = () => {
const [message, setMessage] = useState('')
// 预设方案配置
- // 注意:百分比配置使用整数形式(如2.0表示2%),在应用时会转换为小数(0.02)
+ // 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(0.08)
const presets = {
conservative: {
name: '保守配置',
- desc: '适合新手,风险较低,交易频率适中',
+ desc: '适合新手,风险较低,止损止盈较宽松,避免被正常波动触发',
configs: {
SCAN_INTERVAL: 3600,
MIN_CHANGE_PERCENT: 2.0, // 2%
MIN_SIGNAL_STRENGTH: 5,
TOP_N_SYMBOLS: 10,
MAX_SCAN_SYMBOLS: 150,
- MIN_VOLATILITY: 0.02 // 保持小数形式(波动率)
+ MIN_VOLATILITY: 0.02, // 保持小数形式(波动率)
+ STOP_LOSS_PERCENT: 10.0, // 10%(相对于保证金,更宽松)
+ TAKE_PROFIT_PERCENT: 20.0, // 20%(相对于保证金,给趋势更多空间)
+ MIN_STOP_LOSS_PRICE_PCT: 2.0, // 2%最小价格变动保护
+ MIN_TAKE_PROFIT_PRICE_PCT: 3.0 // 3%最小价格变动保护
}
},
balanced: {
name: '平衡配置',
- desc: '推荐使用,平衡频率和质量',
+ desc: '推荐使用,平衡频率和质量,止损止盈适中',
configs: {
SCAN_INTERVAL: 600,
MIN_CHANGE_PERCENT: 1.5, // 1.5%
MIN_SIGNAL_STRENGTH: 4,
TOP_N_SYMBOLS: 12,
MAX_SCAN_SYMBOLS: 250,
- MIN_VOLATILITY: 0.018 // 保持小数形式(波动率)
+ MIN_VOLATILITY: 0.018, // 保持小数形式(波动率)
+ STOP_LOSS_PERCENT: 8.0, // 8%(相对于保证金,默认值)
+ TAKE_PROFIT_PERCENT: 15.0, // 15%(相对于保证金,默认值)
+ MIN_STOP_LOSS_PRICE_PCT: 2.0, // 2%最小价格变动保护
+ MIN_TAKE_PROFIT_PRICE_PCT: 3.0 // 3%最小价格变动保护
}
},
aggressive: {
name: '激进高频',
- desc: '晚间波动大时使用,交易频率高',
+ desc: '晚间波动大时使用,交易频率高,止损止盈较紧',
configs: {
SCAN_INTERVAL: 300,
MIN_CHANGE_PERCENT: 1.0, // 1%
MIN_SIGNAL_STRENGTH: 3,
TOP_N_SYMBOLS: 18,
MAX_SCAN_SYMBOLS: 350,
- MIN_VOLATILITY: 0.015 // 保持小数形式(波动率)
+ MIN_VOLATILITY: 0.015, // 保持小数形式(波动率)
+ STOP_LOSS_PERCENT: 5.0, // 5%(相对于保证金,较紧)
+ TAKE_PROFIT_PERCENT: 10.0, // 10%(相对于保证金,快速止盈)
+ MIN_STOP_LOSS_PRICE_PCT: 1.5, // 1.5%最小价格变动保护
+ MIN_TAKE_PROFIT_PRICE_PCT: 2.0 // 2%最小价格变动保护
}
}
}
@@ -107,7 +119,7 @@ const ConfigPanel = () => {
// 获取当前值(处理百分比转换)
let currentValue = currentConfig.value
- if (key.includes('PERCENT')) {
+ if (key.includes('PERCENT') || key.includes('PCT')) {
currentValue = currentValue * 100
}
@@ -140,10 +152,37 @@ const ConfigPanel = () => {
try {
const configItems = Object.entries(preset.configs).map(([key, value]) => {
const config = configs[key]
- if (!config) return null
+ if (!config) {
+ // 如果配置项不存在,尝试创建(用于新增的配置项)
+ // 根据key判断类型和分类
+ let type = 'number'
+ let category = 'risk'
+ if (key.includes('PERCENT') || key.includes('PCT')) {
+ type = 'number'
+ if (key.includes('STOP_LOSS') || key.includes('TAKE_PROFIT')) {
+ category = 'risk'
+ } else {
+ category = 'scan'
+ }
+ } else if (key === 'MIN_VOLATILITY') {
+ type = 'number'
+ category = 'scan'
+ } else if (typeof value === 'number') {
+ type = 'number'
+ category = 'scan'
+ }
+
+ return {
+ key,
+ value: (key.includes('PERCENT') || key.includes('PCT')) ? value / 100 : value,
+ type,
+ category,
+ description: `预设方案配置项:${key}`
+ }
+ }
return {
key,
- value: key.includes('PERCENT') ? value / 100 : value,
+ value: (key.includes('PERCENT') || key.includes('PCT')) ? value / 100 : value,
type: config.type,
category: config.category,
description: config.description
@@ -585,8 +624,10 @@ const getConfigDetail = (key) => {
'MIN_POSITION_PERCENT': '单笔最小仓位(账户余额的百分比,如0.01表示1%)。单笔交易允许的最小仓位大小,避免交易过小的仓位,减少手续费影响。建议:1-2%。',
// 风险控制参数
- 'STOP_LOSS_PERCENT': '止损百分比(如0.03表示3%)。当亏损达到此百分比时自动平仓止损,限制单笔交易的最大亏损。值越小止损更严格,单笔损失更小但可能被正常波动触发。值越大允许更大的回撤,但单笔损失可能较大。建议:保守策略3-5%,平衡策略2-3%,激进策略2-3%。注意:止损应该小于止盈,建议盈亏比至少1:1.5。',
- 'TAKE_PROFIT_PERCENT': '止盈百分比(如0.05表示5%)。当盈利达到此百分比时自动平仓止盈,锁定利润。值越大目标利润更高,但可能错过及时止盈的机会,持仓时间更长。值越小能更快锁定利润,但可能错过更大的趋势。建议:保守策略5-8%,平衡策略5-6%,激进策略3-5%。注意:应该大于止损,建议盈亏比至少1:1.5。',
+ 'STOP_LOSS_PERCENT': '止损百分比(如0.08表示8%,相对于保证金)。当亏损达到此百分比时自动平仓止损,限制单笔交易的最大亏损。值越小止损更严格,单笔损失更小但可能被正常波动触发。值越大允许更大的回撤,但单笔损失可能较大。建议:保守策略10-15%,平衡策略8-10%,激进策略5-8%。注意:止损应该小于止盈,建议盈亏比至少1:1.5。系统会结合最小价格变动保护,取更宽松的一个。',
+ 'TAKE_PROFIT_PERCENT': '止盈百分比(如0.15表示15%,相对于保证金)。当盈利达到此百分比时自动平仓止盈,锁定利润。值越大目标利润更高,但可能错过及时止盈的机会,持仓时间更长。值越小能更快锁定利润,但可能错过更大的趋势。建议:保守策略20-30%,平衡策略15-20%,激进策略10-15%。注意:应该大于止损,建议盈亏比至少1:1.5。系统会结合最小价格变动保护,取更宽松的一个。',
+ 'MIN_STOP_LOSS_PRICE_PCT': '最小止损价格变动百分比(如0.02表示2%)。防止止损过紧,即使基于保证金的止损更紧,也会使用至少此百分比的价格变动。建议:保守策略2-3%,平衡策略2%,激进策略1.5-2%。',
+ 'MIN_TAKE_PROFIT_PRICE_PCT': '最小止盈价格变动百分比(如0.03表示3%)。防止止盈过紧,即使基于保证金的止盈更紧,也会使用至少此百分比的价格变动。建议:保守策略3-4%,平衡策略3%,激进策略2-3%。',
// 策略参数
'LEVERAGE': '交易杠杆倍数。放大资金利用率,同时放大收益和风险。杠杆越高,相同仓位下需要的保证金越少,但风险越大。建议:保守策略5-10倍,平衡策略10倍,激进策略10-15倍。注意:高杠杆会增加爆仓风险,请谨慎使用。',
diff --git a/frontend/src/components/StatsDashboard.jsx b/frontend/src/components/StatsDashboard.jsx
index af826b4..a9bb05a 100644
--- a/frontend/src/components/StatsDashboard.jsx
+++ b/frontend/src/components/StatsDashboard.jsx
@@ -79,6 +79,46 @@ const StatsDashboard = () => {
}
}
+ const handleSyncPositions = async () => {
+ if (!window.confirm('确定要同步持仓状态吗?这将检查币安实际持仓并更新数据库状态。')) {
+ return
+ }
+
+ setMessage('正在同步持仓状态...')
+
+ try {
+ const result = await api.syncPositions()
+ console.log('同步结果:', result)
+
+ let message = result.message || '同步完成'
+ if (result.updated_to_closed > 0) {
+ message += `,已更新 ${result.updated_to_closed} 条记录为已平仓`
+ }
+ if (result.missing_in_db && result.missing_in_db.length > 0) {
+ message += `,发现 ${result.missing_in_db.length} 个币安持仓在数据库中没有记录`
+ }
+
+ setMessage(message)
+
+ // 立即刷新数据
+ await loadDashboard()
+
+ // 5秒后清除消息
+ setTimeout(() => {
+ setMessage('')
+ }, 5000)
+ } catch (error) {
+ console.error('Sync positions error:', error)
+ const errorMessage = error.message || error.toString() || '同步失败,请检查网络连接或后端服务'
+ setMessage(`同步失败: ${errorMessage}`)
+
+ // 错误消息5秒后清除
+ setTimeout(() => {
+ setMessage('')
+ }, 5000)
+ }
+ }
+
if (loading) return 加载中...
const account = dashboardData?.account
@@ -86,7 +126,24 @@ const StatsDashboard = () => {
return (
-
仪表板
+
+
仪表板
+
+
{message && (
@@ -205,9 +262,9 @@ const StatsDashboard = () => {
const stopLossConfig = configSource?.STOP_LOSS_PERCENT
const takeProfitConfig = configSource?.TAKE_PROFIT_PERCENT
- // 配置值是小数形式(0.03表示3%),相对于保证金
- let stopLossPercentMargin = 0.03 // 默认3%(相对于保证金)
- let takeProfitPercentMargin = 0.05 // 默认5%(相对于保证金)
+ // 配置值是小数形式(0.08表示8%),相对于保证金
+ let stopLossPercentMargin = 0.08 // 默认8%(相对于保证金,更宽松)
+ let takeProfitPercentMargin = 0.15 // 默认15%(相对于保证金,给趋势更多空间)
if (stopLossConfig) {
const configValue = stopLossConfig.value
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index df47364..13a78ea 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -121,6 +121,21 @@ export const api = {
return response.json();
},
+ // 同步持仓状态
+ syncPositions: async () => {
+ const response = await fetch(buildUrl('/api/account/positions/sync'), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ detail: '同步失败' }));
+ throw new Error(error.detail || '同步失败');
+ }
+ return response.json();
+ },
+
// 交易推荐
getRecommendations: async (params = {}) => {
// 默认使用实时推荐
diff --git a/trading_system/config.py b/trading_system/config.py
index a3b9e94..4cad959 100644
--- a/trading_system/config.py
+++ b/trading_system/config.py
@@ -168,10 +168,10 @@ def _get_trading_config():
'MIN_CHANGE_PERCENT': 0.5, # 降低到0.5%以获取更多推荐(推荐系统可以更宽松)
'TOP_N_SYMBOLS': 50, # 每次扫描后处理的交易对数量(增加到50以获取更多推荐)
'MAX_SCAN_SYMBOLS': 500, # 扫描的最大交易对数量(0表示扫描所有)
- 'STOP_LOSS_PERCENT': 0.03, # 止损百分比(相对于保证金),默认3%
- 'TAKE_PROFIT_PERCENT': 0.05, # 止盈百分比(相对于保证金),默认5%
- 'MIN_STOP_LOSS_PRICE_PCT': 0.01, # 最小止损价格变动百分比(如0.01表示1%),防止止损过紧,默认1%
- 'MIN_TAKE_PROFIT_PRICE_PCT': 0.015, # 最小止盈价格变动百分比(如0.015表示1.5%),防止止盈过紧,默认1.5%
+ 'STOP_LOSS_PERCENT': 0.08, # 止损百分比(相对于保证金),默认8%(更宽松,避免被正常波动触发)
+ 'TAKE_PROFIT_PERCENT': 0.15, # 止盈百分比(相对于保证金),默认15%(更宽松,给趋势更多空间)
+ 'MIN_STOP_LOSS_PRICE_PCT': 0.02, # 最小止损价格变动百分比(如0.02表示2%),防止止损过紧,默认2%
+ 'MIN_TAKE_PROFIT_PRICE_PCT': 0.03, # 最小止盈价格变动百分比(如0.03表示3%),防止止盈过紧,默认3%
'SCAN_INTERVAL': 3600,
'KLINE_INTERVAL': '1h',
'PRIMARY_INTERVAL': '1h',
diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py
index 387933e..02808e1 100644
--- a/trading_system/position_manager.py
+++ b/trading_system/position_manager.py
@@ -163,9 +163,12 @@ class PositionManager:
atr=atr
)
- # 计算止盈(基于保证金,为止损的倍数)
- # 如果止损是保证金的3%,止盈可以是保证金的7.5%(2.5倍)
- take_profit_pct_margin = stop_loss_pct_margin * 2.5
+ # 计算止盈(基于保证金)
+ # 优先使用配置的止盈百分比,如果没有配置则使用止损的2倍
+ take_profit_pct_margin = self.risk_manager.config.get('TAKE_PROFIT_PERCENT', 0.15)
+ # 如果配置中没有设置止盈,则使用止损的2倍作为默认
+ if take_profit_pct_margin is None or take_profit_pct_margin == 0:
+ take_profit_pct_margin = stop_loss_pct_margin * 2.0
take_profit_price = self.risk_manager.get_take_profit_price(
entry_price, side, quantity, leverage,
take_profit_pct=take_profit_pct_margin
@@ -185,61 +188,97 @@ class PositionManager:
if entry_order_id:
logger.info(f"{symbol} [开仓] 币安订单号: {entry_order_id}")
- # 等待订单成交,然后从币安获取实际成交价格
+ # 等待订单成交,检查订单状态并获取实际成交价格
+ # 只有在订单真正成交(FILLED)后才保存到数据库
actual_entry_price = None
- try:
- # 等待一小段时间让订单成交
- await asyncio.sleep(1)
-
- # 从币安获取订单详情,获取实际成交价格
+ order_status = None
+ filled_quantity = 0
+ max_retries = 5 # 最多重试5次,每次等待1秒
+ retry_count = 0
+
+ while retry_count < max_retries:
try:
- order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=entry_order_id)
- if order_info:
- # 优先使用平均成交价格(avgPrice),如果没有则使用价格字段
- actual_entry_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0))
- if actual_entry_price > 0:
- logger.info(f"{symbol} [开仓] 从币安订单获取实际成交价格: {actual_entry_price:.4f} USDT")
- else:
- # 如果订单还没有完全成交,尝试从成交记录获取
- if order_info.get('status') == 'FILLED' and order_info.get('fills'):
- # 计算加权平均成交价格
- total_qty = 0
- total_value = 0
- for fill in order_info.get('fills', []):
- qty = float(fill.get('qty', 0))
- price = float(fill.get('price', 0))
- total_qty += qty
- total_value += qty * price
- if total_qty > 0:
- actual_entry_price = total_value / total_qty
- logger.info(f"{symbol} [开仓] 从成交记录计算平均成交价格: {actual_entry_price:.4f} USDT")
- except Exception as order_error:
- logger.warning(f"{symbol} [开仓] 获取订单详情失败: {order_error},使用备用方法")
-
- # 如果无法从订单获取价格,使用当前价格作为备用
- if not actual_entry_price or actual_entry_price <= 0:
- ticker = await self.client.get_ticker_24h(symbol)
- if ticker:
- actual_entry_price = float(ticker['price'])
- logger.warning(f"{symbol} [开仓] 使用当前价格作为入场价格: {actual_entry_price:.4f} USDT")
- else:
- actual_entry_price = float(order.get('avgPrice', 0)) or float(order.get('price', 0))
- if actual_entry_price <= 0:
- logger.error(f"{symbol} [开仓] 无法获取入场价格,使用订单价格字段")
- actual_entry_price = float(order.get('price', 0)) or entry_price
- except Exception as price_error:
- logger.warning(f"{symbol} [开仓] 获取成交价格时出错: {price_error},使用当前价格")
- ticker = await self.client.get_ticker_24h(symbol)
- actual_entry_price = float(ticker['price']) if ticker else entry_price
+ # 等待一小段时间让订单成交
+ await asyncio.sleep(1)
+
+ # 从币安获取订单详情,检查订单状态
+ try:
+ order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=entry_order_id)
+ if order_info:
+ order_status = order_info.get('status')
+ logger.info(f"{symbol} [开仓] 订单状态: {order_status} (重试 {retry_count + 1}/{max_retries})")
+
+ # 检查订单是否已成交
+ if order_status == 'FILLED':
+ # 订单已完全成交,获取实际成交价格和数量
+ actual_entry_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0))
+ filled_quantity = float(order_info.get('executedQty', 0))
+
+ if actual_entry_price > 0 and filled_quantity > 0:
+ logger.info(f"{symbol} [开仓] ✓ 订单已成交,成交价格: {actual_entry_price:.4f} USDT, 成交数量: {filled_quantity:.4f}")
+ break
+ elif order_info.get('fills'):
+ # 从成交记录计算加权平均成交价格和总成交数量
+ total_qty = 0
+ total_value = 0
+ for fill in order_info.get('fills', []):
+ qty = float(fill.get('qty', 0))
+ price = float(fill.get('price', 0))
+ total_qty += qty
+ total_value += qty * price
+ if total_qty > 0:
+ actual_entry_price = total_value / total_qty
+ filled_quantity = total_qty
+ logger.info(f"{symbol} [开仓] ✓ 订单已成交,从成交记录计算平均成交价格: {actual_entry_price:.4f} USDT, 成交数量: {filled_quantity:.4f}")
+ break
+ elif order_status == 'PARTIALLY_FILLED':
+ # 部分成交,继续等待
+ filled_quantity = float(order_info.get('executedQty', 0))
+ logger.info(f"{symbol} [开仓] ⏳ 订单部分成交 ({filled_quantity:.4f}/{quantity:.4f}),继续等待...")
+ retry_count += 1
+ continue
+ elif order_status in ['NEW', 'PENDING_NEW']:
+ # 订单已提交但未成交,继续等待
+ logger.info(f"{symbol} [开仓] ⏳ 订单已提交但未成交,继续等待...")
+ retry_count += 1
+ continue
+ elif order_status in ['CANCELED', 'REJECTED', 'EXPIRED']:
+ # 订单被取消、拒绝或过期
+ logger.error(f"{symbol} [开仓] ❌ 订单状态异常: {order_status},订单未成交")
+ return None
+ else:
+ logger.warning(f"{symbol} [开仓] ⚠️ 未知订单状态: {order_status},继续等待...")
+ retry_count += 1
+ continue
+ except Exception as order_error:
+ logger.warning(f"{symbol} [开仓] 获取订单详情失败: {order_error},重试中...")
+ retry_count += 1
+ continue
+ except Exception as price_error:
+ logger.warning(f"{symbol} [开仓] 检查订单状态时出错: {price_error},重试中...")
+ retry_count += 1
+ continue
- # 使用实际成交价格(如果获取成功)
- if actual_entry_price and actual_entry_price > 0:
- # 记录下单时的价格(用于对比)
- original_entry_price = entry_price
- entry_price = actual_entry_price
- logger.info(f"{symbol} [开仓] 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f})")
+ # 检查订单是否最终成交
+ if order_status != 'FILLED':
+ logger.error(f"{symbol} [开仓] ❌ 订单未成交,状态: {order_status},不保存到数据库")
+ return None
- # 记录到数据库(使用实际成交价格)
+ if not actual_entry_price or actual_entry_price <= 0:
+ logger.error(f"{symbol} [开仓] ❌ 无法获取实际成交价格,不保存到数据库")
+ return None
+
+ if filled_quantity <= 0:
+ logger.error(f"{symbol} [开仓] ❌ 成交数量为0,不保存到数据库")
+ return None
+
+ # 使用实际成交价格和数量
+ original_entry_price = entry_price
+ entry_price = actual_entry_price
+ quantity = filled_quantity # 使用实际成交数量
+ logger.info(f"{symbol} [开仓] ✓ 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f}), 成交数量: {quantity:.4f}")
+
+ # 记录到数据库(只有在订单真正成交后才保存)
trade_id = None
if DB_AVAILABLE and Trade:
try:
@@ -247,18 +286,19 @@ class PositionManager:
trade_id = Trade.create(
symbol=symbol,
side=side,
- quantity=quantity,
+ quantity=quantity, # 使用实际成交数量
entry_price=entry_price, # 使用实际成交价格
leverage=leverage,
entry_reason=entry_reason,
entry_order_id=entry_order_id # 保存币安订单号
)
- logger.info(f"✓ {symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f})")
+ logger.info(f"✓ {symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})")
except Exception as e:
logger.error(f"❌ 保存交易记录到数据库失败: {e}")
logger.error(f" 错误类型: {type(e).__name__}")
import traceback
logger.error(f" 错误详情:\n{traceback.format_exc()}")
+ return None
elif not DB_AVAILABLE:
logger.debug(f"数据库不可用,跳过保存 {symbol} 交易记录")
elif not Trade:
@@ -1059,10 +1099,11 @@ class PositionManager:
# 计算止损止盈(基于保证金)
leverage = binance_position.get('leverage', 10)
- stop_loss_pct_margin = self.risk_manager.config.get('STOP_LOSS_PERCENT', 0.03)
- take_profit_pct_margin = self.risk_manager.config.get('TAKE_PROFIT_PERCENT', 0.05)
- # 止盈为止损的2.5倍
- take_profit_pct_margin = stop_loss_pct_margin * 2.5
+ stop_loss_pct_margin = self.risk_manager.config.get('STOP_LOSS_PERCENT', 0.08)
+ take_profit_pct_margin = self.risk_manager.config.get('TAKE_PROFIT_PERCENT', 0.15)
+ # 如果配置中没有设置止盈,则使用止损的2倍作为默认
+ if take_profit_pct_margin is None or take_profit_pct_margin == 0:
+ take_profit_pct_margin = stop_loss_pct_margin * 2.0
stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage,
@@ -1153,8 +1194,11 @@ class PositionManager:
# 计算止损止盈(基于保证金)
leverage = position.get('leverage', 10)
- stop_loss_pct_margin = self.risk_manager.config.get('STOP_LOSS_PERCENT', 0.03)
- take_profit_pct_margin = stop_loss_pct_margin * 2.5
+ stop_loss_pct_margin = self.risk_manager.config.get('STOP_LOSS_PERCENT', 0.08)
+ take_profit_pct_margin = self.risk_manager.config.get('TAKE_PROFIT_PERCENT', 0.15)
+ # 如果配置中没有设置止盈,则使用止损的2倍作为默认
+ if take_profit_pct_margin is None or take_profit_pct_margin == 0:
+ take_profit_pct_margin = stop_loss_pct_margin * 2.0
stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage,
diff --git a/trading_system/trade_recommender.py b/trading_system/trade_recommender.py
index 49e8c76..aa36509 100644
--- a/trading_system/trade_recommender.py
+++ b/trading_system/trade_recommender.py
@@ -372,8 +372,8 @@ class TradeRecommender:
estimated_quantity = estimated_position_value / entry_price if entry_price > 0 else 0
# 计算基于保证金的止损止盈
- stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.03)
- take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.05)
+ stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.08)
+ take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.15)
stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price,