# Pipsend Bot API reference

Dense reference for the Pipsend Bot SDK. Source format: the bot code is the **body** of `defineBot({...})`: hook methods in shorthand syntax, each followed by a comma; no top-level `const`/`let`/`var`/`function`/`class`; no `import`/`export`/`return`. State only through `ctx.storage`. All times in milliseconds.

## Contents

- Runtime model and limits
- Hooks
- BotContext
- ctx.run, ctx.inputs
- ctx.market, ctx.order, ctx.portfolio, ctx.storage, ctx.log
- ctx.chart
- Types
- Permissions
- Run configuration
- Patterns

## Runtime model and limits

- Each bot runs in its own Web Worker. TypeScript is compiled before start (target ES2020). API calls are async messages to the main thread, which checks permissions and trade guards.
- Lifecycle: compile, create worker, send code, `ready`, set up subscriptions, `onInit`, running, `onDispose` on stop (worker terminated after a 3 s timeout).
- Hooks run sequentially by priority (lower first): `onDispose` 0, `onInit` 1, `onPositionClosed` 2, `onCandleClose` 3, `onTimer` 4, `onTick` 5.
- Hook timeouts: `onInit` 10,000 ms; every other hook 5,000 ms. On timeout the worker is killed and restarted.
- Queues: `onTick` keep latest (capacity 1); `onCandleClose` FIFO 10 (drops oldest); `onTimer` FIFO 5 (drops oldest); `onInit` and `onDispose` always queued.
- Warning logged when no tick arrives for 10 s.
- Trade guards: 10 orders/minute, 20 open positions, 5% daily loss (of starting equity), 60 s cooldown after any breach. `modify` does not count toward orders/minute; `close` does.
- Message rate limits: logs burst 50, refill 10/s, max 2,000 chars (truncated); storage burst 20, refill 5/s.
- Crash recovery: automatic restart with backoff 1 s, 2 s, 4 s ... capped at 30 s; max 3 restarts per 60 s; beyond that the kill switch activates and the bot cannot start until the user clears it in the bot manager (the user can also trigger it manually).
- Right after start the runtime replays recent closed candles (about 600) through `onCandleClose`; `ctx.run.warmup` is `true`, orders are blocked, drawings are kept.

## Hooks

All optional, all may be `async`.

| Hook | Fires | Payload |
|------|-------|---------|
| `onInit(ctx)` | Once at start, after market data is initialized | - |
| `onTick(ctx, tick)` | Every tick of the configured symbol | `Tick` |
| `onCandleClose(ctx, candle)` | Candle closes on the configured symbol and timeframe | `Candle` |
| `onTimer(ctx)` | At the configured interval (`timerIntervalMs`) | - |
| `onPositionClosed(ctx, deal)` | ANY position of the account is fully closed (tp, sl, trailing, manual, bot, stop out). Requires `read-portfolio`. Not delivered on partial closes. Filter by `deal.tag`. `deal.reason` is `'bot'` when closed through `ctx.order.close` | `Deal` |
| `onDispose(ctx)` | Bot stopped | - |

```typescript
interface BotDefinition {
  id: string; name: string; version: string
  permissions: BotPermission[]
  inputs?: BotInputSchema
  onInit?(ctx: BotContext): void | Promise<void>
  onTick?(ctx: BotContext, tick: Tick): void | Promise<void>
  onCandleClose?(ctx: BotContext, candle: Candle): void | Promise<void>
  onTimer?(ctx: BotContext): void | Promise<void>
  onPositionClosed?(ctx: BotContext, deal: Deal): void | Promise<void>
  onDispose?(ctx: BotContext): void | Promise<void>
}
```

In the editor, id, name, version and permissions are set by the platform (toolbar / creation); you write only `inputs` (optional) and the hooks.

## BotContext

```typescript
interface BotContext {
  run: BotRunInfo
  inputs: BotInputValues      // Record<string, number | boolean | string>
  market: BotMarketAPI
  order: BotOrderAPI
  portfolio: BotPortfolioAPI
  storage: BotStorageAPI
  log: BotLogAPI
  chart: BotChartAPI
}
```

