---
name: pipsend-bots
description: Writes, reviews and debugs Pipsend trading bots built on the Pipsend Bot SDK (defineBot body, lifecycle hooks, ctx.market, ctx.order, ctx.portfolio, ctx.storage, ctx.chart). Use when asked to write, review, fix or explain a Pipsend bot, or when code mentions bot-sdk, defineBot, onCandleClose, ctx.order.send or ctx.storage.
---

# Pipsend bots

A Pipsend bot is TypeScript made of lifecycle hooks. The platform compiles it, runs it in an isolated Web Worker and calls the hooks. When asked for a bot, return **only the code**, no explanation, unless the user asks for one.

## Output format (hard rules)

- The code is the **body** of an object literal `defineBot({...})` that the platform wraps. Write only hook methods in **shorthand syntax**, each ending with a comma. An optional `inputs: {...},` property is also allowed.
- No `defineBot`, no `export`, no `import`/`require`, no `return` at the end.
- **Forbidden at top level:** `const`, `let`, `var`, `function`, `class`. Declare helpers inside the hook that uses them.
- Persistent state only with `ctx.storage.get/set/delete`. Save the configuration (symbol, lot, periods, distances) in `onInit` with `ctx.storage.set`.
- Mark hooks that use `await` as `async`.
- All times are **milliseconds** (`candle.time`, `tick.time`, `deal.closeTime`, `timeStart`, ...).
- Short comments, only where the logic is not obvious.

## Hooks (all optional, run sequentially; timeout 10 s `onInit`, 5 s the rest)

| Hook | Fires |
|------|-------|
| `onInit(ctx)` | Once at start |
| `onTick(ctx, tick)` | Every tick of the symbol; if the bot is slow, old ticks are dropped (keep latest) |
| `onCandleClose(ctx, candle)` | A candle of the configured symbol and timeframe closed |
| `onTimer(ctx)` | Every configured interval |
| `onPositionClosed(ctx, deal)` | ANY position of the account fully closed (needs `read-portfolio`; not emitted on partial closes) |
| `onDispose(ctx)` | Bot stopped |

## Types

```
Tick:       {time, bid, ask}
Candle:     {time, open, high, low, close, volume}
SymbolInfo: {name, digits, point, tickSize, contractSize, volumeMin, volumeMax, volumeStep,
             stopsLevel, tickValue, baseCurrency?, quoteCurrency?}
  point = 10^-digits. tickValue = contractSize * tickSize in quote currency. stopsLevel is currently 0.
Position:   {id, symbol, volume, side:'buy'|'sell', entryPrice, sl?, tp?, openTime, profit, tag?}
Deal:       {positionId, symbol, side, volume, entryPrice, closePrice, openTime, closeTime,
             profit, commission, swap, reason, tag?}
  reason: 'tp'|'sl'|'trailing'|'manual'|'bot'|'stopout'|'admin'|'unknown'
Timeframes: '1m' '2m' '3m' '5m' '10m' '15m' '30m' '1h' '2h' '4h' '1d'
```

## Permissions

`trade` (ctx.order), `read-market` (ctx.market), `read-portfolio` (ctx.portfolio and `onPositionClosed`), `storage` (ctx.storage). A call without its permission is rejected silently. Tell the user which permissions to tick when creating the bot (typically all four). `ctx.log`, `ctx.chart`, `ctx.run`, `ctx.inputs` need none.

## ctx API (compact)

```
ctx.run      {symbol, timeframe, warmup, chartType?, platform?}   read the symbol here; orders are blocked while warmup is true
ctx.inputs   values of the declared inputs (trader's choices over defaults)

market (read-market)
  getSymbolInfo(symbol) -> SymbolInfo | undefined               sync
  getLastTick(symbol)   -> Tick | undefined                     sync
  getCandles(symbol, timeframe, count) -> Promise<Candle[]>     closed candles only, oldest first, count max 10000

order (trade)
  send({symbol, type:'market'|'limit'|'stop', side:'buy'|'sell', volume, price?, sl?, tp?, tag?}) -> Promise<{orderId}>
     volume in lots; price required for limit and stop; tag up to 64 chars, stored on the position
  cancel(orderId) -> Promise<void>
  modify(positionId, {sl?, tp?}) -> Promise<void>     omit = keep, null = remove; does not use the rate limit
  close(positionId, volume?) -> Promise<void>         no volume = full close, volume = partial; async, position
                                                      leaves getPositions() when the fill arrives

portfolio (read-portfolio, data refreshed every 2 s)
  getPositions() -> Promise<Position[]>     ALL account positions, including manual and other bots'
  getBalance() -> Promise<number>
  getEquity() -> Promise<number>
  getHistory({symbol?, from?, to?, limit?}) -> Promise<Deal[]>    closed deals, oldest first, limit max 200 (default 100)

storage (storage):  get<T>(key), set<T>(key, value), delete(key)    in memory, cleared when the bot stops
log: info(msg), warn(msg), error(msg), debug(msg)                   50 burst, 10/s, 2000 chars max
```

`ctx.chart` needs no permission, max 200 objects per bot (all `addMarker` count as one). `draw*` methods return the id; reusing an id replaces the object.

