# Trading Floor Challenge - API reference

Everything your bot needs, in one file. Generated by the server, so the rules
and numbers below are exactly what it is enforcing right now.

Base URL: `https://trading.rob-all.duckdns.org`
Interactive version: `https://trading.rob-all.duckdns.org/docs`
What the game is: `https://trading.rob-all.duckdns.org/guide`

---

## Authentication

Send your team key as a header on **every** request:

```
X-API-Key: tk_yourteamkey
```

All responses are JSON. All timestamps are ISO 8601 UTC. Every response also
carries an `X-Server-Time` header. Trust the server clock, not your own.

---

## Rules the server enforces

| Rule | Value |
|---|---|
| Starting cash | 100,000 |
| Short selling | Not allowed. You can only sell what you hold |
| Position cap | 30% of portfolio value per company, **checked at order time only** |
| Index fund cap | None. `ORB` may be 100% of your portfolio |
| Trading cost | 15 bps half spread each way, so 30 bps per round trip |
| Rate limit | 60 requests per minute **per team key**, sliding window |
| Order types | Market orders only. Fill immediately, whole shares, positive integers |
| Price tick | Every 1 second |
| News reaction | Price starts moving ~90s after a story is published |
| News drift | The move then plays out over ~120s and settles |
| Session length | 60 minutes |
| News cutoff | Nothing new is published in the last 4 minutes |

Three of those bite people:

1. **The cap is checked when you buy, never again.** Buy up to 30%, the
   price rises, and that position keeps growing as a share of your portfolio.
   The server will not sell anything for you. Trim it yourself if you care.
2. **The rate limit is per team, not per person.** Three teammates each running
   the bot share one budget of 60.
3. **No shorting** means bad news is only actionable if you already hold the
   thing. Deciding when to exit matters as much as deciding when to enter.

---

## Instruments

`NWL`, `KMT`, `ARN`, `HLX`, `VTC`, `MRD`, `WRB`, `ORB`

| Symbol | Name | Sector | Start | Index weight | Position cap |
|---|---|---|---|---|---|
| `NWL` | Northwind Logistics | Freight & Logistics | 41.90 | 12% | 30% |
| `KMT` | Kestrel Materials | Industrial Materials | 88.10 | 14% | 30% |
| `ARN` | Arnhold Retail | Consumer Retail | 12.55 | 8% | 30% |
| `HLX` | Helix Biosciences | Pharmaceuticals | 156.40 | 19% | 30% |
| `VTC` | Vantage Compute | Technology / Datacentres | 214.75 | 23% | 30% |
| `MRD` | Meridian Power | Utilities / Energy | 63.20 | 17% | 30% |
| `WRB` | Wire Bank | Banking & Payments | 78.40 | 7% | 30% |
| `ORB` | Orbis Market Index Fund | Index fund | 100.00 | - | 100% |

`ORB` is an index fund holding all seven companies weighted by size. It has no
news of its own: its price is only ever the weighted performance of what it
holds. It has no position cap, so owning the index and doing nothing else is a
legitimate strategy.

---

## Endpoints

### `GET /status`
Session state, the server clock and the rules in force.

```json
{"server_time":"2026-09-03T12:31:05Z","session":"live","state":"running",
 "seconds_remaining":2340.0,"seconds_elapsed":1260.0,"tick_seconds":5.0,
 "rules":{"half_spread_bps":15.0,"max_position_pct":0.3,
          "rate_limit_per_min":60,"short_selling":false},
 "rate_limit_used":7}
```

`state` is `idle`, `running`, `paused` or `closed`. `paused` means the
organiser has frozen the market: keep polling, do not exit, orders will be
refused until it resumes.

### `GET /instruments`
The table above, machine readable. Also returns a `relationships` array: some
stories in this market move a company they never mention, and that array is
how you work out which one. Fetch it once at startup and keep it.

### `GET /market`
```json
{"timestamp":"...","state":"running",
 "instruments":[{"symbol":"NWL","name":"Northwind Logistics",
                "sector":"Freight & Logistics","price":41.27}]}
```

### `GET /history?symbol=NWL&limit=240`
Oldest first. `limit` defaults to 240 and downsamples evenly above that.
```json
{"symbol":"NWL","points":[{"timestamp":"...","elapsed":0.0,"price":41.90}]}
```

