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

@himalaya-quant/ctrader-x

v0.0.29

Published

A typescript cTrader OpenAPI implementation

Readme

@himalaya-quant/ctrader-x

A TypeScript client for the cTrader Open API. Strongly typed, with a clean abstraction over the underlying ProtoBuffer protocol.

npm i @himalaya-quant/ctrader-x

Table of Contents


Quick Start

import { cTraderX } from '@himalaya-quant/ctrader-x';

const client = new cTraderX();

await client.connect();

const symbols = await client.symbols.getSymbolsList();
console.log(symbols);

client.disconnect();

Configuration

All options are optional. When omitted, credentials are read from environment variables (see Authentication).

const client = new cTraderX({
    live: false, // Connect to live API. Defaults to demo.
    clientId: '...',
    clientSecret: '...',
    accessToken: '...',
    ctidTraderAccountId: 123456,
    debug: false, // Enable verbose debug logs
    autoReconnect: true, // Auto-reconnect on server disconnection
    reconnectIntervalMs: 5000, // Wait 5s between reconnection attempts
    logger: customLogger, // Custom ILogger implementation
});

| Option | Type | Default | Description | | --------------------- | --------- | -------- | ------------------------------------------------------------------ | | live | boolean | false | Connects to live.ctraderapi.com instead of demo.ctraderapi.com | | clientId | string | env | Spotware application client ID | | clientSecret | string | env | Spotware application client secret | | accessToken | string | env | OAuth access token | | ctidTraderAccountId | number | env | The trader account ID to operate on | | debug | boolean | false | Emits additional debug-level log entries | | autoReconnect | boolean | true | Automatically reconnects if the connection is lost | | reconnectIntervalMs | number | 5000 | Fixed wait time (ms) between reconnection attempts | | logger | ILogger | built-in | Custom logger (see Logging) |


Authentication

Credentials can be provided inline or via .env:

CTRADERX_SPOTWARE_CLIENT_ID=
CTRADERX_SPOTWARE_CLIENT_SECRET=
CTRADERX_SPOTWARE_ACCESS_TOKEN=
CTRADERX_SPOTWARE_CTID_TRADER_ACCOUNT_ID=

connect() performs both application-level and account-level authentication automatically. There is no need to call any auth method manually.


Connection Lifecycle

await client.connect(); // Opens TCP connection, authenticates app + account
client.disconnect(); // Closes the connection and disposes all listeners

Calling connect() when already connected is a no-op — it returns immediately without opening a second connection or re-authenticating.

Calling disconnect() before connect() is safe and has no effect.

Auto-reconnect is enabled by default. The client uses a dead man's switch that monitors server silence — if no message is received for 30 seconds (the server normally sends heartbeats every ~15 seconds), the connection is considered stale and a reconnect is triggered automatically. On reconnect failure (e.g. network still down), the client retries at a fixed interval (reconnectIntervalMs, default 5s) until the connection is restored. Set autoReconnect: false to disable this behaviour entirely.

Calling disconnect() during a reconnect loop stops the loop immediately. No further reconnection attempts are made.

⚠️ Accessing client.orders, client.symbols, or client.symbolsUpdates before calling connect() throws a ClientNotConnectedError.


Symbols

Accessible via client.symbols after connecting.

Get symbols list

Returns the full list of symbols available for the account, enriched with their details.

const symbols = await client.symbols.getSymbolsList();
// symbols[0].symbolId, symbols[0].symbolName, ...

// Include archived symbols
const all = await client.symbols.getSymbolsList({
    includeArchivedSymbols: true,
});

Get symbol details

Fetches full detail records for one or more symbols by ID.

const details = await client.symbols.getSymbolsDetails([1, 10026]);
console.log(details.symbol[0].schedule);

Get historical bars (OHLCV)

Returns an array of OHLCV tuples: [timestamp, open, high, low, close, volume].

import { ProtoOATrendbarPeriod } from '@himalaya-quant/ctrader-x';

const bars = await client.symbols.getTrendBars({
    symbolId: 1,
    period: ProtoOATrendbarPeriod.M1,
    fromTimestamp: Date.now() - 1000 * 60 * 60, // 1 hour ago
    toTimestamp: Date.now(),
});

// bars[0] → [timestamp, open, high, low, close, volume]

Live Bars

