---
title: Pipsend Trading Bots
description: Developer documentation for writing trading bots on Pipsend with the Bot SDK. Covers lifecycle hooks, the ctx API (market, order, portfolio, storage, log, chart), data types, permissions, limits and complete examples.
---

# Pipsend Trading Bots

## Overview

A Pipsend bot is a small TypeScript program that reacts to market events (ticks, closed candles, timers, closed positions) and can read market data, read the account, send orders and draw on the chart it is attached to.

You write a bot as a set of **lifecycle hook methods**. The platform compiles the code, runs it in an isolated sandbox and calls your hooks when events happen. Everything the bot can do goes through a single object, `ctx`, that is passed to every hook.

A minimal bot:

```typescript
onInit(ctx) {
  ctx.log.info('Hello World')
},

onDispose(ctx) {
  ctx.log.info('Goodbye')
},
```

Main building blocks:

| Block | Purpose |
|-------|---------|
| Hooks | `onInit`, `onTick`, `onCandleClose`, `onTimer`, `onPositionClosed`, `onDispose` |
| `ctx.market` | Symbol specification, last tick, historical candles |
| `ctx.order` | Send, cancel, modify and close orders |
| `ctx.portfolio` | Open positions, balance, equity, closed-deal history |
| `ctx.storage` | Key-value state that lives as long as the bot runs |
| `ctx.log` | Messages shown in the bot output panel |
| `ctx.chart` | Lines, boxes, markers, tables and indicators drawn on the chart |
| `ctx.run` / `ctx.inputs` | Where the bot runs and the parameters chosen by the trader |

## How a bot runs

### Sandbox

- Each bot runs in its own **Web Worker**: a separate thread, isolated from the user interface and from other bots.
- Your TypeScript source is compiled before it starts (target ES2020).
- Every `ctx` call is an asynchronous message between the worker and the main thread. The main thread checks permissions and trading limits before anything reaches the server.

### Lifecycle

```
Compile -> Init worker -> Send code -> 'ready' -> Set up subscriptions -> onInit -> [running] -> onDispose -> Terminate
```

1. The source is compiled.
2. A worker is created and receives the compiled code.
3. The worker loads the code and reports `ready`.
4. The main thread sets up the market data subscriptions for the symbol and timeframe the bot runs on.
5. `onInit` is dispatched.
6. The bot is running and receives ticks, candle closes, timer events and closed-position events.
7. On stop, `onDispose` is dispatched and the worker is terminated after a 3 second timeout.