### `GET /news?since=<cursor>`
Pass the `cursor` from your last response to get only what is new. There is no
sentiment field, no tag, no hint, and no indication of which instrument a story
affects. Reading the text is the exercise.
```json
{"cursor":14,"items":[{"id":"n-0020","timestamp":"...",
  "source":"Consolidated Wire","headline":"...","body":"..."}]}
```
Note `source`. There is more than one, and they are not equally reliable.

### `GET /portfolio`
```json
{"team":"Team Falcon","cash":62480.00,
 "holdings":[{"symbol":"NWL","quantity":500,"avg_price":41.36,
             "current_price":42.90,"market_value":21450.00,"unrealized_pnl":770.00}],
 "portfolio_value":103930.00,"starting_cash":100000.00,"return_pct":3.93,
 "realized_pnl":1240.50,"spread_paid":186.20}
```
`spread_paid` is what your trading has cost you so far. Watch it.

### `POST /order`
```json
{"symbol":"NWL","side":"buy","quantity":25,"client_order_id":"optional-unique"}
```
`client_order_id` is optional but recommended: resend the same id after a
timeout and you get the original fill back instead of filling twice.

```json
{"order_id":"o-00981","symbol":"NWL","side":"buy","quantity":25,
 "fill_price":41.3620,"spread_cost":1.55,"timestamp":"...","cash_after":6386.50}
```

### `GET /orders?limit=50`
Your own fills, oldest first. Useful after a restart: the server remembers what
you own, your process does not.

### `GET /leaderboard`
```json
{"timestamp":"...","standings":[
  {"team":"Team Falcon","portfolio_value":111250.30,"return_pct":11.25,"rank":1}]}
```

### `GET /snapshot?since=<cursor>`
**Use this as your main polling call.** Market, news, portfolio and status in
one request. Polling it every 5s costs 12 of your 60 requests per minute.
Polling the four endpoints separately costs 48 and leaves you no room to trade.
```json
{"server_time":"...","state":"running","session":"live","seconds_remaining":2340.0,
 "instruments":[...],"news":{"cursor":14,"items":[...]},
 "portfolio":{...},"rate_limit_used":12}
```

---

## Errors

Every error from every endpoint has the same flat shape:
`{"error": "code", "message": "human readable"}`

| HTTP | `error` | Meaning |
|---|---|---|
| 401 | `invalid_api_key` | Missing or unknown `X-API-Key` |
| 400 | `insufficient_funds` | Not enough cash |
| 400 | `insufficient_holdings` | Selling more than you hold. There is no shorting |
| 400 | `position_limit_exceeded` | The buy would push that company above its cap |
| 400 | `invalid_side` | `side` must be `buy` or `sell` |
| 400 | `invalid_quantity` | Whole numbers greater than zero |
| 404 | `unknown_symbol` | No such instrument |
| 409 | `market_closed` | Not open yet, or finished |
| 409 | `market_paused` | Organiser paused the market. Wait and retry |
| 429 | `rate_limited` | Includes `retry_after_seconds`. Sleep for it |
| 422 | - | Malformed body, for example a missing field |

Rejections are 4xx responses, not exceptions. If you are not checking the
status code, your bot will believe it owns things it never bought.

---

## Suggested `CLAUDE.md` for your project

Drop this in your project directory. Every Claude Code session there picks it
up, so you stop re-explaining the market and it stops writing code that breaks
rules it was never told about.

```markdown
# Trading bot for the Trading Floor Challenge

## Rules this code must never violate
- Long only. Never sell more than we currently hold.
- Max 30% of portfolio value in any one company, checked at order time.
  ORB is an index fund and is NOT capped.
- 60 requests per minute per key, shared by the whole team.
  Poll GET /snapshot (one call), never four separate endpoints.
- Quantities are positive whole numbers.
- Every round trip costs 30 bps, so do not trade on noise.
- Use the server clock from the response, never local time.
- Handle state "paused": wait, do not exit.
- Errors are flat JSON with an "error" code, returned as 4xx. Check status codes.

## API
Base URL: https://trading.rob-all.duckdns.org
Full reference: https://trading.rob-all.duckdns.org/api.md   Interactive: https://trading.rob-all.duckdns.org/docs
Do not invent endpoints. If unsure, check the reference first.

## Instruments
`NWL`, `KMT`, `ARN`, `HLX`, `VTC`, `MRD`, `WRB`, `ORB`
ORB is an index fund of the seven companies, weighted by size, with no news of
its own and no position cap.
```
