#!/usr/bin/env python3 """ OKX Auto Position Advisor 根据余额自动推荐开仓数量+止盈止损位 Usage: python3 okx_position_advisor.py --symbol ETH --side short --leverage 10 python3 okx_position_advisor.py --symbol BTC --side long --leverage 5 python3 okx_position_advisor.py --symbol ETH --side short # 默认10x """ import re import os import sys import json import argparse import ccxt import math # Import cost performance module sys.path.insert(0, os.path.dirname(__file__)) from cost_performance import calc_cost_performance, calc_min_contracts_for_profit from config_loader import get as cfg def load_credentials(): """Load OKX credentials from ~/.bashrc""" creds = {} with open(os.path.expanduser("~/.bashrc")) as f: for line in f: m = re.match(r'export\s+(OKX_\w+)=(.*)', line.strip()) if m: val = m.group(2).strip() if val.startswith('"') and val.endswith('"'): val = val[1:-1] elif val.startswith("'") and val.endswith("'"): val = val[1:-1] creds[m.group(1)] = val return creds def create_exchange(creds): """Create ccxt OKX exchange instance with proxy""" return ccxt.okx({ 'apiKey': creds['OKX_API_KEY'], 'secret': creds['OKX_SECRET'], 'password': creds['OKX_PASSPHRASE'], 'proxies': { 'http': 'http://127.0.0.1:7890', 'https': 'http://127.0.0.1:7890', }, 'options': {'defaultType': 'swap'}, }) def get_account_info(exchange): """Get account balance and positions""" balance = exchange.fetch_balance() usdt_free = float(balance.get('USDT', {}).get('free', 0)) usdt_total = float(balance.get('USDT', {}).get('total', 0)) positions = exchange.fetch_positions() active = [] used_margin = 0.0 # 所有币种已用保证金总和 for p in positions: 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'], 'contracts': float(p['contracts']), '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, } def get_instrument(exchange, inst_id): """Get contract specifications""" inst = exchange.public_get_public_instruments({ 'instType': 'SWAP', 'instId': inst_id, }) spec = inst['data'][0] return { 'ct_val': float(spec['ctVal']), # contract value in base currency 'min_sz': float(spec['minSz']), # minimum order size 'lot_sz': float(spec['lotSz']), # order step size 'ct_mult': float(spec.get('ctMult', 1)), 'inst_id': inst_id, } def calc_atr(exchange, symbol, timeframe='4h', periods=30): """Calculate Average True Range""" try: ohlcv = exchange.fetch_ohlcv(symbol, timeframe, limit=periods) if len(ohlcv) < 5: return None true_ranges = [] for i in range(1, len(ohlcv)): high = ohlcv[i][2] low = ohlcv[i][3] prev_close = ohlcv[i - 1][4] tr = max(high - low, abs(high - prev_close), abs(low - prev_close)) true_ranges.append(tr) return sum(true_ranges) / len(true_ranges) except Exception: return None def calc_multi_atr(exchange, symbol): """多周期ATR融合: 1H×0.5 + 4H×0.3 + 1D×0.2 × 1.5 比单用4H ATR更灵敏——1H应对短期波动,4H做主心骨,1D兜底。 """ try: atr_1h = calc_atr(exchange, symbol, '1h', 24) atr_4h = calc_atr(exchange, symbol, '4h', 30) atr_1d = calc_atr(exchange, symbol, '1d', 14) values = [v for v in [atr_1h, atr_4h, atr_1d] if v is not None] if not values: return None, None, None, None if atr_1h is not None and atr_4h is not None and atr_1d is not None: fused = (atr_1h * cfg('atr','weight_1h',0.5) + atr_4h * cfg('atr','weight_4h',0.3) + atr_1d * cfg('atr','weight_1d',0.2)) * cfg('atr','multiplier',1.5) elif atr_4h is not None: fused = atr_4h * cfg('atr','multiplier',1.5) else: fused = sum(values) / len(values) * cfg('atr','multiplier',1.5) return fused, atr_1h, atr_4h, atr_1d except Exception: return None, None, None, None def estimate_trend_strength(exchange, symbol): """通过EMA12-EMA26斜率估算趋势强度 Returns: ('strong_up'|'strong_down'|'ranging'|'weak_trend', slope_pct) """ try: ohlcv = exchange.fetch_ohlcv(symbol, '4h', limit=30) closes = [c[4] for c in ohlcv[-26:]] if len(closes) < 14: return 'weak_trend', 0 ema12 = sum(closes[-12:]) / 12 ema26 = sum(closes) / 26 slope = (ema12 - ema26) / ema26 * 100 if slope > 0.5: return 'strong_up', round(slope, 2) if slope < -0.5: return 'strong_down', round(slope, 2) if abs(slope) < 0.1: return 'ranging', round(slope, 2) return 'weak_trend', round(slope, 2) except Exception: return 'weak_trend', 0 def _rr_by_trend(): return cfg('rr_by_trend', 'strong_up', 3.0), cfg('rr_by_trend', 'strong_down', 3.0), cfg('rr_by_trend', 'weak_trend', 2.0), cfg('rr_by_trend', 'ranging', 1.5) RR_BY_TREND = { 'strong_up': cfg('rr_by_trend', 'strong_up', 3.0), 'strong_down': cfg('rr_by_trend', 'strong_down', 3.0), 'weak_trend': cfg('rr_by_trend', 'weak_trend', 2.0), 'ranging': cfg('rr_by_trend', 'ranging', 1.5), } TREND_LABEL = { 'strong_up': '强上升趋势', 'strong_down': '强下降趋势', 'weak_trend': '弱趋势', 'ranging': '震荡', } def recommend_position(symbol, side, leverage, exchange, acct_info): """Calculate recommended position size, TP, SL""" # Get current price ticker = exchange.fetch_ticker(symbol) price = ticker['last'] # Get instrument specs inst_id = symbol.replace('/', '-').replace(':USDT', '-SWAP').replace(':USD', '-SWAP') # Handle common formats: ETH/USDT:USDT -> ETH-USDT-SWAP parts = symbol.split('/') base = parts[0] inst_id = f"{base}-USDT-SWAP" spec = get_instrument(exchange, inst_id) ct_val = spec['ct_val'] min_sz = spec['min_sz'] lot_sz = spec['lot_sz'] # Cap leverage for safety max_lev = cfg('position_sizing', 'max_leverage', 20) if leverage > max_lev: leverage = max_lev 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' # 当前同币种已用保证金 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: return {'error': 'Invalid margin calculation'} raw_contracts = avail_margin / margin_per_contract # Round down to lot_sz contracts = int(raw_contracts / lot_sz) * lot_sz contracts = max(contracts, min_sz) if contracts < min_sz: return { 'error': f'余额不足: 需要至少 {margin_per_contract * min_sz:.2f} USDT, 可用 {acct_info["usdt_free"]:.2f} USDT' } # Calculate A+E+D multi-timeframe ATR fusion (方案A) fused_atr, atr_1h, atr_4h, atr_1d = calc_multi_atr(exchange, symbol) if fused_atr and fused_atr > 0: sl_distance = fused_atr # fused_atr already includes ×1.5 multiplier else: # Fallback: fixed percentage sl_distance = price * cfg('atr', 'fallback_sl_pct', 0.03) # Adaptive R:R based on trend strength (方案D) trend, slope = estimate_trend_strength(exchange, symbol) rr_target = RR_BY_TREND.get(trend, 2.0) tp_distance = sl_distance * rr_target # Calculate TP/SL prices if side == 'sell': # Short tp_price = price - tp_distance sl_price = price + sl_distance else: # Long tp_price = price + tp_distance sl_price = price - sl_distance # Calculate liquidation price estimate if side == 'sell': liq_price = price * (1 + 1 / leverage * cfg('safety', 'liq_estimate_factor', 0.9)) # ~90% of theoretical max else: liq_price = price * (1 - 1 / leverage * cfg('safety', 'liq_estimate_factor', 0.9)) # Safety check: SL must be inside liquidation (20% buffer) if side == 'sell': # Short: SL is above entry, liq is further above # max_sl = entry + (liq - entry) * 0.8 max_sl = price + (liq_price - price) * cfg('safety', 'liq_buffer', 0.8) if sl_price > max_sl: sl_price = max_sl tp_price = price - (sl_price - price) * 2 # Maintain R:R else: # Long: SL is below entry, liq is further below # min_sl = entry - (entry - liq) * 0.8 min_sl = price - (price - liq_price) * cfg('safety', 'liq_buffer', 0.8) if sl_price < min_sl: sl_price = min_sl tp_price = price + (price - sl_price) * 2 # Calculate percentages tp_pct = abs(tp_price - price) / price * 100 sl_pct = abs(sl_price - price) / price * 100 liq_pct = abs(liq_price - price) / price * 100 # Risk/reward ratio rr = tp_pct / sl_pct if sl_pct > 0 else 0 # Total margin used total_margin = contracts * margin_per_contract margin_pct = total_margin / acct_info['usdt_free'] * 100 # Estimated P&L tp_pnl = contracts * ct_val * abs(tp_price - price) sl_pnl = contracts * ct_val * abs(sl_price - price) # Cost-performance check (性价比检查) fee_rate = cfg('cost_performance', 'fee_rate', 0.0005) cost_check = calc_cost_performance( entry_price=price, sl_price=sl_price, tp_price=tp_price, contracts=contracts, ct_val=ct_val, leverage=leverage, fee_rate=fee_rate ) # If profit < 5 USDT, adjust contracts to meet minimum min_profit = cfg('position_sizing', 'min_profit_usdt', 10) if cost_check['profit_amount'] < min_profit: tp_distance = abs(tp_price - price) min_contracts = calc_min_contracts_for_profit(tp_distance, ct_val, min_profit=min_profit) # Round up to lot_sz min_contracts = math.ceil(min_contracts / lot_sz) * lot_sz if min_contracts * margin_per_contract <= acct_info['usdt_free']: contracts = min_contracts # Recalculate P&L tp_pnl = contracts * ct_val * abs(tp_price - price) sl_pnl = contracts * ct_val * abs(sl_price - price) total_margin = contracts * margin_per_contract margin_pct = total_margin / acct_info['usdt_free'] * 100 # Recalculate cost check cost_check = calc_cost_performance( entry_price=price, sl_price=sl_price, tp_price=tp_price, contracts=contracts, ct_val=ct_val, leverage=leverage, fee_rate=fee_rate ) return { 'symbol': f"{base}/USDT", 'side': side, 'side_cn': '做空' if side == 'sell' else '做多', 'leverage': leverage, 'price': price, 'contracts': contracts, 'base_amount': contracts * ct_val, 'margin': round(total_margin, 2), 'margin_pct': round(margin_pct, 1), 'tp_price': round(tp_price, 2), 'tp_pct': round(tp_pct, 2), 'tp_pnl': round(tp_pnl, 2), 'sl_price': round(sl_price, 2), 'sl_pct': round(sl_pct, 2), 'sl_pnl': round(sl_pnl, 2), 'rr': round(rr, 1), 'liq_price': round(liq_price, 2), 'liq_pct': round(liq_pct, 1), 'atr_fused': round(fused_atr, 2) if fused_atr else None, 'atr_1h': round(atr_1h, 2) if atr_1h else None, 'atr_4h': round(atr_4h, 2) if atr_4h else None, 'atr_1d': round(atr_1d, 2) if atr_1d else None, 'trend': trend, 'trend_label': TREND_LABEL.get(trend, ''), 'slope': slope, 'inst_id': inst_id, 'ct_val': ct_val, 'min_sz': min_sz, 'acct_free': round(acct_info['usdt_free'], 2), 'cost_check': cost_check, 'auto_execute': cost_check['auto_execute'], } def format_recommendation(rec): """Format recommendation as readable text""" if 'error' in rec: return f"❌ {rec['error']}" cost_check = rec.get('cost_check', {}) rating = cost_check.get('rating', 'unknown') rating_emoji = cost_check.get('rating_emoji', '') rating_text = cost_check.get('rating_text', '') auto_execute = rec.get('auto_execute', False) # 根据性价比等级选择模板 if rating == 'high': # 性价比高 - 自动开仓后推送 lines = [ f"✅ **{rec['symbol']} {rec['side_cn']}** 自动开仓", f"", f"📊 方向: {rec['side_cn']} | 杠杆: **{rec['leverage']}x**", f"📍 入场: **{rec['price']}**", f"🛑 止损: **{rec['sl_price']}** → 预亏 -{rec['sl_pnl']:.2f} USDT (保证金-{rec['sl_pnl']/max(rec['margin'], 0.01)*100:.0f}%)", f"🎯 止盈: **{rec['tp_price']}** → 预盈 +{rec['tp_pnl']:.2f} USDT (保证金+{rec['tp_pnl']/max(rec['margin'], 0.01)*100:.0f}%)", f"📐 盈亏比: **{rec['rr']}:1** ✅", f"", f"📦 张数: **{rec['contracts']}张** ({rec['base_amount']}个)", f"💰 保证金: {rec['margin']} USDT ({rec['margin_pct']}%)", f"", f"⚖️ 盈利: {cost_check['profit_amount']} USDT | 手续费: {cost_check['fee_cost']} USDT ({cost_check['fee_pct']}%)", ] elif rating == 'medium': # 性价比一般 - 等确认 lines = [ f"⚠️ **{rec['symbol']} {rec['side_cn']}** 性价比一般", f"", f"📊 方向: {rec['side_cn']} | 杠杆: **{rec['leverage']}x**", f"📍 入场: **{rec['price']}**", f"🛑 止损: **{rec['sl_price']}** → 预亏 -{rec['sl_pnl']:.2f} USDT (保证金-{rec['sl_pnl']/max(rec['margin'], 0.01)*100:.0f}%)", f"🎯 止盈: **{rec['tp_price']}** → 预盈 +{rec['tp_pnl']:.2f} USDT (保证金+{rec['tp_pnl']/max(rec['margin'], 0.01)*100:.0f}%)", f"📐 盈亏比: **{rec['rr']}:1** ⚠️", f"", f"📦 张数: **{rec['contracts']}张** ({rec['base_amount']}个)", f"💰 保证金: {rec['margin']} USDT ({rec['margin_pct']}%)", f"", f"⚠️ {cost_check.get('reason', '')}", f"", f"回复 **Y** 仍要开仓 / **N** 取消", ] else: # 性价比低 - 不建议 lines = [ f"❌ **{rec['symbol']} {rec['side_cn']}** 性价比低,不建议", f"", f"📊 方向: {rec['side_cn']} | 杠杆: **{rec['leverage']}x**", f"📍 入场: **{rec['price']}**", f"🛑 止损: **{rec['sl_price']}** → 预亏 -{rec['sl_pnl']:.2f} USDT (保证金-{rec['sl_pnl']/max(rec['margin'], 0.01)*100:.0f}%)", f"🎯 止盈: **{rec['tp_price']}** → 预盈 +{rec['tp_pnl']:.2f} USDT (保证金+{rec['tp_pnl']/max(rec['margin'], 0.01)*100:.0f}%)", f"📐 盈亏比: **{rec['rr']}:1** ❌", f"", f"❌ {cost_check.get('reason', '')}", f"", f"💡 建议:观望或等更好入场点", ] # 添加ATR和趋势信息 if rec.get('atr_fused'): lines.append(f"📊 多周期ATR: 融合${rec['atr_fused']} (1H=${rec.get('atr_1h','?')} 4H=${rec.get('atr_4h','?')} 1D=${rec.get('atr_1d','?')})") if rec.get('trend_label'): lines.append(f"🧭 趋势: {rec['trend_label']} (斜率{rec.get('slope','?')}%)") return '\n'.join(lines) def execute_order(exchange, rec): """Execute the order after user confirmation""" symbol = f"{rec['symbol'].split('/')[0]}/USDT:USDT" inst_id = rec['inst_id'] side = rec['side'] contracts = rec['contracts'] leverage = rec['leverage'] results = {'steps': []} # 1. Set leverage try: exchange.set_leverage(leverage, symbol) results['steps'].append({'step': 'leverage', 'status': 'ok'}) except Exception as e: results['steps'].append({'step': 'leverage', 'status': 'warn', 'msg': str(e)}) # 2. Place market order try: if side == 'sell': order = exchange.create_market_sell_order(symbol, contracts, params={'tdMode': 'cross'}) else: order = exchange.create_market_buy_order(symbol, contracts, params={'tdMode': 'cross'}) results['order'] = { 'id': order['id'], 'status': order['status'], 'side': side, 'amount': contracts, } results['steps'].append({'step': 'order', 'status': 'ok', 'order_id': order['id']}) except Exception as e: results['steps'].append({'step': 'order', 'status': 'error', 'msg': str(e)}) return results # 3. Wait for position update import time time.sleep(2) # 4. Cancel existing algo orders for this instrument (避免多开止盈止损单) cancelled = 0 for otype in ['oco', 'conditional']: try: resp = exchange.private_get_trade_orders_algo_pending({ 'ordType': otype, 'instId': inst_id, }) for algo in resp.get('data', []): try: exchange.private_post_trade_cancel_algos([{ 'algoId': algo['algoId'], 'instId': inst_id, }]) cancelled += 1 except Exception: pass except Exception: pass if cancelled > 0: results['steps'].append({'step': 'cancel_old_algos', 'status': 'ok', 'cancelled': cancelled}) time.sleep(0.5) # wait for cancellation to propagate # 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'] 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: positions = exchange.fetch_positions([symbol]) for p in positions: if float(p.get('contracts', 0)) > 0: results['position'] = { 'side': p['side'], 'contracts': float(p['contracts']), 'entry': float(p['entryPrice']) if p.get('entryPrice') else 0, 'liq': float(p.get('liquidationPrice', 0)) if p.get('liquidationPrice') else 0, 'pnl': float(p.get('unrealizedPnl', 0)), } except Exception: pass return results def format_execution_result(results): """Format execution result for user""" lines = [] for step in results.get('steps', []): if step['step'] == 'leverage': if step['status'] == 'ok': lines.append("✅ 杠杆设置成功") else: lines.append(f"⚠️ 杠杆: {step.get('msg', '')}") elif step['step'] == 'order': if step['status'] == 'ok': lines.append(f"✅ 下单成功 (ID: {step['order_id']})") else: lines.append(f"❌ 下单失败: {step.get('msg', '')}") return '\n'.join(lines) elif step['step'] == 'cancel_old_algos': lines.append(f"🧹 已清理 {step['cancelled']} 个旧止盈止损单") elif step['step'] == 'tp_sl': if step['status'] == 'ok': lines.append(f"✅ 止盈止损设置成功 (ID: {step['algo_id']})") else: lines.append(f"⚠️ 止盈止损: {step.get('msg', '')}") pos = results.get('position') if pos: lines.extend([ "", "📊 **持仓确认:**", f"• 方向: {pos['side']}", f"• 数量: {pos['contracts']}张", f"• 入场价: **{pos['entry']}**", f"• 清算价: {pos['liq']}", ]) algo = results.get('algo') if algo: lines.extend([ f"• 🎯 止盈: {algo['tp']}", f"• 🛑 止损: {algo['sl']}", ]) return '\n'.join(lines) def close_position(exchange, symbol, inst_id): """Close all positions for a symbol and cancel algo orders""" results = {'steps': []} # 1. Get current position positions = exchange.fetch_positions([symbol]) pos = None for p in positions: if float(p.get('contracts', 0)) > 0: pos = p break if not pos: results['steps'].append({'step': 'check', 'status': 'none', 'msg': '没有持仓'}) return results contracts = float(pos['contracts']) side = pos['side'] entry = float(pos['entryPrice']) pnl = float(pos.get('unrealizedPnl', 0)) # 2. Cancel all algo orders for otype in ['oco', 'conditional']: try: resp = exchange.private_get_trade_orders_algo_pending({ 'ordType': otype, 'instId': inst_id, }) for algo in resp.get('data', []): try: exchange.private_post_trade_cancel_algos([{ 'algoId': algo['algoId'], 'instId': inst_id, }]) except Exception: pass except Exception: pass results['steps'].append({'step': 'cancel_algos', 'status': 'ok'}) # 3. Close position with market order try: if side == 'short': order = exchange.create_market_buy_order(symbol, contracts, params={ 'tdMode': 'cross', 'reduceOnly': True, }) else: order = exchange.create_market_sell_order(symbol, contracts, params={ 'tdMode': 'cross', 'reduceOnly': True, }) results['steps'].append({'step': 'close', 'status': 'ok', 'order_id': order['id']}) except Exception as e: results['steps'].append({'step': 'close', 'status': 'error', 'msg': str(e)}) return results # 4. Wait and verify import time time.sleep(2) # 5. Get close price from trades try: fills = exchange.fetch_my_trades(symbol, limit=1) close_price = float(fills[0]['price']) if fills else 0 except Exception: close_price = 0 results['closed'] = { 'symbol': symbol.split('/')[0] + '/USDT', 'side': side, 'contracts': contracts, 'entry': entry, 'close_price': close_price, 'pnl': pnl, } return results def format_close_result(results): """Format close position result""" lines = [] for step in results.get('steps', []): if step['step'] == 'none': return f"ℹ️ {step['msg']}" elif step['step'] == 'close': if step['status'] == 'ok': lines.append("✅ 平仓成功") else: lines.append(f"❌ 平仓失败: {step.get('msg', '')}") return '\n'.join(lines) c = results.get('closed') if c: pnl_emoji = "🟢" if c['pnl'] >= 0 else "🔴" lines.extend([ f"", f"📊 **{c['symbol']} 平仓确认:**", f"• 方向: {c['side']}", f"• 数量: {c['contracts']}张", f"• 入场价: {c['entry']}", f"• 平仓价: **{c['close_price']}**", f"• {pnl_emoji} 盈亏: **{c['pnl']:.2f} USDT**", f"• 已取消止盈止损", ]) return '\n'.join(lines) def main(): parser = argparse.ArgumentParser(description='OKX Position Advisor') parser.add_argument('--symbol', required=True, help='Base currency: ETH, BTC, SOL...') parser.add_argument('--side', choices=['long', 'short', 'buy', 'sell'], help='Position direction (required for open, optional for close)') parser.add_argument('--leverage', type=int, default=10, help='Leverage (default: 10)') parser.add_argument('--execute', action='store_true', help='Execute order (requires prior --json output)') parser.add_argument('--rec-json', type=str, help='Recommendation JSON to execute') parser.add_argument('--close', action='store_true', help='Close position for symbol') parser.add_argument('--close-all', action='store_true', help='Close all positions') parser.add_argument('--json', action='store_true', help='Output as JSON') args = parser.parse_args() # Normalize side (only needed for open) if args.side: side = 'sell' if args.side in ('short', 'sell') else 'buy' else: side = None # Load credentials and create exchange creds = load_credentials() exchange = create_exchange(creds) # Build symbol symbol = f"{args.symbol.upper()}/USDT:USDT" inst_id = f"{args.symbol.upper()}-USDT-SWAP" # Close mode if args.close: results = close_position(exchange, symbol, inst_id) print(format_close_result(results)) return if args.close_all: positions = exchange.fetch_positions() active = [p for p in positions if float(p.get('contracts', 0)) > 0] if not active: print("ℹ️ 没有持仓") return for p in active: sym = p['symbol'] iid = sym.split('/')[0].replace(':USDT', '') + '-USDT-SWAP' results = close_position(exchange, sym, iid) print(format_close_result(results)) print() return # Open mode requires --side if not side: print("❌ 开仓需要指定 --side (long/short/buy/sell)") return # Get account info acct_info = get_account_info(exchange) # Calculate recommendation rec = recommend_position(symbol, side, args.leverage, exchange, acct_info) # Execute mode: run the order if args.execute and args.rec_json: rec = json.loads(args.rec_json) results = execute_order(exchange, rec) # Output JSON for trade_signal_handler to parse if args.json: print(json.dumps(results, ensure_ascii=False)) else: print(format_execution_result(results)) return # Auto-execute mode: if cost-performance is high, execute directly if rec.get('auto_execute') and not args.json: print(f"✅ 性价比高,自动开仓...") results = execute_order(exchange, rec) print(format_execution_result(results)) return if args.json: print(json.dumps(rec, indent=2, ensure_ascii=False)) else: print(format_recommendation(rec)) if __name__ == '__main__': main()