Right after start the runtime replays the most recent closed candles (about 600) through `onCandleClose` so that bots can build their state. While this replay runs, `ctx.run.warmup` is `true`, orders are blocked and drawings are kept. See [ctx.run](#ctxrun).

### Run configuration

When a bot is started (from the toolbar or the bot manager) it receives a run configuration:

```typescript
interface BotRunConfig {
  symbol: string           // Symbol to trade (e.g. 'EURUSD')
  timeframe: string        // Candle timeframe (e.g. '1m', '1h')
  chartType?: string       // Series type the chart paints (e.g. 'candlestick', 'heikin-ashi')
  timerIntervalMs?: number // Timer interval (optional)
}
```

The run configuration determines:

- which symbol's ticks the bot receives in `onTick`;
- which symbol and timeframe's candle closes trigger `onCandleClose`;
- the interval of `onTimer` calls.

A bot bound to a chart follows the chart's timeframe like an indicator: switching the timeframe while the chart shows the bot's symbol restarts the bot on the new timeframe with the same inputs, warm-up included. Switching the chart to another symbol leaves the bot running on its own symbol and hides its drawings until the chart comes back. Switching the chart to or from Heikin Ashi restarts the bot, warm-up included; other chart type changes do not.

Read the run configuration from [`ctx.run`](#ctxrun) instead of hardcoding the symbol.

### Hook execution and timeouts

Hooks run **sequentially** through a priority queue: a hook must finish before the next one starts. Lower number means higher priority.

| Priority | Hook |
|----------|------|
| 0 | `onDispose` |
| 1 | `onInit` |
| 2 | `onPositionClosed` |
| 3 | `onCandleClose` |
| 4 | `onTimer` |
| 5 | `onTick` |

Each hook has a maximum execution time. If it is exceeded, the worker is killed and restarted.

| Hook | Timeout |
|------|---------|
| `onInit` | 10,000 ms |
| `onTick` | 5,000 ms |
| `onCandleClose` | 5,000 ms |
| `onTimer` | 5,000 ms |
| `onPositionClosed` | 5,000 ms |
| `onDispose` | 5,000 ms |

If the bot is slower than the event stream, events are queued or dropped:

| Hook | Policy | Capacity |
|------|--------|----------|
| `onTick` | Keep latest | 1 (older ticks are dropped) |
| `onCandleClose` | FIFO queue | 10 (oldest dropped) |
| `onTimer` | FIFO queue | 5 (oldest dropped) |
| `onInit` | Always queued | - |
| `onDispose` | Always queued | - |

A warning is logged if no tick is received for 10 seconds.

### Crash recovery and kill switch

- A crashed bot is restarted automatically with exponential backoff: 1 s, 2 s, 4 s, and so on, capped at 30 s.
- At most **3 restarts** are allowed within a 60 second window.
- When that limit is exceeded, the **kill switch** activates: the bot cannot be started until the user clears it from the bot manager. The user can also trigger the kill switch manually.

## Writing your first bot

The source you write in the editor is the **body** of a `defineBot({...})` call. The platform adds the wrapper and the metadata (id, name, version, permissions) for you, so you write only the hook methods, in method shorthand syntax, each followed by a comma.

```typescript
onInit(ctx) {
  ctx.storage.set('symbol', 'EURUSD')
  ctx.storage.set('traded', false)

  const info = ctx.market.getSymbolInfo('EURUSD')
  ctx.storage.set('info', info)
},

async onCandleClose(ctx, candle) {
  if (ctx.storage.get('traded')) return

  const symbol = ctx.storage.get('symbol') as string
  const info = ctx.storage.get('info') as SymbolInfo
  const round = (v: number) => parseFloat(v.toFixed(info.digits))

  const sl = round(candle.close - 500 * info.point)
  const tp = round(candle.close + 1000 * info.point)

  try {
    const result = await ctx.order.send({
      symbol,
      type: 'market',
      side: 'buy',
      volume: info.volumeMin,
      sl,
      tp,
    })
    ctx.log.info(`Buy order placed: ${result.orderId}`)
    ctx.storage.set('traded', true)
  } catch (err) {
    ctx.log.error(`Order failed: ${err}`)
  }
},
```

This bot needs the permissions `trade`, `read-market` and `storage`. Select them when you create the bot (see [Permissions](#permissions)).

Things to notice:

- State lives in `ctx.storage`, never in top-level variables (see [Rules and restrictions](#rules-and-restrictions)).
- Hooks that use `await` are marked `async`.
- The order is wrapped in `try/catch` and the error is logged.
- Stop-loss and take-profit distances use `info.point`, and prices are rounded to `info.digits`.

## Hooks

All hooks are optional and all of them may be `async` (return a `Promise`). They receive `ctx` as first argument.

| Hook | Fires | Payload |
|------|-------|---------|
| `onInit(ctx)` | Once when the bot starts, after market data is initialized | - |
| `onTick(ctx, tick)` | Every price tick for the configured symbol | [`Tick`](#tick) |
| `onCandleClose(ctx, candle)` | When a candle closes on the configured symbol and timeframe | [`Candle`](#candle) |
| `onTimer(ctx)` | At the configured interval | - |
| `onPositionClosed(ctx, deal)` | When any position of the account is fully closed | [`Deal`](#deal) |
| `onDispose(ctx)` | When the bot is stopped (graceful shutdown) | - |

### onInit

```typescript
onInit(ctx: BotContext): void | Promise<void>
```

Runs once at start. Use it to save the configuration in `ctx.storage` and to read the symbol specification. `getSymbolInfo` is available from this hook on. Timeout: 10 seconds.

```typescript
onInit(ctx) {
  ctx.storage.set('tradeCount', 0)
  ctx.storage.set('maxDrawdown', 0)
  ctx.storage.set('closes', [])
},
```

### onTick

```typescript
onTick(ctx: BotContext, tick: Tick): void | Promise<void>
```

Called for each tick of the bot's symbol. If the bot is still busy when new ticks arrive, only the latest one is kept. Keep this hook fast (5 second timeout).

```typescript
onTick(ctx, tick) {
  const count = (ctx.storage.get('count') as number) + 1
  ctx.storage.set('count', count)

  if (count % 100 === 0) {
    ctx.log.info(`Tick #${count}: Bid=${tick.bid} Ask=${tick.ask}`)
  }
},
```

### onCandleClose

```typescript
onCandleClose(ctx: BotContext, candle: Candle): void | Promise<void>
```

Called when a candle of the configured symbol and timeframe closes. This is the usual place for strategy logic, because closed-candle indicators do not repaint. Also called during the warm-up replay (check `ctx.run.warmup`).

```typescript
onCandleClose(ctx, candle) {
  const closes = ctx.storage.get('closes') as number[]
  closes.push(candle.close)
  ctx.storage.set('closes', closes)
},
```

### onTimer

```typescript
onTimer(ctx: BotContext): void | Promise<void>
```

Called at the interval configured when the bot is started (`timerIntervalMs`).

```typescript
async onTimer(ctx) {
  const balance = await ctx.portfolio.getBalance()
  const equity = await ctx.portfolio.getEquity()
  ctx.log.info(`Balance: $${balance.toFixed(2)} | Equity: $${equity.toFixed(2)}`)
},
```

### onPositionClosed

```typescript
onPositionClosed(ctx: BotContext, deal: Deal): void | Promise<void>
```

Called when a position is **fully** closed (stop loss, take profit, trailing, manual, bot, stop out). Details:

- It fires for **every** position of the account, not only the bot's own. Filter by `deal.tag` to react only to your positions.
- It requires the `read-portfolio` permission.
- It is not delivered for partial closes.
- `deal.reason` is `'bot'` when the position was closed through `ctx.order.close` by a bot.

```typescript
async onPositionClosed(ctx, deal) {
  if (!deal.tag?.startsWith('alpha:')) return
  ctx.log.info(`Closed ${deal.symbol} by ${deal.reason}: ${deal.profit.toFixed(2)}`)
  const losses = ((ctx.storage.get('losses') as number) ?? 0) + (deal.profit < 0 ? 1 : 0)
  ctx.storage.set('losses', losses)
},
```

### onDispose

```typescript
onDispose(ctx: BotContext): void | Promise<void>
```

Called when the bot is stopped. Use it to log, and optionally to clear the drawings (the platform also clears them automatically when the bot stops).

```typescript
onDispose(ctx) {
  ctx.chart.clearAll()
  ctx.log.info('Bot stopped')
},
```

## The ctx API

`ctx` is injected into every hook.

```typescript
interface BotContext {
  run: BotRunInfo
  inputs: BotInputValues
  market: BotMarketAPI
  order: BotOrderAPI
  portfolio: BotPortfolioAPI
  storage: BotStorageAPI
  log: BotLogAPI
  chart: BotChartAPI
}
```

### ctx.run

Symbol and timeframe of the chart the bot was started on. Read the symbol from here instead of hardcoding it.

```typescript
interface BotRunInfo {
  symbol: string
  timeframe: string
  /** True while the runtime replays recent history right after start; orders are blocked then. */
  warmup: boolean
  /** Series type the chart paints, e.g. 'candlestick' or 'heikin-ashi'. Missing on clients older than trader 1.10.22. */
  chartType?: string
  /** 'web' in the browser and the desktop app, 'mobile' in the phone app. Missing on clients older than trader 1.12.3. */
  platform?: 'web' | 'mobile'
}
```

- `getCandles` and `onCandleClose` always carry real candles. A bot that wants to compute like a TradingView indicator on a Heikin Ashi chart must convert them itself.
- `ctx.run.platform` lets a bot keep heavy drawings off phones (for example a long history of signal markers only on the web). Treat a missing value like `'mobile'` so older clients stay light.

### ctx.inputs

Resolved parameter values: the trader's choices merged over the defaults declared in the bot's `inputs` object. See [Inputs](#inputs-configurable-parameters).

```typescript
type BotInputValues = Record<string, number | boolean | string>
```

### ctx.market

Requires the `read-market` permission.

```typescript
interface BotMarketAPI {
  getSymbolInfo(symbol: string): SymbolInfo | undefined
  getLastTick(symbol: string): Tick | undefined
  getCandles(symbol: string, timeframe: string, count: number): Promise<Candle[]>
}
```

#### getSymbolInfo(symbol)

| | |
|--|--|
| Parameters | `symbol: string` - symbol code, for example `'EURUSD'` |
| Returns | `SymbolInfo` or `undefined` if the symbol is not tradeable on the account |
| Sync/async | Synchronous. Available after `onInit` |

```typescript
const info = ctx.market.getSymbolInfo('EURUSD')
// { name: 'EURUSD', digits: 5, point: 0.00001, tickSize: 0.00001, contractSize: 100000,
//   volumeMin: 0.01, volumeMax: 200, volumeStep: 0.01, stopsLevel: 0, tickValue: 1,
//   baseCurrency: 'EUR', quoteCurrency: 'USD' }

const gold = ctx.market.getSymbolInfo('XAUUSD')
// { name: 'XAUUSD', digits: 2, point: 0.01, tickSize: 0.01, contractSize: 100, ..., tickValue: 1 }
```

Use `point` for price arithmetic and rounding. Do not derive the pip size from `digits` alone: a pip is 10 points on 5-digit forex and 3-digit JPY pairs, but on gold or indices "pip" is ambiguous and `point` is the only safe unit.

```typescript
const round = (v: number) => parseFloat(v.toFixed(info.digits))
const sl = round(entry - 150 * info.point)
```

Position sizing by risk uses `tickValue` and `volumeStep`:

```typescript
const riskUsd = balance * 0.01
const slTicks = Math.abs(entry - sl) / info.tickSize
const rawLots = riskUsd / (slTicks * info.tickValue)
const lots = Math.max(info.volumeMin,
  Math.min(info.volumeMax, Math.floor(rawLots / info.volumeStep) * info.volumeStep))
```

#### getLastTick(symbol)

| | |
|--|--|
| Parameters | `symbol: string` |
| Returns | `Tick` or `undefined` - the most recent tick from the local cache |
| Sync/async | Synchronous |

```typescript
const tick = ctx.market.getLastTick('EURUSD')
if (tick) {
  ctx.log.info(`Bid: ${tick.bid}, Ask: ${tick.ask}`)
}
```

#### getCandles(symbol, timeframe, count)

| | |
|--|--|
| Parameters | `symbol: string`, `timeframe: string`, `count: number` |
| Returns | `Promise<Candle[]>`, closed candles only, oldest first |
| Sync/async | Asynchronous: sends a request to the main thread |

```typescript
const candles = await ctx.market.getCandles('EURUSD', '1h', 100)
const lastCandle = candles[candles.length - 1]
```

- **Supported timeframes:** `1m`, `2m`, `3m`, `5m`, `10m`, `15m`, `30m`, `1h`, `2h`, `4h`, `1d`.
- `count` goes up to 10,000 candles (larger values are capped). Asking for a whole week of `1m` candles in one call works, so a bot can compute statistics over history it does not replay.
- Only closed candles are returned: the candle currently forming is never included, so indicators computed in `onCandleClose` do not repaint.
- Sub-day buckets are aligned to midnight UTC (a `4h` candle starts at 00:00, 04:00, 08:00 UTC).

### ctx.order

Requires the `trade` permission.

```typescript
interface BotOrderAPI {
  send(order: OrderRequest): Promise<{ orderId: string }>
  cancel(orderId: string): Promise<void>
  modify(positionId: string, changes: PositionModifyRequest): Promise<void>
  close(positionId: string, volume?: number): Promise<void>
}
```

Every call that reaches the server can fail: wrap `send`, `modify` and `close` in `try/catch` and log the error.

#### send(order)

Sends a new order. Returns the order ID on success and **throws** on failure. See [`OrderRequest`](#orderrequest).

```typescript
const result = await ctx.order.send({
  symbol: 'EURUSD',
  type: 'market',
  side: 'buy',
  volume: 0.01,
})
ctx.log.info(`Order placed: ${result.orderId}`)
```

**Market order with SL and TP**, using `point` and `digits` from the symbol specification:

```typescript
const info = ctx.market.getSymbolInfo('EURUSD')
const round = (v: number) => parseFloat(v.toFixed(info.digits))

const tick = ctx.market.getLastTick('EURUSD')
const sl = round(tick.ask - 500 * info.point) // 500 points below entry
const tp = round(tick.ask + 1000 * info.point) // 1000 points above entry

await ctx.order.send({
  symbol: 'EURUSD',
  type: 'market',
  side: 'buy',
  volume: 0.01,
  sl,
  tp,
})
```

SL and TP values must be rounded to the symbol's `digits` precision. The server rejects values with too many decimal places.

**Limit order:**

```typescript
await ctx.order.send({
  symbol: 'EURUSD',
  type: 'limit',
  side: 'buy',
  volume: 0.1,
  price: 1.0950,  // Limit entry price
  sl: 1.0900,
  tp: 1.1000,
})
```

**Stop order:**

```typescript
await ctx.order.send({
  symbol: 'EURUSD',
  type: 'stop',
  side: 'sell',
  volume: 0.05,
  price: 1.0850,  // Stop entry price
})
```

**Tagging positions.** `getPositions()` returns every open position of the account, including manual ones and those of other bots. Send a `tag` with each order and filter by it to find your own positions. A tag with a unique suffix lets a bot tell its own positions apart:

```typescript
const tag = `alpha:${candle.time}:tp1`
await ctx.order.send({ symbol, type: 'market', side: 'buy', volume, sl, tp, tag })

const mine = (await ctx.portfolio.getPositions()).filter((p) => p.tag?.startsWith('alpha:'))
```

#### cancel(orderId)

Cancels a pending order (limit or stop). Returns `Promise<void>`.

```typescript
await ctx.order.cancel('12345')
```

#### modify(positionId, changes)

Changes the stop loss and/or take profit of an open position.

- Omit a field to keep it, pass `null` to remove it, pass a price to set it.
- At least one field is required.
- Prices must be rounded to the symbol's `digits`.
- Does **not** count against the orders-per-minute limit.

```typescript
const positions = await ctx.portfolio.getPositions()
for (const pos of positions) {
  if (pos.symbol !== 'EURUSD') continue
  const info = ctx.market.getSymbolInfo(pos.symbol)
  const round = (v: number) => parseFloat(v.toFixed(info.digits))
  const breakEven = round(pos.entryPrice)
  await ctx.order.modify(pos.id, { sl: breakEven })
}

await ctx.order.modify(pos.id, { tp: null })
```

#### close(positionId, volume?)

Closes an open position with a market order.

- Without `volume` the whole position is closed; with `volume` (in lots) only that part is closed.
- The close is asynchronous: the call resolves when the server accepts the order, and the position disappears from `getPositions()` once the fill is processed.
- Counts as one order for the orders-per-minute limit.
- Throws if a close is already in flight for that position or if the position is already closed.

```typescript
await ctx.order.close(pos.id)
await ctx.order.close(pos.id, 0.05)
```

### ctx.portfolio

Requires the `read-portfolio` permission. Portfolio data is pushed from the main thread every **2 seconds**, so values can lag slightly behind the market.

```typescript
interface BotPortfolioAPI {
  getPositions(): Promise<Position[]>
  getBalance(): Promise<number>
  getEquity(): Promise<number>
  getHistory(query?: HistoryQuery): Promise<Deal[]>
}
```

#### getPositions()

Returns **all** open positions of the account, including manual ones and those of other bots. Filter by `tag` to find your own. See [`Position`](#position).

```typescript
const positions = await ctx.portfolio.getPositions()
ctx.log.info(`Open positions: ${positions.length}`)

for (const pos of positions) {
  ctx.log.info(`${pos.side} ${pos.volume} ${pos.symbol} @ ${pos.entryPrice} pnl=${pos.profit.toFixed(2)} tag=${pos.tag ?? '-'}`)
}
```

#### getBalance()

Returns the account balance as `Promise<number>`.

```typescript
const balance = await ctx.portfolio.getBalance()
ctx.log.info(`Balance: $${balance.toFixed(2)}`)
```

#### getEquity()

Returns the account equity (balance plus floating profit and loss) as `Promise<number>`.

```typescript
const equity = await ctx.portfolio.getEquity()
ctx.log.info(`Equity: $${equity.toFixed(2)}`)
```

#### getHistory(query?)

Returns closed positions of the account as [`Deal`](#deal) objects, oldest first. Asynchronous: queries the server. See [`HistoryQuery`](#historyquery) for the filters (the default limit is 100 and the maximum is 200).

```typescript
const since = Date.now() - 7 * 24 * 60 * 60 * 1000
const deals = await ctx.portfolio.getHistory({ symbol: 'EURUSD', from: since })
const mine = deals.filter((d) => d.tag?.startsWith('alpha:'))
const net = mine.reduce((sum, d) => sum + d.profit + d.commission + d.swap, 0)
ctx.log.info(`Weekly net: ${net.toFixed(2)} over ${mine.length} deals`)
```

`reason` is `'bot'` for positions closed through `ctx.order.close` by any bot: the close is recorded on the server, so it survives restarts and appears in `getHistory()`. Use `tag` to attribute deals to a specific bot.

### ctx.storage

Requires the `storage` permission. In-memory key-value store for the bot's state. Reads and writes are synchronous. Data persists for the bot's lifetime and is cleared when the bot stops.

```typescript
interface BotStorageAPI {
  get<T = unknown>(key: string): T | undefined
  set<T = unknown>(key: string, value: T): void
  delete(key: string): void
}
```

Because bot code runs inside an object literal, you cannot use top-level variables. Use `ctx.storage` for all state. Storage calls are rate limited (see [Limits](#limits)).

```typescript
onInit(ctx) {
  ctx.storage.set('tradeCount', 0)
  ctx.storage.set('closes', [])
},

onCandleClose(ctx, candle) {
  const count = ctx.storage.get('tradeCount') as number
  const closes = ctx.storage.get('closes') as number[]
  closes.push(candle.close)
  ctx.storage.set('closes', closes)
},
```

### ctx.log

Writes messages to the bot output panel.

```typescript
interface BotLogAPI {
  info(message: string, ...args: unknown[]): void
  warn(message: string, ...args: unknown[]): void
  error(message: string, ...args: unknown[]): void
  debug(message: string, ...args: unknown[]): void
}
```

```typescript
ctx.log.info('Bot started')
ctx.log.warn('Low margin detected')
ctx.log.error(`Order failed: ${err}`)
ctx.log.debug(`Tick: ${tick.bid}/${tick.ask}`)
```

Logs are rate limited: 50 messages of burst, refilled at 10 per second. Messages longer than 2,000 characters are truncated. Log signals, orders and closes, but do not log on every tick.

### ctx.chart

Draws on the chart the bot is bound to. No permission is required. All methods are fire-and-forget. Methods that create an object return its `id`; pass your own `id` to replace an object in place.

- Times are in **milliseconds**, the same unit as `candle.time`.
- A bot can keep at most **200 objects** on the chart at once. Beyond that, new drawings are ignored and a warning is logged until some are removed. All bar markers added with `addMarker` count as a single object, and so do all time labels.
- Drawings are removed when the bot stops. If the chart the bot was started from is closed, chart commands are dropped silently.
- Reuse fixed ids (`'sl-' + pos.id`, `'stats'`) instead of creating new objects on every candle.

```typescript
interface BotChartAPI {
  addIndicator(config: { type: string; [key: string]: unknown }): void
  removeIndicator(type: string): void

  drawLine(config: BotChartLine): string
  updateLine(id: string, config: { price?: number; priceEnd?: number; color?: string; label?: string; timeEnd?: number }): void
  removeLine(id: string): void

  drawBox(config: BotChartBox): string
  updateBox(id: string, config: Partial<Omit<BotChartBox, 'id'>>): void
  removeBox(id: string): void

  drawMarker(config: BotChartPriceMarker): string
  removeMarker(id: string): void

  addMarker(config: BotChartBarMarker): void
  removeBarMarker?(id: string): void
  clearMarkers(): void

  drawTimeLabel?(config: BotChartTimeLabel): string
  removeTimeLabel?(id: string): void

  drawTable(config: BotChartTable): string
  removeTable(id: string): void

  drawPolyline(config: BotChartPolyline): string
  removePolyline(id: string): void

  clearAll(): void
}
```

Methods marked `?` are missing on older clients; check with `typeof ctx.chart.removeBarMarker === 'function'` before using them.

#### Indicators

`addIndicator` shows one of the **chart's built-in indicators** on the chart the bot is bound to. It does not create new calculations.

```typescript
ctx.chart.addIndicator({ type: 'rsi' })
ctx.chart.removeIndicator('rsi')
```

How it behaves (verified in the trader app):

- Only `type` is read. Other fields such as `period` are **ignored**: the indicator is added with the chart's default settings, and the trader can change them from the indicator's own settings.
- For indicators that allow several instances (marked **multiple** below), every `addIndicator` call adds **one more instance**, so calling it on each start stacks duplicates. For the others, the call **toggles**: if the indicator is already on the chart it is removed.
- `removeIndicator(type)` removes every instance of that type.
- Unknown types do nothing.
- The indicator belongs to the chart, not to the bot: it is not removed when the bot stops, and it can already be there when the bot starts. Check with your own state (`ctx.storage`) before adding, and remove what you added in `onDispose`.

| `type` | Draws | Group | Multiple |
|--------|-------|-------|----------|
| `sma`, `ema`, `wma` | over the candles | trend | yes |
| `bollinger` | over the candles | trend | yes |
| `superTrend` | over the candles | trend | yes |
| `sessions`, `parabolicSar`, `pivotPoints`, `ichimoku` | over the candles | trend | no |
| `keltner`, `donchian` | over the candles | volatility | yes |
| `vwap` | over the candles | volume | no |
| `rsi`, `macd`, `stochastic`, `adx`, `cci`, `williamsR` | own pane below | momentum | yes |
| `mfi` | own pane below | volume | yes |
| `atr` | own pane below | volatility | yes |
| `volume`, `obv`, `volumeProfile` | own pane below | volume | no |

#### Custom indicators

Bots cannot register a new indicator or plot in a separate pane. To show your own calculation, compute it in the bot and draw it **over the candles**:

- a series of values as a line: `drawPolyline` with one `{ time, price }` point per candle
- levels, channels and zones: `drawLine` and `drawBox`
- signals: `drawMarker` or `addMarker`
- a small summary of values: `drawTable`

Reuse fixed ids and `updateLine`, `updateBox` or a `drawPolyline` with the same id to refresh the drawing on each candle instead of creating new objects, and remember the limit of 200 objects per bot. Values that do not live on the price scale (an oscillator between 0 and 100, for example) cannot be plotted in their own pane; show them in a `drawTable` or compute them and add the matching built-in indicator.

#### Lines

`BotChartLine` fields:

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string?` | Reuse an id to replace the line |
| `price` | `number` | Price level |
| `color` | `string?` | CSS color |
| `label` | `string?` | Text shown on the price axis |
| `style` | `'solid' \| 'dashed' \| 'dotted'` | Line style |
| `timeStart` | `number?` | Start time (ms). Without it the line spans the whole chart |
| `timeEnd` | `number?` | End time (ms). Without it a segment extends to the right edge |
| `priceEnd` | `number?` | Price at `timeEnd`: the segment is drawn between `(timeStart, price)` and `(timeEnd, priceEnd)` |
| `width` | `number?` | Line width in pixels, 1 to 4 |
| `priceLabel` | `boolean?` | Set to `false` to hide the price-axis label (useful for many short segments) |

Without `timeStart` the line spans the whole chart and shows its level on the price axis. With `timeStart` it is a segment. A segment does not draw its `label`; put a `drawMarker` anchored `left` at its end instead (see [Markers](#markers)).

```typescript
const sl = ctx.chart.drawLine({ price: 1.0850, color: '#EF4444', label: 'SL', style: 'dashed' })
ctx.chart.updateLine(sl, { price: 1.0860 })

ctx.chart.drawLine({ id: `tp-${pos.id}`, price: tp, color: '#22C55E', label: 'TP',
  timeStart: candle.time, timeEnd: candle.time + 15 * 60 * 60 * 1000 })
ctx.chart.removeLine(`tp-${pos.id}`)
```

#### Boxes

A shaded rectangle between two prices over a time range. Use it for supply and demand zones, order blocks or session ranges. Omit `timeEnd` to extend the box 15 bars past the last candle.

`BotChartBox` fields:

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string?` | Reuse an id to replace the box |
| `timeStart` | `number` | Start time (ms) |
| `timeEnd` | `number?` | End time (ms) |
| `top`, `bottom` | `number` | Prices. `top === bottom` gives a flat level carrying its own label |
| `color` | `string?` | Fill color |
| `borderColor` | `string?` | Border color. `'rgba(0,0,0,0)'` hides the border |
| `borderWidth` | `number?` | Border width |
| `opacity` | `number?` | Fill opacity from 0 to 1, 0.15 by default |
| `label` | `string?` | Box label |
| `labelPosition` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' \| 'center' \| 'left' \| 'right'` | `left` and `right` centre the text vertically; centred and right-hand labels stay on the visible part of a box that extends past the screen |
| `labelColor`, `labelBackground` | `string?` | Label colors |
| `labelSize` | `number?` | Label font size |

```typescript
ctx.chart.drawBox({
  id: 'poi-1',
  timeStart: candle.time,
  top: candle.high,
  bottom: candle.low,
  color: '#F59E0B',
  label: 'POI',
})
ctx.chart.updateBox('poi-1', { timeEnd: Date.now() })
ctx.chart.removeBox('poi-1')
```

A zone with a flat level carrying its own label, the way supply and demand indicators mark a point of interest:

```typescript
ctx.chart.drawBox({ id: 'z1', timeStart: pivot.time, timeEnd: far, top, bottom, color: '#EF4444',
  borderColor: 'rgba(0,0,0,0)', label: 'SUPPLY', labelPosition: 'center', labelBackground: 'rgba(0,0,0,0)' })
ctx.chart.drawBox({ id: 'z1-poi', timeStart: pivot.time, timeEnd: far, top: poi, bottom: poi, color: '#6B7280',
  borderColor: '#6B7280', label: 'POI', labelPosition: 'left', labelBackground: 'rgba(0,0,0,0)', labelSize: 10 })
```

#### Markers

`drawMarker` anchors a label to an exact price and time. `addMarker` is the classic bar marker above or below a candle.

`BotChartPriceMarker` (`drawMarker`):

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string?` | Reuse an id to replace the marker |
| `time` | `number` | Time (ms) |
| `price` | `number` | Price |
| `text` | `string` | Label text |
| `color` | `string?` | Background color of the label |
| `textColor` | `string?` | Text color |
| `opacity` | `number?` | Background opacity from 0 to 1; 0 leaves plain text |
| `size` | `number?` | Size |
| `anchor` | `'top-left' \| 'left' \| 'right' \| 'center'` | Where `(time, price)` sits on the label. `left` and `right` centre it on the price, like a label at the end of a line |
| `pointer` | `boolean?` | Draws a tail from the label to the point, for `left` and `right` anchors |
| `radius` | `number?` | Corner radius |
| `shape` | `'arrowUp' \| 'arrowDown' \| 'label'` | Marker shape |

`BotChartBarMarker` (`addMarker`):

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string?` | Repeating an id replaces the marker; `removeBarMarker(id)` takes it off |
| `time` | `number` | Candle time (ms) |
| `position` | `'above' \| 'below'` | Placement relative to the candle |
| `color` | `string?` | Marker color |
| `shape` | `'circle' \| 'arrowUp' \| 'arrowDown' \| 'square' \| 'tag' \| 'xcross'` | A marker with a `label` becomes a `tag` bubble, except `xcross`, which keeps its text beside it |
| `label` | `string?` | Text |
| `size` | `number?` | Scales the shape |
| `textColor` | `string?` | Text color |

```typescript
ctx.chart.drawMarker({ time: candle.time, price: candle.close, text: 'BUY', color: '#22C55E', shape: 'arrowUp' })
ctx.chart.addMarker({ time: candle.time, position: 'below', shape: 'arrowUp', color: '#22C55E', label: 'BUY' })
ctx.chart.clearMarkers()
```

By default a price marker's label hangs below and to the right of `(time, price)`. A label that reads at the end of an SL or TP line:

```typescript
ctx.chart.drawLine({ id: 'sl', price: sl, color: '#EF4444', timeStart: candle.time, timeEnd: end, priceLabel: false })
ctx.chart.drawMarker({ id: 'sl-label', time: end, price: sl, text: '✕ SL ' + sl, color: '#EF4444',
  anchor: 'left', pointer: true, radius: 3 })
```

Give a bar marker an `id` to manage it on its own: adding another marker with the same `id` replaces it, and `removeBarMarker(id)` removes only that one, which suits a signal shown while the candle is still forming. `removeBarMarker` is missing on clients older than trader 1.10.19, so check that it is a function and fall back to signalling on candle close.

```typescript
if (typeof ctx.chart.removeBarMarker === 'function') {
  ctx.chart.addMarker({ id: 'live', time: bar.time, position: 'above', color: '#DF484C', label: 'SELL' })
  ctx.chart.removeBarMarker('live')
}
```

#### Time labels

`drawTimeLabel` writes a short text under the bar of `time`, pinned a few pixels above the bottom edge of the chart. It does not depend on price, so it stays in place while the price scale scrolls or zooms. It is meant for a row of day names or session tags under the candles. Drawing again with the same `id` replaces the label, `removeTimeLabel(id)` removes it, and all of them count as one chart object. Both are missing on clients older than trader 1.12.3.

Fields: `id?`, `time`, `text`, `color?`, `size?` (font size in pixels, 11 by default).

```typescript
if (typeof ctx.chart.drawTimeLabel === 'function') {
  ctx.chart.drawTimeLabel({ id: 'day-' + candle.time, time: candle.time, text: 'MON', color: '#787B86' })
}
```

#### Tables

A small text panel in a corner of the chart. Redraw with the same `id` to refresh it.

`BotChartTable` fields: `id?`, `position?` (`'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'`), `title?`, `bordered?` (draws a thin grid between cells) and `rows`. A cell is a string or `{ text, color?, background?, bold? }`, enough to colour a win rate or highlight a total column.

```typescript
const deals = await ctx.portfolio.getHistory({ from: Date.now() - 7 * 86_400_000 })
const wins = deals.filter((d) => d.profit > 0).length
ctx.chart.drawTable({
  id: 'stats',
  position: 'top-right',
  title: 'Weekly',
  rows: [
    ['Deals', String(deals.length)],
    ['Win rate', `${deals.length ? Math.round((wins / deals.length) * 100) : 0}%`],
    ['Net', deals.reduce((s, d) => s + d.profit + d.commission + d.swap, 0).toFixed(2)],
  ],
})
```

#### Polylines

A connected path through several `{ time, price }` points, drawn as one chart object. Use it for stepped or custom lines. Fields: `id?`, `points: { time: number; price: number }[]`, `color?`, `width?`, `style?` (`'solid' | 'dashed' | 'dotted'`). `removePolyline(id)` removes it.

```typescript
ctx.chart.drawPolyline({
  id: 'trend',
  points: [{ time: t0, price: level }, { time: t1, price: level }],
  color: '#4899AC',
  width: 2,
  style: 'solid',
})
ctx.chart.removePolyline('trend')
```

#### Cleanup

```typescript
onDispose(ctx) {
  ctx.chart.clearAll()
},
```

`clearAll()` removes every object the bot drew, including tables. The platform calls it automatically when the bot stops.

### Inputs (configurable parameters)

A bot can declare an `inputs` object next to its hooks. The platform renders it as a settings dialog, so the trader can tune parameters without touching the code. The resolved values (the trader's choices over the defaults) are available as `ctx.inputs`.

```typescript
interface BotInputDef {
  type: 'number' | 'boolean' | 'string' | 'select'
  label?: string          // text shown next to the field
  description?: string
  default: number | boolean | string
  min?: number
  max?: number
  step?: number
  options?: Array<string | number | { value: string | number; label?: string }>  // for type 'select'
  group?: string          // section title in the configuration dialog
  locked?: boolean        // shown but not editable; the default always applies
}
```

```typescript
inputs: {
  riskPct: { type: 'number', label: 'Risk per trade (%)', default: 0.5, min: 0.05, max: 5, step: 0.05, group: 'Management' },
  dryRun: { type: 'boolean', label: 'Dry-run mode (does not send orders)', default: true, group: 'Management' },
  trendTf: { type: 'select', label: 'Reference timeframe', default: '15m', options: ['5m', '15m', '30m', '1h', '4h'], group: 'Filter' },
},

onInit(ctx) {
  const p = ctx.inputs as any
  ctx.storage.set('cfg', { symbol: ctx.run.symbol, riskPct: p.riskPct, dryRun: p.dryRun, trendTf: p.trendTf })
},
```

`inputs` is a property of the object literal, not a declaration, so it is allowed at the top level of the bot body. Copy the values you need into `ctx.storage` in `onInit`.

## Types

All times are in **milliseconds since epoch**.

### Tick

```typescript
interface Tick {
  time: number  // Milliseconds since epoch
  bid: number   // Bid price
  ask: number   // Ask price
}
```

### Candle

```typescript
interface Candle {
  time: number    // Milliseconds since epoch
  open: number
  high: number
  low: number
  close: number
  volume: number
}
```

### SymbolInfo

```typescript
interface SymbolInfo {
  name: string           // Symbol display name
  digits: number         // Decimal precision (5 for EURUSD, 2 for XAUUSD)
  point: number          // Smallest price increment by digits: 10^-digits
  tickSize: number       // Minimum price step accepted by the broker (usually equals point)
  contractSize: number   // Units per 1.0 lot (100000 for forex majors, 100 for gold)
  volumeMin: number      // Minimum lot size
  volumeMax: number      // Maximum lot size
  volumeStep: number     // Lot increment
  stopsLevel: number     // Minimum SL/TP distance from price, in points (0 = not enforced)
  tickValue: number      // Value of one tickSize move for 1.0 lot, in quote currency
  baseCurrency?: string  // 'EUR' in EURUSD
  quoteCurrency?: string // 'USD' in EURUSD
}
```

`tickValue` is `contractSize * tickSize` expressed in the quote currency. When the quote currency differs from the account currency (EURGBP on a USD account), convert it with the current GBPUSD rate before using it for risk sizing.

`stopsLevel` is currently always `0`: the platform does not enforce a minimum stop distance. It exists so bots written today keep working if a broker-side limit is added later.

### OrderRequest

```typescript
interface OrderRequest {
  symbol: string                     // Symbol code (e.g. 'EURUSD', 'XAUUSD')
  type: 'market' | 'limit' | 'stop' // Order type
  side: 'buy' | 'sell'              // Trade direction
  volume: number                     // Lot size (e.g. 0.01)
  price?: number                     // Required for 'limit' and 'stop' orders
  sl?: number                        // Stop loss price (optional)
  tp?: number                        // Take profit price (optional)
  tag?: string                       // Free text stored on the position, max 64 chars (optional)
}
```

### PositionModifyRequest

```typescript
interface PositionModifyRequest {
  sl?: number | null  // omit = keep, null = remove, number = set
  tp?: number | null
}
```

### Position

```typescript
interface Position {
  id: string           // Position ID (use it for order.modify / order.close)
  symbol: string       // Symbol code
  volume: number       // Lot size
  side: 'buy' | 'sell' // Direction
  entryPrice: number   // Average entry price
  sl?: number          // Current stop loss, undefined if none
  tp?: number          // Current take profit, undefined if none
  openTime: number     // Open timestamp in ms
  profit: number       // Floating P/L in account currency (refreshed every 2s)
  tag?: string         // Tag sent with the opening order, undefined for manual positions
}
```

### Deal

```typescript
type DealReason = 'tp' | 'sl' | 'trailing' | 'manual' | 'bot' | 'stopout' | 'admin' | 'unknown'

interface Deal {
  positionId: string
  symbol: string
  side: 'buy' | 'sell'
  volume: number       // Lots
  entryPrice: number
  closePrice: number
  openTime: number     // ms
  closeTime: number    // ms
  profit: number       // Realized P/L in account currency, before commission and swap
  commission: number
  swap: number
  reason: DealReason
  tag?: string         // Tag sent with the opening order
}
```

Net result of a deal: `profit + commission + swap`.

### HistoryQuery

```typescript
interface HistoryQuery {
  symbol?: string  // Symbol code filter
  from?: number    // Close time >= from (ms)
  to?: number      // Close time <= to (ms)
  limit?: number   // Max deals, default 100, max 200
}
```

### Timeframes

`'1m'`, `'2m'`, `'3m'`, `'5m'`, `'10m'`, `'15m'`, `'30m'`, `'1h'`, `'2h'`, `'4h'`, `'1d'`.

## Permissions

Bots declare the permissions they need when they are created. A call made without its permission is rejected silently, so a missing permission shows up as a bot that does nothing rather than as an error.

```typescript
type BotPermission = 'trade' | 'read-market' | 'read-portfolio' | 'storage'
```

| Permission | Required for |
|-----------|--------------|
| `trade` | `ctx.order` (`send`, `cancel`, `modify`, `close`) |
| `read-market` | `ctx.market.getCandles()`, `ctx.market.getLastTick()`, `ctx.market.getSymbolInfo()` |
| `read-portfolio` | `ctx.portfolio.getPositions()`, `getBalance()`, `getEquity()`, `getHistory()`, and the `onPositionClosed` hook |
| `storage` | `ctx.storage.get()`, `ctx.storage.set()`, `ctx.storage.delete()` |

`ctx.log`, `ctx.chart`, `ctx.run` and `ctx.inputs` need no permission.

A typical trading bot needs all four permissions: `trade`, `read-market`, `read-portfolio`, `storage`.

## Rules and restrictions

### Code format

The code you write is the **body** of an object literal, `defineBot({...})`, that the platform wraps around it. Therefore:

**Do**

- Write only hook methods in shorthand syntax, each ending with a comma: `onInit(ctx) { ... },`
- Mark hooks that use `await` as `async`.
- Keep all state in `ctx.storage`. Save the configuration (symbol, lot size, periods, distances) in `onInit`.
- Declare helper functions inside the hook that uses them.
- Use an optional `inputs: { ... },` property for trader-tunable parameters.

**Don't**

- Use `defineBot`, `export`, `import` or `require`, or a `return` at the end of the body.
- Declare top-level `const`, `let`, `var`, `function` or `class`.
- Forget the trailing comma after a hook.

Correct:

```typescript
onInit(ctx) {
  ctx.storage.set('count', 0)
  ctx.storage.set('active', false)
},

async onCandleClose(ctx, candle) {
  const count = (ctx.storage.get('count') as number) + 1
  ctx.storage.set('count', count)
  // ...
},
```

Incorrect:

```typescript
// ERROR: top-level const is not valid inside an object literal
const count = 0

onInit(ctx) {
  // ...
},
```

### Time units

Every time value is in **milliseconds**: `candle.time`, `tick.time`, `deal.closeTime`, `position.openTime`, and the `timeStart`, `timeEnd` and `time` fields of chart objects. One day is `86_400_000` ms.

### Prices, volumes and risk

- Express price distances in `info.point`. Never deduce the pip size from `digits`.
- Round `sl` and `tp` with `parseFloat(v.toFixed(info.digits))`.
- Volume is in lots. Size by risk: `lots = riskUSD / (slTicks * tickValue)`, rounded down to `volumeStep` and clamped to `volumeMin`/`volumeMax`.
- If there are fewer candles than the indicator period, return without trading.
- Orders are blocked while `ctx.run.warmup` is `true`.

### Good practice

- Send a `tag` with every order: a short bot prefix plus a unique suffix (for example `'alpha:' + candle.time`, up to 64 characters). Filter `getPositions()` and deals by that prefix so the bot only manages its own positions.
- Wrap every `order.send`, `modify` and `close` in `try/catch` and log the error.
- Use one strategy per bot. Log signals, orders and closes with `ctx.log.info`.
- Reuse fixed ids for drawings instead of creating new ones every candle.
- Keep comments short and only where the logic is not obvious.

### Limits

**Trading guards**

| Rule | Default | Description |
|------|---------|-------------|
| Max orders per minute | 10 | Maximum order submissions per minute (`modify` does not count) |
| Max open positions | 20 | Maximum concurrent open positions |
| Daily loss limit | 5% | Maximum daily loss as a percentage of starting equity |
| Cooldown on breach | 60 s | Trading is paused after any limit is hit |

**Message rate limits**

| Category | Burst | Refill rate | Max length |
|----------|-------|-------------|------------|
| Logs | 50 | 10 per second | 2,000 characters |
| Storage | 20 | 5 per second | - |

**Other limits**

| Item | Limit |
|------|-------|
| Hook timeout | 10 s for `onInit`, 5 s for every other hook |
| `getCandles` count | 10,000 (larger values are capped) |
| `getHistory` limit | 200 deals (default 100) |
| Order `tag` | 64 characters |
| Chart objects per bot | 200 (all bar markers count as one, all time labels count as one) |
| Restarts after a crash | 3 per 60 seconds, then the kill switch activates |

## Complete examples

### Minimal bot

```typescript
onInit(ctx) {
  ctx.log.info('Hello World')
},

onDispose(ctx) {
  ctx.log.info('Goodbye')
},
```

### Tick logger

```typescript
onInit(ctx) {
  ctx.storage.set('count', 0)
},

onTick(ctx, tick) {
  const count = (ctx.storage.get('count') as number) + 1
  ctx.storage.set('count', count)

  if (count % 100 === 0) {
    ctx.log.info(`Tick #${count}: Bid=${tick.bid} Ask=${tick.ask}`)
  }
},
```

### SMA crossover with risk-based sizing

Buys when the 10-period average is above the 20-period average, sizes the lot from the account balance, tags the order, and draws the trade and a weekly statistics table. Permissions: `trade`, `read-market`, `read-portfolio`, `storage`.

```typescript
onInit(ctx) {
  ctx.storage.set('symbol', 'EURUSD')
  ctx.storage.set('riskPct', 1)
  const info = ctx.market.getSymbolInfo('EURUSD')
  ctx.storage.set('info', info)
  ctx.log.info('Bot started')
},

async onCandleClose(ctx, candle) {
  const symbol = ctx.storage.get('symbol') as string
  const info = ctx.storage.get('info') as SymbolInfo
  const round = (v: number) => parseFloat(v.toFixed(info.digits))

  const candles = await ctx.market.getCandles(symbol, '15m', 60)
  if (candles.length < 20) return
  const sma = (n: number) => candles.slice(-n).reduce((a, c) => a + c.close, 0) / n

  const mine = (await ctx.portfolio.getPositions()).filter((p) => p.tag?.startsWith('alpha:'))
  if (mine.length > 0) return

  if (sma(10) > sma(20)) {
    const sl = round(candle.close - 150 * info.point)
    const tp = round(candle.close + 200 * info.point)
    const balance = await ctx.portfolio.getBalance()
    const riskUsd = balance * ((ctx.storage.get('riskPct') as number) / 100)
    const slTicks = Math.abs(candle.close - sl) / info.tickSize
    const raw = riskUsd / (slTicks * info.tickValue)
    const volume = Math.max(info.volumeMin, Math.min(info.volumeMax, Math.floor(raw / info.volumeStep) * info.volumeStep))
    try {
      await ctx.order.send({ symbol, type: 'market', side: 'buy', volume, sl, tp, tag: 'alpha:' + candle.time })
      ctx.chart.drawMarker({ time: candle.time, price: candle.close, text: 'BUY', color: '#22C55E', shape: 'arrowUp' })
      ctx.chart.drawLine({ id: 'sl', price: sl, color: '#EF4444', label: 'SL', style: 'dashed', timeStart: candle.time })
      ctx.log.info('BUY ' + volume + ' @ ' + candle.close)
    } catch (err) {
      ctx.log.error('Order failed: ' + err)
    }
  }
},

async onPositionClosed(ctx, deal) {
  if (!deal.tag?.startsWith('alpha:')) return
  ctx.chart.removeLine('sl')
  const deals = (await ctx.portfolio.getHistory({ from: Date.now() - 7 * 86400000 })).filter((d) => d.tag?.startsWith('alpha:'))
  const wins = deals.filter((d) => d.profit > 0).length
  ctx.chart.drawTable({
    id: 'stats', position: 'top-right', title: 'Alpha 7d',
    rows: [['Deals', String(deals.length)], ['Win', deals.length ? Math.round((wins / deals.length) * 100) + '%' : '-']],
  })
  ctx.log.info('Closed by ' + deal.reason + ': ' + deal.profit.toFixed(2))
},

onDispose(ctx) {
  ctx.chart.clearAll()
  ctx.log.info('Bot stopped')
},
```

### Portfolio monitor

Logs balance, equity and open positions on every timer tick. Permissions: `read-portfolio`, `storage`.

```typescript
onInit(ctx) {
  ctx.storage.set('startEquity', 0)
  ctx.storage.set('initialized', false)
},

async onTimer(ctx) {
  const balance = await ctx.portfolio.getBalance()
  const equity = await ctx.portfolio.getEquity()
  const positions = await ctx.portfolio.getPositions()

  let startEquity = ctx.storage.get('startEquity') as number
  if (!ctx.storage.get('initialized')) {
    startEquity = equity
    ctx.storage.set('startEquity', startEquity)
    ctx.storage.set('initialized', true)
  }

  const pnlPercent = startEquity > 0 ? ((equity - startEquity) / startEquity * 100) : 0

  ctx.log.info(`Balance: $${balance.toFixed(2)} | Equity: $${equity.toFixed(2)} | P/L: ${pnlPercent.toFixed(2)}%`)
  ctx.log.info(`Open positions: ${positions.length}`)

  for (const pos of positions) {
    ctx.log.debug(`  ${pos.side.toUpperCase()} ${pos.volume} ${pos.symbol} @ ${pos.entryPrice}`)
  }
},
```

### Chart demo: support, resistance and markers

Draws dynamic support and resistance lines, adds RSI, and marks strong bullish and bearish candles. Permissions: `read-market`, `storage`.

```typescript
onInit(ctx) {
  ctx.log.info('Chart Demo started')

  ctx.storage.set('lookback', 20)
  ctx.storage.set('closes', [])
  ctx.storage.set('highestHigh', 0)
  ctx.storage.set('lowestLow', Infinity)

  // Add RSI indicator to the chart
  ctx.chart.addIndicator({ type: 'rsi' })
},

onCandleClose(ctx, candle) {
  const LOOKBACK = ctx.storage.get('lookback') as number
  const closes = ctx.storage.get('closes') as number[]
  let highestHigh = ctx.storage.get('highestHigh') as number
  let lowestLow = ctx.storage.get('lowestLow') as number

  closes.push(candle.close)

  if (closes.length < LOOKBACK) {
    ctx.log.debug(`Collecting data: ${closes.length}/${LOOKBACK}`)
    ctx.storage.set('closes', closes)
    return
  }

  // Keep only the last LOOKBACK candles
  const recent = closes.slice(-LOOKBACK)
  ctx.storage.set('closes', recent)

  const newHigh = Math.max(...recent)
  const newLow = Math.min(...recent)

  // Update resistance line when high changes
  if (newHigh !== highestHigh) {
    highestHigh = newHigh
    ctx.storage.set('highestHigh', highestHigh)
    ctx.chart.drawLine({
      id: 'resistance',
      price: highestHigh,
      color: '#EF4444',
      label: 'Resistance',
      style: 'dashed',
    })
    ctx.log.info(`Resistance updated: ${highestHigh.toFixed(5)}`)
  }

  // Update support line when low changes
  if (newLow !== lowestLow) {
    lowestLow = newLow
    ctx.storage.set('lowestLow', lowestLow)
    ctx.chart.drawLine({
      id: 'support',
      price: lowestLow,
      color: '#22C55E',
      label: 'Support',
      style: 'dashed',
    })
    ctx.log.info(`Support updated: ${lowestLow.toFixed(5)}`)
  }

  // Mark strong bullish candles (close near high)
  const range = candle.high - candle.low
  if (range > 0 && (candle.close - candle.low) / range > 0.8) {
    ctx.chart.addMarker({
      time: candle.time,
      position: 'below',
      color: '#22C55E',
      shape: 'arrowUp',
      label: 'Bull',
    })
  }

  // Mark strong bearish candles (close near low)
  if (range > 0 && (candle.high - candle.close) / range > 0.8) {
    ctx.chart.addMarker({
      time: candle.time,
      position: 'above',
      color: '#EF4444',
      shape: 'arrowDown',
      label: 'Bear',
    })
  }
},

onDispose(ctx) {
  ctx.log.info('Chart Demo stopped')
  // Bot drawings are cleared automatically on stop
},
```

## Testing your bot

1. **Create the bot** in the bot editor. Give it a name and a version and select the permissions it needs.
2. **Compile.** Press Compile and open the Compilation tab. A green OK badge means the TypeScript compiled. Syntax errors (for example a missing closing brace) are listed with line numbers and the bot status changes to Error.
3. **Start.** The status goes from Idle to Compiling to Running. Open the Logs tab: the messages from `onInit` appear there in real time, colour-coded by level (`info`, `warn`, `error`, `debug`).
4. **Stop.** The status changes to Stopped and the `onDispose` log message appears. Starting again creates a new worker, so logs and storage start from scratch.
5. **Export and import.** A bot can be exported as a `.pip` file (a ZIP containing `manifest.json` with id, name, version and permissions, and `bot.ts` with the source) and imported again from the bot list.

Tips:

- Start with a log-only bot to check the pipeline:

  ```typescript
  onInit(ctx) {
    ctx.log.info('Bot started!')
    ctx.log.warn('This is a warning log')
    ctx.log.debug('Debug info: everything is working')
  },

  onDispose(ctx) {
    ctx.log.info('Bot stopped. Goodbye!')
  },
  ```

- Expose a `dryRun` input (default `true`) and return before `ctx.order.send` while it is on, so you can watch signals and drawings before risking capital.
- Test with the minimum lot (`info.volumeMin`) first.
- Each running bot has its own worker; several bots can run at the same time without interfering with each other.
- Remember that orders are blocked during warm-up (`ctx.run.warmup`).

## Troubleshooting

| Symptom | Likely cause and fix |
|---------|----------------------|
| The bot runs but nothing happens when it calls an API | The permission is missing. Calls without their permission are rejected silently. Check [Permissions](#permissions) and recreate the bot with the right ones. |
| Compilation error about a declaration | A top-level `const`, `let`, `var`, `function` or `class` is not valid in an object literal. Move it inside the hook that uses it, or keep the value in `ctx.storage`. |
| Compilation error between two hooks | A trailing comma is missing after a hook. |
| `await` is a syntax error | The hook is not marked `async`. |
| `onPositionClosed` never fires | The `read-portfolio` permission is missing, or the position was only partially closed (partial closes are not delivered). |
| `onPositionClosed` fires for positions that are not the bot's | It fires for every position of the account. Filter by `deal.tag`. |
| The bot manages positions it did not open | `getPositions()` returns all account positions. Send a `tag` with each order and filter by its prefix. |
| Order rejected with a price error | `sl`/`tp` are not rounded to `info.digits`. Use `parseFloat(v.toFixed(info.digits))`. |
| `limit` or `stop` order fails | `price` is required for those order types. |
| Orders do not go out right after start | `ctx.run.warmup` is `true` during the history replay; orders are blocked until it ends. |
| Order or close fails after many trades | A trading guard was hit (10 orders per minute, 20 open positions, 5% daily loss). Trading pauses for 60 seconds. |
| `close` throws | A close is already in flight for that position, or it is already closed. |
| The bot restarts repeatedly and then stops | A hook exceeded its timeout (10 s for `onInit`, 5 s otherwise) or threw. After 3 restarts in 60 seconds the kill switch activates; fix the code and clear it in the bot manager. |
| Ticks seem to be skipped | `onTick` keeps only the latest tick when the bot is busy. Do heavy work in `onCandleClose` instead. |
| Candle indicators differ from the chart's forming candle | `getCandles` returns closed candles only. |
| `getSymbolInfo` returns `undefined` | The symbol is not tradeable on the account, or the call was made before `onInit`. |
| Balance or profit looks stale | Portfolio data refreshes every 2 seconds. |
| Drawings do not appear | The chart object limit (200) was reached, the chart the bot was started from is closed, or the chart is showing another symbol. |
| `removeBarMarker` or `drawTimeLabel` is not a function | The client is older than the version that added it. Check `typeof ctx.chart.removeBarMarker === 'function'` first. |
| Log messages are missing | Logging is rate limited (50 burst, 10 per second) and messages over 2,000 characters are truncated. |
