npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

better-moomoo-api

v0.0.2

Published

A clean, typed, reconnect-aware TypeScript SDK for the moomoo/futu OpenD binary protocol

Readme

better-moomoo-api

A clean, typed, reconnect-aware TypeScript SDK for the moomoo/futu OpenD binary protocol. Pure ESM, zero runtime dependencies beyond @bufbuild/protobuf (protobuf-es).

Design rationale and protocol analysis: see DESIGN.md.

Why not the official package?

  • Transparent reconnect: subscriptions, trade unlock and account-push registration are desired state — replayed automatically after every reconnect.
  • Fail-fast semantics: requests during a disconnect reject with DisconnectedError immediately; nothing is silently queued or retried (especially not orders).
  • A real error model: TransportError (retryable) / ProtocolError (bug) / ApiError (OpenD said no, with retType/retMsg) are distinct classes.
  • Typed end to end: generated from the official proto files; enums exported as values (QotMarket.QotMarket_US_Security), no magic numbers.
  • Layered kernel (codec → crypto → conn → client → qot/trd): the wire kernel knows nothing about business protocols; every layer is unit-tested against a mock OpenD, including padding edge cases of the FTAES_ECB cipher.

Install & build

Requires Node.js >= 24 (current LTS). Built with TypeScript 7.

pnpm install
pnpm gen:proto      # buf + protoc-gen-es: proto/*.proto -> src/proto/gen/ (committed output optional)
pnpm gen:registry   # protocol id table -> src/proto/registry.ts
pnpm test
pnpm build

Try it

pnpm demo runs examples/demo.mjs against a running OpenD. By default it is pure request/response — snapshot, static info, market state, quota — with no subscription at all, so it works outside market hours.

pnpm demo                                  # 127.0.0.1:11111, HK.00700
pnpm demo --code 00001
pnpm demo --host 192.168.1.10 --port 11111
pnpm demo --subscribe                      # add the subscription + push feed

Restart OpenD mid-run to watch reconnect replay subscriptions.

Do I need a subscription?

If you just want a number, no — use getSecuritySnapshot. It needs no subscription, works outside market hours, and returns strictly more than getBasicQot: the same OHLC and volume, plus PE/PB/EPS, market cap, dividend and bid/ask. Subscribe only when you want changes pushed to you as they happen.

Why some reads fail without one

Ask: does OpenD already have this data in hand?

getSecuritySnapshot and friends go out to the server on every call, so they always work. getBasicQot and friends never leave the machine — they read the copy of the realtime data that OpenD keeps in memory, and OpenD only maintains that copy for symbols you have subscribed to. No subscription, no copy:

ApiError: Qot_GetBasicQot(3004) failed: retType=-1
          00700 请求获取实时报价接口前,请先订阅Basic数据

That memory-resident set is also why subscriptions belong to a connection rather than an account, and therefore why a reconnect has to replay them.

Which reads are gated

Exactly the six kinds OpenD holds in memory. Qot_Common.SubType enumerates them, so that enum is the test — in it, you need a subscription first; not in it, plain request/response. Which side an API falls on is OpenD's rule, not this SDK's.

| SubType value | gated read | | ------------------------------ | ------------------ | | SubType_Basic | qot.getBasicQot | | SubType_OrderBook (+ _Odd) | qot.getOrderBook | | SubType_Ticker | qot.getTicker | | SubType_RT | qot.getRT | | SubType_KL_* | qot.getKL | | SubType_Broker | Qot_GetBroker |

Everything else is plain request/response — getSecuritySnapshot, getStaticInfo, getMarketState, getSubInfo, requestHistoryKL (quota-limited), and all of client.trd.*.

A tempting but wrong test is "does it have a Qot_Update* push protocol". That set is strictly larger: Qot_UpdatePriceReminder and Qot_UpdateOptionEvent are pushes too, but they are registered by Qot_SetPriceReminder / Qot_SetOptionEventAlert instead of Qot_Sub — so Qot_GetPriceReminder needs no subscription despite having a live feed.

Quick start

import {
  MoomooClient,
  QotMarket,
  SubType,
  TrdEnv,
  TrdMarket,
  TrdSide,
  OrderType,
} from 'better-moomoo-api';

const client = new MoomooClient({ host: '127.0.0.1', port: 11111 });
await client.connect();

// --- market data ---
const tencent = { market: QotMarket.QotMarket_HK_Security, code: '00700' };
await client.qot.subscribe([tencent], [SubType.SubType_Basic]);
for await (const q of client.qot.basicQotStream()) {
  console.log(q.security?.code, q.curPrice);
}

// --- trading ---
await client.trd.unlockTrade({ password: '******' });
const [acc] = await client.trd.getAccList();
const header = { trdEnv: TrdEnv.TrdEnv_Real, accID: acc!.accID, trdMarket: TrdMarket.TrdMarket_HK };
await client.trd.placeOrder({
  header,
  trdSide: TrdSide.TrdSide_Buy,
  orderType: OrderType.OrderType_Normal,
  code: '00700',
  qty: 100,
  price: 500,
});

Encrypted OpenD

import { readFileSync } from 'node:fs';
const client = new MoomooClient({
  rsaPrivateKey: readFileSync('/path/to/rsa_private_key.pem', 'utf8'),
  // packetEncAlgo defaults to FTAES_ECB, matching OpenD's default
});

Anything not wrapped yet

All 144 known protocols are callable through the generic escape hatch:

import { Proto, proto } from 'better-moomoo-api';
const s2c: proto.Qot_GetIpoList.S2C = await client.request(Proto.QotGetIpoList, { market: 1 });

Module map

| module | responsibility | | --------- | ---------------------------------------------------------------------- | | codec/ | 44-byte header pack/unpack, streaming de-framer (pure functions) | | crypto/ | Cipher implementations: None / RSA / FTAES_ECB / AES_ECB / AES_CBC | | proto/ | generated protobuf module + protocol id registry | | conn/ | one TCP connection: handshake, cipher swap, heartbeat, serial matching | | client/ | reconnect with backoff, desired-state replay, middleware, push routing | | qot/ | market data API, refcounted subscriptions, push streams | | trd/ | trading API, unlock management, PacketID injection |

Caveats

  • Push protocol format is always Protobuf (pushProtoFmt = 0); JSON mode is not implemented.
  • The FTAES_ECB implementation round-trips against itself and follows the documented padding scheme, but has not yet been verified against an encryption-enabled OpenD.
  • WebSocket transport (InitWebSocket) is not implemented; TCP only.