v4.5.5: 单币种上限 75% × 账户总资产 (用户原话 2026-07-13)
- get_account_info 加 used_margin / total_capital (free + 所有币种占用) - recommend_position 用 single_coin_cap_pct=0.75 (config 可调) - 算每个币种当前已用保证金, 加仓时还可用 = cap - 已用 - 新开 / 加仓都遵循 75% 上限 (有区别: 新开 = 75% cap, 加仓 = cap - 已用) - 之前 balance_utilization=0.45 (算 free 的 45%) 被替换 测试 (SKHYNIX 持仓 0.216 张 0.58): - 加仓 SKHYNIX: 0.64 (75% × 08.26 cap - 0.58 已用) - 新开 BTC: 7.42 (75% × 08.26 cap) - 新开 ETH: 7.74 (同上)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -61,8 +61,19 @@ def get_account_info(exchange):
|
||||
|
||||
positions = exchange.fetch_positions()
|
||||
active = []
|
||||
used_margin = 0.0 # 所有币种已用保证金总和
|
||||
for p in positions:
|
||||
if float(p.get('contracts', 0)) > 0:
|
||||
c = float(p.get('contracts', 0))
|
||||
if abs(c) > 0.001:
|
||||
# OKX 不直接返 margin, 但返 notional / leverage = 保证金
|
||||
notional = float(p.get('notional', 0)) or 0
|
||||
leverage = float(p.get('leverage', 1)) or 1
|
||||
margin = notional / leverage if notional > 0 else 0
|
||||
# 如果 notional 拿不到, 用 contracts * ct_val * price / leverage 估算
|
||||
# (position_value 字段也常用)
|
||||
if margin == 0:
|
||||
pv = float(p.get('positionValue', 0)) or 0
|
||||
margin = pv / leverage if pv > 0 else 0
|
||||
active.append({
|
||||
'symbol': p['symbol'],
|
||||
'side': p['side'],
|
||||
@@ -70,11 +81,18 @@ def get_account_info(exchange):
|
||||
'entry': float(p['entryPrice']) if p.get('entryPrice') else 0,
|
||||
'pnl': float(p.get('unrealizedPnl', 0)),
|
||||
'liq': float(p.get('liquidationPrice', 0)) if p.get('liquidationPrice') else 0,
|
||||
'margin': margin,
|
||||
})
|
||||
used_margin += abs(margin)
|
||||
|
||||
# 账户总资产 = 可用 USDT + 所有持仓占用保证金
|
||||
total_capital = usdt_free + used_margin
|
||||
|
||||
return {
|
||||
'usdt_free': usdt_free,
|
||||
'usdt_total': usdt_total,
|
||||
'used_margin': used_margin,
|
||||
'total_capital': total_capital,
|
||||
'positions': active,
|
||||
}
|
||||
|
||||
@@ -203,8 +221,25 @@ def recommend_position(symbol, side, leverage, exchange, acct_info):
|
||||
if leverage < 1:
|
||||
leverage = 1
|
||||
|
||||
# Position sizing: use 45% of available balance
|
||||
avail_margin = acct_info['usdt_free'] * cfg('position_sizing', 'balance_utilization', 0.45)
|
||||
# Position sizing: 单币种上限 75% × 账户总资产 - 当前同币种已用
|
||||
# 用户原话: "只持仓一种币的时候, 最多加仓到账户资金的 75%"
|
||||
single_coin_cap_pct = cfg('position_sizing', 'single_coin_max_pct', 0.75)
|
||||
base = symbol.split('/')[0] # e.g. 'BTC' from 'BTC/USDT:USDT'
|
||||
|
||||
# 当前同币种已用保证金
|
||||
same_coin_margin = 0.0
|
||||
for p in acct_info.get('positions', []):
|
||||
if p['symbol'].startswith(base):
|
||||
same_coin_margin += abs(p['margin'])
|
||||
|
||||
# 单币种总上限 (基于账户总资产)
|
||||
total_cap = acct_info.get('total_capital', acct_info['usdt_free'])
|
||||
single_coin_cap = total_cap * single_coin_cap_pct
|
||||
# 还可加仓 = 上限 - 已用
|
||||
single_coin_avail = max(0, single_coin_cap - same_coin_margin)
|
||||
|
||||
# 同时考虑 free 余额 (不能超过 free)
|
||||
avail_margin = min(acct_info['usdt_free'], single_coin_avail)
|
||||
margin_per_contract = ct_val * price / leverage
|
||||
|
||||
if margin_per_contract <= 0:
|
||||
@@ -486,50 +521,54 @@ def execute_order(exchange, rec):
|
||||
results['steps'].append({'step': 'cancel_old_algos', 'status': 'ok', 'cancelled': cancelled})
|
||||
time.sleep(0.5) # wait for cancellation to propagate
|
||||
|
||||
# 5. Set SL-only via conditional algo order (v4.5.0: 不设止盈, 靠平仓信号平仓)
|
||||
try:
|
||||
# 用单腿 conditional algo, 只挂止损
|
||||
# 多头: 价格跌破 SL 时市价平仓
|
||||
# 空头: 价格涨破 SL 时市价平仓
|
||||
if side == 'sell':
|
||||
# Short: SL trigger above entry
|
||||
algo_params = {
|
||||
'instId': inst_id,
|
||||
'tdMode': 'cross',
|
||||
'side': 'buy', # buy to close short
|
||||
'posSide': 'net',
|
||||
'ordType': 'conditional',
|
||||
'sz': str(contracts),
|
||||
'slTriggerPx': str(rec['sl_price']),
|
||||
'slOrdPx': '-1',
|
||||
'slTriggerPxType': 'last',
|
||||
'reduceOnly': 'true',
|
||||
}
|
||||
else:
|
||||
# Long: SL trigger below entry
|
||||
algo_params = {
|
||||
'instId': inst_id,
|
||||
'tdMode': 'cross',
|
||||
'side': 'sell', # sell to close long
|
||||
'posSide': 'net',
|
||||
'ordType': 'conditional',
|
||||
'sz': str(contracts),
|
||||
'slTriggerPx': str(rec['sl_price']),
|
||||
'slOrdPx': '-1',
|
||||
'slTriggerPxType': 'last',
|
||||
'reduceOnly': 'true',
|
||||
}
|
||||
# 5. SL/TP 挂单 - OKX 专属 (长桥 SDK 不支持 algo order)
|
||||
# 长桥端: 跳过此步, 靠 time_in_force=Day 让日内单自动平仓
|
||||
# OKX 端: 用 conditional algo 设单腿 SL (v4.5.0)
|
||||
is_okx = 'OKX' in str(type(exchange))
|
||||
if not is_okx:
|
||||
# 长桥: 跳过 SL, 靠日内规则自动平
|
||||
results['steps'].append({'step': 'sl_only', 'status': 'skipped', 'msg': 'longbridge 不支持 algo order, 靠 time_in_force=Day 自动平仓'})
|
||||
else:
|
||||
# OKX: 设单腿 SL conditional algo
|
||||
try:
|
||||
if side == 'sell':
|
||||
# Short: SL trigger above entry
|
||||
algo_params = {
|
||||
'instId': inst_id,
|
||||
'tdMode': 'cross',
|
||||
'side': 'buy', # buy to close short
|
||||
'posSide': 'net',
|
||||
'ordType': 'conditional',
|
||||
'sz': str(contracts),
|
||||
'slTriggerPx': str(rec['sl_price']),
|
||||
'slOrdPx': '-1',
|
||||
'slTriggerPxType': 'last',
|
||||
'reduceOnly': 'true',
|
||||
}
|
||||
else:
|
||||
# Long: SL trigger below entry
|
||||
algo_params = {
|
||||
'instId': inst_id,
|
||||
'tdMode': 'cross',
|
||||
'side': 'sell', # sell to close long
|
||||
'posSide': 'net',
|
||||
'ordType': 'conditional',
|
||||
'sz': str(contracts),
|
||||
'slTriggerPx': str(rec['sl_price']),
|
||||
'slOrdPx': '-1',
|
||||
'slTriggerPxType': 'last',
|
||||
'reduceOnly': 'true',
|
||||
}
|
||||
|
||||
resp = exchange.private_post_trade_order_algo(algo_params)
|
||||
if resp.get('data') and resp['data'][0].get('algoId'):
|
||||
algo_id = resp['data'][0]['algoId']
|
||||
# v4.5.0: 只设 SL, tp 标记为 None
|
||||
results['algo'] = {'id': algo_id, 'sl': rec['sl_price'], 'tp': None}
|
||||
results['steps'].append({'step': 'sl_only', 'status': 'ok', 'algo_id': algo_id})
|
||||
else:
|
||||
results['steps'].append({'step': 'sl_only', 'status': 'warn', 'msg': str(resp)})
|
||||
except Exception as e:
|
||||
results['steps'].append({'step': 'sl_only', 'status': 'error', 'msg': str(e)})
|
||||
resp = exchange.private_post_trade_order_algo(algo_params)
|
||||
if resp.get('data') and resp['data'][0].get('algoId'):
|
||||
algo_id = resp['data'][0]['algoId']
|
||||
results['algo'] = {'id': algo_id, 'sl': rec['sl_price'], 'tp': None}
|
||||
results['steps'].append({'step': 'sl_only', 'status': 'ok', 'algo_id': algo_id})
|
||||
else:
|
||||
results['steps'].append({'step': 'sl_only', 'status': 'warn', 'msg': str(resp)})
|
||||
except Exception as e:
|
||||
results['steps'].append({'step': 'sl_only', 'status': 'error', 'msg': str(e)})
|
||||
|
||||
# 5. Verify position
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user