Accessible via client.symbolsUpdates after connecting.

Streams real-time OHLCV updates for a given symbol and timeframe. Each emission updates the current forming bar — it is not a completed candle event, it is a live tick-level update.

import { tap } from 'rxjs';
import { ProtoOATrendbarPeriod } from '@himalaya-quant/ctrader-x';

client.symbolsUpdates
    .subscribeLiveTrendBars({
        symbolId: 10026, // BTCUSD
        period: ProtoOATrendbarPeriod.M1,
    })
    .pipe(
        tap(({ symbolId, ohlcv, period }) => {
            const [time, open, high, low, close, volume] = ohlcv;
            console.log(`[${period}] O:${open} H:${high} L:${low} C:${close}`);
        }),
    )
    .subscribe();

Each emission carries:

| Field | Type | Description | | ---------- | ----------------------- | ------------------------------------------------------- | | symbolId | number | Symbol the update belongs to | | period | ProtoOATrendbarPeriod | The subscribed timeframe | | ohlcv | OHLCV | Current bar as [time, open, high, low, close, volume] |

Multiple subscribers, same symbol

Calling subscribeLiveTrendBars multiple times with the same symbolId and period replaces the previous subscriber — the old observable is completed, and the new one takes over. To run two independent consumers on the same feed, .pipe() and .subscribe() on the returned observable from a single call.

Unsubscribe

await client.symbolsUpdates.unsubscribeLiveTrendBars({
    symbolId: 10026,
    period: ProtoOATrendbarPeriod.M1,
});

The underlying API subscription is only removed when there are no remaining subscribers for that symbolId. Unsubscribing a period that still has other period-subscribers on the same symbol will not send an unsubscribe request to the server.

Subscription persistence across reconnects

Active live bar subscriptions are restored automatically after a reconnect. No manual re-subscription is needed.


Orders & Positions

Accessible via client.orders after connecting.

All order operations require that the access token has trade permissions on the account.

Place a new order

Use LotsUtil.lotsToVolume() to convert standard lot sizes to the protocol's internal volume format.

import {
    ProtoOAOrderType,
    ProtoOATradeSide,
    LotsUtil,
} from '@himalaya-quant/ctrader-x';

await client.orders.newOrder({
    symbolId: 1,
    clientOrderId: 'my-order-001', // Your own reference ID
    orderType: ProtoOAOrderType.MARKET,
    tradeSide: ProtoOATradeSide.BUY,
    volume: LotsUtil.lotsToVolume(0.01), // 0.01 lots = 100,000 protocol units
});

newOrder resolves when the order is accepted by the server, not when it is filled. Subscribe to order events to track execution.

Close a position

await client.orders.closePosition({
    positionId: 123456789,
    volume: LotsUtil.lotsToVolume(0.01), // Can be partial
});

Get open positions and pending orders

const { position, order } = await client.orders.getOpenPositions();

Returns all current open positions (position[]) and pending orders (order[]) for the account.

Get unrealized PnL

const result = await client.orders.getPositionUnrealizedPnL();

Cancel a pending order

await client.orders.cancelPendingOrder({ orderId: 987654321 });

Modify a pending order

await client.orders.modifyPendingOrder({
    orderId: 987654321,
    volume: LotsUtil.lotsToVolume(0.02),
    limitPrice: 1.085,
});

Volume conversion

LotsUtil converts between human-readable lot sizes and the cTrader protocol's internal volume unit:

LotsUtil.lotsToVolume(0.01); // → 100_000   (micro lot)
LotsUtil.lotsToVolume(0.1); // → 1_000_000  (mini lot)
LotsUtil.lotsToVolume(1); // → 10_000_000 (standard lot)

LotsUtil.volumeToLots(100_000); // → 0.01

Order Events

Subscribe to a real-time stream of execution events for the account. The observable is hot and shared — all subscribers receive the same events.

import {
    OrderAcceptedEvent,
    OrderFilledEvent,
    OrderRejectedEvent,
    OrderCancelledEvent,
    OrderExpiredEvent,
    OrderErrorEvent,
} from '@himalaya-quant/ctrader-x';

client.orders.subscribeOrdersEvents().subscribe((event) => {
    if (event instanceof OrderFilledEvent) {
        console.log('Filled:', event.order.positionId, event.deal);
    }

    if (event instanceof OrderRejectedEvent) {
        console.log('Rejected:', event.order.orderId);
    }

    if (event instanceof OrderErrorEvent) {
        console.log('Error:', event.error.description);
    }
});

| Event class | Trigger | | --------------------- | ----------------------------------------- | | OrderAcceptedEvent | Order received and accepted by the server | | OrderFilledEvent | Order fully executed | | OrderRejectedEvent | Order rejected by the server | | OrderCancelledEvent | Pending order cancelled | | OrderExpiredEvent | Pending order expired | | OrderErrorEvent | Protocol-level order error |

All events (except OrderErrorEvent) carry .order (ProtoOAOrder) and optionally .deal (ProtoOADeal).


Logging

The built-in logger writes to console. Debug logs are suppressed by default.

Enable debug output via environment variable:

CTRADERX_DEBUG_LOGS=true

Or inject a custom logger at instantiation:

const client = new cTraderX({
    logger: {
        debug: (msg) => myLogger.debug(msg),
        log: (msg) => myLogger.info(msg),
        warn: (msg) => myLogger.warn(msg),
        error: (msg) => myLogger.error(msg),
    },
});

Error Handling

Every manager method throws a typed error on failure. All error classes extend a common base and wrap the original server error.

import { OpenOrderError } from '@himalaya-quant/ctrader-x';

try {
    await client.orders.newOrder({ ... });
} catch (e) {
    if (e instanceof OpenOrderError) {
        // server rejected the order request
    }
}

| Error class | Thrown by | | ------------------------------------- | ------------------------------------------- | | ConnectionError | client.connect() | | ClientNotConnectedError | Accessing any manager before connect() | | OpenOrderError | orders.newOrder() | | ClosePositionError | orders.closePosition() | | CancelPendingOrderError | orders.cancelPendingOrder() | | ModifyPendingOrderError | orders.modifyPendingOrder() | | GetOpenPositionsError | orders.getOpenPositions() | | GetPositionUnrealizedPnLError | orders.getPositionUnrealizedPnL() | | GetSymbolsListError | symbols.getSymbolsList() | | GetSymbolsDetailsError | symbols.getSymbolsDetails() | | GetTrendBarsError | symbols.getTrendBars() | | SubscribeLiveTrendBarsInternalError | symbolsUpdates.subscribeLiveTrendBars() | | UnsubscribeLiveTrendBarsError | symbolsUpdates.unsubscribeLiveTrendBars() |


Design Behaviours

The following are intentional behaviours, not bugs.

newOrder() resolves before fill. The promise resolves when the server acknowledges the request. Use subscribeOrdersEvents() and listen for OrderFilledEvent to react to execution.

Live bars emit the forming candle, not closed candles. Every subscribeLiveTrendBars emission is an update to the current open bar. There is no separate "candle closed" event — a new bar starting is implicit when the ohlcv[0] timestamp changes.

CLOSE in live bars is the current tick price. The cTrader protocol does not provide deltaClose on open bars. The close price in each emission is always the latest bid/ask midpoint, not a settled close.

Duplicate subscribeLiveTrendBars calls replace the subscriber. Calling the method again with the same symbolId and period completes the previous observable and returns a new one. This is by design to avoid silent fan-out and duplicate processing. If you need multiple consumers on the same stream, use RxJS Subject or share() on the observable yourself.

getSymbolsList() is not a lightweight call. Internally it fetches the symbol list and then enriches each entry with full symbol details in a second request. Avoid calling it in hot paths or on every reconnect.

Order events are globally shared. subscribeOrdersEvents() returns a reference to a single Subject. All .subscribe() calls on any instance share the same event stream. This means events are not scoped to a single client instance if you create more than one — all instances dispatch to the same subject.

autoReconnect detects stale connections via a dead man's switch. The underlying ctrader-layer monitors server silence at the socket level. If no message (including heartbeats) is received for 30 seconds, the connection is declared stale and a reconnect is triggered. This catches silent network drops — cases where the TCP connection dies without a FIN packet — which a pure heartbeat-based approach would miss.

autoReconnect restores live bar subscriptions automatically. On reconnect, all active subscribeLiveTrendBars subscriptions are re-established with the server without requiring any action from the caller.

disconnect() is idempotent. Calling it multiple times, or calling it on a client that was never successfully connected, does not throw.