Initial commit: Hermes Agent skills collection

- Trading skills (OKX, dividend, lottery, quantitative)
- Creative skills (ASCII art, diagrams, video)
- Development skills (GitHub, debugging, TDD)
- Research skills (arXiv, blog monitoring)
- Productivity skills (email, documents, notes)
- MCP integration skills
- Custom user skills
This commit is contained in:
Hermes Skills Manager
2026-07-05 02:31:15 -04:00
commit 6770bc9b9d
908 changed files with 239614 additions and 0 deletions
@@ -0,0 +1,791 @@
#!/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()