```
addIndicator({type}) built-in only; options like period are ignored; may toggle or stack, see references/api.md; removeIndicator(type)
drawLine({id?, price, color?, label?, style?:'solid'|'dashed'|'dotted', timeStart?, timeEnd?, priceEnd?, width?, priceLabel?}) -> id
   no timeStart = infinite line with axis label; with timeStart = segment (no label drawn; use a drawMarker anchored 'left')
updateLine(id, {price?, priceEnd?, color?, label?, timeEnd?}), removeLine(id)
drawBox({id?, timeStart, timeEnd?, top, bottom, color?, borderColor?, borderWidth?, opacity?, label?, labelPosition?, labelColor?, labelBackground?, labelSize?}) -> id
   no timeEnd = extends 15 bars past the last candle
updateBox(id, {...}), removeBox(id)
drawMarker({id?, time, price, text, color?, textColor?, opacity?, size?, anchor?:'top-left'|'left'|'right'|'center', pointer?, radius?, shape?:'arrowUp'|'arrowDown'|'label'}) -> id
removeMarker(id)
addMarker({id?, time, position?:'above'|'below', color?, shape?:'circle'|'arrowUp'|'arrowDown'|'square'|'tag'|'xcross', label?, size?, textColor?})
clearMarkers(), removeBarMarker?(id)      optional on old clients: check typeof === 'function'
drawTimeLabel?({id?, time, text, color?, size?}) -> id, removeTimeLabel?(id)    optional on old clients
drawTable({id?, position?:'top-left'|'top-right'|'bottom-left'|'bottom-right', title?, bordered?, rows:(string|{text,color?,background?,bold?})[][]}) -> id
removeTable(id)
drawPolyline({id?, points:[{time,price}], color?, width?, style?}) -> id, removePolyline(id)
clearAll()
```

Optional `inputs` property (trader-tunable parameters): `inputs: { name: { type:'number'|'boolean'|'string'|'select', label?, description?, default, min?, max?, step?, options?, group?, locked? } },` then read `ctx.inputs.name`.

## Trading rules

- Price distances in `info.point`; never deduce the pip from `digits`. Round `sl` and `tp` with `parseFloat(v.toFixed(info.digits))`.
- Risk-based lot: `lots = riskUSD / (slTicks * tickValue)` with `slTicks = |entry - sl| / tickSize`, floor to `volumeStep`, clamp to `volumeMin`/`volumeMax`.
- Send a `tag` on every order: short bot prefix plus unique suffix (for example `'alpha:' + candle.time`). Filter `getPositions()` and deals by that prefix so the bot operates only on its own positions.
- Wrap every `order.send`, `modify` and `close` in `try/catch` and log the error.
- If there are fewer candles than the indicator period, return without trading.
- One strategy per bot. Log signals, orders and closes with `ctx.log.info`.
- In drawings, reuse fixed ids (`'sl-' + pos.id`, `'stats'`) instead of new objects every candle.
- Limits: 10 orders/minute, 20 open positions, 5% daily loss, 60 s cooldown after a breach.

## Pre-flight checklist (run on your own output before answering)

1. No `defineBot`, `export`, `import`, `require` or final `return`.
2. No top-level `const`, `let`, `var`, `function`, `class`; helpers are inside hooks.
3. Every hook is shorthand and ends with a comma.
4. Every hook with `await` is `async`.
5. State goes through `ctx.storage`; configuration saved in `onInit`.
6. Every API used has its permission; the permissions list is stated.
7. Times are in ms; day constant is `86400000`.
8. `sl`/`tp` are in `info.point` units and rounded to `info.digits`; `price` present on limit/stop.
9. Lot size respects `volumeMin`, `volumeMax`, `volumeStep`.
10. Orders carry a `tag`, and `getPositions()`/deals are filtered by its prefix.
11. `send`/`modify`/`close` are in `try/catch` with `ctx.log.error`.
12. Candle count checked before computing indicators; `ctx.run.warmup` respected before ordering.
13. Chart ids are fixed, and drawings are cleaned up (`removeX` on close, `clearAll` in `onDispose`).
14. Only methods and fields listed in this skill are used.

## Common mistakes

- `const x = 0` before the hooks (not valid in an object literal) -> use `ctx.storage`.
- Missing trailing comma after a hook, or `await` inside a non-async hook.
- Treating `getPositions()` as "my positions" (it returns the whole account) -> filter by tag.
- Using seconds instead of milliseconds for times.
- Computing pip size from `digits` -> use `info.point`.
- Unrounded `sl`/`tp`, or lots not aligned to `volumeStep`.
- Forgetting a permission (the call fails silently) or using `ctx.portfolio` without `read-portfolio`.
- Heavy work in `onTick` (5 s timeout, ticks dropped) -> do it in `onCandleClose`.
- Expecting `onPositionClosed` on partial closes, or only for the bot's own positions.
- Expecting the forming candle from `getCandles` (closed candles only).
- Sending orders during warm-up.
- Custom indicators are not supported (no own pane, no new calculations): compute in the bot and draw over the candles with `drawPolyline`, `drawLine`, `drawBox`, `drawMarker` or `drawTable`. `addIndicator` only shows a built-in chart indicator and ignores `period`.

## Worked examples

Moving-average cross with risk sizing (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.log.info('BUY ' + volume + ' @ ' + candle.close)
    } catch (err) {
      ctx.log.error('Order failed: ' + err)
    }
  }
},

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

Reacting to closes of the bot's own positions only:

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

More complete bots are in `examples/` (`sma-crossover-risk.ts`, `portfolio-monitor.ts`, `chart-demo.ts`, and the full-featured `alpha-trend.ts`).

## More detail

See `references/api.md` for the complete API reference (every method, type, limit and chart option).
