案例:自动化交易 bot¶
Python bot 走 gRPC 下单,simulate 先跑一周验证,再切 real。
5 分钟从零到能下第一单¶
假设你已经装好 futu-opend + futucli + Python 3.10+。从零开始:
# 1. 凭据一次性存 OS keychain(避免 --login-pwd 泄漏到 ps aux)
futucli set-login-pwd --account 12345678
# 提示:输入密码 → 密码进 Keychain / Secret Service / Credential Manager
# 2. 首次启动,前台跑 SMS 验证(服务端首次会发短信到你绑定手机)
futu-opend --setup-only --login-account 12345678 --platform futunn
# SMS 到手输验证码,看到 "credentials cached" 即可 Ctrl-C 退出
# 3. 启动网关(无人值守,systemd / Docker / nohup 都行)
futu-opend --login-account 12345678 --platform futunn \
--rest-port 22222 --grpc-port 33333 \
--audit-log /var/log/futu/
# 监听:FTAPI(11111) / REST(22222) / gRPC(33333)
# 4. 看账户列表(v1.4.26 修好后能看到所有 broker 的全部 real/sim 账户)
futucli account
# 输出示例(8 列:Acc ID / Card / Env / Broker / Type / Status / Role / Markets)
# 记下要交易的 acc_id
# 5. 确认行情能拉(订阅 + 拉一次 basic quote)
futucli quote HK.00700,HK.09988
# 6. 实盘交易前先解锁(real 环境必做,sim 跳过)
futucli unlock-trade --trade-pwd-md5 <32位小写hex>
# 7. 下第一单(sim 环境练手,推荐先跑这条)
futucli place-order \
--env simulate --market HK --acc-id <sim_acc_id> \
--code 00700 --side BUY --qty 100 --price 300 --order-type NORMAL
# 8. 查订单 + 成交
futucli order --env simulate --market HK --acc-id <sim_acc_id>
futucli deal --env simulate --market HK --acc-id <sim_acc_id>
走通到这一步,说明 auth + 交易链路全通,bot 只需把 futucli 换成 gRPC 客户端即可。
v1.4.26 新增的行情分析命令¶
v1.4.26 起 futucli 新增 5 个行情分析命令(MCP 对应 tool 同样可用):
# 资金流向时间序列(period_type=1 分时,2/3/4/... 日线/周线)
futucli capital-flow HK.00700 --period-type 1
# 资金分布(超大/大/中/小单流入流出)
futucli capital-distribution HK.00700
# 多只股票市场状态(开盘/休市/午休/盘后)
futucli market-state HK.00700,US.AAPL,HK.09988
# 一批股票各自所属板块(行业/概念/地域)
futucli owner-plate HK.00700,US.AAPL
# 期权链(按到期日列出 call/put 合约)
futucli option-chain HK.00700 --begin 2026-05-01 --end 2026-06-30 --option-type all
MCP 里对应 tool 名:futu_get_capital_flow / futu_get_capital_distribution /
futu_get_market_state / futu_get_owner_plate / futu_get_option_chain,
以及新增的 futu_get_history_kline(带 rehab 控制)+ futu_get_reference
(关联证券)。见 MCP 接入 LLM。
以下是进阶部分 —— 当你确认 5 分钟入门走通后再看。
需求¶
- bot 跑在独立机器,通过 gRPC 连网关
- 策略是低频挂单 + 定时撤单,每天最多 100 单,单笔 ≤ 5 万
- 只做港股
- simulate 验证期一周,之后切 real
- 审计 + 告警 + 熔断必须齐
key 策略¶
两把 key 分环境:
# simulate 验证期
futucli gen-key \
--id bot-sim \
--scopes qot:read,acc:read,trade:simulate \
--allowed-markets HK \
--max-order-value 50000 \
--max-daily-value 500000 \
--max-orders-per-minute 2 \
--hours-window 09:30-16:00 \
--expires 14d # 两周够验证期用
# real 上线
futucli gen-key \
--id bot-prod \
--scopes qot:read,acc:read,trade:real \
--allowed-markets HK \
--max-order-value 50000 \
--max-daily-value 500000 \
--max-orders-per-minute 2 \
--hours-window 09:30-16:00 \
--bind-machines fp_bot_host \
--expires 30d
两把 key 独立统计 —— audit 日志、限额计数、metrics 都按 key_id 区分。
网关 + gRPC¶
futu-opend \
--login-account 12345678 --login-pwd "$FUTU_PWD" \
--grpc-port 33333 \
--grpc-keys-file /etc/futu/keys.json \
--audit-log /var/log/futu/
TLS 终结走 Caddy / nginx(见 部署到生产)。
Python bot 代码框架¶
import grpc
from futu_pb2 import FutuRequest
from futu_pb2_grpc import FutuOpenDStub
# 从 env 读 token
import os
API_KEY = os.environ["BOT_API_KEY"]
# metadata 带 Bearer
metadata_cb = grpc.metadata_call_credentials(
lambda ctx, cb: cb((("authorization", f"Bearer {API_KEY}"),), None)
)
creds = grpc.composite_channel_credentials(
grpc.ssl_channel_credentials(), metadata_cb
)
channel = grpc.secure_channel("api.your-domain.com:443", creds)
stub = FutuOpenDStub(channel)
def get_quote(code: str):
body = encode_get_basic_qot(code)
resp = stub.Request(FutuRequest(proto_id=3004, body=body))
return parse_get_basic_qot(resp.body)
def place_order(acc_id: int, code: str, side: int, qty: int, price: float):
body = encode_place_order(acc_id, code, side, qty, price)
try:
resp = stub.Request(FutuRequest(proto_id=2202, body=body))
return parse_place_order(resp.body)
except grpc.RpcError as e:
code = e.code()
if code == grpc.StatusCode.RESOURCE_EXHAUSTED:
# 限额触发 —— rate / daily / market / symbol / side / hours / per-order
# 读 e.details() 里的 reason 分桶决定怎么处理
reason = e.details()
if "rate limit" in reason:
time.sleep(60) # rate: 等一分钟再重试
raise TransientError(reason)
elif "daily value cap" in reason:
raise StopForToday(reason) # daily: 当天别再交了
else:
raise HardFail(reason) # market / symbol / side / hours: 策略错了
elif code == grpc.StatusCode.PERMISSION_DENIED:
raise ConfigError("key scope 不够")
raise
# 主循环
while trading_window():
data = get_quote("00700")
decision = strategy.decide(data)
if decision:
place_order(**decision)
熔断:本地 circuit breaker¶
防护栏之外的自我保护 —— 万一服务端护栏有 bug,本地也挡一道:
import collections, time
class LocalLimiter:
def __init__(self, max_per_minute=2):
self.max = max_per_minute
self.window = collections.deque()
def try_acquire(self):
now = time.time()
while self.window and now - self.window[0] > 60:
self.window.popleft()
if len(self.window) >= self.max:
return False
self.window.append(now)
return True
limiter = LocalLimiter(max_per_minute=2)
if not limiter.try_acquire():
raise TransientError("local rate limit")
Prometheus 告警¶
alerts.yml(bot 侧)
- alert: BotRejectSpike
expr: rate(futu_auth_events_total{key_id=~"bot-(sim|prod)",outcome="reject"}[5m]) > 0.5
annotations:
summary: "bot {{ $labels.key_id }} 近 5 分钟异常 reject 多"
- alert: BotDailyCapNearLimit
expr: futu_auth_limit_rejects_total{key_id=~"bot-.*",reason="daily"} > 3
annotations:
summary: "bot {{ $labels.key_id }} 当日累计超限 {{ $value }} 次"
上线节奏¶
第 1 周:bot-sim + 极低限额 + 无真实资金
↓ 全绿
第 2 周:bot-sim + 正常限额 + 对比信号与 real 账户(但不真实下单)
↓ 全绿
第 3 周:bot-prod + 5% 资金 + 紧限额
↓ 观察 2 周
第 5 周:bot-prod + 正常资金 + 正常限额
每一步都有:audit 对比、metrics 核对、策略回放。别跳步。