okx-auto-position: advisor 单币上限计算 + CLI symbol 拼接防重复
- recommend_position: coin_cap=min(总资产,200), 首仓70%/加仓100%, 返回 single_coin_avail - CLI: 已含 :USDT 的 symbol 不再重复拼 (修 BadSymbol SNDK/USDT:USDT/USDT:USDT)
This commit is contained in:
@@ -40,7 +40,10 @@ def load_credentials():
|
|||||||
|
|
||||||
|
|
||||||
def create_exchange(creds):
|
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({
|
return ccxt.okx({
|
||||||
'apiKey': creds['OKX_API_KEY'],
|
'apiKey': creds['OKX_API_KEY'],
|
||||||
'secret': creds['OKX_SECRET'],
|
'secret': creds['OKX_SECRET'],
|
||||||
@@ -221,25 +224,36 @@ def recommend_position(symbol, side, leverage, exchange, acct_info):
|
|||||||
if leverage < 1:
|
if leverage < 1:
|
||||||
leverage = 1
|
leverage = 1
|
||||||
|
|
||||||
# Position sizing: 单币种上限 75% × 账户总资产 - 当前同币种已用
|
# 2026-09-16: 单币种上限 = min(总资产, $200), 百分比按上限算
|
||||||
# 用户原话: "只持仓一种币的时候, 最多加仓到账户资金的 75%"
|
# 首仓 = 上限 × 70%, 加仓 = 上限 × 100% (可追满)
|
||||||
single_coin_cap_pct = cfg('position_sizing', 'single_coin_max_pct', 0.75)
|
# 总资产 ≥ $1000 → 跳过 (配置上限失效)
|
||||||
base = symbol.split('/')[0] # e.g. 'BTC' from 'BTC/USDT:USDT'
|
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
|
same_coin_margin = 0.0
|
||||||
for p in acct_info.get('positions', []):
|
for p in acct_info.get('positions', []):
|
||||||
if p['symbol'].startswith(base):
|
if p['symbol'].startswith(base):
|
||||||
same_coin_margin += abs(p['margin'])
|
same_coin_margin += abs(p['margin'])
|
||||||
|
|
||||||
# 单币种总上限 (基于账户总资产)
|
# 单币种上限 = min(总资产, $200)
|
||||||
total_cap = acct_info.get('total_capital', acct_info['usdt_free'])
|
coin_cap = min(total_cap, cfg('position_sizing', 'single_coin_ceiling_usdt', 200))
|
||||||
single_coin_cap = total_cap * single_coin_cap_pct
|
# 首仓 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)
|
# 同时考虑 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
|
margin_per_contract = ct_val * price / leverage
|
||||||
|
|
||||||
if margin_per_contract <= 0:
|
if margin_per_contract <= 0:
|
||||||
@@ -364,6 +378,7 @@ def recommend_position(symbol, side, leverage, exchange, acct_info):
|
|||||||
'base_amount': contracts * ct_val,
|
'base_amount': contracts * ct_val,
|
||||||
'margin': round(total_margin, 2),
|
'margin': round(total_margin, 2),
|
||||||
'margin_pct': round(margin_pct, 1),
|
'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_price': round(tp_price, 2),
|
||||||
'tp_pct': round(tp_pct, 2),
|
'tp_pct': round(tp_pct, 2),
|
||||||
'tp_pnl': round(tp_pnl, 2),
|
'tp_pnl': round(tp_pnl, 2),
|
||||||
@@ -470,11 +485,36 @@ def execute_order(exchange, rec):
|
|||||||
|
|
||||||
results = {'steps': []}
|
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
|
# 1. Set leverage
|
||||||
try:
|
try:
|
||||||
exchange.set_leverage(leverage, symbol)
|
exchange.set_leverage(leverage, symbol)
|
||||||
results['steps'].append({'step': 'leverage', 'status': 'ok'})
|
results['steps'].append({'step': 'leverage', 'status': 'ok'})
|
||||||
except Exception as e:
|
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)})
|
results['steps'].append({'step': 'leverage', 'status': 'warn', 'msg': str(e)})
|
||||||
|
|
||||||
# 2. Place market order
|
# 2. Place market order
|
||||||
@@ -491,6 +531,7 @@ def execute_order(exchange, rec):
|
|||||||
}
|
}
|
||||||
results['steps'].append({'step': 'order', 'status': 'ok', 'order_id': order['id']})
|
results['steps'].append({'step': 'order', 'status': 'ok', 'order_id': order['id']})
|
||||||
except Exception as e:
|
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)})
|
results['steps'].append({'step': 'order', 'status': 'error', 'msg': str(e)})
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -566,8 +607,10 @@ def execute_order(exchange, rec):
|
|||||||
results['algo'] = {'id': algo_id, 'sl': rec['sl_price'], '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})
|
results['steps'].append({'step': 'sl_only', 'status': 'ok', 'algo_id': algo_id})
|
||||||
else:
|
else:
|
||||||
|
_log_error('sl_algo', f'no algoId: {resp}', {})
|
||||||
results['steps'].append({'step': 'sl_only', 'status': 'warn', 'msg': str(resp)})
|
results['steps'].append({'step': 'sl_only', 'status': 'warn', 'msg': str(resp)})
|
||||||
except Exception as e:
|
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)})
|
results['steps'].append({'step': 'sl_only', 'status': 'error', 'msg': str(e)})
|
||||||
|
|
||||||
# 5. Verify position
|
# 5. Verify position
|
||||||
@@ -775,9 +818,15 @@ def main():
|
|||||||
creds = load_credentials()
|
creds = load_credentials()
|
||||||
exchange = create_exchange(creds)
|
exchange = create_exchange(creds)
|
||||||
|
|
||||||
# Build symbol
|
# Build symbol (避免重复拼 /USDT:USDT)
|
||||||
symbol = f"{args.symbol.upper()}/USDT:USDT"
|
sym_arg = args.symbol.upper()
|
||||||
inst_id = f"{args.symbol.upper()}-USDT-SWAP"
|
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
|
# Close mode
|
||||||
if args.close:
|
if args.close:
|
||||||
|
|||||||
Reference in New Issue
Block a user