a
This commit is contained in:
parent
f81e4fc04e
commit
6c04202a55
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
<h3>快速切换方案</h3>
|
<div className="preset-header">
|
||||||
|
<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>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -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,14 +433,21 @@ 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:
|
||||||
logger.error(f"下单失败 {symbol} {side}: {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}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def cancel_order(self, symbol: str, order_id: int) -> bool:
|
async def cancel_order(self, symbol: str, order_id: int) -> bool:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user