Skip to content

Python SDK tips for futu-opend-rs

If you run Futu Python SDK (futu-api PyPI package) on top of a futu-opend-rs daemon for agents / one-shot scripts, you will likely hit a hang-on-exit issue. This is an SDK design choice, not a daemon bug.

This page explains the minimum fix + recommended pattern.

Symptom

Short script reaches end of if __name__ == "__main__": but does not exit. Hangs from seconds to minutes until OS-killed or user Ctrl-C. Agent frameworks / cron jobs / CI running many such scripts will:

  • Accumulate processes → OOM
  • Leak resources (FDs / memory)
  • Block subsequent tasks

Root cause

Futu Python SDK's futu/common/sys_config.py defaults ALL_THREAD_DAEMON = False. All network / push-callback threads spawned by the SDK are non-daemon threads. Python's main thread waits for non-daemon threads to exit naturally on shutdown, but these threads typically don't (running reactor loops / waiting for new connections).

Why the SDK chose False: a historical decision protecting long-running quant strategies from accidental main-thread exit terminating heartbeat/subscription threads. But it backfires for short-script / agent scenarios.

Three-layer fix

Validated empirically by external reviewer, three-layer fallback:

Layer Location Effect
1. SDK config (root fix) At the very top, before import futu SysConfig.set_all_thread_daemon(True). All SDK-spawned threads thereafter are daemon, exit when main business finishes.
2. Explicit cleanup (belt + suspenders) After ctx.close() Call NetManager.default().stop() to explicitly stop the reactor.
3. Force-exit (last resort) End of script os._exit(0) bypasses any residual non-daemon threads. Not graceful but acceptable for short scripts.

We provide a ~50-LoC helper module that auto-applies layers 1 + 2 (layer 3 is left to the application):

  • examples/python/futu_opend_rs_helper.py — helper module
  • examples/python/example_usage.py — usage demo
  • examples/python/README.md — quickstart

How to get: just copy into your Python project (no pip install needed).

Pattern A — global helper (minimum code change)

# Critical: must import BEFORE `from futu import ...`
import futu_opend_rs_helper  # noqa: F401  global side-effects on import

from futu import OpenSecTradeContext, RET_OK, TrdEnv

ctx = OpenSecTradeContext(host="127.0.0.1", port=22221, is_encrypt=False)
ret, data = ctx.get_funds(trd_env=TrdEnv.SIMULATE, acc_id=28701)
if ret == RET_OK:
    print(data)
ctx.close()
# Script exits immediately, no hang
from futu_opend_rs_helper import futu_session
from futu import OpenSecTradeContext, TrdEnv

with futu_session(
    OpenSecTradeContext,
    host="127.0.0.1",
    port=22221,
    is_encrypt=False,
) as ctx:
    ret, data = ctx.get_funds(trd_env=TrdEnv.SIMULATE, acc_id=28701)
    print(data)
# Auto ctx.close() + NetManager.stop() on with-block exit

When NOT to use the helper

  • 24x7 long-running quant strategies: do NOT import the helper. The SDK's default ALL_THREAD_DAEMON=False protects long-running apps from accidental main-thread exit terminating background heartbeat/subscription threads. Long-runners should explicitly handle SIGTERM and manage ctx lifecycle themselves.

  • Absolute force-exit need: layer 3's os._exit(0) is left to the application — it skips atexit / destructors / logging flush. This helper does not impose it.

What the SDK should do (out of our control)

Ideally the Futu Python SDK should:

  • Default ALL_THREAD_DAEMON = True (safer one-shot script default)
  • README annotation: "long-running apps set False; one-shot scripts leave True"
  • Built-in atexit hook for automatic NetManager cleanup
  • Provide with FutuClient(...) context manager for automatic lifecycle

If you can submit feedback / PR to the Futu SDK team, long-term benefit for all users.

Compatibility

  • futu-api >= 7.x verified (2024-2026 versions)
  • Python >= 3.8
  • Daemon-side: no requirement — futu-opend-rs / C++ FutuOpenD both work (Python SDK talks to daemon via binary protocol, agnostic of daemon implementation language)