# Task: integrate the Papertrade REST API

Papertrade (https://papertrade.world) is a paper-trading simulator for 45 stock markets with real broker fees, live prices and replays of past trading days. Everything uses virtual money: nothing reaches a real broker. Use this API to add paper trading to my application (for example a trading bot, a strategy backtester or a dashboard).

Read this whole brief first. Then look at my codebase and build a small, typed Papertrade client module in the language and style it already uses, and wire it into the feature I ask for. If I haven't said what to build, propose a plan and ask before writing a lot of code.

## Basics
- Base URL: https://papertrade.world/api (API version 1, stable: fields may be added, never removed or renamed).
- Auth: every request sends `Authorization: Bearer <key>`. Keys start with `pt_` and are created at https://papertrade.world/mcp#keys. Read the key from an environment variable named `PAPERTRADE_API_KEY`; never hard-code it, log it, commit it or send it to a browser.
- JSON in and out. Send `Content-Type: application/json` with bodies.
- Errors: non-2xx status with body `{ "error": "human-readable message" }`. Surface the message.
- A rejected order is NOT an HTTP error: it returns 200 with `status: "REJECTED"` and a `message` (market closed, not enough margin, after the broker's square-off…). Always check `status`.
- Symbols are `EXCHANGEPREFIX:TICKER`, e.g. `US:AAPL`, `NSE:INFY`, `LSE:HSBA`, `TSE:7203`. Look them up with the search endpoint; a bare ticker uses the simulator's market.
- Times are epoch milliseconds (UTC). In replays they're the replayed day's time, not the wall clock.
- Money is in the simulator's currency (₹ India, $ US, £ UK…). P&L is net of all fees unless labelled gross.
- Live simulators accept orders only while their exchange is open (check `ordersOpen` on the simulator, or the markets endpoint). Historic (replay) simulators trade any time: create one with a past `date`, then play the replay.
- Polling: the get-simulator endpoint returns fresh prices each call; poll at most once per second. There's no websocket.
- Orders placed with an API key are tagged `source: "api"` and the key's name in the user's activity log and reports.

## Endpoints

### Markets

#### Market status: `GET /markets`

Whether each exchange is in session right now, with the reason when it's closed (weekend, holiday, outside hours) and the data source per market. Live simulators accept orders only while their exchange is open.

Example response (trimmed):
```json
{
  "provider": "yahoo",
  "providers": {
    "IN": "yahoo",
    "US": "yahoo",
    "GB": "yahoo",
    "…": "…"
  },
  "sessions": {
    "US": {
      "open": true,
      "today": "2026-11-16",
      "holiday": null,
      "closedReason": null
    },
    "IN": {
      "open": false,
      "today": "2026-11-16",
      "holiday": null,
      "closedReason": "NSE closed at 3:30 PM IST. It opens Tue 17 Nov, 9:15 AM IST."
    }
  }
}
```

#### List brokers: `GET /brokers`

Brokers whose fees can be simulated: commission, whether statutory charges apply, maximum intraday leverage and auto square-off time. Use the id when creating a simulator.

Parameters:
- `market` (query, string): Only brokers for this market id, e.g. "US", "IN", "GB".

Example query: `?market=US`

Example response (trimmed):
```json
[
  {
    "id": "robinhood",
    "name": "Robinhood",
    "markets": [
      "US"
    ],
    "commission": "$0 commission",
    "statutoryCharges": true,
    "maxLeverage": 2,
    "autoSquareOff": null
  }
]
```

#### Search stocks: `GET /instruments`

Find stocks by symbol or company name in one market. Use the returned symbol in watchlist and order calls.

Parameters:
- `q` (query, string, required): Symbol or name, e.g. "apple", "INFY".
- `market` (query, string): Market id (default "IN"), e.g. "US", "GB", "JP".

Example query: `?q=apple&market=US`

Example response (trimmed):
```json
[
  {
    "symbol": "US:AAPL",
    "exchange": "US",
    "tradingsymbol": "AAPL",
    "name": "Apple Inc."
  }
]
```

### Simulators

#### List simulators: `GET /simulators`

All your simulators, newest first, with clock and funds.

Example response (trimmed):
```json
[
  {
    "id": "sim_7e46b23bb2",
    "name": "Momentum",
    "mode": "live",
    "market": "US",
    "broker": "robinhood",
    "leverage": 2,
    "createdAt": 1794501161870,
    "clock": {
      "status": "live",
      "now": 1794666612000,
      "speed": 1,
      "open": 1794666600000,
      "close": 1794690000000,
      "marketOpen": true
    },
    "funds": {
      "capital": 100000,
      "realizedPnl": 74,
      "unrealizedPnl": 412.6,
      "grossPnl": 486.6,
      "charges": 0.36,
      "pnl": 486.24,
      "pnlPct": 0.49,
      "equity": 100486.24,
      "exposure": 39160.2,
      "usedMargin": 19580.1,
      "available": 80906.14,
      "leverage": 2
    },
    "openPositions": 1,
    "orderCount": 8
  }
]
```

#### Create a simulator: `POST /simulators`

A live simulator trades today's prices while the exchange is open. A historic one replays a past trading day: it starts paused at the open (see Control a replay). Every fill pays the chosen broker's fees and the market's taxes.

Parameters:
- `name` (body, string, required): Up to 60 characters.
- `mode` (body, "live" | "historic", required): Live prices, or a replay of a past day.
- `market` (body, string): Market id (default "IN"), e.g. "US", "GB", "DE", "JP", "HK".
- `date` (body, string): Historic only: the trading day to replay, YYYY-MM-DD.
- `capital` (body, number): Starting virtual cash in the market's currency (default about US$100,000 worth).
- `broker` (body, string): Broker id from List brokers (default: the market's first broker; "none" = no fees).
- `leverage` (body, integer): Intraday leverage, 1–5 (default and maximum depend on the market: 5× India, 2× most others).

Example body:
```json
{
  "name": "Momentum",
  "mode": "live",
  "market": "US",
  "capital": 100000,
  "broker": "robinhood"
}
```

Example response (trimmed):
```json
{
  "id": "sim_7e46b23bb2",
  "name": "Momentum",
  "mode": "live",
  "market": "US",
  "broker": "robinhood",
  "leverage": 2,
  "clock": {
    "status": "live",
    "now": 1794666612000,
    "speed": 1,
    "open": 1794666600000,
    "close": 1794690000000,
    "marketOpen": true
  },
  "funds": {
    "capital": 100000,
    "realizedPnl": 0,
    "unrealizedPnl": 0,
    "grossPnl": 0,
    "charges": 0,
    "pnl": 0,
    "pnlPct": 0,
    "equity": 100000,
    "exposure": 0,
    "usedMargin": 0,
    "available": 100000,
    "leverage": 2
  },
  "watchlist": [],
  "positions": [],
  "orders": [],
  "squareOff": null,
  "ordersOpen": true
}
```

#### Get a simulator: `GET /simulators/{id}`

Everything at once: clock, funds (equity, P&L, margin), watchlist prices, positions, orders (newest first), the broker's square-off for today, and whether orders are accepted right now (ordersOpen, with ordersClosedReason when not). Poll this to follow prices; once a second is plenty.

Parameters:
- `id` (path, string, required): Simulator id, e.g. "sim_7e46b23bb2" (from List simulators).

Example response (trimmed):
```json
{
  "id": "sim_7e46b23bb2",
  "name": "Momentum",
  "mode": "live",
  "market": "US",
  "clock": {
    "status": "live",
    "now": 1794666612000,
    "speed": 1,
    "open": 1794666600000,
    "close": 1794690000000,
    "marketOpen": true
  },
  "funds": {
    "capital": 100000,
    "realizedPnl": 74,
    "unrealizedPnl": 412.6,
    "grossPnl": 486.6,
    "charges": 0.36,
    "pnl": 486.24,
    "pnlPct": 0.49,
    "equity": 100486.24,
    "exposure": 39160.2,
    "usedMargin": 19580.1,
    "available": 80906.14,
    "leverage": 2
  },
  "watchlist": [
    {
      "symbol": "US:TSLA",
      "name": "Tesla, Inc.",
      "price": {
        "ltp": 392.4,
        "open": 388.1,
        "high": 394,
        "low": 386.5,
        "ref": 386.9,
        "change": 5.5,
        "changePct": 1.42
      }
    }
  ],
  "positions": [
    {
      "symbol": "US:TSLA",
      "name": "Tesla, Inc.",
      "qty": 10,
      "avgPrice": 391.2,
      "ltp": 392.4,
      "realizedPnl": 0,
      "unrealizedPnl": 12,
      "charges": 0,
      "pnl": 12
    }
  ],
  "orders": [
    {
      "id": "ord_af57592f5d",
      "symbol": "US:TSLA",
      "side": "BUY",
      "type": "MARKET",
      "qty": 10,
      "status": "COMPLETE",
      "avgPrice": 391.2,
      "placedAt": 1794666612000,
      "filledAt": 1794666612000,
      "source": "api",
      "agent": "my-trading-bot",
      "bracket": {
        "stopLoss": 383.4,
        "target": 406.85
      },
      "charges": {
        "brokerage": 0,
        "secFee": 0,
        "taf": 0,
        "total": 0
      }
    }
  ],
  "squareOff": null,
  "ordersOpen": true
}
```

#### Rename a simulator: `PATCH /simulators/{id}`

Change a simulator's name.

Parameters:
- `id` (path, string, required): Simulator id, e.g. "sim_7e46b23bb2" (from List simulators).
- `name` (body, string, required): New name, up to 60 characters.

Example body:
```json
{
  "name": "Momentum v2"
}
```

Example response (trimmed):
```json
{
  "id": "sim_7e46b23bb2",
  "name": "Momentum v2"
}
```

#### Delete a simulator: `DELETE /simulators/{id}`

Permanently deletes the simulator with its orders, positions and results. This can't be undone.

Parameters:
- `id` (path, string, required): Simulator id, e.g. "sim_7e46b23bb2" (from List simulators).

Example response (trimmed):
```json
{
  "deleted": "sim_7e46b23bb2"
}
```

### Trading

#### Place an order: `POST /simulators/{id}/orders`

MARKET fills now. LIMIT rests until the price reaches limitPrice. SL-M (stop-market) waits for triggerPrice, then fills at market; SL (stop-limit) waits for triggerPrice, then rests at limitPrice. A SELL stop's trigger must be below the price, a BUY stop's above. Attach stopLoss and/or target to a MARKET or LIMIT entry and they become exit orders once it fills (one cancels the other). Selling without a position opens a short. A rejected order still returns 200 with status "REJECTED" and a message saying why (e.g. market closed, not enough margin).

Parameters:
- `id` (path, string, required): Simulator id, e.g. "sim_7e46b23bb2" (from List simulators).
- `symbol` (body, string, required): Symbol from Search stocks, e.g. "US:TSLA" or "NSE:INFY". A bare symbol uses the simulator's market.
- `side` (body, "BUY" | "SELL", required): Direction.
- `qty` (body, integer, required): Number of shares, at least 1.
- `type` (body, "MARKET" | "LIMIT" | "SL" | "SL-M"): Default "MARKET".
- `limitPrice` (body, number): LIMIT and SL orders.
- `triggerPrice` (body, number): SL and SL-M orders (optional for SL-M with a trailingStop).
- `stopLoss` (body, number): MARKET/LIMIT entries: exit price if the trade goes against you.
- `target` (body, number): MARKET/LIMIT entries: exit price to take profit.
- `trailingStop` (body, { type: "points" | "percent", value: number }): Trail the stop loss (entries) or the trigger (SL-M) behind the best price.

Example body:
```json
{
  "symbol": "US:TSLA",
  "side": "BUY",
  "qty": 10,
  "type": "MARKET",
  "stopLoss": 383.4,
  "target": 406.85
}
```

Example response (trimmed):
```json
{
  "id": "ord_af57592f5d",
  "symbol": "US:TSLA",
  "side": "BUY",
  "type": "MARKET",
  "qty": 10,
  "status": "COMPLETE",
  "avgPrice": 391.2,
  "placedAt": 1794666612000,
  "filledAt": 1794666612000,
  "source": "api",
  "agent": "my-trading-bot",
  "bracket": {
    "stopLoss": 383.4,
    "target": 406.85
  },
  "charges": {
    "brokerage": 0,
    "secFee": 0,
    "taf": 0,
    "total": 0
  }
}
```

#### Cancel an order: `DELETE /simulators/{id}/orders`

Cancel an OPEN order: a limit, a stop (SL / SL-M), or a bracket's stop-loss or target exit.

Parameters:
- `id` (path, string, required): Simulator id, e.g. "sim_7e46b23bb2" (from List simulators).
- `orderId` (query, string, required): The order id, e.g. "ord_9c1d…".

Example query: `?orderId=ord_9c1d2e3f4a`

Example response (trimmed):
```json
{
  "id": "ord_9c1d2e3f4a",
  "symbol": "US:TSLA",
  "side": "BUY",
  "type": "LIMIT",
  "qty": 10,
  "status": "CANCELLED",
  "placedAt": 1794666612000,
  "source": "api",
  "agent": "my-trading-bot",
  "charges": {
    "brokerage": 0,
    "secFee": 0,
    "taf": 0,
    "total": 0
  },
  "limitPrice": 385
}
```

#### Exit positions: `POST /simulators/{id}/exit`

Close one position at market, or every open position when symbol is left out. Open exit orders for those positions are cancelled.

Parameters:
- `id` (path, string, required): Simulator id, e.g. "sim_7e46b23bb2" (from List simulators).
- `symbol` (body, string): Close only this position. Leave out to close all.

Example body:
```json
{
  "symbol": "US:TSLA"
}
```

Example response (trimmed):
```json
[
  {
    "id": "ord_5b7e1a90c2",
    "symbol": "US:TSLA",
    "side": "SELL",
    "type": "MARKET",
    "qty": 10,
    "status": "COMPLETE",
    "avgPrice": 395.1,
    "placedAt": 1794666612000,
    "filledAt": 1794666612000,
    "source": "api",
    "agent": "my-trading-bot",
    "charges": {
      "brokerage": 0,
      "secFee": 0,
      "taf": 0,
      "total": 0
    }
  }
]
```

### Watchlist

#### Add to watchlist: `POST /simulators/{id}/watchlist`

Track stocks so their prices come back in Get a simulator. You can trade a stock without adding it first.

Parameters:
- `id` (path, string, required): Simulator id, e.g. "sim_7e46b23bb2" (from List simulators).
- `symbols` (body, string[]): Up to 50 symbols, e.g. ["US:AAPL", "US:NVDA"].
- `symbol` (body, string): Or a single symbol.

Example body:
```json
{
  "symbols": [
    "US:AAPL",
    "US:NVDA"
  ]
}
```

Example response (trimmed):
```json
{
  "added": [
    "US:AAPL",
    "US:NVDA"
  ],
  "watchlist": [
    "US:TSLA",
    "US:AAPL",
    "US:NVDA"
  ]
}
```

#### Remove from watchlist: `DELETE /simulators/{id}/watchlist`

Stop tracking a stock. Open positions and orders in it aren't affected.

Parameters:
- `id` (path, string, required): Simulator id, e.g. "sim_7e46b23bb2" (from List simulators).
- `symbol` (query, string, required): e.g. "US:AAPL".

Example query: `?symbol=US:AAPL`

Example response (trimmed):
```json
{
  "watchlist": [
    "US:TSLA",
    "US:NVDA"
  ]
}
```

### Replay

#### Control a replay: `POST /simulators/{id}/clock`

Historic simulators only. Play, pause, change speed (1, 5, 10, 30, 60 or 120×) or jump forward. Orders fill at the replay's prices and time. Returns the clock.

Parameters:
- `id` (path, string, required): Simulator id, e.g. "sim_7e46b23bb2" (from List simulators).
- `action` (body, "play" | "pause" | "speed" | "advance", required): What to do.
- `speed` (body, number): For speed: 1, 5, 10, 30, 60 or 120.
- `minutes` (body, integer): For advance: minutes to jump ahead.

Example body:
```json
{
  "action": "speed",
  "speed": 30
}
```

Example response (trimmed):
```json
{
  "status": "running",
  "now": 1794470400000,
  "speed": 30,
  "open": 1794468600000,
  "close": 1794492000000,
  "marketOpen": true
}
```

### Results

#### Get the results report: `GET /simulators/{id}/report`

The same report as the app's Results: net and gross P&L, charges by type, round-trip trades with win rate and profit factor, day-by-day results (profitable days, best and worst day, streaks), per-stock P&L, peak margin, equity curve and plain-language insights.

Parameters:
- `id` (path, string, required): Simulator id, e.g. "sim_7e46b23bb2" (from List simulators).

Example response (trimmed):
```json
{
  "status": {
    "final": false,
    "label": "Live: results so far"
  },
  "pnl": {
    "net": 1652.66,
    "netPct": 1.65,
    "gross": 1725.24,
    "realized": 1725.24,
    "unrealized": 0,
    "charges": 72.58,
    "equity": 101652.66
  },
  "trades": {
    "closed": 125,
    "open": 0,
    "wins": 70,
    "losses": 55,
    "winRate": 56,
    "profitFactor": 1.12,
    "avgWin": 220.98,
    "avgLoss": -251.22,
    "list": [
      "…"
    ]
  },
  "daily": {
    "traded": 42,
    "green": 24,
    "red": 18,
    "greenRate": 57.14,
    "best": {
      "date": "2026-10-20",
      "net": 1146.01
    },
    "worst": {
      "date": "2026-10-27",
      "net": -871.27
    },
    "streak": {
      "kind": "green",
      "days": 2
    },
    "days": [
      "…"
    ]
  },
  "charges": {
    "total": 72.58,
    "brokerage": 0,
    "secFee": 61.2,
    "taf": 11.38
  },
  "stocks": [
    "…"
  ],
  "equityCurve": [
    "…"
  ],
  "insights": [
    "24 of 42 trading days were profitable; best day 2026-10-20 (+$1,146.01), worst 2026-10-27 (-$871.27)."
  ]
}
```

## Error statuses
- 400: Something in the request is invalid: a missing field, a bad value, or a body that isn't JSON. The message says what.
- 401: No API key, or the key is wrong or deleted.
- 404: That simulator (or order) doesn't exist on your account.
- 409: The simulator was changed by another request at the same moment. Retry.
- 429: Too many requests. Slow down and retry after a moment.
- 502: The market data provider didn't answer. Retry shortly.

## How to build the client
- One function per endpoint you need, with types for requests and responses based on the examples above (treat unknown extra fields as allowed).
- A single request helper that adds the auth header, sets JSON headers, parses JSON, and turns non-2xx responses into an error carrying the status and the `error` message.
- Retry 409, 429 and 502 a few times with backoff; don't retry 400, 401 or 404.
- Treat `status: "REJECTED"` orders as a normal outcome your code handles (log the `message`), not an exception.
- Keep the base URL and key in configuration, so tests can point at a mock.
- Add a short README section on setting `PAPERTRADE_API_KEY`.

Full human-readable reference: https://papertrade.world/docs/api