Skip to content
markpaper

README.md

v0.1.0 · 13 KB

Download file
# @markpaper/qfex-kit

A local, unpublished TypeScript toolkit for QFEX protocol and account primitives. Node.js 22.12 or later, ESM, Apache-2.0. Importing the package starts no socket, timer, request, or account operation. Factories keep state per instance and accept injected transports and clocks.

This package contains exchange functionality: exact decimal arithmetic, metadata and grids, HMAC, REST, trade sessions, public market data, pacing, placement memory, order evidence, account reads, and leverage. It contains no trading strategy, copying logic, account database, credential configuration, or account observations. Test symbols, identifiers, quantities, and balances are synthetic.

## Installation and entries

The package is not published to npm. Use a tarball produced from this repository, or the local workspace package:

```sh
pnpm --filter @markpaper/qfex-kit build
pnpm --filter @markpaper/qfex-kit pack --pack-destination ./artifacts
# In another project, install the tarball path reported by pnpm pack:
pnpm add ./path/to/markpaper-qfex-kit-0.1.0.tgz ws
```

| Entry | Contents |
|---|---|
| `@markpaper/qfex-kit` | Stable namespaces and matching flat named exports |
| `@markpaper/qfex-kit/testing` | `createFakeExchange`, fixture builders and wire adapters |
| `@markpaper/qfex-kit/experimental` | Optional fill ledger, count consistency check, header-auth session and stream-age market data |

The experimental and testing factories are absent from the root export. Tests and source examples are available in this repository; installable tarballs include compiled implementations and declarations.

## API map

| Namespace | Main functions and behavior |
|---|---|
| `numbers` | `parseDec`, decimal arithmetic/comparison, `stepsOf`, `snapToStep`, `isOnStep`, `snapToGrid`, `parseLossless`. Numeric JSON tokens remain strings. Exact decimals may exceed JavaScript number range; account, quantization and market-data reporting reject nonfinite conversions. |
| `markets` | `toSymbol`, `parseSymbol`, `decodeRefdata`, `createQuant`, `mergeRefdata`, `assertMarketsSane`, `createMarketCache`, `listingOf`, `selectBand`. Preserve case and nondecimal lot/tick sizes. Freeze watched missing, invalid or changed grids until reviewed. Stale data remains visibly stale. |
| `auth` | `hmacSignature`, `buildRestAuthHeaders`, `buildWsAuthFrame`, `wsQueryAuthUrl`, `createNonceSource`, write allowlists, auth failure classification, `maskSecrets`, `createOrderTag`. Tag magic, flags and sequence belong to the caller; the package assigns no ownership roles. |
| `rest` | `QFEX_HOSTS`, `createRestClient`, `qfexPath`, contract/book decoders, retry-after/cache/skew helpers, `QfexHttpError`. Public/signed GET, signed POST, reference data, contracts and books; explicit account selection. |
| `frames` | `decodeTradeFrame`, `decodeTradeValue`, `decodeQfexOrder`, `decodeQfexFill`, `decodeQfexIncoming`, `statusClass`, `canonicalStatus`, `remainingIsZero`, `isFinalOrderEvent`. Known response envelopes become discriminated unions; malformed/ambiguous input remains unknown or reports malformed rows. |
| `session` | `createTradeSession`, `buildAddOrderFrame`. Authenticate, subscribe, submit/cancel, serialize reads of the same response kind, correlate exact client IDs and order IDs, expose events/health, and close explicitly. Read timeout recycles the connection epoch. Contradictory symbol, side or order identity cannot confirm a write. |
| `mds` | `createMarketDataStream`: `watch`, `bbo`, `band`, `freshBand`, `health`, `close`. Bands use their own frame timestamp; a new BBO does not renew an old band. Reconnect invalidates old-epoch observations. |
| `throttle` | `createRateBudget`, `createAddSlot`, published weights and budget interval. General and cancel holds are separate. Local EMA decay calibration is supplied by the caller; it is an estimator rather than an exact model of the exchange. Share one budget/add slot across sessions that must share pacing. |
| `pending` | `createWriteMemory`, `placementKey`, `cancelTruth`. Brief placement echoes and cancel masks, placement keys, unknown IOC/close/reduce locks, exact-ID late-event/history resolution. Expiring an unknown requires an independent caller evidence callback. |
| `orders` | `iocFill`, `clampIocPrice`, `fullCloseSize`, `reducible`, `classifyReject`, `rejectScope`, and the stable session factory. IOC evidence uses sent size, cumulative remainder and deduplicated trades; identity is required. Conflicting witnesses use the conservative quantity. |
| `account` | `decodePositions`, `normalizeOpenOrders`, `decodeHistoricByCloid`, `deriveEquity`, `judgeEquity`, `decodeSubaccountEquity`, `judgeOrderPages`, `readCompleteOpenOrders`, `createAccountReader`, `mids`. Signed positions, strict balances, canonical account IDs, complete order-page checks and bracketed account reads. |
| `leverage` | `createLeverageClient`, `leverageChangeAllowed`. Read supported/current levels, reserve a symbol before asynchronous checks, recheck exposure immediately before setting, verify the reported result and hold unknown outcomes. |

Original descriptive names such as `decodeQfexRefdataRows` and `qfexQuant` remain available alongside the shorter aliases. Declarations provide the complete parameter and result types.

## Explicit policies and lifecycle

No application environment is read by the library. Supply keys, canonical account selection, ownership predicates, safety decisions and workload policies explicitly. Omitted required policy throws; clocks, `fetch`, WebSocket construction, entropy and logging can be injected. Error bodies and structured REST problems mask configured secrets.

```ts
import {
  createRestClient, createTradeSession, QFEX_HOSTS,
  type RestClientOptions, type TradeSessionOptions,
} from '@markpaper/qfex-kit';

// The caller chooses pacing, retry, timeout and diagnostic-retention policy.
export function makePublicReader(policy: Omit<RestClientOptions, 'baseUrl' | 'readOnly'>) {
  return createRestClient({ ...policy, baseUrl: 'uat', readOnly: true });
}

export function makeTradeConnection(policy: Omit<TradeSessionOptions, 'url'>) {
  const session = createTradeSession({ ...policy, url: QFEX_HOSTS.uat.trade });
  // Call ensureStarted(), await whenReady(timeoutMs), and always await close().
  return session;
}
```

REST requires a rate ceiling, fallback/bounded rate backoff, retry-delay function, retry count, timeout/wait budget, skew-history capacity and read-only flag. POST retries only definite throttling or one fresh-nonce signature attempt; transport failure, server error, cached signed response or undecodable successful response produces an unknown write outcome without resubmission.

Sessions require authentication/subscription/handshake/heartbeat/reconnect timings, buffer/payload limits, read/fill-grace policy, page size, log/close/body limits, bounded stray-fill and trade-ID memory, and sent-client-ID capacity. The sent-ID store refuses further adds at capacity instead of forgetting an old ID. It lasts for this instance only: persist ownership and unknown intents before restarting an application. Reusing a client ID is not assumed idempotent. A caller-owned add slot serializes ambiguous acknowledgements; no factory submits an order automatically.

Market-data factories require timings, payload/log/close limits and subscription frame size. A market cache requires refresh and failed-refresh intervals. Unknown-write memory requires its retention/hold timings and `canExpire`; timeout or an empty complete overview alone does not prove that a sent order never executed. Leverage changes require an exposure check and unknown-outcome hold. Local timing and capacity values are application policy, not exchange constants.

## Evidence and limitations

- A placement acknowledgement is not a fill. `SubmitResult` distinguishes `not_sent`, `rejected`, `acked` and `unknown`. A timeout/drop after send stays unknown. Do not resubmit until independently resolved.
- `FILLED` events with remaining quantity can describe partial executions. IOC terminal status with zero remaining alone does not establish fill quantity. `iocFill` requires symbol, side, client ID and known order ID or `null`; contradictory identities remain unconfirmed. Position-delta evidence requires caller-proven freshness and no intervening execution.
- Cancellation response and absence do not exclude a racing fill. Use `cancelTruth` with the event and observed-fill flag. Partial `FILLED` is not final; a missing-order reply cannot prove disappearance.
- REST position/equity endpoints expose no execution watermark. `readAccountSnapshot` compares bracket positions and returns `consistency: 'positions_agree'`; agreement can describe the same stale indexed state. Equity acceptance reflects the selected source/tolerance policy, not guaranteed exchange freshness. `readFreshPosition` requires caller evidence and returns `uncertain` when that evidence is absent.
- Repeated IDs, a full final page, epoch change or unapproved multiple pages prevent a complete order overview. TWAP and stop-order records are decoded but the account reader refuses to claim a supported complete account when they are present.
- Reference-data bands can be cached. `selectBand` marks them unfit for a live IOC clamp. Quantization respects metadata quantity bounds and does not impose a universal dollar minimum.
- Preserve canonical UUID spelling returned by account discovery; do not rewrite symbol or client-ID case. The current tag utility requires caller-supplied eight-character hexadecimal magic and encodes no strategy or account identity.
- No account-binding proof is inferred from equal REST and WebSocket order counts. Scope of subaccount event streams and authenticated UAT behavior remains unverified by this package.

## Experimental entry

```ts
import {
  createFillLedger, checkBinding,
  createHeaderAuthTradeSession, createStreamAgeMarketData,
} from '@markpaper/qfex-kit/experimental';
```

`createFillLedger` accepts explicit baseline anchors, trade-ID deduplication and gap/history policies. It tracks expected signed quantities and reports `consistent`, `lagging` or `uncertain`. Stream gaps, unanchored or unexplained changes, bounded history and expired deduplication leave holes. Equal quantities can occur before and after offsetting executions. The ledger is never wired into the account reader automatically and cannot guarantee REST freshness.

`checkBinding` reports count consistency or disagreement; matching counts cannot prove that two transports address the same account. It places no probe orders. `createHeaderAuthTradeSession` explicitly enables the observed header handshake variant, which still sends an auth frame. `createStreamAgeMarketData` explicitly enables the unverified assumption that another symbol frame can renew an older price band's age. Normal factories use query authentication and the band's own timestamp.

## Testing and public smoke

`createFakeExchange` uses in-memory sockets, synthetic order storage and explicit delay/drop/rejection controls. It performs no network I/O and requires `close()`. Its controls represent adverse simulation cases, not a complete exchange or verified venue behavior. Unit tests cover exact numbers, grids, nonce/IDs, retries/redaction, unknown writes, cancellation races, correlation contradictions, stale bands, incomplete reads, numeric overflow and leverage serialization.

```sh
pnpm --filter @markpaper/qfex-kit typecheck
pnpm --filter @markpaper/qfex-kit test
pnpm --filter @markpaper/qfex-kit build
```

The `smoke` script reads only public reference data and contract summaries. Set `QFEX_KIT_SMOKE_ENVIRONMENT` to `uat` or `mainnet` and supply positive values for `QFEX_KIT_SMOKE_RPS`, `QFEX_KIT_SMOKE_BACKOFF_MS`, `QFEX_KIT_SMOKE_BACKOFF_MIN_MS`, `QFEX_KIT_SMOKE_BACKOFF_MAX_MS`, `QFEX_KIT_SMOKE_RETRY_DELAY_MS`, `QFEX_KIT_SMOKE_READ_TRIES`, `QFEX_KIT_SMOKE_TIMEOUT_MS`, `QFEX_KIT_SMOKE_MAX_WAIT_MS`, and `QFEX_KIT_SMOKE_SKEW_SAMPLES`; then run `pnpm --filter @markpaper/qfex-kit smoke`. It uses no keys, account identifiers, signed reads or writes. Public connectivity does not verify account or order behavior.

## Protocol sources

Protocol documentation was checked on 2026-10-03. Public [QFEX documentation](https://docs.qfex.com/) and [WebSocket rate limits](https://docs.qfex.com/websocket/rate) describe authentication, transports, buckets and weights. Limits and weights can change; handle exchange throttling even with a local estimator. The observed combined resting-order ceiling of 20 per symbol is dated 2026-10-01, not a permanent guarantee. Other authenticated observations are bounded by their dates and do not establish all subaccount modes.

The accompanying repository knowledge base is in `knowledge/qfex`: `api-and-auth.md`, `markets-and-numbers.md`, `market-data.md`, `orders.md`, `account-and-leverage.md`, `rate-limits.md`, `ops.md`, and `pitfalls.md`. It records dated evidence and unresolved behavior separately from implementation safeguards. The official Go CLI is a behavioral reference; this package does not include its GPL code.

## License

Apache-2.0. Retain [LICENSE](LICENSE) and [NOTICE](NOTICE) when redistributing this package or derivative work.
All files