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:
@@ -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)
|
||||
Reference in New Issue
Block a user