This is the full developer documentation for NexPips SDK # NexPips SDK > Build MetaTrader 5 trading bots in plain TypeScript. Same code in backtest and live. ## What you can do with the SDK Every NexPips bot is a single TypeScript module that calls `defineStrategy(...)` with a `setup` that returns hooks. The runtime — identical in backtest and live — feeds new bars to your `onBar`, async fills to your `onOrderFilled`, and closed positions to your `onPositionClosed`. ```ts import { defineStrategy } from '@nexpips/sdk-trading'; export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: () => ({ onBar(ctx) { if (!ctx.position.isFlat) return; ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); }, }), }); ``` API reference Every public method, type, hook and helper of the SDK — signatures, examples, pitfalls. [Browse the API](/en/v1/api/order/market-buy/). Cookbook Ready-to-paste snippets for common patterns — entries, exits, position sizing. [Browse the cookbook](/en/v1/cookbook/buy-with-rr-stop/). Indicators The 7 built-in indicators (`SMA`, `EMA`, `RSI`, `MACD`, `ATR`, `BollingerBands`, `DonchianChannel`) and how to build your own. [Browse the indicators](/en/v1/indicators/rsi/). # StrategyInvariantError ## `StrategyInvariantError` **At a glance** — Thrown **synchronously** when your strategy violates a hard invariant while sending an order (or on an invalid series access) — for example a `marketBuy` without a `stopLoss`, a `closePosition` while flat, a limit/stop on the wrong side of the market, or cancelling an unknown ticket. It signals a **code bug** you must fix. Recoverable broker conditions are different: they arrive asynchronously as a [`Rejection`](/en/v1/reference/types/rejection/) through `onOrderRejected`. ## Signature ```ts class StrategyInvariantError extends Error { readonly code: StrategyErrorCode; constructor(code: StrategyErrorCode, message: string); } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` onOrderRejected handles recoverable rejections; invariant errors are bugs you fix in code. ## See also * [`Rejection`](/en/v1/reference/types/rejection/) · [`RejectReason`](/en/v1/reference/types/reject-reason/) * [`marketBuy` / `OrderApi`](/en/v1/api/order/market-buy/) # crossedOver ## `crossedOver` **At a glance** — `true` when series `a` crossed **above** `b` on the current bar: `a` was ≤ `b` one bar ago and `a` > `b` now. `b` can be another `Series` or a constant number. Requires `a.length ≥ 2` (and `b.length ≥ 2` when `b` is a series); returns `false` during warm-up instead of throwing. ## Signature ```ts function crossedOver(a: Series, b: Series | number): boolean; ``` ## Example indicators/ema-crossover ```ts import { crossedOver, crossedUnder, defineStrategy, EMA } from '@nexpips/sdk-trading'; /** * Croisement de moyennes exponentielles. Golden cross (EMA rapide passe * au-dessus de la lente) → entrée ; death cross → sortie. Montre `EMA`, * `crossedOver` et `crossedUnder`. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const fast = api.use(EMA, { source: 'close', period: 12 }); const slow = api.use(EMA, { source: 'close', period: 26 }); return { onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder && crossedOver(fast, slow)) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); return; } if (ctx.position.isLong && crossedUnder(fast, slow)) { ctx.order.closePosition('ema death cross'); } }, }; }, }); ``` Golden cross of a fast and slow EMA. ## See also * [`crossedUnder`](/en/v1/api/helpers/crossed-under/) — the downward counterpart * [`Series`](/en/v1/reference/types/series/) · [`EMA`](/en/v1/indicators/ema/) # crossedUnder ## `crossedUnder` **At a glance** — `true` when series `a` crossed **below** `b` on the current bar: `a` was ≥ `b` one bar ago and `a` < `b` now. `b` can be another `Series` or a constant number. Requires `a.length ≥ 2` (and `b.length ≥ 2` when `b` is a series); returns `false` during warm-up instead of throwing. ## Signature ```ts function crossedUnder(a: Series, b: Series | number): boolean; ``` ## Example indicators/ema-crossover ```ts import { crossedOver, crossedUnder, defineStrategy, EMA } from '@nexpips/sdk-trading'; /** * Croisement de moyennes exponentielles. Golden cross (EMA rapide passe * au-dessus de la lente) → entrée ; death cross → sortie. Montre `EMA`, * `crossedOver` et `crossedUnder`. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const fast = api.use(EMA, { source: 'close', period: 12 }); const slow = api.use(EMA, { source: 'close', period: 26 }); return { onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder && crossedOver(fast, slow)) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); return; } if (ctx.position.isLong && crossedUnder(fast, slow)) { ctx.order.closePosition('ema death cross'); } }, }; }, }); ``` Death cross closes the position. ## See also * [`crossedOver`](/en/v1/api/helpers/crossed-over/) — the upward counterpart * [`Series`](/en/v1/reference/types/series/) · [`EMA`](/en/v1/indicators/ema/) # init ## `init(ctx)` **At a glance** — optional hook that runs **once**, before the first bar. Receives an [`InitContext`](/en/v1/reference/types/init-context/) with `account`, `clock` and `log` — there is no market `series` or `order` API yet. Use it for startup logging or one-off setup. ## Signature ```ts init?(ctx: InitContext): void ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` init logs the starting balance. ## See also * [`InitContext`](/en/v1/reference/types/init-context/) · [`onBar`](/en/v1/api/hooks/on-bar/) * [`StrategyHooks`](/en/v1/reference/types/strategy-hooks/) # onBar ## `onBar(ctx)` **At a glance** — the only **required** hook, and where most logic lives. Fires once per **closed** bar with a [`BarContext`](/en/v1/reference/types/bar-context/): the new `bar`, the indicator `series`, current `position`, `account`, the `order` API, `clock` and `log`. ## Signature ```ts onBar(ctx: BarContext): void ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` onBar drives entries; the other hooks react to events. ## To know * Runs on bar **close**, so values are final (no look-ahead). For intrabar logic use [`onTick`](/en/v1/api/hooks/on-tick/). * Guard entries with `ctx.position.isFlat` and `ctx.position.hasPendingOrder`. ## See also * [`BarContext`](/en/v1/reference/types/bar-context/) · [`onTick`](/en/v1/api/hooks/on-tick/) * [`StrategyHooks`](/en/v1/reference/types/strategy-hooks/) # onOrderFilled ## `onOrderFilled(ctx, fill)` **At a glance** — optional hook fired when an order executes, fully or partially. Receives an [`EventContext`](/en/v1/reference/types/event-context/) and a [`Fill`](/en/v1/reference/types/fill/) with the executed `side`, `price`, `size` and a `partial` flag. This is where a market order’s `OrderTicket` resolves into a real position. ## Signature ```ts onOrderFilled?(ctx: EventContext, fill: Fill): void ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` Log each fill's side, price and size. ## See also * [`Fill`](/en/v1/reference/types/fill/) · [`EventContext`](/en/v1/reference/types/event-context/) * [`onOrderRejected`](/en/v1/api/hooks/on-order-rejected/) # onOrderRejected ## `onOrderRejected(ctx, rejection)` **At a glance** — optional hook fired when the broker **rejects** an order. Receives an [`EventContext`](/en/v1/reference/types/event-context/) and a [`Rejection`](/en/v1/reference/types/rejection/) carrying a [`RejectReason`](/en/v1/reference/types/reject-reason/) and a message. This is a **recoverable** market/broker condition — distinct from a [`StrategyInvariantError`](/en/v1/api/errors/strategy-invariant-error/), which is a synchronous code bug. ## Signature ```ts onOrderRejected?(ctx: EventContext, rejection: Rejection): void ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` Inspect rejection.reason to decide whether to retry. ## See also * [`Rejection`](/en/v1/reference/types/rejection/) · [`RejectReason`](/en/v1/reference/types/reject-reason/) * [`StrategyInvariantError`](/en/v1/api/errors/strategy-invariant-error/) # onPositionClosed ## `onPositionClosed(ctx, closed)` **At a glance** — optional hook fired when the position is fully closed, whether by stop-loss, take-profit or [`closePosition`](/en/v1/api/order/close-position/). Receives an [`EventContext`](/en/v1/reference/types/event-context/) and a [`ClosedPosition`](/en/v1/reference/types/closed-position/) with the `realizedPnl` and `closePrice`. The place to tally results or reset per-trade state. ## Signature ```ts onPositionClosed?(ctx: EventContext, closed: ClosedPosition): void ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` Log realized PnL when a position closes. ## See also * [`ClosedPosition`](/en/v1/reference/types/closed-position/) · [`closePosition`](/en/v1/api/order/close-position/) * [`EventContext`](/en/v1/reference/types/event-context/) # onTick ## `onTick(ctx)` **At a glance** — optional hook fired on every price update **within** a bar. Receives a [`TickContext`](/en/v1/reference/types/tick-context/) — a `BarContext` plus live `bid` and `ask`. Use for spread checks or intrabar exits; it runs far more often than `onBar`, so keep it light. ## Signature ```ts onTick?(ctx: TickContext): void ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` onTick watches the live spread. ## See also * [`TickContext`](/en/v1/reference/types/tick-context/) · [`onBar`](/en/v1/api/hooks/on-bar/) * [`StrategyHooks`](/en/v1/reference/types/strategy-hooks/) # cancel ## `ctx.order.cancel(ticket)` **At a glance** — cancel a still-pending order, identified by the [`OrderTicket`](/en/v1/reference/types/order-ticket/) you got back when you placed it. Use it to retire a `limit`/`stop` order that no longer makes sense. ## Signature ```ts cancel(ticket: OrderTicket): void ``` ## Example order/limit-and-cancel ```ts import { defineStrategy, type OrderTicket } from '@nexpips/sdk-trading'; /** * Ordres en attente : poser un `limitBuy`, l'annuler (`cancel`) s'il n'est * pas rempli en quelques barres, et resserrer le stop d'une position ouverte * (`modifyPosition`). Le `limitBuy` renvoie un `OrderTicket` opaque à conserver. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M15', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: () => { let pending: OrderTicket | null = null; let barsWaiting = 0; return { onBar(ctx) { if (ctx.position.hasPendingOrder && pending) { barsWaiting += 1; if (barsWaiting > 3) { ctx.order.cancel(pending); pending = null; barsWaiting = 0; } return; } if (ctx.position.isLong) { ctx.order.modifyPosition({ stopLoss: { type: 'pips', value: 10 } }); return; } if (ctx.position.isFlat) { const limitPrice = ctx.bar.close - 0.001; pending = ctx.order.limitBuy(limitPrice, { riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); barsWaiting = 0; } }, }; }, }); ``` Cancel a limit order that hasn't filled within a few bars. ## To know * Cancelling an unknown or already-resolved ticket is treated as a code bug and throws [`StrategyInvariantError`](/en/v1/api/errors/strategy-invariant-error/). * Gate on `ctx.position.hasPendingOrder` to know whether a cancel is meaningful. ## See also * [`limitBuy`](/en/v1/api/order/limit-buy/) · [`stopBuy`](/en/v1/api/order/stop-buy/) * [`OrderTicket`](/en/v1/reference/types/order-ticket/) # closePosition ## `ctx.order.closePosition(reason?)` **At a glance** — close the current open position at market. The optional `reason` string is attached to logs and the close event. Returns an [`OrderTicket`](/en/v1/reference/types/order-ticket/); the close is confirmed asynchronously through `onPositionClosed`. ## Signature ```ts closePosition(reason?: string): OrderTicket ``` ## Example indicators/ema-crossover ```ts import { crossedOver, crossedUnder, defineStrategy, EMA } from '@nexpips/sdk-trading'; /** * Croisement de moyennes exponentielles. Golden cross (EMA rapide passe * au-dessus de la lente) → entrée ; death cross → sortie. Montre `EMA`, * `crossedOver` et `crossedUnder`. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const fast = api.use(EMA, { source: 'close', period: 12 }); const slow = api.use(EMA, { source: 'close', period: 26 }); return { onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder && crossedOver(fast, slow)) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); return; } if (ctx.position.isLong && crossedUnder(fast, slow)) { ctx.order.closePosition('ema death cross'); } }, }; }, }); ``` Close the long on an EMA death cross. ## To know * Calling `closePosition` while flat (`ctx.position.isFlat`) is a code bug and throws [`StrategyInvariantError`](/en/v1/api/errors/strategy-invariant-error/). Guard with [`PositionReader`](/en/v1/reference/types/position-reader/). ## See also * [`modifyPosition`](/en/v1/api/order/modify-position/) — adjust instead of close * [`onPositionClosed`](/en/v1/api/hooks/on-position-closed/) · [`PositionReader`](/en/v1/reference/types/position-reader/) # limitBuy ## `ctx.order.limitBuy(price, params)` **At a glance** — place a **pending** buy order at `price`, **below** the current market: it fills only if price dips down to your level. Returns an [`OrderTicket`](/en/v1/reference/types/order-ticket/) you keep to [`cancel`](/en/v1/api/order/cancel/) the order if it never fills. ## Signature ```ts limitBuy(price: number, params: EntryParams): OrderTicket ``` ## Example order/limit-bracket ```ts import { BollingerBands, defineStrategy, type EntryParams } from '@nexpips/sdk-trading'; /** * Fade de range sur bandes de Bollinger : un `limitBuy` sur la bande basse * (repli acheteur) et un `limitSell` sur la bande haute (repli vendeur), * tous deux visant un retour vers la moyenne. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M15', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const bb = api.use(BollingerBands, { source: 'close', period: 20, stdDev: 2 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; const band = bb.at(0); const entry: EntryParams = { riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'price', value: band.middle }, }; ctx.order.limitBuy(band.lower, entry); ctx.order.limitSell(band.upper, entry); }, }; }, }); ``` Fade a range: limitBuy on the lower band, limitSell on the upper. ## To know * A `limitBuy` priced **above** the market is a code bug and throws [`StrategyInvariantError`](/en/v1/api/errors/strategy-invariant-error/). * Check `ctx.position.hasPendingOrder` before placing another order. ## See also * [`limitSell`](/en/v1/api/order/limit-sell/) · [`stopBuy`](/en/v1/api/order/stop-buy/) * [`cancel`](/en/v1/api/order/cancel/) · [`OrderTicket`](/en/v1/reference/types/order-ticket/) # limitSell ## `ctx.order.limitSell(price, params)` **At a glance** — place a **pending** sell order at `price`, **above** the current market: it fills only if price rallies up to your level. Returns an [`OrderTicket`](/en/v1/reference/types/order-ticket/) for later [`cancel`](/en/v1/api/order/cancel/). ## Signature ```ts limitSell(price: number, params: EntryParams): OrderTicket ``` ## Example order/limit-bracket ```ts import { BollingerBands, defineStrategy, type EntryParams } from '@nexpips/sdk-trading'; /** * Fade de range sur bandes de Bollinger : un `limitBuy` sur la bande basse * (repli acheteur) et un `limitSell` sur la bande haute (repli vendeur), * tous deux visant un retour vers la moyenne. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M15', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const bb = api.use(BollingerBands, { source: 'close', period: 20, stdDev: 2 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; const band = bb.at(0); const entry: EntryParams = { riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'price', value: band.middle }, }; ctx.order.limitBuy(band.lower, entry); ctx.order.limitSell(band.upper, entry); }, }; }, }); ``` Fade a range: limitBuy on the lower band, limitSell on the upper. ## To know * A `limitSell` priced **below** the market is a code bug and throws [`StrategyInvariantError`](/en/v1/api/errors/strategy-invariant-error/). ## See also * [`limitBuy`](/en/v1/api/order/limit-buy/) · [`stopSell`](/en/v1/api/order/stop-sell/) * [`cancel`](/en/v1/api/order/cancel/) · [`OrderTicket`](/en/v1/reference/types/order-ticket/) # marketBuy > Send a market buy order with strict risk and stop-loss handling. ## `ctx.order.marketBuy(params)` **At a glance** — send a buy order at market. The position only exists once the broker confirms the fill asynchronously via `onOrderFilled`. Until then, `ctx.position.hasPendingOrder` returns `true`. ## Signature ```ts marketBuy(params: EntryParams): OrderTicket ``` `EntryParams` requires a non-zero `riskPercent` and a `stopLoss` (mandatory by design — the SDK refuses any entry without one to protect users from unbounded losses). ## Examples ### Simple case order/market-buy-simple ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Acheter au marché si on est flat, avec un stop-loss à 20 pips. * * Pattern : entrée propre — vérifier `hasPendingOrder` ET `isFlat` * pour éviter d'envoyer un ordre alors qu'un précédent est encore * en attente de confirmation broker. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5, }, setup: () => ({ onBar(ctx) { if (!ctx.position.isFlat) return; if (ctx.position.hasPendingOrder) return; ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); }, }), }); ``` Buy when flat with a 20-pip stop and 1:2 R:R take-profit. ## To know * The order is **not** filled when the method returns. Treat the returned `OrderTicket` as a correlation ID, then react in `onOrderFilled` / `onOrderRejected`. * The runtime enforces R5–R13 invariants (`maxRiskPercentPerTrade`, `maxOpenPositions`, presence of stop-loss, sane `riskPercent`, etc.). Violating them throws `StrategyInvariantError` at order construction. * Possible rejection reasons surface via `onOrderRejected` with a `RejectReason`: `no_liquidity`, `requote`, `market_closed`, `insufficient_margin`, `broker_error`, `connection_lost`, `invalid_volume_too_small`, `invalid_volume_too_large`. ## See also * [`marketSell`](/en/v1/api/order/market-sell/) — the short counterpart * [`limitBuy`](/en/v1/api/order/limit-buy/) · [`stopBuy`](/en/v1/api/order/stop-buy/) — pending entries * [`crossedOver`](/en/v1/api/helpers/crossed-over/) — pair entries with indicator crossings * [Cookbook — buy with RR stop](/en/v1/cookbook/buy-with-rr-stop/) · [Indicators — RSI](/en/v1/indicators/rsi/) # marketSell ## `ctx.order.marketSell(params)` **At a glance** — send a sell order at market, opening a short position. Takes the same [`EntryParams`](/en/v1/reference/types/entry-params/) as `marketBuy` (a `stopLoss` is mandatory). The position only exists once the broker confirms the fill asynchronously via `onOrderFilled`; until then `ctx.position.hasPendingOrder` is `true`. ## Signature ```ts marketSell(params: EntryParams): OrderTicket ``` ## Example order/sell-short ```ts import { defineStrategy, EMA } from '@nexpips/sdk-trading'; /** * Vente à découvert : `marketSell` ouvre une position short au marché. Les * `EntryParams` sont identiques à un achat (un `stopLoss` reste obligatoire). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const ema = api.use(EMA, { source: 'close', period: 50 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; // Clôture sous l'EMA(50) → biais baissier → vente au marché. if (ctx.series.close.at(0) < ema.at(0)) { ctx.order.marketSell({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` Sell short when price closes below the EMA(50). ## To know * The returned [`OrderTicket`](/en/v1/reference/types/order-ticket/) is a correlation handle, not a fill — react to the outcome in `onOrderFilled` / `onOrderRejected`. * Missing `stopLoss` or breaching the `risk` config throws [`StrategyInvariantError`](/en/v1/api/errors/strategy-invariant-error/) synchronously. ## See also * [`marketBuy`](/en/v1/api/order/market-buy/) — the long counterpart * [`stopSell`](/en/v1/api/order/stop-sell/) · [`limitSell`](/en/v1/api/order/limit-sell/) * [`EntryParams`](/en/v1/reference/types/entry-params/) # modifyPosition ## `ctx.order.modifyPosition(params)` **At a glance** — adjust the stop-loss and/or take-profit of the **open** position without closing it. The go-to for moving a stop to break-even or trailing it. Returns an [`OrderTicket`](/en/v1/reference/types/order-ticket/). ## Signature ```ts modifyPosition(params: ModifyParams): OrderTicket ``` [`ModifyParams`](/en/v1/reference/types/modify-params/) lets you set either or both of `stopLoss` / `takeProfit` as a [`PriceLevel`](/en/v1/reference/types/price-level/). ## Example order/limit-and-cancel ```ts import { defineStrategy, type OrderTicket } from '@nexpips/sdk-trading'; /** * Ordres en attente : poser un `limitBuy`, l'annuler (`cancel`) s'il n'est * pas rempli en quelques barres, et resserrer le stop d'une position ouverte * (`modifyPosition`). Le `limitBuy` renvoie un `OrderTicket` opaque à conserver. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M15', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: () => { let pending: OrderTicket | null = null; let barsWaiting = 0; return { onBar(ctx) { if (ctx.position.hasPendingOrder && pending) { barsWaiting += 1; if (barsWaiting > 3) { ctx.order.cancel(pending); pending = null; barsWaiting = 0; } return; } if (ctx.position.isLong) { ctx.order.modifyPosition({ stopLoss: { type: 'pips', value: 10 } }); return; } if (ctx.position.isFlat) { const limitPrice = ctx.bar.close - 0.001; pending = ctx.order.limitBuy(limitPrice, { riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); barsWaiting = 0; } }, }; }, }); ``` Tighten the stop once the position is open. ## To know * Only valid while a position is open; modifying while flat is a code bug and throws [`StrategyInvariantError`](/en/v1/api/errors/strategy-invariant-error/). ## See also * [`ModifyParams`](/en/v1/reference/types/modify-params/) · [`PriceLevel`](/en/v1/reference/types/price-level/) * [`closePosition`](/en/v1/api/order/close-position/) · [`PositionReader`](/en/v1/reference/types/position-reader/) # stopBuy ## `ctx.order.stopBuy(price, params)` **At a glance** — place a **pending** buy order at `price`, **above** the current market: it triggers on an upside breakout. The classic momentum/breakout entry. Returns an [`OrderTicket`](/en/v1/reference/types/order-ticket/). ## Signature ```ts stopBuy(price: number, params: EntryParams): OrderTicket ``` ## Example order/breakout-bracket ```ts import { defineStrategy, DonchianChannel, type EntryParams } from '@nexpips/sdk-trading'; /** * Straddle de cassure : un `stopBuy` au-dessus du canal de Donchian et un * `stopSell` en dessous. Le premier niveau touché entre dans le sens de la * cassure (l'autre ordre reste en attente — à annuler dans `onOrderFilled` * dans une vraie stratégie). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const dc = api.use(DonchianChannel, { period: 20 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; const band = dc.at(0); const entry: EntryParams = { riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }; ctx.order.stopBuy(band.upper, entry); ctx.order.stopSell(band.lower, entry); }, }; }, }); ``` Straddle a Donchian breakout: stopBuy above, stopSell below. ## To know * A `stopBuy` priced **below** the market is a code bug and throws [`StrategyInvariantError`](/en/v1/api/errors/strategy-invariant-error/). * Pairs naturally with a [`DonchianChannel`](/en/v1/indicators/donchian-channel/) upper bound. ## See also * [`stopSell`](/en/v1/api/order/stop-sell/) · [`limitBuy`](/en/v1/api/order/limit-buy/) * [`OrderTicket`](/en/v1/reference/types/order-ticket/) # stopSell ## `ctx.order.stopSell(price, params)` **At a glance** — place a **pending** sell order at `price`, **below** the current market: it triggers on a downside breakdown. Returns an [`OrderTicket`](/en/v1/reference/types/order-ticket/). ## Signature ```ts stopSell(price: number, params: EntryParams): OrderTicket ``` ## Example order/breakout-bracket ```ts import { defineStrategy, DonchianChannel, type EntryParams } from '@nexpips/sdk-trading'; /** * Straddle de cassure : un `stopBuy` au-dessus du canal de Donchian et un * `stopSell` en dessous. Le premier niveau touché entre dans le sens de la * cassure (l'autre ordre reste en attente — à annuler dans `onOrderFilled` * dans une vraie stratégie). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const dc = api.use(DonchianChannel, { period: 20 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; const band = dc.at(0); const entry: EntryParams = { riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }; ctx.order.stopBuy(band.upper, entry); ctx.order.stopSell(band.lower, entry); }, }; }, }); ``` Straddle a Donchian breakout: stopBuy above, stopSell below. ## To know * A `stopSell` priced **above** the market is a code bug and throws [`StrategyInvariantError`](/en/v1/api/errors/strategy-invariant-error/). * Pairs naturally with a [`DonchianChannel`](/en/v1/indicators/donchian-channel/) lower bound. ## See also * [`stopBuy`](/en/v1/api/order/stop-buy/) · [`limitSell`](/en/v1/api/order/limit-sell/) * [`OrderTicket`](/en/v1/reference/types/order-ticket/) # defineStrategy ## `defineStrategy` **At a glance** — The entry point of every bot. You pass a [`StrategyDefinition`](/en/v1/reference/types/strategy-definition/) (symbol, timeframe, risk and a `setup` function). `setup()` runs once to build the manifest (indicators + inputs), then again when the run starts. It returns an opaque `Strategy` — `export default` it from your bot file. ## Signature ```ts function defineStrategy(def: StrategyDefinition): Strategy; ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` A strategy wiring every lifecycle hook. ## See also * [`StrategyDefinition`](/en/v1/reference/types/strategy-definition/) · [`StrategyHooks`](/en/v1/reference/types/strategy-hooks/) * [`SetupApi`](/en/v1/reference/types/setup-api/) — the `api` passed to `setup` * [`marketBuy`](/en/v1/api/order/market-buy/) # Changelog > History of changes to the NexPips trading SDK. ## v0.1.0 — 2026-05-27 ### Added * Initial public release of `@nexpips/sdk-trading`. * 7 built-in indicators: `SMA`, `EMA`, `RSI`, `MACD`, `ATR`, `BollingerBands`, `DonchianChannel`. * Helpers `crossedOver` / `crossedUnder`. * 3 starter templates: EMA crossover, RSI + Bollinger reversal, Donchian breakout. > This page is generated from `libs/sdk-trading/CHANGELOG.md` by `apps/docs-site/scripts/generate-changelog.ts`. Editing the SDK changelog updates the page at the next build. # Buy with ATR stop and 1:2 R:R take-profit > Recipe — open a long position with an ATR-based stop and a take-profit at twice the risk. ## Recipe — Buy market with ATR stop, 1:2 R:R take-profit **When to use** — when you want a volatility-aware stop (instead of fixed pips) and a take-profit aligned with your risk-reward target. Works on any timeframe; ATR period and multiplier are the levers. cookbook/buy-with-rr-stop ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Recette cookbook : achat market avec stop-loss ATR et take-profit en R:R 1:2. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5, }, setup: (api) => { const riskPercent = api.input.float('riskPercent', 1, { min: 0.1, max: 2, step: 0.1 }); return { onBar(ctx) { if (!ctx.position.isFlat) return; if (ctx.position.hasPendingOrder) return; ctx.order.marketBuy({ riskPercent, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); }, }; }, }); ``` Risk is exposed as an input; stop is ATR(14)×2; TP at 1:2. ## What’s happening * `api.input.float('riskPercent', 1, …)` declares a user-tunable input (the user-facing form in NexPips IDE will expose it; default value is `1`%). * `stopLoss: { type: 'atr', period: 14, multiple: 2 }` sets the stop at 2× ATR(14) away. * `takeProfit: { type: 'rr', value: 2 }` puts the TP at 2× the stop distance — so a winning trade is worth two times the risk taken. ## See also * [`marketBuy`](/en/v1/api/order/market-buy/) * [`RSI`](/en/v1/indicators/rsi/) # ATR ## `ATR` **At a glance** — Average True Range (Wilder smoothing): a volatility measure. Returns a `Series` of positive values in the symbol’s price unit. Most often used to size stops that adapt to current volatility. ## Signature ```ts const atr: Series = api.use(ATR, { period: 14 }); ``` | Field | Type | Description | | -------- | -------------- | --------------------------------- | | `period` | `number` (≥ 1) | Smoothing window. Standard is 14. | Warm-up: `period + 1` bars. ## Example indicators/atr-stop ```ts import { ATR, defineStrategy } from '@nexpips/sdk-trading'; /** * Dimensionner le stop avec la volatilité : un stop-loss exprimé en * multiples d'ATR (`{ type: 'atr' }`) s'élargit quand le marché est agité * et se resserre quand il est calme. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const atr = api.use(ATR, { period: 14 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; // N'entrer que si la volatilité courante est mesurable. if (atr.at(0) <= 0) return; ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); }, }; }, }); ``` Size the stop-loss at 2×ATR. ## See also * [`PriceLevel` — `{ type: 'atr' }`](/en/v1/reference/types/price-level/) * [`SMA`](/en/v1/indicators/sma/) · [`EMA`](/en/v1/indicators/ema/) # BollingerBands ## `BollingerBands` **At a glance** — `middle = SMA(source, period)`; the `upper`/`lower` bands sit `stdDev` population standard deviations away from the middle. The series yields a [`BollingerBandsOutput`](/en/v1/indicators/bollinger-bands-output/) per bar. Population SD (divisor = N), consistent with TradingView / MQL5. ## Signature ```ts const bb: Series = api.use(BollingerBands, { source: 'close', period: 20, stdDev: 2, }); ``` | Field | Type | Description | | -------- | -------------- | ------------------------------------------- | | `period` | `number` (≥ 1) | Window for the moving average and SD. | | `source` | `Source` | Price source. | | `stdDev` | `number` | Band width in standard deviations (e.g. 2). | Warm-up: `period` bars. ## Example indicators/bollinger-reversal ```ts import { BollingerBands, defineStrategy } from '@nexpips/sdk-trading'; /** * Retour à la moyenne sur bandes de Bollinger. La série renvoie un * `BollingerBandsOutput` ({ upper, middle, lower }). Achat quand la clôture * passe sous la bande basse ; take-profit visé sur la moyenne. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M15', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const bb = api.use(BollingerBands, { source: 'close', period: 20, stdDev: 2 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; const band = bb.at(0); if (ctx.series.close.at(0) < band.lower) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'price', value: band.middle }, }); } }, }; }, }); ``` Mean-reversion: buy below the lower band, target the middle. ## See also * [`BollingerBandsOutput`](/en/v1/indicators/bollinger-bands-output/) * [`SMA`](/en/v1/indicators/sma/) # BollingerBandsOutput ## `BollingerBandsOutput` **At a glance** — The per-bar value produced by the [`BollingerBands`](/en/v1/indicators/bollinger-bands/) indicator. Read the latest value with `bb.at(0)`. ## Signature ```ts export interface BollingerBandsOutput { upper: number; middle: number; lower: number; } ``` | Field | Description | | -------- | ------------------------------------------- | | `upper` | Upper band: `middle + stdDev × σ`. | | `middle` | The moving average (`SMA(source, period)`). | | `lower` | Lower band: `middle − stdDev × σ`. | ## Example indicators/bollinger-reversal ```ts import { BollingerBands, defineStrategy } from '@nexpips/sdk-trading'; /** * Retour à la moyenne sur bandes de Bollinger. La série renvoie un * `BollingerBandsOutput` ({ upper, middle, lower }). Achat quand la clôture * passe sous la bande basse ; take-profit visé sur la moyenne. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M15', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const bb = api.use(BollingerBands, { source: 'close', period: 20, stdDev: 2 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; const band = bb.at(0); if (ctx.series.close.at(0) < band.lower) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'price', value: band.middle }, }); } }, }; }, }); ``` Compare the close to the lower / middle bands. ## See also * [`BollingerBands`](/en/v1/indicators/bollinger-bands/) # defineIndicator ## `defineIndicator` **At a glance** — Define a custom indicator. `update(ctx)` is called once per closed bar and returns the output value; the engine accumulates returns into a `Series` exposed to your strategy via `api.use(indicator, params)`. ## Signature ```ts function defineIndicator(def: { name: string; warmup?: (params: TParams) => number; update: (ctx: IndicatorContext) => TOutput; }): IndicatorDef; ``` | Field | Description | | -------- | -------------------------------------------------------------------- | | `name` | Stable identifier, used in the manifest and logs. | | `warmup` | Optional. Bars to skip before the value is meaningful (default `1`). | | `update` | Called per closed bar; returns the indicator value for that bar. | ## Example indicators/custom-indicator ```ts import { defineIndicator, defineStrategy } from '@nexpips/sdk-trading'; /** * Indicateur custom via `defineIndicator` : l'amplitude (range) de la barre * courante. `update` est appelé une fois par barre clôturée et reçoit un * `IndicatorContext`. L'engine accumule les retours dans une `Series`. */ const BarRange = defineIndicator<{ history?: number }, number>({ name: 'BarRange', warmup: () => 1, update: (ctx) => ctx.series.high.at(0) - ctx.series.low.at(0), }); export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const range = api.use(BarRange, {}); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; if (range.at(0) > 0) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` A custom bar-range indicator. ## See also * [`IndicatorContext`](/en/v1/reference/types/indicator-context/) — what `update` receives * [`IndicatorDef`](/en/v1/reference/types/indicator-def/) · [`SetupApi`](/en/v1/reference/types/setup-api/) # DonchianChannel ## `DonchianChannel` **At a glance** — `upper` = highest high and `lower` = lowest low over the last `period` bars; `middle` is their average. The series yields a [`DonchianChannelOutput`](/en/v1/indicators/donchian-channel-output/) per bar. The classic breakout channel. ## Signature ```ts const dc: Series = api.use(DonchianChannel, { period: 20 }); ``` | Field | Type | Description | | -------- | -------------- | ------------------------------------------- | | `period` | `number` (≥ 1) | Look-back window for the high/low extremes. | Warm-up: `period` bars. ## Example indicators/donchian-breakout ```ts import { defineStrategy, DonchianChannel } from '@nexpips/sdk-trading'; /** * Breakout de canal de Donchian. La série renvoie un * `DonchianChannelOutput` ({ upper, middle, lower }). Achat quand la clôture * dépasse le plus-haut des 20 barres précédentes ; stop sur le milieu du canal. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const dc = api.use(DonchianChannel, { period: 20 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; if (dc.length < 2) return; const prevUpper = dc.at(1).upper; if (ctx.series.close.at(0) > prevUpper) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'price', value: dc.at(0).middle }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` Buy when price breaks above the prior 20-bar high. ## See also * [`DonchianChannelOutput`](/en/v1/indicators/donchian-channel-output/) * [`ATR`](/en/v1/indicators/atr/) — pair breakouts with a volatility stop # DonchianChannelOutput ## `DonchianChannelOutput` **At a glance** — The per-bar value produced by the [`DonchianChannel`](/en/v1/indicators/donchian-channel/) indicator. Read the latest value with `dc.at(0)`. ## Signature ```ts export interface DonchianChannelOutput { upper: number; middle: number; lower: number; } ``` | Field | Description | | -------- | ----------------------------------------- | | `upper` | Highest high over the last `period` bars. | | `middle` | Average of `upper` and `lower`. | | `lower` | Lowest low over the last `period` bars. | ## Example indicators/donchian-breakout ```ts import { defineStrategy, DonchianChannel } from '@nexpips/sdk-trading'; /** * Breakout de canal de Donchian. La série renvoie un * `DonchianChannelOutput` ({ upper, middle, lower }). Achat quand la clôture * dépasse le plus-haut des 20 barres précédentes ; stop sur le milieu du canal. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const dc = api.use(DonchianChannel, { period: 20 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; if (dc.length < 2) return; const prevUpper = dc.at(1).upper; if (ctx.series.close.at(0) > prevUpper) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'price', value: dc.at(0).middle }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` Break above the prior upper bound. ## See also * [`DonchianChannel`](/en/v1/indicators/donchian-channel/) # EMA ## `EMA` **At a glance** — Exponential Moving Average: seeded by an SMA over the first `period` bars, then smoothed exponentially (`ema = α·value + (1 − α)·prevEma`, `α = 2/(period + 1)`). Reacts faster than `SMA` to recent prices. ## Signature ```ts const ema: Series = api.use(EMA, { source: 'close', period: 20 }); ``` | Field | Type | Description | | -------- | -------------- | ----------------- | | `period` | `number` (≥ 1) | Look-back window. | | `source` | `Source` | Price source. | Warm-up: `period` bars. ## Example indicators/ema-crossover ```ts import { crossedOver, crossedUnder, defineStrategy, EMA } from '@nexpips/sdk-trading'; /** * Croisement de moyennes exponentielles. Golden cross (EMA rapide passe * au-dessus de la lente) → entrée ; death cross → sortie. Montre `EMA`, * `crossedOver` et `crossedUnder`. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const fast = api.use(EMA, { source: 'close', period: 12 }); const slow = api.use(EMA, { source: 'close', period: 26 }); return { onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder && crossedOver(fast, slow)) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); return; } if (ctx.position.isLong && crossedUnder(fast, slow)) { ctx.order.closePosition('ema death cross'); } }, }; }, }); ``` Golden/death cross of EMA(12) and EMA(26). ## See also * [`SMA`](/en/v1/indicators/sma/) — smoother, more lag * [`crossedOver`](/en/v1/api/helpers/crossed-over/) · [`crossedUnder`](/en/v1/api/helpers/crossed-under/) # MACD ## `MACD` **At a glance** — Moving Average Convergence Divergence. `macd = EMA(fast) − EMA(slow)`, `signal = EMA(macd, signal)`, `hist = macd − signal`. The series yields a [`MacdOutput`](/en/v1/indicators/macd-output/) object per bar. ## Signature ```ts const macd: Series = api.use(MACD, { fast: 12, slow: 26, signal: 9 }); ``` | Field | Type | Description | | -------- | -------- | --------------------------- | | `fast` | `number` | Fast EMA period (e.g. 12). | | `slow` | `number` | Slow EMA period (e.g. 26). | | `signal` | `number` | Signal EMA period (e.g. 9). | Warm-up: `slow + signal` bars (conservative). ## Example indicators/macd-momentum ```ts import { defineStrategy, MACD } from '@nexpips/sdk-trading'; /** * Momentum MACD. La série renvoie un `MacdOutput` ({ macd, signal, hist }). * Entrée quand la ligne MACD repasse au-dessus de sa ligne de signal avec * un histogramme positif. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const macd = api.use(MACD, { fast: 12, slow: 26, signal: 9 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; if (macd.length < 2) return; const now = macd.at(0); const prev = macd.at(1); const crossedUp = prev.macd <= prev.signal && now.macd > now.signal; if (crossedUp && now.hist > 0) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` Enter on MACD crossing above its signal line. ## See also * [`MacdOutput`](/en/v1/indicators/macd-output/) — the per-bar output shape * [`EMA`](/en/v1/indicators/ema/) # MacdOutput ## `MacdOutput` **At a glance** — The per-bar value produced by the [`MACD`](/en/v1/indicators/macd/) indicator. Read the latest value with `macd.at(0)`. ## Signature ```ts export interface MacdOutput { macd: number; signal: number; hist: number; } ``` | Field | Description | | -------- | ------------------------------------------------------------ | | `macd` | The MACD line: `EMA(fast) − EMA(slow)`. | | `signal` | The signal line: `EMA(macd, signal)`. | | `hist` | Histogram: `macd − signal`. Positive means bullish momentum. | ## Example indicators/macd-momentum ```ts import { defineStrategy, MACD } from '@nexpips/sdk-trading'; /** * Momentum MACD. La série renvoie un `MacdOutput` ({ macd, signal, hist }). * Entrée quand la ligne MACD repasse au-dessus de sa ligne de signal avec * un histogramme positif. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const macd = api.use(MACD, { fast: 12, slow: 26, signal: 9 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; if (macd.length < 2) return; const now = macd.at(0); const prev = macd.at(1); const crossedUp = prev.macd <= prev.signal && now.macd > now.signal; if (crossedUp && now.hist > 0) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` Read macd / signal / hist from the series. ## See also * [`MACD`](/en/v1/indicators/macd/) # RSI > Relative Strength Index — classic momentum oscillator. ## `RSI` **At a glance** — the Relative Strength Index, Wilder-style smoothing. Returns a `Series` of values in the `[0, 100]` range. ## Signature ```ts const rsi: Series = api.use(RSI, { period: 14, source: 'close' }); ``` Parameters: | Field | Type | Description | | -------- | -------------- | --------------------------------------------------------------------- | | `period` | `number` (≥ 2) | Lookback window. Standard is 14. | | `source` | `Source` | Price source: `open`, `high`, `low`, `close`, `hl2`, `hlc3`, `ohlc4`. | ## What it’s good for * Mean-reversion entries when crossing back from oversold (`< 30`) or overbought (`> 70`). * Confirming momentum direction alongside another trend indicator. ## Examples indicators/rsi-oversold ```ts import { crossedOver, defineStrategy, RSI } from '@nexpips/sdk-trading'; /** * Achète quand le RSI(14) sort de la zone de survente (passe au-dessus de 30). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M15', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, }, setup: (api) => { const rsi = api.use(RSI, { period: 14, source: 'close' }); return { onBar(ctx) { if (!ctx.position.isFlat) return; if (ctx.position.hasPendingOrder) return; if (crossedOver(rsi, 30)) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 1.5 }, }); } }, }; }, }); ``` Buy when RSI(14) crosses back above 30. ## See also * [`crossedOver` / `crossedUnder` helpers](/en/v1/api/order/market-buy/) * [Cookbook — buy with RR stop](/en/v1/cookbook/buy-with-rr-stop/) # SMA ## `SMA` **At a glance** — Simple Moving Average: the arithmetic mean of the last `period` values of the chosen source. Smooth and lag-prone; great as a trend filter. Computed in O(1) per bar. ## Signature ```ts const sma: Series = api.use(SMA, { source: 'close', period: 50 }); ``` | Field | Type | Description | | -------- | -------------- | --------------------------------------------------------------------- | | `period` | `number` (≥ 1) | Look-back window. | | `source` | `Source` | Price source: `open`, `high`, `low`, `close`, `hl2`, `hlc3`, `ohlc4`. | Warm-up: `period` bars. ## Example indicators/sma-trend ```ts import { defineStrategy, SMA } from '@nexpips/sdk-trading'; /** * Filtre de tendance : n'acheter que lorsque la clôture est au-dessus de * la moyenne mobile simple SMA(50). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const sma = api.use(SMA, { source: 'close', period: 50 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; if (ctx.series.close.at(0) > sma.at(0)) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` Only buy when price is above the SMA(50). ## See also * [`EMA`](/en/v1/indicators/ema/) — faster-reacting average * [`Source`](/en/v1/reference/types/source/) · [`Series`](/en/v1/reference/types/series/) # SDK_VERSION ## `SDK_VERSION` **At a glance** — The SDK version string, frozen at build time. Useful to tag logs or to include when reporting an issue to support. ## Signature ```ts import { SDK_VERSION } from '@nexpips/sdk-trading'; // e.g. SDK_VERSION === '0.1.0' ``` ## Example meta/print-version ```ts import { SDK_VERSION } from '@nexpips/sdk-trading'; /** * Lire la version du SDK à l'exécution — utile dans les logs d'un bot ou * pour le support. `SDK_VERSION` est une constante chaîne figée au build. */ export const runningSdkVersion = `nexpips-sdk@${SDK_VERSION}`; ``` Tag your logs with the running SDK version. ## See also * [`defineStrategy`](/en/v1/api/runtime/define-strategy/) # AccountReader ## `AccountReader` A read-only snapshot of the trading account, available on the strategy context. Use `balance`, `equity`, and `marginFree` to gauge available capital and size risk decisions, and `currency` for the account’s denomination. ## Signature ```ts export interface AccountReader { readonly balance: number; readonly equity: number; readonly marginFree: number; readonly currency: string; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` Inspect account balance and equity from the context. ## See also * [`InitContext`](/en/v1/reference/types/init-context/) * [`BarContext`](/en/v1/reference/types/bar-context/) # Bar ## `Bar` A single OHLCV candle: its `time` (epoch ms) plus `open`, `high`, `low`, `close`, and `volume`. Every field is `readonly`, so the bars handed to your strategy are immutable. The per-field columns are exposed over time through [`MarketSeries`](/en/v1/reference/types/market-series/). ## Signature ```ts export interface Bar { readonly time: number; readonly open: number; readonly high: number; readonly low: number; readonly close: number; readonly volume: number; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` Each closed bar drives the strategy lifecycle. ## See also * [`MarketSeries`](/en/v1/reference/types/market-series/) * [`Series`](/en/v1/reference/types/series/) # BarContext ## `BarContext` **At a glance** — Passed to `onBar()` on each closed bar. It bundles everything a strategy needs to react to the latest candle: the closed `bar`, the historical `series`, the current `position` and `account` state, the `order` API to place trades, plus `clock` and `log`. `TickContext` extends it for intrabar `onTick()` handling. ## Signature ```ts export interface BarContext { readonly bar: Bar; readonly series: MarketSeries; readonly position: PositionReader; readonly account: AccountReader; readonly order: OrderApi; readonly clock: ClockReader; readonly log: LogApi; } ``` ## Example order/market-buy-simple ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Acheter au marché si on est flat, avec un stop-loss à 20 pips. * * Pattern : entrée propre — vérifier `hasPendingOrder` ET `isFlat` * pour éviter d'envoyer un ordre alors qu'un précédent est encore * en attente de confirmation broker. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5, }, setup: () => ({ onBar(ctx) { if (!ctx.position.isFlat) return; if (ctx.position.hasPendingOrder) return; ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); }, }), }); ``` onBar() reads a BarContext and places an order. ## See also * [`TickContext`](/en/v1/reference/types/tick-context/) * [`MarketSeries`](/en/v1/reference/types/market-series/) * [`PositionReader`](/en/v1/reference/types/position-reader/) # ClockReader ## `ClockReader` A deterministic clock exposed on the strategy context. Read `now` (epoch milliseconds) instead of `Date.now()` so your strategy behaves identically in backtests and live trading — the engine drives this value, guaranteeing reproducible runs. ## Signature ```ts export interface ClockReader { readonly now: number; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` Read the deterministic clock instead of Date.now(). ## See also * [`InitContext`](/en/v1/reference/types/init-context/) # ClosedPosition ## `ClosedPosition` **At a glance** — Passed to `onPositionClosed` once a position is fully closed. Read the `realizedPnl` and `closePrice` to record the outcome of the trade. ## Signature ```ts export interface ClosedPosition { ticketId: string; realizedPnl: number; closePrice: number; time: number; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` onPositionClosed receives a ClosedPosition. ## See also * [`EventContext`](/en/v1/reference/types/event-context/) * [`PositionReader`](/en/v1/reference/types/position-reader/) # EntryParams ## `EntryParams` The arguments you pass when opening a position (for example via [`marketBuy`](/en/v1/api/order/market-buy/)). You declare `riskPercent` and a `stopLoss` [`PriceLevel`](/en/v1/reference/types/price-level/) — never a raw lot size, the engine sizes the volume from your risk. `takeProfit` and a free-text `comment` are optional. ## Signature ```ts export interface EntryParams { riskPercent: number; stopLoss: PriceLevel; takeProfit?: PriceLevel; comment?: string; } ``` ## Example order/market-buy-simple ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Acheter au marché si on est flat, avec un stop-loss à 20 pips. * * Pattern : entrée propre — vérifier `hasPendingOrder` ET `isFlat` * pour éviter d'envoyer un ordre alors qu'un précédent est encore * en attente de confirmation broker. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5, }, setup: () => ({ onBar(ctx) { if (!ctx.position.isFlat) return; if (ctx.position.hasPendingOrder) return; ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); }, }), }); ``` Open a position by risk percent with a stop-loss. ## See also * [`PriceLevel`](/en/v1/reference/types/price-level/) * [`marketBuy`](/en/v1/api/order/market-buy/) * [`ModifyParams`](/en/v1/reference/types/modify-params/) # EventContext ## `EventContext` **At a glance** — Passed to the order-lifecycle hooks `onOrderFilled`, `onOrderRejected` and `onPositionClosed`. It exposes `position`, `account`, `order`, `clock` and `log` so you can react to an event — but no `bar` or `series`, since these hooks fire outside the bar/tick cycle. ## Signature ```ts export interface EventContext { readonly position: PositionReader; readonly account: AccountReader; readonly order: OrderApi; readonly clock: ClockReader; readonly log: LogApi; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` Lifecycle hooks receive an EventContext. ## See also * [`Fill`](/en/v1/reference/types/fill/) * [`Rejection`](/en/v1/reference/types/rejection/) * [`ClosedPosition`](/en/v1/reference/types/closed-position/) # Fill ## `Fill` **At a glance** — Passed to `onOrderFilled` when an order is partially or fully executed. Read the executed `price`, `size` and `side` from it, and check `partial` to tell a partial fill from a complete one. ## Signature ```ts export interface Fill { ticketId: string; side: 'buy' | 'sell'; price: number; size: number; partial: boolean; time: number; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` onOrderFilled receives a Fill. ## See also * [`EventContext`](/en/v1/reference/types/event-context/) * [`OrderTicket`](/en/v1/reference/types/order-ticket/) # IndicatorContext ## `IndicatorContext` The context passed to a custom indicator’s `update()` callback. It exposes the resolved `params` for this instance, the raw `series` of market data to compute over, and a `use()` method for composing other indicators inside your own. Parameterized by `TParams`, the shape of the indicator’s configuration. ## Signature ```ts export interface IndicatorContext { readonly params: TParams; readonly series: MarketSeries; use(def: IndicatorDef, params: P): Series; } ``` ## Example indicators/custom-indicator ```ts import { defineIndicator, defineStrategy } from '@nexpips/sdk-trading'; /** * Indicateur custom via `defineIndicator` : l'amplitude (range) de la barre * courante. `update` est appelé une fois par barre clôturée et reçoit un * `IndicatorContext`. L'engine accumule les retours dans une `Series`. */ const BarRange = defineIndicator<{ history?: number }, number>({ name: 'BarRange', warmup: () => 1, update: (ctx) => ctx.series.high.at(0) - ctx.series.low.at(0), }); export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const range = api.use(BarRange, {}); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; if (range.at(0) > 0) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` Reading params and series inside a custom indicator. ## See also * [`defineIndicator`](/en/v1/indicators/define-indicator/) · [`IndicatorDef`](/en/v1/reference/types/indicator-def/) · [`MarketSeries`](/en/v1/reference/types/market-series/) # IndicatorDef ## `IndicatorDef` An opaque handle to an indicator, produced by `defineIndicator()` or supplied as a built-in such as `RSI`. You don’t read its fields — you pass it to `api.use()` to register the indicator and obtain its output series. The phantom `__params` and `__output` fields exist solely to carry the parameter and output types (`TParams`, `TOutput`) through the type system. ## Signature ```ts export interface IndicatorDef { readonly __params?: TParams; readonly __output?: TOutput; } ``` ## Example indicators/custom-indicator ```ts import { defineIndicator, defineStrategy } from '@nexpips/sdk-trading'; /** * Indicateur custom via `defineIndicator` : l'amplitude (range) de la barre * courante. `update` est appelé une fois par barre clôturée et reçoit un * `IndicatorContext`. L'engine accumule les retours dans une `Series`. */ const BarRange = defineIndicator<{ history?: number }, number>({ name: 'BarRange', warmup: () => 1, update: (ctx) => ctx.series.high.at(0) - ctx.series.low.at(0), }); export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const range = api.use(BarRange, {}); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; if (range.at(0) > 0) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` An indicator handle returned by defineIndicator. ## See also * [`defineIndicator`](/en/v1/indicators/define-indicator/) · [`IndicatorContext`](/en/v1/reference/types/indicator-context/) # InitContext ## `InitContext` **At a glance** — Passed to a strategy’s `init()` hook, called once before the first bar. It is intentionally read-only: you get `account`, `clock` and `log` for one-time setup, but no `order` or market `series`, since no trading happens during initialization. ## Signature ```ts export interface InitContext { readonly account: AccountReader; readonly clock: ClockReader; readonly log: LogApi; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` init() receives an InitContext for one-time setup. ## See also * [`AccountReader`](/en/v1/reference/types/account-reader/) * [`ClockReader`](/en/v1/reference/types/clock-reader/) * [`LogApi`](/en/v1/reference/types/log-api/) # InputApi ## `InputApi` Exposed as `api.input` inside `setup()`, this declares the tunable inputs of your strategy. Each method — `int`, `float`, `bool`, and `enum` — returns the resolved value to use immediately, while the declaration drives the strategy manifest and the UI sliders shown to users. Numeric inputs accept optional `min`, `max`, and `step` bounds. ## Signature ```ts export interface InputApi { int(name: string, def: number, opts?: { min?: number; max?: number; step?: number }): number; float(name: string, def: number, opts?: { min?: number; max?: number; step?: number }): number; bool(name: string, def: boolean): boolean; enum(name: string, def: T, options: readonly T[]): T; } ``` ## Example cookbook/buy-with-rr-stop ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Recette cookbook : achat market avec stop-loss ATR et take-profit en R:R 1:2. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5, }, setup: (api) => { const riskPercent = api.input.float('riskPercent', 1, { min: 0.1, max: 2, step: 0.1 }); return { onBar(ctx) { if (!ctx.position.isFlat) return; if (ctx.position.hasPendingOrder) return; ctx.order.marketBuy({ riskPercent, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); }, }; }, }); ``` Declaring inputs that drive the strategy's risk/reward. ## See also * [`SetupApi`](/en/v1/reference/types/setup-api/) # LogApi ## `LogApi` **At a glance** — The structured logger exposed as `ctx.log` on every strategy context. Each level (`info`, `warn`, `error`, `debug`) takes a message and an optional `LogContext` object so you can attach structured fields to a log line. ## Signature ```ts export interface LogApi { info(msg: string, context?: LogContext): void; warn(msg: string, context?: LogContext): void; error(msg: string, context?: LogContext): void; debug(msg: string, context?: LogContext): void; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` Use ctx.log to emit structured logs. ## See also * [`LogContext`](/en/v1/reference/types/log-context/) # LogContext ## `LogContext` **At a glance** — A structured context object attached to a log line. It is an open map of arbitrary keys to unknown values, passed as the optional second argument to any `LogApi` method to enrich a log entry with relevant data. ## Signature ```ts export type LogContext = Record; ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` Attach a LogContext to a log call. ## See also * [`LogApi`](/en/v1/reference/types/log-api/) # MarketSeries ## `MarketSeries` The per-bar OHLCV data exposed to your strategy as `ctx.series`. It gives you one [`Series`](/en/v1/reference/types/series/) per column (`open`, `high`, `low`, `close`, `volume`) that you index into with `at(n)`. ## Signature ```ts export type MarketSeries = MarketSeriesView; ``` ## Example indicators/sma-trend ```ts import { defineStrategy, SMA } from '@nexpips/sdk-trading'; /** * Filtre de tendance : n'acheter que lorsque la clôture est au-dessus de * la moyenne mobile simple SMA(50). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const sma = api.use(SMA, { source: 'close', period: 50 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; if (ctx.series.close.at(0) > sma.at(0)) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` Access OHLCV columns through ctx.series. ## See also * [`Series`](/en/v1/reference/types/series/) * [`Bar`](/en/v1/reference/types/bar/) * [`Source`](/en/v1/reference/types/source/) # ModifyParams ## `ModifyParams` The arguments for `ctx.order.modifyPosition`, used to adjust an open trade’s protective levels. Both `stopLoss` and `takeProfit` are optional [`PriceLevel`](/en/v1/reference/types/price-level/) values — supply only the ones you want to change, for instance to trail a stop. ## Signature ```ts export interface ModifyParams { stopLoss?: PriceLevel; takeProfit?: PriceLevel; } ``` ## Example order/limit-and-cancel ```ts import { defineStrategy, type OrderTicket } from '@nexpips/sdk-trading'; /** * Ordres en attente : poser un `limitBuy`, l'annuler (`cancel`) s'il n'est * pas rempli en quelques barres, et resserrer le stop d'une position ouverte * (`modifyPosition`). Le `limitBuy` renvoie un `OrderTicket` opaque à conserver. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M15', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: () => { let pending: OrderTicket | null = null; let barsWaiting = 0; return { onBar(ctx) { if (ctx.position.hasPendingOrder && pending) { barsWaiting += 1; if (barsWaiting > 3) { ctx.order.cancel(pending); pending = null; barsWaiting = 0; } return; } if (ctx.position.isLong) { ctx.order.modifyPosition({ stopLoss: { type: 'pips', value: 10 } }); return; } if (ctx.position.isFlat) { const limitPrice = ctx.bar.close - 0.001; pending = ctx.order.limitBuy(limitPrice, { riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); barsWaiting = 0; } }, }; }, }); ``` Adjust the stop-loss or take-profit of an open position. ## See also * [`PriceLevel`](/en/v1/reference/types/price-level/) * [`EntryParams`](/en/v1/reference/types/entry-params/) # OrderTicket ## `OrderTicket` **At a glance** — An opaque correlation handle returned by `OrderApi` methods such as `marketBuy`, `marketSell`, `limitBuy`/`limitSell`, `stopBuy`/`stopSell`, `closePosition` and `modifyPosition`. It deliberately carries no price or fill information — the real state lives on `ctx.position`, and the outcome arrives via the asynchronous events `onOrderFilled`, `onOrderRejected` and `onPositionClosed`. Pass it to `ctx.order.cancel()` to cancel a pending order. ## Signature ```ts export interface OrderTicket { readonly id: string; } ``` ## Example order/limit-and-cancel ```ts import { defineStrategy, type OrderTicket } from '@nexpips/sdk-trading'; /** * Ordres en attente : poser un `limitBuy`, l'annuler (`cancel`) s'il n'est * pas rempli en quelques barres, et resserrer le stop d'une position ouverte * (`modifyPosition`). Le `limitBuy` renvoie un `OrderTicket` opaque à conserver. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M15', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: () => { let pending: OrderTicket | null = null; let barsWaiting = 0; return { onBar(ctx) { if (ctx.position.hasPendingOrder && pending) { barsWaiting += 1; if (barsWaiting > 3) { ctx.order.cancel(pending); pending = null; barsWaiting = 0; } return; } if (ctx.position.isLong) { ctx.order.modifyPosition({ stopLoss: { type: 'pips', value: 10 } }); return; } if (ctx.position.isFlat) { const limitPrice = ctx.bar.close - 0.001; pending = ctx.order.limitBuy(limitPrice, { riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); barsWaiting = 0; } }, }; }, }); ``` Keep an OrderTicket to cancel the pending order later. ## See also * [`marketBuy`](/en/v1/api/order/market-buy/) * [`EntryParams`](/en/v1/reference/types/entry-params/) # PositionReader ## `PositionReader` The single source of truth for your current position, exposed as `ctx.position`. Read flags like `isFlat`, `isLong`, `isShort`, and `hasPendingOrder` to branch your logic, and fields such as `size`, `entryPrice`, `stopLoss`, `takeProfit`, `unrealizedPnl`, and `openTime` to inspect an open trade. Everything is `readonly` — you change state through `ctx.order`, never by mutating this view. ## Signature ```ts export interface PositionReader { readonly isFlat: boolean; readonly isLong: boolean; readonly isShort: boolean; readonly hasPendingOrder: boolean; readonly size: number; readonly entryPrice: number | null; readonly stopLoss: number | null; readonly takeProfit: number | null; readonly unrealizedPnl: number; readonly openTime: number | null; } ``` ## Example order/market-buy-simple ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Acheter au marché si on est flat, avec un stop-loss à 20 pips. * * Pattern : entrée propre — vérifier `hasPendingOrder` ET `isFlat` * pour éviter d'envoyer un ordre alors qu'un précédent est encore * en attente de confirmation broker. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5, }, setup: () => ({ onBar(ctx) { if (!ctx.position.isFlat) return; if (ctx.position.hasPendingOrder) return; ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); }, }), }); ``` Check ctx.position before opening a trade. ## See also * [`marketBuy`](/en/v1/api/order/market-buy/) * [`OrderTicket`](/en/v1/reference/types/order-ticket/) # PriceLevel ## `PriceLevel` A discriminated union describing how to express a stop-loss or take-profit. Pick the form that fits your strategy: an absolute `price`, a distance in `pips`, an [`ATR`](/en/v1/indicators/atr/)-based offset (`period` × `multiple`), or a reward-to-risk ratio (`rr`). The engine resolves it to a concrete price at order time. ## Signature ```ts export type PriceLevel = | { type: 'price'; value: number } | { type: 'pips'; value: number } | { type: 'atr'; period: number; multiple: number } | { type: 'rr'; value: number }; ``` ## Example indicators/atr-stop ```ts import { ATR, defineStrategy } from '@nexpips/sdk-trading'; /** * Dimensionner le stop avec la volatilité : un stop-loss exprimé en * multiples d'ATR (`{ type: 'atr' }`) s'élargit quand le marché est agité * et se resserre quand il est calme. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const atr = api.use(ATR, { period: 14 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; // N'entrer que si la volatilité courante est mesurable. if (atr.at(0) <= 0) return; ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); }, }; }, }); ``` Express a stop-loss as an ATR multiple. ## See also * [`EntryParams`](/en/v1/reference/types/entry-params/) * [`ATR`](/en/v1/indicators/atr/) # RejectReason ## `RejectReason` **At a glance** — The closed set of reasons an order can be rejected by the market or broker. It is the `reason` field of a `Rejection` delivered to `onOrderRejected`, letting you branch your handling per cause (no liquidity, requote, market closed, and so on). ## Signature ```ts export type RejectReason = | 'no_liquidity' | 'requote' | 'market_closed' | 'insufficient_margin' | 'broker_error' | 'connection_lost' | 'invalid_volume_too_small' | 'invalid_volume_too_large'; ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` Branch on a RejectReason inside onOrderRejected. ## See also * [`Rejection`](/en/v1/reference/types/rejection/) # Rejection ## `Rejection` **At a glance** — Passed to `onOrderRejected` when the broker or market refuses an order. It describes a *recoverable* asynchronous condition (the `reason` is a `RejectReason`), unlike `StrategyInvariantError`, which signals a synchronous bug in your strategy code. ## Signature ```ts export interface Rejection { ticketId: string; reason: RejectReason; message: string; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` onOrderRejected receives a Rejection. ## See also * [`RejectReason`](/en/v1/reference/types/reject-reason/) * [`StrategyInvariantError`](/en/v1/api/errors/strategy-invariant-error/) # RiskConfig ## `RiskConfig` The risk guardrails declared on your [`StrategyDefinition`](/en/v1/reference/types/strategy-definition/). The engine enforces them on every order: `maxRiskPercentPerTrade` caps per-trade exposure, `maxOpenPositions` limits concurrency, `maxDailyLossPercent` halts trading after a daily drawdown, and the optional `maxConsecutiveRejects` stops the bot after repeated broker rejections. ## Signature ```ts export interface RiskConfig { maxRiskPercentPerTrade: number; maxOpenPositions: number; maxDailyLossPercent: number; maxConsecutiveRejects?: number; } ``` ## Example order/market-buy-simple ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Acheter au marché si on est flat, avec un stop-loss à 20 pips. * * Pattern : entrée propre — vérifier `hasPendingOrder` ET `isFlat` * pour éviter d'envoyer un ordre alors qu'un précédent est encore * en attente de confirmation broker. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5, }, setup: () => ({ onBar(ctx) { if (!ctx.position.isFlat) return; if (ctx.position.hasPendingOrder) return; ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); }, }), }); ``` Declare risk limits enforced by the engine. ## See also * [`StrategyDefinition`](/en/v1/reference/types/strategy-definition/) # Series ## `Series` An indexed, time-ordered stream of values. Read it with `at(0)` for the current bar and `at(n)` for `n` bars ago — indexing is bounded to the past, so there is no look-ahead. `highest(period)` and `lowest(period)` scan the trailing window, and `length` reports how many values are available. Indicators return a `Series`. ## Signature ```ts export interface Series { at(back: number): T; readonly length: number; highest(period: number): number; lowest(period: number): number; } ``` ## Example indicators/sma-trend ```ts import { defineStrategy, SMA } from '@nexpips/sdk-trading'; /** * Filtre de tendance : n'acheter que lorsque la clôture est au-dessus de * la moyenne mobile simple SMA(50). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const sma = api.use(SMA, { source: 'close', period: 50 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; if (ctx.series.close.at(0) > sma.at(0)) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` Read the latest indicator value with at(0). ## See also * [`MarketSeries`](/en/v1/reference/types/market-series/) * [`crossedOver`](/en/v1/api/helpers/crossed-over/) # SetupApi ## `SetupApi` The `api` argument passed to your strategy’s `setup()` function. Call `use()` to register an indicator and receive its output `Series`, and reach for `input` to declare tunable parameters. Everything here runs once during the setup phase, before the first bar — registering indicators or inputs later is not allowed. ## Signature ```ts export interface SetupApi { use(indicator: IndicatorDef, params: P): Series; input: InputApi; } ``` ## Example indicators/ema-crossover ```ts import { crossedOver, crossedUnder, defineStrategy, EMA } from '@nexpips/sdk-trading'; /** * Croisement de moyennes exponentielles. Golden cross (EMA rapide passe * au-dessus de la lente) → entrée ; death cross → sortie. Montre `EMA`, * `crossedOver` et `crossedUnder`. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const fast = api.use(EMA, { source: 'close', period: 12 }); const slow = api.use(EMA, { source: 'close', period: 26 }); return { onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder && crossedOver(fast, slow)) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); return; } if (ctx.position.isLong && crossedUnder(fast, slow)) { ctx.order.closePosition('ema death cross'); } }, }; }, }); ``` Registering two EMAs during setup. ## See also * [`InputApi`](/en/v1/reference/types/input-api/) · [`defineIndicator`](/en/v1/indicators/define-indicator/) # Source ## `Source` The derived price input an indicator computes on. Beyond the raw OHLC fields you can pick blended sources: `hl2` (high+low)/2, `hlc3` (high+low+close)/3, or `ohlc4` (open+high+low+close)/4. Most indicators default to `close`. ## Signature ```ts export type Source = 'open' | 'high' | 'low' | 'close' | 'hl2' | 'hlc3' | 'ohlc4'; ``` ## Example indicators/sma-trend ```ts import { defineStrategy, SMA } from '@nexpips/sdk-trading'; /** * Filtre de tendance : n'acheter que lorsque la clôture est au-dessus de * la moyenne mobile simple SMA(50). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5 }, setup: (api) => { const sma = api.use(SMA, { source: 'close', period: 50 }); return { onBar(ctx) { if (!ctx.position.isFlat || ctx.position.hasPendingOrder) return; if (ctx.series.close.at(0) > sma.at(0)) { ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'atr', period: 14, multiple: 2 }, takeProfit: { type: 'rr', value: 2 }, }); } }, }; }, }); ``` Choose the price source feeding an indicator. ## See also * [`SMA`](/en/v1/indicators/sma/) * [`Series`](/en/v1/reference/types/series/) # Strategy ## `Strategy` The opaque, branded value returned by `defineStrategy()`. You never construct it directly or read its fields — you simply `export default` it from your bot file so the runtime can pick it up. The `__brand` tag exists only to prevent plain objects from being mistaken for a validated strategy at compile time. ## Signature ```ts export interface Strategy { readonly __brand: 'Strategy'; } ``` ## Example order/market-buy-simple ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Acheter au marché si on est flat, avec un stop-loss à 20 pips. * * Pattern : entrée propre — vérifier `hasPendingOrder` ET `isFlat` * pour éviter d'envoyer un ordre alors qu'un précédent est encore * en attente de confirmation broker. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5, }, setup: () => ({ onBar(ctx) { if (!ctx.position.isFlat) return; if (ctx.position.hasPendingOrder) return; ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); }, }), }); ``` Exporting the strategy returned by defineStrategy. ## See also * [`defineStrategy`](/en/v1/api/runtime/define-strategy/) # StrategyDefinition ## `StrategyDefinition` The configuration object you pass to `defineStrategy()`. It pins your strategy to a `symbol` and `timeframe`, declares its `risk` policy, optionally requests a window of `marketHistory` bars to warm up indicators, and supplies a `setup` function that registers indicators and inputs and returns the `StrategyHooks` that drive execution. ## Signature ```ts export interface StrategyDefinition { symbol: string; timeframe: Timeframe; risk: RiskConfig; marketHistory?: number; setup: (api: SetupApi) => StrategyHooks; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` A full strategy definition with every lifecycle hook. ## See also * [`defineStrategy`](/en/v1/api/runtime/define-strategy/) · [`StrategyHooks`](/en/v1/reference/types/strategy-hooks/) · [`RiskConfig`](/en/v1/reference/types/risk-config/) · [`Timeframe`](/en/v1/reference/types/timeframe/) # StrategyHooks ## `StrategyHooks` The object your `setup()` returns. `onBar` is the only required hook — it fires once per closed bar and is where most trading logic lives. The rest are optional lifecycle callbacks: `init` runs once at startup, `onTick` fires on every price update, and `onOrderFilled`, `onOrderRejected`, and `onPositionClosed` react to execution events. ## Signature ```ts export interface StrategyHooks { init?(ctx: InitContext): void; onBar(ctx: BarContext): void; onTick?(ctx: TickContext): void; onOrderFilled?(ctx: EventContext, fill: Fill): void; onOrderRejected?(ctx: EventContext, rejection: Rejection): void; onPositionClosed?(ctx: EventContext, closed: ClosedPosition): void; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` Every hook wired up. ## See also * [`BarContext`](/en/v1/reference/types/bar-context/) · [`EventContext`](/en/v1/reference/types/event-context/) · [`defineStrategy`](/en/v1/api/runtime/define-strategy/) # TickContext ## `TickContext` **At a glance** — Passed to `onTick()` for intrabar updates. It extends `BarContext` with the live `bid` and `ask` prices, so you keep full access to the series, position, account and order API while reacting to price movement before the bar closes. ## Signature ```ts export interface TickContext extends BarContext { readonly bid: number; readonly ask: number; } ``` ## Example hooks/lifecycle ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Cycle de vie complet : tous les hooks optionnels et les contextes qu'ils * reçoivent — `InitContext` (init), `BarContext` (onBar), `TickContext` * (onTick), `EventContext` + `Fill` / `Rejection` / `ClosedPosition` (events). */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'M5', risk: { maxRiskPercentPerTrade: 0.5, maxOpenPositions: 1, maxDailyLossPercent: 3, maxConsecutiveRejects: 5, }, setup: () => ({ init(ctx) { ctx.log.info('strategy started', { balance: ctx.account.balance, at: ctx.clock.now }); }, onBar(ctx) { if (ctx.position.isFlat && !ctx.position.hasPendingOrder) { ctx.order.marketBuy({ riskPercent: 0.5, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); } }, onTick(ctx) { const spread = ctx.ask - ctx.bid; if (spread > 0.0005) ctx.log.debug('wide spread', { spread }); }, onOrderFilled(ctx, fill) { ctx.log.info('filled', { side: fill.side, price: fill.price, size: fill.size }); }, onOrderRejected(ctx, rejection) { // rejection.reason est un RejectReason (union fermée). ctx.log.warn('order rejected', { reason: rejection.reason, message: rejection.message }); }, onPositionClosed(ctx, closed) { ctx.log.info('position closed', { pnl: closed.realizedPnl }); }, }), }); ``` onTick() receives a TickContext with live bid/ask. ## See also * [`BarContext`](/en/v1/reference/types/bar-context/) # Timeframe ## `Timeframe` The bar interval a strategy runs on, from one-minute (`M1`) candles up to weekly (`W1`). You declare it once in your [`StrategyDefinition`](/en/v1/reference/types/strategy-definition/), and the engine feeds your hooks one closed bar of that size at a time. ## Signature ```ts export type Timeframe = 'M1' | 'M5' | 'M15' | 'M30' | 'H1' | 'H4' | 'D1' | 'W1'; ``` ## Example order/market-buy-simple ```ts import { defineStrategy } from '@nexpips/sdk-trading'; /** * Acheter au marché si on est flat, avec un stop-loss à 20 pips. * * Pattern : entrée propre — vérifier `hasPendingOrder` ET `isFlat` * pour éviter d'envoyer un ordre alors qu'un précédent est encore * en attente de confirmation broker. */ export default defineStrategy({ symbol: 'EURUSD', timeframe: 'H1', risk: { maxRiskPercentPerTrade: 1, maxOpenPositions: 1, maxDailyLossPercent: 5, }, setup: () => ({ onBar(ctx) { if (!ctx.position.isFlat) return; if (ctx.position.hasPendingOrder) return; ctx.order.marketBuy({ riskPercent: 1, stopLoss: { type: 'pips', value: 20 }, takeProfit: { type: 'rr', value: 2 }, }); }, }), }); ``` A strategy declares its timeframe and reacts on each closed bar. ## See also * [`StrategyDefinition`](/en/v1/reference/types/strategy-definition/)