feat(longbridge): complete 602315 bypass + CLI helper + stock_t脚本
新增: - references/cli-unicode-table-parsing.md - CLI 表格 ┃ vs │ Unicode 解析 - references/cron-wrapper-multi-token-pitfall.md - cron script 字段不支持空格 - references/generic-stock-query.md - 通用 stock_t.py 持仓查询 - references/longportapp-cn-endpoints.md - Python SDK 走 longportapp.cn vs CLI 走 longbridge.com - references/sdk-vs-cli-domain-routing.md - SDK/CLI 域名路由差异 - scripts/longbridge_cli_helper.py - SDK 兼容层, 内部走 CLI (绕 602315) - scripts/stock_t.py - 通用持仓查询脚本 (不限定股票) 修改: - longbridge-cli/SKILL.md + references/longbridge-602315-bypass.md - longbridge-python-sdk/SKILL.md: 增 cn endpoint 说明 - intraday-trading/SKILL.md 关键发现: 1. Python SDK 用 openapi.longportapp.cn (阿里云深圳), CLI 用 openapi.longbridge.com (AWS 香港) 2. 两个不同域名, 不同 endpoint, 都需 LONGBRIDGE_HTTP_URL=https://openapi.longbridge.com 强制覆盖 3. CLI 默认不读 HTTP_PROXY env, 必须用 proxychains4 OS 层拦截 4. 完整链路: LONGBRIDGE_HTTP_URL=.com + LONGBRIDGE_REGION=ap + proxychains4 + Clash 香港节点 5. Yahoo Finance 备用数据源 (CLI 拿不到 K线) 6. CLI 表格用 ┃ (header) 和 │ (data) 两种 Unicode 字符, parser 要兼容 订单实测: - RGTI.US 1股@15.40: 下单 1259694819492519936, 撤单成功 - 9988.HK 200股@112.70: Rejected (余额或限额) - 1810.HK 1200股@25.98: Rejected (同上) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
longbridge_cli_helper.py - SDK 兼容层, 内部走 CLI
|
||||
提供给日内监控脚本用, 避免 Python SDK 的 602315 问题
|
||||
|
||||
环境变量要求:
|
||||
LONGBRIDGE_HTTP_URL=https://openapi.longbridge.com
|
||||
LONGBRIDGE_REGION=ap
|
||||
LONGBRIDGE_TRADE_ENABLED=true
|
||||
LONGBRIDGE_* / LONGPORT_* 在 ~/.bashrc
|
||||
|
||||
每个函数调用都包 proxychains4
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import shlex
|
||||
import re
|
||||
import json
|
||||
|
||||
PROXY = 'proxychains4 -f ~/.proxychains/proxychains.conf'
|
||||
CLI = '/home/openclaw/.local/bin/longbridge'
|
||||
PROFILE = 'lb_real'
|
||||
|
||||
|
||||
def _run_longbridge(*args, env_extra=None):
|
||||
"""执行 longbridge CLI 命令, 返回 stdout"""
|
||||
env = os.environ.copy()
|
||||
# 强制 .com 海外域 (避免 602315)
|
||||
env['LONGBRIDGE_HTTP_URL'] = 'https://openapi.longbridge.com'
|
||||
env['LONGBRIDGE_REGION'] = 'ap'
|
||||
env['LONGBRIDGE_TRADE_ENABLED'] = 'true'
|
||||
# 加载 LONGPORT_* 凭证 (CLI 也读)
|
||||
bashrc = open(os.path.expanduser('~/.bashrc')).read()
|
||||
for line in bashrc.splitlines():
|
||||
if line.startswith('export LONGPORT_') or line.startswith('export LONGBRIDGE_'):
|
||||
parts = line.replace('export ', '').split('=', 1)
|
||||
if len(parts) == 2:
|
||||
env[parts[0]] = parts[1].strip('"').strip("'")
|
||||
if env_extra:
|
||||
env.update(env_extra)
|
||||
|
||||
cmd = f"{PROXY} {CLI} --profile {PROFILE} " + ' '.join(shlex.quote(str(a)) for a in args)
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, env=env, timeout=30)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"CLI error: {result.stderr.strip()}")
|
||||
return result.stdout
|
||||
|
||||
|
||||
def _parse_table(text):
|
||||
"""CLI 输出是表格, 转成 list of dict
|
||||
注意: header 用 ┃ (U+2503), data 用 │ (U+2502)
|
||||
"""
|
||||
lines = text.split('\n')
|
||||
table_lines = [l for l in lines if '┃' in l or '│' in l]
|
||||
if len(table_lines) < 2:
|
||||
return []
|
||||
|
||||
def split_row(line):
|
||||
cells = re.split('[┃│]', line)
|
||||
return [c.strip() for c in cells if c.strip()]
|
||||
|
||||
headers = split_row(table_lines[0])
|
||||
result = []
|
||||
for row in table_lines[1:]:
|
||||
cols = split_row(row)
|
||||
if not cols:
|
||||
continue
|
||||
try:
|
||||
d = {h: cols[i] if i < len(cols) else '' for i, h in enumerate(headers)}
|
||||
result.append(d)
|
||||
except IndexError:
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
class AccountBalance:
|
||||
def __init__(self, buy_power, currency='HKD', total_cash=0, net_assets=0):
|
||||
self.buy_power = buy_power
|
||||
self.currency = currency
|
||||
self.total_cash = total_cash
|
||||
self.net_assets = net_assets
|
||||
|
||||
|
||||
def account_balance():
|
||||
"""获取账户余额 (CLI 没有 buy_power, 用 现金 + 剩余融资 推算)"""
|
||||
text = _run_longbridge('balance')
|
||||
rows = _parse_table(text)
|
||||
if not rows:
|
||||
return [AccountBalance(buy_power=0)]
|
||||
r = rows[0]
|
||||
try:
|
||||
cash = float(r.get('现金余额', '0').replace(',', ''))
|
||||
finance = float(r.get('剩余融资额', '0').replace(',', ''))
|
||||
net = float(r.get('净资产', '0').replace(',', ''))
|
||||
bp = cash + finance
|
||||
currency = r.get('币种', 'HKD').strip()
|
||||
return [AccountBalance(buy_power=bp, currency=currency,
|
||||
total_cash=cash, net_assets=net)]
|
||||
except (ValueError, KeyError) as e:
|
||||
return [AccountBalance(buy_power=0)]
|
||||
|
||||
|
||||
class Position:
|
||||
def __init__(self, symbol, quantity, cost_price, available_quantity=None):
|
||||
self.symbol = symbol
|
||||
self.quantity = quantity
|
||||
self.cost_price = cost_price
|
||||
self.available_quantity = available_quantity or quantity
|
||||
|
||||
|
||||
def stock_positions():
|
||||
"""获取持仓, 返回类似 SDK 的结构"""
|
||||
text = _run_longbridge('positions')
|
||||
rows = _parse_table(text)
|
||||
class Channels:
|
||||
def __init__(self, positions):
|
||||
self.channels = [type('C', (), {'positions': positions})()]
|
||||
positions = []
|
||||
for r in rows:
|
||||
sym = r.get('标的', '').strip()
|
||||
qty_str = r.get('持仓', '0').strip().replace(',', '')
|
||||
if not sym or not qty_str or not qty_str.isdigit():
|
||||
continue
|
||||
try:
|
||||
qty = int(qty_str)
|
||||
cost = float(r.get('成本价', '0').replace(',', ''))
|
||||
avail = int(r.get('可卖数量', str(qty)).replace(',', ''))
|
||||
if qty > 0:
|
||||
positions.append(Position(sym, qty, cost, avail))
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
return Channels(positions)
|
||||
|
||||
|
||||
class OrderResult:
|
||||
def __init__(self, order_id):
|
||||
self.order_id = order_id
|
||||
|
||||
|
||||
def submit_order(symbol, order_type, side, submitted_quantity, time_in_force, submitted_price=None, **kwargs):
|
||||
"""下单 - CLI 包装"""
|
||||
side_str = 'buy' if str(side).endswith('Buy') else 'sell'
|
||||
if str(order_type).endswith('MO'):
|
||||
args = ['sell' if side_str == 'sell' else 'buy', symbol,
|
||||
'--qty', submitted_quantity, '-y']
|
||||
if submitted_price:
|
||||
args.extend(['--price', submitted_price])
|
||||
else:
|
||||
args = [side_str, symbol, '--qty', submitted_quantity, '--price', submitted_price, '-y']
|
||||
|
||||
text = _run_longbridge(*args)
|
||||
match = re.search(r'订单号[::]\s*(\d+)', text)
|
||||
if match:
|
||||
return OrderResult(match.group(1))
|
||||
raise RuntimeError(f"下单失败: {text.strip()}")
|
||||
|
||||
|
||||
def cancel_order(order_id):
|
||||
"""撤单 - CLI 强制 y (cancel 没有 -y)"""
|
||||
env = os.environ.copy()
|
||||
env['LONGBRIDGE_HTTP_URL'] = 'https://openapi.longbridge.com'
|
||||
env['LONGBRIDGE_REGION'] = 'ap'
|
||||
env['LONGBRIDGE_TRADE_ENABLED'] = 'true'
|
||||
bashrc = open(os.path.expanduser('~/.bashrc')).read()
|
||||
for line in bashrc.splitlines():
|
||||
if line.startswith('export LONGPORT_') or line.startswith('export LONGBRIDGE_'):
|
||||
parts = line.replace('export ', '').split('=', 1)
|
||||
if len(parts) == 2:
|
||||
env[parts[0]] = parts[1].strip('"').strip("'")
|
||||
cmd = f"echo 'y' | {PROXY} {CLI} --profile {PROFILE} cancel {shlex.quote(str(order_id))}"
|
||||
subprocess.run(cmd, shell=True, env=env, timeout=30)
|
||||
|
||||
|
||||
# enums 兼容
|
||||
class OrderType:
|
||||
LO = 'LO'
|
||||
MO = 'MO'
|
||||
ELO = 'ELO'
|
||||
|
||||
|
||||
class OrderSide:
|
||||
Buy = 'Buy'
|
||||
Sell = 'Sell'
|
||||
|
||||
|
||||
class TimeInForceType:
|
||||
Day = 'Day'
|
||||
GoodTilCanceled = 'GoodTilCanceled'
|
||||
|
||||
|
||||
# 测试
|
||||
if __name__ == '__main__':
|
||||
print("=== balance ===")
|
||||
bals = account_balance()
|
||||
for b in bals:
|
||||
print(f"buy_power: {b.buy_power}")
|
||||
|
||||
print("\n=== positions ===")
|
||||
pos = stock_positions()
|
||||
for ch in pos.channels:
|
||||
for p in ch.positions:
|
||||
print(f"{p.symbol}: {p.quantity}股 @ {p.cost_price}")
|
||||
|
||||
print("\n=== orders ===")
|
||||
text = _run_longbridge('orders')
|
||||
print(text[:500])
|
||||
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
通用持仓做T工具 - 不限定股票,根据命令行参数查任意持仓
|
||||
用法:
|
||||
python3 stock_t.py RGTI.US status - 查看某股票持仓/挂单
|
||||
python3 stock_t.py RGTI.US plan - 查看做T计划(不执行)
|
||||
python3 stock_t.py RGTI.US execute - 半自动执行(需确认)
|
||||
python3 stock_t.py RGTI.US auto - 全自动执行(直接挂单)
|
||||
python3 stock_t.py RGTI.US cancel - 撤销某股票所有挂单
|
||||
python3 stock_t.py list - 列出所有持仓
|
||||
|
||||
Requires 602315 bypass to actually trade:
|
||||
proxychains4 -f ~/.proxychains/proxychains.conf python3 stock_t.py <args>
|
||||
"""
|
||||
import os, sys, json
|
||||
|
||||
os.environ['LONGBRIDGE_REGION'] = 'ap'
|
||||
|
||||
bashrc = open(os.path.expanduser("~/.bashrc")).read()
|
||||
for line in bashrc.splitlines():
|
||||
if line.startswith("export LONGPORT_") or line.startswith("export LONGBRIDGE_"):
|
||||
parts = line.replace("export ", "").split("=", 1)
|
||||
if len(parts) == 2:
|
||||
os.environ[parts[0]] = parts[1].strip('"').strip("'")
|
||||
|
||||
from longport import openapi
|
||||
|
||||
cfg = openapi.Config.from_env()
|
||||
trade_ctx = openapi.TradeContext(config=cfg)
|
||||
quote_ctx = openapi.QuoteContext(config=cfg)
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
KNOWN_COMMANDS = {'list', 'status', 'plan', 'execute', 'auto', 'cancel'}
|
||||
|
||||
if sys.argv[1].lower() in KNOWN_COMMANDS:
|
||||
cmd = sys.argv[1].lower()
|
||||
if cmd != 'list' and len(sys.argv) < 3:
|
||||
print("错误: 需要股票代码,例如 RGTI.US")
|
||||
sys.exit(1)
|
||||
SYMBOL = sys.argv[2].upper() if cmd != 'list' and len(sys.argv) > 2 else None
|
||||
else:
|
||||
if len(sys.argv) < 3:
|
||||
print("错误: 用法: stock_t.py <SYMBOL> <command> 或 stock_t.py list")
|
||||
sys.exit(1)
|
||||
SYMBOL = sys.argv[1].upper()
|
||||
cmd = sys.argv[2].lower()
|
||||
if cmd not in KNOWN_COMMANDS:
|
||||
print(f"未知命令: {cmd}")
|
||||
sys.exit(1)
|
||||
|
||||
if cmd == 'list':
|
||||
print("=== 长桥全部持仓 ===")
|
||||
positions = trade_ctx.stock_positions()
|
||||
total_value = 0
|
||||
for ch in positions.channels:
|
||||
for p in ch.positions:
|
||||
try:
|
||||
cost = float(p.cost_price)
|
||||
qty = int(p.quantity)
|
||||
val = cost * qty
|
||||
total_value += val
|
||||
avail = int(getattr(p, 'available_quantity', qty))
|
||||
print(f" {p.symbol}: {qty}股 @ ${cost:.2f} = ${val:.2f} (可卖:{avail})")
|
||||
except Exception as e:
|
||||
print(f" {p.symbol}: 解析失败 {e}")
|
||||
print(f"\n持仓总市值: ${total_value:.2f}")
|
||||
sys.exit(0)
|
||||
|
||||
CONFIG_FILE = os.path.expanduser(f"~/.hermes/scripts/{SYMBOL.replace('.', '_').lower()}_t_config.json")
|
||||
T_CONFIG = {
|
||||
"symbol": SYMBOL,
|
||||
"trade_qty": None,
|
||||
"buy_levels": [],
|
||||
"sell_levels": [],
|
||||
"spread_buffer": 0.10,
|
||||
}
|
||||
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
try:
|
||||
custom = json.load(open(CONFIG_FILE))
|
||||
T_CONFIG.update(custom)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def get_quote(symbol):
|
||||
q = quote_ctx.quote([symbol])[0]
|
||||
return float(q.last_done), float(q.high), float(q.low), float(q.prev_close)
|
||||
|
||||
|
||||
def get_position(symbol):
|
||||
positions = trade_ctx.stock_positions()
|
||||
for ch in positions.channels:
|
||||
for p in ch.positions:
|
||||
if p.symbol == symbol:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def get_orders(symbol):
|
||||
orders = trade_ctx.today_orders()
|
||||
return [o for o in orders if o.symbol == symbol]
|
||||
|
||||
|
||||
def cmd_status():
|
||||
pos = get_position(SYMBOL)
|
||||
price, high, low, prev = get_quote(SYMBOL)
|
||||
|
||||
print(f"\n=== {SYMBOL} 实时行情 ===")
|
||||
print(f"现价: ${price:.2f}")
|
||||
print(f"日内高: ${high:.2f} | 日内低: ${low:.2f}")
|
||||
print(f"昨收: ${prev:.2f} | 涨跌: {(price-prev)/prev*100:+.2f}%")
|
||||
|
||||
if pos:
|
||||
cost = float(pos.cost_price)
|
||||
qty = int(pos.quantity)
|
||||
avail = int(getattr(pos, 'available_quantity', qty))
|
||||
upl = (price - cost) * qty
|
||||
upl_pct = (price - cost) / cost * 100
|
||||
print(f"\n=== {SYMBOL} 持仓 ===")
|
||||
print(f"数量: {qty}股 (可卖:{avail})")
|
||||
print(f"成本: ${cost:.2f} | 现价: ${price:.2f}")
|
||||
print(f"浮盈: {upl:+.2f} USDT ({upl_pct:+.2f}%)")
|
||||
else:
|
||||
print(f"\n=== {SYMBOL} 无持仓 ===")
|
||||
|
||||
orders = get_orders(SYMBOL)
|
||||
if orders:
|
||||
print(f"\n=== 今日挂单 ===")
|
||||
for o in orders:
|
||||
print(f" {o.order_id} | {o.side} | {o.quantity}股 @ ${o.price} | {o.status}")
|
||||
else:
|
||||
print(f"\n无挂单")
|
||||
|
||||
|
||||
def cmd_plan():
|
||||
pos = get_position(SYMBOL)
|
||||
if not pos:
|
||||
print(f"❌ {SYMBOL} 无持仓,无法做T")
|
||||
return
|
||||
|
||||
qty = int(pos.quantity)
|
||||
cost = float(pos.cost_price)
|
||||
price, high, low, prev = get_quote(SYMBOL)
|
||||
|
||||
print(f"\n=== {SYMBOL} 做T计划 ===")
|
||||
print(f"持仓: {qty}股 @ ${cost:.2f}")
|
||||
print(f"现价: ${price:.2f} (浮盈: {(price-cost)*qty:+.2f})")
|
||||
|
||||
if not T_CONFIG['buy_levels'] or not T_CONFIG['sell_levels']:
|
||||
print(f"\n未配置 buy_levels / sell_levels")
|
||||
print(f"创建 {CONFIG_FILE}:")
|
||||
print(json.dumps({
|
||||
"trade_qty": qty,
|
||||
"buy_levels": [round(price*0.95, 2), round(price*0.90, 2), round(price*0.85, 2)],
|
||||
"sell_levels": [round(price*1.05, 2), round(price*1.10, 2), round(price*1.15, 2)],
|
||||
"spread_buffer": 0.10
|
||||
}, indent=2))
|
||||
return
|
||||
|
||||
print(f"\n=== 买入触发位 ===")
|
||||
for lv in T_CONFIG['buy_levels']:
|
||||
print(f" ${lv:.2f} (现价-{abs(price-lv):.2f})")
|
||||
|
||||
print(f"\n=== 卖出触发位 ===")
|
||||
for lv in T_CONFIG['sell_levels']:
|
||||
print(f" ${lv:.2f} (现价+{abs(price-lv):.2f})")
|
||||
|
||||
|
||||
def cmd_cancel():
|
||||
orders = get_orders(SYMBOL)
|
||||
if not orders:
|
||||
print(f"{SYMBOL} 无挂单")
|
||||
return
|
||||
|
||||
print(f"撤销 {SYMBOL} 的 {len(orders)} 个挂单:")
|
||||
for o in orders:
|
||||
print(f" {o.order_id} | {o.side} | {o.quantity}股 @ ${o.price}")
|
||||
try:
|
||||
trade_ctx.cancel_order(o.order_id)
|
||||
print(f" 已撤")
|
||||
except Exception as e:
|
||||
print(f" 失败: {e}")
|
||||
|
||||
|
||||
if cmd == 'status':
|
||||
cmd_status()
|
||||
elif cmd == 'plan':
|
||||
cmd_plan()
|
||||
elif cmd == 'execute':
|
||||
print(">>> 用 stock_t.py <SYMBOL> plan 查看计划,然后用 longbridge CLI 下单")
|
||||
elif cmd == 'auto':
|
||||
print(">>> 手动下单: LONGBRIDGE_REGION=ap LONGBRIDGE_TRADE_ENABLED=true proxychains4 -f ~/.proxychains/proxychains.conf ~/.local/bin/longbridge --profile lb_real buy/sell <SYM> --qty N --price P -y")
|
||||
elif cmd == 'cancel':
|
||||
cmd_cancel()
|
||||
else:
|
||||
print(f"未知命令: {cmd}")
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user