diff --git a/okx-auto-position/scripts/__pycache__/callback_handler.cpython-311.pyc b/okx-auto-position/scripts/__pycache__/callback_handler.cpython-311.pyc deleted file mode 100644 index 74346cf..0000000 Binary files a/okx-auto-position/scripts/__pycache__/callback_handler.cpython-311.pyc and /dev/null differ diff --git a/okx-auto-position/scripts/__pycache__/config_loader.cpython-311.pyc b/okx-auto-position/scripts/__pycache__/config_loader.cpython-311.pyc deleted file mode 100644 index c4e8f22..0000000 Binary files a/okx-auto-position/scripts/__pycache__/config_loader.cpython-311.pyc and /dev/null differ diff --git a/okx-auto-position/scripts/__pycache__/cost_performance.cpython-311.pyc b/okx-auto-position/scripts/__pycache__/cost_performance.cpython-311.pyc deleted file mode 100644 index 3fceedd..0000000 Binary files a/okx-auto-position/scripts/__pycache__/cost_performance.cpython-311.pyc and /dev/null differ diff --git a/okx-auto-position/scripts/__pycache__/okx_position_advisor.cpython-311.pyc b/okx-auto-position/scripts/__pycache__/okx_position_advisor.cpython-311.pyc deleted file mode 100644 index e3b23a6..0000000 Binary files a/okx-auto-position/scripts/__pycache__/okx_position_advisor.cpython-311.pyc and /dev/null differ diff --git a/okx-auto-position/scripts/__pycache__/signal_db.cpython-311.pyc b/okx-auto-position/scripts/__pycache__/signal_db.cpython-311.pyc deleted file mode 100644 index a411b29..0000000 Binary files a/okx-auto-position/scripts/__pycache__/signal_db.cpython-311.pyc and /dev/null differ diff --git a/okx-auto-position/scripts/__pycache__/signal_tracker.cpython-311.pyc b/okx-auto-position/scripts/__pycache__/signal_tracker.cpython-311.pyc deleted file mode 100644 index a99779e..0000000 Binary files a/okx-auto-position/scripts/__pycache__/signal_tracker.cpython-311.pyc and /dev/null differ diff --git a/okx-auto-position/scripts/okx_position_advisor.py b/okx-auto-position/scripts/okx_position_advisor.py index 9ec6218..6ee08c6 100644 --- a/okx-auto-position/scripts/okx_position_advisor.py +++ b/okx-auto-position/scripts/okx_position_advisor.py @@ -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: