Files
Hermes-Skills/okx-auto-position/scripts/okx_position_advisor.py
T
mike 657dc41c46 Initial commit: Trading skills collection
- OKX交易自动化 (okx-auto-position, okx-crypto, okx-exchange)
- 交易信号处理 (signal-confirmation-templates, trading-signal-aggregator)
- 量化因子挖掘 (quant-factor-mining)
- 长桥集成 (longbridge-cli, longbridge-python-sdk)
- 六合彩分析 (lottery-hk)
- 股息投资 (dividend-investing, dividend-scanner)
- 日内交易 (intraday-trading)
- 同花顺 (tonghuashun)
2026-07-05 02:39:41 -04:00

792 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 = []
for p in positions:
if float(p.get('contracts', 0)) > 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,
})
return {
'usdt_free': usdt_free,
'usdt_total': usdt_total,
'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: use 45% of available balance
avail_margin = acct_info['usdt_free'] * cfg('position_sizing', 'balance_utilization', 0.45)
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_pct']}%)",
f"🎯 止盈: **{rec['tp_price']}** (+{rec['tp_pct']}%)",
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_pct']}%)",
f"🎯 止盈: **{rec['tp_price']}** (+{rec['tp_pct']}%)",
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_pct']}%)",
f"🎯 止盈: **{rec['tp_price']}** (+{rec['tp_pct']}%)",
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. Set TP/SL via OCO algo order
try:
# For OCO: tpOrdPx=-1 and slOrdPx=-1 means market order on trigger
if side == 'sell':
# Short: TP trigger below, SL trigger above
algo_params = {
'instId': inst_id,
'tdMode': 'cross',
'side': 'buy', # buy to close short
'posSide': 'net',
'ordType': 'oco',
'sz': str(contracts),
'tpTriggerPx': str(rec['tp_price']),
'tpOrdPx': '-1',
'tpTriggerPxType': 'last',
'slTriggerPx': str(rec['sl_price']),
'slOrdPx': '-1',
'slTriggerPxType': 'last',
'reduceOnly': 'true',
}
else:
# Long: TP trigger above, SL trigger below
algo_params = {
'instId': inst_id,
'tdMode': 'cross',
'side': 'sell', # sell to close long
'posSide': 'net',
'ordType': 'oco',
'sz': str(contracts),
'tpTriggerPx': str(rec['tp_price']),
'tpOrdPx': '-1',
'tpTriggerPxType': 'last',
'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, 'tp': rec['tp_price'], 'sl': rec['sl_price']}
results['steps'].append({'step': 'tp_sl', 'status': 'ok', 'algo_id': algo_id})
else:
results['steps'].append({'step': 'tp_sl', 'status': 'warn', 'msg': str(resp)})
except Exception as e:
results['steps'].append({'step': 'tp_sl', '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()