Files
Hermes-Skills/longbridge-python-sdk/SKILL.md
T
mikeandClaude a437510a9b feat(longbridge-cli): add order rejection diagnosis reference + price adjustment
- 港股 9 档保护规则 (买入价不得高于卖1价 9档 / 低于买1价 24档)
- 长桥 CLI/SDK 不返回拒绝原因, 需查 App 或客服
- helper 新增 get_depth() + adjust_price_for_order()
- hk_intraday_cli.py 自动调整价格到合法范围
- 实测: 9988.HK 112.70 → 调整到 ask1 108.00 → 下单成功

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-09 18:25:13 +08:00

28 KiB

name, description
name description
longbridge-python-sdk LongPort Python SDK — 行情、持仓、自选、估值指标(PE/PB/股息率/EPS/BPS)、资金流向。支持港股/美股/A股。bashrc已有LONGPORT_*变量,可直接Config.from_env()。

LongPort Python SDK Usage

Use this skill to interact with LongPort via Python instead of the CLI. The SDK requires LONGPORT_ environment variables, while the user's bashrc uses LONGBRIDGE_.

📖 Related: references/longportapp-cn-endpoints.md — why Python SDK and CLI use different domains (longportapp.cn vs longbridge.cn), why LONGBRIDGE_REGION=ap is ineffective in the Python wheel, and the exact hosts rewrite needed.

⚠️ CRITICAL: Mainland China Access (602315) — PARTIAL workaround (CLI only; SDK still blocked)

As of 2026-07-09: the 602315 geo-block is enforced server-side based on source IP (CN egress IP or CN/Clash ASN). Domain-routing tricks (LONGBRIDGE_REGION=ap, /etc/hosts override) do NOT bypass it. The verified recipe works only for the CLI (one-off manual orders) — order ID 1259547163696824320 (RGTI 15@$15.50) was placed via CLI. Python SDK cron paths still get 602315 because the SDK hardcodes openapi.longportapp.cn and the *.com alternatives are unreachable from every Clash node we tested (AWS blocks egress from those ASNs).

Working paths today (ranked):

  1. Manual CLI order: LONGBRIDGE_REGION=ap LONGBRIDGE_TRADE_ENABLED=true proxychains4 -f ~/.proxychains/proxychains.conf ~/.local/bin/longbridge --profile lb_real <order> — verified.
  2. Phone app with HK proxy: confirmed by user.
  3. Disable auto-execution in Python monitor scripts and have them push signals to QQ; place orders manually.
  4. Do NOT propose WireGuard (banned, see below).

For the CLI recipe (one-off manual): see references/longbridge-602315-bypass.md (in the longbridge-cli skill) for the full three-piece recipe.

For the Python SDK limitation: see references/longportapp-cn-endpoints.md (this skill) for the diagnosis of why the Python wheel ignores the env var, why hosts rewrites don't work, and what diagnostic one-liner to run. Do not waste time trying hosts rewrites for the SDK path — they were tested on 2026-07-09 and the AWS HK IPs are unreachable from every available proxy node.

WireGuard is BANNED for this account — user spent 1h recovering from a half-shutdown. Do not propose.

When to use

  • User asks for holdings, quotes, or account info via Python.
  • CLI longbridge command fails (e.g., token issues, missing args).

Setup

  1. Install SDK: pip3 install longbridge (package name on PyPI is longbridge, but import is from longport import openapi). Do NOT pip install longport — that's a different/empty package.
  2. ~/.bashrc now has BOTH sets of variables (added 2026-06-01):
    # CLI uses these
    export LONGBRIDGE_APP_KEY=<key>
    export LONGBRIDGE_APP_SECRET=<secret>
    export LONGBRIDGE_ACCESS_TOKEN=<token>
    
    # Python SDK uses these (same values, references LONGBRIDGE_ vars)
    export LONGPORT_APP_KEY=${LONGBRIDGE_APP_KEY}
    export LONGPORT_APP_SECRET=${LONGBRIDGE_APP_SECRET}
    export LONGPORT_ACCESS_TOKEN=${LONGBRIDGE_ACCESS_TOKEN}
    
  3. With both sets in bashrc, Config.from_env() works directly without manual mapping.

