# Attach a take-profit and stop-loss

A take-profit and a stop-loss have to behave as one unit — if the survivor outlives the leg
that fired, it re-opens the exposure it was supposed to close. This attaches both to an
already-open position as reduce-only legs sharing a single OCO group.

1. ### Find the position

   The server resolves the position from your account plus the `symbol` you send, so that is
   the only thing you need off this read. No open position on that symbol gives
   `404 no_position_for_symbol`.

   ```bash
   curl -s https://api.troncharts.xyz/api/v1/accounts/$ACCOUNT_ID/positions \
     -H "authorization: Bearer $TOKEN" \
     -H "x-tenant-slug: $TC_TENANT_SLUG"
   # → { accountId, accountNumber, positions: [ { symbol, venue, side, qty, entryPrice, … } ] }
   ```
   ```ts
   const { positions } = await sdk.accounts.positions(accountId)
   const pos = positions.find((p) => p.symbol === 'BTC.HL')
   ```
2. ### Attach both legs

   At least one of `tpPrice` / `slPrice` must be present, or you get `400 no_legs_requested`.
   Send them as JSON **numbers** — unlike the compose schema, this one does not coerce
   decimal strings.

   ```bash
   curl -s https://api.troncharts.xyz/api/v1/oms/brackets/attach-to-position \
     -H "authorization: Bearer $TOKEN" \
     -H "x-tenant-slug: $TC_TENANT_SLUG" \
     -H 'content-type: application/json' \
     -H "idempotency-key: $(uuidgen)" \
     -d '{"symbol":"BTC.HL","tpPrice":72000,"slPrice":58500}'
   ```
   ```ts
   const bracket = await sdk.oms.attachBracketsToPosition(
     { symbol: 'BTC.HL', tpPrice: 72000, slPrice: 58500 },
     crypto.randomUUID(),
   )
   ```
   :::caution[Three venues only]
   Hyperliquid, Aster and paper. Anything else returns `400 venue_unsupported` — Polymarket
   and Kalshi brackets ship with their own OMS arc.
   :::

3. ### Read the OCO group off the response

   Both legs carry `ocoGroupId` as their `parentIntentId`; that linkage is what makes them
   one unit. `side` is the *closing* side (a long position gives `sell`), and `qty` is the
   full position size — each leg covers all of it.

   ```json
   { "ok": true, "ocoGroupId": "9c41…", "tpIntentId": "1a2b…",
     "tpIntentIds": ["1a2b…"], "slIntentId": "7d8e…",
     "side": "sell", "qty": "0.01" }
   ```

   The legs dispatch immediately — a position already exists to reduce, so there is nothing
   to wait for. From here the server owns the lifecycle: when one leg fills it cancels the
   survivor, and when the position closes by any route it cancels both.

**Next:** [Split a take-profit across several targets](https://docs.troncharts.xyz/docs/recipes/place-a-scale-out-ladder/) · [Cancel an order or close a position](https://docs.troncharts.xyz/docs/recipes/cancel-and-flatten/) · [Order lifecycle & signing](https://docs.troncharts.xyz/docs/trading/order-lifecycle/)