diff --git a/okx-auto-position/scripts/okx_position_advisor.py b/okx-auto-position/scripts/okx_position_advisor.py index 6a6df35..a1e346d 100644 --- a/okx-auto-position/scripts/okx_position_advisor.py +++ b/okx-auto-position/scripts/okx_position_advisor.py @@ -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: