Case: Automated trading bot¶
Python bot places orders via gRPC, runs in simulate mode for a week, then switches to real.
5 minutes: zero to first order¶
Assuming futu-opend + futucli + Python 3.10+ are installed:
# 1. One-time: stash password in OS keychain (avoid --login-pwd in ps aux)
futucli set-login-pwd --account 12345678
# 2. First run, foreground for SMS verification
futu-opend --setup-only --login-account 12345678 --platform futunn
# Enter SMS code when prompted; Ctrl-C when you see "credentials cached"
# 3. Start the gateway unattended (systemd / Docker / nohup)
futu-opend --login-account 12345678 --platform futunn \
--rest-port 22222 --grpc-port 33333 \
--audit-log /var/log/futu/
# Listens: FTAPI(11111) / REST(22222) / gRPC(33333)
# 4. List accounts (v1.4.26 fixes show all broker's real/sim accounts)
futucli account
# 8 columns: Acc ID / Card / Env / Broker / Type / Status / Role / Markets
# Note the acc_id you plan to trade on.
# 5. Verify quotes work
futucli quote HK.00700,HK.09988
# 6. Unlock trading (real only; skip for simulate)
futucli unlock-trade --trade-pwd-md5 <32-char-lowercase-hex>
# 7. Place first order (simulate env first, strongly recommended)
futucli place-order \
--env simulate --market HK --acc-id <sim_acc_id> \
--code 00700 --side BUY --qty 100 --price 300 --order-type NORMAL
# 8. Inspect orders & fills
futucli order --env simulate --market HK --acc-id <sim_acc_id>
futucli deal --env simulate --market HK --acc-id <sim_acc_id>
Once this works end-to-end, the bot just needs to swap futucli for the gRPC client.
New v1.4.26 analysis commands¶
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
futucli option-chain HK.00700 --begin 2026-05-01 --end 2026-06-30 --option-type all
MCP tools: futu_get_capital_flow / futu_get_capital_distribution /
futu_get_market_state / futu_get_owner_plate / futu_get_option_chain,
plus futu_get_history_kline (with rehab control) and futu_get_reference
(related securities). See MCP Integration.
Advanced details below — read after you've completed the 5-minute warmup.
Requirements¶
- Bot runs on its own machine, connects to the gateway via gRPC.
- Strategy: low-frequency limit orders + timed cancellations; max 100 orders/day, ≤50k per order.
- HK market only.
- One-week simulate validation before cutover to real.
- Audit + alerting + circuit breaker all in place.
Key strategy¶
Two keys, one per environment:
# simulate validation
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 # two weeks covers the validation window
# real production
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
The two keys track independently — audit logs, limit counters, and
metrics are all keyed by key_id.
Gateway + 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 termination via Caddy / nginx (see Production deploy).
Python bot code skeleton¶
import grpc
from futu_pb2 import FutuRequest
from futu_pb2_grpc import FutuOpenDStub
# read token from env
import os
API_KEY = os.environ["BOT_API_KEY"]
# metadata carries 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:
# limit tripped — rate / daily / market / symbol / side / hours / per-order
# read the reason bucket from e.details() and decide
reason = e.details()
if "rate limit" in reason:
time.sleep(60) # rate: wait a minute and retry
raise TransientError(reason)
elif "daily value cap" in reason:
raise StopForToday(reason) # daily: stop trading for today
else:
raise HardFail(reason) # market / symbol / side / hours: strategy bug
elif code == grpc.StatusCode.PERMISSION_DENIED:
raise ConfigError("key scope insufficient")
raise
# main loop
while trading_window():
data = get_quote("00700")
decision = strategy.decide(data)
if decision:
place_order(**decision)
Circuit breaker: local rate limiter¶
Defense in depth — in case the server-side guard has a bug, protect yourself locally too:
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¶
- alert: BotRejectSpike
expr: rate(futu_auth_events_total{key_id=~"bot-(sim|prod)",outcome="reject"}[5m]) > 0.5
annotations:
summary: "bot {{ $labels.key_id }} excessive rejects in the last 5m"
- alert: BotDailyCapNearLimit
expr: futu_auth_limit_rejects_total{key_id=~"bot-.*",reason="daily"} > 3
annotations:
summary: "bot {{ $labels.key_id }} hit the daily cap {{ $value }} times today"
Rollout cadence¶
Week 1: bot-sim + very tight limits + no real money
↓ all green
Week 2: bot-sim + normal limits + compare signals vs real account (still no real orders)
↓ all green
Week 3: bot-prod + 5% capital + tight limits
↓ observe for 2 weeks
Week 5: bot-prod + full capital + normal limits
Every step includes: audit diff review, metrics reconciliation, strategy replay. No skipping steps.