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>: 3600秒(1小时)</li> <li><strong>扫描间隔</strong>: 3600秒(1小时)</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>: 600秒(10分钟)</li> <li><strong>扫描间隔</strong>: 600秒(10分钟)</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>: 300秒(5分钟)</li> <li><strong>扫描间隔</strong>: 300秒(5分钟)</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.0表示2%),在应用时会转换为小数(0.02) // 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(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 {
key,
value: (key.includes('PERCENT') || key.includes('PCT')) ? value / 100 : value,
type,
category,
description: `预设方案配置项:${key}`
}
}
return { return {
key, key,
value: key.includes('PERCENT') ? value / 100 : value, 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">
<h2>仪表板</h2> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<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.03表示3%),相对于保证金 // 配置值是小数形式(0.08表示8%),相对于保证金
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,61 +188,97 @@ 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
try: order_status = None
# 等待一小段时间让订单成交 filled_quantity = 0
await asyncio.sleep(1) max_retries = 5 # 最多重试5次,每次等待1秒
retry_count = 0
# 从币安获取订单详情,获取实际成交价格 while retry_count < max_retries:
try: try:
order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=entry_order_id) # 等待一小段时间让订单成交
if order_info: await asyncio.sleep(1)
# 优先使用平均成交价格(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: try:
ticker = await self.client.get_ticker_24h(symbol) order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=entry_order_id)
if ticker: if order_info:
actual_entry_price = float(ticker['price']) order_status = order_info.get('status')
logger.warning(f"{symbol} [开仓] 使用当前价格作为入场价格: {actual_entry_price:.4f} USDT") logger.info(f"{symbol} [开仓] 订单状态: {order_status} (重试 {retry_count + 1}/{max_retries})")
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
# 使用实际成交价格(如果获取成功) # 检查订单是否已成交
if actual_entry_price and actual_entry_price > 0: if order_status == 'FILLED':
# 记录下单时的价格(用于对比) # 订单已完全成交,获取实际成交价格和数量
original_entry_price = entry_price actual_entry_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0))
entry_price = actual_entry_price filled_quantity = float(order_info.get('executedQty', 0))
logger.info(f"{symbol} [开仓] 使用实际成交价格: {entry_price:.4f} USDT (下单时价格: {original_entry_price:.4f})")
# 记录到数据库(使用实际成交价格) 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 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 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,