a
This commit is contained in:
parent
bd0dd6c336
commit
90f3d019ed
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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小时
|
||||
|
|
|
|||
|
|
@ -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小时(秒)'),
|
||||
|
|
|
|||
|
|
@ -15,53 +15,59 @@ const ConfigGuide = () => {
|
|||
<h2>一、预设方案说明</h2>
|
||||
|
||||
<div className="preset-card">
|
||||
<h3>方案1:保守配置(默认)</h3>
|
||||
<p className="preset-desc">适合新手或稳健型交易者,风险较低,交易频率适中</p>
|
||||
<h3>方案1:保守配置</h3>
|
||||
<p className="preset-desc">适合新手或稳健型交易者,风险较低,止损止盈较宽松,避免被正常波动触发</p>
|
||||
<div className="preset-params">
|
||||
<ul>
|
||||
<li><strong>扫描间隔</strong>: 3600秒(1小时)</li>
|
||||
<li><strong>最小涨跌幅</strong>: 2.0%</li>
|
||||
<li><strong>信号强度</strong>: 5/10</li>
|
||||
<li><strong>处理交易对</strong>: 10个</li>
|
||||
<li><strong>止损</strong>: 10% of margin(最小2%价格变动)</li>
|
||||
<li><strong>止盈</strong>: 20% of margin(最小3%价格变动)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="preset-effect">
|
||||
<strong>效果:</strong>每小时扫描一次,只捕捉2%以上的波动,信号质量高,胜率较高但交易机会较少
|
||||
<strong>效果:</strong>每小时扫描一次,只捕捉2%以上的波动,止损止盈宽松,避免被正常波动触发,适合稳健交易
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="preset-card">
|
||||
<h3>方案2:平衡配置(推荐)</h3>
|
||||
<p className="preset-desc">平衡交易频率和信号质量,适合大多数交易者</p>
|
||||
<p className="preset-desc">平衡交易频率和信号质量,止损止盈适中,适合大多数交易者</p>
|
||||
<div className="preset-params">
|
||||
<ul>
|
||||
<li><strong>扫描间隔</strong>: 600秒(10分钟)</li>
|
||||
<li><strong>最小涨跌幅</strong>: 1.5%</li>
|
||||
<li><strong>信号强度</strong>: 4/10</li>
|
||||
<li><strong>处理交易对</strong>: 12个</li>
|
||||
<li><strong>止损</strong>: 8% of margin(最小2%价格变动)</li>
|
||||
<li><strong>止盈</strong>: 15% of margin(最小3%价格变动)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="preset-effect">
|
||||
<strong>效果:</strong>10分钟扫描一次,捕捉1.5%以上的波动,交易机会增加,信号质量仍然较高
|
||||
<strong>效果:</strong>10分钟扫描一次,捕捉1.5%以上的波动,止损止盈适中,平衡风险与收益,推荐使用
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="preset-card">
|
||||
<h3>方案3:激进高频配置</h3>
|
||||
<p className="preset-desc">适合晚间波动大时使用,交易频率高,需要密切监控</p>
|
||||
<p className="preset-desc">适合晚间波动大时使用,交易频率高,止损止盈较紧,快速止盈止损</p>
|
||||
<div className="preset-params">
|
||||
<ul>
|
||||
<li><strong>扫描间隔</strong>: 300秒(5分钟)</li>
|
||||
<li><strong>最小涨跌幅</strong>: 1.0%</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>
|
||||
</div>
|
||||
<div className="preset-effect">
|
||||
<strong>效果:</strong>5分钟扫描一次,捕捉1%以上的波动,交易机会大幅增加,但需要监控胜率和手续费
|
||||
<strong>效果:</strong>5分钟扫描一次,捕捉1%以上的波动,止损止盈较紧,快速锁定利润或止损,适合高频交易
|
||||
</div>
|
||||
<div className="preset-warning">
|
||||
⚠️ <strong>风险提示:</strong>高频交易会增加手续费成本,建议在波动大的时段使用,并密切监控胜率
|
||||
⚠️ <strong>风险提示:</strong>高频交易会增加手续费成本,止损止盈较紧可能被正常波动触发,建议在波动大的时段使用,并密切监控胜率
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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倍。注意:高杠杆会增加爆仓风险,请谨慎使用。',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
const account = dashboardData?.account
|
||||
|
|
@ -86,7 +126,24 @@ const StatsDashboard = () => {
|
|||
|
||||
return (
|
||||
<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 && (
|
||||
<div className={`message ${message.includes('失败') || message.includes('错误') ? 'error' : 'success'}`}>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {}) => {
|
||||
// 默认使用实时推荐
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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},使用备用方法")
|
||||
# 等待一小段时间让订单成交
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 如果无法从订单获取价格,使用当前价格作为备用
|
||||
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
|
||||
# 从币安获取订单详情,检查订单状态
|
||||
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 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':
|
||||
# 订单已完全成交,获取实际成交价格和数量
|
||||
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 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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user