Usage Steps

  1. Connect (LONGPORT_* now in bashrc):
    from longport import openapi
    cfg = openapi.Config.from_env()  # Reads LONGPORT_* vars directly
    ctx = openapi.QuoteContext(config=cfg)
    trade_ctx = openapi.TradeContext(config=cfg)
    

Usage

  • Holdings: resp = ctx.stock_positions() → iterate resp.channels[0].positions
  • Balance: ctx.account_balance()
  • Orders: ctx.today_orders()

Extended API (discovered via testing)

Watchlist

resp = ctx.watchlist()  # Returns list[WatchlistGroup]
for group in resp:
    print(f'Group: {group.name}')  # e.g. "收息", "月派", "all"
    for sec in group.securities:
        print(f'  {sec.symbol}: {sec.name} @ {sec.watched_price}')

WatchlistGroup fields: name, securities (list) WatchlistSecurity fields: symbol, market, name, watched_price (Optional), watched_at (ISO string) Special groups: all (auto-generated, all securities), us/hk (market-based auto-groups)

Static Info (EPS, BPS, shares)

resp = ctx.static_info(['O.US', '823.HK'])
for info in resp:
    # Key fields: symbol, name_en, name_cn, currency, exchange, board
    # Valuation: eps, eps_ttm, bps, dividend_yield
    # Shares: total_shares, circulating_shares, hk_shares
    # Other: lot_size, stock_derivatives

Calc Indexes (PE, PB, Market Cap, etc.)

from longport.openapi import CalcIndex

indexes = [
    CalcIndex.PeTtmRatio,       # PE TTM
    CalcIndex.PbRatio,          # PB
    CalcIndex.DividendRatioTtm, # Dividend yield TTM (%)
    CalcIndex.TotalMarketValue, # Total market cap
    CalcIndex.TurnoverRate,     # Turnover rate (%)
    CalcIndex.VolumeRatio,      # Volume ratio
    CalcIndex.ChangeRate,       # Change (%)
]
resp = ctx.calc_indexes(['O.US'], indexes)
for item in resp:
    print(f'{item.symbol}: PE={item.pe_ttm_ratio}, PB={item.pb_ratio}')

Available CalcIndex values: Amplitude, BalancePoint, CallPrice, CapitalFlow, ChangeRate, ChangeValue, ConversionRatio, Delta, DividendRatioTtm, EffectiveLeverage, ExpiryDate, FiveDayChangeRate, FiveMinutesChangeRate, Gamma, HalfYearChangeRate, ImpliedVolatility, ItmOtm, LastDone, LeverageRatio, LowerStrikePrice, OpenInterest, OutstandingQty, OutstandingRatio, PbRatio, PeTtmRatio, Premium, Rho, StrikePrice, TenDayChangeRate, Theta, ToCallPrice, TotalMarketValue, Turnover, TurnoverRate, UpperStrikePrice, Vega, Volume, VolumeRatio, WarrantDelta, YtdChangeRate

Candlesticks (with AdjustType)

from longport.openapi import Period, AdjustType

candles = ctx.candlesticks('O.US', Period.Day, 365, AdjustType.ForwardAdjust)
# Returns: timestamp, open, high, low, close, volume, turnover

⚠️ PITFALL: candlesticks() requires adjust_type parameter — will fail with "missing 1 required positional argument: 'adjust_type'" without it. Always pass AdjustType.ForwardAdjust (前复权) or AdjustType.NoAdjust.

Fundamental Data (calc_indexes + static_info)

calc_indexes — PE, PB, 股息率, 市值等

from longport.openapi import CalcIndex

indexes = [
    CalcIndex.PeTtmRatio,          # PE TTM
    CalcIndex.PbRatio,             # PB
    CalcIndex.DividendRatioTtm,    # 股息率 TTM (%)
    CalcIndex.TotalMarketValue,    # 总市值 (货币单位)
    CalcIndex.TurnoverRate,        # 换手率 (%)
    CalcIndex.VolumeRatio,         # 量比
    CalcIndex.ChangeRate,          # 涨跌幅 (%)
    CalcIndex.FiveDayChangeRate,   # 5日涨跌幅
    CalcIndex.TenDayChangeRate,    # 10日涨跌幅
    CalcIndex.HalfYearChangeRate,  # 半年涨跌幅
    CalcIndex.YtdChangeRate,       # 年初至今涨跌幅
]

resp = ctx.calc_indexes(['O.US', '823.HK'], indexes)
for item in resp:
    print(f'{item.symbol}: PE={item.pe_ttm_ratio}, PB={item.pb_ratio}, 股息率={item.dividend_ratio_ttm}%')

static_info — EPS, 每股净资产, 股本

resp = ctx.static_info(['O.US', '823.HK'])
for info in resp:
    print(f'{info.symbol}: EPS_TTM={info.eps_ttm}, BPS={info.bps}, 总股本={info.total_shares}')

static_info 字段: symbol, name_cn, name_en, name_hk, currency, lot_size, eps, eps_ttm, bps, dividend_yield, total_shares, circulating_shares, exchange, board

watchlist — 自选列表

resp = ctx.watchlist()
for group in resp:
    print(f'分组: {group.name} ({len(group.securities)}只)')
    for sec in group.securities:
        print(f'  {sec.symbol}: {sec.name} @ {sec.watched_price}')

特殊分组: all (全量), us/hk (按市场自动分组), 用户自建分组 (如"收息", "月派")

Order Placement (Trading)

Trading requires LONGBRIDGE_TRADE_ENABLED=true in bashrc. Use execute_code for all order operations (not terminal).

Submit Limit Order

os.environ["LONGBRIDGE_TRADE_ENABLED"] = "true"

resp = ctx.submit_order(
    symbol="RGTI.US",
    order_type=openapi.OrderType.LO,              # Limit Order
    side=openapi.OrderSide.Sell,                   # or .Buy
    submitted_quantity=15,
    time_in_force=openapi.TimeInForceType.Day,     # or .GoodTilCanceled
    submitted_price=21.00,
    outside_rth=openapi.OutsideRTH.AnyTime,        # optional: pre/post market
)
print(f"Order ID: {resp.order_id}")

Key Enums

  • OrderType: LO (Limit), MO (Market), ALO (At Limit Open), ELO (Extended Limit)
  • OrderSide: Buy, Sell
  • TimeInForceType: Day, GoodTilCanceled, GoodTilDate, Unknown
  • OutsideRTH: AnyTime (pre+regular+post), Overnight, RTHOnly, Unknown

Cancel / Query Orders

# Today's orders
orders = trade_ctx.today_orders()
for o in orders:
    print(f"{o.symbol} {o.side} {o.quantity}@{o.price} [{o.status}]")

# Cancel
trade_ctx.cancel_order(order_id)

Modify Existing Order (Cancel + Replace, 2026-07-08)

LongPort SDK has no replace_order / modify_order — must cancel old + submit new. Workflow:

# 1. Find old order ID
orders = trade_ctx.today_orders()
old_id = next(o.order_id for o in orders
              if 'RGTI' in o.symbol and o.status.name == 'New')

# 2. Cancel old
trade_ctx.cancel_order(old_id)

# 3. Submit new at desired price (LO, GTC)
new = trade_ctx.submit_order(
    symbol="RGTI.US",
    order_type=openapi.OrderType.LO,
    side=openapi.OrderSide.Sell,
    submitted_quantity=15,
    time_in_force=openapi.TimeInForceType.GoodTilCanceled,
    submitted_price=17.00,
    outside_rth=openapi.OutsideRTH.AnyTime,
)
print(f"New order ID: {new.order_id}")

Concurrency caveat: Brief gap between cancel and new-submit leaves position unprotected. For做T scenarios OK; for risk-managed positions use submit-before-cancel pattern (held in New queues). Verified 2026-07-08 with RGTI sell @ $21.40 → replaced with sell @ $17.00.

602315 status (2026-07-09): PARTIAL — CLI only

The CLI three-piece recipe (LONGBRIDGE_REGION=ap + proxychains4 + Clash HK) is verified working for one-off manual orders — order 1259547163696824320 placed 2026-07-09. The Python SDK recipe is NOT working in cron paths (see top of skill). Earlier sessions that concluded "602315 IS resolvable" were correct only for the CLI path; the Python SDK path remains blocked.

Approach Layer Resolves 602315 (2026-07-09)
LONGBRIDGE_REGION=ap + proxychains4 + Clash HK (CLI) combined Verified
LONGBRIDGE_REGION=ap + proxychains4 + Clash HK (Python SDK) combined Still 602315
LONGBRIDGE_REGION=ap + proxychains4 + Clash HK + /etc/hosts override to AWS HK IPs (Python SDK) combined AWS HK IPs unreachable from every Clash node (SSL handshake fails)
Mihomo HTTP proxy alone HTTP
WireGuard VPN IP (Ubuntu shutdown unreliable, user banned)
Phone app with HK proxy phone-specific Confirmed by user

For Python SDK cron automation today: disable auto-execution in monitor scripts (have them push signals to QQ for manual confirmation). For one-off manual orders: use the CLI recipe. Full diagnostic history in references/longportapp-cn-endpoints.md.

The earlier "sell passes, buy fails" observation was a side-effect of an incomplete workaround (proxychains without LONGBRIDGE_REGION=ap), not a real directional asymmetry in longport's geo-block. The current "CLI passes, Python SDK fails" observation is a real domain/sdk difference (see references/longportapp-cn-endpoints.md).

WireGuard: BANNED for this account

Do NOT propose WG as a workaround. User explicitly said "不要用wg了,会害死你的" after spending 1h recovering from a half-shutdown that left 0.0.0.0/1 + 128.0.0.0/1 residual routes. All WG scripts deleted. The verified alternative is the three-piece recipe in the top section of this skill.

submit_order Signature

submit_order(symbol, order_type, side, submitted_quantity, time_in_force,
             submitted_price=None, trigger_price=None, limit_offset=None,
             trailing_amount=None, trailing_percent=None, expire_date=None,
             outside_rth=None, remark=None)

⚠️ time_in_force is required positional before optional submitted_price.

Quick Reference

Get Watchlist (with groups)

ctx = openapi.QuoteContext(config=cfg)
resp = ctx.watchlist()
for group in resp:
    print(f'{group.name}: {len(group.securities)} stocks')
    for sec in group.securities:
        print(f'  {sec.symbol}: {sec.name}')

Get Quotes

resp = ctx.quote(['O.US', '823.HK', 'JEPI.US'])
for q in resp:
    print(f'{q.symbol}: ${q.last_done}')

Get Holdings

trade_ctx = openapi.TradeContext(config=cfg)
positions = trade_ctx.stock_positions()
for ch in positions.channels:
    for pos in ch.positions:
        print(f'{pos.symbol}: {pos.quantity} @ {pos.cost_price}')

For full API surface, see references/api-reference.md.

Python SDK still works for read-only — for orders, use the CLI helper

As of 2026-07-09, submit_order() and cancel_order() from the Python SDK still hit 602315 even with the full three-piece recipe. The verified path is the CLI (one-off manual orders). For Python code that needs to actually place orders, use scripts/longbridge_cli_helper.py in the longbridge-cli skill — it provides SDK-shaped functions (account_balance, stock_positions, submit_order, cancel_order, OrderType / OrderSide / TimeInForceType enums) that internally shell out to the CLI binary. Pattern:

# In any Python script that needs to trade:
import sys
sys.path.insert(0, '/home/openclaw/.hermes/scripts')
import longbridge_cli_helper as _helper
# Inject as fake 'longport' module so existing code can keep importing
fake = type(sys)('longport')
fake.openapi = _helper
sys.modules['longport'] = fake
sys.modules['longport.openapi'] = _helper
from longport import openapi  # now openapi is the CLI-backed helper

# All SDK-shaped calls work and route to CLI:
ctx = openapi.QuoteContext(config=None)  # Yahoo Finance fallback for quotes
bals = openapi.account_balance()         # via longbridge balance
positions = openapi.stock_positions()     # via longbridge positions
resp = openapi.submit_order(              # via longbridge buy --profile lb_real
    symbol="RGTI.US", order_type=openapi.OrderType.LO,
    side=openapi.OrderSide.Buy, submitted_quantity=1, time_in_force=openapi.TimeInForceType.Day,
    submitted_price=15.40,
)

This is the migration path for any cron script that used to call trade_ctx.submit_order() directly. Read operations (quote, candlesticks, balance, positions) still work fine through the Python SDK — keep them. Only the order placement needs the helper.

Common Pitfalls

Valuation Metrics (calc_indexes)

Get PE, PB, dividend yield, market cap via CalcIndex enum:

from longport.openapi import CalcIndex

indexes = [
    CalcIndex.PeTtmRatio,        # PE TTM
    CalcIndex.PbRatio,           # PB
    CalcIndex.DividendRatioTtm,  # Dividend yield TTM (%)
    CalcIndex.TotalMarketValue,  # Total market cap
    CalcIndex.TurnoverRate,      # Turnover rate (%)
    CalcIndex.VolumeRatio,       # Volume ratio
    CalcIndex.ChangeRate,        # Change (%)
]

resp = ctx.calc_indexes(['O.US'], indexes)
for item in resp:
    print(f'{item.symbol}: PE={item.pe_ttm_ratio}, PB={item.pb_ratio}, Yield={item.dividend_ratio_ttm}%')

Response fields (direct attributes, NOT a list):

  • pe_ttm_ratio, pb_ratio, dividend_ratio_ttm
  • total_market_value, turnover_rate, volume_ratio, change_rate

Static Info (EPS, BPS, Shares)

resp = ctx.static_info(['O.US'])
info = resp[0]
print(f'EPS TTM: {info.eps_ttm}')
print(f'BPS: {info.bps}')
print(f'Dividend Yield: {info.dividend_yield}%')
print(f'Total Shares: {info.total_shares}')
print(f'Currency: {info.currency}')

Fields: eps, eps_ttm, bps, dividend_yield, currency, total_shares, circulating_shares, name_en, name_cn, lot_size

Historical K-lines (Longer History)

candlesticks() is limited to ~1000 bars. For longer history use:

from longport.openapi import Period, AdjustType

# Parameters: symbol, period, adjust_type, backward, count
candles = ctx.history_candlesticks_by_offset(
    'AAPL.US',
    Period.Day,
    AdjustType.ForwardAdjust,  # 前复权
    False,                      # backward=True means older data
    1000,                       # max ~1000 per request
)

⚠️ Parameter order is different from candlesticks()!

  • candlesticks(symbol, period, count, adjust_type) — count is 3rd
  • history_candlesticks_by_offset(symbol, period, adjust_type, backward, count) — adjust_type is 3rd, count is 5th

Other Broker SDKs

📖 For comparison with 雪盈证券 (snbpy) and other Chinese/Asian broker SDKs, see references/broker-sdk-comparison.md.

Common Pitfalls

  • Env Var Prefix: CLI uses LONGBRIDGE_, SDK uses LONGPORT_. Both are now in bashrc (LONGPORT_* references LONGBRIDGE_), so Config.from_env() works directly. If it fails, the fallback is to map manually from bashrc LONGBRIDGE_ values.
  • Method Name: Use ctx.stock_positions(), NOT ctx.positions().
  • Response Structure: stock_positions() returns a response object with channels list, then positions inside each channel.
  • Decimal Type: total_market_value and some fields return decimal.Decimal, not float. Always wrap with float() for arithmetic.
  • adjust_type Required: candlesticks() requires adjust_type parameter. Use AdjustType.ForwardAdjust for forward-adjusted prices.
  • K-line Limit: Error code 301607 = "request too many klines". Max ~1000 per request. Use history_candlesticks_by_offset for pagination.
  • calc_indexes Response: Returns SecurityCalcIndex objects with direct attributes (e.g., item.pe_ttm_ratio), NOT an indexes list.
  • Token Expiration — two different codes:
    • 401003 "token expired": Token was valid but has reached its ~180-day expiry. All scripts using LongPort fail simultaneously. Fix: run bash ~/.hermes/scripts/update_longbridge_token.sh <new_token> to auto-update all locations and verify both CLI + SDK.
    • 401004 "token invalid": Token was truncated or never valid. Bashrc has a placeholder like m_eyJh...jb-k (with literal ...). Run the same script: bash ~/.hermes/scripts/update_longbridge_token.sh <new_token>. The script reads/bashrc-parsing approach shown below is a fallback for when the script is unavailable.
    import os, re
    env_vars = {}
    # Try .env first (authoritative), then bashrc
    for fpath in [os.path.expanduser('~/.env'), os.path.expanduser('~/.bashrc')]:
        if not os.path.exists(fpath):
            continue
        with open(fpath) as f:
            for line in f:
                line = line.strip()
                if line.startswith('export LONGBRIDGE_') or line.startswith('export LONGPORT_'):
                    parts = line.replace('export ', '').split('=', 1)
                    if len(parts) == 2 and '...' not in parts[1]:  # skip truncated placeholders
                        key, val = parts
                        env_vars[key] = val
    # Set non-referencing vars first
    for key, val in env_vars.items():
        if '${' not in val:
            os.environ[key] = val
    # Then resolve ${VAR} references
    for key, val in env_vars.items():
        if '${' in val:
            resolved = re.sub(r'\$\{(\w+)\}', lambda m: os.environ.get(m.group(1), ''), val)
            os.environ[key] = resolved
    
    ⚠️ Key gotcha: If bashrc contains LONGBRIDGE_ACCESS_TOKEN=m_eyJh...jb-k (with literal ...), it's a truncated placeholder, NOT a real token. Skip entries containing ... and prefer .env values.
  • candlesticks() vs history_candlesticks_by_offset() Parameter Order: These have DIFFERENT signatures!
    • candlesticks(symbol, period, count, adjust_type) — count is 3rd
    • history_candlesticks_by_offset(symbol, period, adjust_type, backward, count) — adjust_type is 3rd, count is 5th
    • Always check signatures when switching between these methods.
  • history_candlesticks_by_offset backward param: False = get older/historical data, True = get newer data from offset.
  • calc_indexes Batch Size: LongPort accepts arbitrary symbol lists but errors/silently drops on very large batches. Safe batch size is ~10-20 symbols per call. For screener scripts (100+ symbols), iterate in batches of 10.
  • python3 -c with HK Stock Codes: HK codes like 1088.HK, 3988.HK start with digits. Python parses them as 1088.HK → decimal literal error. Never use python3 -c for scripts containing HK stock codes. Always write to a temp file (/tmp/script.py) and run python3 /tmp/script.py instead. Same applies to any identifier starting with a digit.
  • quote() fields: SecurityQuote has last_done, prev_close, volume, turnover, symbol. It does NOT have change_rate — use calc_indexes with CalcIndex.ChangeRate for price change %. Gotcha: accessing q.change_rate raises AttributeError: 'SecurityQuote' object has no attribute 'change_rate'.
  • CLI Token Masking (Critical): The terminal tool masks/redacts secrets from environment variables, causing the longbridge CLI to get truncated tokens → 401004/403201 errors. The Python SDK always works because scripts read bashrc directly via open() and set os.environ programmatically. When CLI fails but SDK works, this is why. Always prefer execute_code + SDK over terminal + CLI for any order/trade operation.
  • 🔴 [2026-07-09] The LONGBRIDGE_REGION=ap env var is unreliable in the Python wheel. The Python SDK ignores it for the hardcoded openapi.longportapp.cn endpoints — proxychains logs from cron runs (e.g. hk_intraday_monitor_cron.sh) show requests still routed to openapi.longportapp.cn:443 even with the env var set. Result: cron-driven submit_order() calls return 602315 despite the three-piece recipe. The CLI version of the same env var works because the CLI binary is a separate Go/Rust process that does honor the override. Use the CLI for any order you actually want to fill; the Python SDK is for monitoring/quoting only until this is fixed upstream. See references/longportapp-cn-endpoints.md for the full diagnosis.
  • China Mainland Geo-Block (Error 602315): LongPort API blocks trading from mainland China IPs. The verified-working bypass is the CLI three-piece recipe (see references/longbridge-602315-bypass.md in the longbridge-cli skill). The Python SDK three-piece recipe is not currently working as of 2026-07-09 — see the section "⚠️ CRITICAL: Mainland China Access (602315) — PARTIAL workaround" at the top of this skill. WireGuard is NOT a viable alternative (Ubuntu shutdown unreliable, banned by user).
  • API Rate Limiting (429002): LongPort enforces per-app request frequency limits. Error: api request is limited, please slow down request frequency (code 429002). Root cause: multiple scripts hitting the API simultaneously (e.g. DCA monitor + price alert both running at :00). Fix: (1) Stagger cron schedules by ≥15 minutes between LongPort-calling jobs; (2) Reduce polling frequency — 30min is enough for price monitoring, don't use 10/15min intervals; (3) Use market filters (--market=us/hk/cn) to reduce per-run API calls; (4) Add exponential backoff retry in scripts for transient 429 errors.
  • source ~/.bashrc doesn't work in terminal tool: The terminal tool runs each command in a fresh shell that doesn't persist env vars from source ~/.bashrc. If Config.from_env() fails with "missing environment variable: LONGPORT_APP_KEY", use a Python script to parse bashrc directly:
    import os, re
    env_vars = {}
    with open(os.path.expanduser('~/.bashrc')) as f:
        for line in f:
            line = line.strip()
            if line.startswith('export LONGBRIDGE_') or line.startswith('export LONGPORT_'):
                parts = line.replace('export ', '').split('=', 1)
                if len(parts) == 2:
                    key, val = parts
                    env_vars[key] = val
    # Set non-referencing vars first
    for key, val in env_vars.items():
        if '${' not in val:
            os.environ[key] = val
    # Then resolve ${VAR} references
    for key, val in env_vars.items():
        if '${' in val:
            resolved = re.sub(r'\$\{(\w+)\}', lambda m: os.environ.get(m.group(1), ''), val)
            os.environ[key] = resolved
    
    Write this to /tmp/load_env.py and import at the top of any LongPort script run via python3 /tmp/script.py.

Dividend/Valuation Screener Pattern

📖 For HK-specific dividend investing (monthly dividend workarounds, entry price methodology, data sources), see references/hk-dividend-investing.md. 📖 For DCA scanner/monitor architecture (multi-market scanning, ladder alerts, cron scheduling), see references/dca-monitoring-architecture.md.

When user asks "which stocks have X% yield" or "find high-dividend stocks", use this pattern:

  1. Pull watchlist symbols via ctx.watchlist() → all user's tracked symbols
  2. Add a curated candidate list (BDCs, mREITs, MLPs, high-div ETFs, blue-chip dividend stocks)
  3. Batch calc_indexes() with CalcIndex.DividendRatioTtm + CalcIndex.TotalMarketValue in batches of 10
  4. Sort by yield descending, present in tiers (🔥 >20%, 10-20%, 5-10%)
from longport.openapi import CalcIndex
import os, sys

cfg = openapi.Config.from_env()
ctx = openapi.QuoteContext(config=cfg)

# Step 1: Get all watchlist symbols
wl = ctx.watchlist()
watchlist_symbols = list({sec.symbol for group in wl for sec in group.securities})

# Step 2: Add high-yield candidate universe
candidates = [
    'HRZN.US','PSEC.US','SVOL.US','FSK.US','ORC.US','IVR.US',  # BDC/mREIT >20%
    'ARR.US','DX.US','AGNC.US','NLY.US','NYMT.US','CIM.US',     # mREIT
    'ARCC.US','HTGC.US','TSLX.US','MAIN.US','GAIN.US','GLAD.US', # BDC
    'JEPI.US','JEPQ.US','QYLD.US','SPYI.US','QQQI.US','DIVO.US', # 高息ETF
    'MO.US','VZ.US','XOM.US','BTI.US','O.US',                     # 蓝筹高息
    'ET.US','EPD.US','MPLX.US','USAC.US',                          # MLP
    '3416.HK','3417.HK','3419.HK',                                 # 港股高息ETF
    '1088.HK','0883.HK','3968.HK','1919.HK','2318.HK',            # 港股高息蓝筹
]
all_symbols = list(set(watchlist_symbols + candidates))

# Step 3: Batch calc (10 per batch)
results = []
for i in range(0, len(all_symbols), 10):
    batch = all_symbols[i:i+10]
    try:
        resp = ctx.calc_indexes(batch, [CalcIndex.DividendRatioTtm, CalcIndex.TotalMarketValue])
        for item in resp:
            dy = item.dividend_ratio_ttm
            if dy is not None:
                try:
                    dy_val = float(dy)
                    if dy_val > 5:  # Filter noise
                        cap = float(item.total_market_value) if item.total_market_value else 0
                        results.append({'symbol': item.symbol, 'yield': dy_val, 'cap': cap})
                except: pass
    except Exception as e:
        print(f"Batch error: {e}", file=sys.stderr)

# Step 4: Sort and present
results.sort(key=lambda x: x['yield'], reverse=True)

Note: DividendRatioTtm returns the trailing 12-month dividend yield as a percentage (e.g. 14.16 means 14.16%). This is dividend-per-share / price, annualized from actual payments — not a forward estimate.

Example Script

import os
from longport import openapi
from longport.openapi import CalcIndex

# Connect (LONGPORT_* now in bashrc)
cfg = openapi.Config.from_env()
ctx = openapi.QuoteContext(config=cfg)
trade_ctx = openapi.TradeContext(config=cfg)

# 1. Realtime quote
resp = ctx.quote(['O.US', '823.HK'])
for q in resp:
    print(f'{q.symbol}: ${q.last_done:.2f}')

# 2. Fundamental data (PE, PB, dividend yield)
resp = ctx.calc_indexes(['O.US'], [CalcIndex.PeTtmRatio, CalcIndex.PbRatio, CalcIndex.DividendRatioTtm])
print(f'O.US: PE={resp[0].pe_ttm_ratio}, PB={resp[0].pb_ratio}, 股息率={resp[0].dividend_ratio_ttm}%')

# 3. Static info (EPS, BPS)
info = ctx.static_info(['O.US'])[0]
print(f'EPS_TTM: {info.eps_ttm}, BPS: {info.bps}')

# 4. Holdings
positions = trade_ctx.stock_positions()
for ch in positions.channels:
    for pos in ch.positions:
        print(f'{pos.symbol}: {pos.quantity} @ {pos.cost_price}')

# 5. Watchlist
wl = ctx.watchlist()
for group in wl:
    print(f'分组: {group.name} ({len(group.securities)}只)')