# SDK-011 — Trade Cancellation (SDK + CLI)

**Status:** Complete
**Date:** 2026-03-18
**Branch:** `sdk-011/trade-cancellation`
**Location:** `hyperscaled-sdk/`

---

## Summary

Implemented Hyperliquid order cancellation in the SDK and CLI so users can cancel one specific open order or bulk-cancel all currently open orders for the configured account.

This work adds:

- `TradingClient.cancel()` / `cancel_async()` for single-order cancellation
- `TradingClient.cancel_all()` / `cancel_all_async()` for bulk cancellation of currently open orders
- Hyperliquid `Info.open_orders()` discovery so cancellation only targets open/cancellable orders
- structured cancellation result payloads for success, not-found, already-closed, and error cases
- full CLI wiring for `hyperscaled trade cancel <order_id>` and `hyperscaled trade cancel-all`
- mock-based tests covering happy paths and failure paths without live Hyperliquid API calls

Cancellation is still performed directly against Hyperliquid via `hyperliquid-python-sdk`. The SDK uses the configured wallet to discover open orders, then signs the cancel request locally with the same HL private key flow used for trade submission.

---

## What Was Built

### New Files

```text
hyperscaled-sdk/
└── planning/
    └── reports/
        └── SDK-011.md            # This report
```

### Modified Files

| File | Change |
|------|--------|
| `hyperscaled/sdk/trading.py` | Added open-order discovery, `cancel()` / `cancel_all()`, and Hyperliquid cancel-response parsing |
| `hyperscaled/cli/trade.py` | Replaced `cancel` / `cancel-all` stubs with live CLI commands and Rich output |
| `tests/test_trading.py` | Added five mock-based cancellation tests |

---

## Details

### 1. Single-order cancellation flow

`TradingClient.cancel_async(order_id)` follows this sequence:

1. Parse the provided `hl_order_id` string into the integer format Hyperliquid expects
2. Query current open orders for the configured wallet via `Info.open_orders()`
3. Match the requested order ID to an open order to recover the required HL `coin` symbol
4. Return a structured `not_found` result if the order is not currently open/cancellable
5. Call `Exchange.cancel(name, oid)` in `asyncio.to_thread()`
6. Parse the Hyperliquid cancellation response into a stable SDK result dictionary

This avoids ambiguous "success" messages for stale order IDs because the SDK confirms open-order eligibility before submitting the cancel request.

### 2. Bulk cancellation flow

`TradingClient.cancel_all_async()`:

1. Fetches all open orders for the configured wallet
2. Returns a zero-work success result when there are no open orders
3. Converts open orders into `{"coin": ..., "oid": ...}` requests
4. Calls `Exchange.bulk_cancel()` in `asyncio.to_thread()`
5. Returns a structured summary with:
   - overall status
   - total open orders seen
   - cancelled count
   - failed count
   - per-order results

`cancel_all()` only attempts cancellation for orders that Hyperliquid currently reports as open.

### 3. Cancellation result shape

Single-order results are returned as dictionaries shaped like:

```python
{
    "hl_order_id": "123456",
    "pair": "BTC-USDC",
    "status": "cancelled",
    "message": "Order cancelled.",
}
```

Bulk cancellation returns:

```python
{
    "status": "ok",
    "message": "Cancelled 2 of 2 open orders.",
    "total_open_orders": 2,
    "cancelled_count": 2,
    "failed_count": 0,
    "results": [...],
}
```

The parser classifies Hyperliquid error strings into stable SDK statuses:

- `cancelled`
- `not_found`
- `already_closed`
- `error`

### 4. CLI commands

```bash
# Cancel one order
hyperscaled trade cancel 123456

# Cancel all open orders
hyperscaled trade cancel-all
```

The CLI renders Rich panels that make zero, one, or many cancellations obvious. Single-order cancellation shows the order ID, pair, status, and message. Bulk cancellation shows aggregate counts plus per-order outcomes when any orders were processed.

### 5. Wallet and signing requirements

Cancellation reuses the existing SDK trade configuration:

1. The configured Hyperliquid wallet address is required to discover open orders
2. The HL private key is required to sign the cancel action

If either prerequisite is missing, the SDK raises a clear `HyperscaledError`.

---

## Acceptance Criteria Status

| Criterion | Status |
|-----------|--------|
| `client.trade.cancel(order_id)` cancels a single open order and returns structured status data | Pass |
| `client.trade.cancel_all()` cancels all currently open orders for the configured account | Pass |
| `hyperscaled trade cancel` and `hyperscaled trade cancel-all` report success/failure clearly | Pass |
| Cancellation of unknown or already-closed orders is handled gracefully | Pass |
| Mock-based tests cover single cancel success, cancel-all success, no open orders, unknown order ID, and Hyperliquid API failure handling | Pass |

---

## Verification

```bash
python3 -m pytest tests/test_trading.py -q  → 31 passed in 0.42s
python3 -m pytest tests -q                  → 228 passed, 1 warning in 1.39s
```

All cancellation tests pass, and the full SDK suite remains green. The one warning is pre-existing in `tests/test_client.py` and is unrelated to `SDK-011`.

---

## Test Coverage

| Test Area | Coverage |
|-----------|----------|
| Cancel: single success | Open order is matched, `Exchange.cancel()` called with correct `coin` and `oid`, structured `cancelled` result returned |
| Cancel all: success | Multiple open orders are transformed into `bulk_cancel` requests and counted correctly |
| Cancel all: no open orders | Returns zero-work success result without calling `bulk_cancel()` |
| Cancel: unknown order ID | Returns structured `not_found` result without calling Hyperliquid cancel |
| Cancel: HL transport failure | SDK wraps SDK/network exception as `HyperscaledError` |

---

## Design Decisions

### Why `cancel(order_id)` looks up open orders first

Hyperliquid `Exchange.cancel()` needs both `coin` and `oid`, while the public SDK API exposes cancellation by `hl_order_id` alone. Looking up open orders first lets the SDK:

1. Recover the required `coin` symbol
2. Avoid a speculative cancel call for orders that are no longer open
3. Return a cleaner `not_found` result for stale or invalid order IDs

### Why `cancel_all()` uses `bulk_cancel()`

Hyperliquid already supports bulk cancellation in one signed action. Using `Exchange.bulk_cancel()` keeps the implementation aligned with the upstream API and reduces repetitive round-trips when multiple open orders must be cleared.

### Why cancellation returns structured dicts instead of plain strings

The sprint ticket explicitly calls for structured status data and clear handling for unknown/already-closed orders. Returning dictionaries makes the result easy to inspect in scripts and straightforward to render in the CLI.

---

## Open Follow-up Items

- **SDK-012:** Local rule enforcement remains a no-op seam on submission and is still next on the trade client
- **SDK-013:** Open positions / orders can reuse the same Hyperliquid/validator vocabulary work started here for order-state handling
- Cancellation currently returns structured dictionaries; if the SDK later standardizes richer action-result models, this flow could migrate to a typed result model
