Skip to content

Long-running daemon deployment guide (v1.4.103+)

This guide covers configuration, self-healing, and fallback paths specific to running a daemon long-term (24h+ / 7 days / 30 days) in production.

Applicable scenarios: - VPS / cloud server running daemon 24×7 - Containerized deployment (Docker / Kubernetes) - Multi-account in a single daemon (multi-session) - Options / futures strategies needing persistent subscription + push

If you only run daemon locally for short stretches (CLI usage / manual testing), this guide is not required — defaults are sufficient.


v1.4.103 long-running hardening switches (env opt-in, default OFF)

v1.4.103 adds 2 env opt-in switches that let a long-running daemon proactively renew client_sig before invalidation, and proactively re-login on persistent failures, reducing the frequency of admin reload / restart needs.

Default OFF because both paths depend on server-side response behavior. Production deployments wanting to enable them should first validate the flow on a non-critical account by following this section.

FUTU_CLIENT_SIG_PROACTIVE_REFRESH=1 — 1h-ahead proactive client_sig refresh

FUTU_CLIENT_SIG_PROACTIVE_REFRESH=1 ./futu-opend ...

When effective: at startup, daemon reads the server-provided client_sig_invalid_local_time_s field from the /authority/ response. If this timestamp > 0 (server provided an expiry), the daemon spawns a timer that fires at (invalid_time - 3600s) to refresh qot-login, without waiting for backend errors.

Why default OFF: server acceptance of multi-hour-ahead refresh vs "too early" reject can depend on account and server state. Validate with:

  1. Start daemon: FUTU_CLIENT_SIG_PROACTIVE_REFRESH=1 RUST_LOG=info futu-opend ...
  2. log should contain "v1.4.103 G1: client_sig invalidate scheduled" + ttl_secs
  3. log should contain "v1.4.103 G1: spawning client_sig proactive timer"
  4. After timer fires (production typically 24-48h later), log should contain "v1.4.103 G1: refresh completed"
  5. After timer fires, daemon continues to query / push without disconnect = backend accepts ahead-of-time refresh.

FUTU_CLIENT_SIG_REACTIVE_REFRESH=1 — reactive refresh on persistent tcp_login failures

FUTU_CLIENT_SIG_REACTIVE_REFRESH=1 ./futu-opend ...

When effective: daemon TCP heartbeat fails → reconnect monitor starts. If tcp_login fails ≥ 3 consecutive times during reconnect, daemon assumes client_sig may be invalid, automatically calls AuthRefresher::refresh_qot_login() + auth::reauth_via_remember_login() to obtain a fresh AuthResult, swaps the cached client_sig, and the next tcp_login uses the fresh sig.

Why default OFF: tcp_login failure ret_type semantics are not exclusively "client_sig invalid" — they can also indicate server-side rate limiting or risk control. A misfired refresh may amplify limits. Validate with:

  1. In test environment, advance system clock by +30 days (past natural client_sig expiry), or wait 30 days of long-running.
  2. Start daemon: FUTU_CLIENT_SIG_REACTIVE_REFRESH=1 ...
  3. heartbeat must fail → reconnect monitor starts
  4. log should show "reconnect login failed" (×3)
  5. log should show "v1.4.103 G4: persistent tcp_login failures → trying reactive client_sig refresh"
  6. log should show "v1.4.103 G4: refresh_qot_login OK → reauth_via_remember_login"
  7. Subsequent tcp_login should succeed; daemon recovers push.

⚠️ G1 / G4 remain experimental (default OFF). Multi-hour-ahead refresh acceptance and cipher rate-limit thresholds can vary by account and server state. The table below is for self-validated deployments, not default production guidance.

Environment G1 G4 Notes
Local short-running (CLI / test) OFF OFF Defaults sufficient, no long-runner need
Production long-running (24h+) early adopter, self-validating ON (experimental) OFF Validate G1 on a non-critical account first; G4 still relies on admin reload
Production long-running (7 days+) full opt-in (experimental) ON (experimental) ON (experimental) Expect zero admin reload; validate on a non-critical account before using on critical accounts
Multi-account single daemon (5+ accounts) ON (experimental) OFF G4 risk: one account triggers rate-limit, others affected

⚠️ Validate the full long-running flow in your own environment before flipping these switches ON. Daemon logs redact sensitive values by default, but review logs before sharing. The current state does not claim these switches are safe as default production settings.


Containerization examples

Docker

# docker-compose.yml fragment
services:
  futu-opend:
    image: ghcr.io/futuleaf/futu-opend-rs:v1.4.103
    restart: unless-stopped
    environment:
      FUTU_ACCOUNT: <account>
      FUTU_PWD: <password>
      FUTU_CLIENT_SIG_PROACTIVE_REFRESH: "1"   # long-runner hardening
      # FUTU_CLIENT_SIG_REACTIVE_REFRESH: "1"  # single-account scenarios only
    volumes:
      - ./.futu-opend-rs:/root/.futu-opend-rs    # device_id + credentials persistence
    ports:
      - "11111:11111"   # gRPC
      - "11112:11112"   # REST
      - "11113:11113"   # WebSocket

systemd unit

[Unit]
Description=futu-opend long-running gateway
After=network-online.target

[Service]
Type=simple
User=futu
EnvironmentFile=/etc/futu-opend.env
Environment=FUTU_CLIENT_SIG_PROACTIVE_REFRESH=1
ExecStart=/usr/local/bin/futu-opend --rest-port 11112 --grpc-port 11111
Restart=on-failure
RestartSec=30s

[Install]
WantedBy=multi-user.target

/etc/futu-opend.env contains FUTU_ACCOUNT=... + FUTU_PWD=... (mode 0600, owner=futu).


Self-healing vs manual intervention

Scenario v1.4.103 behavior v1.4.103 behavior env required
client_sig natural expiry (30 days) daemon error → admin reload needed G1 timer auto-renews 1h before FUTU_CLIENT_SIG_PROACTIVE_REFRESH=1
Network jitter, tcp_login 1-2 fails exponential backoff retry same
Network jitter, tcp_login ≥ 3 fails persistent fail / manual intervention G4 auto-refresh + retry FUTU_CLIENT_SIG_REACTIVE_REFRESH=1
Server rate-limit (error_code=15) 60s wait + retry same (G4 doesn't trigger on rate-limit)
Account risk control / password change / device lock Manual SMS / --reset-device same
broker auth_code 30-day expiry v1.4.103 G2 RepullAuthCode auto-refresh same (default ON since v1.4.103)

Troubleshooting

G1 timer not spawning after enable

In log, look for "v1.4.103 G1 skip" reason: - "client_sig_invalid_local_time_s=0": backend didn't return this field → old backend / old credentials shell. --reset-device to redo password auth and obtain new field. - "env FUTU_CLIENT_SIG_PROACTIVE_REFRESH not set to 1": env var didn't propagate. - "TooLate": invalid_time already past or < 1h away, skipped. Restart daemon to let first login obtain new invalid_time.

G4 refresh not triggering

In log, look for "v1.4.103 G4 skip" reason: - "consecutive failures below threshold": failure count not yet 3 - "auth_refresher not injected": internal wiring issue → file an issue - "already attempted refresh this cycle": refresh-fail-loop guard. Restart daemon to reset.

G4 refresh failed, daemon continues with stale auth

By design. Log should contain "v1.4.103 G4: refresh_qot_login failed (continue with stale AuthResult)" or "v1.4.103 G4: reauth_via_remember_login failed (continue with stale AuthResult; user may need admin reload)". G4 is best-effort — failure doesn't break the existing fallback (admin reload path fully retained).


Verification Roadmap

  • After enough real deployments show G1/G4 do not trigger extra rate limiting or risk-control behavior, decide separately whether to flip default ON and update this guide's "default OFF" wording.
  • Actual moomoo-path broker channel routing requires separate validation; current builds only lay infrastructure, while routing still uses the main client_sig for C++-equivalent behavior.

For deployment issues, file an issue or contact via the official site.