This commit is contained in:
薇薇安 2026-01-17 00:58:39 +08:00
parent bd0dd6c336
commit 90f3d019ed
10 changed files with 406 additions and 97 deletions

View File

@ -544,3 +544,145 @@ async def close_position(symbol: str):
logger.error(f"错误类型: {type(e).__name__}") logger.error(f"错误类型: {type(e).__name__}")
logger.error("=" * 60, exc_info=True) logger.error("=" * 60, exc_info=True)
raise HTTPException(status_code=500, detail=error_msg) 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)

View File

@ -119,8 +119,10 @@ class ConfigManager:
'TOP_N_SYMBOLS': self.get('TOP_N_SYMBOLS', 10), 'TOP_N_SYMBOLS': self.get('TOP_N_SYMBOLS', 10),
# 风险控制 # 风险控制
'STOP_LOSS_PERCENT': self.get('STOP_LOSS_PERCENT', 0.03), 'STOP_LOSS_PERCENT': self.get('STOP_LOSS_PERCENT', 0.08), # 默认8%(更宽松,避免被正常波动触发)
'TAKE_PROFIT_PERCENT': self.get('TAKE_PROFIT_PERCENT', 0.05), '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小时主周期 # 市场扫描1小时主周期
'SCAN_INTERVAL': self.get('SCAN_INTERVAL', 3600), # 1小时 'SCAN_INTERVAL': self.get('SCAN_INTERVAL', 3600), # 1小时

View File

