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:
2026-07-09 18:19:58 +08:00
co-authored by Claude
parent 0d17865a7c
commit 32d5d7dc0b
11 changed files with 1034 additions and 498 deletions
@@ -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])