## ctx.run and ctx.inputs

```typescript
interface BotRunInfo {
  symbol: string
  timeframe: string
  warmup: boolean            // true while recent history is replayed after start; orders blocked
  chartType?: string         // 'candlestick', 'heikin-ashi', 'line', ... (missing on clients older than trader 1.10.22)
  platform?: 'web' | 'mobile' // 'web' = browser and desktop app, 'mobile' = phone app (missing on clients older than trader 1.12.3; treat missing as 'mobile')
}
```

`getCandles` and `onCandleClose` always carry real candles; a bot that wants Heikin Ashi converts them itself.

Inputs: optional `inputs` property in the bot body; the platform renders a settings dialog.

```typescript
type BotInputType = 'number' | 'boolean' | 'string' | 'select'
interface BotInputDef {
  type: BotInputType
  label?: string; description?: string
  default: number | boolean | string
  min?: number; max?: number; step?: number
  options?: Array<string | number | { value: string | number; label?: string }>   // for 'select'
  group?: string       // section title in the dialog
  locked?: boolean     // shown, not editable; default always applies
}
type BotInputSchema = Record<string, BotInputDef>
```

## ctx.market (permission read-market)

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

- `getSymbolInfo`: sync, available after `onInit`; `undefined` when the symbol is not tradeable on the account.
- `getLastTick`: sync, most recent tick from the local cache.
- `getCandles`: async. Timeframes `1m 2m 3m 5m 10m 15m 30m 1h 2h 4h 1d`. `count` max 10,000 (larger capped). Closed candles only (never the forming one), oldest first. Sub-day buckets aligned to midnight UTC (`4h` starts 00:00, 04:00, 08:00 UTC).

## ctx.order (permission trade)

```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>
}
```

- `send`: throws on failure. `price` required for `limit` and `stop`. `sl`/`tp` must be rounded to the symbol's `digits` (server rejects extra decimals). `tag` max 64 chars, stored on the position.
- `cancel`: cancels a pending limit or stop order.
- `modify`: omit a field to keep it, `null` to remove it, a price to set it. At least one field required. Not counted in orders/minute.
- `close`: no `volume` = full close, `volume` in lots = partial close. Asynchronous: resolves when the server accepts the order; the position leaves `getPositions()` when the fill is processed. Counts as one order. Throws if a close is already in flight for that position or it is already closed.

## ctx.portfolio (permission read-portfolio)

Data is pushed every 2 seconds.

```typescript
interface BotPortfolioAPI {
  getPositions(): Promise<Position[]>     // ALL open positions of the account (manual and other bots included)
  getBalance(): Promise<number>
  getEquity(): Promise<number>            // balance + floating P/L
  getHistory(query?: HistoryQuery): Promise<Deal[]>   // closed positions as deals, oldest first
}
```

`getHistory` queries the server. `reason` is `'bot'` for positions closed through `ctx.order.close` by any bot (recorded on the server, survives restarts). Use `tag` to attribute deals to a specific bot. Net result of a deal: `profit + commission + swap`.

## ctx.storage (permission storage)

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

Synchronous, in memory, lives as long as the bot runs (cleared on stop). Rate limited.

## ctx.log

```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
}
```

## ctx.chart (no permission)

Draws on the chart the bot is bound to. Fire-and-forget. Methods creating an object return its `id`; passing an existing `id` replaces the object. Max **200 objects** per bot (beyond that new drawings are ignored and a warning is logged); all `addMarker` bar markers count as one object, and all time labels count as one. Drawings are removed when the bot stops; if the chart is closed, commands are dropped silently. Switching the chart to another symbol hides the drawings until it comes back.

```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       // missing on clients older than trader 1.10.19
  clearMarkers(): void
  drawTimeLabel?(config: BotChartTimeLabel): string   // missing on clients older than trader 1.12.3
  removeTimeLabel?(id: string): void                  // missing on clients older than trader 1.12.3
  drawTable(config: BotChartTable): string
  removeTable(id: string): void
  drawPolyline(config: BotChartPolyline): string
  removePolyline(id: string): void
  clearAll(): void                          // removes everything the bot drew, tables included; also called automatically on stop
}
```

Check optional methods with `typeof ctx.chart.removeBarMarker === 'function'` before use.

```typescript
type BotChartLineStyle = 'solid' | 'dashed' | 'dotted'

interface BotChartLine {
  id?: string; price: number; color?: string; label?: string
  style?: BotChartLineStyle
  timeStart?: number   // none = infinite line with price-axis label; set = segment
  timeEnd?: number     // none = extends to the right edge
  priceEnd?: number    // price at timeEnd (sloped segment)
  width?: number       // pixels, 1 to 4
  priceLabel?: boolean // false hides the price-axis label
}
// A segment does not draw its `label`: put a drawMarker anchored 'left' at its end.

type BotChartBoxLabelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center' | 'left' | 'right'
interface BotChartBox {
  id?: string; timeStart: number; timeEnd?: number   // no timeEnd = extends 15 bars past the last candle
  top: number; bottom: number                        // top === bottom = flat level with its own label
  color?: string; borderColor?: string               // 'rgba(0,0,0,0)' hides the border
  borderWidth?: number
  opacity?: number                                   // fill 0..1, default 0.15
  label?: string; labelPosition?: BotChartBoxLabelPosition
  labelColor?: string; labelBackground?: string; labelSize?: number
}

interface BotChartPriceMarker {            // drawMarker: label anchored to price and time
  id?: string; time: number; price: number; text: string
  color?: string        // label background
  textColor?: string
  opacity?: number      // background opacity 0..1; 0 = plain text
  size?: number
  anchor?: 'top-left' | 'left' | 'right' | 'center'   // default: label hangs below-right of the point; left/right centre it on the price
  pointer?: boolean     // tail to the point, for left/right anchors
  radius?: number
  shape?: 'arrowUp' | 'arrowDown' | 'label'
}

interface BotChartBarMarker {              // addMarker: marker on a candle
  id?: string           // same id replaces; removeBarMarker(id) removes only this one
  time: number
  position?: 'above' | 'below'
  color?: string
  shape?: 'circle' | 'arrowUp' | 'arrowDown' | 'square' | 'tag' | 'xcross'   // with a label it becomes a 'tag' bubble, except xcross
  label?: string; size?: number; textColor?: string
}

interface BotChartTimeLabel { id?: string; time: number; text: string; color?: string; size?: number }  // size px, default 11; pinned to the bottom edge, independent of price

interface BotChartPoint { time: number; price: number }
interface BotChartPolyline { id?: string; points: BotChartPoint[]; color?: string; width?: number; style?: BotChartLineStyle }

interface BotChartTableCellStyle { text: string; color?: string; background?: string; bold?: boolean }
type BotChartTableCell = string | BotChartTableCellStyle
interface BotChartTable {
  id?: string
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
  title?: string
  bordered?: boolean     // thin grid between cells
  rows: BotChartTableCell[][]
}
```

Examples:

```typescript
ctx.chart.addIndicator({ type: 'sma' })  // only `type` is read; period and other options are ignored
ctx.chart.drawLine({ id: 'sl', price: sl, color: '#EF4444', label: 'SL', style: 'dashed' })
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 })
ctx.chart.drawBox({ id: 'poi-1', timeStart: candle.time, top: candle.high, bottom: candle.low, color: '#F59E0B', label: 'POI' })
ctx.chart.addMarker({ time: candle.time, position: 'below', shape: 'arrowUp', color: '#22C55E', label: 'BUY' })
ctx.chart.drawTable({ id: 'stats', position: 'top-right', title: 'Weekly', rows: [['Deals', '12'], ['Win rate', '58%']] })
ctx.chart.drawPolyline({ id: 'trend', points: [{ time: t0, price: level }, { time: t1, price: level }], color: '#4899AC', width: 2 })
```

