okx-auto-position: advisor 单币上限计算 + CLI symbol 拼接防重复

- recommend_position: coin_cap=min(总资产,200), 首仓70%/加仓100%, 返回 single_coin_avail
- CLI: 已含 :USDT 的 symbol 不再重复拼 (修 BadSymbol SNDK/USDT:USDT/USDT:USDT)
This commit is contained in:
2026-09-25 01:24:26 +08:00
parent c3ce51747b
commit 9c40e7eaf5
@@ -40,7 +40,10 @@ def load_credentials():
def create_exchange(creds):
"""Create ccxt OKX exchange instance with proxy"""
"""Create ccxt OKX exchange instance with proxy
2026-08-31 验证: 国内 VPS 直连 www.okx.com 不通 (No route to host),
必须走 proxychains/proxies 127.0.0.1:7890
"""
return ccxt.okx({
'apiKey': creds['OKX_API_KEY'],
'secret': creds['OKX_SECRET'],
@@ -221,25 +224,36 @@ def recommend_position(symbol, side, leverage, exchange, acct_info):
if leverage < 1:
leverage = 1
# 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'
# 2026-09-16: 单币种上限 = min(总资产, $200), 百分比按上限算
# 首仓 = 上限 × 70%, 加仓 = 上限 × 100% (可追满)
# 总资产 ≥ $1000 → 跳过 (配置上限失效)
total_cap = acct_info.get('total_capital', acct_info['usdt_free'])
disable_threshold = cfg('position_sizing', 'single_coin_total_cap_disable_usdt', 1000)
if total_cap >= disable_threshold:
return {'error': f'⚠️ 总资产 ${total_cap:.2f} ≥ ${disable_threshold}, 配置上限失效, 跳过'}
# 当前同币种已用保证金
base = symbol.split('/')[0]
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
# 单币种上限 = min(总资产, $200)
coin_cap = min(total_cap, cfg('position_sizing', 'single_coin_ceiling_usdt', 200))
# 首仓 vs 加仓
if same_coin_margin == 0:
cap = coin_cap * cfg('position_sizing', 'single_coin_max_pct', 0.70)
else:
cap = coin_cap * cfg('position_sizing', 'single_coin_add_pct', 1.00)
# 还可加仓 = 上限 - 已用
single_coin_avail = max(0, single_coin_cap - same_coin_margin)
single_coin_avail = max(0, cap - same_coin_margin)
# 同时考虑 free 余额 (不能超过 free)
avail_margin = min(acct_info['usdt_free'], single_coin_avail)
# 2026-09-02: 加 1.07 缓冲 (OKX 实际冻结 = 保证金 + 维持 0.5% + 手续费 + funding),
# 不加会复现 SKHYNIX 加仓 51008 失败 (Free $98 + 实际冻结 $105 = 差 $7)
SAFETY_FACTOR = 1.07
avail_margin = min(acct_info['usdt_free'] / SAFETY_FACTOR, single_coin_avail / SAFETY_FACTOR)
margin_per_contract = ct_val * price / leverage
if margin_per_contract <= 0:
@@ -364,6 +378,7 @@ def recommend_position(symbol, side, leverage, exchange, acct_info):
'base_amount': contracts * ct_val,
'margin': round(total_margin, 2),
'margin_pct': round(margin_pct, 1),
'single_coin_avail': round(single_coin_avail, 2), # 2026-09-16: hard check 用
'tp_price': round(tp_price, 2),
'tp_pct': round(tp_pct, 2),
'tp_pnl': round(tp_pnl, 2),
@@ -470,11 +485,36 @@ def execute_order(exchange, rec):
results = {'steps': []}
# 0. 写错误日志的辅助函数 (用户要求 - 失败要主动记, 不然跟定时任务一样不知有没有成功)
import json as _json
from datetime import datetime as _dt
_ERR_LOG = os.path.expanduser('~/.hermes/trading/order_errors.log')
def _log_error(stage, err_msg, extra=None):
"""主动记录订单错误到独立文件 (用户要求 2026-08-06)"""
try:
log_entry = {
'ts': _dt.now().isoformat(),
'stage': stage,
'symbol': symbol,
'inst_id': inst_id,
'side': side,
'contracts': contracts,
'leverage': leverage,
'error': str(err_msg)[:500],
'extra': extra or {},
}
with open(_ERR_LOG, 'a') as f:
f.write(_json.dumps(log_entry, ensure_ascii=False) + '\n')
except Exception:
pass # 写日志不能 fail 主流程
# 1. Set leverage
try:
exchange.set_leverage(leverage, symbol)
results['steps'].append({'step': 'leverage', 'status': 'ok'})
except Exception as e:
_log_error('set_leverage', e, {'rec': {k: v for k, v in rec.items() if k in ['tp_price', 'sl_price', 'price']}})
results['steps'].append({'step': 'leverage', 'status': 'warn', 'msg': str(e)})
# 2. Place market order
@@ -491,6 +531,7 @@ def execute_order(exchange, rec):
}
results['steps'].append({'step': 'order', 'status': 'ok', 'order_id': order['id']})
except Exception as e:
_log_error('place_order', e, {'contracts': contracts, 'price': rec.get('price')})
results['steps'].append({'step': 'order', 'status': 'error', 'msg': str(e)})
return results
@@ -566,8 +607,10 @@ def execute_order(exchange, rec):
results['algo'] = {'id': algo_id, 'sl': rec['sl_price'], 'tp': None}
results['steps'].append({'step': 'sl_only', 'status': 'ok', 'algo_id': algo_id})
else:
_log_error('sl_algo', f'no algoId: {resp}', {})
results['steps'].append({'step': 'sl_only', 'status': 'warn', 'msg': str(resp)})
except Exception as e:
_log_error('sl_algo', e, {'sl_price': rec.get('sl_price')})
results['steps'].append({'step': 'sl_only', 'status': 'error', 'msg': str(e)})
# 5. Verify position
@@ -775,9 +818,15 @@ def main():
creds = load_credentials()
exchange = create_exchange(creds)
# Build symbol
symbol = f"{args.symbol.upper()}/USDT:USDT"
inst_id = f"{args.symbol.upper()}-USDT-SWAP"
# Build symbol (避免重复拼 /USDT:USDT)
sym_arg = args.symbol.upper()
if ':USDT' in sym_arg or '/USDT:' in sym_arg:
symbol = sym_arg
else:
symbol = f"{sym_arg}/USDT:USDT"
inst_id = sym_arg.replace('/', '-').replace(':USDT', '-SWAP').replace(':USD', '-SWAP')
if '-SWAP' not in inst_id:
inst_id = f"{sym_arg.split('/')[0]}-USDT-SWAP"
# Close mode
if args.close: