---
name: longbridge-python-sdk
description: 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_`.

## 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):
   ```bash
   # 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):
   ```python
   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
```python
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)
```python
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.)
```python
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)
```python
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, 股息率, 市值等
```python
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, 每股净资产, 股本
```python
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 — 自选列表
```python
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
```python
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
```python
# 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)
```

### submit_order Signature
```python
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)
```python
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
```python
resp = ctx.quote(['O.US', '823.HK', 'JEPI.US'])
for q in resp:
    print(f'{q.symbol}: ${q.last_done}')
```

### Get Holdings
```python
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`.

## Common Pitfalls
## Valuation Metrics (calc_indexes)

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

```python
from longport.openapi import CalcIndex

indexes = [
    CalcIndex.PeTtmRatio,        # PE TTM
    CalcIndex.PbRatio,           # PB
    CalcIndex.DividendRatioTtm,  # 股息率 TTM (%)
    CalcIndex.TotalMarketValue,  # 总市值
    CalcIndex.TurnoverRate,      # 换手率
    CalcIndex.VolumeRatio,       # 量比
    CalcIndex.ChangeRate,        # 涨跌幅 (%)
]

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}, 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)

```python
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:

```python
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)`
- `history_candlesticks_by_offset(symbol, period, adjust_type, backward, count)`

## 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.
  ```python
  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.
- **China Mainland Geo-Block (Error 602315)**: LongPort API blocks trading from mainland China IPs. Error: `"Due to Mainland China regulatory requirements, you are currently located in Mainland China and cannot perform this action."` (code 602315). Read-only operations (quotes, positions) may still work. **Fix**: Use WireGuard VPN via overseas VPS. On-demand scripts (`wg-trade`, `wg-on/off/status`) route only trading traffic through VPN. Full setup in `references/wireguard-proxy-setup.md`.
- **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:
  ```python
  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 stocks
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%)

```python
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
```python
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)}只)')
```
