a
This commit is contained in:
parent
14773e530a
commit
5d7166d404
|
|
@ -1041,6 +1041,204 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
|
||||||
raise HTTPException(status_code=500, detail=error_msg)
|
raise HTTPException(status_code=500, detail=error_msg)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/positions/{symbol}/open")
|
||||||
|
async def open_position_from_recommendation(
|
||||||
|
symbol: str,
|
||||||
|
entry_price: float = Query(..., description="入场价格"),
|
||||||
|
stop_loss_price: float = Query(..., description="止损价格"),
|
||||||
|
direction: str = Query(..., description="交易方向: BUY 或 SELL"),
|
||||||
|
notional_usdt: float = Query(..., description="下单名义价值(USDT)"),
|
||||||
|
leverage: int = Query(10, description="杠杆倍数"),
|
||||||
|
account_id: int = Depends(get_account_id)
|
||||||
|
):
|
||||||
|
"""根据推荐信息手动开仓"""
|
||||||
|
try:
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info(f"收到手动开仓请求: {symbol}")
|
||||||
|
logger.info(f" 入场价: {entry_price}, 止损价: {stop_loss_price}")
|
||||||
|
logger.info(f" 方向: {direction}, 名义价值: {notional_usdt} USDT, 杠杆: {leverage}x")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
if direction not in ('BUY', 'SELL'):
|
||||||
|
raise HTTPException(status_code=400, detail="交易方向必须是 BUY 或 SELL")
|
||||||
|
|
||||||
|
if notional_usdt <= 0:
|
||||||
|
raise HTTPException(status_code=400, detail="下单名义价值必须大于0")
|
||||||
|
|
||||||
|
if entry_price <= 0 or stop_loss_price <= 0:
|
||||||
|
raise HTTPException(status_code=400, detail="入场价和止损价必须大于0")
|
||||||
|
|
||||||
|
api_key, api_secret, use_testnet = Account.get_credentials(account_id)
|
||||||
|
|
||||||
|
if not api_key or not api_secret:
|
||||||
|
error_msg = f"API密钥未配置(account_id={account_id})"
|
||||||
|
logger.warning(f"[account_id={account_id}] {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
|
||||||
|
|
||||||
|
# 创建客户端
|
||||||
|
logger.info(f"创建BinanceClient (testnet={use_testnet})...")
|
||||||
|
client = BinanceClient(
|
||||||
|
api_key=api_key,
|
||||||
|
api_secret=api_secret,
|
||||||
|
testnet=use_testnet
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("连接币安API...")
|
||||||
|
await client.connect()
|
||||||
|
logger.info("✓ 币安API连接成功")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 设置杠杆
|
||||||
|
await client.set_leverage(symbol, leverage)
|
||||||
|
logger.info(f"✓ 已设置杠杆: {leverage}x")
|
||||||
|
|
||||||
|
# 获取交易对信息
|
||||||
|
symbol_info = await client.get_symbol_info(symbol)
|
||||||
|
if not symbol_info:
|
||||||
|
raise HTTPException(status_code=400, detail=f"无法获取 {symbol} 的交易对信息")
|
||||||
|
|
||||||
|
# 计算下单数量:数量 = 名义价值 / 入场价
|
||||||
|
quantity = notional_usdt / entry_price
|
||||||
|
logger.info(f"计算下单数量: {quantity:.8f} (名义价值: {notional_usdt} USDT / 入场价: {entry_price})")
|
||||||
|
|
||||||
|
# 调整数量精度
|
||||||
|
adjusted_quantity = client._adjust_quantity_precision(quantity, symbol_info)
|
||||||
|
if adjusted_quantity <= 0:
|
||||||
|
raise HTTPException(status_code=400, detail=f"调整后的数量无效: {adjusted_quantity}")
|
||||||
|
|
||||||
|
logger.info(f"调整后的数量: {adjusted_quantity:.8f}")
|
||||||
|
|
||||||
|
# 检查最小名义价值
|
||||||
|
min_notional = symbol_info.get('minNotional', 5.0)
|
||||||
|
actual_notional = adjusted_quantity * entry_price
|
||||||
|
if actual_notional < min_notional:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"订单名义价值不足: {actual_notional:.2f} USDT < 最小要求: {min_notional:.2f} USDT"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 下 limit 订单
|
||||||
|
logger.info(f"开始下 limit 订单: {symbol} {direction} {adjusted_quantity} @ {entry_price}")
|
||||||
|
order = await client.place_order(
|
||||||
|
symbol=symbol,
|
||||||
|
side=direction,
|
||||||
|
quantity=adjusted_quantity,
|
||||||
|
order_type='LIMIT',
|
||||||
|
price=entry_price,
|
||||||
|
reduce_only=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if not order:
|
||||||
|
raise HTTPException(status_code=500, detail="下单失败:币安API返回None")
|
||||||
|
|
||||||
|
order_id = order.get('orderId')
|
||||||
|
logger.info(f"✓ 订单已提交: orderId={order_id}")
|
||||||
|
|
||||||
|
# 等待订单成交(最多等待30秒)
|
||||||
|
import asyncio
|
||||||
|
filled_order = None
|
||||||
|
for i in range(30):
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
try:
|
||||||
|
order_status = await client.client.futures_get_order(symbol=symbol, orderId=order_id)
|
||||||
|
if order_status.get('status') == 'FILLED':
|
||||||
|
filled_order = order_status
|
||||||
|
logger.info(f"✓ 订单已成交: orderId={order_id}")
|
||||||
|
break
|
||||||
|
elif order_status.get('status') in ('CANCELED', 'EXPIRED', 'REJECTED'):
|
||||||
|
raise HTTPException(status_code=400, detail=f"订单未成交,状态: {order_status.get('status')}")
|
||||||
|
except Exception as e:
|
||||||
|
if i == 29: # 最后一次尝试
|
||||||
|
logger.warning(f"订单状态查询失败或未成交: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not filled_order:
|
||||||
|
logger.warning(f"订单 {order_id} 在30秒内未成交,但订单已提交")
|
||||||
|
return {
|
||||||
|
"message": f"{symbol} 订单已提交但未成交(请稍后检查)",
|
||||||
|
"symbol": symbol,
|
||||||
|
"order_id": order_id,
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 订单已成交,保存到数据库
|
||||||
|
avg_price = float(filled_order.get('avgPrice', entry_price))
|
||||||
|
executed_qty = float(filled_order.get('executedQty', adjusted_quantity))
|
||||||
|
|
||||||
|
# 计算实际使用的名义价值和保证金
|
||||||
|
actual_notional = executed_qty * avg_price
|
||||||
|
actual_margin = actual_notional / leverage
|
||||||
|
|
||||||
|
# 保存交易记录
|
||||||
|
trade_id = Trade.create(
|
||||||
|
account_id=account_id,
|
||||||
|
symbol=symbol,
|
||||||
|
side=direction,
|
||||||
|
quantity=executed_qty,
|
||||||
|
entry_price=avg_price,
|
||||||
|
leverage=leverage,
|
||||||
|
entry_order_id=order_id,
|
||||||
|
entry_reason='manual_from_recommendation',
|
||||||
|
notional_usdt=actual_notional,
|
||||||
|
margin_usdt=actual_margin,
|
||||||
|
stop_loss_price=stop_loss_price,
|
||||||
|
# 如果有推荐中的止盈价,也可以传入,这里先不传
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"✓ 交易记录已保存: trade_id={trade_id}")
|
||||||
|
|
||||||
|
# 尝试挂止损/止盈保护单(如果系统支持)
|
||||||
|
try:
|
||||||
|
# 这里可以调用 _ensure_exchange_sltp_for_symbol 来挂保护单
|
||||||
|
# 但需要先获取持仓信息来确定方向
|
||||||
|
positions = await client.get_open_positions()
|
||||||
|
position = next((p for p in positions if p['symbol'] == symbol), None)
|
||||||
|
if position:
|
||||||
|
# 可以在这里挂止损单,但需要知道 take_profit_price
|
||||||
|
# 暂时只记录止损价到数据库,由系统自动监控
|
||||||
|
logger.info(f"止损价已记录到数据库: {stop_loss_price}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"挂保护单失败(不影响开仓): {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": f"{symbol} 开仓成功",
|
||||||
|
"symbol": symbol,
|
||||||
|
"order_id": order_id,
|
||||||
|
"trade_id": trade_id,
|
||||||
|
"quantity": executed_qty,
|
||||||
|
"entry_price": avg_price,
|
||||||
|
"notional_usdt": actual_notional,
|
||||||
|
"margin_usdt": actual_margin,
|
||||||
|
"status": "filled"
|
||||||
|
}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
logger.info("断开币安API连接...")
|
||||||
|
await client.disconnect()
|
||||||
|
logger.info("✓ 已断开连接")
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"开仓失败: {str(e)}"
|
||||||
|
logger.error("=" * 60)
|
||||||
|
logger.error(f"开仓操作异常: {error_msg}")
|
||||||
|
logger.error(f"错误类型: {type(e).__name__}")
|
||||||
|
logger.error("=" * 60, exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=error_msg)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/positions/sync")
|
@router.post("/positions/sync")
|
||||||
async def sync_positions(account_id: int = Depends(get_account_id)):
|
async def sync_positions(account_id: int = Depends(get_account_id)):
|
||||||
"""同步币安实际持仓状态与数据库状态"""
|
"""同步币安实际持仓状态与数据库状态"""
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,54 @@ function Recommendations() {
|
||||||
const [generating, setGenerating] = useState(false)
|
const [generating, setGenerating] = useState(false)
|
||||||
const [showDetails, setShowDetails] = useState({})
|
const [showDetails, setShowDetails] = useState({})
|
||||||
const [bookmarking, setBookmarking] = useState({}) // 记录正在标记的推荐ID
|
const [bookmarking, setBookmarking] = useState({}) // 记录正在标记的推荐ID
|
||||||
|
const [ordering, setOrdering] = useState({}) // 记录正在下单的推荐ID
|
||||||
|
|
||||||
|
// 获取当前账号ID
|
||||||
|
const getCurrentAccountId = () => {
|
||||||
|
try {
|
||||||
|
const v = localStorage.getItem('ats_account_id');
|
||||||
|
const n = parseInt(v || '1', 10);
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : 1;
|
||||||
|
} catch (e) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取默认下单量(按账号存储)
|
||||||
|
const getDefaultOrderSize = () => {
|
||||||
|
try {
|
||||||
|
const accountId = getCurrentAccountId();
|
||||||
|
const key = `ats_default_order_size_${accountId}`;
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
if (value) {
|
||||||
|
const size = parseFloat(value);
|
||||||
|
if (Number.isFinite(size) && size > 0) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return 1; // 默认1U
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置默认下单量(按账号存储)
|
||||||
|
const setDefaultOrderSize = (size) => {
|
||||||
|
try {
|
||||||
|
const accountId = getCurrentAccountId();
|
||||||
|
const key = `ats_default_order_size_${accountId}`;
|
||||||
|
const sizeNum = parseFloat(String(size || '1'));
|
||||||
|
if (Number.isFinite(sizeNum) && sizeNum > 0) {
|
||||||
|
localStorage.setItem(key, String(sizeNum));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [defaultOrderSize, setDefaultOrderSizeState] = useState(getDefaultOrderSize());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadRecommendations()
|
loadRecommendations()
|
||||||
|
|
@ -291,11 +339,92 @@ function Recommendations() {
|
||||||
return 'signal-weak'
|
return 'signal-weak'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleQuickOrder = async (rec) => {
|
||||||
|
const recKey = rec.symbol || rec.id;
|
||||||
|
if (!rec.suggested_limit_price && !rec.planned_entry_price) {
|
||||||
|
alert('该推荐没有入场价格,无法下单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!rec.suggested_stop_loss) {
|
||||||
|
alert('该推荐没有止损价格,无法下单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryPrice = parseFloat(rec.suggested_limit_price || rec.planned_entry_price);
|
||||||
|
const stopLossPrice = parseFloat(rec.suggested_stop_loss);
|
||||||
|
const direction = rec.direction;
|
||||||
|
const notionalUsdt = defaultOrderSize;
|
||||||
|
const leverage = rec.suggested_leverage || 10;
|
||||||
|
|
||||||
|
if (!window.confirm(
|
||||||
|
`确认下单?\n` +
|
||||||
|
`交易对: ${rec.symbol}\n` +
|
||||||
|
`方向: ${direction === 'BUY' ? '做多' : '做空'}\n` +
|
||||||
|
`入场价: ${entryPrice} USDT\n` +
|
||||||
|
`止损价: ${stopLossPrice} USDT\n` +
|
||||||
|
`下单量: ${notionalUsdt} USDT\n` +
|
||||||
|
`杠杆: ${leverage}x`
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setOrdering(prev => ({ ...prev, [recKey]: true }));
|
||||||
|
const result = await api.openPositionFromRecommendation(
|
||||||
|
rec.symbol,
|
||||||
|
entryPrice,
|
||||||
|
stopLossPrice,
|
||||||
|
direction,
|
||||||
|
notionalUsdt,
|
||||||
|
leverage
|
||||||
|
);
|
||||||
|
alert(`下单成功!\n订单ID: ${result.order_id}\n交易ID: ${result.trade_id}`);
|
||||||
|
// 可以刷新推荐列表或更新状态
|
||||||
|
} catch (err) {
|
||||||
|
alert(`下单失败: ${err.message}`);
|
||||||
|
console.error('下单失败:', err);
|
||||||
|
} finally {
|
||||||
|
setOrdering(prev => {
|
||||||
|
const newState = { ...prev };
|
||||||
|
delete newState[recKey];
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetDefaultOrderSize = () => {
|
||||||
|
const newSize = window.prompt(`设置默认下单量(USDT)\n当前值: ${defaultOrderSize} USDT`, String(defaultOrderSize));
|
||||||
|
if (newSize === null) return;
|
||||||
|
const size = parseFloat(newSize);
|
||||||
|
if (!Number.isFinite(size) || size <= 0) {
|
||||||
|
alert('请输入有效的正数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (setDefaultOrderSize(size)) {
|
||||||
|
setDefaultOrderSizeState(size);
|
||||||
|
alert(`默认下单量已设置为 ${size} USDT`);
|
||||||
|
} else {
|
||||||
|
alert('设置失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="recommendations-container">
|
<div className="recommendations-container">
|
||||||
<div className="recommendations-header">
|
<div className="recommendations-header">
|
||||||
<h2>交易推荐</h2>
|
<h2>交易推荐</h2>
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
|
<div className="order-size-config" style={{ display: 'flex', alignItems: 'center', gap: '10px', marginRight: '10px' }}>
|
||||||
|
<span>默认下单量:</span>
|
||||||
|
<span style={{ fontWeight: 'bold', color: '#2ecc71' }}>{defaultOrderSize} USDT</span>
|
||||||
|
<button
|
||||||
|
className="btn-config"
|
||||||
|
onClick={handleSetDefaultOrderSize}
|
||||||
|
style={{ padding: '4px 8px', fontSize: '12px' }}
|
||||||
|
title="点击设置默认下单量"
|
||||||
|
>
|
||||||
|
⚙️ 设置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
className="btn-generate"
|
className="btn-generate"
|
||||||
onClick={handleGenerate}
|
onClick={handleGenerate}
|
||||||
|
|
@ -462,6 +591,28 @@ function Recommendations() {
|
||||||
<span className="ug-pill stop">
|
<span className="ug-pill stop">
|
||||||
止损 {fmtPrice(rec.suggested_stop_loss)} USDT
|
止损 {fmtPrice(rec.suggested_stop_loss)} USDT
|
||||||
</span>
|
</span>
|
||||||
|
{(rec.suggested_limit_price || rec.planned_entry_price) && rec.suggested_stop_loss && (
|
||||||
|
<button
|
||||||
|
className="btn-quick-order"
|
||||||
|
onClick={() => handleQuickOrder(rec)}
|
||||||
|
disabled={ordering[rec.symbol || rec.id]}
|
||||||
|
style={{
|
||||||
|
marginLeft: '8px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
backgroundColor: '#2ecc71',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: ordering[rec.symbol || rec.id] ? 'not-allowed' : 'pointer',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '13px',
|
||||||
|
opacity: ordering[rec.symbol || rec.id] ? 0.6 : 1
|
||||||
|
}}
|
||||||
|
title={`一键下单 ${defaultOrderSize} USDT`}
|
||||||
|
>
|
||||||
|
{ordering[rec.symbol || rec.id] ? '下单中...' : '⚡ 一键下单'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<span className="ug-pill tp1">
|
<span className="ug-pill tp1">
|
||||||
目标1 {fmtPrice(rec.suggested_take_profit_1)} USDT
|
目标1 {fmtPrice(rec.suggested_take_profit_1)} USDT
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,7 @@ export const api = {
|
||||||
|
|
||||||
// 平仓操作
|
// 平仓操作
|
||||||
closePosition: async (symbol) => {
|
closePosition: async (symbol) => {
|
||||||
const response = await fetch(buildUrl(`/api/account/positions/${symbol}/close`), {
|
const response = await fetch(buildUrl(`/api/accounts/positions/${symbol}/close`), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
...withAccountHeaders({ 'Content-Type': 'application/json' }),
|
...withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
|
@ -369,7 +369,7 @@ export const api = {
|
||||||
|
|
||||||
// 补挂止盈止损(交易所保护单)
|
// 补挂止盈止损(交易所保护单)
|
||||||
ensurePositionSLTP: async (symbol) => {
|
ensurePositionSLTP: async (symbol) => {
|
||||||
const response = await fetch(buildUrl(`/api/account/positions/${symbol}/sltp/ensure`), {
|
const response = await fetch(buildUrl(`/api/accounts/positions/${symbol}/sltp/ensure`), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
|
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||||
});
|
});
|
||||||
|
|
@ -381,7 +381,7 @@ export const api = {
|
||||||
},
|
},
|
||||||
|
|
||||||
ensureAllPositionsSLTP: async (limit = 50) => {
|
ensureAllPositionsSLTP: async (limit = 50) => {
|
||||||
const response = await fetch(buildUrl(`/api/account/positions/sltp/ensure-all?limit=${encodeURIComponent(limit)}`), {
|
const response = await fetch(buildUrl(`/api/accounts/positions/sltp/ensure-all?limit=${encodeURIComponent(limit)}`), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
|
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||||
});
|
});
|
||||||
|
|
@ -394,7 +394,7 @@ export const api = {
|
||||||
|
|
||||||
// 同步持仓状态
|
// 同步持仓状态
|
||||||
syncPositions: async () => {
|
syncPositions: async () => {
|
||||||
const response = await fetch(buildUrl('/api/account/positions/sync'), {
|
const response = await fetch(buildUrl('/api/accounts/positions/sync'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
...withAccountHeaders({ 'Content-Type': 'application/json' }),
|
...withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
|
@ -407,6 +407,26 @@ export const api = {
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 根据推荐信息手动开仓
|
||||||
|
openPositionFromRecommendation: async (symbol, entryPrice, stopLossPrice, direction, notionalUsdt, leverage = 10) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
entry_price: String(entryPrice),
|
||||||
|
stop_loss_price: String(stopLossPrice),
|
||||||
|
direction: direction,
|
||||||
|
notional_usdt: String(notionalUsdt),
|
||||||
|
leverage: String(leverage),
|
||||||
|
});
|
||||||
|
const response = await fetch(buildUrl(`/api/account/positions/${symbol}/open?${params}`), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: withAccountHeaders({ '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 = {}) => {
|
||||||
// 默认使用实时推荐
|
// 默认使用实时推荐
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user