@ -141,8 +141,10 @@ INSERT INTO `trading_config` (`config_key`, `config_value`, `config_type`, `cate
('MAX_SCAN_SYMBOLS', '500', 'number', 'scan', '扫描的最大交易对数量0表示扫描所有建议100-500'), ('MAX_SCAN_SYMBOLS', '500', 'number', 'scan', '扫描的最大交易对数量0表示扫描所有建议100-500'),
-- 风险控制 -- 风险控制
('STOP_LOSS_PERCENT', '0.03', 'number', 'risk', '止损3%'), ('STOP_LOSS_PERCENT', '0.08', 'number', 'risk', '止损8%(相对于保证金,更宽松避免被正常波动触发)'),
('TAKE_PROFIT_PERCENT', '0.05', 'number', 'risk', '止盈5%'), ('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小时主周期 -- 市场扫描1小时主周期
('SCAN_INTERVAL', '3600', 'number', 'scan', '扫描间隔1小时'), ('SCAN_INTERVAL', '3600', 'number', 'scan', '扫描间隔1小时'),

View File

@ -15,53 +15,59 @@ const ConfigGuide = () => {
<h2>预设方案说明</h2> <h2>预设方案说明</h2>
<div className="preset-card"> <div className="preset-card">
<h3>方案1保守配置默认</h3> <h3>方案1保守配置</h3>
<p className="preset-desc">适合新手或稳健型交易者风险较低交易频率适中</p> <p className="preset-desc">适合新手或稳健型交易者风险较低止损止盈较宽松避免被正常波动触发</p>
<div className="preset-params"> <div className="preset-params">
<ul> <ul>
<li><strong>扫描间隔</strong>: 36001小时</li> <li><strong>扫描间隔</strong>: 36001小时</li>
<li><strong>最小涨跌幅</strong>: 2.0%</li> <li><strong>最小涨跌幅</strong>: 2.0%</li>
<li><strong>信号强度</strong>: 5/10</li> <li><strong>信号强度</strong>: 5/10</li>
<li><strong>处理交易对</strong>: 10</li> <li><strong>处理交易对</strong>: 10</li>
<li><strong>止损</strong>: 10% of margin最小2%价格变动</li>
<li><strong>止盈</strong>: 20% of margin最小3%价格变动</li>
</ul> </ul>
</div> </div>
<div className="preset-effect"> <div className="preset-effect">
<strong>效果</strong>每小时扫描一次只捕捉2%以上的波动信号质量高胜率较高但交易机会较少 <strong>效果</strong>每小时扫描一次只捕捉2%以上的波动止损止盈宽松避免被正常波动触发适合稳健交易
</div> </div>
</div> </div>
<div className="preset-card"> <div className="preset-card">
<h3>方案2平衡配置推荐</h3> <h3>方案2平衡配置推荐</h3>
<p className="preset-desc">平衡交易频率和信号质量适合大多数交易者</p> <p className="preset-desc">平衡交易频率和信号质量止损止盈适中适合大多数交易者</p>
<div className="preset-params"> <div className="preset-params">
<ul> <ul>
<li><strong>扫描间隔</strong>: 60010分钟</li> <li><strong>扫描间隔</strong>: 60010分钟</li>
<li><strong>最小涨跌幅</strong>: 1.5%</li> <li><strong>最小涨跌幅</strong>: 1.5%</li>
<li><strong>信号强度</strong>: 4/10</li> <li><strong>信号强度</strong>: 4/10</li>
<li><strong>处理交易对</strong>: 12</li> <li><strong>处理交易对</strong>: 12</li>
<li><strong>止损</strong>: 8% of margin最小2%价格变动</li>
<li><strong>止盈</strong>: 15% of margin最小3%价格变动</li>
</ul> </ul>
</div> </div>
<div className="preset-effect"> <div className="preset-effect">
<strong>效果</strong>10分钟扫描一次捕捉1.5%以上的波动交易机会增加信号质量仍然较高 <strong>效果</strong>10分钟扫描一次捕捉1.5%以上的波动止损止盈适中平衡风险与收益推荐使用
</div> </div>
</div> </div>
<div className="preset-card"> <div className="preset-card">
<h3>方案3激进高频配置</h3> <h3>方案3激进高频配置</h3>
<p className="preset-desc">适合晚间波动大时使用交易频率高需要密切监控</p> <p className="preset-desc">适合晚间波动大时使用交易频率高止损止盈较紧快速止盈止损</p>
<div className="preset-params"> <div className="preset-params">
<ul> <ul>
<li><strong>扫描间隔</strong>: 3005分钟</li> <li><strong>扫描间隔</strong>: 3005分钟</li>
<li><strong>最小涨跌幅</strong>: 1.0%</li> <li><strong>最小涨跌幅</strong>: 1.0%</li>
<li><strong>信号强度</strong>: 3/10</li> <li><strong>信号强度</strong>: 3/10</li>
<li><strong>处理交易对</strong>: 20</li> <li><strong>处理交易对</strong>: 18</li>
<li><strong>止损</strong>: 5% of margin最小1.5%价格变动</li>
<li><strong>止盈</strong>: 10% of margin最小2%价格变动</li>
</ul> </ul>
</div> </div>
<div className="preset-effect"> <div className="preset-effect">
<strong>效果</strong>5分钟扫描一次捕捉1%以上的波动交易机会大幅增加但需要监控胜率和手续费 <strong>效果</strong>5分钟扫描一次捕捉1%以上的波动止损止盈较紧快速锁定利润或止损适合高频交易
</div> </div>
<div className="preset-warning"> <div className="preset-warning">
<strong>风险提示</strong>高频交易会增加手续费成本建议在波动大的时段使用并密切监控胜率 <strong>风险提示</strong>高频交易会增加手续费成本止损止盈较紧可能被正常波动触发建议在波动大的时段使用并密切监控胜率
</div> </div>
</div> </div>
</section> </section>

View File

@ -10,42 +10,54 @@ const ConfigPanel = () => {
const [message, setMessage] = useState('') const [message, setMessage] = useState('')
// //
// 使2.02%0.02 // 使8.08%0.08
const presets = { const presets = {
conservative: { conservative: {
name: '保守配置', name: '保守配置',
desc: '适合新手,风险较低,交易频率适中', desc: '适合新手,风险较低,止损止盈较宽松,避免被正常波动触发',
configs: { configs: {
SCAN_INTERVAL: 3600, SCAN_INTERVAL: 3600,
MIN_CHANGE_PERCENT: 2.0, // 2% MIN_CHANGE_PERCENT: 2.0, // 2%
MIN_SIGNAL_STRENGTH: 5, MIN_SIGNAL_STRENGTH: 5,
TOP_N_SYMBOLS: 10, TOP_N_SYMBOLS: 10,
MAX_SCAN_SYMBOLS: 150, 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: { balanced: {
name: '平衡配置', name: '平衡配置',
desc: '推荐使用,平衡频率和质量', desc: '推荐使用,平衡频率和质量,止损止盈适中',
configs: { configs: {
SCAN_INTERVAL: 600, SCAN_INTERVAL: 600,
MIN_CHANGE_PERCENT: 1.5, // 1.5% MIN_CHANGE_PERCENT: 1.5, // 1.5%
MIN_SIGNAL_STRENGTH: 4, MIN_SIGNAL_STRENGTH: 4,
TOP_N_SYMBOLS: 12, TOP_N_SYMBOLS: 12,
MAX_SCAN_SYMBOLS: 250, 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: { aggressive: {
name: '激进高频', name: '激进高频',
desc: '晚间波动大时使用,交易频率高', desc: '晚间波动大时使用,交易频率高,止损止盈较紧',
configs: { configs: {
SCAN_INTERVAL: 300, SCAN_INTERVAL: 300,
MIN_CHANGE_PERCENT: 1.0, // 1% MIN_CHANGE_PERCENT: 1.0, // 1%
MIN_SIGNAL_STRENGTH: 3, MIN_SIGNAL_STRENGTH: 3,
TOP_N_SYMBOLS: 18, TOP_N_SYMBOLS: 18,
MAX_SCAN_SYMBOLS: 350, 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 let currentValue = currentConfig.value
if (key.includes('PERCENT')) { if (key.includes('PERCENT') || key.includes('PCT')) {
currentValue = currentValue * 100 currentValue = currentValue * 100
} }
@ -140,10 +152,37 @@ const ConfigPanel = () => {
try { try {
const configItems = Object.entries(preset.configs).map(([key, value]) => { const configItems = Object.entries(preset.configs).map(([key, value]) => {
const config = configs[key] 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 { return {
key, key,
value: key.includes('PERCENT') ? value / 100 : value, value: (key.includes('PERCENT') || key.includes('PCT')) ? value / 100 : value,
type,
category,
description: `预设方案配置项:${key}`
}
}
return {
key,
value: (key.includes('PERCENT') || key.includes('PCT')) ? value / 100 : value,
type: config.type, type: config.type,
category: config.category, category: config.category,
description: config.description description: config.description
@ -585,8 +624,10 @@ const getConfigDetail = (key) => {
'MIN_POSITION_PERCENT': '单笔最小仓位账户余额的百分比如0.01表示1%。单笔交易允许的最小仓位大小避免交易过小的仓位减少手续费影响。建议1-2%。', 'MIN_POSITION_PERCENT': '单笔最小仓位账户余额的百分比如0.01表示1%。单笔交易允许的最小仓位大小避免交易过小的仓位减少手续费影响。建议1-2%。',
// //
'STOP_LOSS_PERCENT': '止损百分比如0.03表示3%。当亏损达到此百分比时自动平仓止损限制单笔交易的最大亏损。值越小止损更严格单笔损失更小但可能被正常波动触发。值越大允许更大的回撤但单笔损失可能较大。建议保守策略3-5%平衡策略2-3%激进策略2-3%。注意止损应该小于止盈建议盈亏比至少1:1.5。', 'STOP_LOSS_PERCENT': '止损百分比如0.08表示8%相对于保证金。当亏损达到此百分比时自动平仓止损限制单笔交易的最大亏损。值越小止损更严格单笔损失更小但可能被正常波动触发。值越大允许更大的回撤但单笔损失可能较大。建议保守策略10-15%平衡策略8-10%激进策略5-8%。注意止损应该小于止盈建议盈亏比至少1:1.5。系统会结合最小价格变动保护,取更宽松的一个。',
'TAKE_PROFIT_PERCENT': '止盈百分比如0.05表示5%。当盈利达到此百分比时自动平仓止盈锁定利润。值越大目标利润更高但可能错过及时止盈的机会持仓时间更长。值越小能更快锁定利润但可能错过更大的趋势。建议保守策略5-8%平衡策略5-6%激进策略3-5%。注意应该大于止损建议盈亏比至少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倍。注意高杠杆会增加爆仓风险请谨慎使用。', 'LEVERAGE': '交易杠杆倍数。放大资金利用率同时放大收益和风险。杠杆越高相同仓位下需要的保证金越少但风险越大。建议保守策略5-10倍平衡策略10倍激进策略10-15倍。注意高杠杆会增加爆仓风险请谨慎使用。',

View File

@ -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 <div className="loading">加载中...</div> if (loading) return <div className="loading">加载中...</div>
const account = dashboardData?.account const account = dashboardData?.account
@ -86,7 +126,24 @@ const StatsDashboard = () => {
return ( return (
<div className="dashboard"> <div className="dashboard">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h2>仪表板</h2> <h2>仪表板</h2>
<button
onClick={handleSyncPositions}
style={{
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
title="同步币安实际持仓状态与数据库状态"
>
同步持仓状态
</button>
</div>
{message && ( {message && (
<div className={`message ${message.includes('失败') || message.includes('错误') ? 'error' : 'success'}`}> <div className={`message ${message.includes('失败') || message.includes('错误') ? 'error' : 'success'}`}>
@ -205,9 +262,9 @@ const StatsDashboard = () => {
const stopLossConfig = configSource?.STOP_LOSS_PERCENT const stopLossConfig = configSource?.STOP_LOSS_PERCENT
const takeProfitConfig = configSource?.TAKE_PROFIT_PERCENT const takeProfitConfig = configSource?.TAKE_PROFIT_PERCENT
// 0.033% // 0.088%
let stopLossPercentMargin = 0.03 // 3% let stopLossPercentMargin = 0.08 // 8%
let takeProfitPercentMargin = 0.05 // 5% let takeProfitPercentMargin = 0.15 // 15%
if (stopLossConfig) { if (stopLossConfig) {
const configValue = stopLossConfig.value const configValue = stopLossConfig.value

View File

@ -121,6 +121,21 @@ export const api = {
return response.json(); 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 = {}) => { getRecommendations: async (params = {}) => {
// 默认使用实时推荐 // 默认使用实时推荐

View File

@ -168,10 +168,10 @@ def _get_trading_config():
'MIN_CHANGE_PERCENT': 0.5, # 降低到0.5%以获取更多推荐(推荐系统可以更宽松) 'MIN_CHANGE_PERCENT': 0.5, # 降低到0.5%以获取更多推荐(推荐系统可以更宽松)
'TOP_N_SYMBOLS': 50, # 每次扫描后处理的交易对数量增加到50以获取更多推荐 'TOP_N_SYMBOLS': 50, # 每次扫描后处理的交易对数量增加到50以获取更多推荐
'MAX_SCAN_SYMBOLS': 500, # 扫描的最大交易对数量0表示扫描所有 'MAX_SCAN_SYMBOLS': 500, # 扫描的最大交易对数量0表示扫描所有
'STOP_LOSS_PERCENT': 0.03, # 止损百分比相对于保证金默认3% 'STOP_LOSS_PERCENT': 0.08, # 止损百分比相对于保证金默认8%(更宽松,避免被正常波动触发)
'TAKE_PROFIT_PERCENT': 0.05, # 止盈百分比(相对于保证金),默认5% 'TAKE_PROFIT_PERCENT': 0.15, # 止盈百分比(相对于保证金),默认15%(更宽松,给趋势更多空间)
'MIN_STOP_LOSS_PRICE_PCT': 0.01, # 最小止损价格变动百分比如0.01表示1%防止止损过紧默认1% 'MIN_STOP_LOSS_PRICE_PCT': 0.02, # 最小止损价格变动百分比如0.02表示2%防止止损过紧默认2%
'MIN_TAKE_PROFIT_PRICE_PCT': 0.015, # 最小止盈价格变动百分比如0.015表示1.5%防止止盈过紧默认1.5% 'MIN_TAKE_PROFIT_PRICE_PCT': 0.03, # 最小止盈价格变动百分比如0.03表示3%防止止盈过紧默认3%
'SCAN_INTERVAL': 3600, 'SCAN_INTERVAL': 3600,
'KLINE_INTERVAL': '1h', 'KLINE_INTERVAL': '1h',
'PRIMARY_INTERVAL': '1h', 'PRIMARY_INTERVAL': '1h',

View File

@ -163,9 +163,12 @@ class PositionManager:
atr=atr atr=atr
) )
# 计算止盈(基于保证金,为止损的倍数) # 计算止盈(基于保证金)
# 如果止损是保证金的3%止盈可以是保证金的7.5%2.5倍) # 优先使用配置的止盈百分比如果没有配置则使用止损的2倍
take_profit_pct_margin = stop_loss_pct_margin * 2.5 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( take_profit_price = self.risk_manager.get_take_profit_price(
entry_price, side, quantity, leverage, entry_price, side, quantity, leverage,
take_profit_pct=take_profit_pct_margin take_profit_pct=take_profit_pct_margin
@ -185,24 +188,37 @@ class PositionManager:
if entry_order_id: if entry_order_id:
logger.info(f"{symbol} [开仓] 币安订单号: {entry_order_id}") logger.info(f"{symbol} [开仓] 币安订单号: {entry_order_id}")
# 等待订单成交,然后从币安获取实际成交价格 # 等待订单成交,检查订单状态并获取实际成交价格
# 只有在订单真正成交FILLED后才保存到数据库
actual_entry_price = None actual_entry_price = None
order_status = None
filled_quantity = 0
max_retries = 5 # 最多重试5次每次等待1秒
retry_count = 0
while retry_count < max_retries:
try: try:
# 等待一小段时间让订单成交 # 等待一小段时间让订单成交
await asyncio.sleep(1) await asyncio.sleep(1)
# 从币安获取订单详情,获取实际成交价格 # 从币安获取订单详情,检查订单状态
try: try:
order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=entry_order_id) order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=entry_order_id)
if order_info: if order_info:
# 优先使用平均成交价格avgPrice如果没有则使用价格字段 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)) actual_entry_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0))
if actual_entry_price > 0: filled_quantity = float(order_info.get('executedQty', 0))
logger.info(f"{symbol} [开仓] 从币安订单获取实际成交价格: {actual_entry_price:.4f} USDT")
else: if actual_entry_price > 0 and filled_quantity > 0:
# 如果订单还没有完全成交,尝试从成交记录获取 logger.info(f"{symbol} [开仓] ✓ 订单已成交,成交价格: {actual_entry_price:.4f} USDT, 成交数量: {filled_quantity:.4f}")
if order_info.get('status') == 'FILLED' and order_info.get('fills'): break
# 计算加权平均成交价格 elif order_info.get('fills'):
# 从成交记录计算加权平均成交价格和总成交数量
total_qty = 0 total_qty = 0
total_value = 0 total_value = 0
for fill in order_info.get('fills', []): for fill in order_info.get('fills', []):
@ -212,34 +228,57 @@ class PositionManager:
total_value += qty * price total_value += qty * price
if total_qty > 0: if total_qty > 0:
actual_entry_price = total_value / total_qty actual_entry_price = total_value / total_qty
logger.info(f"{symbol} [开仓] 从成交记录计算平均成交价格: {actual_entry_price:.4f} USDT") filled_quantity = total_qty
except Exception as order_error: logger.info(f"{symbol} [开仓] ✓ 订单已成交,从成交记录计算平均成交价格: {actual_entry_price:.4f} USDT, 成交数量: {filled_quantity:.4f}")
logger.warning(f"{symbol} [开仓] 获取订单详情失败: {order_error},使用备用方法") break
elif order_status == 'PARTIALLY_FILLED':
# 如果无法从订单获取价格,使用当前价格作为备用 # 部分成交,继续等待
if not actual_entry_price or actual_entry_price <= 0: filled_quantity = float(order_info.get('executedQty', 0))
ticker = await self.client.get_ticker_24h(symbol) logger.info(f"{symbol} [开仓] ⏳ 订单部分成交 ({filled_quantity:.4f}/{quantity:.4f}),继续等待...")
if ticker: retry_count += 1
actual_entry_price = float(ticker['price']) continue
logger.warning(f"{symbol} [开仓] 使用当前价格作为入场价格: {actual_entry_price:.4f} USDT") 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: else:
actual_entry_price = float(order.get('avgPrice', 0)) or float(order.get('price', 0)) logger.warning(f"{symbol} [开仓] ⚠️ 未知订单状态: {order_status},继续等待...")
if actual_entry_price <= 0: retry_count += 1
logger.error(f"{symbol} [开仓] 无法获取入场价格,使用订单价格字段") continue
actual_entry_price = float(order.get('price', 0)) or entry_price except Exception as order_error:
logger.warning(f"{symbol} [开仓] 获取订单详情失败: {order_error},重试中...")
retry_count += 1
continue
except Exception as price_error: except Exception as price_error:
logger.warning(f"{symbol} [开仓] 获取成交价格时出错: {price_error},使用当前价格") logger.warning(f"{symbol} [开仓] 检查订单状态时出错: {price_error},重试中...")
ticker = await self.client.get_ticker_24h(symbol) retry_count += 1
actual_entry_price = float(ticker['price']) if ticker else entry_price continue
# 使用实际成交价格(如果获取成功) # 检查订单是否最终成交
if actual_entry_price and actual_entry_price > 0: 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 original_entry_price = entry_price
entry_price = actual_entry_price entry_price = actual_entry_price
logger.info(f"{symbol} [开仓] 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f})") quantity = filled_quantity # 使用实际成交数量
logger.info(f"{symbol} [开仓] ✓ 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f}), 成交数量: {quantity:.4f}")
# 记录到数据库(使用实际成交价格) # 记录到数据库(只有在订单真正成交后才保存
trade_id = None trade_id = None
if DB_AVAILABLE and Trade: if DB_AVAILABLE and Trade:
try: try:
@ -247,18 +286,19 @@ class PositionManager:
trade_id = Trade.create( trade_id = Trade.create(
symbol=symbol, symbol=symbol,
side=side, side=side,
quantity=quantity, quantity=quantity, # 使用实际成交数量
entry_price=entry_price, # 使用实际成交价格 entry_price=entry_price, # 使用实际成交价格
leverage=leverage, leverage=leverage,
entry_reason=entry_reason, entry_reason=entry_reason,
entry_order_id=entry_order_id # 保存币安订单号 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: except Exception as e:
logger.error(f"❌ 保存交易记录到数据库失败: {e}") logger.error(f"❌ 保存交易记录到数据库失败: {e}")
logger.error(f" 错误类型: {type(e).__name__}") logger.error(f" 错误类型: {type(e).__name__}")
import traceback import traceback
logger.error(f" 错误详情:\n{traceback.format_exc()}") logger.error(f" 错误详情:\n{traceback.format_exc()}")
return None
elif not DB_AVAILABLE: elif not DB_AVAILABLE:
logger.debug(f"数据库不可用,跳过保存 {symbol} 交易记录") logger.debug(f"数据库不可用,跳过保存 {symbol} 交易记录")
elif not Trade: elif not Trade:
@ -1059,10 +1099,11 @@ class PositionManager:
# 计算止损止盈(基于保证金) # 计算止损止盈(基于保证金)
leverage = binance_position.get('leverage', 10) leverage = binance_position.get('leverage', 10)
stop_loss_pct_margin = self.risk_manager.config.get('STOP_LOSS_PERCENT', 0.03) 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.05) take_profit_pct_margin = self.risk_manager.config.get('TAKE_PROFIT_PERCENT', 0.15)
# 止盈为止损的2.5倍 # 如果配置中没有设置止盈则使用止损的2倍作为默认
take_profit_pct_margin = stop_loss_pct_margin * 2.5 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( stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage, entry_price, side, quantity, leverage,
@ -1153,8 +1194,11 @@ class PositionManager:
# 计算止损止盈(基于保证金) # 计算止损止盈(基于保证金)
leverage = position.get('leverage', 10) leverage = position.get('leverage', 10)
stop_loss_pct_margin = self.risk_manager.config.get('STOP_LOSS_PERCENT', 0.03) stop_loss_pct_margin = self.risk_manager.config.get('STOP_LOSS_PERCENT', 0.08)
take_profit_pct_margin = stop_loss_pct_margin * 2.5 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( stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage, entry_price, side, quantity, leverage,

View File

@ -372,8 +372,8 @@ class TradeRecommender:
estimated_quantity = estimated_position_value / entry_price if entry_price > 0 else 0 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) 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.05) take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.15)
stop_loss_price = self.risk_manager.get_stop_loss_price( stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, entry_price,