## Types

```typescript
interface Tick { time: number; bid: number; ask: number }
interface Candle { time: number; open: number; high: number; low: number; close: number; volume: number }

interface SymbolInfo {
  name: string
  digits: number          // decimal precision (EURUSD 5, XAUUSD 2)
  point: number           // 10^-digits
  tickSize: number        // minimum accepted price step (usually = point)
  contractSize: number    // units per 1.0 lot (100000 forex majors, 100 gold)
  volumeMin: number; volumeMax: number; volumeStep: number
  stopsLevel: number      // minimum SL/TP distance in points; currently always 0 (not enforced)
  tickValue: number       // value of one tickSize move for 1.0 lot, in quote currency (= contractSize * tickSize)
  baseCurrency?: string; quoteCurrency?: string
}
// When quoteCurrency differs from the account currency, convert tickValue before risk sizing.

interface OrderRequest {
  symbol: string
  type: 'market' | 'limit' | 'stop'
  side: 'buy' | 'sell'
  volume: number          // lots
  price?: number          // required for limit and stop
  sl?: number; tp?: number
  tag?: string            // max 64 chars
}

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

interface Position {
  id: string; symbol: string; volume: number; side: 'buy' | 'sell'
  entryPrice: number
  sl?: number; tp?: number
  openTime: number        // ms
  profit: number          // floating P/L, account currency, refreshed every 2 s
  tag?: string            // undefined for manual positions
}

type DealReason = 'tp' | 'sl' | 'trailing' | 'manual' | 'bot' | 'stopout' | 'admin' | 'unknown'
interface Deal {
  positionId: string; symbol: string; side: 'buy' | 'sell'
  volume: number; entryPrice: number; closePrice: number
  openTime: number; closeTime: number    // ms
  profit: number          // realized, before commission and swap
  commission: number; swap: number
  reason: DealReason
  tag?: string
}

interface HistoryQuery {
  symbol?: string
  from?: number           // closeTime >= from (ms)
  to?: number             // closeTime <= to (ms)
  limit?: number          // default 100, max 200
}
```

## Permissions

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

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

A call without the permission is rejected silently. `log`, `chart`, `run`, `inputs` need none.

## Run configuration

```typescript
interface BotRunConfig {
  symbol: string
  timeframe: string
  chartType?: string
  timerIntervalMs?: number
}
```

Determines the symbol of `onTick`, the symbol and timeframe of `onCandleClose`, and the `onTimer` interval. A chart-bound bot follows the chart timeframe (restart with the same inputs, warm-up included). Switching to or from Heikin Ashi restarts the bot; other chart type changes do not.

## Patterns

Price and risk:

```typescript
const round = (v: number) => parseFloat(v.toFixed(info.digits))
const sl = round(entry - 150 * info.point)
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))
```

Own positions by tag:

```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:'))
```

Weekly statistics:

```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)
```

Break-even and removing a take profit:

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

Close all or part:

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

Optional chart API fallback:

```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')
}
```

### Indicators on the chart (important)

- `addIndicator({ type })` shows a built-in chart indicator with its default settings. Options such as `period` are ignored.
- Valid types: `sma ema wma bollinger superTrend keltner donchian` (several instances allowed), `rsi macd stochastic adx cci williamsR mfi atr` (own pane, several instances allowed), `sessions parabolicSar pivotPoints ichimoku vwap volume obv volumeProfile` (one instance only).
- When several instances are allowed, each call adds another one; otherwise the call toggles (an indicator already on the chart is removed). `removeIndicator(type)` removes all of that type.
- The indicator stays after the bot stops. Remove what the bot added in `onDispose`.
- Bots cannot create custom indicators or plot in a separate pane. Compute the values in the bot and draw them over the candles with `drawPolyline`, `drawLine`, `drawBox`, `drawMarker` or `drawTable`.
