Initial commit: Hermes Agent skills collection

- Trading skills (OKX, dividend, lottery, quantitative)
- Creative skills (ASCII art, diagrams, video)
- Development skills (GitHub, debugging, TDD)
- Research skills (arXiv, blog monitoring)
- Productivity skills (email, documents, notes)
- MCP integration skills
- Custom user skills
This commit is contained in:
Hermes Skills Manager
2026-07-05 02:31:15 -04:00
commit 6770bc9b9d
908 changed files with 239614 additions and 0 deletions
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "longbridge-kit",
"installedVersion": "1.0.2",
"installedAt": 1774800584243
}
+530
View File
@@ -0,0 +1,530 @@
# longbridge
English | [中文](#中文)
---
## English
A command-line tool built on the [LongPort OpenAPI](https://open.longportapp.com/) Python SDK. Supports real-time quotes, account positions, order management, and market data. Ships with a `SKILL.md` for direct integration as a Claude Code Skill.
---
### Features
- **Quotes**: Real-time prices, order book, trade ticks, candlesticks, static info
- **Account**: Balance & net assets, stock positions, fund positions
- **Orders**: Today's / historical orders (read-only); limit buy/sell, cancel (requires trade permission)
- **Market**: Market temperature, capital flow, capital distribution, option chain
---
### Authentication
Apply for an API Key at [open.longportapp.com](https://open.longportapp.com/), then configure credentials using either method below.
#### Method 1: .env file (recommended)
Create a `.env` file in the current directory or home directory (`~/.env`):
```bash
LONGBRIDGE_APP_KEY=your_app_key
LONGBRIDGE_APP_SECRET=your_app_secret
LONGBRIDGE_ACCESS_TOKEN=your_access_token
# Optional: enable trading (default is read-only)
LONGBRIDGE_TRADE_ENABLED=true
```
Search order: **current directory****home directory**. Values in `.env` do **not** override existing system environment variables (system env takes precedence).
#### Method 3: Multiple accounts with `--profile`
Use named profile files to switch between accounts (e.g., paper trading vs live):
```bash
# Create .paper.env and .live.env in the current or home directory
# Each file contains the credentials for that account
longbridge --profile paper balance
longbridge --profile live positions
```
The `--profile paper` flag loads `.paper.env` instead of `.env`. If the file is not found, an error is raised.
#### Method 2: Shell environment variables
```bash
export LONGBRIDGE_APP_KEY="your_app_key"
export LONGBRIDGE_APP_SECRET="your_app_secret"
export LONGBRIDGE_ACCESS_TOKEN="your_access_token"
```
Add to `~/.zshrc` or `~/.bashrc` for persistence.
| Variable | Required | Description |
|----------|----------|-------------|
| `LONGBRIDGE_APP_KEY` | ✅ | App Key |
| `LONGBRIDGE_APP_SECRET` | ✅ | App Secret |
| `LONGBRIDGE_ACCESS_TOKEN` | ✅ | Access Token |
| `LONGBRIDGE_TRADE_ENABLED` | Optional | Set to `true` to enable trading (default: read-only) |
---
### Read-only Mode & Trade Permission
**Default is read-only**: `buy`, `sell`, `cancel` are disabled unless `LONGBRIDGE_TRADE_ENABLED=true` is set:
```
Error: 当前为只读模式,下单/撤单操作已禁用。
如需开启交易权限,请设置环境变量:
export LONGBRIDGE_TRADE_ENABLED=true
⚠️ 开启后请确保操作正确,下单指令将直接提交至长桥交易系统。
```
Read commands (`orders`, `history-orders`, `positions`, etc.) are always available without extra configuration.
---
### Usage
All commands support `--json` for machine-readable output.
#### Quotes
```bash
# Real-time quotes (multiple symbols)
longbridge quote AAPL.US 700.HK
longbridge quote AAPL.US --json
# Order book (top 5 bid/ask)
longbridge depth 700.HK
# Trade ticks
longbridge trades 700.HK --count 20
# Candlesticks
# period: 1m 5m 15m 30m 60m day week month quarter year
longbridge candlesticks AAPL.US day --count 30
longbridge candlesticks 700.HK 60m --count 100 --json
# Static security info
longbridge info AAPL.US 700.HK
```
#### Account & Positions
```bash
longbridge balance
longbridge balance --json
longbridge positions
longbridge funds
```
#### Order Management
```bash
# Read-only — no trade permission required
longbridge orders
longbridge history-orders --start 2026-01-01 --end 2026-03-14
# Requires LONGBRIDGE_TRADE_ENABLED=true
# Limit buy (confirmation prompt before execution)
longbridge buy AAPL.US --qty 100 --price 180.0
# Limit sell (confirmation prompt)
longbridge sell 700.HK --qty 500 --price 320.0
# Cancel order (confirmation prompt)
longbridge cancel 701234567890
# Skip confirmation for programmatic / scripted use
longbridge buy AAPL.US --qty 100 --price 180.0 --yes
longbridge sell 700.HK --qty 500 --price 320.0 -y
```
#### Market Data
```bash
# Market temperature (US / HK / CN / SG)
longbridge temperature US
# Capital flow
longbridge capital-flow 700.HK
# Capital distribution (large / medium / small orders)
longbridge capital-dist 700.HK
# Option chain expiry dates
longbridge option-chain AAPL.US
```
---
### AI Agent Integration
#### OpenClaw Skill
Copy `SKILL.md` to your OpenClaw skills directory to register it as a Clawdbot Skill:
```bash
mkdir -p ~/.openclaw/skills/longbridge-cli
cp SKILL.md ~/.openclaw/skills/longbridge-cli/SKILL.md
```
Once installed, Claude can invoke longbridge-cli commands automatically when you ask about account balances, positions, quotes, or orders.
#### Usage as a Skill
This project includes `SKILL.md` and can be registered as a Clawdbot Skill for AI-driven workflows. All commands support `--json` for structured output:
```bash
longbridge positions --json
longbridge quote AAPL.US 700.HK --json
longbridge orders --json
```
Example JSON output (`longbridge quote AAPL.US --json`):
```json
[
{
"symbol": "AAPL.US",
"last_done": 213.49,
"open": 211.50,
"high": 214.20,
"low": 210.30,
"volume": 52345678,
"turnover": 11162345678.0,
"change_rate": 0.94,
"prev_close": 211.51
}
]
```
---
### Symbol Format
| Market | Format | Examples |
|--------|--------|---------|
| US | `TICKER.US` | `AAPL.US`, `NVDA.US` |
| HK | `4-digit code.HK` | `0700.HK`, `9988.HK` |
---
### Project Structure
```
longbridge-cli/
├── longbridge_cli/
│ ├── __init__.py
│ ├── __main__.py # python -m entry point
│ ├── cli.py # Click root command group
│ ├── config.py # Config initialization
│ ├── formatters.py # Unified text/json output
│ └── commands/
│ ├── quote.py # Quote commands
│ ├── account.py # Account & positions
│ ├── order.py # Order management
│ └── market.py # Market data
├── SKILL.md # Claude Code Skill document
├── _meta.json # Skill registration
├── LICENSE
├── requirements.txt
└── pyproject.toml
```
---
### Dependencies
```
longbridge # LongPort official Python SDK
click>=8.0 # CLI framework
rich>=13.0 # Terminal output formatting
```
---
### Notes
- **Read-only mode**: `buy`/`sell`/`cancel` require `LONGBRIDGE_TRADE_ENABLED=true`
- **Trade confirmation**: Even with trade permission enabled, all write commands prompt for confirmation before executing. Use `--yes` / `-y` to skip the prompt for scripted or programmatic use
- **Multi-account profiles**: Use `longbridge --profile <name>` to load `.<name>.env` credentials (e.g., `--profile paper` loads `.paper.env`). Useful for switching between paper trading and live accounts
- **HK symbol format**: Must use 4-digit format, e.g. `0700.HK` (leading zero required)
- **Error handling**: SDK exceptions are caught and displayed as friendly messages
- **Decimal serialization**: Decimal values are automatically converted to float in JSON output
- **.env precedence**: System environment variables take precedence over `.env` file values (system env is not overwritten)
---
### License
MIT
---
## 中文
基于[长桥 LongPort OpenAPI](https://open.longportapp.com/) Python SDK 的命令行工具,支持行情查询、账户持仓、订单管理、市场数据。封装为 Claude Code Skill,可直接被 AI Agent 调用。
---
### 功能概览
- **行情**:实时报价、盘口、逐笔成交、K 线、标的信息
- **账户**:余额净资产、股票持仓、基金持仓
- **订单**:今日/历史订单查询(只读);限价买入/卖出、撤单(需开启交易权限)
- **市场**:市场温度、资金流向、资金分布、期权链
---
### 认证配置
在长桥开放平台 [open.longportapp.com](https://open.longportapp.com/) 申请 API Key 后,通过以下任一方式配置凭证。
#### 方式 1.env 文件(推荐)
在当前目录或用户主目录创建 `.env` 文件:
```bash
LONGBRIDGE_APP_KEY=your_app_key
LONGBRIDGE_APP_SECRET=your_app_secret
LONGBRIDGE_ACCESS_TOKEN=your_access_token
# 可选:开启交易权限,默认为只读模式,不允许交易下单
LONGBRIDGE_TRADE_ENABLED=true
```
查找顺序:**当前工作目录** → **用户主目录**`.env` 中的值**不会**覆盖已有的系统环境变量(系统环境变量优先)。
#### 方式 3:多账户 `--profile` 切换
使用具名 profile 文件切换账户(如模拟盘与实盘):
```bash
# 在当前目录或主目录分别创建 .paper.env 和 .live.env
# 每个文件填写对应账户的凭证
longbridge --profile paper balance
longbridge --profile live positions
```
`--profile paper` 会加载 `.paper.env` 而不是 `.env`。若文件不存在则报错。
#### 方式 2Shell 环境变量
```bash
export LONGBRIDGE_APP_KEY="your_app_key"
export LONGBRIDGE_APP_SECRET="your_app_secret"
export LONGBRIDGE_ACCESS_TOKEN="your_access_token"
```
写入 `~/.zshrc``~/.bashrc` 永久生效。
| 环境变量 | 必填 | 说明 |
|----------|------|------|
| `LONGBRIDGE_APP_KEY` | ✅ | 应用 App Key |
| `LONGBRIDGE_APP_SECRET` | ✅ | 应用 App Secret |
| `LONGBRIDGE_ACCESS_TOKEN` | ✅ | 用户访问 Token |
| `LONGBRIDGE_TRADE_ENABLED` | 可选 | 设为 `true` 开启交易权限(默认只读) |
---
### 只读模式与交易权限
**默认为只读模式**`buy``sell``cancel` 命令在未配置交易权限时会被拒绝:
```
Error: 当前为只读模式,下单/撤单操作已禁用。
如需开启交易权限,请设置环境变量:
export LONGBRIDGE_TRADE_ENABLED=true
⚠️ 开启后请确保操作正确,下单指令将直接提交至长桥交易系统。
```
查询类命令(`orders``history-orders``positions` 等)不受限制,无需额外配置。
---
### 使用方法
所有命令均支持 `--json` 选项输出机器可读的 JSON 格式。
#### 行情
```bash
# 实时报价(支持批量)
longbridge quote AAPL.US 700.HK
longbridge quote AAPL.US --json
# 盘口(买5卖5
longbridge depth 700.HK
# 逐笔成交
longbridge trades 700.HK --count 20
# K 线(period 可选:1m 5m 15m 30m 60m day week month quarter year
longbridge candlesticks AAPL.US day --count 30
longbridge candlesticks 700.HK 60m --count 100 --json
# 标的静态信息
longbridge info AAPL.US 700.HK
```
#### 账户与持仓
```bash
longbridge balance
longbridge balance --json
longbridge positions
longbridge funds
```
#### 订单管理
```bash
# 只读,无需交易权限
longbridge orders
longbridge history-orders --start 2026-01-01 --end 2026-03-14
# 以下命令需设置 LONGBRIDGE_TRADE_ENABLED=true
# 限价买入(执行前有确认提示)
longbridge buy AAPL.US --qty 100 --price 180.0
# 限价卖出(执行前有确认提示)
longbridge sell 700.HK --qty 500 --price 320.0
# 撤销订单(执行前有确认提示)
longbridge cancel 701234567890
# 跳过确认提示(程序化/脚本调用时使用)
longbridge buy AAPL.US --qty 100 --price 180.0 --yes
longbridge sell 700.HK --qty 500 --price 320.0 -y
```
#### 市场数据
```bash
# 市场温度(US / HK / CN / SG
longbridge temperature US
# 资金流向
longbridge capital-flow 700.HK
# 资金分布(大单/中单/小单)
longbridge capital-dist 700.HK
# 期权链到期日
longbridge option-chain AAPL.US
```
---
### AI Agent 集成
#### OpenClaw Skill 接入
`SKILL.md` 复制到 OpenClaw skills 目录,即可注册为 Clawdbot Skill
```bash
mkdir -p ~/.openclaw/skills/longbridge-cli
cp SKILL.md ~/.openclaw/skills/longbridge-cli/SKILL.md
```
安装后,当你询问账户余额、持仓、行情、订单等问题时,Claude 会自动调用 longbridge-cli 命令。
#### 作为 Skill 使用
本项目包含 `SKILL.md`,可直接注册为 Clawdbot Skill 供 AI 调用。所有命令均支持 `--json`,输出结构化数据,方便 AI 解析:
```bash
longbridge positions --json
longbridge quote AAPL.US 700.HK --json
longbridge orders --json
```
JSON 输出示例(`longbridge quote AAPL.US --json`):
```json
[
{
"symbol": "AAPL.US",
"last_done": 213.49,
"open": 211.50,
"high": 214.20,
"low": 210.30,
"volume": 52345678,
"turnover": 11162345678.0,
"change_rate": 0.94,
"prev_close": 211.51
}
]
```
---
### 代码标的格式
| 市场 | 格式 | 示例 |
|------|------|------|
| 美股 | `代码.US` | `AAPL.US`, `NVDA.US` |
| 港股 | `4位代码.HK` | `0700.HK`, `9988.HK` |
---
### 项目结构
```
longbridge-cli/
├── longbridge_cli/
│ ├── __init__.py
│ ├── __main__.py # python -m 入口
│ ├── cli.py # Click 根命令组
│ ├── config.py # Config 初始化
│ ├── formatters.py # text/json 统一输出
│ └── commands/
│ ├── quote.py # 行情命令
│ ├── account.py # 账户与持仓
│ ├── order.py # 订单管理
│ └── market.py # 市场数据
├── SKILL.md # Claude Code Skill 文档
├── _meta.json # Skill 注册信息
├── LICENSE
├── requirements.txt
└── pyproject.toml
```
---
### 依赖
```
longbridge # 长桥官方 Python SDK
click>=8.0 # CLI 框架
rich>=13.0 # 终端美化输出
```
---
### 注意事项
- **只读模式**:默认禁止 `buy`/`sell`/`cancel`,需设置 `LONGBRIDGE_TRADE_ENABLED=true` 才能下单
- **下单/撤单**:开启交易权限后,执行前仍有二次确认提示,防止误操作。脚本/程序化调用可加 `--yes``-y` 跳过确认
- **多账户切换**:使用 `longbridge --profile <名称>` 加载 `.<名称>.env` 凭证文件(如 `--profile paper` 加载 `.paper.env`),方便在模拟盘与实盘之间切换
- **港股代码**:必须使用 4 位格式,如 `0700.HK`(不能省略前导零)
- **错误处理**:SDK 异常会转换为友好提示,不暴露原始堆栈
- **Decimal 序列化**JSON 输出中 Decimal 类型自动转为 float
- **.env 优先级**:系统环境变量优先级高于 `.env` 文件,已存在的环境变量不会被 `.env` 覆盖
---
### License
MIT
+149
View File
@@ -0,0 +1,149 @@
---
name: longbridge
version: 1.0.2
description: |
长桥 LongPort OpenAPI CLI 工具,提供股票行情查询、账户持仓、订单管理、市场数据四大功能。
当用户提到"长桥"、"LongPort"、"longbridge",或要求查看股票报价、实时行情、K线、盘口、
逐笔成交、账户余额、持仓、基金持仓、下单、买入、卖出、撤单、今日订单、历史订单、
资金流向、资金分布、市场温度、期权链时,使用此 skill。即使用户只是随口问"看看苹果股价"、
"我的持仓怎么样"、"帮我下个单",也应触发此 skill。
metadata: {"clawdbot":{"emoji":"📊","os":["darwin","linux"],"requires":{"bins":["python3","uv"],"env":["LONGBRIDGE_APP_KEY","LONGBRIDGE_APP_SECRET","LONGBRIDGE_ACCESS_TOKEN"]}}}
---
# 长桥 OpenAPI CLI 工具
通过 `longbridge` CLI 命令调用长桥 OpenAPI,覆盖行情查询、账户持仓、订单管理、市场数据四大模块。
## 前置条件
### 环境变量
需设置以下环境变量(可在 `~/.zshrc``~/.bashrc` 中配置):
```bash
export LONGBRIDGE_APP_KEY="your_app_key"
export LONGBRIDGE_APP_SECRET="your_app_secret"
export LONGBRIDGE_ACCESS_TOKEN="your_access_token"
# 可选:开启交易权限(默认只读,禁止 buy/sell/cancel
# export LONGBRIDGE_TRADE_ENABLED=false
```
也可以在当前目录或主目录创建 `.env` 文件(系统环境变量优先级更高,不会被 `.env` 覆盖)。
多账户切换:使用 `--profile <名称>` 加载 `.<名称>.env` 文件,例如:
```bash
longbridge --profile paper balance # 加载 .paper.env
longbridge --profile live positions # 加载 .live.env
```
### 安装
先检测是否已安装:
```bash
which longbridge
```
- 若输出路径(如 `/Users/xxx/.local/bin/longbridge`),说明已安装,跳过安装步骤。
- 若未找到,从本 skill 目录安装(`SKILL_DIR` 为本文件所在目录):
```bash
uv tool install "$SKILL_DIR"
```
验证安装:
```bash
longbridge --help
```
## 使用方式
所有命令均支持 `--json` 选项输出 JSON 格式。
### 行情模块
```bash
# 实时报价(支持多个标的)
longbridge quote AAPL.US 700.HK
longbridge quote AAPL.US --json
# 盘口(买5卖5
longbridge depth 700.HK
# 逐笔成交
longbridge trades 700.HK --count 20
# K 线(period 可选:1m 5m 15m 30m 60m day week month quarter year
longbridge candlesticks AAPL.US day --count 30
longbridge candlesticks 700.HK 60m --count 100
# 标的静态信息
longbridge info 700.HK AAPL.US
```
### 账户模块
```bash
# 账户余额与净资产
longbridge balance
# 股票持仓
longbridge positions
longbridge positions --json
# 基金持仓
longbridge funds
```
### 订单模块
```bash
# 今日订单(只读,无需交易权限)
longbridge orders
# 历史订单(只读,无需交易权限)
longbridge history-orders --start 2026-01-01 --end 2026-03-14
# 以下命令需设置 LONGBRIDGE_TRADE_ENABLED=true
# 限价买入(会有确认提示,防止误操作)
longbridge buy AAPL.US --qty 100 --price 180.0
# 限价卖出(会有确认提示)
longbridge sell 700.HK --qty 500 --price 320.0
# 撤销订单(会有确认提示)
longbridge cancel 701234567890
# 跳过确认提示(AI Agent 程序化调用时推荐加 --yes)
longbridge buy AAPL.US --qty 100 --price 180.0 --yes
longbridge sell 700.HK --qty 500 --price 320.0 -y
```
### 市场数据模块
```bash
# 市场温度(US/HK/CN/SG
longbridge temperature US
longbridge temperature HK
# 资金流向
longbridge capital-flow 700.HK
# 资金分布(大单/中单/小单)
longbridge capital-dist 700.HK
# 期权链到期日列表
longbridge option-chain AAPL.US
```
## 注意事项
- **下单/撤单命令**`buy``sell``cancel` 均有确认提示,确认后才会执行,防止误操作。AI Agent 程序化调用时请加 `--yes``-y` 跳过确认
- **港股代码格式**:使用 4 位 + `.HK` 后缀,如 `0700.HK``9988.HK`
- **美股代码格式**:使用股票代码 + `.US` 后缀,如 `AAPL.US``NVDA.US`
- **JSON 输出**:所有命令加 `--json` 可输出机器可读的 JSON 格式,便于 AI 解析
- **错误处理**:SDK 异常会输出友好提示,不暴露原始堆栈
@@ -0,0 +1,6 @@
{
"ownerId": "kn710bpt2w6p9mw4nm8g03f82582w94c",
"slug": "longbridge-kit",
"version": "1.0.2",
"publishedAt": 1774793905098
}
@@ -0,0 +1 @@
"""longbridge_cli 包初始化"""
@@ -0,0 +1,5 @@
"""支持 python -m longbridge_cli 调用"""
from longbridge_cli.cli import cli
if __name__ == "__main__":
cli()
@@ -0,0 +1,69 @@
"""longbridge-cli 根命令组"""
import click
from longbridge_cli.commands.quote import (
quote_cmd,
depth_cmd,
trades_cmd,
candlesticks_cmd,
info_cmd,
)
from longbridge_cli.commands.account import balance_cmd, positions_cmd, funds_cmd
from longbridge_cli.commands.order import (
orders_cmd,
history_orders_cmd,
buy_cmd,
sell_cmd,
cancel_cmd,
)
from longbridge_cli.commands.market import (
temperature_cmd,
capital_flow_cmd,
capital_dist_cmd,
option_chain_cmd,
)
@click.group()
@click.version_option("1.0.0", prog_name="longbridge")
@click.option("--profile", default=None, help="账户 profile(如 paper),加载 .{profile}.env 凭证文件")
@click.pass_context
def cli(ctx, profile):
"""长桥 LongPort OpenAPI CLI 工具
\b
行情: quote depth trades candlesticks info
账户: balance positions funds
订单: orders history-orders buy sell cancel
市场: temperature capital-flow capital-dist option-chain
所有命令支持 --json 输出 JSON 格式。
"""
ctx.ensure_object(dict)
ctx.obj["profile"] = profile
# 行情
cli.add_command(quote_cmd, name="quote")
cli.add_command(depth_cmd, name="depth")
cli.add_command(trades_cmd, name="trades")
cli.add_command(candlesticks_cmd, name="candlesticks")
cli.add_command(info_cmd, name="info")
# 账户
cli.add_command(balance_cmd, name="balance")
cli.add_command(positions_cmd, name="positions")
cli.add_command(funds_cmd, name="funds")
# 订单
cli.add_command(orders_cmd, name="orders")
cli.add_command(history_orders_cmd, name="history-orders")
cli.add_command(buy_cmd, name="buy")
cli.add_command(sell_cmd, name="sell")
cli.add_command(cancel_cmd, name="cancel")
# 市场
cli.add_command(temperature_cmd, name="temperature")
cli.add_command(capital_flow_cmd, name="capital-flow")
cli.add_command(capital_dist_cmd, name="capital-dist")
cli.add_command(option_chain_cmd, name="option-chain")
@@ -0,0 +1,147 @@
"""账户与持仓命令模块"""
import click
from longbridge.openapi import TradeContext
from longbridge_cli.config import get_config
from longbridge_cli.formatters import print_table, print_json, print_error
@click.command("balance")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def balance_cmd(ctx, output_json):
"""查看账户余额与净资产
示例:longbridge balance
"""
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.account_balance()
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"currency": b.currency,
"total_cash": float(b.total_cash),
"max_finance_amount": float(b.max_finance_amount),
"remaining_finance_amount": float(b.remaining_finance_amount),
"risk_level": b.risk_level,
"margin_call": float(b.margin_call),
"net_assets": float(b.net_assets),
"init_margin": float(b.init_margin),
"maintenance_margin": float(b.maintenance_margin),
}
for b in resp
]
print_json(data)
else:
headers = ["币种", "现金余额", "净资产", "最大融资额", "剩余融资额", "风险等级"]
rows = [
[
b.currency,
f"{float(b.total_cash):,.2f}",
f"{float(b.net_assets):,.2f}",
f"{float(b.max_finance_amount):,.2f}",
f"{float(b.remaining_finance_amount):,.2f}",
str(b.risk_level),
]
for b in resp
]
print_table(headers, rows, title="账户余额")
@click.command("positions")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def positions_cmd(ctx, output_json):
"""查看股票持仓
示例:longbridge positions
"""
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.stock_positions()
except Exception as e:
print_error(str(e))
channels = resp.channels if resp else []
if output_json:
data = []
for ch in channels:
for p in ch.positions:
data.append({
"symbol": p.symbol,
"symbol_name": p.symbol_name,
"quantity": p.quantity,
"available_quantity": p.available_quantity,
"currency": p.currency,
"cost_price": float(p.cost_price),
"init_quantity": p.init_quantity,
"market": str(p.market),
})
print_json(data)
else:
headers = ["标的", "名称", "持仓", "可卖", "成本价", "初始持仓", "市场", "币种"]
rows = []
for ch in channels:
for p in ch.positions:
rows.append([
p.symbol,
p.symbol_name,
str(p.quantity),
str(p.available_quantity),
f"{float(p.cost_price):.3f}",
str(p.init_quantity),
str(p.market),
p.currency,
])
print_table(headers, rows, title="股票持仓")
@click.command("funds")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def funds_cmd(ctx, output_json):
"""查看基金持仓
示例:longbridge funds
"""
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.fund_positions()
except Exception as e:
print_error(str(e))
channels = resp.channels if resp else []
if output_json:
data = []
for ch in channels:
for f in ch.positions:
data.append({
"symbol": f.symbol,
"symbol_name": f.symbol_name,
"holding_units": float(f.holding_units),
"current_net_asset_value": float(f.current_net_asset_value),
"cost_net_asset_value": float(f.cost_net_asset_value),
"net_asset_value_day": f.net_asset_value_day.isoformat() if f.net_asset_value_day else None,
"market_value": float(f.market_value) if hasattr(f, "market_value") else None,
})
print_json(data)
else:
headers = ["基金代码", "名称", "持有份额", "当前净值", "成本净值", "净值日期"]
rows = []
for ch in channels:
for f in ch.positions:
rows.append([
f.symbol,
f.symbol_name,
f"{float(f.holding_units):.4f}",
f"{float(f.current_net_asset_value):.4f}",
f"{float(f.cost_net_asset_value):.4f}",
f.net_asset_value_day.strftime("%Y-%m-%d") if f.net_asset_value_day else "-",
])
print_table(headers, rows, title="基金持仓")
@@ -0,0 +1,162 @@
"""市场数据命令模块"""
import click
from longbridge.openapi import QuoteContext, Market
from longbridge_cli.config import get_config
from longbridge_cli.formatters import print_table, print_json, print_kv, print_error
MARKET_MAP = {
"US": Market.US,
"HK": Market.HK,
"CN": Market.CN,
"SG": Market.SG,
}
@click.command("temperature")
@click.argument("market", type=click.Choice(["US", "HK", "CN", "SG"]))
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def temperature_cmd(ctx, market, output_json):
"""查看市场温度
MARKET 可选:US HK CN SG
示例:longbridge temperature US
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.market_temperature(MARKET_MAP[market])
except Exception as e:
print_error(str(e))
if output_json:
data = {
"market": market,
"temperature": resp.temperature,
"description": resp.description,
"valuation": float(resp.valuation) if hasattr(resp, "valuation") else None,
}
print_json(data)
else:
pairs = [
("市场", market),
("温度", resp.temperature),
("描述", resp.description),
]
if hasattr(resp, "valuation"):
pairs.append(("估值", float(resp.valuation)))
print_kv(pairs, title=f"市场温度 - {market}")
@click.command("capital-flow")
@click.argument("symbol")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def capital_flow_cmd(ctx, symbol, output_json):
"""查看资金流向
示例:longbridge capital-flow 700.HK
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.capital_flow(symbol)
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"timestamp": item.timestamp.isoformat() if item.timestamp else None,
"inflow": float(item.inflow),
}
for item in resp
]
print_json(data)
else:
headers = ["时间", "净流入"]
rows = [
[
item.timestamp.strftime("%Y-%m-%d %H:%M") if item.timestamp else "-",
f"{float(item.inflow):+,.0f}",
]
for item in resp
]
print_table(headers, rows, title=f"资金流向 - {symbol}")
@click.command("capital-dist")
@click.argument("symbol")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def capital_dist_cmd(ctx, symbol, output_json):
"""查看资金分布
示例:longbridge capital-dist 700.HK
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.capital_distribution(symbol)
except Exception as e:
print_error(str(e))
if output_json:
data = {
"symbol": symbol,
"timestamp": resp.timestamp.isoformat() if resp.timestamp else None,
"capital_in": {
"large": float(resp.capital_in.large),
"medium": float(resp.capital_in.medium),
"small": float(resp.capital_in.small),
},
"capital_out": {
"large": float(resp.capital_out.large),
"medium": float(resp.capital_out.medium),
"small": float(resp.capital_out.small),
},
}
print_json(data)
else:
headers = ["方向", "大单", "中单", "小单"]
rows = [
[
"流入",
f"{float(resp.capital_in.large):,.0f}",
f"{float(resp.capital_in.medium):,.0f}",
f"{float(resp.capital_in.small):,.0f}",
],
[
"流出",
f"{float(resp.capital_out.large):,.0f}",
f"{float(resp.capital_out.medium):,.0f}",
f"{float(resp.capital_out.small):,.0f}",
],
]
print_table(headers, rows, title=f"资金分布 - {symbol}")
@click.command("option-chain")
@click.argument("symbol")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def option_chain_cmd(ctx, symbol, output_json):
"""查看期权链到期日列表
示例:longbridge option-chain AAPL.US
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.option_chain_expiry_date_list(symbol)
except Exception as e:
print_error(str(e))
if output_json:
data = {
"symbol": symbol,
"expiry_dates": [d.isoformat() for d in resp],
}
print_json(data)
else:
headers = ["序号", "到期日"]
rows = [[str(i + 1), d.strftime("%Y-%m-%d")] for i, d in enumerate(resp)]
print_table(headers, rows, title=f"期权链到期日 - {symbol}")
@@ -0,0 +1,188 @@
"""订单管理命令模块"""
from datetime import datetime
from decimal import Decimal
import click
from longbridge.openapi import (
TradeContext,
OrderType,
OrderSide,
TimeInForceType,
)
from longbridge_cli.config import get_config, require_trade_enabled
from longbridge_cli.formatters import print_table, print_json, print_error
@click.command("orders")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def orders_cmd(ctx, output_json):
"""查看今日订单
示例:longbridge orders
"""
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.today_orders()
except Exception as e:
print_error(str(e))
_print_orders(resp, output_json, "今日订单")
@click.command("history-orders")
@click.option("--start", required=True, help="开始日期 (YYYY-MM-DD)")
@click.option("--end", required=True, help="结束日期 (YYYY-MM-DD)")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def history_orders_cmd(ctx, start, end, output_json):
"""查看历史订单
示例:longbridge history-orders --start 2026-01-01 --end 2026-03-14
"""
try:
start_dt = datetime.strptime(start, "%Y-%m-%d")
end_dt = datetime.strptime(end, "%Y-%m-%d")
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.history_orders(start_at=start_dt, end_at=end_dt)
except ValueError:
print_error("日期格式错误,请使用 YYYY-MM-DD")
return
except Exception as e:
print_error(str(e))
_print_orders(resp, output_json, f"历史订单 ({start} ~ {end})")
@click.command("buy")
@click.argument("symbol")
@click.option("--qty", required=True, type=int, help="买入数量")
@click.option("--price", required=True, type=float, help="限价价格")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.option("--yes", "-y", is_flag=True, help="跳过交互确认(程序化调用时使用)")
@click.pass_context
def buy_cmd(ctx, symbol, qty, price, output_json, yes):
"""限价买入
示例:longbridge buy AAPL.US --qty 100 --price 180.0
"""
require_trade_enabled()
if not yes:
click.confirm(f"确认买入 {symbol} 数量 {qty} 限价 {price}", abort=True)
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.submit_order(
symbol,
OrderType.LO,
OrderSide.Buy,
qty,
TimeInForceType.Day,
submitted_price=Decimal(str(price)),
)
except Exception as e:
print_error(str(e))
return
if output_json:
print_json({"order_id": resp.order_id})
else:
click.echo(f"下单成功,订单号:{resp.order_id}")
@click.command("sell")
@click.argument("symbol")
@click.option("--qty", required=True, type=int, help="卖出数量")
@click.option("--price", required=True, type=float, help="限价价格")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.option("--yes", "-y", is_flag=True, help="跳过交互确认(程序化调用时使用)")
@click.pass_context
def sell_cmd(ctx, symbol, qty, price, output_json, yes):
"""限价卖出
示例:longbridge sell 700.HK --qty 500 --price 320.0
"""
require_trade_enabled()
if not yes:
click.confirm(f"确认卖出 {symbol} 数量 {qty} 限价 {price}", abort=True)
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.submit_order(
symbol,
OrderType.LO,
OrderSide.Sell,
qty,
TimeInForceType.Day,
submitted_price=Decimal(str(price)),
)
except Exception as e:
print_error(str(e))
return
if output_json:
print_json({"order_id": resp.order_id})
else:
click.echo(f"下单成功,订单号:{resp.order_id}")
@click.command("cancel")
@click.argument("order_id")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def cancel_cmd(ctx, order_id, output_json):
"""撤销订单
示例:longbridge cancel 701234567890
"""
require_trade_enabled()
click.confirm(f"确认撤销订单 {order_id}", abort=True)
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
trade_ctx.cancel_order(order_id)
except Exception as e:
print_error(str(e))
if output_json:
print_json({"order_id": order_id, "status": "cancelled"})
else:
click.echo(f"订单 {order_id} 已撤销")
def _print_orders(orders, output_json: bool, title: str) -> None:
"""内部辅助:统一输出订单列表"""
if output_json:
data = [
{
"order_id": o.order_id,
"symbol": o.symbol,
"side": str(o.side),
"order_type": str(o.order_type),
"quantity": o.quantity,
"executed_quantity": o.executed_quantity,
"price": float(o.price) if o.price else None,
"executed_price": float(o.executed_price) if o.executed_price else None,
"status": str(o.status),
"submitted_at": o.submitted_at.isoformat() if o.submitted_at else None,
"updated_at": o.updated_at.isoformat() if o.updated_at else None,
}
for o in orders
]
print_json(data)
else:
headers = ["订单号", "标的", "方向", "类型", "数量", "已成交", "委托价", "成交价", "状态", "提交时间"]
rows = [
[
o.order_id,
o.symbol,
str(o.side),
str(o.order_type),
str(o.quantity),
str(o.executed_quantity),
f"{float(o.price):.3f}" if o.price else "-",
f"{float(o.executed_price):.3f}" if o.executed_price else "-",
str(o.status),
o.submitted_at.strftime("%Y-%m-%d %H:%M:%S") if o.submitted_at else "-",
]
for o in orders
]
print_table(headers, rows, title=title)
@@ -0,0 +1,239 @@
"""行情命令模块"""
import click
from longbridge.openapi import QuoteContext, Period, AdjustType
from longbridge_cli.config import get_config
from longbridge_cli.formatters import print_table, print_json, print_error
PERIOD_MAP = {
"1m": Period.Min_1,
"5m": Period.Min_5,
"15m": Period.Min_15,
"30m": Period.Min_30,
"60m": Period.Min_60,
"day": Period.Day,
"week": Period.Week,
"month": Period.Month,
"quarter": Period.Quarter,
"year": Period.Year,
}
@click.command("quote")
@click.argument("symbols", nargs=-1, required=True)
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def quote_cmd(ctx, symbols, output_json):
"""获取实时报价(支持多个标的)
示例:longbridge quote AAPL.US 700.HK
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.quote(list(symbols))
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"symbol": q.symbol,
"last_done": float(q.last_done),
"open": float(q.open),
"high": float(q.high),
"low": float(q.low),
"volume": q.volume,
"turnover": float(q.turnover),
"change_rate": round(float(q.last_done / q.prev_close - 1) * 100, 2) if q.prev_close else None,
"prev_close": float(q.prev_close),
}
for q in resp
]
print_json(data)
else:
headers = ["标的", "最新价", "涨跌幅", "开盘", "最高", "最低", "成交量", "成交额"]
rows = []
for q in resp:
change_rate = round(float(q.last_done / q.prev_close - 1) * 100, 2) if q.prev_close else "-"
change_str = f"{change_rate:+.2f}%" if isinstance(change_rate, float) else change_rate
rows.append([
q.symbol,
f"{float(q.last_done):.3f}",
change_str,
f"{float(q.open):.3f}",
f"{float(q.high):.3f}",
f"{float(q.low):.3f}",
f"{q.volume:,}",
f"{float(q.turnover):,.0f}",
])
print_table(headers, rows, title="实时报价")
@click.command("depth")
@click.argument("symbol")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def depth_cmd(ctx, symbol, output_json):
"""查看盘口(买5卖5
示例:longbridge depth 700.HK
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.depth(symbol)
except Exception as e:
print_error(str(e))
if output_json:
data = {
"symbol": symbol,
"asks": [{"position": a.position, "price": float(a.price), "volume": a.volume} for a in resp.asks],
"bids": [{"position": b.position, "price": float(b.price), "volume": b.volume} for b in resp.bids],
}
print_json(data)
else:
headers = ["方向", "档位", "价格", "数量"]
rows = []
for a in reversed(resp.asks):
rows.append(["", str(a.position), f"{float(a.price):.3f}", f"{a.volume:,}"])
for b in resp.bids:
rows.append(["", str(b.position), f"{float(b.price):.3f}", f"{b.volume:,}"])
print_table(headers, rows, title=f"盘口 - {symbol}")
@click.command("trades")
@click.argument("symbol")
@click.option("--count", default=20, show_default=True, help="返回条数(最多 1000")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def trades_cmd(ctx, symbol, count, output_json):
"""查看最近逐笔成交
示例:longbridge trades 700.HK --count 20
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.trades(symbol, count)
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"price": float(t.price),
"volume": t.volume,
"timestamp": t.timestamp.isoformat() if t.timestamp else None,
"direction": str(t.trade_type),
}
for t in resp
]
print_json(data)
else:
headers = ["时间", "价格", "数量", "类型"]
rows = [
[
t.timestamp.strftime("%H:%M:%S") if t.timestamp else "-",
f"{float(t.price):.3f}",
f"{t.volume:,}",
str(t.trade_type),
]
for t in resp
]
print_table(headers, rows, title=f"逐笔成交 - {symbol}")
@click.command("candlesticks")
@click.argument("symbol")
@click.argument("period", type=click.Choice(list(PERIOD_MAP.keys())))
@click.option("--count", default=30, show_default=True, help="返回K线条数(最多 1000")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def candlesticks_cmd(ctx, symbol, period, count, output_json):
"""查看 K 线数据
PERIOD 可选:1m 5m 15m 30m 60m day week month quarter year
示例:longbridge candlesticks AAPL.US day --count 30
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.candlesticks(symbol, PERIOD_MAP[period], count, AdjustType.NoAdjust)
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"timestamp": c.timestamp.isoformat() if c.timestamp else None,
"open": float(c.open),
"high": float(c.high),
"low": float(c.low),
"close": float(c.close),
"volume": c.volume,
"turnover": float(c.turnover),
}
for c in resp
]
print_json(data)
else:
headers = ["时间", "开盘", "最高", "最低", "收盘", "成交量"]
rows = [
[
c.timestamp.strftime("%Y-%m-%d %H:%M") if c.timestamp else "-",
f"{float(c.open):.3f}",
f"{float(c.high):.3f}",
f"{float(c.low):.3f}",
f"{float(c.close):.3f}",
f"{c.volume:,}",
]
for c in resp
]
print_table(headers, rows, title=f"K线 - {symbol} ({period})")
@click.command("info")
@click.argument("symbols", nargs=-1, required=True)
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def info_cmd(ctx, symbols, output_json):
"""查看标的静态基本信息(名称、交易所、类型等)
示例:longbridge info 700.HK AAPL.US
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.static_info(list(symbols))
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"symbol": s.symbol,
"name_cn": s.name_cn,
"name_en": s.name_en,
"exchange": s.exchange,
"currency": s.currency,
"lot_size": s.lot_size,
"total_shares": s.total_shares,
"circulating_shares": s.circulating_shares,
"board": str(s.board),
}
for s in resp
]
print_json(data)
else:
headers = ["标的", "中文名", "英文名", "交易所", "币种", "手数", "板块"]
rows = [
[
s.symbol,
s.name_cn,
s.name_en,
s.exchange,
s.currency,
str(s.lot_size),
str(s.board),
]
for s in resp
]
print_table(headers, rows, title="标的信息")
@@ -0,0 +1,87 @@
"""长桥 OpenAPI 配置初始化"""
import os
from pathlib import Path
import click
from longbridge.openapi import Config
# ---------------------------------------------------------------------------
# .env 加载(CLI 包自包含,不依赖 trader 包)
# ---------------------------------------------------------------------------
def _load_dotenv_for_profile(profile: str | None = None) -> None:
"""Load .env (or .{profile}.env) into os.environ.
查找顺序: cwd → home dir。
profile=None + 文件不存在 → 静默跳过(向后兼容)。
profile 非空 + 文件不存在 → 抛 FileNotFoundError。
已存在的环境变量不会被覆盖。
"""
filename = f".{profile}.env" if profile else ".env"
env_path: Path | None = None
for base in [Path.cwd(), Path.home()]:
candidate = base / filename
if candidate.is_file():
env_path = candidate
break
if env_path is None:
if profile is not None:
raise FileNotFoundError(
f"Profile '{profile}' 的 env 文件未找到。"
f"请在当前目录或主目录创建 {filename}"
)
return
with open(env_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = value
# 下单权限控制:设置 LONGBRIDGE_TRADE_ENABLED=true 才允许 buy/sell/cancel
_TRADE_ENV_VAR = "LONGBRIDGE_TRADE_ENABLED"
def is_trade_enabled() -> bool:
"""返回是否已开启交易权限(默认关闭,只读模式)"""
return os.environ.get(_TRADE_ENV_VAR, "").strip().lower() == "true"
def require_trade_enabled() -> None:
"""若未开启交易权限,抛出友好错误并提示配置方式"""
if not is_trade_enabled():
raise click.ClickException(
"当前为只读模式,下单/撤单操作已禁用。\n"
f"如需开启交易权限,请设置环境变量:\n"
f" export {_TRADE_ENV_VAR}=true\n"
"⚠️ 开启后请确保操作正确,下单指令将直接提交至长桥交易系统。"
)
def get_config(profile: str | None = None) -> Config:
"""从环境变量初始化长桥配置。支持 --profile 切换账户。
需要设置以下环境变量:
LONGBRIDGE_APP_KEY
LONGBRIDGE_APP_SECRET
LONGBRIDGE_ACCESS_TOKEN
"""
_load_dotenv_for_profile(profile)
try:
return Config.from_apikey_env()
except Exception as e:
raise click.ClickException(
"无法初始化长桥配置,请确认已设置以下环境变量:\n"
" LONGBRIDGE_APP_KEY\n"
" LONGBRIDGE_APP_SECRET\n"
" LONGBRIDGE_ACCESS_TOKEN\n"
f"错误详情:{e}"
)
@@ -0,0 +1,56 @@
"""统一输出格式模块(text/json"""
import json
import sys
from decimal import Decimal
from typing import Any
import click
from rich.console import Console
from rich.table import Table
console = Console()
class _DecimalEncoder(json.JSONEncoder):
"""将 Decimal 序列化为 float"""
def default(self, obj: Any) -> Any:
if isinstance(obj, Decimal):
return float(obj)
return super().default(obj)
def print_json(data: Any) -> None:
"""以 JSON 格式输出数据"""
click.echo(json.dumps(data, ensure_ascii=False, indent=2, cls=_DecimalEncoder))
def print_table(headers: list[str], rows: list[list[Any]], title: str = "") -> None:
"""用 rich 表格输出数据
Args:
headers: 列标题列表
rows: 数据行列表(每行为与 headers 对应的值列表)
title: 可选表格标题
"""
table = Table(title=title, show_header=True, header_style="bold cyan")
for h in headers:
table.add_column(h, style="white")
for row in rows:
table.add_row(*[str(v) if v is not None else "-" for v in row])
console.print(table)
def print_kv(pairs: list[tuple[str, Any]], title: str = "") -> None:
"""以键值对形式输出(用于单条记录)"""
if title:
console.print(f"[bold cyan]{title}[/bold cyan]")
for k, v in pairs:
v_str = str(v) if v is not None else "-"
console.print(f" [bold]{k}[/bold]: {v_str}")
def print_error(msg: str) -> None:
"""输出错误信息到 stderr"""
click.echo(f"错误:{msg}", err=True)
sys.exit(1)
@@ -0,0 +1,8 @@
Metadata-Version: 2.4
Name: longbridge-cli
Version: 1.0.0
Summary: 长桥 LongPort OpenAPI CLI 工具
Requires-Python: >=3.9
Requires-Dist: longbridge
Requires-Dist: click>=8.0
Requires-Dist: rich>=13.0
@@ -0,0 +1,18 @@
README.md
pyproject.toml
longbridge_cli/__init__.py
longbridge_cli/__main__.py
longbridge_cli/cli.py
longbridge_cli/config.py
longbridge_cli/formatters.py
longbridge_cli.egg-info/PKG-INFO
longbridge_cli.egg-info/SOURCES.txt
longbridge_cli.egg-info/dependency_links.txt
longbridge_cli.egg-info/entry_points.txt
longbridge_cli.egg-info/requires.txt
longbridge_cli.egg-info/top_level.txt
longbridge_cli/commands/__init__.py
longbridge_cli/commands/account.py
longbridge_cli/commands/market.py
longbridge_cli/commands/order.py
longbridge_cli/commands/quote.py
@@ -0,0 +1,2 @@
[console_scripts]
longbridge = longbridge_cli.cli:cli
@@ -0,0 +1,3 @@
longbridge
click>=8.0
rich>=13.0
@@ -0,0 +1 @@
longbridge_cli
@@ -0,0 +1 @@
"""longbridge_cli 包初始化"""
@@ -0,0 +1,5 @@
"""支持 python -m longbridge_cli 调用"""
from longbridge_cli.cli import cli
if __name__ == "__main__":
cli()
@@ -0,0 +1,69 @@
"""longbridge-cli 根命令组"""
import click
from longbridge_cli.commands.quote import (
quote_cmd,
depth_cmd,
trades_cmd,
candlesticks_cmd,
info_cmd,
)
from longbridge_cli.commands.account import balance_cmd, positions_cmd, funds_cmd
from longbridge_cli.commands.order import (
orders_cmd,
history_orders_cmd,
buy_cmd,
sell_cmd,
cancel_cmd,
)
from longbridge_cli.commands.market import (
temperature_cmd,
capital_flow_cmd,
capital_dist_cmd,
option_chain_cmd,
)
@click.group()
@click.version_option("1.0.0", prog_name="longbridge")
@click.option("--profile", default=None, help="账户 profile(如 paper),加载 .{profile}.env 凭证文件")
@click.pass_context
def cli(ctx, profile):
"""长桥 LongPort OpenAPI CLI 工具
\b
行情: quote depth trades candlesticks info
账户: balance positions funds
订单: orders history-orders buy sell cancel
市场: temperature capital-flow capital-dist option-chain
所有命令支持 --json 输出 JSON 格式。
"""
ctx.ensure_object(dict)
ctx.obj["profile"] = profile
# 行情
cli.add_command(quote_cmd, name="quote")
cli.add_command(depth_cmd, name="depth")
cli.add_command(trades_cmd, name="trades")
cli.add_command(candlesticks_cmd, name="candlesticks")
cli.add_command(info_cmd, name="info")
# 账户
cli.add_command(balance_cmd, name="balance")
cli.add_command(positions_cmd, name="positions")
cli.add_command(funds_cmd, name="funds")
# 订单
cli.add_command(orders_cmd, name="orders")
cli.add_command(history_orders_cmd, name="history-orders")
cli.add_command(buy_cmd, name="buy")
cli.add_command(sell_cmd, name="sell")
cli.add_command(cancel_cmd, name="cancel")
# 市场
cli.add_command(temperature_cmd, name="temperature")
cli.add_command(capital_flow_cmd, name="capital-flow")
cli.add_command(capital_dist_cmd, name="capital-dist")
cli.add_command(option_chain_cmd, name="option-chain")
@@ -0,0 +1,147 @@
"""账户与持仓命令模块"""
import click
from longbridge.openapi import TradeContext
from longbridge_cli.config import get_config
from longbridge_cli.formatters import print_table, print_json, print_error
@click.command("balance")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def balance_cmd(ctx, output_json):
"""查看账户余额与净资产
示例:longbridge balance
"""
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.account_balance()
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"currency": b.currency,
"total_cash": float(b.total_cash),
"max_finance_amount": float(b.max_finance_amount),
"remaining_finance_amount": float(b.remaining_finance_amount),
"risk_level": b.risk_level,
"margin_call": float(b.margin_call),
"net_assets": float(b.net_assets),
"init_margin": float(b.init_margin),
"maintenance_margin": float(b.maintenance_margin),
}
for b in resp
]
print_json(data)
else:
headers = ["币种", "现金余额", "净资产", "最大融资额", "剩余融资额", "风险等级"]
rows = [
[
b.currency,
f"{float(b.total_cash):,.2f}",
f"{float(b.net_assets):,.2f}",
f"{float(b.max_finance_amount):,.2f}",
f"{float(b.remaining_finance_amount):,.2f}",
str(b.risk_level),
]
for b in resp
]
print_table(headers, rows, title="账户余额")
@click.command("positions")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def positions_cmd(ctx, output_json):
"""查看股票持仓
示例:longbridge positions
"""
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.stock_positions()
except Exception as e:
print_error(str(e))
channels = resp.channels if resp else []
if output_json:
data = []
for ch in channels:
for p in ch.positions:
data.append({
"symbol": p.symbol,
"symbol_name": p.symbol_name,
"quantity": p.quantity,
"available_quantity": p.available_quantity,
"currency": p.currency,
"cost_price": float(p.cost_price),
"init_quantity": p.init_quantity,
"market": str(p.market),
})
print_json(data)
else:
headers = ["标的", "名称", "持仓", "可卖", "成本价", "初始持仓", "市场", "币种"]
rows = []
for ch in channels:
for p in ch.positions:
rows.append([
p.symbol,
p.symbol_name,
str(p.quantity),
str(p.available_quantity),
f"{float(p.cost_price):.3f}",
str(p.init_quantity),
str(p.market),
p.currency,
])
print_table(headers, rows, title="股票持仓")
@click.command("funds")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def funds_cmd(ctx, output_json):
"""查看基金持仓
示例:longbridge funds
"""
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.fund_positions()
except Exception as e:
print_error(str(e))
channels = resp.channels if resp else []
if output_json:
data = []
for ch in channels:
for f in ch.positions:
data.append({
"symbol": f.symbol,
"symbol_name": f.symbol_name,
"holding_units": float(f.holding_units),
"current_net_asset_value": float(f.current_net_asset_value),
"cost_net_asset_value": float(f.cost_net_asset_value),
"net_asset_value_day": f.net_asset_value_day.isoformat() if f.net_asset_value_day else None,
"market_value": float(f.market_value) if hasattr(f, "market_value") else None,
})
print_json(data)
else:
headers = ["基金代码", "名称", "持有份额", "当前净值", "成本净值", "净值日期"]
rows = []
for ch in channels:
for f in ch.positions:
rows.append([
f.symbol,
f.symbol_name,
f"{float(f.holding_units):.4f}",
f"{float(f.current_net_asset_value):.4f}",
f"{float(f.cost_net_asset_value):.4f}",
f.net_asset_value_day.strftime("%Y-%m-%d") if f.net_asset_value_day else "-",
])
print_table(headers, rows, title="基金持仓")
@@ -0,0 +1,162 @@
"""市场数据命令模块"""
import click
from longbridge.openapi import QuoteContext, Market
from longbridge_cli.config import get_config
from longbridge_cli.formatters import print_table, print_json, print_kv, print_error
MARKET_MAP = {
"US": Market.US,
"HK": Market.HK,
"CN": Market.CN,
"SG": Market.SG,
}
@click.command("temperature")
@click.argument("market", type=click.Choice(["US", "HK", "CN", "SG"]))
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def temperature_cmd(ctx, market, output_json):
"""查看市场温度
MARKET 可选:US HK CN SG
示例:longbridge temperature US
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.market_temperature(MARKET_MAP[market])
except Exception as e:
print_error(str(e))
if output_json:
data = {
"market": market,
"temperature": resp.temperature,
"description": resp.description,
"valuation": float(resp.valuation) if hasattr(resp, "valuation") else None,
}
print_json(data)
else:
pairs = [
("市场", market),
("温度", resp.temperature),
("描述", resp.description),
]
if hasattr(resp, "valuation"):
pairs.append(("估值", float(resp.valuation)))
print_kv(pairs, title=f"市场温度 - {market}")
@click.command("capital-flow")
@click.argument("symbol")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def capital_flow_cmd(ctx, symbol, output_json):
"""查看资金流向
示例:longbridge capital-flow 700.HK
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.capital_flow(symbol)
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"timestamp": item.timestamp.isoformat() if item.timestamp else None,
"inflow": float(item.inflow),
}
for item in resp
]
print_json(data)
else:
headers = ["时间", "净流入"]
rows = [
[
item.timestamp.strftime("%Y-%m-%d %H:%M") if item.timestamp else "-",
f"{float(item.inflow):+,.0f}",
]
for item in resp
]
print_table(headers, rows, title=f"资金流向 - {symbol}")
@click.command("capital-dist")
@click.argument("symbol")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def capital_dist_cmd(ctx, symbol, output_json):
"""查看资金分布
示例:longbridge capital-dist 700.HK
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.capital_distribution(symbol)
except Exception as e:
print_error(str(e))
if output_json:
data = {
"symbol": symbol,
"timestamp": resp.timestamp.isoformat() if resp.timestamp else None,
"capital_in": {
"large": float(resp.capital_in.large),
"medium": float(resp.capital_in.medium),
"small": float(resp.capital_in.small),
},
"capital_out": {
"large": float(resp.capital_out.large),
"medium": float(resp.capital_out.medium),
"small": float(resp.capital_out.small),
},
}
print_json(data)
else:
headers = ["方向", "大单", "中单", "小单"]
rows = [
[
"流入",
f"{float(resp.capital_in.large):,.0f}",
f"{float(resp.capital_in.medium):,.0f}",
f"{float(resp.capital_in.small):,.0f}",
],
[
"流出",
f"{float(resp.capital_out.large):,.0f}",
f"{float(resp.capital_out.medium):,.0f}",
f"{float(resp.capital_out.small):,.0f}",
],
]
print_table(headers, rows, title=f"资金分布 - {symbol}")
@click.command("option-chain")
@click.argument("symbol")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def option_chain_cmd(ctx, symbol, output_json):
"""查看期权链到期日列表
示例:longbridge option-chain AAPL.US
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.option_chain_expiry_date_list(symbol)
except Exception as e:
print_error(str(e))
if output_json:
data = {
"symbol": symbol,
"expiry_dates": [d.isoformat() for d in resp],
}
print_json(data)
else:
headers = ["序号", "到期日"]
rows = [[str(i + 1), d.strftime("%Y-%m-%d")] for i, d in enumerate(resp)]
print_table(headers, rows, title=f"期权链到期日 - {symbol}")
@@ -0,0 +1,188 @@
"""订单管理命令模块"""
from datetime import datetime
from decimal import Decimal
import click
from longbridge.openapi import (
TradeContext,
OrderType,
OrderSide,
TimeInForceType,
)
from longbridge_cli.config import get_config, require_trade_enabled
from longbridge_cli.formatters import print_table, print_json, print_error
@click.command("orders")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def orders_cmd(ctx, output_json):
"""查看今日订单
示例:longbridge orders
"""
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.today_orders()
except Exception as e:
print_error(str(e))
_print_orders(resp, output_json, "今日订单")
@click.command("history-orders")
@click.option("--start", required=True, help="开始日期 (YYYY-MM-DD)")
@click.option("--end", required=True, help="结束日期 (YYYY-MM-DD)")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def history_orders_cmd(ctx, start, end, output_json):
"""查看历史订单
示例:longbridge history-orders --start 2026-01-01 --end 2026-03-14
"""
try:
start_dt = datetime.strptime(start, "%Y-%m-%d")
end_dt = datetime.strptime(end, "%Y-%m-%d")
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.history_orders(start_at=start_dt, end_at=end_dt)
except ValueError:
print_error("日期格式错误,请使用 YYYY-MM-DD")
return
except Exception as e:
print_error(str(e))
_print_orders(resp, output_json, f"历史订单 ({start} ~ {end})")
@click.command("buy")
@click.argument("symbol")
@click.option("--qty", required=True, type=int, help="买入数量")
@click.option("--price", required=True, type=float, help="限价价格")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.option("--yes", "-y", is_flag=True, help="跳过交互确认(程序化调用时使用)")
@click.pass_context
def buy_cmd(ctx, symbol, qty, price, output_json, yes):
"""限价买入
示例:longbridge buy AAPL.US --qty 100 --price 180.0
"""
require_trade_enabled()
if not yes:
click.confirm(f"确认买入 {symbol} 数量 {qty} 限价 {price}", abort=True)
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.submit_order(
symbol,
OrderType.LO,
OrderSide.Buy,
qty,
TimeInForceType.Day,
submitted_price=Decimal(str(price)),
)
except Exception as e:
print_error(str(e))
return
if output_json:
print_json({"order_id": resp.order_id})
else:
click.echo(f"下单成功,订单号:{resp.order_id}")
@click.command("sell")
@click.argument("symbol")
@click.option("--qty", required=True, type=int, help="卖出数量")
@click.option("--price", required=True, type=float, help="限价价格")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.option("--yes", "-y", is_flag=True, help="跳过交互确认(程序化调用时使用)")
@click.pass_context
def sell_cmd(ctx, symbol, qty, price, output_json, yes):
"""限价卖出
示例:longbridge sell 700.HK --qty 500 --price 320.0
"""
require_trade_enabled()
if not yes:
click.confirm(f"确认卖出 {symbol} 数量 {qty} 限价 {price}", abort=True)
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
resp = trade_ctx.submit_order(
symbol,
OrderType.LO,
OrderSide.Sell,
qty,
TimeInForceType.Day,
submitted_price=Decimal(str(price)),
)
except Exception as e:
print_error(str(e))
return
if output_json:
print_json({"order_id": resp.order_id})
else:
click.echo(f"下单成功,订单号:{resp.order_id}")
@click.command("cancel")
@click.argument("order_id")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def cancel_cmd(ctx, order_id, output_json):
"""撤销订单
示例:longbridge cancel 701234567890
"""
require_trade_enabled()
click.confirm(f"确认撤销订单 {order_id}", abort=True)
try:
trade_ctx = TradeContext(get_config(ctx.obj.get("profile")))
trade_ctx.cancel_order(order_id)
except Exception as e:
print_error(str(e))
if output_json:
print_json({"order_id": order_id, "status": "cancelled"})
else:
click.echo(f"订单 {order_id} 已撤销")
def _print_orders(orders, output_json: bool, title: str) -> None:
"""内部辅助:统一输出订单列表"""
if output_json:
data = [
{
"order_id": o.order_id,
"symbol": o.symbol,
"side": str(o.side),
"order_type": str(o.order_type),
"quantity": o.quantity,
"executed_quantity": o.executed_quantity,
"price": float(o.price) if o.price else None,
"executed_price": float(o.executed_price) if o.executed_price else None,
"status": str(o.status),
"submitted_at": o.submitted_at.isoformat() if o.submitted_at else None,
"updated_at": o.updated_at.isoformat() if o.updated_at else None,
}
for o in orders
]
print_json(data)
else:
headers = ["订单号", "标的", "方向", "类型", "数量", "已成交", "委托价", "成交价", "状态", "提交时间"]
rows = [
[
o.order_id,
o.symbol,
str(o.side),
str(o.order_type),
str(o.quantity),
str(o.executed_quantity),
f"{float(o.price):.3f}" if o.price else "-",
f"{float(o.executed_price):.3f}" if o.executed_price else "-",
str(o.status),
o.submitted_at.strftime("%Y-%m-%d %H:%M:%S") if o.submitted_at else "-",
]
for o in orders
]
print_table(headers, rows, title=title)
@@ -0,0 +1,239 @@
"""行情命令模块"""
import click
from longbridge.openapi import QuoteContext, Period, AdjustType
from longbridge_cli.config import get_config
from longbridge_cli.formatters import print_table, print_json, print_error
PERIOD_MAP = {
"1m": Period.Min_1,
"5m": Period.Min_5,
"15m": Period.Min_15,
"30m": Period.Min_30,
"60m": Period.Min_60,
"day": Period.Day,
"week": Period.Week,
"month": Period.Month,
"quarter": Period.Quarter,
"year": Period.Year,
}
@click.command("quote")
@click.argument("symbols", nargs=-1, required=True)
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def quote_cmd(ctx, symbols, output_json):
"""获取实时报价(支持多个标的)
示例:longbridge quote AAPL.US 700.HK
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.quote(list(symbols))
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"symbol": q.symbol,
"last_done": float(q.last_done),
"open": float(q.open),
"high": float(q.high),
"low": float(q.low),
"volume": q.volume,
"turnover": float(q.turnover),
"change_rate": round(float(q.last_done / q.prev_close - 1) * 100, 2) if q.prev_close else None,
"prev_close": float(q.prev_close),
}
for q in resp
]
print_json(data)
else:
headers = ["标的", "最新价", "涨跌幅", "开盘", "最高", "最低", "成交量", "成交额"]
rows = []
for q in resp:
change_rate = round(float(q.last_done / q.prev_close - 1) * 100, 2) if q.prev_close else "-"
change_str = f"{change_rate:+.2f}%" if isinstance(change_rate, float) else change_rate
rows.append([
q.symbol,
f"{float(q.last_done):.3f}",
change_str,
f"{float(q.open):.3f}",
f"{float(q.high):.3f}",
f"{float(q.low):.3f}",
f"{q.volume:,}",
f"{float(q.turnover):,.0f}",
])
print_table(headers, rows, title="实时报价")
@click.command("depth")
@click.argument("symbol")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def depth_cmd(ctx, symbol, output_json):
"""查看盘口(买5卖5
示例:longbridge depth 700.HK
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.depth(symbol)
except Exception as e:
print_error(str(e))
if output_json:
data = {
"symbol": symbol,
"asks": [{"position": a.position, "price": float(a.price), "volume": a.volume} for a in resp.asks],
"bids": [{"position": b.position, "price": float(b.price), "volume": b.volume} for b in resp.bids],
}
print_json(data)
else:
headers = ["方向", "档位", "价格", "数量"]
rows = []
for a in reversed(resp.asks):
rows.append(["", str(a.position), f"{float(a.price):.3f}", f"{a.volume:,}"])
for b in resp.bids:
rows.append(["", str(b.position), f"{float(b.price):.3f}", f"{b.volume:,}"])
print_table(headers, rows, title=f"盘口 - {symbol}")
@click.command("trades")
@click.argument("symbol")
@click.option("--count", default=20, show_default=True, help="返回条数(最多 1000")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def trades_cmd(ctx, symbol, count, output_json):
"""查看最近逐笔成交
示例:longbridge trades 700.HK --count 20
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.trades(symbol, count)
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"price": float(t.price),
"volume": t.volume,
"timestamp": t.timestamp.isoformat() if t.timestamp else None,
"direction": str(t.trade_type),
}
for t in resp
]
print_json(data)
else:
headers = ["时间", "价格", "数量", "类型"]
rows = [
[
t.timestamp.strftime("%H:%M:%S") if t.timestamp else "-",
f"{float(t.price):.3f}",
f"{t.volume:,}",
str(t.trade_type),
]
for t in resp
]
print_table(headers, rows, title=f"逐笔成交 - {symbol}")
@click.command("candlesticks")
@click.argument("symbol")
@click.argument("period", type=click.Choice(list(PERIOD_MAP.keys())))
@click.option("--count", default=30, show_default=True, help="返回K线条数(最多 1000")
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def candlesticks_cmd(ctx, symbol, period, count, output_json):
"""查看 K 线数据
PERIOD 可选:1m 5m 15m 30m 60m day week month quarter year
示例:longbridge candlesticks AAPL.US day --count 30
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.candlesticks(symbol, PERIOD_MAP[period], count, AdjustType.NoAdjust)
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"timestamp": c.timestamp.isoformat() if c.timestamp else None,
"open": float(c.open),
"high": float(c.high),
"low": float(c.low),
"close": float(c.close),
"volume": c.volume,
"turnover": float(c.turnover),
}
for c in resp
]
print_json(data)
else:
headers = ["时间", "开盘", "最高", "最低", "收盘", "成交量"]
rows = [
[
c.timestamp.strftime("%Y-%m-%d %H:%M") if c.timestamp else "-",
f"{float(c.open):.3f}",
f"{float(c.high):.3f}",
f"{float(c.low):.3f}",
f"{float(c.close):.3f}",
f"{c.volume:,}",
]
for c in resp
]
print_table(headers, rows, title=f"K线 - {symbol} ({period})")
@click.command("info")
@click.argument("symbols", nargs=-1, required=True)
@click.option("--json", "output_json", is_flag=True, help="以 JSON 格式输出")
@click.pass_context
def info_cmd(ctx, symbols, output_json):
"""查看标的静态基本信息(名称、交易所、类型等)
示例:longbridge info 700.HK AAPL.US
"""
try:
quote_ctx = QuoteContext(get_config(ctx.obj.get("profile")))
resp = quote_ctx.static_info(list(symbols))
except Exception as e:
print_error(str(e))
if output_json:
data = [
{
"symbol": s.symbol,
"name_cn": s.name_cn,
"name_en": s.name_en,
"exchange": s.exchange,
"currency": s.currency,
"lot_size": s.lot_size,
"total_shares": s.total_shares,
"circulating_shares": s.circulating_shares,
"board": str(s.board),
}
for s in resp
]
print_json(data)
else:
headers = ["标的", "中文名", "英文名", "交易所", "币种", "手数", "板块"]
rows = [
[
s.symbol,
s.name_cn,
s.name_en,
s.exchange,
s.currency,
str(s.lot_size),
str(s.board),
]
for s in resp
]
print_table(headers, rows, title="标的信息")
@@ -0,0 +1,87 @@
"""长桥 OpenAPI 配置初始化"""
import os
from pathlib import Path
import click
from longbridge.openapi import Config
# ---------------------------------------------------------------------------
# .env 加载(CLI 包自包含,不依赖 trader 包)
# ---------------------------------------------------------------------------
def _load_dotenv_for_profile(profile: str | None = None) -> None:
"""Load .env (or .{profile}.env) into os.environ.
查找顺序: cwd → home dir。
profile=None + 文件不存在 → 静默跳过(向后兼容)。
profile 非空 + 文件不存在 → 抛 FileNotFoundError。
已存在的环境变量不会被覆盖。
"""
filename = f".{profile}.env" if profile else ".env"
env_path: Path | None = None
for base in [Path.cwd(), Path.home()]:
candidate = base / filename
if candidate.is_file():
env_path = candidate
break
if env_path is None:
if profile is not None:
raise FileNotFoundError(
f"Profile '{profile}' 的 env 文件未找到。"
f"请在当前目录或主目录创建 {filename}"
)
return
with open(env_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = value
# 下单权限控制:设置 LONGBRIDGE_TRADE_ENABLED=true 才允许 buy/sell/cancel
_TRADE_ENV_VAR = "LONGBRIDGE_TRADE_ENABLED"
def is_trade_enabled() -> bool:
"""返回是否已开启交易权限(默认关闭,只读模式)"""
return os.environ.get(_TRADE_ENV_VAR, "").strip().lower() == "true"
def require_trade_enabled() -> None:
"""若未开启交易权限,抛出友好错误并提示配置方式"""
if not is_trade_enabled():
raise click.ClickException(
"当前为只读模式,下单/撤单操作已禁用。\n"
f"如需开启交易权限,请设置环境变量:\n"
f" export {_TRADE_ENV_VAR}=true\n"
"⚠️ 开启后请确保操作正确,下单指令将直接提交至长桥交易系统。"
)
def get_config(profile: str | None = None) -> Config:
"""从环境变量初始化长桥配置。支持 --profile 切换账户。
需要设置以下环境变量:
LONGBRIDGE_APP_KEY
LONGBRIDGE_APP_SECRET
LONGBRIDGE_ACCESS_TOKEN
"""
_load_dotenv_for_profile(profile)
try:
return Config.from_apikey_env()
except Exception as e:
raise click.ClickException(
"无法初始化长桥配置,请确认已设置以下环境变量:\n"
" LONGBRIDGE_APP_KEY\n"
" LONGBRIDGE_APP_SECRET\n"
" LONGBRIDGE_ACCESS_TOKEN\n"
f"错误详情:{e}"
)
@@ -0,0 +1,56 @@
"""统一输出格式模块(text/json"""
import json
import sys
from decimal import Decimal
from typing import Any
import click
from rich.console import Console
from rich.table import Table
console = Console()
class _DecimalEncoder(json.JSONEncoder):
"""将 Decimal 序列化为 float"""
def default(self, obj: Any) -> Any:
if isinstance(obj, Decimal):
return float(obj)
return super().default(obj)
def print_json(data: Any) -> None:
"""以 JSON 格式输出数据"""
click.echo(json.dumps(data, ensure_ascii=False, indent=2, cls=_DecimalEncoder))
def print_table(headers: list[str], rows: list[list[Any]], title: str = "") -> None:
"""用 rich 表格输出数据
Args:
headers: 列标题列表
rows: 数据行列表(每行为与 headers 对应的值列表)
title: 可选表格标题
"""
table = Table(title=title, show_header=True, header_style="bold cyan")
for h in headers:
table.add_column(h, style="white")
for row in rows:
table.add_row(*[str(v) if v is not None else "-" for v in row])
console.print(table)
def print_kv(pairs: list[tuple[str, Any]], title: str = "") -> None:
"""以键值对形式输出(用于单条记录)"""
if title:
console.print(f"[bold cyan]{title}[/bold cyan]")
for k, v in pairs:
v_str = str(v) if v is not None else "-"
console.print(f" [bold]{k}[/bold]: {v_str}")
def print_error(msg: str) -> None:
"""输出错误信息到 stderr"""
click.echo(f"错误:{msg}", err=True)
sys.exit(1)
@@ -0,0 +1,21 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "longbridge-cli"
version = "1.0.0"
description = "长桥 LongPort OpenAPI CLI 工具"
requires-python = ">=3.9"
dependencies = [
"longbridge",
"click>=8.0",
"rich>=13.0",
]
[project.scripts]
longbridge = "longbridge_cli.cli:cli"
[tool.setuptools.packages.find]
where = ["."]
include = ["longbridge_cli*"]
@@ -0,0 +1,3 @@
longbridge
click>=8.0
rich>=13.0