This commit is contained in:
薇薇安 2026-01-13 23:44:42 +08:00
parent f81e4fc04e
commit 6c04202a55
3 changed files with 232 additions and 8 deletions

View File

@ -38,12 +38,53 @@
margin-top: 1.5rem; margin-top: 1.5rem;
} }
.preset-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 1rem;
}
.preset-section h3 { .preset-section h3 {
margin: 0 0 1rem 0; margin: 0;
color: #34495e; color: #34495e;
font-size: 1rem; font-size: 1rem;
} }
.current-preset-status {
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-label {
font-size: 0.9rem;
color: #666;
font-weight: 500;
}
.status-badge {
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
transition: all 0.3s;
}
.status-badge.preset {
background: #4CAF50;
color: white;
box-shadow: 0 2px 4px rgba(76, 175, 80, 0.3);
}
.status-badge.custom {
background: #FF9800;
color: white;
box-shadow: 0 2px 4px rgba(255, 152, 0, 0.3);
}
.preset-buttons { .preset-buttons {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
@ -69,6 +110,19 @@
box-shadow: 0 4px 8px rgba(0,0,0,0.1); box-shadow: 0 4px 8px rgba(0,0,0,0.1);
} }
.preset-btn.active {
border-color: #4CAF50;
background: #e8f5e9;
box-shadow: 0 2px 6px rgba(76, 175, 80, 0.2);
}
.preset-btn.active:hover:not(:disabled) {
border-color: #4CAF50;
background: #c8e6c9;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(76, 175, 80, 0.3);
}
.preset-btn:disabled { .preset-btn:disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
@ -78,6 +132,15 @@
font-weight: bold; font-weight: bold;
color: #2c3e50; color: #2c3e50;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.active-indicator {
color: #4CAF50;
font-size: 1.2rem;
font-weight: bold;
} }
.preset-desc { .preset-desc {

View File

@ -88,6 +88,45 @@ const ConfigPanel = () => {
} }
} }
//
const detectCurrentPreset = () => {
if (!configs || Object.keys(configs).length === 0) return null
for (const [presetKey, preset] of Object.entries(presets)) {
let match = true
for (const [key, expectedValue] of Object.entries(preset.configs)) {
const currentConfig = configs[key]
if (!currentConfig) {
match = false
break
}
//
let currentValue = currentConfig.value
if (key.includes('PERCENT')) {
currentValue = currentValue * 100
}
//
if (typeof expectedValue === 'number' && typeof currentValue === 'number') {
if (Math.abs(currentValue - expectedValue) > 0.01) {
match = false
break
}
} else if (currentValue !== expectedValue) {
match = false
break
}
}
if (match) {
return presetKey
}
}
return null
}
const applyPreset = async (presetKey) => { const applyPreset = async (presetKey) => {
const preset = presets[presetKey] const preset = presets[presetKey]
if (!preset) return if (!preset) return
@ -122,6 +161,8 @@ const ConfigPanel = () => {
} }
} }
const currentPreset = detectCurrentPreset()
if (loading) return <div className="loading">加载中...</div> if (loading) return <div className="loading">加载中...</div>
const configCategories = { const configCategories = {
@ -145,17 +186,28 @@ const ConfigPanel = () => {
{/* 预设方案快速切换 */} {/* 预设方案快速切换 */}
<div className="preset-section"> <div className="preset-section">
<div className="preset-header">
<h3>快速切换方案</h3> <h3>快速切换方案</h3>
<div className="current-preset-status">
<span className="status-label">当前方案</span>
<span className={`status-badge ${currentPreset ? 'preset' : 'custom'}`}>
{currentPreset ? presets[currentPreset].name : '自定义'}
</span>
</div>
</div>
<div className="preset-buttons"> <div className="preset-buttons">
{Object.entries(presets).map(([key, preset]) => ( {Object.entries(presets).map(([key, preset]) => (
<button <button
key={key} key={key}
className="preset-btn" className={`preset-btn ${currentPreset === key ? 'active' : ''}`}
onClick={() => applyPreset(key)} onClick={() => applyPreset(key)}
disabled={saving} disabled={saving}
title={preset.desc} title={preset.desc}
> >
<div className="preset-name">{preset.name}</div> <div className="preset-name">
{preset.name}
{currentPreset === key && <span className="active-indicator"></span>}
</div>
<div className="preset-desc">{preset.desc}</div> <div className="preset-desc">{preset.desc}</div>
</button> </button>
))} ))}

View File

@ -38,6 +38,7 @@ class BinanceClient:
self.socket_manager: Optional[BinanceSocketManager] = None self.socket_manager: Optional[BinanceSocketManager] = None
self.unicorn_manager: Optional[UnicornWebSocketManager] = None self.unicorn_manager: Optional[UnicornWebSocketManager] = None
self.use_unicorn = config.TRADING_CONFIG.get('USE_UNICORN_WEBSOCKET', True) self.use_unicorn = config.TRADING_CONFIG.get('USE_UNICORN_WEBSOCKET', True)
self._symbol_info_cache: Dict[str, Dict] = {} # 缓存交易对信息
async def connect(self, timeout: int = None, retries: int = None): async def connect(self, timeout: int = None, retries: int = None):
""" """
@ -294,6 +295,97 @@ class BinanceClient:
logger.error(f"获取持仓信息失败: {e}") logger.error(f"获取持仓信息失败: {e}")
return [] return []
async def get_symbol_info(self, symbol: str) -> Optional[Dict]:
"""
获取交易对的精度和限制信息
Args:
symbol: 交易对
Returns:
交易对信息字典包含 quantityPrecision, minQty, stepSize
"""
# 先检查缓存
if symbol in self._symbol_info_cache:
return self._symbol_info_cache[symbol]
try:
exchange_info = await self.client.futures_exchange_info()
for s in exchange_info['symbols']:
if s['symbol'] == symbol:
# 提取数量精度信息
quantity_precision = s.get('quantityPrecision', 8)
# 从filters中提取minQty和stepSize
min_qty = None
step_size = None
for f in s.get('filters', []):
if f['filterType'] == 'LOT_SIZE':
min_qty = float(f.get('minQty', 0))
step_size = float(f.get('stepSize', 0))
break
info = {
'quantityPrecision': quantity_precision,
'minQty': min_qty or 0,
'stepSize': step_size or 0
}
# 缓存信息
self._symbol_info_cache[symbol] = info
logger.debug(f"获取 {symbol} 精度信息: {info}")
return info
logger.warning(f"未找到交易对 {symbol} 的信息")
return None
except Exception as e:
logger.error(f"获取 {symbol} 交易对信息失败: {e}")
return None
def _adjust_quantity_precision(self, quantity: float, symbol_info: Dict) -> float:
"""
调整数量精度使其符合币安要求
Args:
quantity: 原始数量
symbol_info: 交易对信息
Returns:
调整后的数量
"""
if not symbol_info:
# 如果没有交易对信息使用默认精度3位小数
return round(quantity, 3)
quantity_precision = symbol_info.get('quantityPrecision', 8)
step_size = symbol_info.get('stepSize', 0)
min_qty = symbol_info.get('minQty', 0)
# 如果有stepSize按照stepSize调整
if step_size > 0:
# 向下取整到stepSize的倍数使用浮点数除法
adjusted = float(int(quantity / step_size)) * step_size
else:
# 否则按照精度调整
adjusted = round(quantity, quantity_precision)
# 确保不小于最小数量
if min_qty > 0 and adjusted < min_qty:
# 如果小于最小数量,尝试向上取整到最小数量
if step_size > 0:
adjusted = min_qty
else:
adjusted = round(min_qty, quantity_precision)
logger.warning(f"数量 {quantity} 小于最小数量 {min_qty},调整为 {adjusted}")
# 最终精度调整
adjusted = round(adjusted, quantity_precision)
if adjusted != quantity:
logger.info(f"数量精度调整: {quantity} -> {adjusted} (精度: {quantity_precision}, stepSize: {step_size}, minQty: {min_qty})")
return adjusted
async def place_order( async def place_order(
self, self,
symbol: str, symbol: str,
@ -316,12 +408,22 @@ class BinanceClient:
订单信息 订单信息
""" """
try: try:
# 获取交易对精度信息并调整数量
symbol_info = await self.get_symbol_info(symbol)
adjusted_quantity = self._adjust_quantity_precision(quantity, symbol_info)
if adjusted_quantity <= 0:
logger.error(f"调整后的数量无效: {adjusted_quantity} (原始: {quantity})")
return None
logger.info(f"下单: {symbol} {side} {adjusted_quantity} (原始: {quantity}) @ {order_type}")
if order_type == 'MARKET': if order_type == 'MARKET':
order = await self.client.futures_create_order( order = await self.client.futures_create_order(
symbol=symbol, symbol=symbol,
side=side, side=side,
type='MARKET', type='MARKET',
quantity=quantity quantity=adjusted_quantity
) )
else: else:
if price is None: if price is None:
@ -331,13 +433,20 @@ class BinanceClient:
side=side, side=side,
type='LIMIT', type='LIMIT',
timeInForce='GTC', timeInForce='GTC',
quantity=quantity, quantity=adjusted_quantity,
price=price price=price
) )
logger.info(f"下单成功: {symbol} {side} {quantity} @ {order_type}") logger.info(f"下单成功: {symbol} {side} {adjusted_quantity} @ {order_type}")
return order return order
except BinanceAPIException as e: except BinanceAPIException as e:
error_code = e.code if hasattr(e, 'code') else None
if error_code == -1111:
logger.error(f"下单失败 {symbol} {side}: 精度错误 - {e}")
logger.error(f" 原始数量: {quantity}")
if symbol_info:
logger.error(f" 交易对精度: {symbol_info}")
else:
logger.error(f"下单失败 {symbol} {side}: {e}") logger.error(f"下单失败 {symbol} {side}: {e}")
return None return None